Skip to main content

dsfb_gray/
audit.rs

1//! Audit trace: deterministic, reproducible record of every classification decision.
2//!
3//! Every grammar transition, heuristic match, and envelope classification is
4//! recorded in an [`AuditTrace`] that can be replayed offline for verification.
5//! This is the DSFB equivalent of a flight data recorder — if the system
6//! says "Boundary due to ConsensusHeartbeatDegradation," the audit trace
7//! shows exactly which residual signs, at which timestamps, with which
8//! drift and slew values, produced that classification.
9
10/// A single audit event recording one observation-classification cycle.
11#[derive(Debug, Clone, Copy)]
12pub struct AuditEvent {
13    /// Monotonic timestamp (nanoseconds).
14    pub timestamp_ns: u64,
15    /// Raw residual value at this observation.
16    pub residual: f64,
17    /// Estimated drift at this observation.
18    pub drift: f64,
19    /// Estimated slew at this observation.
20    pub slew: f64,
21    /// Envelope position classification.
22    pub envelope_position: u8, // 0=Interior, 1=Boundary, 2=Exterior
23    /// Grammar state after this observation.
24    pub grammar_state: u8, // 0=Admissible, 1=Boundary, 2=Violation
25    /// Whether a grammar transition occurred at this step.
26    pub transition_occurred: bool,
27}
28
29/// Fixed-capacity audit trace buffer (stack-allocated, no_alloc compatible).
30///
31/// Stores the most recent `N` audit events in a ring buffer. When the
32/// buffer is full, the oldest events are overwritten. For full-history
33/// audit, use the `std` feature to write events to a file or stream.
34pub struct AuditTrace {
35    /// Ring buffer of audit events.
36    events: [AuditEvent; 256],
37    /// Write head position.
38    head: usize,
39    /// Total events recorded (may exceed buffer capacity).
40    total_count: u64,
41}
42
43impl AuditTrace {
44    /// Create a new empty audit trace.
45    pub fn new() -> Self {
46        Self {
47            events: [AuditEvent {
48                timestamp_ns: 0,
49                residual: 0.0,
50                drift: 0.0,
51                slew: 0.0,
52                envelope_position: 0,
53                grammar_state: 0,
54                transition_occurred: false,
55            }; 256],
56            head: 0,
57            total_count: 0,
58        }
59    }
60
61    /// Record an audit event.
62    pub fn record(&mut self, event: AuditEvent) {
63        self.events[self.head] = event;
64        self.head = (self.head + 1) % 256;
65        self.total_count += 1;
66    }
67
68    /// Total number of events recorded (may exceed buffer capacity).
69    pub fn total_count(&self) -> u64 {
70        self.total_count
71    }
72
73    /// Number of events currently in the buffer (max 256).
74    pub fn buffered_count(&self) -> usize {
75        if self.total_count < 256 {
76            self.total_count as usize
77        } else {
78            256
79        }
80    }
81
82    /// Iterate over buffered events in chronological order.
83    pub fn iter(&self) -> AuditTraceIter<'_> {
84        let count = self.buffered_count();
85        let start = if self.total_count < 256 { 0 } else { self.head };
86        AuditTraceIter {
87            trace: self,
88            pos: start,
89            remaining: count,
90        }
91    }
92
93    /// Get the most recent audit event, if any.
94    pub fn last(&self) -> Option<&AuditEvent> {
95        if self.total_count == 0 {
96            None
97        } else {
98            let idx = if self.head == 0 { 255 } else { self.head - 1 };
99            Some(&self.events[idx])
100        }
101    }
102
103    /// Reset the audit trace.
104    pub fn reset(&mut self) {
105        self.head = 0;
106        self.total_count = 0;
107    }
108}
109
110impl Default for AuditTrace {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116/// Iterator over audit trace events in chronological order.
117pub struct AuditTraceIter<'a> {
118    trace: &'a AuditTrace,
119    pos: usize,
120    remaining: usize,
121}
122
123impl<'a> Iterator for AuditTraceIter<'a> {
124    type Item = &'a AuditEvent;
125
126    fn next(&mut self) -> Option<Self::Item> {
127        if self.remaining == 0 {
128            return None;
129        }
130        let event = &self.trace.events[self.pos];
131        self.pos = (self.pos + 1) % 256;
132        self.remaining -= 1;
133        Some(event)
134    }
135
136    fn size_hint(&self) -> (usize, Option<usize>) {
137        (self.remaining, Some(self.remaining))
138    }
139}
140
141impl<'a> ExactSizeIterator for AuditTraceIter<'a> {}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_empty_trace() {
149        let trace = AuditTrace::new();
150        assert_eq!(trace.total_count(), 0);
151        assert_eq!(trace.buffered_count(), 0);
152        assert!(trace.last().is_none());
153    }
154
155    #[test]
156    fn test_record_and_retrieve() {
157        let mut trace = AuditTrace::new();
158        trace.record(AuditEvent {
159            timestamp_ns: 1000,
160            residual: 0.5,
161            drift: 0.01,
162            slew: 0.001,
163            envelope_position: 0,
164            grammar_state: 0,
165            transition_occurred: false,
166        });
167        assert_eq!(trace.total_count(), 1);
168        assert_eq!(trace.buffered_count(), 1);
169        let last = trace.last().unwrap();
170        assert_eq!(last.timestamp_ns, 1000);
171    }
172
173    #[test]
174    fn test_ring_buffer_wraps() {
175        let mut trace = AuditTrace::new();
176        for i in 0..300u64 {
177            trace.record(AuditEvent {
178                timestamp_ns: i,
179                residual: i as f64,
180                drift: 0.0,
181                slew: 0.0,
182                envelope_position: 0,
183                grammar_state: 0,
184                transition_occurred: false,
185            });
186        }
187        assert_eq!(trace.total_count(), 300);
188        assert_eq!(trace.buffered_count(), 256);
189        // Most recent event should be 299
190        assert_eq!(trace.last().unwrap().timestamp_ns, 299);
191    }
192
193    #[test]
194    fn test_iter_chronological() {
195        let mut trace = AuditTrace::new();
196        for i in 0..10u64 {
197            trace.record(AuditEvent {
198                timestamp_ns: i * 100,
199                residual: 0.0,
200                drift: 0.0,
201                slew: 0.0,
202                envelope_position: 0,
203                grammar_state: 0,
204                transition_occurred: false,
205            });
206        }
207        let timestamps: Vec<u64> = trace.iter().map(|e| e.timestamp_ns).collect();
208        assert_eq!(
209            timestamps,
210            vec![0, 100, 200, 300, 400, 500, 600, 700, 800, 900]
211        );
212    }
213}