clnrm_core/validation/
orchestrator.rs

1//! Orchestrator for running all OTEL PRD validations
2//!
3//! Provides unified interface to run all validation checks and generate reports.
4
5use crate::error::{CleanroomError, Result};
6use crate::validation::count_validator::CountExpectation;
7use crate::validation::graph_validator::GraphExpectation;
8use crate::validation::hermeticity_validator::HermeticityExpectation;
9use crate::validation::span_validator::SpanData;
10use crate::validation::window_validator::WindowExpectation;
11
12/// Complete PRD validation expectations
13#[derive(Debug, Clone, Default)]
14pub struct PrdExpectations {
15    /// Graph topology expectations (parent-child relationships)
16    pub graph: Option<GraphExpectation>,
17    /// Span count expectations (exact, min, max counts)
18    pub counts: Option<CountExpectation>,
19    /// Temporal window expectations (containment)
20    pub windows: Vec<WindowExpectation>,
21    /// Hermeticity expectations (isolation, no cross-contamination)
22    pub hermeticity: Option<HermeticityExpectation>,
23}
24
25impl PrdExpectations {
26    /// Create new empty expectations
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Set graph expectations
32    pub fn with_graph(mut self, graph: GraphExpectation) -> Self {
33        self.graph = Some(graph);
34        self
35    }
36
37    /// Set count expectations
38    pub fn with_counts(mut self, counts: CountExpectation) -> Self {
39        self.counts = Some(counts);
40        self
41    }
42
43    /// Add window expectation
44    pub fn add_window(mut self, window: WindowExpectation) -> Self {
45        self.windows.push(window);
46        self
47    }
48
49    /// Set hermeticity expectations
50    pub fn with_hermeticity(mut self, hermeticity: HermeticityExpectation) -> Self {
51        self.hermeticity = Some(hermeticity);
52        self
53    }
54
55    /// Run all validations in order
56    ///
57    /// Validation order:
58    /// 1. Graph topology (structural correctness)
59    /// 2. Span counts (expected spans exist)
60    /// 3. Temporal windows (timing and ordering)
61    /// 4. Hermeticity (isolation and no contamination)
62    ///
63    /// # Arguments
64    /// * `spans` - Slice of span data to validate
65    ///
66    /// # Returns
67    /// * `Result<ValidationReport>` - Report with passes and failures
68    pub fn validate_all(&self, spans: &[SpanData]) -> Result<ValidationReport> {
69        let mut report = ValidationReport::new();
70
71        // 1. Validate graph topology
72        if let Some(ref graph) = self.graph {
73            match graph.validate(spans) {
74                Ok(_) => report.add_pass("graph_topology"),
75                Err(e) => report.add_fail("graph_topology", e.to_string()),
76            }
77        }
78
79        // 2. Validate counts
80        if let Some(ref counts) = self.counts {
81            match counts.validate(spans) {
82                Ok(_) => report.add_pass("span_counts"),
83                Err(e) => report.add_fail("span_counts", e.to_string()),
84            }
85        }
86
87        // 3. Validate temporal windows
88        for (idx, window) in self.windows.iter().enumerate() {
89            let name = format!("window_{}_outer_{}", idx, window.outer);
90            match window.validate(spans) {
91                Ok(_) => report.add_pass(&name),
92                Err(e) => report.add_fail(&name, e.to_string()),
93            }
94        }
95
96        // 4. Validate hermeticity
97        if let Some(ref hermetic) = self.hermeticity {
98            match hermetic.validate(spans) {
99                Ok(_) => report.add_pass("hermeticity"),
100                Err(e) => report.add_fail("hermeticity", e.to_string()),
101            }
102        }
103
104        Ok(report)
105    }
106
107    /// Validate and return Result (fail on first error)
108    pub fn validate_strict(&self, spans: &[SpanData]) -> Result<()> {
109        let report = self.validate_all(spans)?;
110        if report.is_success() {
111            Ok(())
112        } else {
113            Err(CleanroomError::validation_error(format!(
114                "Validation failed with {} errors: {}",
115                report.failure_count(),
116                report.first_error().unwrap_or("unknown error")
117            )))
118        }
119    }
120}
121
122/// Validation report containing passes and failures
123#[derive(Debug, Clone, Default)]
124pub struct ValidationReport {
125    /// Names of passed validations
126    passes: Vec<String>,
127    /// Failed validations with error messages
128    failures: Vec<(String, String)>,
129}
130
131impl ValidationReport {
132    /// Create new empty report
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Record a passing validation
138    pub fn add_pass(&mut self, name: &str) {
139        self.passes.push(name.to_string());
140    }
141
142    /// Record a failing validation
143    pub fn add_fail(&mut self, name: &str, error: String) {
144        self.failures.push((name.to_string(), error));
145    }
146
147    /// Check if all validations passed
148    pub fn is_success(&self) -> bool {
149        self.failures.is_empty()
150    }
151
152    /// Get number of passed validations
153    pub fn pass_count(&self) -> usize {
154        self.passes.len()
155    }
156
157    /// Get number of failed validations
158    pub fn failure_count(&self) -> usize {
159        self.failures.len()
160    }
161
162    /// Get all passing validation names
163    pub fn passes(&self) -> &[String] {
164        &self.passes
165    }
166
167    /// Get all failures
168    pub fn failures(&self) -> &[(String, String)] {
169        &self.failures
170    }
171
172    /// Get first error message if any
173    pub fn first_error(&self) -> Option<&str> {
174        self.failures.first().map(|(_, msg)| msg.as_str())
175    }
176
177    /// Generate human-readable summary
178    pub fn summary(&self) -> String {
179        if self.is_success() {
180            format!("✓ All {} validations passed", self.pass_count())
181        } else {
182            format!(
183                "✗ {} passed, {} failed\n{}",
184                self.pass_count(),
185                self.failure_count(),
186                self.failures
187                    .iter()
188                    .map(|(name, err)| format!("  - {}: {}", name, err))
189                    .collect::<Vec<_>>()
190                    .join("\n")
191            )
192        }
193    }
194}