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}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::validation::count_validator::{CountBound, CountExpectation};
200    use crate::validation::graph_validator::GraphExpectation;
201    use crate::validation::hermeticity_validator::HermeticityExpectation;
202    use std::collections::HashMap;
203
204    fn create_test_span(name: &str, span_id: &str, parent_id: Option<&str>) -> SpanData {
205        SpanData {
206            name: name.to_string(),
207            trace_id: "test_trace".to_string(),
208            span_id: span_id.to_string(),
209            parent_span_id: parent_id.map(|s| s.to_string()),
210            attributes: HashMap::new(),
211            start_time_unix_nano: Some(1000000),
212            end_time_unix_nano: Some(2000000),
213            kind: None,
214            events: None,
215            resource_attributes: HashMap::new(),
216        }
217    }
218
219    #[test]
220    fn test_orchestrator_all_validations_pass() {
221        // Arrange
222        let spans = vec![
223            create_test_span("root", "s1", None),
224            create_test_span("child", "s2", Some("s1")),
225        ];
226
227        let graph = GraphExpectation::new(vec![("root".to_string(), "child".to_string())]);
228
229        let counts = CountExpectation::new()
230            .with_name_count("root".to_string(), CountBound::eq(1))
231            .with_name_count("child".to_string(), CountBound::eq(1));
232
233        let hermeticity = HermeticityExpectation::default();
234
235        let expectations = PrdExpectations::new()
236            .with_graph(graph)
237            .with_counts(counts)
238            .with_hermeticity(hermeticity);
239
240        // Act
241        let report = expectations.validate_all(&spans).unwrap();
242
243        // Assert
244        assert!(report.is_success());
245        assert!(report.pass_count() >= 2); // graph + counts (hermeticity may pass too)
246    }
247
248    #[test]
249    fn test_orchestrator_graph_validation_fails() {
250        // Arrange
251        let spans = vec![create_test_span("root", "s1", None)];
252
253        let graph = GraphExpectation::new(vec![("root".to_string(), "missing_child".to_string())]);
254
255        let expectations = PrdExpectations::new().with_graph(graph);
256
257        // Act
258        let report = expectations.validate_all(&spans).unwrap();
259
260        // Assert
261        assert!(!report.is_success());
262        assert_eq!(report.failure_count(), 1);
263        assert!(report.first_error().unwrap().contains("missing_child"));
264    }
265
266    #[test]
267    fn test_orchestrator_count_validation_fails() {
268        // Arrange
269        let spans = vec![create_test_span("root", "s1", None)];
270
271        let counts = CountExpectation::new().with_name_count("root".to_string(), CountBound::eq(2)); // Expect 2, have 1
272
273        let expectations = PrdExpectations::new().with_counts(counts);
274
275        // Act
276        let report = expectations.validate_all(&spans).unwrap();
277
278        // Assert
279        assert!(!report.is_success());
280        assert_eq!(report.failure_count(), 1);
281    }
282
283    #[test]
284    fn test_validation_report_summary() {
285        // Arrange
286        let mut report = ValidationReport::new();
287        report.add_pass("test1");
288        report.add_pass("test2");
289        report.add_fail("test3", "Error message".to_string());
290
291        // Act
292        let summary = report.summary();
293
294        // Assert
295        assert!(summary.contains("2 passed"));
296        assert!(summary.contains("1 failed"));
297        assert!(summary.contains("test3"));
298        assert!(summary.contains("Error message"));
299    }
300
301    #[test]
302    fn test_validate_strict_fails_on_error() {
303        // Arrange
304        let spans = vec![create_test_span("root", "s1", None)];
305
306        let counts = CountExpectation::new().with_name_count("root".to_string(), CountBound::eq(2));
307
308        let expectations = PrdExpectations::new().with_counts(counts);
309
310        // Act
311        let result = expectations.validate_strict(&spans);
312
313        // Assert
314        assert!(result.is_err());
315    }
316}