Skip to main content

asupersync/trace/
format.rs

1//! Formatting utilities for trace output.
2//!
3//! Provides human-readable and machine-readable formatting for traces.
4
5use super::buffer::TraceBuffer;
6use super::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
7use super::event::TraceEvent;
8use serde::{Deserialize, Serialize};
9use std::io::{self, Write};
10
11/// Schema version for golden trace fixtures.
12pub const GOLDEN_TRACE_SCHEMA_VERSION: u32 = 1;
13
14/// Minimal configuration snapshot for golden trace fixtures.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct GoldenTraceConfig {
17    /// Deterministic seed used to run the workload.
18    pub seed: u64,
19    /// Entropy seed used for capability randomness.
20    pub entropy_seed: u64,
21    /// Virtual worker count.
22    pub worker_count: usize,
23    /// Trace buffer capacity.
24    pub trace_capacity: usize,
25    /// Maximum steps before termination (if set).
26    pub max_steps: Option<u64>,
27    /// Maximum number of Foata layers to keep in the canonical prefix.
28    pub canonical_prefix_layers: usize,
29    /// Maximum number of events to keep in the canonical prefix.
30    pub canonical_prefix_events: usize,
31}
32
33/// Summary of oracle results for a golden trace run.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GoldenTraceOracleSummary {
36    /// Sorted list of oracle violation tags (empty if all invariants held).
37    pub violations: Vec<String>,
38}
39
40/// Golden trace fixture for deterministic verification.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct GoldenTraceFixture {
43    /// Fixture schema version.
44    pub schema_version: u32,
45    /// Configuration snapshot.
46    pub config: GoldenTraceConfig,
47    /// Canonical trace fingerprint.
48    pub fingerprint: u64,
49    /// Number of events in the trace.
50    pub event_count: u64,
51    /// Canonicalized prefix (Foata layers of stable event keys).
52    pub canonical_prefix: Vec<Vec<TraceEventKey>>,
53    /// Oracle summary captured at end of the run.
54    pub oracle_summary: GoldenTraceOracleSummary,
55}
56
57impl GoldenTraceFixture {
58    /// Build a golden trace fixture from a trace event slice.
59    #[must_use]
60    pub fn from_events(
61        config: GoldenTraceConfig,
62        events: &[TraceEvent],
63        oracle_violations: impl IntoIterator<Item = impl Into<String>>,
64    ) -> Self {
65        let canonical_prefix = canonical_prefix(
66            events,
67            config.canonical_prefix_layers,
68            config.canonical_prefix_events,
69        );
70        let mut violations: Vec<String> = oracle_violations.into_iter().map(Into::into).collect();
71        violations.sort();
72        violations.dedup();
73
74        Self {
75            schema_version: GOLDEN_TRACE_SCHEMA_VERSION,
76            fingerprint: trace_fingerprint(events),
77            event_count: u64::try_from(events.len()).unwrap_or(u64::MAX),
78            canonical_prefix,
79            oracle_summary: GoldenTraceOracleSummary { violations },
80            config,
81        }
82    }
83
84    /// Compare two fixtures and return a diff if any field changed.
85    pub fn verify(&self, actual: &Self) -> Result<(), GoldenTraceDiff> {
86        GoldenTraceDiff::from_fixtures(self, actual).into_result()
87    }
88
89    /// Build a structured replay-delta report against another fixture.
90    #[must_use]
91    pub fn delta_report(&self, actual: &Self) -> GoldenTraceDeltaReport {
92        GoldenTraceDiff::from_fixtures(self, actual).to_delta_report(self, actual)
93    }
94}
95
96/// Category of replay-delta mismatch.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum GoldenTraceDeltaClass {
100    /// Runtime configuration or schema changed.
101    Config,
102    /// Timing envelope changed while replay semantics may still match.
103    Timing,
104    /// Core semantic behavior changed (event stream/fingerprint/prefix).
105    Semantic,
106    /// Diagnostic or oracle-surface behavior changed.
107    Observability,
108}
109
110/// Severity for replay-delta mismatch.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum GoldenTraceDeltaSeverity {
114    /// Informational drift that does not typically fail a gate.
115    Info,
116    /// Drift that should trigger review and potential gate escalation.
117    Warning,
118    /// Drift that should fail verification unless explicitly approved.
119    Error,
120}
121
122/// Single replay-delta mismatch.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct GoldenTraceDelta {
125    /// Logical drift class for this mismatch.
126    pub class: GoldenTraceDeltaClass,
127    /// Severity assigned to this mismatch.
128    pub severity: GoldenTraceDeltaSeverity,
129    /// Stable field identifier for machine parsing.
130    pub field: String,
131    /// Human-readable mismatch summary.
132    pub message: String,
133}
134
135/// Structured replay-delta report between two fixtures.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[allow(clippy::struct_excessive_bools)]
138pub struct GoldenTraceDeltaReport {
139    /// Fingerprint recorded in the baseline fixture.
140    pub expected_fingerprint: u64,
141    /// Fingerprint recorded in the candidate fixture.
142    pub actual_fingerprint: u64,
143    /// Event count from the baseline fixture.
144    pub expected_event_count: u64,
145    /// Event count from the candidate fixture.
146    pub actual_event_count: u64,
147    /// True when schema or configuration changed.
148    pub config_drift: bool,
149    /// True when core semantic behavior changed.
150    pub semantic_drift: bool,
151    /// True when replay timing envelope changed.
152    pub timing_drift: bool,
153    /// True when oracle/diagnostic surface changed.
154    pub observability_drift: bool,
155    /// Detailed mismatch entries.
156    pub deltas: Vec<GoldenTraceDelta>,
157}
158
159impl GoldenTraceDeltaReport {
160    /// Returns true when no drift is present.
161    #[must_use]
162    pub fn is_clean(&self) -> bool {
163        self.deltas.is_empty()
164    }
165
166    /// Serialize the report to pretty JSON for CI artifacts.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if serialization fails.
171    pub fn to_json(&self) -> Result<String, serde_json::Error> {
172        serde_json::to_string_pretty(self)
173    }
174}
175
176/// Diff between two golden trace fixtures.
177#[derive(Debug, Default)]
178pub struct GoldenTraceDiff {
179    mismatches: Vec<GoldenTraceMismatch>,
180}
181
182impl GoldenTraceDiff {
183    /// Returns true if no mismatches were recorded.
184    #[must_use]
185    pub fn is_empty(&self) -> bool {
186        self.mismatches.is_empty()
187    }
188
189    fn push(&mut self, mismatch: GoldenTraceMismatch) {
190        self.mismatches.push(mismatch);
191    }
192
193    fn from_fixtures(expected: &GoldenTraceFixture, actual: &GoldenTraceFixture) -> Self {
194        let mut diff = Self::default();
195        if expected.schema_version != actual.schema_version {
196            diff.push(GoldenTraceMismatch::SchemaVersion {
197                expected: expected.schema_version,
198                actual: actual.schema_version,
199            });
200        }
201        if expected.config != actual.config {
202            diff.push(GoldenTraceMismatch::Config {
203                expected: expected.config.clone(),
204                actual: actual.config.clone(),
205            });
206        }
207        if expected.fingerprint != actual.fingerprint {
208            diff.push(GoldenTraceMismatch::Fingerprint {
209                expected: expected.fingerprint,
210                actual: actual.fingerprint,
211            });
212        }
213        if expected.event_count != actual.event_count {
214            diff.push(GoldenTraceMismatch::EventCount {
215                expected: expected.event_count,
216                actual: actual.event_count,
217            });
218        }
219        if expected.canonical_prefix != actual.canonical_prefix {
220            diff.push(GoldenTraceMismatch::CanonicalPrefix {
221                expected_layers: expected.canonical_prefix.len(),
222                actual_layers: actual.canonical_prefix.len(),
223                first_mismatch: first_prefix_mismatch(
224                    &expected.canonical_prefix,
225                    &actual.canonical_prefix,
226                ),
227            });
228        }
229        if expected.oracle_summary != actual.oracle_summary {
230            diff.push(GoldenTraceMismatch::OracleViolations {
231                expected: expected.oracle_summary.violations.clone(),
232                actual: actual.oracle_summary.violations.clone(),
233            });
234        }
235        diff
236    }
237
238    fn into_result(self) -> Result<(), Self> {
239        if self.is_empty() { Ok(()) } else { Err(self) }
240    }
241
242    /// Convert mismatch data to a structured replay-delta report.
243    #[must_use]
244    pub fn to_delta_report(
245        &self,
246        expected: &GoldenTraceFixture,
247        actual: &GoldenTraceFixture,
248    ) -> GoldenTraceDeltaReport {
249        let mut config_drift = false;
250        let mut semantic_drift = false;
251        let mut timing_drift = false;
252        let mut observability_drift = false;
253        let mut deltas = Vec::with_capacity(self.mismatches.len());
254
255        for mismatch in &self.mismatches {
256            let (class, severity, field) = classify_delta(mismatch);
257            match class {
258                GoldenTraceDeltaClass::Config => config_drift = true,
259                GoldenTraceDeltaClass::Timing => timing_drift = true,
260                GoldenTraceDeltaClass::Semantic => semantic_drift = true,
261                GoldenTraceDeltaClass::Observability => observability_drift = true,
262            }
263            deltas.push(GoldenTraceDelta {
264                class,
265                severity,
266                field: field.to_string(),
267                message: mismatch.to_string(),
268            });
269        }
270
271        GoldenTraceDeltaReport {
272            expected_fingerprint: expected.fingerprint,
273            actual_fingerprint: actual.fingerprint,
274            expected_event_count: expected.event_count,
275            actual_event_count: actual.event_count,
276            config_drift,
277            semantic_drift,
278            timing_drift,
279            observability_drift,
280            deltas,
281        }
282    }
283}
284
285fn classify_delta(
286    mismatch: &GoldenTraceMismatch,
287) -> (
288    GoldenTraceDeltaClass,
289    GoldenTraceDeltaSeverity,
290    &'static str,
291) {
292    match mismatch {
293        GoldenTraceMismatch::SchemaVersion { .. } => (
294            GoldenTraceDeltaClass::Config,
295            GoldenTraceDeltaSeverity::Error,
296            "schema_version",
297        ),
298        GoldenTraceMismatch::Config { .. } => (
299            GoldenTraceDeltaClass::Config,
300            GoldenTraceDeltaSeverity::Error,
301            "config",
302        ),
303        GoldenTraceMismatch::Fingerprint { .. } => (
304            GoldenTraceDeltaClass::Semantic,
305            GoldenTraceDeltaSeverity::Error,
306            "fingerprint",
307        ),
308        GoldenTraceMismatch::EventCount { .. } => (
309            GoldenTraceDeltaClass::Timing,
310            GoldenTraceDeltaSeverity::Warning,
311            "event_count",
312        ),
313        GoldenTraceMismatch::CanonicalPrefix { .. } => (
314            GoldenTraceDeltaClass::Semantic,
315            GoldenTraceDeltaSeverity::Error,
316            "canonical_prefix",
317        ),
318        GoldenTraceMismatch::OracleViolations { .. } => (
319            GoldenTraceDeltaClass::Observability,
320            GoldenTraceDeltaSeverity::Warning,
321            "oracle_violations",
322        ),
323    }
324}
325
326impl std::fmt::Display for GoldenTraceDiff {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        for mismatch in &self.mismatches {
329            writeln!(f, "{mismatch}")?;
330        }
331        Ok(())
332    }
333}
334
335impl std::error::Error for GoldenTraceDiff {}
336
337#[derive(Debug)]
338enum GoldenTraceMismatch {
339    SchemaVersion {
340        expected: u32,
341        actual: u32,
342    },
343    Config {
344        expected: GoldenTraceConfig,
345        actual: GoldenTraceConfig,
346    },
347    Fingerprint {
348        expected: u64,
349        actual: u64,
350    },
351    EventCount {
352        expected: u64,
353        actual: u64,
354    },
355    CanonicalPrefix {
356        expected_layers: usize,
357        actual_layers: usize,
358        first_mismatch: Option<(usize, usize)>,
359    },
360    OracleViolations {
361        expected: Vec<String>,
362        actual: Vec<String>,
363    },
364}
365
366impl std::fmt::Display for GoldenTraceMismatch {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        match self {
369            Self::SchemaVersion { expected, actual } => {
370                write!(
371                    f,
372                    "schema_version changed (expected {expected}, actual {actual})"
373                )
374            }
375            Self::Config { expected, actual } => {
376                write!(
377                    f,
378                    "config changed (expected {expected:?}, actual {actual:?})"
379                )
380            }
381            Self::Fingerprint { expected, actual } => {
382                write!(
383                    f,
384                    "fingerprint changed (expected 0x{expected:016X}, actual 0x{actual:016X})"
385                )
386            }
387            Self::EventCount { expected, actual } => write!(
388                f,
389                "event_count changed (expected {expected}, actual {actual})"
390            ),
391            Self::CanonicalPrefix {
392                expected_layers,
393                actual_layers,
394                first_mismatch,
395            } => {
396                if let Some((layer, index)) = first_mismatch {
397                    write!(
398                        f,
399                        "canonical_prefix mismatch (layer {layer}, index {index}; expected_layers={expected_layers}, actual_layers={actual_layers})"
400                    )
401                } else {
402                    write!(
403                        f,
404                        "canonical_prefix mismatch (expected_layers={expected_layers}, actual_layers={actual_layers})"
405                    )
406                }
407            }
408            Self::OracleViolations { expected, actual } => {
409                write!(
410                    f,
411                    "oracle violations changed (expected {expected:?}, actual {actual:?})"
412                )
413            }
414        }
415    }
416}
417
418fn canonical_prefix(
419    events: &[TraceEvent],
420    max_layers: usize,
421    max_events: usize,
422) -> Vec<Vec<TraceEventKey>> {
423    let foata = canonicalize(events);
424    let mut remaining = max_events;
425    let mut prefix = Vec::new();
426
427    for layer in foata.layers().iter().take(max_layers) {
428        if remaining == 0 {
429            break;
430        }
431        let mut keys = Vec::new();
432        for event in layer {
433            if remaining == 0 {
434                break;
435            }
436            keys.push(trace_event_key(event));
437            remaining = remaining.saturating_sub(1);
438        }
439        if !keys.is_empty() {
440            prefix.push(keys);
441        }
442    }
443
444    prefix
445}
446
447fn first_prefix_mismatch(
448    expected: &[Vec<TraceEventKey>],
449    actual: &[Vec<TraceEventKey>],
450) -> Option<(usize, usize)> {
451    let layers = expected.len().min(actual.len());
452    for layer_idx in 0..layers {
453        let expected_layer = &expected[layer_idx];
454        let actual_layer = &actual[layer_idx];
455        let events = expected_layer.len().min(actual_layer.len());
456        for event_idx in 0..events {
457            if expected_layer[event_idx] != actual_layer[event_idx] {
458                return Some((layer_idx, event_idx));
459            }
460        }
461        if expected_layer.len() != actual_layer.len() {
462            return Some((layer_idx, events));
463        }
464    }
465    if expected.len() != actual.len() {
466        return Some((layers, 0));
467    }
468    None
469}
470
471/// Formats a trace buffer as human-readable text.
472pub fn format_trace(buffer: &TraceBuffer, w: &mut impl Write) -> io::Result<()> {
473    writeln!(w, "=== Trace ({} events) ===", buffer.len())?;
474    for event in buffer.iter() {
475        writeln!(w, "{event}")?;
476    }
477    writeln!(w, "=== End Trace ===")?;
478    Ok(())
479}
480
481/// Formats a trace buffer as a string.
482#[must_use]
483pub fn trace_to_string(buffer: &TraceBuffer) -> String {
484    let mut s = Vec::new();
485    format_trace(buffer, &mut s).expect("writing to Vec should not fail");
486    String::from_utf8(s).expect("trace should be valid UTF-8")
487}
488
489#[cfg(test)]
490mod tests {
491    #![allow(
492        clippy::pedantic,
493        clippy::nursery,
494        clippy::expect_fun_call,
495        clippy::map_unwrap_or,
496        clippy::cast_possible_wrap,
497        clippy::future_not_send
498    )]
499    use super::*;
500    use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
501    use crate::types::Time;
502
503    #[test]
504    fn format_empty_trace() {
505        let buffer = TraceBuffer::new(10);
506        let output = trace_to_string(&buffer);
507        assert!(output.contains("0 events"));
508    }
509
510    #[test]
511    fn format_with_events() {
512        let mut buffer = TraceBuffer::new(10);
513        buffer.push(TraceEvent::new(
514            1,
515            Time::from_millis(100),
516            TraceEventKind::UserTrace,
517            TraceData::Message("test".to_string()),
518        ));
519        let output = trace_to_string(&buffer);
520        assert!(output.contains("1 events"));
521        assert!(output.contains("test"));
522    }
523
524    // Pure data-type tests (wave 14 – CyanBarn)
525
526    #[test]
527    fn golden_trace_config_debug_clone_eq() {
528        let cfg = GoldenTraceConfig {
529            seed: 42,
530            entropy_seed: 7,
531            worker_count: 4,
532            trace_capacity: 1000,
533            max_steps: Some(500),
534            canonical_prefix_layers: 10,
535            canonical_prefix_events: 100,
536        };
537        let dbg = format!("{cfg:?}");
538        assert!(dbg.contains("GoldenTraceConfig"));
539
540        let cloned = cfg.clone();
541        assert_eq!(cfg, cloned);
542    }
543
544    #[test]
545    fn golden_trace_config_ne() {
546        let a = GoldenTraceConfig {
547            seed: 1,
548            entropy_seed: 0,
549            worker_count: 1,
550            trace_capacity: 10,
551            max_steps: None,
552            canonical_prefix_layers: 1,
553            canonical_prefix_events: 1,
554        };
555        let mut b = a.clone();
556        b.seed = 2;
557        assert_ne!(a, b);
558    }
559
560    #[test]
561    fn golden_trace_oracle_summary_debug_clone_eq() {
562        let summary = GoldenTraceOracleSummary {
563            violations: vec!["leak".to_string()],
564        };
565        let dbg = format!("{summary:?}");
566        assert!(dbg.contains("GoldenTraceOracleSummary"));
567
568        let cloned = summary.clone();
569        assert_eq!(summary, cloned);
570    }
571
572    #[test]
573    fn golden_trace_oracle_summary_empty() {
574        let summary = GoldenTraceOracleSummary { violations: vec![] };
575        assert!(summary.violations.is_empty());
576    }
577
578    #[test]
579    fn golden_trace_diff_default_is_empty() {
580        let diff = GoldenTraceDiff::default();
581        assert!(diff.is_empty());
582    }
583
584    #[test]
585    fn golden_trace_diff_debug() {
586        let diff = GoldenTraceDiff::default();
587        let dbg = format!("{diff:?}");
588        assert!(dbg.contains("GoldenTraceDiff"));
589    }
590
591    #[test]
592    fn golden_trace_diff_display_empty() {
593        let diff = GoldenTraceDiff::default();
594        let display = diff.to_string();
595        assert!(display.is_empty());
596    }
597
598    #[test]
599    fn golden_trace_diff_error_trait() {
600        let diff = GoldenTraceDiff::default();
601        let err: &dyn std::error::Error = &diff;
602        assert!(err.source().is_none());
603    }
604
605    #[test]
606    fn golden_trace_mismatch_display_all_variants() {
607        let m = GoldenTraceMismatch::SchemaVersion {
608            expected: 1,
609            actual: 2,
610        };
611        assert!(m.to_string().contains("schema_version"));
612
613        let m = GoldenTraceMismatch::Fingerprint {
614            expected: 0xAB,
615            actual: 0xCD,
616        };
617        assert!(m.to_string().contains("fingerprint"));
618
619        let m = GoldenTraceMismatch::EventCount {
620            expected: 10,
621            actual: 20,
622        };
623        assert!(m.to_string().contains("event_count"));
624
625        let m = GoldenTraceMismatch::CanonicalPrefix {
626            expected_layers: 3,
627            actual_layers: 5,
628            first_mismatch: Some((1, 2)),
629        };
630        let s = m.to_string();
631        assert!(s.contains("canonical_prefix"));
632        assert!(s.contains("layer 1"));
633
634        let m = GoldenTraceMismatch::CanonicalPrefix {
635            expected_layers: 3,
636            actual_layers: 5,
637            first_mismatch: None,
638        };
639        assert!(m.to_string().contains("expected_layers=3"));
640
641        let m = GoldenTraceMismatch::OracleViolations {
642            expected: vec!["a".into()],
643            actual: vec!["b".into()],
644        };
645        assert!(m.to_string().contains("oracle violations"));
646    }
647
648    #[test]
649    fn golden_trace_mismatch_config_variant() {
650        let cfg1 = GoldenTraceConfig {
651            seed: 1,
652            entropy_seed: 0,
653            worker_count: 1,
654            trace_capacity: 10,
655            max_steps: None,
656            canonical_prefix_layers: 1,
657            canonical_prefix_events: 1,
658        };
659        let cfg2 = GoldenTraceConfig { seed: 2, ..cfg1 };
660        let m = GoldenTraceMismatch::Config {
661            expected: cfg1,
662            actual: cfg2,
663        };
664        assert!(m.to_string().contains("config changed"));
665    }
666
667    #[test]
668    fn golden_trace_mismatch_debug() {
669        let m = GoldenTraceMismatch::SchemaVersion {
670            expected: 1,
671            actual: 2,
672        };
673        let dbg = format!("{m:?}");
674        assert!(dbg.contains("SchemaVersion"));
675    }
676
677    #[test]
678    fn schema_version_constant() {
679        assert_eq!(GOLDEN_TRACE_SCHEMA_VERSION, 1);
680    }
681
682    #[test]
683    fn first_prefix_mismatch_identical() {
684        let a: Vec<Vec<TraceEventKey>> = vec![];
685        assert!(first_prefix_mismatch(&a, &a).is_none());
686    }
687
688    #[test]
689    fn first_prefix_mismatch_different_lengths() {
690        let a: Vec<Vec<TraceEventKey>> = vec![vec![]];
691        let b: Vec<Vec<TraceEventKey>> = vec![];
692        let m = first_prefix_mismatch(&a, &b);
693        assert!(m.is_some());
694    }
695
696    #[test]
697    fn golden_trace_delta_report_clean_when_equal() {
698        let config = GoldenTraceConfig {
699            seed: 1,
700            entropy_seed: 1,
701            worker_count: 1,
702            trace_capacity: 32,
703            max_steps: Some(128),
704            canonical_prefix_layers: 2,
705            canonical_prefix_events: 8,
706        };
707        let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
708        let report = expected.delta_report(&expected);
709        assert!(report.is_clean());
710        assert!(!report.config_drift);
711        assert!(!report.semantic_drift);
712        assert!(!report.timing_drift);
713        assert!(!report.observability_drift);
714        assert!(report.to_json().expect("json").contains("\"deltas\""));
715    }
716
717    #[test]
718    fn golden_trace_delta_report_detects_drift_classes() {
719        let config = GoldenTraceConfig {
720            seed: 1,
721            entropy_seed: 1,
722            worker_count: 1,
723            trace_capacity: 32,
724            max_steps: Some(128),
725            canonical_prefix_layers: 2,
726            canonical_prefix_events: 8,
727        };
728        let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
729        let mut actual = expected.clone();
730        actual.config.seed = 2;
731        actual.fingerprint ^= 0xA5A5;
732        actual.event_count = actual.event_count.saturating_add(1);
733        actual.oracle_summary.violations = vec!["TaskLeak".to_string()];
734
735        let report = expected.delta_report(&actual);
736        assert!(!report.is_clean());
737        assert!(report.config_drift);
738        assert!(report.semantic_drift);
739        assert!(report.timing_drift);
740        assert!(report.observability_drift);
741        assert!(report.deltas.iter().any(|d| d.field == "config"));
742        assert!(report.deltas.iter().any(|d| d.field == "fingerprint"));
743        assert!(report.deltas.iter().any(|d| d.field == "event_count"));
744        assert!(report.deltas.iter().any(|d| d.field == "oracle_violations"));
745    }
746}