import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'; import prettier from 'eslint-config-prettier/flat'; const eslintConfig = [ ...nextCoreWebVitals, prettier, { // Scope the typescript-eslint rule overrides to TS/TSX files. Without // the `files` filter, eslint flat-config attempts to apply these // rules to every walked file (including root-level JS / mjs / json // configs) and fails because the typescript-eslint plugin only // registers itself for TS/TSX. Surfaced 2026-05-14 when CI's // `pnpm lint` command ran across the whole repo root. files: ['**/*.ts', '**/*.tsx'], rules: { '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // React Compiler safety rules shipped with eslint-config-next@16 / // react-hooks@7. Triage status (2026-05-13 sweep): // purity, set-state-in-render, immutability, refs, // set-state-in-effect — promoted to error after the cleanup // sweep (Wave 3 of the 2026-05-12 audit). All hits migrated to // either useQuery, render-phase derivation, key-based remount, // or a justified eslint-disable for canonical setState-on- // subscription patterns. New regressions block CI. // incompatible-library — informational only ("Compiler // skipped this file because of a non-Compiler-safe import"). // No action needed; silenced to keep `pnpm lint` output // actionable. 'react-hooks/purity': 'error', 'react-hooks/set-state-in-render': 'error', 'react-hooks/immutability': 'error', 'react-hooks/refs': 'error', 'react-hooks/set-state-in-effect': 'error', 'react-hooks/incompatible-library': 'off', // Icon-only buttons must carry a label that screen readers can // surface — either an explicit `aria-label`, an `aria-labelledby`, // a `title`, or a visible-but-sr-only text child. Catches the // pattern where a `` ships with no // accessible name. Default Next config enables this at `error`; // we keep it loud so new code doesn't regress. 'jsx-a11y/control-has-associated-label': [ 'warn', { labelAttributes: ['label'], controlComponents: ['Button'], ignoreElements: ['audio', 'canvas', 'embed', 'input', 'textarea', 'tr', 'video'], ignoreRoles: [ 'grid', 'listbox', 'menu', 'menubar', 'radiogroup', 'row', 'tablist', 'toolbar', 'tree', 'treegrid', ], depth: 5, }, ], }, }, { // User-facing copy in src/components and src/app should never use // em-dashes (—) in JSX text. The user reads em-dashes as a // tell-tale "AI-generated" marker; we prefer periods, commas, or // simple hyphens. Code comments / audit-log strings / templates // outside these directories are exempt. // // Same rule block also nudges new code toward CSS logical properties // (ms-/me-/ps-/pe-/text-start/text-end/border-s/border-e) instead of // physical Tailwind utilities. RTL isn't a roadmap requirement today, // but every new ml-/mr-/pl-/pr-/text-left/text-right we accept now // is a class we'd have to migrate later. Existing 1,000+ sites stay // untouched (warn-only). Inline `// eslint-disable-next-line` when // the directional intent is truly physical (e.g. a chevron icon). files: ['src/components/**/*.tsx', 'src/app/**/*.tsx'], rules: { // Both selectors share `warn` severity because the RTL nudge is // grandfathered (1,000+ existing sites use ml-/mr-/etc). The // em-dash sweep cleared every existing instance (2026-05-21), so // `warn` still effectively gates new code — it just doesn't break // CI on grandfathered RTL utilities. Inline // `// eslint-disable-next-line no-restricted-syntax` when the // directional intent is truly physical. 'no-restricted-syntax': [ 'warn', { selector: "JSXText[value=/\\u2014/]", message: 'No em-dash in user-facing JSX text. Use period, comma, or hyphen instead.', }, { selector: "JSXAttribute[name.name='className'] > Literal[value=/(?:^|[\\s:])(?:ml-|mr-|pl-|pr-|text-left|text-right|border-l\\b|border-r\\b|rounded-l-|rounded-r-)/]", message: 'Prefer CSS logical properties (ms-/me-/ps-/pe-/text-start/text-end/border-s/border-e/rounded-s-/rounded-e-) over physical directional Tailwind utilities. Existing code is grandfathered; new code should default to logical so a future RTL pass is bounded.', }, ], }, }, { // Tests assert response shape via expect() — narrowing every // `res.json()` to a structural type adds boilerplate without catching // bugs. Allow `any` casts at JSON boundaries in test files. Also // relax unused-vars to warn (destructured-but-unused helpers are // common in setup/teardown patterns). files: ['tests/**/*.ts', 'tests/**/*.tsx'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], }, }, { ignores: [ 'client-portal/**', 'next-env.d.ts', // Agent worktree artifacts — not part of the canonical tree. '.claude/**', // Build output + Next generated types '.next/**', 'dist/**', // Other sub-projects with their own toolchains 'website/**', ], }, ]; export default eslintConfig;