Skip to main content

lean_ctx/core/context_kernel/
receipt_chain.rs

1//! Evidence chain for context delivery lifecycles.
2
3use std::sync::{Mutex, MutexGuard, OnceLock};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use super::accounting_fix::PostDeliveryAccounting;
7use super::token_envelope::TokenEnvelope;
8use super::types::ReceiptOutcome;
9
10/// A single entry in the receipt chain — one context delivery lifecycle.
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct ChainEntry {
13    /// Unique chain ID (monotonic counter).
14    pub chain_id: u64,
15    /// Tool or proxy path that initiated the request.
16    pub source: String,
17    /// Token envelope for the request.
18    pub envelope: TokenEnvelope,
19    /// Honest accounting for this delivery.
20    pub accounting: PostDeliveryAccounting,
21    /// Final outcome.
22    pub outcome: ReceiptOutcome,
23    /// Whether context was supplemented by kernel.
24    pub kernel_supplemented: bool,
25    /// Kernel budget used (tokens).
26    pub kernel_budget_used: usize,
27    /// Epoch seconds when recorded.
28    pub recorded_at: u64,
29}
30
31#[derive(Default)]
32struct ReceiptChain {
33    entries: Vec<ChainEntry>,
34    next_id: u64,
35}
36
37static CHAIN: OnceLock<Mutex<ReceiptChain>> = OnceLock::new();
38
39/// Aggregate statistics for the receipt chain.
40#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
41pub struct ChainSummary {
42    /// Number of recorded delivery lifecycles.
43    pub total_entries: usize,
44    /// Number of accepted deliveries.
45    pub accepted: usize,
46    /// Number of rejected deliveries.
47    pub rejected: usize,
48    /// Number of kernel-supplemented deliveries.
49    pub kernel_supplemented: usize,
50    /// Average kernel budget among supplemented deliveries.
51    pub avg_kernel_budget: f64,
52    /// Total tokens delivered across the chain.
53    pub total_tokens_delivered: usize,
54    /// Sum of phantom-savings percentages across the chain.
55    pub total_phantom_savings_pct: f64,
56}
57
58fn chain() -> MutexGuard<'static, ReceiptChain> {
59    let mutex = CHAIN.get_or_init(|| Mutex::new(ReceiptChain::default()));
60    match mutex.lock() {
61        Ok(guard) => guard,
62        Err(poisoned) => poisoned.into_inner(),
63    }
64}
65
66/// Records a context delivery lifecycle and returns its monotonic chain ID.
67pub fn record_chain_entry(
68    source: &str,
69    envelope: TokenEnvelope,
70    accounting: PostDeliveryAccounting,
71    outcome: ReceiptOutcome,
72    kernel_supplemented: bool,
73    kernel_budget_used: usize,
74) -> u64 {
75    let mut chain = chain();
76    let chain_id = chain.next_id;
77    chain.next_id = chain.next_id.saturating_add(1);
78    let recorded_at = SystemTime::now()
79        .duration_since(UNIX_EPOCH)
80        .map_or(0, |duration| duration.as_secs());
81    chain.entries.push(ChainEntry {
82        chain_id,
83        source: source.to_owned(),
84        envelope,
85        accounting,
86        outcome,
87        kernel_supplemented,
88        kernel_budget_used,
89        recorded_at,
90    });
91    chain_id
92}
93
94/// Returns the number of entries in the chain.
95pub fn chain_length() -> usize {
96    chain().entries.len()
97}
98
99/// Returns an owned snapshot of the full chain.
100pub fn chain_entries() -> Vec<ChainEntry> {
101    chain().entries.clone()
102}
103
104/// Returns aggregate statistics for all recorded entries.
105pub fn chain_summary() -> ChainSummary {
106    let chain = chain();
107    let mut summary = ChainSummary {
108        total_entries: chain.entries.len(),
109        ..ChainSummary::default()
110    };
111    for entry in &chain.entries {
112        match entry.outcome {
113            ReceiptOutcome::Accepted => summary.accepted += 1,
114            ReceiptOutcome::Rejected => summary.rejected += 1,
115            ReceiptOutcome::Partial | ReceiptOutcome::Unknown => {}
116        }
117        if entry.kernel_supplemented {
118            summary.kernel_supplemented += 1;
119            summary.avg_kernel_budget += entry.kernel_budget_used as f64;
120        }
121        summary.total_tokens_delivered = summary
122            .total_tokens_delivered
123            .saturating_add(entry.accounting.delivered_tokens);
124        summary.total_phantom_savings_pct += entry.accounting.phantom_savings_pct;
125    }
126    if summary.kernel_supplemented > 0 {
127        summary.avg_kernel_budget /= summary.kernel_supplemented as f64;
128    }
129    summary
130}
131
132/// Returns an owned snapshot containing only accepted entries.
133pub fn accepted_entries() -> Vec<ChainEntry> {
134    outcome_entries(ReceiptOutcome::Accepted)
135}
136
137/// Returns an owned snapshot containing only rejected entries.
138pub fn rejected_entries() -> Vec<ChainEntry> {
139    outcome_entries(ReceiptOutcome::Rejected)
140}
141
142fn outcome_entries(outcome: ReceiptOutcome) -> Vec<ChainEntry> {
143    chain()
144        .entries
145        .iter()
146        .filter(|entry| entry.outcome == outcome)
147        .cloned()
148        .collect()
149}
150
151/// Returns the fraction of entries supplemented by the kernel.
152pub fn kernel_hit_rate() -> f64 {
153    let summary = chain_summary();
154    if summary.total_entries == 0 {
155        0.0
156    } else {
157        summary.kernel_supplemented as f64 / summary.total_entries as f64
158    }
159}
160
161/// Clears all entries and resets the monotonic counter for testing.
162pub fn reset_chain() {
163    *chain() = ReceiptChain::default();
164}
165
166#[cfg(test)]
167mod tests {
168    use std::sync::{Mutex, MutexGuard};
169
170    use super::*;
171    use crate::core::context_kernel::accounting_fix::compute_honest_accounting;
172
173    static TEST_LOCK: Mutex<()> = Mutex::new(());
174
175    fn isolated() -> MutexGuard<'static, ()> {
176        let guard = match TEST_LOCK.lock() {
177            Ok(guard) => guard,
178            Err(poisoned) => poisoned.into_inner(),
179        };
180        reset_chain();
181        guard
182    }
183
184    fn record(outcome: ReceiptOutcome, supplemented: bool) -> u64 {
185        record_chain_entry(
186            "test",
187            TokenEnvelope::default(),
188            compute_honest_accounting(100, 50, 10, 5),
189            outcome,
190            supplemented,
191            if supplemented { 10 } else { 0 },
192        )
193    }
194
195    fn record_many(outcome: ReceiptOutcome, count: usize) {
196        for _ in 0..count {
197            record(outcome, false);
198        }
199    }
200
201    macro_rules! isolated_test {
202        ($name:ident, $body:block) => {
203            #[test]
204            fn $name() {
205                let _guard = isolated();
206                $body
207            }
208        };
209    }
210
211    isolated_test!(record_and_retrieve, {
212        record_many(ReceiptOutcome::Unknown, 3);
213        assert_eq!(chain_length(), 3);
214    });
215
216    isolated_test!(chain_ids_monotonic, {
217        assert_eq!(record(ReceiptOutcome::Unknown, false), 0);
218        assert_eq!(record(ReceiptOutcome::Unknown, false), 1);
219        assert_eq!(record(ReceiptOutcome::Unknown, false), 2);
220    });
221
222    isolated_test!(summary_counts, {
223        record_many(ReceiptOutcome::Accepted, 5);
224        record_many(ReceiptOutcome::Rejected, 2);
225        let summary = chain_summary();
226        assert_eq!(summary.accepted, 5);
227        assert_eq!(summary.rejected, 2);
228    });
229
230    isolated_test!(kernel_hit_rate_tracks_supplements, {
231        for supplemented in [true, true, true, false, false] {
232            record(ReceiptOutcome::Unknown, supplemented);
233        }
234        assert!((kernel_hit_rate() - 0.6).abs() < f64::EPSILON);
235    });
236
237    isolated_test!(accepted_filter, {
238        record(ReceiptOutcome::Accepted, false);
239        record(ReceiptOutcome::Rejected, false);
240        record(ReceiptOutcome::Accepted, false);
241        assert_eq!(accepted_entries().len(), 2);
242    });
243
244    isolated_test!(reset_clears, {
245        record(ReceiptOutcome::Accepted, true);
246        reset_chain();
247        assert_eq!(chain_length(), 0);
248        assert_eq!(record(ReceiptOutcome::Unknown, false), 0);
249    });
250}