1use super::config::{Complete80_20Config, EightyTwentyConfig, ValidationConfig, ValidationMode};
8use serde::{Deserialize, Serialize};
9
10pub struct ConformanceValidator {
12 config: ValidationConfig,
13 eighty_twenty_config: Option<EightyTwentyConfig>,
14 #[allow(dead_code)]
15 complete_config: Option<Complete80_20Config>,
16}
17
18impl ConformanceValidator {
19 pub fn new(config: ValidationConfig) -> Self {
21 Self {
22 config,
23 eighty_twenty_config: None,
24 complete_config: None,
25 }
26 }
27
28 pub fn with_80_20_config(config: ValidationConfig, eighty_twenty: EightyTwentyConfig) -> Self {
30 Self {
31 config,
32 eighty_twenty_config: Some(eighty_twenty),
33 complete_config: None,
34 }
35 }
36
37 pub fn with_complete_config(complete: Complete80_20Config) -> Self {
39 Self {
40 config: complete.validation_config.clone(),
41 eighty_twenty_config: None,
42 complete_config: Some(complete),
43 }
44 }
45
46 pub fn validate(&self, report: &ConformanceReport) -> ValidationResult {
48 let start = std::time::Instant::now();
49
50 let result = match self.config.mode {
51 ValidationMode::Strict => self.validate_strict(report),
52 ValidationMode::Lenient => self.validate_lenient(report),
53 ValidationMode::EightyTwenty => self.validate_eighty_twenty(report),
54 ValidationMode::Minimal => self.validate_minimal(report),
55 };
56
57 let duration_ms = start.elapsed().as_millis() as u64;
58
59 let within_budget = duration_ms <= self.config.max_validation_time_ms;
61
62 ValidationResult {
63 mode: self.config.mode,
64 violations: result.0,
65 coverage: result.1,
66 passed: result.2,
67 duration_ms,
68 within_time_budget: within_budget,
69 }
70 }
71
72 fn validate_strict(&self, report: &ConformanceReport) -> (Vec<Violation>, f64, bool) {
74 let mut violations = Vec::new();
75
76 for span in &report.required_spans {
78 if !report.present_spans.contains(span) {
79 violations.push(Violation::MissingSpan(span.clone()));
80 }
81 }
82
83 for attr in &report.required_attributes {
85 if !report.present_attributes.contains(attr) {
86 violations.push(Violation::MissingAttribute(attr.clone()));
87 }
88 }
89
90 if self.config.fail_on_missing_optional {
92 for attr in &report.optional_attributes {
93 if !report.present_attributes.contains(attr) {
94 violations.push(Violation::MissingOptionalAttribute(attr.clone()));
95 }
96 }
97 }
98
99 let coverage = self.calculate_coverage(report);
100 let passed = violations.is_empty() && coverage >= self.config.coverage_threshold;
101
102 (violations, coverage, passed)
103 }
104
105 fn validate_lenient(&self, report: &ConformanceReport) -> (Vec<Violation>, f64, bool) {
107 let mut violations = Vec::new();
108
109 for span in &report.required_spans {
111 if !report.present_spans.contains(span) {
112 violations.push(Violation::MissingSpan(span.clone()));
113 }
114 }
115
116 for attr in &report.required_attributes {
118 if !report.present_attributes.contains(attr) {
119 violations.push(Violation::MissingAttribute(attr.clone()));
120 }
121 }
122
123 let coverage = self.calculate_coverage(report);
126 let passed = violations.is_empty() && coverage >= self.config.coverage_threshold;
127
128 (violations, coverage, passed)
129 }
130
131 fn validate_eighty_twenty(&self, report: &ConformanceReport) -> (Vec<Violation>, f64, bool) {
133 let eighty_twenty = match &self.eighty_twenty_config {
134 Some(config) => config,
135 None => {
136 &EightyTwentyConfig::default()
138 }
139 };
140
141 let mut violations = Vec::new();
142
143 for span in &eighty_twenty.critical_spans {
145 if !report.present_spans.contains(span) {
146 violations.push(Violation::MissingCriticalSpan(span.clone()));
147 }
148 }
149
150 for attr in &eighty_twenty.required_attributes {
152 if !report.present_attributes.contains(attr) {
153 violations.push(Violation::MissingCriticalAttribute(attr.clone()));
154 }
155 }
156
157 let coverage = self.calculate_critical_coverage(report, eighty_twenty);
160 let passed = violations.is_empty() && coverage >= self.config.coverage_threshold;
161
162 (violations, coverage, passed)
163 }
164
165 fn validate_minimal(&self, report: &ConformanceReport) -> (Vec<Violation>, f64, bool) {
167 let eighty_twenty = match &self.eighty_twenty_config {
168 Some(config) => config,
169 None => &EightyTwentyConfig::default(),
170 };
171
172 let mut violations = Vec::new();
173
174 for span in &eighty_twenty.critical_spans {
176 if !report.present_spans.contains(span) {
177 violations.push(Violation::MissingCriticalSpan(span.clone()));
178 }
179 }
180
181 let minimal_attrs = ["container.id", "test.hermetic", "test.result"];
183 for attr in &minimal_attrs {
184 if !report.present_attributes.contains(&attr.to_string()) {
185 violations.push(Violation::MissingCriticalAttribute(attr.to_string()));
186 }
187 }
188
189 let coverage = self.calculate_minimal_coverage(report, &minimal_attrs);
190 let passed = violations.is_empty() && coverage >= self.config.coverage_threshold;
191
192 (violations, coverage, passed)
193 }
194
195 fn calculate_coverage(&self, report: &ConformanceReport) -> f64 {
197 let total_spans = report.required_spans.len();
198 let total_attrs = report.required_attributes.len();
199 let total = total_spans + total_attrs;
200
201 if total == 0 {
202 return 100.0;
203 }
204
205 let present_spans = report
206 .required_spans
207 .iter()
208 .filter(|s| report.present_spans.contains(*s))
209 .count();
210
211 let present_attrs = report
212 .required_attributes
213 .iter()
214 .filter(|a| report.present_attributes.contains(*a))
215 .count();
216
217 let present = present_spans + present_attrs;
218 (present as f64 / total as f64) * 100.0
219 }
220
221 fn calculate_critical_coverage(
223 &self,
224 report: &ConformanceReport,
225 eighty_twenty: &EightyTwentyConfig,
226 ) -> f64 {
227 let total_critical_spans = eighty_twenty.critical_spans.len();
228 let total_required_attrs = eighty_twenty.required_attributes.len();
229 let total = total_critical_spans + total_required_attrs;
230
231 if total == 0 {
232 return 100.0;
233 }
234
235 let present_spans = eighty_twenty
236 .critical_spans
237 .iter()
238 .filter(|s| report.present_spans.contains(*s))
239 .count();
240
241 let present_attrs = eighty_twenty
242 .required_attributes
243 .iter()
244 .filter(|a| report.present_attributes.contains(*a))
245 .count();
246
247 let present = present_spans + present_attrs;
248 (present as f64 / total as f64) * 100.0
249 }
250
251 fn calculate_minimal_coverage(
253 &self,
254 report: &ConformanceReport,
255 minimal_attrs: &[&str],
256 ) -> f64 {
257 let eighty_twenty = match &self.eighty_twenty_config {
258 Some(config) => config,
259 None => &EightyTwentyConfig::default(),
260 };
261
262 let total_spans = eighty_twenty.critical_spans.len();
263 let total_attrs = minimal_attrs.len();
264 let total = total_spans + total_attrs;
265
266 if total == 0 {
267 return 100.0;
268 }
269
270 let present_spans = eighty_twenty
271 .critical_spans
272 .iter()
273 .filter(|s| report.present_spans.contains(*s))
274 .count();
275
276 let present_attrs = minimal_attrs
277 .iter()
278 .filter(|a| report.present_attributes.contains(&a.to_string()))
279 .count();
280
281 let present = present_spans + present_attrs;
282 (present as f64 / total as f64) * 100.0
283 }
284
285 pub fn get_coverage_breakdown(&self, report: &ConformanceReport) -> CoverageBreakdown {
287 let eighty_twenty = match &self.eighty_twenty_config {
288 Some(config) => config,
289 None => &EightyTwentyConfig::default(),
290 };
291
292 let critical_spans_total = eighty_twenty.critical_spans.len();
294 let critical_spans_present = eighty_twenty
295 .critical_spans
296 .iter()
297 .filter(|s| report.present_spans.contains(*s))
298 .count();
299 let critical_spans_coverage = if critical_spans_total > 0 {
300 (critical_spans_present as f64 / critical_spans_total as f64) * 100.0
301 } else {
302 100.0
303 };
304
305 let required_attrs_total = eighty_twenty.required_attributes.len();
307 let required_attrs_present = eighty_twenty
308 .required_attributes
309 .iter()
310 .filter(|a| report.present_attributes.contains(*a))
311 .count();
312 let required_attrs_coverage = if required_attrs_total > 0 {
313 (required_attrs_present as f64 / required_attrs_total as f64) * 100.0
314 } else {
315 100.0
316 };
317
318 let optional_attrs_total = eighty_twenty.optional_attributes.len();
320 let optional_attrs_present = eighty_twenty
321 .optional_attributes
322 .iter()
323 .filter(|a| report.present_attributes.contains(*a))
324 .count();
325 let optional_attrs_coverage = if optional_attrs_total > 0 {
326 (optional_attrs_present as f64 / optional_attrs_total as f64) * 100.0
327 } else {
328 100.0
329 };
330
331 CoverageBreakdown {
332 critical_spans_coverage,
333 critical_spans_present,
334 critical_spans_total,
335 required_attributes_coverage: required_attrs_coverage,
336 required_attributes_present: required_attrs_present,
337 required_attributes_total: required_attrs_total,
338 optional_attributes_coverage: optional_attrs_coverage,
339 optional_attributes_present: optional_attrs_present,
340 optional_attributes_total: optional_attrs_total,
341 }
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ConformanceReport {
348 pub required_spans: Vec<String>,
350 pub present_spans: Vec<String>,
352 pub required_attributes: Vec<String>,
354 pub present_attributes: Vec<String>,
356 pub optional_attributes: Vec<String>,
358}
359
360impl ConformanceReport {
361 pub fn new() -> Self {
363 Self {
364 required_spans: Vec::new(),
365 present_spans: Vec::new(),
366 required_attributes: Vec::new(),
367 present_attributes: Vec::new(),
368 optional_attributes: Vec::new(),
369 }
370 }
371
372 pub fn add_required_span(&mut self, span: String) {
374 self.required_spans.push(span);
375 }
376
377 pub fn add_present_span(&mut self, span: String) {
379 self.present_spans.push(span);
380 }
381
382 pub fn add_required_attribute(&mut self, attr: String) {
384 self.required_attributes.push(attr);
385 }
386
387 pub fn add_present_attribute(&mut self, attr: String) {
389 self.present_attributes.push(attr);
390 }
391
392 pub fn add_optional_attribute(&mut self, attr: String) {
394 self.optional_attributes.push(attr);
395 }
396}
397
398impl Default for ConformanceReport {
399 fn default() -> Self {
400 Self::new()
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ValidationResult {
407 pub mode: ValidationMode,
409 pub violations: Vec<Violation>,
411 pub coverage: f64,
413 pub passed: bool,
415 pub duration_ms: u64,
417 pub within_time_budget: bool,
419}
420
421impl ValidationResult {
422 pub fn is_success(&self) -> bool {
424 self.passed && self.within_time_budget
425 }
426
427 pub fn summary(&self) -> String {
429 if self.passed {
430 format!(
431 "✅ Validation PASSED ({:?} mode): {:.1}% coverage in {}ms",
432 self.mode, self.coverage, self.duration_ms
433 )
434 } else {
435 format!(
436 "❌ Validation FAILED ({:?} mode): {:.1}% coverage, {} violations",
437 self.mode,
438 self.coverage,
439 self.violations.len()
440 )
441 }
442 }
443
444 pub fn print_report(&self) {
446 println!("\n{}", "=".repeat(60));
447 println!("WEAVER VALIDATION REPORT ({:?} MODE)", self.mode);
448 println!("{}", "=".repeat(60));
449
450 println!(
451 "\nStatus: {}",
452 if self.passed {
453 "✅ PASSED"
454 } else {
455 "❌ FAILED"
456 }
457 );
458 println!("Coverage: {:.1}%", self.coverage);
459 println!("Duration: {}ms", self.duration_ms);
460 println!(
461 "Time Budget: {}",
462 if self.within_time_budget {
463 "✅ Met"
464 } else {
465 "⚠️ Exceeded"
466 }
467 );
468
469 if !self.violations.is_empty() {
470 println!("\n{} VIOLATIONS FOUND:", self.violations.len());
471 for (i, violation) in self.violations.iter().enumerate() {
472 println!(" {}. {}", i + 1, violation);
473 }
474 }
475
476 println!("\n{}", "=".repeat(60));
477 }
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
482pub enum Violation {
483 MissingSpan(String),
485 MissingAttribute(String),
487 MissingOptionalAttribute(String),
489 MissingCriticalSpan(String),
491 MissingCriticalAttribute(String),
493}
494
495impl std::fmt::Display for Violation {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497 match self {
498 Violation::MissingSpan(span) => write!(f, "Missing required span: {}", span),
499 Violation::MissingAttribute(attr) => write!(f, "Missing required attribute: {}", attr),
500 Violation::MissingOptionalAttribute(attr) => {
501 write!(f, "Missing optional attribute: {}", attr)
502 }
503 Violation::MissingCriticalSpan(span) => write!(f, "Missing CRITICAL span: {}", span),
504 Violation::MissingCriticalAttribute(attr) => {
505 write!(f, "Missing CRITICAL attribute: {}", attr)
506 }
507 }
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct CoverageBreakdown {
514 pub critical_spans_coverage: f64,
516 pub critical_spans_present: usize,
518 pub critical_spans_total: usize,
520 pub required_attributes_coverage: f64,
522 pub required_attributes_present: usize,
524 pub required_attributes_total: usize,
526 pub optional_attributes_coverage: f64,
528 pub optional_attributes_present: usize,
530 pub optional_attributes_total: usize,
532}
533
534impl CoverageBreakdown {
535 pub fn print(&self) {
537 println!("\n{}", "=".repeat(60));
538 println!("COVERAGE BREAKDOWN");
539 println!("{}", "=".repeat(60));
540
541 println!(
542 "\nCritical Spans: {:.1}% ({}/{})",
543 self.critical_spans_coverage, self.critical_spans_present, self.critical_spans_total
544 );
545
546 println!(
547 "Required Attributes: {:.1}% ({}/{})",
548 self.required_attributes_coverage,
549 self.required_attributes_present,
550 self.required_attributes_total
551 );
552
553 println!(
554 "Optional Attributes: {:.1}% ({}/{})",
555 self.optional_attributes_coverage,
556 self.optional_attributes_present,
557 self.optional_attributes_total
558 );
559
560 println!("\n{}", "=".repeat(60));
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 fn create_test_report() -> ConformanceReport {
569 let mut report = ConformanceReport::new();
570
571 report.add_required_span("span1".to_string());
573 report.add_required_span("span2".to_string());
574 report.add_required_span("span3".to_string());
575
576 report.add_present_span("span1".to_string());
578 report.add_present_span("span2".to_string());
579
580 report.add_required_attribute("attr1".to_string());
582 report.add_required_attribute("attr2".to_string());
583
584 report.add_present_attribute("attr1".to_string());
586
587 report
588 }
589
590 #[test]
591 fn test_strict_mode_all_present() {
592 let config = ValidationConfig::strict();
593 let validator = ConformanceValidator::new(config);
594
595 let mut report = ConformanceReport::new();
596 report.add_required_span("span1".to_string());
597 report.add_present_span("span1".to_string());
598 report.add_required_attribute("attr1".to_string());
599 report.add_present_attribute("attr1".to_string());
600
601 let result = validator.validate(&report);
602
603 assert!(result.passed);
604 assert_eq!(result.coverage, 100.0);
605 assert!(result.violations.is_empty());
606 }
607
608 #[test]
609 fn test_strict_mode_missing_span() {
610 let config = ValidationConfig::strict();
611 let validator = ConformanceValidator::new(config);
612
613 let mut report = ConformanceReport::new();
614 report.add_required_span("span1".to_string());
615 report.add_required_attribute("attr1".to_string());
617 report.add_present_attribute("attr1".to_string());
618
619 let result = validator.validate(&report);
620
621 assert!(!result.passed);
622 assert_eq!(result.violations.len(), 1);
623 assert!(matches!(result.violations[0], Violation::MissingSpan(_)));
624 }
625
626 #[test]
627 fn test_eighty_twenty_mode_critical_only() {
628 let config = ValidationConfig::eighty_twenty();
629 let eighty_twenty = EightyTwentyConfig {
630 critical_spans: vec!["critical_span".to_string()],
631 required_attributes: vec!["critical_attr".to_string()],
632 optional_attributes: vec![],
633 };
634
635 let validator = ConformanceValidator::with_80_20_config(config, eighty_twenty);
636
637 let mut report = ConformanceReport::new();
638 report.add_required_span("critical_span".to_string());
639 report.add_required_span("optional_span".to_string());
640 report.add_present_span("critical_span".to_string());
641 report.add_required_attribute("critical_attr".to_string());
644 report.add_present_attribute("critical_attr".to_string());
645
646 let result = validator.validate(&report);
647
648 assert!(result.passed);
650 assert_eq!(result.coverage, 100.0); }
652
653 #[test]
654 fn test_coverage_calculation() {
655 let config = ValidationConfig::strict();
656 let validator = ConformanceValidator::new(config);
657
658 let report = create_test_report();
659 let result = validator.validate(&report);
660
661 assert_eq!(result.coverage, 60.0);
663 }
664
665 #[test]
666 fn test_minimal_mode() {
667 let config = ValidationConfig::minimal();
668 let validator = ConformanceValidator::new(config);
669
670 let mut report = ConformanceReport::new();
671
672 for span in &[
674 "clnrm.test.execute",
675 "clnrm.container.start",
676 "clnrm.container.stop",
677 "clnrm.test.cleanup",
678 "clnrm.cli.health",
679 ] {
680 report.add_required_span(span.to_string());
681 report.add_present_span(span.to_string());
682 }
683
684 for attr in &["container.id", "test.hermetic", "test.result"] {
686 report.add_required_attribute(attr.to_string());
687 report.add_present_attribute(attr.to_string());
688 }
689
690 let result = validator.validate(&report);
691
692 assert!(result.passed);
693 assert_eq!(result.coverage, 100.0);
694 }
695
696 #[test]
697 fn test_coverage_breakdown() {
698 let config = ValidationConfig::eighty_twenty();
699 let eighty_twenty = EightyTwentyConfig {
700 critical_spans: vec!["span1".to_string(), "span2".to_string()],
701 required_attributes: vec!["attr1".to_string(), "attr2".to_string()],
702 optional_attributes: vec!["opt1".to_string()],
703 };
704
705 let validator = ConformanceValidator::with_80_20_config(config, eighty_twenty);
706
707 let mut report = ConformanceReport::new();
708 report.add_present_span("span1".to_string());
709 report.add_present_attribute("attr1".to_string());
711 report.add_present_attribute("attr2".to_string());
712 let breakdown = validator.get_coverage_breakdown(&report);
715
716 assert_eq!(breakdown.critical_spans_coverage, 50.0); assert_eq!(breakdown.required_attributes_coverage, 100.0); assert_eq!(breakdown.optional_attributes_coverage, 0.0); }
720
721 #[test]
722 fn test_validation_time_budget() {
723 let config = ValidationConfig {
724 mode: ValidationMode::EightyTwenty,
725 fail_on_violation: true,
726 fail_on_missing_optional: false,
727 coverage_threshold: 80.0,
728 max_validation_time_ms: 1, };
730
731 let validator = ConformanceValidator::new(config);
732 let report = create_test_report();
733
734 let result = validator.validate(&report);
735
736 assert!(result.duration_ms >= 0);
739 }
740
741 #[test]
742 fn test_lenient_mode() {
743 let config = ValidationConfig::lenient();
744 let validator = ConformanceValidator::new(config);
745
746 let mut report = ConformanceReport::new();
747 report.add_required_span("span1".to_string());
748 report.add_present_span("span1".to_string());
749 report.add_required_attribute("attr1".to_string());
750 report.add_present_attribute("attr1".to_string());
751
752 report.add_optional_attribute("optional1".to_string());
754
755 let result = validator.validate(&report);
756
757 assert!(result.passed);
758 assert_eq!(result.coverage, 100.0);
759 }
760}