Skip to main content

ailint_core/rules/semantic/
negative_constraint_overload.rs

1//! AIL104 `negative-constraint-overload` — negative constraints dominate affirmative guidance.
2//!
3//! See: `docs/rules/semantic/AIL104.md`
4
5use crate::parser::{DocumentContent, ParsedDocument};
6use crate::rules::semantic::AIL104;
7use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
8
9/// AIL104 negative-constraint-overload: list dominated by "do not" constraints.
10#[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 run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
23        let md = match &doc.content {
24            DocumentContent::Markdown(m) => m,
25            _ => return Vec::new(),
26        };
27
28        if md.list_items.len() < 5 {
29            return Vec::new();
30        }
31
32        // Prefix match only: mid-sentence negations ("prefer X, not Y") are fine.
33        let negative_prefixes = [
34            "do not", "don't", "never", "avoid", "stop ", "no ", "must not",
35        ];
36
37        let mut negative_count = 0;
38        for item in &md.list_items {
39            let lower_text = item
40                .text
41                .trim_start_matches(['*', '_', '`'])
42                .trim()
43                .to_lowercase();
44            if negative_prefixes.iter().any(|p| lower_text.starts_with(p)) {
45                negative_count += 1;
46            }
47        }
48
49        if negative_count > md.list_items.len() / 2 {
50            let mut v = Violation::new(
51                AIL104,
52                ctx.severity,
53                doc.path.clone(),
54                format!(
55                    "Negative constraint overload: {} out of {} list items are negative constraints. LLMs perform better with affirmative instruction phrasing.",
56                    negative_count,
57                    md.list_items.len()
58                ),
59            );
60            v.fix_hint = Some("Refactor constraints to state what the agent *should* do, rather than exhaustive lists of prohibitions.".to_string());
61            vec![v]
62        } else {
63            Vec::new()
64        }
65    }
66}