Skip to main content

lean_ctx_protocol/
gap.rs

1use serde::{Deserialize, Serialize};
2
3/// Gap lifecycle — append-only audit events.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub enum GapReason {
6    BufferFull,
7    NetworkTimeout,
8    ProcessCrash,
9    SidecarUnreachable,
10    DiskFull,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct EvidenceGapOpenedV1 {
15    pub gap_id: String,
16    pub tenant_id: String,
17    pub source_instance_id: String,
18    pub first_missing_sequence: u64,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub last_missing_sequence: Option<u64>,
21    pub started_at: String,
22    pub detected_at: String,
23    pub reason: GapReason,
24    pub affected_event_types: Vec<String>,
25    pub runtime_version: String,
26    pub previous_evidence_hash: String,
27    pub signature: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct EvidenceGapClosedV1 {
32    pub gap_id: String,
33    pub last_missing_sequence: u64,
34    pub ended_at: String,
35    pub resolved_at: String,
36    pub total_missing_events: u64,
37    pub billing_period_status: BillingPeriodStatus,
38    pub signature: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum BillingPeriodStatus {
43    Complete,
44    Incomplete,
45    NonBillable,
46}
47
48#[cfg(test)]
49mod tests {
50    use crate::{BillingPeriodStatus, EvidenceGapClosedV1, EvidenceGapOpenedV1, GapReason};
51
52    #[test]
53    fn opened_serialization_round_trip() {
54        let opened = EvidenceGapOpenedV1 {
55            gap_id: "gap-1".to_owned(),
56            tenant_id: "tenant-1".to_owned(),
57            source_instance_id: "runtime-1".to_owned(),
58            first_missing_sequence: 7,
59            last_missing_sequence: None,
60            started_at: "2026-01-01T00:00:00Z".to_owned(),
61            detected_at: "2026-01-01T00:01:00Z".to_owned(),
62            reason: GapReason::NetworkTimeout,
63            affected_event_types: vec!["observation".to_owned()],
64            runtime_version: "1.0.0".to_owned(),
65            previous_evidence_hash: "sha256:abc".to_owned(),
66            signature: "signature".to_owned(),
67        };
68        let json = serde_json::to_string(&opened).expect("opened gap should serialize");
69        let decoded = serde_json::from_str(&json).expect("opened gap should deserialize");
70        assert_eq!(opened, decoded);
71    }
72
73    #[test]
74    fn closed_serialization_round_trip() {
75        let closed = EvidenceGapClosedV1 {
76            gap_id: "gap-1".to_owned(),
77            last_missing_sequence: 9,
78            ended_at: "2026-01-01T00:02:00Z".to_owned(),
79            resolved_at: "2026-01-01T00:03:00Z".to_owned(),
80            total_missing_events: 3,
81            billing_period_status: BillingPeriodStatus::Incomplete,
82            signature: "signature".to_owned(),
83        };
84        let json = serde_json::to_string(&closed).expect("closed gap should serialize");
85        let decoded = serde_json::from_str(&json).expect("closed gap should deserialize");
86        assert_eq!(closed, decoded);
87    }
88}