---
phase: 4
slug: server-rebuild-mvp
status: complete
nyquist_compliant: true
wave_0_complete: true
created: 2026-05-06
finalized: 2026-05-07
---

# Phase 4 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.
> Source: `04-RESEARCH.md` § Validation Architecture (committed `f747f5b`).
> Closed by Plan 04-13 — every Phase-4 SRV-01..SRV-14 row has its Plan ID
> filled, Status flipped to ✅ green, and the Sign-Off checklist fully
> ticked.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | vitest 4.1.5 (already adopted in `tools/db-schema`; standardize for Phase 4) |
| **Config files** | `apps/server/vitest.config.ts`, `packages/protocol/vitest.config.ts`, `packages/game-logic/vitest.config.ts`, `packages/db/vitest.config.ts` (all Wave 0) |
| **Quick run command** | `pnpm --filter <package> test` |
| **Full suite command** | `pnpm verify:phase-4` (composite gate per D-25 — runs `pnpm -r test` + 6 lints + ADR 0004 lint + drizzle-kit emit-check + traceable-reqs check) |
| **Estimated runtime** | ~45 s quick (single package) / ~3 min full composite |

---

## Sampling Rate

- **After every task commit:** `pnpm --filter <touched-package> test` + lint(s) for any artifact touched
- **After every plan wave:** `pnpm -r test && pnpm verify:phase-4 --quick`
- **Before `/gsd-verify-work`:** `pnpm verify:phase-4` full + manual sanity (server boot + two ws clients move + chat)
- **Max feedback latency:** 60 s

---

## Per-Task Verification Map

> Authoritative requirement→test contract. Plan IDs filled by 04-13 audit;
> Status reflects post-execution state of every Phase 4 plan as of HEAD
> 2026-05-07.

| Req ID | Plan | Wave | Behavior | Test Type | Automated Command | File Exists | Status |
|--------|------|------|----------|-----------|-------------------|-------------|--------|
| SRV-01 | 04-02 | 2 | Protocol package round-trips msgpackr; PROTOCOL_VERSION enforced on packet 1 | unit | `pnpm --filter @rebno/protocol test` | ✅ `packages/protocol/test/codec.test.ts` | ✅ green |
| SRV-02 | 04-03 | 2 | step() pure: same inputs+state → byte-identical output across runs | unit | `pnpm --filter @rebno/game-logic test golden` | ✅ `packages/game-logic/test/golden.test.ts` | ✅ green |
| SRV-02 | 04-03 | 2 | step() determinism Node vs browser | int | `pnpm --filter @rebno/game-logic test:browser` | ✅ `packages/game-logic/test/golden.browser.test.ts` | ✅ green |
| SRV-03 | 04-05 | 3 | apps/server boots; Colyseus serves /colyseus; /health returns 200 | int | `pnpm --filter @rebno/server test boot` | ✅ `apps/server/test/boot.integ.test.ts` | ✅ green |
| SRV-04 | 04-06 | 3 | Fabricated position rejected; chat sender field ignored — server tags from auth | int | `pnpm --filter @rebno/server test authority` | ✅ `apps/server/test/authority.integ.test.ts` | ✅ green |
| SRV-05 | 04-05 | 3 | 20 Hz fixed-tick: under jitter, step() called integer-times-per-frame with dt=50 | unit | `pnpm --filter @rebno/server test tick` | ✅ `apps/server/test/tick-accumulator.test.ts` | ✅ green |
| SRV-06 | 04-05 | 3 | Heartbeat (15 s) + reconnect grace (10 s) — drop client, reopen <10 s, state preserved | int | `pnpm --filter @rebno/server test reconnect` | ✅ `apps/server/test/reconnect.integ.test.ts` | ✅ green |
| SRV-07 | 04-08 | 3 | Token-bucket: 50 inputs/sec → drops; sustained drops → mute; mute lifts at 60 s | unit | `pnpm --filter @rebno/server test rate-limit` | ✅ `apps/server/test/rate-limit.test.ts` | ✅ green |
| SRV-08 | 04-09 | 3 | SIGTERM grace flushes characters; kill -9 mid-tick → on-restart, characters table valid | int | `pnpm --filter @rebno/server test sigterm` | ✅ `apps/server/test/sigterm.integ.test.ts` | ✅ green (SIGTERM half) / ⏳ kill -9 deferred to Phase 5 |
| SRV-09 | 04-07 | 3 | argon2id from packet 1 — sign-up writes `$argon2id$...`; verify roundtrip | int | `pnpm --filter @rebno/server test auth` | ✅ `apps/server/test/auth.integ.test.ts` | ✅ green |
| SRV-10 | 04-10 | 3 | Legacy first-login: plaintext row → silent rehash + drop staging in 1 txn | int | `pnpm --filter @rebno/server test legacy-login` | ✅ `apps/server/test/legacy-login.integ.test.ts` | ✅ green |
| SRV-11 | 04-10 | 3 | bcrypt-weak/plaintext → force-password-change overlay path | int | `pnpm --filter @rebno/server test legacy-login` | ✅ same file | ✅ green |
| SRV-12 | 04-11 | 3 | Admin-stubs throw NotImplemented; no Ctrl+E surface anywhere | unit + grep | `pnpm --filter @rebno/server test admin-stubs` + `pnpm lint:no-clipboard-rce` | ✅ `apps/server/test/admin-stubs.integ.test.ts` + `tools/scripts/lint-no-clipboard-rce.mjs` | ✅ green |
| SRV-13 | 04-12 (W3 runtime) + 04-12b (W4 lint+ADR) | 3 / 4 | RoomRegistry: drop layout file → fs.watch fires → broadcast w/ valid Ed25519 sig; client packages have ZERO room JSON in dist | int | `pnpm --filter @rebno/server test room-hot-reload` | ✅ `apps/server/test/room-hot-reload.integ.test.ts` | ✅ green |
| SRV-14 | 04-02 (protocol shape) + 04-03 (simulation determinism) | 2 | PlatformState in RoomState even when zero platforms; step() advances platforms deterministically | unit | `pnpm --filter @rebno/protocol test schema-shape` + `pnpm --filter @rebno/game-logic test platform-cycle` | ✅ `packages/protocol/test/schema-shape.test.ts` + `packages/game-logic/test/platform-cycle.test.ts` | ✅ green |
| ADR 0004 | 04-12b | 4 | Hot-reload contract committed | doc | `pnpm trace:check` (verifies `[doc->REQ-SRV-13]` in ADR) + `pnpm lint:adr:0004` | ✅ `docs/adr/0004-room-hot-reload.md` | ✅ green |
| Verify gate | 04-13 | 4 | All of the above + 6 lints + ADR 0004 lint + drizzle emit-check + trace:check | composite | `pnpm verify:phase-4` | ✅ `scripts/verify-phase-4.mjs` (14 canonical steps locked by `scripts/verify-phase-4.test.mjs`) | ✅ green (Linux CI canonical; Windows local fails at Phase-3 carry-over due to pre-existing CRLF drift — see Deferred Issues) |

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

---

## Wave 0 Requirements

All Wave-0 deliverables landed in Plan 04-01 (commit `7588f08`):

- [x] `pnpm-workspace.yaml` — declares `apps/*`, `packages/*`
- [x] Promote `tools/db-schema/src/tables.ts` → `packages/db/src/tables.ts` (per RESEARCH O-01) — `git mv` preserved history
- [x] `apps/server/{package.json, tsconfig.json, vitest.config.ts}`
- [x] `packages/protocol/{package.json, tsconfig.json, vitest.config.ts}`
- [x] `packages/game-logic/{package.json, tsconfig.json, vitest.config.ts}`
- [x] `packages/db/{package.json, tsconfig.json, vitest.config.ts}` (post-promotion)
- [x] `packages/protocol/test/codec.test.ts` — SRV-01 stubs (Wave 0) → real assertions in 04-02
- [x] `packages/game-logic/test/golden.test.ts` — SRV-02 stubs (Wave 0) → real assertions in 04-03
- [x] `packages/game-logic/test/platform-cycle.test.ts` — SRV-14 stubs (Wave 0) → real assertions in 04-03
- [x] `apps/server/test/{boot,authority,tick-accumulator,reconnect,rate-limit,sigterm,auth,legacy-login,admin-stubs,room-hot-reload}.{,integ.}test.ts` — SRV-03..SRV-14 stubs (Wave 0) → real assertions in 04-05..04-12
- [x] `apps/server/test/test-utils.ts` — shared helpers (spawnServer fixture, fakeClient ws+msgpackr, tmpdir for room files, in-memory SQLite)
- [x] Framework install: `pnpm -w add -D vitest@4.1.5`
- [x] Browser-mode setup for SRV-02 cross-runtime determinism (vitest browser-mode w/ Playwright provider — Plan 04-03)
- [x] `scripts/verify-phase-4.mjs` composite gate — final canonical 14-step form locked in 04-13
- [x] `.github/workflows/verify-phase-4.yml` (CI workflow — single-root install pattern)
- [x] Lint scripts referenced by composite gate: `lint-protocol-sync.mjs` (04-02), `lint-game-logic-purity.mjs` (04-03), `lint-better-auth-schema-sync.mjs` (04-07), `lint-rate-limit-budgets.mjs` (04-08), `lint-no-clipboard-rce.mjs` (04-11), `lint-room-layout.mjs` (04-12b), `lint-adr:0004` (04-12b)

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Two ws clients move + chat in same room over deployed-style boot | SRV-03 + SRV-04 + SRV-13 (smoke) | End-to-end multi-client UX feel; integration tests cover protocol surface but not jitter/visual smoothness | Boot `pnpm --filter @rebno/server start`; open two `wscat` or scripted clients; send move intents from both; verify state-diff envelopes contain both players; verify chat round-trips |
| kill -9 mid-tick recoverability on staging | SRV-08 | OS signal semantics differ test-vs-prod; `kill -9` cannot be sent to a child by a unit test on Windows reliably | **DEFERRED to Phase 5 first Fly.io staging deploy.** Verbatim procedure recorded in `04-09-SUMMARY.md` § "Manual Verification (Phase 5 Debt)". Phase 5 owner: SRV-08 Phase-5 verification debt closure. |
| argon2id memory parameter feel under load | SRV-09 | Memory-cost tuning is workload-dependent; CI machines differ from prod | Run `apps/server/scripts/argon2-bench.ts` on a representative box; assert hash time ∈ [200ms, 500ms] target band |

---

## Composite Gate (Final, Locked)

`scripts/verify-phase-4.mjs` runs the following 14 steps in canonical order
(asserted by `scripts/verify-phase-4.test.mjs` via deepEqual on the labels
list — any addition/removal/reorder fails the gate-test):

1. Phase 3 carry-over: verify-phase-3
2. Workspace: typecheck
3. Workspace: test
4. Drizzle: emit-check
5. Drizzle: schema-sync
6. Drizzle: source-comments
7. Lint: protocol-sync
8. Lint: game-logic-purity
9. Lint: better-auth-schema-sync
10. Lint: rate-limit-budgets
11. Lint: no-clipboard-rce
12. Lint: room-layout
13. ADR 0004 lint
14. Traceable-reqs: check

Phase 3 carry-over is invariantly first (Phase 4 must not regress Phase 3).
Traceable-reqs check is invariantly last (depends on every prior step's
tags being committed). The middle ordering groups workspace surface →
DB/drizzle gates → Phase-4 lints (in plan-introduced order).

---

## Phase 4 Trace Coverage

`pnpm trace:check` exits non-zero overall (other phases' `doc` requirements
remain incomplete and pre-existing planner-template prose surfaces
parse_error / undeclared_id findings). For the **Phase 4 SRV-01..SRV-14
scope** the gate is GREEN — every requirement reports `+doc +impl +unit +int`
across all four required stages:

```
[OK] REQ-SRV-01..14 required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
```

Pre-existing trace findings persist as Phase-5 verification debt (CLAUDE.md
explicitly defers `pnpm trace:check` from a CI hard-gate to Phase 5
DEP-04). See § "Pre-Existing Findings" below.

---

## Pre-Existing Findings (Phase-5 Verification Debt)

Carried over from `deferred-items.md` and the per-plan SUMMARYs. Plan 04-13
**does not** mutate the committed plan/SUMMARY content to chase these,
because (a) the findings are instructional planner-template prose
(literal `[<stage>->REQ-ID]` shapes and `REQ-SRV-XX` placeholders shown as
examples, not as tagging tokens), (b) rewriting committed plan content
to make a non-blocking lint happy is revisionist, and (c) CLAUDE.md
explicitly defers trace:check to Phase 5 DEP-04 as a CI hard-gate.

| Finding | Locations | Disposition |
|---|---|---|
| `parse_error` Tag-shaped tokens (e.g. `[<stage>->...]` template prose) | 21 occurrences across plans 04-01, 04-02, 04-03, 04-05..04-12, deferred-items.md, 04-09-SUMMARY.md | Phase-5 debt — escape pattern lint OR upgrade traceable-reqs |
| `undeclared_id REQ-SRV-XX` placeholders | 16 occurrences across 04-01-PLAN/SUMMARY and 04-13-PLAN | Phase-5 debt — same disposition |
| `missing_stage` for non-Phase-4 reqs (AST/CDOC/CLI/DEP/EXT/PAR/SDOC) | 47 occurrences | Phase-5/6/7 debt — those phases close their own stages |
| `pnpm protocol-doc:verify` Windows CRLF drift | Phase-3 artifact | Phase-3 deferral; Linux CI is canonical |
| `traceable-reqs 0.1.0` doesn't scan `apps/**` | scanner limitation | Phase-5 DEP-04 evaluates a version bump or sidecar; SUMMARY-tagging convention used as workaround |

---

## Schema-push Gate (Plan 04-13 Final Audit)

Final invariant check: `ls packages/db/migrations/` returns
`0001_baseline.sql` only. Phase 4 introduces ZERO new SQL migrations
beyond the Phase 3 baseline.

- Plan 04-04 schema-push gate: passed.
- Plan 04-07 Better-Auth integration used `auth-tables` codegen as a
  TypeScript shim only — no migration emission. The auth-tables.ts file
  exposes Drizzle definitions that reuse Phase-3 baseline tables for
  the persistence layer and add no new tables to the DB.
- Plan 04-13 final audit: confirmed `packages/db/migrations/` contains
  exactly `0001_baseline.sql` plus the `meta/` directory.

The orchestrator's "no new migrations beyond Phase 3 baseline" invariant
**held**. No `## Phase 4 Deviations — Schema-push` section is needed.

---

## Validation Sign-Off

- [x] All Phase 4 tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all ❌ MISSING references in the verification map
- [x] No watch-mode flags in CI/composite-gate commands
- [x] Feedback latency < 60 s for per-task quick run
- [x] `nyquist_compliant: true` set in frontmatter
- [x] Per-Task Verification Map Plan IDs filled
- [x] All Phase-4 SRV-01..SRV-14 four-stage trace coverage closed
- [x] CI workflow `.github/workflows/verify-phase-4.yml` finalized on ubuntu-latest

**Approval:** approved.
