Skip to main content

claude_native/rules/
foundation.rs

1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4// ── Rule 1.1: CLAUDE.md must exist ──────────────────────────────────
5
6pub 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
31// ── Rule 1.2: CLAUDE.md is concise ─────────────────────────────────
32
33pub 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
73// ── Rule 1.3: CLAUDE.md contains actionable instructions ────────────
74
75pub 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
131// ── Rule 1.4: CLAUDE.md has build/test commands ─────────────────────
132
133pub 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
184// ── Rule 1.5: .claudeignore exists ──────────────────────────────────
185
186pub 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
211// ── Rule 1.6: .claude/ directory exists ─────────────────────────────
212
213pub 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
238// ── Rule 1.7: .claude/settings.json with permissions ────────────────
239
240pub 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// Rule 1.8 (AgentsMdExists) is in foundation_extra.rs