Skip to main content

claude_native/rules/project_specific/
legacy.rs

1use crate::detection::ProjectType;
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_legacy(pt: &ProjectType) -> bool {
6    pt.flags.is_legacy
7}
8
9// ── Rule LEG1: CLAUDE.md documents the mess ─────────────────────────
10
11pub 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
59// ── Rule LEG2: Correct patterns are identified ──────────────────────
60
61pub 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        // Check for pattern references with file paths
78        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
99// ── Rule LEG3: Tests exist for modified code ────────────────────────
100
101pub 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        // In legacy projects, any tests are a win
113        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
129// ── Rule LEG4: Dead code is flagged ─────────────────────────────────
130
131pub 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        // Check if they're documented in CLAUDE.md
155        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
176// ── Rule LEG5: Mega-files are documented ────────────────────────────
177
178pub 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}