Files
familysync/.planning/milestones/v1.1-phases/13-real-lint-gate-eslint/13-01-PLAN.md
T
2026-06-18 22:21:38 -04:00

216 lines
18 KiB
Markdown

---
phase: 13-real-lint-gate-eslint
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- package.json
- eslint.config.js
- .prettierrc
- .prettierignore
- apps/api/package.json
- apps/pwa/package.json
- pnpm-lock.yaml
autonomous: true
requirements: []
must_haves:
truths:
- 'ESLint flat config exists at repo root and loads without error (D-13-01: typescript-eslint recommendedTypeChecked via projectService:true; D-13-03: no strict/strictTypeChecked presets)'
- 'React + react-hooks rules apply ONLY to apps/pwa/** globs; apps/api receives no React config (D-13-02)'
- 'Every package lint invocation bakes --max-warnings 0 so any warning fails (D-13-04)'
- 'Lint covers app src/, vitest tests, Playwright e2e specs, and config files (D-13-09); config + apps/api/tests files get a disableTypeChecked override so type-aware linting does not error on out-of-project files (D-13-10)'
- 'Prettier is wired as a standalone gate (format / format:check scripts) with eslint-config-prettier/flat imported LAST in the flat config to disable conflicting formatting rules (D-13-07)'
- 'Running `pnpm lint` on an introduced deliberate violation exits non-zero (SC-1); the throwaway file is deleted and never committed'
- 'Running `pnpm format:check` on an introduced unformatted file exits non-zero (SC-1); the throwaway file is deleted and never committed'
artifacts:
- path: 'eslint.config.js'
provides: 'Root flat ESLint config: base type-aware block, pwa-react block, disableTypeChecked override block, prettier-last'
contains: 'recommendedTypeChecked'
min_lines: 40
- path: '.prettierrc'
provides: 'Prettier defaults matching existing single-quote codebase style'
contains: 'singleQuote'
- path: '.prettierignore'
provides: 'Prettier excludes (dist, node_modules, lockfile, generated migrations, html)'
- path: 'package.json'
provides: 'Root type:module + format/format:check scripts'
contains: 'format:check'
- path: 'apps/api/package.json'
provides: 'api lint script mirroring typecheck idiom'
contains: '--max-warnings 0'
- path: 'apps/pwa/package.json'
provides: 'pwa lint script mirroring typecheck idiom'
contains: '--max-warnings 0'
key_links:
- from: 'package.json (root lint script)'
to: 'apps/api/package.json + apps/pwa/package.json lint scripts'
via: 'pnpm -r --if-present lint'
pattern: 'pnpm -r --if-present lint'
- from: 'eslint.config.js'
to: 'apps/api/tsconfig.json + apps/pwa/tsconfig.json + apps/pwa/tsconfig.e2e.json'
via: 'parserOptions.projectService:true + tsconfigRootDir'
pattern: 'projectService'
---
<objective>
Install ESLint v9 + typescript-eslint + React/react-hooks + Prettier as root devDependencies, author the root flat `eslint.config.js`, add Prettier config, wire per-package `lint` scripts and root `format`/`format:check` scripts, and add `"type": "module"` to the root package.json. Then prove the gate actually fails (SC-1) with a throwaway-violation smoke test for BOTH `pnpm lint` and `pnpm format:check`, deleting the throwaway files (they must not be committed).
This is the foundation wave: nothing fixes existing violations yet — `pnpm lint` is EXPECTED to report the codebase's real first-run violations at the end of this plan. That cleanup is Plan 02. This plan's success is: config loads, the gate fails on a deliberate violation, and the gate machinery is fully wired.
Purpose: Replace the hollow no-op CI lint slot with a real, type-aware gate. Without this, `pnpm lint` exits 0 regardless of code quality.
Output: eslint.config.js, .prettierrc, .prettierignore, root + per-package script changes, pnpm-lock.yaml updated. No source-code fixes.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md
@.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md
@.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
@.planning/phases/13-real-lint-gate-eslint/13-VALIDATION.md
# Files this plan reads/edits:
@package.json
@apps/api/package.json
@apps/pwa/package.json
@apps/api/tsconfig.json
@apps/pwa/tsconfig.json
@apps/pwa/tsconfig.e2e.json
</context>
<artifacts_this_phase_produces>
New files created in THIS plan:
- `eslint.config.js` (root, ESM flat config)
- `.prettierrc` (root)
- `.prettierignore` (root)
New scripts added in THIS plan:
- root `package.json`: `format`, `format:check`, plus `"type": "module"`
- `apps/api/package.json`: `lint`
- `apps/pwa/package.json`: `lint`
New devDependencies (root): eslint, @eslint/js, typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, eslint-config-prettier, prettier.
Downstream (later plans): Plan 02 fixes the violations this config surfaces; Plan 03 adds the CI `Format check` step and asserts the green baseline.
</artifacts_this_phase_produces>
<tasks>
<task type="auto">
<name>Task 1: Install lint/format devDependencies + author flat config + Prettier config</name>
<files>package.json, eslint.config.js, .prettierrc, .prettierignore, apps/api/package.json, apps/pwa/package.json, pnpm-lock.yaml</files>
<read_first>
- 13-RESEARCH.md Pattern 1 (full flat-config skeleton, verbatim authority), Pitfalls 1/2/3/7, "Prettier Configuration" section, Standard Stack version table.
- 13-PATTERNS.md "eslint.config.js (root) — GREENFIELD" + the config-files override list + the typecheck→lint script-mirroring section.
- apps/api/tsconfig.json (confirms `exclude: ["tests"]` → apps/api/tests must go in the disableTypeChecked override), apps/pwa/tsconfig.json, apps/pwa/tsconfig.e2e.json.
- package.json (root) — existing `lint`/`typecheck` script idiom to mirror.
</read_first>
<action>
Install all lint/format packages as ROOT workspace devDependencies (single install so one eslint invocation sees every tsconfig for projectService) at the EXACT pinned versions from RESEARCH.md Standard Stack: eslint@9.39.4, @eslint/js@9.39.4, typescript-eslint@8.61.0, eslint-plugin-react@7.37.5, eslint-plugin-react-hooks@7.1.1, eslint-config-prettier@10.1.8, prettier@3.8.4. Use `pnpm add -D -w` from the repo root. DO NOT install eslint 10.x — it breaks eslint-plugin-react@7.37.5 at runtime (jsx-eslint/eslint-plugin-react#3977, "contextOrFilename.getFilename is not a function"); the 9.39.4 pin is non-negotiable per D-13-01 compatibility (Pitfall 1).
Add `"type": "module"` to the root package.json (PATTERNS.md: root has no CJS entry points, so this is safe and is the chosen route over a `.mjs` extension). This makes `eslint.config.js` parse as ESM.
Author `eslint.config.js` at repo root following RESEARCH.md Pattern 1 skeleton. Required structure, in order:
1. Global `ignores` block: dist (`**/dist/**` — build output incl. apps/api/dist, apps/pwa/dist and the generated sw.js under dist), `**/node_modules/**`, generated Drizzle migrations (`apps/api/src/db/migrations/**`), `pnpm-lock.yaml`. (apps/api/dist must be ignored — grep already showed lint-able-looking JS there.)
2. Base block — files `apps/**/*.{ts,tsx}`, extends `js.configs.recommended` + `tseslint.configs.recommendedTypeChecked` (D-13-01; NOT strict/strictTypeChecked per D-13-03), with `languageOptions.parserOptions.projectService: true` and `tsconfigRootDir: import.meta.dirname`. Add the `@typescript-eslint/no-unused-vars` rule override with `argsIgnorePattern: '^_'` + `varsIgnorePattern: '^_'` (RESEARCH no-unused-vars section).
3. PWA-React block — files `apps/pwa/**/*.{ts,tsx}` ONLY (D-13-02), extends `reactPlugin.configs.flat.recommended` + `reactHooks.configs.flat.recommended`, `settings.react.version: 'detect'`. Explicitly turn OFF `react/prop-types` (TypeScript supersedes it in React 19 — RESEARCH Open Question 2, reduces first-run noise).
4. disableTypeChecked override block (D-13-10) — files: `apps/api/drizzle.config.ts`, `apps/api/vitest.config.ts`, `apps/pwa/vite.config.ts`, `apps/pwa/vitest.config.ts`, `apps/pwa/playwright.config.ts`, AND `apps/api/tests/**/*.ts` (excluded from apps/api/tsconfig.json per Pitfall 2) — extends `tseslint.configs.disableTypeChecked`. Also add `eslint.config.js` itself if projectService complains about it.
5. `eslint-config-prettier/flat` imported LAST in the array (D-13-07, Pitfall 7 — use the `/flat` import path, NOT bare `eslint-config-prettier`). Import the react plugin under the canonical name (no aliasing) so prettier-config can disable its formatting rules.
Do NOT use `--cache` anywhere — incompatible with type-aware linting (RESEARCH Performance / Anti-Patterns).
Author `.prettierrc` with RESEARCH.md defaults: `semi: true`, `singleQuote: true` (matches the existing single-quote codebase — avoids a needless massive diff), `tabWidth: 2`, `trailingComma: "all"`, `printWidth: 100`.
Author `.prettierignore`: `dist/`, `node_modules/`, `pnpm-lock.yaml`, `apps/api/src/db/migrations/`, `*.html`.
Add scripts:
- root package.json: `"format": "prettier --write ."`, `"format:check": "prettier --check ."`. Leave the existing root `"lint": "pnpm -r --if-present lint"` unchanged (it auto-activates once the package lint scripts below exist).
- apps/api/package.json: `"lint": "eslint src/ tests/ --max-warnings 0"` (mirror the existing `typecheck` script style: single command, directory-scoped, no wrappers). `--max-warnings 0` per D-13-04.
- apps/pwa/package.json: `"lint": "eslint src/ e2e/ --max-warnings 0"`. e2e/ is covered by tsconfig.e2e.json which projectService discovers automatically.
</action>
<verify>
<automated>cd /home/luc/Projects/familysync && node -e "import('eslint').then(()=>console.log('eslint loads'))" && grep -q "recommendedTypeChecked" eslint.config.js && grep -q "projectService" eslint.config.js && grep -q "disableTypeChecked" eslint.config.js && grep -q "eslint-config-prettier/flat" eslint.config.js && grep -q "apps/pwa/\*\*" eslint.config.js && grep -q "apps/api/tests" eslint.config.js && grep -q '"type": *"module"' package.json && grep -q "singleQuote" .prettierrc && grep -q -- "--max-warnings 0" apps/api/package.json && grep -q -- "--max-warnings 0" apps/pwa/package.json && grep -q "format:check" package.json && echo ALL_CONFIG_PRESENT</automated>
</verify>
<done>eslint, typescript-eslint, react/react-hooks, eslint-config-prettier, prettier installed at pinned versions in root devDependencies (pnpm-lock.yaml updated). eslint.config.js exists with all five blocks in order (ignores → base recommendedTypeChecked → pwa-react → disableTypeChecked override incl. apps/api/tests → prettier-last). .prettierrc + .prettierignore exist. Root has `type: module` + format/format:check scripts. Both apps have a `lint` script with `--max-warnings 0`. The grep gate above prints ALL_CONFIG_PRESENT.</done>
</task>
<task type="auto">
<name>Task 2: Prove the gate fails (SC-1) — deliberate-violation smoke test for lint AND format, then delete throwaways</name>
<files>(throwaway, never committed) apps/api/src/_lint-gate-test.ts, (throwaway, never committed) _format-gate-test.ts</files>
<read_first>
- 13-VALIDATION.md "Deliberate-Violation Test (ROADMAP success criterion 1)" + "Manual-Only Verifications" table.
- 13-RESEARCH.md "Deliberate-Violation Test" subsection (the exact throwaway file content).
</read_first>
<action>
Prove SC-1 — the gate is real, not a no-op. This is a one-time smoke proof; the throwaway files MUST NOT be committed.
Lint gate proof:
1. Create `apps/api/src/_lint-gate-test.ts` containing a function that returns void and has an unhandled promise on its own line — `Promise.resolve(1)` with no await/void/.catch — which triggers `@typescript-eslint/no-floating-promises` under recommendedTypeChecked. (This file IS inside apps/api/src so it is in the tsconfig project and gets type-aware rules.)
2. Run `pnpm lint` (or `pnpm --filter @familysync/api lint`). Capture the exit code. It MUST be non-zero AND the output MUST mention `no-floating-promises`. NOTE: the rest of the codebase's real violations will ALSO report here — that is expected at this wave; the proof is that this specific deliberate violation is detected and exit is non-zero.
3. Delete `apps/api/src/_lint-gate-test.ts`.
Format gate proof:
1. Create a temporary file at repo root `_format-gate-test.ts` with deliberately bad formatting that Prettier will reject — e.g. double quotes + no trailing semicolons + irregular indentation (opposite of the .prettierrc settings).
2. Run `pnpm format:check`. Exit code MUST be non-zero and the output MUST list `_format-gate-test.ts`.
3. Delete `_format-gate-test.ts`.
HARD RULE: both throwaway files are deleted in this task. Do NOT `git add` them. The SUMMARY must record both exit codes observed (non-zero) as evidence for SC-1. Do not attempt to make the whole `pnpm lint` exit 0 here — that is Plan 02's job.
</action>
<verify>
<automated>cd /home/luc/Projects/familysync && test ! -e apps/api/src/_lint-gate-test.ts && test ! -e _format-gate-test.ts && ! git ls-files --error-unmatch apps/api/src/_lint-gate-test.ts 2>/dev/null && ! git ls-files --error-unmatch _format-gate-test.ts 2>/dev/null && echo THROWAWAYS_ABSENT_AND_UNTRACKED</automated>
</verify>
<done>SC-1 demonstrated: `pnpm lint` exited non-zero with `no-floating-promises` reported on the deliberate file, and `pnpm format:check` exited non-zero listing the unformatted throwaway. Both throwaway files are deleted and were never staged/committed. The verify gate prints THROWAWAYS_ABSENT_AND_UNTRACKED. Exit codes are recorded in the SUMMARY as SC-1 evidence.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
| ----------------------------------- | ------------------------------------------------------------------------------- |
| npm registry → repo devDependencies | New dev-tooling packages (eslint et al.) enter the supply chain at install time |
This is a dev-tooling-only phase: no runtime, network, auth, or user-data surface is touched. STRIDE surface is limited to supply-chain (Tampering) at install time.
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
| --------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T-13-SC | Tampering | pnpm devDependency installs (eslint, typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, eslint-config-prettier, prettier) | accept | RESEARCH.md Package Legitimacy Audit ran the gate: all 7 packages map to canonical official GitHub repos with tens/hundreds of millions of weekly downloads; none flagged SLOP; no postinstall scripts; three SUS-flagged only on a too-new-release signal and explicitly approved (no human checkpoint required). Versions pinned exactly. |
| T-13-01 | Tampering | flat config formatting-rule conflict | mitigate | Import `eslint-config-prettier/flat` LAST (D-13-07, Pitfall 7) so ESLint never re-reports formatting that Prettier owns — prevents a fix/format feedback loop. Verified by grep gate. |
</threat_model>
<verification>
- `node -e "import('eslint')..."` resolves → eslint installed and importable.
- All flat-config blocks present (grep gate in Task 1): recommendedTypeChecked, projectService, disableTypeChecked, eslint-config-prettier/flat, apps/pwa/** scoping, apps/api/tests override.
- Both apps' lint scripts carry `--max-warnings 0`; root has format/format:check + type:module.
- SC-1: deliberate lint violation → `pnpm lint` exits non-zero (no-floating-promises); deliberate unformatted file → `pnpm format:check` exits non-zero; both throwaways deleted and untracked.
</verification>
<success_criteria>
- ESLint flat config loads without error and is structured exactly per RESEARCH Pattern 1 (covers D-13-01/02/03/04/07/09/10).
- Prettier config + scripts wired; root is `type: module`.
- `pnpm lint` is now a REAL gate that fails on a deliberate violation (SC-1, lint half).
- `pnpm format:check` fails on a deliberate unformatted file (SC-1, format half).
- No throwaway test files committed.
- (Out of scope here: making the whole repo lint/format-clean — that is Plans 02 and 03.)
</success_criteria>
<output>
Create `.planning/phases/13-real-lint-gate-eslint/13-01-SUMMARY.md` when done. Record: pinned versions actually installed, the SC-1 lint and format:check exit codes observed, and an inventory of the violation classes `pnpm lint` reported across both apps (counts per rule) — Plan 02 consumes this inventory to scope the fixes.
</output>