ailint_core/rules/structural/
required_section.rs1use std::collections::BTreeMap;
6
7use serde::Deserialize;
8
9use crate::file_type::FileType;
10use crate::parser::{DocumentContent, ParsedDocument};
11use crate::rules::structural::AIL003;
12use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
13
14#[derive(Debug, Default)]
16pub struct MissingRequiredSectionRule;
17
18#[derive(Debug, Default, Deserialize)]
19#[serde(default)]
20struct RuleOptions {
21 required: Vec<String>,
22 per_file_type: BTreeMap<String, Vec<String>>,
23}
24
25impl Rule for MissingRequiredSectionRule {
26 fn id(&self) -> RuleId {
27 AIL003
28 }
29
30 fn default_severity(&self) -> Severity {
31 Severity::Warning
32 }
33
34 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
35 let DocumentContent::Markdown(md) = &doc.content else {
36 return Vec::new();
37 };
38 let opts: RuleOptions = match ctx.options {
39 Some(v) => match serde_yaml::from_value(v.clone()) {
40 Ok(c) => c,
41 Err(_) => return Vec::new(),
42 },
43 None => RuleOptions::default(),
44 };
45 let key = file_type_key(doc.file_type);
46 let required = opts
47 .per_file_type
48 .get(key)
49 .cloned()
50 .unwrap_or(opts.required);
51 if required.is_empty() {
52 return Vec::new();
53 }
54 let top_headings: Vec<String> = md
55 .headings
56 .iter()
57 .filter(|h| h.level <= 2)
58 .map(|h| h.text.to_ascii_lowercase())
59 .collect();
60 let mut out = Vec::new();
61 for entry in required {
62 let needle = entry.to_ascii_lowercase();
63 if top_headings.iter().any(|h| h.contains(&needle)) {
64 continue;
65 }
66 let mut v = Violation::new(
67 AIL003,
68 self.default_severity(),
69 doc.path.clone(),
70 format!("missing required section: '{entry}'"),
71 )
72 .at(1, 1);
73 v.fix_hint = Some(format!("add a top-level heading containing '{entry}'"));
74 out.push(v);
75 }
76 out
77 }
78}
79
80fn file_type_key(ft: FileType) -> &'static str {
81 match ft {
82 FileType::ClaudeMd => "claudemd",
83 FileType::AgentsMd => "agentsmd",
84 FileType::CopilotCustomization => "copilotcustomization",
85 FileType::CopilotInstructions => "copilotinstructions",
86 FileType::CursorRules => "cursorrules",
87 FileType::WindsurfRules => "windsurfrules",
88 FileType::ClineRules => "clinerules",
89 FileType::JunieGuidelines => "junieguidelines",
90 FileType::GenericSystemPrompt => "genericsystemprompt",
91 FileType::AiderConventions => "aiderconventions",
92 FileType::ContinueRules => "continuerules",
93 FileType::GitHubSkill => "githubskill",
94 FileType::CustomProjectRules => "customprojectrules",
95 FileType::GenericMarkdown => "genericmarkdown",
96 FileType::GenericYaml => "genericyaml",
97 FileType::Unknown => "unknown",
98 }
99}