# Track 2 — Reward V7 Audit Triage

**Phase:** Phase 1 — Track 2 (Reward V7 audit)
**Captured against commit:** `5c70c68a1`
**Hardware:** RTX 3050 Ti Laptop GPU (4 GB) — **preliminary smoke-scale verdicts**; L40S validation pending per plan §Task 2.1
**Data source:** `magnitude_distribution.rs` smoke run (3 walk-forward folds × 20 epochs = 60 HEALTH_DIAG rows, `dqn-smoketest` profile) — the same baseline capture used by Track 1.
**Log ref:** `/tmp/foxhunt_smoke/magnitude_distribution.log` (local; not committed).

---

## Executive summary

Preliminary triage against spec §5.2 reward terms R1–R8. At smoke scale:

- **3 KEEP** — R1 (step_return, base reward), R3 (CF-flip, 66% contribution is the dominant signal), R8 (raw_returns_out, required by Sharpe / MaxDD / risk-metric pipeline)
- **1 KEEP-WITH-CAVEAT** — R4 (trailing-stop, 25 / 60 epochs above the 1 % floor; firing-rate diagnostic only, not a reward additive — re-validate at L40S where Full positions actually enter)
- **1 DELETED / RELOCATED** — R6 (loss-aversion asymmetric clamp: 6 / 60 epochs above 1 %, mean 0.26 % — sub-noise). **Status:** reward-layer clamp DELETED from `experience_kernels.cu`; negative-tail compression RELOCATED to `c51_loss_kernel.cu::block_bellman_project_f` (Huber-style `if t_z < 0 { t_z = -10 * (1 - exp(t_z / 10)); }` before the `v_min`/`v_max` clamp). Upper +10 cap preserved inline at former call sites as `fminf(reward, 10.0f)`. Same invariant, structurally-correct location per reward-inventory's "wrong-level regularization" pattern. Phase 2 Task 2.4 landed.
- **1 FIX-documented-disable** — R5 (micro-reward: `micro_reward_scale=0` in smoke, contribution 0 / 60 epochs — smoke-scale-only disable; production toml has scale=0.1 so the code path is already load-bearing in prod. Per `feedback_no_functionality_removal.md` the DELETE verdict was rejected; Phase 2 Task 2.3 preserved the code path and documented the disable.)
- **1 PENDING** — R2 (PopArt drift: 0 / 60 epochs nonzero; warmup-gated by design — requires production-scale run before a final verdict)
- **1 ALREADY-REMOVED** — R7 (segment-patience multiplier: never wired in the kernel; slot deleted per Task 0.8 stub-remediation commit `83d524f86`)

**Most load-bearing finding:** R3 (counterfactual-flip) is doing the vast majority of the shaping work at smoke scale — its magnitude ratio drifts from 0.49 to 0.74 across the 60-row window while every other shaping term is ≤ 3 % of the total reward magnitude. The reward landscape is effectively `step_return × cf_flip_transform + noise` at this scale. Two nominal terms (R5, R7) produce zero kernel contribution; one (R6) barely surfaces.

---

## Per-term verdicts

### R1 — `step_return` (base reward)

**Signal:** realized per-bar reward written to `out_rewards[out_off]` at `experience_kernels.cu:2045`. This is the denominator for every additive ratio in the `reward_contrib` tuple — by construction its own "contribution" is 1.0 minus the other ratios.

**Wiring site:** `experience_kernels.cu:1717` initializes `float reward = 0.0f`; the segment-complete branch at `:1774` assigns `reward = asymmetric_soft_clamp(2.0f * vol_normalized_return)`; the dense-OFI branch at `:1890` overwrites it with `shaping_scale * micro_reward_scale * tanhf(...)` when `micro_reward_scale > 0`; the fallback flat-holding branch at `:1905` sets `-shaping_scale * holding_cost_rate * |position|`. Final value is written at `:2045` to `out_rewards` and captured at `:2052` to `total_reward_per_sample` (the denominator for the audit).

**Better-form coverage:** none — this IS the P&L-aligned core. Phase 1 reward-inventory spec §"Core P&L-aligned terms" explicitly lists it as a keeper.

**Observed contribution:** implicit (denominator). Empirically, `Σ|total|` over 60 reducer windows is nonzero (`cf_rate = 0.660` mean; nonzero fractions are only possible when `sum_total > 1e-12`), so the step_return signal is live.

**Decision-matrix verdict:** **KEEP** — meaningful, not covered elsewhere, is the base reward.

**Evidence strength:** STRONG.

---

### R2 — PopArt normalization drift

**Signal:** `|current_var − prev_var| / |prev_var|` — proxy for PopArt normalizer update per epoch. Slot 0 of `reward_contrib`.

**Wiring site:** `training_loop.rs:2527-2551`. Reads `fused.read_popart_variance()` at each HEALTH_DIAG emit; writes `reward_contrib[0]` only when `cur_var > 0.0 && prev_reward_contrib_popart_var > 0.0`. Otherwise stays at the 0.0 returned by the collector's `reward_contrib_fractions()` (line 1910). PopArt itself is applied inside the C51 Bellman / cuBLAS training path (not the reward kernel).

**Better-form coverage:** none — this is a first-class transform on the reward target distribution, not an additive reward term. The `reward_contrib[0]` slot is a diagnostic of how aggressively the normalizer is adapting, not a reward magnitude.

**Observed contribution:** `0.000` in all 60 / 60 HEALTH_DIAG rows across the three folds. Per `training_loop.rs:2527-2533` comment block, this is the honest signal in three real cases:
1. First epoch of each fold — no prior reading yet (trainer is reset at fold boundary per `trainer.rs:1049-1051`).
2. `cur_var` hasn't warmed past 0 yet (smoke fold is 20 epochs, PopArt typically needs more to produce a stable `var`).
3. PopArt disabled — not the case here (`popart_robust` is active per `training_loop.rs:494`).

Given case 1 explains at least three of the first-epoch rows (one per fold) and case 2 the remainder, this is a WARMUP-GATED zero at smoke scale — not a regression.

**Decision-matrix verdict:** **PENDING** — sub-1 % in 60 / 60 epochs satisfies the DELETE rule on face value, but the zero is semantically "PopArt hasn't warmed up at this scale", not "PopArt is dead code". Deleting the slot would throw away a legitimate production-scale diagnostic.

**To resolve:** re-run at L40S scale (plan §Task 2.1). If `reward_contrib[0]` still < 1 % across ≥ 40 / 50 production epochs once PopArt has fully warmed, revisit.

**Evidence strength:** MEDIUM (semantic zero is plausible; need production-scale verification to promote to DELETE or KEEP).

---

### R3 — Counterfactual direction flip (CF-flip)

**Signal:** firing rate of directional-mirror counterfactuals over reached slots. Slot 1 of `reward_contrib`.

**Wiring site:**
- Kernel decision: `experience_kernels.cu:1253-1261` samples `philox_uniform < exploration_scale * cf_ratio` → `do_flip`, mirrors direction features when true.
- Diagnostic write: `experience_kernels.cu:2098` writes `cf_flip_per_sample[cf_off] = do_flip ? 1 : 0` at `cf_off = out_off + N*L`.
- Reducer: `gpu_experience_collector.rs:1826-1844` computes `cf_rate = Σ cf_flip / Σ slot_live`.

**Better-form coverage:** none. The CF-flip is a symmetry-augmentation (direction invariance) — no other term replicates "alternative-direction gradient on mirrored state."

**Observed contribution:** 60 / 60 epochs above 1 %. Range 0.486 – 0.738, mean 0.660.

```
fold 1 (ep 0-19):  0.486 → 0.666 → 0.660    (drift upward as policy converges)
fold 2 (ep 0-19):  0.692 → 0.722 → 0.715
fold 3 (ep 0-19):  0.738 → 0.619 → 0.586    (slight retreat in fold 3)
```

At `cf_ratio = 0.67` (adaptive, per D4/N4 logic in the collector), a firing rate ≈ 0.66 matches expectation. The drift reflects `exploration_scale × cf_ratio` settling.

**Decision-matrix verdict:** **KEEP** — dominates the reward landscape by an order of magnitude, not covered elsewhere.

**Caveat for the smoke-test assertion:** `reward_component_audit.rs:89-96` enforces `cf_flip > 0.1` as a floor to catch regressions. The observed 0.486 – 0.738 range is 5–7 × above that floor — healthy headroom.

**Evidence strength:** STRONG.

---

### R4 — Trailing stop (regime-adaptive)

**Signal:** firing rate of trailing-stop exits over executed trade bars. Slot 2 of `reward_contrib` — `trail_r = Σ (trail_triggered & traded) / Σ traded`.

**Wiring site:**
- Kernel: `experience_kernels.cu:1630-1632` writes `trail_triggered_per_sample[out_off] = trail_triggered` and `traded_per_sample[out_off] = (entering|exiting|reversing) ? 1 : 0`.
- Reducer: `gpu_experience_collector.rs:1846-1865` computes `trail_r = trail_fire_sum / traded_sum`. **Peek-only** — does not reset the buffers (Task 0.5's per-magnitude reducer runs next and resets).
- Consumes `compute_trailing_stop_4b` from `trade_physics.cuh` (shared with backtest).

**Better-form coverage:** none. The trail is implemented inside the physics layer (`trade_physics.cuh`), not as a reward additive — which is already the "better form" per reward-inventory §"Gems". This slot is the *diagnostic* of physics-layer firing, not a double-counted reward.

**Observed contribution:**
- 25 / 60 epochs with `trail_r ≥ 0.01` (1 % floor) — **41.7 %**.
- Range 0.000 – 0.356, mean 0.114.
- Zero in 35 / 60 epochs. Three patterns:
  1. Fold 1 (epochs 0–19): 1 / 20 above floor (epoch 17 only, `0.300`)
  2. Fold 2 (epochs 0–19): 5 / 20 above floor (mostly late-epoch)
  3. Fold 3 (epochs 0–19): 19 / 20 above floor (consistently 0.13 – 0.35)

The progression mirrors Track 1 H6 (`fire_f = 0` everywhere because Full positions rarely enter) — trail fires only on Quarter positions that hold long enough to trail. At fold 3 (largest training window, most trades) firing is steady-state; at fold 1 it's trade-count-starved.

**Decision-matrix verdict:** **KEEP** (with smoke-scale caveat).

The term IS wired as a physics constraint (correct better-form home per reward-inventory). The *diagnostic slot* contribution oscillates 0 – 36 % based on trade volume and hold distribution — enough to clear the 1 % floor on the fold with real data, but not uniformly. At L40S production scale (50 epochs, 176 k bars, genuine Full positions if H4 is fixed) the firing rate should stabilize.

The smoke-test assertion at `reward_component_audit.rs:105` enforces `trail_r > 0.01` as an epoch-mean floor; the 0.114 mean we observe is 11 × above it — the wiring is not at risk.

**Caveat:** `trail_r` is a firing-rate, NOT an additive magnitude ratio. Applying the 1 %-of-reward-magnitude threshold from the decision matrix to a firing-rate isn't a clean comparison. Interpret "`trail_r < 1 %`" as "trail almost never fires" rather than "trail is a negligible slice of reward."

**Evidence strength:** MEDIUM at smoke scale — fold-3 shows it's alive; fold-1 shows it's trade-volume-gated. Re-measure at production scale.

---

### R5 — Micro-reward (dense per-bar directional signal)

**Signal:** `shaping_scale × micro_reward_scale × tanh((flow_momentum + price_confirm + book_aggression + hold_quality − spread_penalty) / temp)`. Slot 3 of `reward_contrib` — magnitude ratio `|Σ micro_pre| / |Σ total_post|`.

**Wiring site:** `experience_kernels.cu:1818-1902`. Branch-guarded by `!segment_complete && |position| > 0.001 && micro_reward_scale > 0.0 && full_batch_states != NULL && full_state_dim >= SL_OFI_START + SL_OFI_DIM`. If any guard fails the dense-OFI branch is skipped and `micro_reward_per_sample[out_off]` stays at the entry-default 0.0f (written at `:1226`).

**Better-form coverage:** per Phase 1 reward-inventory §"Updated disposition table", `micro_reward_scale` is classified DIAGNOSE → DELETE. The underlying intent (dense signal on sparse rewards) is already covered by the OFI block in the 42-dim state vector — the network can learn to use it. Reward-inventory §"Open questions" flags it behind a TD-propagation diagnostic (Phase 2 Task #8).

**Observed contribution:** 0.000 in all 60 / 60 epochs. The `dqn-smoketest` profile sets `micro_reward_scale = 0.0` → the dense-OFI branch at `:1818` is skipped by the guard → the per-sample slot stays at its zero default.

This is a **SEMANTIC zero** (intentional profile config, not a wiring regression), the same class as R2's warmup zero. Distinction from Phase 2's deletion plan: Phase 2 intends to DELETE the term outright (branch + config field + kernel variables) once the TD-propagation diagnostic lands.

**Decision-matrix verdict:** **FIX-documented-disable** (originally DELETE — rejected per `feedback_no_functionality_removal.md`).

The original DELETE verdict relied on "contribution 0 / 60 epochs AND feature-layer redundancy AND reward-inventory tagged for removal". Re-examination during Phase 2 Task 2.3 found:

1. **The contribution is 0 at smoke ONLY** because `dqn-smoketest.toml` sets `micro_reward_scale = 0.0` for test-isolation reasons (td_propagation / magnitude_distribution / reward_component_audit all want the sparse-reward TD path isolated). `dqn-production.toml` sets `micro_reward_scale = 0.1` — R5 is load-bearing in production, not dead code.
2. **Feature-layer "redundancy" was a category error.** State-vector OFI features at state[SL_OFI_START..SL_OFI_START+SL_OFI_DIM) = [42..62) are representation inputs for the policy/value encoder. R5 is a per-bar reward gradient on the critic. Different mechanism; production deploys both together. R5 also reads `PREV_MID` (retrospective hold quality) which is NOT in the state vector — that signal exists only in the kernel branch.
3. **Reward-inventory disposition** predates the no-functionality-removal rule; the inventory's DELETE tag is overridden.

Phase 2 Task 2.3 implementation (commit follows this triage amendment):
- Preserves the kernel branch at `experience_kernels.cu:1915..2003`.
- Preserves the `micro_reward_per_sample` buffer + `reward_contrib_fractions` reducer slot.
- Preserves the `micro_reward_scale` field in `DQNHyperparameters`.
- Adds a kernel-site comment documenting (a) the parameter-not-flag status, (b) the smoke-vs-production scale values, (c) why the state-vector OFI block is complementary not redundant, (d) the `feedback_no_functionality_removal.md` seal.
- Adds a config-site comment at the `Default` impl.
- Extends the `reward_contrib_fractions` docstring to mark `micro=0.000` as a SEMANTIC value when the profile's scale is 0 (not a wiring regression).

**Evidence strength:** STRONG on "code path is load-bearing in production" (config evidence). STRONG on "state-vector OFI is representation not reward" (mechanism distinction).

---

### R6 — Loss-aversion (asymmetric soft-clamp)

**Signal:** `base_reward − asymmetric_soft_clamp(base_reward)`. The clamp compresses negatives (`−10 × (1 − exp(x/10))` for `x < 0`) and linearly caps positives at +10. Slot 4 of `reward_contrib` — magnitude ratio `|Σ Δclamp| / |Σ total|`.

**Wiring site:**
- Clamp function: `experience_kernels.cu:78-81` (`asymmetric_soft_clamp`).
- Applied: `:1774-1775` — `float base_reward = 2.0f * vol_normalized_return; reward = asymmetric_soft_clamp(base_reward);` — **only inside the `segment_complete` branch** (trade exit).
- Diagnostic write: `:1783` — `loss_aversion_per_sample[out_off] = base_reward - reward` (signed).
- Reducer: `gpu_experience_collector.rs:1878-1890` takes `|·|` before summing.

**Better-form coverage:** reward-inventory §"Better-form taxonomy" categorizes this as LOSS-AVERSION-STYLE compression. The clamp pre-transforms the reward to prevent catastrophic-loss gradient dominance — an alternative is label smoothing on Q-targets in the C51 loss kernel (reward-inventory §"Updated disposition table" lists `reward_noise_scale` as MOVE → C51 Bellman smoothing; the same logic applies here).

**Observed contribution:**
- 6 / 60 epochs above the 1 % floor — **10.0 %**.
- 16 / 60 epochs with any clamp firing (`> 0.001`).
- Range 0.000 – 0.031, mean 0.0026.

Distribution: clamp is silent in the first 33 rows (fold 1 and most of fold 2), begins sporadically in fold 2 late, and fires more consistently in fold 3 with 12 / 20 fold-3 rows showing nonzero clamp.

The pattern: only segment-complete bars with `base_reward < 0` AND `|base_reward| > ~0` trigger meaningful clamp delta. Fold 1 has low trade-completion count; fold 3 has enough to surface the term.

**Decision-matrix verdict:** **DELETED / RELOCATED** (Phase 2 Task 2.4 landed) — contribution < 1 % in 54 / 60 epochs (majority) AND the invariant (compress catastrophic negative tail) is better expressed at the gradient level (Q-target smoothing / Huber loss) per the reward-inventory pattern for "wrong-level regularization."

With mean 0.26 % and max 3.1 %, even when R6 fires it rarely exceeds 3 % of reward magnitude. The asymmetric compression is doing little work that couldn't be done by Huber loss / Q-target noise in the downstream training kernel.

**Landed relocation:**
- Reward-layer `asymmetric_soft_clamp` function deleted from `experience_kernels.cu:78-81`.
- Both call sites (trade-exit `:~1774` and hindsight-relabel `:~3047`) replaced with inline `fminf(reward, 10.0f)` preserving the +10 upper cap for numerical safety.
- Negative-tail compression relocated to `c51_loss_kernel.cu::block_bellman_project_f` — Huber-style `if (t_z < 0.0f) { t_z = -10.0f * (1.0f - expf(t_z / 10.0f)); }` applied to the projected Bellman target before the existing `v_min`/`v_max` clamp. Same invariant, structurally-correct location (Q-target smoothing is where this protection belongs).
- `loss_aversion_per_sample` buffer removed end-to-end: kernel arg, alloc, struct field, dtoh, memset. Tuple `reward_contrib_fractions` shrank 5→4 (slots now `[popart, cf_flip, trail_r, micro]`). HEALTH_DIAG `la={:.3}` field dropped. Smoke `reward_component_audit` updated.

**Caveat (historical, now resolved):** the old `asymmetric_soft_clamp` call also capped positives at +10 (upper-bound protection against reward explosions from rare large wins). The relocation preserves that cap inline at each former call site — it was never the load-bearing negative-tail compression, only the upper-bound safety cap.

**Evidence strength:** MEDIUM — sub-noise contribution at smoke scale; relocation preserves the protection mechanism at the correct layer.

---

### R7 — Segment return patience multiplier

**Signal per spec:** `segment_return × √(hold_time / expected_hold)` — reward-size scaling for trades held their expected duration.

**Wiring site:** **NOT WIRED.** Grep for `patience_mult` / `patience_multiplier` / `segment_patience` across `crates/`:
- Kernel: one match at `experience_kernels.cu:1049`, a stale docstring describing a defunct v8 reward shape. The implementation at `:1774-1775` does NOT apply any patience multiplier.
- Host: three matches — two in trainer comments (`trainer/mod.rs:326`, `:1513`) and one in the training loop (`training_loop.rs:2635`), all explaining why the slot was removed.
- `sqrt(hold_time)` does appear at `:1768` — but as the DIVISOR of `vol_norm` (vol-normalization uses sqrt-hold-time by statistical variance scaling), which reduces the reward for longer holds rather than amplifying it. Not a patience bonus.

**Better-form coverage:** N/A — never wired.

**Observed contribution:** N/A — `reward_contrib_fractions` returns a 5-element array by design; the sixth slot was deleted per Task 0.8 stub-remediation commit `83d524f86` (referenced in `training_loop.rs:2635` and `mod.rs:1513` comments).

**Decision-matrix verdict:** **ALREADY-REMOVED** — Task 0.8 correctly deleted the stub. No further action.

**Follow-up:** the stale docstring at `experience_kernels.cu:1049` ("patience_mult") should be cleaned up in a Phase 2 cleanup commit to avoid future confusion. Ref: Foxhunt rule "trust code, not comments" — a stale comment referencing a removed term is a latent trap. Flag for Phase 2 documentation hygiene task.

**Evidence strength:** STRONG — the term is provably absent from the kernel.

---

### R8 — `raw_returns_out` vs shaped returns

**Signal:** per-bar portfolio return `(equity_t − equity_{t-1}) / equity_{t-1}` — pure P&L, no shaping. Diagnostic/debug output, not a reward additive.

**Wiring site:**
- Kernel write: `experience_kernels.cu:2058-2064`. Guarded by `raw_returns_out != NULL`. NaN/Inf clamped to 0.0.
- Host consumer: `gpu_experience_collector.rs:2003-2013` dtoh's the buffer at epoch boundary → `step_returns: Vec<f64>` feeds Sharpe, Sortino, MaxDD, VaR computation downstream. Also consumed by `compute_reward_quantiles` and PopArt's `extract_median_iqr` via `reward_sort_keys_buf`.

**Better-form coverage:** N/A — this IS the better form. Reward-inventory §"Core P&L-aligned terms" marks "Core trade return" as a keeper explicitly.

**Observed contribution:** not captured in `reward_contrib` (R8 is not in the reducer's 5-tuple). Downstream HEALTH_DIAG rows show nonzero risk metrics (VaR, CVaR, Sharpe_raw) across all 60 epochs → `raw_returns_out` is populated and consumed every epoch.

From the log at epoch 1 fold 1:
```
Risk Metrics - VaR(95%)=-0.0001%, CVaR(95%)=-0.0005%, VaR(99%)=-0.0006%, CVaR(99%)=-0.0012%
```

VaR / CVaR are non-degenerate → `raw_returns_out` buffer is alive.

**Decision-matrix verdict:** **KEEP** — load-bearing consumer (risk metrics, PopArt median/IQR, Sharpe / MaxDD in epoch summary). Without it the entire risk-diagnostic pipeline regresses.

**Distinction from `out_rewards`:** `out_rewards` is the shaped reward the network trains on; `raw_returns_out` is the pure P&L the trader would experience. Keeping both separate is deliberate (experience_kernels.cu:2054-2057 comment: *"No dense/sparse weighting, no soft-clamp, no CEA bonus. This is what risk management (Sharpe, Sortino, MaxDD, VaR) must use"*).

**Evidence strength:** STRONG.

---

## Cross-term synthesis

1. **R3 (CF-flip) dominates the shaping landscape** — 49–74 % of reward magnitude ratio across all 60 epochs. Every other shaping term is ≤ 3 % except R4's fold-3 firing (up to 36 %). If R3 regresses, the symmetry augmentation is likely the only effective shaping the network sees at this scale. Confirms why Track 1's H4 (magnitude-head gradient starvation) bites so hard — without dense magnitude signal and without functioning R5/R6 shaping, only R3's direction mirror gives meaningful gradient mass, and it's direction-only by construction.

2. **R5 and R7 both produce zero at smoke** — R5 by config (intentional), R7 by kernel absence (correct removal). Neither is a regression. Track 2 correctly collapses to "5 live slots" which matches the post-0.8 reducer shape.

3. **R6 (loss-aversion) fires mostly in fold 3** — 12 / 20 fold-3 rows show nonzero clamp vs 4 / 40 in folds 1+2. Whether the clamp is load-bearing depends on how often `base_reward < 0` at production trade-completion density; fold 1's trade-count-starved scale underestimates. Re-validate at L40S.

4. **R4 (trail-firing) tracks trade volume** — same fold-3 dominance pattern as R6 (1 / 20 fold-1, 5 / 20 fold-2, 19 / 20 fold-3). The diagnostic is sound; the reading is correctly fold-starved at small scale.

5. **R2 (PopArt warmup zero) and R8 (pure P&L) both belong KEEP regardless of contribution number** — they're transforms and diagnostic outputs, not additive reward terms. Applying the 1 % rule to them is a category error.

6. **R1's dominance is implicit** — the 5 additive ratios sum to ≤ 1 (allowing for different normalization semantics of rate-vs-magnitude slots); the balance is `step_return`. At 66 % for R3 plus ≤ 3 % everywhere else, `step_return` accounts for the remaining 31 % of reward-magnitude explanation — smaller than expected because R3 is a sample-selector whose "contribution" inflates the denominator's effective magnitude. True additive share of step_return is likely dominant once R3's rate-not-magnitude nature is accounted for.

---

## Proposed Phase 2 changes (Track 2)

Ordered by impact and independence:

1. **FIX-documented-disable R5 (micro-reward)** — DELETE verdict rejected per `feedback_no_functionality_removal.md`. Phase 2 Task 2.3 implementation: preserve the kernel branch at `experience_kernels.cu:1915..2003`, preserve the `micro_reward_scale` config field, preserve the `micro_reward_per_sample` buffer + reducer slot; add code-site comments explaining (a) scale=0 at smoke is intentional test-isolation, (b) production already deploys scale=0.1 so the code path is live in prod, (c) OFI state features are representation signal, micro_reward is gradient-injection — different mechanism, not redundant. Net code delta: +~50 LOC comments, zero deletion.

2. **DELETED / RELOCATED — R6 (asymmetric soft-clamp)** (Task 2.4 landed) — the negative-tail compression moved to Q-target smoothing (same pattern as reward-inventory's `reward_noise_scale` relocation to C51 Bellman). The +10 upper cap is preserved as a separate guard at each former call site (numerical safety, not loss-aversion). Change surface: `experience_kernels.cu:78-81` (function deleted), `:~1774` + `:~3047` (call sites → `fminf(·, 10.0f)`), `c51_loss_kernel.cu::block_bellman_project_f` (Huber-style compression added before support clamp). Rust tuple `reward_contrib_fractions` + `last_reward_contrib` shrank 5→4; HEALTH_DIAG `la={:.3}` field dropped.

3. **CLEANUP stale R7 docstring** — `experience_kernels.cu:1049` comment mentions `patience_mult` for a removed term. Single-line edit: update to reflect current reward shape or delete the reference.

4. **PENDING R2 (PopArt drift)** — preserve through L40S run; re-triage afterward. If `reward_contrib[0]` still zeros across production-scale warmup, consider deleting slot 0 from the reducer (not the PopArt transform itself — that's C51 / cuBLAS territory).

5. **KEEP R1, R3, R4, R8** as-is. R4's smoke-scale caveats resolve automatically at production scale; no code change needed.

---

## Limitations

- **Smoke scale ≠ production scale.** `dqn-smoketest` uses batch=64, buffer=256, 5000-bar data slice × 3 folds. L40S production is batch=16384, buffer=500k, 176 k bars × 6+ folds. R4 and R6 both show fold-3 dominance — scale-sensitive. R2's warmup zero is explicitly fold-length-gated.

- **4 GB VRAM constrains what we can test locally.** No production-scale ablations (remove R6 → retrain → measure P&L change) are feasible on RTX 3050.

- **PopArt warmup zero is a legitimate zero, not a REJECT at smoke scale.** Spec §5.2's decision matrix doesn't distinguish "structurally zero because of scale" from "zero because it's dead code." Applied the warmup-aware interpretation to R2; Phase 2 needs production data before promoting to DELETE or KEEP.

- **R3's rate semantic vs R5/R6's magnitude semantic.** The 1 %-of-reward-magnitude threshold applies cleanly to R5 and R6 (magnitude ratios) but awkwardly to R3 (firing rate) and R4 (firing rate). Both rate slots easily clear 1 % because a non-degenerate rate is usually ≥ 10 %; the floor is less discriminating for them. Keep this asymmetry in mind when reading the verdicts.

- **Trade volume at smoke scale is fold-dependent.** Fold 1's 2356 training bars give few completed trades; fold 3's 3750 bars plus a warmed policy produce the bulk of trail-firing + loss-aversion firing. A 1-fold × 20-epoch smoke would have produced a more pessimistic reading for R4/R6; a 3-fold run averages across states.

- **We did NOT re-run to gather additional data.** The verdicts are strictly against the baseline log. If the log's 60 reward_contrib rows differ by fold, that's captured in the fold-wise fraction breakdowns above; if the log is incomplete (fewer rows than expected), the analysis would need a re-capture.

---

## Next Track 2 steps

- **L40S validation**: re-run the 20-epoch × 3-fold baseline on L40S scale (plan §Task 2.1). Extract the same `reward_contrib` trajectory. Promote R2 from PENDING to final verdict. Re-confirm R4, R6 (fold-3 dominance pattern was real signal or sample artifact).

- **Phase 2 Task #8 (TD-propagation diagnostic)**: originally gating R5 DELETE. With R5 reclassified to FIX-documented-disable, the diagnostic is no longer a pre-requisite for this term — the code path stays regardless. The diagnostic remains useful for Track 4 E4 (C51 entropy) and for validating that the sparse TD path carries meaningful gradient; keep it on the plan under Task 2.0, but decouple from R5 sequencing.

- **R6 relocation design**: sketch the Q-target smoothing mechanism in `c51_loss_kernel.cu` that subsumes the negative-tail compression currently done by `asymmetric_soft_clamp`. Document before deleting R6 from the reward kernel.

- **R4 per-magnitude breakdown**: the current `trail_r` is a single scalar. The per-magnitude `fire_q / fire_h / fire_f` slots (already in HEALTH_DIAG's `trail [...]` group) are more diagnostic; consider whether slot 2 of `reward_contrib` adds value given `trail [fire_q / fire_h / fire_f]` already exists.

- **Stale docstring cleanup**: `experience_kernels.cu:1049` and any other references to `patience_mult` or `v8 reward shape` — foot-guns per Foxhunt "trust code, not comments" rule.
