claude_native/rules/project_specific/
doc_site.rs1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_doc_site(pt: &ProjectType) -> bool {
6 matches!(pt.primary, PrimaryType::DocSite(_))
7}
8
9pub struct BuildOutputIgnored;
12
13impl Rule for BuildOutputIgnored {
14 fn id(&self) -> &str { "DOC1" }
15 fn name(&self) -> &str { "Build output is ignored" }
16 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17 fn severity(&self) -> Severity { Severity::High }
18
19 fn applies_to(&self, pt: &ProjectType) -> bool { is_doc_site(pt) }
20
21 fn check(&self, ctx: &ProjectContext) -> RuleResult {
22 let outputs = ["build", "dist", "public", ".docusaurus", "site", "_site", "out"];
23 let present: Vec<&&str> = outputs.iter().filter(|o| ctx.root.join(o).is_dir()).collect();
24
25 if present.is_empty() {
26 return self.pass();
27 }
28
29 let all_ignored = present.iter().all(|o| ctx.claudeignore_contains(o));
30 if all_ignored {
31 self.pass()
32 } else {
33 self.fail(
34 &format!("Doc site build output dirs not ignored: {}", present.iter().map(|o| o.to_string()).collect::<Vec<_>>().join(", ")),
35 Suggestion {
36 priority: SuggestionPriority::QuickWin,
37 title: "Ignore build output".into(),
38 description: "HTML output is 5-10x larger than markdown source. Add build/, dist/, .docusaurus/, site/ to .claudeignore.".into(),
39 effort: Effort::Minutes,
40 },
41 )
42 }
43 }
44}
45
46pub struct MarkdownSourceFocus;
49
50impl Rule for MarkdownSourceFocus {
51 fn id(&self) -> &str { "DOC2" }
52 fn name(&self) -> &str { "CLAUDE.md points to markdown source" }
53 fn dimension(&self) -> Dimension { Dimension::Foundation }
54 fn severity(&self) -> Severity { Severity::Medium }
55
56 fn applies_to(&self, pt: &ProjectType) -> bool { is_doc_site(pt) }
57
58 fn check(&self, ctx: &ProjectContext) -> RuleResult {
59 let content = match &ctx.claude_md_content {
60 Some(c) => c.to_lowercase(),
61 None => return self.skip(),
62 };
63
64 let has_source_ref = content.contains("docs/")
65 || content.contains("content/")
66 || content.contains("markdown")
67 || content.contains("source");
68
69 if has_source_ref {
70 self.pass()
71 } else {
72 self.warn(
73 "CLAUDE.md doesn't point to the markdown source directory",
74 Suggestion {
75 priority: SuggestionPriority::NiceToHave,
76 title: "Document markdown source location".into(),
77 description: "Add to CLAUDE.md: 'Markdown source: docs/ (Claude reads this). Build output: build/ (don't read).'".into(),
78 effort: Effort::Minutes,
79 },
80 )
81 }
82 }
83}
84
85pub struct NavigationDocumented;
88
89impl Rule for NavigationDocumented {
90 fn id(&self) -> &str { "DOC3" }
91 fn name(&self) -> &str { "Navigation/sidebar config documented" }
92 fn dimension(&self) -> Dimension { Dimension::Foundation }
93 fn severity(&self) -> Severity { Severity::Medium }
94
95 fn applies_to(&self, pt: &ProjectType) -> bool { is_doc_site(pt) }
96
97 fn check(&self, ctx: &ProjectContext) -> RuleResult {
98 let content = match &ctx.claude_md_content {
99 Some(c) => c.to_lowercase(),
100 None => return self.skip(),
101 };
102
103 let has_nav_docs = content.contains("sidebar")
104 || content.contains("navigation")
105 || content.contains("nav")
106 || content.contains("menu");
107
108 if has_nav_docs {
109 self.pass()
110 } else {
111 self.warn(
112 "CLAUDE.md doesn't explain how to add new pages to navigation",
113 Suggestion {
114 priority: SuggestionPriority::NiceToHave,
115 title: "Document nav config".into(),
116 description: "Add to CLAUDE.md how to register new pages (which sidebar/nav file to update). Each doc framework has different config.".into(),
117 effort: Effort::Minutes,
118 },
119 )
120 }
121 }
122}