@felixgeelhaar/cclint
Version:
Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.
189 lines • 8.15 kB
JavaScript
import { ContextFile } from '../domain/ContextFile.js';
import { Violation } from '../domain/Violation.js';
import { Location } from '../domain/Location.js';
import { Severity } from '../domain/Severity.js';
/**
* Rule that validates content appropriateness for CLAUDE.md files
*
* @remarks
* Validates that CLAUDE.md content:
* - Is specific and actionable (not overly generic)
* - Belongs in CLAUDE.md vs README or other docs
* - Follows "frequently used prompt" refinement principle
* - Is concise and focused
*
* @see {@link https://www.anthropic.com/engineering/claude-code-best-practices | Claude Code Best Practices}
*
* @category Rules
*/
export class ContentAppropriatenessRule {
id = 'content-appropriateness';
description = 'Validates that content is appropriate for CLAUDE.md and not overly generic';
maxFileSize;
maxSectionSize;
constructor(options) {
this.maxFileSize = options?.maxFileSize ?? 5000; // ~5KB recommended
this.maxSectionSize = options?.maxSectionSize ?? 1000; // ~1KB per section
}
appliesTo(file) {
return file.isMarkdown();
}
lint(file) {
const violations = [];
// Check file size (should be concise)
violations.push(...this.checkFileSize(file));
// Check for overly generic instructions
violations.push(...this.checkGenericInstructions(file));
// Check for content that belongs elsewhere
violations.push(...this.checkMisplacedContent(file));
// Check section sizes
violations.push(...this.checkSectionSizes(file));
// Check for actionable content
violations.push(...this.checkActionability(file));
return violations;
}
/**
* Check if file is too large
*/
checkFileSize(file) {
const violations = [];
const contentLength = file.content.length;
if (contentLength > this.maxFileSize) {
violations.push(new Violation(this.id, `CLAUDE.md is ${contentLength} characters (recommended: <${this.maxFileSize}). Consider using imports to reference external documentation: @README.md, @docs/guidelines.md`, Severity.WARNING, new Location(1, 1)));
}
return violations;
}
/**
* Check for overly generic instructions
*/
checkGenericInstructions(file) {
const violations = [];
const genericPatterns = [
{
pattern: /follow\s+best\s+practices/i,
message: 'Avoid generic "follow best practices". Be specific: "Use 2-space indentation" or "Run prettier before commit"',
},
{
pattern: /write\s+good\s+code/i,
message: 'Avoid vague "write good code". Be specific about standards: "Pass ESLint with no warnings" or "Maintain >80% test coverage"',
},
{
pattern: /do\s+it\s+correctly/i,
message: 'Avoid generic "do it correctly". Define what correct means with measurable criteria',
},
{
pattern: /use\s+common\s+sense/i,
message: 'Avoid "use common sense". Document specific decision criteria or examples',
},
{
pattern: /be\s+careful/i,
message: 'Instead of "be careful", specify the exact risk and mitigation: "Always backup database before migration"',
},
];
for (let i = 0; i < file.lines.length; i++) {
const line = file.lines[i] ?? '';
for (const { pattern, message } of genericPatterns) {
if (pattern.test(line)) {
violations.push(new Violation(this.id, `Generic instruction detected: ${message}`, Severity.WARNING, new Location(i + 1, 1)));
}
}
}
return violations;
}
/**
* Check for content that should be in README or other docs
*/
checkMisplacedContent(file) {
const violations = [];
// Check for extensive project descriptions
const hasExtensiveDescription = file.content.match(/^#{1,2}\s+(About|Overview|Description|Introduction)/im);
if (hasExtensiveDescription) {
const sectionStart = file.content.indexOf(hasExtensiveDescription[0] ?? '');
const lineNumber = file.content
.substring(0, sectionStart)
.split('\n').length;
violations.push(new Violation(this.id, 'Extensive project description detected. Keep CLAUDE.md concise - use @README.md to reference project overview', Severity.INFO, new Location(lineNumber, 1)));
}
// Check for API documentation
if (file.content.includes('API Documentation') ||
/^#{1,3}\s+API/im.test(file.content)) {
violations.push(new Violation(this.id, 'API documentation detected. Move API docs to separate files and reference via imports: @docs/api.md', Severity.INFO, new Location(1, 1)));
}
// Check for installation instructions
if (/^#{1,3}\s+Installation/im.test(file.content) ||
file.content.includes('npm install') ||
file.content.includes('pip install')) {
violations.push(new Violation(this.id, 'Installation instructions detected. Keep setup commands minimal - reference @README.md for full installation guide', Severity.INFO, new Location(1, 1)));
}
return violations;
}
/**
* Check section sizes
*/
checkSectionSizes(file) {
const violations = [];
const sections = this.extractSections(file);
for (const section of sections) {
if (section.content.length > this.maxSectionSize) {
violations.push(new Violation(this.id, `Section "${section.title}" is ${section.content.length} characters (recommended: <${this.maxSectionSize}). Consider breaking into subsections or using imports`, Severity.INFO, new Location(section.line, 1)));
}
}
return violations;
}
/**
* Check for actionable content
*/
checkActionability(file) {
const violations = [];
// Check for statements without actions
const nonActionablePatterns = [
/(?:remember|note|keep in mind|don't forget)(?!\s+to\s+)/i,
// Match both "it's important" and "it is important"; the
// negative lookahead allows "it's important to <verb>" and
// "it is important to <verb>" through.
/it(?:'s| is) important(?!\s+to\s+)/i,
/you should know(?!\s+that\s+)/i,
];
for (let i = 0; i < file.lines.length; i++) {
const line = file.lines[i] ?? '';
for (const pattern of nonActionablePatterns) {
if (pattern.test(line)) {
violations.push(new Violation(this.id, 'Statement is not actionable. Convert to specific instruction: "Run tests before commit" instead of "Remember to test"', Severity.INFO, new Location(i + 1, 1)));
}
}
}
return violations;
}
/**
* Extract sections from file
*/
extractSections(file) {
const sections = [];
let currentSection = null;
for (let i = 0; i < file.lines.length; i++) {
const line = file.lines[i] ?? '';
const headerMatch = line.match(/^(#{1,3})\s+(.+)$/);
if (headerMatch) {
// Save previous section
if (currentSection) {
sections.push(currentSection);
}
// Start new section
currentSection = {
title: headerMatch[2] ?? '',
line: i + 1,
content: '',
};
}
else if (currentSection) {
currentSection.content += line + '\n';
}
}
// Save last section
if (currentSection) {
sections.push(currentSection);
}
return sections;
}
}
//# sourceMappingURL=ContentAppropriatenessRule.js.map