use std::collections::BTreeMap;
use serde::Deserialize;
use crate::file_type::FileType;
use crate::parser::{DocumentContent, ParsedDocument};
use crate::rules::structural::AIL003;
use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
#[derive(Debug, Default)]
pub struct MissingRequiredSectionRule;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct RuleOptions {
required: Vec<String>,
per_file_type: BTreeMap<String, Vec<String>>,
}
impl Rule for MissingRequiredSectionRule {
fn id(&self) -> RuleId {
AIL003
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
let DocumentContent::Markdown(md) = &doc.content else {
return Vec::new();
};
let opts: RuleOptions = match ctx.options {
Some(v) => match serde_yaml::from_value(v.clone()) {
Ok(c) => c,
Err(_) => return Vec::new(),
},
None => RuleOptions::default(),
};
let key = file_type_key(doc.file_type);
let required = opts
.per_file_type
.get(key)
.cloned()
.unwrap_or(opts.required);
if required.is_empty() {
return Vec::new();
}
let top_headings: Vec<String> = md
.headings
.iter()
.filter(|h| h.level <= 2)
.map(|h| h.text.to_ascii_lowercase())
.collect();
let mut out = Vec::new();
for entry in required {
let needle = entry.to_ascii_lowercase();
if top_headings.iter().any(|h| h.contains(&needle)) {
continue;
}
let mut v = Violation::new(
AIL003,
self.default_severity(),
doc.path.clone(),
format!("missing required section: '{entry}'"),
)
.at(1, 1);
v.fix_hint = Some(format!("add a top-level heading containing '{entry}'"));
out.push(v);
}
out
}
}
fn file_type_key(ft: FileType) -> &'static str {
match ft {
FileType::ClaudeMd => "claudemd",
FileType::AgentsMd => "agentsmd",
FileType::CopilotCustomization => "copilotcustomization",
FileType::CopilotInstructions => "copilotinstructions",
FileType::CursorRules => "cursorrules",
FileType::WindsurfRules => "windsurfrules",
FileType::ClineRules => "clinerules",
FileType::JunieGuidelines => "junieguidelines",
FileType::GenericSystemPrompt => "genericsystemprompt",
FileType::AiderConventions => "aiderconventions",
FileType::ContinueRules => "continuerules",
FileType::GitHubSkill => "githubskill",
FileType::CustomProjectRules => "customprojectrules",
FileType::GenericMarkdown => "genericmarkdown",
FileType::GenericYaml => "genericyaml",
FileType::Unknown => "unknown",
}
}