clnrm_core/validation/
order_validator.rs

1//! Temporal ordering validation for span sequences
2//!
3//! Validates that spans occur in expected temporal order based on timestamps.
4
5use crate::error::{CleanroomError, Result};
6use crate::validation::span_validator::SpanData;
7use serde::{Deserialize, Serialize};
8
9/// Temporal ordering expectations
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct OrderExpectation {
12    /// Edges where first must precede second (first.end <= second.start)
13    pub must_precede: Vec<(String, String)>,
14    /// Edges where first must follow second (first.start >= second.end)
15    pub must_follow: Vec<(String, String)>,
16}
17
18impl Default for OrderExpectation {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl OrderExpectation {
25    /// Create a new OrderExpectation with no constraints
26    pub fn new() -> Self {
27        Self {
28            must_precede: Vec::new(),
29            must_follow: Vec::new(),
30        }
31    }
32
33    /// Add must_precede constraints
34    pub fn with_must_precede(mut self, edges: Vec<(String, String)>) -> Self {
35        self.must_precede = edges;
36        self
37    }
38
39    /// Add must_follow constraints
40    pub fn with_must_follow(mut self, edges: Vec<(String, String)>) -> Self {
41        self.must_follow = edges;
42        self
43    }
44
45    /// Validate temporal ordering constraints
46    pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
47        // Validate must_precede constraints
48        for (first_name, second_name) in &self.must_precede {
49            self.validate_precedes(spans, first_name, second_name)?;
50        }
51
52        // Validate must_follow constraints
53        for (first_name, second_name) in &self.must_follow {
54            self.validate_follows(spans, first_name, second_name)?;
55        }
56
57        Ok(())
58    }
59
60    fn validate_precedes(&self, spans: &[SpanData], first: &str, second: &str) -> Result<()> {
61        let first_spans: Vec<_> = spans.iter().filter(|s| s.name == first).collect();
62        let second_spans: Vec<_> = spans.iter().filter(|s| s.name == second).collect();
63
64        if first_spans.is_empty() {
65            return Err(CleanroomError::validation_error(format!(
66                "Order validation failed: span '{}' not found for must_precede constraint",
67                first
68            )));
69        }
70
71        if second_spans.is_empty() {
72            return Err(CleanroomError::validation_error(format!(
73                "Order validation failed: span '{}' not found for must_precede constraint",
74                second
75            )));
76        }
77
78        // Check if any first span precedes any second span
79        let mut found_valid_order = false;
80        for first_span in &first_spans {
81            for second_span in &second_spans {
82                if self.span_precedes(first_span, second_span)? {
83                    found_valid_order = true;
84                    break;
85                }
86            }
87            if found_valid_order {
88                break;
89            }
90        }
91
92        if !found_valid_order {
93            return Err(CleanroomError::validation_error(format!(
94                "Order validation failed: '{}' must precede '{}' but no valid ordering found",
95                first, second
96            )));
97        }
98
99        Ok(())
100    }
101
102    fn validate_follows(&self, spans: &[SpanData], first: &str, second: &str) -> Result<()> {
103        // "first must follow second" is same as "second must precede first"
104        self.validate_precedes(spans, second, first)
105    }
106
107    fn span_precedes(&self, first: &SpanData, second: &SpanData) -> Result<bool> {
108        let first_end = first.end_time_unix_nano.ok_or_else(|| {
109            CleanroomError::validation_error(format!(
110                "Span '{}' missing end timestamp for order validation",
111                first.name
112            ))
113        })?;
114
115        let second_start = second.start_time_unix_nano.ok_or_else(|| {
116            CleanroomError::validation_error(format!(
117                "Span '{}' missing start timestamp for order validation",
118                second.name
119            ))
120        })?;
121
122        // First precedes second if first.end <= second.start
123        Ok(first_end <= second_start)
124    }
125}