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