// eslint.config.js — ESLint 9 flat config // // Phase 1, Plan 02 (CORE-10): the architectural firewall. // // This file declares the seven src/ subsystem element types plus the // template-provided `app` and `game` types, and one rule: // // `src/sim/` MUST NOT import from `src/render/` or `src/ui/`. // // The simulation core must remain rendering-agnostic and headless so the // offline-catchup math in Phase 2 can run deterministically without React // or Phaser. See CLAUDE.md "Architectural Firewall (load-bearing)" and // .planning/phases/01-foundations-and-doctrine/01-CONTEXT.md (D-10). // // We intentionally do NOT pull in `js.configs.recommended` or the // typescript-eslint *rule sets* here. Plan 02 owns exactly one // architectural rule; broader code-quality lint is out of scope for // Phase 1 (and would expand Wave-2 surface area on a clean greenfield // codebase). Future phases may layer more rules on top of this config // without touching the firewall block. // // We DO use `typescript-eslint`'s *parser* — it is the only way ESLint // can parse `.ts` / `.tsx` files at all (Espree, ESLint's default // parser, doesn't understand TypeScript syntax or JSX). This is a // parser-only integration; no `tseslint.configs.*` rule sets are // applied. This is documented as a Plan 02 deviation (Rule 3 — Blocking) // in 01-02-SUMMARY.md. import boundaries from 'eslint-plugin-boundaries'; import tseslint from 'typescript-eslint'; export default [ // --------------------------------------------------------------------- // 1. Default-lint exclusions. // // The deliberate-violation fixture under src/sim/__test_violation__/ // exists ONLY to be lint-tested by Task 2's Vitest test (which runs // ESLint programmatically with `ignore: false`). It must NOT trip // `npm run lint` in CI — the rule is verified by the unit test, not // by the default lint glob. // --------------------------------------------------------------------- { ignores: [ 'src/sim/__test_violation__/**', 'dist/**', 'node_modules/**', 'coverage/**', '*.tsbuildinfo', ], }, // --------------------------------------------------------------------- // 3. Phase-2 sim-purity rule (CONTEXT D-33, RESEARCH Pitfall 1). // // Bans Date.now() and setInterval() inside src/sim/** to enforce the // "Sim modules are pure — no Date.now(), no setInterval" rule from // CLAUDE.md Code Style. The single allowed wall-clock owner is // src/sim/scheduler/clock.ts (which exports the Clock interface and // the wallClock + FakeClock implementations). // // Severity is `error` so `npm run lint --max-warnings 0` fails on a // violation. The deliberate-violation fixture under // src/sim/__test_violation__/ is excluded; it exists ONLY to be lint- // tested by Task 3's Vitest test (which runs ESLint programmatically // with `ignore: false`). // --------------------------------------------------------------------- { files: ['src/sim/**/*.{ts,tsx}'], // Per-block ignores. Note: src/sim/__test_violation__/** is NOT // listed here even though it's globally ignored by Block 1 — the // programmatic ESLint test (with `ignore: false`) overrides the // global ignore, and we WANT the rule to apply to the violator // fixture in that test path so the test can assert it fires. ignores: ['src/sim/scheduler/clock.ts'], languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true }, }, }, rules: { 'no-restricted-syntax': [ 'error', { selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']", message: "src/sim/** must inject time; only src/sim/scheduler/clock.ts may read Date.now() (CONTEXT D-33).", }, { selector: "CallExpression[callee.name='setInterval']", message: "src/sim/** must not use setInterval; the scheduler drives ticks via the Phaser game loop (CORE-02).", }, ], }, }, // --------------------------------------------------------------------- // 2. Phase-1 architectural firewall (CORE-10). // // Seven src/ subsystem types matching CONTEXT D-10's directory layout, // plus `app` (the React/Phaser bridge files at src/main.tsx, src/App.tsx, // src/PhaserGame.tsx) and `game` (the Phaser scene tree at src/game/**). // // Default posture is `allow` — Phase 1 enforces ONE rule, not a // closed-by-default architecture. Future phases may add cross-subsystem // restrictions (e.g., `render` cannot import `save`) without changing // the default. // --------------------------------------------------------------------- { files: ['src/**/*.{ts,tsx,js,jsx,mjs,cjs}'], plugins: { boundaries }, languageOptions: { // Parser-only integration with typescript-eslint. Lets ESLint // parse TS / TSX (incl. JSX) so the boundaries rule can inspect // imports. No tseslint rule sets are enabled — that is out of // Phase-1 scope (Plan 02 owns ONE rule: CORE-10). 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/**/*'], // Quietly tolerate files that aren't classified (e.g., src/vite-env.d.ts, // src/__sentinel__.test.ts). The firewall rule only fires on // sim → {render, ui} edges; unclassified files don't trigger it. 'boundaries/ignore': ['src/vite-env.d.ts', 'src/__sentinel__.test.ts'], // eslint-plugin-boundaries needs to RESOLVE import paths to disk // files in order to classify the import target's element type. // Without a TS-aware resolver, `import x from '../../render/foo'` // (no extension) cannot be resolved to `src/render/foo.ts` and // the target is marked `isUnknown`, silently skipping the rule. // eslint-import-resolver-typescript reads tsconfig.json to follow // bare-extension TS imports. Verified empirically during Plan 02 // execution; see 01-02-SUMMARY.md "Deviations" (Rule 1 — Bug fix). 'import/resolver': { typescript: { alwaysTryTypes: true, project: ['./tsconfig.app.json', './tsconfig.node.json'], // Suppress "Multiple projects found" noise — we deliberately // use the referenced-projects tsconfig layout (root tsconfig // with `references`) per Plan 01. noWarnOnMultipleProjects: true, }, }, }, rules: { // CORE-10: the simulation core cannot reach into render or UI. // Severity MUST be `error` — `npm run lint` runs with // `--max-warnings 0` (per Plan 01), so a warning would also fail // CI, but `error` makes intent unambiguous. 'boundaries/element-types': ['error', { default: 'allow', rules: [ { from: ['sim'], disallow: ['render', 'ui'] }, ], }], }, }, ];