Skip to main content

urge_runtime/
audit.rs

1//! Audit log — durable, append-only record of all governance decisions.
2//!
3//! Every `Verdict` can be logged here. The log is the external evidence that
4//! the governance system operated correctly. In healthcare: the log IS the
5//! compliance audit trail.
6
7use alloc::{string::String, vec::Vec};
8use urge_core::decision::Verdict;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// A single entry in the governance audit log.
14#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct AuditEntry {
17    /// Sequence number (monotonically increasing).
18    pub seq: u64,
19    /// Logical timestamp (ns since epoch or monotonic counter).
20    pub timestamp_ns: u64,
21    /// The expression that was evaluated.
22    pub expression: String,
23    /// Whether the verdict permitted or denied.
24    pub permitted: bool,
25    /// Confidence score [0, 255].
26    pub confidence: u8,
27    /// Number of paradigms evaluated.
28    pub paradigm_count: u8,
29    /// Number of inter-paradigm conflicts detected.
30    pub conflicts: u8,
31    /// Formal logic notation of the evaluated expression.
32    pub formal_notation: String,
33    /// The full logic trace serialized to JSON (if serde feature enabled).
34    #[cfg(feature = "serde")]
35    pub trace_json: Option<String>,
36    /// Optional correlation ID from external system (e.g., request ID, patient ID).
37    pub correlation_id: Option<String>,
38}
39
40/// Append-only governance audit log.
41pub struct AuditLog {
42    entries: Vec<AuditEntry>,
43    seq: u64,
44}
45
46impl AuditLog {
47    pub fn new() -> Self {
48        AuditLog {
49            entries: Vec::new(),
50            seq: 0,
51        }
52    }
53
54    /// Record a governance decision.
55    pub fn record(
56        &mut self,
57        expression: &str,
58        verdict: &Verdict,
59        timestamp_ns: u64,
60        correlation_id: Option<&str>,
61    ) -> u64 {
62        let seq = self.seq;
63        self.seq += 1;
64
65        self.entries.push(AuditEntry {
66            seq,
67            timestamp_ns,
68            expression: expression.into(),
69            permitted: verdict.valid,
70            confidence: verdict.confidence.0,
71            paradigm_count: urge_core::engine::Paradigm::ALL
72                .iter()
73                .filter(|&&p| verdict.paradigms_evaluated.contains(p))
74                .count() as u8,
75            conflicts: verdict.cross_validation.conflicts_detected,
76            formal_notation: verdict.formal_notation.clone(),
77            #[cfg(feature = "serde")]
78            trace_json: None, // Could serialize verdict.trace if desired.
79            correlation_id: correlation_id.map(Into::into),
80        });
81
82        seq
83    }
84
85    /// Returns all entries since `after_seq` (exclusive).
86    pub fn entries_since(&self, after_seq: u64) -> &[AuditEntry] {
87        let start = self
88            .entries
89            .iter()
90            .position(|e| e.seq > after_seq)
91            .unwrap_or(self.entries.len());
92        &self.entries[start..]
93    }
94
95    pub fn total_entries(&self) -> u64 {
96        self.seq
97    }
98    pub fn permitted_count(&self) -> usize {
99        self.entries.iter().filter(|e| e.permitted).count()
100    }
101    pub fn denied_count(&self) -> usize {
102        self.entries.iter().filter(|e| !e.permitted).count()
103    }
104
105    /// Export all entries as NDJSON (one JSON object per line).
106    #[cfg(feature = "serde")]
107    pub fn to_ndjson(&self) -> String {
108        self.entries
109            .iter()
110            .filter_map(|e| serde_json::to_string(e).ok())
111            .collect::<Vec<_>>()
112            .join("\n")
113    }
114}
115
116impl Default for AuditLog {
117    fn default() -> Self {
118        Self::new()
119    }
120}