--- phase: 01 plan: 02 type: execute wave: 2 depends_on: [01-01] files_modified: - eslint.config.js - src/sim/__test_violation__/violator.ts - src/sim/__test_violation__/.eslintignore-marker - src/sim/__test_violation__/lint-firewall.test.ts autonomous: true requirements: [CORE-10] must_haves: truths: - "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)" artifacts: - path: eslint.config.js provides: "ESLint flat config with eslint-plugin-boundaries declaring 9 element types and one rule: sim cannot import from render or ui" contains: "boundaries/element-types" - path: src/sim/__test_violation__/violator.ts provides: "Deliberate sim → render import that the test consumes to assert the rule fires" - path: src/sim/__test_violation__/lint-firewall.test.ts provides: "Vitest test that runs ESLint programmatically against violator.ts and asserts the rule error" key_links: - from: src/sim/ to: "src/render/, src/ui/" via: "ESLint boundaries plugin (forbidden import path)" pattern: "boundaries/element-types.*disallow.*\\['render', 'ui'\\]" - from: src/sim/__test_violation__/lint-firewall.test.ts to: src/sim/__test_violation__/violator.ts via: "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 `` test, not just by "lint exits 0 on clean code" (which proves nothing about the rule actually working). @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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. 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. - `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. - 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. 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.