1#![forbid(unsafe_code)]
2#![warn(rustdoc::broken_intra_doc_links)]
3
4use std::{
67 borrow::Cow,
68 time::{SystemTime, UNIX_EPOCH},
69};
70
71use serde::{Deserialize, Serialize};
72use sha2::{Digest, Sha256};
73use thiserror::Error;
74
75#[cfg(feature = "asupersync")]
76pub mod asupersync;
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum RuntimeMode {
81 Strict,
82 Hardened,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum DecisionAction {
88 Allow,
89 Reject,
90 Repair,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum IssueKind {
96 UnknownFeature,
97 MalformedInput,
98 JoinCardinality,
99 PolicyOverride,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct CompatibilityIssue {
104 pub kind: IssueKind,
105 pub subject: String,
106 pub detail: String,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct EvidenceTerm {
111 pub name: Cow<'static, str>,
112 pub log_likelihood_if_compatible: f64,
113 pub log_likelihood_if_incompatible: f64,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
117pub struct LossMatrix {
118 pub allow_if_compatible: f64,
119 pub allow_if_incompatible: f64,
120 pub reject_if_compatible: f64,
121 pub reject_if_incompatible: f64,
122 pub repair_if_compatible: f64,
123 pub repair_if_incompatible: f64,
124}
125
126impl Default for LossMatrix {
127 fn default() -> Self {
128 Self {
129 allow_if_compatible: 0.0,
130 allow_if_incompatible: 100.0,
131 reject_if_compatible: 6.0,
132 reject_if_incompatible: 0.5,
133 repair_if_compatible: 2.0,
134 repair_if_incompatible: 3.0,
135 }
136 }
137}
138
139const UNKNOWN_FEATURE_PRIOR: f64 = 0.25;
140const JOIN_ADMISSION_PRIOR: f64 = 0.6;
141const PRIOR_COMPATIBLE_EPSILON: f64 = 1e-10;
142
143const UNKNOWN_FEATURE_EVIDENCE: [EvidenceTerm; 2] = [
144 EvidenceTerm {
145 name: Cow::Borrowed("compatibility_allowlist_miss"),
146 log_likelihood_if_compatible: -3.5,
147 log_likelihood_if_incompatible: -0.2,
148 },
149 EvidenceTerm {
150 name: Cow::Borrowed("unknown_protocol_field"),
151 log_likelihood_if_compatible: -2.0,
152 log_likelihood_if_incompatible: -0.1,
153 },
154];
155
156const JOIN_ADMISSION_EVIDENCE_WITHIN_CAP: [EvidenceTerm; 2] = [
157 EvidenceTerm {
158 name: Cow::Borrowed("estimator_overflow_risk"),
159 log_likelihood_if_compatible: -0.3,
160 log_likelihood_if_incompatible: -1.2,
161 },
162 EvidenceTerm {
163 name: Cow::Borrowed("memory_budget_signal"),
164 log_likelihood_if_compatible: -0.4,
165 log_likelihood_if_incompatible: -1.5,
166 },
167];
168
169const JOIN_ADMISSION_EVIDENCE_OVER_CAP: [EvidenceTerm; 2] = [
170 EvidenceTerm {
171 name: Cow::Borrowed("estimator_overflow_risk"),
172 log_likelihood_if_compatible: -2.8,
173 log_likelihood_if_incompatible: -0.1,
174 },
175 EvidenceTerm {
176 name: Cow::Borrowed("memory_budget_signal"),
177 log_likelihood_if_compatible: -2.2,
178 log_likelihood_if_incompatible: -0.2,
179 },
180];
181
182const JOIN_ADMISSION_LOSS: LossMatrix = LossMatrix {
183 allow_if_compatible: 0.0,
184 allow_if_incompatible: 130.0,
185 reject_if_compatible: 5.0,
186 reject_if_incompatible: 0.5,
187 repair_if_compatible: 1.5,
188 repair_if_incompatible: 3.0,
189};
190
191const DEFAULT_CONFORMAL_ALPHA: f64 = 0.1;
192const MIN_CONFORMAL_ALPHA: f64 = 0.01;
193const MAX_CONFORMAL_ALPHA: f64 = 0.5;
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct DecisionMetrics {
197 pub posterior_compatible: f64,
198 pub bayes_factor_compatible_over_incompatible: f64,
199 pub expected_loss_allow: f64,
200 pub expected_loss_reject: f64,
201 pub expected_loss_repair: f64,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct DecisionRecord {
206 pub ts_unix_ms: u64,
207 pub mode: RuntimeMode,
208 pub action: DecisionAction,
209 pub issue: CompatibilityIssue,
210 pub prior_compatible: f64,
211 pub metrics: DecisionMetrics,
212 pub evidence: Vec<EvidenceTerm>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct SemanticIndexIdentity {
217 pub role: String,
218 pub len: usize,
219 pub has_duplicates: bool,
220 pub fingerprint: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SemanticWitnessRecord {
225 pub ts_unix_ms: u64,
226 pub operation: String,
227 pub materialization_reason: String,
228 pub alignment_mode: String,
229 pub input_index_identity: Vec<SemanticIndexIdentity>,
230 pub output_index_identity: SemanticIndexIdentity,
231 pub null_nan_policy: String,
232 pub output_ordering_contract: String,
233}
234
235impl SemanticWitnessRecord {
236 #[must_use]
237 pub fn new(
238 operation: impl Into<String>,
239 materialization_reason: impl Into<String>,
240 alignment_mode: impl Into<String>,
241 input_index_identity: Vec<SemanticIndexIdentity>,
242 output_index_identity: SemanticIndexIdentity,
243 null_nan_policy: impl Into<String>,
244 output_ordering_contract: impl Into<String>,
245 ) -> Self {
246 Self {
247 ts_unix_ms: now_unix_ms().unwrap_or_default(),
248 operation: operation.into(),
249 materialization_reason: materialization_reason.into(),
250 alignment_mode: alignment_mode.into(),
251 input_index_identity,
252 output_index_identity,
253 null_nan_policy: null_nan_policy.into(),
254 output_ordering_contract: output_ordering_contract.into(),
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260pub struct GalaxyBrainCard {
261 pub title: String,
262 pub equation: String,
263 pub substitution: String,
264 pub intuition: String,
265}
266
267impl GalaxyBrainCard {
268 #[must_use]
269 pub fn render_plain(&self) -> String {
270 let capacity = self.title.len()
271 + self.equation.len()
272 + self.substitution.len()
273 + self.intuition.len()
274 + 5;
275 let mut rendered = String::with_capacity(capacity);
276 rendered.push('[');
277 rendered.push_str(&self.title);
278 rendered.push_str("]\n");
279 rendered.push_str(&self.equation);
280 rendered.push('\n');
281 rendered.push_str(&self.substitution);
282 rendered.push('\n');
283 rendered.push_str(&self.intuition);
284 rendered
285 }
286}
287
288#[must_use]
289pub fn decision_to_card(record: &DecisionRecord) -> GalaxyBrainCard {
290 GalaxyBrainCard {
291 title: format!("{}::{:?}", record.issue.subject, record.action),
292 equation: "argmin_a Σ_s L(a,s) P(s|evidence)".to_owned(),
293 substitution: format!(
294 "P(compatible|e)={:.4}, E[allow]={:.4}, E[reject]={:.4}, E[repair]={:.4}",
295 record.metrics.posterior_compatible,
296 record.metrics.expected_loss_allow,
297 record.metrics.expected_loss_reject,
298 record.metrics.expected_loss_repair
299 ),
300 intuition: "Lower expected loss wins; strict mode may still force fail-closed.".to_owned(),
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct EvidenceLedger {
306 records: Vec<DecisionRecord>,
307 #[serde(default)]
308 semantic_witnesses: Vec<SemanticWitnessRecord>,
309 #[serde(skip)]
315 record_semantic_witnesses: bool,
316}
317
318impl Default for EvidenceLedger {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324impl EvidenceLedger {
325 #[must_use]
326 pub fn new() -> Self {
327 Self {
328 records: Vec::new(),
329 semantic_witnesses: Vec::new(),
330 record_semantic_witnesses: true,
331 }
332 }
333
334 #[must_use]
339 pub fn without_semantic_witnesses(mut self) -> Self {
340 self.record_semantic_witnesses = false;
341 self
342 }
343
344 #[must_use]
346 pub fn records_semantic_witnesses(&self) -> bool {
347 self.record_semantic_witnesses
348 }
349
350 pub fn push(&mut self, record: DecisionRecord) {
351 self.records.push(record);
352 }
353
354 pub fn push_semantic_witness(&mut self, record: SemanticWitnessRecord) {
355 self.semantic_witnesses.push(record);
356 }
357
358 #[must_use]
359 pub fn records(&self) -> &[DecisionRecord] {
360 &self.records
361 }
362
363 #[must_use]
364 pub fn semantic_witnesses(&self) -> &[SemanticWitnessRecord] {
365 &self.semantic_witnesses
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct RuntimePolicy {
371 pub mode: RuntimeMode,
372 pub fail_closed_unknown_features: bool,
373 pub hardened_join_row_cap: Option<usize>,
374}
375
376impl RuntimePolicy {
377 #[must_use]
378 pub fn strict() -> Self {
379 Self {
380 mode: RuntimeMode::Strict,
381 fail_closed_unknown_features: true,
382 hardened_join_row_cap: None,
383 }
384 }
385
386 #[must_use]
387 pub fn hardened(join_row_cap: Option<usize>) -> Self {
388 Self {
389 mode: RuntimeMode::Hardened,
390 fail_closed_unknown_features: false,
391 hardened_join_row_cap: join_row_cap,
392 }
393 }
394
395 pub fn decide_unknown_feature(
396 &self,
397 subject: impl Into<String>,
398 detail: impl Into<String>,
399 ledger: &mut EvidenceLedger,
400 ) -> DecisionAction {
401 let issue = CompatibilityIssue {
402 kind: IssueKind::UnknownFeature,
403 subject: subject.into(),
404 detail: detail.into(),
405 };
406
407 let mut record = decide(
408 self.mode,
409 issue,
410 UNKNOWN_FEATURE_PRIOR,
411 LossMatrix::default(),
412 UNKNOWN_FEATURE_EVIDENCE.to_vec(),
413 );
414 if self.fail_closed_unknown_features {
415 record.action = DecisionAction::Reject;
416 }
417 let action = record.action;
418 ledger.push(record);
419 action
420 }
421
422 pub fn decide_join_admission(
423 &self,
424 estimated_rows: usize,
425 ledger: &mut EvidenceLedger,
426 ) -> DecisionAction {
427 let issue = CompatibilityIssue {
428 kind: IssueKind::JoinCardinality,
429 subject: "join_estimator".to_owned(),
430 detail: format!("estimated_rows={estimated_rows}"),
431 };
432
433 let cap = self.hardened_join_row_cap.unwrap_or(usize::MAX);
434 let evidence = if estimated_rows <= cap {
435 JOIN_ADMISSION_EVIDENCE_WITHIN_CAP.to_vec()
436 } else {
437 JOIN_ADMISSION_EVIDENCE_OVER_CAP.to_vec()
438 };
439 let mut record = decide(
440 self.mode,
441 issue,
442 JOIN_ADMISSION_PRIOR,
443 JOIN_ADMISSION_LOSS,
444 evidence,
445 );
446
447 if matches!(self.mode, RuntimeMode::Hardened) && estimated_rows > cap {
448 record.action = DecisionAction::Repair;
449 }
450
451 let action = record.action;
452 ledger.push(record);
453 action
454 }
455}
456
457impl Default for RuntimePolicy {
458 fn default() -> Self {
459 Self::strict()
460 }
461}
462
463#[derive(Debug, Error)]
464pub enum RuntimeError {
465 #[error("system clock is before UNIX_EPOCH")]
466 ClockSkew,
467}
468
469fn now_unix_ms() -> Result<u64, RuntimeError> {
470 let ms = SystemTime::now()
471 .duration_since(UNIX_EPOCH)
472 .map_err(|_| RuntimeError::ClockSkew)?
473 .as_millis();
474 Ok(ms as u64)
475}
476
477fn normalize_prior_compatible(prior_compatible: f64) -> f64 {
478 if !prior_compatible.is_finite() {
479 return 0.5;
480 }
481 prior_compatible.clamp(PRIOR_COMPATIBLE_EPSILON, 1.0 - PRIOR_COMPATIBLE_EPSILON)
482}
483
484fn decide(
485 mode: RuntimeMode,
486 issue: CompatibilityIssue,
487 prior_compatible: f64,
488 loss: LossMatrix,
489 evidence: Vec<EvidenceTerm>,
490) -> DecisionRecord {
491 let prior_compatible = normalize_prior_compatible(prior_compatible);
492 let log_odds_prior = (prior_compatible / (1.0 - prior_compatible)).ln();
493 let llr_sum: f64 = evidence
494 .iter()
495 .map(|term| term.log_likelihood_if_compatible - term.log_likelihood_if_incompatible)
496 .sum();
497 let log_odds_post = log_odds_prior + llr_sum;
498
499 let posterior_compatible = 1.0 / (1.0 + (-log_odds_post).exp());
500 let posterior_incompatible = 1.0 - posterior_compatible;
501
502 let expected_loss_allow = loss.allow_if_compatible * posterior_compatible
503 + loss.allow_if_incompatible * posterior_incompatible;
504 let expected_loss_reject = loss.reject_if_compatible * posterior_compatible
505 + loss.reject_if_incompatible * posterior_incompatible;
506 let expected_loss_repair = loss.repair_if_compatible * posterior_compatible
507 + loss.repair_if_incompatible * posterior_incompatible;
508
509 let mut best_action = DecisionAction::Allow;
510 let mut best_loss = expected_loss_allow;
511
512 if expected_loss_repair < best_loss {
513 best_action = DecisionAction::Repair;
514 best_loss = expected_loss_repair;
515 }
516 if expected_loss_reject < best_loss {
517 best_action = DecisionAction::Reject;
518 }
519
520 DecisionRecord {
521 ts_unix_ms: now_unix_ms().unwrap_or_default(),
522 mode,
523 action: best_action,
524 issue,
525 prior_compatible,
526 metrics: DecisionMetrics {
527 posterior_compatible,
528 bayes_factor_compatible_over_incompatible: llr_sum.exp(),
529 expected_loss_allow,
530 expected_loss_reject,
531 expected_loss_repair,
532 },
533 evidence,
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538pub struct RaptorQEnvelope {
539 pub artifact_id: String,
540 pub artifact_type: String,
541 pub source_hash: String,
542 pub raptorq: RaptorQMetadata,
543 pub scrub: ScrubStatus,
544 pub decode_proofs: Vec<DecodeProof>,
545}
546
547pub const MAX_DECODE_PROOFS: usize = 1_000;
548pub const DEFAULT_RAPTORQ_SYMBOL_BYTES: usize = 1_024;
549
550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
551pub struct RaptorQMetadata {
552 pub k: u32,
553 pub repair_symbols: u32,
554 pub overhead_ratio: f64,
555 pub symbol_hashes: Vec<String>,
556}
557
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
559pub struct ScrubStatus {
560 pub last_ok_unix_ms: u64,
561 pub status: String,
562}
563
564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
565pub struct DecodeProof {
566 pub ts_unix_ms: u64,
567 pub reason: String,
568 pub recovered_blocks: u32,
569 pub proof_hash: String,
570}
571
572impl RaptorQEnvelope {
573 #[must_use]
574 pub fn from_source_bytes(
575 artifact_id: impl Into<String>,
576 artifact_type: impl Into<String>,
577 source_bytes: &[u8],
578 repair_symbols: u32,
579 ) -> Self {
580 let symbol_hashes: Vec<String> = source_bytes
581 .chunks(DEFAULT_RAPTORQ_SYMBOL_BYTES)
582 .map(sha256_prefixed_hex)
583 .collect();
584 let k = u32::try_from(symbol_hashes.len()).unwrap_or(u32::MAX);
585 let overhead_ratio = if k == 0 {
586 0.0
587 } else {
588 f64::from(repair_symbols) / f64::from(k)
589 };
590
591 Self {
592 artifact_id: artifact_id.into(),
593 artifact_type: artifact_type.into(),
594 source_hash: sha256_prefixed_hex(source_bytes),
595 raptorq: RaptorQMetadata {
596 k,
597 repair_symbols,
598 overhead_ratio,
599 symbol_hashes,
600 },
601 scrub: ScrubStatus {
602 last_ok_unix_ms: now_unix_ms().unwrap_or_default(),
603 status: "ok".to_owned(),
604 },
605 decode_proofs: Vec::new(),
606 }
607 }
608
609 pub fn push_decode_proof_capped(&mut self, proof: DecodeProof) {
613 if self.decode_proofs.len() >= MAX_DECODE_PROOFS {
614 let overflow = self.decode_proofs.len() + 1 - MAX_DECODE_PROOFS;
615 self.decode_proofs.drain(0..overflow);
616 }
617 self.decode_proofs.push(proof);
618 }
619}
620
621#[must_use]
622pub fn semantic_fingerprint_bytes(bytes: &[u8]) -> String {
623 sha256_prefixed_hex(bytes)
624}
625
626#[derive(Debug)]
627pub struct SemanticFingerprintBuilder {
628 hasher: Sha256,
629}
630
631impl Default for SemanticFingerprintBuilder {
632 fn default() -> Self {
633 Self::new()
634 }
635}
636
637impl SemanticFingerprintBuilder {
638 #[must_use]
639 pub fn new() -> Self {
640 Self {
641 hasher: Sha256::new(),
642 }
643 }
644
645 pub fn update(&mut self, bytes: &[u8]) {
646 self.hasher.update(bytes);
647 }
648
649 #[must_use]
650 pub fn finish(self) -> String {
651 let digest = self.hasher.finalize();
652 let mut output = String::with_capacity(7 + 64);
653 output.push_str("sha256:");
654 append_sha256_digest_hex(&mut output, digest);
655 output
656 }
657}
658
659#[cfg(test)]
660fn sha256_hex(bytes: &[u8]) -> String {
661 let digest = Sha256::digest(bytes);
662 sha256_digest_hex(digest)
663}
664
665fn sha256_prefixed_hex(bytes: &[u8]) -> String {
666 let digest = Sha256::digest(bytes);
667 let mut hex = String::with_capacity(7 + 64);
668 hex.push_str("sha256:");
669 append_sha256_digest_hex(&mut hex, digest);
670 hex
671}
672
673#[cfg(test)]
674fn sha256_digest_hex(digest: impl IntoIterator<Item = u8>) -> String {
675 let mut hex = String::with_capacity(64);
676 append_sha256_digest_hex(&mut hex, digest);
677 hex
678}
679
680fn append_sha256_digest_hex(hex: &mut String, digest: impl IntoIterator<Item = u8>) {
681 const HEX: &[u8; 16] = b"0123456789abcdef";
682 for byte in digest {
683 hex.push(char::from(HEX[usize::from(byte >> 4)]));
684 hex.push(char::from(HEX[usize::from(byte & 0x0f)]));
685 }
686}
687
688fn nonconformity_score(record: &DecisionRecord) -> f64 {
693 let p = record
695 .metrics
696 .posterior_compatible
697 .clamp(1e-15, 1.0 - 1e-15);
698 (p / (1.0 - p)).ln().abs()
699}
700
701fn normalize_conformal_alpha(alpha: f64) -> f64 {
702 if alpha.is_finite() {
703 alpha.clamp(MIN_CONFORMAL_ALPHA, MAX_CONFORMAL_ALPHA)
704 } else {
705 DEFAULT_CONFORMAL_ALPHA
706 }
707}
708
709fn select_conformal_quantile(mut scores: Vec<f64>, alpha: f64) -> Option<f64> {
710 if scores.len() < 2 {
711 return None;
712 }
713 let n = scores.len() as f64;
715 let level = (1.0 - normalize_conformal_alpha(alpha)) * (1.0 + 1.0 / n);
716 let idx = (level * n).ceil() as usize;
717 let idx = idx.min(scores.len()).saturating_sub(1);
718 let (_, quantile, _) = scores.select_nth_unstable_by(idx, f64::total_cmp);
719 Some(*quantile)
720}
721
722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724pub struct ConformalPredictionSet {
725 pub quantile_threshold: f64,
727 pub current_score: f64,
729 pub bayesian_action_in_set: bool,
731 pub admissible_actions: Vec<DecisionAction>,
733 pub empirical_coverage: f64,
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize)]
739pub struct ConformalGuard {
740 scores: Vec<f64>,
742 window_size: usize,
744 alpha: f64,
746 in_set_count: usize,
748 total_count: usize,
750}
751
752impl ConformalGuard {
753 #[must_use]
755 pub fn new(window_size: usize, alpha: f64) -> Self {
756 let window_size = window_size.max(1);
757 Self {
758 scores: Vec::with_capacity(window_size),
759 window_size,
760 alpha: normalize_conformal_alpha(alpha),
761 in_set_count: 0,
762 total_count: 0,
763 }
764 }
765
766 #[must_use]
768 pub fn default_config() -> Self {
769 Self::new(1000, 0.1)
770 }
771
772 #[must_use]
775 pub fn conformal_quantile(&self) -> Option<f64> {
776 let finite = self
777 .scores
778 .iter()
779 .copied()
780 .filter(|score| score.is_finite())
781 .collect();
782 select_conformal_quantile(finite, self.alpha)
783 }
784
785 pub fn evaluate(&mut self, record: &DecisionRecord) -> ConformalPredictionSet {
788 self.normalize_runtime_config();
789 let score = nonconformity_score(record);
790
791 debug_assert!(self.scores.iter().all(|score| score.is_finite()));
792 let quantile = select_conformal_quantile(self.scores.clone(), self.alpha);
793
794 if self.scores.len() >= self.window_size {
796 self.scores.remove(0);
797 }
798 self.scores.push(score);
799
800 let threshold = match quantile {
801 Some(q) => q,
802 None => {
803 self.total_count += 1;
805 self.in_set_count += 1;
806 return ConformalPredictionSet {
807 quantile_threshold: f64::INFINITY,
808 current_score: score,
809 bayesian_action_in_set: true,
810 admissible_actions: vec![
811 DecisionAction::Allow,
812 DecisionAction::Reject,
813 DecisionAction::Repair,
814 ],
815 empirical_coverage: 1.0,
816 };
817 }
818 };
819
820 let bayesian_in_set = score <= threshold;
821
822 let admissible = if bayesian_in_set {
826 vec![record.action]
827 } else {
828 vec![
829 DecisionAction::Allow,
830 DecisionAction::Reject,
831 DecisionAction::Repair,
832 ]
833 };
834
835 self.total_count += 1;
836 if bayesian_in_set {
837 self.in_set_count += 1;
838 }
839
840 let empirical_coverage = if self.total_count > 0 {
841 self.in_set_count as f64 / self.total_count as f64
842 } else {
843 1.0
844 };
845
846 ConformalPredictionSet {
847 quantile_threshold: threshold,
848 current_score: score,
849 bayesian_action_in_set: bayesian_in_set,
850 admissible_actions: admissible,
851 empirical_coverage,
852 }
853 }
854
855 #[must_use]
857 pub fn empirical_coverage(&self) -> f64 {
858 if self.total_count == 0 {
859 return 1.0;
860 }
861 self.in_set_count.min(self.total_count) as f64 / self.total_count as f64
862 }
863
864 #[must_use]
866 pub fn calibration_count(&self) -> usize {
867 self.scores.len()
868 }
869
870 #[must_use]
872 pub fn is_calibrated(&self) -> bool {
873 self.scores.iter().filter(|score| score.is_finite()).count() >= 2
874 }
875
876 #[must_use]
878 pub fn coverage_alert(&self) -> bool {
879 self.total_count >= 100
880 && self.empirical_coverage() < (1.0 - normalize_conformal_alpha(self.alpha))
881 }
882
883 fn normalize_runtime_config(&mut self) {
884 self.window_size = self.window_size.max(1);
885 self.alpha = normalize_conformal_alpha(self.alpha);
886 self.scores.retain(|score| score.is_finite());
887 if self.scores.len() > self.window_size {
888 let overflow = self.scores.len() - self.window_size;
889 self.scores.drain(0..overflow);
890 }
891 self.in_set_count = self.in_set_count.min(self.total_count);
892 }
893}
894
895#[cfg(feature = "asupersync")]
896#[must_use]
897pub fn outcome_to_action<T, E>(outcome: &::asupersync::Outcome<T, E>) -> DecisionAction {
898 match outcome {
899 ::asupersync::Outcome::Ok(_) => DecisionAction::Allow,
900 ::asupersync::Outcome::Err(_) => DecisionAction::Repair,
901 ::asupersync::Outcome::Cancelled(_) | ::asupersync::Outcome::Panicked(_) => {
902 DecisionAction::Reject
903 }
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use std::{borrow::Cow, hint::black_box, time::Instant};
910
911 use serde::Serialize;
912
913 use super::{
914 ConformalGuard, DecisionAction, EvidenceLedger, GalaxyBrainCard, RaptorQEnvelope,
915 RuntimeMode, RuntimePolicy, SemanticIndexIdentity, SemanticWitnessRecord, decision_to_card,
916 };
917
918 const ASUPERSYNC_PACKET_ID: &str = "ASUPERSYNC-E";
919 const REPLAY_PREFIX: &str = "cargo test -p fp-runtime --";
920
921 #[test]
925 fn semantic_fingerprint_sha256_known_answers_01gdm() {
926 assert_eq!(
928 super::semantic_fingerprint_bytes(b""),
929 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
930 );
931 assert_eq!(
932 super::semantic_fingerprint_bytes(b"abc"),
933 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
934 );
935
936 let f = super::semantic_fingerprint_bytes(b"frankenpandas");
938 assert!(f.starts_with("sha256:"), "prefix");
939 assert_eq!(f.len(), 7 + 64, "length");
940 assert!(
941 f[7..]
942 .bytes()
943 .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)),
944 "lowercase hex"
945 );
946 assert_eq!(
947 f,
948 super::semantic_fingerprint_bytes(b"frankenpandas"),
949 "deterministic"
950 );
951
952 let inputs: [&[u8]; 5] = [b"a", b"b", b"ab", b"ba", b""];
954 for (i, x) in inputs.iter().enumerate() {
955 for (j, y) in inputs.iter().enumerate() {
956 if i != j {
957 assert_ne!(
958 super::semantic_fingerprint_bytes(x),
959 super::semantic_fingerprint_bytes(y),
960 "distinct {i} vs {j}"
961 );
962 }
963 }
964 }
965
966 let mut b = super::SemanticFingerprintBuilder::new();
968 b.update(b"hello");
969 b.update(b" ");
970 b.update(b"world");
971 assert_eq!(
972 b.finish(),
973 super::semantic_fingerprint_bytes(b"hello world")
974 );
975 }
976
977 #[test]
978 #[ignore = "foreground release attribution harness"]
979 fn raptorq_prefixed_sha256_single_buffer_ab_qfz4j() {
980 fn former(bytes: &[u8]) -> String {
981 format!("sha256:{}", super::sha256_hex(bytes))
982 }
983
984 fn one_buffer(bytes: &[u8]) -> String {
985 super::sha256_prefixed_hex(bytes)
986 }
987
988 fn elapsed(source: &[u8], hash: impl Fn(&[u8]) -> String) -> u128 {
989 let start = Instant::now();
990 let mut digest = 0_usize;
991 for chunk in source.chunks(super::DEFAULT_RAPTORQ_SYMBOL_BYTES) {
992 digest = digest.wrapping_add(black_box(hash(black_box(chunk))).len());
993 }
994 black_box(digest);
995 start.elapsed().as_nanos()
996 }
997
998 fn median(values: &mut [u128]) -> u128 {
999 values.sort_unstable();
1000 values[values.len() / 2]
1001 }
1002
1003 for len in [0, 1, 31, 1_023, 1_024, 1_025, 4_097] {
1004 let bytes: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
1005 assert_eq!(former(&bytes), one_buffer(&bytes));
1006 }
1007
1008 let source: Vec<u8> = (0..4 * 1_024 * 1_024)
1009 .map(|i| ((i * 131 + i / 17) % 251) as u8)
1010 .collect();
1011 for chunk in source.chunks(super::DEFAULT_RAPTORQ_SYMBOL_BYTES) {
1012 assert_eq!(former(chunk), one_buffer(chunk));
1013 }
1014
1015 for _ in 0..2 {
1016 black_box(elapsed(&source, former));
1017 black_box(elapsed(&source, one_buffer));
1018 }
1019
1020 let mut former_samples = Vec::with_capacity(18);
1021 let mut candidate_samples = Vec::with_capacity(18);
1022 for block in 0..9 {
1023 if block % 2 == 0 {
1024 former_samples.push(elapsed(&source, former));
1025 candidate_samples.push(elapsed(&source, one_buffer));
1026 candidate_samples.push(elapsed(&source, one_buffer));
1027 former_samples.push(elapsed(&source, former));
1028 } else {
1029 candidate_samples.push(elapsed(&source, one_buffer));
1030 former_samples.push(elapsed(&source, former));
1031 former_samples.push(elapsed(&source, former));
1032 candidate_samples.push(elapsed(&source, one_buffer));
1033 }
1034 }
1035
1036 let former_p50 = median(&mut former_samples);
1037 let candidate_p50 = median(&mut candidate_samples);
1038 eprintln!(
1039 "RAPTORQ_HASH_AB bytes={} symbols={} former_p50_ns={} candidate_p50_ns={} ratio={:.6}",
1040 source.len(),
1041 source.len() / super::DEFAULT_RAPTORQ_SYMBOL_BYTES,
1042 former_p50,
1043 candidate_p50,
1044 former_p50 as f64 / candidate_p50 as f64,
1045 );
1046 eprintln!("RAPTORQ_HASH_AB former_samples_ns={former_samples:?}");
1047 eprintln!("RAPTORQ_HASH_AB candidate_samples_ns={candidate_samples:?}");
1048 }
1049
1050 #[test]
1051 #[ignore = "foreground release attribution harness"]
1052 fn semantic_fingerprint_one_buffer_ab_9yiey() {
1053 const BATCH: usize = 2_048;
1054 const BLOCKS: usize = 10;
1055
1056 fn former(bytes: &[u8]) -> String {
1057 format!("sha256:{}", super::sha256_hex(bytes))
1058 }
1059
1060 fn candidate(bytes: &[u8]) -> String {
1061 super::semantic_fingerprint_bytes(bytes)
1062 }
1063
1064 fn elapsed(bytes: &[u8], fingerprint: fn(&[u8]) -> String) -> u128 {
1065 let started = Instant::now();
1066 let mut digest = 0_u8;
1067 for _ in 0..BATCH {
1068 let output = black_box(fingerprint(black_box(bytes)));
1069 digest ^= output.as_bytes()[70];
1070 }
1071 black_box(digest);
1072 started.elapsed().as_nanos() / BATCH as u128
1073 }
1074
1075 fn percentile(samples: &mut [u128], pct: usize) -> u128 {
1076 samples.sort_unstable();
1077 let rank = (samples.len() * pct).div_ceil(100).saturating_sub(1);
1078 samples[rank]
1079 }
1080
1081 for len in [0, 1, 31, 64, 65, 1_024, 1_025] {
1082 let bytes = (0..len)
1083 .map(|i| ((i * 131 + i / 7) % 251) as u8)
1084 .collect::<Vec<_>>();
1085 assert_eq!(former(&bytes), candidate(&bytes));
1086 }
1087
1088 let bytes = (0..64)
1089 .map(|i| ((i * 131 + i / 7) % 251) as u8)
1090 .collect::<Vec<_>>();
1091 for _ in 0..2 {
1092 black_box(elapsed(&bytes, former));
1093 black_box(elapsed(&bytes, candidate));
1094 }
1095
1096 let mut former_samples = Vec::with_capacity(BLOCKS * 2);
1097 let mut candidate_samples = Vec::with_capacity(BLOCKS * 2);
1098 for block in 0..BLOCKS {
1099 if block.is_multiple_of(2) {
1100 former_samples.push(elapsed(&bytes, former));
1101 candidate_samples.push(elapsed(&bytes, candidate));
1102 candidate_samples.push(elapsed(&bytes, candidate));
1103 former_samples.push(elapsed(&bytes, former));
1104 } else {
1105 candidate_samples.push(elapsed(&bytes, candidate));
1106 former_samples.push(elapsed(&bytes, former));
1107 former_samples.push(elapsed(&bytes, former));
1108 candidate_samples.push(elapsed(&bytes, candidate));
1109 }
1110 }
1111
1112 let former_p50 = percentile(&mut former_samples, 50);
1113 let candidate_p50 = percentile(&mut candidate_samples, 50);
1114 let former_p95 = percentile(&mut former_samples, 95);
1115 let candidate_p95 = percentile(&mut candidate_samples, 95);
1116 eprintln!(
1117 "SEMANTIC_FINGERPRINT_AB bytes={} batch={BATCH} samples={} former_p50_ns={former_p50} candidate_p50_ns={candidate_p50} ratio={:.6} former_p95_ns={former_p95} candidate_p95_ns={candidate_p95}",
1118 bytes.len(),
1119 BLOCKS * 2,
1120 former_p50 as f64 / candidate_p50 as f64,
1121 );
1122 eprintln!("SEMANTIC_FINGERPRINT_AB former_samples_ns={former_samples:?}");
1123 eprintln!("SEMANTIC_FINGERPRINT_AB candidate_samples_ns={candidate_samples:?}");
1124 }
1125
1126 #[test]
1127 #[ignore = "foreground release attribution harness"]
1128 fn semantic_fingerprint_builder_finish_one_buffer_ab_88cuv() {
1129 const BATCH: usize = 2_048;
1130 const BLOCKS: usize = 10;
1131
1132 fn former(hasher: super::Sha256) -> String {
1133 format!(
1134 "sha256:{}",
1135 super::sha256_digest_hex(super::Digest::finalize(hasher))
1136 )
1137 }
1138
1139 fn candidate(hasher: super::Sha256) -> String {
1140 super::SemanticFingerprintBuilder { hasher }.finish()
1141 }
1142
1143 fn seeded_hasher(len: usize) -> super::Sha256 {
1144 let mut hasher = <super::Sha256 as super::Digest>::new();
1145 let bytes = (0..len)
1146 .map(|i| ((i * 131 + i / 7) % 251) as u8)
1147 .collect::<Vec<_>>();
1148 super::Digest::update(&mut hasher, &bytes);
1149 hasher
1150 }
1151
1152 fn elapsed(template: &super::Sha256, finish: fn(super::Sha256) -> String) -> u128 {
1153 let started = Instant::now();
1154 let mut digest = 0_u8;
1155 for _ in 0..BATCH {
1156 let output = black_box(finish(black_box(template.clone())));
1157 digest ^= output.as_bytes().last().copied().unwrap_or_default();
1158 }
1159 black_box(digest);
1160 started.elapsed().as_nanos() / BATCH as u128
1161 }
1162
1163 fn percentile(samples: &mut [u128], pct: usize) -> u128 {
1164 samples.sort_unstable();
1165 let rank = (samples.len() * pct).div_ceil(100).saturating_sub(1);
1166 samples[rank]
1167 }
1168
1169 for len in [0, 1, 31, 64, 65, 1_024, 1_025] {
1170 let template = seeded_hasher(len);
1171 assert_eq!(former(template.clone()), candidate(template));
1172 }
1173
1174 let template = seeded_hasher(64);
1175 for _ in 0..2 {
1176 black_box(elapsed(&template, former));
1177 black_box(elapsed(&template, candidate));
1178 }
1179
1180 let mut former_samples = Vec::with_capacity(BLOCKS * 2);
1181 let mut candidate_samples = Vec::with_capacity(BLOCKS * 2);
1182 for block in 0..BLOCKS {
1183 if block.is_multiple_of(2) {
1184 former_samples.push(elapsed(&template, former));
1185 candidate_samples.push(elapsed(&template, candidate));
1186 candidate_samples.push(elapsed(&template, candidate));
1187 former_samples.push(elapsed(&template, former));
1188 } else {
1189 candidate_samples.push(elapsed(&template, candidate));
1190 former_samples.push(elapsed(&template, former));
1191 former_samples.push(elapsed(&template, former));
1192 candidate_samples.push(elapsed(&template, candidate));
1193 }
1194 }
1195
1196 let former_p50 = percentile(&mut former_samples, 50);
1197 let candidate_p50 = percentile(&mut candidate_samples, 50);
1198 let former_p95 = percentile(&mut former_samples, 95);
1199 let candidate_p95 = percentile(&mut candidate_samples, 95);
1200 eprintln!(
1201 "SEMANTIC_BUILDER_FINISH_AB bytes=64 batch={BATCH} samples={} former_p50_ns={former_p50} candidate_p50_ns={candidate_p50} ratio={:.6} former_p95_ns={former_p95} candidate_p95_ns={candidate_p95}",
1202 BLOCKS * 2,
1203 former_p50 as f64 / candidate_p50 as f64,
1204 );
1205 eprintln!("SEMANTIC_BUILDER_FINISH_AB former_samples_ns={former_samples:?}");
1206 eprintln!("SEMANTIC_BUILDER_FINISH_AB candidate_samples_ns={candidate_samples:?}");
1207 }
1208
1209 #[test]
1210 fn semantic_fingerprint_streaming_equals_oneshot_h2i8m() {
1211 let mut st: u64 = 0x4f1e_0b1c_2d3e_4f50;
1215 let mut next = || {
1216 st = st
1217 .wrapping_mul(6_364_136_223_846_793_005)
1218 .wrapping_add(1_442_695_040_888_963_407);
1219 (st >> 33) as u32
1220 };
1221 for iter in 0..400u32 {
1222 let total = (next() % 40) as usize;
1223 let bytes: Vec<u8> = (0..total).map(|_| (next() % 256) as u8).collect();
1224 let mut builder = super::SemanticFingerprintBuilder::new();
1226 let mut pos = 0usize;
1227 while pos < bytes.len() {
1228 let remaining = bytes.len() - pos;
1229 let take = (next() as usize % (remaining + 1)).min(remaining);
1230 builder.update(&bytes[pos..pos + take]);
1231 pos += take;
1232 if take == 0 {
1233 builder.update(&bytes[pos..pos + 1.min(bytes.len() - pos)]);
1235 pos += 1;
1236 }
1237 }
1238 assert_eq!(
1239 builder.finish(),
1240 super::semantic_fingerprint_bytes(&bytes),
1241 "streaming==one-shot iter={iter} total={total}"
1242 );
1243 }
1244 }
1245
1246 #[test]
1249 fn raptorq_envelope_from_source_bytes_invariants_b9vvk() {
1250 let sym = super::DEFAULT_RAPTORQ_SYMBOL_BYTES;
1251 let repair = 3u32;
1252 for &len in &[0usize, 1, sym, sym + 1, 3 * sym - 72] {
1253 let source: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
1254 let env = RaptorQEnvelope::from_source_bytes("pkt-1", "conformance", &source, repair);
1255
1256 let expected_k = len.div_ceil(sym) as u32; assert_eq!(env.raptorq.k, expected_k, "k for len={len}");
1258 assert_eq!(
1259 env.raptorq.symbol_hashes.len() as u32,
1260 expected_k,
1261 "one symbol hash per source symbol, len={len}"
1262 );
1263 assert_eq!(
1264 env.raptorq.repair_symbols, repair,
1265 "repair_symbols len={len}"
1266 );
1267 assert_eq!(
1268 env.source_hash,
1269 super::semantic_fingerprint_bytes(&source),
1270 "source_hash == fingerprint, len={len}"
1271 );
1272 let expected_overhead = if expected_k == 0 {
1273 0.0
1274 } else {
1275 f64::from(repair) / f64::from(expected_k)
1276 };
1277 assert_eq!(
1278 env.raptorq.overhead_ratio, expected_overhead,
1279 "overhead len={len}"
1280 );
1281 assert_eq!(env.scrub.status, "ok", "scrub ok len={len}");
1282 assert!(
1283 env.decode_proofs.is_empty(),
1284 "no decode proofs yet len={len}"
1285 );
1286 assert!(
1288 env.raptorq
1289 .symbol_hashes
1290 .iter()
1291 .all(|h| h.starts_with("sha256:") && h.len() == 7 + 64),
1292 "symbol hash format len={len}"
1293 );
1294 }
1295 }
1296
1297 #[test]
1300 fn raptorq_decode_proof_cap_fifo_bhlwt() {
1301 let mut env = RaptorQEnvelope::from_source_bytes("p", "t", b"x", 1);
1302 let cap = super::MAX_DECODE_PROOFS;
1303 let total = cap + 5;
1304 for i in 0..total {
1305 env.push_decode_proof_capped(super::DecodeProof {
1306 ts_unix_ms: i as u64,
1307 reason: "scrub".to_owned(),
1308 recovered_blocks: i as u32,
1309 proof_hash: "sha256:deadbeef".to_owned(),
1310 });
1311 }
1312 assert_eq!(
1314 env.decode_proofs.len(),
1315 cap,
1316 "history capped at MAX_DECODE_PROOFS"
1317 );
1318 assert_eq!(
1320 env.decode_proofs.first().unwrap().recovered_blocks,
1321 (total - cap) as u32,
1322 "oldest evicted; first retained is seq=overflow"
1323 );
1324 assert_eq!(
1325 env.decode_proofs.last().unwrap().recovered_blocks,
1326 (total - 1) as u32,
1327 "newest retained"
1328 );
1329 assert!(
1331 env.decode_proofs
1332 .windows(2)
1333 .all(|w| w[1].recovered_blocks == w[0].recovered_blocks + 1),
1334 "retained window is contiguous"
1335 );
1336 }
1337
1338 #[test]
1341 fn runtime_policy_failclosed_and_join_cap_mbjpj() {
1342 let s = RuntimePolicy::strict();
1344 assert_eq!(s.mode, RuntimeMode::Strict);
1345 assert!(s.fail_closed_unknown_features);
1346 assert_eq!(s.hardened_join_row_cap, None);
1347 let h = RuntimePolicy::hardened(Some(10));
1348 assert_eq!(h.mode, RuntimeMode::Hardened);
1349 assert!(!h.fail_closed_unknown_features);
1350 assert_eq!(h.hardened_join_row_cap, Some(10));
1351 assert_eq!(
1352 RuntimePolicy::default().mode,
1353 RuntimeMode::Strict,
1354 "default is strict"
1355 );
1356
1357 let mut led = EvidenceLedger::new();
1359 let action =
1360 RuntimePolicy::strict().decide_unknown_feature("widget", "no handler", &mut led);
1361 assert_eq!(
1362 action,
1363 DecisionAction::Reject,
1364 "strict fail-closes unknown features"
1365 );
1366 assert_eq!(led.records().len(), 1, "decision recorded");
1367
1368 let mut led2 = EvidenceLedger::new();
1370 let over = RuntimePolicy::hardened(Some(10)).decide_join_admission(1_000, &mut led2);
1371 assert_eq!(over, DecisionAction::Repair, "hardened caps over-cap joins");
1372 assert_eq!(led2.records().len(), 1, "join decision recorded");
1373 }
1374
1375 #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1376 struct StructuredTestLog {
1377 packet_id: String,
1378 case_id: String,
1379 mode: RuntimeMode,
1380 seed: u64,
1381 trace_id: String,
1382 assertion_path: String,
1383 result: String,
1384 replay_cmd: String,
1385 }
1386
1387 fn make_structured_log(
1388 case_id: &str,
1389 mode: RuntimeMode,
1390 seed: u64,
1391 assertion_path: &str,
1392 result: &str,
1393 ) -> StructuredTestLog {
1394 StructuredTestLog {
1395 packet_id: ASUPERSYNC_PACKET_ID.to_owned(),
1396 case_id: case_id.to_owned(),
1397 mode,
1398 seed,
1399 trace_id: format!("{ASUPERSYNC_PACKET_ID}:{case_id}:{seed:016x}"),
1400 assertion_path: assertion_path.to_owned(),
1401 result: result.to_owned(),
1402 replay_cmd: format!("{REPLAY_PREFIX} {case_id} --nocapture"),
1403 }
1404 }
1405
1406 fn assert_required_log_fields(log: &serde_json::Value) {
1407 for field in [
1408 "packet_id",
1409 "case_id",
1410 "mode",
1411 "seed",
1412 "trace_id",
1413 "assertion_path",
1414 "result",
1415 "replay_cmd",
1416 ] {
1417 assert!(
1418 log.get(field).is_some(),
1419 "structured log missing field: {field}"
1420 );
1421 }
1422 }
1423
1424 #[test]
1425 fn evidence_ledger_records_semantic_witnesses_tn6qb3() {
1426 let mut ledger = EvidenceLedger::new();
1427 let witness = SemanticWitnessRecord::new(
1428 "series.add",
1429 "series_binary_arithmetic_materialization",
1430 "outer",
1431 vec![
1432 SemanticIndexIdentity {
1433 role: "left".to_owned(),
1434 len: 2,
1435 has_duplicates: false,
1436 fingerprint: super::semantic_fingerprint_bytes(b"left"),
1437 },
1438 SemanticIndexIdentity {
1439 role: "right".to_owned(),
1440 len: 2,
1441 has_duplicates: false,
1442 fingerprint: super::semantic_fingerprint_bytes(b"right"),
1443 },
1444 ],
1445 SemanticIndexIdentity {
1446 role: "output".to_owned(),
1447 len: 3,
1448 has_duplicates: false,
1449 fingerprint: super::semantic_fingerprint_bytes(b"output"),
1450 },
1451 "missing aligned operands materialize as NaN/null before arithmetic",
1452 "outer union preserves left order then right-only labels",
1453 );
1454
1455 ledger.push_semantic_witness(witness);
1456
1457 let witnesses = ledger.semantic_witnesses();
1458 assert_eq!(witnesses.len(), 1);
1459 assert_eq!(witnesses[0].operation, "series.add");
1460 assert_eq!(witnesses[0].alignment_mode, "outer");
1461 assert_eq!(witnesses[0].output_index_identity.len, 3);
1462 assert_eq!(witnesses[0].input_index_identity[0].role, "left");
1463 assert!(
1464 witnesses[0].input_index_identity[0]
1465 .fingerprint
1466 .starts_with("sha256:")
1467 );
1468 }
1469
1470 fn decide_join_admission_baseline(
1471 policy: &RuntimePolicy,
1472 estimated_rows: usize,
1473 ledger: &mut EvidenceLedger,
1474 ) -> DecisionAction {
1475 let issue = super::CompatibilityIssue {
1476 kind: super::IssueKind::JoinCardinality,
1477 subject: "join_estimator".to_owned(),
1478 detail: format!("estimated_rows={estimated_rows}"),
1479 };
1480 let cap = policy.hardened_join_row_cap.unwrap_or(usize::MAX);
1481 let evidence = vec![
1482 super::EvidenceTerm {
1483 name: Cow::Owned("estimator_overflow_risk".to_owned()),
1484 log_likelihood_if_compatible: if estimated_rows <= cap { -0.3 } else { -2.8 },
1485 log_likelihood_if_incompatible: if estimated_rows <= cap { -1.2 } else { -0.1 },
1486 },
1487 super::EvidenceTerm {
1488 name: Cow::Owned("memory_budget_signal".to_owned()),
1489 log_likelihood_if_compatible: if estimated_rows <= cap { -0.4 } else { -2.2 },
1490 log_likelihood_if_incompatible: if estimated_rows <= cap { -1.5 } else { -0.2 },
1491 },
1492 ];
1493 let loss = super::LossMatrix {
1494 allow_if_compatible: 0.0,
1495 allow_if_incompatible: 130.0,
1496 reject_if_compatible: 5.0,
1497 reject_if_incompatible: 0.5,
1498 repair_if_compatible: 1.5,
1499 repair_if_incompatible: 3.0,
1500 };
1501 let mut record = super::decide(policy.mode, issue, 0.6, loss, evidence);
1502 if matches!(policy.mode, RuntimeMode::Hardened) && estimated_rows > cap {
1503 record.action = DecisionAction::Repair;
1504 }
1505 let action = record.action;
1506 ledger.push(record);
1507 action
1508 }
1509
1510 fn assert_join_record_equivalent(
1511 optimized: &super::DecisionRecord,
1512 baseline: &super::DecisionRecord,
1513 ) {
1514 assert_eq!(optimized.mode, baseline.mode);
1515 assert_eq!(optimized.action, baseline.action);
1516 assert_eq!(optimized.issue.kind, baseline.issue.kind);
1517 assert_eq!(optimized.issue.subject, baseline.issue.subject);
1518 assert_eq!(optimized.issue.detail, baseline.issue.detail);
1519 assert_eq!(optimized.prior_compatible, baseline.prior_compatible);
1520 assert_eq!(optimized.metrics, baseline.metrics);
1521 assert_eq!(optimized.evidence.len(), baseline.evidence.len());
1522 for (left, right) in optimized.evidence.iter().zip(&baseline.evidence) {
1523 assert_eq!(left.name.as_ref(), right.name.as_ref());
1524 assert_eq!(
1525 left.log_likelihood_if_compatible,
1526 right.log_likelihood_if_compatible
1527 );
1528 assert_eq!(
1529 left.log_likelihood_if_incompatible,
1530 right.log_likelihood_if_incompatible
1531 );
1532 }
1533 }
1534
1535 fn quantile_from_sorted(samples: &[u128], pct: usize) -> u128 {
1536 let len = samples.len();
1537 assert!(len > 0);
1538 let idx = (len.saturating_sub(1) * pct) / 100;
1539 samples[idx]
1540 }
1541
1542 fn latency_quantiles(mut samples_ns: Vec<u128>) -> (u128, u128, u128) {
1543 samples_ns.sort_unstable();
1544 (
1545 quantile_from_sorted(&samples_ns, 50),
1546 quantile_from_sorted(&samples_ns, 95),
1547 quantile_from_sorted(&samples_ns, 99),
1548 )
1549 }
1550
1551 #[test]
1552 fn asupersync_join_admission_optimized_path_is_isomorphic_to_baseline() {
1553 let policy = RuntimePolicy::hardened(Some(1024));
1554 let mut optimized = EvidenceLedger::new();
1555 let mut baseline = EvidenceLedger::new();
1556
1557 for seed in 0_usize..256 {
1558 let rows = if seed % 2 == 0 {
1559 512 + seed
1560 } else {
1561 4096 + seed
1562 };
1563 let optimized_action = policy.decide_join_admission(rows, &mut optimized);
1564 let baseline_action = decide_join_admission_baseline(&policy, rows, &mut baseline);
1565 assert_eq!(optimized_action, baseline_action);
1566
1567 let optimized_record = optimized.records().last().expect("optimized record");
1568 let baseline_record = baseline.records().last().expect("baseline record");
1569 assert_join_record_equivalent(optimized_record, baseline_record);
1570 }
1571 }
1572
1573 #[test]
1574 fn asupersync_join_admission_profile_snapshot_reports_allocation_delta() {
1575 const ITERATIONS: usize = 256;
1576 let policy = RuntimePolicy::hardened(Some(2048));
1577 let mut optimized = EvidenceLedger::new();
1578 let mut baseline = EvidenceLedger::new();
1579 let mut optimized_ns = Vec::with_capacity(ITERATIONS);
1580 let mut baseline_ns = Vec::with_capacity(ITERATIONS);
1581
1582 for seed in 0_usize..ITERATIONS {
1583 let rows = if seed % 3 == 0 {
1584 1024 + seed
1585 } else {
1586 8192 + seed
1587 };
1588
1589 let baseline_start = Instant::now();
1590 let baseline_action = decide_join_admission_baseline(&policy, rows, &mut baseline);
1591 baseline_ns.push(baseline_start.elapsed().as_nanos());
1592 black_box(baseline_action);
1593
1594 let optimized_start = Instant::now();
1595 let optimized_action = policy.decide_join_admission(rows, &mut optimized);
1596 optimized_ns.push(optimized_start.elapsed().as_nanos());
1597 black_box(optimized_action);
1598 }
1599
1600 for (optimized_record, baseline_record) in
1601 optimized.records().iter().zip(baseline.records())
1602 {
1603 assert_join_record_equivalent(optimized_record, baseline_record);
1604 }
1605
1606 let (baseline_p50_ns, baseline_p95_ns, baseline_p99_ns) = latency_quantiles(baseline_ns);
1607 let (optimized_p50_ns, optimized_p95_ns, optimized_p99_ns) =
1608 latency_quantiles(optimized_ns);
1609 let baseline_name_bytes_per_call =
1610 "estimator_overflow_risk".len() + "memory_budget_signal".len();
1611 let baseline_name_bytes_total = baseline_name_bytes_per_call * ITERATIONS;
1612 let optimized_name_bytes_total = 0_usize;
1613 assert!(baseline_name_bytes_total > optimized_name_bytes_total);
1614
1615 println!(
1616 "asupersync_join_admission_profile_snapshot baseline_ns[p50={baseline_p50_ns},p95={baseline_p95_ns},p99={baseline_p99_ns}] optimized_ns[p50={optimized_p50_ns},p95={optimized_p95_ns},p99={optimized_p99_ns}] name_alloc_bytes_baseline={baseline_name_bytes_total} name_alloc_bytes_optimized={optimized_name_bytes_total}"
1617 );
1618 }
1619
1620 #[test]
1621 fn asupersync_structured_log_contains_required_fields() {
1622 let log = make_structured_log(
1623 "asupersync_structured_log_contains_required_fields",
1624 RuntimeMode::Strict,
1625 42,
1626 "ASUPERSYNC-E/log_schema",
1627 "pass",
1628 );
1629 let value = serde_json::to_value(log).expect("serialize log");
1630 assert_required_log_fields(&value);
1631 }
1632
1633 #[test]
1634 fn asupersync_structured_log_is_deterministic_for_same_inputs() {
1635 let left = make_structured_log(
1636 "asupersync_structured_log_is_deterministic_for_same_inputs",
1637 RuntimeMode::Hardened,
1638 1337,
1639 "ASUPERSYNC-E/log_determinism",
1640 "pass",
1641 );
1642 let right = make_structured_log(
1643 "asupersync_structured_log_is_deterministic_for_same_inputs",
1644 RuntimeMode::Hardened,
1645 1337,
1646 "ASUPERSYNC-E/log_determinism",
1647 "pass",
1648 );
1649 assert_eq!(left, right);
1650 let left_json = serde_json::to_string(&left).expect("left json");
1651 let right_json = serde_json::to_string(&right).expect("right json");
1652 assert_eq!(left_json, right_json);
1653 }
1654
1655 #[test]
1656 fn asupersync_property_strict_unknown_feature_always_rejects() {
1657 let policy = RuntimePolicy::strict();
1658 let mut ledger = EvidenceLedger::new();
1659 let case_id = "asupersync_property_strict_unknown_feature_always_rejects";
1660
1661 for seed in 0_u64..128 {
1662 let action = policy.decide_unknown_feature(
1663 format!("unknown_subject_{seed}"),
1664 format!("unknown_detail_{:08x}", seed.wrapping_mul(37)),
1665 &mut ledger,
1666 );
1667 let log = make_structured_log(
1668 case_id,
1669 RuntimeMode::Strict,
1670 seed,
1671 "ASUPERSYNC-E/strict_unknown_feature_reject",
1672 if action == DecisionAction::Reject {
1673 "pass"
1674 } else {
1675 "fail"
1676 },
1677 );
1678 let log_json = serde_json::to_value(log).expect("serialize log");
1679 assert_required_log_fields(&log_json);
1680 assert_eq!(
1681 action,
1682 DecisionAction::Reject,
1683 "strict mode must reject unknown feature; log={}",
1684 serde_json::to_string(&log_json).expect("json")
1685 );
1686 }
1687
1688 assert_eq!(ledger.records().len(), 128);
1689 }
1690
1691 #[test]
1692 fn asupersync_property_hardened_over_cap_forces_repair() {
1693 let cap = 1024_usize;
1694 let policy = RuntimePolicy::hardened(Some(cap));
1695 let mut ledger = EvidenceLedger::new();
1696 let case_id = "asupersync_property_hardened_over_cap_forces_repair";
1697
1698 for seed in 0_u64..256 {
1699 let rows = if seed % 2 == 0 {
1700 cap + 1 + (seed as usize % 10_000)
1701 } else {
1702 cap.saturating_sub(seed as usize % cap)
1703 };
1704 let action = policy.decide_join_admission(rows, &mut ledger);
1705 let log = make_structured_log(
1706 case_id,
1707 RuntimeMode::Hardened,
1708 seed,
1709 "ASUPERSYNC-E/hardened_join_cap_boundary",
1710 if rows > cap && action == DecisionAction::Repair {
1711 "pass"
1712 } else {
1713 "check"
1714 },
1715 );
1716 let log_json = serde_json::to_value(log).expect("serialize log");
1717 assert_required_log_fields(&log_json);
1718 if rows > cap {
1719 assert_eq!(
1720 action,
1721 DecisionAction::Repair,
1722 "rows over cap must force repair; rows={rows}; log={}",
1723 serde_json::to_string(&log_json).expect("json")
1724 );
1725 }
1726 }
1727 }
1728
1729 #[test]
1730 fn asupersync_property_decision_metrics_are_finite_and_bounded() {
1731 let policy = RuntimePolicy::hardened(Some(2048));
1732 let mut ledger = EvidenceLedger::new();
1733 let case_id = "asupersync_property_decision_metrics_are_finite_and_bounded";
1734
1735 for seed in 0_u64..128 {
1736 let rows = 1 + (seed as usize * 97 % 500_000);
1737 policy.decide_join_admission(rows, &mut ledger);
1738 let record = ledger.records().last().expect("record");
1739 let metrics = &record.metrics;
1740 let posterior = metrics.posterior_compatible;
1741 let bounded = (0.0..=1.0).contains(&posterior);
1742 let finite = metrics
1743 .bayes_factor_compatible_over_incompatible
1744 .is_finite()
1745 && metrics.expected_loss_allow.is_finite()
1746 && metrics.expected_loss_reject.is_finite()
1747 && metrics.expected_loss_repair.is_finite();
1748
1749 let log = make_structured_log(
1750 case_id,
1751 RuntimeMode::Hardened,
1752 seed,
1753 "ASUPERSYNC-E/decision_metrics_finite",
1754 if bounded && finite { "pass" } else { "fail" },
1755 );
1756 let log_json = serde_json::to_value(log).expect("serialize log");
1757 assert_required_log_fields(&log_json);
1758 assert!(bounded, "posterior out of range; log={log_json}");
1759 assert!(finite, "non-finite metrics; log={log_json}");
1760 }
1761 }
1762
1763 #[test]
1764 fn decide_clamps_boundary_priors_to_finite_range() {
1765 for (input_prior, expected_prior) in [
1766 (0.0, super::PRIOR_COMPATIBLE_EPSILON),
1767 (1.0, 1.0 - super::PRIOR_COMPATIBLE_EPSILON),
1768 ] {
1769 let record = super::decide(
1770 RuntimeMode::Strict,
1771 super::CompatibilityIssue {
1772 kind: super::IssueKind::MalformedInput,
1773 subject: "prior_clamp_test".to_owned(),
1774 detail: "boundary prior".to_owned(),
1775 },
1776 input_prior,
1777 super::LossMatrix::default(),
1778 Vec::new(),
1779 );
1780
1781 assert_eq!(
1782 record.prior_compatible, expected_prior,
1783 "prior should be clamped into open interval (0,1)"
1784 );
1785 assert!(
1786 record.metrics.posterior_compatible.is_finite(),
1787 "posterior must remain finite for boundary priors"
1788 );
1789 assert!(
1790 record.metrics.expected_loss_allow.is_finite()
1791 && record.metrics.expected_loss_reject.is_finite()
1792 && record.metrics.expected_loss_repair.is_finite(),
1793 "expected-loss metrics must remain finite for boundary priors"
1794 );
1795 }
1796 }
1797
1798 #[test]
1799 fn decide_normalizes_non_finite_priors_to_neutral() {
1800 for input_prior in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1801 let record = super::decide(
1802 RuntimeMode::Strict,
1803 super::CompatibilityIssue {
1804 kind: super::IssueKind::MalformedInput,
1805 subject: "prior_clamp_test".to_owned(),
1806 detail: "non-finite prior".to_owned(),
1807 },
1808 input_prior,
1809 super::LossMatrix::default(),
1810 Vec::new(),
1811 );
1812
1813 assert_eq!(
1814 record.prior_compatible, 0.5,
1815 "non-finite priors should normalize to neutral prior"
1816 );
1817 assert!(
1818 record.metrics.posterior_compatible.is_finite(),
1819 "posterior must remain finite for non-finite priors"
1820 );
1821 }
1822 }
1823
1824 #[test]
1825 fn asupersync_adversarial_extreme_join_estimate_remains_repair_and_loggable() {
1826 let policy = RuntimePolicy::hardened(Some(8));
1827 let mut ledger = EvidenceLedger::new();
1828 let action = policy.decide_join_admission(usize::MAX, &mut ledger);
1829 assert_eq!(action, DecisionAction::Repair);
1830 let record = ledger.records().last().expect("record");
1831 assert_eq!(record.mode, RuntimeMode::Hardened);
1832 assert!(
1833 record.issue.detail.contains("estimated_rows="),
1834 "issue detail should include estimated_rows"
1835 );
1836
1837 let log = make_structured_log(
1838 "asupersync_adversarial_extreme_join_estimate_remains_repair_and_loggable",
1839 RuntimeMode::Hardened,
1840 u64::MAX,
1841 "ASUPERSYNC-E/adversarial_extreme_rows",
1842 "pass",
1843 );
1844 let log_json = serde_json::to_value(log).expect("serialize log");
1845 assert_required_log_fields(&log_json);
1846 }
1847
1848 #[test]
1849 fn strict_mode_fails_closed_for_unknown_features() {
1850 let mut ledger = EvidenceLedger::new();
1851 let policy = RuntimePolicy::strict();
1852
1853 let action = policy.decide_unknown_feature("csv", "field=experimental", &mut ledger);
1854 assert_eq!(action, DecisionAction::Reject);
1855 assert_eq!(ledger.records()[0].mode, RuntimeMode::Strict);
1856 }
1857
1858 #[test]
1859 fn hardened_mode_repairs_large_join_estimates() {
1860 let mut ledger = EvidenceLedger::new();
1861 let policy = RuntimePolicy::hardened(Some(10_000));
1862
1863 let action = policy.decide_join_admission(100_000, &mut ledger);
1864 assert_eq!(action, DecisionAction::Repair);
1865 assert_eq!(ledger.records().len(), 1);
1866 }
1867
1868 #[test]
1869 fn source_backed_raptorq_envelope_records_manifest_fields() {
1870 let mut source = vec![7_u8; super::DEFAULT_RAPTORQ_SYMBOL_BYTES];
1871 source.extend_from_slice(b"tail");
1872
1873 let envelope = RaptorQEnvelope::from_source_bytes("packet-001", "conformance", &source, 3);
1874
1875 assert_eq!(envelope.artifact_id, "packet-001");
1876 assert_eq!(envelope.artifact_type, "conformance");
1877 assert!(envelope.source_hash.starts_with("sha256:"));
1878 assert_eq!(envelope.source_hash.len(), "sha256:".len() + 64);
1879 assert_eq!(envelope.raptorq.k, 2);
1880 assert_eq!(envelope.raptorq.repair_symbols, 3);
1881 assert_eq!(envelope.raptorq.overhead_ratio, 1.5);
1882 assert_eq!(envelope.raptorq.symbol_hashes.len(), 2);
1883 assert!(
1884 envelope
1885 .raptorq
1886 .symbol_hashes
1887 .iter()
1888 .all(|hash| hash.starts_with("sha256:") && hash.len() == "sha256:".len() + 64)
1889 );
1890 assert_eq!(envelope.scrub.status, "ok");
1891 assert!(envelope.scrub.last_ok_unix_ms > 0);
1892 }
1893
1894 #[test]
1895 fn decode_proof_append_is_capped_and_evicts_oldest() {
1896 let mut envelope =
1897 RaptorQEnvelope::from_source_bytes("packet-001", "conformance", b"source", 1);
1898 let total = super::MAX_DECODE_PROOFS + 5;
1899
1900 for idx in 0..total {
1901 envelope.push_decode_proof_capped(super::DecodeProof {
1902 ts_unix_ms: u64::try_from(idx).expect("idx within u64 range"),
1903 reason: format!("proof-{idx}"),
1904 recovered_blocks: u32::try_from(idx).expect("idx within u32 range"),
1905 proof_hash: format!("sha256:{idx:08x}"),
1906 });
1907 }
1908
1909 assert_eq!(envelope.decode_proofs.len(), super::MAX_DECODE_PROOFS);
1910 assert_eq!(
1911 envelope.decode_proofs[0].proof_hash,
1912 format!("sha256:{:08x}", total - super::MAX_DECODE_PROOFS)
1913 );
1914 assert_eq!(
1915 envelope
1916 .decode_proofs
1917 .last()
1918 .expect("decode proof should exist")
1919 .proof_hash,
1920 format!("sha256:{:08x}", total - 1)
1921 );
1922 }
1923
1924 #[test]
1925 fn decision_card_is_renderable_for_ftui_consumers() {
1926 let mut ledger = EvidenceLedger::new();
1927 let policy = RuntimePolicy::strict();
1928 policy.decide_unknown_feature("csv", "field=experimental", &mut ledger);
1929
1930 let card = decision_to_card(&ledger.records()[0]);
1931 let rendered = card.render_plain();
1932 assert!(rendered.contains("argmin_a"));
1933 assert!(rendered.contains("P(compatible|e)"));
1934
1935 let edge_card = GalaxyBrainCard {
1936 title: "brain {card} ]\nnext".into(),
1937 equation: "lambda -> integral".into(),
1938 substitution: "Unicode: cafe\u{301}, data, 🧠".into(),
1939 intuition: "embedded\nnewlines\nremain".into(),
1940 };
1941 assert_eq!(
1942 edge_card.render_plain(),
1943 "[brain {card} ]\nnext]\nlambda -> integral\nUnicode: cafe\u{301}, data, 🧠\nembedded\nnewlines\nremain"
1944 );
1945 }
1946
1947 #[test]
1948 #[ignore = "foreground profile-first A/B"]
1949 fn galaxy_brain_card_render_plain_profile_lzy5c() {
1950 #[inline(never)]
1951 fn former(card: &GalaxyBrainCard) -> String {
1952 format!(
1953 "[{}]\n{}\n{}\n{}",
1954 card.title, card.equation, card.substitution, card.intuition
1955 )
1956 }
1957
1958 #[inline(never)]
1959 fn candidate(card: &GalaxyBrainCard) -> String {
1960 card.render_plain()
1961 }
1962
1963 fn elapsed(cards: &[GalaxyBrainCard], renderer: fn(&GalaxyBrainCard) -> String) -> u128 {
1964 let started = Instant::now();
1965 let rendered = black_box(cards).iter().map(renderer).collect::<Vec<_>>();
1966 black_box(&rendered);
1967 let elapsed = started.elapsed().as_nanos();
1968 black_box(rendered);
1969 elapsed
1970 }
1971
1972 fn percentile(samples: &[u128], percent: usize) -> u128 {
1973 let mut sorted = samples.to_vec();
1974 sorted.sort_unstable();
1975 let rank = (sorted.len() * percent).div_ceil(100).saturating_sub(1);
1976 sorted[rank]
1977 }
1978
1979 let edge_cards = [
1980 GalaxyBrainCard {
1981 title: String::new(),
1982 equation: String::new(),
1983 substitution: String::new(),
1984 intuition: String::new(),
1985 },
1986 GalaxyBrainCard {
1987 title: "csv::Reject".into(),
1988 equation: "argmin_a sum_s L(a,s) P(s|evidence)".into(),
1989 substitution: "P(compatible|e)=0.1250, E[allow]=9.5".into(),
1990 intuition: "Lower expected loss wins.".into(),
1991 },
1992 GalaxyBrainCard {
1993 title: "brain {card} ]\nnext".into(),
1994 equation: "lambda -> integral".into(),
1995 substitution: "Unicode: cafe\u{301}, data, 🧠".into(),
1996 intuition: "embedded\nnewlines\nremain".into(),
1997 },
1998 ];
1999 for card in &edge_cards {
2000 assert_eq!(candidate(card), former(card));
2001 }
2002
2003 const CARDS: usize = 4_096;
2004 const SAMPLES: usize = 12;
2005 let cards = (0..CARDS)
2006 .map(|index| GalaxyBrainCard {
2007 title: format!("packet-{index:04}::{:?}", index % 3),
2008 equation: "argmin_a sum_s L(a,s) P(s|evidence)".into(),
2009 substitution: format!(
2010 "P(compatible|e)=0.{:04}, E[allow]={:.4}, E[reject]={:.4}",
2011 index % 10_000,
2012 (index % 97) as f64 / 7.0,
2013 (index % 89) as f64 / 11.0
2014 ),
2015 intuition: if index % 7 == 0 {
2016 "Strict mode may force fail-closed. 🧠".into()
2017 } else {
2018 "Lower expected loss wins; evidence remains auditable.".into()
2019 },
2020 })
2021 .collect::<Vec<_>>();
2022 for card in &cards {
2023 assert_eq!(candidate(card), former(card));
2024 }
2025
2026 for _ in 0..2 {
2027 black_box(elapsed(&cards, former));
2028 black_box(elapsed(&cards, candidate));
2029 }
2030 let mut former_ns = Vec::with_capacity(SAMPLES);
2031 let mut candidate_ns = Vec::with_capacity(SAMPLES);
2032 for sample in 0..SAMPLES {
2033 if sample % 2 == 0 {
2034 former_ns.push(elapsed(&cards, former));
2035 candidate_ns.push(elapsed(&cards, candidate));
2036 } else {
2037 candidate_ns.push(elapsed(&cards, candidate));
2038 former_ns.push(elapsed(&cards, former));
2039 }
2040 }
2041
2042 let former_p50 = percentile(&former_ns, 50);
2043 let former_p95 = percentile(&former_ns, 95);
2044 let former_p99 = percentile(&former_ns, 99);
2045 let candidate_p50 = percentile(&candidate_ns, 50);
2046 let candidate_p95 = percentile(&candidate_ns, 95);
2047 let candidate_p99 = percentile(&candidate_ns, 99);
2048 println!(
2049 "GALAXY_CARD_RENDER cards={CARDS} former_p50_ns={former_p50} candidate_p50_ns={candidate_p50} speedup_p50={:.6} former_p95_ns={former_p95} candidate_p95_ns={candidate_p95} speedup_p95={:.6} former_p99_ns={former_p99} candidate_p99_ns={candidate_p99} speedup_p99={:.6} former_samples={former_ns:?} candidate_samples={candidate_ns:?}",
2050 former_p50 as f64 / candidate_p50 as f64,
2051 former_p95 as f64 / candidate_p95 as f64,
2052 former_p99 as f64 / candidate_p99 as f64,
2053 );
2054 }
2055
2056 #[test]
2059 fn conformal_guard_uncalibrated_accepts_all() {
2060 let mut guard = ConformalGuard::new(100, 0.1);
2061 assert!(!guard.is_calibrated());
2062
2063 let mut ledger = EvidenceLedger::new();
2064 let policy = RuntimePolicy::strict();
2065 policy.decide_unknown_feature("test", "detail", &mut ledger);
2066
2067 let ps = guard.evaluate(&ledger.records()[0]);
2068 assert!(ps.bayesian_action_in_set);
2069 assert_eq!(ps.admissible_actions.len(), 3); assert_eq!(ps.quantile_threshold, f64::INFINITY);
2071 }
2072
2073 #[test]
2074 fn conformal_guard_calibrates_after_sufficient_data() {
2075 let mut guard = ConformalGuard::new(100, 0.1);
2076 let mut ledger = EvidenceLedger::new();
2077 let policy = RuntimePolicy::hardened(Some(100_000));
2078
2079 for _ in 0..10 {
2081 policy.decide_join_admission(50_000, &mut ledger);
2082 }
2083
2084 for record in ledger.records() {
2085 guard.evaluate(record);
2086 }
2087
2088 assert!(guard.is_calibrated());
2089 assert!(guard.conformal_quantile().is_some());
2090 assert_eq!(guard.calibration_count(), 10);
2091 }
2092
2093 #[test]
2094 fn conformal_guard_rolling_window_evicts_old_scores() {
2095 let mut guard = ConformalGuard::new(5, 0.1);
2096 let mut ledger = EvidenceLedger::new();
2097 let policy = RuntimePolicy::hardened(Some(100_000));
2098
2099 for _ in 0..10 {
2100 policy.decide_join_admission(1000, &mut ledger);
2101 }
2102
2103 for record in ledger.records() {
2104 guard.evaluate(record);
2105 }
2106
2107 assert_eq!(guard.calibration_count(), 5);
2109 }
2110
2111 #[test]
2112 fn conformal_guard_coverage_tracking() {
2113 let mut guard = ConformalGuard::new(50, 0.1);
2114 let mut ledger = EvidenceLedger::new();
2115 let policy = RuntimePolicy::hardened(Some(100_000));
2116
2117 for _ in 0..20 {
2119 policy.decide_join_admission(1000, &mut ledger);
2120 }
2121
2122 for record in ledger.records() {
2123 guard.evaluate(record);
2124 }
2125
2126 let coverage = guard.empirical_coverage();
2128 assert!(coverage > 0.5, "coverage should be reasonable: {coverage}");
2129 }
2130
2131 #[test]
2132 fn conformal_guard_no_coverage_alert_under_100_decisions() {
2133 let mut guard = ConformalGuard::new(100, 0.1);
2134 let mut ledger = EvidenceLedger::new();
2135 let policy = RuntimePolicy::hardened(Some(100_000));
2136
2137 for _ in 0..10 {
2138 policy.decide_join_admission(1000, &mut ledger);
2139 }
2140 for record in ledger.records() {
2141 guard.evaluate(record);
2142 }
2143
2144 assert!(!guard.coverage_alert());
2146 }
2147
2148 #[test]
2149 fn conformal_guard_zero_window_size_is_clamped() {
2150 let mut guard = ConformalGuard::new(0, 0.1);
2151 let mut ledger = EvidenceLedger::new();
2152 let policy = RuntimePolicy::hardened(Some(100_000));
2153
2154 policy.decide_join_admission(1000, &mut ledger);
2155 let set = guard.evaluate(&ledger.records()[0]);
2156
2157 assert!(set.bayesian_action_in_set);
2158 assert_eq!(guard.calibration_count(), 1);
2159 }
2160
2161 #[test]
2162 fn conformal_guard_non_finite_alpha_uses_default() {
2163 let guard = ConformalGuard::new(100, f64::NAN);
2164 assert_eq!(guard.alpha, super::DEFAULT_CONFORMAL_ALPHA);
2165 assert!(!guard.coverage_alert());
2166 }
2167
2168 #[test]
2169 fn conformal_guard_repairs_deserialized_zero_window_before_evaluate() {
2170 let mut guard: ConformalGuard = serde_json::from_str(
2171 r#"{"scores":[],"window_size":0,"alpha":0.1,"in_set_count":0,"total_count":0}"#,
2172 )
2173 .expect("deserialize guard");
2174 let mut ledger = EvidenceLedger::new();
2175 let policy = RuntimePolicy::hardened(Some(100_000));
2176
2177 policy.decide_join_admission(1000, &mut ledger);
2178 let set = guard.evaluate(&ledger.records()[0]);
2179
2180 assert!(set.bayesian_action_in_set);
2181 assert_eq!(guard.window_size, 1);
2182 assert_eq!(guard.calibration_count(), 1);
2183 }
2184
2185 #[test]
2186 fn conformal_quantile_ignores_non_finite_persisted_scores() {
2187 let guard = ConformalGuard {
2188 scores: vec![f64::NAN, f64::INFINITY, 1.0, 2.0],
2189 window_size: 10,
2190 alpha: f64::NAN,
2191 in_set_count: 5,
2192 total_count: 3,
2193 };
2194
2195 assert!(guard.is_calibrated());
2196 assert_eq!(guard.conformal_quantile(), Some(2.0));
2197 assert_eq!(guard.empirical_coverage(), 1.0);
2198 }
2199
2200 #[test]
2201 fn conformal_normalized_quantile_matches_robust_path_qckka() {
2202 let mut state = 0xa076_1d64_78bd_642f_u64;
2203 let random_scores = (0..1_000)
2204 .map(|_| {
2205 state = state
2206 .wrapping_mul(6_364_136_223_846_793_005)
2207 .wrapping_add(1_442_695_040_888_963_407);
2208 (state >> 11) as f64 / ((1_u64 << 53) as f64)
2209 })
2210 .collect::<Vec<_>>();
2211 let cases = [
2212 (Vec::new(), 0, f64::NAN),
2213 (vec![f64::NAN, 1.0, f64::INFINITY], 8, 0.1),
2214 (
2215 vec![f64::NEG_INFINITY, -0.0, 0.0, 1.0, 1.0, f64::MAX],
2216 4,
2217 f64::INFINITY,
2218 ),
2219 (random_scores, 257, 0.99),
2220 ];
2221
2222 for (scores, window_size, alpha) in cases {
2223 let mut guard = ConformalGuard {
2224 scores,
2225 window_size,
2226 alpha,
2227 in_set_count: 9,
2228 total_count: 4,
2229 };
2230 guard.normalize_runtime_config();
2231 let former = guard.conformal_quantile().map(f64::to_bits);
2232 let candidate = super::select_conformal_quantile(guard.scores.clone(), guard.alpha)
2233 .map(f64::to_bits);
2234 assert_eq!(candidate, former, "window_size={window_size} alpha={alpha}");
2235 }
2236 }
2237
2238 #[test]
2239 fn conformal_evaluate_preserves_observables_qckka() {
2240 let mut ledger = EvidenceLedger::new();
2241 RuntimePolicy::hardened(Some(100_000)).decide_join_admission(1_000, &mut ledger);
2242 let record = &ledger.records()[0];
2243 let mut guard = ConformalGuard {
2244 scores: vec![f64::NAN, 0.25, 1.5, f64::INFINITY, -0.0, 2.5],
2245 window_size: 4,
2246 alpha: f64::NAN,
2247 in_set_count: 9,
2248 total_count: 4,
2249 };
2250 let mut expected_guard = guard.clone();
2251 expected_guard.normalize_runtime_config();
2252 let expected_threshold = expected_guard
2253 .conformal_quantile()
2254 .expect("normalized fixture is calibrated");
2255 let expected_score = super::nonconformity_score(record);
2256 if expected_guard.scores.len() >= expected_guard.window_size {
2257 expected_guard.scores.remove(0);
2258 }
2259 expected_guard.scores.push(expected_score);
2260 let expected_in_set = expected_score <= expected_threshold;
2261 let expected_actions = if expected_in_set {
2262 vec![record.action]
2263 } else {
2264 vec![
2265 DecisionAction::Allow,
2266 DecisionAction::Reject,
2267 DecisionAction::Repair,
2268 ]
2269 };
2270 expected_guard.total_count += 1;
2271 if expected_in_set {
2272 expected_guard.in_set_count += 1;
2273 }
2274 let expected_set = super::ConformalPredictionSet {
2275 quantile_threshold: expected_threshold,
2276 current_score: expected_score,
2277 bayesian_action_in_set: expected_in_set,
2278 admissible_actions: expected_actions,
2279 empirical_coverage: expected_guard.in_set_count as f64
2280 / expected_guard.total_count as f64,
2281 };
2282
2283 let actual_set = guard.evaluate(record);
2284 assert_eq!(actual_set, expected_set);
2285 assert_eq!(
2286 serde_json::to_vec(&actual_set).expect("serialize actual prediction set"),
2287 serde_json::to_vec(&expected_set).expect("serialize expected prediction set")
2288 );
2289 assert_eq!(
2290 serde_json::to_vec(&guard).expect("serialize actual guard"),
2291 serde_json::to_vec(&expected_guard).expect("serialize expected guard")
2292 );
2293 }
2294
2295 #[test]
2296 fn conformal_guard_quantile_is_deterministic() {
2297 let mut guard = ConformalGuard::new(100, 0.1);
2298 let mut ledger = EvidenceLedger::new();
2299 let policy = RuntimePolicy::hardened(Some(100_000));
2300
2301 for _ in 0..5 {
2302 policy.decide_join_admission(1000, &mut ledger);
2303 }
2304 for record in ledger.records() {
2305 guard.evaluate(record);
2306 }
2307
2308 let q1 = guard.conformal_quantile();
2309 let q2 = guard.conformal_quantile();
2310 assert_eq!(q1, q2);
2311 }
2312
2313 fn full_sort_conformal_quantile(guard: &ConformalGuard) -> Option<f64> {
2314 let mut sorted = guard
2315 .scores
2316 .iter()
2317 .copied()
2318 .filter(|score| score.is_finite())
2319 .collect::<Vec<_>>();
2320 if sorted.len() < 2 {
2321 return None;
2322 }
2323 sorted.sort_by(f64::total_cmp);
2324 let n = sorted.len() as f64;
2325 let level = (1.0 - super::normalize_conformal_alpha(guard.alpha)) * (1.0 + 1.0 / n);
2326 let idx = (level * n).ceil() as usize;
2327 let idx = idx.min(sorted.len()).saturating_sub(1);
2328 Some(sorted[idx])
2329 }
2330
2331 #[test]
2332 #[ignore = "foreground normal-release attribution probe"]
2333 fn conformal_normalized_window_profile_qckka() {
2334 const WINDOW: usize = 1_000;
2335 const BATCH: usize = 128;
2336 const SAMPLES: usize = 15;
2337
2338 fn normalized_quantile(guard: &ConformalGuard) -> Option<f64> {
2339 super::select_conformal_quantile(guard.scores.clone(), guard.alpha)
2340 }
2341
2342 fn elapsed(guard: &ConformalGuard, normalized: bool) -> u128 {
2343 let started = Instant::now();
2344 let mut digest = 0_u64;
2345 for _ in 0..BATCH {
2346 let quantile = if normalized {
2347 normalized_quantile(black_box(guard))
2348 } else {
2349 black_box(guard).conformal_quantile()
2350 };
2351 digest = digest.wrapping_add(quantile.map_or(0, f64::to_bits));
2352 }
2353 black_box(digest);
2354 started.elapsed().as_nanos() / BATCH as u128
2355 }
2356
2357 fn percentile(samples: &mut [u128], pct: usize) -> u128 {
2358 samples.sort_unstable();
2359 let rank = (samples.len() * pct).div_ceil(100).saturating_sub(1);
2360 samples[rank]
2361 }
2362
2363 let mut state = 0xa076_1d64_78bd_642f_u64;
2364 let scores = (0..WINDOW)
2365 .map(|_| {
2366 state = state
2367 .wrapping_mul(6_364_136_223_846_793_005)
2368 .wrapping_add(1_442_695_040_888_963_407);
2369 (state >> 11) as f64 / ((1_u64 << 53) as f64)
2370 })
2371 .collect::<Vec<_>>();
2372 let guard = ConformalGuard {
2373 scores,
2374 window_size: WINDOW,
2375 alpha: 0.1,
2376 in_set_count: 0,
2377 total_count: 0,
2378 };
2379 assert_eq!(
2380 guard.conformal_quantile().map(f64::to_bits),
2381 normalized_quantile(&guard).map(f64::to_bits)
2382 );
2383
2384 for _ in 0..3 {
2385 black_box(elapsed(&guard, false));
2386 black_box(elapsed(&guard, true));
2387 }
2388 let mut filtered = Vec::with_capacity(SAMPLES * 2);
2389 let mut normalized = Vec::with_capacity(SAMPLES * 2);
2390 for sample in 0_usize..SAMPLES {
2391 if sample.is_multiple_of(2) {
2392 filtered.push(elapsed(&guard, false));
2393 normalized.push(elapsed(&guard, true));
2394 normalized.push(elapsed(&guard, true));
2395 filtered.push(elapsed(&guard, false));
2396 } else {
2397 normalized.push(elapsed(&guard, true));
2398 filtered.push(elapsed(&guard, false));
2399 filtered.push(elapsed(&guard, false));
2400 normalized.push(elapsed(&guard, true));
2401 }
2402 }
2403 let filtered_p50 = percentile(&mut filtered, 50);
2404 let normalized_p50 = percentile(&mut normalized, 50);
2405 let filtered_p95 = percentile(&mut filtered, 95);
2406 let normalized_p95 = percentile(&mut normalized, 95);
2407 eprintln!(
2408 "CONFORMAL_NORMALIZED_PROFILE window={WINDOW} batch={BATCH} filtered_p50_ns={filtered_p50} normalized_p50_ns={normalized_p50} ratio={:.6} filtered_p95_ns={filtered_p95} normalized_p95_ns={normalized_p95}",
2409 filtered_p50 as f64 / normalized_p50 as f64
2410 );
2411 eprintln!("CONFORMAL_NORMALIZED_PROFILE filtered_distribution_ns={filtered:?}");
2412 eprintln!("CONFORMAL_NORMALIZED_PROFILE normalized_distribution_ns={normalized:?}");
2413 }
2414
2415 #[test]
2416 fn conformal_quantile_selection_matches_full_sort_bh91q() {
2417 let mut state = 0xd1b5_4a32_d192_ed03_u64;
2418 let mut random_scores = Vec::with_capacity(1_005);
2419 for _ in 0..1_000 {
2420 state = state
2421 .wrapping_mul(6_364_136_223_846_793_005)
2422 .wrapping_add(1_442_695_040_888_963_407);
2423 random_scores.push((state >> 11) as f64 / ((1_u64 << 53) as f64));
2424 }
2425 random_scores.extend([f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.0, 0.0]);
2426 let cases = [
2427 Vec::new(),
2428 vec![1.0],
2429 vec![f64::NAN, f64::INFINITY, 2.0],
2430 vec![-0.0, 0.0, -1.0, 1.0, 1.0, f64::MAX, f64::MIN],
2431 random_scores,
2432 ];
2433
2434 for scores in cases {
2435 for alpha in [0.01, 0.1, 0.5, 0.99, f64::NAN, f64::INFINITY] {
2436 let guard = ConformalGuard {
2437 window_size: scores.len().max(1),
2438 scores: scores.clone(),
2439 alpha,
2440 in_set_count: 0,
2441 total_count: 0,
2442 };
2443 let former = full_sort_conformal_quantile(&guard).map(f64::to_bits);
2444 let candidate = guard.conformal_quantile().map(f64::to_bits);
2445 assert_eq!(candidate, former, "len={} alpha={alpha}", scores.len());
2446 }
2447 }
2448 }
2449
2450 #[test]
2451 #[ignore = "foreground performance probe"]
2452 fn conformal_quantile_selection_ab_bh91q() {
2453 const WINDOW: usize = 1_000;
2454 const BATCH: usize = 64;
2455 const SAMPLES: usize = 31;
2456 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
2457 let scores = (0..WINDOW)
2458 .map(|_| {
2459 state = state
2460 .wrapping_mul(6_364_136_223_846_793_005)
2461 .wrapping_add(1_442_695_040_888_963_407);
2462 (state >> 11) as f64 / ((1_u64 << 53) as f64)
2463 })
2464 .collect::<Vec<_>>();
2465 let guard = ConformalGuard {
2466 scores,
2467 window_size: WINDOW,
2468 alpha: 0.1,
2469 in_set_count: 0,
2470 total_count: 0,
2471 };
2472
2473 let former = full_sort_conformal_quantile(&guard).map(f64::to_bits);
2474 let candidate = guard.conformal_quantile().map(f64::to_bits);
2475 assert_eq!(candidate, former);
2476
2477 let measure_former = || {
2478 let started = Instant::now();
2479 let mut digest = 0_u64;
2480 for _ in 0..BATCH {
2481 let quantile = full_sort_conformal_quantile(black_box(&guard));
2482 digest = digest.wrapping_add(quantile.map_or(0, f64::to_bits));
2483 }
2484 black_box(digest);
2485 started.elapsed().as_nanos() / BATCH as u128
2486 };
2487 let measure_candidate = || {
2488 let started = Instant::now();
2489 let mut digest = 0_u64;
2490 for _ in 0..BATCH {
2491 let quantile = black_box(&guard).conformal_quantile();
2492 digest = digest.wrapping_add(quantile.map_or(0, f64::to_bits));
2493 }
2494 black_box(digest);
2495 started.elapsed().as_nanos() / BATCH as u128
2496 };
2497
2498 for _ in 0..3 {
2499 black_box(measure_former());
2500 black_box(measure_candidate());
2501 }
2502 let mut former_a = Vec::with_capacity(SAMPLES);
2503 let mut former_b = Vec::with_capacity(SAMPLES);
2504 let mut candidate_a = Vec::with_capacity(SAMPLES);
2505 let mut candidate_b = Vec::with_capacity(SAMPLES);
2506 for sample in 0..SAMPLES {
2507 if sample.is_multiple_of(2) {
2508 former_a.push(measure_former());
2509 candidate_a.push(measure_candidate());
2510 candidate_b.push(measure_candidate());
2511 former_b.push(measure_former());
2512 } else {
2513 former_b.push(measure_former());
2514 candidate_b.push(measure_candidate());
2515 candidate_a.push(measure_candidate());
2516 former_a.push(measure_former());
2517 }
2518 }
2519 former_a.sort_unstable();
2520 former_b.sort_unstable();
2521 candidate_a.sort_unstable();
2522 candidate_b.sort_unstable();
2523 let percentile = |samples: &[u128], pct: usize| {
2524 let rank = (samples.len() * pct).div_ceil(100).saturating_sub(1);
2525 samples[rank]
2526 };
2527 let former_a_p50 = percentile(&former_a, 50);
2528 let former_b_p50 = percentile(&former_b, 50);
2529 let candidate_a_p50 = percentile(&candidate_a, 50);
2530 let candidate_b_p50 = percentile(&candidate_b, 50);
2531 let former_mean = (former_a_p50 + former_b_p50) as f64 / 2.0;
2532 let candidate_mean = (candidate_a_p50 + candidate_b_p50) as f64 / 2.0;
2533 println!(
2534 "fp-runtime conformal quantile A/B: window={WINDOW} batch={BATCH} samples={SAMPLES}"
2535 );
2536 println!("former full-sort p50 A/B: {former_a_p50} / {former_b_p50} ns");
2537 println!("candidate selection p50 A/B: {candidate_a_p50} / {candidate_b_p50} ns");
2538 println!(
2539 "former full-sort p95/p99 A: {} / {} ns; B: {} / {} ns",
2540 percentile(&former_a, 95),
2541 percentile(&former_a, 99),
2542 percentile(&former_b, 95),
2543 percentile(&former_b, 99)
2544 );
2545 println!(
2546 "candidate selection p95/p99 A: {} / {} ns; B: {} / {} ns",
2547 percentile(&candidate_a, 95),
2548 percentile(&candidate_a, 99),
2549 percentile(&candidate_b, 95),
2550 percentile(&candidate_b, 99)
2551 );
2552 println!(
2553 "former/candidate ratio: {:.6}x",
2554 former_mean / candidate_mean
2555 );
2556 }
2557
2558 #[test]
2562 fn conformal_quantile_basic() {
2563 let mut guard = ConformalGuard::new(100, 0.1);
2564 let mut ledger = EvidenceLedger::new();
2566 let policy = RuntimePolicy::hardened(Some(100_000));
2567
2568 for _ in 0..5 {
2570 policy.decide_join_admission(1000, &mut ledger);
2571 }
2572 for record in ledger.records() {
2573 guard.evaluate(record);
2574 }
2575
2576 let q = guard.conformal_quantile();
2577 assert!(q.is_some());
2578 let quantile = q.unwrap();
2579 assert!(quantile.is_finite(), "quantile must be finite: {quantile}");
2580 assert!(quantile >= 0.0, "quantile must be non-negative: {quantile}");
2581 }
2582
2583 #[test]
2585 fn conformal_quantile_trivial() {
2586 let mut guard = ConformalGuard::new(100, 0.1);
2587 let mut ledger = EvidenceLedger::new();
2588 let policy = RuntimePolicy::hardened(Some(100_000));
2589
2590 policy.decide_join_admission(1000, &mut ledger);
2592 policy.decide_join_admission(1000, &mut ledger);
2593 guard.evaluate(&ledger.records()[0]);
2594 guard.evaluate(&ledger.records()[1]);
2595
2596 let q = guard.conformal_quantile();
2597 assert!(q.is_some());
2598 }
2599
2600 #[test]
2602 fn conformal_quantile_empty() {
2603 let guard = ConformalGuard::new(100, 0.1);
2604 assert!(guard.conformal_quantile().is_none());
2605 assert!(!guard.is_calibrated());
2606 }
2607
2608 #[test]
2611 fn conformal_guard_agrees_with_bayesian() {
2612 let mut guard = ConformalGuard::new(100, 0.1);
2613 let mut ledger = EvidenceLedger::new();
2614 let policy = RuntimePolicy::hardened(Some(100_000));
2615
2616 for _ in 0..20 {
2618 policy.decide_join_admission(1000, &mut ledger);
2619 }
2620
2621 let mut bayesian_agreed = 0;
2622 let mut total = 0;
2623
2624 for record in ledger.records() {
2625 let ps = guard.evaluate(record);
2626 total += 1;
2627 if ps.bayesian_action_in_set && ps.admissible_actions.len() == 1 {
2628 assert_eq!(ps.admissible_actions[0], record.action);
2630 bayesian_agreed += 1;
2631 }
2632 }
2633
2634 assert!(total > 0, "should have evaluated at least one decision");
2636 assert!(
2638 bayesian_agreed > 0 || total < 3,
2639 "at least some decisions should agree with Bayesian"
2640 );
2641 }
2642
2643 #[test]
2646 fn conformal_guard_widens_on_uncertainty() {
2647 let mut guard = ConformalGuard::new(10, 0.1);
2648 let mut ledger = EvidenceLedger::new();
2649 let policy = RuntimePolicy::hardened(Some(100_000));
2650
2651 for _ in 0..10 {
2653 policy.decide_join_admission(100, &mut ledger);
2654 }
2655 for record in ledger.records() {
2656 guard.evaluate(record);
2657 }
2658
2659 let mut outlier_ledger = EvidenceLedger::new();
2661 let extreme_policy = RuntimePolicy::hardened(Some(10));
2662 extreme_policy.decide_join_admission(1_000_000, &mut outlier_ledger);
2663
2664 let ps = guard.evaluate(&outlier_ledger.records()[0]);
2665 if !ps.bayesian_action_in_set {
2667 assert_eq!(
2668 ps.admissible_actions.len(),
2669 3,
2670 "widened set should admit all actions"
2671 );
2672 }
2673 }
2674
2675 #[test]
2677 fn conformal_coverage_guarantee_1000_decisions() {
2678 let mut guard = ConformalGuard::new(1000, 0.1);
2679 let mut ledger = EvidenceLedger::new();
2680 let policy = RuntimePolicy::hardened(Some(100_000));
2681
2682 for i in 0..1000 {
2684 let rows = 1000 + (i * 7) % 500;
2686 policy.decide_join_admission(rows, &mut ledger);
2687 }
2688
2689 for record in ledger.records() {
2690 guard.evaluate(record);
2691 }
2692
2693 let coverage = guard.empirical_coverage();
2696 assert!(
2697 coverage >= 0.7,
2698 "coverage {coverage} should be >= 0.7 (relaxed bound for finite sample)"
2699 );
2700 }
2701
2702 #[test]
2704 fn conformal_rolling_window_exact_eviction() {
2705 let window_size = 5;
2706 let mut guard = ConformalGuard::new(window_size, 0.1);
2707 let mut ledger = EvidenceLedger::new();
2708 let policy = RuntimePolicy::hardened(Some(100_000));
2709
2710 for _ in 0..15 {
2712 policy.decide_join_admission(1000, &mut ledger);
2713 }
2714
2715 for record in ledger.records() {
2716 guard.evaluate(record);
2717 }
2718
2719 assert_eq!(
2720 guard.calibration_count(),
2721 window_size,
2722 "window should be exactly {window_size}"
2723 );
2724 }
2725
2726 #[test]
2729 fn conformal_galaxy_brain_card_content() {
2730 let mut ledger = EvidenceLedger::new();
2731 let policy = RuntimePolicy::hardened(Some(100_000));
2732 policy.decide_join_admission(50_000, &mut ledger);
2733
2734 let card = decision_to_card(&ledger.records()[0]);
2735 assert!(card.equation.contains("argmin_a"));
2736 assert!(card.substitution.contains("P(compatible|e)"));
2737 assert!(card.substitution.contains("E[allow]"));
2738 assert!(card.substitution.contains("E[reject]"));
2739 assert!(card.substitution.contains("E[repair]"));
2740 }
2741
2742 #[test]
2743 fn conformal_prediction_set_serializes() {
2744 let mut guard = ConformalGuard::new(100, 0.1);
2745 let mut ledger = EvidenceLedger::new();
2746 let policy = RuntimePolicy::hardened(Some(100_000));
2747 policy.decide_join_admission(1000, &mut ledger);
2748
2749 let ps = guard.evaluate(&ledger.records()[0]);
2750 let json = serde_json::to_string(&ps).expect("serialize");
2751 let _: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
2752 assert!(json.contains("quantile_threshold"));
2753 assert!(json.contains("empirical_coverage"));
2754 }
2755}