Files
josh 39563f6934 docs(01): plan phase 1 — 7 plans across 3 waves, verified after 1 revision
Wave 1: Plan 01 (scaffold + test infra)
Wave 2: Plans 02 (eslint firewall), 03 (save layer), 04 (content pipeline),
        05 (asset provenance — autonomous:false human-curate checkpoint),
        06 (doctrine docs)
Wave 3: Plan 07 (CI workflow)

All 16 Phase-1 REQ-IDs covered. Plan-checker found 4 blockers + 6 warnings
on first pass; revision iteration 1 landed all 10 fixes; iteration 2
returned VERIFICATION PASSED. Two orchestrator judgment calls during
revision: (1) implement CORE-04 localStorage fallback in Phase 1 (the
literal requirement and ROADMAP success criterion #2 both call for it),
(2) reclassify STRY-09 as vacuously satisfied in Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:09:08 -04:00

19 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01 02 execute 2
01-01
eslint.config.js
src/sim/__test_violation__/violator.ts
src/sim/__test_violation__/.eslintignore-marker
src/sim/__test_violation__/lint-firewall.test.ts
true
CORE-10
truths artifacts key_links
ESLint flags any import from `src/sim/` of a module under `src/render/` or `src/ui/` as an error
A deliberate-violation fixture file proves the rule fires (lint output contains a `boundaries/element-types` error mentioning 'sim' and 'render' or 'ui')
A Vitest test invokes ESLint programmatically against the violator fixture and asserts the boundary error appears in output
`npm run lint` on the rest of the codebase exits 0 (the violator is excluded from CI lint via a path-based override or removed-from-default-glob trick)
path provides contains
eslint.config.js ESLint flat config with eslint-plugin-boundaries declaring 9 element types and one rule: sim cannot import from render or ui boundaries/element-types
path provides
src/sim/__test_violation__/violator.ts Deliberate sim → render import that the test consumes to assert the rule fires
path provides
src/sim/__test_violation__/lint-firewall.test.ts Vitest test that runs ESLint programmatically against violator.ts and asserts the rule error
from to via pattern
src/sim/ src/render/, src/ui/ ESLint boundaries plugin (forbidden import path) boundaries/element-types.*disallow.*['render', 'ui']
from to via
src/sim/__test_violation__/lint-firewall.test.ts src/sim/__test_violation__/violator.ts ESLint Linter API run against the fixture; output asserted to contain `boundaries/element-types`
Lock the architectural firewall in code: install `eslint-plugin-boundaries`, write a flat-config ESLint configuration that declares the seven `src/` element types plus the template's `app`/`game` types, and add exactly one rule — `sim` cannot import from `render` or `ui` (CORE-10). Prove the rule fires by committing a deliberate-violation fixture under `src/sim/__test_violation__/` and a Vitest test that runs ESLint against the fixture and asserts the boundary error appears. Exclude the fixture from the default lint glob so `npm run lint` (which Plan 07's CI workflow runs) stays green for the rest of the codebase.

Purpose: This is the structural enforcement of CLAUDE.md's "architectural firewall (load-bearing)" — the simulation core must not import rendering or UI code, ever. Without this, the offline-catchup math in Phase 2 will silently entangle with React/Phaser and break headless determinism. CONTEXT D-10 explicitly maps the seven directories Plan 01 created onto this lint rule.

Output: A green-on-clean-codebase ESLint config plus a self-contained test that proves the firewall fires. Both go in this plan to satisfy the Nyquist Rule — the rule itself is covered by an automated <verify> test, not just by "lint exits 0 on clean code" (which proves nothing about the rule actually working).

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-foundations-and-doctrine/01-CONTEXT.md @.planning/phases/01-foundations-and-doctrine/01-RESEARCH.md @.planning/phases/01-foundations-and-doctrine/01-01-SUMMARY.md @CLAUDE.md Task 1: Migrate to ESLint flat config + add boundaries plugin + define element types and the firewall rule eslint.config.js - .planning/phases/01-foundations-and-doctrine/01-01-SUMMARY.md (drift report from Plan 01 — did the template ship `.eslintrc.cjs` or already `eslint.config.js`? what plugins / rules does the template-baseline ESLint use? this determines whether Step 1 is a migration or a creation) - .planning/phases/01-foundations-and-doctrine/01-RESEARCH.md § "Pattern 5: ESLint Boundary Rule" (verbatim flat-config snippet) and § "Common Pitfalls — Pitfall 6: ESLint flat-config plugin imports break with mixed CJS/ESM template baseline" - .planning/phases/01-foundations-and-doctrine/01-CONTEXT.md (D-10 — the seven directories the rule lints against) - The existing eslint config at the repo root (whatever Plan 01 left it as): `eslint.config.js` if flat, otherwise `.eslintrc.cjs` / `.eslintrc.js` — read it before writing to preserve template-provided rules. Per RESEARCH § Pitfall 6: if the Phaser template shipped a legacy `.eslintrc.cjs`, migrate it to a flat `eslint.config.js` first (do not run both — ESLint 9 will produce conflicting messages). If the template already shipped flat config, extend it.
**Step 1 — Detect the template's ESLint baseline.** Look at Plan 01's SUMMARY for the drift report. Likely outcomes:
- **(a) Template shipped flat config (`eslint.config.js`):** read it, extend it (preserve all template rules; append the boundaries plugin block).
- **(b) Template shipped legacy (`.eslintrc.cjs` / `.eslintrc.json`):** rewrite into a single flat `eslint.config.js`, port the existing rules, then add the boundaries block. Delete the legacy file.

**Step 2 — Write `eslint.config.js`.** Use the Write tool with the structure from RESEARCH § "Pattern 5", merged with whatever the template provided. Concrete shape:
```javascript
// eslint.config.js — ESLint 9+ flat config
import boundaries from 'eslint-plugin-boundaries';
import tseslint from 'typescript-eslint'; // if template uses it; omit if not
import js from '@eslint/js';

export default [
  // 1. Template's existing config goes here (preserve verbatim from drift report).
  js.configs.recommended,
  // ...tseslint.configs.recommended (if template used it)...

  // 2. Phase-1 architectural firewall.
  {
    files: ['src/**/*.{ts,tsx,js,jsx}'],
    plugins: { boundaries },
    settings: {
      'boundaries/elements': [
        { type: 'sim',     pattern: 'src/sim/**' },
        { type: 'render',  pattern: 'src/render/**' },
        { type: 'ui',      pattern: 'src/ui/**' },
        { type: 'save',    pattern: 'src/save/**' },
        { type: 'content', pattern: 'src/content/**' },
        { type: 'audio',   pattern: 'src/audio/**' },
        { type: 'store',   pattern: 'src/store/**' },
        { type: 'app',     pattern: 'src/{main,App,PhaserGame}.{ts,tsx}' },
        { type: 'game',    pattern: 'src/game/**' },
      ],
      'boundaries/include': ['src/**/*'],
    },
    rules: {
      'boundaries/element-types': ['error', {
        default: 'allow',
        rules: [
          // CORE-10: simulation core cannot reach into render or ui
          { from: ['sim'], disallow: ['render', 'ui'] },
        ],
      }],
    },
  },

  // 3. Default-lint exclusion: the test fixture under __test_violation__
  // intentionally violates the rule for Task 2's assertion. Exclude from `npm run lint`.
  {
    ignores: ['src/sim/__test_violation__/**', 'dist/**', 'node_modules/**', 'coverage/**'],
  },
];
```
Per CONTEXT D-10, the element types listed above MUST match exactly: `sim`, `render`, `ui`, `save`, `content`, `audio`, `store`, plus the template's `app` (for `main.tsx`/`App.tsx`/`PhaserGame.tsx`) and `game` (for `src/game/`).

Per RESEARCH CI Pitfall C, the rule severity MUST be `'error'` (not `'warn'`); `npm run lint` from Plan 01 already includes `--max-warnings 0`.

Per RESEARCH § "Anti-Patterns to Avoid", `default: 'allow'` is the correct posture — Phase 1 enforces only the one CORE-10 rule, not a closed-by-default architecture.

**Step 3 — If a legacy `.eslintrc.cjs` / `.eslintrc.json` / `.eslintrc.js` existed, delete it.** Two configs at once break ESLint 9.

**Step 4 — Run `npm run lint` and confirm exit 0.** It should be green now (the violator fixture from Task 2 doesn't exist yet, and the `ignores` block already excludes the path).

**Step 5 — Commit `chore(01-02): migrate to ESLint flat config + boundaries plugin + CORE-10 firewall rule`.**
npm run lint && grep -q "boundaries/element-types" eslint.config.js && grep -E "disallow:\s*\[.*\b(render|ui)\b.*\]" eslint.config.js - `eslint.config.js` exists at repo root — verify with `test -f eslint.config.js`. - `eslint.config.js` contains the string `boundaries/element-types` — verify with `grep -q "boundaries/element-types" eslint.config.js`. - `eslint.config.js` declares all 7 firewall element types: `sim`, `render`, `ui`, `save`, `content`, `audio`, `store` — verify with `grep -cE "type: '(sim|render|ui|save|content|audio|store)'" eslint.config.js | grep -q ^7$` (or count manually if the regex form differs). - The disallow rule forbids both `render` AND `ui` from `sim` (allow whitespace and quote-style variation; both elements may appear in any order) — verify with `grep -E "disallow:\\s*\\[.*\\b(render|ui)\\b.*\\]" eslint.config.js`. The Task 2 firewall test is the load-bearing end-to-end check that proves both elements actually fire. - No legacy `.eslintrc.*` file remains — verify with `! ls .eslintrc.* 2>/dev/null | head -1` returns falsy (or list is empty). - `npm run lint` exits 0 (green on clean codebase, since the violator fixture is excluded by the `ignores` block). - The `ignores` block excludes `src/sim/__test_violation__/**` — verify with `grep -q "__test_violation__" eslint.config.js`. Single flat ESLint config at repo root with `eslint-plugin-boundaries` declaring all 7 firewall element types plus `app` and `game`, one error-severity rule forbidding `sim` from importing `render` or `ui`, the test-violation directory excluded from default lint, `npm run lint` green, commit landed. Task 2: Add deliberate-violation fixture + Vitest test that asserts the boundary rule fires src/sim/__test_violation__/violator.ts, src/sim/__test_violation__/lint-firewall.test.ts - eslint.config.js (verify the `ignores` block from Task 1 already excludes `src/sim/__test_violation__/**`, otherwise this task's deliberate violation will break `npm run lint`) - .planning/phases/01-foundations-and-doctrine/01-RESEARCH.md § "Pattern 5: ESLint Boundary Rule — CI integration" (the assertion shape: run ESLint programmatically, assert the boundary error appears) and § Validation Architecture (CORE-10 row: "static-analysis (CI) — `npm run lint` (with deliberate violator fixture)") - eslint-plugin-boundaries 6.0.2 README — `Linter` / `ESLint` API usage for programmatic invocation Per Nyquist Rule, the rule needs an automated test (not just "lint exits 0 on clean code, trust me"). Write a fixture that violates the rule and a Vitest test that programmatically runs ESLint against the fixture and asserts the violation is reported.
**Step 1 — Create the fixture `src/sim/__test_violation__/violator.ts`.** This file deliberately imports from `src/render/` to trigger the rule. Use Write tool:
```typescript
// DELIBERATE VIOLATION OF CORE-10 — DO NOT USE OUTSIDE THE FIREWALL TEST.
// This file lives under src/sim/__test_violation__/ and is excluded from
// `npm run lint` via the `ignores` block in eslint.config.js. Its sole
// purpose is to be lint-tested by lint-firewall.test.ts to prove the
// boundaries/element-types rule actually fires.
//
// The import below targets a path under src/render/ which doesn't need to
// exist as a real module — ESLint's boundaries plugin lints the import
// path against element-type rules without resolving the module.
// @ts-expect-error -- this import is not meant to resolve; it exists for the lint test.
import { thisDoesNotExist } from '../../render/this-file-does-not-exist';

export const VIOLATION_MARKER = 'sim-imports-render';
export const _ref = thisDoesNotExist;
```

The `@ts-expect-error` comment suppresses the TypeScript compiler error so `npm run build` continues to work; the lint rule fires before the type-checker sees this file (and the file is also outside the default-build glob since it's under `__test_violation__/`).

**Step 2 — Create the Vitest test `src/sim/__test_violation__/lint-firewall.test.ts`.** This test runs ESLint programmatically against `violator.ts` and asserts that the output contains a `boundaries/element-types` error mentioning the firewall. Use Write tool:
```typescript
import { describe, it, expect } from 'vitest';
import { ESLint } from 'eslint';
import { resolve } from 'node:path';

describe('CORE-10: src/sim/ cannot import from src/render/ or src/ui/', () => {
  it('eslint-plugin-boundaries flags a sim → render import as an error', async () => {
    // Programmatic ESLint run against the deliberate-violation fixture.
    // We override `ignore` so ESLint doesn't honor the eslint.config.js
    // `ignores` block (which exists to keep `npm run lint` green on this fixture).
    const eslint = new ESLint({
      overrideConfigFile: resolve(process.cwd(), 'eslint.config.js'),
      ignore: false,
    });

    const fixturePath = resolve(process.cwd(), 'src/sim/__test_violation__/violator.ts');
    const results = await eslint.lintFiles([fixturePath]);

    expect(results).toHaveLength(1);
    const messages = results[0].messages;
    const boundaryErrors = messages.filter(
      (m) => m.ruleId === 'boundaries/element-types' && m.severity === 2,
    );

    expect(boundaryErrors.length).toBeGreaterThan(0);
    // The error message should mention 'sim' and either 'render' or 'ui' (the disallowed targets).
    const combined = boundaryErrors.map((m) => m.message).join(' | ');
    expect(combined).toMatch(/sim/i);
    expect(combined).toMatch(/render|ui/i);
  });
});
```

Per RESEARCH § "Common Pitfalls — Pitfall 7: Synthetic v0→v1 migration test that doesn't actually exercise the registry" (the principle generalizes): the test must invoke the rule machinery, not just assert "the config file contains the right string."

**Step 3 — Run the test:** `npm test` should still exit 0 (sentinel test from Plan 01 + this new firewall test = 2 passing tests). The new test asserts the rule fires; it does NOT assert that ESLint exits non-zero on this file directly (which would require a separate process). Instead it inspects the messages via the JS API.

**Step 4 — Verify `npm run lint` STILL exits 0.** The fixture is excluded by the `ignores` block in `eslint.config.js`, so `npm run lint` doesn't see the violation; only the test sees it (via `ignore: false` override).

**Step 5 — Commit `test(01-02): add CORE-10 firewall test + violator fixture`.**
npx vitest run src/sim/__test_violation__/lint-firewall.test.ts && npm run lint - `src/sim/__test_violation__/violator.ts` exists and contains an import from `'../../render/`'` — verify with `grep -q "from '../../render/" src/sim/__test_violation__/violator.ts`. - `src/sim/__test_violation__/lint-firewall.test.ts` exists and references `boundaries/element-types` — verify with `grep -q "boundaries/element-types" src/sim/__test_violation__/lint-firewall.test.ts`. - The test invokes ESLint programmatically (not as a child process) — verify with `grep -q "import { ESLint } from 'eslint'" src/sim/__test_violation__/lint-firewall.test.ts`. - The test asserts the error message mentions both `sim` and `render|ui` — verify with `grep -E "toMatch.*sim|toMatch.*render" src/sim/__test_violation__/lint-firewall.test.ts | wc -l` returns at least 2. - `npx vitest run src/sim/__test_violation__/lint-firewall.test.ts` exits 0 with the firewall test passing — this is the load-bearing check that the rule actually fires end-to-end. - `npm run lint` STILL exits 0 (the fixture is excluded by the `ignores` block) — verify exit code 0. - Running `npx eslint --no-config-lookup -c eslint.config.js --no-ignore src/sim/__test_violation__/violator.ts` exits non-zero (proving the rule WOULD fire if the file weren't ignored) — verify with `npx eslint --no-ignore src/sim/__test_violation__/violator.ts; test $? -ne 0`. Deliberate-violation fixture committed; Vitest test programmatically runs ESLint and asserts the boundary error fires with severity=error and message mentioning sim+render/ui; `npx vitest run src/sim/__test_violation__/lint-firewall.test.ts` exits green; `npm run lint` exits 0 because the fixture is in the `ignores` block; commit landed.

<threat_model> No security-relevant code in this plan; this is static-analysis tooling. The boundary rule's mitigation effect is architectural integrity (preventing the simulation core from being non-deterministic or non-headless), not security. See Plan 03 for the only Phase-1 plan with security-relevant code. </threat_model>

- `npm run lint` exits 0 on the clean codebase (no actual sim/render imports exist yet — the directories are empty). - `npm test` runs the firewall test and the test passes (proving the rule actually fires when invoked against the fixture). - Running ESLint directly against the fixture (with `--no-ignore`) exits non-zero (proving the rule is wired correctly outside the test harness). - Plan 07's CI workflow will run `npm run lint && npm run test` — both green here means Plan 07 will be green on this rule.

<success_criteria>

  • ESLint flat config at eslint.config.js declares 9 element types and one rule (sim cannot import render or ui).
  • Deliberate violation fixture at src/sim/__test_violation__/violator.ts is excluded from npm run lint but lint-tested by Vitest.
  • npm test includes a boundaries/element-types assertion that passes.
  • The firewall is structurally enforced for every future Phase-2 commit. </success_criteria>
After completion, create `.planning/phases/01-foundations-and-doctrine/01-02-SUMMARY.md` documenting: - Final shape of `eslint.config.js` (was the template flat or legacy? what rules were preserved?). - The exact ESLint version installed (template-default + `eslint-plugin-boundaries@6.0.2`). - Confirmation that `npm run lint` is green and the firewall test is green. - Note for Phase 2: when `src/sim/` starts containing real modules, the existing config will lint them; no further wiring needed.