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