use std::fmt::Write as _;
use serde::{Deserialize, Serialize};
use super::{OracleReport, OracleStats};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvidenceStrength {
Against,
Negligible,
Positive,
Strong,
VeryStrong,
}
impl EvidenceStrength {
#[must_use]
pub fn from_log10_bf(log10_bf: f64) -> Self {
if log10_bf.is_nan() {
Self::Negligible
} else if log10_bf < 0.0 {
Self::Against
} else if log10_bf < 0.5 {
Self::Negligible
} else if log10_bf < 1.3 {
Self::Positive
} else if log10_bf < 2.2 {
Self::Strong
} else {
Self::VeryStrong
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Against => "against",
Self::Negligible => "negligible",
Self::Positive => "positive",
Self::Strong => "strong",
Self::VeryStrong => "very strong",
}
}
}
impl std::fmt::Display for EvidenceStrength {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BayesFactor {
pub log10_bf: f64,
pub hypothesis: String,
pub strength: EvidenceStrength,
}
impl BayesFactor {
#[must_use]
pub fn from_log_likelihoods(
log10_likelihood_h1: f64,
log10_likelihood_h0: f64,
hypothesis: String,
) -> Self {
let log10_bf = log10_likelihood_h1 - log10_likelihood_h0;
Self {
log10_bf,
hypothesis,
strength: EvidenceStrength::from_log10_bf(log10_bf),
}
}
#[must_use]
pub fn value(&self) -> f64 {
10.0_f64.powf(self.log10_bf.clamp(-300.0, 300.0))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogLikelihoodContributions {
pub structural: f64,
pub detection: f64,
pub total: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceLine {
pub equation: String,
pub substitution: String,
pub intuition: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceEntry {
pub invariant: String,
pub passed: bool,
pub bayes_factor: BayesFactor,
pub log_likelihoods: LogLikelihoodContributions,
pub evidence_lines: Vec<EvidenceLine>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceSummary {
pub total_invariants: usize,
pub violations_detected: usize,
pub strongest_violation: Option<String>,
pub strongest_clean: Option<String>,
pub aggregate_log10_bf: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceLedger {
pub entries: Vec<EvidenceEntry>,
pub summary: EvidenceSummary,
pub check_time_nanos: u64,
}
#[derive(Debug, Clone)]
pub struct DetectionModel {
pub per_entity_detection_rate: f64,
pub false_positive_rate: f64,
}
impl Default for DetectionModel {
fn default() -> Self {
Self {
per_entity_detection_rate: 0.9,
false_positive_rate: 0.001,
}
}
}
impl DetectionModel {
#[must_use]
pub fn p_detection_given_violation(&self, entities_tracked: usize) -> f64 {
let n = f64::from(entities_tracked.max(1).min(u32::MAX as usize) as u32);
1.0 - (1.0 - self.per_entity_detection_rate).powf(n)
}
#[must_use]
pub fn p_pass_given_clean(&self) -> f64 {
1.0 - self.false_positive_rate
}
#[must_use]
pub fn p_pass_given_violation(&self, entities_tracked: usize) -> f64 {
let n = f64::from(entities_tracked.max(1).min(u32::MAX as usize) as u32);
(1.0 - self.per_entity_detection_rate).powf(n)
}
#[must_use]
pub fn compute_evidence(
&self,
invariant: &str,
passed: bool,
stats: &OracleStats,
) -> (BayesFactor, LogLikelihoodContributions, Vec<EvidenceLine>) {
let n = stats.entities_tracked;
if passed {
let p_h0 = self.p_pass_given_clean();
let p_h1 = self.p_pass_given_violation(n);
let detection = p_h1.log10() - p_h0.log10();
let structural = -structural_contribution(stats);
let total = structural + detection;
let bf_val = 10.0_f64.powf(total.clamp(-300.0, 300.0));
let bf = BayesFactor {
log10_bf: total,
hypothesis: format!("{invariant} violated"),
strength: EvidenceStrength::from_log10_bf(total),
};
let lines = vec![
EvidenceLine {
equation: "BF_violation = P(pass | violated) / P(pass | holds)".into(),
substitution: format!("BF = {p_h1:.6} / {p_h0:.6} = {bf_val:.4}"),
intuition: format!(
"{} evidence against '{invariant}' violation ({n} entities tracked, oracle saw pass)",
bf.strength.label().to_uppercase(),
),
},
EvidenceLine {
equation: "P(pass | violated) = (1 − p)^n".into(),
substitution: format!(
"P(pass | violated) = (1 − {:.2})^{n} = {:.6}",
self.per_entity_detection_rate, p_h1,
),
intuition: format!(
"With {n} entities, a real violation would be missed with probability {p_h1:.6}",
),
},
];
let ll = LogLikelihoodContributions {
structural,
detection,
total,
};
(bf, ll, lines)
} else {
let p_h1 = self.p_detection_given_violation(n);
let p_h0 = self.false_positive_rate;
let log10_h1 = p_h1.log10();
let log10_h0 = p_h0.log10();
let structural = structural_contribution(stats);
let detection = log10_h1 - log10_h0;
let total = structural + detection;
let bf_val = 10.0_f64.powf(total.clamp(-300.0, 300.0));
let bf = BayesFactor {
log10_bf: total,
hypothesis: format!("{invariant} violated"),
strength: EvidenceStrength::from_log10_bf(total),
};
let lines = vec![
EvidenceLine {
equation:
"BF_violation = P(violation_observed | violated) / P(violation_observed | holds)"
.into(),
substitution: format!("BF = {p_h1:.6} / {p_h0:.6} = {bf_val:.1}"),
intuition: format!(
"{} evidence that '{invariant}' is violated ({n} entities tracked, violation observed)",
bf.strength.label().to_uppercase(),
),
},
EvidenceLine {
equation: "P(violation_observed | violated) = 1 − (1 − p)^n".into(),
substitution: format!(
"P(detected | violated) = 1 − (1 − {:.2})^{n} = {:.6}",
self.per_entity_detection_rate, p_h1,
),
intuition: format!(
"With {n} entities, a real violation would be detected with probability {p_h1:.6}",
),
},
];
let ll = LogLikelihoodContributions {
structural,
detection,
total,
};
(bf, ll, lines)
}
}
}
fn structural_contribution(stats: &OracleStats) -> f64 {
let events = stats.events_recorded.min(u32::MAX as usize) as u32;
(1.0 + f64::from(events) / 100.0).log10()
}
impl EvidenceLedger {
#[must_use]
pub fn from_report(report: &OracleReport) -> Self {
Self::from_report_with_model(report, &DetectionModel::default())
}
#[must_use]
pub fn from_report_with_model(report: &OracleReport, model: &DetectionModel) -> Self {
let entries: Vec<EvidenceEntry> = report
.entries
.iter()
.map(|entry| {
let (bf, ll, lines) =
model.compute_evidence(&entry.invariant, entry.passed, &entry.stats);
EvidenceEntry {
invariant: entry.invariant.clone(),
passed: entry.passed,
bayes_factor: bf,
log_likelihoods: ll,
evidence_lines: lines,
}
})
.collect();
let summary = Self::compute_summary(&entries);
Self {
entries,
summary,
check_time_nanos: report.check_time_nanos,
}
}
fn compute_summary(entries: &[EvidenceEntry]) -> EvidenceSummary {
let total_invariants = entries.len();
let violations_detected = entries.iter().filter(|e| !e.passed).count();
let strongest_violation = entries
.iter()
.filter(|e| !e.passed)
.max_by(|a, b| {
a.bayes_factor
.log10_bf
.partial_cmp(&b.bayes_factor.log10_bf)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|e| e.invariant.clone());
let strongest_clean = entries
.iter()
.filter(|e| e.passed)
.min_by(|a, b| {
a.bayes_factor
.log10_bf
.partial_cmp(&b.bayes_factor.log10_bf)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|e| e.invariant.clone());
let aggregate_log10_bf: f64 = entries
.iter()
.filter(|e| !e.passed)
.map(|e| e.bayes_factor.log10_bf)
.sum();
EvidenceSummary {
total_invariants,
violations_detected,
strongest_violation,
strongest_clean,
aggregate_log10_bf,
}
}
#[must_use]
pub fn violations_by_strength(&self) -> Vec<&EvidenceEntry> {
let mut v: Vec<_> = self.entries.iter().filter(|e| !e.passed).collect();
v.sort_by(|a, b| {
b.bayes_factor
.log10_bf
.partial_cmp(&a.bayes_factor.log10_bf)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
#[must_use]
pub fn clean_by_confidence(&self) -> Vec<&EvidenceEntry> {
let mut v: Vec<_> = self.entries.iter().filter(|e| e.passed).collect();
v.sort_by(|a, b| {
a.bayes_factor
.log10_bf
.partial_cmp(&b.bayes_factor.log10_bf)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
#[must_use]
pub fn to_text(&self) -> String {
let mut out = String::new();
let _ = writeln!(
&mut out,
"╔══════════════════════════════════════════════════╗"
);
let _ = writeln!(
&mut out,
"║ EVIDENCE LEDGER — ORACLE DIAGNOSTICS ║"
);
let _ = writeln!(
&mut out,
"╚══════════════════════════════════════════════════╝"
);
let _ = writeln!(&mut out);
let _ = writeln!(
&mut out,
" Invariants examined: {}",
self.summary.total_invariants
);
let _ = writeln!(
&mut out,
" Violations detected: {}",
self.summary.violations_detected
);
if let Some(ref s) = self.summary.strongest_violation {
let _ = writeln!(&mut out, " Strongest violation: {s}");
}
let _ = writeln!(
&mut out,
" Aggregate log₁₀(BF): {:.3}",
self.summary.aggregate_log10_bf
);
let _ = writeln!(&mut out, " Check time: {}ns", self.check_time_nanos);
let _ = writeln!(&mut out);
let violations = self.violations_by_strength();
if !violations.is_empty() {
let _ = writeln!(
&mut out,
"── VIOLATIONS ──────────────────────────────────────"
);
for entry in violations {
write_entry(&mut out, entry);
}
}
let clean = self.clean_by_confidence();
if !clean.is_empty() {
let _ = writeln!(
&mut out,
"── CLEAN INVARIANTS ────────────────────────────────"
);
for entry in clean {
write_entry(&mut out, entry);
}
}
out
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or_default()
}
}
fn write_entry(out: &mut String, entry: &EvidenceEntry) {
let status = if entry.passed { "PASS" } else { "FAIL" };
let _ = writeln!(out);
let _ = writeln!(
out,
" [{status}] {inv} (BF = {bf:.2}, strength = {strength})",
inv = entry.invariant,
bf = entry.bayes_factor.value(),
strength = entry.bayes_factor.strength,
);
let _ = writeln!(
out,
" log₁₀(BF) = {:.4} [structural={:.4}, detection={:.4}]",
entry.log_likelihoods.total,
entry.log_likelihoods.structural,
entry.log_likelihoods.detection,
);
for (i, line) in entry.evidence_lines.iter().enumerate() {
let _ = writeln!(out, " ({}) {}", i + 1, line.equation);
let _ = writeln!(out, " → {}", line.substitution);
let _ = writeln!(out, " ⇒ {}", line.intuition);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lab::oracle::{OracleEntryReport, OracleReport};
fn make_clean_report() -> OracleReport {
OracleReport {
entries: vec![
OracleEntryReport {
invariant: "task_leak".into(),
passed: true,
violation: None,
stats: OracleStats {
entities_tracked: 5,
events_recorded: 10,
},
},
OracleEntryReport {
invariant: "obligation_leak".into(),
passed: true,
violation: None,
stats: OracleStats {
entities_tracked: 3,
events_recorded: 6,
},
},
],
total: 2,
passed: 2,
failed: 0,
check_time_nanos: 42,
}
}
fn make_violation_report() -> OracleReport {
OracleReport {
entries: vec![
OracleEntryReport {
invariant: "task_leak".into(),
passed: false,
violation: Some("leaked 2 tasks".into()),
stats: OracleStats {
entities_tracked: 5,
events_recorded: 10,
},
},
OracleEntryReport {
invariant: "quiescence".into(),
passed: true,
violation: None,
stats: OracleStats {
entities_tracked: 2,
events_recorded: 4,
},
},
OracleEntryReport {
invariant: "obligation_leak".into(),
passed: false,
violation: Some("leaked 1 obligation".into()),
stats: OracleStats {
entities_tracked: 1,
events_recorded: 2,
},
},
],
total: 3,
passed: 1,
failed: 2,
check_time_nanos: 100,
}
}
#[test]
fn strength_from_log10_bf() {
assert_eq!(
EvidenceStrength::from_log10_bf(-1.0),
EvidenceStrength::Against
);
assert_eq!(
EvidenceStrength::from_log10_bf(0.0),
EvidenceStrength::Negligible
);
assert_eq!(
EvidenceStrength::from_log10_bf(0.49),
EvidenceStrength::Negligible
);
assert_eq!(
EvidenceStrength::from_log10_bf(0.5),
EvidenceStrength::Positive
);
assert_eq!(
EvidenceStrength::from_log10_bf(1.29),
EvidenceStrength::Positive
);
assert_eq!(
EvidenceStrength::from_log10_bf(1.3),
EvidenceStrength::Strong
);
assert_eq!(
EvidenceStrength::from_log10_bf(2.19),
EvidenceStrength::Strong
);
assert_eq!(
EvidenceStrength::from_log10_bf(2.2),
EvidenceStrength::VeryStrong
);
assert_eq!(
EvidenceStrength::from_log10_bf(5.0),
EvidenceStrength::VeryStrong
);
}
#[test]
fn strength_labels() {
assert_eq!(EvidenceStrength::Against.label(), "against");
assert_eq!(EvidenceStrength::Negligible.label(), "negligible");
assert_eq!(EvidenceStrength::Positive.label(), "positive");
assert_eq!(EvidenceStrength::Strong.label(), "strong");
assert_eq!(EvidenceStrength::VeryStrong.label(), "very strong");
}
#[test]
fn strength_display() {
assert_eq!(format!("{}", EvidenceStrength::Strong), "strong");
}
#[test]
fn bayes_factor_from_log_likelihoods() {
let bf = BayesFactor::from_log_likelihoods(-0.5, -3.0, "test".into());
assert!((bf.log10_bf - 2.5).abs() < 1e-10);
assert_eq!(bf.strength, EvidenceStrength::VeryStrong);
}
#[test]
fn bayes_factor_value() {
let bf = BayesFactor::from_log_likelihoods(0.0, -2.0, "test".into());
assert!((bf.value() - 100.0).abs() < 1e-6);
}
#[test]
fn bayes_factor_value_clamped() {
let bf = BayesFactor {
log10_bf: 1000.0,
hypothesis: "extreme".into(),
strength: EvidenceStrength::VeryStrong,
};
assert!(bf.value().is_finite());
}
#[test]
fn detection_model_default() {
let m = DetectionModel::default();
assert!((m.per_entity_detection_rate - 0.9).abs() < 1e-10);
assert!((m.false_positive_rate - 0.001).abs() < 1e-10);
}
#[test]
fn detection_model_p_detection_single_entity() {
let m = DetectionModel::default();
let p = m.p_detection_given_violation(1);
assert!((p - 0.9).abs() < 1e-10);
}
#[test]
fn detection_model_p_detection_multiple_entities() {
let m = DetectionModel::default();
let p = m.p_detection_given_violation(2);
assert!((p - 0.99).abs() < 1e-10);
}
#[test]
fn detection_model_p_detection_zero_entities_uses_one() {
let m = DetectionModel::default();
let p = m.p_detection_given_violation(0);
assert!(
(p - 0.9).abs() < 1e-10,
"zero entities should be treated as 1"
);
}
#[test]
fn detection_model_p_pass_given_clean() {
let m = DetectionModel::default();
assert!((m.p_pass_given_clean() - 0.999).abs() < 1e-10);
}
#[test]
fn detection_model_p_pass_given_violation() {
let m = DetectionModel::default();
assert!((m.p_pass_given_violation(1) - 0.1).abs() < 1e-10);
assert!((m.p_pass_given_violation(2) - 0.01).abs() < 1e-10);
}
#[test]
fn structural_contribution_zero_events() {
let s = structural_contribution(&OracleStats {
entities_tracked: 0,
events_recorded: 0,
});
assert!((s - 0.0_f64.log10()).abs() < 1e-10 || (s - (1.0_f64).log10()).abs() < 1e-10);
assert!(s.abs() < 1e-10);
}
#[test]
fn structural_contribution_increases_with_events() {
let s1 = structural_contribution(&OracleStats {
entities_tracked: 0,
events_recorded: 10,
});
let s2 = structural_contribution(&OracleStats {
entities_tracked: 0,
events_recorded: 100,
});
assert!(s2 > s1);
}
#[test]
fn clean_entry_stays_against_violation_even_with_many_events() {
let report = OracleReport {
entries: vec![OracleEntryReport {
invariant: "task_leak".into(),
passed: true,
violation: None,
stats: OracleStats {
entities_tracked: 1,
events_recorded: 1_000_000,
},
}],
total: 1,
passed: 1,
failed: 0,
check_time_nanos: 7,
};
let ledger = EvidenceLedger::from_report(&report);
let entry = &ledger.entries[0];
assert!(
entry.bayes_factor.log10_bf < 0.0,
"clean pass must remain evidence against violation even with large event counts"
);
assert!(
entry.log_likelihoods.structural < 0.0,
"clean pass should record structural support in the clean direction"
);
assert_eq!(entry.bayes_factor.strength, EvidenceStrength::Against);
assert!(
entry.evidence_lines[0].intuition.contains("against"),
"clean intuition should explicitly describe evidence against violation"
);
}
#[test]
fn ledger_from_clean_report() {
let report = make_clean_report();
let ledger = EvidenceLedger::from_report(&report);
assert_eq!(ledger.entries.len(), 2);
assert_eq!(ledger.summary.total_invariants, 2);
assert_eq!(ledger.summary.violations_detected, 0);
assert!(ledger.summary.strongest_violation.is_none());
assert!((ledger.summary.aggregate_log10_bf).abs() < 1e-10);
assert_eq!(ledger.check_time_nanos, 42);
}
#[test]
fn ledger_clean_entries_have_negative_bf() {
let report = make_clean_report();
let ledger = EvidenceLedger::from_report(&report);
for entry in &ledger.entries {
assert!(entry.passed);
assert!(
entry.bayes_factor.log10_bf < 0.0,
"clean entry '{inv}' should have BF < 1, got log10_bf={bf}",
inv = entry.invariant,
bf = entry.bayes_factor.log10_bf,
);
assert_eq!(entry.bayes_factor.strength, EvidenceStrength::Against);
}
}
#[test]
fn ledger_clean_evidence_lines() {
let report = make_clean_report();
let ledger = EvidenceLedger::from_report(&report);
for entry in &ledger.entries {
assert!(
!entry.evidence_lines.is_empty(),
"entry should have evidence lines"
);
assert!(
entry.evidence_lines[0]
.equation
.contains("P(pass | violated)")
);
}
}
#[test]
fn ledger_from_violation_report() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
assert_eq!(ledger.entries.len(), 3);
assert_eq!(ledger.summary.total_invariants, 3);
assert_eq!(ledger.summary.violations_detected, 2);
assert!(ledger.summary.strongest_violation.is_some());
assert!(ledger.summary.aggregate_log10_bf > 0.0);
}
#[test]
fn ledger_violation_entries_have_positive_bf() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
for entry in ledger.entries.iter().filter(|e| !e.passed) {
assert!(
entry.bayes_factor.log10_bf > 0.0,
"violation entry '{inv}' should have BF > 1, got log10_bf={bf}",
inv = entry.invariant,
bf = entry.bayes_factor.log10_bf,
);
}
}
#[test]
fn ledger_violation_evidence_lines() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let task_entry = ledger
.entries
.iter()
.find(|e| e.invariant == "task_leak")
.unwrap();
assert!(!task_entry.passed);
assert!(
task_entry.evidence_lines[0]
.equation
.contains("P(violation_observed | violated)")
);
}
#[test]
fn violations_by_strength_ordering() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let violations = ledger.violations_by_strength();
assert_eq!(violations.len(), 2);
assert!(violations[0].bayes_factor.log10_bf >= violations[1].bayes_factor.log10_bf);
}
#[test]
fn clean_by_confidence_ordering() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let clean = ledger.clean_by_confidence();
assert_eq!(clean.len(), 1);
assert_eq!(clean[0].invariant, "quiescence");
}
#[test]
fn ledger_to_text_contains_header() {
let report = make_clean_report();
let ledger = EvidenceLedger::from_report(&report);
let text = ledger.to_text();
assert!(text.contains("EVIDENCE LEDGER"));
assert!(text.contains("Invariants examined: 2"));
assert!(text.contains("Violations detected: 0"));
assert!(text.contains("CLEAN INVARIANTS"));
}
#[test]
fn ledger_to_text_violations_section() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let text = ledger.to_text();
assert!(text.contains("VIOLATIONS"));
assert!(text.contains("[FAIL] task_leak"));
assert!(text.contains("[FAIL] obligation_leak"));
assert!(text.contains("[PASS] quiescence"));
}
#[test]
fn ledger_to_text_evidence_lines() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let text = ledger.to_text();
assert!(text.contains("BF ="));
assert!(text.contains("log₁₀(BF)"));
}
#[test]
fn ledger_json_roundtrip() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let json = serde_json::to_string(&ledger).unwrap();
let deserialized: EvidenceLedger = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.entries.len(), ledger.entries.len());
assert_eq!(
deserialized.summary.violations_detected,
ledger.summary.violations_detected
);
assert_eq!(deserialized.check_time_nanos, ledger.check_time_nanos);
}
#[test]
fn ledger_to_json_structure() {
let report = make_clean_report();
let ledger = EvidenceLedger::from_report(&report);
let json = ledger.to_json();
assert!(json["entries"].is_array());
assert!(json["summary"].is_object());
assert_eq!(json["summary"]["total_invariants"], 2);
assert_eq!(json["check_time_nanos"], 42);
}
#[test]
fn custom_detection_model() {
let model = DetectionModel {
per_entity_detection_rate: 0.5,
false_positive_rate: 0.01,
};
let report = make_violation_report();
let ledger = EvidenceLedger::from_report_with_model(&report, &model);
let default_ledger = EvidenceLedger::from_report(&report);
for (custom, default) in ledger
.entries
.iter()
.zip(default_ledger.entries.iter())
.filter(|(_, d)| !d.passed)
{
assert!(
custom.bayes_factor.log10_bf < default.bayes_factor.log10_bf,
"lower detection rate should produce weaker evidence"
);
}
}
#[test]
fn log_likelihood_components_sum_to_total() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
for entry in &ledger.entries {
let expected_total = entry.log_likelihoods.structural + entry.log_likelihoods.detection;
assert!(
(entry.log_likelihoods.total - expected_total).abs() < 1e-10,
"total should equal structural + detection"
);
}
}
#[test]
fn ledger_from_oracle_suite() {
let mut suite = super::super::OracleSuite::new();
let report = suite.report(crate::types::Time::ZERO);
let ledger = EvidenceLedger::from_report(&report);
#[cfg(not(feature = "messaging-fabric"))]
assert_eq!(ledger.entries.len(), 24);
#[cfg(feature = "messaging-fabric")]
assert_eq!(ledger.entries.len(), 28);
assert_eq!(ledger.summary.violations_detected, 0);
for entry in &ledger.entries {
assert!(entry.passed);
assert_eq!(entry.bayes_factor.strength, EvidenceStrength::Against);
}
}
#[test]
fn evidence_entry_fields() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
let task_entry = &ledger.entries[0];
assert_eq!(task_entry.invariant, "task_leak");
assert!(!task_entry.passed);
assert!(!task_entry.evidence_lines.is_empty());
assert!(task_entry.bayes_factor.log10_bf.is_finite());
assert!(task_entry.log_likelihoods.total.is_finite());
}
#[test]
fn evidence_summary_strongest_violation() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
assert_eq!(
ledger.summary.strongest_violation.as_deref(),
Some("task_leak")
);
}
#[test]
fn evidence_summary_strongest_clean() {
let report = make_violation_report();
let ledger = EvidenceLedger::from_report(&report);
assert_eq!(
ledger.summary.strongest_clean.as_deref(),
Some("quiescence")
);
}
}