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;