clnrm_core/telemetry/
validation_analyzer.rs

1//! Weaver validation result analyzer
2//!
3//! This module analyzes Weaver live-check validation reports to determine
4//! if telemetry meets production release criteria.
5
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10
11/// Weaver validation report structure
12#[derive(Debug, Deserialize, Serialize)]
13pub struct WeaverValidationReport {
14    /// Count of advice by level
15    pub advice_level_counts: AdviceCounts,
16    /// Registry coverage percentage (0.0 to 1.0)
17    pub registry_coverage: f64,
18    /// All advice items
19    pub all_advice: Vec<Advice>,
20    /// Attributes that match registry schemas
21    pub seen_registry_attributes: HashMap<String, u32>,
22    /// Attributes that don't match any schema
23    pub seen_non_registry_attributes: HashMap<String, u32>,
24}
25
26/// Counts of advice by severity level
27#[derive(Debug, Deserialize, Serialize)]
28pub struct AdviceCounts {
29    /// Violations - MUST be zero for release
30    pub violation: u32,
31    /// Improvements - should be addressed
32    pub improvement: u32,
33    /// Information - FYI only
34    pub information: u32,
35}
36
37/// Individual advice item from Weaver
38#[derive(Debug, Deserialize, Serialize, Clone)]
39pub struct Advice {
40    /// Severity level: violation, improvement, information
41    pub advice_level: String,
42    /// Type of advice
43    pub advice_type: String,
44    /// Human-readable message
45    pub message: String,
46    /// Signal name (span/metric/event name)
47    pub signal_name: String,
48    /// Signal type (span, metric, event)
49    pub signal_type: String,
50}
51
52/// Analysis of validation report
53#[derive(Debug, Clone)]
54pub struct ValidationAnalysis {
55    /// Whether validation passed (no violations)
56    pub passed: bool,
57    /// Total number of violations
58    pub total_violations: u32,
59    /// Registry coverage (0.0 to 1.0)
60    pub coverage: f64,
61    /// List of violations
62    pub violations: Vec<Advice>,
63    /// List of improvements
64    pub improvements: Vec<Advice>,
65    /// Missing critical attributes
66    pub missing_critical_attributes: Vec<String>,
67}
68
69impl ValidationAnalysis {
70    /// Load and analyze a Weaver validation report
71    pub fn from_report_file(path: &Path) -> Result<Self> {
72        let json = std::fs::read_to_string(path).map_err(|e| {
73            CleanroomError::validation_error(format!(
74                "Failed to read validation report at {}: {}",
75                path.display(),
76                e
77            ))
78        })?;
79
80        let report: WeaverValidationReport = serde_json::from_str(&json).map_err(|e| {
81            CleanroomError::validation_error(format!("Failed to parse validation report: {}", e))
82        })?;
83
84        Self::from_report(report)
85    }
86
87    /// Analyze a Weaver validation report
88    pub fn from_report(report: WeaverValidationReport) -> Result<Self> {
89        let violations: Vec<Advice> = report
90            .all_advice
91            .iter()
92            .filter(|a| a.advice_level == "violation")
93            .cloned()
94            .collect();
95
96        let improvements: Vec<Advice> = report
97            .all_advice
98            .iter()
99            .filter(|a| a.advice_level == "improvement")
100            .cloned()
101            .collect();
102
103        // Check for missing critical attributes
104        let critical_attributes = [
105            "container.id",
106            "test.isolated",
107            "test.result",
108            "container.destroyed_at",
109        ];
110
111        let missing_critical: Vec<String> = critical_attributes
112            .iter()
113            .filter(|&&attr| {
114                !report
115                    .seen_registry_attributes
116                    .contains_key(attr)
117            })
118            .map(|s| s.to_string())
119            .collect();
120
121        Ok(Self {
122            passed: report.advice_level_counts.violation == 0,
123            total_violations: report.advice_level_counts.violation,
124            coverage: report.registry_coverage,
125            violations,
126            improvements,
127            missing_critical_attributes: missing_critical,
128        })
129    }
130
131    /// Print a human-readable summary
132    pub fn print_summary(&self) {
133        println!("\n=== WEAVER VALIDATION SUMMARY ===");
134        println!(
135            "Status: {}",
136            if self.passed {
137                "✅ PASSED"
138            } else {
139                "❌ FAILED"
140            }
141        );
142        println!("Violations: {}", self.total_violations);
143        println!("Coverage: {:.1}%", self.coverage * 100.0);
144
145        if !self.missing_critical_attributes.is_empty() {
146            println!("\n⚠️  MISSING CRITICAL ATTRIBUTES:");
147            for attr in &self.missing_critical_attributes {
148                println!("  - {}", attr);
149            }
150        }
151
152        if !self.passed {
153            println!("\n❌ VIOLATIONS DETECTED:");
154            for violation in &self.violations {
155                println!(
156                    "  - [{}] {}: {}",
157                    violation.signal_type, violation.signal_name, violation.message
158                );
159            }
160        }
161
162        if !self.improvements.is_empty() {
163            println!("\n💡 IMPROVEMENTS SUGGESTED:");
164            for improvement in &self.improvements {
165                println!(
166                    "  - [{}] {}: {}",
167                    improvement.signal_type, improvement.signal_name, improvement.message
168                );
169            }
170        }
171    }
172
173    /// Check if validation meets release criteria
174    pub fn meets_release_criteria(&self) -> bool {
175        // Must have zero violations
176        if !self.passed {
177            return false;
178        }
179
180        // Must have 85%+ coverage
181        if self.coverage < 0.85 {
182            return false;
183        }
184
185        // Must have all critical attributes
186        if !self.missing_critical_attributes.is_empty() {
187            return false;
188        }
189
190        true
191    }
192
193    /// Get blocking issues that prevent release
194    pub fn blocking_issues(&self) -> Vec<String> {
195        let mut issues = Vec::new();
196
197        if !self.passed {
198            issues.push(format!("{} telemetry violations", self.total_violations));
199        }
200
201        if self.coverage < 0.85 {
202            issues.push(format!("Coverage too low: {:.1}%", self.coverage * 100.0));
203        }
204
205        if !self.missing_critical_attributes.is_empty() {
206            issues.push(format!(
207                "Missing {} critical attributes",
208                self.missing_critical_attributes.len()
209            ));
210        }
211
212        issues
213    }
214}
215
216/// Final validation result for release decision
217#[derive(Debug, Clone)]
218pub struct WeaverValidationResult {
219    /// Overall validation status
220    pub status: ValidationStatus,
221    /// Whether schemas are valid
222    pub schema_valid: bool,
223    /// Whether telemetry is valid
224    pub telemetry_valid: bool,
225    /// Registry coverage
226    pub coverage: f64,
227    /// List of violations
228    pub violations: Vec<String>,
229    /// Recommendations for improvement
230    pub recommendations: Vec<String>,
231}
232
233/// Validation status
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum ValidationStatus {
236    /// All checks passed
237    Passed,
238    /// Some checks failed
239    Failed,
240    /// Validation incomplete
241    Incomplete,
242}
243
244impl WeaverValidationResult {
245    /// Create from validation analysis
246    pub fn from_analysis(analysis: ValidationAnalysis) -> Self {
247        let status = if analysis.meets_release_criteria() {
248            ValidationStatus::Passed
249        } else {
250            ValidationStatus::Failed
251        };
252
253        let violations: Vec<String> = analysis
254            .violations
255            .iter()
256            .map(|v| format!("[{}] {}: {}", v.signal_type, v.signal_name, v.message))
257            .collect();
258
259        let recommendations: Vec<String> = analysis
260            .improvements
261            .iter()
262            .map(|i| format!("[{}] {}: {}", i.signal_type, i.signal_name, i.message))
263            .collect();
264
265        Self {
266            status,
267            schema_valid: true, // Assume schemas validated separately
268            telemetry_valid: analysis.passed,
269            coverage: analysis.coverage,
270            violations,
271            recommendations,
272        }
273    }
274
275    /// Check if ready for release
276    pub fn is_release_ready(&self) -> bool {
277        self.status == ValidationStatus::Passed
278            && self.schema_valid
279            && self.telemetry_valid
280            && self.coverage >= 0.85
281    }
282
283    /// Get blocking issues
284    pub fn blocking_issues(&self) -> Vec<String> {
285        let mut issues = Vec::new();
286
287        if !self.schema_valid {
288            issues.push("Schema validation failed".to_string());
289        }
290
291        if !self.telemetry_valid {
292            issues.push(format!("{} telemetry violations", self.violations.len()));
293        }
294
295        if self.coverage < 0.85 {
296            issues.push(format!("Coverage too low: {:.1}%", self.coverage * 100.0));
297        }
298
299        issues
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_validation_analysis_from_report() {
309        let report = WeaverValidationReport {
310            advice_level_counts: AdviceCounts {
311                violation: 0,
312                improvement: 2,
313                information: 5,
314            },
315            registry_coverage: 0.92,
316            all_advice: vec![Advice {
317                advice_level: "improvement".to_string(),
318                advice_type: "missing_attribute".to_string(),
319                message: "Consider adding container.runtime attribute".to_string(),
320                signal_name: "clnrm.container_lifecycle".to_string(),
321                signal_type: "span".to_string(),
322            }],
323            seen_registry_attributes: HashMap::from([
324                ("container.id".to_string(), 10),
325                ("test.isolated".to_string(), 8),
326                ("test.result".to_string(), 8),
327                ("container.destroyed_at".to_string(), 10),
328            ]),
329            seen_non_registry_attributes: HashMap::new(),
330        };
331
332        let analysis = ValidationAnalysis::from_report(report).unwrap();
333
334        assert!(analysis.passed);
335        assert_eq!(analysis.total_violations, 0);
336        assert_eq!(analysis.coverage, 0.92);
337        assert!(analysis.meets_release_criteria());
338    }
339
340    #[test]
341    fn test_validation_with_violations() {
342        let report = WeaverValidationReport {
343            advice_level_counts: AdviceCounts {
344                violation: 2,
345                improvement: 0,
346                information: 0,
347            },
348            registry_coverage: 0.60,
349            all_advice: vec![Advice {
350                advice_level: "violation".to_string(),
351                advice_type: "missing_required_attribute".to_string(),
352                message: "Missing required attribute: container.id".to_string(),
353                signal_name: "clnrm.test_execution".to_string(),
354                signal_type: "span".to_string(),
355            }],
356            seen_registry_attributes: HashMap::new(),
357            seen_non_registry_attributes: HashMap::new(),
358        };
359
360        let analysis = ValidationAnalysis::from_report(report).unwrap();
361
362        assert!(!analysis.passed);
363        assert_eq!(analysis.total_violations, 2);
364        assert_eq!(analysis.coverage, 0.60);
365        assert!(!analysis.meets_release_criteria());
366    }
367
368    #[test]
369    fn test_blocking_issues() {
370        let report = WeaverValidationReport {
371            advice_level_counts: AdviceCounts {
372                violation: 1,
373                improvement: 0,
374                information: 0,
375            },
376            registry_coverage: 0.70,
377            all_advice: vec![],
378            seen_registry_attributes: HashMap::new(),
379            seen_non_registry_attributes: HashMap::new(),
380        };
381
382        let analysis = ValidationAnalysis::from_report(report).unwrap();
383        let issues = analysis.blocking_issues();
384
385        assert!(issues.contains(&"1 telemetry violations".to_string()));
386        assert!(issues.contains(&"Coverage too low: 70.0%".to_string()));
387    }
388}