1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4pub struct ClaudeMdExists;
7
8impl Rule for ClaudeMdExists {
9 fn id(&self) -> &str { "1.1" }
10 fn name(&self) -> &str { "CLAUDE.md must exist" }
11 fn dimension(&self) -> Dimension { Dimension::Foundation }
12 fn severity(&self) -> Severity { Severity::Critical }
13
14 fn check(&self, ctx: &ProjectContext) -> RuleResult {
15 if ctx.has_claude_md() {
16 self.pass()
17 } else {
18 self.fail(
19 "No CLAUDE.md found at project root or .claude/CLAUDE.md",
20 Suggestion {
21 priority: SuggestionPriority::QuickWin,
22 title: "Create a CLAUDE.md file".into(),
23 description: "Run `claude-native --init` to auto-generate CLAUDE.md with build/test commands for your detected project type. Or create manually with:\n # Project Name\n Build: `<your build cmd>`\n Test: `<your test cmd>`\n ## Code Patterns\n <describe conventions>".into(),
24 effort: Effort::Minutes,
25 },
26 )
27 }
28 }
29}
30
31pub struct ClaudeMdConcise;
34
35impl Rule for ClaudeMdConcise {
36 fn id(&self) -> &str { "1.2" }
37 fn name(&self) -> &str { "CLAUDE.md is concise (<200 lines)" }
38 fn dimension(&self) -> Dimension { Dimension::Foundation }
39 fn severity(&self) -> Severity { Severity::High }
40
41 fn check(&self, ctx: &ProjectContext) -> RuleResult {
42 if ctx.claude_md_content.is_none() {
43 return self.skip();
44 }
45
46 let lines = ctx.claude_md_line_count();
47 if lines <= 200 {
48 self.pass()
49 } else if lines <= 400 {
50 self.warn(
51 &format!("CLAUDE.md is {lines} lines (target: <200). Every line costs tokens on every request."),
52 Suggestion {
53 priority: SuggestionPriority::HighImpact,
54 title: "Trim CLAUDE.md below 200 lines".into(),
55 description: "Move specialized instructions to .claude/rules/ (path-scoped, loaded on-demand) or .claude/skills/ (invoked explicitly). Keep only essentials in CLAUDE.md.".into(),
56 effort: Effort::Hour,
57 },
58 )
59 } else {
60 self.fail(
61 &format!("CLAUDE.md is {lines} lines (target: <200). This wastes ~{} extra tokens per request.", (lines - 200) * 2),
62 Suggestion {
63 priority: SuggestionPriority::HighImpact,
64 title: "Significantly reduce CLAUDE.md".into(),
65 description: "Your CLAUDE.md is very long. Move domain-specific rules to .claude/rules/*.md with paths: frontmatter. Move workflows to .claude/skills/. Keep CLAUDE.md to: build/test commands, key conventions, gotchas.".into(),
66 effort: Effort::Hour,
67 },
68 )
69 }
70 }
71}
72
73pub struct ClaudeMdActionable;
76
77impl Rule for ClaudeMdActionable {
78 fn id(&self) -> &str { "1.3" }
79 fn name(&self) -> &str { "CLAUDE.md has actionable instructions" }
80 fn dimension(&self) -> Dimension { Dimension::Foundation }
81 fn severity(&self) -> Severity { Severity::Medium }
82
83 fn check(&self, ctx: &ProjectContext) -> RuleResult {
84 let content = match &ctx.claude_md_content {
85 Some(c) => c,
86 None => return self.skip(),
87 };
88 let (has_actionable, prose_ratio) = analyze_actionability(content);
89 if has_actionable {
90 if prose_ratio > 0.6 {
91 self.warn(
92 "CLAUDE.md seems too prose-heavy. It should focus on commands and rules, not explanations.",
93 Suggestion {
94 priority: SuggestionPriority::NiceToHave,
95 title: "Make CLAUDE.md more actionable".into(),
96 description: "Replace prose paragraphs with bullet points and code blocks. Claude needs commands and rules, not tutorials.".into(),
97 effort: Effort::Minutes,
98 },
99 )
100 } else {
101 self.pass()
102 }
103 } else {
104 self.fail(
105 "CLAUDE.md appears to lack code blocks or runnable commands",
106 Suggestion {
107 priority: SuggestionPriority::QuickWin,
108 title: "Add build/test commands to CLAUDE.md".into(),
109 description: "Add code-fenced commands that Claude can run: build, test, lint. Format as `command here`. Claude can't infer your project's specific commands.".into(),
110 effort: Effort::Minutes,
111 },
112 )
113 }
114 }
115}
116
117fn analyze_actionability(content: &str) -> (bool, f64) {
118 let has_code = content.contains("```") || content.contains(" ");
119 let has_cmds = content.contains('`') && ["npm ", "cargo ", "go ", "python ", "flutter ", "make ", "./"]
120 .iter().any(|c| content.contains(c));
121 let total = content.lines().count();
122 let prose = content.lines().filter(|l| {
123 let l = l.trim();
124 !l.is_empty() && !l.starts_with('#') && !l.starts_with('-')
125 && !l.starts_with('*') && !l.starts_with('`') && !l.starts_with("```")
126 }).count();
127 let ratio = if total > 0 { prose as f64 / total as f64 } else { 0.0 };
128 (has_code || has_cmds, ratio)
129}
130
131pub struct ClaudeMdHasCommands;
134
135impl Rule for ClaudeMdHasCommands {
136 fn id(&self) -> &str { "1.4" }
137 fn name(&self) -> &str { "CLAUDE.md has build/test commands" }
138 fn dimension(&self) -> Dimension { Dimension::Foundation }
139 fn severity(&self) -> Severity { Severity::High }
140
141 fn check(&self, ctx: &ProjectContext) -> RuleResult {
142 let content = match &ctx.claude_md_content {
143 Some(c) => c,
144 None => return self.skip(),
145 };
146
147 let lower = content.to_lowercase();
148 let has_build = lower.contains("build")
149 || lower.contains("compile")
150 || lower.contains("make");
151 let has_test = lower.contains("test")
152 || lower.contains("spec")
153 || lower.contains("check");
154
155 if has_build && has_test {
156 self.pass()
157 } else if has_build || has_test {
158 self.warn(
159 &format!(
160 "CLAUDE.md is missing {} commands",
161 if has_build { "test" } else { "build" }
162 ),
163 Suggestion {
164 priority: SuggestionPriority::QuickWin,
165 title: format!("Add {} command to CLAUDE.md", if has_build { "test" } else { "build" }),
166 description: "Claude needs to know how to verify its own changes. Add both build and test commands.".into(),
167 effort: Effort::Minutes,
168 },
169 )
170 } else {
171 self.fail(
172 "CLAUDE.md has no build or test commands",
173 Suggestion {
174 priority: SuggestionPriority::QuickWin,
175 title: "Add build and test commands to CLAUDE.md".into(),
176 description: "Add lines like:\n Build: `cargo build`\n Test: `cargo test`\nClaude uses these to verify its changes work.".into(),
177 effort: Effort::Minutes,
178 },
179 )
180 }
181 }
182}
183
184pub struct ClaudeignoreExists;
187
188impl Rule for ClaudeignoreExists {
189 fn id(&self) -> &str { "1.5" }
190 fn name(&self) -> &str { ".claudeignore exists and excludes noise" }
191 fn dimension(&self) -> Dimension { Dimension::Foundation }
192 fn severity(&self) -> Severity { Severity::High }
193
194 fn check(&self, ctx: &ProjectContext) -> RuleResult {
195 if ctx.claudeignore_content.is_some() {
196 self.pass()
197 } else {
198 self.fail(
199 "No .claudeignore file found",
200 Suggestion {
201 priority: SuggestionPriority::QuickWin,
202 title: "Create .claudeignore".into(),
203 description: "Run `claude-native --init` to auto-generate .claudeignore for your project type. Or create manually with:\n node_modules/\n .venv/\n vendor/\n dist/\n build/\n target/\n coverage/\n Cargo.lock\n *.log\n .env\n .DS_Store".into(),
204 effort: Effort::Minutes,
205 },
206 )
207 }
208 }
209}
210
211pub struct ClaudeDirExists;
214
215impl Rule for ClaudeDirExists {
216 fn id(&self) -> &str { "1.6" }
217 fn name(&self) -> &str { ".claude/ directory exists" }
218 fn dimension(&self) -> Dimension { Dimension::Foundation }
219 fn severity(&self) -> Severity { Severity::Medium }
220
221 fn check(&self, ctx: &ProjectContext) -> RuleResult {
222 if ctx.has_claude_dir {
223 self.pass()
224 } else {
225 self.fail(
226 "No .claude/ directory found",
227 Suggestion {
228 priority: SuggestionPriority::QuickWin,
229 title: "Create .claude/ directory".into(),
230 description: "Create .claude/ at project root. This is the home for: settings.json (permissions), rules/ (path-scoped instructions), skills/ (custom workflows), and hooks.".into(),
231 effort: Effort::Minutes,
232 },
233 )
234 }
235 }
236}
237
238pub struct SettingsJsonExists;
241
242impl Rule for SettingsJsonExists {
243 fn id(&self) -> &str { "1.7" }
244 fn name(&self) -> &str { "settings.json has permissions" }
245 fn dimension(&self) -> Dimension { Dimension::Foundation }
246 fn severity(&self) -> Severity { Severity::Medium }
247
248 fn check(&self, ctx: &ProjectContext) -> RuleResult {
249 if ctx.settings_json.is_none() {
250 return self.fail(
251 "No .claude/settings.json found",
252 Suggestion {
253 priority: SuggestionPriority::QuickWin,
254 title: "Create .claude/settings.json".into(),
255 description: "Run `claude-native --init` to auto-generate settings.json. Or create manually:\n {\"permissions\": {\"allow\": [\"Bash(cargo test:*)\", \"Bash(git:*)\"]}}\nPre-approved commands let Claude work without interrupting you.".into(),
256 effort: Effort::Minutes,
257 },
258 );
259 }
260
261 if ctx.settings_has_permissions() {
262 self.pass()
263 } else {
264 self.warn(
265 "settings.json exists but has no permission allow-list",
266 Suggestion {
267 priority: SuggestionPriority::QuickWin,
268 title: "Add permissions to settings.json".into(),
269 description: "Add a permissions.allow list for common safe commands (test runners, build tools, git). This lets Claude work without interrupting you for every command.".into(),
270 effort: Effort::Minutes,
271 },
272 )
273 }
274 }
275}
276
277