crtx-core 0.1.1

Core IDs, errors, and schema constants for Cortex.
Documentation
//! Causal chains of events.
//!
//! A [`Trace`] is the canonical container for a sequence of related events
//! (e.g. all events in one agent invocation). The shape mirrors BUILD_SPEC §9.2.
//!
//! Lifecycle:
//!
//! - [`TraceStatus::Open`] — accepting new events; not yet sealed.
//! - [`TraceStatus::Closed`] — sealed; the chain integrity check has passed
//!   and `closed_at` is set. Closed traces are immutable.
//! - [`TraceStatus::Quarantined`] — closed but flagged: integrity check
//!   failed, an unresolved contradiction was attached, or an explicit
//!   operator action quarantined it. Reflection MUST skip quarantined
//!   traces unless explicitly asked.

use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::ids::{EventId, TraceId};

/// Lifecycle state of a [`Trace`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TraceStatus {
    /// Accepting new events; not yet sealed.
    Open,
    /// Sealed; integrity verified; immutable.
    Closed,
    /// Sealed but flagged (integrity failure, unresolved contradiction, or
    /// explicit operator action).
    Quarantined,
}

/// A causal chain of events.
///
/// Field order matches BUILD_SPEC §9.2 wire order; do not reorder without a
/// schema bump.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Trace {
    /// Stable identifier.
    pub id: TraceId,
    /// Schema version this row was written under.
    pub schema_version: u16,
    /// When the trace was opened.
    pub opened_at: DateTime<Utc>,
    /// When the trace was sealed (`Closed` or `Quarantined`); `None` while
    /// `Open`.
    pub closed_at: Option<DateTime<Utc>>,
    /// Ordered event IDs in this trace. Ordering is the canonical
    /// ledger ordering (event-hash chain), not wall-clock.
    pub event_ids: Vec<EventId>,
    /// Free-form trace type tag (e.g. `agent_run`, `manual_session`,
    /// `replay`). Conventions live with the producer.
    pub trace_type: String,
    /// Lifecycle status.
    pub status: TraceStatus,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::SCHEMA_VERSION;
    use chrono::TimeZone;

    fn fixture_trace() -> Trace {
        Trace {
            id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
            schema_version: SCHEMA_VERSION,
            opened_at: Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap(),
            closed_at: Some(Utc.with_ymd_and_hms(2026, 1, 1, 12, 5, 0).unwrap()),
            event_ids: vec![
                "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
                "evt_01ARZ3NDEKTSV4RRFFQ69G5FAW".parse().unwrap(),
            ],
            trace_type: "agent_run".into(),
            status: TraceStatus::Closed,
        }
    }

    #[test]
    fn trace_serde_round_trip() {
        let t = fixture_trace();
        let j = serde_json::to_value(&t).expect("serialize");
        let back: Trace = serde_json::from_value(j.clone()).expect("deserialize");
        assert_eq!(t, back);

        let obj = j.as_object().expect("trace serializes as object");
        for k in [
            "id",
            "schema_version",
            "opened_at",
            "closed_at",
            "event_ids",
            "trace_type",
            "status",
        ] {
            assert!(obj.contains_key(k), "trace JSON missing field `{k}`");
        }
        assert_eq!(obj["status"], serde_json::json!("closed"));
    }

    #[test]
    fn trace_status_wire_strings() {
        assert_eq!(
            serde_json::to_value(TraceStatus::Open).unwrap(),
            serde_json::json!("open")
        );
        assert_eq!(
            serde_json::to_value(TraceStatus::Closed).unwrap(),
            serde_json::json!("closed")
        );
        assert_eq!(
            serde_json::to_value(TraceStatus::Quarantined).unwrap(),
            serde_json::json!("quarantined")
        );
    }

    #[test]
    fn open_trace_round_trips_with_null_closed_at() {
        let mut t = fixture_trace();
        t.status = TraceStatus::Open;
        t.closed_at = None;
        let j = serde_json::to_value(&t).unwrap();
        assert_eq!(j["closed_at"], serde_json::Value::Null);
        let back: Trace = serde_json::from_value(j).unwrap();
        assert_eq!(t, back);
    }
}