Skip to main content

chant/score/
vague.rs

1//! Vague language detection utility for spec text analysis.
2//!
3//! Detects vague language patterns that may indicate unclear requirements
4//! or underspecified acceptance criteria.
5
6use std::collections::HashSet;
7
8/// Default vague language patterns to detect
9pub const DEFAULT_VAGUE_PATTERNS: &[&str] =
10    &["improve", "as needed", "etc", "and related", "similar"];
11
12/// Detects vague language patterns in text.
13///
14/// Performs case-insensitive matching and returns a list of matched patterns.
15/// Each pattern is reported at most once, even if it appears multiple times.
16///
17/// # Arguments
18///
19/// * `text` - The text to analyze for vague patterns
20/// * `patterns` - The patterns to search for
21///
22/// # Returns
23///
24/// A vector of matched pattern strings (deduplicated, in order of first match)
25///
26/// # Examples
27///
28/// ```
29/// use chant::score::vague::detect_vague_patterns;
30///
31/// let text = "improve performance";
32/// let patterns = vec!["improve".to_string()];
33/// let matches = detect_vague_patterns(text, &patterns);
34/// assert_eq!(matches, vec!["improve"]);
35/// ```
36///
37/// ```
38/// use chant::score::vague::detect_vague_patterns;
39///
40/// let text = "Add feature and related tests";
41/// let patterns = vec!["and related".to_string()];
42/// let matches = detect_vague_patterns(text, &patterns);
43/// assert_eq!(matches, vec!["and related"]);
44/// ```
45///
46/// ```
47/// use chant::score::vague::detect_vague_patterns;
48///
49/// let text = "Clean code";
50/// let patterns = vec!["improve".to_string()];
51/// let matches = detect_vague_patterns(text, &patterns);
52/// assert_eq!(matches, Vec::<String>::new());
53/// ```
54pub fn detect_vague_patterns(text: &str, patterns: &[String]) -> Vec<String> {
55    // Handle edge cases
56    if text.is_empty() || patterns.is_empty() {
57        return Vec::new();
58    }
59
60    let text_lower = text.to_lowercase();
61    let mut found_patterns = Vec::new();
62    let mut seen = HashSet::new();
63
64    // Check each pattern
65    for pattern in patterns {
66        let pattern_lower = pattern.to_lowercase();
67
68        // Only add if we haven't seen this pattern yet and it's in the text
69        if !seen.contains(&pattern_lower) && text_lower.contains(&pattern_lower) {
70            found_patterns.push(pattern.clone());
71            seen.insert(pattern_lower);
72        }
73    }
74
75    found_patterns
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_detect_single_pattern() {
84        let text = "improve performance";
85        let patterns = vec!["improve".to_string()];
86        let result = detect_vague_patterns(text, &patterns);
87        assert_eq!(result, vec!["improve"]);
88    }
89
90    #[test]
91    fn test_detect_pattern_in_phrase() {
92        let text = "Add feature and related tests";
93        let patterns = vec!["and related".to_string()];
94        let result = detect_vague_patterns(text, &patterns);
95        assert_eq!(result, vec!["and related"]);
96    }
97
98    #[test]
99    fn test_no_match() {
100        let text = "Clean code";
101        let patterns = vec!["improve".to_string()];
102        let result = detect_vague_patterns(text, &patterns);
103        assert_eq!(result, Vec::<String>::new());
104    }
105
106    #[test]
107    fn test_empty_text() {
108        let text = "";
109        let patterns = vec!["improve".to_string()];
110        let result = detect_vague_patterns(text, &patterns);
111        assert_eq!(result, Vec::<String>::new());
112    }
113
114    #[test]
115    fn test_empty_patterns() {
116        let text = "improve performance";
117        let patterns: Vec<String> = vec![];
118        let result = detect_vague_patterns(text, &patterns);
119        assert_eq!(result, Vec::<String>::new());
120    }
121
122    #[test]
123    fn test_case_insensitive() {
124        let text = "IMPROVE Performance";
125        let patterns = vec!["improve".to_string()];
126        let result = detect_vague_patterns(text, &patterns);
127        assert_eq!(result, vec!["improve"]);
128    }
129
130    #[test]
131    fn test_case_insensitive_pattern() {
132        let text = "improve performance";
133        let patterns = vec!["IMPROVE".to_string()];
134        let result = detect_vague_patterns(text, &patterns);
135        assert_eq!(result, vec!["IMPROVE"]);
136    }
137
138    #[test]
139    fn test_multiple_patterns() {
140        let text = "improve performance and related metrics etc";
141        let patterns = vec![
142            "improve".to_string(),
143            "and related".to_string(),
144            "etc".to_string(),
145        ];
146        let result = detect_vague_patterns(text, &patterns);
147        assert_eq!(result, vec!["improve", "and related", "etc"]);
148    }
149
150    #[test]
151    fn test_overlapping_patterns_reported_once() {
152        let text = "improve improve improve";
153        let patterns = vec!["improve".to_string()];
154        let result = detect_vague_patterns(text, &patterns);
155        // Should only be reported once
156        assert_eq!(result, vec!["improve"]);
157        assert_eq!(result.len(), 1);
158    }
159
160    #[test]
161    fn test_duplicate_patterns_in_list() {
162        let text = "improve performance";
163        let patterns = vec!["improve".to_string(), "improve".to_string()];
164        let result = detect_vague_patterns(text, &patterns);
165        // Should only report first occurrence
166        assert_eq!(result, vec!["improve"]);
167        assert_eq!(result.len(), 1);
168    }
169
170    #[test]
171    fn test_partial_word_match() {
172        // "improve" should match "improved" or "improvement"
173        let text = "we need improvement here";
174        let patterns = vec!["improve".to_string()];
175        let result = detect_vague_patterns(text, &patterns);
176        assert_eq!(result, vec!["improve"]);
177    }
178
179    #[test]
180    fn test_default_patterns() {
181        // Test that default patterns are defined
182        assert!(DEFAULT_VAGUE_PATTERNS.contains(&"improve"));
183        assert!(DEFAULT_VAGUE_PATTERNS.contains(&"as needed"));
184        assert!(DEFAULT_VAGUE_PATTERNS.contains(&"etc"));
185        assert!(DEFAULT_VAGUE_PATTERNS.contains(&"and related"));
186        assert!(DEFAULT_VAGUE_PATTERNS.contains(&"similar"));
187    }
188
189    #[test]
190    fn test_all_default_patterns() {
191        let text = "improve as needed, etc and related similar things";
192        let patterns: Vec<String> = DEFAULT_VAGUE_PATTERNS
193            .iter()
194            .map(|s| s.to_string())
195            .collect();
196        let result = detect_vague_patterns(text, &patterns);
197        assert_eq!(result.len(), 5);
198        assert!(result.contains(&"improve".to_string()));
199        assert!(result.contains(&"as needed".to_string()));
200        assert!(result.contains(&"etc".to_string()));
201        assert!(result.contains(&"and related".to_string()));
202        assert!(result.contains(&"similar".to_string()));
203    }
204}