import js from "@eslint/js"; import tseslint from "typescript-eslint"; import reactHooks from "eslint-plugin-react-hooks"; /* The clock guard. The artifact's sync bug was a fresh device stamping its own empty defaults as newer than the server's real data. db/writes.ts owns the distinction between a seed write (updated_at stays 0) and a user edit (stamped), so it is the only file under src/db/ allowed to read the clock. Anywhere else, reaching for Date.now() means someone is about to stamp a row without deciding which kind of write it is. */ const CLOCK_BAN = [ { selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']", message: "Only db/writes.ts may read the clock. Use a seedX() helper (leaves updated_at 0) " + "or an editX() helper (stamps it) — see the timestamp rule in db/writes.ts.", }, { selector: "NewExpression[callee.name='Date']", message: "Only db/writes.ts may read the clock. Use a seedX() or editX() helper from db/writes.ts.", }, ]; export default tseslint.config( // lib/, data/ and validate.mjs are copied in from the export bundle // unchanged and must stay byte-identical — they are not ours to restyle. { ignores: [ "**/dist/**", "**/node_modules/**", "export/**", "vendor/**", "app/android/**", "lib/**", "data/**", "validate.mjs", ], }, js.configs.recommended, ...tseslint.configs.recommended, // Build scripts and the shared modules are plain Node ESM. { files: ["tools/**/*.mjs", "shared/**/*.mjs", "*.config.js"], languageOptions: { globals: { console: "readonly", process: "readonly", URL: "readonly", fetch: "readonly", Buffer: "readonly", }, }, }, /* The rules of hooks, and exhaustive deps in particular. A stale closure in an effect is what let the tutor seed its opening turn three times. */ { files: ["app/src/**/*.{ts,tsx}"], plugins: { "react-hooks": reactHooks }, rules: { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", }, }, { files: ["app/src/db/**/*.ts"], rules: { "no-restricted-syntax": ["error", ...CLOCK_BAN] }, }, { files: ["app/src/db/writes.ts"], rules: { "no-restricted-syntax": "off" }, }, );