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
This commit is contained in:
@@ -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).
|
// 2. Phase-1 architectural firewall (CORE-10).
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -47,3 +47,37 @@ describe('CORE-10: src/sim/ cannot import from src/render/ or src/ui/', () => {
|
|||||||
expect(combined).toMatch(/render|ui/i);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user