Skip to main content

driven/validation/
completeness.rs

1//! Coverage analysis
2
3use crate::{Result, format::RuleCategory, parser::UnifiedRule};
4
5/// Analyzes rule coverage
6#[derive(Debug, Default)]
7pub struct CoverageAnalyzer;
8
9impl CoverageAnalyzer {
10    /// Create a new analyzer
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Analyze coverage gaps
16    pub fn analyze(&self, rules: &[UnifiedRule]) -> Result<Vec<String>> {
17        let mut gaps = Vec::new();
18
19        // Check for coverage of important categories
20        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        // Check for persona
37        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        // Check for context
45        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        // Should report missing categories and no persona/context
82        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}