clnrm_core/validation/
span_validator.rs

1//! Span validator for OTEL self-testing
2//!
3//! Validates that clnrm produced expected OTEL spans to prove functionality.
4//! This enables "testing via telemetry" - validating framework behavior by
5//! analyzing the spans it emitted.
6
7use crate::error::{CleanroomError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11
12/// OTEL span kind enumeration
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SpanKind {
16    /// Internal span (default)
17    Internal,
18    /// Server span (receiving a request)
19    Server,
20    /// Client span (making a request)
21    Client,
22    /// Producer span (message queue producer)
23    Producer,
24    /// Consumer span (message queue consumer)
25    Consumer,
26}
27
28impl SpanKind {
29    /// Parse span kind from string (custom parser, not std::str::FromStr trait)
30    pub fn parse_kind(s: &str) -> Result<Self> {
31        match s.to_lowercase().as_str() {
32            "internal" => Ok(SpanKind::Internal),
33            "server" => Ok(SpanKind::Server),
34            "client" => Ok(SpanKind::Client),
35            "producer" => Ok(SpanKind::Producer),
36            "consumer" => Ok(SpanKind::Consumer),
37            _ => Err(CleanroomError::validation_error(format!(
38                "Invalid span kind: '{}'. Must be one of: internal, server, client, producer, consumer",
39                s
40            ))),
41        }
42    }
43
44    /// Convert to OTEL integer representation
45    pub fn to_otel_int(&self) -> i32 {
46        match self {
47            SpanKind::Internal => 1,
48            SpanKind::Server => 2,
49            SpanKind::Client => 3,
50            SpanKind::Producer => 4,
51            SpanKind::Consumer => 5,
52        }
53    }
54
55    /// Parse from OTEL integer representation
56    pub fn from_otel_int(i: i32) -> Result<Self> {
57        match i {
58            1 => Ok(SpanKind::Internal),
59            2 => Ok(SpanKind::Server),
60            3 => Ok(SpanKind::Client),
61            4 => Ok(SpanKind::Producer),
62            5 => Ok(SpanKind::Consumer),
63            _ => Err(CleanroomError::validation_error(format!(
64                "Invalid OTEL span kind integer: {}",
65                i
66            ))),
67        }
68    }
69}
70
71/// Represents a single OTEL span from the collector's file exporter
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SpanData {
74    /// Span name (e.g., "clnrm.run", "clnrm.test")
75    pub name: String,
76    /// Span attributes as key-value pairs
77    pub attributes: HashMap<String, serde_json::Value>,
78    /// Trace ID this span belongs to
79    pub trace_id: String,
80    /// Span ID
81    pub span_id: String,
82    /// Parent span ID (if any)
83    pub parent_span_id: Option<String>,
84    /// Span start time (Unix timestamp in nanoseconds)
85    pub start_time_unix_nano: Option<u64>,
86    /// Span end time (Unix timestamp in nanoseconds)
87    pub end_time_unix_nano: Option<u64>,
88    /// Span kind (internal, server, client, producer, consumer)
89    pub kind: Option<SpanKind>,
90    /// Span events (array of event names)
91    pub events: Option<Vec<String>>,
92    /// Resource attributes (shared across all spans in a resource)
93    #[serde(default)]
94    pub resource_attributes: HashMap<String, serde_json::Value>,
95}
96
97impl SpanData {
98    /// Calculate span duration in milliseconds
99    pub fn duration_ms(&self) -> Option<f64> {
100        match (self.start_time_unix_nano, self.end_time_unix_nano) {
101            (Some(start), Some(end)) => {
102                if end >= start {
103                    Some((end - start) as f64 / 1_000_000.0)
104                } else {
105                    None
106                }
107            }
108            _ => None,
109        }
110    }
111}
112
113/// Assertion types for span validation
114#[derive(Debug, Clone)]
115pub enum SpanAssertion {
116    /// Assert a span with given name exists
117    SpanExists { name: String },
118    /// Assert exact count of spans with given name
119    SpanCount { name: String, count: usize },
120    /// Assert span has specific attribute with value
121    SpanAttribute {
122        name: String,
123        attribute_key: String,
124        attribute_value: String,
125    },
126    /// Assert span hierarchy (parent-child relationship)
127    SpanHierarchy { parent: String, child: String },
128
129    // NEW PRD-aligned assertions
130    /// Assert span kind (internal, server, client, producer, consumer)
131    SpanKind { name: String, kind: SpanKind },
132
133    /// Assert all attributes match (attrs.all from PRD)
134    /// All key-value pairs must be present in the span
135    SpanAllAttributes {
136        name: String,
137        attributes: HashMap<String, String>,
138    },
139
140    /// Assert at least one attribute pattern matches (attrs.any from PRD)
141    /// Patterns are in format "key=value"
142    /// At least one pattern must match
143    SpanAnyAttributes {
144        name: String,
145        attribute_patterns: Vec<String>,
146    },
147
148    /// Assert span has specific events (events.any from PRD)
149    /// At least one event name must be present
150    SpanEvents { name: String, events: Vec<String> },
151
152    /// Assert span duration is within bounds (duration_ms from PRD)
153    /// Both min and max are optional
154    SpanDuration {
155        name: String,
156        min_ms: Option<u64>,
157        max_ms: Option<u64>,
158    },
159}
160
161/// Validation failure details for precise error reporting
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FailureDetails {
164    /// Rule that failed (e.g., "expect.span[clnrm.run].attrs.all")
165    pub rule: String,
166    /// Span name that was validated
167    pub span_name: String,
168    /// Expected value
169    pub expected: String,
170    /// Actual value (if any)
171    pub actual: Option<String>,
172    /// Human-readable error message
173    pub message: String,
174}
175
176/// Validation result with detailed pass/fail information
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ValidationResult {
179    /// Whether all validations passed
180    pub passed: bool,
181    /// List of failures (empty if passed)
182    pub failures: Vec<FailureDetails>,
183    /// Number of validations performed
184    pub validations_count: usize,
185}
186
187impl ValidationResult {
188    /// Create a successful validation result
189    pub fn success(validations_count: usize) -> Self {
190        Self {
191            passed: true,
192            failures: Vec::new(),
193            validations_count,
194        }
195    }
196
197    /// Create a failed validation result
198    pub fn failure(failure: FailureDetails) -> Self {
199        Self {
200            passed: false,
201            failures: vec![failure],
202            validations_count: 1,
203        }
204    }
205
206    /// Add a failure to the result
207    pub fn add_failure(&mut self, failure: FailureDetails) {
208        self.passed = false;
209        self.failures.push(failure);
210    }
211
212    /// Merge multiple validation results
213    pub fn merge(results: Vec<ValidationResult>) -> Self {
214        let passed = results.iter().all(|r| r.passed);
215        let failures: Vec<FailureDetails> =
216            results.iter().flat_map(|r| r.failures.clone()).collect();
217        let validations_count: usize = results.iter().map(|r| r.validations_count).sum();
218
219        Self {
220            passed,
221            failures,
222            validations_count,
223        }
224    }
225}
226
227/// Span validator for OTEL self-testing
228pub struct SpanValidator {
229    /// Loaded span data from OTEL collector export
230    pub(crate) spans: Vec<SpanData>,
231}
232
233impl SpanValidator {
234    /// Create SpanValidator from OpenTelemetry SpanData
235    ///
236    /// Converts OpenTelemetry SDK span data to validator span format
237    /// for runtime validation against test expectations.
238    ///
239    /// # Arguments
240    ///
241    /// * `spans` - OpenTelemetry SpanData from telemetry collection
242    ///
243    /// # Returns
244    ///
245    /// * `Result<Self>` - SpanValidator instance or error
246    pub fn from_span_data(spans: &[opentelemetry_sdk::trace::SpanData]) -> Result<Self> {
247        let converted_spans: Vec<SpanData> = spans.iter().map(Self::convert_otel_span).collect();
248
249        Ok(Self {
250            spans: converted_spans,
251        })
252    }
253
254    /// Convert OpenTelemetry SpanData to validator SpanData
255    fn convert_otel_span(span: &opentelemetry_sdk::trace::SpanData) -> SpanData {
256        // Convert attributes
257        let mut attributes = std::collections::HashMap::new();
258        for kv in &span.attributes {
259            let key = kv.key.to_string();
260            let value = match &kv.value {
261                opentelemetry::Value::Bool(b) => serde_json::json!(b),
262                opentelemetry::Value::I64(i) => serde_json::json!(i),
263                opentelemetry::Value::F64(f) => serde_json::json!(f),
264                opentelemetry::Value::String(s) => serde_json::json!(s.to_string()),
265                _ => serde_json::json!(kv.value.to_string()),
266            };
267            attributes.insert(key, value);
268        }
269
270        // Convert span kind (use opentelemetry::trace::SpanKind from opentelemetry crate)
271        let kind = match span.span_kind {
272            opentelemetry::trace::SpanKind::Internal => Some(SpanKind::Internal),
273            opentelemetry::trace::SpanKind::Server => Some(SpanKind::Server),
274            opentelemetry::trace::SpanKind::Client => Some(SpanKind::Client),
275            opentelemetry::trace::SpanKind::Producer => Some(SpanKind::Producer),
276            opentelemetry::trace::SpanKind::Consumer => Some(SpanKind::Consumer),
277        };
278
279        // Convert events
280        let events = if span.events.is_empty() {
281            None
282        } else {
283            Some(span.events.iter().map(|e| e.name.to_string()).collect())
284        };
285
286        // Get parent span ID
287        let parent_span_id = if span.parent_span_id != opentelemetry::trace::SpanId::INVALID {
288            Some(format!("{:x}", span.parent_span_id))
289        } else {
290            None
291        };
292
293        // Convert timestamps
294        let start_time_unix_nano = span
295            .start_time
296            .duration_since(std::time::SystemTime::UNIX_EPOCH)
297            .ok()
298            .map(|d| d.as_nanos() as u64);
299
300        let end_time_unix_nano = span
301            .end_time
302            .duration_since(std::time::SystemTime::UNIX_EPOCH)
303            .ok()
304            .map(|d| d.as_nanos() as u64);
305
306        SpanData {
307            name: span.name.to_string(),
308            attributes,
309            trace_id: format!("{:x}", span.span_context.trace_id()),
310            span_id: format!("{:x}", span.span_context.span_id()),
311            parent_span_id,
312            start_time_unix_nano,
313            end_time_unix_nano,
314            kind,
315            events,
316            resource_attributes: std::collections::HashMap::new(),
317        }
318    }
319
320    /// Create a new SpanValidator by loading spans from a JSON file
321    ///
322    /// The file should be in the format produced by OTEL collector's file exporter.
323    ///
324    /// # Arguments
325    /// * `path` - Path to the spans JSON file
326    ///
327    /// # Returns
328    /// * `Result<Self>` - SpanValidator instance or error
329    ///
330    /// # Errors
331    /// * File read errors
332    /// * JSON parsing errors
333    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
334        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
335            CleanroomError::config_error(format!("Failed to read spans file: {}", e))
336        })?;
337
338        Self::from_json(&content)
339    }
340
341    /// Create a new SpanValidator from JSON string
342    ///
343    /// # Arguments
344    /// * `json` - JSON string containing span data
345    ///
346    /// # Returns
347    /// * `Result<Self>` - SpanValidator instance or error
348    ///
349    /// # Errors
350    /// * JSON parsing errors
351    pub fn from_json(json: &str) -> Result<Self> {
352        // OTEL file exporter produces newline-delimited JSON (NDJSON)
353        // Each line is a complete JSON object representing one or more spans
354        let mut all_spans = Vec::new();
355
356        for line in json.lines() {
357            let line = line.trim();
358            if line.is_empty() {
359                continue;
360            }
361
362            // Try parsing as an array of spans first
363            if let Ok(spans) = serde_json::from_str::<Vec<SpanData>>(line) {
364                all_spans.extend(spans);
365            } else if let Ok(span) = serde_json::from_str::<SpanData>(line) {
366                // Single span
367                all_spans.push(span);
368            } else {
369                // Try parsing as OTEL JSON format with resource spans
370                if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
371                    if let Some(spans) = Self::extract_spans_from_otel_format(&value) {
372                        all_spans.extend(spans);
373                    }
374                }
375            }
376        }
377
378        Ok(Self { spans: all_spans })
379    }
380
381    /// Extract spans from OTEL collector JSON format
382    fn extract_spans_from_otel_format(value: &serde_json::Value) -> Option<Vec<SpanData>> {
383        let mut spans = Vec::new();
384
385        // Navigate OTEL structure: resourceSpans -> scopeSpans -> spans
386        if let Some(resource_spans) = value.get("resourceSpans").and_then(|v| v.as_array()) {
387            for resource_span in resource_spans {
388                if let Some(scope_spans) =
389                    resource_span.get("scopeSpans").and_then(|v| v.as_array())
390                {
391                    for scope_span in scope_spans {
392                        if let Some(span_array) = scope_span.get("spans").and_then(|v| v.as_array())
393                        {
394                            for span_obj in span_array {
395                                if let Some(span) = Self::parse_otel_span(span_obj) {
396                                    spans.push(span);
397                                }
398                            }
399                        }
400                    }
401                }
402            }
403        }
404
405        if spans.is_empty() {
406            None
407        } else {
408            Some(spans)
409        }
410    }
411
412    /// Parse a single OTEL span object
413    fn parse_otel_span(span_obj: &serde_json::Value) -> Option<SpanData> {
414        let name = span_obj.get("name")?.as_str()?.to_string();
415        let trace_id = span_obj.get("traceId")?.as_str()?.to_string();
416        let span_id = span_obj.get("spanId")?.as_str()?.to_string();
417        let parent_span_id = span_obj
418            .get("parentSpanId")
419            .and_then(|v| v.as_str())
420            .map(|s| s.to_string());
421
422        let start_time_unix_nano = span_obj
423            .get("startTimeUnixNano")
424            .and_then(|v| v.as_str())
425            .and_then(|s| s.parse::<u64>().ok());
426
427        let end_time_unix_nano = span_obj
428            .get("endTimeUnixNano")
429            .and_then(|v| v.as_str())
430            .and_then(|s| s.parse::<u64>().ok());
431
432        // Parse span kind
433        let kind = span_obj
434            .get("kind")
435            .and_then(|v| v.as_i64())
436            .and_then(|i| SpanKind::from_otel_int(i as i32).ok());
437
438        // Parse attributes
439        let mut attributes = HashMap::new();
440        if let Some(attrs) = span_obj.get("attributes").and_then(|v| v.as_array()) {
441            for attr in attrs {
442                if let (Some(key), Some(value)) =
443                    (attr.get("key").and_then(|k| k.as_str()), attr.get("value"))
444                {
445                    attributes.insert(key.to_string(), value.clone());
446                }
447            }
448        }
449
450        // Parse events
451        let events = span_obj
452            .get("events")
453            .and_then(|v| v.as_array())
454            .map(|events_array| {
455                events_array
456                    .iter()
457                    .filter_map(|event| {
458                        event.get("name").and_then(|n| n.as_str()).map(String::from)
459                    })
460                    .collect()
461            });
462
463        Some(SpanData {
464            name,
465            attributes,
466            trace_id,
467            span_id,
468            parent_span_id,
469            start_time_unix_nano,
470            end_time_unix_nano,
471            kind,
472            events,
473            resource_attributes: HashMap::new(),
474        })
475    }
476
477    /// Get all spans
478    pub fn spans(&self) -> &[SpanData] {
479        &self.spans
480    }
481
482    /// Find spans by name
483    pub fn find_spans_by_name(&self, name: &str) -> Vec<&SpanData> {
484        self.spans.iter().filter(|s| s.name == name).collect()
485    }
486
487    /// Find span by exact trace ID and span ID
488    pub fn find_span(&self, trace_id: &str, span_id: &str) -> Option<&SpanData> {
489        self.spans
490            .iter()
491            .find(|s| s.trace_id == trace_id && s.span_id == span_id)
492    }
493
494    /// Check if a span with the given name exists
495    pub fn has_span(&self, name: &str) -> bool {
496        self.spans.iter().any(|s| s.name == name)
497    }
498
499    /// Count spans with given name
500    pub fn count_spans(&self, name: &str) -> usize {
501        self.spans.iter().filter(|s| s.name == name).count()
502    }
503
504    /// Validate spans against PRD-style expectations with detailed error tracking
505    ///
506    /// This is the primary validation method that processes SpanExpectationConfig
507    /// from TOML and returns structured validation results.
508    ///
509    /// # Arguments
510    /// * `expectations` - List of span expectations from TOML `[[expect.span]]` blocks
511    ///
512    /// # Returns
513    /// * `Result<ValidationResult>` - Detailed validation results with failure tracking
514    pub fn validate_expectations(
515        &self,
516        expectations: &[crate::config::SpanExpectationConfig],
517    ) -> Result<ValidationResult> {
518        let mut results = Vec::new();
519
520        for expectation in expectations {
521            let result = self.validate_single_expectation(expectation)?;
522            results.push(result);
523        }
524
525        Ok(ValidationResult::merge(results))
526    }
527
528    /// Validate a single span expectation
529    fn validate_single_expectation(
530        &self,
531        expectation: &crate::config::SpanExpectationConfig,
532    ) -> Result<ValidationResult> {
533        let span_name = &expectation.name;
534
535        // 1. Check span existence
536        let matching_spans = self.find_spans_by_name(span_name);
537        if matching_spans.is_empty() {
538            return Ok(ValidationResult::failure(FailureDetails {
539                rule: format!("expect.span[{}].existence", span_name),
540                span_name: span_name.clone(),
541                expected: format!("Span '{}' to exist", span_name),
542                actual: None,
543                message: format!("Span '{}' not found in trace", span_name),
544            }));
545        }
546
547        let mut validation_count = 1; // existence check
548        let mut failures = Vec::new();
549
550        // Find first matching span for detailed validation
551        // In production, we may want to validate all matching spans
552        let span = matching_spans[0];
553
554        // 2. Validate parent relationship
555        if let Some(ref parent_name) = expectation.parent {
556            validation_count += 1;
557            if let Some(failure) = self.validate_parent_relationship(span, parent_name, span_name) {
558                failures.push(failure);
559            }
560        }
561
562        // 3. Validate span kind
563        if let Some(ref kind_str) = expectation.kind {
564            validation_count += 1;
565            if let Some(failure) = self.validate_span_kind(span, kind_str, span_name)? {
566                failures.push(failure);
567            }
568        }
569
570        // 4. Validate attributes
571        if let Some(ref attrs_config) = expectation.attrs {
572            // attrs.all - ALL attributes must match
573            if let Some(ref all_attrs) = attrs_config.all {
574                validation_count += all_attrs.len();
575                if let Some(failure) = self.validate_attrs_all(span, all_attrs, span_name) {
576                    failures.push(failure);
577                }
578            }
579
580            // attrs.any - At least ONE attribute must match
581            if let Some(ref any_attrs) = attrs_config.any {
582                validation_count += 1;
583                if let Some(failure) = self.validate_attrs_any(span, any_attrs, span_name) {
584                    failures.push(failure);
585                }
586            }
587        }
588
589        // 5. Validate events
590        if let Some(ref events_config) = expectation.events {
591            if let Some(ref any_events) = events_config.any {
592                validation_count += 1;
593                if let Some(failure) = self.validate_events_any(span, any_events, span_name) {
594                    failures.push(failure);
595                }
596            }
597
598            if let Some(ref all_events) = events_config.all {
599                validation_count += all_events.len();
600                if let Some(failure) = self.validate_events_all(span, all_events, span_name) {
601                    failures.push(failure);
602                }
603            }
604        }
605
606        // 6. Validate duration
607        if let Some(ref duration_config) = expectation.duration_ms {
608            validation_count += 1;
609            if let Some(failure) = self.validate_duration(span, duration_config, span_name) {
610                failures.push(failure);
611            }
612        }
613
614        if failures.is_empty() {
615            Ok(ValidationResult::success(validation_count))
616        } else {
617            Ok(ValidationResult {
618                passed: false,
619                failures,
620                validations_count: validation_count,
621            })
622        }
623    }
624
625    /// Validate parent relationship
626    fn validate_parent_relationship(
627        &self,
628        span: &SpanData,
629        parent_name: &str,
630        span_name: &str,
631    ) -> Option<FailureDetails> {
632        // Find parent spans by name
633        let parent_spans = self.find_spans_by_name(parent_name);
634        if parent_spans.is_empty() {
635            return Some(FailureDetails {
636                rule: format!("expect.span[{}].parent", span_name),
637                span_name: span_name.to_string(),
638                expected: format!("Parent span '{}'", parent_name),
639                actual: None,
640                message: format!(
641                    "Parent span '{}' not found for span '{}'",
642                    parent_name, span_name
643                ),
644            });
645        }
646
647        // Check if span has a parent_span_id matching any of the parent spans
648        if let Some(ref parent_id) = span.parent_span_id {
649            if parent_spans.iter().any(|p| &p.span_id == parent_id) {
650                return None; // Valid parent relationship
651            }
652
653            // Parent exists but IDs don't match
654            Some(FailureDetails {
655                rule: format!("expect.span[{}].parent", span_name),
656                span_name: span_name.to_string(),
657                expected: format!("Parent span '{}'", parent_name),
658                actual: Some(format!("Different parent (ID: {})", parent_id)),
659                message: format!(
660                    "Span '{}' parent mismatch: expected '{}', found different parent",
661                    span_name, parent_name
662                ),
663            })
664        } else {
665            // Span has no parent
666            Some(FailureDetails {
667                rule: format!("expect.span[{}].parent", span_name),
668                span_name: span_name.to_string(),
669                expected: format!("Parent span '{}'", parent_name),
670                actual: Some("none".to_string()),
671                message: format!(
672                    "Span '{}' parent mismatch: expected '{}', found none",
673                    span_name, parent_name
674                ),
675            })
676        }
677    }
678
679    /// Validate span kind
680    fn validate_span_kind(
681        &self,
682        span: &SpanData,
683        kind_str: &str,
684        span_name: &str,
685    ) -> Result<Option<FailureDetails>> {
686        let expected_kind = SpanKind::parse_kind(kind_str)?;
687
688        match span.kind {
689            Some(actual_kind) if actual_kind == expected_kind => Ok(None),
690            Some(actual_kind) => Ok(Some(FailureDetails {
691                rule: format!("expect.span[{}].kind", span_name),
692                span_name: span_name.to_string(),
693                expected: format!("{:?}", expected_kind),
694                actual: Some(format!("{:?}", actual_kind)),
695                message: format!(
696                    "Span '{}' kind mismatch: expected {:?}, found {:?}",
697                    span_name, expected_kind, actual_kind
698                ),
699            })),
700            None => Ok(Some(FailureDetails {
701                rule: format!("expect.span[{}].kind", span_name),
702                span_name: span_name.to_string(),
703                expected: format!("{:?}", expected_kind),
704                actual: None,
705                message: format!(
706                    "Span '{}' kind mismatch: expected {:?}, found none",
707                    span_name, expected_kind
708                ),
709            })),
710        }
711    }
712
713    /// Validate attrs.all - ALL attributes must match exactly
714    fn validate_attrs_all(
715        &self,
716        span: &SpanData,
717        all_attrs: &HashMap<String, String>,
718        span_name: &str,
719    ) -> Option<FailureDetails> {
720        let mut missing = Vec::new();
721
722        for (key, expected_value) in all_attrs {
723            let matches = span
724                .attributes
725                .get(key)
726                .and_then(|v| v.as_str())
727                .map(|v| v == expected_value)
728                .unwrap_or(false);
729
730            if !matches {
731                let actual = span
732                    .attributes
733                    .get(key)
734                    .and_then(|v| v.as_str())
735                    .map(|s| s.to_string());
736
737                if actual.is_none() {
738                    missing.push(format!("{}={}", key, expected_value));
739                } else {
740                    missing.push(format!(
741                        "{}={} (found: {})",
742                        key,
743                        expected_value,
744                        actual.unwrap_or_default()
745                    ));
746                }
747            }
748        }
749
750        if missing.is_empty() {
751            None
752        } else {
753            Some(FailureDetails {
754                rule: format!("expect.span[{}].attrs.all", span_name),
755                span_name: span_name.to_string(),
756                expected: format!("All attributes: {:?}", all_attrs),
757                actual: Some(format!("Missing/incorrect: [{}]", missing.join(", "))),
758                message: format!(
759                    "Span '{}' missing required attributes: [{}]",
760                    span_name,
761                    missing.join(", ")
762                ),
763            })
764        }
765    }
766
767    /// Validate attrs.any - At least ONE attribute must be present
768    fn validate_attrs_any(
769        &self,
770        span: &SpanData,
771        any_attrs: &HashMap<String, String>,
772        span_name: &str,
773    ) -> Option<FailureDetails> {
774        let has_any = any_attrs.iter().any(|(key, expected_value)| {
775            span.attributes
776                .get(key)
777                .and_then(|v| v.as_str())
778                .map(|v| v == expected_value)
779                .unwrap_or(false)
780        });
781
782        if has_any {
783            None
784        } else {
785            let patterns: Vec<String> = any_attrs
786                .iter()
787                .map(|(k, v)| format!("{}={}", k, v))
788                .collect();
789
790            Some(FailureDetails {
791                rule: format!("expect.span[{}].attrs.any", span_name),
792                span_name: span_name.to_string(),
793                expected: format!("Any of: [{}]", patterns.join(", ")),
794                actual: None,
795                message: format!(
796                    "Span '{}' missing any of required attributes: [{}]",
797                    span_name,
798                    patterns.join(", ")
799                ),
800            })
801        }
802    }
803
804    /// Validate events.any - At least ONE event must be present
805    fn validate_events_any(
806        &self,
807        span: &SpanData,
808        any_events: &[String],
809        span_name: &str,
810    ) -> Option<FailureDetails> {
811        if let Some(ref span_events) = span.events {
812            let has_any = any_events.iter().any(|event| span_events.contains(event));
813
814            if has_any {
815                return None;
816            }
817        }
818
819        Some(FailureDetails {
820            rule: format!("expect.span[{}].events.any", span_name),
821            span_name: span_name.to_string(),
822            expected: format!("Any of: [{}]", any_events.join(", ")),
823            actual: span.events.as_ref().map(|events| format!("{:?}", events)),
824            message: format!(
825                "Span '{}' missing required events: [{}]",
826                span_name,
827                any_events.join(", ")
828            ),
829        })
830    }
831
832    /// Validate events.all - ALL events must be present
833    fn validate_events_all(
834        &self,
835        span: &SpanData,
836        all_events: &[String],
837        span_name: &str,
838    ) -> Option<FailureDetails> {
839        if let Some(ref span_events) = span.events {
840            let missing: Vec<&String> = all_events
841                .iter()
842                .filter(|event| !span_events.contains(event))
843                .collect();
844
845            if missing.is_empty() {
846                return None;
847            }
848
849            return Some(FailureDetails {
850                rule: format!("expect.span[{}].events.all", span_name),
851                span_name: span_name.to_string(),
852                expected: format!("All of: [{}]", all_events.join(", ")),
853                actual: Some(format!("Missing: {:?}", missing)),
854                message: format!(
855                    "Span '{}' missing required events: {:?}",
856                    span_name, missing
857                ),
858            });
859        }
860
861        Some(FailureDetails {
862            rule: format!("expect.span[{}].events.all", span_name),
863            span_name: span_name.to_string(),
864            expected: format!("All of: [{}]", all_events.join(", ")),
865            actual: None,
866            message: format!("Span '{}' has no events", span_name),
867        })
868    }
869
870    /// Validate duration constraints
871    fn validate_duration(
872        &self,
873        span: &SpanData,
874        duration_config: &crate::config::DurationBoundConfig,
875        span_name: &str,
876    ) -> Option<FailureDetails> {
877        let duration_ms = span.duration_ms()?;
878
879        // Check minimum duration
880        if let Some(min) = duration_config.min {
881            if duration_ms < min {
882                return Some(FailureDetails {
883                    rule: format!("expect.span[{}].duration_ms.min", span_name),
884                    span_name: span_name.to_string(),
885                    expected: format!("duration >= {}ms", min),
886                    actual: Some(format!("{}ms", duration_ms)),
887                    message: format!(
888                        "Span '{}' duration {}ms < min {}ms",
889                        span_name, duration_ms, min
890                    ),
891                });
892            }
893        }
894
895        // Check maximum duration
896        if let Some(max) = duration_config.max {
897            if duration_ms > max {
898                return Some(FailureDetails {
899                    rule: format!("expect.span[{}].duration_ms.max", span_name),
900                    span_name: span_name.to_string(),
901                    expected: format!("duration <= {}ms", max),
902                    actual: Some(format!("{}ms", duration_ms)),
903                    message: format!(
904                        "Span '{}' duration {}ms > max {}ms",
905                        span_name, duration_ms, max
906                    ),
907                });
908            }
909        }
910
911        None
912    }
913
914    /// Get the first failure from validation results (for error reporting)
915    pub fn first_failure(result: &ValidationResult) -> Option<&FailureDetails> {
916        result.failures.first()
917    }
918
919    /// Validate a single assertion
920    pub fn validate_assertion(&self, assertion: &SpanAssertion) -> Result<()> {
921        match assertion {
922            SpanAssertion::SpanExists { name } => {
923                if !self.has_span(name) {
924                    return Err(CleanroomError::validation_error(format!(
925                        "Span assertion failed: span '{}' does not exist",
926                        name
927                    )));
928                }
929                Ok(())
930            }
931            SpanAssertion::SpanCount { name, count } => {
932                let actual_count = self.count_spans(name);
933                if actual_count != *count {
934                    return Err(CleanroomError::validation_error(format!(
935                        "Span count assertion failed: expected {} spans named '{}', found {}",
936                        count, name, actual_count
937                    )));
938                }
939                Ok(())
940            }
941            SpanAssertion::SpanAttribute {
942                name,
943                attribute_key,
944                attribute_value,
945            } => {
946                let spans = self.find_spans_by_name(name);
947                if spans.is_empty() {
948                    return Err(CleanroomError::validation_error(format!(
949                        "Span attribute assertion failed: span '{}' does not exist",
950                        name
951                    )));
952                }
953
954                // Check if any span has the expected attribute
955                let has_attribute = spans.iter().any(|span| {
956                    // SAFE: unwrap_or with safe default (false) - missing attribute means no match
957                    span.attributes
958                        .get(attribute_key)
959                        .and_then(|v| v.as_str())
960                        .map(|v| v == attribute_value)
961                        .unwrap_or(false)
962                });
963
964                if !has_attribute {
965                    return Err(CleanroomError::validation_error(format!(
966                        "Span attribute assertion failed: no span '{}' has attribute '{}' = '{}'",
967                        name, attribute_key, attribute_value
968                    )));
969                }
970                Ok(())
971            }
972            SpanAssertion::SpanHierarchy { parent, child } => {
973                let parent_spans = self.find_spans_by_name(parent);
974                let child_spans = self.find_spans_by_name(child);
975
976                if parent_spans.is_empty() {
977                    return Err(CleanroomError::validation_error(format!(
978                        "Span hierarchy assertion failed: parent span '{}' does not exist",
979                        parent
980                    )));
981                }
982
983                if child_spans.is_empty() {
984                    return Err(CleanroomError::validation_error(format!(
985                        "Span hierarchy assertion failed: child span '{}' does not exist",
986                        child
987                    )));
988                }
989
990                // Check if any child span has any of the parent spans as its parent
991                let has_hierarchy = child_spans.iter().any(|child_span| {
992                    if let Some(parent_id) = &child_span.parent_span_id {
993                        parent_spans.iter().any(|p| &p.span_id == parent_id)
994                    } else {
995                        false
996                    }
997                });
998
999                if !has_hierarchy {
1000                    return Err(CleanroomError::validation_error(format!(
1001                        "Span hierarchy assertion failed: no '{}' span is a child of '{}' span",
1002                        child, parent
1003                    )));
1004                }
1005                Ok(())
1006            }
1007
1008            // NEW PRD-aligned assertion implementations
1009            SpanAssertion::SpanKind { name, kind } => {
1010                let spans = self.find_spans_by_name(name);
1011                if spans.is_empty() {
1012                    return Err(CleanroomError::validation_error(format!(
1013                        "Span kind assertion failed: span '{}' does not exist",
1014                        name
1015                    )));
1016                }
1017
1018                // Check if any span has the expected kind
1019                // SAFE: unwrap_or with safe default (false) - missing kind means no match
1020                let has_kind = spans
1021                    .iter()
1022                    .any(|span| span.kind.map(|k| k == *kind).unwrap_or(false));
1023
1024                if !has_kind {
1025                    return Err(CleanroomError::validation_error(format!(
1026                        "Span kind assertion failed: no span '{}' has kind '{:?}'",
1027                        name, kind
1028                    )));
1029                }
1030                Ok(())
1031            }
1032
1033            SpanAssertion::SpanAllAttributes { name, attributes } => {
1034                let spans = self.find_spans_by_name(name);
1035                if spans.is_empty() {
1036                    return Err(CleanroomError::validation_error(format!(
1037                        "Span all attributes assertion failed: span '{}' does not exist",
1038                        name
1039                    )));
1040                }
1041
1042                // Check if any span has ALL the expected attributes
1043                let has_all_attributes = spans.iter().any(|span| {
1044                    attributes.iter().all(|(key, expected_value)| {
1045                        // SAFE: unwrap_or with safe default (false) - missing attribute means no match
1046                        span.attributes
1047                            .get(key)
1048                            .and_then(|v| v.as_str())
1049                            .map(|v| v == expected_value)
1050                            .unwrap_or(false)
1051                    })
1052                });
1053
1054                if !has_all_attributes {
1055                    let missing: Vec<String> = attributes
1056                        .iter()
1057                        .filter(|(key, expected_value)| {
1058                            !spans.iter().any(|span| {
1059                                // SAFE: unwrap_or with safe default (false) - missing attribute means no match
1060                                span.attributes
1061                                    .get(*key)
1062                                    .and_then(|v| v.as_str())
1063                                    .map(|v| v == *expected_value)
1064                                    .unwrap_or(false)
1065                            })
1066                        })
1067                        .map(|(k, v)| format!("{}={}", k, v))
1068                        .collect();
1069
1070                    return Err(CleanroomError::validation_error(format!(
1071                        "Span all attributes assertion failed: span '{}' is missing attributes: [{}]",
1072                        name,
1073                        missing.join(", ")
1074                    )));
1075                }
1076                Ok(())
1077            }
1078
1079            SpanAssertion::SpanAnyAttributes {
1080                name,
1081                attribute_patterns,
1082            } => {
1083                let spans = self.find_spans_by_name(name);
1084                if spans.is_empty() {
1085                    return Err(CleanroomError::validation_error(format!(
1086                        "Span any attributes assertion failed: span '{}' does not exist",
1087                        name
1088                    )));
1089                }
1090
1091                // Parse patterns and check if ANY pattern matches
1092                let has_any_match = spans.iter().any(|span| {
1093                    attribute_patterns.iter().any(|pattern| {
1094                        if let Some((key, value)) = pattern.split_once('=') {
1095                            // SAFE: unwrap_or with safe default (false) - missing attribute means no match
1096                            span.attributes
1097                                .get(key)
1098                                .and_then(|v| v.as_str())
1099                                .map(|v| v == value)
1100                                .unwrap_or(false)
1101                        } else {
1102                            false
1103                        }
1104                    })
1105                });
1106
1107                if !has_any_match {
1108                    return Err(CleanroomError::validation_error(format!(
1109                        "Span any attributes assertion failed: span '{}' does not have any of the patterns: [{}]",
1110                        name,
1111                        attribute_patterns.join(", ")
1112                    )));
1113                }
1114                Ok(())
1115            }
1116
1117            SpanAssertion::SpanEvents { name, events } => {
1118                let spans = self.find_spans_by_name(name);
1119                if spans.is_empty() {
1120                    return Err(CleanroomError::validation_error(format!(
1121                        "Span events assertion failed: span '{}' does not exist",
1122                        name
1123                    )));
1124                }
1125
1126                // Check if any span has at least one of the expected events
1127                let has_any_event = spans.iter().any(|span| {
1128                    if let Some(span_events) = &span.events {
1129                        events.iter().any(|event| span_events.contains(event))
1130                    } else {
1131                        false
1132                    }
1133                });
1134
1135                if !has_any_event {
1136                    return Err(CleanroomError::validation_error(format!(
1137                        "Span events assertion failed: span '{}' does not have any of the events: [{}]",
1138                        name,
1139                        events.join(", ")
1140                    )));
1141                }
1142                Ok(())
1143            }
1144
1145            SpanAssertion::SpanDuration {
1146                name,
1147                min_ms,
1148                max_ms,
1149            } => {
1150                let spans = self.find_spans_by_name(name);
1151                if spans.is_empty() {
1152                    return Err(CleanroomError::validation_error(format!(
1153                        "Span duration assertion failed: span '{}' does not exist",
1154                        name
1155                    )));
1156                }
1157
1158                // Check if any span has duration within bounds
1159                let has_valid_duration = spans.iter().any(|span| {
1160                    if let Some(duration) = span.duration_ms() {
1161                        let duration_u64 = duration as u64;
1162
1163                        let min_ok = min_ms.map(|min| duration_u64 >= min).unwrap_or(true);
1164                        let max_ok = max_ms.map(|max| duration_u64 <= max).unwrap_or(true);
1165
1166                        min_ok && max_ok
1167                    } else {
1168                        false
1169                    }
1170                });
1171
1172                if !has_valid_duration {
1173                    let bounds = match (min_ms, max_ms) {
1174                        (Some(min), Some(max)) => format!("between {}ms and {}ms", min, max),
1175                        (Some(min), None) => format!("at least {}ms", min),
1176                        (None, Some(max)) => format!("at most {}ms", max),
1177                        (None, None) => "any duration".to_string(),
1178                    };
1179
1180                    return Err(CleanroomError::validation_error(format!(
1181                        "Span duration assertion failed: span '{}' does not have duration {}",
1182                        name, bounds
1183                    )));
1184                }
1185                Ok(())
1186            }
1187        }
1188    }
1189
1190    /// Validate multiple assertions
1191    pub fn validate_assertions(&self, assertions: &[SpanAssertion]) -> Result<()> {
1192        for assertion in assertions {
1193            self.validate_assertion(assertion)?;
1194        }
1195        Ok(())
1196    }
1197
1198    /// Get a span by name (returns first match)
1199    pub fn get_span(&self, name: &str) -> Option<&SpanData> {
1200        self.spans.iter().find(|s| s.name == name)
1201    }
1202
1203    /// Get a span by span_id
1204    pub fn get_span_by_id(&self, span_id: &str) -> Option<&SpanData> {
1205        self.spans.iter().find(|s| s.span_id == span_id)
1206    }
1207
1208    /// Get all spans (for iteration)
1209    pub fn all_spans(&self) -> &[SpanData] {
1210        &self.spans
1211    }
1212}