ailint_core/rules/semantic/
instruction_bloat.rs1use 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#[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}