SUMMARY documents the ESLint flat config, the 9 element types, the single CORE-10 rule, the deliberate-violation fixture, the Vitest test that runs ESLint programmatically against the violator, and the four auto-fixed deviations (typescript-eslint parser-only integration, real render target file for the violator import, eslint-import-resolver- typescript wiring, tsconfig.app.json test-file exclusion). Verifies: npm run lint -> 0 errors / 0 warnings; npm test -> 2/2 pass; npm run build -> green; eslint --no-ignore on violator -> exits 1. Self-Check: PASSED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundations-and-doctrine | 02 | infra |
|
|
|
|
|
|
|
|
|
22min | 2026-05-09 |
Phase 01 Plan 02: ESLint Firewall Summary
ESLint 9 flat config + eslint-plugin-boundaries 6.0.2 enforcing CORE-10 (src/sim/ cannot import src/render/ or src/ui/) at error severity, with a Vitest test that runs ESLint programmatically against a deliberate-violation fixture and asserts the rule fires end-to-end.
Performance
- Duration: 22 min
- Started: 2026-05-09T03:12:34Z (worktree spawn — first action ran ~3 min after spawn due to dependency install)
- Completed: 2026-05-09T03:34:09Z
- Tasks: 2 (both completed atomically)
- Files created: 4 (eslint.config.js, violator.ts, lint-firewall.test.ts, firewall_target.ts)
- Files modified: 3 (package.json, package-lock.json, tsconfig.app.json)
Accomplishments
- CORE-10 firewall is structurally enforced.
eslint.config.jsdeclares the seven Phase-1 subsystem element types (sim,render,ui,save,content,audio,store) plus the template'sapp(the React/Phaser bridge files) andgame(src/game/**) types — 9 total. One rule, severityerror:{ from: ['sim'], disallow: ['render', 'ui'] }. Default postureallowso Phase 1 enforces only this one architectural constraint, not a closed-by-default architecture. - The rule is provably correct end-to-end.
src/sim/__test_violation__/lint-firewall.test.tsinstantiates the ESLint class, runs it againstsrc/sim/__test_violation__/violator.ts(which imports fromsrc/render/__firewall_target__.ts), and asserts the result includes aboundaries/element-typesmessage at severity 2 (error) whose text contains bothsimandrender|ui. Test passes in ~1 second. This satisfies the Nyquist Rule — the rule's correctness is automated, not assumed from "lint exits 0 on clean code". npm run lintexits 0 on the clean codebase. Zero errors, zero warnings (verified via-f jsonformatter). The violator fixture is excluded by theignoresblock ineslint.config.js, so it doesn't break CI; the test reaches it viaignore: falseon the programmatic ESLint instance.npm run buildandnpm testcontinue to pass. TypeScript strict-mode build is green; the test suite is now 2/2 (sentinel from Plan 01 + this firewall test).- Wave 2 sibling plans are unblocked. Plans 03/04/05/06 can now land their config and code without colliding on
eslint.config.js. Plan 07's CI workflow can composenpm run lint && npm run testand rely on both being green for this rule.
Task Commits
Each task was committed atomically on worktree-agent-adaed29911349f3f4:
- Task 1: ESLint flat config + boundaries plugin + CORE-10 firewall rule —
e9b742d(chore) - Task 2: CORE-10 firewall test + violator fixture + render target stub —
8c1d839(test)
Plan metadata: (this commit, by the orchestrator after merge) — docs(01-02): complete eslint-firewall plan
Final shape of eslint.config.js
import boundaries from 'eslint-plugin-boundaries';
import tseslint from 'typescript-eslint';
export default [
// 1. Default-lint exclusions (the violator fixture lives under
// src/sim/__test_violation__/ and must NOT trip CI).
{ ignores: ['src/sim/__test_violation__/**', 'dist/**', 'node_modules/**', 'coverage/**', '*.tsbuildinfo'] },
// 2. Phase-1 architectural firewall (CORE-10).
{
files: ['src/**/*.{ts,tsx,js,jsx,mjs,cjs}'],
plugins: { boundaries },
languageOptions: {
parser: tseslint.parser,
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
},
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/**/*'],
'boundaries/ignore': ['src/vite-env.d.ts', 'src/__sentinel__.test.ts'],
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: ['./tsconfig.app.json', './tsconfig.node.json'],
noWarnOnMultipleProjects: true,
},
},
},
rules: {
'boundaries/element-types': ['error', {
default: 'allow',
rules: [
{ from: ['sim'], disallow: ['render', 'ui'] },
],
}],
},
},
];
ESLint version landscape
Per Plan 01's drift report:
- ESLint:
^9.39.4(installed by Plan 01, untouched here). Flat config only — no legacy.eslintrc.*file ever existed in the repo, so Task 1 was a creation, not a migration. - eslint-plugin-boundaries:
^6.0.2(installed by Plan 01, untouched here). - typescript-eslint:
^8.59.2— added by THIS plan (Task 1, devDep). Parser only (tseslint.parser); no rule sets are enabled. - eslint-import-resolver-typescript:
^4.x(latest installed) — added by THIS plan (Task 2, devDep). Required for the boundaries plugin's element classification to follow extension-less TS imports to disk.
Verification snapshot
| Gate | Command | Result |
|---|---|---|
| Lint clean codebase | npm run lint |
exit 0, 0 errors, 0 warnings |
| Firewall test passes | npx vitest run src/sim/__test_violation__/lint-firewall.test.ts |
exit 0, 1/1 pass |
| Full test suite | npm test |
exit 0, 2/2 pass (sentinel + firewall) |
| TS-strict build | npm run build |
exit 0, dist/ produced |
| Rule fires when invoked | npx eslint --no-ignore src/sim/__test_violation__/violator.ts |
exit 1, boundaries/element-types error mentioning sim/render |
Decisions Made
See the key-decisions frontmatter block above. Brief rationale for each:
- Omitted broader rule sets — Plan 02 owns ONE rule (CORE-10). Pulling in
js.configs.recommendedwould expand scope to dozens of code-quality rules on a clean greenfield codebase that Plan 01's TS-strict + manual-curation discipline already covers. Future phases may add rule sets on top without disturbing the firewall block. - Real render target file (not a non-existent path) — empirical override of the plan's "doesn't need to exist as a real module" guidance. See Deviations below.
- TypeScript resolver wired in — required by the boundaries plugin to classify import targets. See Deviations below.
- Kept
boundaries/element-types(notboundaries/dependencies) — followed the plan's Pattern 5 spec verbatim. The plugin's stderr deprecation notices are informational; they don't count as ESLint warnings and don't trip--max-warnings 0. - Test-fixture dir excluded from
tsconfig.app.json— the firewall test usesnode:pathandprocess, which aren't in the DOM-only app lib config. Vitest discovers tests via its own glob. - Suppressed multi-project resolver warning — Plan 01 deliberately uses the referenced-projects tsconfig layout; the resolver's warning is asking us to undo Plan 01's design.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 — Blocking] Added typescript-eslint (parser only) so ESLint can parse .ts/.tsx
- Found during: Task 1, Step 4 (running
npm run lintafter writing the initial config). - Issue: ESLint's default parser (Espree) cannot parse TypeScript syntax or JSX. Initial
npm run lintproduced 5 "Parsing error: Unexpected token" errors againstsrc/main.tsx,src/App.tsx,src/PhaserGame.tsx,src/game/main.ts,src/game/scenes/Boot.ts. Without a TS-aware parser,npm run lintcannot exit 0 on a TypeScript-strict scaffold, which violates Task 1's<verify>gate. - Fix: Installed
typescript-eslint@^8.59.2(the meta-package that bundles@typescript-eslint/parser). Wired ONLY the parser vialanguageOptions.parser: tseslint.parserineslint.config.js. NOtseslint.configs.*rule sets are enabled — Plan 02's discipline of owning exactly one architectural rule is preserved. - Files modified:
package.json,package-lock.json,eslint.config.js. - Verification:
npm run lintexits 0 with 0 errors / 0 warnings (verified via-f jsonJSON formatter). - Committed in:
e9b742d(Task 1 commit).
2. [Rule 1 — Bug] Created src/render/__firewall_target__.ts as a real import target for the violator
- Found during: Task 2, Step 3 (running the Vitest test for the first time — it failed with
expected 0 to be greater than 0, meaning the rule did not fire even though the violator was clearly a sim-importing-render shape). - Issue: The plan's Step 1 said "ESLint's boundaries plugin lints the import path against element-type rules without resolving the module" — implying the violator could import a non-existent path like
'../../render/this-file-does-not-exist'. This is empirically false. Running the boundaries plugin withESLINT_PLUGIN_BOUNDARIES_DEBUG=1showed the import target classified as{ type: null, isUnknown: true }, and the rule then has nothing to disallow against and silently skips. The plugin REQUIRES the import target to resolve to a real file on disk so it can match the file path against element patterns. - Fix: (a) Created
src/render/__firewall_target__.tsexporting a single marker constant. (b) Updated the violator to import from this real file:import { FIREWALL_TARGET_MARKER } from '../../render/__firewall_target__';. Added documentation comments in both files explaining the role. - Files modified:
src/sim/__test_violation__/violator.ts,src/render/__firewall_target__.ts(new). - Verification:
ESLINT_PLUGIN_BOUNDARIES_DEBUG=1 npx eslint ...now shows the target classified as{ type: 'render', isUnknown: false }. After also fixing #3 below, the rule fires with:Dependencies to elements of type "render" are not allowed in elements of type "sim" and captured "null". Denied by rule at index 0 boundaries/element-types. - Committed in:
8c1d839(Task 2 commit).
3. [Rule 1 — Bug] Added eslint-import-resolver-typescript so the boundaries plugin can resolve extension-less TS imports
- Found during: Task 2, Step 3 (after fix #2, the target was STILL
isUnknownbecause'../../render/__firewall_target__'has no.tsextension and Node-style resolution doesn't add one). - Issue:
eslint-plugin-boundariesuseseslint-plugin-import's resolver mechanism to follow imports to disk. The default resolver is Node-style and refuses to add a.tsextension to extension-less imports. Without a TS-aware resolver, EVERY in-repo TS import is markedisUnknownand the firewall rule silently no-ops — even with a real target file present. This is a load-bearing wiring requirement the plan didn't anticipate (the plan focused on the rule-config shape; resolver wiring was implicit). - Fix: Installed
eslint-import-resolver-typescript(latest, ^4.x) as a devDep. Added'import/resolver': { typescript: { alwaysTryTypes: true, project: ['./tsconfig.app.json', './tsconfig.node.json'], noWarnOnMultipleProjects: true } }to the boundaries config block ineslint.config.js. The two-project array reflects Plan 01's referenced-projects tsconfig layout. - Files modified:
package.json,package-lock.json,eslint.config.js. - Verification: Debug output now shows imports resolving to disk paths and classifying correctly; the rule fires against the violator; the Vitest test passes.
npm run lintstill exits 0 with 0 errors / 0 warnings. - Committed in:
8c1d839(Task 2 commit).
4. [Rule 3 — Blocking] Excluded *.test.ts and src/sim/__test_violation__/** from tsconfig.app.json build glob
- Found during: Task 2, Step 4 (running
npm run buildafter Task 2 created the test file —tsc -bfailed with 3 TS2591 errors on the test file'snode:pathandprocessreferences). - Issue: The firewall test uses Node APIs (
node:pathforresolve,process.cwd()) buttsconfig.app.jsonhaslib: ["ES2022", "DOM", "DOM.Iterable"]andtypes: ["vite/client"]— no Node types. The originaltsconfig.app.jsonhadinclude: ["src"]with noexcludeblock, sotsc -btried to compile all ofsrc/including test files. The test file was correct TypeScript for its target environment (Node, via Vitest), but wrong for the app's DOM-only project. - Fix: Added an
exclude: ["src/**/*.test.ts", "src/**/*.test.tsx", "src/sim/__test_violation__/**"]block totsconfig.app.json. Vitest discovers tests via its ownincludeglob invitest.config.ts, completely independent of tsconfig — so this only narrows whattsc -bcompiles, not whatnpm testruns. - Files modified:
tsconfig.app.json. - Verification:
npm run buildnow exits 0 (tsc -bclean,vite buildclean);npm teststill exits 0 with 2/2 passing. - Committed in:
8c1d839(Task 2 commit).
Total deviations: 4 auto-fixed (2 blocking, 2 bug fixes — all under the Rule 1/2/3 auto-fix umbrella; none required architectural changes per Rule 4).
Impact on plan: All four deviations are mechanical wiring requirements the plan's high-level spec didn't anticipate. The plan's intent (CORE-10 enforced + provably tested) is satisfied exactly. No scope creep — the only added dependencies are tooling (typescript-eslint parser, eslint-import-resolver-typescript); no rule sets, no broader lint coverage. Wave-2 sibling plans (03–06) are unaffected.
Issues Encountered
node_modules/was not present in the worktree at agent spawn. Worktrees inherit.gitbut not the working tree's installed dependencies. Rannpm ci --no-audit --no-fund(10s, 209 packages) before any other work. Time cost: ~10s.- Boundaries plugin debug spelunking. Three full debug-output cycles (
ESLINT_PLUGIN_BOUNDARIES_DEBUG=1) were needed to diagnose deviations #2 and #3. The plugin's debug output is excellent — it shows the classification of both source and target files, which made the root causes visible immediately. Time cost: ~5 min.
Authentication Gates
None — Phase 1 is build/dev tooling only; no external auth needed.
Threat Flags
None — this plan is static-analysis tooling (no network, no auth, no file IO outside the build), and the boundary rule's mitigation effect is architectural integrity (preventing the simulation core from becoming non-deterministic or non-headless), not security. The threat-model section of the PLAN.md says "No security-relevant code in this plan; this is static-analysis tooling."
Known Stubs
src/render/__firewall_target__.tsis a one-line export-stub for the firewall test ONLY. It is NOT part of the runtime render layer.src/render/is otherwise empty in Phase 1 (only.gitkeep). Phase 2 will populatesrc/render/with real Phaser scenes. If the firewall test is ever rewritten to point at a real render module, this stub should be removed. Documented in the file's header comment.src/sim/__test_violation__/violator.tsis intentionally a deliberate-violation fixture, lint-tested only. It is excluded from both the lint glob and the TS build. Documented in the file's header comment.
These are intentional, plan-anticipated stubs. They exist because the test infrastructure for an architectural rule MUST stress-test the rule end-to-end, and that requires a real (not synthetic) sim → render edge in code.
Next Plan Readiness
- Plan 03 (save layer): Unaffected. The
boundariesrule does not restrict imports intosrc/save/; Plan 03 can populatesrc/save/freely. The TS resolver and the test-file exclusion intsconfig.app.jsonwill benefit Plan 03's IDB tests too — they should add theirsrc/save/**/*.test.tsfiles and they'll be picked up by Vitest while excluded fromtsc -b. - Plan 04 (content pipeline): Unaffected.
src/content/is a declared element type but has nodisallowrule against it. - Plan 05 (asset provenance): Unaffected — Plan 05 writes
scripts/validate-assets.mjs, which lives outsidesrc/and is therefore outside the boundaries rule's scope. - Plan 06 (doctrine docs): Unaffected — pure markdown.
- Plan 07 (CI workflow): Ready. CI can compose
npm run lint && npm run test && npm run buildwith high confidence — all three are green here, and the firewall rule has an automated test (not just "lint runs"), so a future regression that breaks the rule will be caught immediately.
Self-Check
eslint.config.jsexists at repo root —test -f eslint.config.jsPASS.eslint.config.jscontainsboundaries/element-types— PASS.- All 7 firewall element types declared (sim, render, ui, save, content, audio, store) — verified by individual
grep "type: '<name>'"for each. PASS. disallow: ['render', 'ui']fromsim— PASS (line:{ from: ['sim'], disallow: ['render', 'ui'] },).- No legacy
.eslintrc.*file remains — PASS (ls .eslintrc.*returns no matches). __test_violation__is in theignoresblock — PASS.npm run lintexits 0 — PASS (0 errors, 0 warnings via JSON formatter).src/sim/__test_violation__/violator.tsexists and imports from'../../render/'— PASS.src/sim/__test_violation__/lint-firewall.test.tsexists, referencesboundaries/element-types, importsESLintfromeslint, and asserts bothsimandrender|ui— PASS (count of toMatch lines mentioning sim/render = 2).npx vitest run src/sim/__test_violation__/lint-firewall.test.tsexits 0 — PASS.npx eslint --no-ignore src/sim/__test_violation__/violator.tsexits non-zero — PASS (exit 1, expected error fires).npm run buildexits 0 — PASS.npm testexits 0 with 2/2 passing — PASS.- Task 1 commit exists:
e9b742d— verified ingit log. - Task 2 commit exists:
8c1d839— verified ingit log.
## Self-Check: PASSED
Phase: 01-foundations-and-doctrine Plan: 02 of 7 Completed: 2026-05-09