Skip to main content

mempill_types/
ledger.rs

1//! LedgerEntry: append-only audit record for every state transition.
2//!
3//! The audit ledger is maintained by the AuditLedger component. Every `ingest_claim`
4//! call produces at least one ledger entry. Entries are queryable by `agent_id`,
5//! `claim_ref`, and transaction-time range via `query_audit`.
6
7use crate::disposition::Disposition;
8use crate::identity::{AgentId, ClaimRef};
9use crate::time::TransactionTime;
10
11/// Immutable audit record appended on every disposition event by the AuditLedger.
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct LedgerEntry {
14    /// Unique ID for this ledger entry (random UUID, minted at write time).
15    pub entry_id: uuid::Uuid,
16    /// The agent that owns the claim this entry is about.
17    pub agent_id: AgentId,
18    /// The claim this ledger entry is recording a transition for.
19    pub claim_ref: ClaimRef,
20    /// The event type that triggered this ledger entry.
21    pub event_kind: LedgerEventKind,
22    /// The disposition assigned to the claim at the time of this entry.
23    pub disposition: Disposition,
24    /// Rationale and measured estimators from the adjudication gate (for replay audit).
25    pub rationale: Option<serde_json::Value>,
26    /// Engine-stamped.
27    pub recorded_at: TransactionTime,
28}
29
30/// The event that triggered this ledger entry.
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32#[non_exhaustive]
33pub enum LedgerEventKind {
34    /// A new claim was committed to the store.
35    ClaimCommitted,
36    /// A validity assertion (start/end bound) was appended.
37    ValidityAsserted,
38    /// An adjudication was requested from the oracle.
39    AdjudicationRequested,
40    /// The oracle returned a verdict and it was applied.
41    AdjudicationResolved,
42    /// A claim was detected as a recall re-entry (the agent is re-injecting its own output).
43    RecallReEntryDetected,
44    /// A claim was quarantined (burst/loop signature or incoherent timestamps).
45    Quarantined,
46    /// A dependent claim was flagged PendingReview because its parent was superseded.
47    DependentFlaggedPendingReview,
48    /// Claim was served as a query result — recorded so the Amplification Guard can detect later recall re-entry.
49    ServedAsInjected,
50    /// Adjudication TTL elapsed — challenger reverted to Contested via the sweep or lazy expiry path.
51    AdjudicationExpired,
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::disposition::Disposition;
58    use crate::identity::AgentId;
59    use chrono::Utc;
60
61    fn make_entry(kind: LedgerEventKind) -> LedgerEntry {
62        LedgerEntry {
63            entry_id: uuid::Uuid::new_v4(),
64            agent_id: AgentId("agent-1".into()),
65            claim_ref: ClaimRef::new_random(),
66            event_kind: kind,
67            disposition: Disposition::CommittedCheap,
68            rationale: None,
69            recorded_at: TransactionTime(Utc::now()),
70        }
71    }
72
73    #[test]
74    fn served_as_injected_event_is_present() {
75        let e = make_entry(LedgerEventKind::ServedAsInjected);
76        assert_eq!(e.event_kind, LedgerEventKind::ServedAsInjected);
77    }
78
79    #[test]
80    fn dependent_flagged_pending_review_event_is_present() {
81        let e = make_entry(LedgerEventKind::DependentFlaggedPendingReview);
82        assert_eq!(e.event_kind, LedgerEventKind::DependentFlaggedPendingReview);
83    }
84
85    #[test]
86    fn all_event_kinds_round_trip_serde() {
87        let kinds = [
88            LedgerEventKind::ClaimCommitted,
89            LedgerEventKind::ValidityAsserted,
90            LedgerEventKind::AdjudicationRequested,
91            LedgerEventKind::AdjudicationResolved,
92            LedgerEventKind::RecallReEntryDetected,
93            LedgerEventKind::Quarantined,
94            LedgerEventKind::DependentFlaggedPendingReview,
95            LedgerEventKind::ServedAsInjected,
96            LedgerEventKind::AdjudicationExpired,
97        ];
98        for k in &kinds {
99            let json = serde_json::to_string(k).unwrap();
100            let back: LedgerEventKind = serde_json::from_str(&json).unwrap();
101            assert_eq!(k, &back);
102        }
103    }
104
105    #[test]
106    fn ledger_entry_round_trip_serde() {
107        let e = make_entry(LedgerEventKind::ClaimCommitted);
108        let json = serde_json::to_string(&e).unwrap();
109        let back: LedgerEntry = serde_json::from_str(&json).unwrap();
110        assert_eq!(e.entry_id, back.entry_id);
111        assert_eq!(e.event_kind, back.event_kind);
112        assert_eq!(e.disposition, back.disposition);
113    }
114}