Skip to main content

fp_runtime/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(rustdoc::broken_intra_doc_links)]
3
4//! Runtime policy + decision-recording layer for **frankenpandas**.
5//!
6//! Pandas operations frequently hit "do we accept this input or fail
7//! closed?" decisions: a dtype that doesn't quite match, a frequency
8//! that's almost-but-not-quite regular, an alignment that produces
9//! NaNs the user maybe didn't expect. fp-runtime gives the rest of
10//! the workspace a single place to record those decisions, score
11//! their compatibility, and persist a verifiable evidence trail so
12//! pipelines can audit "why did the IO layer / groupby / merge make
13//! this choice on this input?" after the fact.
14//!
15//! ## Decision recording
16//!
17//! - [`RuntimePolicy`]: the active policy bundle — mode, fail-closed
18//!   flags, decision thresholds. Constructed once per pipeline and
19//!   threaded through hot-path code.
20//! - [`RuntimeMode`]: the top-level mode (Permissive / Hardened /
21//!   Strict) controlling how aggressively the policy fails on
22//!   ambiguity.
23//! - [`EvidenceLedger`]: append-only log of [`DecisionRecord`]
24//!   entries. Thread it through long-running pipelines to capture
25//!   every decision made; serialize at the end for audit.
26//! - [`DecisionRecord`]: one decision's structured trail —
27//!   [`DecisionAction`] taken, the [`DecisionMetrics`] /
28//!   [`LossMatrix`] / [`EvidenceTerm`] inputs, any
29//!   [`CompatibilityIssue`] entries surfaced.
30//! - [`GalaxyBrainCard`]: human-readable summary of one decision
31//!   suitable for surfacing in IDE plugins or CI logs.
32//! - [`decision_to_card`]: convert a [`DecisionRecord`] to a card.
33//!
34//! ## Conformal prediction guards
35//!
36//! - [`ConformalGuard`]: rolling-window nonconformity calibration
37//!   used to gate uncertain decisions inside hot-path code (e.g.
38//!   "this dtype inference is too unsure — fail closed").
39//! - [`ConformalPredictionSet`]: the calibrated prediction set
40//!   (inclusion / exclusion of candidate labels) the guard
41//!   produces.
42//!
43//! ## RaptorQ envelopes
44//!
45//! - [`RaptorQEnvelope`] / [`RaptorQMetadata`] / [`ScrubStatus`] /
46//!   [`DecodeProof`]: forward-error-correction envelope types used
47//!   for verifying / scrubbing on-disk artifacts that the runtime
48//!   policy needs to trust.
49//!
50//! ## Error reporting
51//!
52//! - [`RuntimeError`]: structural errors in policy construction or
53//!   ledger serialization.
54//! - [`IssueKind`]: enum tagging the category of a
55//!   [`CompatibilityIssue`].
56//!
57//! ## Cargo features
58//!
59//! - `asupersync` (off by default): enables the `asupersync`
60//!   submodule and the `outcome_to_action` helper for converting
61//!   an `asupersync::Outcome` into a [`DecisionAction`]. Pulls in
62//!   the `asupersync` crate as an optional dep. (Items are gated
63//!   behind the feature so they don't appear in the default
64//!   docs.rs render.)
65
66use 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    // Runtime-only switch: when false, callers that build a throwaway ledger
310    // (e.g. the public `Series::add` convenience wrappers that discard the
311    // ledger) can skip the expensive semantic-witness fingerprinting. Not part
312    // of the serialized audit artifact, and `#[serde(skip)]` keeps the on-disk
313    // format unchanged. Per br-frankenpandas-b75cc.
314    #[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    /// Build a ledger that does not record semantic witnesses. Used by the
335    /// public arithmetic convenience methods that discard their ledger, so the
336    /// AACE witness fingerprint (a sha256 over the index) is not computed when
337    /// nothing will read it. Observable operation output is unaffected.
338    #[must_use]
339    pub fn without_semantic_witnesses(mut self) -> Self {
340        self.record_semantic_witnesses = false;
341        self
342    }
343
344    /// Whether this ledger records AACE semantic witnesses (default true).
345    #[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    /// Append a decode proof while enforcing a bounded history size.
610    ///
611    /// When the cap is exceeded, oldest proofs are evicted first.
612    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
688// === Conformal Calibration for Decision Engine (bd-2t5e.9, AG-09) ===
689
690/// Nonconformity score computed from a single decision record.
691/// Higher score = more "strange" relative to calibration window.
692fn nonconformity_score(record: &DecisionRecord) -> f64 {
693    // Score is the absolute log-posterior-odds: high when decision is extreme
694    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    // Quantile at level (1 - alpha)(1 + 1/n) per split conformal prediction.
714    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/// Conformal prediction set: which actions are admissible at significance level alpha.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724pub struct ConformalPredictionSet {
725    /// The conformal quantile threshold at significance level alpha.
726    pub quantile_threshold: f64,
727    /// The nonconformity score of the current decision.
728    pub current_score: f64,
729    /// Whether the Bayesian argmin action is inside the conformal set.
730    pub bayesian_action_in_set: bool,
731    /// Actions that are admissible (score <= threshold).
732    pub admissible_actions: Vec<DecisionAction>,
733    /// Empirical coverage rate over the calibration window.
734    pub empirical_coverage: f64,
735}
736
737/// Calibration window for conformal guard.
738#[derive(Debug, Clone, Serialize, Deserialize)]
739pub struct ConformalGuard {
740    /// Rolling window of nonconformity scores.
741    scores: Vec<f64>,
742    /// Maximum window size.
743    window_size: usize,
744    /// Significance level (e.g., 0.1 for 90% coverage).
745    alpha: f64,
746    /// Count of decisions where Bayesian action was in the conformal set.
747    in_set_count: usize,
748    /// Total decisions evaluated.
749    total_count: usize,
750}
751
752impl ConformalGuard {
753    /// Create a new conformal guard with the given window size and significance level.
754    #[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    /// Default: 1000-element window, alpha=0.1 (90% coverage guarantee).
767    #[must_use]
768    pub fn default_config() -> Self {
769        Self::new(1000, 0.1)
770    }
771
772    /// Compute the conformal quantile from the calibration window.
773    /// Returns None if the window has fewer than 2 scores.
774    #[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    /// Evaluate a decision record against the conformal guard.
786    /// Returns the prediction set and whether the Bayesian action is admissible.
787    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        // Add score to calibration window (rolling)
795        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                // Insufficient calibration data: accept all actions
804                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        // Determine which actions would have scores <= threshold
823        // For now, if the Bayesian action is in set, it's the only admissible one.
824        // If not, we admit all actions (conformal guard widens the set).
825        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    /// Current empirical coverage rate.
856    #[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    /// Number of scores in the calibration window.
865    #[must_use]
866    pub fn calibration_count(&self) -> usize {
867        self.scores.len()
868    }
869
870    /// Whether the calibration window has sufficient data.
871    #[must_use]
872    pub fn is_calibrated(&self) -> bool {
873        self.scores.iter().filter(|score| score.is_finite()).count() >= 2
874    }
875
876    /// Whether coverage has dropped below target for the alert threshold.
877    #[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    /// br-frankenpandas-01gdm: semantic_fingerprint_bytes underpins reproducibility
922    /// ledgers / RaptorQ provenance. Known-answer SHA-256 vectors prove it's the
923    /// real hash (not a stub), plus determinism, format, and builder equivalence.
924    #[test]
925    fn semantic_fingerprint_sha256_known_answers_01gdm() {
926        // Standard SHA-256 known-answer vectors.
927        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        // Format: "sha256:" + 64 lowercase hex chars; deterministic.
937        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        // Distinctness on a small sample (no collisions).
953        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        // Builder (chunked update) == one-shot over the concatenation.
967        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        // Metamorphic (br-frankenpandas-h2i8m): chunked SemanticFingerprintBuilder
1212        // updates == one-shot over the concatenation, for arbitrary bytes and chunk
1213        // boundaries (generalizes the fixed 3-chunk case in 01gdm). Seeded LCG.
1214        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            // Split into chunks at random boundaries (some possibly empty).
1225            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                    // Avoid infinite loop on a zero-take; force progress.
1234                    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    /// br-frankenpandas-b9vvk: RaptorQ provenance envelopes (AGENTS.md
1247    /// RaptorQ-Everywhere mandate). Structural invariants of from_source_bytes.
1248    #[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; // chunk count (0 for empty)
1257            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            // Every symbol hash is a well-formed sha256 fingerprint.
1287            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    /// br-frankenpandas-bhlwt: push_decode_proof_capped keeps a bounded history,
1298    /// evicting oldest first (defensive bounded recovery).
1299    #[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        // Never exceeds the cap.
1313        assert_eq!(
1314            env.decode_proofs.len(),
1315            cap,
1316            "history capped at MAX_DECODE_PROOFS"
1317        );
1318        // Oldest-first eviction: the retained window is the newest `cap` entries.
1319        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        // Retained sequence is contiguous and strictly increasing.
1330        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    /// br-frankenpandas-mbjpj: RuntimePolicy security mandates — strict fail-closes
1339    /// unknown features; hardened caps oversized joins (bounded recovery).
1340    #[test]
1341    fn runtime_policy_failclosed_and_join_cap_mbjpj() {
1342        // Configuration.
1343        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        // Fail-closed: strict mode rejects unknown features and records the decision.
1358        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        // Bounded recovery: hardened mode repairs (caps) an over-cap join.
1369        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    // === Conformal Calibration Tests (bd-2t5e.9) ===
2057
2058    #[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); // all actions admissible
2070        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        // Feed 10 decisions to build calibration window
2080        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        // Window should be capped at 5
2108        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        // Generate consistent decisions
2118        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        // With consistent decisions, most should be in the conformal set
2127        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        // Under 100 decisions, no alert regardless of coverage
2145        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    // --- AG-09-T: Conformal Calibration Tests ---
2559
2560    /// AG-09-T #1: conformal_quantile with known scores returns correct quantile.
2561    #[test]
2562    fn conformal_quantile_basic() {
2563        let mut guard = ConformalGuard::new(100, 0.1);
2564        // Manually feed scores into the window
2565        let mut ledger = EvidenceLedger::new();
2566        let policy = RuntimePolicy::hardened(Some(100_000));
2567
2568        // Generate 5 decisions to fill window
2569        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    /// AG-09-T #2: Single-element score (after 2 evals) -> returns finite quantile.
2584    #[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        // Need at least 2 scores
2591        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    /// AG-09-T #3: Empty calibration window -> returns None.
2601    #[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    /// AG-09-T #4: When conformal set is singleton (Bayesian in set),
2609    /// guard agrees with Bayesian argmin.
2610    #[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        // Build calibration window with consistent decisions
2617        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                // Singleton set: only the Bayesian action is admissible
2629                assert_eq!(ps.admissible_actions[0], record.action);
2630                bayesian_agreed += 1;
2631            }
2632        }
2633
2634        // Most decisions with consistent data should agree
2635        assert!(total > 0, "should have evaluated at least one decision");
2636        // With uniform decisions, some will be in set
2637        assert!(
2638            bayesian_agreed > 0 || total < 3,
2639            "at least some decisions should agree with Bayesian"
2640        );
2641    }
2642
2643    /// AG-09-T #5: When conformal set widens (score > threshold),
2644    /// guard admits multiple actions.
2645    #[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        // Build a tight calibration window with small-row decisions
2652        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        // Now make a very different decision (large row estimate -> different posterior)
2660        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 the score exceeds threshold, the set should widen to 3 actions
2666        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    /// AG-09-T #8: Coverage guarantee over 1000 exchangeable decisions.
2676    #[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        // Generate 1000 similar decisions (exchangeable)
2683        for i in 0..1000 {
2684            // Vary the row estimate slightly to create some variance
2685            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        // With exchangeable data, coverage should be >= 1 - alpha = 0.9
2694        // Allow some slack for finite sample effects
2695        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    /// AG-09-T #10: Rolling window correctly drops oldest entries.
2703    #[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        // Generate more decisions than window size
2711        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    /// AG-09-T #12: decision_to_card produces a valid galaxy brain card
2727    /// with conformal-relevant information.
2728    #[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}