1use 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#[derive(Debug, Clone)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct AuditEntry {
19 pub seq: u64,
21 pub timestamp_ns: u64,
23 pub expression: String,
25 pub permitted: bool,
27 pub confidence: u8,
29 pub paradigm_count: u8,
31 pub conflicts: u8,
33 pub formal_notation: String,
35 #[cfg(feature = "serde")]
37 pub trace_json: Option<String>,
38 pub correlation_id: Option<String>,
40}
41
42pub 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 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, correlation_id: correlation_id.map(Into::into),
82 });
83
84 seq
85 }
86
87 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 #[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}