# Phase 06.3: cycle-4 gap-closure D-40/D-45/D-51/D-53/D-54/D-55/D-56/D-57/D-58/D-59 + D-52 diag carry (UAT 2026-05-14) - Context

**Gathered:** 2026-05-13
**Status:** Ready for planning
**Derived from:** /gsd-discuss-phase 06.3 (4 selected gray areas + discretion overrides + 2 new findings surfaced mid-discussion). Builds directly on 06.2-CONTEXT.md, 06.2-VERIFICATION.md (verdict: NOT APPROVED @ 688d334), and operator UAT 2026-05-13 cycle-4 findings.

<domain>
## Phase Boundary

Close the cycle-4 UAT findings from 2026-05-13 on staging build `688d334` (06.2 closed NOT APPROVED). Re-run human UAT end-to-end on staging after merge. Capture `06.3-CLI-08-milestone.mp4` on PASS verdict — CLI-08 milestone video was deferred from cycles 1-4 and lands this cycle (or carries forward again if not approved).

**In scope (carry-in from 06.2 + 2 new findings):**

- **D-40 / D-54** — server `room_layout` payload completeness. 06.2-05 shipped `tiles[]` (H1 fix); cycle-4 still shows (a) collision grid extent mismatches visible floor extent (only LEFT edge blocks player; other edges don't stop or stop at wrong point) and (b) gray-border tiles render outside layout region. D-54 = the new collision-extent class.
- **D-45** — three sub-issues from cycle-4 regression of 06.2-07 nameplate fix: (a) offset OVERSHOT (text too high; legacy 10 px gap between bottom-of-glyph and top-of-player-head vs current 25 px — Phaser Text line-height descender padding); (b) vertical flicker on remote nameplates during movement; (c) depth z-order wrong — nameplates overlapped by player sprites + floor tiles.
- **D-51** — replace-prior-session eviction crashes BOTH tabs. Server code at `apps/server/src/RebnoRoom.ts:409-495` appears correctly scoped to `priorClient` (collects priorSessionIds before mutation, deletes prior player state FIRST to bypass 10s reconnect grace, sends `force_reset`, then `priorClient.leave(4001)`). Yet UAT cycle-4: "Login PERMITTED. Game freezes + WS-disconnect on BOTH tabs. Tab close → no auto-reconnect."
- **D-52** — perfect-spawn reset (rare). Diagnostic surface (`window.__rebno.lastReconcileX/Y/Reason/Seq/At` from 06.2-03) live on staging since cycle-4; no repros captured. Carries spike-only.
- **D-53** — `spawnDelayTicks=30` landed in 06.2-10 code but operator-invisible. Not published to `window.__rebno`. Also: `ensureLocal` idempotency may skip reinit on auth-flow re-entry path.
- **D-55** — teleport in/out animation missing (operator override INCLUDED 06.3, was Phase 7 default).
- **D-56** — chat box readability (small CSS polish on `apps/client/src/ui/ChatHUD.ts`).
- **D-57** — client-side movement micro-freeze every few seconds → ~500-1000ms jump. Steady-state cadence (not one-shot spawn). Likely periodic reconciler snap.
- **D-58 (NEW)** — player sprite position desync after movement stops. Remote view ≠ local view in steady state. Reconciler doesn't converge when `vx/vy=0` (Stand state); tiny accumulated drift never resolves.
- **D-59 (NEW)** — nameplate font inconsistency. Screenshot evidence: only `uat_a` on uat_b's client renders Fixedsys; other 3 nameplates render fallback monospace. Hypothesis: WOFF2 race; `Phaser.GameObjects.Text` snapshots font-availability at construction time; no re-render trigger on font load complete.

**Anti-recurrence discipline (cross-cutting, MANDATORY this cycle):**

06.1 ✗ NOT APPROVED → 06.2 ✗ NOT APPROVED. Pattern: unit tests passed, CI green, but visual / behavioral regressions remained. Cycle-4 e2e gate `cli-08-nameplate-offset.e2e` PASS but caught nothing because its assertion matched the (overshot) code. The GREEN gate uplift area below is treated as a first-class scope element, not a workflow note.

**Out of scope (defer to Phase 7):**

- All Phase 06.1 + 06.2 deferred items still defer (falls/abyss, ice/movement tiles, Hexport, JokerShell, top/left/right borders, multi-floor depth, depth-registry consolidation, all rooms beyond mvp-lobby and a single sanity-check room).
- `persistCharacter` FOREIGN KEY failures (Phase 7).
- Wider Colyseus version bump (0.17 → 0.18) if D-51 spike points there — explicit single-cycle commitment was given but architectural Colyseus surgery escalates to its own phase.
- Production deploy/provisioning (Phase 8).

</domain>

<decisions>
## Implementation Decisions

### D-40 / D-54 — server room_layout payload completeness

**Approach:** Spike-first, end-to-end payload trace. Wave 2 fix path conditional on spike output. Same shape as 06.2 D6.2-01/02 cycle (which worked for tiles[], but did not extend to collision payload).

**Spike scope:**

- **Rooms instrumented:** MVP-lobby + 1 sanity-check room (operator picks; suggest BNCentral or first other registered room in `RoomRegistry`). Catches systemic broadcast bug without instrumenting every room.
- **Telemetry surface:** BOTH — `window.__rebno` (post-decode client state) + server pino log (pre-encode broadcast payload). Server log catches encode-side bugs; `window.__rebno` catches client-side bugs. Doubles spike work but airtight.
- **Spike-exposed fields:** `layoutTilesCount`, `walkableCellsTrue`, `walkableCellsTotal`, `tilesOOB` (count of tiles[] entries whose x/y exceed width_tiles*tile_w / height_tiles*tile_h bounds), `payloadHasCollisionPolys` (bool — schema is `newLayoutSchema` in `packages/protocol/src/intents.ts:155-200` which does NOT define `collision_polys`; legacy shape at 115-153 does), `payloadWallBorder` (top/bottom/left/right edges + thickness).

**Likely Wave 2 fix paths (pending spike):**

1. Server `broadcastRoomLayout` (in `apps/server/src/RebnoRoom.ts:214-222`) emits sparse `tiles[]` (partial room coverage) → fix server-side to emit full grid.
2. `newLayoutSchema` (`packages/protocol/src/intents.ts:158-200`) lacks `collision_polys` — extend schema + server emit + client `RoomCollision.ts:23-60` consume.
3. Client-side `RoomCollision.fromLayout()` (`apps/client/src/render/RoomCollision.ts:42-58`) miscomputes row/col index from `tiles[]` x/y → fix derivation math.
4. RoomRenderer emits sprites for tiles[] entries OOB of width_tiles/height_tiles bounds → add bounds-check in RoomRenderer iteration.

### D-45 — nameplate three sub-issues (three separate plans)

**Plan A: offset retune (constant change).** `NAMETAG_OFFSET_Y` in `apps/client/src/render/Nameplate.ts:42` changes from **16 → 1**. Operator measurement: bottom-of-`a`-glyph to top-of-player-head = 10 px in legacy BNO vs 25 px in current client (15 px overshoot). Root cause: `Phaser.GameObjects.Text` bbox includes line-height descender padding below visible glyph; legacy GM `draw_text` does not. Formula `sprite.y - displayHeight - NAMETAG_OFFSET_Y` stays correct in shape; only the constant changes. Unit test in `nameplate.test.ts` MUST be updated to assert the new expected y values; document the Phaser-vs-legacy font-metric gap in the test comments to prevent future drift.

**Plan B: flicker root-cause spike.** Spike scoped to FIRST connected REMOTE player (not local) — operator confirms flicker is remote-only. Publish ring buffer to `window.__rebno.firstRemoteNameplateHistory`: `[{nameplate_y, sprite_y, sprite_displayHeight, sprite_scaleY, sprite_originY}]` at each `Nameplate.follow()` call. Operator captures flicker episode + reads literal. Hypotheses to disambiguate: sub-pixel jitter from prediction interpolation / `displayHeight` oscillation across frame variants / `Phaser.Text` re-render at fractional Y / `scaleY` instability. Atlas confirms NaviStandD + NaviRunD both 36×48 (see `apps/client/public/atlas-mvp.json`) — frame-bbox flicker hypothesis is therefore weak unless other frame variants differ.

**Plan C: depth z-order from legacy.** Researcher reads `extracted/client-5-8/objects/0042-player/events/Draw.dnd.json` and `extracted/client-5-8/objects/0000-server/events/Draw.dnd.json` for `depth_set` values used around player + nameplate draw calls. Replicate legacy depth-sort convention exactly. CONTEXT note: operator unsure what currently overlaps nameplate; legacy is authoritative. Set `Nameplate.text.setDepth()` per legacy spec (likely a constant high value or `-y` sort offset).

### D-51 — replace-prior-session evict crashes both tabs

**Approach:** Spike-first. H1 (cookie/localStorage share) DISPROVEN by operator — tabs were different Chrome PROFILES, not different windows in same profile. Remaining hypotheses (H2 seat-race / H3 broadcast side-effect / H4 client-side handler error).

**Spike scope (hybrid):**

- **Primary:** Playwright multi-tab fixture that hits dup-login deterministically (auth as same `uat_a` from 2 separate browser contexts). Forces repro.
- **Secondary (instrumentation, runs in parallel with fixture):**
  - Server: pino-log every `s2c.send` + `broadcast` (targeted-or-not) inside the eviction window — timestamp + recipient sessionId + s2c event type. Server log onJoin start/end timestamps to catch seat-race (Colyseus 0.17 default seat-reservation timeout ≈ 2s; eviction loop must finish well inside that).
  - Client: console.log every inbound state-change + Colyseus event during onJoin window. Wrap `force_reset` handler + `onLeave` handler in try/catch + emit `window.__rebno.lastClientError`.

**Why hybrid:** without envelope, Playwright might pass without revealing root cause (timing-fragile). Without Playwright, instrumentation has no deterministic trigger.

**Fix cycle:** single-cycle commit — spike + fix in 06.3. Phase 06.3 closes only when D-51 verified via Playwright fixture GREEN + operator UAT pass. Explicit caveat: if spike points to Colyseus version bump (0.17 → 0.18), scope escalates to its own phase; do NOT bundle a major dependency upgrade into 06.3.

### D-52 / D-57 / D-58 — reconciler-snap family (single folded spike)

**Approach:** All three share `window.__rebno.lastReconcileX/Y/Reason/Seq/At` diagnostic surface (06.2-03 already live on staging). Single shared diagnostic plan; operator reproduces each class and reads literal. Three distinct cadences:

- **D-52:** perfect-spawn reset; rare; one-shot snap to spawn coords.
- **D-57:** every-few-seconds micro-freeze → ~500-1000ms jump; steady-state cadence during motion.
- **D-58:** idle-state desync; remote view ≠ local view AFTER motion stops; persistent drift never converges.

**Spike output expectations:**

- D-58 likely root: reconciler skips position diff when `vx === 0 && vy === 0`. Server `last_input_seq` advancement stalls while client sends no inputs; idle-state divergence never reconciled.
- D-57 likely root: periodic reconciler invocation (e.g. every N ticks regardless of motion) with snap threshold too tight; tiny drift accumulates until threshold breaches → visible snap.
- D-52 likely root: reconciler reading stale `last_input_seq` causing application of an outdated snapshot whose (x, y) is spawn position. Or server cookie-resume teleport firing mid-session.

Fix plans for each class are deferred to W2/W3 of this phase, conditional on spike output. If spike resolves all three under a single fix (e.g. "reconciler must always reconcile, never skip on idle"), they collapse to one plan.

### D-53 — spawn delay operator-visibility

**Approach:** Two-part single small plan.

- **(a) Publish `spawnDelayTicks` to `window.__rebno`.** Add `window.__rebno.spawnDelayTicks` (current value, decremented per sim-tick) + `window.__rebno.spawnDelayTicksRemaining` so operator can read literal from devtools console.
- **(b) Audit `ensureLocal` reinit path.** `PlayerRenderer.ensureLocal` is idempotent — if local state already exists pre-spawn, the spawnDelayTicks counter may never reinit to 30 on the auth flow re-entry. Trace: login → onJoin → onLocalAdd. Confirm spawnDelayTicks is set to 30 on EVERY fresh local spawn, including reconnect-after-evict and refresh-during-session paths.
- **Test extension:** `playerRenderer.test.ts` MUST cover the reinit path (spawn → simulate evict → respawn → assert spawnDelayTicks === 30 again, not 0).

### D-55 — teleport in/out animation (operator OVERRIDE — included in 06.3)

**Approach:** Legacy spec extraction + Phaser tween + state machine plumbing. NOT default; operator chose to include this cycle.

- **(a) Legacy spec extraction.** Read `extracted/client-5-8/objects/0042-player/events/` for spawn-in / leave-out animation triggers. Pin exact frame sequence + tween parameters. Likely an alpha or scale tween over N ticks.
- **(b) Implement.** Add `SpriteState.TeleportIn` / `SpriteState.TeleportOut` states in `SpriteStateMachine`. Wire `GameScene.onLocalAdd` to start with TeleportIn; wire `GameScene.onRemoteRemove` to play TeleportOut before destroy.
- **(c) Integration with D-53 spawnDelayTicks.** TeleportIn animation should run DURING the spawnDelayTicks 30-tick hold; the player isn't input-responsive until both teleport-in finishes AND spawnDelayTicks decrements to 0. Verify these don't double-count.

### D-56 — chat box readability (UI polish)

**Approach:** Small CSS / Phaser styling bump on `apps/client/src/ui/ChatHUD.ts`. Increase font size + container height + line-height. Single small plan; no spike. Operator confirms visual on staging.

### D-59 — nameplate font race (NEW — direct fix)

**Approach:** Gate Phaser boot on `document.fonts.ready`. Scene `create` (or earlier in boot pipeline) `await document.fonts.load('1em Fixedsys')` (or `document.fonts.ready`) before instantiating ANY `Phaser.GameObjects.Text`. Existing nameplates instantiated post-boot inherit Fixedsys.

- **Late-joiner safety:** if a player joins after boot but before font fully loaded (rare), the boot gate ensures `document.fonts.ready` already resolved — no race possible.
- **Regression test:** new unit/integration test asserts `document.fonts.check('1em Fixedsys')` returns true before any Nameplate is created.
- **Locality fix:** Add a CSS preload hint (`<link rel="preload" as="font" type="font/woff2" crossorigin>`) in `apps/client/index.html` so the WOFF2 fetches at HTML parse time, not at Phaser boot — minimizes wait at boot gate.

### GREEN gate uplift — anti-recurrence (CROSS-CUTTING, MANDATORY)

**Four gates total. Three HARD, one SOFT.**

**HARD gate 1 — Playwright e2e all GREEN on staging pre-merge.** All `cli-08-*` + `camera-follow` Playwright suites MUST PASS against deployed staging build before phase close. Currently 5 FAIL on cycle-4 (`cli-08-local-anim`, `cli-08-floor-collision`, `camera-follow`, `cli-08-camera`, `cli-08-anim`). Phase 06.3 verification rejects close if any FAIL.

**HARD gate 2 — Operator visual per-fix UAT (Wave-2 boundary).** Each Wave-2 fix requires operator's visual nod on staging BEFORE next Wave-2 fix starts. Mirrors 06.2's UAT loop but earlier in cycle (per-fix instead of per-phase). Specifically catches over-correction patterns like D-45 cycle-3→cycle-4 overshoot.

**HARD gate 3 — E2e assertion-correctness review.** Add `gsd-code-reviewer` gate scoped to e2e test code: verify each assertion's expected value cross-checks against a canonical ref (legacy GML, CLAUDE.md constant, SPEC doc, or operator-stated measurement). Cycle-4 `cli-08-nameplate-offset.e2e` PASSED because its expectation matched the (wrong) code. Pre-merge review checks: does the assertion's expected value cite where the value comes from?

**SOFT gate 4 — Staging redeploy + smoke at Wave-2 boundary.** After all Wave-2 fixes merge, redeploy staging + run 5-test smoke (login + room enter + walk + chat + logout) before moving to Wave-3 verification. Not per-fix (too slow for ~7 plans); per-wave-boundary.

**Enforcement:** BOTH structural mechanisms — per-plan checklist (lives in each 06.3-NN-PLAN.md) AND Wave-3 hard verification gate.

- **Per-plan checklist (in each PLAN.md):** (a) e2e assertion canonical-ref cite, (b) operator UAT step, (c) staging post-deploy smoke step. `gsd-plan-checker` rejects plans missing any.
- **Wave-3 verification gate:** explicit checkboxes in `06.3-VERIFICATION.md` — (a) all e2e GREEN on staging, (b) operator HUMAN-UAT passes, (c) e2e reviewer report attached. `/gsd-verify-work 06.3` fails if any checkbox unfilled.

### CLI-08 milestone mp4 capture

**Capture this cycle on PASS verdict.** Deferred from cycles 1-4; carry-on-fail again if 06.3 NOT APPROVED. Record `06.3-CLI-08-milestone.mp4` showing two players moving + chatting on deployed staging. Operator captures during Wave-3 final UAT.

</decisions>

<execution_constraints>
## Wave / Ordering

**W1 — spikes + read-only legacy extractions (parallel-safe; diagnostic + spec only, no behavior change):**

- D-40/D-54 server payload trace spike (telemetry: window.__rebno + server pino log)
- D-45 Plan B flicker spike (first-remote-only nameplate history ring buffer)
- D-51 dup-login Playwright fixture + server + client envelope instrumentation
- D-52/D-57/D-58 folded reconciler diagnostic spike (window.__rebno.lastReconcile* already live; extend with idle-state fields)
- D-53 (a) spawnDelayTicks → window.__rebno + (b) ensureLocal reinit audit
- D-55 legacy teleport anim spec extraction (read-only)
- D-45 Plan C legacy depth_set spec extraction (read-only)

**W2 — fixes (depends on W1 spike outcomes + spec extractions):**

- D-40/D-54 fix path conditional on spike (one of 4 hypotheses)
- D-45 Plan A offset constant retune (16 → 1) — no spike needed; ships first
- D-45 Plan B flicker fix conditional on spike
- D-45 Plan C depth_set replication
- D-51 fix conditional on spike (H2/H3/H4 — if Colyseus version bump required, escalate)
- D-52/D-57/D-58 fix(es) conditional on shared spike — may collapse to single plan
- D-55 teleport anim implementation + state machine plumb
- D-56 chat box CSS bump
- D-59 document.fonts.ready boot gate + preload hint

**W3 — verification:**

- All cli-08-* + camera-follow Playwright e2e GREEN on staging
- E2e reviewer report (HARD gate 3)
- End-to-end re-run of HUMAN-UAT.md on staging
- Operator approval
- Capture `06.3-CLI-08-milestone.mp4` if PASS
- Wave-3 gate checkboxes filled in 06.3-VERIFICATION.md

**Wave boundary discipline (HARD gate 2):**

- Operator visual UAT REQUIRED between each Wave-2 fix and the next. No "batch all fixes then test" pattern.
- Staging redeploy + 5-test smoke (SOFT gate 4) at Wave-2 → Wave-3 boundary.

## Test boundary

- **Unit + integration tests REQUIRED** for: D-45 Plan A (offset), D-53 (spawnDelayTicks reinit path), D-55 (state machine transitions), D-59 (font readiness).
- **Spikes** produce diagnostic instrumentation + spec extraction; follow-up fixes in same cycle: D-40/D-54, D-45 Plan B (flicker), D-51, D-52/D-57/D-58.
- **e2e:** existing `06.1-HUMAN-UAT.md` is the regression gate; extend with new cli-08-* tests for D-40/D-54 collision extent and D-51 dup-login.

## Anti-Recurrence Discipline (CROSS-CUTTING)

Phase 06.1 ✗ → 06.2 ✗. Cycle-4 e2e gate caught nothing (PASS because assertion matched wrong code). Phase 06.3 MUST:

1. Reject plans missing canonical-ref cites in e2e assertions (HARD gate 3).
2. Block on operator visual UAT at every Wave-2 fix boundary (HARD gate 2).
3. Reject phase close if any Playwright e2e FAILs on staging (HARD gate 1).
4. Redeploy + smoke at Wave-2 → Wave-3 boundary (SOFT gate 4).

</execution_constraints>

<canonical_refs>
## Canonical References (MANDATORY reading for researcher + planner)

**Carry-forward from 06.2-CONTEXT.md and 06.2-VERIFICATION.md:**

- `.planning/phases/06.2-gap-closure-d-50-d-53-uat-2026-05-13/06.2-CONTEXT.md` — 06.2 implementation decisions; many shapes carry forward (e.g. window.__rebno telemetry pattern, replace-prior-session policy).
- `.planning/phases/06.2-gap-closure-d-50-d-53-uat-2026-05-13/06.2-VERIFICATION.md` — cycle-4 UAT verdict + per-test results + per-finding closure status.
- `.planning/phases/06.2-gap-closure-d-50-d-53-uat-2026-05-13/06.2-UAT-FINDINGS-2026-05-13.md` — original cycle-4 findings.

**Server (D-40/D-54, D-51, D-58 server-side surface):**

- `apps/server/src/RebnoRoom.ts:214-222` — `broadcastRoomLayout()` (D-40/D-54 payload origin).
- `apps/server/src/RebnoRoom.ts:409-495` — replace-prior-session eviction block (D-51).
- `apps/server/src/RebnoRoom.ts:107-200` — onCreate / onJoin top-level, room state schema (D-51 context).
- `apps/server/src/RoomRegistry.ts` — room registration (D-40/D-54 scope: pick 1 sanity-check room here).

**Protocol (wire schema):**

- `packages/protocol/src/intents.ts:115-153` — `legacyLayoutSchema` (has `collision_polys` — D-40/D-54 reference shape).
- `packages/protocol/src/intents.ts:155-200+` — `newLayoutSchema` (lacks `collision_polys` — likely extension target for D-40/D-54 fix path 2).
- `packages/protocol/src/version.ts` — PROTOCOL_VERSION. **Bump checklist (from 06.2-09):** if D-51 fix or D-40/D-54 fix bumps PROTOCOL_VERSION, MUST update two hardcoded asserts: (1) `packages/protocol/test/state.test.ts:11-12` (`PROTOCOL_VERSION === N` literal); (2) `apps/client/src/__test__/colyseus-client.test.ts:131,141` (`protocol_version: N` in joinOrCreate match). Both burned us cycle-3.

**Client render (D-40/D-54 client side, D-45, D-59):**

- `apps/client/src/render/RoomCollision.ts:23-60` — walkable grid derivation from `tiles[]` (D-40/D-54 client side).
- `apps/client/src/render/RoomRenderer.ts` — tile rendering iteration; bounds check candidate for D-54 outer-gray-tile fix.
- `apps/client/src/render/Nameplate.ts:42` — `NAMETAG_OFFSET_Y` constant (D-45 Plan A: 16 → 1).
- `apps/client/src/render/Nameplate.ts:98-100` — `follow()` formula `spriteY - spriteHeight - NAMETAG_OFFSET_Y` (D-45).
- `apps/client/public/atlas-mvp.json` — NaviStandD + NaviRunD both 36×48 sourceSize (rules out frame-bbox flicker for D-45 Plan B).
- `apps/client/index.html` — Fixedsys `@font-face` declaration; add preload `<link>` hint for D-59.
- `apps/client/src/ui/ChatHUD.ts` — chat box for D-56 CSS bump.

**Client prediction / reconciler (D-52/D-57/D-58):**

- `apps/client/src/prediction/reconciler.ts` — reconciler logic; idle-state convergence target for D-58.
- `apps/client/src/render/PlayerRenderer.ts` — `ensureLocal`, `spawnDelayTicks` (D-53 audit target).
- `apps/client/src/scenes/GameScene.ts` — `onLocalAdd`, `onRemoteRemove` (D-55 teleport state plumbing target).

**Legacy / extracted (read-only spec sources):**

- `extracted/client-5-8/objects/0042-player/events/Draw.dnd.json` — legacy player draw with depth_set (D-45 Plan C).
- `extracted/client-5-8/objects/0000-server/events/Draw.dnd.json` — legacy nameplate draw + color rule (D-45 Plan C).
- `extracted/client-5-8/objects/0042-player/events/Create.gml` — legacy spawn-in alarm/counter (D-53 verify, D-55 teleport spec).
- `extracted/client-5-8/objects/0042-player/events/` (broader scan) — D-55 teleport in/out anim sequence.

**Project / constants:**

- `CLAUDE.md` — extracted constants table (tile 44×40, view 640×480, tick 30 Hz). D-45 cites `text_y = sprite.y - 16` canonical — note that this canonical is REINTERPRETED in light of Phaser Text bbox padding; the literal interpretation produced the cycle-3→cycle-4 overshoot.

**External-system gotchas (from psyche / 06.2 cycle):**

- Worktree merge cwd drift: `cd` to repo root BEFORE `git merge $WT_BRANCH`.
- Staging deploy: `vite build` FIRST, re-copy atlas AFTER (vite wipes public/).
- CI rebuilds dist/ from source; local stale dist pass ≠ CI pass.
- `__rebno` test-infra hooks MUST be unconditional (no env gate); env gate silenced Playwright on staging in cycle-3 (RC1).
- CSP: `img-src` needs `blob:` for Phaser atlas PNG.
- CI `pnpm trace:check` SKIP set on verify-phase-4 due to REQ-SRV-XX placeholders in 04 plan/summary (non-blocking, deferred).

</canonical_refs>

<code_context>
## Reusable Assets + Patterns from Prior Phases

- **Window.__rebno telemetry pattern (06.2-01..03):** publish diagnostic state as `window.__rebno.fieldName` for operator devtools-console reads + Playwright extraction. Hooks MUST be unconditional (no `process.env.NODE_ENV` gate — env gate burned us in cycle-3 RC1).
- **Replace-prior-session policy (06.2-09):** `RebnoRoom.ts:409-495` — collect priorSessionIds FIRST (safe-iteration), delete state BEFORE `priorClient.leave(4001)` (bypass 10s grace), force_reset s2c event, code 4001 marker on leave. Pattern stays; D-51 spike investigates why the BOTH-tabs symptom occurs despite the policy looking sound.
- **`force_reset` s2c event shape:** `{ type: 'force_reset', reason: 'replaced_by_new_session' }`. Client banner reuses reconnect-banner DOM overlay.
- **Playwright multi-tab fixture pattern (NEW for D-51):** two browser CONTEXTS (not tabs in same context) to mimic separate-Chrome-profile UAT scenario.
- **SpriteStateMachine extension pattern (06.1-04):** fractional sub-tick frame advance + state transition table. D-55 TeleportIn/Out extends this.
- **e2e canonical-ref discipline (NEW this phase):** each Playwright assertion's expected value MUST cite source — legacy GML / CLAUDE.md / SPEC doc / operator-stated measurement. Plan checklist enforces.

</code_context>

<deferred>
## Deferred Ideas (out of scope for 06.3 — captured for future phases)

- **Colyseus 0.17 → 0.18 upgrade** — if D-51 spike points to a seat-reservation-timeout fix requiring a Colyseus version bump, escalate to its own phase (do NOT bundle into 06.3).
- **Glyph-metric-aware nameplate anchoring** — durable alternative to NAMETAG_OFFSET_Y constant retuning (D-45 Plan A). Would use `canvas.measureText().actualBoundingBoxAscent` etc. to anchor by visible glyph bottom. Deferred to Phase 7 (UI polish wave).
- **BitmapText / sprite-font glyphs** — Phaser BitmapText with pre-baked Fixedsys glyphs. Removes both Phaser Text font-metric padding (D-45 Plan A) AND the WOFF2 font-load race (D-59). Larger refactor; defer to Phase 7.
- **Multi-room collision/payload generalization** — D-40/D-54 spike only covers MVP-lobby + 1 sanity-check room. Full RoomRegistry coverage at Phase 7.
- **Phase 7 carry-forward (cumulative):** falls/abyss, ice/movement tiles, Hexport, JokerShell, top/left/right borders, multi-floor depth, depth-registry consolidation, all rooms beyond mvp-lobby, `persistCharacter` FK failures, prod provisioning.

</deferred>

<traceability>
## Requirements Touched

- **REQ-CLI-04** — animation contract (D-45 nameplate, D-53 spawn delay reinit, D-55 teleport anim, D-58 idle convergence, D-59 nameplate font).
- **REQ-CLI-06** — renderer / atlas / asset pipeline (D-40, D-54 collision + render extent).
- **REQ-CLI-07** — UI rendering (D-56 chat box readability).
- **REQ-CLI-08** — CLI-08 hard milestone re-gate (cross-cutting; mp4 capture this cycle).
- **REQ-SRV-03** — authoritative server state (D-51 evict policy regression).
- **REQ-SRV-14** — per-entity sim contract (D-52, D-57, D-58 reconciler family).

</traceability>

---

*06.3-CONTEXT.md written from operator UAT verdict (cycle-4 NOT APPROVED 2026-05-13) + 4 selected gray-area discussions + 2 new findings (D-58, D-59) surfaced during discussion + discretion overrides (D-55 IN, D-56 IN, D-52/D-57/D-58 folded). Resume in a fresh session via `/gsd-plan-phase 06.3` against this CONTEXT.*
