claude_native/rules/project_specific/
micro_repo.rs1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5pub 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
55pub 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 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
109pub 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 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 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
179pub 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}