---
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.