Skip to main content

ailint_core/rules/semantic/
instruction_bloat.rs

1//! AIL106 `detect-instruction-bloat` — flags monolithic prose paragraphs.
2//!
3//! See: `docs/rules/semantic/AIL106.md`
4
5use serde::Deserialize;
6
7use crate::file_type::FileType;
8use crate::parser::{DocumentContent, ParsedDocument};
9use crate::rules::semantic::AIL106;
10use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
11
12const DEFAULT_MAX_WORDS: usize = 120;
13
14#[derive(Debug, Default, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct Options {
17    max_words: Option<usize>,
18}
19
20/// AIL106 detect-instruction-bloat: flags oversized prose paragraphs.
21#[derive(Debug, Default)]
22pub struct DetectInstructionBloatRule;
23
24impl Rule for DetectInstructionBloatRule {
25    fn id(&self) -> RuleId {
26        AIL106
27    }
28
29    fn default_severity(&self) -> Severity {
30        Severity::Warning
31    }
32
33    fn description(&self) -> &'static str {
34        "Prose paragraph is longer than the configured word budget."
35    }
36
37    fn fix_hint(&self) -> &'static str {
38        "Break the paragraph into shorter statements or a bulleted list."
39    }
40
41    fn applies_to(&self, file_type: FileType) -> bool {
42        file_type.has_prose_content()
43    }
44
45    fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
46        let md = match &doc.content {
47            DocumentContent::Markdown(m) => m,
48            _ => return Vec::new(),
49        };
50        let opts: Options = ctx
51            .options
52            .and_then(|v| serde_yaml::from_value(v.clone()).ok())
53            .unwrap_or_default();
54        let max_words = opts.max_words.unwrap_or(DEFAULT_MAX_WORDS);
55
56        let mut out = Vec::new();
57        for p in &md.paragraphs {
58            let count = p.text.split_whitespace().count();
59            if count <= max_words {
60                continue;
61            }
62            let v = Violation::new(
63                AIL106,
64                self.default_severity(),
65                doc.path.clone(),
66                format!("paragraph exceeds {} words", max_words),
67            )
68            .at(p.line, 1)
69            .with_detail(format!("{} words", count));
70            out.push(v);
71        }
72        out
73    }
74}