From de39c1b7c338615098edd8a819406023705d9d63 Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 8 May 2026 23:38:19 -0400 Subject: [PATCH] docs(01-02): complete eslint-firewall plan 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) --- .../01-02-eslint-firewall-SUMMARY.md | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 .planning/phases/01-foundations-and-doctrine/01-02-eslint-firewall-SUMMARY.md diff --git a/.planning/phases/01-foundations-and-doctrine/01-02-eslint-firewall-SUMMARY.md b/.planning/phases/01-foundations-and-doctrine/01-02-eslint-firewall-SUMMARY.md new file mode 100644 index 0000000..52eed37 --- /dev/null +++ b/.planning/phases/01-foundations-and-doctrine/01-02-eslint-firewall-SUMMARY.md @@ -0,0 +1,268 @@ +--- +phase: 01-foundations-and-doctrine +plan: 02 +subsystem: infra +tags: [eslint, eslint-plugin-boundaries, typescript-eslint, firewall, lint, vitest, architectural-firewall] + +# Dependency graph +requires: + - phase: 01-foundations-and-doctrine/01 + provides: "Phaser 4 + React 19 + Vite 8 + TS 6 scaffold with the seven src/ firewall directories pre-created (sim, render, ui, save, content, audio, store), ESLint 9.39.4 + eslint-plugin-boundaries 6.0.2 pre-installed, npm 'lint' script pre-declared with --max-warnings 0" +provides: + - "ESLint 9 flat config (eslint.config.js) declaring 9 element types — the seven Phase-1 firewall subsystems plus the template's app + game — and one rule (severity: error): src/sim/ MUST NOT import from src/render/ or src/ui/ (CORE-10)" + - "Deliberate-violation fixture (src/sim/__test_violation__/violator.ts) excluded from default lint glob via the eslint.config.js ignores block" + - "Vitest test (src/sim/__test_violation__/lint-firewall.test.ts) that runs ESLint programmatically against the violator and asserts boundaries/element-types fires with severity=error and message containing both 'sim' and 'render|ui'" + - "Render-side stub file (src/render/__firewall_target__.ts) — minimal export so the boundaries plugin can resolve the violator's import to a real path on disk. Without this, the plugin marks the import target as isUnknown and silently skips the rule (verified empirically; see Deviations)." + - "TypeScript-aware import resolution for ESLint via eslint-import-resolver-typescript (devDep), wired through eslint.config.js settings.import/resolver" + - "Build-glob exclusions in tsconfig.app.json for *.test.ts and src/sim/__test_violation__/** so 'tsc -b' does not try to typecheck Vitest test files (which use Node APIs) under the DOM-only project lib settings" + +affects: [01-03-save-layer, 01-04-content-pipeline, 01-05-asset-provenance, 01-07-ci-workflow, 02-onwards] + +# Tech tracking +tech-stack: + added: + - "typescript-eslint@^8.59.2 — parser only (provides @typescript-eslint/parser bundled). NO rule sets enabled. Required because ESLint's default Espree parser cannot parse .ts/.tsx syntax. Documented as a Plan 02 deviation (Rule 3 — Blocking) below." + - "eslint-import-resolver-typescript@^4 — required by eslint-plugin-boundaries to follow extension-less TS imports ('../../render/foo' -> src/render/foo.ts). Without it, the boundaries plugin marks all TS-import targets as isUnknown and the firewall rule silently skips (verified via the plugin's debug output). Documented as a Plan 02 deviation (Rule 1 — Bug fix)." + patterns: + - "Single ESLint flat config at repo root with element-types + ignores + parser-only typescript-eslint integration. No legacy .eslintrc.* file. Plan 02 owns ONE architectural rule; broader code-quality lint sets (js.configs.recommended, tseslint.configs.recommended) are deliberately omitted to keep Phase 1 scope tight." + - "Default posture is 'allow' — Phase 1 enforces ONE rule (CORE-10), not a closed-by-default architecture. Future phases may add cross-subsystem restrictions (e.g., render cannot import save) by adding entries to the rules array without changing the default." + - "Lint-rule-correctness-via-Vitest pattern: the firewall rule's end-to-end correctness is proven by a Vitest test that runs ESLint via the JS API against a deliberate-violation fixture, NOT by 'lint exits 0 on clean code' (which proves nothing about the rule). The fixture is excluded from the default lint glob so CI stays green; the test passes ignore:false to override the exclusion." + - "Test-violation fixtures live under __test_violation__/ subdirectories and are doubly-excluded — from eslint.config.js ignores AND from tsconfig.app.json's exclude block — so neither 'npm run lint' nor 'tsc -b' trip on them. Vitest's own include glob (src/**/*.test.ts) discovers the test inside that directory." + +key-files: + created: + - eslint.config.js (ESLint 9 flat config — 9 element types, 1 rule, parser+resolver wiring, default-lint exclusions) + - src/sim/__test_violation__/violator.ts (deliberate sim → render import) + - src/sim/__test_violation__/lint-firewall.test.ts (Vitest test that asserts the rule fires) + - src/render/__firewall_target__.ts (minimal render-side export stub the violator targets) + modified: + - package.json (added typescript-eslint and eslint-import-resolver-typescript devDeps) + - package-lock.json (lockfile entries for the two new devDeps and their transitive closure: 18 + 14 packages) + - tsconfig.app.json (added exclude block for *.test.ts and src/sim/__test_violation__/**) + +key-decisions: + - "Omitted js.configs.recommended and tseslint.configs.recommended rule sets. Plan 02 owns exactly one architectural rule (CORE-10); broader code-quality lint is out of Phase 1 scope. Future phases may layer more rules on top of this config without touching the firewall block. Plan 01's SUMMARY confirmed no template eslint baseline existed to preserve." + - "Created src/render/__firewall_target__.ts as a real TS module (not a non-existent path) for the violator to import. The plan's Step 1 said 'doesn't need to exist as a real module' but empirical testing showed the boundaries plugin classifies unresolvable imports as isUnknown and silently skips the rule — the import MUST resolve to a real file under src/render/ to be classified as type:render and trigger the disallow." + - "Wired eslint-import-resolver-typescript via the import/resolver setting (boundaries plugin reads this). Without it, ext-less TS imports cannot be followed and EVERY in-repo TS import is marked isUnknown — the firewall rule would silently no-op even when called against the right shape of code." + - "Used the legacy boundaries/element-types rule + array-form selectors ({ from: ['sim'], disallow: ['render', 'ui'] }) per the plan's Pattern 5 spec. The plugin emits stderr deprecation notices recommending boundaries/dependencies + object-form selectors (the v6 modern shape), but those notices are informational — they do NOT count as ESLint warnings (verified via -f json: 0 errors, 0 warnings) and do NOT trip --max-warnings 0. Migration to the v6 modern shape is deferred to a future phase if it ever becomes load-bearing." + - "Excluded src/sim/__test_violation__/** and *.test.ts from tsconfig.app.json's build glob (added an exclude block). Vitest discovers test files via its own include glob, completely independent of tsconfig — so this only narrows what 'tsc -b' compiles, not what 'npm test' runs. Required because the firewall test imports node:path / process which aren't in the DOM-only app lib config." + - "Suppressed the 'Multiple projects found' notice from eslint-import-resolver-typescript via noWarnOnMultipleProjects:true. The referenced-projects tsconfig layout (root tsconfig with references to tsconfig.app.json + tsconfig.node.json) is deliberate Plan 01 design — the resolver sees both as 'projects' and warns; we explicitly opt out." + +patterns-established: + - "Lint-rule correctness via Vitest + ESLint Node API pattern: any architectural rule landed in this project should be paired with a Vitest test that imports the ESLint class, runs it programmatically against a deliberate-violation fixture (under __test_violation__/), and asserts the expected ruleId + severity fires. This satisfies the Nyquist Rule for static-analysis rules ('lint exits 0' proves nothing about whether a specific rule actually works)." + - "Double-exclusion pattern for test-violation fixtures: ignored by eslint.config.js (so npm run lint stays green) AND excluded from tsconfig.app.json (so tsc -b doesn't typecheck them). The Vitest test that consumes them passes ignore:false to ESLint to override the lint-side exclusion." + - "Real-target-required pattern for boundaries plugin tests: deliberate-violation fixtures must import from REAL files under the target element directory, not from non-existent paths. The boundaries plugin classifies import targets via element pattern after resolving the import to a file path; unresolvable imports are isUnknown and silently skip rule evaluation." + +requirements-completed: [CORE-10] + +# Metrics +duration: 22min +completed: 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.js` declares the seven Phase-1 subsystem element types (`sim`, `render`, `ui`, `save`, `content`, `audio`, `store`) plus the template's `app` (the React/Phaser bridge files) and `game` (`src/game/**`) types — 9 total. One rule, severity `error`: `{ from: ['sim'], disallow: ['render', 'ui'] }`. Default posture `allow` so 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.ts` instantiates the ESLint class, runs it against `src/sim/__test_violation__/violator.ts` (which imports from `src/render/__firewall_target__.ts`), and asserts the result includes a `boundaries/element-types` message at severity 2 (error) whose text contains both `sim` and `render|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 lint` exits 0 on the clean codebase.** Zero errors, zero warnings (verified via `-f json` formatter). The violator fixture is excluded by the `ignores` block in `eslint.config.js`, so it doesn't break CI; the test reaches it via `ignore: false` on the programmatic ESLint instance. +- **`npm run build` and `npm test` continue 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 compose `npm run lint && npm run test` and rely on both being green for this rule. + +## Task Commits + +Each task was committed atomically on `worktree-agent-adaed29911349f3f4`: + +1. **Task 1: ESLint flat config + boundaries plugin + CORE-10 firewall rule** — `e9b742d` (chore) +2. **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` + +```javascript +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: + +1. **Omitted broader rule sets** — Plan 02 owns ONE rule (CORE-10). Pulling in `js.configs.recommended` would 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. +2. **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. +3. **TypeScript resolver wired in** — required by the boundaries plugin to classify import targets. See Deviations below. +4. **Kept `boundaries/element-types` (not `boundaries/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`. +5. **Test-fixture dir excluded from `tsconfig.app.json`** — the firewall test uses `node:path` and `process`, which aren't in the DOM-only app lib config. Vitest discovers tests via its own glob. +6. **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 lint` after writing the initial config). +- **Issue:** ESLint's default parser (Espree) cannot parse TypeScript syntax or JSX. Initial `npm run lint` produced 5 "Parsing error: Unexpected token" errors against `src/main.tsx`, `src/App.tsx`, `src/PhaserGame.tsx`, `src/game/main.ts`, `src/game/scenes/Boot.ts`. Without a TS-aware parser, `npm run lint` cannot exit 0 on a TypeScript-strict scaffold, which violates Task 1's `` gate. +- **Fix:** Installed `typescript-eslint@^8.59.2` (the meta-package that bundles `@typescript-eslint/parser`). Wired ONLY the parser via `languageOptions.parser: tseslint.parser` in `eslint.config.js`. NO `tseslint.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 lint` exits 0 with 0 errors / 0 warnings (verified via `-f json` JSON 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 with `ESLINT_PLUGIN_BOUNDARIES_DEBUG=1` showed 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__.ts` exporting 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 `isUnknown` because `'../../render/__firewall_target__'` has no `.ts` extension and Node-style resolution doesn't add one). +- **Issue:** `eslint-plugin-boundaries` uses `eslint-plugin-import`'s resolver mechanism to follow imports to disk. The default resolver is Node-style and refuses to add a `.ts` extension to extension-less imports. Without a TS-aware resolver, EVERY in-repo TS import is marked `isUnknown` and 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 in `eslint.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 lint` still 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 build` after Task 2 created the test file — `tsc -b` failed with 3 TS2591 errors on the test file's `node:path` and `process` references). +- **Issue:** The firewall test uses Node APIs (`node:path` for `resolve`, `process.cwd()`) but `tsconfig.app.json` has `lib: ["ES2022", "DOM", "DOM.Iterable"]` and `types: ["vite/client"]` — no Node types. The original `tsconfig.app.json` had `include: ["src"]` with no `exclude` block, so `tsc -b` tried to compile all of `src/` 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 to `tsconfig.app.json`. Vitest discovers tests via its own `include` glob in `vitest.config.ts`, completely independent of tsconfig — so this only narrows what `tsc -b` compiles, not what `npm test` runs. +- **Files modified:** `tsconfig.app.json`. +- **Verification:** `npm run build` now exits 0 (`tsc -b` clean, `vite build` clean); `npm test` still 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 `.git` but not the working tree's installed dependencies. Ran `npm 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__.ts`** is 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 populate `src/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.ts`** is 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 `boundaries` rule does not restrict imports into `src/save/`; Plan 03 can populate `src/save/` freely. The TS resolver and the test-file exclusion in `tsconfig.app.json` will benefit Plan 03's IDB tests too — they should add their `src/save/**/*.test.ts` files and they'll be picked up by Vitest while excluded from `tsc -b`. +- **Plan 04 (content pipeline):** Unaffected. `src/content/` is a declared element type but has no `disallow` rule against it. +- **Plan 05 (asset provenance):** Unaffected — Plan 05 writes `scripts/validate-assets.mjs`, which lives outside `src/` 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 build` with 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 + +- [x] `eslint.config.js` exists at repo root — `test -f eslint.config.js` PASS. +- [x] `eslint.config.js` contains `boundaries/element-types` — PASS. +- [x] All 7 firewall element types declared (sim, render, ui, save, content, audio, store) — verified by individual `grep "type: ''"` for each. PASS. +- [x] `disallow: ['render', 'ui']` from `sim` — PASS (line: `{ from: ['sim'], disallow: ['render', 'ui'] },`). +- [x] No legacy `.eslintrc.*` file remains — PASS (`ls .eslintrc.*` returns no matches). +- [x] `__test_violation__` is in the `ignores` block — PASS. +- [x] `npm run lint` exits 0 — PASS (0 errors, 0 warnings via JSON formatter). +- [x] `src/sim/__test_violation__/violator.ts` exists and imports from `'../../render/'` — PASS. +- [x] `src/sim/__test_violation__/lint-firewall.test.ts` exists, references `boundaries/element-types`, imports `ESLint` from `eslint`, and asserts both `sim` and `render|ui` — PASS (count of toMatch lines mentioning sim/render = 2). +- [x] `npx vitest run src/sim/__test_violation__/lint-firewall.test.ts` exits 0 — PASS. +- [x] `npx eslint --no-ignore src/sim/__test_violation__/violator.ts` exits non-zero — PASS (exit 1, expected error fires). +- [x] `npm run build` exits 0 — PASS. +- [x] `npm test` exits 0 with 2/2 passing — PASS. +- [x] Task 1 commit exists: `e9b742d` — verified in `git log`. +- [x] Task 2 commit exists: `8c1d839` — verified in `git log`. + +**## Self-Check: PASSED** + +--- +*Phase: 01-foundations-and-doctrine* +*Plan: 02 of 7* +*Completed: 2026-05-09*