# KNOWN DEVIATIONS — every place we knowingly differ from the original

**Status:** the honest register. Consolidated at the Phase 2 → Phase 3 boundary from all fourteen
Phase 2 specifications, with the ids renumbered `D-<AREA>-nn` because six documents independently
used `D-1…` for unrelated things (`CLASSIC_MODE_CONTRACT.md` C-21).

**The rule this file enforces:** a deviation is legitimate only if it is *recorded here with its
justification and with what would remove it*. An undeclared difference is a defect, not a
deviation. A deviation that turns out to be visible in play is promoted to a defect and gets a
finding in `GAUNTLET_LOG.md`.

Some sections are empty. That is the correct state for them and they are kept so that a future
contributor has somewhere obvious to put the entry.

Confidence labels as elsewhere: `SOURCE_CONFIRMED` · `MAME_CONFIRMED` · `MULTI_SOURCE_CONFIRMED` ·
`INFERRED` · `UNRESOLVED` · `VERIFIED-HERE`.

---

## 0. Classification

| Class | Meaning | Standard |
|---|---|---|
| **M — mechanical** | changes what the machine *does*, however slightly | must be justified against `ORIGINAL_MISSION.md` and must be listed in `CLASSIC_INVARIANTS.md` if it touches an invariant |
| **R — representational** | different data shape, identical behavior | must have a test proving behavioral identity |
| **P — presentational** | changes what the player *sees or hears*, never the simulation | must have a replay-identity test |
| **S — scope** | not built yet, deliberately | must name the phase that builds it |
| **E — Enhanced-only** | absent from Classic Mode entirely | must be off by default and labeled in the UI |

---

## 1. Core and determinism

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-CORE-1` | R | **The core owns the framebuffer and rasterises even in Enhanced Mode.** | `CLASSIC_MODE_CONTRACT.md` C-08. `LASR`/`LASL` are scheduled processes that write screen bytes directly (`defa7.src:2792-2884`), and `SBOMB` gates on `OBJX`, which only the draw pass sets. A core holding only entity state cannot express the laser. Enhanced therefore pays for a blit it does not display. | Nothing. Skipping the blit would change `OBJX` and therefore change collision and smart-bomb reach. |
| `D-CORE-2` | M | **The overload governor has no CPU-load input.** `EXEC`'s own accumulator never rises of its own accord, so the governor never fires *because the machine is busy*. **It is not dead code and it does fire:** `TERBLO` forces `OVCNT = 8` thirty-two times per planet destruction (`CLASSIC_MODE_CONTRACT` C-26, ROM `$EE3D`, the only non-`EXEC` writer in the set), cutting the star field to 3 and culling one object per `EXEC` pass. The whole routine is implemented, including the 16-bit `0x6000..0x9FFF` relocation and the `OFSHIT` erase (C-31). | The browser core has no cycle model, so the *load* term is always 0. L-030. `ovcnt` and `strcnt` are on the snapshot and forceable from the Lab overlay. | A cost model charging per drawn byte against a frame budget. It is the only thing between this build and a bit-exact one under sustained load. |
| `D-CORE-3` | M | **`GETOB` exhaustion does not hard-reset the machine.** The ROM's `ERROR` jumps to `SINIT` and wipes the game. | Reproducing a machine reset in a browser is user-hostile and untestable. Lab build asserts; shipping build drops the allocation silently and the caller drops the object. | Nothing. Recorded so a RAM trace against the ROM is known to diverge here. |
| `D-CORE-4` | R | **The two IRQ passes are serialised around the executive pass rather than interleaved by interrupt.** | Determinism. An interrupt model would make replays non-reproducible, which `ARCHITECTURE.md` §6 forbids. The serialisation is exact — see `CLASSIC_MODE_CONTRACT.md` §2, and note that the *banding* is preserved and is **not** a deviation (C-25). | Nothing. |
| `D-CORE-5` | R | **`XXX2`, the band boundary, evaluates to 120 every frame** instead of tracking the beam. The arithmetic `xxx2 = min(vertctAtService − 8, 0xA8)` is written out and runs at C0; `vertctAtService` is the constant 128. | `RENDERING_SPEC.md` §5.3, as corrected by `CLASSIC_MODE_CONTRACT` C-24: the two bands are **complementary** (half-open predicates about the single shared boundary `XXX2`), so their union is rows 1..255 for *any* `XXX2` and the value cannot change *which* objects are drawn. What it does change is which **pass** draws a row, and therefore whether that row carries a frame of position lag (C-25) — which is why the gate is kept and Lab-forceable rather than constant-folded. | A cycle-accurate 6809. |
| `D-CORE-6` | R | **`PLADIR`/`PLAXC` are committed once per step at C5**, not at the two `PRDISP` call sites. | Proved equivalent in `CONTROL_SYSTEM_SPEC.md` §2.4 for both fields. | Nothing, unless a renderer needs per-band commit for a visual reason — in which case it must not move the state write. |
| `D-CORE-7` | R | **The `±$0100` `PLAXV` clamp is implemented but provably never fires.** | Drag holds `V16 ≤ 192`. Kept for code shape and for the Lab overlay's "clamp fired" counter. | Nothing; documented dead code. |
| `D-CORE-8` | M | **The anti-tamper trap `SBLNK` (L-084) is not implemented.** | It corrupts RAM by design. | Nothing. Recorded so a RAM trace against a patched ROM is known to diverge here. |
| `D-CORE-9` | R | **`SNDX` is stored as `(tableIndex, entryIndex)` rather than a raw 6809 address.** | Behaviorally identical — Defender never advances past a table's terminating `0`. | Nothing. |
| `D-CORE-11` | R | **The scanline-0 palette-upload gate `CMPA #8 / BHI I01` is always taken.** On the real machine a late-serviced IRQ skips the 16-byte `PCRAM → CRAM` transfer and the frame shows the previous palette (`defa7.src:1966-1968`; cocktail path gates at `CMPA #4`, `2043-2044`). | Same reason as `D-CORE-5`: `vertctAtService` is fixed at 0, so the gate is satisfied on every frame. Written as a gate, not folded away, so a Lab timing-stress feature has something to hook. `CLASSIC_MODE_CONTRACT` §2.3.1. | A cycle model. Recorded for symmetry with `D-CORE-2` and `D-CORE-5`, which got this treatment and this one did not until iteration 4. |
| `D-CORE-10` | S | **`STINIT`'s exact `rand()` call count is measured, not asserted.** | Data-dependent (two rejection loops per star). Bounded in `CLASSIC_MODE_CONTRACT.md` C-19: min 32, mean ≈ 58.8. It runs at every respawn, so replay determinism across a death depends on it. | A MAME trace from `SINIT` with the power-up seed, recording every entry to `$D715` during one `INIT20` pass. Then freeze the number. Replay `RP-18` is the guard until then. |

---

## 2. Scope — built later or not at all

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-SCOPE-1` | S | **Single player only.** `PLSAV`/`PLRES` swap the entire world between two alternating players. Not built. | `CLASSIC_MODE_CONTRACT.md` C-22. The per-player block is already a length-2 array with `curPlr` fixed at 0, so this is a data and lifecycle change later, not a refactor. Two-player HUD mirror positions (`P2DISP`, `P2LAT`, `P2SBD`) are unverified anyway. | L-125 / `OPEN_QUESTIONS` Q-06: trace `PLSAV`/`PLRES` across a swap in MAME with objects in flight. |
| `D-SCOPE-2` | S | **Attract-mode logo decompressors are not implemented.** The WILLIAMS turtle-graphics, DEFENDER run-length and 5×7 copyright decoders are located (L-081) but not specified to pseudocode. | Out of scope for the first Classic build; the game may start at the title without the animated logo. Each decoder is ~40 lines. | A follow-on pass specifying `LOGO`, `DEFNNN` and `COPYRT`. |
| ~~`D-SCOPE-3`~~ | — | ~~**The thrust flame is not drawn.**~~ **CLOSED, Phase 5.** `PRDISP` ($E213) was read end to end and the four routines implemented from the ROM: `THOUT` $E15C (right-facing ship, flame at byte-columns `PLAXC-1 … -4`), `THOUT1` $E19B (left-facing, `PLAXC+8 … +11`), `THOFF` $E1F0 / `THOFF1` $E1CD (the erases — `POFF`'s 8×6 block does **not** cover the flame, so both are load-bearing), and `THPROC` $E9BF (`NAP 4`, `THX` 0..32 inclusive, also cycling `FBX`). Both erases clear **twelve** bytes, unconditionally — the same twelve the thrusting draw writes. `THINIT` (entry **$E149**, not $E14C, which is its second instruction; its one caller `JMP`s to $E149 from defend.1 $D7C3) now stores its 33 samples instead of discarding them; the rand() traffic is unchanged. Four columns is half the ship's width and the flame trails behind, as reported. Idle = 5 bytes in one column; thrust adds three narrowing runs, 12 total. Drawn in whichever banded pass owns the ship's row (C-25), never both. `tests/thrust.test.js`, THR-01..THR-51. | — | — |
| `D-SCOPE-3b` | P | **The thrust flame draws at ~9/16 of the machine's density.** `THINIT` stores `RAND` output unmasked, so the flame's nibbles select all sixteen palette slots uniformly — it is colored static, not one color. Slots 1–9 are live (`COLR`, `CRTAB`, `WCTAB`); slots `A`, `C`, `D`, `E`, `F` have no writer in this build because `CBOMB` and `TIECOL` are unimplemented, and `B` is written only by the death sequence. Measured in play through the renderer's own `buildRgbaLut`, 20 consecutive thrusting frames: **min 12, max 16, mean 13.70 of 24 nibbles lit** — against the 24 × 9/16 = 13.5 the nine live slots predict. (An earlier draft of this row claimed 15–17; that was optimistic and is corrected here. The count steps in runs of four frames, which is `THPROC`'s `NAP 4` showing through.) | The flame **is** visible and is not the `CRINIT`/`COLR` failure mode of a feature landing wholly in a dead slot; the dark nibbles read as holes in the noise, which the flame has on real hardware too — just fewer of them. No color was substituted to compensate. | Implement `CBOMB` and `TIECOL` and create them in `startLifeProcesses` (they are the two remaining `NEWP`s of `PLSTR5`). That lights slots `A` and `D`/`E`/`F` and takes the flame to 15/16. |
| `D-SCOPE-4` | S | **`SCORING_SPEC.md` was never created** as a standalone document; scoring lives in `WAVES_DIFFICULTY_SPEC.md` §7 and is indexed here as `SCO-*`. | Consolidation, not omission. Every claim `md/03` §9 asks for is present. | Nothing; noted so the constitution's file list reconciles. |

---

## 3. Enemies and waves

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-ENE-1` | M | **`GTARG`'s 32-slot scan is modeled with slots 20–31 reading as permanent zero.** The ROM walks `TLIST..TLIST+62` while `TLIST` is only 40 bytes, so slots 20–31 read `FISTAB` (`phr6.src:421-428`, confirmed `VERIFIED-HERE`) — non-zero exactly while a laser is in flight, which can hand `GTARG` a bogus target pointer. | This is the original's behavior whenever the laser is idle, and it keeps the round-robin walk length correct at 32. Reproducing the bogus-pointer path could park the global cursor outside `OLIST..OLEND`. L-123. | A MAME watchpoint on `GT3`; log any returned `D` outside `OLIST..OLEND` over a long session, then follow whether `LANDS0` ever passes the `cvect+1` test on such a pointer. |
| `D-ENE-2` | M | **`ASTCLR`'s 64-entry scan and its `ERROR` trap are bounded.** | Same reasoning as `D-CORE-3`. | Same. |
| `D-ENE-3` | M | **`TERBLO` allocates its explosion objects properly and frees them after `XSVCT` has snapshotted.** The ROM writes into the free-list head (`OFREE`) without unlinking, relying on `XSVCT` snapshotting first; with an empty free list this corrupts page zero. | Reproducing a page-zero corruption is untestable and its consequences are unbounded. The burst is skipped entirely if the free list is empty. | Confirm in MAME that `XSVCT` fully copies the descriptor before returning. |
| `D-ENE-4` | M | **`GA2 = 0` is clamped.** With `GA2 = 0` and `GA1 = 5` the ROM's zero-guard is applied before the ceiling test, so `n = 0` from the ceiling still enters the delta loop and `DECA/BNE` runs it 256 times. Operator settings are not range-clamped (L-085). | We clamp `n` to `[0,255]` and skip the loop when `n == 0` after the ceiling — what the code was obviously meant to do. | Set `GA2 = 0` in MAME's CMOS and read the resulting `PENEMY`. If a real cabinet produces the 256-iteration result, reproduce it. |
| `D-ENE-5` | M | **`astCnt` underflow is guarded.** The ROM's `ASTCLR` is `DEC ASTCNT / BNE …` with no floor. | Underflow to 255 would flip the terrain bit back on and make the planet reappear. Guarded at 0. | Establish whether the underflow is reachable; if it is, decide whether the resulting behavior is observable. |
| `D-ENE-6` | R | **The `ABSORBING` sub-state is derived from the Lander's process address** rather than stored on the humanoid. `HUMANOID_RESCUE_SPEC.md` §1.4. | There is no humanoid struct in the original either; the state lives in the object record plus the abductor's process. A builder who prefers an explicit enum must prove equivalence. | Nothing. |
| `D-ENE-7` | P | **`CBOMB` and `TIECOL` palette animation are emitted as core palette events with no process-table cost.** In the original they are real processes occupying real slots. | The slot cost is unobservable — the pool is never exhausted by two extra records — and modeling them as events lets the Enhanced renderer consume them. | A test showing the process pool reaching exhaustion in play. |
| `D-ENE-8` | R | **Audit meters are recorded but not persisted.** `GEXEC` bumps meter 6 every 240 ticks and `SCORE` bumps meter 5 on each replay award. | There is no coin door and no bookkeeping printout. The counters exist on the snapshot for the Lab overlay. | Building the operator bookkeeping screen. |

---

## 4. Movement, collision and the world

*(No deviations. Every claim in `MOVEMENT_PHYSICS_SPEC.md` is implemented as read.)*

The three entries that document previously carried are now classified elsewhere: the `GETOB`
reset is `D-CORE-3`, the velocity clamp is `D-CORE-7`, and **the "one object screen-position pass"
entry is withdrawn entirely** — `CLASSIC_MODE_CONTRACT.md` C-25 shows the two banded passes are
not cosmetic, so collapsing them is a defect and not a permissible deviation.

---

## 5. Rendering

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-REN-1` | R | **The Classic renderer holds an `ImageData` and a persistence buffer.** | Performance. Both are derived and discardable; `REN-73` asserts they cannot affect the core. | Nothing. |
| `D-REN-2` | P | **Presentation options (`clean` / `raster` / `crt`) do not exist on the original.** | They are strictly downstream of the finished 292 × 240 buffer, cannot read core state, and are proven non-influential by `REN-72..74` and `INT-18`. `clean` is the default. | Nothing; they are the mission's "better looking" half, kept out of the simulation. |
| `D-REN-3` | P | **Lab overlays, captions and debug markers are drawn on the presented surface, after the 80:73 stretch.** | Anything written into the 304 × 256 buffer is simulation state and can be read back by collision. `INT-18` guards it. | Nothing. |

---

## 6. Audio

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-AUD-1` | R | **`BG1`/`THRUST` `SAMPC` is seeded deterministically.** The original stores whatever was in RAM. | Indeterminate power-up RAM cannot be reproduced. The chosen value only needs to be documented and stable for replay determinism. | Nothing settles it. |
| `D-AUD-2` | M | **`SCREAM` (`$1A`) always uses a full 256-sample first block.** The original does not initialize `TEMPB`, so the first block is 1–256 samples depending on what the previous sound left in RAM. | Up to 56 ms of timing difference at the very start of the astronaut scream, and a different phase relationship between the four voices thereafter. `UNRESOLVED`. | A MAME trace of sound-board RAM `$0012` at the moment `$1A` is dispatched, across several contexts. If it is consistently one value, adopt it. |
| `D-AUD-3` | M | **No gameplay sound is emitted during attract; only the Hall-of-Fame organ plays.** | Partially confirmed since Phase 2: attract runs with `STATUS` bit 7 set (`$FF`/`$D9`/`$DB`, `amode1.src:122, 479, 534`), so `APSND` is explicitly suppressed by `APST` and the thrust drone is gated off by `(status & 0x98)`. `CLASSIC_MODE_CONTRACT.md` C-13. Whether other `SNDLD` sites run in attract is still `UNRESOLVED`. | A MAME trace of every write to `$CC02` across a full attract cycle. |
| `D-AUD-4` | P | **The analogue reconstruction filter after the MC1408 is unmodelled.** Classic ships area integration + a 10 Hz DC blocker + master gain, and is therefore brighter than a real cabinet. | The schematic is documented nowhere available and MAME models none either. Enhanced's Remastered position guesses a 2-pole Butterworth at 14 kHz and says so. | The Williams sound-board schematic, or a direct-line recording of a known-good board running the operator audio test, from which the filter can be fitted. |
| `D-AUD-5` | P | **Soft clip above 0.95.** The original clips at the rails. | Headphone-player protection. An "authentic hard clip" toggle is exposed in the Lab overlay. | A product decision, not a research question. |
| `D-AUD-6` | P | **No per-command loudness trim.** Every engine is rendered at the DAC values the program produces. | The perceived balance depends on the same unmodelled analogue stage as `D-AUD-4`. If anything exceeds −1 dBFS the master is lowered, never the command. Peak/RMS per command is recorded in the reference-library metadata. | The same measurement as `D-AUD-4`, plus a calibrated cabinet recording. |
| `D-AUD-7` | R | **`$18 RADIO` and `$10 BG2` are reachable only from the Lab sound browser.** `LGSND` is a fully formed table (`defa7.src:687`) that no code ever loads. | They are dead in the game and must stay unreachable from gameplay code. | A disassembly of a pre-release ROM image, if one surfaces. |
| `D-AUD-8` | R | **The organ arm-then-select gap is 1 500 sound-CPU cycles**, derived from the original's 255-iteration `DECB` busy-wait rather than measured. | Any gap larger than one interrupt latency works. | Cycle-count `HALL3` against the real 6809 clock. Low value. |
| `D-AUD-9` | R | **Sound-board interrupt latency is modeled as zero.** Real hardware finishes the current instruction first (2–17 E cycles). | Below 20 µs and inaudible. | Not worth settling. |
| `D-AUD-10` | M | **`$12` is inert and the silence is reproduced.** Red Label's `LNDFX0` has a register bug (`LDA`/`LDB`) so the intended astronaut-lift sound never plays; the Lander lift is silent after the ten `$0E` commands. | `ARCHITECTURE.md` §9. This is the one place where "do nothing" is a positive requirement and must survive refactoring. Guarded by `AC-03`. | Nothing. It is the shipped behavior of the baseline ROM. |
| `D-AUD-11` | E | **Enhanced Mode continues the abduction pitch ladder** into the silence `D-AUD-10` creates. | Off in Original and Remastered, on in Enhanced, labeled. It fills a silence that exists only because of a shipped bug — which is exactly the kind of judgment the human gauntlet exists to overrule. First on the list of things the critics are asked about. | Gauntlet `EH-3` (game-audio mixer) or `EH-4` (information test) flagging it. Then cut it. |
| `D-AUD-12` | E | **The optional adaptive score.** | Off by default, Enhanced only, labeled "Adaptive score (not original)" everywhere, built from the machine's own organ/square/noise vocabulary in the key the organ actually sounds in. The mission asked for "background music" the original never had; this is the standing goal-drift risk and it is deliberately quarantined. | An adversarial review against `ORIGINAL_MISSION.md`. Fallback: move it from the game menu into the Lab overlay. |
| `D-SND-9` | M | **`PDTH5` and `PLE2` release `SNDPRI` as well as clearing `SNDTMR`** (`game.js` `sndSilence()`). The ROM writes only `CLR SNDTMR` (`$DACC` and `$DB39`, both `0F B3`). **This is a defect repair, not an option, and it is unconditional.** `PLEND` loads `PDSND` (`$F0`, a 49-frame sequence); in the ROM the next 108 frames are `PLEX`, the 128-particle death explosion, which `JSR PXVCT` enters and never returns from — `PLEX` pops its own return address (`PULS D / STD PD2,U`, `blk71.src:566`), sleeps one frame per particle step, and only `PX8 JMP [PD2,U]` resumes `PDTH5`. On real hardware `CLR SNDTMR` therefore lands on an already-idle sequencer with `SNDPRI` already released. `PLEX` is an unimplemented target here (`PHASE4_CORE_COMPLETION` §5, `REN-36`), so `PDTH5` arrives at `PLEND+37` with `sndTmr` still 13 and freezes `SNDSEQ` mid-sequence. `$D57E` is then unreachable, and it is the **only** write of 0 to `SNDPRI` in the entire ROM set — `rom_peek.py find "97 B2"` returns exactly `$D559` and `$D57E`, and `0F B2`, `D7 B2`, `7F A0 B2`, `FD A0 B1`, `FD A0 B2` return nothing. `$F0` then rejected every table below it at `CMPA SNDPRI / BLO` (`$D555`) for the rest of the session: no laser, no smart bomb, no enemy hit, no explosion, while the thrust drone played on because the poll at `SNDS00` never consults `SNDPRI`. Reported by the user as "after i die, the fire sound effect no longer occurs". | Reproduces the state the ROM is genuinely in when it executes those two instructions. Guarded by `assists.test.js` §1 (fire → die → respawn → fire, three lives). `sound.js`'s `sndKill()` is the literal bypass and is **unchanged**, so §12 `E-2` — a Baiter appearing during the death sequence is silent — still holds and its test still passes. | Implementing `PLEX`. When `PDTH5` blocks for its real 108 frames, delete the `sndPri` line and nothing else; `sndSilence()`'s header says so. |
| `D-AUD-13` | P | **Only the Direct room is shipped.** Cabinet and arcade-room impulse responses are behind a Lab flag. | We have no measured impulse responses and will not fabricate them. | A measured sweep through a real Williams upright, or a published response for the speaker and baffle, with provenance recorded in the asset file. |

---

## 7. HUD and scanner

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-HUD-1` | M | **`SETAB` overflow is bounded rather than reproduced.** The original has no bound check and would write past a 200-byte table with more than 100 blip-bearing objects. We hard-cap at 100 recorded addresses, active list first, and drop the remainder silently. | The overrun's target is unanalysed and its consequences unbounded. Likely unreachable in play. | Static analysis of the RAM map immediately after `SETAB`, or an instrumented MAME run exceeding 100 live objects. |
| `D-HUD-2` | — | **Not a deviation: zero-color objects punch transient holes in the scanner's mini-terrain.** `SCNR10`'s `STD [,U++]` is unconditional (`amode1.src:1272`, `VERIFIED-HERE`), so score popups and player shells write `$0000` at their computed scanner address on every pass. Reproduced exactly; **do not** add a `continue` on `objcol == 0`. | Recorded here because it looks like a bug and a future contributor will try to "fix" it. `HUD_RADAR_SPEC.md` open item. | Nothing. Confirmed. |
| `D-HUD-3` | — | **Not a deviation: the scanner's mini-terrain does not match the terrain the player flies over.** `MTERR` is a fixed 64-entry table (`amode1.src:1278+`), indexed from `BGL >> 2`; nothing derives it from `ALTBL`. | Copied verbatim. Test `HUD-39` exists specifically to stop a future contributor "correcting" the mismatch. | Nothing cheap; accept as-is. |
| `D-HUD-4` | — | **Not a deviation: the "current view" bracket under-reports the viewport by ~12%.** It is fixed furniture, 8 scanner columns = 256 world pixels, against a 292-pixel screen. | Reproduced. **No Enhanced option may widen it to a true projection** — a truthful bracket would tell the player something the original's did not. | Settled; recorded as a finding. |
| `D-HUD-5` | E | **The 60 Hz "smoothed" scanner is OFFERED and is OFF.** Classic redraws the radar band **once every eight frames** — `SCPROC` naps 2, then 2, then 4, and `SCNRV` sits on the third leg — so at terminal velocity the world moves 48 px, one and a half scanner columns, between redraws and the blips visibly **step**. That lag is the authentic reading experience and it is reproduced exactly. `src/core/draw/scanner.js` exposes `setScannerSmoothing()`, which runs the pass once per frame instead. **Default OFF. `setScannerSmoothing(true)` from Classic Mode returns `false` and changes nothing** — §9.3's "Classic Mode cannot enable it at all", enforced rather than documented. Enhanced Mode may switch it on; dropping back to Classic switches it off again rather than leaving it latched. | `HUD_RADAR_SPEC.md` E-1 permits it as legibility, not information: the positions were always available at 7.5 Hz and only the sampling changes. **But this build's claim is strictly larger than the spec's, and that is the reason for the entry.** E-1 classifies smoothing "presentation-only" because it assumes the scanner is drawn onto a renderer surface. In this core it is not: `SCNRV` writes `state.fb`, which is simulation state (`C-08`, `D-CORE-1`), so running it every frame **changes the encoded state**. With smoothing on, two runs of the same replay tuple still agree with each other byte for byte — determinism holds — but they do **not** agree with a Classic run of the same tuple. Scoring, RNG traffic and every entity are untouched; only the framebuffer differs. | Nothing removes the flag; it is a wanted option. What would remove the *asterisk* is drawing the scanner onto a renderer-owned plane instead of into `state.fb`, which would contradict `D-CORE-1` and the ROM, and is therefore not planned. Guards: `tests/scanner.test.js` SCN-T-55, SCN-03, SCN-04. |
| `D-HUD-6` | — | **Not a deviation: `HUD_RADAR_SPEC.md` T-03 is wrong and the ROM is right.** T-03 asserts `fb.word(75, 7) == 0x5555` and `fb.word(84, 7) == 0x5555`. The machine cannot produce that. `BORD2` is `STA ,X` — a **one-byte** store — so the scanner's top frame paints scanline 7 only; scanline 8 at columns 48–111 is interior and stays black, giving `$5500`. The same document says so in §4.9 (which quotes `STA ,X`) and in the §2.1 element table ("Scanner top frame \| 47–112 \| **7**"), so T-03 contradicts its own spec two sections earlier. | `VERIFIED-HERE` against `defend.1 @ $D6BC` and `defa7.src:906-928`. Implemented as the ROM writes it; `tests/hud.test.js` HUD-T-03 asserts `$5500` and carries the correction inline. | An edit to `HUD_RADAR_SPEC.md` §10 T-03. The code needs no change. |
| `D-HUD-7` | R | **`FLPUP` and `FLP2` were appended to the frozen `PADDR` table, which shifts process-record slot ids by one.** `PLSTR5` does `NEWP FLPUP,STYPE` (`defa7.src:1279`) and the process had no body, so the game's only "player up" indicator never ran. Creating it consumes a process slot, so every later `makeProcess` lands one slot further down the free list. | Measured across all 28 replay scenarios: `scoreEvents`, `entityCounts`, `finalRng` and the audit meters are **identical**; the only event-stream difference is the `procId` field carried by `SPAWN`, which is instrumentation. The build fingerprint went `0x52ECF70B / 54` → `0x55C71575 / 56` and the corpus was regenerated with `node tools/gen_replays.js`. | Nothing — this moves *toward* the ROM, which creates six per-life processes. It moved again in Phase 4 when `COLR` landed, exactly as predicted: `COLR` and `COLRLP` were appended for the same reason (`PLSTR5` does `NEWP COLR,STYPE`, `defa7.src:1284`, and the process had no body, so palette slot 1 — the laser AND the color the score digits are drawn in — stayed at `CRTAB`'s `$00` and the score was painted black on black). `COLR` is created in the ROM's position *between* `SCPROC` and `FLPUP`. Fingerprint `0x55C71575 / 56` → `0x751E729E / 58`, corpus regenerated again. It will move once more when `THPROC`, `CBOMB` and `TIECOL` land. Recorded so that regeneration is expected rather than alarming. |

---

## 8. Shell, lifecycle and input

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-SHL-1` | P | **Pause, fullscreen, a settings screen, the Lab overlay and replay tools are all additions.** | The mission requires a browser game with no coins and fullscreen. None of them can be reached from a state where they would alter the simulation; pause freezes `step()` and tells audio to `HOLD` without sending `$13`. | Nothing. |
| `D-SHL-2` | P | **The attract sequence never times out and never stops.** A real cabinet ran it forever too, but on a CRT. | No screensaver, no burn-in concern on an LCD; stopping it would be a behavior the cabinet did not have either. | Nothing. |
| `D-SHL-3` | M | **Mode switching mid-run** (Classic ↔ Enhanced) has no cabinet equivalent. | It is the single clearest demonstration that both modes are the same simulation, which is the case study's whole point. The switch is a presentation change only and `INT-17` proves the event stream is unaffected. | Nothing. |
| `D-SHL-4` | P | **Two open tabs are last-write-wins** for persisted state. Correct behavior would be a cross-tab lock. | Rare, recoverable, and a lock is more failure surface than the bug. | A `BroadcastChannel` lock, if it ever bites. |
| `D-SHL-5` | P | **`Cmd+R` loses the run**, mitigated by the snapshot but not prevented. | The browser owns that key. | Nothing. |
| `D-INP-1` | M | **Analog flight, if enabled, is an explicitly-labeled alternate control mode.** Left stick X is refused outright. | `ARCHITECTURE.md` §7. Off by default; digitisation is engage 0.50 / release 0.35 with hysteresis. | Nothing; it stays opt-in and labeled. |
| `D-INP-3` | M | **Directional thrust — `←`/`→` mean "face and thrust that way" — is the DEFAULT control scheme.** `ARCHITECTURE.md §7` says analog/directional left-right flight, *"if offered at all, is an explicitly-labeled alternate control mode and a recorded mechanical deviation — never the default."* The user asked for it as the default playing experience and it is granted. This row is the record. **The core is not changed by one byte for any of it:** `src/input/flight.js` synthesizes the cabinet's own discrete level-Thrust (PIA2 bit 1) and edge-Reverse (bit 6) lines per tick, from the two arrow keys plus a **read-only** look at the ship's facing, and the module's output is still exactly two PIA bytes. See the classification argument below the table. | **Labeled** everywhere it is user-visible: the preset is named "Laptop — ← → thrust", the controls card and the legend show `Thrust Left ←` / `Thrust Right →` rather than a dead `Thrust —` row, and `sampler.inspect().deviations` carries `"directional thrust (laptop scheme)"` for the replay header and the Lab overlay. **The arcade-faithful scheme is unmodified and permanently selectable** — `applyPreset(profile, 'default' \| 'lefty' \| 'cabinet')` or `applyScheme(profile, 'arcade')` — and `IM-29` runs a held Reverse key through the live core to prove it still turns the ship exactly once. `INPUT_MAPPING.md §2.5`. | A product decision, not a research question. **In the running page it is a control, not a code edit:** the Controls panel's `Control scheme` row (`shell/controls.js` `useScheme`) switches to `Arcade (discrete Thrust + Reverse, as the cabinet)` mid-flight, and that lands on the unmodified §3 layout. In code, `applyScheme(profile, 'arcade')`, or changing one line in `defaultProfile()`, reverts the *default* with nothing else touched. The synthesiser and its tests can stay either way. |
| `D-INP-4` | R | **The input module reads one field of core state**, the facing (`NPLAD`, falling back to `PLADIR`), through a host-supplied `readFacing()` callback, once per `sample()`. `INPUT_MAPPING.md §1` said "never consult the simulation state" and is amended in place. | Unavoidable for `D-INP-3`: "hold ← to fly left" is undefined without knowing which way the ship points, because the machine has no directional thrust — it has a level line pointing wherever `PLADIR` already points and a separate edge that flips it. The exception is one field, read-only, on the tick the bytes are produced; the module holds no core reference, writes nothing, and the **arcade scheme never calls the callback at all**. `IM-28` hands the reader an `Object.freeze`d state and asserts it survives a sample deep-equal unchanged. | Nothing, while `D-INP-3` stands. Removing `D-INP-3` removes this automatically — `flight.js` is the only caller. |
| `D-INP-2` | P | **The trackpad is refused for gameplay.** It drives menus, settings and Lab overlays only. | A considered decision, `INPUT_MAPPING.md` §7.1 — the original's grammar is a 2-way stick and five buttons, and a pointer-driven stick is a different game. The mission's "maybe the trackpad" is honored for navigation. | A critic demonstrating that a pointer-based stick is *more* faithful, which seems unlikely. |

### 8.1 `D-INP-3` classified against `ACCESSIBILITY_SPEC.md` §0

The spec's four classes are *presentation-only* (P), *input-equivalent* (I), *mechanical assist* (M)
and *score-invalidating assist* (S). Directional thrust is **Class I on the mechanism and Class M on
the decision**, and it is filed as **M** because when the two disagree the stricter one wins. The
argument, rather than the assertion:

**Why the mechanism is input-equivalent.** Class I asks one question: *could a cabinet player's hand
have produced this exact sequence of switch closures?* Every clause of the answer is yes.

- The lines emitted are the cabinet's own two, on their own bits, with their own trigger disciplines
  — a level Thrust and an edge Reverse. Nothing new is added to the grammar. There is no horizontal
  axis (`§6.3` still refuses left-stick X outright), no analog thrust magnitude, no auto-aim.
- The reverse pulse is a **single tick high followed by at least eight low**. A human flicking a
  cabinet button produces a longer closure than that, so the emitted trace is *inside* what a hand
  can do, not beyond it. Contrast `A-03` auto-fire, which is Class M precisely because its cadence
  is timer-driven; here there is no timer — the pulse is emitted in response to the player's own
  key transition, and the only frame-counted quantity is the *retry* deadline, which fires solely
  when the machine destroyed the first pulse and is therefore a correction, not a rate.
- The synthesiser cannot press anything the player did not press. It has no autonomy: with no arrow
  held it emits nothing at all, and releasing an arrow drops thrust and leaves the facing alone
  (`IM-25`). It never presses Fire, Smart Bomb or Hyperspace, and it never suppresses them —
  `IM-24` asserts a Fire that collides with a pending pulse goes out and the *pulse* waits.
- It does not remove `CONTROL_SYSTEM_SPEC §3.5`'s lost-edge behavior, which is one of Defender's
  real tells. A reverse can still be destroyed by a lower-bit edge; the synthesiser notices and
  retries, which costs the player 133 ms — visibly, in the ship's hesitation — exactly as a cabinet
  player's lost flick cost them a re-press.
- Replay determinism is untouched. The recorded artifact is still the per-tick PIA word; a replay of
  a laptop-scheme run is byte-identical on re-run and is readable by an arcade-scheme build, because
  by the time anything is recorded there is no such thing as a "directional" input.

**Where it is NOT equivalent, and this is the whole of the case against it.** *Reverse-and-thrust
becomes one gesture where the cabinet needed two hands.* On the cabinet, turning around while under
power is: know which way you are pointing, flick Reverse with a button finger, keep Thrust held with
another — the game's signature maneuver, and `INPUT_MAPPING.md §3`'s revision history is entirely
about how hard that finger independence is. Here it is one key held down, and the *knowing which way
you are pointing* is done for you by reading `NPLAD`. That is not a faster hand; it is a decision the
player no longer has to make. A veteran would reasonably call it a mechanical assist, and they would
be right, which is why the row is Class M and labeled rather than Class I and silent.

**What it is not.** It is not Class S. It cannot produce simultaneous inputs the cabinet could not
(contrast `A-04` one-button mode), it does not pause the simulation, it does not change scoring, and
it takes nothing away — every input a cabinet player could make is still makeable, on the arcade
scheme, unmodified, one click away. A laptop-scheme run is a run under a labeled assist, not a run
under a different game.

---

## 9. Accessibility and feedback — Enhanced and assist options

All of these are additions. Classic Mode's fidelity is unaffected because every Class P option is
proven replay-identical by `ACC-01`/`ACC-02` and every Class I option is proven to emit only
cabinet-legal input transitions by `ACC-03`.

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-ACC-1` | M | **Class M and S options are mechanical assists the machine did not have** (A-03 auto-fire, A-04, A-14, A-17, …). | Labeled in the UI, recorded on the run's score, and never on by default. A run using one is not a Classic run and says so. | Nothing; they are the point of an accessibility layer. |
| `D-ACC-2` | P | **The `monochrome` color-vision variant adds a temporal signature** (blink patterns) the original never had. | Color is Defender's only class cue on the scanner; a monochrome player needs a substitute channel. Presentation-only. | Nothing. |
| `D-ACC-3` | P | **The photosensitivity governor (A-16) can alter the presented image** in ways not derivable from the original. | Safety. It measures at run time (`ACCESSIBILITY_SPEC.md` §5.1) rather than trusting a static table, because the flash inventory's area and luminance figures are estimates. | Nothing. |
| `D-ACC-4` | P | **Captions and the state summary use the game's font but are not the game's text.** | They are new information surfaces; they render on the presented surface, never into the framebuffer. | Nothing. |
| `D-ACC-5` | M | **Sticky/latched thrust (A-02) is classified Class I, unlabelled, default off.** | A matchstick in the cabinet's thrust button produces the same input line. A critic could reasonably say sustained holding is part of the physical challenge. | The accessibility specialist and the veteran player disagreeing in the gauntlet, and the adjudicator ruling. Reclassification to M changes only the label and the score table. |
| `D-FBK-1` | E | **Four Enhanced cues are additions** (R-cue, H-cue, N-cue, C-layer). | Enhanced only, off in Classic, each justified in `INTERACTION_FEEDBACK_SPEC.md` §4. | Gauntlet review. |
| `D-FBK-2` | E | **Screen shake.** Defender has no camera shake. | Enhanced only, scaled by a user control with 0 available, applied to the presented canvas so it can never reveal or hide content. | Gauntlet review. |
| `D-FBK-3` | M | **The Control Trainer disables player collision in scenarios 1–9 and clamps `LSEED` in the hyperspace drill.** | It is a training mode, not a run. Its scores are not recorded and it cannot produce a replay. | Nothing. |
| `D-ASSIST-1` | M | **Infinite smart bombs.** `src/core/options.js` flag `infiniteSmartBombs`. `SBOMB` skips the single `DEC PSBC` (`weapons.js` `sbomb()`) and skips the `PSBC == 0` refusal that precedes it. Nothing else moves: `PSBC` itself is never written, the CMOS byte that seeds both stocks (L-024) is untouched, `score.js`'s bonus-ship award is untouched, and the kill sweep, the `SBFLG` re-entry guard, the `OTYP < 2` predicate (C-11/C-32) and `SBSND` are all exactly as they were. **A run with this engaged is NOT SCORE-VALID** — `assistEngaged` is set the first frame the branch is taken (not the frame the flag is set), `core.scoreValid()` reports it, and the `GAME_OVER` event carries `scoreValid: false`. **HUD:** `SBDSP` is a saturating three-icon lamp in the original (`CMPA #3 / BLS`), so a frozen stock would paint three icons at the first respawn and never move again. With the assist on the strip is driven from the clamp instead of the stock and drops one icon while `SBFLG` is set, so a press visibly knocks an icon out for the ~1 s of the bomb's own flash and it returns — the original's glyph, pitch, clear region and clamp, no new symbol, and `psbc` still not written. | The user asked for it so the public page is enjoyable to try. It is off in the core and turned on by the page only, through the strip's `Assists` button (persisted in `settings.assists`), never by the core itself. Replay identity with the flag off is proven by `assists.test.js` §4 and by `tools/gen_replays.js --check` reproducing all 28 replays and 22 audio scenarios byte for byte with the code present. | Turning the flag off — one click on the page's `Assists` button, which calls `core.setOptions()` and takes effect on the next frame that consults it. Classic Mode never sees it. |
| `D-ASSIST-2` | M | **Risk-free hyperspace.** `src/core/options.js` flag `infiniteHyperspace`. Hyperspace has **no inventory in Defender** — there is no counter to make infinite and the button is already unlimited — so the only thing that can be made free is `HYP2`'s arrival roll, `LDA LSEED / CMPA #192 / LBHI PLEND` (63 of 256 values, 24.6%). With the flag on that branch is not taken. Everything else stands: `STATUS` bit 0 still blocks the first 96 frames of every life and every wave (CTL-33/CTL-54), the destination is still read from the live generator, and the roll still **consumes no `rand()` call** (`LDA LSEED` is a read, §7.3), so the RNG stream is bit-identical with the flag on or off and only the branch differs. **NOT SCORE-VALID** on the same terms as `D-ASSIST-1`; the spared `HYPERSPACE` `stage: 'end'` event carries `assisted: true`. A roll that was going to survive anyway is left completely alone and marks nothing. | As `D-ASSIST-1`. | Turning the flag off, from the same `Assists` button. **Switching it off does not launder a score:** `assistEngaged` records that an assist *fired*, and clearing the flag afterwards does not clear that. |
| `D-ASSIST-3` | M | **Unlimited lives.** `src/core/options.js` flag `infiniteLives`. Defender spends a life at the **start** of a life, not at the death — `PLSTRT`'s single `DEC PLAS` (`game.js` `plstrt()`), which the wave-completion path's `INC PLAS` exists purely to cancel (WAV-43, L-055). The flag skips **that one instruction and nothing else**. `PLEND` still runs in full: the ship is still erased, the death sequence and its explosion still play, the sound channel is still loaded and released, and `PLSTRT` still re-runs `INIT20` including `STINIT`'s data-dependent `rand()` calls. **The death and respawn sequence therefore consumes exactly the same `rand()` calls either way** — measured identical at every matched tick through two deaths and past the tick where the unassisted run reaches `plas == 0` (100/300/500/549/551/700/900/910/912/1000). It stops the counter, not the funeral. What it does NOT claim: the two runs' RNG streams stay identical forever. They diverge once the unassisted run would have ended and the assisted one keeps playing — the divergence is the extra game, not the flag. An earlier draft of this row said 'bit-identical with the flag on or off' full stop; that was false at 2400 ticks and is corrected here rather than left standing. `ple1`'s `plas == 0` game-over test then never fires. **NOT SCORE-VALID** on the same terms as `D-ASSIST-1`: `assistEngaged` is marked on the frame the decrement is actually suppressed, so a run that never dies stays valid. | The user asked for it so the public page is enjoyable to try. Off in the core; the page turns it on through the strip's `Assists` button. | Turning the flag off — one click. Classic Mode never sees it. |

**Why these are flags on the core and not a fork.** `ARCHITECTURE.md` §2: *"If Enhanced Mode needs
to change a mechanic, it does so behind an explicit flag on the core, documented as a deviation —
never by forking the core."* The block is a **non-schema** field on the state, like `state.events`:
`snapshot()`/`restore()` carry it so a Lab snapshot resumes the same machine, and `encodeState()`
cannot see it, so the canonical byte encoding — the whole definition of "the same run" — is
unchanged and every stored replay predates and survives the feature. `setOptions()` throws on an
unknown flag name rather than ignoring it, because a silently-dropped flag is how an "enhanced"
build ships as a classic one and nobody notices.

**What is deliberately NOT in that file.** A repair for a port defect is not optional and does not
get a switch. The post-death sound latch (§6, `D-SND-9`) is fixed unconditionally, because the port
was wrong — not because the player wanted help.

### 9.1 What the shipped page turns on, and where the switch is

`ARCHITECTURE.md` §7 and §0's Class M both require a labeled deviation to be *visible where it is
in force*, not only recorded here. Three are in force on `arondale.com/defender`, and this is the
whole list. Nothing else on that page departs from Classic.

| On the page | Row | Where the visitor switches it | Where the page says so |
|---|---|---|---|
| Directional thrust on `←` `→` | `D-INP-3` | `⌨ Controls` → **Control scheme** → `Arcade` | the flight legend (`#controlsLegend`, rebuilt from the live bindings), the §06 instruction card (its rows are tagged `data-scheme` and only the true set is shown), and `#defaultsNote` |
| Unlimited smart bombs | `D-ASSIST-1` | strip → **Assists** | `#assistLegend`, hidden when they are off |
| Hyperspace that cannot kill you | `D-ASSIST-2` | strip → **Assists** | `#assistLegend`, and the §06 Hyperspace row |
| Unlimited lives | `D-ASSIST-3` | strip → **Assists** | `#assistLegend`, hidden when they are off |

Three properties of that table are load-bearing and are asserted in `tests/shell.test.js`:

1. **The static markup already tells the truth.** `#controlsLegend`'s shipped text must equal
   `controlsLegendText(defaultProfile())`. `applyProfile()` overwrites it the instant the module
   loads, so a stale string is invisible to anyone whose modules load — and perfectly visible to
   the reader most likely to be checking the fidelity claim with scripts off.
2. **No row can print a key the current scheme does not have.** Every `data-bind` on the page is a
   real action *and* is bound in every scheme the row is shown under. Before this rule the default
   scheme rendered `Thrust — —` and `Reverse — —` under two paragraphs of arcade-layout history.
3. **Score validity is one-way.** Turning the assists off mid-run does not restore it.

The core's defaults are unchanged by any of this: `createCore()` with no argument is the Classic
build, every test and all 28 stored replays run with all three off, and the shell is the only
caller that turns any of them on.

---

## 10. Deviations promoted to defects during integration

Entries that were *proposed* as deviations in Phase 2 and are refused here.

| Origin | Proposed deviation | Ruling |
|---|---|---|
| `MOVEMENT_PHYSICS_SPEC.md` §10, `CONTROL_SYSTEM_SPEC.md` §13.1 | "One object screen-position pass per step instead of the original's two raster-banded passes. Cosmetic." | **Refused.** `CLASSIC_MODE_CONTRACT.md` C-25: `OPROC` recomputes position from live `OX16` and live `BGL`, and the A-pass runs before `VELO` and before `BGL` advances. Everything below screen row 120 therefore draws — and collides — one frame later than everything above it, and `COLCHK` reads that lag through `OBJX`. Collapsing the passes is a P0 defect. Guarded by `INT-33`. |
| `AUDIO_ENGINE_SPEC.md` §3.2 | `soundSequencerStep()` first in `step()`. | **Refused as a fact, not a deviation.** C-01: the audio author read only the scanline-128 IRQ path. `SNDSEQ` runs at C1, after the executive. Not a deviation — a misreading, and every tick number in the audio corpus moves. |
| `RENDERING_SPEC.md` (BGLX note) | `BGOUT` is the sole writer of `bglx`. | **Refused.** C-02: `PLAYER` writes it too (`defa7.src:2419-2420`). Under the single-writer model the starfield never scrolls. |

---

## 10A. Phase 3 integration — builder deviations, reviewed

**Reviewed** at the Phase 3 integration pass, one entry per deviation the five parallel builders
declared. The rule applied: *a deviation survives only if it is backed by the ROM or by a
measurement that can be re-run; otherwise it is reverted to spec behavior.* **No builder deviation
was reverted** — every one carried a citation or a number, and in most cases the spec is what is
wrong. Rows marked **SPEC BUG** need an amendment to the named document; rows marked **FIXED**
were defects, not deviations, and were repaired during integration.

Class column as §0. `VERIFIED-HERE` means re-checked against the Red Label images during this
integration pass, not merely inherited.

### Builder 1 — input, player, weapons, collision

| Id | Class | Deviation | Evidence | Disposition |
|---|---|---|---|---|
| `D-P3-01` | — | `CTL-04` asserts `lflg === 3` after 7 steps; the build reports 2 at frame 7, 3 at frame 8. | Contract §2.3's own latency table: Fire sampled at C1 of frame *n* dispatches at B6 of *n+1*. The frame-7 edge has not produced its laser yet. | **SPEC BUG** — `ACCEPTANCE_TESTS.md` CTL-04. |
| `D-P3-02` | — | `CTL-52` asserts `lflg` reached 4; the build produces exactly one laser. | Alternating press/release gives a 2-frame period; SSCAN needs TWO consecutive zero samples, so only the first press ever edges. **CTL-04 and CTL-52 cannot both be true.** The load-bearing half of CTL-52 (`pladir === 0x0300` for 60 frames) passes. | **SPEC BUG** — one of CTL-04/CTL-52 must be re-issued. |
| `D-P3-03` | — | `MOV-23` asserts `plabx === 0x0800` on every frame; the build settles at `0x0840` on frame 80. | **NEW ROM FINDING.** `PV9` does `CLRD / STD BGDELT` then `LDD PCX / PV10 STD PLAX16` — it moves `PLAX16` the full remaining `$0100` with `BGDELT` at 0, so world X jumps 2 px with no `BGL` compensation on the settling frame. `defa7.src:2404-2410`. Reachable: `$2000 → $7000` is exactly 80 × `$100`. | **Reproduced.** Original behavior, not a port bug. |
| `D-P3-04` | — | `MOV-08` asserts `plaxv` reaches 0; it freezes at `0x0000FC`. | The drag term is `−4·V16` at the 24-bit LSB. Once `V16` is 0 the term is identically 0, so the fractional residue byte freezes. The four literal values MOV-08 names all reproduce exactly. | **SPEC BUG** — assert "never increases", not "reaches 0". |
| `D-P3-05` | — | `MOV-27` asserts `bgl` returns to its start after 342 frames. | 342 × 192 = 65664 = 65536 + 128. One lap is 341⅓ frames; `bgl` advances a further 128 WU per lap and only repeats after 1024. The mechanism the test guards — 16-bit overflow, one wrap, no modulus — is intact. | **SPEC BUG** — re-issue as `(start + 342·192) & 0xFFFF`. |
| `D-P3-06` | — | `CTL-35/36/39` drive the exported `hyperTeleport()` / read the reported `lseed` instead of forcing seeds inside a live run. | B4's unconditional `rand()` sits between any external write and B6, so no external forcing can land. CTL-39 instead asserts `died === (lseed > 192)` over 80 seeds and that both outcomes occur — strictly stronger than two forced cases. | **Accepted.** Test method, not behavior. |
| `D-P3-07` | S | `PDEATH` is implemented as its FRAME COUNTS only; `PLSTR5` does not run `ALINIT`/`BGINIT`/`SCLR1`/`PINIT` or start the six per-life animators. `HYPER` emits `SCREEN_CLEAR` rather than running `SCLR1`. | Those belong to the explosion engine, the terrain subsystem and the renderer. The counts are carried so death → respawn has the right length for CTL-47/48/49/54. | **Scope.** Removed when the draw and explosion builders land. |
| `D-P3-08` | R | The `±$0100` velocity clamp counter is a module-level variable, not core state. | `CORE_API.md` §2.3 forbids ad-hoc properties on `state` (they drop out of `encodeState`), and contract §3 has no field. Write-only instrumentation. MOV-03/MOV-07 assert it stays 0 over 50 velocities × 2 facings × 800 frames. | **Accepted.** Same class as `D-CORE-7`. |

### Builder 2 — enemies

| Id | Class | Deviation | Evidence | Disposition |
|---|---|---|---|---|
| `D-P3-09` | — | `LANDG` moves in X **and** Y on the same frame; `ENEMY_BEHAVIOR_SPEC` §3.4 says the tick ends after the X step. | `VERIFIED-HERE`. `$F09C` is `E3 0A ADDD OX16,X / ED 0A STD OX16,X` and the next byte `$F0A0` is `LNDG2`'s `A6 2C LDA OY16,Y`. No branch. Confirmed in both recovered source trees (`defb6.src:757-765`). Only the LATCH test is exclusive. | **SPEC BUG** — `ENEMY_BEHAVIOR_SPEC` §3.4 needs a two-line correction. |
| `D-P3-10` | — | `ENE-46` bound asserted as `\|yv\| <= $200 + $10`, not `<= $200`. | The ROM clamps at `CMPD #$0200 / BLT / LDD #$0200` and **then** applies 1/32 damping and a −16..+15 noise term (`defb6.src:213-233`). A ROM-faithful build ends the tick slightly outside the clamp. Measured peak `0x1F7..0x207` over 200 ticks. | **SPEC BUG** — ENE-46 as written fails every correct build. |
| `D-P3-11` | — | `ENE-60` asserted as `ceil(64 − row/(4·delta))` frames, not exactly 64. Measured 62 at 10 columns of range, 64 at 125. | `GETSHL`'s C-15 cross-seed starts the shell at `column.ROW`, not `column.0`. 64 is exact only for a shooter on row 0, which `YMIN = 42` forbids. ENE-60b's long-range case does land on 64. | **SPEC BUG** — needs the correction or a stated row precondition. |
| `D-P3-12` | — | `ENE-62`'s "the X and Y aim errors are correlated" is contradicted. | Measured over 100 000 draws: largest \|z\| of the joint `(seed & $1F, lseed & $1F)` histogram against the independence prediction is 3.27 across 1024 cells — exactly what multiple testing predicts for independent draws. They are deterministically **paired**, not statistically correlated. | **SPEC BUG** — `ENEMY_BEHAVIOR_SPEC` §2.4 fact 2 and ENE-62's golden histogram both need re-baselining. |
| `D-P3-13` | — | Swarmers get no appear effect, against §2.2's "every enemy spawn ends with APVCT". | `MMSW` (`defb6.src:141-172`) contains no `APVCT` call — it does its own `STX OPTR`, which is what an enemy that skips `APST` must do. The Bomber is the same and §5.2 already says so. | **SPEC BUG** — also weakens §9.4's starvation example. |
| `D-P3-14` | — | `TIEST`'s squad-direction sense is the opposite of §5.2's pseudocode. | The ROM is `COM TFLG / BMI TIEST1 / NEGB` — a NEGATIVE `tflg` **skips** the `NEGB`. §5.2 has the test inverted. Only the phase of the alternation changes, but a replay recorded against the spec's sense would diverge. | **SPEC BUG.** |
| `D-P3-15` | — | `ACCEPTANCE_TESTS.md` uses the id `ENE-70` for two different tests (line 304, the Lander convergence seam; line 340, the explosion duration). | Reported, not resolved — adjudication is not remediation (C-33). | **CORPUS BUG** — one of them must be renumbered. |
| `D-P3-16` | R | `proc.pd8` / `proc.pd9` were created lazily on `state.proc` by `bomber.js`. | `MSPROC` records are 23 bytes against `PSIZE` 15; `TIEST` writes both. `pd9` (the squad's shared cruise altitude) is a random walk with its own history and is not derivable. | **FIXED** — both are schema pool fields now (`state.js` `POOLS.proc`). They snapshot. |

### Builder 3 — terrain, planet, humanoids

| Id | Class | Deviation | Evidence | Disposition |
|---|---|---|---|---|
| `D-P3-17` | — | `HUM-05`'s turn rate is 9/256 = 3.516 %; measured 3.6202 %. | The predicate (`CMPA #8 / BLS`) is exactly right; the expected *frequency* assumes a uniform `rand()`, **and the Red Label generator is not uniform.** Over 10 000 000 draws the rate is 3.6202 %; the per-value histogram for outputs 0..8 runs 36 729..43 166 against a uniform expectation of 39 062. The test asserts the measured value *and* that it is not 9/256, so the finding cannot be quietly lost. | **SPEC BUG** — same class as the `rmax` non-uniformity finding. |
| `D-P3-18` | — | `HUM-09` puts TLIST slot 0 first; the ROM puts it **last**. | `ASTRO` is `LDX PD,U / LEAX 2,X / CMPX #TLIST+32 / BLO / LDX #TLIST / LDX ,X` — it advances **before** it reads, and `PLRES` seeds `PD = &TLIST[0]`. Period (32 frames) and spacing (2 frames) are exactly as stated; only the phase differs. | **SPEC BUG.** |
| `D-P3-19` | — | `ASTRO`'s cursor covers **sixteen** TLIST slots, not twenty. Humanoids in slots 16..19 never walk. | ROM `$ECCB 30 02 8C A1 3A 25 03 8E A1 1A` = `LEAX 2,X / CMPX #$A13A / BLO / LDX #$A11A`, and `$A13A − $A11A` = 32 bytes = 16 words, while `TLIST` is `RMB 40` = 20 slots. | **Reproduced.** Unreachable with ≤ 10 humanoids, but real. |
| `D-P3-20` | — | `HUM-32`'s ascent window 384..387 should be 379..382. | The latch writes `(~lndYv) & 0xFFFF`, which advances the pair by `lndYv + 1` units per frame, not `lndYv`. The test computes the exact crossing and asserts `rate === lndYv + 1` so the cause is visible. HUM-33's window survives. | **SPEC BUG.** |
| `D-P3-21` | — | `HUM-68`'s premise (a deposit at row 200 over alt 224) is geometrically unreachable. | `AFALL2`'s deposit test is `GETALT(x16) < (y16 >> 8)`, so over ground at row 224 the deposit cannot fire above row 225. The clause's real content is asserted on the massif instead. | **SPEC BUG.** |
| `D-P3-22` | — | `PLANET_STATE_SPEC` §2.2's `TEREX` X-displacement formula uses the returned seed in B. | `JSR RAND` is `PSHS B … PULS B,PC` — it **restores** the caller's B. At the first burst B is the 2 that `LDB #2 / STB XTEMP` left; at the second it is whatever `XSVCT` left, which is not determinable from the source. 2 is used for both and said so in the code. Nothing observable turns on it: PLA-12's assertions hold for any low byte. | **UNRESOLVED** — wants a MAME single-step through `XSVCT`. |
| `D-P3-23` | — | `D-2` kept as the ROM has it: `TEREX` writes into the free-list head **without unlinking it**, and both bursts of an iteration share the record. | Allocating properly would perturb the free-list order, which is observable (§3.7) and already pinned. Only the guard D-2 actually asks for is added (skip the burst when `OFREE` is 0 rather than corrupt page zero). | **Reproduced.** PLA-12b asserts the free list is undisturbed. |
| `D-P3-24` | M | In the flattened frame order, `TERBLO`'s `status \|= 2` lands at B6, one phase **after** that tick's A4 `BGOUT`, so the terrain is drawn one last time on the destruction tick. | Inherent to `step()`'s flattening: in the original the IRQ can fire mid-frame. Same class as `D-CORE-4`. | **Accepted, flagged.** Would need a cycle-accurate 6809. |
| `D-P3-25` | R | `PLA-30` ("the player has no terrain collision", L-144) is asserted as a **source-tree invariant** — `player.js` must not import `terrain.js` and must contain no `getalt`. | A negative finding breaks by someone adding the thing. Asserting absence structurally is the only form that catches it. | **Accepted.** Good practice; noted so it is not mistaken for a gap. |

### Builder 4 — difficulty, score, waves, lifecycle

| Id | Class | Deviation | Evidence | Disposition |
|---|---|---|---|---|
| `D-P3-26` | — | `WAV-57`: wave 14's `ufoTim` bottoms at 28 GEXEC ticks, not `$18` = 24. | Wave 14 starts at 88 and the intra delta is −12: 88, 76, 64, 52, 40, 28, then 16, which is below MIN 24 and is therefore **skipped** (WDELT skips, it does not clamp — WAV-07's own rule). 24 is unreachable from 88. Wave 1 *does* reach exactly 24 from 192. | **SPEC BUG** — the number is right for wave 1 and wrong for wave 14. |
| `D-P3-27` | — | `WAV-71`: `lndXv` saturates at 95, not at MAX `$60` = 96. | Wave 1's `lndXv` is 32, intra delta +3: `32 + 3k ≤ 96` gives 95 at k = 21, and 98 exceeds MAX and is skipped. 96 is not on the lattice. | **SPEC BUG.** |
| `D-P3-28` | — | `SCO-11`: 99,999,999 + 150 = **00,000,149**, not 150. | 100,000,149 mod 100,000,000 is 149. `SCORE`'s `DECB / BPL SCR2` discards the carry out of byte 0, exactly as OQ-3's interim describes. S-11 forgot that 99,999,999 + 1 is 0. | **SPEC BUG.** |
| `D-P3-29` | — | `WAV-30`: the third wave-1 squad is **skipped** at tick 61. | After two squads `lndCnt` is 10 and `CMPA #8 / BHS GEX3` skips the drip. Ticks 1 and 31 are correct; a third squad cannot arrive until the count falls below 8. | **SPEC BUG.** |
| `D-P3-30` | — | `SCORE` writes `SCRFLG` with `(scrflg << 1) \| 1`, not `scrflg = 1`. | `VERIFIED-HERE`: `rom_peek.py addr D360 16` → `34 76 1A 01 09 8A …` = `PSHS A,B,X,U,Y / ORCC #$01 / ROL <SCRFLG`. A rotate through a set carry. `state.js`'s schema comment already said "ROL'd by every SCORE call". | **SPEC BUG** — `WAVES_DIFFICULTY_SPEC` §7.1's pseudocode is wrong. The ROM wins. |
| `D-P3-31` | — | With `GA2 = 0` the delta loop is **skipped** rather than run 256 times. | The ROM's zero guard sits before the ceiling test, so a ceiling of 0 leaves `n = 0` and `DECA / BNE` wraps to 256 iterations. `WAVES_DIFFICULTY_SPEC` D-2's interim ruling is to skip. | **Accepted** — sanctioned by the spec's own interim. WAV-77 records the resulting difficulty so the ruling stays checkable. |
| `D-P3-32` | — | The `pwav = 0` W-column select yields column 3 (INTERDELT), not W1 as §10.6 claims. | `CMPA #4 / BLS / LDA #4 ; ADDA #3` gives col = 3 for `pwav = 0`. Reachable only by wrapping past wave 255. | **Reproduced with a comment.** §10.6 is wrong; the case is unreachable in play. |
| `D-P3-33` | P | The "no coins" rule is an **operator setting**, not a patch to the credit gate. `FREE_PLAY_CMOS` sets CMOS `FREEPL = 1`; `FPLAY` (`defa7.src:1092-1099`) then grants two credits. | Reaches the mission's acceptance condition through a path a real operator could and did set. `startGame()` additionally grants credits and skips the switch test; both paths exist and both are tested, so the difference is visible rather than assumed. | **Accepted.** |
| `D-P3-34` | S | Two-player handover pre-selects `curPlr` instead of running `PLE02`'s `PLELP` search loop. | D-4 / C-22 scope single-player, `curPlr` fixed at 0. The machinery is present so the shape is right. | **Scope.** |
| `D-P3-35` | — | `PDTH4` is reached 34 frames after `PLEND`, where `SOURCE_MAP_CORE` §14.2 says 32. | `PXCTB`'s eight colors cost 2 (blank) + 2 (draw) = 32, and the terminating 0 is read after one more blank/sleep pair, adding 2. Measured from the running core. | **SPEC BUG.** |
| `D-P3-36` | R | Fifteen ELIST tuning bytes, `penemy`, `audit`, `credit`, `pdflg`, `pwrflg` and `gamePhase` were plain properties installed by `ensureWaveState()`, invisible to `snapshot()`/`restore()`/`encodeState()`. **A replay could not reproduce a ramped wave.** | Contract §3.11 carried only the reserve and census bytes. | **FIXED** — all are schema rows now. `ELIST_FIELDS_NOT_IN_SCHEMA` is empty and `tests/waves.test.js` fails if anyone reopens it. |
| `D-P3-37` | R | `state.js`'s CMOS defaults disagreed with the ROM's `DEFALT` block in six bytes. | `VERIFIED-HERE`: `python3 tools/crit_defalt.py` against `defend.10 @ 0x06CF` → `CSELCT=03 SLOT1M=01 SLOT2M=04 SLOT3M=01 MINUNT=00 GA1=05 GA2=15 GA3=01`. **`GA2` is the load-bearing one:** at the old default of 5 the difficulty ceiling binds from wave 1 and every wave from 5 upward freezes at wave 4's parameters. | **FIXED** — `state.js` now ships `DEFALT`. `ACCEPTANCE_TESTS.md` §1.2's `ga2: 5` fixture inherits the same error and is a **SPEC BUG**. |

### Builder 5 — audio, replay corpus

| Id | Class | Deviation | Evidence | Disposition |
|---|---|---|---|---|
| `D-P3-38` | — | The thrust poll reads the **previous** frame's PIA sample; `AUDIO_ENGINE_SPEC` §3.4's pseudocode uses `input.thrust` directly and is one frame early. | `SNDSEQ` falls through into `SSCAN` with no `RTS`. The poll is `$D580 96 7B LDA <PIA21`, and `$D5A8` is `B6 CC 01 LDA $CC01` (SSCAN's first instruction). `phr6.src:263-264` names the fields for the same reason. Thrust **physics** keeps its 0-frame latency (PLAYER at C2 reads after the shift); only the drone lags. | **SPEC BUG** — `AUDIO_ENGINE_SPEC` §3.4. |
| `D-P3-39` | — | `AUD-14`'s second clause ("`$0F` is emitted on the first tick after the sequence ends") is not reproducible. | `VERIFIED-HERE`: `SNDLD`'s `CLR THFLG` at `$D54F` is **unconditional** and runs before the priority test. Once `LASSND` is loaded `thFlg` is 0, so the release branch (`LDA THFLG / BEQ SNDSX`, `$D586`) can never fire. Audibly correct — the drone was replaced and never resumed. | **SPEC BUG.** |
| `D-P3-40` | — | `INT-09`'s "`thFlg` is cleared and `$0F` is emitted with tick === n" is unsatisfiable. | `SNDS01` is `LDA THFLG / BNE SNDSX` (`$D590`) — once the drone is latched the routine returns **before** STATUS is read, so the `BITA #$98` gate is unreachable. What actually happens on a death tick is that `PLEND`'s `sndLoad(PDSND)` clears `thFlg` and `PDSND`'s `$11` replaces the roar at C1 of the same tick, which is the substance of the claim. AUD-13 asserts the gate IS live for a press that has not yet latched. | **SPEC BUG.** |
| `D-P3-41` | — | `AUD-06/07/08` asserted as **offsets** from the load tick rather than absolute ticks 0, 4, 8. | `step()` does `state.tick = tick + 1` at A1, so the first frame a core runs is tick 1 and absolute tick 0 is unreachable. AUD-04/05 use their literal ticks and pass unchanged. | **SPEC BUG** (minor). |
| `D-P3-42` | R | The replay corpus's `expect` payloads are a **frozen baseline of the tree**, not a claim about Defender. | Half the subsystems were unwritten when it was generated. `tests/replay.test.js` separates the two: per-scenario determinism (replay the same tuple twice) is true forever; `INT-25`'s baseline comparison is *designed* to fail when behavior changes and its message names `node tools/gen_replays.js`. | **Accepted.** Regenerated at integration; see `PHASE3_BUILD_REPORT.md`. |
| `D-P3-43` | — | **`weapons.js` carried its own `SNDLD` writing `sndEntry = 0`, so `LASSND`, `SBSND` and `PDSND` were completely silent.** | `VERIFIED-HERE`: `SNDLD`'s `LEAX -2,X` at `$D55B` sets `SNDX` to `tableAddr − 2`, i.e. entry index **−1**, so the sequencer's first `LEAX 3,X` lands on entry 0. Writing 0 makes the first advance land on entry ONE, which does not exist in a single-entry table. | **FIXED** — the copy is deleted; `weapons.js` re-exports `sound.js`'s `sndLoad`. There is one `SNDLD`. |

### Integration-created deviations

| Id | Class | Deviation | Justification | What would remove it |
|---|---|---|---|---|
| `D-P3-44` | R | **`obj.ocvect` and `obj.opict` hold real Red Label CODE ADDRESSES**, and synthetic (test-only) vectors are allocated in the reserved page `$0001..$00FF`. | Three modules invented a dispatch table during the parallel build and two id spaces resulted; `collide.js` dispatched small ids while every real object carried a ROM address, so `COLIDE` reported HIT and ran **no death routine at all**, silently. The addresses win because they are globally unique without a registry, and because the ROM's own comparison works unchanged — `LANDS0` does `LDA OCVECT+1,Y / CMPA #ASTKIL&$FF` and `$ED70`'s low byte is `$70`. Addresses re-verified this pass (`ED70`, `EDBC`, `F1E0`, `F20B`). | Nothing. One registry, `src/core/cvect.js`. Guarded by `tests/integration.test.js`. |
| `D-P3-45` | R | **`src/core/subsystems.js` fixes the module import order, and that order is part of the build's identity.** | `PADDR` ids are assigned in registration order and are stored in `proc.paddr`, i.e. inside `encodeState()`. Before the manifest existed, which routines were registered *and what number each had* depended on which module the host imported first — so a replay recorded under one entry point would not restore under another, and processes/vectors nobody imported were simply absent (`COLCHK`'s `procAddrOf('PLEND')` threw; `TIEKIL` was missing). | Name-keyed `PADDR`s stored as strings, at a cost in encoded-state size. Until then: **append to the manifest, never insert.** |
| `D-P3-46` | S | **`init20` runs `FISS → STINIT → OINIT → FBINIT → THINIT` but not `CRINIT`.** | `CRINIT` is the palette copy and belongs to the renderer. The four RNG consumers and their order are exact, including STINIT's data-dependent count (C-19). | The renderer landing. No RNG traffic is involved, so replay identity is unaffected. |

---

## 11. Empty by design

The following categories have **no** known deviations, and that is the claim being made:

- **Scoring.** Values, the BCD arithmetic, the bonus-ship threshold and the end-of-wave humanoid
  bonus are implemented exactly. The 8-digit wrap at 99,999,999 and the 6-digit display wrap at
  1,000,000 are both reproduced.
- **World topology.** 2048 px, 65536 world units, 16-bit overflow wrap, no modulus anywhere, no
  seam special case.
- **Player terrain collision.** There is none, and none has been added (L-144).
- **Wave completion.** `WVCHK`'s seven terms and its four omissions are exact.
- **The RNG.** The original algorithm, the original power-up seed, one unconditional call per
  frame, direct-read consumers preserved.
- **Control grammar, *as the core sees it*.** 2-way vertical stick, Fire, Thrust, Reverse, Smart
  Bomb, Hyperspace. Nothing added to the two PIA bytes, no horizontal axis, no analog magnitude, no
  auto-aim. **AMENDED — this bullet no longer covers the whole chain.** `D-INP-3` makes directional
  thrust the default *scheme*, which merges Reverse and Thrust into one gesture on the player's side
  of the boundary while emitting the same two lines on the core's side. The claim that survives is
  the one above; the claim that "nothing is merged" is now false above the input layer and true
  below it, and the difference is `D-INP-3`. Per §11's own rule this was a mission-level event and
  it was the user's explicit instruction; `GAUNTLET_LOG.md` is where a critic gets to argue with it.

If any of these acquires an entry, it is a mission-level event and belongs in `GAUNTLET_LOG.md`
before it belongs here.

---

## 12. Amendment rule

Adding a row requires: the class, the justification, what would remove it, and the acceptance test
that proves it does not leak into the simulation. Removing a row requires the evidence that
settled it and an edit to every sibling document that cited it in the same commit.
