Skip to main content

claude_native/rules/project_specific/
codegen.rs

1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_codegen(pt: &ProjectType) -> bool {
6    matches!(pt.primary, PrimaryType::CodegenHeavy)
7}
8
9// ── Rule GEN1: Generated code directories ignored ───────────────────
10
11pub struct GeneratedDirsIgnored;
12
13impl Rule for GeneratedDirsIgnored {
14    fn id(&self) -> &str { "GEN1" }
15    fn name(&self) -> &str { "Generated code directories are ignored" }
16    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17    fn severity(&self) -> Severity { Severity::Critical }
18
19    fn applies_to(&self, pt: &ProjectType) -> bool { is_codegen(pt) }
20
21    fn check(&self, ctx: &ProjectContext) -> RuleResult {
22        let gen_count = ctx.all_files.iter().filter(|f| f.is_generated).count();
23        if gen_count == 0 {
24            return self.pass();
25        }
26
27        let ignored = ctx.claudeignore_contains("generated")
28            || ctx.claudeignore_contains("gen/")
29            || ctx.claudeignore_contains("*.pb.")
30            || ctx.claudeignore_contains("*.g.dart");
31
32        if ignored {
33            self.pass()
34        } else {
35            self.fail(
36                &format!("{gen_count} generated files found but not excluded in .claudeignore"),
37                Suggestion {
38                    priority: SuggestionPriority::QuickWin,
39                    title: "Ignore generated code directories".into(),
40                    description: "Add to .claudeignore: src/generated/, gen/, *_generated.*, *.pb.go, *.pb.ts, *.g.dart. Claude should read specs, not generated output.".into(),
41                    effort: Effort::Minutes,
42                },
43            )
44        }
45    }
46}
47
48// ── Rule GEN2: Edit specs, not generated code ───────────────────────
49
50pub struct EditSpecsNotGenerated;
51
52impl Rule for EditSpecsNotGenerated {
53    fn id(&self) -> &str { "GEN2" }
54    fn name(&self) -> &str { "CLAUDE.md says edit specs, not generated" }
55    fn dimension(&self) -> Dimension { Dimension::Foundation }
56    fn severity(&self) -> Severity { Severity::High }
57
58    fn applies_to(&self, pt: &ProjectType) -> bool { is_codegen(pt) }
59
60    fn check(&self, ctx: &ProjectContext) -> RuleResult {
61        let content = match &ctx.claude_md_content {
62            Some(c) => c.to_lowercase(),
63            None => return self.skip(),
64        };
65
66        let has_instruction = content.contains("generated")
67            || content.contains("don't edit")
68            || content.contains("do not edit")
69            || content.contains("auto-generated")
70            || content.contains("codegen")
71            || (content.contains("proto") && content.contains("edit"));
72
73        if has_instruction {
74            self.pass()
75        } else {
76            self.fail(
77                "CLAUDE.md doesn't instruct to edit specs instead of generated code",
78                Suggestion {
79                    priority: SuggestionPriority::QuickWin,
80                    title: "Add 'edit specs, not generated code' rule".into(),
81                    description: "Add to CLAUDE.md: 'Edit .proto/.graphql/schema.prisma files — NEVER edit generated code. Regeneration overwrites changes.' This is the #1 mistake.".into(),
82                    effort: Effort::Minutes,
83                },
84            )
85        }
86    }
87}
88
89// ── Rule GEN3: Regeneration command documented ──────────────────────
90
91pub struct RegenCommandDocumented;
92
93impl Rule for RegenCommandDocumented {
94    fn id(&self) -> &str { "GEN3" }
95    fn name(&self) -> &str { "Regeneration command is documented" }
96    fn dimension(&self) -> Dimension { Dimension::Foundation }
97    fn severity(&self) -> Severity { Severity::High }
98
99    fn applies_to(&self, pt: &ProjectType) -> bool { is_codegen(pt) }
100
101    fn check(&self, ctx: &ProjectContext) -> RuleResult {
102        let content = match &ctx.claude_md_content {
103            Some(c) => c.to_lowercase(),
104            None => return self.skip(),
105        };
106
107        let has_regen = content.contains("generate")
108            || content.contains("codegen")
109            || content.contains("protoc")
110            || content.contains("prisma generate")
111            || content.contains("buf generate");
112
113        if has_regen {
114            self.pass()
115        } else {
116            self.fail(
117                "CLAUDE.md doesn't document the code generation command",
118                Suggestion {
119                    priority: SuggestionPriority::QuickWin,
120                    title: "Document codegen command".into(),
121                    description: "Add the regeneration command to CLAUDE.md (protoc, prisma generate, npm run codegen, etc.). Without it, Claude skips regeneration after spec changes.".into(),
122                    effort: Effort::Minutes,
123                },
124            )
125        }
126    }
127}
128
129// ── Rule GEN4: Auto-regeneration hook ───────────────────────────────
130
131pub struct AutoRegenHook;
132
133impl Rule for AutoRegenHook {
134    fn id(&self) -> &str { "GEN4" }
135    fn name(&self) -> &str { "PostToolUse hook auto-regenerates" }
136    fn dimension(&self) -> Dimension { Dimension::Tooling }
137    fn severity(&self) -> Severity { Severity::Medium }
138
139    fn applies_to(&self, pt: &ProjectType) -> bool { is_codegen(pt) }
140
141    fn check(&self, ctx: &ProjectContext) -> RuleResult {
142        if ctx.has_post_tool_use_hook_for_format() {
143            // If any PostToolUse hooks exist, it's likely codegen is handled
144            self.pass()
145        } else {
146            self.warn(
147                "No PostToolUse hook for auto-regeneration after spec edits",
148                Suggestion {
149                    priority: SuggestionPriority::NiceToHave,
150                    title: "Add auto-regeneration hook".into(),
151                    description: "Add a PostToolUse hook that detects spec file edits (.proto, .graphql, .prisma) and runs codegen automatically. Eliminates a whole class of type-mismatch bugs.".into(),
152                    effort: Effort::Hour,
153                },
154            )
155        }
156    }
157}