ailint_core/rules/semantic/
vague_instruction.rs1use aho_corasick::AhoCorasick;
6use serde::Deserialize;
7
8use crate::parser::{DocumentContent, ParsedDocument};
9use crate::rules::semantic::AIL100;
10use crate::rules::{dictionary_lines, Rule, RuleContext, RuleId, Severity, Violation};
11
12const DEFAULT_PHRASES: &str = include_str!("vague_phrases.txt");
13
14#[derive(Debug, Default, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct Options {
17 phrases: Option<Vec<String>>,
18 extra_phrases: Option<Vec<String>>,
19 case_sensitive: Option<bool>,
20}
21
22#[derive(Debug, Default)]
24pub struct NoVagueInstructionRule;
25
26impl Rule for NoVagueInstructionRule {
27 fn id(&self) -> RuleId {
28 AIL100
29 }
30
31 fn default_severity(&self) -> Severity {
32 Severity::Warning
33 }
34
35 fn description(&self) -> &'static str {
36 "List item contains vague, unactionable phrasing."
37 }
38
39 fn fix_hint(&self) -> &'static str {
40 "Replace with a concrete verb and target the agent can act on."
41 }
42
43 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
44 let md = match &doc.content {
45 DocumentContent::Markdown(m) => m,
46 _ => return Vec::new(),
47 };
48
49 let opts: Options = ctx
50 .options
51 .and_then(|v| serde_yaml::from_value(v.clone()).ok())
52 .unwrap_or_default();
53 let case_sensitive = opts.case_sensitive.unwrap_or(false);
54
55 let mut phrases: Vec<String> = match opts.phrases {
56 Some(p) => p,
57 None => dictionary_lines(DEFAULT_PHRASES)
58 .into_iter()
59 .map(String::from)
60 .collect(),
61 };
62 if let Some(extra) = opts.extra_phrases {
63 phrases.extend(extra);
64 }
65 if !case_sensitive {
66 for p in &mut phrases {
67 *p = p.to_lowercase();
68 }
69 }
70 let Ok(ac) = AhoCorasick::new(&phrases) else {
72 return Vec::new();
73 };
74
75 let mut out = Vec::new();
76 for item in &md.list_items {
77 let haystack = if case_sensitive {
78 item.text.clone()
79 } else {
80 item.text.to_lowercase()
81 };
82 if let Some(m) = ac.find(&haystack) {
83 let phrase = &phrases[m.pattern().as_usize()];
84 let snippet: String = item.text.chars().take(120).collect();
85 let mut v = Violation::new(
86 AIL100,
87 self.default_severity(),
88 doc.path.clone(),
89 "vague phrase",
90 )
91 .at(item.line, 1)
92 .with_detail(phrase.clone());
93 v.snippet = Some(snippet);
94 out.push(v);
95 }
96 }
97 out
98 }
99}