# SP18 Combined-Scope Wire-Up Audit

> Audit captured during Phase 1 Task 1.1 of `docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md`.
>
> **Branch**: `feat/sp18-combined` @ `0b737ec30` (Phase 0 baseline, pre-Phase-1 atomic deletion).
>
> **Audit script**: `scripts/audit_sp18_consumers.sh`. Re-run after every commit that touches the SP13/SP16 chain (D-leg) or the TD(λ) `q_next` site (B-leg). Pre-commit hook (Task 1.2) enforces snapshot-vs-current diff via `--check`.

## Outcome of Phase 1 Task 1.1 (audit-first, halt-on-drift)

The pre-dispatch consumer-audit run surfaced **multiple consumers NOT in the plan's locked checklist** (spec §"Architecture Changes"). Per task input — *"If your audit surfaces ANY additional consumer, halt and report — do NOT proceed with deletion ad-hoc"* — Phase 1 Tasks 1.3-1.5 (atomic deletion) are **HALTED** pending plan / spec amendment.

### Plan's locked D-leg checklist (8 entries)

| # | Site | Action |
|---|------|--------|
| 1 | `experience_kernels.cu:3162-3166, 3637-3643, 3710-3716` (3 cost sites) | DELETE — replace with placeholder comment per Task 1.5 |
| 2 | `training_loop.rs:564` (launcher call) + `:631-667` (hold_cost_scale_diag emit) + `:4171-4226` (host controller block) | DELETE launcher + diag emit + controller; RETAIN slot-382 min_hold_temp_diag emit at `:567-617` |
| 3 | `crates/ml/src/cuda_pipeline/hold_cost_scale_update_kernel.cu` | DELETE entire file |
| 4 | `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` slots [461..468) (`HOLD_COST_SCALE_INDEX` + `HCS_*` Welford) + their `MIN`/`MAX`/`SENTINEL` constants | DELETE constants + the layout-locked tests for them |
| 5 | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` `launch_hold_cost_scale_update` method (line 15837) + `HOLD_COST_SCALE_UPDATE_CUBIN` static (line 2257) + struct field `hold_cost_scale_update_ops` (line 7853) + constructor wiring (lines 24433-24434, 25296) | DELETE all |
| 6 | `crates/ml/src/trainers/dqn/state_reset_registry.rs` 7 entries (`sp16_phase2_hold_cost_scale_adaptive` + `sp16_t3_hcs_*` × 6) | DELETE entries |
| 7 | `crates/ml/build.rs:881` cubin manifest entry `hold_cost_scale_update_kernel.cu` | DELETE |
| 8 | `crates/ml/src/cuda_pipeline/state_layout.cuh:214` (`ISV_HOLD_COST_SCALE_IDX`) + `:228-233` (`ISV_HCS_*` mirror) | DELETE |

### Additional consumers surfaced by audit (NOT in plan checklist)

The following consumers must be added to the atomic-deletion commit. They were missed by the plan's spec §"Architecture Changes" and would have left the deletion partial — per `feedback_no_partial_refactor.md` this would block compile or, worse, leave dangling references that compile but break test/integration paths.

| # | Site | Type | Resolution |
|---|------|------|------------|
| **A1** | `crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs:1240-1323` — `HoldCostScaleUpdateOps` struct + `impl` (kernel-load + launcher) | **PRODUCER WRAPPER** (Rust-side). The plan said the launcher is in `gpu_dqn_trainer.rs` but actually only the trainer's *thin call site* `launch_hold_cost_scale_update` is there; the real `HoldCostScaleUpdateOps::new`/`launch` impl lives in `gpu_aux_trunk.rs`. | DELETE the struct + impl block (84 lines). DELETE the `HOLD_COST_SCALE_UPDATE_CUBIN` import at `gpu_aux_trunk.rs:56` (transitively unused after the struct is gone). |
| **A2** | `crates/ml/tests/sp14_oracle_tests.rs:2174-2920` — 11 GPU oracle tests + the doc-block at the head | **TESTS** (75 occurrences of HOLD_COST_SCALE / HCS_TARGET / hold_cost_scale_update across this file). Tests: `sp16_phase2_hold_cost_scale_climbs_with_overrun`, `sp16_phase2_hold_cost_scale_at_target_is_one`, `sp16_phase2_hold_cost_scale_under_target_is_one`, `sp16_phase2_hold_cost_scale_bounds_clamp`, `sp16_phase2_hold_cost_scale_pearl_a_bootstrap`, `sp16_phase3_alpha_high_during_signal_jump`, `sp16_phase3_alpha_low_in_steady_state`, `sp16_phase3_pearl_a_bootstrap_first_obs`, `sp16_phase3_alpha_naturally_bounded`, `sp16_phase3_no_hardcoded_alpha`, plus the helper `recover_alpha`/`compute_target_scale`/`run_synthetic_sequence` infrastructure (~750 lines). All exercise the now-deleted `hold_cost_scale_update` kernel via `include_bytes!(.../hold_cost_scale_update_kernel.cubin)` — without the kernel they fail to compile. | DELETE the 2 test modules (`sp16_phase2_hold_cost_scale_gpu` + `sp16_t3_hold_cost_scale_alpha_gpu`) and the standalone `sp16_phase3_no_hardcoded_alpha` test. ~750 lines from `sp14_oracle_tests.rs:2174-2920`. |
| **A3** | `crates/ml/src/trainers/dqn/trainer/training_loop.rs:8971-9043` — 7 dispatch arms in `reset_named_state` (`"sp16_phase2_hold_cost_scale_adaptive"` + `"sp16_t3_hcs_*"` × 6) | **DISPATCH ARMS**. Required atomically with the registry-entry deletion (Task 1.4 expanded scope) — the contract test `every_fold_and_soft_reset_entry_has_dispatch_arm` (state_reset_registry.rs:2120-2280) fails if the registry has an entry but no dispatch arm OR vice versa. | DELETE the 7 match arms (slot 461 + HCS slots 462-467) atomically with the registry entries. |
| **A4** | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs:23200-23216` — constructor block writing `HOLD_COST_BASE` to `HOLD_COST_INDEX=380` + `HOLD_RATE_TARGET_DEFAULT` to slot 381 | **CONSTRUCTOR WRITE** of slot 380 (controller output anchor). Per DD7c, slot 380 is RETAINED as part of the observation chain — but it now has no producer. The constructor-write of `HOLD_COST_BASE` keeps it at 0.005 cold-start. After deletion of the host controller, slot 380 becomes a **read-only diagnostic slot** (constructor-init only, no per-step rewrites). | KEEP the constructor-write (DD7c retains slot 380). UPDATE the comment block at lines 23201-23210 to reflect the post-SP18-Phase-1 reality: "After SP18 Phase 1, slot 380 is no longer rewritten by a host-side controller (deleted per DD7=c). The constructor-init is now its only assignment — it serves as a read-only diagnostic anchor surfacing the static base cost. Phase 2 lands the structural Hold opportunity-cost reward at the 3 deleted cost-subtraction sites; that reward does NOT consume slot 380." |
| **A5** | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs:617, 2245-2256` — doc-block prose referring to deleted machinery | **DOC PROSE**. References `launch_hold_cost_scale_update`, `gpu_aux_trunk::HoldCostScaleUpdateOps`, `isv[461]`, etc. | UPDATE comments to remove deleted-machinery references; replace with "(deleted SP18 Phase 1, DD7=c — see docs/sp18-wireup-audit.md)" pointer. No code change. |
| **A6** | `crates/ml/src/cuda_pipeline/sp13_isv_slots.rs:73-77` — `HOLD_COST_BASE`, `HOLD_COST_CONTROLLER_GAIN`, `HOLD_COST_FLOOR_RATIO`, `HOLD_COST_CEIL_RATIO` constants | **HOST-CONTROLLER CONSTANTS**. Used ONLY by the host controller block at `training_loop.rs:4213-4223` we're deleting. Per the SP13 v3 P0a comment block at `sp13_isv_slots.rs:50-72`, `HOLD_COST_BASE` is also used by the constructor-write at `gpu_dqn_trainer.rs:23215`. | KEEP `HOLD_COST_BASE` (consumed by constructor-init at A4 — slot 380 retained). DELETE `HOLD_COST_CONTROLLER_GAIN`, `HOLD_COST_FLOOR_RATIO`, `HOLD_COST_CEIL_RATIO` (sole consumer is the deleted controller block). |
| **A7** | `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs:5579-5585` — comment doc-ref to deleted controller | **COMMENT DOC-REF**. References `training_loop.rs:3604` (stale line — now `:4209`) and the priced-cost flow. | UPDATE prose to reflect post-Phase-1 reality. |
| **A8** | `crates/ml/src/cuda_pipeline/sp5_isv_slots.rs:325-327` — comment doc-ref describing the controller chain | **COMMENT DOC-REF**. Documents slot 380/381/382. Slots 381 + 382 retained per DD7c; slot 380 retained but no longer controller-written. | UPDATE prose to reflect post-Phase-1 reality. |
| **A9** | `crates/ml/src/cuda_pipeline/state_reset_registry.rs:1138-1271` — registry entries already marked RETIRED in PP.4 (commit c66e15f6d) for slots 461 + 462-467 | **REGISTRY ENTRIES**. Already marked RETIRED in Pre-Phase PP.4. Phase 1 promotes RETIRED-with-sentinel-0.0-write to FULL DELETION. | DELETE the 7 entries (slot 461 + HCS slots 462-467) from the registry; layout fingerprint must be updated atomically — the fingerprint seed string covers slot allocations [461..468) which become RESERVED-gap (SP14-C.1 pattern). Slots themselves stay allocated (ISV_TOTAL_DIM unchanged) to preserve checkpoint compatibility. |
| **A10** | `crates/ml/src/trainers/dqn/state_reset_registry.rs:2256-2308` — `lock_sp18_v2_pp4_retired_chain` test which asserts the 7 retired entries STILL EXIST | **CONTRACT TEST**. PP.4 added a lock test asserting the entries exist with sentinel 0.0. After full deletion, this test must be deleted (the entries no longer exist, so asserting their existence panics). | DELETE the `lock_sp18_v2_pp4_retired_chain` test (or rewrite to assert the slots are NOT registered). |

### Out-of-scope mentions (do NOT delete; verify only)

These show up in the audit grep but per DD7c are RETAINED for HEALTH_DIAG continuity OR are independent modules:

| Site | Reason RETAINED |
|------|----------------|
| `crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu` | Producer of slot 382 (observation chain). DD7c retains entire observation chain. |
| `crates/ml/src/cuda_pipeline/min_hold_temperature_update_kernel.cu` | Independent SP16-P1 producer of slot 460 (different chain). Reads same slots 381+382 but writes different output. RETAINED. |
| `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` MHT_* slots [468..474) | SP16-P1 (MIN_HOLD_TEMPERATURE) Welford accumulators — distinct chain. RETAINED. |
| `crates/ml/src/cuda_pipeline/sp13_isv_slots.rs:43` `HOLD_COST_INDEX = 380` | Slot constant for the retained observation chain. RETAINED. |
| `crates/ml/src/cuda_pipeline/state_layout.cuh:200-202` `ISV_HOLD_COST_IDX/ISV_HOLD_RATE_TARGET_IDX/ISV_HOLD_RATE_OBSERVED_EMA_IDX` (slots 380/381/382) | Mirror constants for retained observation chain. RETAINED. |
| `crates/ml/src/trainers/dqn/state_reset_registry.rs:939` `sp13_hold_rate_observed_ema` registry entry for slot 382 | FoldReset entry for retained observation chain. RETAINED. |
| `crates/ml/tests/sp13_phase0_oracle_tests.rs` references to `hold_rate_observer_kernel` | Tests for retained observer kernel. RETAINED. |
| `docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md` and related plan docs | Historical spec/plan archaeology. NOT modified. |

## Path forward

Two options:

1. **EXPAND deletion scope to cover all 10 additional consumers (A1-A10) atomically in the same Phase 1 commit.** The plan's spec §"Architecture Changes" was incomplete; this audit makes the scope explicit. The atomic commit grows from ~8 sites to ~18 sites but stays within the same conceptual change ("delete SP13/SP16 cost-application chain"). Per `feedback_no_partial_refactor`.

2. **Halt and request human review.** The plan author may have intended these consumers to be discovered iteratively (the plan says "trust the script, not the plan author"). But the script's purpose is to *surface* missed consumers BEFORE the deletion, exactly so the deletion stays atomic. Halting honours that intent; expanding the scope ad-hoc honours the deletion's atomicity.

**Recommended**: Option 1, with this audit doc serving as the spec-amendment record. The 10 additional consumers all flow from the same chain — none introduce new architectural concerns. They just expand the byte-count of the atomic-deletion commit.

## Status

**Phase 1 Tasks 1.3-1.6 LANDED (2026-05-09)** — atomic deletion of all 18 sites
(8 plan + 10 audit-expanded A1-A10) committed in a single atomic commit per
`feedback_no_partial_refactor`. The branch was placed in INTERIM STATE: 3 reward
sites (`experience_kernels.cu` segment_complete + per-bar positioned-Hold +
per-bar flat-Hold) were placeholder-commented but contained no Hold cost.

**Phase 2 Tasks 2.1-2.5 LANDED (2026-05-09)** — atomic introduction of the
`compute_sp18_hold_opportunity_cost` device function + 3-site migration +
CPU/GPU oracle tests in a single commit per `feedback_no_partial_refactor`.
**INTERIM STATE LIFTED.** L40S dispatch remains FORBIDDEN pending Phase 3 cap
producer + Phase 4-5 B-leg, but the reward function is now structurally
complete (no placeholder sites, no orphaned consumers).

Per DD7c, observation chain preserved: slot 380 (HOLD_COST_INDEX, constructor-
init-only diagnostic at HOLD_COST_BASE=0.005), slot 381
(HOLD_RATE_TARGET_INDEX, constructor-init at 0.20), slot 382
(HOLD_RATE_OBSERVED_EMA_INDEX, FoldReset chain still wired). MHT_* slots
[468..474) preserved (MIN_HOLD_TEMPERATURE chain RETAINED).

Sites deleted (atomic commit):
- 3 reward subtraction sites in experience_kernels.cu (placeholder comments)
- hold_cost_scale_update_kernel.cu (file deleted)
- HoldCostScaleUpdateOps in gpu_aux_trunk.rs (struct + impl, ~120 lines)
- launch_hold_cost_scale_update + cubin static + struct field + constructor
  + field-init in gpu_dqn_trainer.rs (5 sites)
- HOLD_COST_SCALE_INDEX, HCS_* slots, sentinel/bounds, layout-locked tests in
  sp14_isv_slots.rs
- HOLD_COST_CONTROLLER_GAIN/FLOOR/CEIL constants in sp13_isv_slots.rs
- ISV_HOLD_COST_SCALE_IDX + ISV_HCS_* mirror constants in state_layout.cuh
- build.rs cubin manifest entry
- 7 fold-reset registry entries (slot 461 + HCS_* 462..468)
- 7 dispatch arms in training_loop.rs
- Host controller block in training_loop.rs (lines ~4171-4226)
- launcher call site + hold_cost_scale_diag emit block in training_loop.rs
- 11 GPU oracle tests + helpers in sp14_oracle_tests.rs (~750 lines)
- lock_sp18_v2_pp4_retired_chain test (deleted, no inverse-contract
  replacement — pointless ceremony per user call)
- Layout fingerprint seed updated (HOLD_COST_SCALE + HCS_* removed; range
  [461..468) is RESERVED gap; ISV_TOTAL_DIM stays at 507 per checkpoint
  compatibility — RESERVED-gap pattern from SP14-C.1)

**Validation: cargo check --workspace clean; cargo test -p ml --lib passes
(state_reset_registry::every_fold_and_soft_reset_entry_has_dispatch_arm,
sp16_t3_wiener_welford_slot_layout_locked, sp18_combined_slot_layout_locked
all OK).**

### Phase 2 sites introduced (atomic commit)

- `crates/ml/src/cuda_pipeline/trade_physics.cuh` — new device function
  `compute_sp18_hold_opportunity_cost` after `compute_lump_sum_opp_cost`.
  Single forward-only function; reuses `compute_asymmetric_capped_pnl` for
  the bilateral cap. No new infrastructure beyond existing primitives.
- `crates/ml/src/cuda_pipeline/experience_kernels.cu` — 3 placeholder
  comments replaced atomically:
  - Site 1 (segment_complete branch ~3143): unconditional add into
    `base_reward` BEFORE the SP12 asymmetric cap; preserves the SP12 cap
    interaction and cap participation per the original SP13 P0a comment
    contract.
  - Site 2 (per-bar positioned-Hold ~3641): unconditional add into
    `r_micro` (rc[3]) — same component the deleted subtraction wrote to
    pre-SP18, preserving SP11 reward-subsystem controller signal
    attribution. `vol_proxy` recomputed locally from
    `features[bar_idx * market_dim + 9]` (the per-bar branches are
    outside the segment_complete `vol_proxy` scope).
  - Site 3 (per-bar flat-Hold ~3760): unconditional add into `r_opp_cost`
    (rc[4]) — same component the deleted subtraction wrote to pre-SP18.
    `vol_proxy` recomputed locally as in Site 2.
- `crates/ml/src/cuda_pipeline/sp18_hold_opp_test_kernel.cu` — new GPU
  oracle test wrapper (single-thread single-block). Wraps the device fn
  for the GPU oracle test; mapped-pinned f32 input/output per
  `feedback_no_htod_htoh_only_mapped_pinned`.
- `crates/ml/build.rs` — added `sp18_hold_opp_test_kernel.cu` to the
  cubin manifest.
- `crates/ml/tests/sp18_hold_reward_oracle_tests.rs` — added T1 (CPU
  oracle, 6 cases covering positive/negative/saturation/dir-gate/
  shaping_scale=0) + T2 (GPU oracle parity with CPU at ε=1e-5) per spec
  §"Behavioral Test Predictions".

### Phase 2 design notes

- **`vol_proxy` scoping**: the per-bar Hold sites (Sites 2-3) are outside
  the segment_complete `vol_proxy` scope (declared at
  `experience_kernels.cu:3124`). Each per-bar site recomputes
  `vol_proxy` locally from the same `features[bar_idx * market_dim + 9]`
  ATR-norm input the segment_complete branch and the B.2/D.4c bonus
  blocks consume. The `vol_proxy_pb1`/`vol_proxy_pb2` locals mirror the
  segment_complete computation modulo the `× sqrt(hold_time)` term
  (which is 1 for per-bar / hold_time=1 — matches the device fn's
  contract).
- **ISV cap fallback**: each call site reads `ISV[483]` (POS_CAP) and
  `ISV[484]` (NEG_CAP) with the same sentinel-or-out-of-bounds guard
  used at Site 1: if the slot value is within ε of the sentinel OR
  outside `[POS_CAP_MIN_BOUND, POS_CAP_MAX_BOUND]`, the call falls
  through to the macro defaults (+5/-10). This is bit-identical to a
  fixed +5/-10 cap during cold-start and Pre-Phase-3 (when the producer
  kernel `hold_reward_cap_update_kernel` has not yet landed).
- **Reward-component slot routing**: Sites 1/2/3 write to slots 0/3/4
  (popart/micro/opp_cost) per the SP15 Wave 2 reward-component layout.
  Routing matches the deleted SP13/SP16 subtraction destinations
  exactly, so the SP11 reward-subsystem controller's component-weight
  estimator sees the same input distribution shape (modulo the
  structural-counterfactual vs reactive-cost magnitude shift, which is
  precisely the change the controller is meant to learn).
- **`shaping_scale` gating**: the device fn multiplies the capped output
  by `shaping_scale`. Validation runs at `shaping_scale=0` → the device
  fn returns 0.0 → no Hold opp cost in pure backtest, matching the SP12
  lump-sum `compute_lump_sum_opp_cost` contract.

### Phase 3 sites introduced (atomic commit, 2026-05-09)

D-leg adaptive HOLD_REWARD_POS/NEG_CAP producer kernel + launcher +
per-epoch wiring + GPU oracle tests. Lifts the Phase 2 caps from
"sentinel-driven cold-start" (5.0/-10.0 fixed) to "p99(|step_ret|) over
Long/Short trade closes × 1.5 safety factor" with Wiener-optimal α blend
per `pearl_wiener_optimal_adaptive_alpha`. The Phase 2 consumer
(`compute_sp18_hold_opportunity_cost`) sees the producer-driven slots
[483]/[484] from epoch 1 onward; the sentinel branch in the consumer
remains as the cold-start fallback path (zero-trade-close epoch ⇒
producer early-returns ⇒ slots stay at sentinel ⇒ consumer falls
through to the +5/-10 macro defaults — bit-identical to pre-Phase-3
behavior).

Sites introduced (single atomic commit per `feedback_no_partial_refactor`):

- **Kernel**: `crates/ml/src/cuda_pipeline/hold_reward_cap_update_kernel.cu`
  (NEW, ~310 lines). Single-block 256-thread producer; mirrors the SP14
  P0-A `reward_cap_update_kernel` structural template with three
  differences:
    1. Filter: `(is_close && step_ret != 0)` — both winners AND losers
       (the consumer needs the magnitude scale of *all* Long/Short
       closes, not just winners). Position-side cap filters
       `(is_close && step_ret > 0)`.
    2. Welford-derived Wiener-α (slots [487..493)) replaces fixed α=0.01.
       Floor at WELFORD_ALPHA_MIN=0.4 per
       `pearl_wiener_alpha_floor_for_nonstationary` — the policy-realised
       Long/Short close magnitude distribution is intrinsically
       non-stationary as the policy adapts, sharpening tails.
    3. Bounds [HOLD_REWARD_POS_CAP_MIN_BOUND=0.5,
       HOLD_REWARD_POS_CAP_MAX_BOUND=50.0] (vs. position-side [1.0, 50.0]).

- **Cubin manifest**: `crates/ml/build.rs` — added kernel entry. Build-rs
  cubin static `HOLD_REWARD_CAP_UPDATE_CUBIN` exposed in
  `gpu_dqn_trainer.rs:2190`.

- **Producer wrapper**: `crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs` —
  new `HoldRewardCapUpdateOps` struct (mirrors `RewardCapUpdateOps`)
  with `new`/`launch` impl; pre-loads `CudaFunction` at construction per
  `pearl_no_host_branches_in_captured_graph.md`.

- **Trainer wiring**: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`:
    - Struct field `hold_reward_cap_update_ops` at line 7828.
    - Constructor instantiation at line 24409.
    - Field-init in struct literal at line 25293.
    - Method `launch_hold_reward_cap_update` placed right after
      `launch_reward_cap_update`.

- **Per-epoch wire-up**:
  `crates/ml/src/trainers/dqn/trainer/training_loop.rs` line 511 —
  `fused.trainer().launch_hold_reward_cap_update(step_ret_ptr,
  trade_close_ptr, total_samples)` placed right after
  `launch_reward_cap_update` so both cap producers share the same
  source buffers (read-only; independent ISV slot pairs at
  [452]/[453] and [483]/[484]).

- **HEALTH_DIAG**: `training_loop.rs` (post `td_error_pre` emit) — new
  line `HEALTH_DIAG[N]: hold_reward_cap [pos={:.4} neg={:.4}
  fire_rate={:.4}]`. Reads ISV[483]/[484]/[486] via
  `read_isv_signal_at`; slot 486 (HOLD_OPP_COST_FIRE_RATE_EMA) is the
  engagement-canary diag for Phase 8 to populate.

- **GPU oracle tests**:
  `crates/ml/tests/sp18_hold_reward_oracle_tests.rs` — three new tests
  in the `mod gpu` block:
    - `hold_reward_cap_producer_drives_slots` — T5 from spec: 100
      simulated Long/Short closes with jittered ~N(0, 0.01); verifies
      ISV[483] is bilaterally clamped to MIN_BOUND=0.5 (small p99 ⇒
      target_cap << bound), ISV[484] = -2 × ISV[483], filler outliers
      (close=0) filtered out.
    - `hold_reward_cap_pearl_a_first_observation_replace` — Pearl-A
      bootstrap: 4 large closes with `step_ret = [+10, -20, +30, -40]`,
      target_cap = max(p99, max) × 1.5 ≈ 76.5, clamped to MAX_BOUND=50,
      Pearl-A REPLACE on sentinel writes 50.0 directly (NOT blended
      with sentinel 5.0).
    - `hold_reward_cap_no_closes_preserves_isv` — no closes
      (trade_close=0 ∀i): every ISV slot (POS, NEG, all 6 Welford)
      preserved bit-exactly. Verifies the cold-start fallback path.

- **Audit script**: `scripts/audit_sp18_consumers.sh --check` exits 0
  (no fingerprint drift in tracked sections — SP18 D-leg/B-leg legacy
  references unchanged; the new Phase 3 producer is in its own
  symbol-namespace not tracked by the audit's snapshot, which is by
  design — the audit tracks the DELETED SP13/SP16 legacy chain).

**Validation: cargo check --workspace clean (1m 12s); 3 GPU oracle
tests pass on local RTX 3050 Ti (1.93s wallclock).**

### Phase 3 design notes

- **Why Wiener-α (vs. fixed α=0.01 on the position-side cap)**: the
  position-side cap drifts slowly because the underlying alpha
  distribution is approximately stationary over training. The Hold-side
  cap distribution is intrinsically NON-stationary as the policy adapts
  (sizing tightens, trail shifts). Welford-derived α tracks that
  non-stationarity; the 0.4 floor prevents the canonical "α decays to 0
  once policy converges" trap (SP16 T3 regression at
  train-multi-seed-b5gmp sha 641aa0dfd Fold 1: α=0.248 at HD4 → Hold%
  52.6% at HD5 — see `pearl_wiener_alpha_floor_for_nonstationary`).

- **Why no separate Long/Short flag buffer**: `step_ret_per_sample[i]`
  is non-zero ONLY on close events from positions that had non-zero
  pre_sign — i.e. Long or Short positions. Hold/Flat positions don't
  generate non-zero step_ret per `experience_kernels.cu:2855`.
  Filtering by `(is_close && step_ret != 0)` is the cheapest correct
  extraction of the Long/Short close distribution; no extra
  dir_idx_per_sample buffer needed.

- **Cold-start fallback behavior**: when `total_count == 0` (no closes
  this epoch — Hold-heavy phase), the kernel early-returns at the
  guard in Phase 2 of its body. ALL slots (POS, NEG, all 6 Welford)
  remain unchanged. The Phase 2 consumer
  (`compute_sp18_hold_opportunity_cost`) sees the cold-start sentinels
  (5.0/-10.0) and routes through its own `is_sentinel` branch to the
  macro defaults +5/-10 — bit-identical to pre-Phase-3.

- **Coexistence with `reward_cap_update_kernel`**: both producers
  read the same `step_ret_per_sample` + `trade_close_per_sample`
  buffers (read-only; no shared state). They drive independent ISV
  slot pairs and use different filters. Per-epoch boundary launches
  are sequential (position-side first, Hold-side second) so the
  two_kernel sequence is graph-capturable (single stream, no host
  branches). The redundant block-tree-reduce on the same input
  buffers is a micro-cost (≤ 2 × 64K samples × 4 floats per epoch =
  cheap relative to per-step training cost).

## B-leg verification (Phase 1.1 sub-objective)

Per the plan's locked B-leg checklist:

- **`td_lambda_kernel` launch sites**: 1 site at `gpu_experience_collector.rs:4154` (per the plan). Verified.
- **`q_next` self-bootstrap**: line `~4143` confirmed (`let q_next = rewards`). Phase 5 will replace.
- **PER priority recomputation**: 5 sites in `crates/ml-dqn/src/gpu_replay_buffer.rs`, `crates/ml-dqn/src/replay_buffer_type.rs`, `crates/ml/src/trainers/dqn/config.rs`, `crates/ml/src/trainers/dqn/fused_training.rs`, `crates/ml/tests/gpu_per_integration_test.rs`. NONE store `q_next` — confirms B-DD4 (no PER migration needed).
- **C51 loss target-Q origin**: `c51_loss_kernel.cu` `block_bellman_project_f` already uses target-net Q at loss time — no migration needed.
- **`target_params_buf`**: 17 references in `gpu_dqn_trainer.rs`, Polyak-tracked, ready for B-leg consumption.
- **PopArt slot 63**: catalogued; B-DD11 reset is the only PopArt change in scope.

B-leg pre-conditions verified ✓ — no additional B-leg consumers surfaced (Phase 4-5 atomic-refactor scope is unchanged).

## Captured audit fingerprint (locked snapshot — pre-commit hook `--check` diffs against this)

The fingerprint below is a per-section, per-file hit count summary. It is short
enough to inline in markdown, sortable for human review, and stable enough to
diff cleanly. Catches both new-consumer drift (a file appears in a section
where it didn't before) and expanded-consumer drift (a file's hit count
changes). It does NOT catch line-by-line content drift within a single file —
that's checked by the per-section grep output diffed in PR review.

To regenerate:
```
scripts/audit_sp18_consumers.sh --fingerprint
# update this section between BEGIN/END markers, then:
git add docs/sp18-wireup-audit.md
# the pre-commit hook check_sp18_consumer_audit fails the next commit if
# scripts/audit_sp18_consumers.sh --check reports drift vs this snapshot.
```

After Phase 1's atomic deletion lands, this fingerprint will shrink dramatically
(slot 461 + HCS_* + hold_cost_scale_update sections drop to zero hits). That
re-capture is committed as part of Task 1.6 (close-out).

<!-- BEGIN audit_sp18_consumers.sh FINGERPRINT -->
```
=== D-leg: Slot 380 (HOLD_COST_INDEX) consumers ===
  total_hits=39
  crates/ml/build.rs 1
  crates/ml/src/cuda_pipeline/state_layout.cuh 1
  crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu 1
  crates/ml/src/cuda_pipeline/sp5_isv_slots.rs 1
  crates/ml/src/cuda_pipeline/sp13_isv_slots.rs 1
  crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs 2
  crates/ml/src/trainers/dqn/trainer/training_loop.rs 3
  crates/ml/src/trainers/dqn/state_reset_registry.rs 1
  docs/sp18-wireup-audit.md 5
  docs/dqn-wire-up-audit.md 10
  docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md 4
  docs/superpowers/plans/2026-05-04-sp13-redefine-success-for-predictive-skill.md 5
  docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 4
=== D-leg: Slot 461 (HOLD_COST_SCALE_INDEX) consumers ===
  total_hits=28
  docs/sp18-wireup-audit.md 6
  docs/dqn-wire-up-audit.md 9
  docs/superpowers/plans/2026-05-08-sp16-hold-collapse-structural-fix.md 10
  docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 3
=== D-leg: Slots [462..468) (HCS_* Welford) consumers ===
  total_hits=9
  docs/dqn-wire-up-audit.md 8
  docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 1
=== D-leg: hold_cost_scale_update_kernel references ===
  total_hits=58
  docs/sp18-wireup-audit.md 11
  docs/dqn-wire-up-audit.md 22
  docs/superpowers/specs/2026-05-08-sp18-reward-shape-hold-attractor-design.md 4
  docs/superpowers/plans/2026-05-08-sp16-hold-collapse-structural-fix.md 9
  docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 12
=== D-leg: hold_rate_observer_kernel (RETAINED — diag chain stays per DD7=c) ===
  total_hits=160
  crates/ml/tests/sp13_phase0_oracle_tests.rs 19
  crates/ml/tests/sp14_oracle_tests.rs 20
  crates/ml/build.rs 5
  crates/ml/src/cuda_pipeline/min_hold_temperature_update_kernel.cu 6
  crates/ml/src/cuda_pipeline/state_layout.cuh 1
  crates/ml/src/cuda_pipeline/gpu_experience_collector.rs 12
  crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu 4
  crates/ml/src/cuda_pipeline/sp5_isv_slots.rs 3
  crates/ml/src/cuda_pipeline/sp13_isv_slots.rs 2
  crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs 8
  crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs 5
  crates/ml/src/cuda_pipeline/sp14_isv_slots.rs 5
  crates/ml/src/trainers/dqn/trainer/training_loop.rs 13
  crates/ml/src/trainers/dqn/state_reset_registry.rs 3
  docs/sp18-wireup-audit.md 7
  docs/isv-slots.md 1
  docs/dqn-wire-up-audit.md 11
  docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md 6
  docs/superpowers/specs/2026-05-08-sp18-reward-shape-hold-attractor-design.md 1
  docs/superpowers/plans/2026-05-05-sp14-aux-q-wire-earned-gradient-flow.md 5
  docs/superpowers/plans/2026-05-04-sp13-redefine-success-for-predictive-skill.md 20
  docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 3
=== D-leg: Per-bar Hold-cost subtraction sites (3 lines we change in Phase 1.5) ===
  total_hits=
=== D-leg: state_layout.cuh mirror constants (HOLD_COST + HCS) ===
  total_hits=1
  crates/ml/src/cuda_pipeline/state_layout.cuh 1
=== D-leg: state_reset_registry slot 461 entries ===
  total_hits=
=== D-leg: Cubin manifest references in build.rs ===
  total_hits=2
  crates/ml/build.rs 2
=== B-leg: td_lambda_kernel launch sites ===
  total_hits=20
  crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs 8
  crates/ml/src/cuda_pipeline/gpu_experience_collector.rs 11
  crates/ml/src/cuda_pipeline/nstep_kernel.cu 1
=== B-leg: q_next argument origin (the line ~4143 site) ===
  total_hits=
=== B-leg: rewards_out consumers (downstream of TD(λ) output) ===
  total_hits=
=== B-leg: PER priority computation sites (verify B-DD4: NO migration needed) ===
  total_hits=18
  crates/ml-dqn/src/gpu_replay_buffer.rs 7
  crates/ml-dqn/src/replay_buffer_type.rs 3
  crates/ml/tests/gpu_per_integration_test.rs 3
  crates/ml/src/trainers/dqn/config.rs 4
  crates/ml/src/trainers/dqn/fused_training.rs 1
=== B-leg: Replay buffer schema — q_next storage check (verify NO migration) ===
  total_hits=
=== B-leg: c51_loss_kernel target-Q origin (verify already correct at loss time) ===
  total_hits=
=== B-leg: target_params_buf consumers (verify Polyak-tracked, available at collection time) ===
  total_hits=
=== B-leg: PopArt slot 63 references (verify B-DD11 reset is the only PopArt change) ===
  total_hits=20
  crates/ml/tests/sp18_hold_reward_oracle_tests.rs 17
  crates/ml/tests/sp11_producer_unit_tests.rs 3
=== END ===
  total_hits=
```
<!-- END audit_sp18_consumers.sh FINGERPRINT -->

## Phase 4 close-out — B-leg target-net forward additive infrastructure (2026-05-09)

Phase 4 lands the **additive dead-code** state for the B-leg target-Q
DDQN bootstrap that Phase 5 will swap into `td_lambda_kernel`'s `q_next`
argument at `gpu_experience_collector.rs:~4313` (post-Task-4.1 hoist).

**Scope landed in this commit:**

1. **Task 4.1** — `next_states` build-site relocated. The
   `build_next_states_f32` invocation now precedes the TD(λ) launch
   block (no behavioral change — `next_states` had no observer between
   its pre-Phase-4 build site and the bottom-of-fn batch assembly).
   Pure ordering hoist; verified via the audit grep
   `next_states\b` flagging 0 post-TD(λ) consumers.
2. **Task 4.2** — `compute_q_next_target_bootstrap` skeleton method
   added on `GpuExperienceCollector` with full plan-aligned signature:
   ```rust
   pub fn compute_q_next_target_bootstrap(
       &mut self,
       next_states: &CudaSlice<f32>,
       target_params_ptr: u64,
       online_params_ptr: u64,
       batch_size: usize,
   ) -> Result<CudaSlice<f32>, MLError>
   ```
   Body is a hard-error early-return per `feedback_no_stubs` —
   no production caller exists in Phase 4 (the caller is wired in
   Phase 5 Task 5.1, replacing the self-bootstrap clone at line ~4313).
   Tasks 4.3-4.5 fill in the body atomically before Phase 5 wires
   the production call site. Returning Err instead of `todo!()`
   satisfies Invariant 9 (no deferred-work markers); any accidental
   pre-Phase-5 call site hard-errors with a clear pointer to the
   open work.

**Phase 4 introspection tests** (no GPU required, all pass):

| Test | File | Purpose |
|---|---|---|
| `compute_q_next_target_bootstrap_method_exists` | `crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs` | Asserts method signature exists with documented parameters |
| `next_states_built_before_td_lambda` | same | Asserts Task 4.1 ordering invariant via offset-comparison |
| `td_lambda_still_consumes_self_bootstrap_q_next_in_phase4` | same | Atomic-refactor guard: Phase 4 STILL uses `q_next = rewards_out` clone — Phase 5 Task 5.1 swaps |

**Atomic-refactor invariant (HARD — `feedback_no_partial_refactor`):**

Phase 4 leaves `compute_q_next_target_bootstrap` as **dead code** (no
production caller, only introspection tests). Phase 5.1 makes it live
in lockstep with the `q_next` argument swap at the TD(λ) launch:

> **NO L40S DISPATCH between the Phase 4 close-out commit and the
> Phase 5 Task 5.1 commit** that replaces
> `let q_next = dtod_clone_f32_native(&self.stream, &self.rewards_out, ...)`
> with `let q_next = self.compute_q_next_target_bootstrap(&next_states, ...)`.

**Tasks 4.3-4.5 remaining (deferred to follow-up subagent dispatch):**

- **Task 4.3** — implement online-net forward on `next_states` for
  DDQN per-branch argmax (mirror of `submit_forward_ops_ddqn` infra:
  VSN+BN+trunk+branch-head chain via `cublas_forward.forward_online_raw`,
  output: `[B, 4]` i32 per-branch argmax actions).
- **Task 4.4** — implement target-net forward via
  `cublas_forward.forward_target_raw`, then `compute_expected_q` with
  target's distributional outputs to produce per-action `E[Q]`; gather
  per-branch argmax `Q_target[b, a*_online_branch]` and resolve final
  scalar form (B-DD8.a: dir-branch only, OR sum across 4 branches).
  GPU oracle tests T8 + T11 (`#[ignore = "requires GPU"]`).
- **Task 4.5** — replace `todo!()` body; close-out grep audit asserts
  `todo!` absent in `compute_q_next_target_bootstrap`; Phase 4 ends
  in additive-dead-code state with full body wired.

These tasks each require substantial new infrastructure on the
collector (target-net trunk forward chain duplicating
`gpu_dqn_trainer.rs` Pass 2's VSN+BN+OFI+trunk+branch-head sequence)
and are decomposed in the plan §"Phase 4" as 3 separate TDD cycles
with GPU oracle test gates between each. Atomic-refactor rule binds
the entire chain Task 4.5 → Task 5.1: NO L40S DISPATCH between.

**Sub-task verification status:**

```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml
# → finished `dev` profile [unoptimized + debuginfo] target(s); clean

SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
  cargo test -p ml --test sp18_td_lambda_q_next_oracle_tests
# → 3 passed; 0 failed; 0 ignored
```

**Audit script post-Phase-4 fingerprint diff (vs Phase 1 close-out):**

- `td_lambda_kernel launch sites` count rose from 9 → 19 due to
  the new `sp18_td_lambda_q_next_oracle_tests.rs` introspection test
  file (7 hits via test docstrings + assertions + the build-site
  ordering check); the source `gpu_experience_collector.rs` count
  rose by 3 (additive comments referencing `td_lambda_kernel` per the
  Phase-4 wiring narrative). Fingerprint regenerated above.
- `compute_q_next_target_bootstrap` introduced — 1 producer site
  (the new method in `gpu_experience_collector.rs`) + 1 introspection
  test referencing it. No production caller (Phase 4 = additive dead
  code by design).

