use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditPhase {
Perception,
Decision,
Action,
Recovery,
Escalation,
}
impl fmt::Display for AuditPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuditPhase::Perception => write!(f, "perception"),
AuditPhase::Decision => write!(f, "decision"),
AuditPhase::Action => write!(f, "action"),
AuditPhase::Recovery => write!(f, "recovery"),
AuditPhase::Escalation => write!(f, "escalation"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditEventType {
AgentSelected(String),
SpcRuleFired(String, String), CircuitTransitioned(String, String), RecoveryStarted(String), RecoveryCompleted(bool, i8), EscalationTriggered(String), }
impl fmt::Display for AuditEventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuditEventType::AgentSelected(agent) => write!(f, "agent_selected({})", agent),
AuditEventType::SpcRuleFired(rule, metric) => {
write!(f, "spc_rule_fired({}, {})", rule, metric)
}
AuditEventType::CircuitTransitioned(before, after) => {
write!(f, "circuit_transitioned({} -> {})", before, after)
}
AuditEventType::RecoveryStarted(reason) => write!(f, "recovery_started({})", reason),
AuditEventType::RecoveryCompleted(success, delta) => {
write!(
f,
"recovery_completed(success={}, delta={})",
success, delta
)
}
AuditEventType::EscalationTriggered(reason) => {
write!(f, "escalation_triggered({})", reason)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub timestamp_ns: u128,
pub event_type: AuditEventType,
pub details: String,
pub phase: AuditPhase,
pub cycle_count: u64,
pub event_hash: String,
pub prev_hash: String,
}
impl AuditEvent {
pub fn new(
timestamp_ns: u128,
event_type: AuditEventType,
details: String,
phase: AuditPhase,
cycle_count: u64,
prev_hash: String,
) -> Self {
let mut event = Self {
timestamp_ns,
event_type,
details,
phase,
cycle_count,
event_hash: String::new(),
prev_hash,
};
event.event_hash = event.compute_hash();
event
}
fn compute_hash(&self) -> String {
let serialized = serde_json::json!({
"event_type": self.event_type.to_string(),
"details": self.details,
"phase": self.phase.to_string(),
"cycle_count": self.cycle_count,
"prev_hash": self.prev_hash,
});
let bytes = serialized.to_string().into_bytes();
let hash = blake3::hash(&bytes);
hash.to_hex().to_string()
}
pub fn verify_linkage(&self, prev_event: &AuditEvent) -> bool {
self.prev_hash == prev_event.event_hash
}
}
pub struct AutonomicAuditTrail {
events: Vec<AuditEvent>,
checksum: String,
}
impl AutonomicAuditTrail {
pub fn new() -> Self {
Self {
events: Vec::new(),
checksum: "genesis".to_string(),
}
}
pub fn record_event(
&mut self,
event_type: AuditEventType,
details: String,
phase: AuditPhase,
cycle_count: u64,
) -> bool {
#[cfg(target_arch = "wasm32")]
let timestamp_ns = 0u128;
#[cfg(not(target_arch = "wasm32"))]
let timestamp_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let prev_hash = if self.events.is_empty() {
"genesis".to_string()
} else {
self.events.last().unwrap().event_hash.clone() };
let event = AuditEvent::new(
timestamp_ns,
event_type,
details,
phase,
cycle_count,
prev_hash,
);
if event.event_hash.is_empty() {
return false;
}
let updated_checksum =
blake3::hash(format!("{}:{}", self.checksum, event.event_hash).as_bytes());
self.checksum = updated_checksum.to_hex().to_string();
self.events.push(event);
true
}
pub fn verify_chain(&self) -> bool {
for i in 1..self.events.len() {
let prev = &self.events[i - 1];
let curr = &self.events[i];
if !curr.verify_linkage(prev) {
return false;
}
let recomputed = curr.event_hash.clone();
if recomputed.is_empty() {
return false;
}
}
true
}
pub fn get_events(&self) -> &[AuditEvent] {
&self.events
}
pub fn get_checksum(&self) -> &str {
&self.checksum
}
pub fn export_timeline(&self) -> String {
if self.events.is_empty() {
return "[ Audit trail is empty ]\n".to_string();
}
let mut timeline = String::new();
timeline.push_str("╭─── Autonomic Audit Trail ─────────────────────────────────\n");
for (idx, event) in self.events.iter().enumerate() {
let timestamp = event.timestamp_ns / 1_000_000_000; let phase_marker = match event.phase {
AuditPhase::Perception => "🔍",
AuditPhase::Decision => "🧠",
AuditPhase::Action => "⚡",
AuditPhase::Recovery => "🔧",
AuditPhase::Escalation => "⚠️",
};
let _ = write!(
timeline,
"│ {}: Cycle {} @ {}s\n",
phase_marker, event.cycle_count, timestamp
);
let _ = write!(timeline, "│ Event: {}\n", event.event_type);
let _ = write!(timeline, "│ Details: {}\n", event.details);
if idx < self.events.len() - 1 {
timeline.push_str("│\n");
}
}
timeline.push_str("╰──────────────────────────────────────────────────────────\n");
let _ = write!(
timeline,
"Checksum (Merkle root): {}\n",
&self.checksum[..16]
);
let _ = write!(timeline, "Chain verified: {}\n", self.verify_chain());
timeline
}
#[cfg(test)]
pub fn clear(&mut self) {
self.events.clear();
self.checksum = "genesis".to_string();
}
pub fn event_count(&self) -> usize {
self.events.len()
}
}
impl Default for AutonomicAuditTrail {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_recording() {
let mut trail = AutonomicAuditTrail::new();
let success = trail.record_event(
AuditEventType::AgentSelected("QLearning".to_string()),
"Agent selected by LinUCB bandit".to_string(),
AuditPhase::Decision,
100,
);
assert!(success);
assert_eq!(trail.event_count(), 1);
}
#[test]
fn test_chain_verification() {
let mut trail = AutonomicAuditTrail::new();
trail.record_event(
AuditEventType::SpcRuleFired("rule_1_outlier".to_string(), "event_rate".to_string()),
"Event rate exceeded 3σ".to_string(),
AuditPhase::Perception,
42,
);
trail.record_event(
AuditEventType::CircuitTransitioned("Closed".to_string(), "Open".to_string()),
"Failure threshold exceeded".to_string(),
AuditPhase::Decision,
43,
);
trail.record_event(
AuditEventType::RecoveryStarted("Circuit opened, probe initiated".to_string()),
"HalfOpen state allows recovery probe".to_string(),
AuditPhase::Recovery,
44,
);
assert!(trail.verify_chain());
assert_eq!(trail.event_count(), 3);
}
#[test]
fn test_hash_integrity() {
let mut trail = AutonomicAuditTrail::new();
trail.record_event(
AuditEventType::EscalationTriggered("Manual intervention required".to_string()),
"Health level critical for 10 consecutive cycles".to_string(),
AuditPhase::Escalation,
500,
);
let events = trail.get_events();
let event = &events[0];
assert!(!event.event_hash.is_empty());
assert_eq!(event.event_hash.len(), 64); }
#[test]
fn test_timeline_rendering() {
let mut trail = AutonomicAuditTrail::new();
trail.record_event(
AuditEventType::AgentSelected("SARSA".to_string()),
"SARSA agent learns on-policy".to_string(),
AuditPhase::Decision,
1,
);
trail.record_event(
AuditEventType::RecoveryCompleted(true, -1),
"Health improved from 3 to 2".to_string(),
AuditPhase::Recovery,
2,
);
let timeline = trail.export_timeline();
assert!(timeline.contains("Autonomic Audit Trail"));
assert!(timeline.contains("Cycle"));
assert!(timeline.contains("agent_selected"));
assert!(timeline.contains("recovery_completed"));
assert!(timeline.contains("Checksum"));
assert!(timeline.contains("Chain verified: true"));
}
}