1use 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#[derive(Debug, Clone)]
21pub struct AtpTransferOracle {
22 pub name: String,
23 pub enabled_checks: AtpOracleChecks,
24}
25
26impl AtpTransferOracle {
27 pub fn new(name: impl Into<String>) -> Self {
29 Self {
30 name: name.into(),
31 enabled_checks: AtpOracleChecks::all(),
32 }
33 }
34
35 pub fn basic() -> Self {
37 Self {
38 name: "atp_basic_transfer".to_string(),
39 enabled_checks: AtpOracleChecks::basic(),
40 }
41 }
42
43 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 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 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 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 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 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 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 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 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 EvidenceEntry {
202 invariant: "manifest_integrity".to_string(),
203 passed: true,
204 bayes_factor: BayesFactor {
205 log10_bf: -2.0, 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 EvidenceEntry {
225 invariant: "manifest_integrity".to_string(),
226 passed: false,
227 bayes_factor: BayesFactor {
228 log10_bf: 3.0, 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct AtpTransferState {
651 pub manifest_hash: String,
653 pub expected_manifest_hash: String,
654
655 pub journal_gaps: u32,
657
658 pub pending_operations: u32,
660
661 pub unverified_final_exposures: u32,
663
664 pub pending_cancellation_drains: u32,
666
667 pub leaked_obligations: u32,
669
670 pub path_outcomes_consistent: bool,
672
673 pub proof_bundle_valid: bool,
675
676 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 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#[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 pub fn evidence_summary(&self) -> String {
731 let summary = self.evidence_ledger.evidence_summary();
732 summary.summary_text()
733 }
734
735 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}