Skip to main content

claude_native/rules/
context_extra.rs

1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4// ── Rule 2.5: No secrets in the repo ────────────────────────────────
5
6pub struct NoSecretsInRepo;
7
8impl Rule for NoSecretsInRepo {
9    fn id(&self) -> &str { "2.5" }
10    fn name(&self) -> &str { "No secrets in the repository" }
11    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
12    fn severity(&self) -> Severity { Severity::Critical }
13
14    fn check(&self, ctx: &ProjectContext) -> RuleResult {
15        if ctx.env_files.is_empty() { return self.pass(); }
16        let real: Vec<_> = ctx.env_files.iter()
17            .filter(|f| {
18                let name = f.file_name().and_then(|n| n.to_str()).unwrap_or("");
19                !name.contains("example") && !name.contains("sample") && !name.contains("template")
20            })
21            .collect();
22        if real.is_empty() { return self.pass(); }
23        let names: Vec<String> = real.iter().take(5)
24            .filter_map(|f| f.file_name().and_then(|n| n.to_str()).map(String::from))
25            .collect();
26        self.fail(
27            &format!("Potential secret files: {}", names.join(", ")),
28            Suggestion {
29                priority: SuggestionPriority::QuickWin,
30                title: "Remove/ignore secret files".into(),
31                description: "Add .env, .env.*, *.pem, *.key to .gitignore AND .claudeignore.".into(),
32                effort: Effort::Minutes,
33            },
34        )
35    }
36}
37
38// ── Rule 2.6: README exists and is concise ──────────────────────────
39
40pub struct ReadmeExistsAndConcise;
41
42impl Rule for ReadmeExistsAndConcise {
43    fn id(&self) -> &str { "2.6" }
44    fn name(&self) -> &str { "README exists and is concise" }
45    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
46    fn severity(&self) -> Severity { Severity::Low }
47
48    fn check(&self, ctx: &ProjectContext) -> RuleResult {
49        if ctx.readme_content.is_none() {
50            return self.fail("No README.md found", Suggestion {
51                priority: SuggestionPriority::NiceToHave,
52                title: "Create README.md".into(),
53                description: "Create a concise README.md (<300 lines).".into(),
54                effort: Effort::Hour,
55            });
56        }
57        let lines = ctx.readme_line_count();
58        if lines <= 300 { self.pass() }
59        else {
60            self.warn(
61                &format!("README.md is {lines} lines (target: <300)"),
62                Suggestion {
63                    priority: SuggestionPriority::NiceToHave,
64                    title: "Trim README.md".into(),
65                    description: "Move detailed docs to docs/.".into(),
66                    effort: Effort::Hour,
67                },
68            )
69        }
70    }
71}
72
73// ── Rule 2.7: Subdirectory CLAUDE.md for large projects ─────────────
74
75pub struct SubdirClaudeMd;
76
77impl Rule for SubdirClaudeMd {
78    fn id(&self) -> &str { "2.7" }
79    fn name(&self) -> &str { "Subdirectory CLAUDE.md for large projects" }
80    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
81    fn severity(&self) -> Severity { Severity::Low }
82
83    fn check(&self, ctx: &ProjectContext) -> RuleResult {
84        let count = ctx.source_file_count();
85        if count <= 20 { return self.pass(); }
86        if !ctx.subdirectory_claude_mds.is_empty() { self.pass() }
87        else {
88            self.warn(
89                &format!("{count} source files but no subdirectory CLAUDE.md files"),
90                Suggestion {
91                    priority: SuggestionPriority::NiceToHave,
92                    title: "Add CLAUDE.md to major subdirectories".into(),
93                    description: "Subdirectory CLAUDE.md files load on-demand, saving tokens.".into(),
94                    effort: Effort::Hour,
95                },
96            )
97        }
98    }
99}