---
phase: 06.4
slug: cycle-5-gap-closure-d-51b-d-55b-d-55c-d-57b-d-58b-d-60-d-61
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-05-15
---

# Phase 06.4 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | vitest 1.x (unit/integration) + @playwright/test 1.x (e2e) |
| **Config file** | `vitest.config.ts` per-package; `playwright.config.ts` at root |
| **Quick run command** | `pnpm -r test --run` |
| **Full suite command** | `pnpm -r test --run && pnpm e2e` |
| **Estimated runtime** | ~90s quick; ~6 min full incl. Playwright on staging |

**CRITICAL — test file paths (per PATTERNS.md preface):**
- Server unit/integ tests live at `apps/server/test/*.test.ts` (NOT `apps/server/src/__test__/`)
- Client unit tests live at `apps/client/src/__test__/*.test.ts` (NOT `apps/client/src/render/__test__/`)
- The earlier `__test__/` subdirectory references in this doc were prompt-artifacts; corrected throughout below.

---

## Sampling Rate

- **After every task commit:** Run `pnpm -r test --run` (per-package scope ok)
- **After every plan wave:** Run full suite incl. Playwright on staging
- **Before `/gsd-verify-work`:** Full suite must be green + operator visual UAT signed per fix
- **Max feedback latency:** 120 seconds (quick); 360 seconds (full incl. e2e)

---

## Per-Task Verification Map

> Authoritative mapping — sourced from each `06.4-NN-PLAN.md` frontmatter (`plan`, `wave`, `requirements`, `<automated>` verify commands). Rows 04-11 re-mapped post checker BLOCKER 1 (planner moved spikes D-51c/D-58c/D-45d into Wave-1 sub-phase as plans 04/05/06 instead of intermixed). Wave column matches frontmatter `wave` field literally.

| Plan ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 06.4-01 | D-63 helper + drift test | 1 | REQ-CLI-04 | — | N/A | unit | `pnpm --filter @rebno/client test --run legacy-origin` | ❌ W0 | ⬜ pending |
| 06.4-02 | D-61 wall_border drop | 1 | REQ-CLI-06 | — | N/A | unit + e2e | `pnpm -r test --run` + Playwright `cli-08-room-render` | ✅ | ⬜ pending |
| 06.4-03 | D-60 always-emit room_layout | 1 | REQ-CLI-07, REQ-CLI-06, REQ-SRV-03, REQ-CLI-08 | — | server emits room_layout idempotently on every onJoin (cookie-resume + fresh-auth) | int + e2e | `pnpm --filter @rebno/server test --run d60-always-emit-layout` + Playwright `cli-08-resume-render` | ❌ W0 | ⬜ pending |
| 06.4-04 | D-51c eviction-loop spike | 1 | REQ-SRV-03, REQ-CLI-08 | — | structured pino `d51c_eviction_iter` events at every step; client ring buffer `__rebno.lastEvictionEvents` (last 20); NO behavior change | spike + int + manual | `cd apps/server && pnpm test --run d51c-eviction-trace` + manual operator multi-tab UAT | ❌ W0 | ⬜ pending |
| 06.4-05 | D-58c direction-desync spike | 1 | REQ-CLI-04, REQ-SRV-03, REQ-CLI-08 | — | client publishes `__rebno.localDirection` + `firstRemoteDirection`; server emits `d58c_player_axes` pino for uat_a/uat_b accounts; hypothesis comment in SpriteStateMachine.deriveFacing; NO behavior change | spike + manual | manual operator dual-tab UAT + `window.__rebno.firstRemoteDirection` capture + grep verifies (`grep -c "publishDirectionTelemetry"`, `grep -c "d58c_player_axes"`) | ✅ telem hooks | ⬜ pending |
| 06.4-06 | D-45d nametag flicker spike | 1 | REQ-CLI-07, REQ-CLI-08 | — | extend `firstRemoteNameplateHistory` ring-buffer entry with 10 lerp-inspection fields (5 X + 5 Y); NO change to 0.35 lerp or to Nameplate.ts | spike + manual | `cd apps/client && pnpm test --run` (no regression) + manual operator UAT + `window.__rebno.firstRemoteNameplateHistory` capture | ✅ telem hooks | ⬜ pending |
| 06.4-07 | D-55c origin refactor (via D-63 helper) | 2 | REQ-CLI-04, REQ-CLI-08 | — | startTeleportAnim uses `phaserOriginForLegacyPlayerAttached` helper from D-63; NAVI_HEIGHT_PX dead code removed; no inline `setOrigin(N/variant.width, ...)` math remains | unit + operator UAT | `cd apps/client && pnpm test --run player-renderer.teleport` + operator UAT (HARD gate 2) | ❌ W0 | ⬜ pending |
| 06.4-08 | D-62 deriveLayoutBounds + broadcast + W2→W3 SOFT gate 4 smoke | 2 | REQ-SRV-03, REQ-SRV-14, REQ-CLI-06, REQ-CLI-08 | — | server-authoritative `collision_polys` + `room_size` derived at runtime from `tiles[]` and broadcast as SEPARATE optional envelope fields (preserves manifest_sig); NO PROTOCOL_VERSION bump; PROTOCOL_VERSION literal tests at `packages/protocol/test/state.test.ts:11-12` + `apps/client/src/__test__/colyseus-client.test.ts:131,141` pass UNCHANGED | unit + int + e2e + operator UAT + SOFT gate 4 smoke | `cd apps/server && pnpm test --run layout-derive && pnpm test --run d62-derived-collision` + `cd apps/client && pnpm test:e2e --grep "cli-08-collision-boundary"` + operator UAT + W2→W3 5-test smoke on staging | ❌ W0 | ⬜ pending |
| 06.4-09 | D-51c fix (post-spike, in-scope only) | 3 | REQ-SRV-03, REQ-CLI-08 | — | both-tabs-freeze + reconnect-banner loop eliminated; OR escalation block written to 06.4-09-SUMMARY + 06.5 seed file created | int + e2e + operator UAT | `cd apps/server && pnpm test --run rebno-room-replace-session` + Playwright `cli-08-dup-login` + operator UAT (option-a) OR escalation artifact check (option-b) | ❌ W0 | ⬜ pending |
| 06.4-10 | D-58c fix — server-broadcast facing | 3 | REQ-CLI-04, REQ-SRV-03, REQ-CLI-08 | — | facing is server-authoritative (additive optional `facing` field on PlayerState, no PROTOCOL_VERSION bump); local-reconciler-snap reconciliation symmetric with position-snap convention | unit + int + e2e + operator UAT | `cd packages/game-logic && pnpm test --run && cd ../protocol && pnpm test --run` + `cd apps/server && pnpm test --run d58c-direction-broadcast` + Playwright `cli-08-direction-convergence` + operator UAT | ❌ W0 | ⬜ pending |
| 06.4-11 | D-55d TeleIn trigger gating | 3 | REQ-CLI-04, REQ-CLI-08 | — | TeleIn fires on self-spawn + post-join remote-adds, suppressed on initial-snapshot remote-adds (GameScene `inInitialSnapshot` flag flipped via queueMicrotask in onLocalJoin per RESEARCH Assumption A1) | unit + e2e + operator UAT | `cd apps/client && pnpm test --run sprite-state-machine.teleport-gate` + Playwright `cli-08-snapshot-no-spawn-anim` + operator UAT | ❌ W0 | ⬜ pending |
| 06.4-12 | D-45d fix (lerp source kill) | 3 | REQ-CLI-07 | — | flicker eliminated by suppressing 0.35 fractional smoothedY upstream of nameplate follow | unit + e2e | `pnpm --filter @rebno/client test --run nameplate-stability` + Playwright `cli-08-nameplate-stable` | ❌ W0 | ⬜ pending |
| 06.4-13 | D-57b regression test (anti-revert) | 4 | REQ-CLI-04 | — | reconciler does NOT use tween path | unit | `pnpm --filter @rebno/client test --run reconciler` | ❌ W0 | ⬜ pending |
| 06.4-14 | D-58b regression test (anti-revert) | 4 | REQ-CLI-04 | — | reconciler converges idle position within N ticks of remote update | unit | `pnpm --filter @rebno/client test --run reconciler` | ❌ W0 | ⬜ pending |
| 06.4-15 | Helper-API audit (Gate 5) | 4 | REQ-CLI-04 | — | no new inline `setOrigin(N / variant.width, ...)` math in `apps/client/src/render/` | static | `pnpm trace:check && pnpm grep:no-inline-origin` (or equivalent CI guard) | ❌ W0 | ⬜ pending |
| 06.4-16 | CLI-08 mp4 capture (carry-on-PASS) | 4 | REQ-CLI-08 | — | mp4 captured + linked from VERIFICATION.md | manual | manual operator screen-capture | n/a | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

- [ ] `apps/client/src/__test__/legacy-origin.test.ts` — D-63 helper math + atlas-mvp.json drift fixture + NaviStandD meta.json bbox-bottom fixture
- [ ] `apps/server/test/layout-derive.test.ts` — D-62 `deriveLayoutBounds` shape + boundary cases (mvp-room fixture)
- [ ] `apps/server/test/d60-always-emit-layout.integ.test.ts` — D-60 always-emit room_layout assertion on cookie-resume path
- [ ] `apps/server/test/d62-derived-collision.integ.test.ts` — D-62 step() respects derived collision_polys boundary
- [ ] `apps/server/test/rebno-room-replace-session.integ.test.ts` (extend existing) — D-51c single-eviction + no reconnect loop
- [ ] `apps/server/test/d51c-eviction-trace.integ.test.ts` — D-51c pino-event capture during eviction window (Plan 06.4-04, NEW; per checker WARNING 1)
- [ ] `apps/server/test/d58c-direction-broadcast.integ.test.ts` — D-58c server-authoritative facing on PlayerState (Plan 06.4-10, NEW; per checker WARNING 1)
- [ ] `apps/client/src/__test__/player-renderer.teleport.test.ts` — D-55c origin via helper assertion
- [ ] `apps/client/src/__test__/sprite-state-machine.teleport-gate.test.ts` — D-55d initial-snapshot suppression assertion
- [ ] `apps/client/src/__test__/nameplate-stability.test.ts` — D-45d remote-motion stability assertion
- [ ] `apps/client/src/__test__/reconciler.test.ts` (extend existing describe blocks) — D-57b + D-58b anti-revert regression cases
- [ ] `apps/client/test/e2e/cli-08-resume-render.spec.ts` — D-60 e2e (renamed from `cookie-reload.e2e.test.ts` per checker WARNING 3)
- [ ] `apps/client/test/e2e/cli-08-collision-boundary.spec.ts` — D-62 e2e
- [ ] `apps/client/test/e2e/cli-08-dup-login.e2e.test.ts` — D-51c e2e (extend existing if present)
- [ ] `apps/client/test/e2e/cli-08-direction-convergence.spec.ts` — D-58c e2e
- [ ] `apps/client/test/e2e/cli-08-snapshot-no-spawn-anim.spec.ts` — D-55d e2e
- [ ] `apps/client/test/e2e/cli-08-nameplate-stable.spec.ts` — D-45d e2e
- [ ] `tooling/no-inline-origin.ts` (or pnpm script) — Gate 5 grep guard for `setOrigin(\d+\s*\/\s*\w+\.width`

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| D-55c pixel-perfect TeleIn alignment with restored Navi | REQ-CLI-04 | Operator gates on side-by-side legacy screenshot — algebra alone insufficient (CONTEXT decision) | Open staging in two side-by-side windows: legacy BNO client + REBNO. Trigger self-spawn + warp. Compare TeleIn pixel placement relative to Navi feet. Sign at boundary if pixel-aligned. |
| D-45d residual nameplate flicker source confirmation | REQ-CLI-07 | Spike output reads from staging UAT — automated repro of Phaser Text fractional-Y flicker not yet reliable | Operator opens two tabs on staging. Player B walks a 5s lap. Operator reads `window.__rebno.firstRemoteNameplateHistory` ring buffer (last 20 frames). Sign A3 hypothesis (0.35 lerp upstream) confirmed/refuted. |
| D-58c direction-desync repro confirmation | REQ-CLI-04 | Spike output reads cross-tab via `window.__rebno.localDirection` / `firstRemoteDirection` — visual desync requires operator side-by-side | Open two tabs. Player A walks then stops facing R. Operator reads both literals from each tab + screenshot sprite. Sign root-cause hypothesis (server-broadcast vs client-derive). |
| D-51c multi-tab eviction sanity | REQ-SRV-03 | Replace-prior-session policy requires real two-Chrome-profile UAT — Playwright multi-context covers part, but the "reconnect-banner loop ~5s cadence" symptom needs human ear/eye to confirm killed | Open two Chrome profiles. Log into account X in both. Confirm: (a) prior tab gets single `force_reset` banner, no loop; (b) new tab joins cleanly, no freeze; (c) network tab shows no WS reconnect storm. |
| W2→W3 staging redeploy + 5-test smoke (SOFT gate 4) | REQ-CLI-08, REQ-SRV-03, REQ-CLI-04, REQ-CLI-06 | Operator-driven gate per checker BLOCKER 3 — runs as Plan 06.4-08 Task 5 after Wave-2 operator UATs. Combines staging redeploy + the 5-test smoke (cli-08-room-render, cli-08-resume-render, cli-08-collision-boundary + 2 cli-08 carry-overs from 06.3). Produces an artifact line in 06.4-VERIFICATION.md that Plan 06.4-16 Task 2 verifies. | See Plan 06.4-08 Task 5 `<how-to-verify>` — vite build FIRST then re-copy `apps/client/public/atlas-mvp.json` + `extracted/client-5-8/sprites/0000-NaviStandD/meta.json` after (vite wipes public/ on build); redeploy; run 5 Playwright tests on staging; sign in VERIFICATION.md. |
| CLI-08 hard-milestone mp4 | REQ-CLI-08 | Carry-on-PASS pattern (06.3) — screen-capture by operator after final phase PASS | Record 30s mp4 on staging: two players move + chat + warp. Link from VERIFICATION.md. |
| Operator visual per-fix UAT at Wave-2 + Wave-3 boundary (HARD gate 2) | REQ-CLI-04, REQ-CLI-06, REQ-CLI-07, REQ-SRV-03 | Cycle-3 burn: tests passed but visual fixes were wrong. Operator must sign each fix on staging before next wave starts. | At each Wave boundary, redeploy staging, operator opens staging, manually tests the specific fix per the operator UAT script in each plan's `<operator_uat>` block, signs PASS/FAIL in VERIFICATION.md. |

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 120s quick / 360s full
- [ ] `nyquist_compliant: true` set in frontmatter

**Approval:** pending
