Skip to main content

claude_native/rules/project_specific/
frontend.rs

1use 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
9// ── Rule FE1: Build cache directories ignored ───────────────────────
10
11pub 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
55// ── Rule FE2: Source maps ignored ───────────────────────────────────
56
57pub 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
92// ── Rule FE3: .env.example companion ────────────────────────────────
93
94pub 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
131// ── Rule FE4: Type-check before build ───────────────────────────────
132
133pub 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}