# CLASSIC MODE CONTRACT — the single integrated Phase 3 build target

**Status:** BINDING. Written at the Phase 2 → Phase 3 boundary by the integration adjudicator.
**Supersedes**, on every point where they disagree, the individual subsystem specifications listed
in §0.3. Where this document is silent, the owning subsystem spec governs.

This document exists because five authors wrote in parallel against a shared evidence model and
declared what they *assumed* about each other. §1 records every contradiction found, the evidence
that settled it, and which document must be corrected. §2–§9 are the contract itself.

**Trust rule carried forward:** where recovered source and the Red Label ROM disagree, the ROM
wins. Every resolution in §1 was taken back to `_defender-reference/mwenge-defender/src/*.src` and
cited by file and line; nothing here was resolved by averaging two specifications.

---

## 0. Preliminaries

### 0.1 Confidence labels used below

`SOURCE_CONFIRMED` · `MAME_CONFIRMED` · `MANUAL_CONFIRMED` · `VIDEO_MEASURED` ·
`MULTI_SOURCE_CONFIRMED` · `INFERRED` · `UNRESOLVED` · `VERIFIED-HERE` (read during this
integration pass, cited by file:line, not previously owned by a ledger entry).

### 0.2 The one-line summary of what changed

Four of the five specs agreed on the frame order; the audio spec did not, and was wrong. One
subsystem believed it was the only writer of `BGLX`; it is not, and under its model the starfield
would never scroll. The score is four bytes, not three. The scanner's player marker comes from
`PLAXC`, not `PLABX`. Everything else that looked like a contradiction turned out to be two
correct statements about different contexts, and §1 says which context each belongs to.

### 0.3 Documents integrated

| Area | Document | Author's test prefix |
|---|---|---|
| Controls / weapons | `docs/research/CONTROL_SYSTEM_SPEC.md` | `CS-` |
| Movement / collision | `docs/research/MOVEMENT_PHYSICS_SPEC.md` | `MP-` |
| Enemies | `docs/research/ENEMY_BEHAVIOR_SPEC.md` | `E-` |
| Humanoids | `docs/research/HUMANOID_RESCUE_SPEC.md` | `H-` |
| Waves / scoring | `docs/research/WAVES_DIFFICULTY_SPEC.md` | `W-`, `S-` |
| Planet | `docs/research/PLANET_STATE_SPEC.md` | `P-` |
| Rendering | `docs/research/RENDERING_SPEC.md` | `R-` |
| Audio | `docs/research/AUDIO_ENGINE_SPEC.md` | `T-` |
| HUD / scanner | `docs/research/HUD_RADAR_SPEC.md` | `T-` |
| Shell / lifecycle | `docs/design/SHELL_AND_LIFECYCLE_SPEC.md` | `T-` |
| Feedback | `docs/design/INTERACTION_FEEDBACK_SPEC.md` | `T-` |
| Accessibility | `docs/design/ACCESSIBILITY_SPEC.md` | `T-` |
| Input | `docs/design/INPUT_MAPPING.md` | `IM-` |
| Enhanced audio / visual | `docs/design/AUDIO_ENHANCEMENT_SPEC.md`, `VISUAL_ENHANCEMENT_SPEC.md` | `EA/EH/EO-`, `E-` |

Five documents use the prefix `T-` and two use `E-`. See C-21.

---

## 1. CONTRADICTIONS FOUND AND RESOLVED

Each entry: the claim, the counter-claim, the evidence, the ruling, the document to fix.

---

### C-01 — Where `SNDSEQ` runs inside `step()` · **the audio spec is wrong** · HIGH

**Claim (audio).** `AUDIO_ENGINE_SPEC.md` §3.2: "`soundSequencerStep()` is the first call inside
`core.step()`, before any object, player or scheduler code", deriving the rule *"a sound requested
by game logic during frame N is transmitted at the top of frame N+1."*

**Counter-claim (control, enemies, rendering).** All three place `SNDSEQ` at the head of **phase
C**, after the executive pass: `CONTROL_SYSTEM_SPEC.md` §2.2 step C1, `ENEMY_BEHAVIOR_SPEC.md`
§0.3 step 8, `RENDERING_SPEC.md` §5.2 step C1.

**Evidence.** `defa7.src:1931–1990` is one interrupt handler with two disjoint paths. The audio
author read only the `VERTCT >= 128` path:

```
        LDA    VERTCT
        CMPA   #128
        BLO    I0               ; <-- the OTHER half of the frame
        ...
        JSR    SNDSEQ           ;SOUNDS+SWITCHES
        JSR    PLAYER
        JSR    STOUT
        ...  OPROC / PRDISP / SHELL
I0      ...
        INC    TIMER            ; <-- the executive's gate
        ...  CSCAN / BGOUT / PRDISP / OPROC / VELO
```

`EXEC` (`defa7.src:3046–3048`) spins on `TIMER`, which only the scanline-0 path increments. The
executive therefore runs *between* the two IRQ passes. `SNDSEQ → PLAYER → STOUT → OPROC → PRDISP →
SHELL` is the order **within phase C**, not within the frame.

**Ruling.** `SNDSEQ` runs at **C1**, after the whole executive pass. The audio spec's derived
latency rule is false for the majority of sounds:

| Sound requested in | Emitted at | Latency |
|---|---|---|
| Phase B — all enemy AI, `LFIRE`, `SBOMB`, `COLCHK`/`PLEND`, `GEXEC`, humanoid logic, `TERBLO` | C1 of the **same** tick | 0 |
| Phase C — `PLAYER`, `SHELL` | C1 of the **next** tick | 1 |
| Phase A — nothing calls `sndLoad` in phase A | — | — |

This moves the tick number of nearly every entry in the audio regression corpus by one.
`SNDLD` sets `sndTmr = 1`; the same frame's `SNDSEQ` decrements it to 0 and emits.

Second consequence: the thrust gate `(status & 0x98)` is evaluated at C1, so a death written into
`STATUS` by `COLCHK` at **B2 of the same tick** silences the drone on that tick, not the next.

**Fix:** `AUDIO_ENGINE_SPEC.md` §3.2 (rewrite to this ruling; re-cite `defa7.src:1931-1990` **plus**
`3046-3048` so the citation covers the executive gate; downgrade the label from `SOURCE_CONFIRMED`
to `MULTI_SOURCE_CONFIRMED` with the ROM cross-check `$E7C3 96 5D 27 FC` = `LDA <TIMER / BEQ −4`),
§3.4 (retitle "once per tick, at C1"), **§12 E-1** (the same-tick collision order inverts — see
C-28) and **§13** (regenerate every tick number in the audio corpus).

The sequencer *algorithm* is unaffected. **Everything that reasons about when it runs is not.** The
claim "nothing else in the audio spec moves", carried in iterations 1–3, was false and is withdrawn:
E-1 and §13 both move, and §5.3 of this document had to stop hardcoding `phase: 'C'`. **Applied in
iteration 4.**

---

### C-02 — `BGLX` has two writers per frame, not one · **the rendering spec is wrong** · HIGH

**Claim (rendering).** "`BGOUT` updates `BGLX` to `BGL & $FFE0` at the end of its run… **If `BGLX`
is updated anywhere else, all three desynchronise.**"

**Counter-claim (controls).** "`BGLX` is snapshotted inside `PLAYER` before `BGL` is advanced."

**Evidence.** Both writes exist and both are load-bearing.

```
blk71.src:180-182   (BGOUT, phase A4)
TTER08  LDD    BGL
        ANDB   #$E0
        STD    BGLX

defa7.src:2419-2431 (PLAYER, phase C2)
PV10    STD    PLAX16
        STA    NPLAXC
        LDD    BGL
        STD    BGLX             ; <-- full width, no mask
        ...
        ADDD   BGL
        SUBD   BGDELT
        STD    BGL              ; <-- BGL advanced AFTER the snapshot
```

Under the rendering spec's single-writer model, `STOUT` at C3 computes
`(BGL & $FF80) − (BGLX & $FF80)` where `BGLX` was set from the *same* `BGL` at A4 — the result is
always 0 and **the starfield never scrolls**. That is a visible, shipped-looking bug that the
rendering spec would have produced.

**Ruling (iteration 4).** Two writers, in this order within a frame:

```
A4   BGOUT:   bglx = bgl & 0xFFE0        (bgl is last frame's post-PLAYER value)
C2   PLAYER:  bglx = bgl                 (full width) ; then bgl += plaxv − bgdelt
```

Plus three overrides: `PLEND` `bglx = bgl` ("NOMO SCROLL", `defa7.src:1329-1331`), `HYPER`
`bgl = bglx = SEED:HSEED` (`defa7.src:3226-3227`), `PLSTR5` `bgl = bglx = 0`
(`defa7.src:1241-1244`).

---

#### ⚠ **RE-ADJUDICATED IN PHASE 4 — THIS RULING IS HALF WRONG. `HIGH`.**

**There are TWO RAM CELLS CALLED `BGLX`, and the ruling above conflates them.**

`phr6.src:214` reserves `BGSAV RMB 32` at `$A000`–`$A01F`, commented "RESERVED FOR TERRAIN".
`blk71.src:30-47` re-`ORG`s `$A000` and lays **eleven of the terrain generator's own variables
inside that reservation**, the eleventh of which is its own `BGLX RMB 2` at **`$A015`**. The SYSTEM
`BGLX` (`phr6.src:216`) is at **`$A022`**, immediately after `BGL` at `$A020`. Two different cells,
same name, different modules.

Verified byte for byte against the Red Label images with `tools/rom_peek.py`:

```
BGOUT   $C092  bank 7   DC 20 C4 E0 93 15    LDD BGL / ANDB #$E0 / SUBD <$15
BGOUT   $C0C2  bank 7   DC 20 C4 E0 DD 15    LDD BGL / ANDB #$E0 / STD  <$15
PLAYER  $DA4A           DC 20 DD 22          LDD BGL /            STD  <$22
STOUT   $E08A           DC 22 C4 80 93 6F    LDD <$22 / ANDB #$80 / SUBD ITEMP
EXPU3   $FD64           DC 22 C4 C0          LDD <$22 / ANDB #$C0
SHELL   $E45D           DC 22 C4 E0          LDD <$22 / ANDB #$E0
```

`DD 15` and `93 15` occur **only** inside `defend.6` bank 7 — the terrain module. `DC 22` and
`DD 22` occur **only** outside it. `BGOUT` never touches the system `BGLX`.

**Corrected ruling.** The system `BGLX` at `$A022` has **exactly ONE per-frame writer: `PLAYER` at
C2**, plus the three overrides above. The terrain generator's `$A015` is private to `BGOUT` and is
modeled as `state.bglxt` (see `src/core/state.js` §3.10b and `src/core/draw/index.js`'s header).

**The starfield still scrolls, on a different proof.** `STOUT` runs at C3, *after* `PLAYER` at C2,
so it reads the value `PLAYER` latched this frame — the PRE-move `bgl` — against a `bgl` that
`PLAYER` has already advanced. The difference is this frame's camera step, which is exactly what
`STOUT` wants. The single-writer model is not the bug the two-writer ruling was invented to avoid.

**THE DERIVED CONSEQUENCE IS WITHDRAWN.** It read:

> ~~Explosions are screen-locked while the terrain is alive, and world-locked after the planet is
> destroyed.~~

`EXPU` runs at B3, *before* `PLAYER` at C2, so the system `BGLX` it reads still holds the
**previous** frame's pre-move `bgl` while `bgl` holds the previous frame's post-move value. The
term is therefore the previous frame's camera step — **non-zero whenever the camera moved, in both
halves of the game.** Measured with the terrain alive and the ship thrusting: a steady −3 byte
columns per frame. **A burst is WORLD-locked always; it stays over the ground it happened on.**
The term is zero exactly when the camera did not move, which has nothing to do with `STATUS` bit 1.

Two independent derivations reached this in Phase 4 — the draw-pass builder from the address split
in `draw/index.js`, and the explosion-engine builder from running `INT-05` rather than reading it.
`tests/explosions.test.js` asserts the arithmetic half of `INT-05` verbatim and asserts the
screen-locked half **false**.

**Fix:** `RENDERING_SPEC.md` §9.3's `BGLX` note and §6.4's zero-compensation claim;
`CORE_API.md` builder 1's "`BGOUT` is the FIRST `BGLX` writer" bullet and builder 4's "identically
zero" bullet; `ACCEPTANCE_TESTS.md` `INT-05` — all re-issued in Phase 4.

---

### C-03 — Score storage width: 3 bytes vs 4 · **the rendering spec is wrong** · MEDIUM

**Claim (rendering).** §11.2: `bcd = score BCD, 3 bytes, most significant first`; "a score of
1,000,000 wraps (6 digits only)".
**Counter-claim (HUD, enemies).** 4 bytes, 8 digits, of which 6 are displayed.

**Evidence.** `phr6.src:442` `P1SCR RMB 4`. `SCORE` (`defa7.src:485-505`) computes a byte index
`B = 3 − (A >> 1)` and propagates the BCD carry down to index 0 — four bytes. `SCRTR0`
(`defa7.src:552-559`) loads `LDU #P1SCR+1` and renders six digits, so **bytes 1..3 only**.
`RCHK` compares the same three bytes against the replay threshold.

**Ruling.** Storage is `Uint8Array(4)`, packed BCD, MSB first, 8 digits. The display window is
bytes 1..3 (6 digits) and wraps at 1,000,000; the stored score does not wrap until 99,999,999,
where `SCORE`'s carry out of byte 0 is discarded. The replay-threshold comparison also uses only
bytes 1..3, so bonus ships also restart their cycle at 1,000,000. Rendering's *display* claim was
right; its *state model* was wrong.

**Fix:** `RENDERING_SPEC.md` §11.2 — index into `score[1..3]` of a 4-byte array.

---

### C-04 — What the scanner's player marker reads · **the control spec is wrong** · MEDIUM

**Claim (controls).** "The scanner reads `PLABX` (world X quantised to whole pixels by the
`& 0xFFE0`)".
**Counter-claim (rendering).** "The scanner marker and the laser origin both derive from `PLAXC`,
not from world position."

**Evidence.** `amode1.src:1246-1260`, the player-blip tail of `SCNR`:

```
        LDD    PLAXC
        LSRA / LSRA / LSRA / LSRA
        LSRB / LSRB / LSRB
        ADDD   #$4B00+SCANH-1
```

`PLABX` is never referenced by `SCNR`. Its readers are all enemy AI
(`defb6.src:56, 200, 240, 254, 846, 852, 951, 1007`). Object blips are world-based —
`SCNR10` computes `OX16 − XTEMP` where `XTEMP = BGL − ($8000 − 150*32)` (`amode1.src:1195-1197`).

**Ruling.** Two different mappings coexist in one widget:

- **object blips** — world, player-centered, `blipX = ((ox16 − (bgl − 0x8000 + 4800)) & 0xFFFF) >> 10`
- **player marker** — *screen*, `col = plaxc_hi >> 4`, `row = plaxc_lo >> 3`

The control spec conflated them. The `−0x8000` half-world offset it described is real but belongs
only to the blip path, and it carries a `+150*32` term the control spec omitted.

**Fix:** `CONTROL_SYSTEM_SPEC.md` cross-reference note on the scanner. `HUD_RADAR_SPEC.md` §4.7
already has this right and does not move.

---

### C-05 — `MKPROC` insert position: head of list vs after current · **both right, in different contexts** · MEDIUM

**Claim (controls).** "links the new process at the HEAD of the active list … when two switches are
queued, the second-queued runs FIRST."
**Claim (rendering).** "inserted immediately after the current one with `PTIME = 1` and therefore
runs LATER IN THE SAME dispatch pass (L-082)."

**Evidence.** `defa7.src:80-83`:

```
        LDX    [CRPROC]
        STU    [CRPROC]
        STX    ,U               ;LINK IN
```

Insertion is **immediately after `CRPROC`**, unconditionally. `CRPROC` is set to `#ACTIVE` — the
list-head sentinel — at `EXEC` entry (`defa7.src:3046-3047`) and is only re-pointed by `DISP1`
(`STU CRPROC`, `defa7.src:3121-3124`) when a process is actually run.

**Ruling — one rule, two consequences.**

> `makeProcess()` links the new record immediately after `CRPROC`, with `ptime = 1`.

- During **B5** (switch queue drain) `CRPROC` is still the head sentinel, so switch-spawned
  processes land at the head, and when two are queued the **second-queued runs first**. Controls'
  statement is the correct special case.
- During **B6** (dispatch) `CRPROC` is the running process, so a child lands immediately after its
  parent and `DISP`'s walk reaches it in the same pass. Rendering's statement is the correct
  general rule.

Both spec texts stay; each gains a sentence naming its context.

---

### C-06 — `otyp` during the appear animation: assignment vs bitwise-OR · **the control spec is loose** · LOW

**Claim (controls).** "`APST` sets `OTYP = 2` for the 47-frame appear animation."
**Evidence.** `samexap7.src:57-59` `LDA OTYP,X / ORA #2 / STA OTYP,X`; cleared at `EXPU6`
(`samexap7.src:173-175`) with `ANDA #$FD`.

**Ruling.** `otyp |= 0x02` on appear start, `otyp &= ~0x02` on appear end. It matters for a Lander
that is already `typ == 1` (mid-grab) — it becomes 3, not 2 — and for any future type. The
smart-bomb exemption (`otyp >= 2`) holds either way, so no behavior changes; the spec text does.
The comment block at `phr6.src:490-495` is the authority on the type vocabulary and says
"2 (BIT 1) = APPEAR NON-HYP".

---

### C-07 — Hyperspace materialise: 47 frames or 40? · **statement conflict, not fact conflict** · LOW

**Claim (rendering).** "runs the standard APPEAR engine on it for 47 frames."
**Claim (controls).** the phony object is destroyed 40 frames after `APVCT`.

**Evidence.** `defa7.src:3268-3273`:

```
        JSR    APVCT
        NAP    $28,HYP2         ; $28 = 40
HYP2    LDX    PD,U
        JSR    KILOFF
```

`APVCT → APST` starts `RSIZE = $AF00`, decremented by `$0100` per executive pass until it turns
positive — 47 drawn passes, killed on the 48th (`samexap7.src:63, 158-160`).

**Ruling.** The appear envelope is 47 ticks; the player's is **truncated at 40**. The player's
materialisation is visibly shorter and ends mid-ramp; an enemy's runs to completion. Total
hyperspace = 15 + 40 = **55 ticks = 0.9152 s**.

**Fix:** `RENDERING_SPEC.md` §6.3/hyperspace wording.

---

### C-08 — Does the core own the framebuffer? · **rendering's decision is ratified** · HIGH

`RENDERING_SPEC.md` decision 1 asserts the core owns the framebuffer and `step()` returns it,
reading `ARCHITECTURE.md` §2's "no canvas" as "no Canvas API / no DOM". `ARCHITECTURE.md` §2's
diagram implies renderers synthesize pixels from state. `HUD_RADAR_SPEC.md` §3.1 straddles the two
("I specify WHAT is written and WHERE… the renderer owns the pixels").

**Evidence — and it is not `OBJX`.** The decisive fact is the laser. `LASR`/`LASL`
(`defa7.src:2792-2884`) are ordinary scheduled processes. They write screen bytes directly
(`STB ,X` / `LEAX $100,X`), read `STATUS` bit 6 to self-terminate, erase their own trail on death
(`LRDLP`), and pull their sparkle bytes from `FISTAB`. A core that holds only entity state cannot
express the laser at all — the laser *is* a run of framebuffer writes with a collision test
attached. `SBOMB`'s eligibility test (`defa7.src:3187` `LDD OBJX,X / BEQ SBMB2`) then reads a
field that only the draw pass sets.

**Ruling.**

- The core owns a `Uint8Array(156 × 256)` framebuffer. It is simulation state.
- "No canvas" in `ARCHITECTURE.md` §2 means **no Canvas API, no DOM, no `ImageData`** — recorded
  here as a clarification of that document, not an amendment to it.
- The Classic renderer is a pure `(framebuffer, cram) → RGBA`.
- **Cost, accepted:** Enhanced Mode also pays for Classic rasterisation, because Enhanced runs the
  same core. Recorded in `KNOWN_DEVIATIONS.md` as D-CORE-1. It is not optional: skipping the blit
  in Enhanced would change `OBJX` and therefore change collision and smart-bomb reach.
- No overlay, caption, Lab panel or debug marker may be written into the framebuffer. Everything
  of that kind is applied to the presented surface after the 80:73 stretch.

---

### C-09 — What `GNCIDE` spares · **both right** · LOW

Controls: "kills every process except the death process and coin processes." Enemies: "kills every
process EXCEPT the current one (`CRPROC`)."

`defa7.src:91-102`: `CMPX CRPROC / BEQ GNC1` ("DO NOT KILL SELF") **and**
`LDA PTYPE,X / CMPA #CTYPE / BEQ GNC1`. Both exemptions exist. At `PDTH5` the death process *is*
`CRPROC`, so the two statements describe the same code from two call sites.

**Settles `ENEMY_BEHAVIOR_SPEC.md` open item "GNCIDE's treatment of the current process":** it
spares `CRPROC`, so `BONUS` can take over `GEXEC`'s process record. Close that item.

---

### C-10 — The humanoid stack unwind: what actually survives it · **neither spec said this** · MEDIUM

Controls stated the tri-state correctly ("the whole collision walk is abandoned that frame"), but
neither spec said what happens *after* the abandonment.

**Evidence.** `defb6.src:384-387`:

```
ASTKIL  LDA    PCFLG            ;PLAYER?
        BEQ    ASTK1
        CLRA
        PULS   D,PC             ;IGNORE
```

`PULS D,PC` discards `COLIDE`'s return address into `D` and returns to **`COLCHK`**, with `A = 0`
so `Z` is set. `COLCHK` (`defa7.src:3143-3149`) therefore takes `BNE COLC2` *not taken* and **falls
straight through to the shell test**:

```
        JSR    COLIDE
        PULS   D,U
        BNE    COLC2
        LDX    #SPTR
        JSR    COL0             ; <-- still runs
```

**Ruling.** The unwind abandons the **`OPTR` walk only**. The `SPTR` (enemy shell) walk still runs
on the same tick. A player overlapping a humanoid is immune to every enemy *body* behind that
humanoid in list order, but is still killable by a shell on that tick. Add to
`MOVEMENT_PHYSICS_SPEC.md` §7.4 and `HUMANOID_RESCUE_SPEC.md` §9.

---

### C-11 — Two independent mechanisms protect humanoids from the smart bomb; only one is real · LOW

Controls and enemies both claimed humanoid smart-bomb immunity, attributing it to `typ == $10`.
Correct — `SBOMB` filters `otyp >= 2` (`defa7.src:3189-3191`). But `SBOMB` never sets `PCFLG`, so
if that filter were ever loosened, `ASTKIL` would take the `PCFLG == 0` branch and **kill the
humanoid silently**, with `AHSND` and a full explosion.

**Ruling.** The single authoritative smart-bomb predicate is
`objX !== 0 && otyp < 2`. It is a class invariant, not a convenience test. `INT-11` guards it.

---

### C-12 — `STCHK` has three entry points and one of them reads `PTARG`, not `ASTCNT` · **the planet spec is incomplete** · MEDIUM

`PLANET_STATE_SPEC.md` §1.1: "`STCHK` is the only routine that *recomputes* bit 1, and it does so
from `astCnt`."

**Evidence.** `defa7.src:1319-1325`:

```
STCHK   CLRB
STCHK0  LDA    ASTCNT
STCHKA  BNE    STCHK1
        ORB    #2
STCHK1  STB    STATUS
```

Three entry points. `PLS01` (`defa7.src:1304-1307`) enters at **`STCHKA`** with `A` already loaded
from the per-player `PTARG`:

```
PLS01   JSR    SCLR1
        LDB    #$05
        LDX    PLRX
        LDA    PTARG,X
        BSR    STCHKA
```

**Ruling.** During the 96-frame start delay the terrain bit is derived from the per-player
`PTARG`; from `PLS1` onward it is derived from the global `ASTCNT`. These agree at wave start but
can differ in a two-player game and during the transition. Contract signature:

```
stchk (b = 0)     → status = b | (astCnt === 0 ? 0x02 : 0)
stchk0(b)         → status = b | (astCnt === 0 ? 0x02 : 0)
stchkA(b, a)      → status = b | (a === 0 ? 0x02 : 0)
```

**Fix:** `PLANET_STATE_SPEC.md` §1.1; `CONTROL_SYSTEM_SPEC.md` §10 note.

---

### C-13 — Attract mode's `STATUS` is not `$7F` · **the control spec's table is wrong** · LOW

`CONTROL_SYSTEM_SPEC.md` §10 lists `$7F` as "spawn setup, attract" and separately says bit 7 is
"set by `PLE2`, attract". `$7F` has bit 7 **clear**, so the table contradicts itself.

**Evidence.** `amode1.src` writes `$FF` (line 122-123), `$D9` (479-480) and `$DB` (534-535) —
`"THIS IS THE STATUS / WHY??? WHO KNOWS!"`. All three have bit 7 set. `$7F` is written only by
`PLSTR0`.

**Ruling.** Attract runs with bit 7 set throughout. Therefore `APST`'s `APSND` suppression
(`samexap7.src:47-49`, `BITA #$80`) is active in attract, and `SNDSEQ`'s thrust gate
`(status & 0x98)` is also non-zero, so no drone. This **converts `AUDIO_ENGINE_SPEC.md`'s
deviation D-4 ("emit no gameplay sound during attract") from a guess into a partially confirmed
behavior** for the appear sound and the drone. It remains `UNRESOLVED` for the other `SNDLD`
sites, and the interim (silence) stands.

**Fix:** `CONTROL_SYSTEM_SPEC.md` §10 table row; `AUDIO_ENGINE_SPEC.md` §15 D-4 note.

---

### C-14 — Baiter first-appearance timing: 24.2 s vs 23.96 s · **the HUD spec is wrong** · LOW

`HUD_RADAR_SPEC.md` open item: "cut to 24.2 s at ≤8 enemies". `WAVES_DIFFICULTY_SPEC.md` §5.2:
23.96 s.

The cap is written **before** the decrement (`defa7.src:1667-1691`), so a cap of `c` yields
`c − 1` ticks. `c = 192/2 + 1 = 97` → 96 ticks × 0.2496 s = **23.96 s**. HUD used 97 ticks.
Waves is right. Same correction applies to the 1–3-enemy case: 48 ticks = 11.98 s, not 12.2 s.

---

### C-15 — Shell `OX16`/`OY16` low bytes are seeded from the *other* axis · **undocumented state** · LOW

Neither spec states the birth value of a shell's fractional bytes. `GETSHL`
(`defa7.src:2562-2575`) builds `D = column:row` and then does `STD OX16,X`, `EXG A,B`,
`STD OY16,X`. So at birth:

```
ox16 = (screenByteColumn << 8) | screenRow
oy16 = (screenRow        << 8) | screenByteColumn
```

The low bytes are not zero and not garbage — they are the *other* coordinate, and they seed the
sub-column/sub-row accumulation of the shell's first frames. A bit-exact replay reproduces them.

Confirmed alongside: `BMBCNT` hard cap **20** (`CMPA #20 / BHS STSHX`), off-screen refusal
`(ox16 − bgl) >= 150*64`, row refusal `row <= YMIN`, free-list refusal, lifetime
`ODATA = 20` decremented once per `SHSCAN` (every 8 frames) = **160 frames**.

---

### C-16 — Bonus screen is not silent · MEDIUM

`AUDIO_ENGINE_SPEC.md` assumes "the between-wave period is silent apart from any bonus award".
`WAVES_DIFFICULTY_SPEC.md` §7.3 calls `SCORE` **once per surviving humanoid** (up to 10), and
`SCORE` runs `checkReplay` on **every** call (`defa7.src:510-535`). So the bonus screen can emit
several `RPSND` events, one per threshold crossed, and `SCRFLG`/`SCRTRN` fire ten times.

**Ruling.** The bonus screen emits: zero or more `RPSND` (`$1E`, priority `$FF`), and nothing else.
The `SCORE` event stream carries ten `score` events with `source: 'BONUS'`. Both specs stay; the
audio corpus scenario "wave-clear bonus" must contain the `RPSND` case.

---

### C-17 — The smart-bomb flash inverts one palette slot, not the screen · LOW

`SBOMB`'s flash is `COM PCRAM` (`defa7.src:3197`) — a bitwise complement of **palette slot 0**,
repeated 4 times with `NAP 2` between = **8 ticks**. `VISUAL_ENHANCEMENT_SPEC.md` effect 16 uses
8 ticks and is consistent. Neither the framebuffer nor slots 1..15 are touched. Record it as a
palette event, not a screen event, so the photosensitivity instrument measures the right area.

---

### C-18 — `PLAXV` is clamped to ±`$0100`, and the clamp writes back · LOW

`defa7.src:2426-2432` clamps `PLAXV` to ±`$0100` **and stores the clamped value back** before using
it to advance `BGL`. `MOVEMENT_PHYSICS_SPEC.md` §3.1 correctly calls this "the one that never
fires" — drag holds `V16 ≤ 192`. Contract keeps the clamp, keeps the write-back, and asserts the
branch is never taken (`MOV-03`).

---

### C-19 — `STINIT`'s `rand()` count is bounded, and the bound is now known · LOW

Carried open item from `CONTROL_SYSTEM_SPEC.md`. `defa7.src:2073-2091`: 16 stars, two rejection
loops each.

```
STI0:  accept  a < 0x9C            → p = 156/256 = 0.6094, mean 1.641 calls
STI1:  accept  42 < a <= 0xA8      → p = 126/256 = 0.4922, mean 2.032 calls
```

Expected total ≈ **58.8 `rand()` calls per `STINIT`**, minimum 32, unbounded above with
probability → 0. `STINIT` also sets `STRCNT = 16`. The interim ruling (measure, do not assert a
literal) stands, but a Phase 3 test may now assert `32 ≤ calls` and `mean ∈ [55, 63]` over 1000
seeds.

---

### C-20 — HUD display caps confirmed · LOW

`LDSP` caps the ship icons at **5** (`defa7.src:844-846`); `SBDSP` caps the smart-bomb icons at
**3** (`defa7.src:888-890`). Ships advance by `ADDA #$06` (six byte-columns, rightward); bombs
advance by `ADDB #4` (four **scanlines**, downward). Both stocks themselves are uncapped. Matches
`RENDERING_SPEC.md` §11.3/§11.4 and `HUD_RADAR_SPEC.md` §4.3/§4.4 exactly. No change.

---

### C-21 — Five documents use the test prefix `T-`, two use `E-` · **blocks the test suite** · HIGH

Undeclared and purely mechanical, but it stops Phase 3 dead: `AUDIO_ENGINE_SPEC`,
`HUD_RADAR_SPEC`, `SHELL_AND_LIFECYCLE_SPEC`, `INTERACTION_FEEDBACK_SPEC` and
`ACCESSIBILITY_SPEC` all number their tests `T-01…`; `ENEMY_BEHAVIOR_SPEC` and
`VISUAL_ENHANCEMENT_SPEC` both use `E-nn`; six documents use `D-nn` for unrelated deviations.
`T-39` currently denotes five different tests.

**Ruling.** `docs/qa/ACCEPTANCE_TESTS.md` assigns a globally unique three-letter prefix per
subsystem and preserves each origin number, so the crosswalk is `AUD-12 ← T-12 (audio)`,
`HUD-12 ← T-12 (HUD)`, and so on. Deviation ids are re-issued as `D-<AREA>-nn`. Origin documents
keep their local numbering; the global id is the one the test file uses.

---

### C-22 — Two-player: three specs assume it, two do not · MEDIUM

`HUD_RADAR_SPEC` and `ENEMY_BEHAVIOR_SPEC` describe per-player `pwav / ptarg / penemy / score`
and `PLSAV`/`PLRES` world-swapping; `CONTROL_SYSTEM_SPEC` and `RENDERING_SPEC` write
single-player and flag two-player HUD positions as unverified.

**Ruling.** Classic Mode Phase 3 ships **single-player**. The state structure carries the
per-player block as a length-2 array with `curPlr` fixed at 0, so enabling two-player later is a
data and lifecycle change, not a refactor. Recorded as `D-SCOPE-1`. `L-125` / `OPEN_QUESTIONS`
Q-06 stays open.

---

### C-23 — `GTARG` reads 12 slots past the end of `TLIST` · confirmed layout · LOW

`ENEMY_BEHAVIOR_SPEC`'s open item is confirmed by the RAM map: `phr6.src:421-428` places
`TLIST RMB 40` (20 pointers) immediately before `FISTAB RMB 32`. `GTARG` walks 32 slots = 64 bytes,
so slots 20–31 read `FISTAB[0..23]` — non-zero exactly while a laser is in flight. The interim
model (slots 20–31 read as permanent zero) is retained and the MAME watchpoint stays open.

---

### C-24 — The two draw bands are exactly complementary; there is **no** overlap · **corrected in iteration 4** · HIGH

**Superseded claim (this document, iterations 1–3, and `RENDERING_SPEC` §5.3).** "`XXX2 = 120` and
the two bands tile rows 1..255 with an **overlap at 113–120**; objects in the overlap are erased and
redrawn twice per frame."

**That is wrong, and the root cause is a source misread that was never taken back to the ROM.**
`RENDERING_SPEC` §5.3 read `LDD #$FF70 / STD XXX1` (`defa7.src:1009`, ROM `$D78C
CC FF 70 DD A1`) as seeding one 16-bit variable `XXX1 = $FF70`, and then took `$70` = 112 as band
A's lower bound. `phr6.src:285-287` declares three **adjacent one-byte** variables:

```
XXX1    RMB    1                ;SCREEN OUTPUT PARAMS
XXX2    RMB    1
XXX3    RMB    1
```

so `STD <XXX1` writes `XXX1 = $FF` **and** `XXX2 = $70`, and the following `CLR <XXX3`
(ROM `0F A3`) sets `XXX3 = 0`. `$70` is merely `XXX2`'s value before the first IRQ ever runs.
`LDD XXX1` loads the pair `(XXX1, XXX2)`; `LDD XXX2` loads the pair `(XXX2, XXX3)`. **`XXX2` is a
single shared boundary: band A's lower bound and band C's upper bound are the same byte.**

**Evidence (ROM, redlabel, authoritative).** `$DF3C`:

```
B6 C8 00   LDA VERTCT      ; recomputed EVERY frame, in the scanline-128 IRQ
80 08      SUBA #8
81 A8      CMPA #$A8
23 02      BLS  IRQ1
86 A8      LDA #$A8
97 A2      STA <XXX2       ; $A2 = XXX2
```

`$DF4E` (phase C): `DC A2` `LDD <XXX2` → `(XXX2, XXX3)`, `JSR OPROC`, `DC A2`, `JSR PRDISP`.
`$DF9F` (phase A): `DC A1` `LDD <XXX1` → `(XXX1, XXX2)`, `JSR PRDISP`, `DC A1`, `JSR OPROC`,
`JSR VELO`. Source: `defa7.src:1009, 1951-1957, 1990-1994`.

Both band tests are **half-open**, so they tile without overlap for *any* value of `XXX2`:

| Routine | Stack / registers | Accepts | Band A (`A=$FF, B=XXX2`) | Band C (`A=XXX2, B=$00`) |
|---|---|---|---|---|
| `OPROC` (`defa7.src:2503-2518`) | `PSHS D`; `CMPB ,S / BHI skip`, `CMPB 1,S / BLS skip` | `1,S < row ≤ ,S` | `(XXX2, 255]` | `(0, XXX2]` |
| `PRDISP` (`defa7.src:2294-2300`) | `A→TEMP48`; `CMPA PLAYC / BLS ret`, `CMPB PLAYC / BHI ret` | `B ≤ playc < A` | `[XXX2, 255)` | `[0, XXX2)` |

**Ruling.**

> `xxx1 = 0xFF` and `xxx3 = 0x00` are constants after `INIT`. `xxx2` is **recomputed every frame**
> at the head of phase C as `xxx2 = min(vertct − 8, 0xA8)`, which under the core's normal
> (non-overrunning) timing is **120**. The bands are complementary: no row is drawn twice, and no
> row is skipped.
>
> - `OPROC`: band A = rows `121..255`, band C = rows `1..120`.
> - `PRDISP`: band A = rows `120..254`, band C = rows `0..119`.

The one-byte disagreement at row 120 between `OPROC` and `PRDISP` is real and is reproduced
verbatim: the player at row 120 is committed in the **A** pass, an object at row 120 in the **C**
pass. That is the boundary artifact C-25 reasons about, and it survives this correction intact.

`xxx2` is core state (see §3.16) because it is a per-frame value the Lab may want to force in order
to reproduce a late-serviced frame.

**Fix:** `RENDERING_SPEC.md` §5.3 lines 466–493, its D-1 deviation row and its `L-029`/`L-052`
note; delete `ACCEPTANCE_TESTS.md` `INT-06`; rewrite `INT-30` to assert the union of the two bands
is exactly rows 1..255 with **no gap and no overlap**. `EVIDENCE_LEDGER` `L-029`/`L-052` gain a
correction note.

---

### C-25 — The two banded object passes are **not** cosmetic · **controls and movement are wrong** · HIGH

**Claim (movement).** `MOVEMENT_PHYSICS_SPEC.md` §10, listed as a deviation: *"One object
screen-position pass per step instead of the original's two raster-banded passes … Cosmetic;
flagged to the renderer agent."* `CONTROL_SYSTEM_SPEC.md` §13.1 says the same, and its §2.2 step A3
recomputes **every** active object's screen position rather than band A's.

**Counter-claim (rendering).** `RENDERING_SPEC.md` §5.2/§5.3 keeps both passes and states that
objects at rows 113–120 are erased and redrawn twice.

**Evidence.** `OPROC` (`defa7.src:2503-2545`) does not take a precomputed position — it
recomputes one, from state that has changed between the two calls:

```
OPLP0   LDD    OBJX,X           ; ERASE using the OLD row against the band
        CMPB   ,S / CMPB 1,S
        ... JSR [OBJDEL,Y] ; CLRD ; STD OBJX,X
OPON    LDB    OY16,X           ; DRAW using the CURRENT row against the band
        CMPB   ,S / CMPB 1,S
        LDD    OX16,X
        SUBD   BGL              ; <-- recomputed from live OX16 and live BGL
        CMPD   #150*64
        ... ASLB/ROLA ×2 ; over-width guard CMPA #$9C ; ASLB "SAVE PHASE"
        LDB    OY16,X
        STD    OBJX,X
```

The A-pass call (`A6`) runs **before `VELO`** (`A7`) and before `PLAYER` advances `BGL` (`C2`).
The C-pass call (`C4`) runs after both. Therefore:

| Screen rows | Drawn at | Position used |
|---|---|---|
| 121–255 (band A) | `A6` | **previous frame's** post-`VELO` `ox16`/`oy16` and previous frame's `bgl` |
| 1–120 (band C) | `C4` | **this frame's** post-`VELO` `ox16`/`oy16` and this frame's `bgl` |

> **Everything below screen row 120 is drawn — and therefore collides — one frame later than
> everything above it.** `OBJX` is what `COLCHK` reads at B2 of the next frame (L-049), so this is
> not a rendering detail: the bottom two-thirds of the playfield has one extra frame of collision
> lag, in both object motion and scroll. On a horizontally scrolling game at 6 px/frame that is up
> to 6 px of hitbox displacement, and it differs between the top and the bottom of the screen.

**Ruling.** Both passes are kept, banded, in the order of §2. The bands are **complementary**, not
overlapping — see C-24 as corrected: `OPROC` band A = rows 121..255, band C = rows 1..120;
`PRDISP` band A = rows 120..254, band C = rows 0..119; the boundary byte is `xxx2 = 120`. The
erase decision uses the **old** `objY`; the draw decision uses the **current** `oy16 >> 8`.
Rendering was right that both passes must be kept; the "one pass" simplification is withdrawn and
is **not** a permissible deviation.

**Fix:** delete the deviation row from `MOVEMENT_PHYSICS_SPEC.md` §10 and the corresponding row
from `CONTROL_SYSTEM_SPEC.md` §13.1; change `CONTROL_SYSTEM_SPEC.md` §2.2 step A3 from "every
active object" to "band A" and §2.2 step C4 to band C; change `CONTROL_SYSTEM_SPEC.md` §13.2's
"the renderer may draw everything in one pass" to the two-pass rule; change
`ENEMY_BEHAVIOR_SPEC.md` §0.3 step 10 from a single object-draw step to the banded pair. Guarded by
`INT-33`. **Applied in iteration 4.**

---

## 1B. CONTRADICTIONS FOUND IN ITERATION 4

Three adversarial reviews of the Phase 2 corpus found eight more. Six are the corpus's own diagnosed
failure mode — a fact correct in one document and wrong in a sibling — occurring one level up: §1
adjudicated correctly and then **the sibling edits were never made**. That is fixed in this commit
(see §11A) and C-33 records the process failure itself.

---

### C-26 — The overload governor fires 32 times on every planet destruction · **this document was wrong** · HIGH

**Claim (this document, §2.3 and §10, iterations 1–3).** "In a browser nothing overruns, so `ovcnt`
stays 0 and the governor never fires"; `ACCEPTANCE_TESTS` `INT-31` accordingly asserted
`ovcnt === 0` on every tick of a 30 000-tick replay and `strcnt === 16`.

**Counter-claim (planet).** `PLANET_STATE_SPEC.md` §2.4: *"This is the one place in the
reconstruction where the overload governor must actually fire."* `PLA-15` asserts `ovcnt == 8` is
set exactly 32 times, `strcnt == 3` throughout, and regrowth to 16 afterwards.

**Evidence.** ROM `$EE3D`: `C6 08` `LDB #8` / `D7 5E` `STB <OVCNT` / `7E D0 01` `JMP SLEEP`.
`rom_peek find "C6 08 D7 5E"` returns **one** match in the whole set — this is the only non-`EXEC`
writer of `OVCNT` in the ROM. Source `defb6.src:476-478` (`LDB #8 / STB OVCNT ;PHONY OVERLOAD`),
reached twice per `TERBLO` iteration via `TBL2` and `TBL3`/`TBL4` (`defb6.src:476-491`) across 16
iterations. Governor: `defa7.src:3050-3090`.

**Ruling.** The governor is inert **with respect to CPU load** and driven **by game code**. It has
two writers: `EXEC`'s own accumulator (which decays to 0 here) and `TERBLO`. Planet destruction
forces `ovcnt = 8` thirty-two times over ≈1.13 s, each of which drives the next `B1` to `a = 6`,
cutting `strcnt` to 3 and culling one `otyp === 0` object per `EXEC` pass. Planet is right; this
document was wrong. §2.3 rewritten; §10's row rewritten.

**Fix:** `ACCEPTANCE_TESTS.md` `INT-31` — first clause restricted to replays containing **no**
`TERBLO`; second clause added asserting the 32 forced firings, the `strcnt = 3` cut, the per-pass
cull, and the exact `ox16` delta for a forced `(seed, hseed)` pair (C-31).

---

### C-27 — `rand()` returns the new `SEED`, not the shift intermediate · **this document and enemies were wrong** · HIGH

**Claim (this document §6, `ENEMY_BEHAVIOR_SPEC` §0.4).** `return a`, where
`a = (((lseed >> 3) ^ lseed) & 0xFF) >> 1`.
**Counter-claim (`CONTROL_SYSTEM_SPEC` §9.2).** "the return value **is** the new seed."

**Evidence.** ROM `$D711` (`defend.1` @ `0x0710`), verified with `rom_peek addr D710 32`, ends
`… DB E1  D9 E0  D7 DF  96 DF  35 84` — `ADDB <LSEED / ADCB <HSEED / STB <SEED / **LDA <SEED** /
PULS B,PC`. `A` is reloaded from `SEED` immediately before the return, clobbering the intermediate.
`defa7.src:945-962` agrees. `RMAX` (`defa7.src:1733-1743`) consumes exactly that value.

**Ruling.** Controls is right. §6 rewritten to `return seed`. `ENEMY_BEHAVIOR_SPEC` §0.4's carry
term is separately wrong — it re-reads the already-updated `b` — and is corrected to
`const s1 = b + lseed; b = (s1 + hseed + (s1 > 0xFF ? 1 : 0)) & 0xFF`.

**Blast radius, stated plainly.** `rmax()` drives Lander spawn speed, every enemy shot interval,
Pod burst size, the Baiter reload and `TERBLO`'s pacing. **Every seeded golden vector in the corpus
must be regenerated from the corrected generator before it is trusted.** Until that regeneration
runs, the affected expected-value tables are marked provisional — see §11A and the carried risk in
`PHASE2_GATE.md`. A claims-file assertion pins the first 16 `rand()` returns from
`(0x00, 0xA5, 0x5A)`.

---

### C-28 — Same-tick sound ordering: bypass first, sequencer second · **the audio spec is wrong** · MEDIUM

`AUDIO_ENGINE_SPEC` §12 E-1: "sequencer first (it runs at the top of `step()`), then the bypass …
the second immediately aborts the first." Under C-01 that is inverted. The three `sndOutDirect`
sites (`defa7.src:1384-1386`, `1428-1430`, `defb6.src:820`) fire from `PLEND` / game-over,
dispatched at **B6**; `SNDSEQ` runs at **C1**. So the bypass is first and the sequencer's command is
the one that survives on a collision tick — the *opposite* sound is audible. §5.3 of this document
now carries `phase` per source instead of hardcoding `'C'`. `INT-34` asserts the ordering.

---

### C-29 — One input word, and it is the two PIA bytes · **this document was wrong** · HIGH

§7 defined `rawInput` as seven booleans with no bit order and no `START 1`; §8 then said
"bit order = §7", a dangling reference; `INPUT_MAPPING` §1 defines the module's output as two uint8
bitfields `{pia2, pia3}` with named bit positions, `MULTI_SOURCE_CONFIRMED` against
`phr6.src:120-146` and `williams.cpp:762-786`. Three consequences a builder hits immediately: the
replay packing is unspecified so no two implementations interchange replays; a replay cannot contain
the keypress that starts the game; and §7's own `SWTAB` prose ("`FIRE` (bit 0) before `REVERSE`
(bit 6)") refers to bits that exist only in `INPUT_MAPPING`'s byte.

**Ruling.** `INPUT_MAPPING` §1 wins and is adopted verbatim. §7 rewritten. `inputSequence` becomes a
`Uint16Array` packed `pia2 | (pia3 << 8)`. `INT-35` round-trips a sequence containing a `START 1`
press.

---

### C-30 — The replay tuple has four spellings; this is the one · **`ARCHITECTURE.md` is amended** · HIGH

`ARCHITECTURE.md` §6 (BINDING) says `{seed, inputSequence, tickCount}` "and nothing else"; §8 here
says `{seed, cmos, inputSequence, tickCount}`; `INPUT_MAPPING` §5.7 says `{rngState, inputSequence,
tickCount}` and attributes it to ARCHITECTURE; `AUDIO_ENGINE_SPEC` §13.1's corpus schema declares a
**scalar** `"seed": 12345`, a value the machine's three-byte generator cannot hold.

**Ruling.** `{seed, cmos, inputSequence, tickCount}`, where `seed` is `{seed, hseed, lseed}`.
`ARCHITECTURE.md` §6 is **amended** — the first amendment to a document declared BINDING, recorded
here and in `GAUNTLET_LOG.md` — and the three derived citations are corrected. §11's checklist now
names `ARCHITECTURE.md`.

---

### C-31 — The governor's cull displacement is 16-bit, and it erases first · **this document was wrong** · HIGH

§2.3 specified `ox16 += (seed & 0x3F) + 0x60` and omitted `JSR OFSHIT`.
`PLANET_STATE_SPEC` §2.4 independently said "an enemy is thrown 96..159 px right".

**Evidence.** ROM `$E802`: `DC DF` `LDD <SEED` (SEED `$A0DF` and HSEED `$A0E0` are adjacent,
`phr6.src:355-357`) / `84 3F` `ANDA #$3F` / `8B 60` `ADDA #$60` / `E3 0A` `ADDD OX16,X` /
`ED 0A` `STD OX16,X` / `BD F3 FE` `JSR OFSHIT` / `CC 00 00` / `ED 04` `STD OBJX,X`. Source
`defa7.src:3073-3082`; `OFSHIT` at `defb6.src:1155-1160` = `LDD OBJX,X / LDY OPICT,X /
JSR [OBJDEL,Y]` — the sprite erase.

**Ruling.** The addend is `(((seed & 0x3F) + 0x60) << 8) | hseed` = `0x6000..0x9FFF` =
**768..1280 px** at 32 units/px (`OPROC`'s on-screen test is `CMPD #150*64`, so 64 units per byte
column and 32 per pixel). `OFSHIT` runs **before** `objX` is zeroed. This document was 256× low;
`PLANET_STATE_SPEC` was 8× low. Both corrected. `INT-31` gains the exact expected delta for a
forced `(seed, hseed)`.

---

### C-32 — The "not on screen" sentinel is the 16-bit `OBJX:OBJY` pair · **controls and waves are wrong** · HIGH

`CONTROL_SYSTEM_SPEC` §6.3 (`if obj.objx == 0: continue`), §6.5 ("the `OBJX == 0` test means…"),
`WAVES_DIFFICULTY_SPEC` §7.6 ("`objX != 0`") and `MOVEMENT_PHYSICS_SPEC` §6.3's prose all test the
byte. `MOVEMENT_PHYSICS_SPEC` §7.2, `ENEMY_BEHAVIOR_SPEC` §4.3 and `HUMANOID_RESCUE_SPEC` §5 test
the pair. A 3-vs-3 split on a predicate C-11 declares a class invariant.

**Evidence.** `OPROC` clears it as a word: `CLRD / STD OBJX,X` (`defa7.src:2517`).
`SBOMB` tests it as a word: ROM `$E8DC` `EC 04` `LDD OBJX,X` / `27 0C` `BEQ`
(`defa7.src` `SBMB0`). `COLIDE` likewise: `COL1 LDD OBJX,X / BEQ COLLP` (`defa7.src:2907`).
Reachability: `OPROC` computes `col = (ox16 − bgl) >> 6`, so a `dx` in `[0,63]` yields byte-column
0 with the object still on the active list — `OBJX = 0` but `OBJY = row ∈ 42..240`, so `D ≠ 0`.

**Ruling.** The predicate is `!(objX === 0 && objY === 0)`. C-11's smart-bomb invariant becomes
`(objX !== 0 || objY !== 0) && otyp < 2`. Testing the byte alone creates a 2-px-wide strip at
framebuffer column 0 in which enemies are immune to the smart bomb and to collision. `CTL-57`
places an enemy at `ox16 === bgl`, row 100, detonates a smart bomb and asserts it dies.

---

### C-33 — §1's remediations were adjudicated and not applied · **process failure** · HIGH

Every "Fix:" directive in §1 of iterations 1–3 was still unapplied at the start of iteration 4:
`AUDIO_ENGINE_SPEC.md:114` still carried the C-01 losing text; `RENDERING_SPEC.md:1212,1228` the
C-03 losing text; `MOVEMENT_PHYSICS_SPEC.md:1077` and `CONTROL_SYSTEM_SPEC.md:1456,1464` the C-25
withdrawn deviation; `ENEMY_BEHAVIOR_SPEC` §0.3 step 10 the same single-pass error, which §1 never
even named. `ACCEPTANCE_TESTS.md`'s `[SUPERSEDED → C-nn]` mechanism was declared once and applied
**zero** times, so §3 shipped overturned expected values while §4, 400 lines later, said they were
overturned. The commit that created §11's change-control rule violated it.

**Ruling.** Adjudication is not remediation. §11A now records, per iteration, the applied-edit
manifest with a `grep`-checkable assertion for each, and `tools/check_supersessions.py` fails CI if
any §1/§1B "Fix:" names a file:line whose losing text is still present, or if any
`ACCEPTANCE_TESTS` §4 category has zero corresponding §3 edits. **A builder must never be able to
read the losing text without seeing that it lost.**

---

## 2. THE FRAME ORDER — NORMATIVE

This is the contract's single most important section. It is the 6809's execution order between two
consecutive `INC TIMER` events, flattened into one single-threaded function. It is derived from
`defa7.src:1931-1990` (the IRQ handler, both paths) and `defa7.src:3046-3130` (`EXEC` → `SWP` →
`DISP`). `SOURCE_CONFIRMED`, corroborated `MULTI_SOURCE_CONFIRMED` by `williams_m.cpp:19-29` for
the IRQ scanlines (L-005).

```
                    scanline 0 IRQ            main loop                scanline 128 IRQ
   frame f  ├──────── PHASE A ────────┼─────── PHASE B ───────┼──────── PHASE C ────────┤
```

```js
step(rawInput) {

  // ─────────── PHASE A — top-of-frame draw pass + world integration ───────────
  A1  tick += 1                                     // INC TIMER, the executive's gate
  A2  if (vertctAtService <= 8) {                   // CMPA #8 / BHI I01, defa7:1966-1968
        cram = pcram.slice(); rebuildPaletteLUT()   // atomic 16-entry upload, defa7:1969-1982
      }                                             // see §2.3.1 — always taken in this core
  A3  coinScan()                                    // CSCAN — no simulation effect
  A4  if ((status & 0x02) === 0) bgout()            // terrain scroll + redraw; WRITES bglx
  A5  prdisp(xxx1, xxx2)                            // player: band A, playc in [xxx2, 0xFF)
  A6  oproc(xxx1, xxx2)                             // objects: band A, rows (xxx2, 0xFF]
                                                    //   uses PRE-VELO ox16/oy16 and PRE-C2 bgl
  A7  velo()                                        // integrate every ACTIVE object

  // ─────────── PHASE B — the executive pass ───────────
  B1  overloadGovernor()                            // OVCNT; see §2.3
  B2  colchk()                                      // player vs OPTR, then player vs SPTR
  B3  xuvct()                                       // EXPU: every explosion/appear slot
  B4  rand()                                        // EXACTLY ONE unconditional call
  B5  drainSwitchQueue()                            // slot 0 then slot 1, each → makeProcess()
  B6  disp()                                        // walk the process list; ALL AI runs here

  // ─────────── PHASE C — input, player physics, second draw pass ───────────
  C0  xxx2 = Math.min(vertctAtService - 8, 0xA8)    // recomputed EVERY frame; 120 here (§2.3.1)
  C1  soundSequencerStep(rawInput)                  // SNDSEQ; contains sscan() edge detect
  C2  player()                                      // physics; WRITES bglx, then advances bgl
  C3  stout()                                       // stars: erase all, then draw
  C4  oproc(xxx2, xxx3)                             // objects: band C, rows (0, xxx2]
  C5  prdisp(xxx2, xxx3) → commitPlayerDraw()       // player: band C, playc in [0, xxx2)
  C6  shell()                                       // shells and mines move, test, draw
}
```

**Band arguments are a byte PAIR, not a 16-bit bound.** `LDD XXX1` loads `(xxx1, xxx2)`;
`LDD XXX2` loads `(xxx2, xxx3)`. `xxx1 = 0xFF`, `xxx3 = 0x00`, and `xxx2` is the single shared
boundary. Both band tests are half-open, so the two passes tile rows 1..255 exactly once. C-24.

```js
// The two predicates, verbatim (defa7.src:2508-2518 and 2294-2300):
oproc (hi, lo) → draw/erase object when  lo <  (oy16 >> 8) && (oy16 >> 8) <= hi
prdisp(hi, lo) → draw/commit player when lo <= playc      && playc       <  hi
```

### 2.1 The order is not negotiable — four behaviors depend on it

1. **Enemy AI (B6) reads the player position from before `PLAYER` (C2).** Every enemy aims at where
   the player was at the end of the previous frame. `E-*`, `ENEMY_BEHAVIOR_SPEC.md` §0.3.
2. **`VELO` (A7) runs before the AI that writes the velocity (B6).** A velocity written this frame
   first moves the object next frame.
3. **`COLCHK` (B2) uses `plaxc`/`playc` — the position as last *drawn*, at C5 of the previous
   frame — not the position `PLAYER` computes at C2 of this frame.** L-049. This is what makes the
   player's hitbox lag by one frame.
4. **`SNDSEQ` (C1) runs after all game logic (B6) and before `PLAYER` (C2).** C-01.
5. **`OPROC` runs twice, on complementary bands, and the two calls see different world state.**
   Objects at rows 121..255 are drawn at A6 from the *previous* frame's position and scroll;
   objects at rows 1..120 at C4 from *this* frame's. `OBJX` carries that asymmetry into the next
   frame's `COLCHK`. No object is drawn twice and none is skipped. C-24, C-25.

### 2.2 `commitPlayerDraw()` — one commit, and why that is exact

The original commits `PLADIR ← NPLAD` and `PLAXC:PLAYC ← NPLAXC:NPLAYC` inside `PRDISP`
(`defa7.src:2294-2331`), which runs twice per frame on disjoint bands. A single commit at C5 is
exactly equivalent because `NPLAD` is written only in phase B (`REV`, `HYPER`) and `NPLAXC`/
`NPLAYC` only by `PLAYER` (C2) and by `HYPER` (phase B, with bit 4 set, which suppresses the commit
entirely). `CONTROL_SYSTEM_SPEC.md` §2.4 proves it; ratified.

```js
commitPlayerDraw() {
  if (status & 0x10) return;      // player image + collision inactive
  pladir = nplad;
  plaxc  = nplaxc;
  playc  = nplayc;
}
```

### 2.3 The overload governor

`defa7.src:3050-3090`, ROM `$E7C7`–`$E815`. Kept, implemented, and **live**.

```js
B1: if (status & 0x7D) { ovcnt = 0; }                // BITB #$7D / BEQ  (ROM $E7CB C5 7D 27 04)
    else {
      let a = ((timerAtEntry << 1) + ovcnt - 4) & 0xFF;   // ASLA / ADDA OVCNT / SUBA #4
      if (a & 0x80) a = 0;                           // BPL EXEC01 / CLRA — a SIGN test, not a<0
      ovcnt = a;
      if (a >= 2) {                                  // CMPA #2 / BLO
        strcnt = 3;                                  // CUT DOWN STARS
        if (a > 2) {                                 // CMPA #2 / BLS
          ovcnt = 2;                                 // hard cap
          cullOneObject();                           // "OVERLOAD WIPE OUT A GUY"
        }
      }
    }

function cullOneObject() {
  // walk OPTR for the FIRST record with otyp === 0 ("DONT TOUCH THIS GUY" skips otyp !== 0);
  // if none, do nothing.
  //   ROM $E802:  DC DF     LDD <SEED     ; SEED=$A0DF, HSEED=$A0E0 are ADJACENT (phr6.src:355-357)
  //               84 3F     ANDA #$3F     ; masks the HIGH byte
  //               8B 60     ADDA #$60
  //               E3 0A     ADDD OX16,X   ; SIXTEEN-BIT add
  //               ED 0A     STD  OX16,X   ; "HYPER HIM OUT OF THERE"
  //               BD F3 FE  JSR  OFSHIT   ; ERASE the sprite from fb BEFORE objX is zeroed
  //               CC 00 00 / ED 04        ; STD OBJX,X
  const d  = ((((seed & 0x3F) + 0x60) & 0xFF) << 8) | hseed;   // 0x6000..0x9FFF
  obj.ox16 = (obj.ox16 + d) & 0xFFFF;                          // 24576..40959 units = 768..1280 px
  ofshit(obj);            // LDD OBJX,X / LDY OPICT,X / JSR [OBJDEL,Y]  (defb6.src:1155-1160)
  obj.objX = 0; obj.objY = 0;
  // unlink from OPTR, push onto the head of IPTR
}
```

Three details that were previously wrong here and are corrected in iteration 4:

1. **The displacement is 16-bit.** `LDD SEED` loads the *pair* `SEED:HSEED`; `ANDA`/`ADDA` operate
   on the high byte; `ADDD` adds the whole word. The addend is `0x6000..0x9FFF` world units =
   **768..1280 px**, a third to two thirds of the way around the 2048-px world — which is what
   "HYPER HIM OUT OF THERE" means. The earlier `ox16 += (seed & 0x3F) + 0x60` was 256× too small.
2. **`OFSHIT` runs before `OBJX` is zeroed** and is the sprite *erase*, not a sound. Omitting it
   burns the victim's image permanently into `fb`, which `INT-17`'s framebuffer hashes would then
   bake in as expected output.
3. **The `BPL` test is a bit-7 sign test on the 8-bit result**, not a JavaScript `a < 0`.

**The governor is not inert.** It has no CPU-load input in this core — `timerAtEntry` is always 1,
so `ovcnt` decays to 0 and stays there *of its own accord*. But `ovcnt` has a **second writer**, and
that writer is game code, not the scheduler:

```
TERBLO (defb6.src:476-478, ROM $EE3D  C6 08  D7 5E  7E D0 01)
        LDB    #8
        STB    OVCNT            ;PHONY OVERLOAD
        JMP    SLEEP
```

`STB OVCNT` at `$EE3F` is the only non-`EXEC` write to `OVCNT` in the entire ROM set (one match).
`TERBLO` reaches it **twice per iteration across 16 iterations = 32 times, ≈1.13 s**, on every
planet destruction (`PLANET_STATE_SPEC.md` §2.3/§2.4). Each forced `ovcnt = 8` makes the next `B1`
compute `a = 2 + 8 − 4 = 6 > 2`, so `strcnt` drops to 3 and **one `otyp === 0` object is culled per
`EXEC` pass** for the duration. This is a deliberate, visible, load-bearing effect: during the
planet's destruction the starfield thins and the screen is progressively swept of enemies.

**Implement it, expose `ovcnt` and `strcnt` on the snapshot, allow the Lab overlay to force
`ovcnt`.** Do not delete it. `D-CORE-2` covers only the missing CPU-load *input*, not the routine.

### 2.3.1 Two gates that are always taken in this core, and are still written down

| Gate | Original | Here | Deviation |
|---|---|---|---|
| Palette upload at A2 | `CMPA #8 / BHI I01` (`defa7.src:1966-1968`); a late-serviced scanline-0 IRQ **skips** the 16-byte `PCRAM → CRAM` transfer and the frame shows the previous palette. Cocktail path gates at `CMPA #4` (`defa7.src:2043-2044`). | `vertctAtService` is fixed at 0, so the gate is always satisfied and the upload always happens. | `D-CORE-3` |
| `xxx2` recompute at C0 | `xxx2 = min(VERTCT − 8, 0xA8)`; on a heavily loaded frame `VERTCT` has advanced past 128 by the time the IRQ is serviced and the band boundary moves **down**, shrinking band C. | `vertctAtService` is fixed at 128, so `xxx2 = 120` on every frame. | `D-CORE-3` |

Both are written as gates rather than constant-folded, for the same reason §2.3 keeps the governor:
the core has no cycle model *today*, and a Lab timing-stress feature must have something to hook.
`vertctAtService` is a core state field (§3.16) with the fixed values above; the Lab may force it.

### 2.4 Sub-orderings that must not be collapsed

| Within | Order | Source |
|---|---|---|
| `A5`/`A6` vs `C4`/`C5` | band A does `PRDISP` **then** `OPROC`; band C does `OPROC` **then** `PRDISP` | `defa7.src:1985-1988` vs `1951-1955` |
| `B2` | player vs `OPTR` first; **then** player vs `SPTR`, and the second runs even if the first unwound | `defa7.src:3136-3149`, C-10 |
| `B5` | `SWPROC[0]` before `SWPROC[1]`; both `makeProcess` at the head, so **slot 1 dispatches first** | `defa7.src:3103-3117`, C-05 |
| `B6` | list order, front to back, `ptime--` on every live record, run those hitting 0 | `defa7.src:3119-3128` |
| `C6` | shells only; `SHELL` is skipped entirely when `status & 0x20` | `defa7.src:2609-2611` |

### 2.5 Process cadences, all in frames

| Process | Cadence | Source |
|---|---|---|
| `GEXEC` (wave manager) | `NAP 15` | `defa7.src:1642+`, L-032 |
| `SCPROC` | `ISCAN` → `NAP 2` → `OSCAN`+`SHSCAN` → `NAP 2` → `MAPCH1`+`SCNR` → `NAP 4` (8-frame cycle) | `defa7.src:3296-3307` |
| `COLR` (laser palette) | `SLEEP 2`, 37-entry `COLTAB` | `defa7.src:3010-3022` |
| `CBOMB`, `TIECOL` | every 6 frames | `RENDERING_SPEC.md` §4.2 |
| `THPROC` (thrust flame) | `NAP 4` | `defa7.src:3277-3292` |
| Lander hunt `LANDS0` | every 6 frames | `ENEMY_BEHAVIOR_SPEC.md` §3.3 |
| Mutant `SCZ0` | every 3 frames | §4.3 |
| Baiter `UFOLP` | every 6 frames | §8.2 |
| `SBOMB` flash | 4 × `NAP 2` = 8 frames, then `NAP 10` / release poll / `NAP 10` | `defa7.src:3196-3208` |
| Hyperspace | `NAP 15` then `NAP 40` | `defa7.src:3220, 3271` |
| Start delay | `SLEEP 128` → `PLS01` (`status = $05`) → `SLEEP 96` → `PLS1` | `defa7.src:1300-1313` |

---

## 3. CORE STATE — the complete structure

Integer-only. Every field is at its original fixed-point scale and masked back to width after every
operation. `Number` is the storage type; no float ever reaches the simulation.

```js
const coreState = {

  // ── 3.1 Clock ────────────────────────────────────────────────────────────
  tick:      0,          // uint32, incremented at A1. 1 tick = 16.640 ms exactly.

  // ── 3.2 RNG — one generator, see §6 ──────────────────────────────────────
  seed: 0x00, hseed: 0xA5, lseed: 0x5A,   // uint8 each; power-up values, never re-seeded
  randCalls: 0,                            // instrumentation only, not simulation

  // ── 3.3 Master gate ──────────────────────────────────────────────────────
  status:    0x00,       // uint8, see §3.12
  scrflg:    0,          // uint8, ROL'd by every SCORE call
  pcflg:     0,          // uint8, set around COLIDE from COLCHK only

  // ── 3.4 Player ───────────────────────────────────────────────────────────
  plax16:    0x2000,     // uint16  screen X, hi = byte column, lo = fraction
  play16:    0x8000,     // uint16  screen Y, hi = row, lo = fraction
  plaxv:     0,          // int24   horizontal accumulator; V16 = plaxv >> 8, terminal ±192
  playv:     0,          // int16   vertical, 256 per px/frame; |playv| = |plaxv_px| / 3
  pladir:    0x0300,     // int16   COMMITTED facing/thrust; +0x0300 right, −0x0300 left
  nplad:     0x0300,     // int16   PENDING facing, committed at C5
  plaxc:     0x20,       // uint8   COMMITTED screen byte-column (last drawn)
  playc:     0x80,       // uint8   COMMITTED screen row      (last drawn) = 128, mid-playfield
  nplaxc:    0x20,       // uint8   pending, written by PLAYER at C2
  nplayc:    0x80,       // uint8   pending
  // PLSTR5 writes the pair to BOTH registers in one instruction:
  //   ROM $D9AD  CC 20 80  LDD #$2080 / DD C1 STD <NPLAXC / DD BF STD <PLAXC
  //   (defa7.src:1269-1271; CONTROL_SYSTEM_SPEC §11.2). So plaxc=nplaxc=0x20 and
  //   playc=nplayc=0x80 before the first frame, and the very first COLCHK at B2 of the
  //   spawn frame already has a valid box. Row 128 is the vertical centre of the
  //   42..239 reachable band and is the origin of every timing in
  //   MOVEMENT_PHYSICS_SPEC §4.2 (MOV-11/13/14).
  plabx:     0,          // uint16  player world X, = ((plax16 >> 2) & 0xFFE0) + bgl.
                         //         READ BY ENEMY AI ONLY. Never by the scanner (C-04).
  pcx:       0,          // uint16  camera target column, PLAYER-internal
  bgdelt:    0,          // int16   camera easing term, ±0x40 or 0

  // ── 3.5 World / camera ───────────────────────────────────────────────────
  bgl:       0,          // uint16  world X of the screen's left edge, 32 units/px.
  bglx:      0,          // uint16  TWO WRITERS PER FRAME — see C-02 and §3.13
  // The world is exactly 2048 px = 65536 units. All horizontal wrap is 16-bit
  // overflow. There is no modulus anywhere in Defender and there must not be one here.

  // ── 3.6 Weapons ──────────────────────────────────────────────────────────
  lflg:      0,          // uint8   live player lasers, hard gate < 4 (defa7:2763-2765)
  fisx:      0,          // uint16  cursor into FISTAB, wraps at FISEND-3
  fistab:    new Uint8Array(32),   // regenerated by FISS with 32 rand() calls
  sbflg:     0,          // uint8   smart-bomb in-progress latch
  revflg:    0,          // uint8   reverse debounce latch
  bmbcnt:    0,          // uint8   GLOBAL shell counter, hard cap 20 in GETSHL

  // ── 3.7 Object pool ──────────────────────────────────────────────────────
  // 95 records × 23 bytes (OSIZE = $17, phr6.src:469 + RMB (95)*OSIZE).
  // Modelled as a struct-of-arrays or an array of objects; the STRIDE is not
  // observable, but the POOL SIZE and the FREE-LIST ORDER are.
  obj: [ /* 95 × {
      olink, opict, objX /*uint8*/, objY /*uint8*/, objId,
      ocvect, ox16, oy16, oxv, oyv, objcol, otyp, odata, odata1
  } */ ],
  optr:  0,   // head of the ACTIVE list   (on/near screen)
  iptr:  0,   // head of the INACTIVE list (off-screen, still simulated)
  sptr:  0,   // head of the SHELL list    (enemy shells + Bomber mines)
  ofree: 0,   // head of the free list — allocation order IS observable

  // otyp vocabulary (phr6.src:490-495):
  //   0x00 normal, hyperable
  //   0x01 Lander picking up a humanoid — non-hyperable
  //   bit1 set: mid-appear — non-hyperable, smart-bomb-exempt (otyp |= 2, C-06)
  //   0x10 humanoid — non-hyperable, smart-bomb-exempt
  //   0x11 score popup — non-hyperable, smart-bomb-exempt

  // ── 3.8 Process pool ─────────────────────────────────────────────────────
  // pd/pd2/pd4/pd6 are FOUR 16-bit user words in the process record
  // (ENEMY_BEHAVIOR_SPEC §1.5). LANDS0 keeps its TLIST slot index in pd4;
  // LSHOT decrements its shot timer in pd6. All four are required.
  proc: [ /* {plink, paddr, ptime, ptype, pcod, pd, pd2, pd4, pd6} */ ],
  active: 0,   // list-head SENTINEL — MKPROC inserts after CRPROC, which is
  crproc: 0,   //   this sentinel during B5 and the running record during B6 (C-05)
  free:   0,
  swproc: [0, 0, 0, 0],   // two 4-byte switch queue slots (addr, mask)

  // ── 3.9 Explosion / appear engine — 16 slots, shared ─────────────────────
  // A wave start or a Pod burst CONSUMES explosion capacity (L-074).
  expl: [ /* 16 × { rsize /*int16; <0 = appear*/, obdesc, erases, objptr,
                    topLft, center, eraseTable: Uint16Array } */ ],
  lsexpl: 0,   // round-robin cursor. EXST advances it; APST DOES NOT (samexap7:83 vs 61)
  centmp: 0,   // uint16 screen address of the colliding byte, written by COLIDE's fine scan

  // ── 3.10 Terrain, stars, scanner ─────────────────────────────────────────
  altbl:  new Uint8Array(1024),   // rebuilt by ALINIT at EVERY player start; never random
  tertf0: null, tertf1: null,     // terrain ring buffers, rebuilt by BGINIT
  stbl:   new Uint16Array(152),   // terrain erase table
  smap:   [ /* 16 × {sx, sy, scol} */ ],
  strcnt: 16,                     // stars actually drawn; cut to 3 by the governor
  ovcnt:  0,                      // uint8 overload accumulator. SIMULATION STATE, not
                                  // instrumentation: TERBLO forces it to 8 (C-26, §2.3)
  setab:  new Uint16Array(200),   // scanner erase table, see §3.14
  setend: 0,
  stetab: new Uint16Array(128),   // scanner mini-terrain erase table

  // ── 3.11 Enemy census and difficulty (ELIST) ─────────────────────────────
  lndCnt:0, lndRes:0, tieCnt:0, tieRes:0, prbCnt:0, prbRes:0,
  swCnt:0,  swmRes:0, sczCnt:0,  sczRes:0, ufoCnt:0, ufoTmr:0, ufoTim:0,
  wavSiz:0, wavTim:0, wavTmr:0, astCnt:0, gtime:0,
  tlist: new Uint16Array(20),     // 20 humanoid slots; GTARG walks 32 (C-23)
  tptr:  0,

  // ── 3.12 Per-player block — array of 2, curPlr fixed at 0 (C-22) ─────────
  curPlr: 0, plrCnt: 1,
  player: [ {
      score:  new Uint8Array(4),  // packed BCD, MSB first, EIGHT digits (C-03)
      prpla:  new Uint8Array(3),  // next replay threshold, 6 BCD digits
      plas:   3,                  // ships
      psbc:   3,                  // smart bombs — SAME CMOS byte as ships (L-024)
      pwav:   0,
      ptarg:  10,
      penemy: 0
  }, /* slot 1 present, unused in Phase 3 */ ],
  repla: 0x0100,                  // CMOS REPLAY, BCD; 0 disables the award

  // ── 3.13 Framebuffer — SIMULATION STATE (C-08) ───────────────────────────
  fb: new Uint8Array(156 * 256),  // column-major, 4bpp, HIGH nibble = LEFT pixel
  //  addr = (x >> 1) * 256 + y ;  poke16 writes TWO CONSECUTIVE SCANLINES
  //  addr is masked &0xFFFF and writes at addr >= 156*256 are dropped
  pcram: new Uint8Array(16),      // palette shadow, written by the animators
  cram:  new Uint8Array(16),      // hardware palette, copied from pcram at A2 only

  // ── 3.14 Instrumentation, never read by simulation ───────────────────────
  events: [],                     // cleared by the host after each step(); see §5

  // ── 3.16 Fields §3.1–§3.14 omitted; each is read across a frame boundary ──
  //    (added in iteration 4; see the notes under the block)

  // 3.16a  Input shadow registers — the core's ONLY view of the controls (§7)
  pia21:  0x00,   // uint8  PIA2 port A, THIS frame's sample
  pia22:  0x00,   // uint8  PIA2 port A, PREVIOUS frame's sample — SSCAN's edge detect
  pia31:  0x00,   // uint8  PIA3 port A (up/down/cabinet); level-read, no history needed

  // 3.16b  Sound sequencer — phr6.src:365-372. C-01 reasons about sndTmr and thFlg
  //        by name; a snapshot taken mid-sequence must restore the same sound.
  sndX:    0,     // uint16 cursor into the active sound table
  sndPri:  0,     // uint8  priority of the sound currently playing
  sndTmr:  0,     // uint8  frames until the next entry; SNDLD sets 1, C1 decrements to 0
  sndRep:  0,     // uint8  repeat counter
  sndTable: null, // the table identity (name/id), so restore() resumes the same sound
  sndEntry: 0,    // index within that table
  thFlg:   0,     // uint8  thrust-drone latch (phr6.src:293)

  // 3.16c  Draw-band boundary and its timing input — C-24, §2.3.1
  xxx1: 0xFF,     // uint8  constant after INIT
  xxx2: 0x70,     // uint8  RECOMPUTED at C0 every frame; 120 under this core's timing.
                  //        0x70 is only the pre-first-IRQ value from STD #$FF70.
  xxx3: 0x00,     // uint8  constant after INIT (CLR <XXX3)
  vertctAtService: 0,  // uint8  0 at A2, 128 at C0 — the fixed "no overrun" timing model

  // 3.16d  IRQ re-entry latch — defa7.src:1938-1942 / 1963-1965
  iflg: 0,        // uint8  set by the scanline-128 path, cleared by the scanline-0 path.
                  //        With the two IRQs flattened it is always 0 at step() entry and
                  //        1 between C and the next A; keep it so a snapshot round-trips.

  // 3.16e  Animator table cursors (phr6.src:284-292) — read across frames
  thx:  0,        // uint16 thrust-table index      (THPROC)
  bax:  0,        // uint16 bomb-image pointer      (CBOMB)
  fbx:  0,        // uint16 fireball index          (FBINIT/fireball animator)
  tflg: 0,        // uint8  Bomber tie direction    (TIECOL)
};
```

### 3.16 Notes on the block above

`§3` is titled *the complete structure* and it now is. Six categories were missing in iterations
1–3 and each was read by name by a spec this document governs:

| Field(s) | Read by | Why it must be in core state |
|---|---|---|
| `pia21`, `pia22`, `pia31` | `MOVEMENT_PHYSICS_SPEC` §3.2 (`pia21 & 0x02` thrust) and §4.1 (`pia31 & 0x01`, `pia21 & 0x80`); `CONTROL_SYSTEM_SPEC` §2.3; `CTL-02` fixture | `SSCAN`'s edge detect needs a **two-sample history**; without `pia22` there is no edge and `CTL-03`/`CTL-04` cannot pass |
| the sound block | `sound.js` is inside `src/core/` (§4); C-01 | a `snapshot()`/`restore()` mid-sequence otherwise resumes a *different* sound and `INT-25` fails non-deterministically |
| `xxx1/xxx2/xxx3`, `vertctAtService` | §2, C-24 | the band boundary is per-frame state, and the Lab may force it |
| `pd4`, `pd6` | `ENEMY_BEHAVIOR_SPEC` §1.5, `LANDS0`, `LSHOT` | declared in §3.8 above |
| `iflg` | `defa7.src:1938-1965` | snapshot round-trip fidelity |
| `thx`, `bax`, `fbx`, `tflg` | the animators in `draw/` | they advance every few frames and are not derivable from anything else |

`ovcnt` moved out of "instrumentation" and into §3.11-adjacent simulation state, because C-26 shows
it is written by game code (`TERBLO`) and therefore is not instrumentation: `ovcnt` and `strcnt`
both live in §3.10/§3.11 and both are part of the replay identity.

### 3.15 `STATUS` — the single authoritative bit table

`phr6.src:307-316`. Every subsystem consults it; all five specs agreed on the bit meanings.

| Bit | Mask | Meaning | Consumers |
|---:|---|---|---|
| 7 | `$80` | game over / attract | `APST` suppresses `APSND`; `SNDSEQ` thrust gate |
| 6 | `$40` | player controls inactive **and kill all lasers** | `SWTAB` masks; `LASR0`/`LASL0` |
| 5 | `$20` | stars, objects and shells frozen | `VELO`, `OPROC`, `STOUT`, `SHELL` all return |
| 4 | `$10` | player image + collision inactive | `COLCHK`, `PRDISP`, `commitPlayerDraw` |
| 3 | `$08` | player dead | `GEXEC` skips wave logic; `SNDSEQ` thrust gate |
| 2 | `$04` | appears / explosions disabled | `EXPU` tears down every slot |
| 1 | `$02` | terrain inactive | `BGOUT` skipped at A4; scanner mini-terrain suppressed |
| 0 | `$01` | player start delay — **no hyperspace** | `HYPER`'s `BITA #$FD` gate |

Canonical values: `$00`/`$02` play · `$05`/`$07` 96-frame start delay (controls **live**, hyper
dead) · `$77` hyperspace passes 0–14 · `$50`/`$52` hyperspace passes 15–54 · `$58`/`$5A` death ·
`$7F` spawn setup · `$D9`/`$DB`/`$FF` attract and game over (C-13).

Setters: `stchk` / `stchk0` / `stchkA`, see C-12.

---

## 4. MODULE BOUNDARIES

Refines `ARCHITECTURE.md` §2 and §8; does not replace them.

```
src/core/            ← imports NOTHING outside src/core/
  step.js              the frame order of §2, and nothing else
  state.js             the structure of §3, plus snapshot()/restore()
  rng.js               §6
  objects.js           pool, lists, VELO, OSCAN/ISCAN, GETOB/KILLOB
  process.js           MKPROC/DISP/GNCIDE/SLEEP/NAP/SUICIDE
  player.js            PLAYER, REV, HYPER, LFIRE/LASR/LASL, SBOMB
  collide.js           COLIDE/COL0/COLCHK, CENTMP
  enemies/*.js         one file per class, plus SHOOT/GETSHL/SHELL
  humanoids.js         ASTRO/AFALL/ALAND/ASTKIL/AKIL1 and the popups
  waves.js             GEXEC/WVCHK/GETWV/PLRES/PLSAV/SCORE/TERBLO
  draw/                PRDISP, OPROC, EWRITE/EERASE, BGOUT, STOUT, SCNR, HUD glyphs
  fb.js                the framebuffer primitives of §3.13
  sound.js             SNDSEQ/SNDLD/SNDOUT — emits EVENTS, never audio
  events.js            the schema of §5

src/render/classic/  ← pure (framebuffer, cram) → RGBA. No state across frames.
src/render/enhanced/ ← reads state snapshot + event stream. Never reads fb, except
                       the flagged compatibility path for screens it has no display list for.
src/audio/           ← consumes the event stream. NEVER feeds back into the core.
src/input/           ← produces the raw bitfield of §7. No game logic.
src/lab/             ← reads snapshots and events. May force ovcnt. May not write state
                       except through an explicit, logged debug API.
```

**Hard invariants (also in `CLASSIC_INVARIANTS.md`):**

1. `src/core/**` contains no `import` from outside `src/core/`, no `document`, no `window`, no
   `Math.random`, no `Date`, no `performance`, no float literal in a simulation path.
2. A suspended, failed or absent `AudioContext` produces a byte-identical event stream.
3. Presentation settings (`clean` / `raster` / `crt`, scale, overlays, captions, haptics, palette
   variants) cannot influence `step()`. Proven by replay-identity tests, not by inspection.
4. `step()` is pure with respect to the host: same `(state, input)` → same next state, always.
5. Nothing writes into `fb` except core draw code. Overlays live on the presented surface.

---

## 5. EVENT STREAM SCHEMA

The core appends to `state.events` during `step()`. The host drains it after each `step()` and
hands the same array to the renderer, the audio engine, the feedback layer and the Lab overlay.
**One array, one order, one source of truth.** An event is a plain object; every event carries
`tick` and `phase`.

```js
{
  tick:   uint32,          // state.tick at emission
  phase:  'A'|'B'|'C',     // which phase emitted it — needed to reproduce C-01 ordering
  kind:   string,          // from the table below
  // kind-specific fields follow
}
```

Ordering rule: events appear in **emission order within the tick**, which is phase order and then
program order inside the phase. Two events on the same tick are never reordered, deduplicated or
coalesced.

### 5.1 Simulation events

| `kind` | Fields | Emitted at | Notes |
|---|---|---|---|
| `LANDER_KILL` `MUTANT_KILL` `SWARMER_KILL` `BAITER_KILL` `BOMBER_KILL` `POD_KILL` `MINE_KILL` | `objId, x16, y16, objX, objY, byPlayer:'laser'\|'bomb'\|'collision'` | B2 or B6 | one per `cvect` invocation |
| `ASTRO_GRAB` `ASTRO_LIFT` `ASTRO_ABSORB` `ASTRO_FREE` `ASTRO_CATCH` `ASTRO_DEPOSIT` `ASTRO_KILL` | `objId, x16, y16, landerId?` | B6 (B2 for `ASTRO_CATCH`) | |
| `MUTATE` | `objId` | B6 | same object record; **no spawn/despawn pair** |
| `SPAWN` | `objId, cls, x16, y16, appearSlot\|null` | B6 | `appearSlot === null` means the 16-slot pool was full (L-074) |
| `PLAYER_FIRE` | `lflg, plaxc, playc, dir` | B6 | |
| `PLAYER_HIT` | `by:'object'\|'shell', objId` | B2 | |
| `PLAYER_DEATH` | `cause:'collision'\|'hyperspace'` | B2 or B6 | |
| `SMART_BOMB` | `psbc, victims:[objId]` | B6 | `victims` in kill order |
| `HYPERSPACE` | `phase:'enter'\|'arrive'\|'resolve', bgl, plax16, play16, lseed` | B6 | three events per jump |
| `BAITER_SPAWN` | `objId, ufoCnt` | B6 | |
| `WAVE_CLEAR` | `pwav` | B6 | |
| `WAVE_START` | `pwav, elist` | B6 | full difficulty snapshot |
| `BONUS_PAID` | `count, perHead` | B6 | one per surviving humanoid, plus `score` events |
| `REPLAY_AWARD` | `plas, psbc` | B6 | from `checkReplay`; a tick may carry several (C-16) |
| `PLANET_DESTROYED` `PLANET_RESTORED` | `astCnt` | B6 | |
| `GAME_OVER` | `finalScore` | B6 | |

### 5.2 Scoring events — the determinism gate

```js
{ tick, phase:'B', kind:'score', value: 150, source:'LKILL', objId: 42 }
{ tick, phase:'B', kind:'bonusShip' }     // always AFTER the score event that caused it
```

`value` is the decimal value actually added, derived from `SCORE(A = exponent, B = BCD mantissa)`.
A replay `{seed, inputSequence, tickCount}` must reproduce this substream **exactly** — same
order, same tick, same `value`, same `source`, same `objId`. This is the hard gate of
`ARCHITECTURE.md` §6.

### 5.3 Sound events

```js
{ tick, phase:'C', kind:'SND', cmd: 0x14, pri: 0xE0, table: 'LASSND', bus: 0xEB, src: 'SNDSEQ' }
{ tick, phase:'B', kind:'SND', cmd: 0x12, pri: 0xFF, table: null,     bus: 0xED, src: 'DIRECT' }
```

Emitted by `SNDSEQ` at C1 (`phase: 'C'`, `src: 'SNDSEQ'`) **and** by the three `sndOutDirect`
bypass sites (`defa7.src:1384-1386`, `defa7.src:1428-1430`, `defb6.src:820`), which run inside
`PLEND` / game-over — processes dispatched at **B6**, so those events carry `phase: 'B'` and
`src: 'DIRECT'`. `phase` is not a constant. `cmd === 0` is dropped before the event is created
(`SNDOUT(0)` writes `$3F` twice and produces no CB1 edge). `bus` is `(~cmd & 0x3F) | 0xC0` and is
recorded so a trace can be diffed against a MAME capture.

Per C-01: a `sndLoad` issued in phase B produces its `SND` event **on the same tick**; one issued
in phase C produces it on the next.

**Same-tick ordering (C-28).** When a bypass and a sequencer emission land on the same tick, the
**bypass is first** (B6) and the sequencer's is second (C1) — so on a death-plus-event tick the
sequencer's command is the one the board finally latches. `AUDIO_ENGINE_SPEC` §12 E-1 stated the
inverse and is corrected. `INT-34` asserts the ordering.

### 5.4 Presentation events — emitted by the core, consumed only downstream

| `kind` | Fields | Notes |
|---|---|---|
| `PALETTE` | `slot, value, source:'COLR'\|'CBOMB'\|'TIECOL'\|'SBOMB'` | `SBOMB`'s is `COM PCRAM` on slot 0 (C-17) |
| `EXPLOSION_STEP` | `slot, rsize, size, topLft, center` | the Enhanced renderer's display-list input |
| `APPEAR_STEP` | `slot, rsize, objId` | |
| `LASER_STEP` | `procId, head, mid, tail, dir` | screen addresses, so Enhanced can re-draw it |
| `SCANNER_REDRAW` | — | every 8th frame, from `SCPROC` leg 3 |
| `SCREEN_CLEAR` | `region` | `SCLR1`, `BLKCLR` |
| `STARS` | `strcnt` | |

A renderer that consumes `EXPLOSION_STEP`, `APPEAR_STEP`, `LASER_STEP` and `STARS` can reconstruct
every draw without reading `fb`. That is the contract Enhanced Mode was promised.

### 5.5 What the core does **not** emit

No hyperspace sound. No reverse sound. No wave-start, wave-clear or mutation sound. No background
music. No event for a `sndLoad` that lost the priority test — the rejection is silent, exactly as
`SNDLD` is.

---

## 6. THE RNG CONTRACT

`SOURCE_CONFIRMED`, L-106, `defa7.src:943-962`. **One generator. One schedule. Any subsystem that
adds or removes a call changes every seeded test in every suite.**

```js
function rand() {                        // RETURNS THE NEW SEED. See the note below.
  let b = (seed * 3 + 17) & 0xFF;                   // LDB SEED / LDA #3 / MUL / ADDB #17
  const t  = ((lseed >> 3) ^ lseed) & 0xFF;         // LDA LSEED / LSRA×3 / EORA LSEED
  const c  = t & 1;                                 // LSRA -> carry
  const ch = hseed & 1;
  hseed = ((c  << 7) | (hseed >> 1)) & 0xFF;        // ROR HSEED
  lseed = ((ch << 7) | (lseed >> 1)) & 0xFF;        // ROR LSEED
  const s1 = b + lseed;                             // ADDB LSEED
  b = (s1 + hseed + (s1 > 0xFF ? 1 : 0)) & 0xFF;    // ADCB HSEED — carry is from s1
  seed = b;                                         // STB SEED
  randCalls++;
  return seed;                                      // LDA SEED  <-- A is RELOADED here
}
```

**The return value is the new `SEED`, not the shift intermediate.** ROM `$D711`
(`defend.1` @ `0x0710`):

```
34 04  D6 DF  86 03  3D  CB 11  96 E1  44 44 44  98 E1  44
06 E0  06 E1  DB E1  D9 E0  D7 DF  96 DF  35 84
                     ^^^^^  ^^^^^  ^^^^^
                     STB SEED  LDA SEED  PULS B,PC
```

`defa7.src:945-962` agrees. The intermediate `((lseed>>3) ^ lseed) >> 1` that iterations 1–3
returned is computed into `A`, used only to drive the two `ROR`s, and is then **clobbered by
`LDA SEED`**. `RMAX` (`defa7.src:1733-1743`) is `PSHS A / JSR RAND / CMPA ,S / …` — it consumes
exactly this reloaded value. `CONTROL_SYSTEM_SPEC` §9.2 had it right all along
("the return value IS the new seed"); this document and `ENEMY_BEHAVIOR_SPEC` §0.4 did not. C-27.

- **Seeded once, at power-up**, to `seed = 0x00, hseed = 0xA5, lseed = 0x5A` (`SINIT`,
  `defa7.src:1006-1007` — `HSEED:LSEED = $A55A`; `SEED` is left 0 by the RAM clear). **Never
  re-seeded** at game start, wave start, life start or spawn. Measured period 57337 (non-primitive:
  x^16 + x^13 + 1).
- **Exactly one unconditional call per frame**, at **B4** (`defa7.src:3095`).
- **Many consumers read `seed` / `hseed` / `lseed` directly without calling `rand()`** — `CBOMB`
  (`COLTAB[seed & 0x1F]`), the overload governor (`seed & 0x3F`), hyperspace's destination
  (`SEED:HSEED` used raw as `BGL`) and its death roll (`lseed > 192`), `SBLNK`, several enemy
  decisions. **Same-frame decisions are therefore correlated by construction.** A port that draws a
  fresh random per decision is not differently-random, it is differently-behaved.
- **`rmax(n)`** (`defa7.src:1733-1743`, L-067) is `let a = rand(); while (a > n) a >>= 1; return a + 1;`
  → result in `1..n+1`, and it is **not uniform**. Distributions are tabulated in
  `ENEMY_BEHAVIOR_SPEC.md` §0.5 and are asserted by `ENE-*`.
- **The player-death particle engine has its own generator**, separate from this one: two 16-bit
  LFSRs seeded `$0808` and `$1732`, recurrence `bit15' = bit2 ^ bit1 of the low byte`. It does not
  touch `seed/hseed/lseed`.
- **The sound board's noise LFSR is a third generator**, seeded `HI=$3C, LO=$00` once at engine
  construction, advanced only by the command stream. It is **not** part of the replay tuple.

### 6.1 The per-respawn call budget

| Site | Calls | Confidence |
|---|---|---|
| `EXEC` B4 | 1 per frame, unconditional | `SOURCE_CONFIRMED` |
| `FISS` (laser fizz table) | 32 | `SOURCE_CONFIRMED` (`FISTAB RMB 32`) |
| `FBINIT` | 24 | `SOURCE_CONFIRMED` |
| `THINIT` | 33 | `SOURCE_CONFIRMED` |
| `STINIT` | variable, mean ≈ 58.8, min 32 | `VERIFIED-HERE`, C-19 — **measure, do not assert a literal** |
| enemy AI | per call site | `SOURCE_CONFIRMED` per class |

`STINIT` runs inside `INIT20` at every respawn, so replay determinism **across a death** depends on
its count. Implement both rejection loops verbatim (`defa7.src:2073-2091`), and have the test
harness measure rather than assert. The MAME trace that would freeze the number stays open.

---

## 7. INPUT CONTRACT

`docs/design/INPUT_MAPPING.md` is authoritative for bindings **and for the word the core consumes**,
and is adopted wholesale. There is exactly one definition, and it is `INPUT_MAPPING` §1's two PIA
bytes — not a boolean object. C-29.

```js
// Produced by src/input/, consumed by step() once per frame at C1.
// Two uint8 bitfields, MULTI_SOURCE_CONFIRMED (L-088) against phr6.src:120-146 and
// williams.cpp:762-786. INPUT_MAPPING §1 owns the bit assignments; they are
// reproduced here only so the packing below is unambiguous.
rawInput = { pia2: uint8, pia3: uint8 };

//   pia2  bit 0 FIRE      bit 1 THRUST   bit 2 SMART BOMB  bit 3 HYPERSPACE
//         bit 4 START 2   bit 5 START 1  bit 6 REVERSE     bit 7 (down, cocktail-dependent)
//   pia3  bit 0 UP        …              bit 7 cabinet type
```

At C1 the core does `pia22 = pia21; pia21 = rawInput.pia2; pia31 = rawInput.pia3;` — the two-sample
history `SSCAN` needs for its edge detect (§3.16a). **`START 1` and `START 2` are part of the word**,
so a replay can contain the keypress that starts the game, which `SHELL_AND_LIFECYCLE_SPEC` §5's
ATTRACT → READY → PLAYING chain and the audio corpus's `coin-and-start-1p` scenario both require.
The boolean `rawInput` of iterations 1–3 is deleted: it had no bit order, could not express
`START 1`, and made §8's "bit order = §7" a dangling reference.

- **Sample-and-hold, once per `step()`.** No buffering, no press latch, no coalescing.
  `KeyboardEvent.repeat` is ignored absolutely. The bitfield is derived from a `Set` of
  currently-down `code` values, not from events.
- The gamepad is polled once per **render** frame and OR'd into the same bitfield. Keyboard and
  gamepad are always both live; there is no input mode.
- **Level-read, no switch process:** `thrust`, `up`, `down` — read directly by `PLAYER` at C2,
  zero latency.
- **Edge-detected through `SSCAN` → `SWPROC` → `MKPROC`:** `fire`, `reverse`, `bomb`, `hyper` —
  one frame of latency, because the process spawned at C1 of frame *n* dispatches at B6 of *n+1*.
- **`SWTAB` order is `FIRE` (bit 0) before `REVERSE` (bit 6)** — which is why Reverse+Fire on the
  same tick fires with the **old** facing.
- Gating masks: `FIRE` and `REVERSE` rejected when `status & $E8`; `BOMB` and `HYPER` when
  `status & $F8`; `HYPER` additionally requires `(status & $FD) === 0`.
- `preventDefault` on keydown **and** keyup for currently-bound codes only.
  `navigator.keyboard.lock()` is never requested, so Escape always reaches the pause chain.

---

## 8. REPLAY FORMAT

```js
{
  version:   1,
  rom:       'redlabel',                 // must match ROM_IMAGE_PROVENANCE.md
  seed:      { seed: 0x00, hseed: 0xA5, lseed: 0x5A },
  cmos:      { nship: 3, replay: 0x0100, ga1: 5, ga2: 5, ga4: 5, /* … */ },
  inputSequence: Uint16Array,            // ONE WORD PER TICK: word = pia2 | (pia3 << 8).
                                         // Bit assignments: INPUT_MAPPING §1, restated in §7.
  tickCount: 18000,
  expect: {                              // the assertion payload, 07 §6
    scoreEvents:     [ /* the §5.2 substream, verbatim */ ],
    entityCounts:    [ { tick, lndCnt, sczCnt, tieCnt, prbCnt, swCnt, ufoCnt, astCnt, bmbCnt } ],
    stateTransitions:[ { tick, kind, detail } ],
    rngAtEnd:        { seed, hseed, lseed, randCalls },
    hashes: { fbSha1AtTicks: { 600: '…', 1200: '…' } }
  }
}
```

**The tuple that determines the run is `{seed, cmos, inputSequence, tickCount}` and nothing else.**
`expect` is the recorded outcome, not an input. `rom` and `version` are provenance.

`cmos` is part of the tuple because operator settings change difficulty tables and starting stock;
a replay recorded at `nship = 5` is not reproducible at `nship = 3`.

`seed` is the **three-byte** RNG state `{seed, hseed, lseed}` of §6, never a scalar. A corpus
scenario that starts mid-session records the live three bytes at its first tick; a scenario that
starts at power-up records `{0x00, 0xA5, 0x5A}`. C-30.

**This spelling is now the only one in the corpus.** `ARCHITECTURE.md` §6 has been amended from
`{seed, inputSequence, tickCount}` to match, `INPUT_MAPPING.md` §5.7's `{rngState, …}` and
`AUDIO_ENGINE_SPEC.md` §13.1's scalar `"seed": 12345` are corrected, and `AUDIO_ENHANCEMENT_SPEC`
`EA-7` cites this section rather than restating the tuple. §11's amendment checklist now names
`ARCHITECTURE.md` explicitly.

The audio board's LFSR is deliberately excluded: it is deterministic from a fixed seed and consumes
only the command stream, so it is derived, not input.

---

## 9. WHAT PHASE 3 BUILDS FIRST

The order below minimizes the number of unverifiable claims in flight at any moment.

1. `fb.js`, `rng.js`, `state.js`, `process.js` — plus tests `REN-01..08`, `CTL-51..56` (RNG),
   `INT-01..03`. Nothing else can be checked until the framebuffer addressing and the generator are
   provably right.
2. `objects.js` + `VELO` + list migration + `draw/oproc` → **`INT-30`** (the band tiling; `INT-06` was deleted by C-24), `INT-33`, `ENE-01..08`, `MOV-40..48`.
3. `player.js` movement + camera + `BGOUT` + `STOUT` → **`INT-04`/`INT-05` (the `BGLX` order)**,
   `MOV-01..30`, `REN-49..56`.
4. `collide.js` + `COLCHK` + the humanoid unwind → `INT-10`, `MOV-31..39`, `HUM-*`.
5. `sound.js` → **`INT-07`/`INT-08` (the `SNDSEQ` phase)**, `AUD-*`.
6. Enemies, waves, planet, HUD, scanner.
7. The Classic renderer, then the shell, then accessibility, then Enhanced.

`INT-*` are the integration tests introduced by this document; see
`docs/qa/ACCEPTANCE_TESTS.md` §2.

---

## 10. OPEN ITEMS THIS DOCUMENT DID NOT CLOSE

Carried forward, with the owning document. None of them blocks Phase 3; each has an interim ruling
already recorded in its own spec, and each is listed again in `KNOWN_DEVIATIONS.md` where the
interim is a knowing difference rather than an unknown.

| Item | Owner | Interim |
|---|---|---|
| `STINIT` exact call count | `CONTROL_SYSTEM_SPEC` §13.2 | measure, do not assert (C-19 bounds it) |
| `L-147/148/149` id collision in `MOVEMENT_ENVELOPE` §9 | ledger owner | cite ROM addresses, not ledger ids |
| `XCNT`/`YCNT` underflow in the fine scan | `CONTROL_SYSTEM_SPEC` | lab assertion `1 ≤ n ≤ 16` |
| Keyboard n-key rollover on the default layout | `INPUT_MAPPING` | ship + Rollover Test panel |
| Terrain output column direction | `RENDERING_SPEC` §14 | §9.3 as written; check against `TERRAIN_PROFILE` §4 silhouette |
| Thrust flame draw offsets (`THTAB`) | `RENDERING_SPEC` §14 | **do not draw it** rather than invent it |
| Humanoid rendered ground contact (Q-01a, L-146) | `TERRAIN_PROFILE` | draw exactly what the arithmetic says |
| Zero-color objects punching holes in the scanner | `HUD_RADAR_SPEC` | unconditional `STD [,U++]`, confirmed at `amode1.src:1272` |
| `SCREAM` `$1A` uninitialised `TEMPB` | `AUDIO_ENGINE_SPEC` D-3 | full 256-sample first block |
| The analogue reconstruction filter | `AUDIO_ENGINE_SPEC` D-5 | area integration + 10 Hz DC block |
| Two-player world swap (`PLSAV`/`PLRES`) | `WAVES_DIFFICULTY_SPEC`, L-125 | single-player only (C-22) |
| `GTARG` reading into `FISTAB` | `ENEMY_BEHAVIOR_SPEC`, L-123 | slots 20–31 read as zero (C-23) |
| Explosion-slot starvation rate | `ENEMY_BEHAVIOR_SPEC`, L-074 | faithful 16-slot round robin |
| No CPU-load model, so the *scheduler* never raises `OVCNT` | `RENDERING_SPEC` | governor implemented and live; `ovcnt` decays to 0 on its own, but `TERBLO` forces it to 8 thirty-two times per planet destruction and the cull fires (§2.3, C-26). `D-CORE-2` covers only the missing load input |
| No cycle model, so the A2 palette gate and the C0 `xxx2` recompute are always taken | this document §2.3.1 | implemented as gates with `vertctAtService` fixed at 0/128; `D-CORE-3` |
| Nothing in the corpus has been observed running | Phase 1 gate risk 3 | first-build screenshot pass |

---

## 11. CHANGE CONTROL

This document is amended, never forked. An amendment must:

1. cite ROM or source by file and line, or a MAME trace with its command line;
2. name every document that must change in the same commit (the corpus's recurring failure mode is
   **stale sibling documents** — a fact corrected in one file and left wrong in another).
   `docs/design/ARCHITECTURE.md` is explicitly in scope: it is BINDING, every other document is
   told to target it, and it is therefore the most expensive place to leave a stale fact (C-30);
3. name every acceptance test whose expected value moves;
4. **apply the sibling edits in the same commit and record them in §11A**, with a `grep`-checkable
   assertion per edit. Adjudication is not remediation (C-33);
5. append a row to `docs/qa/GAUNTLET_LOG.md`.

Amendments that touch §2 (frame order), §5 (event schema), §6 (RNG) or §8 (replay format) invalidate
the entire replay corpus and must regenerate it in the same commit.

### 11A. APPLIED-EDIT MANIFEST

Every "Fix:" directive in §1 and §1B, with the sibling edit that discharges it. CI
(`tools/check_supersessions.py`) fails if a row's `absent` string is still findable or its `present`
string is not.

| Ruling | File | Absent after this commit | Present after this commit |
|---|---|---|---|
| C-01 | `research/AUDIO_ENGINE_SPEC.md` §3.2 | `is the first call inside` | `runs at C1** — the head of the scanline-128 pass` |
| C-01 | `research/AUDIO_ENGINE_SPEC.md` §3.4 | `once per tick, first` | `once per tick, at C1` |
| C-01 | `research/AUDIO_ENGINE_SPEC.md` §13 | — | `REGENERATE — C-01` |
| C-03 | `research/RENDERING_SPEC.md` §11.2 | `score BCD, 3 bytes` | `a 3-byte WINDOW into the 4-byte` |
| C-24 | `research/RENDERING_SPEC.md` §5.3 | `BAND_A = { upper: 0xFF, lower: 0x70 }` | `The bands — complementary` |
| C-25 | `research/MOVEMENT_PHYSICS_SPEC.md` §10 | `| One object screen-position pass per step` | `WITHDRAWN — not a permissible deviation` |
| C-25 | `research/CONTROL_SYSTEM_SPEC.md` §13.1 | `| One object screen-position pass per step` | `WITHDRAWN — `CLASSIC_MODE_CONTRACT` C-25` |
| C-25 | `research/CONTROL_SYSTEM_SPEC.md` §13.2 | `The renderer may draw everything in one pass.` | `may **not** draw everything in one pass` |
| C-25 | `research/CONTROL_SYSTEM_SPEC.md` §2.2 | `recompute every active object's screen position` | `is the single normative frame order` |
| C-25 | `research/ENEMY_BEHAVIOR_SPEC.md` §0.3 | `object draw      (renderer; sets objX/objY, or 0 if not drawn)` | `TWO BANDED PASSES, NOT ONE` |
| C-26/C-31 | `research/PLANET_STATE_SPEC.md` §2.4 | `thrown 96..159 px right` | `768..1280 px` |
| C-27 | `design/CLASSIC_MODE_CONTRACT.md` §6 | `  return a;` | `return seed;` |
| C-27 | `research/ENEMY_BEHAVIOR_SPEC.md` §0.4 | `  return a;` | `return seed;` |
| C-28 | `research/AUDIO_ENGINE_SPEC.md` §12 E-1 | `the order is: sequencer first` | `**bypass first**` |
| C-29 | `design/CLASSIC_MODE_CONTRACT.md` §7 | `fire:  bool, thrust: bool, reverse: bool` | `rawInput = { pia2: uint8, pia3: uint8 }` |
| C-30 | `design/ARCHITECTURE.md` §6 | `A replay is `{seed, inputSequence, tickCount}` and nothing else.` | `A replay is `{seed, cmos, inputSequence, tickCount}` and nothing else` |
| C-30 | `design/INPUT_MAPPING.md` §5.7 | `This is what makes` | `The replay tuple is` |
| C-30 | `research/AUDIO_ENGINE_SPEC.md` §13.1 | `"seed": 12345,` | `"hseed": 165, "lseed": 90 }` |
| C-32 | `research/CONTROL_SYSTEM_SPEC.md` §6.3/§6.5 | `if obj.objx == 0:      continue` | `a SIXTEEN-BIT test` |
| C-32 | `research/WAVES_DIFFICULTY_SPEC.md` §7.6 | `**currently displayed** (`objX != 0`);` | `where "displayed" is the` |
| F-02/F-03 | `research/ENEMY_BEHAVIOR_SPEC.md` | `-lndYv` | `(~lndYv) & 0xFFFF` |
| F-02/F-03 | `research/ENEMY_BEHAVIOR_SPEC.md` | `-szYv` | `(~szYv) & 0xFFFF` |
| F-02 | `research/ENEMY_BEHAVIOR_SPEC.md` §3.2 | `? -m : +m` | `(~m) & 0xFFFF : m` |
| F-02/F-03 | `research/HUMANOID_RESCUE_SPEC.md` | `-lndYv` | `(~lndYv) & 0xFFFF` |
| F-04 | `research/ENEMY_BEHAVIOR_SPEC.md` §8.1 | `<< 8) \| lseed) + bgl` | `\| hseed) + bgl) & 0xFFFF` |
| F-08 | `research/MOVEMENT_PHYSICS_SPEC.md` §3.1/§8.1 | `ROM `$E24B`–`$E31A`, byte-identical` | `ROM **`$E263`**–`$E31A`, byte-identical` |
| F-09 | `research/CONTROL_SYSTEM_SPEC.md` §5.6 | `Verified: the three bytes at `$E5A8+3` are` | `Verified: the three bytes at **`$E5B6`** are` |
| SYS-06 | `design/AUDIO_ENHANCEMENT_SPEC.md` §1 | `message published by the core once per tick` | `derived in `src/audio/`, as a pure` |
| SYS-07 | `design/SHELL_AND_LIFECYCLE_SPEC.md` §6.1 L-17 | `never simulated ones` | `the simulation is THROTTLED` |
| SYS-08 | `research/RENDERING_SPEC.md` §3.2 | `function present(ctx, fbCanvas, mid) {` | `function measure(ctx, mid, view) {` |
| SYS-09 | `research/RENDERING_SPEC.md` §13A | — | `13A. PERFORMANCE — the Classic budget` |
| SYS-10 | `design/INPUT_MAPPING.md` §3 | `left **middle**, home position \| Held continuously` | `left **thumb** \| **The only sustained hold` |
