use crate::lab::oracle::evidence::{EvidenceEntry, EvidenceStrength};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtpEvidenceLedger {
pub schema_version: u32,
pub entries: Vec<AtpEvidenceEntry>,
pub seeds: BTreeMap<String, u64>,
pub artifact_paths: Vec<PathBuf>,
pub metadata: BTreeMap<String, String>,
}
impl AtpEvidenceLedger {
pub fn new() -> Self {
Self {
schema_version: 1,
entries: Vec::new(),
seeds: BTreeMap::new(),
artifact_paths: Vec::new(),
metadata: BTreeMap::new(),
}
}
pub fn record_oracle_result(
&mut self,
oracle_name: impl Into<String>,
evidence: EvidenceEntry,
artifact_path: Option<PathBuf>,
) {
let timestamp = u64::try_from(self.entries.len()).unwrap_or(u64::MAX);
self.record_oracle_result_at(oracle_name, evidence, artifact_path, timestamp);
}
pub fn record_oracle_result_at(
&mut self,
oracle_name: impl Into<String>,
evidence: EvidenceEntry,
artifact_path: Option<PathBuf>,
timestamp: u64,
) {
self.record_optional_artifact_path(artifact_path.as_ref());
let entry = AtpEvidenceEntry {
oracle_name: oracle_name.into(),
evidence,
artifact_path,
timestamp,
};
self.entries.push(entry);
}
pub fn record_artifact_path(&mut self, artifact_path: impl Into<PathBuf>) {
let artifact_path = artifact_path.into();
if !self.artifact_paths.contains(&artifact_path) {
self.artifact_paths.push(artifact_path); }
}
fn record_optional_artifact_path(&mut self, artifact_path: Option<&PathBuf>) {
if let Some(path) = artifact_path {
self.record_artifact_path(path.clone());
}
}
pub fn record_seed(&mut self, name: impl Into<String>, seed: u64) {
self.seeds.insert(name.into(), seed);
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
pub fn violation_entries(&self) -> Vec<&AtpEvidenceEntry> {
self.entries
.iter()
.filter(|entry| {
matches!(
entry.evidence.bayes_factor.strength,
EvidenceStrength::Positive
| EvidenceStrength::Strong
| EvidenceStrength::VeryStrong
)
})
.collect()
}
pub fn evidence_summary(&self) -> EvidenceSummary {
let mut summary = EvidenceSummary::default();
for entry in &self.entries {
match entry.evidence.bayes_factor.strength {
EvidenceStrength::Against => summary.against += 1,
EvidenceStrength::Negligible => summary.negligible += 1,
EvidenceStrength::Positive => summary.positive += 1,
EvidenceStrength::Strong => summary.strong += 1,
EvidenceStrength::VeryStrong => summary.very_strong += 1,
}
}
summary.total = self.entries.len();
summary
}
pub fn export_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn import_json(json: &str) -> serde_json::Result<Self> {
serde_json::from_str(json)
}
}
impl Default for AtpEvidenceLedger {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtpEvidenceEntry {
pub oracle_name: String,
pub evidence: EvidenceEntry,
pub artifact_path: Option<PathBuf>,
pub timestamp: u64,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct EvidenceSummary {
pub total: usize,
pub against: usize,
pub negligible: usize,
pub positive: usize,
pub strong: usize,
pub very_strong: usize,
}
impl EvidenceSummary {
pub fn violation_count(&self) -> usize {
self.positive + self.strong + self.very_strong
}
pub fn has_strong_violations(&self) -> bool {
self.strong > 0 || self.very_strong > 0
}
pub fn summary_text(&self) -> String {
format!(
"Evidence: {} total, {} violations ({} strong+), {} against, {} negligible",
self.total,
self.violation_count(),
self.strong + self.very_strong,
self.against,
self.negligible
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lab::oracle::evidence::{BayesFactor, EvidenceLine, LogLikelihoodContributions};
fn evidence_entry(
invariant: &str,
passed: bool,
log10_bf: f64,
strength: EvidenceStrength,
) -> EvidenceEntry {
EvidenceEntry {
invariant: invariant.to_string(),
passed,
bayes_factor: BayesFactor {
log10_bf,
hypothesis: format!("{invariant} violation"),
strength,
},
log_likelihoods: LogLikelihoodContributions {
structural: log10_bf / 2.0,
detection: log10_bf / 2.0,
total: log10_bf,
},
evidence_lines: vec![EvidenceLine {
equation: "BF = P(data | violation) / P(data | clean)".to_string(),
substitution: format!("log10_bf={log10_bf}"),
intuition: format!("{strength} evidence for {invariant}"),
}],
}
}
#[test]
fn default_recording_uses_deterministic_entry_index_timestamps() {
let mut ledger = AtpEvidenceLedger::new();
ledger.record_oracle_result(
"manifest_integrity",
evidence_entry("manifest_integrity", true, -2.0, EvidenceStrength::Against),
None,
);
ledger.record_oracle_result(
"journal_consistency",
evidence_entry("journal_consistency", false, 1.4, EvidenceStrength::Strong),
None,
);
assert_eq!(ledger.entries[0].timestamp, 0);
assert_eq!(ledger.entries[1].timestamp, 1);
let exported_once = ledger.export_json().expect("ledger serializes");
let exported_twice = ledger
.export_json()
.expect("ledger serializes deterministically");
assert_eq!(exported_once, exported_twice);
}
#[test]
fn explicit_recording_preserves_logical_timestamp_and_dedupes_artifacts() {
let mut ledger = AtpEvidenceLedger::new();
let artifact_path = PathBuf::from("artifacts/transfer.atp-trace");
ledger.record_oracle_result_at(
"proof_bundle_validity",
evidence_entry(
"proof_bundle_validity",
false,
2.4,
EvidenceStrength::VeryStrong,
),
Some(artifact_path.clone()),
42,
);
ledger.record_oracle_result_at(
"path_consistency",
evidence_entry("path_consistency", false, 1.1, EvidenceStrength::Positive),
Some(artifact_path.clone()),
43,
);
assert_eq!(ledger.entries[0].timestamp, 42);
assert_eq!(ledger.entries[1].timestamp, 43);
assert_eq!(ledger.artifact_paths, vec![artifact_path]);
}
#[test]
fn summary_and_violation_entries_are_strength_based() {
let mut ledger = AtpEvidenceLedger::new();
ledger.record_oracle_result(
"clean",
evidence_entry("clean", true, -1.0, EvidenceStrength::Against),
None,
);
ledger.record_oracle_result(
"weak",
evidence_entry("weak", true, 0.2, EvidenceStrength::Negligible),
None,
);
ledger.record_oracle_result(
"strong",
evidence_entry("strong", false, 1.8, EvidenceStrength::Strong),
None,
);
let summary = ledger.evidence_summary();
assert_eq!(summary.total, 3);
assert_eq!(summary.against, 1);
assert_eq!(summary.negligible, 1);
assert_eq!(summary.strong, 1);
assert_eq!(summary.violation_count(), 1);
assert!(summary.has_strong_violations());
let violations = ledger.violation_entries();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].oracle_name, "strong");
}
#[test]
fn json_roundtrip_preserves_seeds_artifacts_and_metadata() {
let mut ledger = AtpEvidenceLedger::new();
ledger.record_seed("lab", 7);
ledger.add_metadata("transfer_id", "tx-123");
ledger.record_oracle_result(
"manifest_integrity",
evidence_entry(
"manifest_integrity",
false,
2.5,
EvidenceStrength::VeryStrong,
),
Some(PathBuf::from("artifacts/manifest")),
);
let json = ledger.export_json().expect("ledger serializes");
let roundtrip = AtpEvidenceLedger::import_json(&json).expect("ledger deserializes");
assert_eq!(roundtrip.schema_version, 1);
assert_eq!(roundtrip.seeds.get("lab"), Some(&7));
assert_eq!(
roundtrip.metadata.get("transfer_id"),
Some(&"tx-123".to_string())
);
assert_eq!(
roundtrip.artifact_paths,
vec![PathBuf::from("artifacts/manifest")]
);
assert_eq!(roundtrip.entries[0].timestamp, 0);
}
}