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}