# CPU keeper lateral-carry correction — 2026-10-30-r3

Current-builder note: historical rollback commands on this page also require
`--legacy-outlet-deadline` after the r4 input deadline correction.

A player can skate in from the boards, move the puck across the keeper and
finish while he remains committed to the old side. Native lateral saves can
start during the shooter's windup, before the puck is released; their final pose
then holds for 32 update ticks while further keeper target updates are blocked.
The carried-puck target also uses the midpoint between the skater and his puck,
so a puck carried well out on the stick can already be on the opposite side.

The correction follows the actual carried puck. During a live CPU keeper's
lateral save, its long final hold can end sooner when an opposing skater still
has the puck and is moving across him toward the other side. The remaining
hold is capped at eight native ticks; its natural animation completion and
get-up remain. A stale cached movement decision is cleared so he can reassess
once the pose ends. There are no position, velocity, rating or shot-power writes.

Manual keepers, actual loose-shot/pass saves, catch dives and stopped/highlight/
special contexts retain their native hold. The correction applies to CPU
keepers across difficulties. Crit locking, immediate B switching and the Stars
panel repairs from r2 remain. The Hard win-rate investigation stays stopped.

## Targeted evidence

The exact candidate passes 82 native cases checking both teams/sides, eligible
and excluded states, all registers, stack, condition codes and the whole keeper
object. Only the declared final-hold and stale movement-cache fields can change.

The paired live fixture runs 48 distinct carry/windup/release scenarios: both
teams, both ends, both sides, three depths and two shot timings. It initializes
one controlled setup, including the native sorted collision cache and correct
net-side flag, with other skaters idle away from the crease, then uses real
directional/C input. There are no position, score,
clock, ownership or RNG writes during observation. The previous r2 allows 10
goals and makes 15 keeper catches; this image allows 4 goals and makes 22 catches.
The reported representative approach changes from a goal to a save. Individual
outcomes can still differ in either direction; keepers are not made invulnerable.
This is a controlled regression set, not 48 full games or a new win-rate series.
An additional 16 paired Normal/Easy approaches cover both clubs, ends and sides:
r2 allows 6 goals and makes 4 catches; current allows 4 goals and makes 7 catches.
Native goal attribution and advancing clocks pass in all of these contexts.
Another 64 diagonal/closer approaches (32 with C, 32 without) allow 1 goal on r2
and 0 on the candidate; catches are 24 versus 23. These contexts do not trigger
the stale lateral-hold condition, and all outcomes remain recorded.

Earlier invalid setup probes, unsuccessful no-anticipation/recovery candidates
and all failed attempts are retained privately. A draft that hooked the general
animation updater was rejected. The final design leaves that updater intact.

## Hooks and exact source

- `D8F8`: `E2 40` (ASR) becomes `4E 71` (NOP). The surrounding native subtraction
  and addition now retain actual puck X rather than halving its offset from the
  skater. The existing CPU keeper decision path selects this code.
- `D650`: only the operand of the existing conditional branch changes. The
  original recovery-bit test and ordinary untaken path remain intact.
- `14BE6`: a six-byte JMP veneer uses dead padding behind the existing unconditional
  glass hook at `14BE0`. The glass implementation already jumps directly to
  `14C04`, `14C2C` or `14C70`; its hook and continuations remain exact.
- `2DB800`: `goalie_anticipation.s`, 206 bytes, handles only the taken recovery
  branch and returns through original `15464`, preserving registers and SR.

Builder: `experimental/build_test.py`; tests: `verify_goalie_lateral.py`,
`verify_goalie_lateral_gameplay.py` and `goalie_lateral_gameplay_hook.py`.
With the normal plain patched builder input:

```
python3 tools/experimental/build_test.py base.bin r2.bin \
  --legacy-goalie-lateral --version 2026-10-30-r2
```

This recreates the exact preceding r2. Five independent builds verify the current
image, r2 rollback, original 38-win rollback, older 2026-10-29-r3 rollback and
version-only parent. The original-v5.1W IPS reconstructs the full current ROM.
Only 219 bytes differ from r2, including header/checksum, the three declared
low-ROM regions and the new guarded module. Hard tactical modules, rosters,
attributes, native skating, shots, passing and controller assignment are exact.

## Replay-test scope

r2's original maintenance requirement was to retain all 40 old no-B trajectories.
A deliberate keeper correction changes gameplay, so that assertion cannot be
claimed for r3. The amended test keeps its strict original default mode. Its
explicit keeper-change mode freshly repeats all 40 exact r2 controls against
the immutable original traces, records all 40 current trajectories and verifies
the complete keeper-only binary diff against r2. The original observer's source
hash remains fixed. Current trajectory differences are reported, never labeled
unchanged. The first strict-invariance failures remain retained as evidence.

All 69 exact-source checks executed: **68 passed**. The inherited receiver-hold positioning check still fails, so the strict release gate remains FAILED. This is the same documented AI limitation retained in the user-selected playtest. See [QA_STATUS.json](QA_STATUS.json) for every result.
The current gate commands, including report validation, require
`--keeper-reference exact-r2.bin`; the rollback command above produces that image.
ROM SHA256: 3453b3fba1a4e3b76c8f859311a499f3f2bf8d9a493280f43ad41eca8b784057.
CRC32: A33889B1. Fresh progress applies at release; remaps/config/history migrate.

## Delivery

The exact r3 ROM/IPS, code, byte exports and QA report are published. Downloads
and the canonical workspace now contain r3; the preceding current files were
replaced. Local progress cleanup completed, and controller-remap/play-history
hashes were preserved. Steam RetroArch launched r3 successfully. Bodin’s existing
playlist update is queued; remote installation has not yet been confirmed.
