1use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10
11#[derive(Debug, Deserialize, Serialize)]
13pub struct WeaverValidationReport {
14 pub advice_level_counts: AdviceCounts,
16 pub registry_coverage: f64,
18 pub all_advice: Vec<Advice>,
20 pub seen_registry_attributes: HashMap<String, u32>,
22 pub seen_non_registry_attributes: HashMap<String, u32>,
24}
25
26#[derive(Debug, Deserialize, Serialize)]
28pub struct AdviceCounts {
29 pub violation: u32,
31 pub improvement: u32,
33 pub information: u32,
35}
36
37#[derive(Debug, Deserialize, Serialize, Clone)]
39pub struct Advice {
40 pub advice_level: String,
42 pub advice_type: String,
44 pub message: String,
46 pub signal_name: String,
48 pub signal_type: String,
50}
51
52#[derive(Debug, Clone)]
54pub struct ValidationAnalysis {
55 pub passed: bool,
57 pub total_violations: u32,
59 pub coverage: f64,
61 pub violations: Vec<Advice>,
63 pub improvements: Vec<Advice>,
65 pub missing_critical_attributes: Vec<String>,
67}
68
69impl ValidationAnalysis {
70 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 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 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 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 pub fn meets_release_criteria(&self) -> bool {
175 if !self.passed {
177 return false;
178 }
179
180 if self.coverage < 0.85 {
182 return false;
183 }
184
185 if !self.missing_critical_attributes.is_empty() {
187 return false;
188 }
189
190 true
191 }
192
193 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#[derive(Debug, Clone)]
218pub struct WeaverValidationResult {
219 pub status: ValidationStatus,
221 pub schema_valid: bool,
223 pub telemetry_valid: bool,
225 pub coverage: f64,
227 pub violations: Vec<String>,
229 pub recommendations: Vec<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum ValidationStatus {
236 Passed,
238 Failed,
240 Incomplete,
242}
243
244impl WeaverValidationResult {
245 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, telemetry_valid: analysis.passed,
269 coverage: analysis.coverage,
270 violations,
271 recommendations,
272 }
273 }
274
275 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 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}