//! SP5 per-branch + per-group adaptation ISV slot allocation.
//!
//! All adaptive values live on ISV via Pearls A+D smoothing per
//! `feedback_isv_for_adaptive_bounds`. Slot ranges:
//!
//!   174..226  Per-branch (4 branches × 13 quantities for Pearls 1/2/3 + shared signal)
//!   226..250  Per-group Adam β1/β2/ε (8 groups × 3 hparams)
//!   250..270  Per-branch IQN τ schedule (4 branches × 5 quantiles)
//!   270..274  Per-direction trail distance (4 directions)
//!   274..278  Per-branch num_atoms (4 branches)
//!   278..280  Intentional 2-slot carve-out gap (boundary marker, not allocated)
//!   280..286  Cross-fold-persistent Kelly slots (NOT in fold-reset registry)
//!   286..290  Layer D D1: PnL aggregation outputs (4 slots, fold-reset)
//!   290..294  Layer D D2: Health composition outputs (4 slots, fold-reset)
//!   294..297  Layer D D3: Training metrics EMA outputs (3 slots, fold-reset)
//!   297..313  SP7 T1 loss-budget Wiener stats (16 slots, fold-reset)
//!   313..321  SP7 activation-flag fix per-(head, branch) (8 slots, fold-reset)
//!   321..330  SP8 (Fix 36) train_active_frac canary + LB_MAX_BUDGET (9 slots, fold-reset)
//!   330..339  SP9 (Fix 37) Kelly warmup floor + EMA targets + eval_dist (9 slots, fold-reset)
//!   339..340  SP10 (Fix 38) eval Thompson temperature (1 slot, fold-reset)
//!   340..367  SP11 (Fix 39) reward-subsystem controller (27 slots, fold-reset) — see sp11_isv_slots
//!             [includes B1b fix-up slot 360 = POPART_COMPONENT_MAG_EMA_INDEX
//!              and B1b smoke-recovery 6 var EMAs at 361..367 for z-score normalization]
//!   367..372  Intentional 5-slot boundary gap separating SP11 controller block from SP13
//!   372..380  SP13 (2026-05-04) directional-skill instrumentation (8 slots) — see sp13_isv_slots
//!   380..383  SP13 v3 (2026-05-04) Hold-pricing controller (3 slots) — see sp13_isv_slots
//!
//! Total: 202 new SP5 ISV slots (52 + 24 + 20 + 4 + 4 + 6 + 4 + 4 + 3 + 16 + 8 + 9 + 9 + 1 + 27 + 8 + 3).

// Re-export SP11 + SP13 slot constants so existing consumers can import every
// slot constant from a single location (`crate::cuda_pipeline::sp5_isv_slots::*`).
// SP11 / SP13 own their files but participate in the SP5 linear-span window
// (SP5_SLOT_BASE..SP5_SLOT_END covers all SP5/SP7/SP8/SP9/SP10/SP11/SP13 slots).
pub use crate::cuda_pipeline::sp11_isv_slots::*;
pub use crate::cuda_pipeline::sp13_isv_slots::*;

pub const SP5_SLOT_BASE: usize = 174;

// ── Pearl 1: per-branch atom span (16 slots) ──────────────────────────
/// Adapts the atom distribution center per branch; tracks reward signal magnitude
/// so C51 atoms span the live return distribution rather than a fixed range.
pub const ATOM_V_CENTER_BASE: usize = 174;       // [4]
pub const ATOM_V_HALF_BASE: usize = 178;         // [4]
pub const ATOM_HEADROOM_BASE: usize = 182;       // [4]
pub const ATOM_CLIP_RATE_BASE: usize = 186;      // [4]

// ── Pearl 2: per-branch loss budget (20 slots) ────────────────────────
/// Adapts loss component budget weights per branch; balances C51/IQN/CQL/Ens
/// contributions based on measured gradient magnitudes.
pub const BUDGET_C51_BASE: usize = 190;          // [4]
pub const BUDGET_IQN_BASE: usize = 194;          // [4]
pub const BUDGET_CQL_BASE: usize = 198;          // [4]
pub const BUDGET_ENS_BASE: usize = 202;          // [4]
pub const FLATNESS_BASE: usize = 206;            // [4]

// ── Pearl 3: per-branch NoisyNet σ (12 slots) ─────────────────────────
/// Adapts NoisyNet exploration noise per branch; scales σ to maintain
/// target branch entropy without manual schedule tuning.
pub const NOISY_SIGMA_BASE: usize = 210;         // [4]
pub const SIGMA_FRACTION_BASE: usize = 214;      // [4]
pub const BRANCH_ENTROPY_BASE: usize = 218;      // [4]

// ── shared signal: per-branch Q variance (4 slots) ────────────────────
/// Q-value variance per branch; shared diagnostic consumed by Pearls 1 and 2.
pub const Q_VAR_PER_BRANCH_BASE: usize = 222;    // [4]

// ── Pearl 4: per-group Adam hyperparams (24 slots) ────────────────────
/// Adapts Adam β1/β2/ε per param group using Wiener-optimal α (Pearl D);
/// replaces fixed Adam hparams with signal-driven estimates.
pub const ADAM_BETA1_BASE: usize = 226;          // [8 param groups]
pub const ADAM_BETA2_BASE: usize = 234;          // [8]
pub const ADAM_EPS_BASE: usize = 242;            // [8]

// ── Pearl 5: per-branch IQN τ schedule (20 slots) ─────────────────────
/// Adapts IQN quantile schedule per branch; concentrates τ samples where
/// the distributional loss is highest.
pub const IQN_TAU_BASE: usize = 250;             // [4 branches × 5 quantiles]

// ── Pearl 8: per-direction trail distance (4 slots) ───────────────────
/// Adapts trailing stop distance per direction (Short/Hold/Long/Flat);
/// derived from per-direction return volatility on ISV.
pub const TRAIL_DIST_PER_DIR_BASE: usize = 270;  // [4 dirs: Short, Hold, Long, Flat]

// ── Pearl 1-ext: per-branch num_atoms (4 slots) ───────────────────────
/// Adapts C51 atom count per branch; extends Pearl 1 atom-span adaptation
/// with a dynamic resolution axis.
pub const ATOM_NUM_ATOMS_BASE: usize = 274;      // [4]

// ── Pearl 6: cross-fold-persistent Kelly slots (6 slots) ──────────────
/// Cross-fold-persistent Kelly fraction and trade history signals.
/// NOT in fold-reset registry — these slots persist trade statistics
/// across fold boundaries. Allocated in a separate range 280..286 with
/// an intentional 2-slot gap (278, 279) from the per-fold block to make
/// the carve-out structurally visible in the layout.
pub const KELLY_F_SMOOTH_INDEX: usize = 280;
pub const CONVICTION_SMOOTH_INDEX: usize = 281;
pub const TRADE_VAR_SMOOTH_INDEX: usize = 282;
pub const KELLY_SAMPLE_COUNT_INDEX: usize = 283;
pub const WIN_RATE_SMOOTH_INDEX: usize = 284;
pub const LOSS_RATE_SMOOTH_INDEX: usize = 285;

// ── Layer D Task D1: PnL aggregation outputs (4 slots) ───────────────
/// Layer D D1 (rewrite, 2026-05-02): GPU-aggregated per-epoch PnL summary.
/// Replaces the host-side aggregation in `compute_epoch_financials`
/// (`financials.rs`) per `feedback_no_cpu_compute_strict.md`. Producer is
/// `pnl_aggregation_kernel.cu` — chained through `apply_pearls_ad_kernel`
/// for Pearls A+D smoothing (sentinel-bootstrap on first observation,
/// Wiener-optimal α at steady state). Per-fold reset semantics: PnL is
/// not cross-fold persistent (unlike Pearl 6's Kelly stats), so all four
/// slots get sentinel 0.0 at fold boundary.
///
/// Output semantics (mirror financials.rs verbatim):
///   - PNL_TOTAL: log-space compounded total return over the rollout
///     (`exp(Σ ln(max(1+r, 1e-10))) − 1`, computed from per-bar
///     `step_returns`).
///   - PNL_MEAN: per-trade mean (`sum_returns / n_trades`); the experience
///     collector reduces `sum_returns` per trade exit, not per bar — see
///     `TradeStats` in `gpu_experience_collector.rs`.
///   - PNL_VAR: per-trade population variance
///     (`max(0, sum_sq_returns/n_trades − mean²)`).
///   - PNL_MAX_DD: per-bar equity-curve max drawdown over the last 10K
///     bars with episode-boundary resets at `done_flags > 0.5`, capped
///     at 1.0.
pub const PNL_TOTAL_INDEX: usize = 286;
pub const PNL_MEAN_INDEX: usize = 287;
pub const PNL_VAR_INDEX: usize = 288;
pub const PNL_MAX_DD_INDEX: usize = 289;

// ── Layer D Task D2: Health composition outputs (4 slots) ────────────
/// Layer D D2 (2026-05-02): GPU-composed `LearningHealth` outputs.
/// Replaces the host-side `NormalizedComponents::from_raw` +
/// `NormalizedComponents::compose` chain in
/// `learning_health.rs` (called from `training_loop.rs:2658-2745`) per
/// `feedback_no_cpu_compute_strict.md`. Producer is
/// `health_composition_kernel.cu` — chained through `apply_pearls_ad_kernel`
/// for Pearls A+D smoothing (sentinel-bootstrap on first observation,
/// Wiener-optimal α at steady state). Per-fold reset semantics: the
/// underlying EMAs (q_gap_ema/q_var_ema/grad_norm_ema) reset at fold
/// boundary, so the composition outputs reset together — all four slots
/// get sentinel 0.0 at fold boundary. The four exposed signals are the
/// composed score plus the three normalised inputs the task brief flagged
/// for HEALTH_DIAG observability (q_gap_norm / q_var_norm / grad_norm_norm
/// = `grad_stable`); the other 4 normalised intermediates
/// (atom_util_norm, ens_agree, grad_consistency_norm, spectral_gap_norm)
/// are computed in-register inside the kernel and folded into the score
/// without ISV exposure. Future Layer D extensions can expose them
/// without a launcher-signature change.
pub const HEALTH_SCORE_INDEX: usize = 290;
pub const Q_GAP_NORM_INDEX: usize = 291;
pub const Q_VAR_NORM_INDEX: usize = 292;
pub const GRAD_NORM_NORM_INDEX: usize = 293;

// ── Layer D Task D3: Training metrics EMA outputs (3 slots) ──────────
/// Layer D D3 (2026-05-02): GPU-aggregated training-loop EMA outputs.
/// Replaces the host-side per-epoch EMA arithmetic block at
/// `training_loop.rs:5039-5113` (the post-financials adversarial-regime
/// hysteresis + adaptive-DSR Sharpe block) per
/// `feedback_no_cpu_compute_strict.md`. Producer is
/// `training_metrics_ema_kernel.cu` — chained through
/// `apply_pearls_ad_kernel` for Pearls A+D smoothing on top of the
/// host-formula EMA (sentinel-bootstrap on first observation,
/// Wiener-optimal α at steady state). Per-fold reset semantics: the
/// underlying host scalars (`training_sharpe_ema`,
/// `training_sharpe_ema_initialized`, `max_dd_ema`, `low_dd_ratio`) reset
/// at fold boundary, so all three slots get sentinel 0.0 at fold boundary.
///
/// Slot naming follows the host-side field names in
/// `crates/ml/src/trainers/dqn/trainer/mod.rs:319/603/607` directly. The
/// SP5 plan §D Task D3 names the third metric `gamma_blend` (a host-
/// aggregated eval result) but no such field exists in the codebase; per
/// `feedback_trust_code_not_docs.md` the migration follows the actual
/// host-side EMA which is `low_dd_ratio` (the binary low-drawdown
/// indicator EMA driving the adversarial-regime hysteresis at
/// `training_loop.rs:5054-5070`). All three migrate with their host-side
/// formula constants verbatim:
///
///   training_sharpe_ema   adaptive-α `clamp(0.05 + 0.3 × |err|, 0, 0.5)`,
///                         host-tracked `_initialized` sentinel.
///   max_dd_ema            fixed α=0.1, sentinel `prev < 1e-12 ⇒ replace`.
///   low_dd_ratio          fixed α=0.15 over binary `is_low` input from
///                         `(max_dd < new_max_dd_ema × 0.5) ? 1 : 0`. No
///                         host-side sentinel; constructor zero behaves
///                         correctly under the EMA recurrence.
pub const TRAINING_SHARPE_EMA_INDEX: usize = 294;
pub const MAX_DD_EMA_INDEX: usize = 295;
pub const LOW_DD_RATIO_INDEX: usize = 296;

// ── SP7: Loss-balance controller state-tracker EMAs ──────────────────
//
// Per-branch (4 branches each: dir, mag, ord, urg) Wiener-α state for
// the 2 managed loss heads (CQL and C51). The kernel writes raw
// observations of (candidate − old_budget)² and (h_norm)² to the
// producer scratch buffer; `apply_pearls_ad_kernel` smooths them into
// these slots — same Pearl A sentinel-bootstrap + Pearl D Wiener-EMA
// as every other SP5 producer.
pub const LB_DIFF_VAR_CQL_BASE: usize = 297;     // [4] (candidate_cql − old_cql)² EMA
pub const LB_SAMPLE_VAR_CQL_BASE: usize = 301;   // [4] (cql_norm)² EMA
pub const LB_DIFF_VAR_C51_BASE: usize = 305;     // [4] (candidate_c51 − old_c51)² EMA
pub const LB_SAMPLE_VAR_C51_BASE: usize = 309;   // [4] (c51_norm)² EMA

// ── SP7 activation-flag fix (2026-05-03): per-(head, branch) activation slots ─
//
// 8 new ISV slots — 4 per managed head (CQL / C51), one per branch.
// The controller writes 1.0 to scratch_active when its active path
// computes a real budget update; 0.0 (a no-op via apply_pearls_ad's
// `step_obs == 0.0` short-circuit) when the genuine cold-start branch
// fires. The consumer dispatches on this flag instead of the prior
// numeric-equality bootstrap (CQL_BOOTSTRAP_BUDGET=0.02 was identical
// to the kernel's COLD_START_FLOOR_CQL — making the two states
// indistinguishable). Activation is monotonic per fold; fold-reset
// zeroes the slots so the new fold re-activates from scratch as
// gradients populate.
pub const LB_CQL_ACTIVE_BASE: usize = 313;       // [4] activation flag for CQL controller
pub const LB_C51_ACTIVE_BASE: usize = 317;       // [4] activation flag for C51 controller

// ── SP8 (2026-05-03): adaptive MAX_BUDGET cap (Fix 36) ─────────────────
//
// Per `pearl_controller_anchors_isv_driven.md` + `feedback_isv_for_adaptive_bounds.md`:
// the prior `const float MAX_BUDGET = 1.0f` in
// `loss_balance_controller_kernel.cu` was a regime-encoded constant. The
// canary signal that fires under val-Flat-collapse is `train_active_frac`
// (the train-side counterpart to val_active_frac). This block lifts MAX_BUDGET
// onto ISV with that canary as the driver:
//
//   TRAIN_ACTIVE_FRAC_INDEX  [1] EMA-smoothed (Pearls A+D) train-rollout
//                                Long+Short / total fraction. Producer
//                                `train_active_frac_compute_kernel` reads
//                                `monitoring_summary[5..17)` after the
//                                monitoring reduce; downstream consumer
//                                is the max_budget compute kernel.
//   LB_MAX_BUDGET_CQL_BASE   [4] Per-branch CQL MAX_BUDGET cap.
//   LB_MAX_BUDGET_C51_BASE   [4] Per-branch C51 MAX_BUDGET cap.
//
// All 9 slots get sentinel 0 at fold boundary (Pearl A bootstrap path).
pub const TRAIN_ACTIVE_FRAC_INDEX: usize = 321;     // [1]
pub const LB_MAX_BUDGET_CQL_BASE:  usize = 322;     // [4]
pub const LB_MAX_BUDGET_C51_BASE:  usize = 326;     // [4]

// ── SP9 (2026-05-03): Kelly cold-start warmup floor — fully ISV-driven (Fix 37) ──
//
// Per `pearl_controller_anchors_isv_driven.md` + `pearl_cold_start_exit_signal_or.md`:
// the Kelly cap's prior `warmup_floor = max(conviction, health_floor)` was a
// signal-driven floor for the static cold-start regime, but the cold-start
// release condition itself was implicit in Kelly statistics maturity (single-
// axis statistical via `total_trades >= 10`). Under val-Flat-collapse the cap
// stays closed → no trades → Kelly samples never accumulate → cap stays closed.
// The pearl prescribes OR'd exit signals across statistical / behavioral /
// temporal axes; this block lifts the floor and its release condition fully
// onto ISV.
//
// Co-located eval_dist GPU migration (paired DtoH-elimination win): the
// existing host-side `read_eval_intent_magnitude_distribution()` at
// `gpu_backtest_evaluator.rs:1353` performs a DtoH readback violating
// `feedback_no_cpu_compute_strict.md` and `feedback_no_htod_htoh_only_mapped_pinned.md`.
// SP9 migrates that distribution to GPU-resident ISV storage and deletes the
// host function in the same atomic commit per `feedback_no_partial_refactor.md`.
//
// All 9 slots get sentinel 0 at fold boundary (Pearl A bootstrap path) — the
// cold-start exit re-engages on each new fold so the cap protects new-fold
// learning until OR'd confidence accumulates.
pub const KELLY_WARMUP_FLOOR_INDEX:        usize = 330;  // [1] adaptive floor consumed by kelly_position_cap
pub const Q_VAR_MAG_EMA_INDEX:             usize = 331;  // [1] EMA-tracked baseline for self-relative ratio
pub const INTENT_EVAL_DIVERGENCE_INDEX:    usize = 332;  // [1] training intent_f vs eval eval_f ratio
pub const KELLY_SAMPLE_COUNT_TARGET_INDEX: usize = 333;  // [1] EMA target for statistical-confidence axis
pub const KELLY_DIVERGENCE_TARGET_INDEX:   usize = 334;  // [1] EMA target for behavioral-confidence axis
pub const KELLY_TEMPORAL_TARGET_INDEX:     usize = 335;  // [1] EMA target for temporal-confidence axis
pub const EVAL_DIST_Q_INDEX:               usize = 336;  // [1] eval-side Quarter mag fraction (replaces DtoH)
pub const EVAL_DIST_H_INDEX:               usize = 337;  // [1] eval-side Half mag fraction
pub const EVAL_DIST_F_INDEX:               usize = 338;  // [1] eval-side Full mag fraction

// ── SP10 (Fix 38, 2026-05-03): eval Thompson selector temperature ────
//
// Per `pearl_thompson_for_distributional_action_selection.md` (amended) +
// `pearl_controller_anchors_isv_driven.md` + `pearl_blend_formulas_must_have_permanent_floor.md`:
// the rollout selector is unconditional Thompson sampling at all times
// (training AND eval). Argmax is reserved for the Bellman target Q
// computation only. The Thompson sample is blended with E[Q] via an
// ISV-driven temperature parameter:
//
//   q_eff = E[Q] + temp × (q_sample − E[Q])
//
// where temp ∈ [MIN_TEMP=0.5, MAX_TEMP=2.0] is derived from the SP9
// `intent_eval_divergence` canary:
//
//   temp = clamp(divergence / max(divergence_target, EPS_DIV), 0.5, 2.0)
//
// MIN_TEMP=0.5 enforces the permanent-stochasticity floor per
// `pearl_blend_formulas_must_have_permanent_floor`; the eval selector is
// never fully deterministic. Healthy training (divergence ≈ target ⇒
// temp ≈ 1.0) → pure Thompson; collapsed eval (divergence ≫ target ⇒
// temp = 2.0) → exaggerated exploration; healthy and converging
// (divergence < target ⇒ temp = 0.5) → closer to argmax behavior, but
// never fully deterministic. FoldReset sentinel 0; Pearl A bootstraps
// from the first observation.
pub const EVAL_THOMPSON_TEMP_INDEX:        usize = 339;  // [1] eval Thompson selector temperature

// ── SP11 (Fix 39, 2026-05-04): reward-subsystem controller ────────────
//
// Per `pearl_reward_as_controlled_subsystem.md` + `pearl_controller_anchors_isv_driven.md`:
// the reward subsystem is migrated from a fixed-weight composition (host-side
// constants in `unified_reward_kernel.cu` and friends) to a fully ISV-driven
// controller producing 6 component weights + 4 controller scalars + 10 canaries.
//
// All 20 slot constants live in `crate::cuda_pipeline::sp11_isv_slots`
// (re-exported above). They occupy ISV[340..360) and are FoldReset (sentinel 0;
// Pearl A bootstrap on first observation). SP5_SLOT_END bumps 340 → 360 so the
// linear-span window covers the new block; the wiener_state_buf lockstep is
// handled by the existing bulk memset of `wiener_state_buf` covered by the
// `sp4_wiener_state` registry entry's dispatch arm.

// ── SP13 (2026-05-04): redefine success for predictive skill ──────────
//
// 8 new SP13 ISV slots at ISV[372..380) for the directional-accuracy
// instrumentation + reward-composition controllers. Slots 367..372 are an
// intentional 5-slot gap separating the SP11 controller block (ends at 367)
// from this SP13 block — a structural boundary marker, not allocated. Per
// `pearl_controller_anchors_isv_driven.md` + `feedback_isv_for_adaptive_bounds`:
// every adaptive bound lives on ISV; the static defaults at slots 372 and
// 376..379 are constructor-written Invariant-1 anchors (not stateful EMAs)
// and will be lifted to controllers in Layer C if Phase 0a confirms the
// hypothesis. All 8 slot constants live in `crate::cuda_pipeline::sp13_isv_slots`
// (re-exported above).
//
// SP13 v3 (P0a.T3, 2026-05-04): 3 additional slots at ISV[380..383) for the
// Hold-pricing observation chain — `HOLD_COST_INDEX=380` (constructor-init
// diagnostic anchor at HOLD_COST_BASE=0.005), `HOLD_RATE_TARGET_INDEX=381`
// (static MFT target 0.20), `HOLD_RATE_OBSERVED_EMA_INDEX=382` (per-fold-reset
// Hold-pick rate EMA from `hold_rate_observer_kernel`). Slot 382 feeds the
// SP16-P1 MIN_HOLD_TEMPERATURE producer at slot 460.
pub const SP5_SLOT_END: usize = 383;

/// Wiener-buffer producer-count constant. Sizes `wiener_state_buf` via
/// `(SP4_PRODUCER_COUNT + SP5_PRODUCER_COUNT) * SP4_WIENER_FLOATS_PER_SLOT`.
///
/// **Important**: this is NOT the count of unique allocated SP5 ISV slots
/// (137) — it is the linear span of the SP5 ISV index range
/// `[SP5_SLOT_BASE..SP5_SLOT_END) = [174..313) = 139`, including the
/// intentional 2-slot carve-out gap (278..280) between the per-fold block
/// and the cross-fold-persistent Kelly block. The wiener offset formula
/// `wiener_off = SP4_PRODUCER_COUNT * 3 + (isv_slot - SP5_SLOT_BASE) * 3`
/// indexes linearly by ISV slot, so the buffer must cover the entire
/// linear span — the 2 gap floats and the 6 Pearl 6 floats are
/// **reserved-unused** (Pearl 6 does not call `launch_apply_pearls`,
/// so its 18 wiener floats stay zero-initialised; the gap floats are
/// likewise never accessed).
///
/// Layer D Task D1 (2026-05-02) extended SP5 from 110 unique slots @
/// 174..278 ∪ 280..286 to 114 unique slots @ 174..278 ∪ 280..290; the
/// linear span grew from 112 to 116. Pre-D1 the constant happened to
/// equal both the unique-slot count AND the linear span (110 = 112 −
/// 2 gap, by coincidence at the time the Kelly carve-out landed).
/// Post-D1 the linear span (116) and unique-slot count (114) diverge
/// permanently — `SP5_PRODUCER_COUNT` is now strictly the wiener-buffer
/// linear-span constant.
///
/// Layer D Task D2 (2026-05-02) further extends to 118 unique slots @
/// 174..278 ∪ 280..294; the linear span grows 116 → 120. Updates to
/// docstrings, fingerprint, and the slot-contiguity test are kept in
/// lockstep with the constant per `feedback_no_partial_refactor.md`.
///
/// Layer D Task D3 (2026-05-02) further extends to 121 unique slots @
/// 174..278 ∪ 280..297; the linear span grows 120 → 123. Three new
/// training-metrics EMA outputs (sharpe / max_dd / low_dd_ratio) sit
/// immediately after the D2 health composition block at slots 294..297.
///
/// SP7 Task 1 (2026-05-03): allocates 16 SP7 ISV slots
/// (`LB_DIFF_VAR_{CQL,C51}_BASE` + `LB_SAMPLE_VAR_{CQL,C51}_BASE`, 4 each)
/// at ISV[297..313). Unique-slot count grows 121 → 137; the linear span
/// (and `SP5_PRODUCER_COUNT`) grows 123 → 139. Pearl 6 carve-out at
/// [280..286) remains unchanged.
///
/// SP7 activation-flag fix (2026-05-03): allocates 8 new SP7 ISV slots
/// (`LB_{CQL,C51}_ACTIVE_BASE`, 4 each) at ISV[313..321). Unique-slot
/// count grows 137 → 145; the linear span (and `SP5_PRODUCER_COUNT`)
/// grows 139 → 147. Activation flags are monotonic per fold (FoldReset
/// at boundary), so the new fold re-bootstraps cleanly through Pearl A.
///
/// SP8 (2026-05-03, Fix 36): allocates 9 new SP8 ISV slots —
/// `TRAIN_ACTIVE_FRAC_INDEX` (1) + `LB_MAX_BUDGET_{CQL,C51}_BASE`
/// (4 each) — at ISV[321..330). Unique-slot count grows 145 → 154; the
/// linear span (and `SP5_PRODUCER_COUNT`) grows 147 → 156. Replaces the
/// hardcoded `MAX_BUDGET = 1.0f` regime-encoded constant in
/// `loss_balance_controller_kernel.cu` with an ISV-driven adaptive cap
/// per `pearl_controller_anchors_isv_driven.md`. All 9 slots are
/// FoldReset (sentinel 0; Pearl A bootstrap on first observation).
///
/// SP9 (2026-05-03, Fix 37): allocates 9 new SP9 ISV slots — Kelly
/// cold-start warmup floor + 3 EMA targets + intent/eval divergence + 3
/// eval_dist mag bins — at ISV[330..339). Unique-slot count grows 154 →
/// 163; the linear span (and `SP5_PRODUCER_COUNT`) grows 156 → 165.
/// Replaces the prior implicit cold-start exit (single-axis statistical
/// via `total_trades >= 10` in `kelly_position_cap`) with an OR'd
/// statistical / behavioral / temporal confidence per
/// `pearl_cold_start_exit_signal_or.md`. Co-located eval_dist GPU
/// migration deletes the DtoH `read_eval_intent_magnitude_distribution()`
/// host helper per `feedback_no_cpu_compute_strict.md`. All 9 slots are
/// FoldReset (sentinel 0; Pearl A bootstrap on first observation).
///
/// SP10 (2026-05-03, Fix 38): allocates 1 new SP10 ISV slot —
/// `EVAL_THOMPSON_TEMP_INDEX` — at ISV[339..340). Unique-slot count grows
/// 163 → 164; the linear span (and `SP5_PRODUCER_COUNT`) grows 165 → 166.
/// Drives the eval-side Thompson selector temperature per
/// `pearl_thompson_for_distributional_action_selection.md` (amended) and
/// `pearl_controller_anchors_isv_driven.md`. Produced as a side-output of
/// the existing `intent_eval_divergence_compute_kernel` (single producer,
/// two outputs); FoldReset (sentinel 0; Pearl A bootstrap on first
/// observation).
///
/// SP11 (2026-05-04, Fix 39): allocates 27 new SP11 ISV slots at
/// ISV[340..367) — 6 reward component weights + 4 controller scalar
/// outputs + 11 canaries (val-sharpe Δ/var, 6 mag-ratios, saboteur
/// engagement, PnL EMA, popart-component magnitude EMA) + 6 per-
/// reward-component variance EMAs (B1b smoke-recovery 2026-05-04 for
/// the z-score normalised mag-ratio canary). Unique-slot count grows
/// 164 → 191; the linear span (and `SP5_PRODUCER_COUNT`) grows
/// 166 → 193. The 21st slot — `POPART_COMPONENT_MAG_EMA_INDEX` at 360 —
/// is the B1b fix-up (2026-05-04) for the PopArt-input vs popart-
/// component-magnitude overload (see spec §4 amendment "Why slot 360").
/// Slots 22..27 are the B1b smoke-recovery (2026-05-04, spec §4
/// amendment lines 564-619): per-component variance EMAs at
/// `REWARD_COMPONENT_VAR_EMA_BASE=361..367) feeding the z-score
/// normaliser in `reward_component_mag_ratio_compute_kernel`. Drives
/// the reward subsystem controller per `pearl_reward_as_controlled_subsystem.md`
/// + `pearl_controller_anchors_isv_driven.md`. All 27 slots are
/// FoldReset (sentinel 0; Pearl A bootstrap on first observation).
/// Constants for the SP11 block live in `crate::cuda_pipeline::sp11_isv_slots`
/// and are re-exported above.
pub const SP5_PRODUCER_COUNT: usize = 209;
// linear span = SP5_SLOT_END - SP5_SLOT_BASE = 383 - 174 = 209 wiener triples
// unique-slot count = 202 (52 per-branch + 24 Adam + 20 IQN τ + 4 trail
//                          + 4 num_atoms + 6 Kelly + 4 Layer D D1 PnL aggregation
//                          + 4 Layer D D2 health composition
//                          + 3 Layer D D3 training metrics EMA
//                          + 16 SP7 T1 loss-budget Wiener stats
//                          + 8 SP7 activation-flag fix per-(head,branch) flags
//                          + 1 SP8 train_active_frac canary
//                          + 8 SP8 LB_MAX_BUDGET per-(head,branch)
//                          + 9 SP9 Kelly cold-start warmup floor + targets + eval_dist
//                          + 1 SP10 eval Thompson temperature
//                          + 27 SP11 reward-subsystem controller [includes B1b fix-up
//                            slot 360 + B1b smoke-recovery 6 per-component var EMAs 361..367]
//                          + 8 SP13 directional-skill instrumentation [372..380); slots
//                            367..372 are an intentional 5-slot gap, reserved-unused
//                            in both ISV and the wiener buffer
//                          + 3 SP13 v3 Hold-pricing controller [380..383))

// ── Convenience accessors ────────────────────────────────────────────
#[inline] pub const fn atom_v_center(b: usize) -> usize { ATOM_V_CENTER_BASE + b }
#[inline] pub const fn atom_v_half(b: usize) -> usize   { ATOM_V_HALF_BASE + b }
#[inline] pub const fn atom_headroom(b: usize) -> usize { ATOM_HEADROOM_BASE + b }
#[inline] pub const fn atom_clip_rate(b: usize) -> usize { ATOM_CLIP_RATE_BASE + b }
#[inline] pub const fn budget_c51(b: usize) -> usize    { BUDGET_C51_BASE + b }
#[inline] pub const fn budget_iqn(b: usize) -> usize    { BUDGET_IQN_BASE + b }
#[inline] pub const fn budget_cql(b: usize) -> usize    { BUDGET_CQL_BASE + b }
#[inline] pub const fn budget_ens(b: usize) -> usize    { BUDGET_ENS_BASE + b }
#[inline] pub const fn flatness(b: usize) -> usize       { FLATNESS_BASE + b }
#[inline] pub const fn noisy_sigma(b: usize) -> usize    { NOISY_SIGMA_BASE + b }
#[inline] pub const fn sigma_fraction(b: usize) -> usize { SIGMA_FRACTION_BASE + b }
#[inline] pub const fn branch_entropy(b: usize) -> usize { BRANCH_ENTROPY_BASE + b }
#[inline] pub const fn q_var_per_branch(b: usize) -> usize { Q_VAR_PER_BRANCH_BASE + b }
#[inline] pub const fn adam_beta1(g: usize) -> usize     { ADAM_BETA1_BASE + g }
#[inline] pub const fn adam_beta2(g: usize) -> usize     { ADAM_BETA2_BASE + g }
#[inline] pub const fn adam_eps(g: usize) -> usize        { ADAM_EPS_BASE + g }
#[inline] pub const fn iqn_tau(b: usize, q: usize) -> usize { IQN_TAU_BASE + b * 5 + q }
#[inline] pub const fn trail_dist_per_dir(d: usize) -> usize { TRAIL_DIST_PER_DIR_BASE + d }
#[inline] pub const fn atom_num_atoms(b: usize) -> usize  { ATOM_NUM_ATOMS_BASE + b }
#[inline] pub const fn lb_diff_var_cql(b: usize)   -> usize { LB_DIFF_VAR_CQL_BASE + b }
#[inline] pub const fn lb_sample_var_cql(b: usize) -> usize { LB_SAMPLE_VAR_CQL_BASE + b }
#[inline] pub const fn lb_diff_var_c51(b: usize)   -> usize { LB_DIFF_VAR_C51_BASE + b }
#[inline] pub const fn lb_sample_var_c51(b: usize) -> usize { LB_SAMPLE_VAR_C51_BASE + b }
#[inline] pub const fn lb_cql_active(b: usize)     -> usize { LB_CQL_ACTIVE_BASE + b }
#[inline] pub const fn lb_c51_active(b: usize)     -> usize { LB_C51_ACTIVE_BASE + b }
// SP8 (2026-05-03): per-(head, branch) MAX_BUDGET cap (Fix 36).
#[inline] pub const fn lb_max_budget_cql(b: usize) -> usize { LB_MAX_BUDGET_CQL_BASE + b }
#[inline] pub const fn lb_max_budget_c51(b: usize) -> usize { LB_MAX_BUDGET_C51_BASE + b }

/// Layout fingerprint contribution. Appended to the existing
/// LAYOUT_FINGERPRINT_SEED in gpu_dqn_trainer.rs so existing checkpoints
/// fail-fast on layout change.
pub const SP5_LAYOUT_FINGERPRINT_FRAGMENT: &str =
    "SP5_BASE=174;ATOM_V_CENTER_BASE=174;ATOM_V_HALF_BASE=178;ATOM_HEADROOM_BASE=182;\
     ATOM_CLIP_RATE_BASE=186;BUDGET_C51_BASE=190;BUDGET_IQN_BASE=194;BUDGET_CQL_BASE=198;\
     BUDGET_ENS_BASE=202;FLATNESS_BASE=206;NOISY_SIGMA_BASE=210;SIGMA_FRACTION_BASE=214;\
     BRANCH_ENTROPY_BASE=218;Q_VAR_PER_BRANCH_BASE=222;ADAM_BETA1_BASE=226;ADAM_BETA2_BASE=234;\
     ADAM_EPS_BASE=242;IQN_TAU_BASE=250;TRAIL_DIST_PER_DIR_BASE=270;ATOM_NUM_ATOMS_BASE=274;\
     KELLY_F_SMOOTH=280;CONVICTION_SMOOTH=281;TRADE_VAR_SMOOTH=282;\
     KELLY_SAMPLE_COUNT=283;WIN_RATE_SMOOTH=284;LOSS_RATE_SMOOTH=285;\
     PNL_TOTAL=286;PNL_MEAN=287;PNL_VAR=288;PNL_MAX_DD=289;\
     HEALTH_SCORE=290;Q_GAP_NORM=291;Q_VAR_NORM=292;GRAD_NORM_NORM=293;\
     TRAINING_SHARPE_EMA=294;MAX_DD_EMA=295;LOW_DD_RATIO=296;\
     LB_DIFF_VAR_CQL_BASE=297;LB_SAMPLE_VAR_CQL_BASE=301;\
     LB_DIFF_VAR_C51_BASE=305;LB_SAMPLE_VAR_C51_BASE=309;\
     LB_CQL_ACTIVE_BASE=313;LB_C51_ACTIVE_BASE=317;\
     TRAIN_ACTIVE_FRAC_INDEX=321;\
     LB_MAX_BUDGET_CQL_BASE=322;LB_MAX_BUDGET_C51_BASE=326;\
     KELLY_WARMUP_FLOOR=330;Q_VAR_MAG_EMA=331;\
     INTENT_EVAL_DIVERGENCE=332;KELLY_SAMPLE_COUNT_TARGET=333;\
     KELLY_DIVERGENCE_TARGET=334;KELLY_TEMPORAL_TARGET=335;\
     EVAL_DIST_Q=336;EVAL_DIST_H=337;EVAL_DIST_F=338;\
     EVAL_THOMPSON_TEMP=339;\
     REWARD_POPART_WEIGHT=340;REWARD_CF_WEIGHT=341;REWARD_TRAIL_WEIGHT=342;\
     REWARD_MICRO_WEIGHT=343;REWARD_OPP_COST_WEIGHT=344;REWARD_BONUS_WEIGHT=345;\
     CURIOSITY_PRESSURE=346;SABOTEUR_INTENSITY_MULT=347;\
     REWARD_WEIGHT_FLOOR=348;CURIOSITY_BOUND=349;\
     VAL_SHARPE_DELTA_EMA=350;VAL_SHARPE_VAR_EMA=351;\
     REWARD_COMPONENT_MAG_RATIO_BASE=352;\
     SABOTEUR_ENGAGEMENT_RATE=358;PNL_REWARD_MAGNITUDE_EMA=359;\
     POPART_COMPONENT_MAG_EMA=360;\
     REWARD_COMPONENT_VAR_EMA_BASE=361;\
     TARGET_DIR_ACC=372;AUX_DIR_ACC_SHORT_EMA=373;AUX_DIR_ACC_LONG_EMA=374;\
     AUX_DIR_PREDICTION=375;DIR_SKILL_BONUS_ALPHA=376;DIR_SKILL_BONUS_BETA=377;\
     LUCK_WIN_DISCOUNT=378;SKILL_BONUS_CAP_RATIO=379;\
     HOLD_COST=380;HOLD_RATE_TARGET=381;HOLD_RATE_OBSERVED_EMA=382;\
     ISV_TOTAL_DIM=383";

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    #[test]
    fn slot_layout_no_overlaps_and_total_correct() {
        let mut slots = HashSet::new();

        // Pearl 1: atom span (b in 0..4)
        for b in 0..4 {
            slots.insert(atom_v_center(b));
            slots.insert(atom_v_half(b));
            slots.insert(atom_headroom(b));
            slots.insert(atom_clip_rate(b));
        }
        // Pearl 2: loss budget (b in 0..4)
        for b in 0..4 {
            slots.insert(budget_c51(b));
            slots.insert(budget_iqn(b));
            slots.insert(budget_cql(b));
            slots.insert(budget_ens(b));
            slots.insert(flatness(b));
        }
        // Pearl 3: NoisyNet σ (b in 0..4)
        for b in 0..4 {
            slots.insert(noisy_sigma(b));
            slots.insert(sigma_fraction(b));
            slots.insert(branch_entropy(b));
        }
        // Shared signal: Q variance (b in 0..4)
        for b in 0..4 {
            slots.insert(q_var_per_branch(b));
        }
        // Pearl 4: per-group Adam hparams (g in 0..8)
        for g in 0..8 {
            slots.insert(adam_beta1(g));
            slots.insert(adam_beta2(g));
            slots.insert(adam_eps(g));
        }
        // Pearl 5: IQN τ schedule (b in 0..4, q in 0..5)
        for b in 0..4 {
            for q in 0..5 {
                slots.insert(iqn_tau(b, q));
            }
        }
        // Pearl 8: trail distance (d in 0..4)
        for d in 0..4 {
            slots.insert(trail_dist_per_dir(d));
        }
        // Pearl 1-ext: num_atoms (b in 0..4)
        for b in 0..4 {
            slots.insert(atom_num_atoms(b));
        }
        // Pearl 6: cross-fold-persistent Kelly (6 scalar constants)
        slots.insert(KELLY_F_SMOOTH_INDEX);
        slots.insert(CONVICTION_SMOOTH_INDEX);
        slots.insert(TRADE_VAR_SMOOTH_INDEX);
        slots.insert(KELLY_SAMPLE_COUNT_INDEX);
        slots.insert(WIN_RATE_SMOOTH_INDEX);
        slots.insert(LOSS_RATE_SMOOTH_INDEX);
        // Layer D D1: PnL aggregation outputs (4 scalar constants)
        slots.insert(PNL_TOTAL_INDEX);
        slots.insert(PNL_MEAN_INDEX);
        slots.insert(PNL_VAR_INDEX);
        slots.insert(PNL_MAX_DD_INDEX);
        // Layer D D2: Health composition outputs (4 scalar constants)
        slots.insert(HEALTH_SCORE_INDEX);
        slots.insert(Q_GAP_NORM_INDEX);
        slots.insert(Q_VAR_NORM_INDEX);
        slots.insert(GRAD_NORM_NORM_INDEX);
        // Layer D D3: Training metrics EMA outputs (3 scalar constants)
        slots.insert(TRAINING_SHARPE_EMA_INDEX);
        slots.insert(MAX_DD_EMA_INDEX);
        slots.insert(LOW_DD_RATIO_INDEX);
        // SP7: loss-balance controller state-tracker EMAs (b in 0..4)
        for b in 0..4 {
            slots.insert(lb_diff_var_cql(b));
            slots.insert(lb_sample_var_cql(b));
            slots.insert(lb_diff_var_c51(b));
            slots.insert(lb_sample_var_c51(b));
        }
        // SP7 activation-flag fix: per-(head, branch) flags (b in 0..4)
        for b in 0..4 {
            slots.insert(lb_cql_active(b));
            slots.insert(lb_c51_active(b));
        }
        // SP8 (Fix 36): train_active_frac canary + per-(head, branch) MAX_BUDGET
        slots.insert(TRAIN_ACTIVE_FRAC_INDEX);
        for b in 0..4 {
            slots.insert(lb_max_budget_cql(b));
            slots.insert(lb_max_budget_c51(b));
        }
        // SP9 (Fix 37): Kelly cold-start warmup floor + 3 EMA targets + 3 eval_dist
        slots.insert(KELLY_WARMUP_FLOOR_INDEX);
        slots.insert(Q_VAR_MAG_EMA_INDEX);
        slots.insert(INTENT_EVAL_DIVERGENCE_INDEX);
        slots.insert(KELLY_SAMPLE_COUNT_TARGET_INDEX);
        slots.insert(KELLY_DIVERGENCE_TARGET_INDEX);
        slots.insert(KELLY_TEMPORAL_TARGET_INDEX);
        slots.insert(EVAL_DIST_Q_INDEX);
        slots.insert(EVAL_DIST_H_INDEX);
        slots.insert(EVAL_DIST_F_INDEX);
        // SP10 (Fix 38): eval Thompson selector temperature
        slots.insert(EVAL_THOMPSON_TEMP_INDEX);

        // SP11 (Fix 39): include the 27 new reward-subsystem controller slots.
        for s in SP11_SLOT_BASE..SP11_SLOT_END {
            slots.insert(s);
        }

        // SP13 (2026-05-04): include the 8 directional-skill slots + the
        // 3 v3 Hold-pricing controller slots — `SP13_SLOT_BASE..SP13_SLOT_END`
        // covers both blocks contiguously since v3 extended the upper end
        // from 380 to 383.
        for s in SP13_SLOT_BASE..SP13_SLOT_END {
            slots.insert(s);
        }

        // 1. Exactly 202 unique slots (164 pre-SP11 + 27 SP11 + 8 SP13 + 3 SP13 v3).
        assert_eq!(slots.len(), 202, "expected 202 unique slots, got {}", slots.len());

        // 2. Min slot is SP5_SLOT_BASE = 174.
        assert_eq!(*slots.iter().min().unwrap(), 174);

        // 3. Max slot is SP5_SLOT_END - 1 = 382.
        assert_eq!(*slots.iter().max().unwrap(), 382);

        // 4. Intentional carve-out gap (278, 279) is absent.
        assert!(!slots.contains(&278), "slot 278 must be absent (carve-out gap)");
        assert!(!slots.contains(&279), "slot 279 must be absent (carve-out gap)");

        // 5. SP13 boundary gap (367..372) is absent — separates SP11's controller
        //    block from SP13's directional-skill block.
        for s in 367..372 {
            assert!(!slots.contains(&s),
                "slot {} must be absent (SP11→SP13 boundary gap)", s);
        }

        // 6. Set equals {174..278} ∪ {280..367} ∪ {372..383} — no holes (other
        //    than the carve-out gap 278..280 and the SP11→SP13 boundary gap
        //    367..372), no overlaps. Layer D D1 extended the upper end
        //    286 → 290; D2 extended it 290 → 294; D3 extends it 294 → 297;
        //    SP7 T1 extended it 297 → 313; SP7 activation-flag fix extends
        //    it 313 → 321; SP8 (Fix 36) extends it 321 → 330; SP9 (Fix 37)
        //    extends it 330 → 339; SP10 (Fix 38) extends it 339 → 340;
        //    SP11 (Fix 39) extends it 340 → 360; SP11 B1b fix-up
        //    (2026-05-04) extends it 360 → 361 with POPART_COMPONENT_MAG_EMA_INDEX;
        //    SP11 B1b smoke-recovery (2026-05-04) extends it 361 → 367 with
        //    `REWARD_COMPONENT_VAR_EMA_BASE..+6` for the z-score normalised
        //    mag-ratio canary; SP13 (2026-05-04) appends 372 → 380 with
        //    8 directional-skill slots after the boundary gap 367..372;
        //    SP13 v3 P0a.T3 (2026-05-04) appends 380 → 383 with the 3 Hold-
        //    pricing controller slots (HOLD_COST=380, HOLD_RATE_TARGET=381,
        //    HOLD_RATE_OBSERVED_EMA=382).
        let expected: HashSet<usize> = (174..278).chain(280..367).chain(372..383).collect();
        assert_eq!(slots, expected,
            "slot set does not match expected {{174..278}} ∪ {{280..367}} ∪ {{372..383}}");
    }

    #[test]
    fn cross_fold_persistent_slots_distinct_from_per_fold_block() {
        // Kelly slots 280..286 are SEPARATE from the per-branch/per-group
        // block (174..278) by an intentional 2-slot gap (278, 279).
        assert!(KELLY_F_SMOOTH_INDEX >= 280);
        assert!(LOSS_RATE_SMOOTH_INDEX < 286);
        // Verify no overlap with ATOM_NUM_ATOMS or trail
        for b in 0..4 {
            assert!(atom_num_atoms(b) < KELLY_F_SMOOTH_INDEX);
        }
    }

    #[test]
    fn pnl_aggregation_slots_contiguous_and_above_kelly_block() {
        // Layer D D1: PnL aggregation outputs occupy a contiguous 4-slot
        // block immediately after the cross-fold-persistent Kelly block.
        assert_eq!(PNL_TOTAL_INDEX, 286);
        assert_eq!(PNL_MEAN_INDEX, 287);
        assert_eq!(PNL_VAR_INDEX, 288);
        assert_eq!(PNL_MAX_DD_INDEX, 289);
        // Strictly above the Kelly block (Layer D layout is per-fold; Kelly
        // is cross-fold-persistent — the contracts must remain disjoint).
        assert!(PNL_TOTAL_INDEX > LOSS_RATE_SMOOTH_INDEX);
        // SP5_SLOT_END must reflect the post-SP8 end-of-block.
        assert_eq!(SP5_SLOT_END, 383);
        // SP5_PRODUCER_COUNT is the wiener-buffer linear span (slot-range
        // width including the 2-slot carve-out gap), NOT the unique-slot
        // count. See SP5_PRODUCER_COUNT docstring for the rationale.
        assert_eq!(SP5_PRODUCER_COUNT, 209);
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn health_composition_slots_contiguous_and_above_pnl_block() {
        // Layer D D2: Health composition outputs occupy a contiguous 4-slot
        // block immediately after the Layer D D1 PnL aggregation block.
        assert_eq!(HEALTH_SCORE_INDEX, 290);
        assert_eq!(Q_GAP_NORM_INDEX, 291);
        assert_eq!(Q_VAR_NORM_INDEX, 292);
        assert_eq!(GRAD_NORM_NORM_INDEX, 293);
        // Strictly above the D1 PnL block (Layer D's three producer commits
        // land in disjoint slot ranges so D4's atomic consumer wiring sees
        // each block at fixed offsets).
        assert!(HEALTH_SCORE_INDEX > PNL_MAX_DD_INDEX);
        // 4-slot block is internally contiguous.
        assert_eq!(GRAD_NORM_NORM_INDEX - HEALTH_SCORE_INDEX, 3);
        // SP5_SLOT_END must reflect the post-SP8 end-of-block.
        assert_eq!(SP5_SLOT_END, 383);
        // SP5_PRODUCER_COUNT linear-span check matches the new end.
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn training_metrics_ema_slots_contiguous_and_above_health_block() {
        // Layer D D3: Training metrics EMA outputs occupy a contiguous 3-slot
        // block immediately after the Layer D D2 health composition block.
        assert_eq!(TRAINING_SHARPE_EMA_INDEX, 294);
        assert_eq!(MAX_DD_EMA_INDEX, 295);
        assert_eq!(LOW_DD_RATIO_INDEX, 296);
        // Strictly above the D2 health composition block (Layer D's three
        // producer commits land in disjoint slot ranges so D4's atomic
        // consumer wiring sees each block at fixed offsets per
        // `feedback_no_partial_refactor.md`).
        assert!(TRAINING_SHARPE_EMA_INDEX > GRAD_NORM_NORM_INDEX);
        // 3-slot block is internally contiguous (no gaps).
        assert_eq!(MAX_DD_EMA_INDEX - TRAINING_SHARPE_EMA_INDEX, 1);
        assert_eq!(LOW_DD_RATIO_INDEX - MAX_DD_EMA_INDEX, 1);
        // SP5_SLOT_END must reflect the post-SP8 end-of-block.
        assert_eq!(SP5_SLOT_END, 383);
        // SP5_PRODUCER_COUNT linear-span check matches the new end. The
        // wiener buffer must cover the entire linear span — including the
        // 2-slot carve-out gap (278..280) and the Pearl 6 reserved-but-
        // unused 6-float block (slots 280..286 don't call apply_pearls).
        assert_eq!(SP5_PRODUCER_COUNT, 209);
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn sp7_activation_flag_slots_contiguous_and_above_lb_block() {
        // SP7 activation-flag fix (2026-05-03): 8 new ISV slots —
        // 4 per managed head — at ISV[313..321) immediately after the
        // SP7 T1 LB Wiener-stat block.
        assert_eq!(LB_CQL_ACTIVE_BASE, 313);
        assert_eq!(LB_C51_ACTIVE_BASE, 317);
        // Strictly above the SP7 T1 LB Wiener-stat block (LB_*_VAR_C51_BASE
        // ends at 313).
        assert!(LB_CQL_ACTIVE_BASE > LB_SAMPLE_VAR_C51_BASE);
        // Both 4-slot blocks internally contiguous.
        for b in 0..4 {
            assert_eq!(lb_cql_active(b), LB_CQL_ACTIVE_BASE + b);
            assert_eq!(lb_c51_active(b), LB_C51_ACTIVE_BASE + b);
        }
        // SP5_SLOT_END / SP5_PRODUCER_COUNT cover the new range.
        assert_eq!(SP5_SLOT_END, 383);
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn sp8_max_budget_slots_contiguous_and_above_activation_block() {
        // SP8 (Fix 36, 2026-05-03): 9 new ISV slots — 1 train_active_frac
        // canary + 4 LB_MAX_BUDGET per managed head — at ISV[321..330)
        // immediately after the SP7 activation-flag block at ISV[317..321).
        assert_eq!(TRAIN_ACTIVE_FRAC_INDEX, 321);
        assert_eq!(LB_MAX_BUDGET_CQL_BASE, 322);
        assert_eq!(LB_MAX_BUDGET_C51_BASE, 326);
        // Strictly above the SP7 activation-flag block.
        assert!(TRAIN_ACTIVE_FRAC_INDEX > LB_C51_ACTIVE_BASE);
        // Both 4-slot LB_MAX_BUDGET blocks internally contiguous.
        for b in 0..4 {
            assert_eq!(lb_max_budget_cql(b), LB_MAX_BUDGET_CQL_BASE + b);
            assert_eq!(lb_max_budget_c51(b), LB_MAX_BUDGET_C51_BASE + b);
        }
        // Layout: 1 + 4 + 4 = 9 contiguous slots (321..330).
        assert_eq!(SP5_SLOT_END, 383);
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn all_sp5_slots_fit_within_isv_total_dim() {
        use crate::cuda_pipeline::gpu_dqn_trainer::ISV_TOTAL_DIM;
        assert!(
            SP5_SLOT_END <= ISV_TOTAL_DIM,
            "SP5_SLOT_END={} exceeds ISV_TOTAL_DIM={} — bus too small for SP5/SP7 slots; \
             bump ISV_TOTAL_DIM in gpu_dqn_trainer.rs (and update layout_fingerprint_seed()).",
            SP5_SLOT_END, ISV_TOTAL_DIM,
        );
    }

    #[test]
    fn sp9_slots_contiguous_above_sp8_block() {
        // SP9 (Fix 37, 2026-05-03): 9 new ISV slots at [330..339) — Kelly
        // cold-start warmup floor + 3 EMA targets + intent/eval divergence +
        // 3 eval_dist mag bins — immediately after the SP8 MAX_BUDGET block
        // at [321..330).
        assert_eq!(KELLY_WARMUP_FLOOR_INDEX, 330);
        assert_eq!(Q_VAR_MAG_EMA_INDEX, 331);
        assert_eq!(INTENT_EVAL_DIVERGENCE_INDEX, 332);
        assert_eq!(KELLY_SAMPLE_COUNT_TARGET_INDEX, 333);
        assert_eq!(KELLY_DIVERGENCE_TARGET_INDEX, 334);
        assert_eq!(KELLY_TEMPORAL_TARGET_INDEX, 335);
        assert_eq!(EVAL_DIST_Q_INDEX, 336);
        assert_eq!(EVAL_DIST_H_INDEX, 337);
        assert_eq!(EVAL_DIST_F_INDEX, 338);
        // Strictly above the SP8 MAX_BUDGET block.
        assert!(KELLY_WARMUP_FLOOR_INDEX > LB_MAX_BUDGET_C51_BASE + 3);
        // Block end at ISV[340) post-SP10 (Fix 38).
        assert_eq!(SP5_SLOT_END, 383);
        // Linear span matches producer count (no gaps in SP9 block).
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
    }

    #[test]
    fn sp10_thompson_temp_slot_above_sp9_block() {
        // SP10 (Fix 38, 2026-05-03): 1 new ISV slot at [339..340) — eval
        // Thompson selector temperature — immediately after the SP9 block
        // at [330..339).
        assert_eq!(EVAL_THOMPSON_TEMP_INDEX, 339);
        // Strictly above the SP9 eval_dist block (last entry at 338).
        assert!(EVAL_THOMPSON_TEMP_INDEX > EVAL_DIST_F_INDEX);
        // SP5_SLOT_END reflects the post-SP10 end-of-block.
        assert_eq!(SP5_SLOT_END, 383);
        // Linear span matches producer count (single new slot at top).
        assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE);
        assert_eq!(SP5_PRODUCER_COUNT, 209);
    }
}
