clnrm_core/validation/
window_validator.rs

1//! Temporal window validator for OTEL span containment
2//!
3//! Validates that child spans are temporally contained within parent spans.
4//! This ensures proper span lifecycle management and helps detect timing issues.
5
6use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9
10/// Represents a temporal window expectation
11///
12/// Validates that all specified child spans are temporally contained within
13/// an outer span, meaning:
14/// - outer.start_time <= child.start_time
15/// - child.end_time <= outer.end_time
16///
17/// # Example
18///
19/// ```toml
20/// [[expect.window]]
21/// outer = "root_span_name"
22/// contains = ["child_a", "child_b"]
23/// ```
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct WindowExpectation {
26    /// Name of the outer (parent) span that should contain children
27    pub outer: String,
28    /// Names of child spans that must be temporally contained
29    pub contains: Vec<String>,
30}
31
32impl WindowExpectation {
33    /// Create a new window expectation
34    ///
35    /// # Arguments
36    /// * `outer` - Name of the outer span
37    /// * `contains` - Names of child spans that must be contained
38    pub fn new(outer: impl Into<String>, contains: Vec<String>) -> Self {
39        Self {
40            outer: outer.into(),
41            contains,
42        }
43    }
44
45    /// Validate temporal containment across all spans
46    ///
47    /// # Arguments
48    /// * `spans` - All spans to validate against
49    ///
50    /// # Returns
51    /// * `Ok(())` if all children are temporally contained in outer span
52    /// * `Err` with detailed message if validation fails
53    ///
54    /// # Errors
55    /// * Outer span not found
56    /// * Child span not found
57    /// * Missing timestamps on any span
58    /// * Temporal containment violation (child outside parent window)
59    pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
60        // Find the outer span by name
61        let outer_span = self.find_span_by_name(spans, &self.outer)?;
62
63        // Validate outer span has timestamps
64        let (outer_start, outer_end) = self.extract_timestamps(outer_span, &self.outer)?;
65
66        // Validate each child span
67        for child_name in &self.contains {
68            let child_span = self.find_span_by_name(spans, child_name)?;
69            let (child_start, child_end) = self.extract_timestamps(child_span, child_name)?;
70
71            // Check temporal containment
72            self.validate_containment(
73                &self.outer,
74                outer_start,
75                outer_end,
76                child_name,
77                child_start,
78                child_end,
79            )?;
80        }
81
82        Ok(())
83    }
84
85    /// Find a span by name
86    fn find_span_by_name<'a>(&self, spans: &'a [SpanData], name: &str) -> Result<&'a SpanData> {
87        spans.iter().find(|s| s.name == name).ok_or_else(|| {
88            CleanroomError::validation_error(format!(
89                "Window validation failed: span '{}' not found in trace",
90                name
91            ))
92        })
93    }
94
95    /// Extract and validate timestamps from a span
96    fn extract_timestamps(&self, span: &SpanData, span_name: &str) -> Result<(u64, u64)> {
97        let start_time = span.start_time_unix_nano.ok_or_else(|| {
98            CleanroomError::validation_error(format!(
99                "Window validation failed: span '{}' missing start_time_unix_nano",
100                span_name
101            ))
102        })?;
103
104        let end_time = span.end_time_unix_nano.ok_or_else(|| {
105            CleanroomError::validation_error(format!(
106                "Window validation failed: span '{}' missing end_time_unix_nano",
107                span_name
108            ))
109        })?;
110
111        Ok((start_time, end_time))
112    }
113
114    /// Validate temporal containment between parent and child
115    fn validate_containment(
116        &self,
117        outer_name: &str,
118        outer_start: u64,
119        outer_end: u64,
120        child_name: &str,
121        child_start: u64,
122        child_end: u64,
123    ) -> Result<()> {
124        // Check: outer.start <= child.start
125        if child_start < outer_start {
126            return Err(CleanroomError::validation_error(format!(
127                "Window validation failed: child span '{}' started before outer span '{}' \
128                 (child_start: {}, outer_start: {})",
129                child_name, outer_name, child_start, outer_start
130            )));
131        }
132
133        // Check: child.end <= outer.end
134        if child_end > outer_end {
135            return Err(CleanroomError::validation_error(format!(
136                "Window validation failed: child span '{}' ended after outer span '{}' \
137                 (child_end: {}, outer_end: {})",
138                child_name, outer_name, child_end, outer_end
139            )));
140        }
141
142        Ok(())
143    }
144}
145
146/// Window validator for validating multiple window expectations
147pub struct WindowValidator;
148
149impl WindowValidator {
150    /// Validate multiple window expectations against a set of spans
151    ///
152    /// # Arguments
153    /// * `expectations` - Window expectations to validate
154    /// * `spans` - Spans to validate against
155    ///
156    /// # Returns
157    /// * `Ok(())` if all expectations pass
158    /// * `Err` with the first validation failure
159    pub fn validate_windows(expectations: &[WindowExpectation], spans: &[SpanData]) -> Result<()> {
160        for expectation in expectations {
161            expectation.validate(spans)?;
162        }
163        Ok(())
164    }
165}