Skip to main content

claude_native/rules/project_specific/
doc_site.rs

1use 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
9// ── Rule DOC1: Build output ignored ─────────────────────────────────
10
11pub 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
46// ── Rule DOC2: Markdown source is the focus ─────────────────────────
47
48pub 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
85// ── Rule DOC3: Navigation config documented ─────────────────────────
86
87pub 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}