Skip to main content

asupersync/lab/crashpack/
oracle.rs

1//! ATP transfer oracles for manifest, journal, and proof bundle validation.
2//!
3//! Implements specific oracles required by ATP-L2:
4//! - Manifest integrity oracle
5//! - Journal consistency oracle
6//! - Quiescence oracle
7//! - Obligation leak oracle
8//! - Path outcome consistency oracle
9//! - Proof bundle validity oracle
10
11use crate::lab::crashpack::evidence_ledger::AtpEvidenceLedger;
12use crate::lab::oracle::OracleStats;
13use crate::lab::oracle::evidence::{
14    BayesFactor, EvidenceEntry, EvidenceLine, EvidenceStrength, LogLikelihoodContributions,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19/// Composite ATP oracle that runs all transfer validation checks.
20#[derive(Debug, Clone)]
21pub struct AtpTransferOracle {
22    pub name: String,
23    pub enabled_checks: AtpOracleChecks,
24}
25
26impl AtpTransferOracle {
27    /// Create a new ATP transfer oracle with all checks enabled.
28    pub fn new(name: impl Into<String>) -> Self {
29        Self {
30            name: name.into(),
31            enabled_checks: AtpOracleChecks::all(),
32        }
33    }
34
35    /// Create an oracle with only basic checks enabled.
36    pub fn basic() -> Self {
37        Self {
38            name: "atp_basic_transfer".to_string(),
39            enabled_checks: AtpOracleChecks::basic(),
40        }
41    }
42
43    /// Run all enabled oracle checks against the transfer state.
44    pub fn validate(&self, state: &AtpTransferState) -> AtpOracleResult {
45        let mut evidence_ledger = AtpEvidenceLedger::new();
46        let mut stats = OracleStats {
47            entities_tracked: 0,
48            events_recorded: 0,
49        };
50        let mut passed = true;
51
52        // Manifest integrity check
53        if self.enabled_checks.manifest_integrity {
54            let evidence = self.check_manifest_integrity(state);
55            let oracle_passed = matches!(
56                evidence.bayes_factor.strength,
57                EvidenceStrength::Against | EvidenceStrength::Negligible
58            );
59
60            evidence_ledger.record_oracle_result("manifest_integrity", evidence, None);
61            stats.events_recorded += 1;
62
63            if !oracle_passed {
64                stats.entities_tracked += 1;
65                passed = false;
66            }
67        }
68
69        // Journal consistency check
70        if self.enabled_checks.journal_consistency {
71            let evidence = self.check_journal_consistency(state);
72            let oracle_passed = matches!(
73                evidence.bayes_factor.strength,
74                EvidenceStrength::Against | EvidenceStrength::Negligible
75            );
76
77            evidence_ledger.record_oracle_result("journal_consistency", evidence, None);
78            stats.events_recorded += 1;
79
80            if !oracle_passed {
81                stats.entities_tracked += 1;
82                passed = false;
83            }
84        }
85
86        // Quiescence check
87        if self.enabled_checks.quiescence {
88            let evidence = self.check_quiescence(state);
89            let oracle_passed = matches!(
90                evidence.bayes_factor.strength,
91                EvidenceStrength::Against | EvidenceStrength::Negligible
92            );
93
94            evidence_ledger.record_oracle_result("quiescence", evidence, None);
95            stats.events_recorded += 1;
96
97            if !oracle_passed {
98                stats.entities_tracked += 1;
99                passed = false;
100            }
101        }
102
103        // Final exposure check
104        if self.enabled_checks.final_exposure {
105            let evidence = self.check_final_exposure(state);
106            let oracle_passed = matches!(
107                evidence.bayes_factor.strength,
108                EvidenceStrength::Against | EvidenceStrength::Negligible
109            );
110
111            evidence_ledger.record_oracle_result("final_exposure", evidence, None);
112            stats.events_recorded += 1;
113
114            if !oracle_passed {
115                stats.entities_tracked += 1;
116                passed = false;
117            }
118        }
119
120        // Cancellation drain check
121        if self.enabled_checks.cancellation_drain {
122            let evidence = self.check_cancellation_drain(state);
123            let oracle_passed = matches!(
124                evidence.bayes_factor.strength,
125                EvidenceStrength::Against | EvidenceStrength::Negligible
126            );
127
128            evidence_ledger.record_oracle_result("cancellation_drain", evidence, None);
129            stats.events_recorded += 1;
130
131            if !oracle_passed {
132                stats.entities_tracked += 1;
133                passed = false;
134            }
135        }
136
137        // Obligation leak check
138        if self.enabled_checks.obligation_leak {
139            let evidence = self.check_obligation_leak(state);
140            let oracle_passed = matches!(
141                evidence.bayes_factor.strength,
142                EvidenceStrength::Against | EvidenceStrength::Negligible
143            );
144
145            evidence_ledger.record_oracle_result("obligation_leak", evidence, None);
146            stats.events_recorded += 1;
147
148            if !oracle_passed {
149                stats.entities_tracked += 1;
150                passed = false;
151            }
152        }
153
154        // Path consistency check
155        if self.enabled_checks.path_consistency {
156            let evidence = self.check_path_consistency(state);
157            let oracle_passed = matches!(
158                evidence.bayes_factor.strength,
159                EvidenceStrength::Against | EvidenceStrength::Negligible
160            );
161
162            evidence_ledger.record_oracle_result("path_consistency", evidence, None);
163            stats.events_recorded += 1;
164
165            if !oracle_passed {
166                stats.entities_tracked += 1;
167                passed = false;
168            }
169        }
170
171        // Proof bundle validity check
172        if self.enabled_checks.proof_bundle_validity {
173            let evidence = self.check_proof_bundle_validity(state);
174            let oracle_passed = matches!(
175                evidence.bayes_factor.strength,
176                EvidenceStrength::Against | EvidenceStrength::Negligible
177            );
178
179            evidence_ledger.record_oracle_result("proof_bundle_validity", evidence, None);
180            stats.events_recorded += 1;
181
182            if !oracle_passed {
183                stats.entities_tracked += 1;
184                passed = false;
185            }
186        }
187
188        AtpOracleResult {
189            oracle_name: self.name.clone(),
190            evidence_ledger,
191            stats,
192            passed,
193        }
194    }
195
196    fn check_manifest_integrity(&self, state: &AtpTransferState) -> EvidenceEntry {
197        let hash_match = state.manifest_hash == state.expected_manifest_hash;
198
199        if hash_match {
200            // Evidence against violation (manifest is correct)
201            EvidenceEntry {
202                invariant: "manifest_integrity".to_string(),
203                passed: true,
204                bayes_factor: BayesFactor {
205                    log10_bf: -2.0, // Strong evidence against violation
206                    hypothesis: "manifest corruption".to_string(),
207                    strength: EvidenceStrength::from_log10_bf(-2.0),
208                },
209                log_likelihoods: LogLikelihoodContributions {
210                    structural: -1.0,
211                    detection: -1.0,
212                    total: -2.0,
213                },
214                evidence_lines: vec![EvidenceLine {
215                    equation:
216                        "P(hash_match | manifest_correct) / P(hash_match | manifest_corrupted)"
217                            .to_string(),
218                    substitution: "0.999 / 0.001 = 999".to_string(),
219                    intuition: "Very strong evidence that manifest is correct".to_string(),
220                }],
221            }
222        } else {
223            // Evidence for violation (manifest is corrupted)
224            EvidenceEntry {
225                invariant: "manifest_integrity".to_string(),
226                passed: false,
227                bayes_factor: BayesFactor {
228                    log10_bf: 3.0, // Very strong evidence for violation
229                    hypothesis: "manifest corruption".to_string(),
230                    strength: EvidenceStrength::VeryStrong,
231                },
232                log_likelihoods: LogLikelihoodContributions {
233                    structural: 1.5,
234                    detection: 1.5,
235                    total: 3.0,
236                },
237                evidence_lines: vec![
238                    EvidenceLine {
239                        equation: "P(hash_mismatch | manifest_correct) / P(hash_mismatch | manifest_corrupted)".to_string(),
240                        substitution: "0.001 / 0.999 = 0.001".to_string(),
241                        intuition: format!("Very strong evidence of manifest corruption: expected={}, actual={}",
242                                         state.expected_manifest_hash, state.manifest_hash),
243                    },
244                ],
245            }
246        }
247    }
248
249    fn check_journal_consistency(&self, state: &AtpTransferState) -> EvidenceEntry {
250        let has_gaps = state.journal_gaps > 0;
251
252        if !has_gaps {
253            EvidenceEntry {
254                invariant: "journal_consistency".to_string(),
255                passed: true,
256                bayes_factor: BayesFactor {
257                    log10_bf: -1.5,
258                    hypothesis: "journal inconsistency".to_string(),
259                    strength: EvidenceStrength::from_log10_bf(-1.5),
260                },
261                log_likelihoods: LogLikelihoodContributions {
262                    structural: -0.7,
263                    detection: -0.8,
264                    total: -1.5,
265                },
266                evidence_lines: vec![EvidenceLine {
267                    equation: "P(no_gaps | journal_consistent) / P(no_gaps | journal_inconsistent)"
268                        .to_string(),
269                    substitution: "0.95 / 0.05 = 19".to_string(),
270                    intuition: "Strong evidence that journal is consistent".to_string(),
271                }],
272            }
273        } else {
274            let log_bf = (state.journal_gaps as f64).log10() + 1.0;
275            EvidenceEntry {
276                invariant: "journal_consistency".to_string(),
277                passed: false,
278                bayes_factor: BayesFactor {
279                    log10_bf: log_bf,
280                    hypothesis: "journal inconsistency".to_string(),
281                    strength: EvidenceStrength::from_log10_bf(log_bf),
282                },
283                log_likelihoods: LogLikelihoodContributions {
284                    structural: log_bf / 2.0,
285                    detection: log_bf / 2.0,
286                    total: log_bf,
287                },
288                evidence_lines: vec![EvidenceLine {
289                    equation: "P(gaps | journal_consistent) / P(gaps | journal_inconsistent)"
290                        .to_string(),
291                    substitution: format!("0.01 / 0.9 = {:.3}", 10.0_f64.powf(log_bf)),
292                    intuition: format!(
293                        "Evidence of journal inconsistency: {} gaps detected",
294                        state.journal_gaps
295                    ),
296                }],
297            }
298        }
299    }
300
301    fn check_quiescence(&self, state: &AtpTransferState) -> EvidenceEntry {
302        let has_pending = state.pending_operations > 0;
303
304        if !has_pending {
305            EvidenceEntry {
306                invariant: "quiescence".to_string(),
307                passed: true,
308                bayes_factor: BayesFactor {
309                    log10_bf: -1.0,
310                    hypothesis: "non-quiescence".to_string(),
311                    strength: EvidenceStrength::from_log10_bf(-1.0),
312                },
313                log_likelihoods: LogLikelihoodContributions {
314                    structural: -0.5,
315                    detection: -0.5,
316                    total: -1.0,
317                },
318                evidence_lines: vec![EvidenceLine {
319                    equation: "P(no_pending | quiescent) / P(no_pending | not_quiescent)"
320                        .to_string(),
321                    substitution: "0.9 / 0.1 = 9".to_string(),
322                    intuition: "Positive evidence of quiescence".to_string(),
323                }],
324            }
325        } else {
326            let log_bf = (state.pending_operations as f64 / 10.0).log10() + 0.5;
327            EvidenceEntry {
328                invariant: "quiescence".to_string(),
329                passed: false,
330                bayes_factor: BayesFactor {
331                    log10_bf: log_bf,
332                    hypothesis: "non-quiescence".to_string(),
333                    strength: EvidenceStrength::from_log10_bf(log_bf),
334                },
335                log_likelihoods: LogLikelihoodContributions {
336                    structural: log_bf / 2.0,
337                    detection: log_bf / 2.0,
338                    total: log_bf,
339                },
340                evidence_lines: vec![EvidenceLine {
341                    equation: "P(pending | quiescent) / P(pending | not_quiescent)".to_string(),
342                    substitution: format!("0.05 / 0.8 = {:.3}", 10.0_f64.powf(log_bf)),
343                    intuition: format!(
344                        "Evidence against quiescence: {} operations pending",
345                        state.pending_operations
346                    ),
347                }],
348            }
349        }
350    }
351
352    fn check_final_exposure(&self, state: &AtpTransferState) -> EvidenceEntry {
353        let exposures = state.unverified_final_exposures;
354
355        if exposures == 0 {
356            EvidenceEntry {
357                invariant: "final_exposure".to_string(),
358                passed: true,
359                bayes_factor: BayesFactor {
360                    log10_bf: -1.4,
361                    hypothesis: "unverified final exposure".to_string(),
362                    strength: EvidenceStrength::from_log10_bf(-1.4),
363                },
364                log_likelihoods: LogLikelihoodContributions {
365                    structural: -0.7,
366                    detection: -0.7,
367                    total: -1.4,
368                },
369                evidence_lines: vec![EvidenceLine {
370                    equation:
371                        "P(no_exposure | verified_publish) / P(no_exposure | premature_publish)"
372                            .to_string(),
373                    substitution: "0.97 / 0.03 = 32.3".to_string(),
374                    intuition: "Strong evidence that final exposure waited for verification"
375                        .to_string(),
376                }],
377            }
378        } else {
379            let log_bf = (exposures as f64).log10() + 1.7;
380            EvidenceEntry {
381                invariant: "final_exposure".to_string(),
382                passed: false,
383                bayes_factor: BayesFactor {
384                    log10_bf: log_bf,
385                    hypothesis: "unverified final exposure".to_string(),
386                    strength: EvidenceStrength::from_log10_bf(log_bf),
387                },
388                log_likelihoods: LogLikelihoodContributions {
389                    structural: log_bf / 2.0,
390                    detection: log_bf / 2.0,
391                    total: log_bf,
392                },
393                evidence_lines: vec![EvidenceLine {
394                    equation: "P(exposure | verified_publish) / P(exposure | premature_publish)"
395                        .to_string(),
396                    substitution: format!("0.005 / 0.9 = {:.3}", 10.0_f64.powf(log_bf)),
397                    intuition: format!(
398                        "Strong evidence of unverified final exposure: {exposures} exposure(s)"
399                    ),
400                }],
401            }
402        }
403    }
404
405    fn check_cancellation_drain(&self, state: &AtpTransferState) -> EvidenceEntry {
406        let pending_drains = state.pending_cancellation_drains;
407
408        if pending_drains == 0 {
409            EvidenceEntry {
410                invariant: "cancellation_drain".to_string(),
411                passed: true,
412                bayes_factor: BayesFactor {
413                    log10_bf: -1.3,
414                    hypothesis: "undrained cancellation".to_string(),
415                    strength: EvidenceStrength::from_log10_bf(-1.3),
416                },
417                log_likelihoods: LogLikelihoodContributions {
418                    structural: -0.6,
419                    detection: -0.7,
420                    total: -1.3,
421                },
422                evidence_lines: vec![EvidenceLine {
423                    equation:
424                        "P(no_pending_drains | cancel_correct) / P(no_pending_drains | cancel_leak)"
425                            .to_string(),
426                    substitution: "0.96 / 0.04 = 24".to_string(),
427                    intuition: "Strong evidence that cancellation drained before replay close"
428                        .to_string(),
429                }],
430            }
431        } else {
432            let log_bf = (pending_drains as f64).log10() + 1.5;
433            EvidenceEntry {
434                invariant: "cancellation_drain".to_string(),
435                passed: false,
436                bayes_factor: BayesFactor {
437                    log10_bf: log_bf,
438                    hypothesis: "undrained cancellation".to_string(),
439                    strength: EvidenceStrength::from_log10_bf(log_bf),
440                },
441                log_likelihoods: LogLikelihoodContributions {
442                    structural: log_bf / 2.0,
443                    detection: log_bf / 2.0,
444                    total: log_bf,
445                },
446                evidence_lines: vec![EvidenceLine {
447                    equation:
448                        "P(pending_drains | cancel_correct) / P(pending_drains | cancel_leak)"
449                            .to_string(),
450                    substitution: format!("0.02 / 0.85 = {:.3}", 10.0_f64.powf(log_bf)),
451                    intuition: format!(
452                        "Strong evidence of undrained cancellation: {pending_drains} pending drain(s)"
453                    ),
454                }],
455            }
456        }
457    }
458
459    fn check_obligation_leak(&self, state: &AtpTransferState) -> EvidenceEntry {
460        let has_leaks = state.leaked_obligations > 0;
461
462        if !has_leaks {
463            EvidenceEntry {
464                invariant: "obligation_leak".to_string(),
465                passed: true,
466                bayes_factor: BayesFactor {
467                    log10_bf: -1.5,
468                    hypothesis: "obligation leak".to_string(),
469                    strength: EvidenceStrength::from_log10_bf(-1.5),
470                },
471                log_likelihoods: LogLikelihoodContributions {
472                    structural: -0.7,
473                    detection: -0.8,
474                    total: -1.5,
475                },
476                evidence_lines: vec![EvidenceLine {
477                    equation: "P(no_leaks | correct_cleanup) / P(no_leaks | obligation_leak)"
478                        .to_string(),
479                    substitution: "0.95 / 0.05 = 19".to_string(),
480                    intuition: "Strong evidence of correct obligation cleanup".to_string(),
481                }],
482            }
483        } else {
484            let log_bf = (state.leaked_obligations as f64).log10() + 1.5;
485            EvidenceEntry {
486                invariant: "obligation_leak".to_string(),
487                passed: false,
488                bayes_factor: BayesFactor {
489                    log10_bf: log_bf,
490                    hypothesis: "obligation leak".to_string(),
491                    strength: EvidenceStrength::from_log10_bf(log_bf),
492                },
493                log_likelihoods: LogLikelihoodContributions {
494                    structural: log_bf / 2.0,
495                    detection: log_bf / 2.0,
496                    total: log_bf,
497                },
498                evidence_lines: vec![EvidenceLine {
499                    equation: "P(leaks | correct_cleanup) / P(leaks | obligation_leak)".to_string(),
500                    substitution: format!("0.01 / 0.95 = {:.3}", 10.0_f64.powf(log_bf)),
501                    intuition: format!(
502                        "Strong evidence of obligation leak: {} leaked",
503                        state.leaked_obligations
504                    ),
505                }],
506            }
507        }
508    }
509
510    fn check_path_consistency(&self, state: &AtpTransferState) -> EvidenceEntry {
511        if state.path_outcomes_consistent {
512            EvidenceEntry {
513                invariant: "path_consistency".to_string(),
514                passed: true,
515                bayes_factor: BayesFactor {
516                    log10_bf: -1.0,
517                    hypothesis: "path inconsistency".to_string(),
518                    strength: EvidenceStrength::from_log10_bf(-1.0),
519                },
520                log_likelihoods: LogLikelihoodContributions {
521                    structural: -0.5,
522                    detection: -0.5,
523                    total: -1.0,
524                },
525                evidence_lines: vec![EvidenceLine {
526                    equation: "P(consistent | correct_paths) / P(consistent | inconsistent_paths)"
527                        .to_string(),
528                    substitution: "0.9 / 0.1 = 9".to_string(),
529                    intuition: "Positive evidence of path consistency".to_string(),
530                }],
531            }
532        } else {
533            EvidenceEntry {
534                invariant: "path_consistency".to_string(),
535                passed: false,
536                bayes_factor: BayesFactor {
537                    log10_bf: 2.0,
538                    hypothesis: "path inconsistency".to_string(),
539                    strength: EvidenceStrength::Strong,
540                },
541                log_likelihoods: LogLikelihoodContributions {
542                    structural: 1.0,
543                    detection: 1.0,
544                    total: 2.0,
545                },
546                evidence_lines: vec![EvidenceLine {
547                    equation:
548                        "P(inconsistent | correct_paths) / P(inconsistent | inconsistent_paths)"
549                            .to_string(),
550                    substitution: "0.05 / 0.8 = 100".to_string(),
551                    intuition: "Strong evidence of path inconsistency".to_string(),
552                }],
553            }
554        }
555    }
556
557    fn check_proof_bundle_validity(&self, state: &AtpTransferState) -> EvidenceEntry {
558        if state.proof_bundle_valid {
559            EvidenceEntry {
560                invariant: "proof_bundle_validity".to_string(),
561                passed: true,
562                bayes_factor: BayesFactor {
563                    log10_bf: -1.2,
564                    hypothesis: "invalid proof bundle".to_string(),
565                    strength: EvidenceStrength::from_log10_bf(-1.2),
566                },
567                log_likelihoods: LogLikelihoodContributions {
568                    structural: -0.6,
569                    detection: -0.6,
570                    total: -1.2,
571                },
572                evidence_lines: vec![EvidenceLine {
573                    equation: "P(valid_bundle | correct_proof) / P(valid_bundle | invalid_proof)"
574                        .to_string(),
575                    substitution: "0.92 / 0.08 = 11.5".to_string(),
576                    intuition: "Strong evidence of valid proof bundle".to_string(),
577                }],
578            }
579        } else {
580            EvidenceEntry {
581                invariant: "proof_bundle_validity".to_string(),
582                passed: false,
583                bayes_factor: BayesFactor {
584                    log10_bf: 1.8,
585                    hypothesis: "invalid proof bundle".to_string(),
586                    strength: EvidenceStrength::Strong,
587                },
588                log_likelihoods: LogLikelihoodContributions {
589                    structural: 0.9,
590                    detection: 0.9,
591                    total: 1.8,
592                },
593                evidence_lines: vec![EvidenceLine {
594                    equation:
595                        "P(invalid_bundle | correct_proof) / P(invalid_bundle | invalid_proof)"
596                            .to_string(),
597                    substitution: "0.02 / 0.9 = 63".to_string(),
598                    intuition: "Strong evidence of invalid proof bundle".to_string(),
599                }],
600            }
601        }
602    }
603}
604
605/// Configuration for which ATP oracle checks to enable.
606#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
607pub struct AtpOracleChecks {
608    pub manifest_integrity: bool,
609    pub journal_consistency: bool,
610    pub quiescence: bool,
611    pub final_exposure: bool,
612    pub cancellation_drain: bool,
613    pub obligation_leak: bool,
614    pub path_consistency: bool,
615    pub proof_bundle_validity: bool,
616}
617
618impl AtpOracleChecks {
619    /// Enable all oracle checks.
620    pub fn all() -> Self {
621        Self {
622            manifest_integrity: true,
623            journal_consistency: true,
624            quiescence: true,
625            final_exposure: true,
626            cancellation_drain: true,
627            obligation_leak: true,
628            path_consistency: true,
629            proof_bundle_validity: true,
630        }
631    }
632
633    /// Enable only basic checks (manifest, journal, quiescence).
634    pub fn basic() -> Self {
635        Self {
636            manifest_integrity: true,
637            journal_consistency: true,
638            quiescence: true,
639            final_exposure: false,
640            cancellation_drain: false,
641            obligation_leak: false,
642            path_consistency: false,
643            proof_bundle_validity: false,
644        }
645    }
646}
647
648/// Complete state snapshot for ATP oracle validation.
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct AtpTransferState {
651    // Manifest integrity
652    pub manifest_hash: String,
653    pub expected_manifest_hash: String,
654
655    // Journal consistency
656    pub journal_gaps: u32,
657
658    // Quiescence
659    pub pending_operations: u32,
660
661    // Final exposure
662    pub unverified_final_exposures: u32,
663
664    // Cancellation drain
665    pub pending_cancellation_drains: u32,
666
667    // Obligation tracking
668    pub leaked_obligations: u32,
669
670    // Path consistency
671    pub path_outcomes_consistent: bool,
672
673    // Proof bundle validity
674    pub proof_bundle_valid: bool,
675
676    // Additional metadata
677    pub metadata: BTreeMap<String, String>,
678}
679
680impl AtpTransferState {
681    pub fn new() -> Self {
682        Self {
683            manifest_hash: String::new(),
684            expected_manifest_hash: String::new(),
685            journal_gaps: 0,
686            pending_operations: 0,
687            unverified_final_exposures: 0,
688            pending_cancellation_drains: 0,
689            leaked_obligations: 0,
690            path_outcomes_consistent: true,
691            proof_bundle_valid: true,
692            metadata: BTreeMap::new(),
693        }
694    }
695
696    /// Create a clean state (no violations expected).
697    pub fn clean() -> Self {
698        Self {
699            manifest_hash: "clean_hash_123".to_string(),
700            expected_manifest_hash: "clean_hash_123".to_string(),
701            journal_gaps: 0,
702            pending_operations: 0,
703            unverified_final_exposures: 0,
704            pending_cancellation_drains: 0,
705            leaked_obligations: 0,
706            path_outcomes_consistent: true,
707            proof_bundle_valid: true,
708            metadata: BTreeMap::new(),
709        }
710    }
711}
712
713impl Default for AtpTransferState {
714    fn default() -> Self {
715        Self::new()
716    }
717}
718
719/// Result of ATP oracle validation with evidence ledger.
720#[derive(Debug, Clone)]
721pub struct AtpOracleResult {
722    pub oracle_name: String,
723    pub evidence_ledger: AtpEvidenceLedger,
724    pub stats: OracleStats,
725    pub passed: bool,
726}
727
728impl AtpOracleResult {
729    /// Get summary of evidence strength distribution.
730    pub fn evidence_summary(&self) -> String {
731        let summary = self.evidence_ledger.evidence_summary();
732        summary.summary_text()
733    }
734
735    /// Check if there are any high-confidence violations.
736    pub fn has_strong_violations(&self) -> bool {
737        self.evidence_ledger
738            .evidence_summary()
739            .has_strong_violations()
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use crate::lab::oracle::evidence::EvidenceStrength;
747
748    #[test]
749    fn clean_transfer_passes_all_enabled_oracles() {
750        let result = AtpTransferOracle::new("clean_transfer").validate(&AtpTransferState::clean());
751        let summary = result.evidence_ledger.evidence_summary();
752
753        assert!(
754            result.passed,
755            "clean transfer should not be classified as a violation"
756        );
757        assert_eq!(result.stats.events_recorded, 8);
758        assert_eq!(result.stats.entities_tracked, 0);
759        assert_eq!(summary.total, 8);
760        assert_eq!(summary.against, 8);
761        assert_eq!(summary.violation_count(), 0);
762        assert!(!result.has_strong_violations());
763    }
764
765    #[test]
766    fn clean_basic_oracle_records_only_basic_checks_as_passing() {
767        let result = AtpTransferOracle::basic().validate(&AtpTransferState::clean());
768        let summary = result.evidence_ledger.evidence_summary();
769
770        assert!(result.passed);
771        assert_eq!(result.stats.events_recorded, 3);
772        assert_eq!(summary.total, 3);
773        assert_eq!(summary.against, 3);
774        assert_eq!(summary.violation_count(), 0);
775    }
776
777    #[test]
778    fn corrupted_transfer_fails_with_violation_strength() {
779        let mut state = AtpTransferState::clean();
780        state.manifest_hash = "tampered".to_string();
781        state.journal_gaps = 2;
782        state.unverified_final_exposures = 1;
783        state.pending_cancellation_drains = 2;
784        state.leaked_obligations = 1;
785        state.proof_bundle_valid = false;
786
787        let result = AtpTransferOracle::new("corrupted_transfer").validate(&state);
788        let summary = result.evidence_ledger.evidence_summary();
789
790        assert!(!result.passed);
791        assert_eq!(result.stats.events_recorded, 8);
792        assert_eq!(result.stats.entities_tracked, 6);
793        assert_eq!(summary.against, 2);
794        assert_eq!(summary.violation_count(), 6);
795        assert!(result.has_strong_violations());
796    }
797
798    #[test]
799    fn final_exposure_and_cancellation_drain_have_dedicated_evidence_entries() {
800        let mut state = AtpTransferState::clean();
801        state.unverified_final_exposures = 2;
802        state.pending_cancellation_drains = 3;
803
804        let result = AtpTransferOracle::new("publish_and_cancel").validate(&state);
805
806        assert!(!result.passed);
807        assert_eq!(result.stats.events_recorded, 8);
808        assert_eq!(result.stats.entities_tracked, 2);
809
810        let final_exposure = result
811            .evidence_ledger
812            .entries
813            .iter()
814            .find(|entry| entry.oracle_name == "final_exposure")
815            .expect("final exposure entry is recorded");
816        assert!(!final_exposure.evidence.passed);
817        assert_eq!(final_exposure.evidence.invariant, "final_exposure");
818        assert!(
819            final_exposure.evidence.evidence_lines[0]
820                .intuition
821                .contains("2 exposure(s)")
822        );
823
824        let cancellation_drain = result
825            .evidence_ledger
826            .entries
827            .iter()
828            .find(|entry| entry.oracle_name == "cancellation_drain")
829            .expect("cancellation drain entry is recorded");
830        assert!(!cancellation_drain.evidence.passed);
831        assert_eq!(cancellation_drain.evidence.invariant, "cancellation_drain");
832        assert!(
833            cancellation_drain.evidence.evidence_lines[0]
834                .intuition
835                .contains("3 pending drain(s)")
836        );
837    }
838
839    #[test]
840    fn negative_log_bayes_factor_maps_to_against_violation() {
841        for entry in &AtpTransferOracle::new("polarity")
842            .validate(&AtpTransferState::clean())
843            .evidence_ledger
844            .entries
845        {
846            assert_eq!(
847                entry.evidence.bayes_factor.strength,
848                EvidenceStrength::Against,
849                "{} should provide evidence against violation",
850                entry.oracle_name
851            );
852        }
853    }
854}