driven/validation/
completeness.rs1use crate::{Result, format::RuleCategory, parser::UnifiedRule};
4
5#[derive(Debug, Default)]
7pub struct CoverageAnalyzer;
8
9impl CoverageAnalyzer {
10 pub fn new() -> Self {
12 Self
13 }
14
15 pub fn analyze(&self, rules: &[UnifiedRule]) -> Result<Vec<String>> {
17 let mut gaps = Vec::new();
18
19 let covered_categories = self.get_covered_categories(rules);
21
22 let recommended = [
23 RuleCategory::Style,
24 RuleCategory::Naming,
25 RuleCategory::ErrorHandling,
26 RuleCategory::Testing,
27 RuleCategory::Documentation,
28 ];
29
30 for cat in recommended {
31 if !covered_categories.contains(&cat) {
32 gaps.push(format!("No rules for category: {:?}", cat));
33 }
34 }
35
36 let has_persona = rules
38 .iter()
39 .any(|r| matches!(r, UnifiedRule::Persona { .. }));
40 if !has_persona {
41 gaps.push("No AI persona defined".to_string());
42 }
43
44 let has_context = rules
46 .iter()
47 .any(|r| matches!(r, UnifiedRule::Context { .. }));
48 if !has_context {
49 gaps.push("No project context defined".to_string());
50 }
51
52 Ok(gaps)
53 }
54
55 fn get_covered_categories(
56 &self,
57 rules: &[UnifiedRule],
58 ) -> std::collections::HashSet<RuleCategory> {
59 rules
60 .iter()
61 .filter_map(|r| {
62 if let UnifiedRule::Standard { category, .. } = r {
63 Some(*category)
64 } else {
65 None
66 }
67 })
68 .collect()
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn test_analyzer_empty() {
78 let analyzer = CoverageAnalyzer::new();
79 let gaps = analyzer.analyze(&[]).unwrap();
80
81 assert!(!gaps.is_empty());
83 }
84
85 #[test]
86 fn test_analyzer_complete() {
87 let analyzer = CoverageAnalyzer::new();
88 let rules = vec![
89 UnifiedRule::Persona {
90 name: "Test".to_string(),
91 role: "Tester".to_string(),
92 identity: None,
93 style: None,
94 traits: vec![],
95 principles: vec![],
96 },
97 UnifiedRule::Context {
98 includes: vec![],
99 excludes: vec![],
100 focus: vec![],
101 },
102 UnifiedRule::Standard {
103 category: RuleCategory::Style,
104 priority: 0,
105 description: "Style rule".to_string(),
106 pattern: None,
107 },
108 UnifiedRule::Standard {
109 category: RuleCategory::Naming,
110 priority: 0,
111 description: "Naming rule".to_string(),
112 pattern: None,
113 },
114 UnifiedRule::Standard {
115 category: RuleCategory::ErrorHandling,
116 priority: 0,
117 description: "Error rule".to_string(),
118 pattern: None,
119 },
120 UnifiedRule::Standard {
121 category: RuleCategory::Testing,
122 priority: 0,
123 description: "Testing rule".to_string(),
124 pattern: None,
125 },
126 UnifiedRule::Standard {
127 category: RuleCategory::Documentation,
128 priority: 0,
129 description: "Doc rule".to_string(),
130 pattern: None,
131 },
132 ];
133
134 let gaps = analyzer.analyze(&rules).unwrap();
135 assert!(gaps.is_empty());
136 }
137}