Skip to main content

claude_native/rules/project_specific/
micro_repo.rs

1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5// ── Rule μ1: README is the primary documentation ────────────────────
6
7pub struct ReadmeIsPrimary;
8
9impl Rule for ReadmeIsPrimary {
10    fn id(&self) -> &str { "μ1" }
11    fn name(&self) -> &str { "README is primary documentation" }
12    fn dimension(&self) -> Dimension { Dimension::CodeQuality }
13    fn severity(&self) -> Severity { Severity::Medium }
14
15    fn applies_to(&self, pt: &ProjectType) -> bool {
16        matches!(pt.primary, PrimaryType::MicroRepo)
17    }
18
19    fn check(&self, ctx: &ProjectContext) -> RuleResult {
20        if ctx.readme_content.is_none() {
21            return self.fail(
22                "Micro-repo has no README.md — this is the primary documentation for consumers",
23                Suggestion {
24                    priority: SuggestionPriority::QuickWin,
25                    title: "Create README.md".into(),
26                    description: "For micro-repos, README is THE documentation. Include: purpose, installation, usage examples, API surface. Claude reads this to understand the package contract.".into(),
27                    effort: Effort::Hour,
28                },
29            );
30        }
31
32        let _lines = ctx.readme_line_count();
33        let content = ctx.readme_content.as_ref().unwrap();
34        let lower = content.to_lowercase();
35
36        let has_install = lower.contains("install") || lower.contains("setup") || lower.contains("getting started");
37        let has_usage = lower.contains("usage") || lower.contains("example") || lower.contains("```");
38
39        if has_install && has_usage {
40            self.pass()
41        } else {
42            self.warn(
43                "README.md may be missing installation or usage sections",
44                Suggestion {
45                    priority: SuggestionPriority::NiceToHave,
46                    title: "Add install/usage to README".into(),
47                    description: "README should include installation and usage examples. Claude uses these to understand how consumers interact with your package.".into(),
48                    effort: Effort::Minutes,
49                },
50            )
51        }
52    }
53}
54
55// ── Rule μ2: Comprehensive tests ────────────────────────────────────
56
57pub struct ComprehensiveTests;
58
59impl Rule for ComprehensiveTests {
60    fn id(&self) -> &str { "μ2" }
61    fn name(&self) -> &str { "Comprehensive tests (micro-repo)" }
62    fn dimension(&self) -> Dimension { Dimension::CodeQuality }
63    fn severity(&self) -> Severity { Severity::High }
64
65    fn applies_to(&self, pt: &ProjectType) -> bool {
66        matches!(pt.primary, PrimaryType::MicroRepo)
67    }
68
69    fn check(&self, ctx: &ProjectContext) -> RuleResult {
70        let source_count = ctx.source_file_count();
71        let test_files = ctx.test_files.len();
72        let test_fns = ctx.test_function_count();
73
74        if source_count == 0 {
75            return self.skip();
76        }
77
78        // Use the better of two metrics: file ratio OR function-to-source ratio
79        let file_ratio = test_files as f64 / source_count as f64;
80        let fn_ratio = test_fns as f64 / source_count as f64;
81        let best_ratio = file_ratio.max(fn_ratio);
82
83        if best_ratio >= 0.5 {
84            self.pass()
85        } else if best_ratio >= 0.2 {
86            self.warn(
87                &format!("{test_fns} test functions in {test_files} files for {source_count} source files ({:.0}%).", best_ratio * 100.0),
88                Suggestion {
89                    priority: SuggestionPriority::HighImpact,
90                    title: "Add more tests".into(),
91                    description: "Micro-repos need higher coverage. Aim for at least 1 test per 2 source files.".into(),
92                    effort: Effort::HalfDay,
93                },
94            )
95        } else {
96            self.fail(
97                &format!("Micro-repo has {test_fns} test functions for {source_count} source files — needs more coverage"),
98                Suggestion {
99                    priority: SuggestionPriority::HighImpact,
100                    title: "Significantly improve test coverage".into(),
101                    description: "Micro-repos need high test coverage. Claude can't verify changes without tests.".into(),
102                    effort: Effort::HalfDay,
103                },
104            )
105        }
106    }
107}
108
109// ── Rule μ3: Package manifest is complete ───────────────────────────
110
111pub struct ManifestComplete;
112
113impl Rule for ManifestComplete {
114    fn id(&self) -> &str { "μ3" }
115    fn name(&self) -> &str { "Package manifest is complete" }
116    fn dimension(&self) -> Dimension { Dimension::CodeQuality }
117    fn severity(&self) -> Severity { Severity::Medium }
118
119    fn applies_to(&self, pt: &ProjectType) -> bool {
120        matches!(pt.primary, PrimaryType::MicroRepo)
121    }
122
123    fn check(&self, ctx: &ProjectContext) -> RuleResult {
124        // Check package.json completeness
125        if let Some(pj) = &ctx.package_json {
126            let has_name = pj.get("name").is_some();
127            let has_version = pj.get("version").is_some();
128            let has_description = pj.get("description").and_then(|d| d.as_str()).map(|s| !s.is_empty()).unwrap_or(false);
129            let has_license = pj.get("license").is_some();
130
131            let mut missing = Vec::new();
132            if !has_name { missing.push("name"); }
133            if !has_version { missing.push("version"); }
134            if !has_description { missing.push("description"); }
135            if !has_license { missing.push("license"); }
136
137            if missing.is_empty() {
138                return self.pass();
139            } else {
140                return self.warn(
141                    &format!("package.json missing: {}", missing.join(", ")),
142                    Suggestion {
143                        priority: SuggestionPriority::NiceToHave,
144                        title: "Complete package.json fields".into(),
145                        description: format!("Add missing fields: {}. Claude uses the manifest to understand the package contract and how consumers use it.", missing.join(", ")),
146                        effort: Effort::Minutes,
147                    },
148                );
149            }
150        }
151
152        // Check Cargo.toml completeness
153        if let Some(content) = ctx.read_root_file("Cargo.toml") {
154            let has_description = content.contains("description");
155            let has_license = content.contains("license");
156
157            if has_description && has_license {
158                return self.pass();
159            } else {
160                let mut missing = Vec::new();
161                if !has_description { missing.push("description"); }
162                if !has_license { missing.push("license"); }
163                return self.warn(
164                    &format!("Cargo.toml missing: {}", missing.join(", ")),
165                    Suggestion {
166                        priority: SuggestionPriority::NiceToHave,
167                        title: "Complete Cargo.toml fields".into(),
168                        description: format!("Add missing fields: {}. These help Claude understand the package purpose.", missing.join(", ")),
169                        effort: Effort::Minutes,
170                    },
171                );
172            }
173        }
174
175        self.pass()
176    }
177}
178
179// ── Rule μ4: Examples exist ─────────────────────────────────────────
180
181pub struct ExamplesExist;
182
183impl Rule for ExamplesExist {
184    fn id(&self) -> &str { "μ4" }
185    fn name(&self) -> &str { "Examples directory or inline examples" }
186    fn dimension(&self) -> Dimension { Dimension::Navigation }
187    fn severity(&self) -> Severity { Severity::Low }
188
189    fn applies_to(&self, pt: &ProjectType) -> bool {
190        matches!(pt.primary, PrimaryType::MicroRepo)
191    }
192
193    fn check(&self, ctx: &ProjectContext) -> RuleResult {
194        let has_examples_dir = ctx.has_file("examples") || ctx.root.join("examples").is_dir();
195        let has_readme_examples = ctx.readme_content.as_ref().map(|c| {
196            c.contains("```") && (c.to_lowercase().contains("example") || c.to_lowercase().contains("usage"))
197        }).unwrap_or(false);
198
199        if has_examples_dir || has_readme_examples {
200            self.pass()
201        } else {
202            self.warn(
203                "No examples/ directory or code examples in README",
204                Suggestion {
205                    priority: SuggestionPriority::NiceToHave,
206                    title: "Add usage examples".into(),
207                    description: "Create an examples/ directory or add code examples to README. Claude uses examples to understand intended usage patterns.".into(),
208                    effort: Effort::Hour,
209                },
210            )
211        }
212    }
213}