From 2a8d354b58daa3e3450b72835565cc46fb413c25 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 9 May 2026 09:20:44 -0400 Subject: [PATCH] chore(02-01): eslint sim-purity rule + Date.now violator fixture - eslint.config.js block 3: no-restricted-syntax bans Date.now() and setInterval() inside src/sim/**, with src/sim/scheduler/clock.ts as the single allowed wall-clock owner (CONTEXT D-33, RESEARCH Pitfall 1) - date-now-violator.ts deliberate-violation fixture (excluded from default lint by Block 1's top-level ignores; the programmatic ESLint test passes ignore: false to override) - lint-firewall.test.ts gains 2 new cases: positive (rule fires on violator) + negative (rule does NOT fire on clock.ts the one exception) - Existing CORE-10 firewall test left untouched and remains green --- eslint.config.js | 48 +++++++++++++++++++ .../__test_violation__/date-now-violator.ts | 14 ++++++ .../__test_violation__/lint-firewall.test.ts | 34 +++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/sim/__test_violation__/date-now-violator.ts diff --git a/eslint.config.js b/eslint.config.js index d1e35b1..619f8d8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -49,6 +49,54 @@ export default [ ], }, + // --------------------------------------------------------------------- + // 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). // diff --git a/src/sim/__test_violation__/date-now-violator.ts b/src/sim/__test_violation__/date-now-violator.ts new file mode 100644 index 0000000..b1f0d59 --- /dev/null +++ b/src/sim/__test_violation__/date-now-violator.ts @@ -0,0 +1,14 @@ +// DELIBERATE VIOLATION OF CONTEXT D-33 — 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 +// no-restricted-syntax rule (Phase 2 sim-purity) actually fires. +// +// The Vitest test runs ESLint programmatically with `ignore: false` +// against this file and asserts that `no-restricted-syntax` fires with +// the D-33 message. + +export function violator(): number { + return Date.now(); // intentional violation — Phase 2 Plan 02-01 Task 3 +} diff --git a/src/sim/__test_violation__/lint-firewall.test.ts b/src/sim/__test_violation__/lint-firewall.test.ts index 5675a47..6686bae 100644 --- a/src/sim/__test_violation__/lint-firewall.test.ts +++ b/src/sim/__test_violation__/lint-firewall.test.ts @@ -47,3 +47,37 @@ describe('CORE-10: src/sim/ cannot import from src/render/ or src/ui/', () => { expect(combined).toMatch(/render|ui/i); }); }); + +describe('Phase 2 sim-purity rule (CONTEXT D-33)', () => { + it('eslint flags Date.now() inside src/sim/** as no-restricted-syntax', async () => { + const eslint = new ESLint({ + overrideConfigFile: resolve(process.cwd(), 'eslint.config.js'), + ignore: false, + }); + const fixturePath = resolve( + process.cwd(), + 'src/sim/__test_violation__/date-now-violator.ts', + ); + const results = await eslint.lintFiles([fixturePath]); + + expect(results).toHaveLength(1); + const violations = results[0].messages.filter( + (m) => m.ruleId === 'no-restricted-syntax', + ); + expect(violations.length).toBeGreaterThanOrEqual(1); + expect(violations[0].message).toMatch(/inject time|D-33/); + }); + + it('does NOT flag Date.now() inside src/sim/scheduler/clock.ts (the one exception)', async () => { + const eslint = new ESLint({ + overrideConfigFile: resolve(process.cwd(), 'eslint.config.js'), + ignore: false, + }); + const clockPath = resolve(process.cwd(), 'src/sim/scheduler/clock.ts'); + const results = await eslint.lintFiles([clockPath]); + const noRestrictedViolations = results[0].messages.filter( + (m) => m.ruleId === 'no-restricted-syntax', + ); + expect(noRestrictedViolations).toHaveLength(0); + }); +});