# Phase 4: Server Rebuild (MVP) — Pattern Map

**Mapped:** 2026-05-06
**Files analyzed:** 30 (workspace bootstrap + 4 packages + apps/server + tools/room-converter + 4 lints + ADR + verify gate + CI workflow)
**Analogs found:** 28 / 30 (2 net-new files have no direct analog and follow Pattern blocks from RESEARCH.md instead)

This map answers, file-by-file, "What existing code in this repo should the new file copy patterns from?" The closest analogs are concentrated in:
- `tools/db-schema/` — TS package shape, drizzle-kit wiring, vitest 4.1.5, schema-shape test pattern, source-comment-lint script (PROMOTES to `packages/db/` per O-01)
- `tools/extract-gmd/`, `tools/protocol-doc/`, `tools/save-format-doc/`, `tools/asset-catalog/` — `package.json` scripts shape, `cli.ts` dispatcher pattern, fixtures-and-output-emit conventions, lint-script template
- `tools/protocol-doc/scripts/lint-protocol.mjs` + `tools/save-format-doc/scripts/lint-save-formats.mjs` + `tools/db-schema/scripts/check-source-comments.mjs` + `tools/asset-catalog/scripts/lint-parity-checklist.mjs` — four canonical lint-script shapes for D-25's four new lints
- `scripts/verify-phase-3.mjs` + `.github/workflows/verify-phase-3.yml` — composite-gate template
- `docs/adr/0002-persistence-layer.md`, `0003-canonical-snapshot.md` — ADR template for `0004-room-hot-reload.md`

## File Classification

| New / Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---------------------|------|-----------|----------------|---------------|
| `pnpm-workspace.yaml` | workspace-config | n/a (build-time) | (none — net-new; mirrors STACK.md / D-18) | none |
| `package.json` (root, modified) | workspace-config | n/a | `package.json` (root, current) | exact (modify-in-place) |
| `tsconfig.base.json` (new, optional per planning) | ts-config | n/a (compile-time) | `tools/db-schema/tsconfig.json` | exact |
| `packages/db/package.json` | package-config | n/a | `tools/db-schema/package.json` | exact (PROMOTE per O-01) |
| `packages/db/tsconfig.json` | ts-config | n/a | `tools/db-schema/tsconfig.json` | exact |
| `packages/db/drizzle.config.ts` | tooling-config | n/a | `tools/db-schema/drizzle.config.ts` | exact (relocate) |
| `packages/db/vitest.config.ts` | test-config | n/a | `tools/db-schema/vitest.config.ts` | exact |
| `packages/db/src/tables.ts` | runtime-source (schema) | n/a | `tools/db-schema/src/tables.ts` | exact (MOVE per O-01) |
| `packages/db/src/auth-tables.ts` | runtime-source (schema) | n/a (codegen) | `tools/db-schema/src/tables.ts` (style) | role-match (generated by `@better-auth/cli`) |
| `packages/db/src/index.ts` | runtime-source (barrel) | n/a | `tools/db-schema/src/index.ts` | exact |
| `packages/db/tests/schema-shape.test.ts` (relocated) | test (unit) | n/a | `tools/db-schema/tests/schema-shape.test.ts` | exact (relocate) |
| `packages/db/migrations/0001_baseline.sql` (relocated) | migration | n/a | `tools/db-schema/migrations/0001_baseline.sql` | exact (relocate) |
| `packages/protocol/package.json` | package-config | n/a | `tools/db-schema/package.json` (workspace deps) | role-match |
| `packages/protocol/tsconfig.json` | ts-config | n/a | `tools/db-schema/tsconfig.json` | exact |
| `packages/protocol/src/state.ts` | runtime-source (Schema classes) | state-diff (server→client) | (none — net-new; uses RESEARCH §Pattern 1 + Pattern 6) | none |
| `packages/protocol/src/intents.ts` | runtime-source (zod) | request-response (c2s) | `tools/save-format-doc/src/types.ts` (zod-style discipline) | role-match |
| `packages/protocol/src/events.ts` | runtime-source (msgpackr) | event-driven (s2c one-shot) | (none — net-new; uses RESEARCH §Pattern 1) | none |
| `packages/protocol/src/version.ts` | runtime-source (constant) | n/a | `tools/extract-gmd/src/types.ts` (export-only module) | partial |
| `packages/protocol/src/legacy-opcodes.ts` | build-time-copy | n/a | `tools/protocol-doc/output/protocol.ts` (the SOURCE of the copy) | exact (consumer side of copy) |
| `packages/protocol/scripts/sync-from-tools-protocol-doc.mjs` | build-script | file-I/O | `tools/db-schema/scripts/check-source-comments.mjs` (script style) | role-match |
| `packages/protocol/tests/*.test.ts` | test (unit, round-trip) | n/a | `tools/db-schema/tests/schema-shape.test.ts` | role-match |
| `packages/game-logic/package.json` | package-config | n/a | `tools/db-schema/package.json` (test-only deps) | role-match |
| `packages/game-logic/tsconfig.json` | ts-config | n/a | `tools/db-schema/tsconfig.json` | exact |
| `packages/game-logic/src/step.ts` | runtime-source (pure simulation) | transform (state, inputs, dt → state) | `tools/extract-gmd/src/extract.ts` (pure-function discipline) | partial |
| `packages/game-logic/src/rng.ts` | runtime-source (pure RNG) | transform | (none — net-new; splitmix64 from RESEARCH) | none |
| `packages/game-logic/test/golden.test.ts` | test (golden) | n/a | `tools/db-schema/tests/schema-shape.test.ts` (vitest setup) | role-match |
| `apps/server/package.json` | app-config | n/a | `tools/db-schema/package.json` + RESEARCH installation block | role-match |
| `apps/server/tsconfig.json` | ts-config | n/a | `tools/db-schema/tsconfig.json` | exact |
| `apps/server/src/index.ts` | runtime-source (boot) | event-driven (server lifecycle) | `tools/extract-gmd/cli.ts` (dispatch shape) | partial (boot, not CLI) |
| `apps/server/src/RebnoRoom.ts` | runtime-source (Colyseus Room) | request-response + state-diff | (none — net-new; uses RESEARCH §Pattern 3, 4, 5) | none |
| `apps/server/src/RoomRegistry.ts` | runtime-source (subsystem) | event-driven (fs.watch + broadcast) | (none — net-new; uses RESEARCH §Pattern 6) | none |
| `apps/server/src/auth.ts` | runtime-source (Better-Auth) | request-response (HTTP) | (none — net-new; uses RESEARCH §Pattern 4) | none |
| `apps/server/src/db.ts` | runtime-source (DB singleton) | n/a | (none — net-new; uses RESEARCH §"Verified: Litestream synchronous=NORMAL guidance") | none |
| `apps/server/src/sigterm.ts` | runtime-source (lifecycle) | event-driven | (none — net-new; uses RESEARCH §Pattern 8) | none |
| `apps/server/src/rate-limit.ts` | runtime-source (utility) | event-driven | (none — net-new; uses RESEARCH §Pattern 7) | none |
| `apps/server/src/admin-stubs.ts` | runtime-source (stubs) | n/a | `docs/extracted-server/admin-anti-port.md` (source) | exact (consumer of doc) |
| `apps/server/src/env.ts` | runtime-source (config) | n/a | `tools/db-schema/src/index.ts` (export-only barrel style) | partial |
| `apps/server/scripts/migrate-legacy-accounts.ts` | one-shot CLI | batch (file → DB) | `tools/extract-gmd/cli.ts` | role-match |
| `apps/server/rooms/mvp-lobby/000.json` + `000.sig` | data artifact | n/a | `tools/save-format-doc/fixtures/User_DBUpdated.bnu` (fixture-shape) | role-match |
| `apps/server/Dockerfile` | dev-config | n/a | (none — net-new; defer Phase 5 hardening) | none |
| `apps/server/.env.example` | dev-config | n/a | (none — net-new) | none |
| `apps/server/tests/**` | test (integration) | n/a | `tools/extract-gmd/tests/` (vitest integration include/exclude split) | role-match |
| `tools/room-converter/package.json` | tool-config | n/a | `tools/save-format-doc/package.json` | exact |
| `tools/room-converter/tsconfig.json` | ts-config | n/a | `tools/save-format-doc/tsconfig.json` (≡ `tools/db-schema/tsconfig.json`) | exact |
| `tools/room-converter/cli.ts` | tool-CLI | batch (extracted → JSON+sig) | `tools/extract-gmd/cli.ts` AND `tools/protocol-doc/cli.ts` | exact |
| `tools/room-converter/src/types.ts` | tool-source | n/a | `tools/extract-gmd/src/types.ts` | exact |
| `tools/scripts/lint-protocol-sync.mjs` | lint-script | file-I/O | `tools/protocol-doc/scripts/lint-protocol.mjs` | exact |
| `tools/scripts/lint-game-logic-purity.mjs` | lint-script | file-I/O (grep-style) | `tools/db-schema/scripts/check-source-comments.mjs` | exact |
| `tools/scripts/lint-room-layout.mjs` | lint-script | file-I/O | `tools/save-format-doc/scripts/lint-save-formats.mjs` | exact |
| `tools/scripts/lint-better-auth-schema-sync.mjs` | lint-script | file-I/O (drift-guard) | `tools/db-schema/scripts/check-source-comments.mjs` + `lint:schema-sync` inline node script in root `package.json:32` | exact |
| `scripts/verify-phase-4.mjs` | composite-gate | spawn-orchestrator | `scripts/verify-phase-3.mjs` | exact |
| `scripts/verify-phase-4.test.mjs` | gate-test | spawn-mock | `scripts/verify-phase-3.test.mjs` | exact |
| `.github/workflows/verify-phase-4.yml` | CI-workflow | n/a | `.github/workflows/verify-phase-3.yml` | exact |
| `docs/adr/0004-room-hot-reload.md` | ADR | n/a | `docs/adr/0003-canonical-snapshot.md` | exact |

## Pattern Assignments

### `pnpm-workspace.yaml` (workspace-config — NEW)

**No analog.** This file does not exist in the repo. Net-new.

**Source for shape:** `04-CONTEXT.md` D-18 + `04-RESEARCH.md` §"Recommended CONTEXT.md Overrides" O-01.

**Authoritative content (per D-18):**
```yaml
packages:
  - 'apps/*'
  - 'packages/*'
  # NOTE: tools/* deliberately excluded — preserves Phase 1 D-17 / Phase 2 D-15
  # boundary that tools are standalone, not workspace members. Their outputs
  # flow into the workspace via build-time copy (D-19).
```

---

### `package.json` (root — MODIFIED)

**Analog (and source-to-modify):** `C:\Users\decid\Documents\projects\rebno\package.json` (current — read in full above)

**Imports/scripts pattern to keep** (lines 6–35):
- Existing `extract:*`, `catalog:*`, `protocol-doc:*`, `save-format-doc:*`, `lint:*`, `db:*`, `verify:phase-3`, `trace:*` scripts MUST stay (Phase 1/2/3 contracts).

**Additive scripts to introduce** (mirror the existing shape; one-line per script, kebab-case):
```json
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "pnpm -r test",
"dev:server": "pnpm --filter @rebno/server dev",
"migrate:legacy-accounts": "pnpm --filter @rebno/server exec tsx scripts/migrate-legacy-accounts.ts",
"room:edit": "pnpm --filter room-converter exec tsx cli.ts edit",
"db:auth:gen": "pnpm exec @better-auth/cli generate --output packages/db/src/auth-tables.ts",
"lint:protocol-sync": "node tools/scripts/lint-protocol-sync.mjs",
"lint:game-logic-purity": "node tools/scripts/lint-game-logic-purity.mjs",
"lint:room-layout": "node tools/scripts/lint-room-layout.mjs",
"lint:better-auth-schema-sync": "node tools/scripts/lint-better-auth-schema-sync.mjs",
"verify:phase-4": "node scripts/verify-phase-4.mjs"
```

**Critical updates to existing scripts** (per O-01 promotion):
- `db:generate` — change `tools/db-schema` → `packages/db`
- `db:test` — change `tools/db-schema` → `packages/db`
- `db:emit-check` — update both source and dest paths
- `lint:schema-sync` — update both paths (the inline `node -e` script on line 32)
- `lint:source-comments` — change `tools/db-schema` → `packages/db`

---

### `packages/db/*` — PROMOTION from `tools/db-schema/` (per O-01)

**Action: MOVE** the following files in-place (preserves Phase 3 D-13 schema verbatim):

| Old path | New path |
|----------|----------|
| `tools/db-schema/src/tables.ts` | `packages/db/src/tables.ts` |
| `tools/db-schema/src/index.ts` | `packages/db/src/index.ts` |
| `tools/db-schema/migrations/0001_baseline.sql` | `packages/db/migrations/0001_baseline.sql` |
| `tools/db-schema/tests/*.test.ts` | `packages/db/tests/*.test.ts` |
| `tools/db-schema/drizzle.config.ts` | `packages/db/drizzle.config.ts` |
| `tools/db-schema/vitest.config.ts` | `packages/db/vitest.config.ts` |
| `tools/db-schema/scripts/check-source-comments.mjs` | `packages/db/scripts/check-source-comments.mjs` |

**`packages/db/package.json`** — derive from `tools/db-schema/package.json` (read in full above):

Copy lines 1–22 verbatim with these field deltas:
- `"name": "db-schema"` → `"name": "@rebno/db"` (matches A10 convention)
- Append `"description": "Phase 4 SRV-01..03 runtime DB package..."` (replace the Phase-3-staging copy on line 6)
- Add `"main": "./src/index.ts"` and `"exports": { ".": "./src/index.ts" }` (workspace consumption)
- Promote `drizzle-orm` from `devDependencies` to `dependencies` (Phase 4 imports it at runtime; Phase 3 only emitted SQL)
- Add `dependencies: { "better-sqlite3": "12.9.0" }` for runtime
- Add `scripts.auth:gen: "pnpm exec @better-auth/cli generate --output src/auth-tables.ts"`

**`packages/db/tsconfig.json`** — copy `tools/db-schema/tsconfig.json` byte-identical (it's already correct: target ES2022, module NodeNext, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes per Phase 1 D-17 lock).

**`packages/db/vitest.config.ts`** — copy `tools/db-schema/vitest.config.ts` byte-identical (forks pool, maxWorkers 1).

**`packages/db/src/index.ts` — extend** the existing 1-line re-export with auth-tables:
```typescript
// CURRENT content (tools/db-schema/src/index.ts:7):
export * from './tables.js';
// EXTEND to:
export * from './tables.js';
export * from './auth-tables.js'; // Phase 4 D-08 — generated by @better-auth/cli
```

**`packages/db/src/auth-tables.ts`** — generated, NOT hand-authored. Generation command (per `04-CONTEXT.md` D-08):
```bash
pnpm exec @better-auth/cli generate --output packages/db/src/auth-tables.ts
```
Style discipline of generated file (citing `tools/db-schema/src/tables.ts` lines 20–46 as the in-repo Drizzle-style reference): `sqliteTable('<name>', { ... })`; `text/integer/blob` column constructors; per-column `// SOURCE:` comments are NOT required for generated rows (the lint must be configured to ignore `auth-tables.ts` — see `lint-better-auth-schema-sync.mjs` below).

---

### `packages/protocol/*` (NEW workspace package)

**Analog for package shape:** `tools/db-schema/package.json` + `tsconfig.json` + `vitest.config.ts` (read above).

**`packages/protocol/package.json`** — derive from `tools/db-schema/package.json`:
- `"name": "@rebno/protocol"`
- Add `dependencies: { "@colyseus/schema": "4.0.23", "msgpackr": "1.11.10", "zod": "^3.23" }` (per RESEARCH §Standard Stack)
- `scripts.prebuild: "node scripts/sync-from-tools-protocol-doc.mjs"` (D-19 build-time copy)
- `scripts.test: "vitest run"`
- `scripts.typecheck: "tsc --noEmit"`

**`packages/protocol/src/state.ts`** — net-new. Copy literal Schema-class structure from `04-RESEARCH.md` lines 268–294:

```typescript
import { Schema, MapSchema, type } from "@colyseus/schema";

export class PlayerState extends Schema {
  @type("string") account_id: string;
  @type("string") name: string;
  @type("number") room_id: number;
  @type("number") x: number;
  @type("number") y: number;
  @type("number") vx: number;
  @type("number") vy: number;
  @type("number") sprite_id: number;
  @type("number") last_input_seq: number;
}

export class PlatformState extends Schema {
  @type("string") id: string;
  @type("number") x: number;
  @type("number") y: number;
  @type("number") vx: number;
  @type("number") vy: number;
}

export class RoomState extends Schema {
  @type("number") rev: number = 0;
  @type({ map: PlayerState }) players = new MapSchema<PlayerState>();
  @type({ map: PlatformState }) platforms = new MapSchema<PlatformState>();
}
```

**`packages/protocol/src/intents.ts`** — net-new (zod schemas for c2s.* per D-04).

**`packages/protocol/src/events.ts`** — net-new (msgpackr encode/decode helpers per D-02).

**`packages/protocol/src/version.ts`** — net-new (single export):
```typescript
// Source: 04-CONTEXT.md D-03 — uint16 first field of c2s.auth; bumping = deploy ritual
export const PROTOCOL_VERSION = 1;
```

**`packages/protocol/src/legacy-opcodes.ts`** — build-time copy of `tools/protocol-doc/output/protocol.ts` (existing, committed). The `prebuild` script below writes it; `lint-protocol-sync.mjs` exits non-zero if stale.

**`packages/protocol/scripts/sync-from-tools-protocol-doc.mjs`** — net-new copy script. Skeleton mirrors the inline `lint:schema-sync` script in root `package.json:32` (already an in-repo file-copy + diff-check pattern):

Structure (concrete):
```javascript
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

const SRC = fileURLToPath(new URL('../../../tools/protocol-doc/output/protocol.ts', import.meta.url));
const DST = fileURLToPath(new URL('../src/legacy-opcodes.ts', import.meta.url));
const HEADER = '// AUTO-GENERATED by packages/protocol/scripts/sync-from-tools-protocol-doc.mjs\n// Source: tools/protocol-doc/output/protocol.ts (committed)\n// DO NOT EDIT — run `pnpm --filter @rebno/protocol prebuild` to regenerate.\n\n';
writeFileSync(DST, HEADER + readFileSync(SRC, 'utf-8'));
console.log(`synced legacy-opcodes from ${SRC}`);
```

---

### `packages/game-logic/*` (NEW workspace package)

**Analog for package shape:** `tools/db-schema/` (test-only devDeps, no runtime deps).

**`packages/game-logic/package.json`** — derive from `tools/db-schema/package.json`:
- `"name": "@rebno/game-logic"`
- Empty `dependencies` (purity per D-25; no I/O, no network, no clock — game-logic is a pure transform)
- `devDependencies`: `vitest@4.1.5`, `typescript@5.6.3`, `@types/node@25.6.0`

**`packages/game-logic/tsconfig.json`** — copy `tools/db-schema/tsconfig.json` byte-identical.

**`packages/game-logic/src/step.ts`** — net-new, but the FUNCTION-PURITY discipline mirrors the deterministic-emitter pattern enforced in `tools/extract-gmd/src/extract.ts` and the source-order/sorted-key discipline already established by Phase 1 D-15. Use `04-RESEARCH.md` §Pattern 2 + §Pattern 3 + §Pattern 8 verbatim signature:
```typescript
export function step(
  state: WorldState,
  inputs: ReadonlyMap<string /* account_id */, InputFrame>,
  dt_ms: number, // ALWAYS 50 — accumulator caller enforces this
): WorldState
```

**`packages/game-logic/test/golden.test.ts`** — net-new. Vitest setup mirrors `tools/db-schema/tests/schema-shape.test.ts` lines 1–8 (read above):
```typescript
import { describe, it, expect } from 'vitest';
import { step } from '../src/step.js';
```

---

### `apps/server/*` (NEW workspace app)

**Analog for package shape:** `tools/db-schema/package.json` (workspace shape) + `04-RESEARCH.md` §Standard Stack §Installation block (lines 152–173) for the runtime deps.

**`apps/server/package.json`** — net-new but copy the shape conventions from `tools/db-schema/package.json:1–22`:
- `"name": "@rebno/server"`, `"private": true`, `"type": "module"`
- `dependencies`: `colyseus@0.17.10`, `@colyseus/schema@4.0.23`, `ws@8.20.0`, `better-auth@1.6.9`, `argon2@0.44.0`, `better-sqlite3@12.9.0`, `drizzle-orm@0.45.2`, `pino@^9`, `msgpackr@1.11.10`, `zod@^3.23`, `express@^4`, `cookie-parser@^1`, `@rebno/protocol: workspace:*`, `@rebno/game-logic: workspace:*`, `@rebno/db: workspace:*`
- `devDependencies`: `tsx@4.21.0`, `vitest@4.1.5`, `typescript@5.6.3`, `@types/node@25.6.0`, `@types/better-sqlite3`, `@types/express`
- `scripts`:
  - `dev`: `tsx watch src/index.ts`
  - `build`: `tsc`
  - `start`: `node dist/index.js`
  - `test`: `vitest run --exclude 'tests/integration/**'`
  - `test:integration`: `vitest run tests/integration/`
  - `test:full`: `vitest run`
  - `typecheck`: `tsc --noEmit`

**`apps/server/tsconfig.json`** — copy `tools/db-schema/tsconfig.json` byte-identical (strict triple is the project lock).

**`apps/server/src/index.ts`** — net-new boot sequence per D-21 + RESEARCH §Component Responsibilities table line 231. Use the dispatcher-skeleton shape of `tools/extract-gmd/cli.ts:1-30` for the top-level `main(argv)` async function + early-error pattern, but the body is the boot sequence (env → keys → db → migrations → Express → Colyseus → RoomRegistry.scan() → SIGTERM → tick → /health → log ready), NOT a CLI dispatcher.

**`apps/server/src/RebnoRoom.ts`** — net-new. Copy verbatim the skeleton from `04-RESEARCH.md` §Pattern 3 (lines 366–394) + §Pattern 4 onAuth excerpt (lines 444–457) + §Pattern 5 onLeave (lines 466–482) + §"Verified: Colyseus 0.17 Room skeleton" (lines 783–797).

**`apps/server/src/RoomRegistry.ts`** — net-new. Copy the skeleton from `04-RESEARCH.md` §Pattern 6 (lines 491–533).

**`apps/server/src/auth.ts`** — net-new. Copy the betterAuth + drizzleAdapter + custom argon2id `password.{hash,verify}` hook from `04-RESEARCH.md` §Pattern 4 (lines 403–436) + §"Verified: Better-Auth custom argon2id hook" (lines 759–775).

**`apps/server/src/db.ts`** — net-new. Copy verbatim from `04-RESEARCH.md` §"Verified: Litestream synchronous=NORMAL guidance" (lines 813–824):
```typescript
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from "@rebno/db";

const sqlite = new Database(process.env.DATABASE_URL ?? "/data/rebno.db");
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("synchronous = NORMAL");
sqlite.pragma("foreign_keys = ON");
sqlite.pragma("busy_timeout = 5000");

export const db = drizzle(sqlite, { schema });
```

**`apps/server/src/sigterm.ts`** — net-new. Copy verbatim from `04-RESEARCH.md` §Pattern 8 (lines 592–614).

**`apps/server/src/rate-limit.ts`** — net-new. Copy the `TokenBucket` class verbatim from `04-RESEARCH.md` §Pattern 7 (lines 542–583).

**`apps/server/src/admin-stubs.ts`** — net-new. Source: `docs/extracted-server/admin-anti-port.md` (Phase 3 D-20 — already committed). Copy each modernized intent shape verbatim into a typed `interface`/`type` declaration; each handler body is `throw new Error('admin endpoints not implemented in Phase 4 — see Phase 7 PAR-07');`. NO behavior, only typed stubs (CLAUDE.md hard rule #3).

**`apps/server/scripts/migrate-legacy-accounts.ts`** — net-new one-shot CLI.

**Closest analog:** `tools/extract-gmd/cli.ts:1–30` for the CLI dispatch + exit-code matrix shape.

**Imports pattern** (lines 1–4 mirror `tools/extract-gmd/cli.ts:14–16`):
```typescript
#!/usr/bin/env node
import { fileURLToPath } from 'node:url';
import { db } from '../src/db.js';
import { legacyCredentialsStaging } from '@rebno/db';
import { parseLocalListTxt } from 'tools-save-format-doc-output/save-formats.js';
// ↑ exact import path TBD at planning; the file is committed at
//   tools/save-format-doc/output/save-formats.ts per Phase 3 plan 03-03
```

**Exit code matrix** (mirror `tools/extract-gmd/cli.ts:1–13`):
```
0  success (rows imported)
1  functional failure (file not found, parse error, db error)
2  usage error (missing arg, --help)
```

---

### `apps/server/rooms/mvp-lobby/000.json` + `000.sig` (data artifact — NEW)

**Closest analog:** `tools/save-format-doc/fixtures/User_DBUpdated.bnu` (committed fixture for tests). Same role: a stable on-disk artifact under version control, consumed by tests + runtime.

**Generation procedure** (D-12 + D-09):
1. Pick smallest navigable room from `extracted/client-5-8/rooms/` (planning task).
2. Run `pnpm room:edit mvp-lobby` (the `tools/room-converter` CLI, see below).
3. The CLI writes `000.json` + `000.sig` atomically.

**`000.json` schema** (per D-09): `{ tile_grid, collision_polys, spawn_points, platform_defs, scripted_triggers, room_size, tile_atlas_ref, bg_atlas_ref }` — locked by zod schema in `packages/protocol/src/intents.ts` (re-validated at runtime by `RoomRegistry`).

---

### `tools/room-converter/*` (NEW standalone tool — NOT in workspace per D-18)

**Analog for package shape:** `tools/save-format-doc/package.json` (read above — exact mirror).

**`tools/room-converter/package.json`** — copy `tools/save-format-doc/package.json:1–23` byte-identical with field deltas:
- `"name": "room-converter"`
- `"description": "Phase 4 D-09/D-13 — extracted GM5 rooms → canonical layout JSON + Ed25519-signed manifest. Standalone Node CLI per Phase 1 D-17."`
- `"bin": { "room-converter": "./cli.ts" }`
- `scripts`:
  - `convert`: `tsx cli.ts convert`
  - `edit`: `tsx cli.ts edit`
  - `verify`: `tsx cli.ts verify`
- `devDependencies`: same as save-format-doc plus optionally `zod@^3.23` (validation matches `packages/protocol`)

**`tools/room-converter/tsconfig.json`** — copy `tools/save-format-doc/tsconfig.json` (≡ `tools/db-schema/tsconfig.json`) byte-identical.

**`tools/room-converter/cli.ts`** — copy the dispatcher shape verbatim from `tools/protocol-doc/cli.ts:1-40` (read above):
- Same exit-code matrix (`0|1|2`) — Phase 1 D-18 contract.
- Same `printUsage()` structure with subcommands `convert | edit | verify`.
- Same `async function main(argv: string[]): Promise<number>` signature.

**`tools/room-converter/src/types.ts`** — net-new. Reuses `tools/extract-gmd/src/types.ts` `Room` interface (lines 1–18 of that file: `interface ProjectFile { ..., rooms?: Room[] }`) for the input side; defines `CanonicalLayout` for the output side per D-09 schema.

---

### `tools/scripts/lint-*.mjs` (4 NEW lint-scripts — D-25)

The directory `tools/scripts/` does not yet exist. Created in Phase 4. Each script's analog is one of the four established lint patterns in the repo:

#### `tools/scripts/lint-protocol-sync.mjs` — D-19 drift guard

**Closest analog:** `tools/protocol-doc/scripts/lint-protocol.mjs` (read lines 1–80 above).

**Imports pattern** (lines 1–13):
```javascript
#!/usr/bin/env node
// tools/scripts/lint-protocol-sync.mjs
// Source: 04-CONTEXT.md D-19 — drift guard for the build-time copy of
// tools/protocol-doc/output/protocol.ts → packages/protocol/src/legacy-opcodes.ts.

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
```

**Header / usage / exit pattern** (mirror lines 1–30):
```
Usage: node tools/scripts/lint-protocol-sync.mjs
Exit: 0 success, 1 drift detected, 2 usage error.
```

**Core check** — byte-compare source and copy:
```javascript
const src = readFileSync('tools/protocol-doc/output/protocol.ts', 'utf-8');
const copy = readFileSync('packages/protocol/src/legacy-opcodes.ts', 'utf-8');
// Strip the auto-gen header before compare.
if (!copy.endsWith(src)) {
  process.stderr.write('legacy-opcodes.ts is out of sync. Run `pnpm --filter @rebno/protocol prebuild`.\n');
  process.exit(1);
}
```

---

#### `tools/scripts/lint-game-logic-purity.mjs` — D-25 purity guard

**Closest analog:** `tools/db-schema/scripts/check-source-comments.mjs` (read lines 1–80 above) — same pattern: read source files in a directory, regex over each line, count violations, exit 1 on any.

**Imports + walk pattern** (mirror lines 1–24):
```javascript
#!/usr/bin/env node
// tools/scripts/lint-game-logic-purity.mjs
// Source: 04-CONTEXT.md D-25 — purity guard for packages/game-logic.
// Greps for forbidden APIs: Date.*, Math.random, process.*, fs.*, network APIs.
// Exit: 0 success, 1 forbidden API found.

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';

const FORBIDDEN = [
  /\bDate\s*\./,                          // Date.now(), Date.parse(), new Date()
  /\bMath\s*\.\s*random\b/,
  /\bprocess\s*\.[a-z]/i,                 // process.env, process.hrtime, etc.
  /\bfs\s*\./,                            // any fs API
  /\bfetch\s*\(/,
  /require\s*\(\s*['"]https?\b/,          // require('http')/('https')
];
```

**Core regex iteration** — mirrors lines 32–80 of `check-source-comments.mjs` (same line-by-line scan pattern).

---

#### `tools/scripts/lint-room-layout.mjs` — D-25 room validation

**Closest analog:** `tools/save-format-doc/scripts/lint-save-formats.mjs` (read lines 1–80 above).

**Imports + usage + walk pattern** (mirror lines 1–32):
```javascript
#!/usr/bin/env node
// tools/scripts/lint-room-layout.mjs
// Source: 04-CONTEXT.md D-25 — validates every apps/server/rooms/<room>/<rev>.json
// against the zod layout schema, verifies <rev>.sig companion exists, verifies
// Ed25519 signature against the server's pinned pubkey.

import { readFileSync, readdirSync } from 'node:fs';
import { createPublicKey, verify } from 'node:crypto';
```

**Schema enum pattern** (lines 42–52 of save-formats lint show the `Set` pattern for valid extensions / encodings — copy that same shape for valid room layout fields).

**Iteration loop** (lines 60–80) — same `for (const f of formats)` pattern, accumulating `errors` count, exit 1 if non-zero.

**Ed25519 verify** (per RESEARCH §Pattern 6 line 528):
```javascript
const ok = verify(null, Buffer.concat([Buffer.from(room_id), Buffer.from(rev), sha256(json)]), pubKey, sig);
```

---

#### `tools/scripts/lint-better-auth-schema-sync.mjs` — D-08/D-25 drift guard

**Closest analog:** the inline `lint:schema-sync` script in root `package.json:32` (already an in-repo byte-compare pattern). Hoist it into a named .mjs file using `tools/db-schema/scripts/check-source-comments.mjs` (lines 1–14) as the file-skeleton template.

**Pattern:** regenerate `auth-tables.ts` to a temp path, byte-compare against committed `packages/db/src/auth-tables.ts`, exit 1 on diff.

```javascript
#!/usr/bin/env node
// tools/scripts/lint-better-auth-schema-sync.mjs
// Source: 04-CONTEXT.md D-08/D-25 — Better-Auth schema regeneration drift guard.
// Regenerates auth-tables.ts, byte-compares against committed copy, exit 1 on diff.

import { spawnSync } from 'node:child_process';
import { readFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const tmp = mkdtempSync(join(tmpdir(), 'rebno-auth-'));
const tmpOut = join(tmp, 'auth-tables.ts');
const r = spawnSync('pnpm', ['exec', '@better-auth/cli', 'generate', '--output', tmpOut],
  { encoding: 'utf-8', shell: process.platform === 'win32' });
if (r.status !== 0) { process.stderr.write(r.stderr); process.exit(1); }
const fresh = readFileSync(tmpOut, 'utf-8');
const committed = readFileSync('packages/db/src/auth-tables.ts', 'utf-8');
if (fresh !== committed) {
  process.stderr.write('auth-tables.ts drift. Run `pnpm db:auth:gen`.\n');
  process.exit(1);
}
console.log('auth-tables.ts in sync.');
```

---

### `scripts/verify-phase-4.mjs` — composite gate (NEW)

**Closest analog:** `scripts/verify-phase-3.mjs` (read in full above — 78 lines, already in-repo).

**Imports + header pattern** (lines 1–13 — copy verbatim with the `3` → `4` rename):
```javascript
#!/usr/bin/env node
// scripts/verify-phase-4.mjs
// Source: Phase 4 SRV-01..14 composite verify gate.
//
// Sequential orchestrator running every Phase 4 lint + workspace test +
// schema-emit gate + ADR lint suite. First non-zero exit fails.
//
// Usage: node scripts/verify-phase-4.mjs   OR: pnpm verify:phase-4
// Exit: 0 success, 1 first-failed-step.

import { spawnSync } from 'node:child_process';

const isWindows = process.platform === 'win32';
```

**Steps array pattern** (lines 18–54 — copy the `[label, cmd, args]` tuple shape verbatim):
```javascript
const steps = [
  // Phase 3 carry-over (still must pass — phase-3 gate is a prerequisite)
  ['Phase 3 carry-over: verify-phase-3', 'pnpm', ['verify:phase-3']],
  // Workspace build + typecheck + test fan-out (Phase 4 first uses these)
  ['Workspace: build',                'pnpm', ['-r', 'build']],
  ['Workspace: typecheck',            'pnpm', ['-r', 'typecheck']],
  ['Workspace: test',                 'pnpm', ['-r', 'test']],
  // Phase 4 lint forcing-functions (D-25)
  ['Lint: protocol-sync',             'pnpm', ['lint:protocol-sync']],
  ['Lint: game-logic-purity',         'pnpm', ['lint:game-logic-purity']],
  ['Lint: room-layout',               'pnpm', ['lint:room-layout']],
  ['Lint: better-auth-schema-sync',   'pnpm', ['lint:better-auth-schema-sync']],
  // ADR 0004 lint
  ['ADR 0004 lint',                   'pnpm', ['lint:adr:0004']],
  // Drizzle emit-check (paths now point at packages/db/)
  ['Drizzle: emit-check',             'pnpm', ['db:emit-check']],
  ['Drizzle: schema-sync',            'pnpm', ['lint:schema-sync']],
  ['Drizzle: source-comments',        'pnpm', ['lint:source-comments']],
];
```

**For-loop + failure-capture + exit pattern** (lines 56–77 — copy verbatim).

---

### `scripts/verify-phase-4.test.mjs` — gate-test (NEW)

**Closest analog:** `scripts/verify-phase-3.test.mjs` (committed alongside verify-phase-3.mjs).

Action: copy file byte-identical, replace `3` → `4` in path/label references.

---

### `.github/workflows/verify-phase-4.yml` — CI workflow (NEW)

**Closest analog:** `.github/workflows/verify-phase-3.yml` (read in full above).

**Header + name pattern** (lines 1–10 — copy verbatim with `3` → `4`):
```yaml
# .github/workflows/verify-phase-4.yml
# Source: Phase 4 SRV-01..14 composite verify gate.
# Triggered on changes to Phase 4 artifacts; runs `pnpm verify:phase-4`.
name: verify-phase-4
```

**Trigger paths pattern** (lines 12–38) — replace Phase 3 paths with:
```yaml
on:
  push:
    branches: [main]
    paths:
      - 'apps/**'
      - 'packages/**'
      - 'tools/room-converter/**'
      - 'tools/scripts/**'
      - 'docs/adr/0004-*.md'
      - 'pnpm-workspace.yaml'
      - 'package.json'
      - 'scripts/verify-phase-4.mjs'
      - '.github/workflows/verify-phase-4.yml'
  pull_request:
    paths: [...same...]
```

**Job pattern** (lines 40–70 — copy verbatim with two deltas):
- Replace per-tool `pnpm install --frozen-lockfile` blocks with a single `pnpm install --frozen-lockfile` at workspace root (Phase 4 has a workspace; Phase 3 didn't).
- Update final `git diff --exit-code` paths from `tools/db-schema/migrations/` → `packages/db/migrations/`.

---

### `docs/adr/0004-room-hot-reload.md` (NEW)

**Closest analog:** `docs/adr/0003-canonical-snapshot.md` (read lines 1–60 above).

**Header pattern** (lines 1–14 — copy verbatim with title swap):
```markdown
# ADR 0004: Room Hot-Reload — fs.watch + Ed25519 Signed Manifests + Atomic Writes

**Date:** 2026-05-XX
**Phase:** 04 (during planning)

## Status

**Accepted** — locked at start of Phase 4 (D-09..D-13). Re-evaluation gates
listed in **Forcing Functions for Re-Open** below; absent any of those triggers,
the contract here is binding through Phase 7 PAR-03 (full room set
materialization — reuses this exact contract with no protocol changes).

Supersedes: nothing. Superseded by: nothing.
```

**Sections to mirror** from ADR 0003 (read TOC by section headers):
- `## Context` (lines 17–60 of 0003 = problem framing with verbatim file citations)
- `## Decision` (mirror Phase 3 D-09..D-13 + RESEARCH §Pattern 6 verbatim)
- `## Consequences` (mirror — list player-facing UX, ops impact, security posture)
- `## Forcing Functions for Re-Open` (mirror — list specific re-evaluation triggers, e.g., `fs.watch` proves unreliable on Fly Volume + ext4, signed-manifest verify exceeds 5 ms, etc.)

**Lint guard:** root `package.json` already has `lint:adr:0003` and `lint:adrs` (line 24); add a `lint:adr:0004` entry mirroring line 23, and append `&& pnpm lint:adr:0004` to the `lint:adrs` chain on line 24.

---

## Shared Patterns

### Pattern A — Workspace TS package shape

**Source:** `tools/db-schema/{package.json, tsconfig.json, vitest.config.ts}` (files read above).
**Apply to:** `packages/db/`, `packages/protocol/`, `packages/game-logic/`, `apps/server/` (all 4 new workspace members).

**Concrete excerpts:**
- `package.json` baseline fields (`tools/db-schema/package.json:1–7`):
  ```json
  { "name": "...", "version": "0.1.0", "private": true, "type": "module", "description": "..." }
  ```
- `tsconfig.json` byte-identical (file contents reproduced above — strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes is the project lock per Phase 1 D-17).
- `vitest.config.ts` byte-identical when file-I/O determinism matters (forks pool, maxWorkers 1) — `packages/db` keeps it; `packages/game-logic` could use defaults (no I/O); `packages/protocol` keeps it for round-trip determinism; `apps/server` integration tests need maxWorkers 1 (DB + fs.watch fixtures).

### Pattern B — Lint-script header + usage + exit-code

**Source:** `tools/protocol-doc/scripts/lint-protocol.mjs:1–30` (read above) + `tools/save-format-doc/scripts/lint-save-formats.mjs:1–32`.
**Apply to:** all 4 new lint scripts under `tools/scripts/`.

**Concrete excerpt** (the canonical 30-line opening):
```javascript
#!/usr/bin/env node
// tools/scripts/lint-<name>.mjs
// Source: 04-CONTEXT.md D-25 (<purpose>).
//
// <one-line description>
//
// Usage: node tools/scripts/lint-<name>.mjs [args]
// Exit: 0 success, 1 drift detected, 2 usage error.

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

function printUsage() { process.stderr.write('Usage: ...\n'); }
const arg = process.argv[2];
if (arg === '--help' || arg === '-h') { printUsage(); process.exit(0); }
```

### Pattern C — Composite-gate orchestrator

**Source:** `scripts/verify-phase-3.mjs:12–77`.
**Apply to:** `scripts/verify-phase-4.mjs`.

The key pattern is the labelled `[label, cmd, args]` tuple array driven by a `spawnSync` for-loop with first-failure exit. The label is a stable contract — referenced in the verify-gate plan's acceptance criteria (line 18 comment of verify-phase-3.mjs is explicit about this).

### Pattern D — Tool CLI dispatcher

**Source:** `tools/extract-gmd/cli.ts:1–60` AND `tools/protocol-doc/cli.ts:1–40` (read above — same shape).
**Apply to:** `tools/room-converter/cli.ts`.

**Concrete excerpt** (exit-code matrix comment block at top — Phase 1 D-18 contract):
```typescript
// Exit code matrix (Plan 06 contract):
//   0  success
//   1  functional failure (parse error, drift detected, IO error)
//   2  usage error (missing/unknown command, missing required args)
```

### Pattern E — Build-time copy with drift-guard

**Source:** root `package.json:31-32` (the inline `db:emit-check` + `lint:schema-sync` pair) + `tools/protocol-doc/output/protocol.ts` (the in-repo "AUTO-GENERATED" header convention).
**Apply to:** `packages/protocol/src/legacy-opcodes.ts` (D-19) AND `packages/db/src/auth-tables.ts` (D-08).

**Discipline:**
1. The generated/copied file has a banner comment: `// AUTO-GENERATED by <script>. DO NOT EDIT.`
2. A regen script (`pnpm --filter ... prebuild` or `pnpm db:auth:gen`) writes it.
3. A lint script (`lint:protocol-sync` / `lint:better-auth-schema-sync`) byte-compares against the source/regenerated output and exits 1 on diff.
4. The lint is wired into `verify:phase-4` so CI catches unmaintained generated files.

### Pattern F — Per-column SOURCE comment lint (Phase 3 carry-over)

**Source:** `tools/db-schema/scripts/check-source-comments.mjs` (read lines 1–80 above) + `tools/db-schema/src/tables.ts:29–46` (the inline `// SOURCE:` comment style).
**Apply to:** `packages/db/src/tables.ts` (relocate, lint follows).

When `tables.ts` moves to `packages/db/src/tables.ts`, the lint script `check-source-comments.mjs` must follow (the path resolution at line 15: `new URL('../src/tables.ts', import.meta.url)` — the `../src/` is relative-correct after the move).

`packages/db/src/auth-tables.ts` is generated and EXEMPT — the lint must skip it (add a path-allowlist OR keep lint scoped to `tables.ts` only, which it already is — line 15 hardcodes the filename).

### Pattern G — Test split: unit + integration

**Source:** `tools/extract-gmd/package.json:11-13` and `tools/save-format-doc/package.json:12-14` — both define:
```json
"test": "vitest run --exclude 'tests/integration/**'",
"test:full": "vitest run",
"test:integration": "vitest run tests/integration/"
```
**Apply to:** `apps/server/package.json` (auth flow / room hot-reload / SIGTERM grace tests are integration; rate-limit unit tests are pure unit; D-24 explicitly delineates).

### Pattern H — ADR template

**Source:** `docs/adr/0003-canonical-snapshot.md` (full file — first 60 lines read above) + `docs/adr/0002-persistence-layer.md` (first 50 lines read above — same template).
**Apply to:** `docs/adr/0004-room-hot-reload.md`.

Standard sections (in order): `# ADR <NNNN>: <title>` → `**Date** / **Phase**` → `## Status` → `## Context` (with verbatim file citations) → `## Decision` → `## Consequences` → `## Forcing Functions for Re-Open` (re-evaluation gates) → `## References`.

### Pattern I — CI workflow shape

**Source:** `.github/workflows/verify-phase-3.yml` (read in full above).
**Apply to:** `.github/workflows/verify-phase-4.yml`.

**Discipline points:** ubuntu-latest is the determinism reference platform (line 5–8 comment is the rationale — `core.autocrlf=true` on Windows surfaces spurious drift). `actions/checkout@v4` + `pnpm/action-setup@v4` + `actions/setup-node@v4 with cache: 'pnpm'` is the trio. Phase 4 collapses the 4 per-tool installs (lines 53–61 of phase-3 yml) into a single root `pnpm install --frozen-lockfile` — that's the workspace dividend.

---

## No Analog Found

| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `apps/server/Dockerfile` | dev-config | n/a | No prior Dockerfile in repo. Phase 4 ships dev-only; Phase 5 DEP-01 hardens for Fly. |
| `apps/server/.env.example` | dev-config | n/a | No prior `.env.example` in repo. Net-new; document the env-var schema (per RESEARCH §Runtime State Inventory line 658). |
| `pnpm-workspace.yaml` | workspace-config | n/a | No prior workspace file (workspaces are first-introduced in Phase 4 per O-01). Content is 4 lines; see `### pnpm-workspace.yaml` above. |
| `packages/protocol/src/state.ts` | Schema classes | state-diff | No `@colyseus/schema` usage exists yet. Source pattern lives in RESEARCH §Pattern 1 verbatim. |
| `packages/protocol/src/events.ts` | msgpackr S2C | event-driven | No `msgpackr` usage exists yet. Source pattern lives in RESEARCH §Pattern 1 + §"Two-channel wire". |
| `packages/game-logic/src/step.ts` | pure simulation | transform | No deterministic simulation in repo. Source pattern lives in RESEARCH §Pattern 2 + Pitfall 1. |
| `packages/game-logic/src/rng.ts` | pure RNG | transform | splitmix64 in repo for the first time. Source: D-20. |
| `apps/server/src/RebnoRoom.ts` | Colyseus Room | request-response + state-diff | First Colyseus code. Source: RESEARCH §Pattern 3, 4, 5 + §"Verified: Colyseus 0.17 Room skeleton". |
| `apps/server/src/RoomRegistry.ts` | fs.watch + broadcast | event-driven | First fs.watch / Ed25519 code. Source: RESEARCH §Pattern 6. |
| `apps/server/src/auth.ts` | Better-Auth | request-response | First Better-Auth code. Source: RESEARCH §Pattern 4 + §"Verified: Better-Auth custom argon2id hook". |
| `apps/server/src/db.ts` | better-sqlite3 + Drizzle | n/a | First runtime DB code. Source: RESEARCH §"Verified: Litestream synchronous=NORMAL guidance". |
| `apps/server/src/sigterm.ts` | lifecycle | event-driven | First SIGTERM handler. Source: RESEARCH §Pattern 8. |
| `apps/server/src/rate-limit.ts` | token-bucket | event-driven | First rate-limiter. Source: RESEARCH §Pattern 7. |

**For these files the planner MUST cite the corresponding RESEARCH.md §Pattern verbatim** in plan actions; concrete code excerpts already live there with line numbers.

---

## Metadata

**Analog search scope:** `tools/`, `scripts/`, `.github/workflows/`, `docs/adr/`, root `package.json`. Searched for: TS package shapes, drizzle wiring, lint scripts, vitest configs, CLI dispatchers, ADRs, CI workflows.
**Files scanned:** 16 read in full or partial (cli.ts × 2, package.json × 4, tsconfig × 2, vitest config × 1, drizzle config × 1, lint scripts × 4, schema sources × 2, ADRs × 2, verify-phase-3 + workflow × 2).
**Pattern extraction date:** 2026-05-06.
**Override flagged in RESEARCH:** O-01 — `tools/db-schema/` MUST be promoted to `packages/db/` (move + adopt). The pattern map above assumes promotion; if the planner instead chooses copy + drift-guard (RESEARCH option 2), all `packages/db/*` rows above remain valid but the lint script `check-source-comments.mjs` doubles its scope (run on both source AND copy).
