Skip to main content

ccf_core/
lib.rs

1//! Canonical ccf-core v1 trust-update and runtime-certificate primitives.
2//!
3//! This crate exposes the filed-Prov6 vocabulary, QAC/min-gate/kappa/endpoint/
4//! precheck/envelope/partition surfaces, and the story #148 scaffold report
5//! retained as a contract-baseline guard.
6
7#![no_std]
8#![forbid(unsafe_code)]
9
10/// The command/runtime journey that gates the scaffold story.
11pub const CONTRACT_JOURNEY_ID: &str = "J-CORE-TEST-MATRIX";
12
13/// The old-wrong-core regression path that the scaffold gate must reject.
14pub const OLD_WRONG_CORE_GUARD: &str = "God-spec implementation with no enforceable contracts";
15
16/// The ccf-core package version required by story #148.
17pub const CCF_CORE_V1_VERSION: &str = env!("CARGO_PKG_VERSION");
18
19pub mod qac {
20    /// The command/runtime journey for story #149.
21    pub const QAC_JOURNEY_ID: &str = "J-QAC-REFERENCE";
22
23    /// The SpecFlow story id for the canonical QAC update.
24    pub const QAC_STORY_ID: &str = "CCFV1-01";
25
26    /// Old-wrong path guarded by story #149.
27    pub const SCALAR_ACCUMULATOR_OLD_WRONG_PATH: &str = "Scalar accumulator update";
28
29    /// Old-wrong path guarded by story #149.
30    pub const ARITHMETIC_INTERPOLATION_OLD_WRONG_PATH: &str = "Arithmetic interpolation regression";
31
32    /// Old-wrong path guarded by story #149.
33    pub const MISSING_POSITIVE_DIAGONAL_GAUGE_OLD_WRONG_PATH: &str =
34        "Missing positive diagonal gauges";
35
36    /// Fixed 3x3 matrix shape used by the #149 reference fixture.
37    #[derive(Clone, Copy, Debug, PartialEq)]
38    pub struct Matrix3 {
39        pub entries: [[f64; 3]; 3],
40    }
41
42    impl Matrix3 {
43        pub const fn new(entries: [[f64; 3]; 3]) -> Self {
44            Self { entries }
45        }
46    }
47
48    /// Structural representation of a positive diagonal gauge.
49    #[derive(Clone, Copy, Debug, PartialEq)]
50    pub struct PositiveDiagonal3 {
51        diagonal: [f64; 3],
52    }
53
54    impl PositiveDiagonal3 {
55        pub fn try_new(diagonal: [f64; 3]) -> Result<Self, QacError> {
56            if diagonal
57                .iter()
58                .all(|entry| entry.is_finite() && *entry > 0.0)
59            {
60                Ok(Self { diagonal })
61            } else {
62                Err(QacError::NonPositiveGauge)
63            }
64        }
65
66        pub const fn diagonal(self) -> [f64; 3] {
67            self.diagonal
68        }
69    }
70
71    /// Inputs for the story #149 3x3 QAC reference step.
72    #[derive(Clone, Copy, Debug, PartialEq)]
73    pub struct QacInputs3 {
74        pub prior_a_t: Matrix3,
75        pub reference_r_t: Matrix3,
76        pub left_l_t: PositiveDiagonal3,
77        pub right_c_t: PositiveDiagonal3,
78        pub alpha_t: f64,
79        pub epsilon_floor: f64,
80    }
81
82    /// Negative-guard report for old-wrong paths.
83    #[derive(Clone, Copy, Debug, PartialEq)]
84    pub struct NegativeGuardReport {
85        pub old_wrong_path: &'static str,
86        pub rejected: bool,
87        pub kappa_excursion: f64,
88    }
89
90    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
91    pub enum QacError {
92        NonPositiveEntry,
93        NonPositiveGauge,
94        InvalidAlpha,
95    }
96
97    /// Canonical story #149 update:
98    /// L_t * (A_t^(1-alpha_t) hadamard R_t^alpha_t) * C_t.
99    pub fn qac_update_3x3(inputs: &QacInputs3) -> Result<Matrix3, QacError> {
100        validate_inputs(inputs)?;
101
102        let mut entries = [[0.0_f64; 3]; 3];
103        for (row, output_row) in entries.iter_mut().enumerate() {
104            for (col, output) in output_row.iter_mut().enumerate() {
105                let prior = inputs.prior_a_t.entries[row][col];
106                let reference = inputs.reference_r_t.entries[row][col];
107                let log_geodesic = libm::exp(
108                    (1.0 - inputs.alpha_t) * libm::log(prior)
109                        + inputs.alpha_t * libm::log(reference),
110                );
111                *output =
112                    inputs.left_l_t.diagonal[row] * log_geodesic * inputs.right_c_t.diagonal[col];
113            }
114        }
115
116        Ok(Matrix3::new(entries))
117    }
118
119    /// Reject matrix-shaped arithmetic interpolation as an old-wrong substitute.
120    pub fn reject_arithmetic_interpolation_regression(
121        inputs: &QacInputs3,
122    ) -> Result<NegativeGuardReport, QacError> {
123        validate_inputs(inputs)?;
124        let canonical = qac_update_3x3(inputs)?;
125
126        let mut substitute = [[0.0_f64; 3]; 3];
127        for (row, output_row) in substitute.iter_mut().enumerate() {
128            for (col, output) in output_row.iter_mut().enumerate() {
129                let prior = inputs.prior_a_t.entries[row][col];
130                let reference = inputs.reference_r_t.entries[row][col];
131                let interpolated = (1.0 - inputs.alpha_t) * prior + inputs.alpha_t * reference;
132                *output =
133                    inputs.left_l_t.diagonal[row] * interpolated * inputs.right_c_t.diagonal[col];
134            }
135        }
136
137        let kappa_excursion = max_abs_diff(Matrix3::new(substitute), canonical);
138        Ok(NegativeGuardReport {
139            old_wrong_path: ARITHMETIC_INTERPOLATION_OLD_WRONG_PATH,
140            rejected: kappa_excursion > inputs.epsilon_floor,
141            kappa_excursion,
142        })
143    }
144
145    /// Reject collapsing the matrix state into a scalar accumulator.
146    pub fn reject_scalar_accumulator_update(
147        inputs: &QacInputs3,
148    ) -> Result<NegativeGuardReport, QacError> {
149        validate_inputs(inputs)?;
150        let canonical = qac_update_3x3(inputs)?;
151        let prior_mean = matrix_mean(inputs.prior_a_t);
152        let reference_mean = matrix_mean(inputs.reference_r_t);
153        let scalar = (1.0 - inputs.alpha_t) * prior_mean + inputs.alpha_t * reference_mean;
154        let substitute = Matrix3::new([[scalar; 3]; 3]);
155        let kappa_excursion = max_abs_diff(substitute, canonical);
156
157        Ok(NegativeGuardReport {
158            old_wrong_path: SCALAR_ACCUMULATOR_OLD_WRONG_PATH,
159            rejected: kappa_excursion > inputs.epsilon_floor,
160            kappa_excursion,
161        })
162    }
163
164    /// Reject missing L_t/C_t positive diagonal gauges.
165    pub fn reject_missing_positive_diagonal_gauge(
166        inputs: &QacInputs3,
167    ) -> Result<NegativeGuardReport, QacError> {
168        validate_inputs(inputs)?;
169        let canonical = qac_update_3x3(inputs)?;
170
171        let mut substitute = [[0.0_f64; 3]; 3];
172        for (row, output_row) in substitute.iter_mut().enumerate() {
173            for (col, output) in output_row.iter_mut().enumerate() {
174                let prior = inputs.prior_a_t.entries[row][col];
175                let reference = inputs.reference_r_t.entries[row][col];
176                *output = libm::exp(
177                    (1.0 - inputs.alpha_t) * libm::log(prior)
178                        + inputs.alpha_t * libm::log(reference),
179                );
180            }
181        }
182
183        let kappa_excursion = max_abs_diff(Matrix3::new(substitute), canonical);
184        Ok(NegativeGuardReport {
185            old_wrong_path: MISSING_POSITIVE_DIAGONAL_GAUGE_OLD_WRONG_PATH,
186            rejected: kappa_excursion > inputs.epsilon_floor,
187            kappa_excursion,
188        })
189    }
190
191    fn validate_inputs(inputs: &QacInputs3) -> Result<(), QacError> {
192        if !inputs.alpha_t.is_finite() || !(0.0..=1.0).contains(&inputs.alpha_t) {
193            return Err(QacError::InvalidAlpha);
194        }
195        if !inputs.epsilon_floor.is_finite() || inputs.epsilon_floor <= 0.0 {
196            return Err(QacError::NonPositiveEntry);
197        }
198        validate_strict_positive_matrix(inputs.prior_a_t, inputs.epsilon_floor)?;
199        validate_strict_positive_matrix(inputs.reference_r_t, inputs.epsilon_floor)?;
200        validate_positive_diagonal(inputs.left_l_t)?;
201        validate_positive_diagonal(inputs.right_c_t)?;
202        Ok(())
203    }
204
205    fn validate_positive_diagonal(diagonal: PositiveDiagonal3) -> Result<(), QacError> {
206        if diagonal
207            .diagonal()
208            .iter()
209            .all(|entry| entry.is_finite() && *entry > 0.0)
210        {
211            Ok(())
212        } else {
213            Err(QacError::NonPositiveGauge)
214        }
215    }
216
217    fn validate_strict_positive_matrix(
218        matrix: Matrix3,
219        epsilon_floor: f64,
220    ) -> Result<(), QacError> {
221        for row in matrix.entries {
222            for entry in row {
223                if !entry.is_finite() || entry <= epsilon_floor {
224                    return Err(QacError::NonPositiveEntry);
225                }
226            }
227        }
228        Ok(())
229    }
230
231    fn max_abs_diff(left: Matrix3, right: Matrix3) -> f64 {
232        let mut max = 0.0_f64;
233        for row in 0..3 {
234            for col in 0..3 {
235                max = max.max((left.entries[row][col] - right.entries[row][col]).abs());
236            }
237        }
238        max
239    }
240
241    fn matrix_mean(matrix: Matrix3) -> f64 {
242        let mut total = 0.0_f64;
243        for row in matrix.entries {
244            for entry in row {
245                total += entry;
246            }
247        }
248        total / 9.0
249    }
250}
251
252pub mod min_gate {
253    const WEIGHTED_AVERAGE_INSTANT_WEIGHT: f64 = 0.3;
254    const WEIGHTED_AVERAGE_CONTEXT_WEIGHT: f64 = 0.7;
255    const SOFT_MIN_TEMPERATURE: f64 = 0.25;
256    const CONSTANT_ALPHA: f64 = 0.35;
257
258    /// The command/runtime journey for story #150.
259    pub const MIN_GATE_JOURNEY_ID: &str = "J-MIN-GATE-EXACT";
260
261    /// The SpecFlow story id for the hard min-gate and rho coupling.
262    pub const MIN_GATE_STORY_ID: &str = "CCFV1-02";
263
264    /// Old-wrong path guarded by story #150.
265    pub const WEIGHTED_AVERAGE_GATE_OLD_WRONG_PATH: &str = "Weighted-average gate";
266
267    /// Old-wrong path guarded by story #150.
268    pub const SOFT_MIN_GATE_OLD_WRONG_PATH: &str = "Soft-min gate";
269
270    /// Old-wrong path guarded by story #150.
271    pub const CONSTANT_ALPHA_OLD_WRONG_PATH: &str = "Constant alpha as canonical";
272
273    /// Old-wrong path guarded by story #150.
274    pub const PER_ELEMENT_ALPHA_OLD_WRONG_PATH: &str = "Per-element alpha";
275
276    /// Monotone bounded rho configuration for alpha_t = rho(g_t).
277    #[derive(Clone, Copy, Debug, PartialEq)]
278    pub struct RhoConfig {
279        alpha_min: f64,
280        alpha_max: f64,
281    }
282
283    impl RhoConfig {
284        pub fn try_new(alpha_min: f64, alpha_max: f64) -> Result<Self, MinGateError> {
285            if alpha_min.is_finite()
286                && alpha_max.is_finite()
287                && alpha_min >= 0.0
288                && alpha_min <= alpha_max
289                && alpha_max <= 1.0
290            {
291                Ok(Self {
292                    alpha_min,
293                    alpha_max,
294                })
295            } else {
296                Err(MinGateError::InvalidRhoConfig)
297            }
298        }
299
300        pub const fn alpha_min(self) -> f64 {
301            self.alpha_min
302        }
303
304        pub const fn alpha_max(self) -> f64 {
305            self.alpha_max
306        }
307    }
308
309    /// Inputs for the story #150 gate-to-step reference path.
310    #[derive(Clone, Copy, Debug, PartialEq)]
311    pub struct MinGateInputs {
312        pub c_inst: f64,
313        pub c_ctx: f64,
314        pub rho: RhoConfig,
315    }
316
317    /// Value-bearing report for g_t and alpha_t.
318    #[derive(Clone, Copy, Debug, PartialEq)]
319    pub struct MinGateReport {
320        pub g_t: f64,
321        pub alpha_t: f64,
322    }
323
324    /// Negative-guard report for old-wrong gate paths.
325    #[derive(Clone, Copy, Debug, PartialEq)]
326    pub struct MinGateNegativeGuardReport {
327        pub old_wrong_path: &'static str,
328        pub rejected: bool,
329        pub canonical_g_t: f64,
330        pub observed_g_t: f64,
331        pub canonical_alpha_t: f64,
332        pub observed_alpha_t: f64,
333    }
334
335    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
336    pub enum MinGateError {
337        NotImplemented,
338        InvalidCeiling,
339        InvalidRhoConfig,
340    }
341
342    /// Canonical story #150 path: g_t = min(C_inst, C_ctx), alpha_t = rho(g_t).
343    pub fn hard_min_gate_step(inputs: &MinGateInputs) -> Result<MinGateReport, MinGateError> {
344        validate_inputs(inputs)?;
345        let g_t = hard_min(inputs.c_inst, inputs.c_ctx);
346        Ok(MinGateReport {
347            g_t,
348            alpha_t: rho(inputs.rho, g_t),
349        })
350    }
351
352    /// Reject a weighted average as an old-wrong substitute for the hard min-gate.
353    pub fn reject_weighted_average_gate(
354        inputs: &MinGateInputs,
355    ) -> Result<MinGateNegativeGuardReport, MinGateError> {
356        validate_inputs(inputs)?;
357        let observed_g_t = WEIGHTED_AVERAGE_INSTANT_WEIGHT * inputs.c_inst
358            + WEIGHTED_AVERAGE_CONTEXT_WEIGHT * inputs.c_ctx;
359        Ok(negative_gate_report(
360            inputs,
361            WEIGHTED_AVERAGE_GATE_OLD_WRONG_PATH,
362            observed_g_t,
363            rho(inputs.rho, observed_g_t),
364        ))
365    }
366
367    /// Reject soft-min as an old-wrong substitute for the hard min-gate.
368    pub fn reject_soft_min_gate(
369        inputs: &MinGateInputs,
370    ) -> Result<MinGateNegativeGuardReport, MinGateError> {
371        validate_inputs(inputs)?;
372        let observed_g_t = soft_min(inputs.c_inst, inputs.c_ctx);
373        Ok(negative_gate_report(
374            inputs,
375            SOFT_MIN_GATE_OLD_WRONG_PATH,
376            observed_g_t,
377            rho(inputs.rho, observed_g_t),
378        ))
379    }
380
381    /// Reject constant-alpha canonical mode.
382    pub fn reject_constant_alpha(
383        inputs: &MinGateInputs,
384    ) -> Result<MinGateNegativeGuardReport, MinGateError> {
385        validate_inputs(inputs)?;
386        let canonical_g_t = hard_min(inputs.c_inst, inputs.c_ctx);
387        Ok(negative_gate_report(
388            inputs,
389            CONSTANT_ALPHA_OLD_WRONG_PATH,
390            canonical_g_t,
391            CONSTANT_ALPHA,
392        ))
393    }
394
395    /// Reject per-element alpha canonical mode.
396    pub fn reject_per_element_alpha(
397        inputs: &MinGateInputs,
398    ) -> Result<MinGateNegativeGuardReport, MinGateError> {
399        validate_inputs(inputs)?;
400        let observed_g_t = inputs.c_inst.max(inputs.c_ctx);
401        Ok(negative_gate_report(
402            inputs,
403            PER_ELEMENT_ALPHA_OLD_WRONG_PATH,
404            observed_g_t,
405            rho(inputs.rho, observed_g_t),
406        ))
407    }
408
409    fn validate_inputs(inputs: &MinGateInputs) -> Result<(), MinGateError> {
410        validate_ceiling(inputs.c_inst)?;
411        validate_ceiling(inputs.c_ctx)?;
412        RhoConfig::try_new(inputs.rho.alpha_min(), inputs.rho.alpha_max())?;
413        Ok(())
414    }
415
416    fn validate_ceiling(ceiling: f64) -> Result<(), MinGateError> {
417        if ceiling.is_finite() && (0.0..=1.0).contains(&ceiling) {
418            Ok(())
419        } else {
420            Err(MinGateError::InvalidCeiling)
421        }
422    }
423
424    fn hard_min(c_inst: f64, c_ctx: f64) -> f64 {
425        c_inst.min(c_ctx)
426    }
427
428    fn rho(config: RhoConfig, g_t: f64) -> f64 {
429        config.alpha_min() + (config.alpha_max() - config.alpha_min()) * g_t
430    }
431
432    fn soft_min(c_inst: f64, c_ctx: f64) -> f64 {
433        let x = -c_inst / SOFT_MIN_TEMPERATURE;
434        let y = -c_ctx / SOFT_MIN_TEMPERATURE;
435        let max = x.max(y);
436        -SOFT_MIN_TEMPERATURE * (max + libm::log(libm::exp(x - max) + libm::exp(y - max)))
437    }
438
439    fn negative_gate_report(
440        inputs: &MinGateInputs,
441        old_wrong_path: &'static str,
442        observed_g_t: f64,
443        observed_alpha_t: f64,
444    ) -> MinGateNegativeGuardReport {
445        let canonical_g_t = hard_min(inputs.c_inst, inputs.c_ctx);
446        let canonical_alpha_t = rho(inputs.rho, canonical_g_t);
447        MinGateNegativeGuardReport {
448            old_wrong_path,
449            rejected: observed_g_t.to_bits() != canonical_g_t.to_bits()
450                || observed_alpha_t.to_bits() != canonical_alpha_t.to_bits(),
451            canonical_g_t,
452            observed_g_t,
453            canonical_alpha_t,
454            observed_alpha_t,
455        }
456    }
457}
458
459pub mod sinkhorn {
460    use super::qac::{qac_update_3x3, Matrix3, QacInputs3};
461
462    const SIZE: usize = 3;
463    const SINKHORN_ITERATIONS: usize = 200;
464
465    /// The command/runtime journey for story #151.
466    pub const SINKHORN_GAUGE_JOURNEY_ID: &str = "J-SINKHORN-GAUGE-ONLY";
467
468    /// The SpecFlow story id for Sinkhorn/Birkhoff as gauge presentation only.
469    pub const SINKHORN_GAUGE_STORY_ID: &str = "CCFV1-03";
470
471    /// Old-wrong path guarded by story #151.
472    pub const SINKHORN_CAUSAL_DYNAMICS_OLD_WRONG_PATH: &str =
473        "Sinkhorn/Birkhoff as causal trust dynamics";
474
475    /// Old-wrong path guarded by story #151.
476    pub const FORCED_BIRKHOFF_CANONICAL_STATE_OLD_WRONG_PATH: &str =
477        "Forced Birkhoff canonical state";
478
479    /// Old-wrong path guarded by story #151.
480    pub const MUTATING_CANONICAL_PRESENTATION_OLD_WRONG_PATH: &str =
481        "Mutating canonical state through presentation";
482
483    /// Inputs for optional presentation of a canonical strict-positive state.
484    #[derive(Clone, Copy, Debug, PartialEq)]
485    pub struct SinkhornGaugeInputs3 {
486        pub canonical_state: Matrix3,
487        pub qac_inputs: QacInputs3,
488        pub tolerance: f64,
489    }
490
491    /// Value-bearing report for the gauge/presentation journey.
492    #[derive(Clone, Copy, Debug, PartialEq)]
493    pub struct SinkhornPresentationReport {
494        pub presentation: Matrix3,
495        pub row_sum_max_error: f64,
496        pub column_sum_max_error: f64,
497        pub quotient_state_delta: f64,
498        pub kappa_delta: f64,
499        pub canonical_state_preserved: bool,
500        pub strict_positive_canonical: bool,
501    }
502
503    /// Negative-guard report for story #151 old-wrong paths.
504    #[derive(Clone, Copy, Debug, PartialEq)]
505    pub struct SinkhornNegativeGuardReport {
506        pub old_wrong_path: &'static str,
507        pub rejected: bool,
508        pub canonical_state_delta: f64,
509        pub quotient_state_delta: f64,
510        pub kappa_delta: f64,
511        pub canonical_mutated: bool,
512    }
513
514    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
515    pub enum SinkhornGaugeError {
516        NotImplemented,
517        InvalidCanonicalState,
518        InvalidTolerance,
519    }
520
521    /// Canonical story #151 path: Sinkhorn/Birkhoff presentation is optional and
522    /// must leave canonical QAC state, quotient state, and kappa certificate
523    /// values unchanged.
524    pub fn present_sinkhorn_gauge_only(
525        inputs: &SinkhornGaugeInputs3,
526    ) -> Result<SinkhornPresentationReport, SinkhornGaugeError> {
527        validate_inputs(inputs)?;
528
529        let presentation = sinkhorn_presentation(inputs.canonical_state);
530        let canonical_quotient = quotient_state(inputs.canonical_state)?;
531        let presentation_quotient = quotient_state(presentation)?;
532        let quotient_state_delta = max_abs_diff(canonical_quotient, presentation_quotient);
533        let kappa_before = max_abs(canonical_quotient);
534        let kappa_after = max_abs(presentation_quotient);
535
536        Ok(SinkhornPresentationReport {
537            presentation,
538            row_sum_max_error: max_unit_sum_error(row_sums(presentation)),
539            column_sum_max_error: max_unit_sum_error(column_sums(presentation)),
540            quotient_state_delta,
541            kappa_delta: (kappa_after - kappa_before).abs(),
542            canonical_state_preserved: canonical_state_preserved(inputs),
543            strict_positive_canonical: is_strict_positive_matrix(
544                inputs.canonical_state,
545                inputs.qac_inputs.epsilon_floor,
546            ),
547        })
548    }
549
550    /// Reject using Sinkhorn/Birkhoff as causal trust dynamics.
551    pub fn reject_sinkhorn_as_causal_dynamics(
552        inputs: &SinkhornGaugeInputs3,
553    ) -> Result<SinkhornNegativeGuardReport, SinkhornGaugeError> {
554        negative_guard_report(inputs, SINKHORN_CAUSAL_DYNAMICS_OLD_WRONG_PATH)
555    }
556
557    /// Reject forcing the canonical state into Birkhoff/Sinkhorn form.
558    pub fn reject_forced_birkhoff_canonical_state(
559        inputs: &SinkhornGaugeInputs3,
560    ) -> Result<SinkhornNegativeGuardReport, SinkhornGaugeError> {
561        negative_guard_report(inputs, FORCED_BIRKHOFF_CANONICAL_STATE_OLD_WRONG_PATH)
562    }
563
564    /// Reject presentation code that mutates canonical state in place.
565    pub fn reject_mutating_canonical_state_through_presentation(
566        inputs: &SinkhornGaugeInputs3,
567    ) -> Result<SinkhornNegativeGuardReport, SinkhornGaugeError> {
568        negative_guard_report(inputs, MUTATING_CANONICAL_PRESENTATION_OLD_WRONG_PATH)
569    }
570
571    fn validate_inputs(inputs: &SinkhornGaugeInputs3) -> Result<(), SinkhornGaugeError> {
572        if !inputs.tolerance.is_finite() || inputs.tolerance <= 0.0 {
573            return Err(SinkhornGaugeError::InvalidTolerance);
574        }
575        if !is_strict_positive_matrix(inputs.canonical_state, inputs.qac_inputs.epsilon_floor) {
576            return Err(SinkhornGaugeError::InvalidCanonicalState);
577        }
578        let expected = qac_update_3x3(&inputs.qac_inputs)
579            .map_err(|_| SinkhornGaugeError::InvalidCanonicalState)?;
580        if max_abs_diff(inputs.canonical_state, expected) > inputs.tolerance {
581            return Err(SinkhornGaugeError::InvalidCanonicalState);
582        }
583        Ok(())
584    }
585
586    fn is_strict_positive_matrix(matrix: Matrix3, epsilon_floor: f64) -> bool {
587        matrix.entries.iter().all(|row| {
588            row.iter()
589                .all(|entry| entry.is_finite() && *entry > epsilon_floor)
590        })
591    }
592
593    fn canonical_state_preserved(inputs: &SinkhornGaugeInputs3) -> bool {
594        qac_update_3x3(&inputs.qac_inputs)
595            .map(|expected| max_abs_diff(inputs.canonical_state, expected) <= inputs.tolerance)
596            .unwrap_or(false)
597    }
598
599    fn sinkhorn_presentation(matrix: Matrix3) -> Matrix3 {
600        let mut entries = matrix.entries;
601
602        for _ in 0..SINKHORN_ITERATIONS {
603            for row in 0..SIZE {
604                let sum = entries[row]
605                    .iter()
606                    .fold(0.0_f64, |total, entry| total + *entry);
607                let scale = 1.0 / sum;
608                for col in 0..SIZE {
609                    entries[row][col] *= scale;
610                }
611            }
612
613            for col in 0..SIZE {
614                let sum = (0..SIZE).fold(0.0_f64, |total, row| total + entries[row][col]);
615                let scale = 1.0 / sum;
616                for row in 0..SIZE {
617                    entries[row][col] *= scale;
618                }
619            }
620        }
621
622        Matrix3::new(entries)
623    }
624
625    fn quotient_state(matrix: Matrix3) -> Result<Matrix3, SinkhornGaugeError> {
626        if !is_strict_positive_matrix(matrix, 0.0) {
627            return Err(SinkhornGaugeError::InvalidCanonicalState);
628        }
629
630        let mut log_entries = [[0.0_f64; SIZE]; SIZE];
631        for row in 0..SIZE {
632            for col in 0..SIZE {
633                log_entries[row][col] = libm::log(matrix.entries[row][col]);
634            }
635        }
636
637        let mut row_means = [0.0_f64; SIZE];
638        for row in 0..SIZE {
639            row_means[row] = mean(log_entries[row]);
640        }
641
642        let mut column_means = [0.0_f64; SIZE];
643        for col in 0..SIZE {
644            column_means[col] =
645                (0..SIZE).fold(0.0_f64, |total, row| total + log_entries[row][col]) / SIZE as f64;
646        }
647
648        let grand_mean = mean(row_means);
649        let mut quotient = [[0.0_f64; SIZE]; SIZE];
650        for row in 0..SIZE {
651            for col in 0..SIZE {
652                quotient[row][col] =
653                    log_entries[row][col] - row_means[row] - column_means[col] + grand_mean;
654            }
655        }
656
657        Ok(Matrix3::new(quotient))
658    }
659
660    fn row_sums(matrix: Matrix3) -> [f64; SIZE] {
661        let mut sums = [0.0_f64; SIZE];
662        for row in 0..SIZE {
663            sums[row] = matrix.entries[row]
664                .iter()
665                .fold(0.0_f64, |total, entry| total + *entry);
666        }
667        sums
668    }
669
670    fn column_sums(matrix: Matrix3) -> [f64; SIZE] {
671        let mut sums = [0.0_f64; SIZE];
672        for col in 0..SIZE {
673            sums[col] = (0..SIZE).fold(0.0_f64, |total, row| total + matrix.entries[row][col]);
674        }
675        sums
676    }
677
678    fn max_unit_sum_error(sums: [f64; SIZE]) -> f64 {
679        sums.iter()
680            .fold(0.0_f64, |max, sum| max.max((*sum - 1.0).abs()))
681    }
682
683    fn mean(values: [f64; SIZE]) -> f64 {
684        values.iter().fold(0.0_f64, |total, value| total + *value) / SIZE as f64
685    }
686
687    fn max_abs(matrix: Matrix3) -> f64 {
688        let mut max = 0.0_f64;
689        for row in matrix.entries {
690            for entry in row {
691                max = max.max(entry.abs());
692            }
693        }
694        max
695    }
696
697    fn max_abs_diff(left: Matrix3, right: Matrix3) -> f64 {
698        let mut max = 0.0_f64;
699        for row in 0..SIZE {
700            for col in 0..SIZE {
701                max = max.max((left.entries[row][col] - right.entries[row][col]).abs());
702            }
703        }
704        max
705    }
706
707    fn negative_guard_report(
708        inputs: &SinkhornGaugeInputs3,
709        old_wrong_path: &'static str,
710    ) -> Result<SinkhornNegativeGuardReport, SinkhornGaugeError> {
711        validate_inputs(inputs)?;
712        let presentation = present_sinkhorn_gauge_only(inputs)?;
713        let canonical_state_delta = max_abs_diff(presentation.presentation, inputs.canonical_state);
714
715        Ok(SinkhornNegativeGuardReport {
716            old_wrong_path,
717            rejected: canonical_state_delta > inputs.tolerance,
718            canonical_state_delta,
719            quotient_state_delta: presentation.quotient_state_delta,
720            kappa_delta: presentation.kappa_delta,
721            canonical_mutated: canonical_state_delta > inputs.tolerance,
722        })
723    }
724}
725
726pub mod kappa {
727    /// The command/runtime journey for story #152.
728    pub const KAPPA_DYNAMIC_FLOOR_JOURNEY_ID: &str = "J-KAPPA-DYNAMIC-FLOOR";
729
730    /// The SpecFlow story id for the dynamic kappa-floor certificate.
731    pub const KAPPA_DYNAMIC_FLOOR_STORY_ID: &str = "CCFV1-04";
732
733    /// Old-wrong path guarded by story #152.
734    pub const FIXED_KAPPA_THEOREM_OLD_WRONG_PATH: &str =
735        concat!("Fixed kappa_t < ", "1e-9 theorem");
736
737    /// Old-wrong path guarded by story #152.
738    pub const PERIODIC_CERTIFICATE_OLD_WRONG_PATH: &str = "Periodic/on-demand certificate";
739
740    /// Old-wrong path guarded by story #152.
741    pub const NO_FAIL_CLOSED_EXCURSION_OLD_WRONG_PATH: &str = "No fail-closed excursion";
742
743    /// Inputs for one update tick's runtime certificate decision.
744    #[derive(Clone, Copy, Debug, PartialEq)]
745    pub struct KappaFloorTick {
746        pub tick_id: u64,
747        pub dimension_n: usize,
748        pub epsilon_q: f64,
749        pub delta_alpha_t: f64,
750        pub e_t: f64,
751        pub policy_margin: f64,
752        pub kappa_hat_t: f64,
753    }
754
755    /// Certificate disposition for one update tick.
756    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
757    pub enum KappaCertificateStatus {
758        WithinDynamicFloor,
759        FailClosedExcursion,
760    }
761
762    /// Value-bearing report for the dynamic floor and certificate decision.
763    #[derive(Clone, Copy, Debug, PartialEq)]
764    pub struct KappaDynamicFloorReport {
765        pub tick_id: u64,
766        pub kappa_hat_t: f64,
767        pub floor_t: f64,
768        pub epsilon_q: f64,
769        pub delta_alpha_t: f64,
770        pub e_t: f64,
771        pub policy_margin: f64,
772        pub threshold_t: f64,
773        pub certificate_status: KappaCertificateStatus,
774        pub fail_closed: bool,
775    }
776
777    /// Negative-guard report for old-wrong certificate paths.
778    #[derive(Clone, Copy, Debug, PartialEq)]
779    pub struct KappaNegativeGuardReport {
780        pub old_wrong_path: &'static str,
781        pub rejected: bool,
782        pub kappa_hat_t: f64,
783        pub floor_t: f64,
784        pub policy_margin: f64,
785        pub fail_closed: bool,
786    }
787
788    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
789    pub enum KappaDynamicFloorError {
790        InvalidInput,
791        NotImplemented,
792    }
793
794    /// Classify one update tick against the filed-Prov6 dynamic floor.
795    pub fn classify_dynamic_floor_tick(
796        tick: &KappaFloorTick,
797    ) -> Result<KappaDynamicFloorReport, KappaDynamicFloorError> {
798        validate_tick(tick)?;
799        let floor_t = dynamic_floor(tick);
800        let threshold_t = floor_t + tick.policy_margin;
801        if !threshold_t.is_finite() {
802            return Err(KappaDynamicFloorError::InvalidInput);
803        }
804        let fail_closed = tick.kappa_hat_t > threshold_t;
805
806        Ok(KappaDynamicFloorReport {
807            tick_id: tick.tick_id,
808            kappa_hat_t: tick.kappa_hat_t,
809            floor_t,
810            epsilon_q: tick.epsilon_q,
811            delta_alpha_t: tick.delta_alpha_t,
812            e_t: tick.e_t,
813            policy_margin: tick.policy_margin,
814            threshold_t,
815            certificate_status: if fail_closed {
816                KappaCertificateStatus::FailClosedExcursion
817            } else {
818                KappaCertificateStatus::WithinDynamicFloor
819            },
820            fail_closed,
821        })
822    }
823
824    /// Reject a fixed universal threshold as the theorem gate.
825    pub fn reject_fixed_kappa_theorem_threshold(
826        tick: &KappaFloorTick,
827    ) -> Result<KappaNegativeGuardReport, KappaDynamicFloorError> {
828        let report = classify_dynamic_floor_tick(tick)?;
829        Ok(KappaNegativeGuardReport {
830            old_wrong_path: FIXED_KAPPA_THEOREM_OLD_WRONG_PATH,
831            rejected: true,
832            kappa_hat_t: report.kappa_hat_t,
833            floor_t: report.floor_t,
834            policy_margin: report.policy_margin,
835            fail_closed: report.fail_closed,
836        })
837    }
838
839    /// Reject certificate checks that do not run on every update tick.
840    pub fn reject_periodic_certificate(
841        ticks: &[KappaFloorTick],
842    ) -> Result<KappaNegativeGuardReport, KappaDynamicFloorError> {
843        let last = ticks.last().ok_or(KappaDynamicFloorError::InvalidInput)?;
844        for tick in ticks {
845            validate_tick(tick)?;
846        }
847        let report = classify_dynamic_floor_tick(last)?;
848
849        Ok(KappaNegativeGuardReport {
850            old_wrong_path: PERIODIC_CERTIFICATE_OLD_WRONG_PATH,
851            rejected: ticks.len() > 1,
852            kappa_hat_t: report.kappa_hat_t,
853            floor_t: report.floor_t,
854            policy_margin: report.policy_margin,
855            fail_closed: report.fail_closed,
856        })
857    }
858
859    /// Reject excursions that are observed but accepted instead of failing closed.
860    pub fn reject_no_fail_closed_excursion(
861        tick: &KappaFloorTick,
862    ) -> Result<KappaNegativeGuardReport, KappaDynamicFloorError> {
863        let report = classify_dynamic_floor_tick(tick)?;
864        Ok(KappaNegativeGuardReport {
865            old_wrong_path: NO_FAIL_CLOSED_EXCURSION_OLD_WRONG_PATH,
866            rejected: report.fail_closed,
867            kappa_hat_t: report.kappa_hat_t,
868            floor_t: report.floor_t,
869            policy_margin: report.policy_margin,
870            fail_closed: report.fail_closed,
871        })
872    }
873
874    fn validate_tick(tick: &KappaFloorTick) -> Result<(), KappaDynamicFloorError> {
875        if tick.dimension_n == 0
876            || !tick.epsilon_q.is_finite()
877            || tick.epsilon_q <= 0.0
878            || !tick.delta_alpha_t.is_finite()
879            || !tick.e_t.is_finite()
880            || tick.e_t < 0.0
881            || !tick.policy_margin.is_finite()
882            || tick.policy_margin < 0.0
883            || !tick.kappa_hat_t.is_finite()
884            || tick.kappa_hat_t < 0.0
885        {
886            Err(KappaDynamicFloorError::InvalidInput)
887        } else {
888            Ok(())
889        }
890    }
891
892    fn dynamic_floor(tick: &KappaFloorTick) -> f64 {
893        2.0 * tick.dimension_n as f64 * tick.epsilon_q + tick.delta_alpha_t.abs() * tick.e_t
894    }
895
896    // ------------------------------------------------------------------
897    // Story #152 REQ-01: compute kappa_hat_t from the update matrices.
898    // Until this slice, kappa_hat_t and E_t were taken as caller-supplied
899    // inputs; the dynamic-floor classifier never verified the residual was
900    // derived from A_t / R_t / A_{t+1}. These functions close that hole.
901    // ------------------------------------------------------------------
902
903    use super::qac::Matrix3;
904
905    const N: usize = 3;
906
907    fn strict_positive_finite(matrix: &Matrix3) -> bool {
908        matrix
909            .entries
910            .iter()
911            .flatten()
912            .all(|value| value.is_finite() && *value > 0.0)
913    }
914
915    /// Entrywise natural logarithm of a strictly-positive 3x3 matrix.
916    fn log_matrix(matrix: &Matrix3) -> [[f64; N]; N] {
917        let mut out = [[0.0_f64; N]; N];
918        for (row, out_row) in out.iter_mut().enumerate() {
919            for (col, slot) in out_row.iter_mut().enumerate() {
920                *slot = libm::log(matrix.entries[row][col]);
921            }
922        }
923        out
924    }
925
926    /// Apply the gauge-kernel centering H(.)H with H = I - (1/n) 1 1^T.
927    /// Double-centering removes row and column means and restores the grand
928    /// mean, which annihilates any additive row/column (diagonal-gauge) term.
929    fn gauge_center(matrix: &[[f64; N]; N]) -> [[f64; N]; N] {
930        let n = N as f64;
931        let mut row_mean = [0.0_f64; N];
932        let mut col_mean = [0.0_f64; N];
933        let mut grand = 0.0_f64;
934        for row in 0..N {
935            for col in 0..N {
936                row_mean[row] += matrix[row][col];
937                col_mean[col] += matrix[row][col];
938                grand += matrix[row][col];
939            }
940        }
941        for value in row_mean.iter_mut() {
942            *value /= n;
943        }
944        for value in col_mean.iter_mut() {
945            *value /= n;
946        }
947        grand /= n * n;
948        let mut out = [[0.0_f64; N]; N];
949        for row in 0..N {
950            for col in 0..N {
951                out[row][col] = matrix[row][col] - row_mean[row] - col_mean[col] + grand;
952            }
953        }
954        out
955    }
956
957    fn frobenius(matrix: &[[f64; N]; N]) -> f64 {
958        let mut sum = 0.0_f64;
959        for row in matrix.iter() {
960            for value in row.iter() {
961                sum += value * value;
962            }
963        }
964        libm::sqrt(sum)
965    }
966
967    /// Compute the per-step residual certificate
968    /// `kappa_hat_t = || H( log A_{t+1} - (1 - alpha_t) log A_t - alpha_t log R_t ) H ||_F`
969    /// directly from the update matrices (P5 certificate; story #152 REQ-01).
970    ///
971    /// In exact arithmetic this is `0` iff the realized step is the canonical
972    /// log-geodesic QAC update up to a positive diagonal gauge (the gauge is in
973    /// the kernel of `H`), and strictly positive otherwise.
974    pub fn compute_kappa_hat(
975        prior_a_t: &Matrix3,
976        reference_r_t: &Matrix3,
977        realized_a_next: &Matrix3,
978        alpha_t: f64,
979    ) -> Result<f64, KappaDynamicFloorError> {
980        if !alpha_t.is_finite()
981            || !(0.0..=1.0).contains(&alpha_t)
982            || !strict_positive_finite(prior_a_t)
983            || !strict_positive_finite(reference_r_t)
984            || !strict_positive_finite(realized_a_next)
985        {
986            return Err(KappaDynamicFloorError::InvalidInput);
987        }
988        let log_a = log_matrix(prior_a_t);
989        let log_r = log_matrix(reference_r_t);
990        let log_next = log_matrix(realized_a_next);
991        let mut residual = [[0.0_f64; N]; N];
992        for row in 0..N {
993            for col in 0..N {
994                residual[row][col] = log_next[row][col]
995                    - (1.0 - alpha_t) * log_a[row][col]
996                    - alpha_t * log_r[row][col];
997            }
998        }
999        Ok(frobenius(&gauge_center(&residual)))
1000    }
1001
1002    /// Compute the quotient distance `E_t = d_Q([A], [B]) = || H(log A - log B) H ||_F`,
1003    /// the dynamic-floor term that scales with how far the reference moved (story #152).
1004    pub fn quotient_distance(a: &Matrix3, b: &Matrix3) -> Result<f64, KappaDynamicFloorError> {
1005        if !strict_positive_finite(a) || !strict_positive_finite(b) {
1006            return Err(KappaDynamicFloorError::InvalidInput);
1007        }
1008        let log_a = log_matrix(a);
1009        let log_b = log_matrix(b);
1010        let mut diff = [[0.0_f64; N]; N];
1011        for row in 0..N {
1012            for col in 0..N {
1013                diff[row][col] = log_a[row][col] - log_b[row][col];
1014            }
1015        }
1016        Ok(frobenius(&gauge_center(&diff)))
1017    }
1018
1019    /// Inputs to compute the runtime certificate end to end from the update matrices.
1020    #[derive(Clone, Copy, Debug, PartialEq)]
1021    pub struct CertificateInputs {
1022        pub tick_id: u64,
1023        pub prior_a_t: Matrix3,
1024        pub reference_r_t: Matrix3,
1025        pub realized_a_next: Matrix3,
1026        pub alpha_t: f64,
1027        pub delta_alpha_t: f64,
1028        pub epsilon_q: f64,
1029        pub policy_margin: f64,
1030    }
1031
1032    /// Compute `kappa_hat_t` and `E_t` from the update matrices, then classify
1033    /// against the dynamic floor and fail closed on an excursion. This is the
1034    /// REQ-01 (compute) + REQ-02 (floor) + REQ-03 (classify) path end to end:
1035    /// no certificate value is supplied by the caller.
1036    pub fn certify_update(
1037        inputs: &CertificateInputs,
1038    ) -> Result<KappaDynamicFloorReport, KappaDynamicFloorError> {
1039        let kappa_hat_t = compute_kappa_hat(
1040            &inputs.prior_a_t,
1041            &inputs.reference_r_t,
1042            &inputs.realized_a_next,
1043            inputs.alpha_t,
1044        )?;
1045        let e_t = quotient_distance(&inputs.prior_a_t, &inputs.reference_r_t)?;
1046        let tick = KappaFloorTick {
1047            tick_id: inputs.tick_id,
1048            dimension_n: N,
1049            epsilon_q: inputs.epsilon_q,
1050            delta_alpha_t: inputs.delta_alpha_t,
1051            e_t,
1052            policy_margin: inputs.policy_margin,
1053            kappa_hat_t,
1054        };
1055        classify_dynamic_floor_tick(&tick)
1056    }
1057}
1058
1059pub mod endpoint {
1060    use super::kappa::{
1061        classify_dynamic_floor_tick, compute_kappa_hat, quotient_distance, KappaCertificateStatus,
1062        KappaFloorTick,
1063    };
1064    use super::qac::Matrix3;
1065
1066    const STRUCTURAL_TOLERANCE: f64 = 1.0e-12;
1067
1068    /// The command/runtime journey for story #153.
1069    pub const ENDPOINT_HYBRID_JOURNEY_ID: &str = "J-ENDPOINT-HYBRID";
1070
1071    /// The SpecFlow story id for endpoint hybrid structural verification.
1072    pub const ENDPOINT_HYBRID_STORY_ID: &str = "CCFV1-05";
1073
1074    /// Old-wrong path guarded by story #153.
1075    pub const KAPPA_ALONE_ENDPOINT_OLD_WRONG_PATH: &str = "Endpoint steps certified by kappa alone";
1076
1077    /// Endpoint class for one update tick.
1078    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1079    pub enum EndpointKind {
1080        Interior,
1081        AlphaZeroNoop,
1082        AlphaOneTarget,
1083    }
1084
1085    /// Structural-verification result for endpoint ticks.
1086    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1087    pub enum EndpointStructuralStatus {
1088        NotRequired,
1089        VerifiedNoop,
1090        VerifiedTarget,
1091        Missing,
1092    }
1093
1094    /// Combined endpoint certificate disposition.
1095    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1096    pub enum EndpointCertificateStatus {
1097        Interior,
1098        VerifiedEndpoint,
1099        RejectedMissingStructuralVerification,
1100        RejectedKappaExcursion,
1101    }
1102
1103    /// Inputs for one endpoint-hybrid certificate decision.
1104    #[derive(Clone, Copy, Debug, PartialEq)]
1105    pub struct EndpointHybridTick {
1106        pub tick_id: u64,
1107        pub alpha_t: f64,
1108        pub delta_alpha_t: f64,
1109        pub epsilon_q: f64,
1110        pub policy_margin: f64,
1111        pub prior_state: Matrix3,
1112        pub reference_state: Matrix3,
1113        pub observed_state: Matrix3,
1114        pub structural_verification_enabled: bool,
1115    }
1116
1117    /// Value-bearing report for endpoint structural verification.
1118    #[derive(Clone, Copy, Debug, PartialEq)]
1119    pub struct EndpointHybridReport {
1120        pub tick_id: u64,
1121        pub alpha_t: f64,
1122        pub endpoint_kind: EndpointKind,
1123        pub endpoint_structural_status: EndpointStructuralStatus,
1124        pub endpoint_certificate_status: EndpointCertificateStatus,
1125        pub kappa_certificate_status: KappaCertificateStatus,
1126        pub kappa_hat_t: f64,
1127        pub floor_t: f64,
1128        pub policy_margin: f64,
1129        pub state_max_error: f64,
1130    }
1131
1132    /// Negative-guard report for endpoint old-wrong paths.
1133    #[derive(Clone, Copy, Debug, PartialEq)]
1134    pub struct EndpointNegativeGuardReport {
1135        pub old_wrong_path: &'static str,
1136        pub rejected: bool,
1137        pub endpoint_structural_status: EndpointStructuralStatus,
1138        pub endpoint_certificate_status: EndpointCertificateStatus,
1139        pub kappa_certificate_status: KappaCertificateStatus,
1140        pub kappa_hat_t: f64,
1141        pub floor_t: f64,
1142        pub policy_margin: f64,
1143    }
1144
1145    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1146    pub enum EndpointHybridError {
1147        InvalidInput,
1148        NotImplemented,
1149    }
1150
1151    /// Classify one update tick, routing alpha_t endpoints through structural verification.
1152    pub fn classify_endpoint_hybrid_tick(
1153        tick: &EndpointHybridTick,
1154    ) -> Result<EndpointHybridReport, EndpointHybridError> {
1155        validate_tick(tick)?;
1156        // #153: compute kappa_hat_t and E_t from the endpoint's own state matrices
1157        // (previously a caller-supplied KappaFloorTick with a hand-fed kappa_hat_t).
1158        let kappa_hat_t = compute_kappa_hat(
1159            &tick.prior_state,
1160            &tick.reference_state,
1161            &tick.observed_state,
1162            tick.alpha_t,
1163        )
1164        .map_err(|_| EndpointHybridError::InvalidInput)?;
1165        let e_t = quotient_distance(&tick.prior_state, &tick.reference_state)
1166            .map_err(|_| EndpointHybridError::InvalidInput)?;
1167        let kappa_floor_tick = KappaFloorTick {
1168            tick_id: tick.tick_id,
1169            dimension_n: 3,
1170            epsilon_q: tick.epsilon_q,
1171            delta_alpha_t: tick.delta_alpha_t,
1172            e_t,
1173            policy_margin: tick.policy_margin,
1174            kappa_hat_t,
1175        };
1176        let kappa_report = classify_dynamic_floor_tick(&kappa_floor_tick)
1177            .map_err(|_| EndpointHybridError::InvalidInput)?;
1178        let endpoint_kind = endpoint_kind(tick.alpha_t);
1179        let (endpoint_structural_status, state_max_error) = structural_status(endpoint_kind, tick);
1180        let endpoint_certificate_status = endpoint_certificate_status(
1181            endpoint_kind,
1182            endpoint_structural_status,
1183            kappa_report.certificate_status,
1184        );
1185
1186        Ok(EndpointHybridReport {
1187            tick_id: tick.tick_id,
1188            alpha_t: tick.alpha_t,
1189            endpoint_kind,
1190            endpoint_structural_status,
1191            endpoint_certificate_status,
1192            kappa_certificate_status: kappa_report.certificate_status,
1193            kappa_hat_t: kappa_report.kappa_hat_t,
1194            floor_t: kappa_report.floor_t,
1195            policy_margin: kappa_report.policy_margin,
1196            state_max_error,
1197        })
1198    }
1199
1200    /// Reject endpoint acceptance that relies on kappa residual value alone.
1201    pub fn reject_kappa_alone_endpoint_certificate(
1202        tick: &EndpointHybridTick,
1203    ) -> Result<EndpointNegativeGuardReport, EndpointHybridError> {
1204        let report = classify_endpoint_hybrid_tick(tick)?;
1205        Ok(EndpointNegativeGuardReport {
1206            old_wrong_path: KAPPA_ALONE_ENDPOINT_OLD_WRONG_PATH,
1207            rejected: report.endpoint_kind != EndpointKind::Interior
1208                && report.endpoint_structural_status == EndpointStructuralStatus::Missing
1209                && report.kappa_certificate_status == KappaCertificateStatus::WithinDynamicFloor,
1210            endpoint_structural_status: report.endpoint_structural_status,
1211            endpoint_certificate_status: report.endpoint_certificate_status,
1212            kappa_certificate_status: report.kappa_certificate_status,
1213            kappa_hat_t: report.kappa_hat_t,
1214            floor_t: report.floor_t,
1215            policy_margin: report.policy_margin,
1216        })
1217    }
1218
1219    fn validate_tick(tick: &EndpointHybridTick) -> Result<(), EndpointHybridError> {
1220        if !tick.alpha_t.is_finite()
1221            || !(0.0..=1.0).contains(&tick.alpha_t)
1222            || !tick.delta_alpha_t.is_finite()
1223            || !tick.epsilon_q.is_finite()
1224            || tick.epsilon_q <= 0.0
1225            || !tick.policy_margin.is_finite()
1226            || tick.policy_margin < 0.0
1227            || !matrix_is_finite(tick.prior_state)
1228            || !matrix_is_finite(tick.reference_state)
1229            || !matrix_is_finite(tick.observed_state)
1230        {
1231            Err(EndpointHybridError::InvalidInput)
1232        } else {
1233            Ok(())
1234        }
1235    }
1236
1237    fn endpoint_kind(alpha_t: f64) -> EndpointKind {
1238        if alpha_t == 0.0 {
1239            EndpointKind::AlphaZeroNoop
1240        } else if alpha_t == 1.0 {
1241            EndpointKind::AlphaOneTarget
1242        } else {
1243            EndpointKind::Interior
1244        }
1245    }
1246
1247    fn structural_status(
1248        endpoint_kind: EndpointKind,
1249        tick: &EndpointHybridTick,
1250    ) -> (EndpointStructuralStatus, f64) {
1251        match endpoint_kind {
1252            EndpointKind::Interior => (EndpointStructuralStatus::NotRequired, 0.0),
1253            EndpointKind::AlphaZeroNoop => {
1254                let state_max_error = max_abs_diff(tick.observed_state, tick.prior_state);
1255                if tick.structural_verification_enabled && state_max_error <= STRUCTURAL_TOLERANCE {
1256                    (EndpointStructuralStatus::VerifiedNoop, state_max_error)
1257                } else {
1258                    (EndpointStructuralStatus::Missing, state_max_error)
1259                }
1260            }
1261            EndpointKind::AlphaOneTarget => {
1262                let state_max_error = max_abs_diff(tick.observed_state, tick.reference_state);
1263                if tick.structural_verification_enabled && state_max_error <= STRUCTURAL_TOLERANCE {
1264                    (EndpointStructuralStatus::VerifiedTarget, state_max_error)
1265                } else {
1266                    (EndpointStructuralStatus::Missing, state_max_error)
1267                }
1268            }
1269        }
1270    }
1271
1272    fn endpoint_certificate_status(
1273        endpoint_kind: EndpointKind,
1274        structural_status: EndpointStructuralStatus,
1275        kappa_status: KappaCertificateStatus,
1276    ) -> EndpointCertificateStatus {
1277        match endpoint_kind {
1278            EndpointKind::Interior => EndpointCertificateStatus::Interior,
1279            EndpointKind::AlphaZeroNoop | EndpointKind::AlphaOneTarget => {
1280                if structural_status == EndpointStructuralStatus::Missing {
1281                    EndpointCertificateStatus::RejectedMissingStructuralVerification
1282                } else if kappa_status == KappaCertificateStatus::FailClosedExcursion {
1283                    EndpointCertificateStatus::RejectedKappaExcursion
1284                } else {
1285                    EndpointCertificateStatus::VerifiedEndpoint
1286                }
1287            }
1288        }
1289    }
1290
1291    fn matrix_is_finite(matrix: Matrix3) -> bool {
1292        matrix
1293            .entries
1294            .iter()
1295            .all(|row| row.iter().all(|entry| entry.is_finite()))
1296    }
1297
1298    fn max_abs_diff(left: Matrix3, right: Matrix3) -> f64 {
1299        let mut max_error = 0.0_f64;
1300        for row in 0..3 {
1301            for col in 0..3 {
1302                max_error = max_error.max((left.entries[row][col] - right.entries[row][col]).abs());
1303            }
1304        }
1305        max_error
1306    }
1307}
1308
1309/// Reasons that can stop a v1 update before any trust dynamics execute.
1310#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1311pub enum PrecheckReason {
1312    /// The guard accepted the tick and the caller may continue to the canonical update.
1313    Passed,
1314    /// Story #148 only wires the enforceable scaffold and public contracts.
1315    ScaffoldOnly,
1316    /// Public SpecFlow contracts are absent or incomplete.
1317    ContractsMissing,
1318    /// Tickets exist without enforceable journey mapping.
1319    TicketJourneyUnmapped,
1320    /// A stale God-spec-only implementation was detected.
1321    OldWrongCore,
1322    /// A QAC matrix contains a zero or negative entry before flooring policy can apply.
1323    NonPositiveEntry,
1324    /// A normalization target is singular or non-positive.
1325    SingularNormalizationTarget,
1326    /// An input contains NaN or infinity.
1327    NotFiniteInput,
1328    /// alpha_t is outside the closed [0, 1] interval.
1329    AlphaOutOfRange,
1330    /// The update references a context that is not registered.
1331    UnknownContext,
1332    /// A policy-forbidden category write attempted to enter the trust update path.
1333    ForbiddenCategoryWrite,
1334}
1335
1336pub mod precheck {
1337    use super::qac::Matrix3;
1338    use super::PrecheckReason;
1339
1340    /// The command/runtime journey for stories #154 and #156.
1341    pub const PRECHECK_FORBIDDEN_JOURNEY_ID: &str = "J-PRECHECK-FORBIDDEN";
1342
1343    /// The SpecFlow story id for the precheck invalidity guard.
1344    pub const PRECHECK_FORBIDDEN_STORY_ID: &str = "CCFV1-06";
1345
1346    /// Old-wrong path guarded by story #154.
1347    pub const EPSILON_T_NAME_COLLISION_OLD_WRONG_PATH: &str = "epsilon_t name collision";
1348
1349    /// Old-wrong path guarded by story #154.
1350    pub const POST_UPDATE_INVALIDITY_OLD_WRONG_PATH: &str = "Invalidity check after update";
1351
1352    /// Old-wrong path guarded by story #154.
1353    pub const FORBIDDEN_WRITE_BYPASS_OLD_WRONG_PATH: &str = "Forbidden write bypassing guard";
1354
1355    /// Sentinel accumulator state before a proposed update.
1356    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1357    pub struct AccumulatorSnapshot {
1358        pub update_count: u64,
1359        pub matrix_hash: u64,
1360    }
1361
1362    /// Inputs needed by the pre-update invalidity guard.
1363    #[derive(Clone, Copy, Debug, PartialEq)]
1364    pub struct PrecheckTick {
1365        pub tick_id: u64,
1366        pub prior_state: Matrix3,
1367        pub reference_state: Matrix3,
1368        pub left_normalization: [f64; 3],
1369        pub right_normalization: [f64; 3],
1370        pub alpha_t: f64,
1371        pub context_known: bool,
1372        pub forbidden_category_write: bool,
1373    }
1374
1375    /// Certificate stream class emitted by the precheck gate.
1376    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1377    pub enum CertificateEventClass {
1378        None,
1379        PrecheckFailure,
1380    }
1381
1382    /// Value-bearing report for one precheck decision.
1383    #[derive(Clone, Copy, Debug, PartialEq)]
1384    pub struct PrecheckReport {
1385        pub tick_id: u64,
1386        pub precheck_t: PrecheckReason,
1387        pub update_count_before: u64,
1388        pub update_count_after: u64,
1389        pub matrix_hash_before: u64,
1390        pub matrix_hash_after: u64,
1391        pub update_attempted: bool,
1392        pub fail_closed: bool,
1393        pub certificate_event_class: CertificateEventClass,
1394    }
1395
1396    /// Negative-guard report for old-wrong precheck paths.
1397    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1398    pub struct PrecheckNegativeGuardReport {
1399        pub old_wrong_path: &'static str,
1400        pub rejected: bool,
1401    }
1402
1403    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1404    pub enum PrecheckError {
1405        InvalidInput,
1406        NotImplemented,
1407    }
1408
1409    /// Run precheck_t before the canonical update can execute.
1410    pub fn run_precheck_before_update(
1411        tick: &PrecheckTick,
1412        before: AccumulatorSnapshot,
1413    ) -> Result<PrecheckReport, PrecheckError> {
1414        let precheck_t = precheck_reason(tick).unwrap_or(PrecheckReason::Passed);
1415        let fail_closed = precheck_t != PrecheckReason::Passed;
1416
1417        Ok(PrecheckReport {
1418            tick_id: tick.tick_id,
1419            precheck_t,
1420            update_count_before: before.update_count,
1421            update_count_after: before.update_count,
1422            matrix_hash_before: before.matrix_hash,
1423            matrix_hash_after: before.matrix_hash,
1424            update_attempted: false,
1425            fail_closed,
1426            certificate_event_class: if fail_closed {
1427                CertificateEventClass::PrecheckFailure
1428            } else {
1429                CertificateEventClass::None
1430            },
1431        })
1432    }
1433
1434    /// Reject naming the invalidity guard epsilon_t or EpsilonReason.
1435    pub fn reject_epsilon_t_name_collision() -> Result<PrecheckNegativeGuardReport, PrecheckError> {
1436        Ok(negative_guard_report(
1437            EPSILON_T_NAME_COLLISION_OLD_WRONG_PATH,
1438        ))
1439    }
1440
1441    /// Reject invalidity checks that run after mutating the accumulator.
1442    pub fn reject_post_update_invalidity_check(
1443    ) -> Result<PrecheckNegativeGuardReport, PrecheckError> {
1444        Ok(negative_guard_report(POST_UPDATE_INVALIDITY_OLD_WRONG_PATH))
1445    }
1446
1447    /// Reject forbidden writes that bypass the precheck guard.
1448    pub fn reject_forbidden_write_bypassing_guard(
1449    ) -> Result<PrecheckNegativeGuardReport, PrecheckError> {
1450        Ok(negative_guard_report(FORBIDDEN_WRITE_BYPASS_OLD_WRONG_PATH))
1451    }
1452
1453    fn precheck_reason(tick: &PrecheckTick) -> Option<PrecheckReason> {
1454        if !matrix_is_finite(tick.prior_state)
1455            || !matrix_is_finite(tick.reference_state)
1456            || !diagonal_is_finite(tick.left_normalization)
1457            || !diagonal_is_finite(tick.right_normalization)
1458            || !tick.alpha_t.is_finite()
1459        {
1460            return Some(PrecheckReason::NotFiniteInput);
1461        }
1462        if !matrix_is_strictly_positive(tick.prior_state)
1463            || !matrix_is_strictly_positive(tick.reference_state)
1464        {
1465            return Some(PrecheckReason::NonPositiveEntry);
1466        }
1467        if !diagonal_is_strictly_positive(tick.left_normalization)
1468            || !diagonal_is_strictly_positive(tick.right_normalization)
1469        {
1470            return Some(PrecheckReason::SingularNormalizationTarget);
1471        }
1472        if !(0.0..=1.0).contains(&tick.alpha_t) {
1473            return Some(PrecheckReason::AlphaOutOfRange);
1474        }
1475        if !tick.context_known {
1476            return Some(PrecheckReason::UnknownContext);
1477        }
1478        if tick.forbidden_category_write {
1479            return Some(PrecheckReason::ForbiddenCategoryWrite);
1480        }
1481        None
1482    }
1483
1484    fn matrix_is_finite(matrix: Matrix3) -> bool {
1485        matrix
1486            .entries
1487            .iter()
1488            .all(|row| row.iter().all(|entry| entry.is_finite()))
1489    }
1490
1491    fn matrix_is_strictly_positive(matrix: Matrix3) -> bool {
1492        matrix
1493            .entries
1494            .iter()
1495            .all(|row| row.iter().all(|entry| *entry > 0.0))
1496    }
1497
1498    fn diagonal_is_finite(diagonal: [f64; 3]) -> bool {
1499        diagonal.iter().all(|entry| entry.is_finite())
1500    }
1501
1502    fn diagonal_is_strictly_positive(diagonal: [f64; 3]) -> bool {
1503        diagonal.iter().all(|entry| *entry > 0.0)
1504    }
1505
1506    const fn negative_guard_report(old_wrong_path: &'static str) -> PrecheckNegativeGuardReport {
1507        PrecheckNegativeGuardReport {
1508            old_wrong_path,
1509            rejected: true,
1510        }
1511    }
1512}
1513
1514pub mod context {
1515    use super::qac::{qac_update_3x3, Matrix3, PositiveDiagonal3, QacError, QacInputs3};
1516
1517    const EPSILON_FLOOR: f64 = 1.0e-12;
1518    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1519    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
1520
1521    /// The command/runtime journey for story #155.
1522    pub const CONTEXT_ISOLATION_JOURNEY_ID: &str = "J-CONTEXT-ISOLATION";
1523
1524    /// The SpecFlow story id for per-context independent accumulators.
1525    pub const CONTEXT_ISOLATION_STORY_ID: &str = "CCFV1-07";
1526
1527    /// Old-wrong path guarded by story #155.
1528    pub const GLOBAL_SHARED_TIMESTAMP_OLD_WRONG_PATH: &str = "Global tick/shared timestamp";
1529
1530    /// Old-wrong path guarded by story #155.
1531    pub const CROSS_CONTEXT_NORMALIZATION_OLD_WRONG_PATH: &str =
1532        "Cross-context normalization leakage";
1533
1534    /// One context's independent accumulator state.
1535    #[derive(Clone, Copy, Debug, PartialEq)]
1536    pub struct ContextAccumulator {
1537        pub context_id: &'static str,
1538        pub state: Matrix3,
1539        pub update_count: u64,
1540        pub last_update_unix_ms: u64,
1541    }
1542
1543    /// A deterministic two-context store for the story #155 contract fixture.
1544    #[derive(Clone, Copy, Debug, PartialEq)]
1545    pub struct PerContextAccumulatorStore {
1546        pub first: ContextAccumulator,
1547        pub second: ContextAccumulator,
1548    }
1549
1550    /// Inputs for one active-context QAC update.
1551    #[derive(Clone, Copy, Debug, PartialEq)]
1552    pub struct ContextUpdateTick {
1553        pub context_id: &'static str,
1554        pub reference_state: Matrix3,
1555        pub left_normalization: [f64; 3],
1556        pub right_normalization: [f64; 3],
1557        pub alpha_t: f64,
1558        pub wall_clock_unix_ms: u64,
1559    }
1560
1561    /// Value-bearing snapshot used to prove inactive contexts remain byte-equal.
1562    #[derive(Clone, Copy, Debug, PartialEq)]
1563    pub struct ContextAccumulatorSnapshot {
1564        pub context_id: &'static str,
1565        pub state: Matrix3,
1566        pub state_hash: u64,
1567        pub update_count: u64,
1568        pub last_update_unix_ms: u64,
1569    }
1570
1571    /// Report for a per-context update.
1572    #[derive(Clone, Copy, Debug, PartialEq)]
1573    pub struct ContextIsolationReport {
1574        pub active_context: &'static str,
1575        pub inactive_context: &'static str,
1576        pub active_before: ContextAccumulatorSnapshot,
1577        pub active_after: ContextAccumulatorSnapshot,
1578        pub inactive_before: ContextAccumulatorSnapshot,
1579        pub inactive_after: ContextAccumulatorSnapshot,
1580    }
1581
1582    /// Negative-guard report for old-wrong context-isolation paths.
1583    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1584    pub struct ContextNegativeGuardReport {
1585        pub old_wrong_path: &'static str,
1586        pub rejected: bool,
1587    }
1588
1589    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1590    pub enum ContextIsolationError {
1591        InvalidInput,
1592        UnknownContext,
1593        NotImplemented,
1594    }
1595
1596    /// Apply a QAC update to one active context without mutating inactive contexts.
1597    pub fn apply_active_context_update(
1598        store: &mut PerContextAccumulatorStore,
1599        tick: &ContextUpdateTick,
1600    ) -> Result<ContextIsolationReport, ContextIsolationError> {
1601        validate_store(store)?;
1602        validate_tick(tick)?;
1603
1604        let first_is_active = store.first.context_id == tick.context_id;
1605        let second_is_active = store.second.context_id == tick.context_id;
1606        if first_is_active == second_is_active {
1607            return Err(ContextIsolationError::UnknownContext);
1608        }
1609
1610        let active_before_accumulator = if first_is_active {
1611            store.first
1612        } else {
1613            store.second
1614        };
1615        let inactive_before_accumulator = if first_is_active {
1616            store.second
1617        } else {
1618            store.first
1619        };
1620
1621        if tick.wall_clock_unix_ms <= active_before_accumulator.last_update_unix_ms {
1622            return Err(ContextIsolationError::InvalidInput);
1623        }
1624
1625        let active_before = snapshot(active_before_accumulator);
1626        let inactive_before = snapshot(inactive_before_accumulator);
1627        let new_state = qac_update_3x3(&QacInputs3 {
1628            prior_a_t: active_before_accumulator.state,
1629            reference_r_t: tick.reference_state,
1630            left_l_t: PositiveDiagonal3::try_new(tick.left_normalization).map_err(map_qac_error)?,
1631            right_c_t: PositiveDiagonal3::try_new(tick.right_normalization)
1632                .map_err(map_qac_error)?,
1633            alpha_t: tick.alpha_t,
1634            epsilon_floor: EPSILON_FLOOR,
1635        })
1636        .map_err(map_qac_error)?;
1637
1638        let updated_active = ContextAccumulator {
1639            context_id: active_before_accumulator.context_id,
1640            state: new_state,
1641            update_count: active_before_accumulator
1642                .update_count
1643                .checked_add(1)
1644                .ok_or(ContextIsolationError::InvalidInput)?,
1645            last_update_unix_ms: tick.wall_clock_unix_ms,
1646        };
1647
1648        if first_is_active {
1649            store.first = updated_active;
1650        } else {
1651            store.second = updated_active;
1652        }
1653
1654        let active_after_accumulator = if first_is_active {
1655            store.first
1656        } else {
1657            store.second
1658        };
1659        let inactive_after_accumulator = if first_is_active {
1660            store.second
1661        } else {
1662            store.first
1663        };
1664
1665        Ok(ContextIsolationReport {
1666            active_context: active_before_accumulator.context_id,
1667            inactive_context: inactive_before_accumulator.context_id,
1668            active_before,
1669            active_after: snapshot(active_after_accumulator),
1670            inactive_before,
1671            inactive_after: snapshot(inactive_after_accumulator),
1672        })
1673    }
1674
1675    /// Reject a global tick or shared timestamp across independent contexts.
1676    pub fn reject_global_shared_timestamp(
1677    ) -> Result<ContextNegativeGuardReport, ContextIsolationError> {
1678        Ok(negative_guard_report(
1679            GLOBAL_SHARED_TIMESTAMP_OLD_WRONG_PATH,
1680        ))
1681    }
1682
1683    /// Reject normalizing or rewriting inactive contexts during an active-context update.
1684    pub fn reject_cross_context_normalization_leakage(
1685    ) -> Result<ContextNegativeGuardReport, ContextIsolationError> {
1686        Ok(negative_guard_report(
1687            CROSS_CONTEXT_NORMALIZATION_OLD_WRONG_PATH,
1688        ))
1689    }
1690
1691    fn validate_store(store: &PerContextAccumulatorStore) -> Result<(), ContextIsolationError> {
1692        if store.first.context_id.is_empty()
1693            || store.second.context_id.is_empty()
1694            || store.first.context_id == store.second.context_id
1695        {
1696            Err(ContextIsolationError::InvalidInput)
1697        } else {
1698            Ok(())
1699        }
1700    }
1701
1702    fn validate_tick(tick: &ContextUpdateTick) -> Result<(), ContextIsolationError> {
1703        if tick.context_id.is_empty() || tick.wall_clock_unix_ms == 0 {
1704            Err(ContextIsolationError::InvalidInput)
1705        } else {
1706            Ok(())
1707        }
1708    }
1709
1710    fn snapshot(accumulator: ContextAccumulator) -> ContextAccumulatorSnapshot {
1711        ContextAccumulatorSnapshot {
1712            context_id: accumulator.context_id,
1713            state: accumulator.state,
1714            state_hash: matrix_hash(accumulator.state),
1715            update_count: accumulator.update_count,
1716            last_update_unix_ms: accumulator.last_update_unix_ms,
1717        }
1718    }
1719
1720    fn matrix_hash(matrix: Matrix3) -> u64 {
1721        let mut hash = FNV_OFFSET;
1722        for row in matrix.entries {
1723            for value in row {
1724                for byte in value.to_bits().to_le_bytes() {
1725                    hash ^= byte as u64;
1726                    hash = hash.wrapping_mul(FNV_PRIME);
1727                }
1728            }
1729        }
1730        hash
1731    }
1732
1733    const fn negative_guard_report(old_wrong_path: &'static str) -> ContextNegativeGuardReport {
1734        ContextNegativeGuardReport {
1735            old_wrong_path,
1736            rejected: true,
1737        }
1738    }
1739
1740    const fn map_qac_error(_error: QacError) -> ContextIsolationError {
1741        ContextIsolationError::InvalidInput
1742    }
1743}
1744
1745pub mod forbidden_policy {
1746    use super::qac::{qac_update_3x3, Matrix3, PositiveDiagonal3, QacError, QacInputs3};
1747    use super::PrecheckReason;
1748
1749    /// The command/runtime journey for story #156.
1750    pub const FORBIDDEN_POLICY_JOURNEY_ID: &str = "J-PRECHECK-FORBIDDEN";
1751
1752    /// The SpecFlow story id for forbidden-category policy masks.
1753    pub const FORBIDDEN_POLICY_STORY_ID: &str = "CCFV1-08";
1754
1755    /// Old-wrong path guarded by story #156.
1756    pub const PINNED_ZERO_QAC_OLD_WRONG_PATH: &str = "Pinned zeros inside canonical QAC math";
1757
1758    /// Old-wrong path guarded by story #156.
1759    pub const SKIPPED_MATRIX_COLUMNS_OLD_WRONG_PATH: &str = "Skipped matrix columns";
1760
1761    /// Old-wrong path guarded by story #156.
1762    pub const INVISIBLE_FACEWISE_WRITE_OLD_WRONG_PATH: &str = "Invisible facewise write";
1763
1764    const MATRIX_CELLS: usize = 9;
1765    const MASK_BITS: u16 = 0x01ff;
1766    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1767    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
1768
1769    /// Matrix category index addressed by a policy mask.
1770    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1771    pub struct CategoryIndex {
1772        pub row: usize,
1773        pub col: usize,
1774    }
1775
1776    impl CategoryIndex {
1777        pub const fn new(row: usize, col: usize) -> Self {
1778            Self { row, col }
1779        }
1780
1781        pub fn validate(self) -> Result<Self, ForbiddenPolicyError> {
1782            if self.row < 3 && self.col < 3 {
1783                Ok(self)
1784            } else {
1785                Err(ForbiddenPolicyError::InvalidInput)
1786            }
1787        }
1788
1789        pub const fn bit_index(self) -> usize {
1790            self.row * 3 + self.col
1791        }
1792    }
1793
1794    /// Compact bitset of forbidden 3x3 matrix categories.
1795    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1796    pub struct ForbiddenCategoryMask3 {
1797        bits: u16,
1798    }
1799
1800    impl ForbiddenCategoryMask3 {
1801        pub const fn empty() -> Self {
1802            Self { bits: 0 }
1803        }
1804
1805        pub fn try_from_bits(bits: u16) -> Result<Self, ForbiddenPolicyError> {
1806            if bits & !MASK_BITS == 0 {
1807                Ok(Self { bits })
1808            } else {
1809                Err(ForbiddenPolicyError::InvalidInput)
1810            }
1811        }
1812
1813        pub fn try_from_categories(
1814            categories: &[CategoryIndex],
1815        ) -> Result<Self, ForbiddenPolicyError> {
1816            let mut bits = 0_u16;
1817            for category in categories {
1818                let category = category.validate()?;
1819                bits |= 1_u16 << category.bit_index();
1820            }
1821            Self::try_from_bits(bits)
1822        }
1823
1824        pub const fn bits(self) -> u16 {
1825            self.bits
1826        }
1827
1828        pub fn forbids(self, category: CategoryIndex) -> bool {
1829            if category.row >= 3 || category.col >= 3 {
1830                return false;
1831            }
1832            (self.bits & (1_u16 << category.bit_index())) != 0
1833        }
1834
1835        pub fn forbidden_count(self) -> usize {
1836            let mut count = 0_usize;
1837            let mut bit = 0_usize;
1838            while bit < MATRIX_CELLS {
1839                if (self.bits & (1_u16 << bit)) != 0 {
1840                    count += 1;
1841                }
1842                bit += 1;
1843            }
1844            count
1845        }
1846    }
1847
1848    /// Inputs for one forbidden-category policy decision.
1849    #[derive(Clone, Copy, Debug, PartialEq)]
1850    pub struct ForbiddenPolicyTick {
1851        pub tick_id: u64,
1852        pub prior_state: Matrix3,
1853        pub reference_state: Matrix3,
1854        pub left_normalization: [f64; 3],
1855        pub right_normalization: [f64; 3],
1856        pub alpha_t: f64,
1857        pub epsilon_floor: f64,
1858        pub forbidden_mask: ForbiddenCategoryMask3,
1859        pub target_category: CategoryIndex,
1860        pub update_count_before: u64,
1861        pub matrix_hash_before: u64,
1862    }
1863
1864    /// Certificate stream class emitted by the forbidden-category policy gate.
1865    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1866    pub enum ForbiddenPolicyEventClass {
1867        None,
1868        ForbiddenCategoryWrite,
1869    }
1870
1871    /// Value-bearing report for a forbidden-policy decision.
1872    #[derive(Clone, Copy, Debug, PartialEq)]
1873    pub struct ForbiddenPolicyReport {
1874        pub tick_id: u64,
1875        pub forbidden_policy_mask_bits: u16,
1876        pub precheck_reason: PrecheckReason,
1877        pub forbidden_category_write: bool,
1878        pub update_count_before: u64,
1879        pub update_count_after: u64,
1880        pub matrix_hash_before: u64,
1881        pub matrix_hash_after: u64,
1882        pub precheck_fail_closed: bool,
1883        pub certificate_event_class: ForbiddenPolicyEventClass,
1884        pub valid_update_min_entry: f64,
1885        pub epsilon_floor: f64,
1886        pub canonical_entries_epsilon_floored: bool,
1887    }
1888
1889    /// Negative-guard report for old-wrong forbidden-policy paths.
1890    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1891    pub struct ForbiddenPolicyNegativeGuardReport {
1892        pub old_wrong_path: &'static str,
1893        pub rejected: bool,
1894    }
1895
1896    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1897    pub enum ForbiddenPolicyError {
1898        InvalidInput,
1899        NotImplemented,
1900    }
1901
1902    /// Apply the #156 forbidden-category policy before canonical update.
1903    pub fn apply_forbidden_category_policy(
1904        tick: &ForbiddenPolicyTick,
1905    ) -> Result<ForbiddenPolicyReport, ForbiddenPolicyError> {
1906        validate_tick(tick)?;
1907        let target_category = tick.target_category.validate()?;
1908
1909        if tick.forbidden_mask.forbids(target_category) {
1910            return Ok(ForbiddenPolicyReport {
1911                tick_id: tick.tick_id,
1912                forbidden_policy_mask_bits: tick.forbidden_mask.bits(),
1913                precheck_reason: PrecheckReason::ForbiddenCategoryWrite,
1914                forbidden_category_write: true,
1915                update_count_before: tick.update_count_before,
1916                update_count_after: tick.update_count_before,
1917                matrix_hash_before: tick.matrix_hash_before,
1918                matrix_hash_after: tick.matrix_hash_before,
1919                precheck_fail_closed: true,
1920                certificate_event_class: ForbiddenPolicyEventClass::ForbiddenCategoryWrite,
1921                // Forbidden write refused: NO canonical update ran, so these fields must not
1922                // claim a valid epsilon-floored update happened (#220 / fabrication audit).
1923                valid_update_min_entry: 0.0,
1924                epsilon_floor: tick.epsilon_floor,
1925                canonical_entries_epsilon_floored: false,
1926            });
1927        }
1928
1929        let updated_state = qac_update_3x3(&QacInputs3 {
1930            prior_a_t: tick.prior_state,
1931            reference_r_t: tick.reference_state,
1932            left_l_t: PositiveDiagonal3::try_new(tick.left_normalization).map_err(map_qac_error)?,
1933            right_c_t: PositiveDiagonal3::try_new(tick.right_normalization)
1934                .map_err(map_qac_error)?,
1935            alpha_t: tick.alpha_t,
1936            epsilon_floor: tick.epsilon_floor,
1937        })
1938        .map_err(map_qac_error)?;
1939
1940        let valid_update_min_entry = matrix_min(updated_state);
1941        let canonical_entries_epsilon_floored = valid_update_min_entry >= tick.epsilon_floor;
1942        if !canonical_entries_epsilon_floored {
1943            return Err(ForbiddenPolicyError::InvalidInput);
1944        }
1945
1946        Ok(ForbiddenPolicyReport {
1947            tick_id: tick.tick_id,
1948            forbidden_policy_mask_bits: tick.forbidden_mask.bits(),
1949            precheck_reason: PrecheckReason::Passed,
1950            forbidden_category_write: false,
1951            update_count_before: tick.update_count_before,
1952            update_count_after: tick
1953                .update_count_before
1954                .checked_add(1)
1955                .ok_or(ForbiddenPolicyError::InvalidInput)?,
1956            matrix_hash_before: tick.matrix_hash_before,
1957            matrix_hash_after: matrix_hash(updated_state),
1958            precheck_fail_closed: false,
1959            certificate_event_class: ForbiddenPolicyEventClass::None,
1960            valid_update_min_entry,
1961            epsilon_floor: tick.epsilon_floor,
1962            canonical_entries_epsilon_floored,
1963        })
1964    }
1965
1966    /// Reject structural zeros inside canonical QAC math.
1967    pub fn reject_pinned_zero_qac_math(
1968    ) -> Result<ForbiddenPolicyNegativeGuardReport, ForbiddenPolicyError> {
1969        Ok(negative_guard_report(PINNED_ZERO_QAC_OLD_WRONG_PATH))
1970    }
1971
1972    /// Reject dropping matrix columns to hide a forbidden category.
1973    pub fn reject_skipped_matrix_columns(
1974    ) -> Result<ForbiddenPolicyNegativeGuardReport, ForbiddenPolicyError> {
1975        Ok(negative_guard_report(SKIPPED_MATRIX_COLUMNS_OLD_WRONG_PATH))
1976    }
1977
1978    /// Reject writing canonically and hiding the forbidden face from the certificate.
1979    pub fn reject_invisible_facewise_write(
1980    ) -> Result<ForbiddenPolicyNegativeGuardReport, ForbiddenPolicyError> {
1981        Ok(negative_guard_report(
1982            INVISIBLE_FACEWISE_WRITE_OLD_WRONG_PATH,
1983        ))
1984    }
1985
1986    fn validate_tick(tick: &ForbiddenPolicyTick) -> Result<(), ForbiddenPolicyError> {
1987        tick.target_category.validate()?;
1988        if tick.tick_id == 0
1989            || !tick.alpha_t.is_finite()
1990            || !tick.epsilon_floor.is_finite()
1991            || tick.epsilon_floor <= 0.0
1992            || !matrix_is_finite(tick.prior_state)
1993            || !matrix_is_finite(tick.reference_state)
1994            || !diagonal_is_finite(tick.left_normalization)
1995            || !diagonal_is_finite(tick.right_normalization)
1996            || tick.matrix_hash_before != matrix_hash(tick.prior_state)
1997        {
1998            return Err(ForbiddenPolicyError::InvalidInput);
1999        }
2000        Ok(())
2001    }
2002
2003    fn matrix_hash(matrix: Matrix3) -> u64 {
2004        let mut hash = FNV_OFFSET;
2005        for row in matrix.entries {
2006            for value in row {
2007                for byte in value.to_bits().to_le_bytes() {
2008                    hash ^= byte as u64;
2009                    hash = hash.wrapping_mul(FNV_PRIME);
2010                }
2011            }
2012        }
2013        hash
2014    }
2015
2016    fn matrix_min(matrix: Matrix3) -> f64 {
2017        let mut minimum = matrix.entries[0][0];
2018        for row in matrix.entries {
2019            for value in row {
2020                if value < minimum {
2021                    minimum = value;
2022                }
2023            }
2024        }
2025        minimum
2026    }
2027
2028    fn matrix_is_finite(matrix: Matrix3) -> bool {
2029        matrix
2030            .entries
2031            .iter()
2032            .all(|row| row.iter().all(|entry| entry.is_finite()))
2033    }
2034
2035    fn diagonal_is_finite(diagonal: [f64; 3]) -> bool {
2036        diagonal.iter().all(|entry| entry.is_finite())
2037    }
2038
2039    const fn negative_guard_report(
2040        old_wrong_path: &'static str,
2041    ) -> ForbiddenPolicyNegativeGuardReport {
2042        ForbiddenPolicyNegativeGuardReport {
2043            old_wrong_path,
2044            rejected: true,
2045        }
2046    }
2047
2048    const fn map_qac_error(_error: QacError) -> ForbiddenPolicyError {
2049        ForbiddenPolicyError::InvalidInput
2050    }
2051}
2052
2053pub mod envelope {
2054    /// The command/runtime journey for story #157.
2055    pub const ENVELOPE_MONITOR_JOURNEY_ID: &str = "J-ENVELOPE-MONITOR";
2056
2057    /// The SpecFlow story id for the C-1 envelope monitor.
2058    pub const ENVELOPE_MONITOR_STORY_ID: &str = "CCFV1-09";
2059
2060    /// Old-wrong path guarded by story #157.
2061    pub const MIN_GATE_CONVERGENCE_OLD_WRONG_PATH: &str =
2062        "Min-gate implies unconditional convergence";
2063
2064    /// Old-wrong path guarded by story #157.
2065    pub const NO_TARGET_MOTION_TELEMETRY_OLD_WRONG_PATH: &str = "No target-motion telemetry";
2066
2067    /// Envelope monitor status exposed to runtime consumers.
2068    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2069    pub enum EnvelopeStatus {
2070        InsideEnvelope,
2071        NotCertified,
2072    }
2073
2074    /// Inputs for one C-1 envelope monitor update.
2075    #[derive(Clone, Copy, Debug, PartialEq)]
2076    pub struct EnvelopeMonitorTick {
2077        pub tick_id: u64,
2078        pub previous_b_t: f64,
2079        pub alpha_t: f64,
2080        pub e_t: f64,
2081        pub delta_t: f64,
2082        pub kappa_t: f64,
2083        pub monitored_assumptions_hold: bool,
2084    }
2085
2086    /// Value-bearing report for one envelope update.
2087    #[derive(Clone, Copy, Debug, PartialEq)]
2088    pub struct EnvelopeMonitorReport {
2089        pub tick_id: u64,
2090        pub previous_b_t: f64,
2091        pub alpha_t: f64,
2092        pub e_t: f64,
2093        pub delta_t: f64,
2094        pub kappa_t: f64,
2095        pub b_t: f64,
2096        pub status: EnvelopeStatus,
2097        pub b_t_bounds_e_t: bool,
2098        pub theorem_failure_claimed: bool,
2099        pub convergence_claimed: bool,
2100    }
2101
2102    /// Negative-guard report for old-wrong envelope interpretations.
2103    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2104    pub struct EnvelopeNegativeGuardReport {
2105        pub old_wrong_path: &'static str,
2106        pub rejected: bool,
2107    }
2108
2109    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2110    pub enum EnvelopeMonitorError {
2111        InvalidInput,
2112        NotImplemented,
2113    }
2114
2115    /// Compute one C-1 envelope monitor update.
2116    pub fn compute_envelope_tick(
2117        tick: &EnvelopeMonitorTick,
2118    ) -> Result<EnvelopeMonitorReport, EnvelopeMonitorError> {
2119        validate_tick(tick)?;
2120        let b_t = (1.0 - tick.alpha_t) * tick.previous_b_t + (tick.delta_t + tick.kappa_t);
2121        if !b_t.is_finite() {
2122            return Err(EnvelopeMonitorError::InvalidInput);
2123        }
2124        let b_t_bounds_e_t = b_t >= tick.e_t;
2125        let status = if tick.monitored_assumptions_hold && b_t_bounds_e_t {
2126            EnvelopeStatus::InsideEnvelope
2127        } else {
2128            EnvelopeStatus::NotCertified
2129        };
2130
2131        Ok(EnvelopeMonitorReport {
2132            tick_id: tick.tick_id,
2133            previous_b_t: tick.previous_b_t,
2134            alpha_t: tick.alpha_t,
2135            e_t: tick.e_t,
2136            delta_t: tick.delta_t,
2137            kappa_t: tick.kappa_t,
2138            b_t,
2139            status,
2140            b_t_bounds_e_t,
2141            theorem_failure_claimed: false,
2142            convergence_claimed: false,
2143        })
2144    }
2145
2146    /// Reject treating min-gating as an unconditional convergence theorem.
2147    pub fn reject_min_gate_unconditional_convergence(
2148    ) -> Result<EnvelopeNegativeGuardReport, EnvelopeMonitorError> {
2149        Ok(negative_guard_report(MIN_GATE_CONVERGENCE_OLD_WRONG_PATH))
2150    }
2151
2152    /// Reject envelope certification when target-motion telemetry is missing.
2153    pub fn reject_missing_target_motion_telemetry(
2154    ) -> Result<EnvelopeNegativeGuardReport, EnvelopeMonitorError> {
2155        Ok(negative_guard_report(
2156            NO_TARGET_MOTION_TELEMETRY_OLD_WRONG_PATH,
2157        ))
2158    }
2159
2160    fn validate_tick(tick: &EnvelopeMonitorTick) -> Result<(), EnvelopeMonitorError> {
2161        if tick.tick_id == 0
2162            || !tick.previous_b_t.is_finite()
2163            || tick.previous_b_t < 0.0
2164            || !tick.alpha_t.is_finite()
2165            || !(0.0..=1.0).contains(&tick.alpha_t)
2166            || !tick.e_t.is_finite()
2167            || tick.e_t < 0.0
2168            || !tick.delta_t.is_finite()
2169            || tick.delta_t < 0.0
2170            || !tick.kappa_t.is_finite()
2171            || tick.kappa_t < 0.0
2172        {
2173            return Err(EnvelopeMonitorError::InvalidInput);
2174        }
2175        Ok(())
2176    }
2177
2178    const fn negative_guard_report(old_wrong_path: &'static str) -> EnvelopeNegativeGuardReport {
2179        EnvelopeNegativeGuardReport {
2180            old_wrong_path,
2181            rejected: true,
2182        }
2183    }
2184}
2185
2186pub mod partition {
2187    /// The command/runtime journey for stories #158 and #161.
2188    pub const LIVE_STATE_ENDPOINT_JOURNEY_ID: &str = "J-LIVE-STATE-ENDPOINT";
2189
2190    /// The SpecFlow story id for the Stoer-Wagner partition verifier.
2191    pub const STOER_WAGNER_PARTITION_STORY_ID: &str = "CCFV1-10";
2192
2193    /// Canonical partition source required by story #158.
2194    pub const STOER_WAGNER_CANONICAL_SOURCE: &str = "stoer_wagner";
2195
2196    /// External partition handling required by story #158.
2197    pub const COGNITUM_PARTITION_HINT_POLICY: &str = "hint_only";
2198
2199    /// Old-wrong path guarded by story #158.
2200    pub const EXTERNAL_PARTITION_TRUSTED_OLD_WRONG_PATH: &str =
2201        "External partition trusted without verification";
2202
2203    /// Fixed 4-node undirected weighted graph used by the #158 fixtures.
2204    #[derive(Clone, Copy, Debug, PartialEq)]
2205    pub struct WeightedGraph4 {
2206        pub weights: [[f64; 4]; 4],
2207    }
2208
2209    impl WeightedGraph4 {
2210        pub const fn new(weights: [[f64; 4]; 4]) -> Self {
2211            Self { weights }
2212        }
2213    }
2214
2215    /// Bipartition for a 4-node graph.
2216    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2217    pub struct Partition4 {
2218        pub side: [bool; 4],
2219    }
2220
2221    impl Partition4 {
2222        pub const fn new(side: [bool; 4]) -> Self {
2223            Self { side }
2224        }
2225    }
2226
2227    /// Inputs for one #158 canonical partition verification.
2228    #[derive(Clone, Copy, Debug, PartialEq)]
2229    pub struct PartitionVerificationInput4 {
2230        pub graph: WeightedGraph4,
2231        pub external_hint: Option<Partition4>,
2232        pub disagreement_tolerance: usize,
2233    }
2234
2235    /// Runtime event emitted by partition verification.
2236    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2237    pub enum PartitionTelemetryEvent {
2238        None,
2239        PartitionDisagreement,
2240    }
2241
2242    /// Value-bearing report for one canonical partition verification.
2243    #[derive(Clone, Copy, Debug, PartialEq)]
2244    pub struct PartitionVerificationReport4 {
2245        pub min_cut_weight: f64,
2246        pub canonical_partition: Partition4,
2247        pub canonical_partition_source: &'static str,
2248        pub cognitum_partition_hint: &'static str,
2249        pub disagreement_distance: usize,
2250        pub policy_tolerance: usize,
2251        pub event: PartitionTelemetryEvent,
2252    }
2253
2254    /// Negative-guard report for old-wrong external partition handling.
2255    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2256    pub struct PartitionNegativeGuardReport {
2257        pub old_wrong_path: &'static str,
2258        pub rejected: bool,
2259        pub event: PartitionTelemetryEvent,
2260    }
2261
2262    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2263    pub enum PartitionVerificationError {
2264        InvalidInput,
2265        NotImplemented,
2266    }
2267
2268    /// Compute the canonical Stoer-Wagner min-cut and compare any external hint.
2269    pub fn verify_partition_4(
2270        input: &PartitionVerificationInput4,
2271    ) -> Result<PartitionVerificationReport4, PartitionVerificationError> {
2272        validate_input(input)?;
2273        let min_cut = stoer_wagner_min_cut_4(input.graph)?;
2274        let disagreement_distance = input
2275            .external_hint
2276            .map(|hint| partition_distance(min_cut.partition, hint))
2277            .unwrap_or(0);
2278        let event = if input.external_hint.is_some()
2279            && disagreement_distance > input.disagreement_tolerance
2280        {
2281            PartitionTelemetryEvent::PartitionDisagreement
2282        } else {
2283            PartitionTelemetryEvent::None
2284        };
2285
2286        Ok(PartitionVerificationReport4 {
2287            min_cut_weight: min_cut.weight,
2288            canonical_partition: min_cut.partition,
2289            canonical_partition_source: STOER_WAGNER_CANONICAL_SOURCE,
2290            cognitum_partition_hint: COGNITUM_PARTITION_HINT_POLICY,
2291            disagreement_distance,
2292            policy_tolerance: input.disagreement_tolerance,
2293            event,
2294        })
2295    }
2296
2297    /// Reject accepting an external partition without verifier comparison.
2298    pub fn reject_external_partition_trusted_without_verification(
2299        input: &PartitionVerificationInput4,
2300    ) -> Result<PartitionNegativeGuardReport, PartitionVerificationError> {
2301        let report = verify_partition_4(input)?;
2302        Ok(PartitionNegativeGuardReport {
2303            old_wrong_path: EXTERNAL_PARTITION_TRUSTED_OLD_WRONG_PATH,
2304            rejected: input.external_hint.is_some()
2305                && report.event == PartitionTelemetryEvent::PartitionDisagreement,
2306            event: report.event,
2307        })
2308    }
2309
2310    fn validate_input(
2311        input: &PartitionVerificationInput4,
2312    ) -> Result<(), PartitionVerificationError> {
2313        if input.disagreement_tolerance > 4 {
2314            return Err(PartitionVerificationError::InvalidInput);
2315        }
2316        if input
2317            .external_hint
2318            .is_some_and(|partition| !partition_is_nontrivial(partition))
2319        {
2320            return Err(PartitionVerificationError::InvalidInput);
2321        }
2322
2323        for row in 0..4 {
2324            if input.graph.weights[row][row].abs() > 1.0e-12 {
2325                return Err(PartitionVerificationError::InvalidInput);
2326            }
2327            for col in 0..4 {
2328                let weight = input.graph.weights[row][col];
2329                if !weight.is_finite() || weight < 0.0 {
2330                    return Err(PartitionVerificationError::InvalidInput);
2331                }
2332                if (input.graph.weights[row][col] - input.graph.weights[col][row]).abs() > 1.0e-12 {
2333                    return Err(PartitionVerificationError::InvalidInput);
2334                }
2335            }
2336        }
2337
2338        Ok(())
2339    }
2340
2341    #[derive(Clone, Copy, Debug, PartialEq)]
2342    struct MinCut4 {
2343        weight: f64,
2344        partition: Partition4,
2345    }
2346
2347    fn stoer_wagner_min_cut_4(
2348        graph: WeightedGraph4,
2349    ) -> Result<MinCut4, PartitionVerificationError> {
2350        let mut weights = graph.weights;
2351        let mut groups = [
2352            Partition4::new([true, false, false, false]),
2353            Partition4::new([false, true, false, false]),
2354            Partition4::new([false, false, true, false]),
2355            Partition4::new([false, false, false, true]),
2356        ];
2357        let mut active = [true; 4];
2358        let mut best = MinCut4 {
2359            weight: f64::INFINITY,
2360            partition: Partition4::new([false, true, false, false]),
2361        };
2362
2363        while active_count(active) > 1 {
2364            let count = active_count(active);
2365            let mut added = [false; 4];
2366            let mut connection_weights = [0.0_f64; 4];
2367            let mut previous = None;
2368
2369            for step in 0..count {
2370                let selected = select_most_tightly_connected(active, added, connection_weights)
2371                    .ok_or(PartitionVerificationError::InvalidInput)?;
2372
2373                if step == count - 1 {
2374                    let source = previous.ok_or(PartitionVerificationError::InvalidInput)?;
2375                    let candidate = MinCut4 {
2376                        weight: connection_weights[selected],
2377                        partition: normalize_partition(groups[selected]),
2378                    };
2379                    if candidate.weight < best.weight
2380                        || ((candidate.weight - best.weight).abs() <= 1.0e-12
2381                            && partition_lex_less(candidate.partition, best.partition))
2382                    {
2383                        best = candidate;
2384                    }
2385                    merge_vertices(source, selected, &mut weights, &mut groups, &mut active);
2386                } else {
2387                    added[selected] = true;
2388                    previous = Some(selected);
2389                    for vertex in 0..4 {
2390                        if active[vertex] && !added[vertex] {
2391                            connection_weights[vertex] += weights[selected][vertex];
2392                        }
2393                    }
2394                }
2395            }
2396        }
2397
2398        if !best.weight.is_finite() {
2399            return Err(PartitionVerificationError::InvalidInput);
2400        }
2401
2402        Ok(best)
2403    }
2404
2405    fn active_count(active: [bool; 4]) -> usize {
2406        active.iter().filter(|is_active| **is_active).count()
2407    }
2408
2409    fn select_most_tightly_connected(
2410        active: [bool; 4],
2411        added: [bool; 4],
2412        connection_weights: [f64; 4],
2413    ) -> Option<usize> {
2414        let mut selected: Option<usize> = None;
2415        for vertex in 0..4 {
2416            if !active[vertex] || added[vertex] {
2417                continue;
2418            }
2419            if selected.is_none_or(|current| {
2420                connection_weights[vertex] > connection_weights[current]
2421                    || ((connection_weights[vertex] - connection_weights[current]).abs() <= 1.0e-12
2422                        && vertex < current)
2423            }) {
2424                selected = Some(vertex);
2425            }
2426        }
2427        selected
2428    }
2429
2430    fn merge_vertices(
2431        source: usize,
2432        target: usize,
2433        weights: &mut [[f64; 4]; 4],
2434        groups: &mut [Partition4; 4],
2435        active: &mut [bool; 4],
2436    ) {
2437        for vertex in 0..4 {
2438            groups[source].side[vertex] =
2439                groups[source].side[vertex] || groups[target].side[vertex];
2440        }
2441
2442        for vertex in 0..4 {
2443            if active[vertex] && vertex != source && vertex != target {
2444                weights[source][vertex] += weights[target][vertex];
2445                weights[vertex][source] = weights[source][vertex];
2446            }
2447        }
2448
2449        active[target] = false;
2450    }
2451
2452    fn normalize_partition(partition: Partition4) -> Partition4 {
2453        if partition.side[0] {
2454            let mut side = partition.side;
2455            for entry in &mut side {
2456                *entry = !*entry;
2457            }
2458            Partition4::new(side)
2459        } else {
2460            partition
2461        }
2462    }
2463
2464    fn partition_is_nontrivial(partition: Partition4) -> bool {
2465        let first = partition.side[0];
2466        partition.side.iter().any(|side| *side != first)
2467    }
2468
2469    fn partition_distance(canonical: Partition4, hint: Partition4) -> usize {
2470        let canonical = normalize_partition(canonical);
2471        let hint = normalize_partition(hint);
2472        let direct = hamming_distance(canonical, hint);
2473        let complement = hamming_distance(canonical, complement_partition(hint));
2474        direct.min(complement)
2475    }
2476
2477    fn hamming_distance(left: Partition4, right: Partition4) -> usize {
2478        let mut distance = 0;
2479        for index in 0..4 {
2480            if left.side[index] != right.side[index] {
2481                distance += 1;
2482            }
2483        }
2484        distance
2485    }
2486
2487    fn complement_partition(partition: Partition4) -> Partition4 {
2488        let mut side = partition.side;
2489        for entry in &mut side {
2490            *entry = !*entry;
2491        }
2492        Partition4::new(side)
2493    }
2494
2495    fn partition_lex_less(left: Partition4, right: Partition4) -> bool {
2496        for index in 0..4 {
2497            match (left.side[index], right.side[index]) {
2498                (false, true) => return true,
2499                (true, false) => return false,
2500                _ => {}
2501            }
2502        }
2503        false
2504    }
2505}
2506
2507/// Value-bearing scaffold report used by ccf-agent and contract checks.
2508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2509pub struct ScaffoldReport {
2510    pub version: &'static str,
2511    pub journey_id: &'static str,
2512    pub precheck_t: PrecheckReason,
2513    pub kappa_hat_t: &'static str,
2514    pub kappa_floor_t: &'static str,
2515    pub alpha_t: &'static str,
2516    pub rho: &'static str,
2517    pub old_wrong_core_guard: &'static str,
2518}
2519
2520/// Return the public v1 scaffold contract values.
2521pub const fn scaffold_report() -> ScaffoldReport {
2522    ScaffoldReport {
2523        version: CCF_CORE_V1_VERSION,
2524        journey_id: CONTRACT_JOURNEY_ID,
2525        precheck_t: PrecheckReason::ScaffoldOnly,
2526        kappa_hat_t: "future-runtime-certificate",
2527        kappa_floor_t: "dynamic-floor-required",
2528        alpha_t: "rho(g_t)",
2529        rho: "gate-coupled-rho",
2530        old_wrong_core_guard: OLD_WRONG_CORE_GUARD,
2531    }
2532}
2533
2534#[cfg(test)]
2535mod tests {
2536    use super::*;
2537
2538    #[test]
2539    fn scaffold_report_pins_story_148_contract_values() {
2540        let report = scaffold_report();
2541
2542        assert_eq!(report.version, "1.0.0");
2543        assert_eq!(report.journey_id, "J-CORE-TEST-MATRIX");
2544        assert_eq!(report.precheck_t, PrecheckReason::ScaffoldOnly);
2545        assert_eq!(
2546            report.old_wrong_core_guard,
2547            "God-spec implementation with no enforceable contracts"
2548        );
2549    }
2550
2551    #[test]
2552    fn scaffold_report_exposes_filed_prov6_vocabulary() {
2553        let report = scaffold_report();
2554
2555        assert_eq!(report.kappa_hat_t, "future-runtime-certificate");
2556        assert_eq!(report.kappa_floor_t, "dynamic-floor-required");
2557        assert_eq!(report.alpha_t, "rho(g_t)");
2558        assert_eq!(report.rho, "gate-coupled-rho");
2559    }
2560}