Skip to main content

bcinr_cmca/allocator/
mod.rs

1//! # Cascade Resource Allocator
2//!
3//! This module provides the core implementation of the resource allocation engine for the
4//! Covariance Monitoring and Calibration Assessment (CMCA) substrate.
5//!
6//! Under the strict mandates of the Radon Law, this allocator enforces:
7//! - **Zero Heap Allocations**: All computations are performed on stack-allocated structures.
8//! - **Constant-Time Execution ($CC=1$)**: Absolutely no input-dependent loops, conditional jumps,
9//!   or branches.
10//! - **Typed Refusals**: Any out-of-envelope or invalid operational state yields a specific
11//!   [`StabilityRefusal`] code without panic or unwinding.
12//!
13//! ## Core Mathematical Algorithms
14//!
15//! The resource allocation algorithm executes in four distinct phases:
16//!
17//! ### 1. Cascade Allocation
18//! The allocator distributes resource flows hierarchically down a forest structure of $N$ nodes.
19//! Let the tree structure be defined by a parent vector $P \in \mathbb{I}^N$ where $P_i$ denotes the parent
20//! index of node $i$, or $-1$ if $i$ is a root.
21//!
22//! The initial resource flow is distributed to the roots of the forest.
23//! For a given policy lens $q \in \{0, \dots, Q-1\}$ and target index $k \in \{0, \dots, K-1\}$, the initial
24//! root weights are:
25//!
26//! $$ W_{\text{root}}(i) = \exp_2\left( q_{\text{val}} \cdot \log_2(M_{k, i}) - A_{\text{max\_root}} \right) $$
27//!
28//! where $M_{k, i}$ is the clipped semantic mass of node $i$, and $A_{\text{max\_root}}$ is a normalization scalar
29//! to prevent arithmetic overflow. The initial root allocation flow is then:
30//!
31//! $$ \text{alloc\_flow}\[r\] = \frac{W_{\text{root}}(r)}{\sum_{j \in \text{roots}} W_{\text{root}}(j)} $$
32//!
33//! Non-root nodes are initialized with zero flow. The allocator then executes exactly $N$ iterations of a
34//! straight-line propagation function (`flow_step`).
35//!
36//! At each step, for every node $v$:
37//! - If $v$ is a leaf, the incoming flow is collected into the final allocation vector.
38//! - If $v$ has children, the incoming flow is split into a direct leaf allocation part ($F_v$) and a child
39//!   propagation part ($D_v$):
40//!
41//!   $$ F_v = (1 - \rho_v) \cdot \text{alloc\_flow}\[v\] $$
42//!   $$ D_v = \rho_v \cdot \text{alloc\_flow}\[v\] $$
43//!
44//!   where $\rho_v \in [0, 1]$ is the local routing parameter.
45//! - The direct part $F_v$ is distributed to all descendant leaves under $v$ proportional to the leaf weights:
46//!
47//!   $$ \text{flat\_alloc}\[x\] \leftarrow \text{flat\_alloc}\[x\] + F_v \cdot \frac{W_{\text{leaf}}(v, x)}{\sum_{y \in \text{leaves}(v)} W_{\text{leaf}}(v, y)} $$
48//!
49//! - The descendant part $D_v$ is distributed to direct children of $v$ proportional to the child weights:
50//!
51//!   $$ \text{alloc\_flow}\[c\] \leftarrow \text{alloc\_flow}\[c\] + D_v \cdot \frac{W_{\text{child}}(v, c)}{\sum_{d \in \text{children}(v)} W_{\text{child}}(v, d)} $$
52//!
53//! ### 2. Multiplicative Weights Update (MWU) Step Updates
54//! For each internal node $v$, routing weights between direct leaf allocation and child propagation
55//! are adjusted dynamically based on payoff feedback.
56//! The updates are controlled by a local divergence metric $\kappa_v$ (relative entropy) computed via `compute_kappa`:
57//!
58//! $$ \kappa_v = \sum_{c \in \text{children}(v)} s_{\text{leaf}}(c) \cdot \log_2\left( \frac{s_{\text{leaf}}(c)}{s_{\text{meas}}(c)} \right) $$
59//!
60//! If $\kappa_v > \epsilon_{\kappa}$, the weights are updated using learning rate $\beta$:
61//!
62//! $$ w_{t+1}(v, d) = w_t(v, d) \cdot \exp\left( \beta \cdot \text{payoff}(v, d) \right) $$
63//!
64//! followed by normalization.
65//!
66//! ### 3. Stable Projections
67//! Combined allocation is projected based on resource prices $\mu_x$ and operational costs $c_x$:
68//!
69//! $$ P_{\mu}(x) = \frac{\pi_{\text{combined}}(x) \cdot \exp(-\mu_x \cdot c_x)}{\sum_{y \in \text{leaves}} \pi_{\text{combined}}(y) \cdot \exp(-\mu_y \cdot c_y)} $$
70//!
71//! ### 4. Explore Floors
72//! A uniform exploration floor is mixed into the final allocation vector to guarantee minimal search and prevent
73//! numerical singularity:
74//!
75//! $$ \pi_{\text{res}}(x) = \eta \cdot \frac{1}{n_L} + (1 - \eta) \cdot P_{\mu}(x) $$
76//!
77//! where $n_L$ is the number of leaf nodes in the tree.
78//!
79//! ## Algorithmic Complexity
80//!
81//! - **Time Complexity**: $O(K \cdot Q \cdot N^2)$ operations, where $N=8$, $K=4$, $Q=4$ are constants.
82//!   Thus, execution time is strictly bounded and $O(1)$.
83//! - **Space Complexity**: $O(1)$ auxiliary stack space. No heap allocations.
84//! - **Cyclomatic Complexity**: $CC = 1$ (no conditional control-flow branches).
85
86#![allow(non_upper_case_globals, unused_assignments, unused_mut, dead_code)]
87
88#[macro_export]
89macro_rules! unroll_8_static {
90    ($var:ident, $body:expr) => {{
91        {
92            const $var: usize = 0;
93            $body
94        }
95        {
96            const $var: usize = 1;
97            $body
98        }
99        {
100            const $var: usize = 2;
101            $body
102        }
103        {
104            const $var: usize = 3;
105            $body
106        }
107        {
108            const $var: usize = 4;
109            $body
110        }
111        {
112            const $var: usize = 5;
113            $body
114        }
115        {
116            const $var: usize = 6;
117            $body
118        }
119        {
120            const $var: usize = 7;
121            $body
122        }
123    }};
124}
125
126#[macro_export]
127macro_rules! unroll_9_static {
128    ($var:ident, $body:expr) => {{
129        {
130            const $var: usize = 0;
131            $body
132        }
133        {
134            const $var: usize = 1;
135            $body
136        }
137        {
138            const $var: usize = 2;
139            $body
140        }
141        {
142            const $var: usize = 3;
143            $body
144        }
145        {
146            const $var: usize = 4;
147            $body
148        }
149        {
150            const $var: usize = 5;
151            $body
152        }
153        {
154            const $var: usize = 6;
155            $body
156        }
157        {
158            const $var: usize = 7;
159            $body
160        }
161        {
162            const $var: usize = 8;
163            $body
164        }
165    }};
166}
167
168#[macro_export]
169macro_rules! unroll_4_static {
170    ($var:ident, $body:expr) => {{
171        {
172            const $var: usize = 0;
173            $body
174        }
175        {
176            const $var: usize = 1;
177            $body
178        }
179        {
180            const $var: usize = 2;
181            $body
182        }
183        {
184            const $var: usize = 3;
185            $body
186        }
187    }};
188}
189
190#[macro_export]
191macro_rules! unroll_32_static {
192    ($var:ident, $body:expr) => {{
193        {
194            const $var: usize = 0;
195            $body
196        }
197        {
198            const $var: usize = 1;
199            $body
200        }
201        {
202            const $var: usize = 2;
203            $body
204        }
205        {
206            const $var: usize = 3;
207            $body
208        }
209        {
210            const $var: usize = 4;
211            $body
212        }
213        {
214            const $var: usize = 5;
215            $body
216        }
217        {
218            const $var: usize = 6;
219            $body
220        }
221        {
222            const $var: usize = 7;
223            $body
224        }
225        {
226            const $var: usize = 8;
227            $body
228        }
229        {
230            const $var: usize = 9;
231            $body
232        }
233        {
234            const $var: usize = 10;
235            $body
236        }
237        {
238            const $var: usize = 11;
239            $body
240        }
241        {
242            const $var: usize = 12;
243            $body
244        }
245        {
246            const $var: usize = 13;
247            $body
248        }
249        {
250            const $var: usize = 14;
251            $body
252        }
253        {
254            const $var: usize = 15;
255            $body
256        }
257        {
258            const $var: usize = 16;
259            $body
260        }
261        {
262            const $var: usize = 17;
263            $body
264        }
265        {
266            const $var: usize = 18;
267            $body
268        }
269        {
270            const $var: usize = 19;
271            $body
272        }
273        {
274            const $var: usize = 20;
275            $body
276        }
277        {
278            const $var: usize = 21;
279            $body
280        }
281        {
282            const $var: usize = 22;
283            $body
284        }
285        {
286            const $var: usize = 23;
287            $body
288        }
289        {
290            const $var: usize = 24;
291            $body
292        }
293        {
294            const $var: usize = 25;
295            $body
296        }
297        {
298            const $var: usize = 26;
299            $body
300        }
301        {
302            const $var: usize = 27;
303            $body
304        }
305        {
306            const $var: usize = 28;
307            $body
308        }
309        {
310            const $var: usize = 29;
311            $body
312        }
313        {
314            const $var: usize = 30;
315            $body
316        }
317        {
318            const $var: usize = 31;
319            $body
320        }
321    }};
322}
323
324macro_rules! unroll_5_static {
325    ($var:ident, $body:expr) => {{
326        {
327            const $var: usize = 0;
328            $body
329        }
330        {
331            const $var: usize = 1;
332            $body
333        }
334        {
335            const $var: usize = 2;
336            $body
337        }
338        {
339            const $var: usize = 3;
340            $body
341        }
342        {
343            const $var: usize = 4;
344            $body
345        }
346    }};
347}
348
349use crate::fixed::{NonNegativeFixed, SignedFixed};
350use crate::generated::consequence_mass::case_studies::{
351    LensSpec, PackedSemanticState, FACTOR_ACCESS_FREQUENCY, FACTOR_BUSINESS_VALUE,
352    FACTOR_DOWNSTREAM_CONSEQUENCE, FACTOR_RECOMPUTATION_COST, FACTOR_RETRIEVAL_DEMAND,
353    FACTOR_SCHEDULING_DEMAND, FACTOR_SEARCH_DEMAND, FACTOR_STANDING, FACTOR_VERIFICATION_COST, K,
354    MEASURE_CACHE, MEASURE_RETRIEVAL, MEASURE_SCHEDULING, MEASURE_SEARCH, N, Q,
355};
356
357/// Refusal reasons returned by the allocator when stability invariants are violated.
358///
359/// In compliance with the substrate rules, these are typed error codes rather than
360/// text logs to avoid allocation and branching in the hot path.
361#[derive(Copy, Clone, Debug, PartialEq, Eq)]
362pub enum StabilityRefusal {
363    CertificateMissing,
364    BlockGainBoundExceeded,
365    ContractionMarginInsufficient,
366    LearningRateOutsideEnvelope,
367    ModeDwellTimeViolated,
368    QRangeDestabilizing,
369    MassClampUnsafe,
370    PriceGainUnsafe,
371    StandingProjectionGainUnsafe,
372    RuntimeEnvelopeViolated,
373    CertificateDigestMismatch,
374    ControlModeUncertified,
375    ControlModeSwitchTooFast,
376    YieldGainBoundViolated,
377    RewardBoundViolated,
378    ResourceResponseBoundViolated,
379    StandingResetBoundViolated,
380    LearningFrozen,
381    NumericRangeExceeded,
382    UnsupportedDomain,
383    ContractViolation,
384    /// CMCA-122: the explore-floor parameter `eta` is outside its
385    /// envelope. Previously folded into [`Self::LearningRateOutsideEnvelope`]
386    /// alongside `zeta`/`beta` even though `eta` has nothing to do with the
387    /// learning-rate parameters -- a caller debugging off the old, shared
388    /// reason was routed to the wrong subsystem.
389    ExploreFloorOutsideEnvelope,
390}
391
392impl core::fmt::Display for StabilityRefusal {
393    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
394        core::fmt::Debug::fmt(self, f)
395    }
396}
397
398#[cfg(feature = "std")]
399impl std::error::Error for StabilityRefusal {}
400
401impl StabilityRefusal {
402    /// Parses a raw `u32` value into a `StabilityRefusal` branchlessly.
403    ///
404    /// # Complexity
405    /// $O(1)$ constant time with no branches.
406    ///
407    /// # Examples
408    ///
409    /// ```rust
410    /// use bcinr_cmca::allocator::StabilityRefusal;
411    ///
412    /// assert_eq!(StabilityRefusal::from_u32(0), Some(StabilityRefusal::CertificateMissing));
413    /// assert_eq!(StabilityRefusal::from_u32(99), None);
414    /// ```
415    pub fn from_u32(val: u32) -> Option<Self> {
416        let lookup = [
417            Some(Self::CertificateMissing),
418            Some(Self::BlockGainBoundExceeded),
419            Some(Self::ContractionMarginInsufficient),
420            Some(Self::LearningRateOutsideEnvelope),
421            Some(Self::ModeDwellTimeViolated),
422            Some(Self::QRangeDestabilizing),
423            Some(Self::MassClampUnsafe),
424            Some(Self::PriceGainUnsafe),
425            Some(Self::StandingProjectionGainUnsafe),
426            Some(Self::RuntimeEnvelopeViolated),
427            Some(Self::CertificateDigestMismatch),
428            Some(Self::ControlModeUncertified),
429            Some(Self::ControlModeSwitchTooFast),
430            Some(Self::YieldGainBoundViolated),
431            Some(Self::RewardBoundViolated),
432            Some(Self::ResourceResponseBoundViolated),
433            Some(Self::StandingResetBoundViolated),
434            Some(Self::LearningFrozen),
435            Some(Self::NumericRangeExceeded),
436            Some(Self::UnsupportedDomain),
437            Some(Self::ContractViolation),
438            Some(Self::ExploreFloorOutsideEnvelope),
439            None,
440            None,
441            None,
442            None,
443            None,
444            None,
445            None,
446            None,
447            None,
448        ];
449
450        let in_bounds = const_lt_u32(val, 22);
451        let idx = const_select_u32(in_bounds, val, 22) as usize;
452        lookup[idx & 31]
453    }
454}
455
456const REFUSALS: [StabilityRefusal; 32] = [
457    StabilityRefusal::CertificateMissing,
458    StabilityRefusal::BlockGainBoundExceeded,
459    StabilityRefusal::ContractionMarginInsufficient,
460    StabilityRefusal::LearningRateOutsideEnvelope,
461    StabilityRefusal::ModeDwellTimeViolated,
462    StabilityRefusal::QRangeDestabilizing,
463    StabilityRefusal::MassClampUnsafe,
464    StabilityRefusal::PriceGainUnsafe,
465    StabilityRefusal::StandingProjectionGainUnsafe,
466    StabilityRefusal::RuntimeEnvelopeViolated,
467    StabilityRefusal::CertificateDigestMismatch,
468    StabilityRefusal::ControlModeUncertified,
469    StabilityRefusal::ControlModeSwitchTooFast,
470    StabilityRefusal::YieldGainBoundViolated,
471    StabilityRefusal::RewardBoundViolated,
472    StabilityRefusal::ResourceResponseBoundViolated,
473    StabilityRefusal::StandingResetBoundViolated,
474    StabilityRefusal::LearningFrozen,
475    StabilityRefusal::NumericRangeExceeded,
476    StabilityRefusal::UnsupportedDomain,
477    StabilityRefusal::ContractViolation,
478    StabilityRefusal::ExploreFloorOutsideEnvelope,
479    StabilityRefusal::CertificateMissing,
480    StabilityRefusal::CertificateMissing,
481    StabilityRefusal::CertificateMissing,
482    StabilityRefusal::CertificateMissing,
483    StabilityRefusal::CertificateMissing,
484    StabilityRefusal::CertificateMissing,
485    StabilityRefusal::CertificateMissing,
486    StabilityRefusal::CertificateMissing,
487    StabilityRefusal::CertificateMissing,
488    StabilityRefusal::CertificateMissing,
489];
490
491/// Typed refusal for hierarchy-shape validation (`CMCA_CONTRACT.md` ยง9,
492/// "Hierarchy Acyclicity"), distinct from [`StabilityRefusal`] (which covers
493/// stability/certificate/numeric refusals raised *during* [`allocate`]'s
494/// branchless flow). See [`check_hierarchy_acyclic`].
495#[derive(Copy, Clone, Debug, PartialEq, Eq)]
496pub enum HierarchyRefusal {
497    /// `parent` contains a cycle (or a chain deeper than `N - 1` hops
498    /// permits on a well-formed forest) -- the same ancestor-doubling
499    /// witness `allocate` already checks internally (folded there into the
500    /// generic [`StabilityRefusal::ContractViolation`]).
501    Cyclic,
502}
503
504/// Ancestor-doubling table over `parent`: `P[level][j]` is `j`'s ancestor
505/// `2^level` hops up the `parent` forest (`-1` once a chain reaches a root).
506/// Shared by [`allocate`] (which needs the full table for its
507/// `is_descendant` computation) and [`check_hierarchy_acyclic`] (which only
508/// needs the cycle witness `P[7][j] != -1`) so the two never compute this
509/// independently and risk drifting apart.
510#[allow(non_snake_case)]
511pub(crate) fn ancestor_doubling_table(parent: &[i32; N]) -> [[i32; N]; 8] {
512    let mut P = [[-1i32; N]; 8];
513    unroll_8_static!(j, {
514        P[0][j] = parent[j];
515    });
516
517    // Level 1
518    unroll_8_static!(j, {
519        let parent_node = P[0][j];
520        let mut p_next = -1i32;
521        unroll_8_static!(p_idx, {
522            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
523            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
524        });
525        P[1][j] = p_next;
526    });
527
528    // Level 2
529    unroll_8_static!(j, {
530        let parent_node = P[1][j];
531        let mut p_next = -1i32;
532        unroll_8_static!(p_idx, {
533            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
534            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
535        });
536        P[2][j] = p_next;
537    });
538
539    // Level 3
540    unroll_8_static!(j, {
541        let parent_node = P[2][j];
542        let mut p_next = -1i32;
543        unroll_8_static!(p_idx, {
544            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
545            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
546        });
547        P[3][j] = p_next;
548    });
549
550    // Level 4
551    unroll_8_static!(j, {
552        let parent_node = P[3][j];
553        let mut p_next = -1i32;
554        unroll_8_static!(p_idx, {
555            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
556            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
557        });
558        P[4][j] = p_next;
559    });
560
561    // Level 5
562    unroll_8_static!(j, {
563        let parent_node = P[4][j];
564        let mut p_next = -1i32;
565        unroll_8_static!(p_idx, {
566            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
567            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
568        });
569        P[5][j] = p_next;
570    });
571
572    // Level 6
573    unroll_8_static!(j, {
574        let parent_node = P[5][j];
575        let mut p_next = -1i32;
576        unroll_8_static!(p_idx, {
577            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
578            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
579        });
580        P[6][j] = p_next;
581    });
582
583    // Level 7
584    unroll_8_static!(j, {
585        let parent_node = P[6][j];
586        let mut p_next = -1i32;
587        unroll_8_static!(p_idx, {
588            let matches = const_eq_u32(parent_node as u32, p_idx as u32);
589            p_next = const_select_u32(matches, parent[p_idx] as u32, p_next as u32) as i32;
590        });
591        P[7][j] = p_next;
592    });
593
594    P
595}
596
597/// Validate that `parent` describes an acyclic forest on `N` nodes, per
598/// `CMCA_CONTRACT.md` ยง9 ("Hierarchy Acyclicity (DAG Property)"): a
599/// well-formed forest always reaches a root (`-1`) within `N` hops from any
600/// node, so `P[7][j] != -1` for any `j` (`P` from `ancestor_doubling_table`)
601/// is exactly the branchless witness of a cycle -- the same check
602/// [`allocate`] already performs internally before it would otherwise
603/// silently degrade to `root_w_sum == 0` and an all-`eta` output. Callers
604/// that want the `CMCA_CONTRACT.md`-documented `Err(HierarchyRefusal::Cyclic)`
605/// distinguished from `allocate`'s other, unrelated `ContractViolation`
606/// causes should call this first.
607///
608/// # Complexity
609/// $O(1)$: bounded, unrolled `N`-node ancestor doubling, no data-dependent
610/// loops.
611pub fn check_hierarchy_acyclic(parent: &[i32; N]) -> Result<(), HierarchyRefusal> {
612    let p = ancestor_doubling_table(parent);
613    let mut has_cycle = false;
614    unroll_8_static!(j, {
615        has_cycle |= p[7][j] != -1;
616    });
617    if has_cycle {
618        Err(HierarchyRefusal::Cyclic)
619    } else {
620        Ok(())
621    }
622}
623
624// Bounded leaf reciprocal lookup table (nl from 1 to 8)
625const LEAF_RECIP: [NonNegativeFixed; 9] = [
626    NonNegativeFixed::from_bits(0),
627    NonNegativeFixed::from_bits(65536), // 1.0
628    NonNegativeFixed::from_bits(32768), // 0.5
629    NonNegativeFixed::from_bits(21845), // 0.33333
630    NonNegativeFixed::from_bits(16384), // 0.25
631    NonNegativeFixed::from_bits(13107), // 0.2
632    NonNegativeFixed::from_bits(10922), // 0.16667
633    NonNegativeFixed::from_bits(9362),  // 0.14285
634    NonNegativeFixed::from_bits(8192),  // 0.125
635];
636
637/// Wraps the resource allocation array and an error status code into a branchless `Result`.
638///
639/// If `err_code == u32::MAX`, returns `Ok(pi_res)`.
640/// Otherwise, maps the code to a [`StabilityRefusal`] and returns `Err`.
641///
642/// # Inputs
643/// - `pi_res`: The computed resource allocation distribution array.
644/// - `err_code`: The status/error code.
645///
646/// # Complexity
647/// $O(1)$ constant time, branchless.
648///
649/// # Examples
650///
651/// ```rust
652/// use bcinr_cmca::fixed::NonNegativeFixed;
653/// use bcinr_cmca::allocator::{wrap_result, StabilityRefusal};
654///
655/// let pi = [NonNegativeFixed::ZERO; 8];
656/// let ok_res = wrap_result(pi, u32::MAX);
657/// assert_eq!(ok_res, Ok(pi));
658///
659/// let err_res = wrap_result(pi, 0);
660/// assert_eq!(err_res, Err(StabilityRefusal::CertificateMissing));
661/// ```
662///
663/// # Branchless Contract
664pub fn wrap_result(
665    pi_res: [NonNegativeFixed; N],
666    err_code: u32,
667) -> Result<[NonNegativeFixed; N], StabilityRefusal> {
668    let err_val = REFUSALS[(err_code as usize) & 31];
669    let is_ok = const_eq_u32(err_code, u32::MAX);
670    let outcomes = [Err(err_val), Ok(pi_res)];
671    outcomes[(is_ok as usize) & 1]
672}
673
674/// Selects branchlessly between two `u32` values based on a condition mask.
675///
676/// If `condition != 0`, returns `a`. Otherwise, returns `b`.
677///
678/// # Inputs
679/// - `condition`: A mask/condition value.
680/// - `a`: Return value if condition is non-zero.
681/// - `b`: Return value if condition is zero.
682///
683/// # Complexity
684/// $O(1)$ constant time, branchless.
685///
686/// # Examples
687///
688/// ```rust
689/// use bcinr_cmca::allocator::const_select_u32;
690///
691/// assert_eq!(const_select_u32(1, 42, 100), 42);
692/// assert_eq!(const_select_u32(0, 42, 100), 100);
693/// ```
694///
695/// # Branchless Contract
696#[inline(always)]
697pub fn const_select_u32(condition: u32, a: u32, b: u32) -> u32 {
698    let cond = core::hint::black_box(condition);
699    let cond_val = (cond | cond.wrapping_neg()) >> 31;
700    let mask = 0u32.wrapping_sub(cond_val);
701    (core::hint::black_box(a) & mask) | (core::hint::black_box(b) & !mask)
702}
703
704/// Performs a branchless "less than" comparison between two `u32` values.
705///
706/// Returns `1` if `a < b`, and `0` otherwise.
707///
708/// # Inputs
709/// - `a`: First value.
710/// - `b`: Second value.
711///
712/// # Complexity
713/// $O(1)$ constant time, branchless.
714///
715/// # Examples
716///
717/// ```rust
718/// use bcinr_cmca::allocator::const_lt_u32;
719///
720/// assert_eq!(const_lt_u32(5, 10), 1);
721/// assert_eq!(const_lt_u32(10, 5), 0);
722/// assert_eq!(const_lt_u32(5, 5), 0);
723/// ```
724///
725/// # Branchless Contract
726#[inline(always)]
727pub fn const_lt_u32(a: u32, b: u32) -> u32 {
728    let a_bb = core::hint::black_box(a);
729    let b_bb = core::hint::black_box(b);
730    ((a_bb ^ ((a_bb ^ b_bb) | (a_bb.wrapping_sub(b_bb) ^ b_bb))) >> 31) & 1
731}
732
733/// Performs a branchless "equals" check between two `u32` values.
734///
735/// Returns `1` if `a == b`, and `0` otherwise.
736///
737/// # Inputs
738/// - `a`: First value.
739/// - `b`: Second value.
740///
741/// # Complexity
742/// $O(1)$ constant time, branchless.
743///
744/// # Examples
745///
746/// ```rust
747/// use bcinr_cmca::allocator::const_eq_u32;
748///
749/// assert_eq!(const_eq_u32(42, 42), 1);
750/// assert_eq!(const_eq_u32(42, 100), 0);
751/// ```
752///
753/// # Branchless Contract
754#[inline(always)]
755pub fn const_eq_u32(a: u32, b: u32) -> u32 {
756    let x = core::hint::black_box(a) ^ core::hint::black_box(b);
757    let nonzero = (x | x.wrapping_neg()) >> 31;
758    1u32.wrapping_sub(nonzero)
759}
760
761/// Selects branchlessly between two boolean values based on a condition mask.
762///
763/// If `condition != 0`, returns `a`. Otherwise, returns `b`.
764///
765/// # Complexity
766/// $O(1)$ constant time, branchless.
767///
768/// # Examples
769///
770/// ```rust
771/// use bcinr_cmca::allocator::const_select_bool;
772///
773/// assert_eq!(const_select_bool(1, true, false), true);
774/// assert_eq!(const_select_bool(0, true, false), false);
775/// ```
776#[inline(always)]
777pub fn const_select_bool(condition: u32, a: bool, b: bool) -> bool {
778    const_select_u32(condition, a as u32, b as u32) != 0
779}
780
781/// Computes the maximum of two `i32` values branchlessly.
782///
783/// # Complexity
784/// $O(1)$ constant time, branchless.
785#[inline(always)]
786pub(crate) fn const_max_i32(a: i32, b: i32) -> i32 {
787    let diff_64 = (a as i64).wrapping_sub(b as i64);
788    let is_lt = (diff_64 >> 63) & 1;
789    const_select_u32(is_lt as u32, b as u32, a as u32) as i32
790}
791
792/// Marker struct indicating certified learning mode is active.
793///
794/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
795/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
796/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
797/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
798#[doc(hidden)]
799#[deprecated(
800    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
801)]
802#[derive(Copy, Clone, Debug, PartialEq, Eq)]
803pub struct CertifiedLearning {
804    _sealed: (),
805}
806
807#[allow(deprecated)]
808impl CertifiedLearning {
809    #[inline(always)]
810    #[allow(deprecated)]
811    pub(crate) const fn new() -> Self {
812        Self { _sealed: () }
813    }
814
815    #[inline(always)]
816    #[deprecated(
817        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
818    )]
819    #[allow(deprecated)]
820    pub const fn admit_learning() -> Self {
821        Self { _sealed: () }
822    }
823}
824
825/// Marker struct indicating selection-only mode is active.
826///
827/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
828/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
829/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
830/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
831#[doc(hidden)]
832#[deprecated(
833    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
834)]
835#[derive(Copy, Clone, Debug, PartialEq, Eq)]
836pub struct CertifiedSelectionOnly {
837    _sealed: (),
838}
839
840#[allow(deprecated)]
841impl CertifiedSelectionOnly {
842    #[inline(always)]
843    #[allow(deprecated)]
844    pub(crate) const fn new() -> Self {
845        Self { _sealed: () }
846    }
847
848    #[inline(always)]
849    #[deprecated(
850        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
851    )]
852    #[allow(deprecated)]
853    pub const fn admit_selection_only() -> Self {
854        Self { _sealed: () }
855    }
856}
857
858/// Proof token certifying that the control state has been admitted.
859///
860/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
861/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
862/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
863/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
864#[doc(hidden)]
865#[deprecated(
866    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
867)]
868#[derive(Copy, Clone, Debug, PartialEq, Eq)]
869pub struct AdmittedControlState {
870    pub(crate) digest: u64,
871}
872
873#[allow(deprecated)]
874impl AdmittedControlState {
875    #[inline(always)]
876    #[allow(deprecated)]
877    pub(crate) const fn new(digest: u64) -> Self {
878        Self { digest }
879    }
880
881    #[inline(always)]
882    #[deprecated(
883        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
884    )]
885    #[allow(deprecated)]
886    pub const fn admit_control_state(digest: u64) -> Self {
887        Self { digest }
888    }
889}
890
891/// Proof token certifying receipt of a valid security certificate.
892///
893/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
894/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
895/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
896/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
897#[doc(hidden)]
898#[deprecated(
899    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
900)]
901#[derive(Copy, Clone, Debug, PartialEq, Eq)]
902pub struct CertificateReceipt {
903    pub(crate) digest: u64,
904}
905
906#[allow(deprecated)]
907impl CertificateReceipt {
908    #[inline(always)]
909    #[allow(deprecated)]
910    pub(crate) const fn new(digest: u64) -> Self {
911        Self { digest }
912    }
913
914    #[inline(always)]
915    #[deprecated(
916        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
917    )]
918    #[allow(deprecated)]
919    pub const fn admit_certificate(digest: u64) -> Self {
920        Self { digest }
921    }
922}
923
924/// Proof token certifying receipt of a valid envelope.
925///
926/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
927/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
928/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
929/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
930#[doc(hidden)]
931#[deprecated(
932    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
933)]
934#[derive(Copy, Clone, Debug, PartialEq, Eq)]
935pub struct EnvelopeReceipt {
936    pub(crate) digest: u64,
937}
938
939#[allow(deprecated)]
940impl EnvelopeReceipt {
941    #[inline(always)]
942    #[allow(deprecated)]
943    pub(crate) const fn new(digest: u64) -> Self {
944        Self { digest }
945    }
946
947    #[inline(always)]
948    #[deprecated(
949        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
950    )]
951    #[allow(deprecated)]
952    pub const fn admit_envelope(digest: u64) -> Self {
953        Self { digest }
954    }
955}
956
957/// Proof token certifying receipt of a valid outcome.
958///
959/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
960/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
961/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
962/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
963#[doc(hidden)]
964#[deprecated(
965    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
966)]
967#[derive(Copy, Clone, Debug, PartialEq, Eq)]
968pub struct OutcomeReceipt {
969    pub(crate) digest: u64,
970}
971
972#[allow(deprecated)]
973impl OutcomeReceipt {
974    #[inline(always)]
975    #[allow(deprecated)]
976    pub(crate) const fn new(digest: u64) -> Self {
977        Self { digest }
978    }
979
980    #[inline(always)]
981    #[deprecated(
982        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
983    )]
984    #[allow(deprecated)]
985    pub const fn admit_outcome(digest: u64) -> Self {
986        Self { digest }
987    }
988}
989
990/// A proof token certifying that an adaptive update is authorized.
991///
992/// Constructed via `AdaptiveUpdate::new` when the control mode and environmental bounds
993/// are validated.
994///
995/// Hidden-by-design (CMCA-102, Branch B): part of the recovery authority chain
996/// withheld from the crate's public-facing docs pending a Hoare-logic proof of the
997/// chain's dependency closure. Still `pub` (not `pub(crate)`) because this crate's
998/// own `tests/*.rs` integration suite reaches it via `bcinr_cmca::allocator::*`.
999#[doc(hidden)]
1000#[deprecated(
1001    note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
1002)]
1003#[derive(Debug, PartialEq, Eq)]
1004pub struct AdaptiveUpdate<Mode> {
1005    _mode: core::marker::PhantomData<Mode>,
1006}
1007
1008#[allow(deprecated)]
1009impl<Mode> Clone for AdaptiveUpdate<Mode> {
1010    #[inline(always)]
1011    fn clone(&self) -> Self {
1012        *self
1013    }
1014}
1015#[allow(deprecated)]
1016impl<Mode> Copy for AdaptiveUpdate<Mode> {}
1017
1018#[allow(deprecated)]
1019impl AdaptiveUpdate<CertifiedLearning> {
1020    /// Constructs a new `AdaptiveUpdate` receipt under certified learning mode.
1021    ///
1022    /// Validates that the temperature does not exceed the profile ceiling and the
1023    /// distinguishability meets the profile floor.
1024    ///
1025    /// # Complexity
1026    /// $O(1)$ constant time, branchless.
1027    #[inline(always)]
1028    #[deprecated(
1029        note = "CMCA-102/CMCA-114: authority chain pending Hoare-logic verification, do not use in production code"
1030    )]
1031    pub fn admit_adaptive_update(
1032        state: AdmittedControlState,
1033        cert: CertificateReceipt,
1034        env: EnvelopeReceipt,
1035        outcome: OutcomeReceipt,
1036        temperature: NonNegativeFixed,
1037        distinguishability: NonNegativeFixed,
1038        _mode: CertifiedLearning,
1039    ) -> Option<Self> {
1040        let temp_ceil = ((crate::generated::stability_profile::PROFILE
1041            .temperature_ceiling
1042            .raw
1043            * 65536)
1044            / 1_000_000_000) as u32;
1045        let dist_floor = ((crate::generated::stability_profile::PROFILE
1046            .distinguishability_floor
1047            .raw
1048            * 65536)
1049            / 1_000_000_000) as u32;
1050
1051        let temp_ok = (const_lt_u32(temp_ceil, temperature.val) == 0) as u32;
1052        let dist_ok = (const_lt_u32(distinguishability.val, dist_floor) == 0) as u32;
1053
1054        let digests_ok = (((state.digest ^ cert.digest)
1055            | (state.digest ^ env.digest)
1056            | (state.digest ^ outcome.digest))
1057            == 0) as u32;
1058
1059        let ok = temp_ok & dist_ok & digests_ok;
1060
1061        let outcomes = [
1062            None,
1063            Some(Self {
1064                _mode: core::marker::PhantomData,
1065            }),
1066        ];
1067        outcomes[(ok as usize) & 1]
1068    }
1069}
1070
1071/// Computes `base^exponent` branchlessly using fixed-point log2 and exp2 approximations.
1072///
1073/// # Complexity
1074/// $O(1)$ constant time, branchless.
1075///
1076/// Made `pub` (was `pub(crate)`) so `crate::escort` can build a
1077/// fractional-exponent escort distribution on top of it -- see that
1078/// module's docs for why `cascade::escort_weight`'s exact, integer-only
1079/// repeated multiplication isn't sufficient for every caller.
1080#[inline(always)]
1081pub fn power(base: NonNegativeFixed, exponent: SignedFixed) -> NonNegativeFixed {
1082    let base_is_zero = const_eq_u32(base.val, 0);
1083    let log_val = base.log2();
1084    // `saturating_mul`, not a raw `wrapping_mul` + truncating `as i32` cast:
1085    // the manual version this replaced discarded overflow silently (product
1086    // could wrap past i32::MAX/MIN with no signal) and `from_bits` then
1087    // manufactured a fresh `err = u32::MAX`, erasing `log_val.err` too.
1088    // `saturating_mul` clamps on overflow and propagates both operands' err.
1089    let product = exponent.saturating_mul(log_val);
1090    let pow_val = product.exp2();
1091    let exp_val = exponent.val;
1092    let exp_gt_zero = (((0i32.wrapping_sub(exp_val)) >> 31) & 1) as u32;
1093    let exp_eq_zero = const_eq_u32(exponent.val as u32, 0);
1094    let zero_res = const_select_u32(
1095        exp_eq_zero,
1096        NonNegativeFixed::ONE.val,
1097        const_select_u32(exp_gt_zero, 0, u32::MAX),
1098    );
1099    // The zero-base branch is exact (no `exp2` approximation involved), so
1100    // it reports no fault regardless of `pow_val.err` -- *except* for
1101    // `0^(negative)`, which is mathematically `+infinity`: undefined/
1102    // degenerate, not a valid large number. That one sub-case must be
1103    // refused via `StabilityRefusal::UnsupportedDomain` (the same
1104    // discriminant `NonNegativeFixed::saturating_div`'s zero-denominator
1105    // path and `SignedFixed::log2`'s zero-input path already use for this
1106    // exact "undefined at zero" shape -- see `fixed.rs`), not silently
1107    // tagged `err = u32::MAX` ("no fault"). See CMCA-109.
1108    let exp_lt_zero = const_select_u32(exp_gt_zero | exp_eq_zero, 0, u32::MAX);
1109    let zero_base_err = const_select_u32(
1110        exp_lt_zero,
1111        StabilityRefusal::UnsupportedDomain as u32,
1112        u32::MAX,
1113    );
1114    NonNegativeFixed {
1115        val: const_select_u32(base_is_zero, zero_res, pow_val.val),
1116        err: const_select_u32(base_is_zero, zero_base_err, pow_val.err),
1117    }
1118}
1119
1120/// Clamps a fixed-point value within `[min_val, max_val]` branchlessly.
1121///
1122/// # Complexity
1123/// $O(1)$ constant time, branchless.
1124#[inline(always)]
1125pub(crate) fn clip(
1126    val: NonNegativeFixed,
1127    min_val: NonNegativeFixed,
1128    max_val: NonNegativeFixed,
1129) -> NonNegativeFixed {
1130    let lt_min = const_lt_u32(val.val, min_val.val);
1131    let val_or_min = const_select_u32(lt_min, min_val.val, val.val);
1132    let gt_max = const_lt_u32(max_val.val, val_or_min);
1133    NonNegativeFixed::from_bits(const_select_u32(gt_max, max_val.val, val_or_min))
1134}
1135
1136#[cfg(test)]
1137mod clip_tests {
1138    //! CMCA-122: `clip`'s call site inside `allocate_in` (the `mu_actual`
1139    //! computation) is now unreachable on any `Ok` path -- `price_err`
1140    //! refuses unconditionally before a clamped value can be observed
1141    //! through the public API (see the `mu_actual` comment above and
1142    //! `hostile_mutants.rs`'s `kill_mutant_5_consequence_truncation`, which
1143    //! can no longer distinguish clip-present from clip-removed through
1144    //! `allocate`/`allocate_in`). These tests exercise `clip` directly --
1145    //! bypassing the admission gate entirely -- so its clamping behavior
1146    //! still has real, non-degenerate regression coverage regardless of
1147    //! whether any caller-reachable input can currently observe it.
1148    use super::*;
1149
1150    fn fx(v: u32) -> NonNegativeFixed {
1151        NonNegativeFixed::from_bits(v)
1152    }
1153
1154    #[test]
1155    fn clip_clamps_values_above_max_down_to_max() {
1156        let result = clip(fx(1_000_000), fx(0), fx(500_000));
1157        assert_eq!(result.val, 500_000, "value above max must clamp to max");
1158    }
1159
1160    #[test]
1161    fn clip_clamps_values_below_min_up_to_min() {
1162        let result = clip(fx(10), fx(100), fx(500_000));
1163        assert_eq!(result.val, 100, "value below min must clamp to min");
1164    }
1165
1166    #[test]
1167    fn clip_passes_in_range_values_through_unchanged() {
1168        let result = clip(fx(250_000), fx(0), fx(500_000));
1169        assert_eq!(
1170            result.val, 250_000,
1171            "in-range value must pass through as identity"
1172        );
1173    }
1174
1175    #[test]
1176    fn clip_at_exact_bounds_is_identity() {
1177        assert_eq!(clip(fx(0), fx(0), fx(500_000)).val, 0);
1178        assert_eq!(clip(fx(500_000), fx(0), fx(500_000)).val, 500_000);
1179    }
1180}
1181
1182/// Performs a single straight-line flow propagation step down the node forest.
1183///
1184/// Divides the incoming flow into flat and descendant parts, distributing them
1185/// branchlessly according to normalized leaf and child weights.
1186///
1187/// # Complexity
1188/// $O(N^2)$ operations, which is $O(1)$ since $N=8$.
1189#[inline(never)]
1190#[allow(clippy::too_many_arguments)] // deliberate wide parameter list for a hot, branchless flow-step kernel
1191fn flow_step(
1192    parent: &[i32; N],
1193    is_leaf: &[bool; N],
1194    is_subtree_leaf: &[[bool; N]; N],
1195    rho: &[NonNegativeFixed; N],
1196    child_w: &[[NonNegativeFixed; N]; N],
1197    cw_sum: &[NonNegativeFixed; N],
1198    leaf_w: &[[NonNegativeFixed; N]; N],
1199    lw_sum: &[NonNegativeFixed; N],
1200    alloc_flow: &mut [NonNegativeFixed; N],
1201    flat_alloc: &mut [NonNegativeFixed; N],
1202) {
1203    unroll_8_static!(v, {
1204        let has_children = !is_leaf[v & 7];
1205
1206        let flat_part = NonNegativeFixed::from_bits(const_select_u32(
1207            has_children as u32,
1208            ((NonNegativeFixed::ONE - rho[v & 7]) * alloc_flow[v & 7]).val,
1209            0,
1210        ));
1211        let desc_part = NonNegativeFixed::from_bits(const_select_u32(
1212            has_children as u32,
1213            (rho[v & 7] * alloc_flow[v & 7]).val,
1214            0,
1215        ));
1216
1217        #[allow(unused_variables)]
1218        let l_cond = const_eq_u32(lw_sum[v & 7].val, 0);
1219        #[cfg(feature = "mutant_3")]
1220        let lw_denom = NonNegativeFixed::ONE.val;
1221        #[cfg(not(feature = "mutant_3"))]
1222        let lw_denom = const_select_u32(l_cond, NonNegativeFixed::ONE.val, lw_sum[v & 7].val);
1223
1224        let c_cond = const_eq_u32(cw_sum[v & 7].val, 0);
1225        let cw_denom = const_select_u32(c_cond, NonNegativeFixed::ONE.val, cw_sum[v & 7].val);
1226
1227        unroll_8_static!(x, {
1228            let is_sub = is_subtree_leaf[v & 7][x & 7] & has_children;
1229            let flat_addition = flat_part
1230                * leaf_w[v & 7][x & 7].saturating_div(NonNegativeFixed::from_bits(lw_denom));
1231            flat_alloc[x & 7] +=
1232                NonNegativeFixed::from_bits(const_select_u32(is_sub as u32, flat_addition.val, 0));
1233
1234            let is_child = (parent[x & 7] == v as i32) & has_children;
1235            let flow_addition = desc_part
1236                * child_w[v & 7][x & 7].saturating_div(NonNegativeFixed::from_bits(cw_denom));
1237            alloc_flow[x & 7] += NonNegativeFixed::from_bits(const_select_u32(
1238                is_child as u32,
1239                flow_addition.val,
1240                0,
1241            ));
1242        });
1243
1244        alloc_flow[v & 7] = NonNegativeFixed::from_bits(const_select_u32(
1245            has_children as u32,
1246            0,
1247            alloc_flow[v & 7].val,
1248        ));
1249    });
1250}
1251
1252/// Computes the allocation vector $\pi_{k, q}$ for a specific model `k` and lens spec `q`.
1253///
1254/// Traverses down the hierarchy by initializing roots and propagating flow via repeated
1255/// straight-line iterations of [`flow_step`].
1256///
1257/// # Complexity
1258/// $O(N^2)$ operations, which is $O(1)$ since $N=8$.
1259#[inline(never)]
1260#[allow(clippy::too_many_arguments)] // deliberate wide parameter list for a hot, branchless kernel
1261pub(crate) fn compute_pi_kq_for_kq(
1262    k_actual: usize,
1263    q_idx: usize,
1264    q_val_mutated: SignedFixed,
1265    parent: &[i32; N],
1266    is_leaf: &[bool; N],
1267    is_subtree_leaf: &[[bool; N]; N],
1268    node_masses: &[[NonNegativeFixed; N]; K],
1269    local_weights: &[[NonNegativeFixed; 2 * Q]; N],
1270) -> [NonNegativeFixed; N] {
1271    let mut a_roots = [0i32; N];
1272    let mut a_max_root = i32::MIN;
1273    unroll_8_static!(i, {
1274        let is_r = parent[i & 7] == -1;
1275        let a_i = (((q_val_mutated.val as i64)
1276            .wrapping_mul(node_masses[k_actual & 3][i & 7].log2().val as i64))
1277            >> 16) as i32;
1278        a_roots[i & 7] = const_select_u32(is_r as u32, a_i as u32, i32::MIN as u32) as i32;
1279        a_max_root = const_max_i32(a_max_root, a_roots[i & 7]);
1280    });
1281
1282    let mut root_w = [NonNegativeFixed::ZERO; N];
1283    let mut root_w_sum = NonNegativeFixed::ZERO;
1284    unroll_8_static!(i, {
1285        root_w[i & 7] = NonNegativeFixed::from_bits(const_select_u32(
1286            (parent[i & 7] == -1) as u32,
1287            SignedFixed::from_bits(a_roots[i & 7].wrapping_sub(a_max_root))
1288                .exp2()
1289                .val,
1290            0,
1291        ));
1292        root_w_sum += root_w[i & 7];
1293    });
1294
1295    let mut alloc_flow = [NonNegativeFixed::ZERO; N];
1296    unroll_8_static!(i, {
1297        let is_r = parent[i & 7] == -1;
1298        let r_cond = const_eq_u32(root_w_sum.val, 0);
1299        let flow_val = root_w[i & 7].saturating_div(NonNegativeFixed::from_bits(const_select_u32(
1300            r_cond,
1301            NonNegativeFixed::ONE.val,
1302            root_w_sum.val,
1303        )));
1304        alloc_flow[i & 7] =
1305            NonNegativeFixed::from_bits(const_select_u32(is_r as u32, flow_val.val, 0));
1306    });
1307
1308    let mut rho = [NonNegativeFixed::ZERO; N];
1309    let mut child_w = [[NonNegativeFixed::ZERO; N]; N];
1310    let mut cw_sum = [NonNegativeFixed::ZERO; N];
1311    let mut leaf_w = [[NonNegativeFixed::ZERO; N]; N];
1312    let mut lw_sum = [NonNegativeFixed::ZERO; N];
1313
1314    unroll_8_static!(v, {
1315        let w_sum =
1316            local_weights[v & 7][(2 * q_idx) & 7] + local_weights[v & 7][(2 * q_idx + 1) & 7];
1317        rho[v & 7] = NonNegativeFixed::from_bits(const_select_u32(
1318            const_eq_u32(w_sum.val, 0),
1319            32768,
1320            local_weights[v & 7][(2 * q_idx + 1) & 7]
1321                .saturating_div(w_sum)
1322                .val,
1323        ));
1324
1325        let mut a_c = [0i32; N];
1326        let mut a_max_c = i32::MIN;
1327        unroll_8_static!(c, {
1328            let is_c = parent[c & 7] == v as i32;
1329            a_c[c & 7] = const_select_u32(
1330                is_c as u32,
1331                (((q_val_mutated.val as i64)
1332                    .wrapping_mul(node_masses[k_actual & 3][c & 7].log2().val as i64))
1333                    >> 16) as u32,
1334                i32::MIN as u32,
1335            ) as i32;
1336            a_max_c = const_max_i32(a_max_c, a_c[c & 7]);
1337        });
1338        unroll_8_static!(c, {
1339            let matches = a_c[c & 7] != i32::MIN;
1340            child_w[v & 7][c & 7] = NonNegativeFixed::from_bits(const_select_u32(
1341                matches as u32,
1342                SignedFixed::from_bits(a_c[c & 7].wrapping_sub(a_max_c))
1343                    .exp2()
1344                    .val,
1345                0,
1346            ));
1347            cw_sum[v & 7] += child_w[v & 7][c & 7];
1348        });
1349
1350        let mut a_l = [0i32; N];
1351        let mut a_max_l = i32::MIN;
1352        unroll_8_static!(x, {
1353            let is_sub = is_subtree_leaf[v & 7][x & 7];
1354            a_l[x & 7] = const_select_u32(
1355                is_sub as u32,
1356                (((q_val_mutated.val as i64)
1357                    .wrapping_mul(node_masses[k_actual & 3][x & 7].log2().val as i64))
1358                    >> 16) as u32,
1359                i32::MIN as u32,
1360            ) as i32;
1361            a_max_l = const_max_i32(a_max_l, a_l[x & 7]);
1362        });
1363        unroll_8_static!(x, {
1364            let matches = a_l[x & 7] != i32::MIN;
1365            leaf_w[v & 7][x & 7] = NonNegativeFixed::from_bits(const_select_u32(
1366                matches as u32,
1367                SignedFixed::from_bits(a_l[x & 7].wrapping_sub(a_max_l))
1368                    .exp2()
1369                    .val,
1370                0,
1371            ));
1372            lw_sum[v & 7] += leaf_w[v & 7][x & 7];
1373        });
1374    });
1375
1376    let mut flat_alloc = [NonNegativeFixed::ZERO; N];
1377
1378    // Call flow_step 8 times sequentially to avoid stack frame nesting
1379    flow_step(
1380        parent,
1381        is_leaf,
1382        is_subtree_leaf,
1383        &rho,
1384        &child_w,
1385        &cw_sum,
1386        &leaf_w,
1387        &lw_sum,
1388        &mut alloc_flow,
1389        &mut flat_alloc,
1390    );
1391    flow_step(
1392        parent,
1393        is_leaf,
1394        is_subtree_leaf,
1395        &rho,
1396        &child_w,
1397        &cw_sum,
1398        &leaf_w,
1399        &lw_sum,
1400        &mut alloc_flow,
1401        &mut flat_alloc,
1402    );
1403    flow_step(
1404        parent,
1405        is_leaf,
1406        is_subtree_leaf,
1407        &rho,
1408        &child_w,
1409        &cw_sum,
1410        &leaf_w,
1411        &lw_sum,
1412        &mut alloc_flow,
1413        &mut flat_alloc,
1414    );
1415    flow_step(
1416        parent,
1417        is_leaf,
1418        is_subtree_leaf,
1419        &rho,
1420        &child_w,
1421        &cw_sum,
1422        &leaf_w,
1423        &lw_sum,
1424        &mut alloc_flow,
1425        &mut flat_alloc,
1426    );
1427    flow_step(
1428        parent,
1429        is_leaf,
1430        is_subtree_leaf,
1431        &rho,
1432        &child_w,
1433        &cw_sum,
1434        &leaf_w,
1435        &lw_sum,
1436        &mut alloc_flow,
1437        &mut flat_alloc,
1438    );
1439    flow_step(
1440        parent,
1441        is_leaf,
1442        is_subtree_leaf,
1443        &rho,
1444        &child_w,
1445        &cw_sum,
1446        &leaf_w,
1447        &lw_sum,
1448        &mut alloc_flow,
1449        &mut flat_alloc,
1450    );
1451    flow_step(
1452        parent,
1453        is_leaf,
1454        is_subtree_leaf,
1455        &rho,
1456        &child_w,
1457        &cw_sum,
1458        &leaf_w,
1459        &lw_sum,
1460        &mut alloc_flow,
1461        &mut flat_alloc,
1462    );
1463    flow_step(
1464        parent,
1465        is_leaf,
1466        is_subtree_leaf,
1467        &rho,
1468        &child_w,
1469        &cw_sum,
1470        &leaf_w,
1471        &lw_sum,
1472        &mut alloc_flow,
1473        &mut flat_alloc,
1474    );
1475
1476    let mut res = [NonNegativeFixed::ZERO; N];
1477    unroll_8_static!(x, res[x & 7] = flat_alloc[x & 7] + alloc_flow[x & 7]);
1478    res
1479}
1480
1481/// `mass^q_val` in Q16.16, via `exp2(q_val * log2(mass))` -- the same
1482/// log-domain power construction `compute_pi_kq_for_kq` already uses for its
1483/// max-shift-stabilized root/child/leaf weights, but *unshifted*.
1484///
1485/// CMCA-112: the "a missing max-shift constant cancels out algebraically"
1486/// claim this comment previously made is **false** in Q16.16. Unlike
1487/// `compute_pi_kq_for_kq`'s per-group max-shift (which subtracts the group's
1488/// max exponent before `exp2`, so every value in the group stays within
1489/// `exp2`'s representable range and only underflows when *genuinely*
1490/// negligible relative to the group max), this function computes each
1491/// `mass^q_val` independently and saturates in absolute (not relative)
1492/// terms: `exp2` saturates to `MAX` once its integer part reaches 16
1493/// (`mass >= 2^(16/q_val)`) and underflows to `0` below `-17`
1494/// (`mass < 2^(-17/q_val)`) regardless of any sibling's value. Both bounds
1495/// are reachable well inside `FeasibleRegion::CURRENT`
1496/// (`~[9.2e-5, 1000]`) at `q_val` near the proptest domain's `1.99` bound --
1497/// see `kappa_saturation_tests::fixed_pow_saturates_within_feasible_region_at_q_near_2`
1498/// and its underflow counterpart. Once two sibling masses both saturate to
1499/// the identical `MAX` (or both underflow to `0`), `compute_kappa`'s ratios
1500/// stop differentiating between them even though their true `mass^q_val`
1501/// ratio is large. `compute_kappa` compensates for the resulting `0/0` case
1502/// (all direct children of `v` underflow) by excluding that child's
1503/// contribution rather than trusting `saturating_div`'s spurious `MAX`; the
1504/// saturated-but-nonzero case (both siblings pinned to `MAX`) is a genuine
1505/// precision loss this function does not yet correct -- a max-shift variant
1506/// matching `compute_pi_kq_for_kq`'s approach would be required to close
1507/// that remaining gap.
1508#[inline(always)]
1509fn fixed_pow(mass: NonNegativeFixed, q_val: SignedFixed) -> NonNegativeFixed {
1510    let log_m = mass.log2();
1511    let exponent =
1512        SignedFixed::from_bits((((q_val.val as i64).wrapping_mul(log_m.val as i64)) >> 16) as i32);
1513    exponent.exp2()
1514}
1515
1516/// `mass_pow[i] = fixed_pow(node_masses[MEASURE_CACHE][i], q_val)` for every
1517/// node, computed once for a fixed `q_val`. `compute_kappa`'s `mass_pow`
1518/// array has no `v`-dependence (CMCA-120), so callers that invoke
1519/// `compute_kappa` once per `(v, q_idx)` pair must call this once per
1520/// `q_idx` and reuse the result across the `v` loop instead of recomputing
1521/// it per `(v, q_idx)` pair -- an 8x reduction in `fixed_pow` calls with no
1522/// behavior change.
1523#[inline(always)]
1524fn fixed_pow_per_node(
1525    q_val: SignedFixed,
1526    node_masses: &[[NonNegativeFixed; N]; K],
1527) -> [NonNegativeFixed; N] {
1528    let mut mass_pow = [NonNegativeFixed::ZERO; N];
1529    unroll_8_static!(i, {
1530        mass_pow[i & 7] = fixed_pow(node_masses[MEASURE_CACHE][i & 7], q_val);
1531    });
1532    mass_pow
1533}
1534
1535/// Divergence guard $\kappa_v$ for internal node `v` under lens `q_val`,
1536/// matching the module doc comment's
1537/// $\kappa_v = \sum_{c \in \text{children}(v)} s_{\text{leaf}}(c) \cdot
1538/// \log_2(s_{\text{leaf}}(c) / s_{\text{meas}}(c))$ and the f64 reference
1539/// oracle's `allocate_f64` (`tests/reference.rs`) -- CMCA-107's root cause
1540/// was this guard being entirely absent from the Q16.16 path, so
1541/// `allocate_in` always updated weights while the f64 oracle only updated
1542/// them when `kappa_v > epsilon_kappa`. See CMCA-107 for the full
1543/// investigation.
1544///
1545/// Uses `node_masses[MEASURE_CACHE]`, mirroring the f64 reference's use of
1546/// `node_masses[0]` (`MEASURE_CACHE == 0`).
1547///
1548/// `mass_pow` (`mass^q_val` per node, under `MEASURE_CACHE`) depends only on
1549/// `node_masses[MEASURE_CACHE]` and `q_val` -- neither varies with `v` -- so
1550/// callers looping over `v` for a fixed `q_idx` must compute it once
1551/// (`fixed_pow_per_node(q_val, node_masses)`) and pass the same array in for
1552/// every `v`, rather than recomputing it per call (CMCA-120).
1553#[inline(never)]
1554fn compute_kappa(
1555    v: usize,
1556    mass_pow: &[NonNegativeFixed; N],
1557    parent: &[i32; N],
1558    is_subtree_leaf: &[[bool; N]; N],
1559) -> SignedFixed {
1560    let mut sum_meas_den = NonNegativeFixed::ZERO;
1561    unroll_8_static!(c, {
1562        let is_child = parent[c & 7] == v as i32;
1563        sum_meas_den +=
1564            NonNegativeFixed::from_bits(const_select_u32(is_child as u32, mass_pow[c & 7].val, 0));
1565    });
1566
1567    let mut sum_leaf_den = NonNegativeFixed::ZERO;
1568    unroll_8_static!(x, {
1569        let is_sub = is_subtree_leaf[v & 7][x & 7];
1570        sum_leaf_den +=
1571            NonNegativeFixed::from_bits(const_select_u32(is_sub as u32, mass_pow[x & 7].val, 0));
1572    });
1573
1574    let mut kappa = SignedFixed::ZERO;
1575    unroll_8_static!(c, {
1576        let is_child = parent[c & 7] == v as i32;
1577
1578        let mut l_q_c = NonNegativeFixed::ZERO;
1579        unroll_8_static!(x, {
1580            let is_sub_c = is_subtree_leaf[c & 7][x & 7];
1581            l_q_c += NonNegativeFixed::from_bits(const_select_u32(
1582                is_sub_c as u32,
1583                mass_pow[x & 7].val,
1584                0,
1585            ));
1586        });
1587
1588        // `sum_meas_den == 0` means every direct child's mass_pow underflowed
1589        // to zero: `s_meas` for this child is a genuine `0/0`, not a real
1590        // ratio. `saturating_div` forces that to `NonNegativeFixed::MAX`
1591        // regardless of numerator (fixed.rs's `den_is_zero` branch), which
1592        // would inject a spurious large finite `s_meas` into `log_ratio`
1593        // below. The f64 reference oracle's equivalent `0.0/0.0` is `NaN`,
1594        // which poisons its whole `kappa` sum to `NaN` -- and
1595        // `kappa > epsilon_kappa` is `false` for `NaN` under IEEE-754, so the
1596        // oracle fails safe to "no update" for the entire node, not just
1597        // this child. Mirror that by excluding this child's contribution
1598        // (CMCA-112): a node with no measurable direct children carries no
1599        // divergence signal.
1600        let meas_den_is_zero = sum_meas_den.val == 0;
1601
1602        let s_meas = mass_pow[c & 7].saturating_div(sum_meas_den);
1603        let s_leaf = l_q_c.saturating_div(sum_leaf_den);
1604        let s_leaf_pos = s_leaf.val > 0;
1605
1606        let log_ratio = s_leaf.saturating_div(s_meas).log2();
1607        let term = (((s_leaf.val as i64).wrapping_mul(log_ratio.val as i64)) >> 16) as i32;
1608
1609        let contribution = const_select_u32(
1610            (is_child & s_leaf_pos & !meas_den_is_zero) as u32,
1611            term as u32,
1612            0,
1613        ) as i32;
1614        kappa = SignedFixed::from_bits(kappa.val.wrapping_add(contribution));
1615    });
1616    kappa
1617}
1618
1619#[cfg(test)]
1620mod kappa_saturation_tests {
1621    //! Regression tests for CMCA-112: `fixed_pow` saturation reachability
1622    //! within the actually-feasible mass domain, and `compute_kappa`'s
1623    //! `0/0` fail-safe behavior versus the f64 reference oracle's `NaN`
1624    //! semantics (`tests/reference.rs`).
1625    use super::*;
1626
1627    fn to_fixed(v: f64) -> NonNegativeFixed {
1628        NonNegativeFixed::from_bits((v * 65536.0).round() as u32)
1629    }
1630
1631    fn to_signed(v: f64) -> SignedFixed {
1632        SignedFixed::from_bits((v * 65536.0).round() as i32)
1633    }
1634
1635    /// CMCA-112 root cause #1: `fixed_pow` saturates to `MAX` for masses
1636    /// that are inside `FeasibleRegion::CURRENT` (`~[9.2e-5, 1000]`) and the
1637    /// differential proptest's lens-exponent domain (`-1.99..1.99`). This
1638    /// confirms the saturation is reachable, not merely a theoretical edge
1639    /// -- two masses whose true `mass^q` ratio is ~23:1 collapse to the
1640    /// identical saturated `MAX` value, destroying `compute_kappa`'s
1641    /// differentiation between them.
1642    #[test]
1643    fn fixed_pow_saturates_within_feasible_region_at_q_near_2() {
1644        let q = to_signed(1.99);
1645
1646        let low = fixed_pow(to_fixed(300.0), q);
1647        let high = fixed_pow(to_fixed(1000.0), q);
1648
1649        assert_eq!(
1650            low.val,
1651            NonNegativeFixed::MAX.val,
1652            "expected mass=300 at q=1.99 to saturate to MAX (2^(16/1.99) ~= 263 < 300)"
1653        );
1654        assert_eq!(
1655            high.val,
1656            NonNegativeFixed::MAX.val,
1657            "expected mass=1000 at q=1.99 to saturate to MAX"
1658        );
1659        // Both saturate to the identical value despite a true mass^q ratio
1660        // of ~23:1 -- the differentiation `compute_kappa` depends on is
1661        // gone at this boundary.
1662        assert_eq!(low.val, high.val);
1663    }
1664
1665    /// CMCA-112 root cause #1, underflow side: masses near the feasible
1666    /// region's floor at q near 2 underflow `fixed_pow` to exactly zero.
1667    #[test]
1668    fn fixed_pow_underflows_within_feasible_region_at_q_near_2() {
1669        let q = to_signed(1.99);
1670        let near_floor = fixed_pow(to_fixed(0.000_092), q);
1671        assert_eq!(
1672            near_floor.val, 0,
1673            "expected mass near feasible-region floor at q=1.99 to underflow to 0"
1674        );
1675    }
1676
1677    /// CMCA-112 root cause #2: when every direct child of `v` underflows to
1678    /// `mass_pow == 0` (so `sum_meas_den == 0`) while a deeper subtree leaf
1679    /// does not (so `sum_leaf_den > 0`, `s_leaf > 0`), `compute_kappa` must
1680    /// exclude that child's `0/0` `s_meas` term rather than let
1681    /// `saturating_div` force it to `NonNegativeFixed::MAX`, matching the
1682    /// f64 oracle's `NaN`-poisons-to-no-update fail-safe.
1683    #[test]
1684    fn compute_kappa_fails_safe_on_direct_child_measure_zero_zero() {
1685        // Tree: 0 is root; 1 is its only direct child; 2 is a grandchild
1686        // under 1 (so 2 is a subtree-leaf of both 1 and 0, but not a direct
1687        // child of 0). All other slots are isolated placeholder roots so
1688        // they don't participate in node 0's child/leaf sums.
1689        let mut parent = [-1i32; N];
1690        parent[1] = 0;
1691        parent[2] = 1;
1692
1693        let mut is_subtree_leaf = [[false; N]; N];
1694        // Node 1's only subtree leaf is 2.
1695        is_subtree_leaf[1][2] = true;
1696        // Node 0's subtree leaves are whatever 1's subtree leaves are (2),
1697        // since 1 is not itself a leaf.
1698        is_subtree_leaf[0][2] = true;
1699
1700        let q = to_signed(1.99);
1701        let mut node_masses = [[NonNegativeFixed::ZERO; N]; K];
1702        // Node 1 (v=0's only direct child) has a mass that underflows
1703        // fixed_pow to 0 at q=1.99, driving sum_meas_den (over v=0's direct
1704        // children) to 0 -- a genuine 0/0 for s_meas.
1705        node_masses[MEASURE_CACHE][1] = to_fixed(0.000_092);
1706        // Node 2 (a deeper subtree leaf, not a direct child of v=0) has a
1707        // mass well inside the feasible region that does NOT underflow, so
1708        // sum_leaf_den > 0 and s_leaf > 0 for node 1.
1709        node_masses[MEASURE_CACHE][2] = to_fixed(10.0);
1710
1711        let mass_pow = fixed_pow_per_node(q, &node_masses);
1712        let kappa = compute_kappa(0, &mass_pow, &parent, &is_subtree_leaf);
1713
1714        // Fail-safe: no measurable direct children under v=0 means no
1715        // divergence signal, matching the f64 oracle's NaN-poisoned kappa
1716        // failing every `kappa > epsilon_kappa` comparison.
1717        assert_eq!(
1718            kappa.val, 0,
1719            "0/0 s_meas must not inject a spurious divergence signal"
1720        );
1721    }
1722
1723    /// Sanity check: a non-degenerate case (no 0/0 anywhere, and where a
1724    /// direct child's own mass diverges from its subtree-leaf mass) still
1725    /// produces a real, nonzero divergence signal, so the fail-safe above
1726    /// isn't just zeroing everything out.
1727    #[test]
1728    fn compute_kappa_nonzero_for_non_degenerate_case() {
1729        // Tree: 0 is root with direct children 1 and 2; 1's only subtree
1730        // leaf is 3, 2's only subtree leaf is 4. Node 1 and node 2 have
1731        // equal direct mass (s_meas equal, 0.5/0.5), but their subtree
1732        // leaves 3 and 4 have very different mass, so s_leaf diverges from
1733        // s_meas and kappa should be nonzero.
1734        let mut parent = [-1i32; N];
1735        parent[1] = 0;
1736        parent[2] = 0;
1737        parent[3] = 1;
1738        parent[4] = 2;
1739
1740        let mut is_subtree_leaf = [[false; N]; N];
1741        is_subtree_leaf[1][3] = true;
1742        is_subtree_leaf[2][4] = true;
1743        is_subtree_leaf[0][3] = true;
1744        is_subtree_leaf[0][4] = true;
1745
1746        let q = to_signed(0.5);
1747        let mut node_masses = [[NonNegativeFixed::ZERO; N]; K];
1748        node_masses[MEASURE_CACHE][1] = to_fixed(5.0);
1749        node_masses[MEASURE_CACHE][2] = to_fixed(5.0);
1750        node_masses[MEASURE_CACHE][3] = to_fixed(1.0);
1751        node_masses[MEASURE_CACHE][4] = to_fixed(100.0);
1752
1753        let mass_pow = fixed_pow_per_node(q, &node_masses);
1754        let kappa = compute_kappa(0, &mass_pow, &parent, &is_subtree_leaf);
1755        assert!(
1756            kappa.val != 0,
1757            "diverging subtree-leaf mass should produce a nonzero divergence signal"
1758        );
1759    }
1760}
1761
1762mod feasible_region;
1763pub use feasible_region::FeasibleRegion;
1764
1765/// [`allocate`] parameterized by an explicit [`FeasibleRegion`] instead of
1766/// [`FeasibleRegion::CURRENT`]. See `allocate`'s docs for the mechanism,
1767/// inputs, and complexity -- identical here except for `region`.
1768///
1769/// # Branchless Contract
1770#[allow(clippy::too_many_arguments)] // deliberate wide parameter list preserving the public allocation API
1771#[allow(deprecated)] // signature legitimately carries the CMCA-114 authority-chain proof type
1772pub fn allocate_in(
1773    region: &FeasibleRegion,
1774    states: &[PackedSemanticState; N],
1775    lenses: &[LensSpec; Q],
1776    lambda: &[[NonNegativeFixed; Q]; K],
1777    eta: NonNegativeFixed,
1778    parent: &[i32; N],
1779    weights: &mut [[NonNegativeFixed; 2 * Q]; N],
1780    payoffs: &[[NonNegativeFixed; 2 * Q]; N],
1781    zeta: NonNegativeFixed,
1782    epsilon_kappa: NonNegativeFixed,
1783    mu: &[NonNegativeFixed; N],
1784    costs: &[NonNegativeFixed; N],
1785    t: u32,
1786    last_switch_t: &mut u32,
1787    prev_mode: &mut u32,
1788    tau_d: u32,
1789    digest: [u8; 32],
1790    proof: Option<&AdaptiveUpdate<CertifiedLearning>>,
1791) -> Result<[NonNegativeFixed; N], StabilityRefusal> {
1792    let mut local_weights = *weights;
1793    let mut local_last_switch_t = *last_switch_t;
1794    let mut local_prev_mode = *prev_mode;
1795
1796    let beta_max = region.beta_max;
1797    let m_min = region.m_min;
1798    let m_max = region.m_max;
1799    let mu_max = region.mu_max;
1800
1801    let proof_some = proof.is_some();
1802    let degrade_to_certified_selection = proof.is_none();
1803
1804    let mut digest_match = 1u32;
1805    unroll_32_static!(i, {
1806        digest_match &= const_eq_u32(
1807            digest[i & 31] as u32,
1808            crate::generated::stability_profile::CERTIFICATE_DIGEST[i & 31] as u32,
1809        );
1810    });
1811    let digest_err = const_eq_u32(digest_match, 0) != 0;
1812
1813    let mut gd_ok = true;
1814    unroll_5_static!(i, {
1815        let mut sum_g_d = 0u128;
1816        unroll_5_static!(j, {
1817            let g_raw = crate::generated::stability_profile::GAIN_MATRIX[i][j].raw as u128;
1818            let d_raw = crate::generated::stability_profile::WEIGHT_VECTOR[j].raw as u128;
1819            sum_g_d += g_raw * d_raw;
1820        });
1821        let lhs = sum_g_d / 1_000_000_000;
1822        let d_i_raw = crate::generated::stability_profile::WEIGHT_VECTOR[i].raw as u128;
1823        let delta_raw = crate::generated::stability_profile::CONTRACTION_MARGIN.raw as u128;
1824        let rhs = d_i_raw - (delta_raw * d_i_raw / 1_000_000_000);
1825        gd_ok &= lhs <= rhs;
1826    });
1827
1828    let zeta_w_max_q16 =
1829        ((crate::generated::stability_profile::ZETA_W_MAX.raw * 65536) / 1_000_000_000) as u32;
1830    let eta_g_min_q16 =
1831        ((crate::generated::stability_profile::ETA_G_MIN.raw * 65536) / 1_000_000_000) as u32;
1832    // CMCA-110: `eta_actual` feeds the explore-floor blend unconditionally
1833    // (`(NonNegativeFixed::ONE - eta_actual) * p_mu` below), and `sub` is
1834    // `saturating_sub` -- when `eta_actual.val > NonNegativeFixed::ONE.val`
1835    // that subtraction underflows, silently clamps to 0, and discards the
1836    // priced allocation in favor of pure uniform explore with no refusal.
1837    // `ETA_G_MAX` is exactly `NonNegativeFixed::ONE` (eta is a mixing weight
1838    // in [ETA_G_MIN, 1.0]; anything above 1.0 is out of domain for a blend
1839    // coefficient), so the ceiling is derived from the blend math itself, not
1840    // guessed.
1841    let eta_g_max_q16 = NonNegativeFixed::ONE.val;
1842
1843    let lr_err = const_lt_u32(zeta_w_max_q16, zeta.val) != 0;
1844    let dwell_err = const_lt_u32(
1845        tau_d,
1846        crate::generated::stability_profile::MODE_DWELL_ROUNDS_MIN,
1847    ) != 0;
1848
1849    let mut q_err = false;
1850    unroll_4_static!(q_idx, {
1851        let q_val = lenses[q_idx & 3].q.val;
1852        q_err |= !(-131072..=131072).contains(&q_val);
1853    });
1854
1855    let mut price_err = false;
1856    unroll_8_static!(i, {
1857        price_err |= const_lt_u32(mu_max.val, mu[i & 7].val) != 0;
1858    });
1859
1860    let eta_err =
1861        (const_lt_u32(eta.val, eta_g_min_q16) != 0) | (const_lt_u32(eta_g_max_q16, eta.val) != 0);
1862
1863    let is_zeta_less = const_lt_u32(zeta.val, beta_max.val);
1864    let beta = NonNegativeFixed::from_bits(const_select_u32(is_zeta_less, zeta.val, beta_max.val));
1865    let beta_m_max_q16 =
1866        ((crate::generated::stability_profile::BETA_M_MAX.raw * 65536) / 1_000_000_000) as u32;
1867    let beta_err = const_lt_u32(beta_m_max_q16, beta.val) != 0;
1868
1869    let has_error =
1870        !gd_ok | digest_err | lr_err | beta_err | eta_err | dwell_err | q_err | price_err;
1871    let freeze_learning = has_error & degrade_to_certified_selection;
1872
1873    let mut is_leaf = [true; N];
1874    unroll_8_static!(i, {
1875        unroll_8_static!(j, {
1876            let is_match = parent[j & 7] == i as i32;
1877            is_leaf[i & 7] &= !is_match;
1878        });
1879    });
1880
1881    #[allow(non_snake_case)]
1882    let P = ancestor_doubling_table(parent);
1883
1884    // A well-formed parent forest on N=8 nodes always reaches a root (-1) within
1885    // N hops from any node: P[6] already covers the deepest possible chain (7
1886    // edges), and once a node's ancestor is -1 it stays -1 (no p_idx in 0..7
1887    // matches parent_node == -1). So P[7][j] != -1 for any j is exactly the
1888    // branchless witness of a cycle (or a chain deeper than N-1 permits) that
1889    // would otherwise silently degrade to root_w_sum == 0 and an all-eta output.
1890    // Same witness [`check_hierarchy_acyclic`] exposes under its
1891    // `CMCA_CONTRACT.md`-documented `HierarchyRefusal::Cyclic` name.
1892    let mut has_cycle = false;
1893    unroll_8_static!(j, {
1894        has_cycle |= P[7][j] != -1;
1895    });
1896
1897    #[allow(non_snake_case)]
1898    let P_bb = core::hint::black_box(P);
1899
1900    let mut is_descendant = [[false; N]; N];
1901    unroll_8_static!(i, {
1902        unroll_8_static!(j, {
1903            let mut matched = const_eq_u32(j as u32, i as u32);
1904            unroll_8_static!(level, {
1905                matched |= const_eq_u32(P_bb[level][j] as u32, i as u32);
1906            });
1907            is_descendant[i][j] = matched != 0;
1908        });
1909    });
1910
1911    let is_descendant = core::hint::black_box(is_descendant);
1912
1913    let mut is_subtree_leaf = [[false; N]; N];
1914    unroll_8_static!(i, {
1915        unroll_8_static!(k, {
1916            is_subtree_leaf[i & 7][k & 7] = is_leaf[k & 7] & is_descendant[i & 7][k & 7];
1917        });
1918    });
1919
1920    let mut node_masses = [[NonNegativeFixed::ZERO; N]; K];
1921    unroll_8_static!(i, {
1922        let state = &states[i & 7];
1923        let f_recomp = state.factors[FACTOR_RECOMPUTATION_COST];
1924        let f_verify = state.factors[FACTOR_VERIFICATION_COST];
1925        let f_stand = state.factors[FACTOR_STANDING];
1926        let f_access = state.factors[FACTOR_ACCESS_FREQUENCY];
1927        let f_search = state.factors[FACTOR_SEARCH_DEMAND];
1928        let f_retrieve = state.factors[FACTOR_RETRIEVAL_DEMAND];
1929        let f_sched = state.factors[FACTOR_SCHEDULING_DEMAND];
1930        let f_bval = state.factors[FACTOR_BUSINESS_VALUE];
1931        let f_conseq = state.factors[FACTOR_DOWNSTREAM_CONSEQUENCE];
1932
1933        let m_cache = (f_recomp * NonNegativeFixed::from_num(5) + f_verify) * f_access * f_stand;
1934        let m_search = (f_bval + f_conseq) * f_search * f_stand;
1935        let m_retrieval = f_bval * f_retrieve;
1936        let m_sched = f_bval * f_sched;
1937
1938        node_masses[MEASURE_CACHE][i & 7] = m_cache;
1939        node_masses[MEASURE_RETRIEVAL][i & 7] = m_retrieval;
1940        node_masses[MEASURE_SCHEDULING][i & 7] = m_sched;
1941        node_masses[MEASURE_SEARCH][i & 7] = m_search;
1942    });
1943
1944    unroll_4_static!(k, {
1945        unroll_8_static!(i, {
1946            node_masses[k & 3][i & 7] = clip(node_masses[k & 3][i & 7], m_min, m_max);
1947        });
1948    });
1949
1950    let mut root_idx = 0usize;
1951    unroll_8_static!(i, {
1952        let is_root = parent[i & 7] == -1;
1953        root_idx = const_select_u32(is_root as u32, i as u32, root_idx as u32) as usize;
1954    });
1955
1956    // Load root weights branchlessly
1957    let mut root_weights = [NonNegativeFixed::ZERO; 2 * Q];
1958    unroll_8_static!(idx, {
1959        let matches = const_eq_u32(root_idx as u32, idx as u32);
1960        unroll_8_static!(e, {
1961            root_weights[e & 7] = NonNegativeFixed::from_bits(const_select_u32(
1962                matches,
1963                local_weights[idx & 7][e & 7].val,
1964                root_weights[e & 7].val,
1965            ));
1966        });
1967    });
1968
1969    let mut max_w = NonNegativeFixed::ZERO;
1970    let mut dom_mode = 0u32;
1971    unroll_8_static!(e, {
1972        let w = root_weights[e & 7];
1973        let is_greater = const_lt_u32(max_w.val, w.val);
1974        max_w = NonNegativeFixed::from_bits(const_select_u32(is_greater, w.val, max_w.val));
1975        dom_mode = const_select_u32(is_greater, e as u32, dom_mode);
1976    });
1977
1978    let switch_wanted = dom_mode != local_prev_mode;
1979    let can_switch = t.wrapping_sub(local_last_switch_t) >= tau_d;
1980    let update_allowed = !(switch_wanted & !can_switch) & !freeze_learning & proof_some;
1981
1982    // CMCA-120: `compute_kappa`'s `mass_pow` array depends only on
1983    // `node_masses[MEASURE_CACHE]` and `q_val`, neither of which varies
1984    // across the `v` loop below for a fixed `q_idx`. Compute it once per
1985    // `q_idx` here (4 arrays) instead of once per `(v, q_idx)` pair (32
1986    // times) -- an 8x reduction in `fixed_pow` calls with no behavior
1987    // change.
1988    let mut mass_pow_by_q = [[NonNegativeFixed::ZERO; N]; Q];
1989    unroll_4_static!(q_idx, {
1990        let mut q_val_mutated = SignedFixed::from_bits(lenses[q_idx & 3].q.val);
1991        #[cfg(feature = "mutant_2")]
1992        {
1993            q_val_mutated = SignedFixed::from_bits(0i32.wrapping_sub(q_val_mutated.val));
1994        }
1995        mass_pow_by_q[q_idx & 3] = fixed_pow_per_node(q_val_mutated, &node_masses);
1996    });
1997
1998    unroll_8_static!(v, {
1999        let has_children = !is_leaf[v & 7];
2000
2001        let mut is_subtree_leaf_v = [false; N];
2002        unroll_8_static!(x, {
2003            is_subtree_leaf_v[x] = is_subtree_leaf[v & 7][x & 7];
2004        });
2005
2006        unroll_4_static!(q_idx, {
2007            let w_flat = local_weights[v & 7][(2 * q_idx) & 7];
2008            let w_desc = local_weights[v & 7][(2 * q_idx + 1) & 7];
2009
2010            // Divergence guard (CMCA-107): only update this (v, q_idx) slot's
2011            // weights when the local divergence kappa_v exceeds
2012            // epsilon_kappa, matching the module doc comment's contract and
2013            // the f64 reference oracle's `update_active = kappa > epsilon_kappa`.
2014            let kappa = compute_kappa(v, &mass_pow_by_q[q_idx & 3], parent, &is_subtree_leaf);
2015            let kappa_exceeds = kappa.val > (epsilon_kappa.val as i32);
2016            let is_updating = has_children & update_allowed & kappa_exceeds;
2017            local_weights[v & 7][(2 * q_idx) & 7] = NonNegativeFixed::from_bits(const_select_u32(
2018                is_updating as u32,
2019                (w_flat
2020                    * SignedFixed::from_bits((beta * payoffs[v & 7][(2 * q_idx) & 7]).val as i32)
2021                        .exp())
2022                .val,
2023                w_flat.val,
2024            ));
2025            local_weights[v & 7][(2 * q_idx + 1) & 7] =
2026                NonNegativeFixed::from_bits(const_select_u32(
2027                    is_updating as u32,
2028                    (w_desc
2029                        * SignedFixed::from_bits(
2030                            (beta * payoffs[v & 7][(2 * q_idx + 1) & 7]).val as i32,
2031                        )
2032                        .exp())
2033                    .val,
2034                    w_desc.val,
2035                ));
2036        });
2037
2038        unroll_4_static!(q_idx, {
2039            let w_flat = local_weights[v & 7][(2 * q_idx) & 7];
2040            let w_desc = local_weights[v & 7][(2 * q_idx + 1) & 7];
2041            let sum_div = w_flat + w_desc;
2042            local_weights[v & 7][(2 * q_idx) & 7] = NonNegativeFixed::from_bits(const_select_u32(
2043                update_allowed as u32,
2044                w_flat.saturating_div(sum_div).val,
2045                w_flat.val,
2046            ));
2047            local_weights[v & 7][(2 * q_idx + 1) & 7] =
2048                NonNegativeFixed::from_bits(const_select_u32(
2049                    update_allowed as u32,
2050                    w_desc.saturating_div(sum_div).val,
2051                    w_desc.val,
2052                ));
2053        });
2054    });
2055
2056    let mut new_dom_mode = 0u32;
2057    let mut new_max_w = NonNegativeFixed::ZERO;
2058
2059    // Reload root weights
2060    unroll_8_static!(idx, {
2061        let matches = const_eq_u32(root_idx as u32, idx as u32);
2062        unroll_8_static!(e, {
2063            root_weights[e & 7] = NonNegativeFixed::from_bits(const_select_u32(
2064                matches,
2065                local_weights[idx & 7][e & 7].val,
2066                root_weights[e & 7].val,
2067            ));
2068        });
2069    });
2070
2071    unroll_8_static!(e, {
2072        let w = root_weights[e & 7];
2073        let is_greater = const_lt_u32(new_max_w.val, w.val);
2074        new_max_w = NonNegativeFixed::from_bits(const_select_u32(is_greater, w.val, new_max_w.val));
2075        new_dom_mode = const_select_u32(is_greater, e as u32, new_dom_mode);
2076    });
2077
2078    let did_switch = (new_dom_mode != local_prev_mode) & can_switch & !freeze_learning & proof_some;
2079    local_last_switch_t = const_select_u32(did_switch as u32, t, local_last_switch_t);
2080    local_prev_mode = const_select_u32(did_switch as u32, new_dom_mode, local_prev_mode);
2081
2082    let mut pi_kq = [[[NonNegativeFixed::ZERO; N]; Q]; K];
2083
2084    unroll_4_static!(k, {
2085        #[cfg(feature = "mutant_1")]
2086        const k_actual: usize = 0;
2087        #[cfg(not(feature = "mutant_1"))]
2088        const k_actual: usize = k;
2089
2090        unroll_4_static!(q_idx, {
2091            let q_val_mutated = SignedFixed::from_bits(lenses[q_idx & 3].q.val);
2092            #[cfg(feature = "mutant_2")]
2093            let q_val_mutated = SignedFixed::from_bits(0i32.wrapping_sub(q_val_mutated.val));
2094
2095            let res_kq = compute_pi_kq_for_kq(
2096                k_actual,
2097                q_idx,
2098                q_val_mutated,
2099                parent,
2100                &is_leaf,
2101                &is_subtree_leaf,
2102                &node_masses,
2103                &local_weights,
2104            );
2105            unroll_8_static!(x, pi_kq[k & 3][q_idx & 3][x & 7] = res_kq[x & 7]);
2106        });
2107    });
2108
2109    let mut pi_combined = [NonNegativeFixed::ZERO; N];
2110    unroll_4_static!(k, {
2111        unroll_4_static!(q_idx, {
2112            unroll_8_static!(x, {
2113                let term = lambda[k & 3][q_idx & 3] * pi_kq[k & 3][q_idx & 3][x & 7];
2114                pi_combined[x & 7] += term;
2115            });
2116        });
2117    });
2118
2119    let mut pi_res = [NonNegativeFixed::ZERO; N];
2120    let mut priced_sum = NonNegativeFixed::ZERO;
2121    unroll_8_static!(x, {
2122        // CMCA-122: since CMCA-103, `price_err` (mu[x] > mu_max) refuses
2123        // *unconditionally* before this computation's result can be
2124        // observed on any `Ok` path (see `selection_critical_error` below),
2125        // so `clip` can only ever be non-identity here on a path that is
2126        // already guaranteed to be refused. It is retained as an explicit
2127        // defense-in-depth clamp -- not load-bearing for correctness on the
2128        // `Ok` path, but cheap and branchless -- rather than removed
2129        // outright. `clip`'s own clamping behavior is unit-tested directly
2130        // in `clip_tests` below, independent of this admission gate.
2131        //
2132        // Mutant_5 slot, retired and re-armed 2026-10-01 (ledger:
2133        // tests/mutant_kill_survivors.rs): the original mutation here
2134        // dropped the defensive `clip` and SURVIVED by construction -- the
2135        // clip is provably non-identity only on already-refused paths
2136        // (CMCA-122), so no admitted-input test can kill it. The slot now
2137        // injects `mu_actual = mu_max`: the priced softmax reads the region
2138        // price CEILING where the admitted price vector `mu` is
2139        // contractual (p = pi_combined * exp(-mu*costs)). Unlike the clip
2140        // drop, that corruption is observable on `Ok` paths (any fixture
2141        // with mu != mu_max pricing), and `mu_max` is in scope here as the
2142        // same clamp bound the defensive clip uses.
2143        #[cfg(feature = "mutant_5")]
2144        let mu_actual = mu_max;
2145        #[cfg(not(feature = "mutant_5"))]
2146        let mu_actual = clip(mu[x & 7], NonNegativeFixed::ZERO, mu_max);
2147
2148        let p = pi_combined[x & 7]
2149            * SignedFixed::from_bits(0i32.wrapping_sub((mu_actual * costs[x & 7]).val as i32))
2150                .exp();
2151        priced_sum +=
2152            NonNegativeFixed::from_bits(const_select_u32(is_leaf[x & 7] as u32, p.val, 0));
2153    });
2154    let psd = NonNegativeFixed::from_bits(const_select_u32(
2155        const_eq_u32(priced_sum.val, 0),
2156        NonNegativeFixed::ONE.val,
2157        priced_sum.val,
2158    ));
2159    let mut nl = 0u32;
2160    unroll_8_static!(i, {
2161        nl += is_leaf[i & 7] as u32;
2162    });
2163    unroll_8_static!(x, {
2164        // Second mutant_5 site (2026-10-01 retirement + re-arm, ledger:
2165        // tests/mutant_kill_survivors.rs): same substitution as the
2166        // `priced_sum` loop above -- the mutant prices `p_mu` from the
2167        // region ceiling `mu_max` instead of the admitted `mu`, keeping the
2168        // denominator and numerator of the softmax consistently (not
2169        // incoherently) corrupted so the observable divergence is the price
2170        // law itself.
2171        #[cfg(feature = "mutant_5")]
2172        let mu_actual = mu_max;
2173        #[cfg(not(feature = "mutant_5"))]
2174        let mu_actual = clip(mu[x & 7], NonNegativeFixed::ZERO, mu_max);
2175
2176        let p_mu = (pi_combined[x & 7]
2177            * SignedFixed::from_bits(0i32.wrapping_sub((mu_actual * costs[x & 7]).val as i32))
2178                .exp())
2179        .saturating_div(psd);
2180
2181        #[cfg(feature = "mutant_4")]
2182        let eta_actual = zeta;
2183        #[cfg(not(feature = "mutant_4"))]
2184        let eta_actual = eta;
2185
2186        // Use lookup table to avoid division
2187        let mut nl_recip = NonNegativeFixed::ZERO;
2188        unroll_9_static!(idx, {
2189            let matches = const_eq_u32(nl, idx as u32);
2190            nl_recip = NonNegativeFixed::from_bits(const_select_u32(
2191                matches,
2192                LEAF_RECIP[idx].val,
2193                nl_recip.val,
2194            ));
2195        });
2196
2197        let val = (eta_actual * nl_recip) + ((NonNegativeFixed::ONE - eta_actual) * p_mu);
2198        let pi_val = pi_res[x & 7];
2199        pi_res[x & 7] = NonNegativeFixed::from_bits(const_select_u32(
2200            is_leaf[x & 7] as u32,
2201            val.val,
2202            pi_val.val,
2203        ));
2204    });
2205
2206    // Numeric refusals accumulated through the Q16.16 arithmetic chain (overflow,
2207    // underflow, division by zero) were previously discarded here: `wrap_result`
2208    // only ever saw `err_val` below, never `pi_res[x].err`. Fold them in so
2209    // `NumericRangeExceeded`/`UnsupportedDomain` become reachable.
2210    let mut numeric_err = u32::MAX;
2211    unroll_8_static!(x, {
2212        numeric_err = crate::fixed::branchless_err_acc(numeric_err, pi_res[x & 7].err);
2213    });
2214    let numeric_has_err = const_eq_u32(numeric_err, u32::MAX) == 0;
2215
2216    let has_error = has_error | has_cycle;
2217    // CMCA-103: investigation found the proof=None / degrade-to-certified-
2218    // selection path is NOT uniformly allowed to bypass admission -- but it
2219    // legitimately bypasses admission for a specific subset of `has_error`'s
2220    // components, and that subset does not include `q_err`.
2221    //
2222    // `q_err`, `price_err`, and `eta_err` all feed *unconditionally* into
2223    // the selection computation below (`lenses[..].q` in the `pi_kq`
2224    // pass, `mu`/`mu_actual` in the pricing pass, and `eta_actual` in the
2225    // explore-floor blend all execute regardless of `proof`/`update_allowed`
2226    // -- see the `pi_kq`, `pi_res`/`priced_sum`, and explore-floor blocks
2227    // above). An out-of-range value there silently corrupts the *returned*
2228    // allocation even when only "certified selection" (no learning update)
2229    // is being performed, so these must refuse unconditionally -- this is
2230    // the CMCA-103 defect (confirmed via
2231    // crates/bcinr-cmca/tests/runtime_semantic_classification.rs and, prior
2232    // to this fix, silently accepted on the proof=None path exercised by
2233    // nearly every caller in this crate).
2234    //
2235    // The remaining `has_error` components -- `digest_err`, `gd_ok`
2236    // (gain-matrix contraction), `lr_err`/`beta_err` (learning rate), and
2237    // `dwell_err` (mode-switch timing), plus `has_cycle` -- are consumed only
2238    // by the weight-update / mode-switch code, which is already
2239    // independently gated by `proof_some`/`update_allowed` (see
2240    // `is_updating`, `did_switch` above): with proof=None that code is
2241    // already a no-op regardless of these flags, so refusing on them too
2242    // would only ever discard an unmodified, already-certified selection.
2243    // Continuing to gate refusal on those by `!degrade_to_certified_selection`
2244    // is a legitimate, deliberately documented contract -- see
2245    // `jtbd_drift_refusal_routes_to_selection_only_without_state_drift` in
2246    // tests/jtbd_certified_actuation_chicago.rs, which Chicago-TDD-asserts
2247    // that absence of adaptive authority (proof=None) must degrade a
2248    // digest-mismatched call to certified selection rather than refuse it.
2249    //
2250    // CMCA-110: `numeric_has_err` is different from those -- it is folded
2251    // from `pi_res[x].err`, i.e. errors from the Q16.16 arithmetic chain in
2252    // the `pi_kq`/pricing/explore-floor blend that *does* execute
2253    // unconditionally (see the block comment above). A numeric fault
2254    // produced there (e.g. the eta-underflow this ticket closes) must
2255    // refuse unconditionally too, so it belongs in `selection_critical_error`
2256    // rather than the proof-gated `has_error` bucket.
2257    let selection_critical_error = q_err | price_err | eta_err | numeric_has_err;
2258    let has_refusal = selection_critical_error | (has_error & !degrade_to_certified_selection);
2259    unroll_8_static!(v, {
2260        unroll_8_static!(e, {
2261            weights[v & 7][e & 7] = NonNegativeFixed::from_bits(const_select_u32(
2262                has_refusal as u32,
2263                weights[v & 7][e & 7].val,
2264                local_weights[v & 7][e & 7].val,
2265            ));
2266        });
2267    });
2268    *last_switch_t = const_select_u32(has_refusal as u32, *last_switch_t, local_last_switch_t);
2269    *prev_mode = const_select_u32(has_refusal as u32, *prev_mode, local_prev_mode);
2270
2271    // CMCA-122: `eta_err` and `price_err` each now get a dedicated branch
2272    // instead of being folded into `LearningRateOutsideEnvelope` (`eta_err`)
2273    // or relying on the innermost fallback (`price_err`) -- see the 5-whys
2274    // in CMCA-122.md. `price_err` is checked ahead of `eta_err` so that when
2275    // both are true simultaneously, the reported reason is the pricing
2276    // fault (`PriceGainUnsafe`) rather than a misleading learning-rate/
2277    // explore-floor reason -- either co-occurring condition previously
2278    // masked, whichever this chain checks second, so an explicit priority
2279    // is required and pricing is chosen as the higher-priority admission
2280    // concern.
2281    let err_val = const_select_u32(
2282        has_cycle as u32,
2283        StabilityRefusal::ContractViolation as u32,
2284        const_select_u32(
2285            q_err as u32,
2286            5,
2287            const_select_u32(
2288                dwell_err as u32,
2289                4,
2290                const_select_u32(
2291                    price_err as u32,
2292                    StabilityRefusal::PriceGainUnsafe as u32,
2293                    const_select_u32(
2294                        eta_err as u32,
2295                        StabilityRefusal::ExploreFloorOutsideEnvelope as u32,
2296                        const_select_u32(
2297                            (lr_err | beta_err) as u32,
2298                            3,
2299                            const_select_u32(
2300                                (!gd_ok) as u32,
2301                                1,
2302                                const_select_u32(
2303                                    digest_err as u32,
2304                                    10,
2305                                    const_select_u32(numeric_has_err as u32, numeric_err, 7),
2306                                ),
2307                            ),
2308                        ),
2309                    ),
2310                ),
2311            ),
2312        ),
2313    );
2314    wrap_result(
2315        pi_res,
2316        const_select_u32(has_refusal as u32, err_val, u32::MAX),
2317    )
2318}
2319
2320/// Allocates resources down the node forest branchlessly, performing MWU step updates,
2321/// stable projections, and explore floors.
2322///
2323/// This is the entry point for the Cascade Allocation engine. It is
2324/// [`allocate_in`] with the bounds this crate has always used
2325/// ([`FeasibleRegion::CURRENT`]) -- preserved as a thin wrapper so no
2326/// existing caller needs to change:
2327/// `allocate(x) == allocate_in(&FeasibleRegion::CURRENT, x)` for every
2328/// input, by construction. Call `allocate_in` directly to use a different
2329/// [`FeasibleRegion`].
2330///
2331/// # Fixed shape: `N = 8`, `K = 4`, `Q = 4` -- not a generic parameter
2332///
2333/// This function is compiled against this crate's own `N`, `K`, `Q`
2334/// constants ([`crate::generated::consequence_mass::case_studies::N`] `= 8`,
2335/// `K = 4`, `Q = 4`), not a caller-chosen size. `N`/`K`/`Q` are plain `const`
2336/// items, not `const` generic parameters -- there is no way to call this
2337/// function at a different shape without this crate itself being
2338/// regenerated against a different `ontology/*.ttl`.
2339///
2340/// This is deliberate, not an oversight (see CMCA-108): the branchless,
2341/// $CC=1$, allocation-free implementation below unrolls its inner loops
2342/// (`unroll_8_static!`, `unroll_4_static!`) against the literal constants
2343/// `8`/`4`/`4` at dozens of call sites in the private kernel this function
2344/// and [`allocate_single_lens`] both call into. Making `N`/`K`/`Q` into
2345/// `const` generic parameters on just these two public functions would not,
2346/// by itself, generalize the algorithm -- the unrolled kernel beneath them
2347/// would still silently assume 8/4/4 (e.g. `is_leaf[i & 7]` indexing with a
2348/// literal `& 7` mask), so a naive signature change risks *compiling* at a
2349/// different shape while computing the wrong answer. Correctly widening
2350/// this kernel is a substantially larger rewrite of the unrolling
2351/// infrastructure itself, out of scope for a targeted fix.
2352///
2353/// If your data does not have exactly 8 objects / 4 measures / 4 lenses,
2354/// this function is not callable for your shape. Use
2355/// `crate::cascade::consequence_mass` instead: it takes a tree of
2356/// **any** shape and a lens per level (trading the branchless/$O(1)$
2357/// guarantee for that generality -- see the `crate::cascade` module docs
2358/// for the full tradeoff).
2359///
2360/// # Mathematical Behavior
2361///
2362/// 1. **Divergence Guard & MWU**: For each internal node, computes the divergence $\kappa_v$ between child
2363///    allocations and subtree leaf distributions. If $\kappa_v > \epsilon_{\kappa}$ and learning is authorized
2364///    by `proof`, updates routing weights multiplicatively using payoffs scaled by learning rate $\beta$.
2365/// 2. **Cascade flow propagation**: Distributes flow from roots to leaves over the hierarchy of $N$ nodes.
2366/// 3. **Stable projection**: Scales leaf allocations by $\exp(-\mu_x \cdot c_x)$ and normalizes.
2367/// 4. **Explore floor mixture**: Restricts allocations from dropping below $\frac{\eta}{n_L}$ by mixing the
2368///    projection with a uniform distribution.
2369///
2370/// # Inputs
2371/// - `states`: Packed semantic states for the $N$ nodes.
2372/// - `lenses`: Lenses defining policy priorities.
2373/// - `lambda`: Weighting matrix mapping models and lenses to overall priority.
2374/// - `eta`: Explore floor parameter $\eta \in [0, 1]$.
2375/// - `parent`: Forest structure represented by parent indices (where `-1` indicates root).
2376/// - `weights`: Multiplicative routing weights (updated in place).
2377/// - `payoffs`: Environment payoff feedback for each decision slot.
2378/// - `zeta`: Learning rate parameter.
2379/// - `epsilon_kappa`: Divergence update threshold.
2380/// - `mu`: Resource prices vector.
2381/// - `costs`: Operational costs vector.
2382/// - `t`: Current epoch index.
2383/// - `last_switch_t`: Epoch of the last policy switch (updated in place).
2384/// - `prev_mode`: Currently active policy mode index (updated in place).
2385/// - `tau_d`: Minimum dwell rounds constraint.
2386/// - `digest`: Security certificate digest.
2387/// - `proof`: Verification proof authorizing learning updates.
2388///
2389/// # Outputs
2390/// Returns a nonnegative resource-allocation vector over the $N$ nodes if
2391/// successful, or a [`StabilityRefusal`] code otherwise. In the ordinary
2392/// normalized regime the components sum approximately to `ONE` (see
2393/// [`FeasibleRegion::contains_allocation`]); a documented fallback regime
2394/// (`priced_sum` underflowing to zero) can legitimately return a
2395/// subnormalized vector instead -- this is not a probability distribution
2396/// by construction, only in the common case. See `contains_allocation`'s
2397/// docs for the counterexample that established this.
2398///
2399/// # Complexity
2400/// - **Time Complexity**: $O(K \cdot Q \cdot N^2)$ operations ($O(1)$ constant time).
2401/// - **Space Complexity**: $O(1)$ auxiliary stack space.
2402/// - **Cyclomatic Complexity**: $CC = 1$.
2403///
2404/// # Examples
2405///
2406/// ```rust
2407/// use bcinr_cmca::fixed::NonNegativeFixed;
2408/// use bcinr_cmca::allocator::{allocate, AdaptiveUpdate, AdmittedControlState, CertificateReceipt, EnvelopeReceipt, OutcomeReceipt, CertifiedLearning};
2409/// use bcinr_cmca::generated::consequence_mass::case_studies::{OBJECT_REGISTRY, LENS_REGISTRY, LAMBDA, ETA, N, Q};
2410/// use bcinr_cmca::generated::stability_profile::CERTIFICATE_DIGEST;
2411///
2412/// let mut weights = [[NonNegativeFixed::ONE; 2 * Q]; N];
2413/// let payoffs = [[NonNegativeFixed::ZERO; 2 * Q]; N];
2414/// let mut last_switch_t = 0;
2415/// let mut prev_mode = 0;
2416/// let parent = [-1; N];
2417/// let mu = [NonNegativeFixed::ZERO; N];
2418/// let costs = [NonNegativeFixed::ZERO; N];
2419///
2420/// let proof = AdaptiveUpdate::admit_adaptive_update(
2421///     AdmittedControlState::admit_control_state(0),
2422///     CertificateReceipt::admit_certificate(0),
2423///     EnvelopeReceipt::admit_envelope(0),
2424///     OutcomeReceipt::admit_outcome(0),
2425///     NonNegativeFixed::ZERO,
2426///     NonNegativeFixed::ONE,
2427///     CertifiedLearning::admit_learning(),
2428/// );
2429///
2430/// let result = allocate(
2431///     &OBJECT_REGISTRY,
2432///     &LENS_REGISTRY,
2433///     &LAMBDA,
2434///     ETA,
2435///     &parent,
2436///     &mut weights,
2437///     &payoffs,
2438///     NonNegativeFixed::ZERO,
2439///     NonNegativeFixed::ZERO,
2440///     &mu,
2441///     &costs,
2442///     0,
2443///     &mut last_switch_t,
2444///     &mut prev_mode,
2445///     500,
2446///     CERTIFICATE_DIGEST,
2447///     proof.as_ref(),
2448/// );
2449/// assert!(result.is_ok());
2450/// ```
2451#[allow(clippy::too_many_arguments)] // deliberate wide parameter list preserving the public allocation API
2452#[allow(deprecated)] // signature legitimately carries the CMCA-114 authority-chain proof type
2453pub fn allocate(
2454    states: &[PackedSemanticState; N],
2455    lenses: &[LensSpec; Q],
2456    lambda: &[[NonNegativeFixed; Q]; K],
2457    eta: NonNegativeFixed,
2458    parent: &[i32; N],
2459    weights: &mut [[NonNegativeFixed; 2 * Q]; N],
2460    payoffs: &[[NonNegativeFixed; 2 * Q]; N],
2461    zeta: NonNegativeFixed,
2462    epsilon_kappa: NonNegativeFixed,
2463    mu: &[NonNegativeFixed; N],
2464    costs: &[NonNegativeFixed; N],
2465    t: u32,
2466    last_switch_t: &mut u32,
2467    prev_mode: &mut u32,
2468    tau_d: u32,
2469    digest: [u8; 32],
2470    proof: Option<&AdaptiveUpdate<CertifiedLearning>>,
2471) -> Result<[NonNegativeFixed; N], StabilityRefusal> {
2472    allocate_in(
2473        &FeasibleRegion::CURRENT,
2474        states,
2475        lenses,
2476        lambda,
2477        eta,
2478        parent,
2479        weights,
2480        payoffs,
2481        zeta,
2482        epsilon_kappa,
2483        mu,
2484        costs,
2485        t,
2486        last_switch_t,
2487        prev_mode,
2488        tau_d,
2489        digest,
2490        proof,
2491    )
2492}
2493
2494/// Why [`allocate_single_lens`] refused to produce a single-lens allocation.
2495///
2496/// A typed, non-panicking refusal, matching the crate's established
2497/// one-variant-per-check convention (mirrors `CertificationRefusal`'s
2498/// shape).
2499#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2500pub enum LensSelectionRefusal {
2501    /// `measure` was not a valid index into the `K` measures (`0..K`).
2502    MeasureIndexOutOfRange { measure: usize },
2503    /// `lens_idx` was not a valid index into the `Q` lenses (`0..Q`).
2504    LensIndexOutOfRange { lens_idx: usize },
2505    /// `lenses[lens_idx].q`'s magnitude exceeded
2506    /// `crate::cascade::MAX_LENS_MAGNITUDE` -- the same, unconditionally
2507    /// enforced bound `crate::escort::escort_distribution` checks. Not to
2508    /// be confused with `allocate_in`'s separate, `proof`-conditional
2509    /// `q in [-2, 2]` admission policy (see
2510    /// `crate::generated_profile::MAX_LENS_MAGNITUDE`'s doc comment for why
2511    /// the two are different, independently-sourced domains).
2512    QMagnitudeExceeded { q: SignedFixed },
2513    /// `parent` contains a cycle (or a chain deeper than `N - 1` hops
2514    /// permits) -- the same witness [`check_hierarchy_acyclic`] checks.
2515    Cyclic,
2516}
2517
2518impl core::fmt::Display for LensSelectionRefusal {
2519    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2520        core::fmt::Debug::fmt(self, f)
2521    }
2522}
2523
2524#[cfg(feature = "std")]
2525impl std::error::Error for LensSelectionRefusal {}
2526
2527/// Returns exactly one lens's allocation vector `pi_kq[measure][lens_idx]`,
2528/// bypassing the LAMBDA-weighted blend across all `K x Q` measure/lens pairs
2529/// that [`allocate`] always performs.
2530///
2531/// # Fixed shape: `N = 8`, `K = 4`, `Q = 4`
2532///
2533/// Like [`allocate`], this function is bound to this crate's own compiled-in
2534/// `N`/`K`/`Q` (8/4/4) -- see [`allocate`]'s "Fixed shape" section for why,
2535/// and for the escape hatch (`crate::cascade::consequence_mass`) for
2536/// other shapes.
2537///
2538/// # Why this exists
2539///
2540/// [`allocate`] computes `pi_kq[k][q]` for every one of the `K * Q`
2541/// (measure, lens) pairs on every call (from real
2542/// [`PackedSemanticState::factors`] data, via the same per-measure mass
2543/// formulas this function reuses below), then unconditionally sums all of
2544/// them into one LAMBDA-weighted `pi_combined` -- there was no way, before
2545/// this function, for a caller to ask "what would lens `q` alone say."
2546/// `bcinr-cmca`'s own adversarial test suite documented this gap directly
2547/// (`tests/falsification_adversarial.rs`'s "per-lens isolation is not
2548/// observable through the public API").
2549///
2550/// This function reuses `compute_pi_kq_for_kq` -- the exact private
2551/// kernel `allocate_in` already calls once per `(k, q_idx)` pair before
2552/// discarding the individual results into the blend -- rather than
2553/// reimplementing the escort math against `crate::escort::escort_distribution`.
2554///
2555/// # The blend identity -- and its precondition (CMCA-111)
2556///
2557/// `allocate_in` mutates its `weights: &mut` parameter in place: on a
2558/// successful (non-refusing) call it overwrites `weights` with
2559/// `local_weights`, the post-MWU-update state `compute_pi_kq_for_kq` was
2560/// actually evaluated against to build `pi_combined` (see `allocate_in`'s
2561/// weight-update block above and its final `weights[v][e] = select(...)`
2562/// write-back). This function, by contrast, applies no MWU update of its
2563/// own (see "What this deliberately omits" below) -- it evaluates
2564/// `compute_pi_kq_for_kq` against exactly the `weights` slice the caller
2565/// hands it, whatever state that happens to be in.
2566///
2567/// Consequently:
2568///
2569/// ```text
2570/// sum_{k,q} lambda[k][q] * allocate_single_lens(..., k, q, parent, weights) ==
2571///     pi_combined   (allocate(...)'s internal, pre-mu/costs/eta value)
2572/// ```
2573///
2574/// holds **only** when `weights` is the *same array, read after* a
2575/// successful `allocate`/`allocate_in` call made with matching
2576/// `states`/`lenses`/`parent` -- i.e. the post-MWU-update snapshot that
2577/// call left behind via its `&mut weights` write-back. It does **not**
2578/// hold for an independently-held `weights` snapshot taken *before* such a
2579/// call (e.g. the caller's original weights, or a cloned copy): whenever
2580/// that call's payoffs are non-zero and its divergence guard admits an
2581/// update (`kappa_v > epsilon_kappa`), `local_weights` diverges from the
2582/// pre-call `weights`, and `pi_combined` was built from the former, not
2583/// the latter. With all-zero payoffs the MWU multiplicative step is a
2584/// no-op (`exp(beta*0) == 1`), so pre- and post-call weights coincide up
2585/// to a scale-invariant renormalization -- which is why this identity can
2586/// look unconditional under degenerate all-zero-payoff testing, and does
2587/// not hold in general. See
2588/// `tests/single_lens_allocation.rs`'s
2589/// `blend_identity_requires_the_post_mwu_update_weights_snapshot` for a
2590/// non-degenerate (non-zero, differentiated payoffs) demonstration of both
2591/// the pre-call divergence and the post-call reconstruction.
2592///
2593/// # What this deliberately omits
2594///
2595/// No `t`, `last_switch_t`, `prev_mode`, `tau_d`, `mu`, `costs`, `zeta`, or
2596/// `eta` parameters. Those govern `allocate`'s LAMBDA-blend-specific
2597/// learning-rate update and dwell-time mode-switching machinery
2598/// (`dom_mode`/`prev_mode` hysteresis) -- concepts with no defined meaning
2599/// for a single, stateless per-lens query: a lens-selector call either
2600/// returns lens `(k, q)`'s answer or it doesn't, there is no accumulated
2601/// mode to protect from thrashing between successive calls.
2602///
2603/// # Errors
2604///
2605/// Refuses (never panics) on an out-of-range `measure`/`lens_idx`, a
2606/// `q` magnitude beyond `crate::cascade::MAX_LENS_MAGNITUDE`, or a
2607/// cyclic `parent` -- see [`LensSelectionRefusal`].
2608pub fn allocate_single_lens(
2609    states: &[PackedSemanticState; N],
2610    lenses: &[LensSpec; Q],
2611    measure: usize,
2612    lens_idx: usize,
2613    parent: &[i32; N],
2614    weights: &[[NonNegativeFixed; 2 * Q]; N],
2615) -> Result<[NonNegativeFixed; N], LensSelectionRefusal> {
2616    if measure >= K {
2617        return Err(LensSelectionRefusal::MeasureIndexOutOfRange { measure });
2618    }
2619    if lens_idx >= Q {
2620        return Err(LensSelectionRefusal::LensIndexOutOfRange { lens_idx });
2621    }
2622    let q = lenses[lens_idx].q;
2623    // Same bound, same check idiom as `escort::escort_distribution` (escort.rs)
2624    // -- unconditionally enforced, unlike `allocate_in`'s separate `q in
2625    // [-2, 2]` admission policy (see `LensSelectionRefusal::QMagnitudeExceeded`'s
2626    // doc comment).
2627    if q.to_bits().unsigned_abs() > crate::generated_profile::MAX_LENS_MAGNITUDE << 16 {
2628        return Err(LensSelectionRefusal::QMagnitudeExceeded { q });
2629    }
2630    if check_hierarchy_acyclic(parent).is_err() {
2631        return Err(LensSelectionRefusal::Cyclic);
2632    }
2633
2634    let region = &FeasibleRegion::CURRENT;
2635    let m_min = region.m_min;
2636    let m_max = region.m_max;
2637
2638    let mut is_leaf = [true; N];
2639    unroll_8_static!(i, {
2640        unroll_8_static!(j, {
2641            let is_match = parent[j & 7] == i as i32;
2642            is_leaf[i & 7] &= !is_match;
2643        });
2644    });
2645
2646    #[allow(non_snake_case)]
2647    let P = ancestor_doubling_table(parent);
2648    #[allow(non_snake_case)]
2649    let P_bb = core::hint::black_box(P);
2650
2651    let mut is_descendant = [[false; N]; N];
2652    unroll_8_static!(i, {
2653        unroll_8_static!(j, {
2654            let mut matched = const_eq_u32(j as u32, i as u32);
2655            unroll_8_static!(level, {
2656                matched |= const_eq_u32(P_bb[level][j] as u32, i as u32);
2657            });
2658            is_descendant[i][j] = matched != 0;
2659        });
2660    });
2661    let is_descendant = core::hint::black_box(is_descendant);
2662
2663    let mut is_subtree_leaf = [[false; N]; N];
2664    unroll_8_static!(i, {
2665        unroll_8_static!(k, {
2666            is_subtree_leaf[i & 7][k & 7] = is_leaf[k & 7] & is_descendant[i & 7][k & 7];
2667        });
2668    });
2669
2670    // Same per-measure mass derivation `allocate_in` uses (allocator/mod.rs's
2671    // `node_masses` block) -- transcribed here rather than factored into a
2672    // shared helper for this change, since `allocate_in`'s version is
2673    // entangled with its own MWU weight-update state; kept byte-identical in
2674    // formula so the two can never compute different masses for the same
2675    // `states`.
2676    let mut node_masses = [[NonNegativeFixed::ZERO; N]; K];
2677    unroll_8_static!(i, {
2678        let state = &states[i & 7];
2679        let f_recomp = state.factors[FACTOR_RECOMPUTATION_COST];
2680        let f_verify = state.factors[FACTOR_VERIFICATION_COST];
2681        let f_stand = state.factors[FACTOR_STANDING];
2682        let f_access = state.factors[FACTOR_ACCESS_FREQUENCY];
2683        let f_search = state.factors[FACTOR_SEARCH_DEMAND];
2684        let f_retrieve = state.factors[FACTOR_RETRIEVAL_DEMAND];
2685        let f_sched = state.factors[FACTOR_SCHEDULING_DEMAND];
2686        let f_bval = state.factors[FACTOR_BUSINESS_VALUE];
2687        let f_conseq = state.factors[FACTOR_DOWNSTREAM_CONSEQUENCE];
2688
2689        let m_cache = (f_recomp * NonNegativeFixed::from_num(5) + f_verify) * f_access * f_stand;
2690        let m_search = (f_bval + f_conseq) * f_search * f_stand;
2691        let m_retrieval = f_bval * f_retrieve;
2692        let m_sched = f_bval * f_sched;
2693
2694        node_masses[MEASURE_CACHE][i & 7] = m_cache;
2695        node_masses[MEASURE_RETRIEVAL][i & 7] = m_retrieval;
2696        node_masses[MEASURE_SCHEDULING][i & 7] = m_sched;
2697        node_masses[MEASURE_SEARCH][i & 7] = m_search;
2698    });
2699    unroll_4_static!(k, {
2700        unroll_8_static!(i, {
2701            node_masses[k & 3][i & 7] = clip(node_masses[k & 3][i & 7], m_min, m_max);
2702        });
2703    });
2704
2705    Ok(compute_pi_kq_for_kq(
2706        measure,
2707        lens_idx,
2708        q,
2709        parent,
2710        &is_leaf,
2711        &is_subtree_leaf,
2712        &node_masses,
2713        weights,
2714    ))
2715}
2716
2717/// ```compile_fail
2718/// use bcinr_cmca::allocator::AdmittedControlState;
2719/// let state = AdmittedControlState { digest: 0 };
2720/// ```
2721///
2722/// ```compile_fail
2723/// use bcinr_cmca::allocator::CertifiedLearning;
2724/// let mode = CertifiedLearning { _sealed: () };
2725/// ```
2726///
2727/// ```compile_fail
2728/// use bcinr_cmca::allocator::{AdaptiveUpdate, CertifiedLearning};
2729/// let update = AdaptiveUpdate::<CertifiedLearning> { _mode: core::marker::PhantomData };
2730/// ```
2731///
2732/// ```compile_fail
2733/// use bcinr_cmca::allocator::{AdaptiveUpdate, CertifiedLearning, AdmittedControlState, CertificateReceipt, EnvelopeReceipt};
2734/// use bcinr_cmca::fixed::NonNegativeFixed;
2735/// // Missing OutcomeReceipt
2736/// AdaptiveUpdate::admit_adaptive_update(
2737///     AdmittedControlState::admit_control_state(0),
2738///     CertificateReceipt::admit_certificate(0),
2739///     EnvelopeReceipt::admit_envelope(0),
2740///     NonNegativeFixed::ZERO,
2741///     NonNegativeFixed::ONE,
2742///     CertifiedLearning::admit_learning(),
2743/// );
2744/// ```
2745///
2746/// ```compile_fail
2747/// use bcinr_cmca::allocator::{AdaptiveUpdate, CertifiedSelectionOnly, AdmittedControlState, CertificateReceipt, EnvelopeReceipt, OutcomeReceipt};
2748/// use bcinr_cmca::fixed::NonNegativeFixed;
2749/// // mutate from selection-only mode
2750/// AdaptiveUpdate::admit_adaptive_update(
2751///     AdmittedControlState::admit_control_state(0),
2752///     CertificateReceipt::admit_certificate(0),
2753///     EnvelopeReceipt::admit_envelope(0),
2754///     OutcomeReceipt::admit_outcome(0),
2755///     NonNegativeFixed::ZERO,
2756///     NonNegativeFixed::ONE,
2757///     CertifiedSelectionOnly::admit_selection_only(),
2758/// );
2759/// ```
2760pub struct AuthorityCompileFailTests;