claude_native/rules/project_specific/
legacy.rs1use crate::detection::ProjectType;
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_legacy(pt: &ProjectType) -> bool {
6 pt.flags.is_legacy
7}
8
9pub struct DocumentsTheMess;
12
13impl Rule for DocumentsTheMess {
14 fn id(&self) -> &str { "LEG1" }
15 fn name(&self) -> &str { "CLAUDE.md documents known inconsistencies" }
16 fn dimension(&self) -> Dimension { Dimension::Foundation }
17 fn severity(&self) -> Severity { Severity::High }
18
19 fn applies_to(&self, pt: &ProjectType) -> bool { is_legacy(pt) }
20
21 fn check(&self, ctx: &ProjectContext) -> RuleResult {
22 let content = match &ctx.claude_md_content {
23 Some(c) => c.to_lowercase(),
24 None => return self.fail(
25 "Legacy project has no CLAUDE.md — Claude is working blind in a messy codebase",
26 Suggestion {
27 priority: SuggestionPriority::QuickWin,
28 title: "Create CLAUDE.md documenting the mess".into(),
29 description: "Legacy projects NEED CLAUDE.md more than any other type. Document: known inconsistencies, which patterns are 'correct' vs 'legacy', dead code directories, and the preferred approach for changes.".into(),
30 effort: Effort::Hour,
31 },
32 ),
33 };
34
35 let documents_issues = content.contains("legacy")
36 || content.contains("deprecated")
37 || content.contains("inconsisten")
38 || content.contains("old pattern")
39 || content.contains("do not use")
40 || content.contains("don't use")
41 || content.contains("prefer");
42
43 if documents_issues {
44 self.pass()
45 } else {
46 self.warn(
47 "CLAUDE.md doesn't mention legacy patterns or inconsistencies",
48 Suggestion {
49 priority: SuggestionPriority::HighImpact,
50 title: "Document legacy patterns in CLAUDE.md".into(),
51 description: "Add a section listing: which patterns are correct vs legacy, dead code directories, and the incremental approach. Without this, Claude picks the wrong pattern 50% of the time.".into(),
52 effort: Effort::Hour,
53 },
54 )
55 }
56 }
57}
58
59pub struct CorrectPatternsIdentified;
62
63impl Rule for CorrectPatternsIdentified {
64 fn id(&self) -> &str { "LEG2" }
65 fn name(&self) -> &str { "Correct patterns explicitly identified" }
66 fn dimension(&self) -> Dimension { Dimension::Foundation }
67 fn severity(&self) -> Severity { Severity::High }
68
69 fn applies_to(&self, pt: &ProjectType) -> bool { is_legacy(pt) }
70
71 fn check(&self, ctx: &ProjectContext) -> RuleResult {
72 let content = match &ctx.claude_md_content {
73 Some(c) => c.to_lowercase(),
74 None => return self.skip(),
75 };
76
77 let has_pattern_refs = content.contains("follow ")
79 || content.contains("use the pattern in")
80 || content.contains("reference:")
81 || (content.contains("not ") && content.contains("pattern"));
82
83 if has_pattern_refs {
84 self.pass()
85 } else {
86 self.fail(
87 "CLAUDE.md doesn't explicitly identify which patterns are correct",
88 Suggestion {
89 priority: SuggestionPriority::QuickWin,
90 title: "Identify correct patterns with file references".into(),
91 description: "Add to CLAUDE.md:\n- Error handling: follow `src/services/auth.ts` (NOT `src/handlers/legacy.ts`)\n- Data access: use `src/db/repository.ts` pattern\nClaude needs ONE canonical reference per concern.".into(),
92 effort: Effort::Minutes,
93 },
94 )
95 }
96 }
97}
98
99pub struct TestsForModifiedCode;
102
103impl Rule for TestsForModifiedCode {
104 fn id(&self) -> &str { "LEG3" }
105 fn name(&self) -> &str { "Tests exist for code being modified" }
106 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
107 fn severity(&self) -> Severity { Severity::High }
108
109 fn applies_to(&self, pt: &ProjectType) -> bool { is_legacy(pt) }
110
111 fn check(&self, ctx: &ProjectContext) -> RuleResult {
112 if ctx.test_files.is_empty() {
114 self.fail(
115 "Legacy project has ZERO tests — changes can't be verified",
116 Suggestion {
117 priority: SuggestionPriority::HighImpact,
118 title: "Add tests for critical modules".into(),
119 description: "Write tests for the specific modules you're modifying. Claude should write a test FIRST, then make changes. Full coverage is unrealistic in legacy — start with critical paths.".into(),
120 effort: Effort::HalfDay,
121 },
122 )
123 } else {
124 self.pass()
125 }
126 }
127}
128
129pub struct DeadCodeFlagged;
132
133impl Rule for DeadCodeFlagged {
134 fn id(&self) -> &str { "LEG4" }
135 fn name(&self) -> &str { "Dead code directories are flagged" }
136 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
137 fn severity(&self) -> Severity { Severity::Medium }
138
139 fn applies_to(&self, pt: &ProjectType) -> bool { is_legacy(pt) }
140
141 fn check(&self, ctx: &ProjectContext) -> RuleResult {
142 let dead_dirs = ["deprecated", "old", "backup", "archive", "legacy", "unused"];
143 let found: Vec<String> = ctx.directories.iter()
144 .filter_map(|d| {
145 let name = d.file_name()?.to_str()?.to_lowercase();
146 if dead_dirs.contains(&name.as_str()) { Some(name) } else { None }
147 })
148 .collect();
149
150 if found.is_empty() {
151 return self.pass();
152 }
153
154 let documented = ctx.claude_md_content.as_ref().map(|c| {
156 let lower = c.to_lowercase();
157 found.iter().any(|d| lower.contains(d))
158 }).unwrap_or(false);
159
160 if documented {
161 self.pass()
162 } else {
163 self.warn(
164 &format!("Dead code directories exist ({}) but aren't documented in CLAUDE.md", found.join(", ")),
165 Suggestion {
166 priority: SuggestionPriority::QuickWin,
167 title: "Document dead code in CLAUDE.md".into(),
168 description: format!("Add to CLAUDE.md:\n# Dead code (do not use)\n{}\nThis prevents Claude from reading/using deprecated code.", found.iter().map(|d| format!("- {d}/")).collect::<Vec<_>>().join("\n")),
169 effort: Effort::Minutes,
170 },
171 )
172 }
173 }
174}
175
176pub struct MegaFilesDocumented;
179
180impl Rule for MegaFilesDocumented {
181 fn id(&self) -> &str { "LEG5" }
182 fn name(&self) -> &str { "Large legacy files have line-range docs" }
183 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
184 fn severity(&self) -> Severity { Severity::Medium }
185
186 fn applies_to(&self, pt: &ProjectType) -> bool { is_legacy(pt) }
187
188 fn check(&self, ctx: &ProjectContext) -> RuleResult {
189 let mega = ctx.mega_files(500);
190 if mega.is_empty() {
191 return self.pass();
192 }
193
194 let documented = ctx.claude_md_content.as_ref().map(|c| {
195 c.contains("L") && (c.contains("-") || c.contains("lines"))
196 }).unwrap_or(false);
197
198 if documented {
199 self.pass()
200 } else {
201 let examples: Vec<String> = mega.iter().take(3)
202 .map(|f| format!("{} ({} lines)", f.relative_path.display(), f.line_count))
203 .collect();
204 self.warn(
205 &format!("Large legacy files exist without line-range documentation:\n {}", examples.join("\n ")),
206 Suggestion {
207 priority: SuggestionPriority::NiceToHave,
208 title: "Document line ranges in mega-files".into(),
209 description: format!("Add to CLAUDE.md:\n# Large files\n{}\nTell Claude where key logic lives so it doesn't read the entire file.", examples.iter().map(|e| format!("- {e} — auth: L200-350, routing: L400-600")).collect::<Vec<_>>().join("\n")),
210 effort: Effort::Minutes,
211 },
212 )
213 }
214 }
215}