DimitriGilbert / DimitriGilbert/arcade-vibe

Test & quality alignment plan

Open
#5 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

# Test & Quality Alignment Plan — arcade-vibe

- **Project**: `/home/didi/workspace/arcade-vibe` (BTS v3.19.4 monorepo, **Next.js 16 variant**, ~92k LOC, 0 tests)
- **Date**: 2026-09-05
- **Generated by the fleet test-alignment initiative** (28-repo audit → synthesis → per-project plans)
- **Inputs**: [audit report](../.test-alignment-audit/reports/arcade-vibe.md) · [fleet synthesis & decisions](../.test-alignment-audit/synthesis-decisions.md)

_Revised 2026-09-05: CI removed by owner decision — quality gates are local commands run by the owner/agents; no GitHub Actions._

This plan is **plan-only**: it prescribes work for implementer/validator subagent dispatches (per the `subagent-orchestration` methodology). It changes nothing in the repo by itself.

---

## 1. Current state snapshot (from audit, verified against the tree)

| Area | State |
|---|---|
| Tests | 0 test files, 0 test deps, no runner config anywhere; AGENTS.md L22 admits it |
| Lint/format | Nothing — no ESLint/Biome/Prettier dep or config anywhere |
| CI | Nothing — and that is deliberate (owner decision 2026-09-05): gates are local commands (D5) |
| tsconfig | `packages/config/tsconfig.base.json` is strict-plus (`noUncheckedIndexedAccess`, `noUnusedLocals/Parameters`, `noFallthroughCasesInSwitch`, `verbatimModuleSyntax`, `isolatedModules`); all 5 `packages/*` extend it; **`apps/web/tsconfig.json` is standalone `strict`-only** (74% of LOC laxer than AGENTS.md claims) |
| Turbo | `check-types` wired everywhere (the only real gate today); `lint` task exists but is **dangling** (no lint scripts); **no `test` task** |
| Test-ready assets | `docker-compose.yml` (postgres:18-alpine + redis:7-alpine, healthchecks), deterministic seeds (`packages/db/src/seed-dev.ts`, `seed-tier-costs.ts`, `seed-free-theme.ts`), cleanly layered `packages/api` (18.6k LOC: `lib/`, `jobs/`, `routers/`, `middleware/`) |
| Dead scaffold test deps | **None** — `apps/web` has no unused RTL/jsdom (unlike other BTS siblings). DoD item 6 is trivially satisfied. |

**Key structural constraint discovered (grounds the whole plan):** 35 of 50 `packages/api` files transitively import `@arcade-vibe/db/src/index.ts`, which at *import time* (a) validates env via `@arcade-vibe/env/server` (t3-oss throws without `DATABASE_URL`, `BETTER_AUTH_SECRET` ≥32, `BETTER_AUTH_URL`, `CORS_ORIGIN`, `ENCRYPTION_KEY` =32 chars, `GAME_SDK_SECRET` ≥32) and (b) opens a real `pg` Pool. `packages/api/src/jobs/*.ts` also call `main()` + `process.exit()` at module top level. Both are handled explicitly in Phases 0b and 4c.

---

## 2. Adopted decisions (D1–D7), adapted to the Next.js variant

| Decision | Adoption here |
|---|---|
| **D1 — runner & layout** | **vitest v4 line**, pinned via `pnpm-workspace.yaml` `catalog:`. Per-package configs (2 testable packages in v1 < the ≥6 threshold for the `projects`-glob monorepo pattern); a `createTestConfig()` factory goes into `packages/config` only if a 3rd config duplicates it. **Environment: `node` everywhere in v1** — the Next-variant component-test question is answered explicitly, not inherited: jsdom+RTL is **deferred** (see Deviation 2). Tests colocated as `*.test.ts`. `passWithNoTests` is banned. Root one-command entry: `pnpm test` → `turbo test`. |
| **D2 — coverage** | `@vitest/coverage-v8` (v4 line, catalog), reporters `text+json+html`, `include` scoped to `src/**`, exclude seeds/migrations/scripts. arcade-vibe is a **D2 state-3 repo** (zero tests): coverage reported, **no thresholds** at first; once Phases 0–1 seeds land, switch to state-2 mechanics — set `lines` at measured−5, ratchet +5 when green, never lower. Generated-file exclusion baked in from day one (`.next`, `drizzle` migrations output, `next-env.d.ts`). |
| **D3 — lint & format** | **ESLint 9 flat config + typescript-eslint ^8** at repo root (one `eslint.config.js` for the whole monorepo; server packages type-checked, `apps/web` additionally gets `eslint-plugin-react-hooks` + `@next/eslint-plugin-next`). Note: Next 16 has no `next lint`, so a standalone flat config is the *only* option — it happens to be exactly the D3 standard. `import/order` enabled. **Prettier 3: explicitly deferred** (D3 allows this) and the deferral stated in AGENTS.md rather than pretended. The dangling turbo `lint` task gets **implemented** (not deleted). |
| **D4 — quality metrics** | `knip` + `jscpd` report-only (`exitCode` not failing) with the launch-mommy canonical jscpd config (`minLines: 8, minTokens: 80`, exclude tests/generated); `crap-score` over `coverage-final.json` once Phase 5 produces it. Surfaced via the local `scripts/quality-report.mjs` writing a local (git-ignored) HTML report. **Nothing metric-shaped gates anything in v1.** |
| **D5 — no CI by design (owner decision 2026-09-05)** | GitHub Actions rejected fleet-wide: no `.github/workflows`, no reusable-workflow caller, no service containers, no scheduled jobs. Enforcement = the root-level headless verification entrypoint (`pnpm verify`: lint → check-types → test → build) run by the owner and by agents; the plan's phase validation gates are run by implementer/validator agents before any phase is declared done. The integration tier runs locally against the existing compose services (Phase 4). |
| **D6 — shared config** | `@dg/config` adoption-on-touch: this plan touches tsconfig/lint/vitest everywhere, so if `@dg/config` is published by the time a phase runs, that phase consumes it and `packages/config` becomes the re-export shim (BTS indirection kept). Until then, repo-local configs are structured so the swap is mechanical: the vitest factory lives in `packages/config`, the ESLint preset is one root file. |
| **D7 — hooks** | No hooks. AGENTS.md will state: "the headless local verification command (`pnpm verify`) is the quality gate, run by the owner/agents; hooks are local convenience." A green local run with zero hooks is compliant. |

---

## 3. Deviations from the synthesis (reality wins, per plan rules)

1. **"Zero mocks needed" (arcade-vibe delta, D1) is only true for a subset.** The audit's delta block says seeds are "pure routers + jobs, zero mocks needed" — but the `@arcade-vibe/db` singleton validates env and opens a Pool at import time, and `jobs/*.ts` self-execute with `process.exit()`. Consequence: the unit tier uses a setup file that pre-seeds all required env vars, `vi.mock("@arcade-vibe/db")` where a module under test transitively imports the singleton, and Jobs get a **minimal entry-guard refactor** (Phase 4c). The integration tier uses the real compose Postgres. The decision's intent (test server logic, not UI) is unchanged.
2. **Next-variant component-test choice (synthesis §5 demands an explicit pick): node-only in v1; jsdom+RTL deferred.** 68.5k LOC of `apps/web` UI stays uncovered in v1 — accepted and stated. Rationale: `packages/api` + `packages/db` (21.9k LOC of pure/server logic) is the highest-value lowest-setup target; RTL-on-React-19-with-react-compiler setup is a risk sink. Component tests are a named follow-up (Phase 7 candidate), not a silent omission.
3. **Jobs require a small production-code refactor to be testable**: move each job body into an exported `run*()` function, keep `main()` behind an `if` entry guard (or `import.meta.url` check) so importing the module no longer executes or `process.exit()`s. Flagged for validator review; without it the jobs are untestable by construction.
4. **Integration-tier database is local-only (revised 2026-09-05)**: originally this deviation was about provisioning CI service containers; with CI removed (D5) it reduces to a local requirement — the Phase 4 integration tier runs against the existing compose Postgres (`postgres:18-alpine`, optionally `redis:7-alpine`), and tests are `describe.skipIf(!process.env.TEST_DATABASE_URL)`-gated so `pnpm test` stays green when the services are down. Unit tier stays env-seeded with the mocked DB and needs nothing external.
5. **`apps/web` cannot extend the strict base verbatim** — it must keep Next-required overrides (`target`, `lib` incl. `dom`, `plugins: [next]`, `paths`, `noEmit`, `include` of `.next/types`). The acceptance criterion is the **four missing strict flags inheriting from the base**, not byte-equality with `packages/config/tsconfig.base.json`.
6. **D2 sequencing nuance**: thresholds move from state 3 → state 2 *within this plan* (Phase 5), not in a later wave, because seeds land in Phases 0–1.

---

## 4. Alignment Definition of Done — current status per item

| # | DoD item (synthesis §3) | Current status | Closed by |
|---|---|---|---|
| 1 | Root-level headless test command, documented in AGENTS.md | **Missing** — no `test` script anywhere | Phase 0b (+ Phase 6 docs) |
| 2 | Headless verification entrypoint runs typecheck + test + lint in one local command; agent records green before a phase is done | **Missing** — no root aggregate command | Phase 3 |
| 3 | Coverage measured/reported; baseline recorded; thresholds per D2 | **Missing** | Phase 5 |
| 4 | Lint installed, zero errors, wired into the headless entrypoint; dangling turbo `lint` implemented or deleted | **Missing** — no ESLint; `turbo.json` has dangling `lint` task | Phase 2 (+ Phase 3 composition) |
| 5 | AGENTS.md "Testing" section matches reality | **Accurate-but-empty** — "No test commands are configured yet" is true today; becomes phantom the moment Phase 0b lands | Phase 0b minimal update, Phase 6 full rewrite |
| 6 | Dead scaffold test deps removed or used | **N/A — nothing dead** (verified: no RTL/jsdom anywhere) | — |
| 7 | Renovate one-liner; no `"latest"` deps | **Renovate missing**; no `"latest"` pins found (catalog/ranges throughout) | Phase 6 |
| 8 | Quality-report script (knip + jscpd + CRAP) runnable locally, report-only | **Missing** | Phase 5 |
| 9 | No fake passes | **N/A today** (nothing exists to fake); plan bans `passWithNoTests` in the vitest config from Phase 0b | Enforced throughout |

---

## 5. Phased plan

**Effort scale**: S ≤ 0.5 day · M ≈ 1–2 days · L ≈ 3–5 days (implementer + validator cycles included).

**Universal gatekeeping (every implementer/fixer, every phase)**: `pnpm run check-types` must pass; plus `pnpm test` once Phase 0b lands; plus `pnpm run lint` once Phase 2 lands; `pnpm run build` for phases that touch build/tsconfig/config (0, 2, 3). NO-SLOP policy (§6) is pasted verbatim into every dispatch.

### Phase 0 — Foundations: strict-base bypass fix + vitest skeleton — **M**

**Goal**: 74% of the codebase stops compiling laxer than AGENTS.md claims; a one-command, headless test pipeline exists with at least one real passing test — both before any mass seeding.

**Sub-phase 0a — strict base for `apps/web`** (implementer → validator)
- Rewrite `apps/web/tsconfig.json` to `"extends": "@arcade-vibe/config/tsconfig.base.json"` keeping only Next-required overrides (Deviation 5): `target: "ES2017"`, `lib: ["dom","dom.iterable","esnext"]`, `allowJs`, `noEmit`, `incremental`, `plugins: [{"name":"next"}]`, `paths` (`@/* → ./src/*`), existing `include`/`exclude`. The four flags (`noUncheckedIndexedAccess`, `noUnusedLocals`, `noUnusedParameters`, `noFallthroughCasesInSwitch`) must be **inherited, not restated**.
- Fix every resulting type error in `apps/web/src/**` (mechanical: undefined-on-index checks, unused locals). Do not disable or weaken any flag. If the error backlog exceeds what one implementer pass can close (~150 errors), keep the config change and split the fixes into an immediate follow-up sub-phase 0a′ with the same gate — flags are never weakened to make progress.
- **Gate**: `pnpm run check-types` green; `grep '"extends"' apps/web/tsconfig.json` matches the base; `pnpm run build` green.

**Sub-phase 0b — vitest skeleton + first seed** (implementer → validator, after 0a validates)
- `pnpm-workspace.yaml`: add catalog entries `vitest: ^4.` and `@vitest/coverage-v8: ^4.` (exact minor picked by implementer within the v4 line).
- `pnpm --filter @arcade-vibe/api add -D "vitest@catalog:" "@vitest/coverage-v8@catalog:"`
- Create `packages/api/vitest.config.ts`: `environment: "node"`, `include: ["src/**/*.test.ts"]`, `setupFiles: ["./tests/setup/env.ts"]`, no `globals`, **no `passWithNoTests`**, coverage block (D2 config: `provider: "v8"`, reporters `["text","json","html"]`, `include: ["src/**"]`, `exclude` seeds/scripts/migrations), `testTimeout` ≥ 15s.
- Create `packages/api/tests/setup/env.ts`: assigns test values for every required var *before* any import can trigger t3-oss validation — `DATABASE_URL=postgresql://postgres:password@localhost:5432/arcade_vibe_test`, `BETTER_AUTH_SECRET`, `GAME_SDK_SECRET` (32+ chars), `BETTER_AUTH_URL=http://localhost:3001`, `CORS_ORIGIN=http://localhost:3001`, `ENCRYPTION_KEY` (exactly 32 chars), `NODE_ENV=test`.
- Wire the root entry: root `package.json` `"test": "turbo test"`; `turbo.json` gains a `"test"` task (default caching, `outputs: ["coverage/**"]`).
- First seed tests (pipeline proof, real assertions): `packages/api/src/lib/tokenizer.test.ts` (`getTokenCount` counts, fallback encoding path) and `packages/api/src/lib/scoring-config.test.ts` (config invariants: monotone scales, bounds). If workspace `.ts`-exports resolution (`@arcade-vibe/*` → `src/*.ts`) blocks vitest, add `resolve.alias` entries in the config and note it.
- Update AGENTS.md L22 minimally: point to `pnpm test` (full rewrite is Phase 6).
- **Gate**: `pnpm test` from repo root runs headless and passes ≥ 2 tests; `pnpm run check-types` green (test files included).

### Phase 1 — Unit-tier seeds: pure `packages/api` libs — **M**

**Goal**: the genuinely pure server logic is locked under tests before anything that needs a database. Multi-sub-phase: per-sub-phase validators + phase-wide validator.

**Sub-phase 1a — string/crypto-pure modules** (implementer → validator)
- `src/lib/script-sanitizer.test.ts` — imports only `@arcade-vibe/db/schema/*` (no singleton): markdown code-block extraction, `` allow/block URL classification, `SanitizationResult` shape.
- `src/lib/encryption.test.ts` — `encryptApiKey`/decrypt roundtrip, unique IV per call, AES-256-GCM auth tag behavior (env already seeded by setup).
- `src/lib/game-export.test.ts` — JSZip export structure (deterministic inputs → expected entries).
- **Gate**: `pnpm test` green including new files; no `vi.mock` needed in this sub-phase (verify — if a module drags the db singleton in, move it to 1b-mock or integration and note it).

**Sub-phase 1b — modules behind the db singleton** (implementer → validator)
- `src/lib/scoring.test.ts` and `src/lib/credits.test.ts` **unit slices**: with `vi.mock("@arcade-vibe/db")` hoisted, test the pure math/branching that doesn't need real rows (scoring component weighting, clamps, percentile interpolation; credit threshold constants `LOW_BALANCE_THRESHOLDS`, transaction-shape validation). Everything row-dependent is explicitly out of scope here and queued for Phase 4.
- `src/lib/stripe-webhook.test.ts` — signature verification accept/reject with a test secret (pure crypto, no network).
- **Gate**: `pnpm test` green; `pnpm run test:coverage` (temporary local invocation `pnpm --filter @arcade-vibe/api exec vitest run --coverage`) produces a first HTML/text report — numbers recorded in the phase report, **no thresholds set yet**.

**Phase-wide validator**: reads all Phase 0–1 test files together — colocation convention (`*.test.ts`), no `passWithNoTests`, no tautological assertions, no `any`, mocks confined to the `@arcade-vibe/db` seam, coverage excludes correct.

### Phase 2 — Lint: ESLint 9 flat, dangling turbo `lint` implemented — **M**

**Goal**: DoD 4. Style enforcement stops being AGENTS.md prose.

- `pnpm -w add -D eslint @eslint/js typescript-eslint eslint-plugin-react-hooks @next/eslint-plugin-next eslint-plugin-import globals` (versions within D3 families: ESLint ^9, typescript-eslint ^8; Prettier deliberately not installed — stated in AGENTS.md per D3).
- Create root `eslint.config.js` (flat, ESM): `@eslint/js` recommended + `typescript-eslint` recommendedTypeChecked with `projectService: true` for `packages/**`; `apps/web` gets `react-hooks` (v6 flat-compatible) + `@next/eslint-plugin-next`; `import/order` enabled (external/workspace-first, matching AGENTS.md conventions); ignores: `.next/**`, `node_modules`, `dist`, `.turbo`, `next-env.d.ts`, `designs/`, `.task-o-matic/`.
- Add `"lint": "eslint ."` scripts to root + all 6 workspace `package.json`s so the existing turbo `lint` task is implemented end-to-end (DoD 4's "implement or delete" → implemented).
- Fix all findings. Zero errors is the landing condition; `--max-warnings=0` on the script.
- **Gate**: `pnpm run lint` green from root; `pnpm run check-types` green; `pnpm run build` green.

### Phase 3 — Local gate composition: `pnpm verify` — **S**

**Goal**: DoD 2, under D5 (no CI by design). The full gate becomes one headless root-level command the owner and agents run — no workflow files anywhere.

- Add root `package.json` `"verify": "pnpm run lint && pnpm run check-types && pnpm test && pnpm run build"` — the headless verification entrypoint (D5). Document it in the phase report as the command every subsequent phase's validator runs before declaring done.
- Integration-tier tests remain `describe.skipIf(!process.env.TEST_DATABASE_URL)`-gated (Phase 4) so `pnpm verify` is green without the compose services up; the full gate with the DB tier is `docker compose up -d postgres redis` + `TEST_DATABASE_URL=… pnpm verify`.
- No `.github/` directory is created (D5). `gh run list`-style checks are gone with CI — greenness is recorded by the validator in the phase report instead.
- **Gate**: `pnpm verify` exits 0 from repo root; fail-fast behavior proven once by the validator (deliberate lint error in a scratch file → non-zero exit at the lint step).

### Phase 4 — Integration tier: compose-backed Postgres; credits, jobs, routers — **L**

**Goal**: the money logic and the tRPC surface get real-DB tests using the existing `docker-compose.yml` services and seed scripts. Multi-sub-phase (5 sub-phases): implementer → validator each, then phase-wide validator.

**Sub-phase 4a — DB test harness** (implementer → validator)
- `pnpm --filter @arcade-vibe/db add -D "vitest@catalog:"`
- `packages/db/test/global-setup.ts`: connects to the compose Postgres (`postgresql://postgres:password@localhost:5432/postgres`), creates `arcade_vibe_test` if absent, runs Drizzle migrations against it (programmatically or via `drizzle-kit migrate` with env override), exposed through `packages/api/vitest.config.ts` as `globalSetup` only for `tests/integration/**` projects; per-file truncate helper `packages/db/test/truncate.ts`; teardown closes the pool via `db.$client.end()`.
- Use the existing deterministic seeds (`src/seed-dev.ts`, `seed-tier-costs.ts`) as fixture loaders — do not write parallel fixtures.
- Convention: integration tests live in `tests/integration/` (they are fixture-driven, not colocated units) and are gated by `describe.skipIf(!process.env.TEST_DATABASE_URL)` so the suite stays green whenever the compose services are not running locally (Deviation 4).
- **Gate**: with `docker compose up -d postgres` running, one smoke integration test passes; without it, the suite skips cleanly and `pnpm test` stays green.

**Sub-phase 4b — credits (the money logic)** (implementer → validator)
- `tests/integration/credits.test.ts` over `src/lib/credits.ts`: grant/spend against seeded tier costs, `creditBatches` expiry ordering, `LOW_BALANCE_THRESHOLDS` boundary behavior, transactional rollbacks on failure.
- **Gate**: `TEST_DATABASE_URL=…arcade_vibe_test pnpm test` green; truncate helper proven between files.

**Sub-phase 4c — jobs** (implementer → validator)
- Apply Deviation 3's minimal refactor to `src/jobs/recalculate-scores.ts` and `src/jobs/update-platform-stats.ts`: export `runRecalculateScores()` / `runUpdatePlatformStats()`, keep the CLI wrapper behind an entry guard; keep the existing `recalculate-scores`/`update-platform-stats` script entrypoints working identically.
- `tests/integration/jobs.*.test.ts`: seed games/ratings/metrics, run the exported functions, assert recalculated scores, score history rows, and platform-stats rows (including the `parseFloat(averageRating)` guard path from `scoring.ts`).
- **Gate**: integration suite green; `pnpm --filter @arcade-vibe/api recalculate-scores` still runs as before (behavior unchanged); validator reviews the refactor diff explicitly.

**Sub-phase 4d — routers, public surface** (implementer → validator)
- Caller-style tests (`appRouter.createCaller(ctx)` with hand-built `Context`) against the real test DB for: `ratings`, `leaderboard`, `game-leaderboard`, `games`, `themes`, `collections`, `user`. `protectedProcedure`'s suspension check (queries `db.query.userExtended`) is exercised through the real DB; unauthenticated/UNAUTHORIZED paths asserted with an empty ctx.
- **Gate**: integration suite green; no network calls (Stripe/Resend/AI providers remain out of these paths — if a router hard-requires them, it moves to 4e scope with seams noted).

**Sub-phase 4e — routers, admin + rate-limit** (implementer → validator)
- `routers/admin/*` authorization matrix: every admin procedure asserted to reject `participant`/`viewer`/anonymous ctx and accept `admin` (role from `getUserRole` seeded in DB).
- `middleware/rate-limit.test.ts` against compose Redis (`REDIS_URL=redis://localhost:6379`, env optional so unit tier is unaffected); skipped when Redis is absent.
- **Phase-wide validator**: reads the whole integration tier — harness reuse (no per-file DB setup duplication), truncate correctness, no cross-file ordering dependence, seams honest (no mock of what the real DB can provide), Deviation 3 refactor minimal.

### Phase 5 — Coverage baseline + ratchet; knip + jscpd report-only — **M**

**Goal**: DoD 3 + 8. Numbers get measured, recorded, and start ratcheting — nothing new gates yet.

- Enable `test:coverage` officially: root `"test:coverage": "turbo test:coverage"`, `turbo.json` task, per-package script `vitest run --coverage`.
- D2 state-3 → state-2 transition: with both tiers in place, set `coverage.thresholds.lines` at **measured − 5** in `packages/api/vitest.config.ts`; commit the number; **ratchet +5 whenever green**; never lower. (Record the baseline in AGENTS.md, Phase 6.)
- `pnpm -w add -D knip jscpd crap-score`; create `knip.json` (entry: `apps/web/src/app/**`, `packages/*/src/index.ts`; ignore `.next`, generated types) and jscpd config with the canonical `minLines: 8, minTokens: 80`, `exitCode: 0` (report-only until a baseline exists — D4/§5).
- Local `scripts/quality-report.mjs` running the three report-only tools (template: the launch-mommy/ideadump donor script; converged onto the shared `@dg/config` script when it publishes). Reports are written to a git-ignored local directory; run on demand by the owner or during a periodic agent pass — no CI, no artifacts (D5).
- **Gate**: `pnpm run test:coverage` green *with* the threshold active; `pnpm run build` green; `node scripts/quality-report.mjs` produces the local HTML report without failing the shell.

### Phase 6 — Docs, Renovate, closeout — **S**

**Goal**: DoD 5 + 7; the repo's documentation stops being able to drift from reality silently.

- Rewrite AGENTS.md "Testing" section to match reality exactly: runner (vitest v4), commands (`pnpm test`, `pnpm run test:coverage`, `pnpm verify`, integration-tier env requirements + `docker compose up -d postgres redis` prerequisite), locations (colocated units; `packages/api/tests/integration/`), coverage baseline + threshold + ratchet rule, the D7 statement ("the headless local verification command is the quality gate, run by the owner/agents; hooks are local convenience"), and the explicit deferrals (component/E2E tests, Prettier). Delete the "No test commands are configured yet" line.
- Create `renovate.json`: `{ "extends": ["local>DimitriGilbert"] }` (one-liner per D4; org preset assumed to exist — if not, file the fleet-level follow-up, don't fork a local preset).
- Final DoD sweep: all 9 items green or explicitly N/A.
- **Gate**: `pnpm run check-types && pnpm run lint && pnpm test && pnpm run build` all green from a clean checkout perspective; documentation claims verified against `package.json` scripts by the validator.

### Post-plan candidates (not scheduled, named for the ratchet)

- **Phase 7 candidate**: component tests (`apps/web`, jsdom + RTL preset via the shared factory — the explicit Next-variant pick deferred in Deviation 2), then Playwright E2E run locally by the owner/agents.
- jscpd gate once a duplication baseline has existed for a cycle (D4 revisit clause).
- `@dg/config` / shared quality-report script convergence when `@dg/config` publishes (D6).
- AI-provider seam tests (`lib/game-generation.ts`, `lib/ai-providers.ts`) behind a provider-interface mock — high value, high setup cost.

---

## 6. Orchestration protocol (per `subagent-orchestration`)

- **1 implementer → 1 validator per (sub-)phase**, validator dispatched immediately after its implementer, never batched. Validator **actually reads the code** (files, line numbers) — running gates alone is not validation.
- **Multi-sub-phase phases (0, 1, 4)** additionally get a **phase-wide validator** reading all sub-phase output together (integration, shared types, duplicate logic, coherence).
- **Fixers receive ALL validator findings at once**, run the same gatekeeping before reporting, and never validate their own fix; re-validate after each fix; **max 3 fix loops per (sub-)phase**, then halt and report.
- **Implementers/fixers run gatekeeping commands before reporting done** — `pnpm run check-types` always; `pnpm test` after 0b; `pnpm run lint` after Phase 2; `pnpm run build` in phases 0, 2, 3, 5.
- **NO-SLOP policy, pasted verbatim into every implementer/fixer dispatch and enforced by every validator**: NO `any`, `as any`, `: any` anywhere; NO placeholder code, NO `// TODO`, NO `// FIXME`; NO unused imports or variables; NO console.log hacks or void hacks; `import type` for type-only imports (`verbatimModuleSyntax` is on); external imports first, blank line, then local imports; no dev server started; only production-quality code passes.
- **Enforcement story (D5)**: gates are local commands; the plan's phase validation gates are run by implementer/validator agents before any phase is declared done. `pnpm verify` (Phase 3) is the canonical one-shot.
- No phase creates > ~15 files or > ~500 new lines — the sub-phase splits above exist to enforce this on the 92k-LOC tree.

---

## 7. Risks & notes specific to this repo

- **92k LOC — no big-bang coverage promises.** No "% coverage by date" targets anywhere in this plan. The ratchet (measured − 5 → +5 per green cycle) and the seeded-module list are the only progress metrics. `apps/web` (68.5k LOC, 74%) is *deliberately* out of the v1 test surface (Deviation 2); its quality lift in v1 is the strict-base fix (Phase 0a) + lint (Phase 2), not tests.
- **Strict-flag enablement on `apps/web` may surface a large error backlog** (first time under `noUncheckedIndexedAccess`/`noUnused*`). Mitigation: flag fixes are mechanical but voluminous; sub-phase 0a has an explicit split-and-continue path (0a′) rather than any weakening. Never disable a flag to pass a gate.
- **Import-time side effects everywhere**: env validation (t3-oss throws), `pg` Pool, `ioredis` client, Better Auth instance — all constructed at module import. The env-seeding setup file (0b) and the `vi.mock("@arcade-vibe/db")` seam (1b) are load-bearing; pool teardown (`db.$client.end()`) prevents vitest hangs.
- **Next.js 16 specifics**: no `next lint` (flat config is mandatory, matching D3 anyway); `.next/types` must stay in `apps/web` tsconfig `include`; `next/server` in `packages/api/src/context.ts` is a type-only import (erased), so router caller tests do not need a Next runtime — verified, but Phase 4d validator re-confirms.
- **Vitest ↔ workspace `.ts` exports**: `packages/*` export maps point at `src/*.ts`; vitest normally handles this, but if resolution fails the fallback is `resolve.alias` in the config (Phase 0b proves this on the very first seed, before mass seeding begins).
- **The integration tier needs local Postgres**: compose pins `postgres:18-alpine`; tests run locally against `docker compose up -d postgres redis` and skip cleanly when the services are absent (4a gating), so `pnpm test`/`pnpm verify` never go red for environmental reasons.
- **`pnpm onlyBuiltDependencies`** already allowlists `esbuild` (vitest dep) — no install-script surprise expected; re-run `pnpm install` fully after catalog changes.
- **Job refactor blast radius (4c)** is two entrypoints referenced by root `package.json` (`update-platform-stats`) and `packages/api/package.json` (`recalculate-scores`) — validator must confirm both script paths still work post-refactor.
- **Never gate on unmeasured numbers** (synthesis §5): thresholds (5), jscpd/knip/crap (5) all land report-only or baseline-derived; a gate that fails on day one gets disabled, and a disabled gate is worse than none.

---

*End of plan. Execution follows the orchestration protocol in §6; each phase's gate must be green before the next dispatches.*

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the phased plan, apps/web/tsconfig.json, pnpm-workspace.yaml, turbo.json, and the existing packages/api and packages/db layout. Run pnpm run check-types to establish the current baseline, then follow the phase gates for pnpm test, pnpm run lint, pnpm run build, and pnpm verify. Done means the documented local quality commands, test coverage, linting, and report-only quality metrics work without CI.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, postgresql, redis, typescript
Domain
build-system, databases, devtools, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.