use crate::error::DoDResult;
use crate::kernel::{Kernel, KernelDecision};
use crate::observation::Observation;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionRecord {
record_id: String,
observations: Vec<Observation>,
tenant_id: String,
decision: KernelDecision,
recorded_at: chrono::DateTime<chrono::Utc>,
}
impl ExecutionRecord {
pub fn new(
observations: Vec<Observation>, tenant_id: impl Into<String>, decision: KernelDecision,
) -> Self {
Self {
record_id: uuid::Uuid::new_v4().to_string(),
observations,
tenant_id: tenant_id.into(),
decision,
recorded_at: chrono::Utc::now(),
}
}
pub fn record_id(&self) -> &str {
&self.record_id
}
pub fn observations(&self) -> &[Observation] {
&self.observations
}
pub fn decision(&self) -> &KernelDecision {
&self.decision
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayResult {
success: bool,
original_hash: String,
replay_hash: String,
hashes_match: bool,
explanation: String,
}
impl ReplayResult {
pub fn successful(hash: String) -> Self {
Self {
success: true,
original_hash: hash.clone(),
replay_hash: hash,
hashes_match: true,
explanation: "Determinism verified".to_string(),
}
}
pub fn failed(original: String, replay: String, explanation: impl Into<String>) -> Self {
Self {
success: false,
original_hash: original,
replay_hash: replay,
hashes_match: false,
explanation: explanation.into(),
}
}
pub fn is_deterministic(&self) -> bool {
self.hashes_match
}
pub fn explanation(&self) -> &str {
&self.explanation
}
}
pub struct ReplayEngine {
records: BTreeMap<String, ExecutionRecord>,
results: Vec<ReplayResult>,
}
impl ReplayEngine {
pub fn new() -> Self {
Self {
records: BTreeMap::new(),
results: Vec::new(),
}
}
pub fn record(mut self, record: ExecutionRecord) -> Self {
self.records.insert(record.record_id().to_string(), record);
self
}
pub fn replay(&mut self, kernel: &mut Kernel, record_id: &str) -> DoDResult<ReplayResult> {
let record = self
.records
.get(record_id)
.ok_or_else(|| {
crate::error::DoDError::Internal(format!("record not found: {}", record_id))
})?
.clone();
let replay_decision = kernel.decide(
record.observations().to_vec(),
record.decision().observations()[0].tenant_id(),
)?;
let original_hash = record
.decision()
.determinism_hash()
.ok_or_else(|| crate::error::DoDError::DeterminismViolation)?
.to_string();
let replay_hash = replay_decision
.determinism_hash()
.ok_or_else(|| crate::error::DoDError::DeterminismViolation)?
.to_string();
let result = if original_hash == replay_hash {
ReplayResult::successful(original_hash)
} else {
ReplayResult::failed(original_hash, replay_hash, "determinism hash mismatch")
};
self.results.push(result.clone());
if result.is_deterministic() {
kernel.verify_determinism(&record.decision(), &replay_decision)?;
} else {
return Err(crate::error::DoDError::DeterminismViolation);
}
Ok(result)
}
pub fn replay_all(&mut self, kernel: &mut Kernel) -> DoDResult<()> {
let record_ids: Vec<_> = self.records.keys().map(|k| k.clone()).collect();
for record_id in record_ids {
self.replay(kernel, &record_id)?;
}
Ok(())
}
pub fn results(&self) -> &[ReplayResult] {
&self.results
}
pub fn all_deterministic(&self) -> bool {
self.results.iter().all(|r| r.is_deterministic())
}
pub fn determinism_report(&self) -> String {
let total = self.results.len();
let deterministic = self.results.iter().filter(|r| r.is_deterministic()).count();
format!(
"Determinism: {}/{} replays successful ({}%)",
deterministic,
total,
if total > 0 {
(deterministic * 100) / total
} else {
100
}
)
}
}
impl Default for ReplayEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replay_result_success() {
let result = ReplayResult::successful("hash123".to_string());
assert!(result.is_deterministic());
}
#[test]
fn test_replay_result_failure() {
let result = ReplayResult::failed("hash123".to_string(), "hash456".to_string(), "mismatch");
assert!(!result.is_deterministic());
}
#[test]
fn test_replay_engine() {
let engine = ReplayEngine::new();
assert!(engine.all_deterministic());
}
}