ailint_core/rules/semantic/
negative_constraint_overload.rs1use crate::parser::{DocumentContent, ParsedDocument};
6use crate::rules::semantic::AIL104;
7use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
8
9#[derive(Debug, Default)]
11pub struct NegativeConstraintOverloadRule;
12
13impl Rule for NegativeConstraintOverloadRule {
14 fn id(&self) -> RuleId {
15 AIL104
16 }
17
18 fn default_severity(&self) -> Severity {
19 Severity::Warning
20 }
21
22 fn description(&self) -> &'static str {
23 "List is dominated by negative constraints; LLMs perform better with affirmative phrasing."
24 }
25
26 fn fix_hint(&self) -> &'static str {
27 "Rewrite as positive directives (\"Do X\" over \"Don't Y\")."
28 }
29
30 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
31 let md = match &doc.content {
32 DocumentContent::Markdown(m) => m,
33 _ => return Vec::new(),
34 };
35
36 if md.list_items.len() < 5 {
37 return Vec::new();
38 }
39
40 let negative_prefixes = [
42 "do not", "don't", "never", "avoid", "stop ", "no ", "must not",
43 ];
44
45 let mut negative_count = 0;
46 for item in &md.list_items {
47 let lower_text = item
48 .text
49 .trim_start_matches(['*', '_', '`'])
50 .trim()
51 .to_lowercase();
52 if negative_prefixes.iter().any(|p| lower_text.starts_with(p)) {
53 negative_count += 1;
54 }
55 }
56
57 if negative_count > md.list_items.len() / 2 {
58 let v = Violation::new(
59 AIL104,
60 ctx.severity,
61 doc.path.clone(),
62 "list dominated by negative constraints",
63 )
64 .with_detail(format!(
65 "{} of {} items are negative",
66 negative_count,
67 md.list_items.len()
68 ));
69 vec![v]
70 } else {
71 Vec::new()
72 }
73 }
74}