claude_native/rules/project_specific/
frontend.rs1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_frontend(pt: &ProjectType) -> bool {
6 matches!(pt.primary, PrimaryType::Frontend(_))
7}
8
9pub struct BuildCacheIgnored;
12
13impl Rule for BuildCacheIgnored {
14 fn id(&self) -> &str { "FE1" }
15 fn name(&self) -> &str { "Build cache directories 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_frontend(pt) }
20
21 fn check(&self, ctx: &ProjectContext) -> RuleResult {
22 let caches = [".next", ".nuxt", ".angular", "dist", "build", ".vercel", ".turbo"];
23 if ctx.claudeignore_content.is_none() {
24 return self.fail(
25 "No .claudeignore — framework build caches (.next/ can be 500MB+) are visible",
26 Suggestion {
27 priority: SuggestionPriority::QuickWin,
28 title: "Create .claudeignore for frontend project".into(),
29 description: format!("Add to .claudeignore: {}, node_modules/, coverage/", caches.join("/, ") + "/"),
30 effort: Effort::Minutes,
31 },
32 );
33 }
34
35 let missing: Vec<&&str> = caches.iter()
36 .filter(|c| ctx.has_file(c) && !ctx.claudeignore_contains(c))
37 .collect();
38
39 if missing.is_empty() {
40 self.pass()
41 } else {
42 self.fail(
43 &format!("Build caches not in .claudeignore: {}", missing.iter().map(|m| m.to_string()).collect::<Vec<_>>().join(", ")),
44 Suggestion {
45 priority: SuggestionPriority::QuickWin,
46 title: "Add build caches to .claudeignore".into(),
47 description: format!("Add: {}", missing.iter().map(|m| format!("{m}/")).collect::<Vec<_>>().join(", ")),
48 effort: Effort::Minutes,
49 },
50 )
51 }
52 }
53}
54
55pub struct SourceMapsIgnored;
58
59impl Rule for SourceMapsIgnored {
60 fn id(&self) -> &str { "FE2" }
61 fn name(&self) -> &str { "Source maps are ignored" }
62 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
63 fn severity(&self) -> Severity { Severity::High }
64
65 fn applies_to(&self, pt: &ProjectType) -> bool { is_frontend(pt) }
66
67 fn check(&self, ctx: &ProjectContext) -> RuleResult {
68 let has_maps = ctx.all_files.iter().any(|f| {
69 f.path.extension().map(|e| e == "map").unwrap_or(false)
70 });
71
72 if !has_maps {
73 return self.pass();
74 }
75
76 if ctx.claudeignore_contains(".map") || ctx.claudeignore_contains("*.map") {
77 self.pass()
78 } else {
79 self.fail(
80 "Source map files (.map) found but not in .claudeignore",
81 Suggestion {
82 priority: SuggestionPriority::QuickWin,
83 title: "Ignore source maps".into(),
84 description: "Add *.map to .claudeignore. Source maps can be larger than the original source and Claude never needs them.".into(),
85 effort: Effort::Minutes,
86 },
87 )
88 }
89 }
90}
91
92pub struct EnvExampleExists;
95
96impl Rule for EnvExampleExists {
97 fn id(&self) -> &str { "FE3" }
98 fn name(&self) -> &str { ".env.example documents env vars" }
99 fn dimension(&self) -> Dimension { Dimension::Foundation }
100 fn severity(&self) -> Severity { Severity::High }
101
102 fn applies_to(&self, pt: &ProjectType) -> bool { is_frontend(pt) }
103
104 fn check(&self, ctx: &ProjectContext) -> RuleResult {
105 let has_real_env = ctx.env_files.iter().any(|f| {
106 let name = f.file_name().and_then(|n| n.to_str()).unwrap_or("");
107 !name.contains("example") && !name.contains("sample")
108 });
109
110 if !has_real_env {
111 return self.pass();
112 }
113
114 let has_example = ctx.has_file(".env.example") || ctx.has_file(".env.sample");
115 if has_example {
116 self.pass()
117 } else {
118 self.fail(
119 ".env file exists but no .env.example to document required variables",
120 Suggestion {
121 priority: SuggestionPriority::QuickWin,
122 title: "Create .env.example".into(),
123 description: "Create .env.example with all required variable names (no real values). Claude reads this to understand the env shape without seeing secrets.".into(),
124 effort: Effort::Minutes,
125 },
126 )
127 }
128 }
129}
130
131pub struct TypeCheckBeforeBuild;
134
135impl Rule for TypeCheckBeforeBuild {
136 fn id(&self) -> &str { "FE4" }
137 fn name(&self) -> &str { "Type-check prioritized over build" }
138 fn dimension(&self) -> Dimension { Dimension::Foundation }
139 fn severity(&self) -> Severity { Severity::Medium }
140
141 fn applies_to(&self, pt: &ProjectType) -> bool { is_frontend(pt) }
142
143 fn check(&self, ctx: &ProjectContext) -> RuleResult {
144 let content = match &ctx.claude_md_content {
145 Some(c) => c.to_lowercase(),
146 None => return self.skip(),
147 };
148
149 let has_typecheck = content.contains("tsc")
150 || content.contains("type-check")
151 || content.contains("typecheck")
152 || content.contains("--noemit");
153
154 if has_typecheck {
155 self.pass()
156 } else {
157 self.warn(
158 "CLAUDE.md doesn't mention type-checking (tsc --noEmit)",
159 Suggestion {
160 priority: SuggestionPriority::NiceToHave,
161 title: "Add type-check command to CLAUDE.md".into(),
162 description: "Add `tsc --noEmit` or equivalent. Type-checking catches 90% of errors in 5 seconds vs 2+ minutes for a full build.".into(),
163 effort: Effort::Minutes,
164 },
165 )
166 }
167 }
168}