claude_native/rules/project_specific/
codegen.rs1use 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
9pub 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
48pub 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
89pub 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
129pub 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 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}