style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -16,41 +16,41 @@ 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"
|
||||
- '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"
|
||||
- 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"
|
||||
- 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"
|
||||
- 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>
|
||||
@@ -77,6 +77,7 @@ Output: eslint.config.js, .prettierrc, .prettierignore, root + per-package scrip
|
||||
@.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
|
||||
@@ -87,11 +88,13 @@ Output: eslint.config.js, .prettierrc, .prettierignore, root + per-package scrip
|
||||
|
||||
<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`
|
||||
@@ -132,6 +135,7 @@ Downstream (later plans): Plan 02 fixes the violations this config surfaces; Pla
|
||||
- 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>
|
||||
@@ -160,6 +164,7 @@ Downstream (later plans): Plan 02 fixes the violations this config surfaces; Pla
|
||||
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>
|
||||
@@ -170,20 +175,22 @@ Downstream (later plans): Plan 02 fixes the violations this config surfaces; Pla
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 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 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>
|
||||
@@ -194,13 +201,14 @@ This is a dev-tooling-only phase: no runtime, network, auth, or user-data surfac
|
||||
</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>
|
||||
</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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
phase: 13-real-lint-gate-eslint
|
||||
plan: "01"
|
||||
plan: '01'
|
||||
subsystem: dev-tooling
|
||||
tags: [eslint, prettier, lint-gate, typescript-eslint, react-hooks, flat-config]
|
||||
dependency_graph:
|
||||
@@ -10,7 +10,7 @@ dependency_graph:
|
||||
tech_stack:
|
||||
added:
|
||||
- eslint@9.39.4 (root devDep — pinned, ESLint 10 breaks eslint-plugin-react)
|
||||
- "@eslint/js@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
|
||||
@@ -32,13 +32,13 @@ key_files:
|
||||
- apps/pwa/package.json (lint script --max-warnings 0)
|
||||
- pnpm-lock.yaml
|
||||
decisions:
|
||||
- "ESLint pinned to 9.39.4 (non-negotiable): eslint-plugin-react@7.37.5 declares peer ^9.7 and hits TypeError at runtime on ESLint 10 (getFilename removed)"
|
||||
- "react/react-in-jsx-scope explicitly disabled: flat.recommended enables it at error; codebase uses jsx:react-jsx (React 17+ transform) so React import is not required"
|
||||
- "apps/api/tests/** in disableTypeChecked block: excluded from apps/api/tsconfig.json so projectService cannot type-check them"
|
||||
- "eslint.config.js itself added to disableTypeChecked block as precaution (JS file, not in any TS project)"
|
||||
- 'ESLint pinned to 9.39.4 (non-negotiable): eslint-plugin-react@7.37.5 declares peer ^9.7 and hits TypeError at runtime on ESLint 10 (getFilename removed)'
|
||||
- 'react/react-in-jsx-scope explicitly disabled: flat.recommended enables it at error; codebase uses jsx:react-jsx (React 17+ transform) so React import is not required'
|
||||
- 'apps/api/tests/** in disableTypeChecked block: excluded from apps/api/tsconfig.json so projectService cannot type-check them'
|
||||
- 'eslint.config.js itself added to disableTypeChecked block as precaution (JS file, not in any TS project)'
|
||||
metrics:
|
||||
duration_minutes: 8
|
||||
completed_date: "2026-06-11"
|
||||
completed_date: '2026-06-11'
|
||||
tasks_completed: 2
|
||||
files_changed: 7
|
||||
---
|
||||
@@ -68,11 +68,13 @@ Authored `.prettierrc` (singleQuote:true, semi:true, tabWidth:2, trailingComma:a
|
||||
### Task 2: SC-1 smoke proof — deliberate violations, both gates, throwaway cleanup
|
||||
|
||||
**Lint gate (SC-1 lint half):**
|
||||
|
||||
- Created `apps/api/src/_lint-gate-test.ts` with an unhandled `Promise.resolve(1)` expression
|
||||
- Ran `pnpm --filter @familysync/api lint` → **exit code 1**, `@typescript-eslint/no-floating-promises` reported on the deliberate file
|
||||
- Deleted `apps/api/src/_lint-gate-test.ts` — never staged or committed
|
||||
|
||||
**Format gate (SC-1 format half):**
|
||||
|
||||
- Created `_format-gate-test.ts` at repo root with double quotes + irregular indentation (opposite of .prettierrc settings)
|
||||
- Ran `pnpm format:check` → **exit code 1**, `_format-gate-test.ts` listed in warnings
|
||||
- Deleted `_format-gate-test.ts` — never staged or committed
|
||||
@@ -93,10 +95,10 @@ Authored `.prettierrc` (singleQuote:true, semi:true, tabWidth:2, trailingComma:a
|
||||
|
||||
## SC-1 Evidence
|
||||
|
||||
| Gate | Deliberate Violation | Exit Code | Rule Reported |
|
||||
|------|---------------------|-----------|---------------|
|
||||
| `pnpm --filter @familysync/api lint` | `Promise.resolve(1)` — unhandled promise | **1 (non-zero)** | `@typescript-eslint/no-floating-promises` |
|
||||
| `pnpm format:check` | double quotes + irregular indent (opposite of .prettierrc) | **1 (non-zero)** | `_format-gate-test.ts` listed as unformatted |
|
||||
| Gate | Deliberate Violation | Exit Code | Rule Reported |
|
||||
| ------------------------------------ | ---------------------------------------------------------- | ---------------- | -------------------------------------------- |
|
||||
| `pnpm --filter @familysync/api lint` | `Promise.resolve(1)` — unhandled promise | **1 (non-zero)** | `@typescript-eslint/no-floating-promises` |
|
||||
| `pnpm format:check` | double quotes + irregular indent (opposite of .prettierrc) | **1 (non-zero)** | `_format-gate-test.ts` listed as unformatted |
|
||||
|
||||
Both throwaway files confirmed absent and untracked (`THROWAWAYS_ABSENT_AND_UNTRACKED`).
|
||||
|
||||
@@ -106,40 +108,42 @@ Plan 02 consumes this inventory to scope fixes. No violations were fixed in this
|
||||
|
||||
### apps/api — 42 errors total
|
||||
|
||||
| Rule | Count | Description |
|
||||
|------|-------|-------------|
|
||||
| `@typescript-eslint/no-unnecessary-type-assertion` | 24 | Redundant `as` casts (mainly in outboxWorker.ts dispatcher blocks) |
|
||||
| `@typescript-eslint/no-unsafe-assignment` | 3 | `any` assignment from ical.js property access |
|
||||
| `@typescript-eslint/no-unsafe-argument` | 4 | `any` args from ical.js property values |
|
||||
| `@typescript-eslint/no-unused-vars` | 7 | Test file variables assigned but unused |
|
||||
| `@typescript-eslint/no-misused-promises` | 1 | `sse.ts:96` — async callback in void context |
|
||||
| `@typescript-eslint/no-base-to-string` | 1 | Template literal with possible `[object Object]` |
|
||||
| `@typescript-eslint/restrict-template-expressions` | 1 | `Record<string, unknown>` in template literal |
|
||||
| `prefer-const` | 1 | `lists.ts:604` — `let` that is never reassigned |
|
||||
| Rule | Count | Description |
|
||||
| -------------------------------------------------- | ----- | ------------------------------------------------------------------ |
|
||||
| `@typescript-eslint/no-unnecessary-type-assertion` | 24 | Redundant `as` casts (mainly in outboxWorker.ts dispatcher blocks) |
|
||||
| `@typescript-eslint/no-unsafe-assignment` | 3 | `any` assignment from ical.js property access |
|
||||
| `@typescript-eslint/no-unsafe-argument` | 4 | `any` args from ical.js property values |
|
||||
| `@typescript-eslint/no-unused-vars` | 7 | Test file variables assigned but unused |
|
||||
| `@typescript-eslint/no-misused-promises` | 1 | `sse.ts:96` — async callback in void context |
|
||||
| `@typescript-eslint/no-base-to-string` | 1 | Template literal with possible `[object Object]` |
|
||||
| `@typescript-eslint/restrict-template-expressions` | 1 | `Record<string, unknown>` in template literal |
|
||||
| `prefer-const` | 1 | `lists.ts:604` — `let` that is never reassigned |
|
||||
|
||||
### apps/pwa — 77 errors total
|
||||
|
||||
| Rule | Count | Description |
|
||||
|------|-------|-------------|
|
||||
| `@typescript-eslint/require-await` | 23 | Async functions with no `await` expression |
|
||||
| `@typescript-eslint/no-unnecessary-type-assertion` | 14 | Redundant `as` casts |
|
||||
| `@typescript-eslint/no-floating-promises` | 11 | Unhandled promises |
|
||||
| `@typescript-eslint/no-unused-vars` | 4 | Unused variables |
|
||||
| `@typescript-eslint/no-unsafe-assignment` | 4 | `any` assignments |
|
||||
| `@typescript-eslint/no-unsafe-member-access` | 3 | Member access on `any` |
|
||||
| `@typescript-eslint/no-misused-promises` | 3 | Async callbacks in void contexts |
|
||||
| `react/no-unescaped-entities` | 1 | Unescaped `'` in JSX |
|
||||
| Rule | Count | Description |
|
||||
| -------------------------------------------------- | ----- | ------------------------------------------ |
|
||||
| `@typescript-eslint/require-await` | 23 | Async functions with no `await` expression |
|
||||
| `@typescript-eslint/no-unnecessary-type-assertion` | 14 | Redundant `as` casts |
|
||||
| `@typescript-eslint/no-floating-promises` | 11 | Unhandled promises |
|
||||
| `@typescript-eslint/no-unused-vars` | 4 | Unused variables |
|
||||
| `@typescript-eslint/no-unsafe-assignment` | 4 | `any` assignments |
|
||||
| `@typescript-eslint/no-unsafe-member-access` | 3 | Member access on `any` |
|
||||
| `@typescript-eslint/no-misused-promises` | 3 | Async callbacks in void contexts |
|
||||
| `react/no-unescaped-entities` | 1 | Unescaped `'` in JSX |
|
||||
|
||||
**Total:** 119 errors across both apps (0 warnings — `--max-warnings 0` working).
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Created files exist:
|
||||
|
||||
- `eslint.config.js` — FOUND
|
||||
- `.prettierrc` — FOUND
|
||||
- `.prettierignore` — FOUND
|
||||
|
||||
Commits exist:
|
||||
|
||||
- `df62d33` — FOUND (chore(13-01): install ESLint+Prettier, author flat config, wire lint/format scripts)
|
||||
- `7a255ea` — FOUND (fix(13-01): disable react/react-in-jsx-scope for React 19 JSX transform)
|
||||
|
||||
|
||||
@@ -14,20 +14,20 @@ autonomous: true
|
||||
requirements: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every first-run ESLint violation across both apps is resolved so `pnpm lint` exits 0 (D-13-05: fix all violations now; phase not done until green)"
|
||||
- "Fixes ADDRESS the violation, never mask it: no blanket eslint-disable; no `void promise` used to silence a floating promise that should be awaited; every eslint-disable-next-line carries a justifying inline comment explaining why the rule is wrong HERE (D-13-06)"
|
||||
- "Floating-promise findings on the push/outbox/reminder broker paths are reviewed as candidate bugs (real await missing?) BEFORE any `void` is applied; legitimate fire-and-forget setInterval ticks may use `void runX().catch(...)` only with that intent documented (D-13-06)"
|
||||
- "ical.js no-unsafe-* findings in broker sync/expand are handled with targeted eslint-disable-next-line + a justification comment mirroring the EventForm.tsx:271-275 idiom (external-library weak-typing limitation, not a bug) (D-13-06)"
|
||||
- 'Every first-run ESLint violation across both apps is resolved so `pnpm lint` exits 0 (D-13-05: fix all violations now; phase not done until green)'
|
||||
- 'Fixes ADDRESS the violation, never mask it: no blanket eslint-disable; no `void promise` used to silence a floating promise that should be awaited; every eslint-disable-next-line carries a justifying inline comment explaining why the rule is wrong HERE (D-13-06)'
|
||||
- 'Floating-promise findings on the push/outbox/reminder broker paths are reviewed as candidate bugs (real await missing?) BEFORE any `void` is applied; legitimate fire-and-forget setInterval ticks may use `void runX().catch(...)` only with that intent documented (D-13-06)'
|
||||
- 'ical.js no-unsafe-* findings in broker sync/expand are handled with targeted eslint-disable-next-line + a justification comment mirroring the EventForm.tsx:271-275 idiom (external-library weak-typing limitation, not a bug) (D-13-06)'
|
||||
artifacts:
|
||||
- path: "apps/api/src/broker/sync.ts"
|
||||
provides: "ical.js unsafe-access findings resolved (narrowed or justified-suppressed)"
|
||||
- path: "apps/pwa/src/components/EventForm.tsx"
|
||||
provides: "no-explicit-any on the occurrence.recurrence cast handled via the pre-existing justified suppression"
|
||||
- path: 'apps/api/src/broker/sync.ts'
|
||||
provides: 'ical.js unsafe-access findings resolved (narrowed or justified-suppressed)'
|
||||
- path: 'apps/pwa/src/components/EventForm.tsx'
|
||||
provides: 'no-explicit-any on the occurrence.recurrence cast handled via the pre-existing justified suppression'
|
||||
key_links:
|
||||
- from: "pnpm lint (root)"
|
||||
to: "apps/api + apps/pwa lint scripts"
|
||||
via: "exit code 0 after fixes"
|
||||
pattern: "pnpm -r --if-present lint"
|
||||
- from: 'pnpm lint (root)'
|
||||
to: 'apps/api + apps/pwa lint scripts'
|
||||
via: 'exit code 0 after fixes'
|
||||
pattern: 'pnpm -r --if-present lint'
|
||||
---
|
||||
|
||||
<objective>
|
||||
@@ -52,6 +52,7 @@ Output: lint-clean source across apps/api and apps/pwa. `pnpm lint` exits 0.
|
||||
@.planning/phases/13-real-lint-gate-eslint/13-01-SUMMARY.md
|
||||
|
||||
# The hot-spot source files (per RESEARCH First-Run Violations + PATTERNS):
|
||||
|
||||
@apps/api/src/broker/sync.ts
|
||||
@apps/api/src/broker/expand.ts
|
||||
@apps/api/src/broker/outboxWorker.ts
|
||||
@@ -92,6 +93,7 @@ Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory `pnpm
|
||||
apps/api/tests/**: these are linted with non-type-aware rules (disableTypeChecked override from Plan 01). Fix any no-unused-vars / syntactic violations there too. Three test files already carry `eslint-disable-next-line @typescript-eslint/ban-ts-comment` — leave those (already justified by intent of the @ts-expect-error usage; add a one-line justification comment if absent).
|
||||
|
||||
Do NOT run Prettier and do NOT reformat. Keep edits surgical — only touch lines that resolve a violation.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api lint; test $? -eq 0 && echo API_LINT_GREEN</automated>
|
||||
@@ -121,6 +123,7 @@ Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory `pnpm
|
||||
apps/pwa/e2e/**: covered by tsconfig.e2e.json (type-aware). Fix no-unused-vars / no-floating-promises in specs (Playwright actions are awaited; a missing await on a locator action is a real flake bug — fix it, don't void it).
|
||||
|
||||
Do NOT run Prettier and do NOT reformat. Surgical edits only.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa lint; test $? -eq 0 && echo PWA_LINT_GREEN</automated>
|
||||
@@ -146,20 +149,22 @@ Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory `pnpm
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| external library (ical.js) → broker code | Untyped `any` values cross into sync/expand; unsafe access is the lint signal |
|
||||
| Boundary | Description |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| external library (ical.js) → broker code | Untyped `any` values cross into sync/expand; unsafe access is the lint signal |
|
||||
| async dispatch (push/outbox/reminder) → unhandled rejection | A floating/misused promise here can silently swallow a security-relevant error |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-02 | Tampering | masking a real bug with eslint-disable / `void` | mitigate | D-13-06 enforced per-task: no blanket disables; every suppression carries a justification comment; floating promises on push/outbox/reminder paths are reviewed for a missing-await bug BEFORE any `void`. Reviewer checks the diff for un-justified disables. |
|
||||
| T-13-03 | Information Disclosure | floating promise swallowing errors (broker dispatch) | mitigate | `no-floating-promises` (type-aware) surfaces every unhandled promise; each is either awaited/`.catch()`-handled (real fix) or documented fire-and-forget — never silently voided. |
|
||||
| T-13-04 | Tampering | no-unsafe-* on ical.js-derived data | accept | ical.js is a trusted Mozilla-maintained library with weak types; values are validated at the call site. Suppressions are targeted + justified, not blanket. |
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ---------------------- | ---------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-13-02 | Tampering | masking a real bug with eslint-disable / `void` | mitigate | D-13-06 enforced per-task: no blanket disables; every suppression carries a justification comment; floating promises on push/outbox/reminder paths are reviewed for a missing-await bug BEFORE any `void`. Reviewer checks the diff for un-justified disables. |
|
||||
| T-13-03 | Information Disclosure | floating promise swallowing errors (broker dispatch) | mitigate | `no-floating-promises` (type-aware) surfaces every unhandled promise; each is either awaited/`.catch()`-handled (real fix) or documented fire-and-forget — never silently voided. |
|
||||
| T-13-04 | Tampering | no-unsafe-\* on ical.js-derived data | accept | ical.js is a trusted Mozilla-maintained library with weak types; values are validated at the call site. Suppressions are targeted + justified, not blanket. |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -169,11 +174,12 @@ Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory `pnpm
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- `pnpm lint` exits 0 across both apps (D-13-05).
|
||||
- All fixes address violations, none mask them (D-13-06): justified suppressions only, real bugs genuinely fixed.
|
||||
- typecheck + tests still green.
|
||||
- No Prettier reformatting in this plan (reserved for Plan 03, D-13-08).
|
||||
</success_criteria>
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/13-real-lint-gate-eslint/13-02-SUMMARY.md` when done. Record: per-rule counts fixed; every eslint-disable-next-line added (file:line + the rule + the one-line justification); any real bug found-and-fixed on the async dispatch paths (if a missing await was discovered, call it out — it is a genuine bug catch and the whole point of D-13-01's type-aware choice).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
phase: 13-real-lint-gate-eslint
|
||||
plan: "02"
|
||||
plan: '02'
|
||||
subsystem: lint-gate
|
||||
tags: [eslint, typescript-eslint, react-hooks, lint-fix, type-safety]
|
||||
dependency_graph:
|
||||
@@ -44,11 +44,11 @@ key_files:
|
||||
- apps/pwa/src/routes/ListsIndex.tsx
|
||||
- apps/pwa/src/sw.ts
|
||||
decisions:
|
||||
- "D-13-06 enforced: every eslint-disable carries a justifying comment; no blanket suppressions added"
|
||||
- "React Compiler rules (react-hooks v7 flat.recommended) disabled globally with justification — codebase does not use React Compiler"
|
||||
- "ical.js unsafe-access handled with targeted no-unsafe-assignment/argument per-line suppression (same idiom as EventForm.tsx:271-275)"
|
||||
- "void operator applied to queryClient.invalidateQueries and navigate() as legitimate fire-and-forget; broker push/outbox/reminder paths reviewed — no missing awaits found, existing void+catch pattern is correct"
|
||||
- "as unknown as Response used for partial Response mock objects in client.test.ts (TS2352 fix — not a lint fix but a pre-existing typecheck bug surfaced by verification)"
|
||||
- 'D-13-06 enforced: every eslint-disable carries a justifying comment; no blanket suppressions added'
|
||||
- 'React Compiler rules (react-hooks v7 flat.recommended) disabled globally with justification — codebase does not use React Compiler'
|
||||
- 'ical.js unsafe-access handled with targeted no-unsafe-assignment/argument per-line suppression (same idiom as EventForm.tsx:271-275)'
|
||||
- 'void operator applied to queryClient.invalidateQueries and navigate() as legitimate fire-and-forget; broker push/outbox/reminder paths reviewed — no missing awaits found, existing void+catch pattern is correct'
|
||||
- 'as unknown as Response used for partial Response mock objects in client.test.ts (TS2352 fix — not a lint fix but a pre-existing typecheck bug surfaced by verification)'
|
||||
metrics:
|
||||
duration_minutes: ~90
|
||||
completed: 2026-06-11
|
||||
@@ -66,10 +66,10 @@ ESLint violation elimination across both `apps/api` and `apps/pwa` until `pnpm l
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| Task | Description | Commit |
|
||||
|------|-------------|--------|
|
||||
| 1 | Fix all API ESLint violations | 03e9531 |
|
||||
| 2 | Fix all PWA ESLint violations | 03e9531 |
|
||||
| Task | Description | Commit |
|
||||
| ---- | ----------------------------- | ------- |
|
||||
| 1 | Fix all API ESLint violations | 03e9531 |
|
||||
| 2 | Fix all PWA ESLint violations | 03e9531 |
|
||||
|
||||
Both tasks were committed together as a single atomic commit covering 31 files.
|
||||
|
||||
@@ -129,36 +129,42 @@ Both tasks were committed together as a single atomic commit covering 31 files.
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Unused eslint-disable directives caused by wrong placement**
|
||||
|
||||
- **Found during:** Task 1 and 2 (iterative lint runs)
|
||||
- **Issue:** Disable comments placed on `let` declaration lines were flagged as "unused" because the violations fired on assignment expressions inside try blocks, not on the declarations.
|
||||
- **Fix:** Moved disables inside the try blocks, on the line immediately before the violating expression.
|
||||
- **Files:** `sync.ts`, `expand.ts`, `vevent.ts`, `sw.ts`
|
||||
|
||||
**2. [Rule 2 - Missing] React Compiler rules not in original violation inventory**
|
||||
|
||||
- **Found during:** Task 2 (PWA lint run)
|
||||
- **Issue:** `react-hooks` v7.1.1 `flat.recommended` enables `set-state-in-effect`, `immutability`, `purity`, `refs-in-dom` etc. — React Compiler rules not in original audit. These fired on valid pre-Compiler React patterns.
|
||||
- **Fix:** Disabled all Compiler-only rules in eslint.config.js with justification comment.
|
||||
- **Files:** `eslint.config.js`
|
||||
|
||||
**3. [Rule 1 - Bug] TS2352 in client.test.ts (partial Response mocks)**
|
||||
|
||||
- **Found during:** Task 3 (typecheck verification)
|
||||
- **Issue:** `{ ok: true, json: () => ... } as Response` fails tsc strict check — the partial object doesn't sufficiently overlap with `Response`. This was a pre-existing issue surfaced by running typecheck.
|
||||
- **Fix:** Changed to `as unknown as Response` (double-assert through `unknown` for intentional structural mismatch in test mocks).
|
||||
- **Files:** `apps/pwa/src/api/client.test.ts` (18 occurrences)
|
||||
|
||||
**4. [Rule 1 - Bug] TS2322 in SyncStateToast.test.tsx (null assignment)**
|
||||
|
||||
- **Found during:** Task 3 (typecheck verification)
|
||||
- **Issue:** `mockLastSyncedUid.value` inferred as `string`, but later assigned `null`. Pre-existing tsc error.
|
||||
- **Fix:** Added `as string | null` type annotation to the initializer.
|
||||
- **Files:** `apps/pwa/src/components/SyncStateToast.test.tsx`
|
||||
|
||||
**5. [Rule 3 - Blocking] e2e files not in projectService**
|
||||
|
||||
- **Found during:** Task 2 (PWA lint run)
|
||||
- **Issue:** `apps/pwa/e2e/*.ts` not in the `projectService` tsconfig scope; eslint reported parsing errors.
|
||||
- **Fix:** Added them to the `disableTypeChecked` block in `eslint.config.js`.
|
||||
- **Files:** `eslint.config.js`
|
||||
|
||||
**6. [Rule 1 - Bug] await act(sync callback) → await-thenable**
|
||||
|
||||
- **Found during:** Task 2 (PWA lint)
|
||||
- **Issue:** After removing `async` from `act` callbacks (no inner await), the sync `act(() => {})` returns `void`, not a Promise. `await void` triggers `await-thenable`.
|
||||
- **Fix:** Removed both `async` keyword AND `await` from affected `act(...)` calls in `ListDetail.test.tsx`.
|
||||
@@ -179,6 +185,7 @@ An independent re-run of `pnpm lint` after the original executor reported exit 0
|
||||
**Root cause:** The original deviation-4 fix was contradictory — the SUMMARY described removing the `as string | null` assertion AND adding a type annotation, but only the removal was committed (or the removal was not actually staged). The prior execution left the assertion in place.
|
||||
|
||||
**Residual error:**
|
||||
|
||||
```
|
||||
apps/pwa/src/components/SyncStateToast.test.tsx:29:31 error
|
||||
This assertion is unnecessary since the receiver accepts the original type of the expression
|
||||
@@ -190,6 +197,7 @@ apps/pwa/src/components/SyncStateToast.test.tsx:29:31 error
|
||||
**Fix applied (commit `3f2e3ea`):** Restructured the `vi.hoisted()` callback from an arrow returning an object literal to a block body with an explicit typed const: `const mockLastSyncedUid: { value: string | null } = { value: 'test-uid-123' }`. This satisfies ESLint (no inline assertion) and tsc (null assignment is type-safe). No eslint-disable required.
|
||||
|
||||
**Verified:**
|
||||
|
||||
- `pnpm lint` exit 0
|
||||
- `pnpm typecheck` exit 0
|
||||
- `pnpm --filter @familysync/pwa test` — 191/191 passing
|
||||
@@ -197,15 +205,18 @@ apps/pwa/src/components/SyncStateToast.test.tsx:29:31 error
|
||||
## Self-Check: PASSED
|
||||
|
||||
Files verified:
|
||||
|
||||
- `eslint.config.js` — present, modified
|
||||
- `apps/api/src/broker/sync.ts` — present, modified
|
||||
- `apps/pwa/src/sw.ts` — present, modified
|
||||
- `apps/pwa/src/routes/ListDetail.test.tsx` — present, modified
|
||||
|
||||
Commits verified:
|
||||
|
||||
- `03e9531` — present in git log
|
||||
|
||||
Final verification:
|
||||
|
||||
- `pnpm lint` — exits 0 (both apps clean, --max-warnings 0)
|
||||
- `pnpm typecheck` — exits 0 (both apps + e2e tsconfig)
|
||||
- `pnpm --filter @familysync/pwa test` — 191 tests passing
|
||||
|
||||
@@ -16,18 +16,18 @@ requirements: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "All files are reformatted by Prettier as ONE isolated mechanical commit, separate from Plan 02's logic fixes, for reviewability (D-13-08)"
|
||||
- "CI fast-checks job gains a `Format check` step running `pnpm format:check`, inserted after Lint and before Typecheck; the stale no-op lint comment is removed (SC-2)"
|
||||
- "Baseline gate ends green: `pnpm lint` exits 0 AND `pnpm format:check` exits 0 across both apps (SC-3)"
|
||||
- "CI lint step now genuinely blocks a PR to main on violations (no longer the --if-present no-op), and the new format:check step blocks on format violations (SC-2)"
|
||||
- 'CI fast-checks job gains a `Format check` step running `pnpm format:check`, inserted after Lint and before Typecheck; the stale no-op lint comment is removed (SC-2)'
|
||||
- 'Baseline gate ends green: `pnpm lint` exits 0 AND `pnpm format:check` exits 0 across both apps (SC-3)'
|
||||
- 'CI lint step now genuinely blocks a PR to main on violations (no longer the --if-present no-op), and the new format:check step blocks on format violations (SC-2)'
|
||||
artifacts:
|
||||
- path: ".gitea/workflows/ci.yml"
|
||||
provides: "fast-checks job with a Format check step after Lint; stale no-op comment removed"
|
||||
contains: "format:check"
|
||||
- path: '.gitea/workflows/ci.yml'
|
||||
provides: 'fast-checks job with a Format check step after Lint; stale no-op comment removed'
|
||||
contains: 'format:check'
|
||||
key_links:
|
||||
- from: ".gitea/workflows/ci.yml fast-checks job"
|
||||
to: "root package.json lint + format:check scripts"
|
||||
via: "pnpm lint / pnpm format:check steps"
|
||||
pattern: "pnpm format:check"
|
||||
- from: '.gitea/workflows/ci.yml fast-checks job'
|
||||
to: 'root package.json lint + format:check scripts'
|
||||
via: 'pnpm lint / pnpm format:check steps'
|
||||
pattern: 'pnpm format:check'
|
||||
---
|
||||
|
||||
<objective>
|
||||
@@ -55,15 +55,17 @@ Output: reformatted tree (isolated commit), updated ci.yml, green lint + format
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
No NEW files. MODIFIES:
|
||||
|
||||
- The whole source tree (Prettier reformat — isolated commit).
|
||||
- `.gitea/workflows/ci.yml` — adds the `Format check` step; removes the stale 3-line no-op lint comment (ci.yml lines 28-30).
|
||||
|
||||
This completes the phase's full artifact set across all three plans:
|
||||
|
||||
- New config files (Plan 01): eslint.config.js, .prettierrc, .prettierignore
|
||||
- New scripts (Plan 01): root format/format:check + type:module; apps/api & apps/pwa lint
|
||||
- New CI step (Plan 03): Format check
|
||||
- State: lint-clean (Plan 02) + format-clean (Plan 03) baseline, green in CI.
|
||||
</artifacts_this_phase_produces>
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<tasks>
|
||||
|
||||
@@ -80,6 +82,7 @@ This completes the phase's full artifact set across all three plans:
|
||||
This reformat MUST be its own commit, isolated from Plan 02's lint fixes (D-13-08) — do not mix any logic change into it. After formatting, run `pnpm lint` to confirm the reformat did NOT introduce any lint violation (eslint-config-prettier means ESLint does not fight Prettier, so this should stay green from Plan 02). If `pnpm lint` is non-zero after reformat, a formatting/lint conflict exists — investigate the eslint-config-prettier placement (must be last) before proceeding; do not blanket-disable.
|
||||
|
||||
Record `git diff --stat` (file count + line churn) in the SUMMARY as evidence of the mechanical reformat.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm format:check && pnpm lint && echo FORMAT_AND_LINT_GREEN_AFTER_REFORMAT</automated>
|
||||
@@ -104,6 +107,7 @@ This completes the phase's full artifact set across all three plans:
|
||||
Do NOT touch the `api` or `harness` jobs, the `name: CI` line, or the `fast-checks` job id — those job ids are the required branch-protection contexts (STATE / memory: gitea main is protected, contexts must stay valid). No other CI change. No cache (D-PROBE-04 — the comment already documents why; leave it).
|
||||
|
||||
Validate the YAML parses after the edit.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && grep -q "Format check" .gitea/workflows/ci.yml && grep -q "pnpm format:check" .gitea/workflows/ci.yml && ! grep -q "out of this phase's scope" .gitea/workflows/ci.yml && python3 -c "import yaml,sys; yaml.safe_load(open('.gitea/workflows/ci.yml'))" && grep -q "fast-checks:" .gitea/workflows/ci.yml && echo CI_FORMAT_STEP_WIRED</automated>
|
||||
@@ -129,20 +133,22 @@ This completes the phase's full artifact set across all three plans:
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Boundary | Description |
|
||||
| ------------------- | ---------------------------------------------------------------------------------- |
|
||||
| PR → main (CI gate) | The fast-checks job is the quality boundary; lint/format failures must block merge |
|
||||
|
||||
Dev-tooling/CI-config phase: no runtime, network, auth, or user-data surface. The only sensitive concern is preserving the branch-protection job-id contexts.
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-05 | Tampering / Denial of Service (CI integrity) | renaming the fast-checks job id or CI name while editing ci.yml | mitigate | Task 2 explicitly forbids touching `name: CI`, the `fast-checks` job id, and the api/harness jobs — those are the required branch-protection contexts (gitea main is protected; a renamed context would silently un-gate main). Verify gate asserts `fast-checks:` still present. |
|
||||
| T-13-06 | Tampering | format:check step that never fails (mis-wired) | mitigate | SC-1 (Plan 01) already proved `pnpm format:check` exits non-zero on an unformatted file; Plan 03 only adds the step that runs that proven command. Green baseline (Task 3) confirms it exits 0 on the clean tree. |
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | -------------------------------------------- | --------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-13-05 | Tampering / Denial of Service (CI integrity) | renaming the fast-checks job id or CI name while editing ci.yml | mitigate | Task 2 explicitly forbids touching `name: CI`, the `fast-checks` job id, and the api/harness jobs — those are the required branch-protection contexts (gitea main is protected; a renamed context would silently un-gate main). Verify gate asserts `fast-checks:` still present. |
|
||||
| T-13-06 | Tampering | format:check step that never fails (mis-wired) | mitigate | SC-1 (Plan 01) already proved `pnpm format:check` exits non-zero on an unformatted file; Plan 03 only adds the step that runs that proven command. Green baseline (Task 3) confirms it exits 0 on the clean tree. |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -153,10 +159,11 @@ Dev-tooling/CI-config phase: no runtime, network, auth, or user-data surface. Th
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- D-13-08: reformat is one isolated mechanical commit, separate from logic fixes.
|
||||
- SC-2: CI fast-checks job has a real lint step (blocks on violations) + a new format:check step; stale comment removed; branch-protection contexts intact.
|
||||
- SC-3: `pnpm lint` AND `pnpm format:check` both exit 0 across both apps; baseline gate ends green.
|
||||
</success_criteria>
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/13-real-lint-gate-eslint/13-03-SUMMARY.md` when done. Record: `git diff --stat` of the reformat (files + line churn), the exact ci.yml step ordering after the edit, and the final green exit codes for lint / format:check / typecheck / tests (SC-3 evidence). Note that SC-2's "CI actually blocks the PR" confirmation is a one-time observation on the next real PR to main (per 13-VALIDATION.md Manual-Only table) — flag it for the operator.
|
||||
|
||||
@@ -11,6 +11,7 @@ Stand up a real ESLint flat config across both workspaces (`apps/api` — NodeNe
|
||||
**Scope expanded during discussion (flag for planning):** This phase now also adds **Prettier** with a standalone `prettier --check` CI format gate. The original ROADMAP Phase 13 one-liner scopes to "ESLint only" — planning MUST update the ROADMAP entry and success criteria to include the format gate. Treated as an in-domain expansion (still a CI quality gate; CLAUDE.md's stack table lists "ESLint + Prettier"), not a separate phase.
|
||||
|
||||
**In scope:**
|
||||
|
||||
- ESLint flat config (`eslint.config.js`) — typescript-eslint + React + react-hooks plugins
|
||||
- Package-level `lint` scripts in `apps/api` and `apps/pwa` (so root `pnpm -r --if-present lint` runs a real linter)
|
||||
- Prettier config + root `format` / `format:check` scripts
|
||||
@@ -18,34 +19,41 @@ Stand up a real ESLint flat config across both workspaces (`apps/api` — NodeNe
|
||||
- Fixing all first-run violations so `pnpm lint` AND `pnpm format:check` are green across both apps
|
||||
|
||||
**Out of scope:**
|
||||
|
||||
- CI lint-step plumbing — `.gitea/workflows/ci.yml` already runs `pnpm lint`; the slot auto-activates once package `lint` scripts exist. Only the NEW `format:check` step is added.
|
||||
- Desktop E2E coverage (Phase 14), any non-lint CI changes.
|
||||
</domain>
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Ruleset & strictness
|
||||
|
||||
- **D-13-01:** Use typescript-eslint **`recommendedTypeChecked`** (type-aware), not the non-type-aware `recommended`. Rationale: this is an async-heavy backend (outbox drain, push dispatch, CalDAV/reminder schedulers) — type-aware rules catch floating promises, `no-misused-promises`, and unsafe `any` that syntactic linting misses. Enable via `projectService: true` (resolves all tsconfigs automatically).
|
||||
- **D-13-02:** Add React + react-hooks plugins for `apps/pwa` (per ROADMAP goal). `apps/api` is Node/TS only (no React config).
|
||||
- **D-13-03:** Do NOT adopt `strict`/`strictTypeChecked` presets — too much churn on the existing 91-file codebase; CLAUDE.md warns against bikeshedding.
|
||||
|
||||
### Gate threshold
|
||||
|
||||
- **D-13-04:** Run with **`--max-warnings 0`** — any warning fails CI. Every rule must be a real decision: either error-worthy or off. No non-blocking warnings (they rot into ignored noise).
|
||||
|
||||
### First-run violation strategy
|
||||
|
||||
- **D-13-05:** **Fix all violations now.** The phase is not done until `pnpm lint` and `pnpm format:check` are green across both apps. Real bugs (floating promises, misused promises) get genuinely fixed.
|
||||
- **D-13-06 (HARD CONSTRAINT — for executors):** Fixes must **address** the violation, not mask it. No blanket `eslint-disable` and no `void promise` to silence a floating-promise that should actually be `await`ed. Any suppression (`eslint-disable-next-line`) requires a justifying inline comment explaining why the rule is wrong *here*. A type-aware lint finding is a candidate bug — review before suppressing.
|
||||
- **D-13-06 (HARD CONSTRAINT — for executors):** Fixes must **address** the violation, not mask it. No blanket `eslint-disable` and no `void promise` to silence a floating-promise that should actually be `await`ed. Any suppression (`eslint-disable-next-line`) requires a justifying inline comment explaining why the rule is wrong _here_. A type-aware lint finding is a candidate bug — review before suppressing.
|
||||
|
||||
### Prettier
|
||||
|
||||
- **D-13-07:** Add **Prettier + standalone `prettier --check`** as its own CI step (separate from lint), AND add **`eslint-config-prettier`** to the flat config to disable ESLint's formatting rules. Clean separation: ESLint finds bugs, Prettier owns formatting, no double-reporting. (Rejected: `eslint-plugin-prettier` — slower, noisier, discouraged by Prettier docs.)
|
||||
- **D-13-08:** All files get reformatted in this phase — accepted as one large mechanical diff. Planning should consider isolating the reformat commit from logic fixes for reviewability.
|
||||
|
||||
### File coverage
|
||||
|
||||
- **D-13-09:** Lint **all TS/TSX**: app `src/`, vitest tests, Playwright e2e specs (`apps/pwa` uses `tsconfig.e2e.json`), and config files (`vite.config`, `drizzle.config`, `playwright.config`).
|
||||
- **D-13-10:** Config files / non-project files that `projectService` can't type-check need a dedicated override block (non-type-checked rules, or `disableTypeChecked` for those globs) so type-aware linting doesn't error on them.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Flat-config file layout (single root `eslint.config.js` vs per-app configs) — planner/researcher decides; root config with per-package overrides is the common pattern for a small 2-app pnpm workspace.
|
||||
- Exact Prettier options (`.prettierrc`) — standard defaults; no bikeshedding.
|
||||
- CI step ordering within `fast-checks` (lint → format:check → typecheck → tests).
|
||||
@@ -53,21 +61,25 @@ Stand up a real ESLint flat config across both workspaces (`apps/api` — NodeNe
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### CI integration (the slot this phase fills)
|
||||
|
||||
- `.gitea/workflows/ci.yml` — the `fast-checks` job runs `pnpm lint` (currently a no-op; see the inline comment at the Lint step). This phase makes it real and adds a `format:check` step. No other CI plumbing change.
|
||||
- `package.json` (root) — `lint: pnpm -r --if-present lint`, `typecheck: pnpm -r typecheck`. Add root `format` / `format:check`.
|
||||
|
||||
### Workspace / TS config (constrains the flat config)
|
||||
|
||||
- `apps/api/tsconfig.json` — NodeNext ESM, `target ES2023`, `strict`, excludes `tests`. `type: module`.
|
||||
- `apps/pwa/tsconfig.json` — ESNext / Bundler resolution, `jsx: react-jsx`, `strict`, `noEmit`. `type: module`.
|
||||
- `apps/pwa/tsconfig.e2e.json` — separate project for Playwright specs; must be in the lint projectService set for type-aware linting of e2e tests.
|
||||
- `pnpm-workspace.yaml` — packages: `apps/*`.
|
||||
|
||||
### Stack guidance
|
||||
|
||||
- `CLAUDE.md` (Development Tools table) — "ESLint + Prettier | Standard config; no bikeshedding needed." TypeScript 5.x `strict: true`.
|
||||
|
||||
No external ADRs/specs specific to linting — requirements fully captured in decisions above.
|
||||
@@ -75,17 +87,21 @@ No external ADRs/specs specific to linting — requirements fully captured in de
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
|
||||
- None to reuse — greenfield lint config. No ESLint or Prettier anywhere in the repo today (confirmed: no eslint dep in any `package.json`, no config files).
|
||||
|
||||
### Established Patterns
|
||||
|
||||
- Both apps are `type: module` ESM → flat config file must be `eslint.config.js` (ESM) or `.mjs`.
|
||||
- API and PWA each already have a `typecheck` script (`tsc --noEmit`) wired into the root `pnpm -r typecheck` — mirror that wiring style for the new package-level `lint` scripts.
|
||||
- Type-aware linting needs every linted file resolvable by a tsconfig project. PWA has TWO tsconfigs (`tsconfig.json` + `tsconfig.e2e.json`) — `projectService: true` handles multi-project resolution.
|
||||
|
||||
### Integration Points
|
||||
|
||||
- `.gitea/workflows/ci.yml` `fast-checks` job — `pnpm lint` step already present (activates automatically); ADD a `pnpm format:check` step.
|
||||
- File volume for the first-run fix pass: `apps/api/src` = 30 `.ts` files; `apps/pwa/src` = 61 `.ts/.tsx` files, plus tests, e2e specs, and config files.
|
||||
|
||||
@@ -104,11 +120,12 @@ No external ADRs/specs specific to linting — requirements fully captured in de
|
||||
None — discussion stayed within phase scope (Prettier was folded IN as an accepted scope expansion, not deferred).
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
|
||||
- **Gitea CI — full regression on PR to main + build/publish Docker image** (`2026-06-10-gitea-ci-regression-and-docker-publish.md`, score 0.6) — reviewed but NOT folded: this is the Phase 8 CI work, already complete and merged to `main`. Matched only on shared keywords (CI/apps/api). Not in Phase 13 scope.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 13-real-lint-gate-eslint*
|
||||
*Context gathered: 2026-06-11*
|
||||
_Phase: 13-real-lint-gate-eslint_
|
||||
_Context gathered: 2026-06-11_
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
|
||||
## Rule Strictness — ruleset
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| `recommended` (non-type-aware) | Syntactic only, fast, no tsconfig wiring; misses async/promise bugs | |
|
||||
| `recommendedTypeChecked` | Type-aware; catches floating/misused promises + unsafe any; needs projectService, slower, more first-run violations | ✓ |
|
||||
| `strict`/`strictTypeChecked` | Maximal rules; heavy churn + bikeshed risk | |
|
||||
| Option | Description | Selected |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `recommended` (non-type-aware) | Syntactic only, fast, no tsconfig wiring; misses async/promise bugs | |
|
||||
| `recommendedTypeChecked` | Type-aware; catches floating/misused promises + unsafe any; needs projectService, slower, more first-run violations | ✓ |
|
||||
| `strict`/`strictTypeChecked` | Maximal rules; heavy churn + bikeshed risk | |
|
||||
|
||||
**User's choice:** `recommendedTypeChecked`
|
||||
**Notes:** Motivated by async-heavy backend (outbox/push/reminder schedulers). projectService: true.
|
||||
|
||||
## Rule Strictness — gate threshold
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| `--max-warnings 0` | Any warning fails CI; every rule is error-or-off | ✓ |
|
||||
| Errors only | Warnings surface but don't fail; softer rollout, accumulation risk | |
|
||||
| You decide | Pick based on first-run count | |
|
||||
| Option | Description | Selected |
|
||||
| ------------------ | ------------------------------------------------------------------ | -------- |
|
||||
| `--max-warnings 0` | Any warning fails CI; every rule is error-or-off | ✓ |
|
||||
| Errors only | Warnings surface but don't fail; softer rollout, accumulation risk | |
|
||||
| You decide | Pick based on first-run count | |
|
||||
|
||||
**User's choice:** `--max-warnings 0`
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
|
||||
## Existing-Violation Strategy
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Fix all violations now | Phase green across both apps; real bugs fixed; larger phase | ✓ |
|
||||
| Minimal green ruleset, ratchet later | Enable only passing rules; small phase, weaker gate, deferred work | |
|
||||
| Baseline file (snapshot + ratchet) | Snapshot current violations as accepted; new-only fails; extra tooling | |
|
||||
| Option | Description | Selected |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------- | -------- |
|
||||
| Fix all violations now | Phase green across both apps; real bugs fixed; larger phase | ✓ |
|
||||
| Minimal green ruleset, ratchet later | Enable only passing rules; small phase, weaker gate, deferred work | |
|
||||
| Baseline file (snapshot + ratchet) | Snapshot current violations as accepted; new-only fails; extra tooling | |
|
||||
|
||||
**User's choice:** Fix all violations now
|
||||
**Notes:** Hard constraint added (D-13-06): fixes must address the violation, not mask it — no blanket eslint-disable, no `void` to silence floating promises; suppressions need justifying comments.
|
||||
@@ -47,21 +47,21 @@
|
||||
|
||||
## Prettier Scope
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| `eslint-config-prettier` only | Disable conflicting format rules, no Prettier itself; phase stays ESLint-only | |
|
||||
| Add Prettier + format gate too | Full lint+format; scope creep beyond goal; reformats all files now | ✓ |
|
||||
| Defer Prettier entirely | ESLint only, no config-prettier; future conflict risk | |
|
||||
| Option | Description | Selected |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------- | -------- |
|
||||
| `eslint-config-prettier` only | Disable conflicting format rules, no Prettier itself; phase stays ESLint-only | |
|
||||
| Add Prettier + format gate too | Full lint+format; scope creep beyond goal; reformats all files now | ✓ |
|
||||
| Defer Prettier entirely | ESLint only, no config-prettier; future conflict risk | |
|
||||
|
||||
**User's choice:** Add Prettier + format gate too
|
||||
**Notes:** Expands ROADMAP Phase 13 goal ("ESLint only") — flagged for planning to update ROADMAP one-liner + success criteria. Follow-up decided wiring: standalone `prettier --check` + eslint-config-prettier (rejected eslint-plugin-prettier).
|
||||
|
||||
### Prettier wiring (follow-up)
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Standalone + config-prettier | Separate `prettier --check` CI step + eslint-config-prettier off-switch; clean separation | ✓ |
|
||||
| eslint-plugin-prettier | Prettier as an ESLint rule; one gate, slower/noisier, discouraged | |
|
||||
| Option | Description | Selected |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------- | -------- |
|
||||
| Standalone + config-prettier | Separate `prettier --check` CI step + eslint-config-prettier off-switch; clean separation | ✓ |
|
||||
| eslint-plugin-prettier | Prettier as an ESLint rule; one gate, slower/noisier, discouraged | |
|
||||
|
||||
**User's choice:** Standalone + eslint-config-prettier
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
|
||||
## Lint File Coverage
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| All TS/TSX: src + tests + e2e + configs | Most thorough; needs projectService + config-file override; more first-run fixes | ✓ |
|
||||
| src/ + tests, skip configs | Simpler, configs unchecked | |
|
||||
| src/ only | Smallest scope; tests + e2e harness + configs unlinted | |
|
||||
| Option | Description | Selected |
|
||||
| --------------------------------------- | -------------------------------------------------------------------------------- | -------- |
|
||||
| All TS/TSX: src + tests + e2e + configs | Most thorough; needs projectService + config-file override; more first-run fixes | ✓ |
|
||||
| src/ + tests, skip configs | Simpler, configs unchecked | |
|
||||
| src/ only | Smallest scope; tests + e2e harness + configs unlinted | |
|
||||
|
||||
**User's choice:** All TS/TSX (src + tests + e2e + configs)
|
||||
**Notes:** Config files need a non-type-checked override block (D-13-10).
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `eslint.config.js` (root) | config | — | none | greenfield |
|
||||
| `.prettierrc` (root) | config | — | none | greenfield |
|
||||
| `.prettierignore` (root) | config | — | none | greenfield |
|
||||
| `package.json` (root) | config | — | existing `lint`/`typecheck` scripts (lines 11-12) | exact |
|
||||
| `apps/api/package.json` | config | — | existing `typecheck` script (line 13) | exact |
|
||||
| `apps/pwa/package.json` | config | — | existing `typecheck` script (line 11) | exact |
|
||||
| `.gitea/workflows/ci.yml` | config | — | existing `Lint` + `Typecheck` steps (lines 31-36) | exact |
|
||||
| `apps/api/src/broker/*.ts` (violation fixes) | service | batch | existing setInterval+.catch pattern in same files | exact |
|
||||
| `apps/pwa/src/components/EventForm.tsx` (violation fix) | component | request-response | existing `eslint-disable-next-line` + justification comment at line 274 | exact |
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
| ------------------------------------------------------- | --------- | ---------------- | ----------------------------------------------------------------------- | ------------- |
|
||||
| `eslint.config.js` (root) | config | — | none | greenfield |
|
||||
| `.prettierrc` (root) | config | — | none | greenfield |
|
||||
| `.prettierignore` (root) | config | — | none | greenfield |
|
||||
| `package.json` (root) | config | — | existing `lint`/`typecheck` scripts (lines 11-12) | exact |
|
||||
| `apps/api/package.json` | config | — | existing `typecheck` script (line 13) | exact |
|
||||
| `apps/pwa/package.json` | config | — | existing `typecheck` script (line 11) | exact |
|
||||
| `.gitea/workflows/ci.yml` | config | — | existing `Lint` + `Typecheck` steps (lines 31-36) | exact |
|
||||
| `apps/api/src/broker/*.ts` (violation fixes) | service | batch | existing setInterval+.catch pattern in same files | exact |
|
||||
| `apps/pwa/src/components/EventForm.tsx` (violation fix) | component | request-response | existing `eslint-disable-next-line` + justification comment at line 274 | exact |
|
||||
|
||||
---
|
||||
|
||||
@@ -33,6 +33,7 @@ No analog exists in the codebase. Use the full skeleton from RESEARCH.md Pattern
|
||||
- All three worker files (`outboxWorker.ts`, `poller.ts`, `reminderScheduler.ts`) use `setInterval(() => { runX().catch(...) }, N)` — the `.catch()` chain correctly handles the promise. If `no-floating-promises` fires on the `.catch()` return value itself, wrap with `void`: `void runX().catch(...)`.
|
||||
|
||||
**Config files that need `disableTypeChecked` override** (confirmed outside all tsconfig `include` arrays):
|
||||
|
||||
- `apps/api/drizzle.config.ts`
|
||||
- `apps/api/vitest.config.ts`
|
||||
- `apps/pwa/vite.config.ts`
|
||||
@@ -55,12 +56,14 @@ Codebase style observation: all existing source files use single quotes (confirm
|
||||
**Analog:** the existing `lint` and `typecheck` script wiring in the same file.
|
||||
|
||||
**Existing pattern** (`/home/luc/Projects/familysync/package.json`, lines 11-12):
|
||||
|
||||
```json
|
||||
"lint": "pnpm -r --if-present lint",
|
||||
"typecheck": "pnpm -r typecheck"
|
||||
```
|
||||
|
||||
**Apply the same style** for new scripts:
|
||||
|
||||
```json
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
@@ -73,11 +76,13 @@ Note: root `package.json` currently has no `"type": "module"`. Adding it is requ
|
||||
### `apps/api/package.json` — adding `lint` script
|
||||
|
||||
**Analog:** the existing `typecheck` script in the same file (`/home/luc/Projects/familysync/apps/api/package.json`, line 13):
|
||||
|
||||
```json
|
||||
"typecheck": "tsc --noEmit"
|
||||
```
|
||||
|
||||
**Pattern to mirror** — same position in the scripts block, same style:
|
||||
|
||||
```json
|
||||
"lint": "eslint src/ tests/ --max-warnings 0"
|
||||
```
|
||||
@@ -89,11 +94,13 @@ Mirror pattern: single command, no wrapper, scope by directory. The `tests/` dir
|
||||
### `apps/pwa/package.json` — adding `lint` script
|
||||
|
||||
**Analog:** the existing `typecheck` script in the same file (`/home/luc/Projects/familysync/apps/pwa/package.json`, line 11):
|
||||
|
||||
```json
|
||||
"typecheck": "tsc --noEmit && tsc --project tsconfig.e2e.json --noEmit"
|
||||
```
|
||||
|
||||
**Pattern to mirror:**
|
||||
|
||||
```json
|
||||
"lint": "eslint src/ e2e/ --max-warnings 0"
|
||||
```
|
||||
@@ -105,31 +112,34 @@ The `e2e/` directory is covered by `tsconfig.e2e.json`, which `projectService: t
|
||||
### `.gitea/workflows/ci.yml` — adding `Format check` step
|
||||
|
||||
**Analog:** the existing `Lint` and `Typecheck` steps in the `fast-checks` job (`/home/luc/Projects/familysync/.gitea/workflows/ci.yml`, lines 31-36):
|
||||
```yaml
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
```yaml
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
```
|
||||
|
||||
**New step — insert between `Lint` and `Typecheck`** (per RESEARCH ordering: lint → format:check → typecheck → tests):
|
||||
|
||||
```yaml
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
```
|
||||
|
||||
Also remove the now-stale comment above the `Lint` step (lines 28-30):
|
||||
|
||||
```yaml
|
||||
# lint is currently a no-op: no package defines a `lint` script and ESLint is
|
||||
# not installed. `pnpm -r lint` prints ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT but
|
||||
# exits 0, so this step passes. Wiring lint is out of this phase's scope.
|
||||
# lint is currently a no-op: no package defines a `lint` script and ESLint is
|
||||
# not installed. `pnpm -r lint` prints ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT but
|
||||
# exits 0, so this step passes. Wiring lint is out of this phase's scope.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -139,13 +149,14 @@ Also remove the now-stale comment above the `Lint` step (lines 28-30):
|
||||
**Analog:** the existing setInterval worker pattern in the same files. All three workers (`outboxWorker.ts`, `poller.ts`, `reminderScheduler.ts`) already use the same correct structure:
|
||||
|
||||
**Existing correct pattern** (`outboxWorker.ts`, lines 781-787):
|
||||
|
||||
```ts
|
||||
export function startOutboxWorker(): void {
|
||||
setInterval(() => {
|
||||
runOutboxDrain().catch((err: unknown) => {
|
||||
console.error('[outboxWorker] Unhandled runOutboxDrain error:', err)
|
||||
})
|
||||
}, 15 * 1000)
|
||||
console.error('[outboxWorker] Unhandled runOutboxDrain error:', err);
|
||||
});
|
||||
}, 15 * 1000);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -154,9 +165,9 @@ This pattern (inner `.catch()` inside `setInterval` arrow) is already correct fo
|
||||
```ts
|
||||
setInterval(() => {
|
||||
void runOutboxDrain().catch((err: unknown) => {
|
||||
console.error('[outboxWorker] Unhandled runOutboxDrain error:', err)
|
||||
})
|
||||
}, 15 * 1000)
|
||||
console.error('[outboxWorker] Unhandled runOutboxDrain error:', err);
|
||||
});
|
||||
}, 15 * 1000);
|
||||
```
|
||||
|
||||
The `void` operator here is legitimate: it signals intentional fire-and-forget within a `setInterval` that handles its own scheduling. This is NOT masking a bug.
|
||||
@@ -166,7 +177,7 @@ The `void` operator here is legitimate: it signals intentional fire-and-forget w
|
||||
```ts
|
||||
// ical.js getFirstPropertyValue returns a union type; cast to ICAL.Time for date handling
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null;
|
||||
```
|
||||
|
||||
The file already has an inline comment at line 99 explaining the cast — the justification comment is already there. The `eslint-disable-next-line` line is the only addition required per D-13-06.
|
||||
@@ -178,13 +189,14 @@ The file already has an inline comment at line 99 explaining the cast — the ju
|
||||
**Analog:** the file already has an `eslint-disable-next-line` with justification comment at line 274 (the line immediately before the violation):
|
||||
|
||||
**Existing pattern** (`EventForm.tsx`, lines 271-275):
|
||||
|
||||
```ts
|
||||
// (the API expand contract does not expose it in v1 — D-03). We cast to any to
|
||||
// read it if a future API version adds it, and default to 'none' when not present
|
||||
// (WR-03 v1 comment: occurrence edits whose recurrence is not in the cache default
|
||||
// to 'none'; this will be addressed when the occurrence/expand contract is extended).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const derivedRecurrence = (occurrence as any)?.recurrence as RecurrencePreset | undefined
|
||||
// (the API expand contract does not expose it in v1 — D-03). We cast to any to
|
||||
// read it if a future API version adds it, and default to 'none' when not present
|
||||
// (WR-03 v1 comment: occurrence edits whose recurrence is not in the cache default
|
||||
// to 'none'; this will be addressed when the occurrence/expand contract is extended).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const derivedRecurrence = (occurrence as any)?.recurrence as RecurrencePreset | undefined;
|
||||
```
|
||||
|
||||
The `eslint-disable-next-line` comment is already present in the source. This file requires no change for the `no-explicit-any` rule — the suppression is pre-existing and already justified. Verify the comment is on the line immediately before the `const derivedRecurrence` line and that no other `as any` casts exist in the file.
|
||||
@@ -199,6 +211,7 @@ The `eslint-disable-next-line` comment is already present in the source. This fi
|
||||
**Apply to:** `apps/api/package.json`, `apps/pwa/package.json`
|
||||
|
||||
The `typecheck` scripts are the canonical pattern to copy for new per-package tool scripts:
|
||||
|
||||
- One command per script
|
||||
- No wrapper (`pnpm run` prefix, extra flags)
|
||||
- Directory-scoped (not `--ext`, not glob patterns)
|
||||
@@ -221,11 +234,11 @@ Pattern: `- name: Verb noun` (title case, imperative verb), ` run: pnpm <script
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|---|---|---|---|
|
||||
| `eslint.config.js` | config | — | First ESLint config in the repo; use RESEARCH.md skeleton |
|
||||
| `.prettierrc` | config | — | First Prettier config in the repo; use RESEARCH.md defaults |
|
||||
| `.prettierignore` | config | — | First Prettier ignore in the repo; use RESEARCH.md content |
|
||||
| File | Role | Data Flow | Reason |
|
||||
| ------------------ | ------ | --------- | ----------------------------------------------------------- |
|
||||
| `eslint.config.js` | config | — | First ESLint config in the repo; use RESEARCH.md skeleton |
|
||||
| `.prettierrc` | config | — | First Prettier config in the repo; use RESEARCH.md defaults |
|
||||
| `.prettierignore` | config | — | First Prettier ignore in the repo; use RESEARCH.md content |
|
||||
|
||||
---
|
||||
|
||||
@@ -244,18 +257,22 @@ Pattern: `- name: Verb noun` (title case, imperative verb), ` run: pnpm <script
|
||||
**Analogs found:** 6 / 9
|
||||
|
||||
### Coverage
|
||||
|
||||
- Files with exact analog: 6 (3 × package.json scripts, ci.yml step, broker setInterval pattern, EventForm disable pattern)
|
||||
- Files with role-match analog: 0
|
||||
- Files with no analog: 3 (eslint.config.js, .prettierrc, .prettierignore — greenfield)
|
||||
|
||||
### Key Patterns Identified
|
||||
|
||||
- All package-level tool scripts follow the `typecheck` pattern: single command, directory-scoped, no wrappers — copy exactly for `lint` scripts
|
||||
- CI steps follow `- name: Verb noun` / ` run: pnpm <script>` — new `Format check` step slots between `Lint` and `Typecheck`
|
||||
- All three broker workers already use `setInterval(() => { runX().catch(...) })` correctly; if `no-floating-promises` fires on the `.catch()` return, add `void` before `runX()` (legitimate signal, not a mask)
|
||||
- `eslint-disable-next-line` suppression pattern is pre-established in EventForm.tsx with a multi-line justification block — use the same structure for any ical.js unsafe-access suppressions in broker files
|
||||
|
||||
### File Created
|
||||
|
||||
`/home/luc/Projects/familysync/.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md`
|
||||
|
||||
### Ready for Planning
|
||||
|
||||
Pattern mapping complete. Planner can now reference analog patterns in PLAN.md files.
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
- **D-13-01:** Use typescript-eslint `recommendedTypeChecked` (type-aware), not the non-type-aware `recommended`. Enable via `projectService: true`.
|
||||
- **D-13-02:** React + react-hooks plugins for `apps/pwa` only; `apps/api` is Node/TS only (no React config).
|
||||
- **D-13-03:** Do NOT adopt `strict`/`strictTypeChecked` presets — too much churn on the existing codebase.
|
||||
@@ -22,13 +24,15 @@
|
||||
- **D-13-10:** Config files that `projectService` cannot type-check need a dedicated `disableTypeChecked` override block.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Flat-config file layout (single root `eslint.config.js` vs per-app configs).
|
||||
- Exact Prettier options (`.prettierrc`) — standard defaults; no bikeshedding.
|
||||
- CI step ordering within `fast-checks` (lint → format:check → typecheck → tests).
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
|
||||
- None.
|
||||
</user_constraints>
|
||||
</user_constraints>
|
||||
|
||||
---
|
||||
|
||||
@@ -44,30 +48,32 @@ Phase 13 installs ESLint (v9 flat config) + typescript-eslint (v8, `recommendedT
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
| ----------------------- | --------------- | ----------------- | ------------------------------------------------------------------ |
|
||||
| ESLint config authoring | Root (dev-time) | Per-app overrides | Single config controls all workspaces; per-app globs scope plugins |
|
||||
| Type-aware linting | Build-time / CI | Local dev | Requires TypeScript program; TS is already available at build time |
|
||||
| Prettier formatting | Root (dev-time) | CI gate | `format` runs locally; `format:check` runs in CI |
|
||||
| CI lint gate | fast-checks job | — | `pnpm lint` already present; activates when package scripts exist |
|
||||
| CI format gate | fast-checks job | — | New `pnpm format:check` step added after `pnpm lint` |
|
||||
| Type-aware linting | Build-time / CI | Local dev | Requires TypeScript program; TS is already available at build time |
|
||||
| Prettier formatting | Root (dev-time) | CI gate | `format` runs locally; `format:check` runs in CI |
|
||||
| CI lint gate | fast-checks job | — | `pnpm lint` already present; activates when package scripts exist |
|
||||
| CI format gate | fast-checks job | — | New `pnpm format:check` step added after `pnpm lint` |
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core Packages
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| `eslint` | `9.39.4` (pin to 9.x) | Core linter | ESLint 9.x — last stable before ESLint 10 breaks plugin compat |
|
||||
| `@eslint/js` | `9.39.4` | JS recommended rules config | Peer of ESLint; same version |
|
||||
| `typescript-eslint` | `8.61.0` | TS parser + plugins + preset configs | Official combined package; supports ESLint ^9 |
|
||||
| `eslint-plugin-react` | `7.37.5` | React-specific rules | PWA only; 47M downloads/wk |
|
||||
| `eslint-plugin-react-hooks` | `7.1.1` | Hooks rules (`rules-of-hooks`, `exhaustive-deps`) | PWA only; official React team plugin; 80M downloads/wk |
|
||||
| `eslint-config-prettier` | `10.1.8` | Disables ESLint formatting rules that conflict with Prettier | MUST be last in flat config array |
|
||||
| `prettier` | `3.8.4` | Code formatter | Standalone; paired with `eslint-config-prettier` |
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
| --------------------------- | --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| `eslint` | `9.39.4` (pin to 9.x) | Core linter | ESLint 9.x — last stable before ESLint 10 breaks plugin compat |
|
||||
| `@eslint/js` | `9.39.4` | JS recommended rules config | Peer of ESLint; same version |
|
||||
| `typescript-eslint` | `8.61.0` | TS parser + plugins + preset configs | Official combined package; supports ESLint ^9 |
|
||||
| `eslint-plugin-react` | `7.37.5` | React-specific rules | PWA only; 47M downloads/wk |
|
||||
| `eslint-plugin-react-hooks` | `7.1.1` | Hooks rules (`rules-of-hooks`, `exhaustive-deps`) | PWA only; official React team plugin; 80M downloads/wk |
|
||||
| `eslint-config-prettier` | `10.1.8` | Disables ESLint formatting rules that conflict with Prettier | MUST be last in flat config array |
|
||||
| `prettier` | `3.8.4` | Code formatter | Standalone; paired with `eslint-config-prettier` |
|
||||
|
||||
**Version verification:**
|
||||
|
||||
```bash
|
||||
# Verified via npm registry 2026-06-11:
|
||||
# eslint 9.39.4 (maintenance tag; 10.4.1 is latest but breaks react plugin)
|
||||
@@ -98,26 +104,27 @@ pnpm add -D -w \
|
||||
Installing at root (not per-app) is the idiomatic approach for a small 2-app pnpm workspace: all packages share one node_modules resolution, and the single root `eslint.config.js` can import every plugin without cross-package symlink complexity.
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| ESLint 9.39.4 (pin) | ESLint 10.4.1 | 10.x breaks `eslint-plugin-react` — open compat issue; unblocked when plugin releases fix |
|
||||
| `recommendedTypeChecked` | `strictTypeChecked` | `strict` adds ~15 more rules; D-13-03 explicitly rejects the extra churn |
|
||||
| root devDependencies | per-app devDependencies | Per-app adds config import complexity; root is idiomatic for shared dev tooling |
|
||||
| `eslint-plugin-react` + `react-hooks` | `react-hooks` alone | `react-hooks` covers hook rules; `eslint-plugin-react` adds JSX/prop-types/display-name rules worth having for a production PWA |
|
||||
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
| ------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ESLint 9.39.4 (pin) | ESLint 10.4.1 | 10.x breaks `eslint-plugin-react` — open compat issue; unblocked when plugin releases fix |
|
||||
| `recommendedTypeChecked` | `strictTypeChecked` | `strict` adds ~15 more rules; D-13-03 explicitly rejects the extra churn |
|
||||
| root devDependencies | per-app devDependencies | Per-app adds config import complexity; root is idiomatic for shared dev tooling |
|
||||
| `eslint-plugin-react` + `react-hooks` | `react-hooks` alone | `react-hooks` covers hook rules; `eslint-plugin-react` adds JSX/prop-types/display-name rules worth having for a production PWA |
|
||||
|
||||
---
|
||||
|
||||
## Package Legitimacy Audit
|
||||
|
||||
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|
||||
|---------|----------|-----|-----------|-------------|---------|-------------|
|
||||
| `eslint` | npm | 14 yrs | 132M/wk | github.com/eslint/eslint | SUS (too-new publish) | Approved — canonical project, official github.com/eslint/eslint repo confirmed |
|
||||
| `@eslint/js` | npm | 3 yrs | 117M/wk | github.com/eslint/eslint | OK | Approved |
|
||||
| `typescript-eslint` | npm | 6 yrs | 69M/wk | github.com/typescript-eslint/typescript-eslint | SUS (too-new publish) | Approved — canonical official monorepo confirmed |
|
||||
| `eslint-plugin-react` | npm | 11 yrs | 47M/wk | github.com/jsx-eslint/eslint-plugin-react | OK | Approved |
|
||||
| `eslint-plugin-react-hooks` | npm | 6 yrs | 80M/wk | github.com/facebook/react | OK | Approved |
|
||||
| `eslint-config-prettier` | npm | 8 yrs | 56M/wk | github.com/prettier/eslint-config-prettier | OK | Approved |
|
||||
| `prettier` | npm | 9 yrs | 108M/wk | github.com/prettier/prettier | SUS (too-new publish) | Approved — canonical official repo confirmed |
|
||||
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|
||||
| --------------------------- | -------- | ------ | --------- | ---------------------------------------------- | --------------------- | ------------------------------------------------------------------------------ |
|
||||
| `eslint` | npm | 14 yrs | 132M/wk | github.com/eslint/eslint | SUS (too-new publish) | Approved — canonical project, official github.com/eslint/eslint repo confirmed |
|
||||
| `@eslint/js` | npm | 3 yrs | 117M/wk | github.com/eslint/eslint | OK | Approved |
|
||||
| `typescript-eslint` | npm | 6 yrs | 69M/wk | github.com/typescript-eslint/typescript-eslint | SUS (too-new publish) | Approved — canonical official monorepo confirmed |
|
||||
| `eslint-plugin-react` | npm | 11 yrs | 47M/wk | github.com/jsx-eslint/eslint-plugin-react | OK | Approved |
|
||||
| `eslint-plugin-react-hooks` | npm | 6 yrs | 80M/wk | github.com/facebook/react | OK | Approved |
|
||||
| `eslint-config-prettier` | npm | 8 yrs | 56M/wk | github.com/prettier/eslint-config-prettier | OK | Approved |
|
||||
| `prettier` | npm | 9 yrs | 108M/wk | github.com/prettier/prettier | SUS (too-new publish) | Approved — canonical official repo confirmed |
|
||||
|
||||
**Packages removed due to SLOP verdict:** None.
|
||||
**Packages flagged as suspicious (SUS):** `eslint`, `typescript-eslint`, `prettier` — flagged only because each received a new release within the last few weeks (the legitimacy seam's `too-new` signal). All three have canonical GitHub repository URLs matching the well-known official projects and download counts in the tens/hundreds of millions. No postinstall scripts. Approved for use without additional human verification checkpoint.
|
||||
@@ -166,6 +173,7 @@ CI fast-checks job (ci.yml):
|
||||
```
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
repo root
|
||||
├── eslint.config.js # single flat config (ESM); type: module at root
|
||||
@@ -188,11 +196,11 @@ repo root
|
||||
```js
|
||||
// eslint.config.js
|
||||
// Source: typescript-eslint.io/getting-started/typed-linting/
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import reactPlugin from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import prettierConfig from 'eslint-config-prettier/flat'
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import prettierConfig from 'eslint-config-prettier/flat';
|
||||
|
||||
export default tseslint.config(
|
||||
// ── Global ignores (replaces .eslintignore) ──────────────────────────────
|
||||
@@ -200,7 +208,7 @@ export default tseslint.config(
|
||||
ignores: [
|
||||
'**/dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/src/db/migrations/**', // generated Drizzle SQL files
|
||||
'**/src/db/migrations/**', // generated Drizzle SQL files
|
||||
'pnpm-lock.yaml',
|
||||
],
|
||||
},
|
||||
@@ -208,10 +216,7 @@ export default tseslint.config(
|
||||
// ── Base: all TS/TSX files in both apps ──────────────────────────────────
|
||||
{
|
||||
files: ['apps/**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
],
|
||||
extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
@@ -223,10 +228,7 @@ export default tseslint.config(
|
||||
// ── React + Hooks: PWA only ───────────────────────────────────────────────
|
||||
{
|
||||
files: ['apps/pwa/**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
],
|
||||
extends: [reactPlugin.configs.flat.recommended, reactHooks.configs.flat.recommended],
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
@@ -253,7 +255,7 @@ export default tseslint.config(
|
||||
// Disables all ESLint formatting rules that conflict with Prettier.
|
||||
// Source: github.com/prettier/eslint-config-prettier
|
||||
prettierConfig,
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Critical note on `reactPlugin.configs.flat.recommended`:** As of eslint-plugin-react@7.37.5, the flat config export is `reactPlugin.configs.flat.recommended` (not `reactPlugin.configs['flat/recommended']`). [CITED: github.com/jsx-eslint/eslint-plugin-react] The plugin must be imported under the standard name `react` — not aliased — or `eslint-config-prettier` won't be able to disable its formatting rules correctly. [CITED: github.com/prettier/eslint-config-prettier]
|
||||
@@ -289,6 +291,7 @@ The root `pnpm -r --if-present lint` then activates both. Note: the API's `tscon
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **`void promise` to silence `no-floating-promises`:** `void foo()` is a mask — ESLint `no-floating-promises` accepts `void` as a suppression. The rule is flagging a genuinely unhandled promise. Use `.catch()` or `await`. Only use `void` when the promise is truly fire-and-forget AND that is documented explicitly.
|
||||
- **Using `eslint-plugin-prettier`:** Runs Prettier as an ESLint rule — formats files twice, produces noisy diff output. Prettier docs explicitly recommend against this. The correct split is `eslint-config-prettier` (disables conflicting rules in ESLint) + `prettier --check` (standalone format gate). [CITED: prettier.io/docs/en/integrating-with-linters]
|
||||
- **`allowDefaultProject` for config files:** While it works, it is fragile (glob resolution issues in some configurations). The `disableTypeChecked` override on specific config file globs is simpler and explicit. [CITED: typescript-eslint.io/blog/project-service]
|
||||
@@ -299,12 +302,12 @@ The root `pnpm -r --if-present lint` then activates both. Note: the API's `tscon
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Disabling formatting rules | Manual rule-off list | `eslint-config-prettier` | Prettier maintainers curate the list; it covers @typescript-eslint formatting rules too |
|
||||
| TypeScript project resolution in monorepo | Manual tsconfig glob list | `projectService: true` | v8 projectService auto-discovers all tsconfigs; no maintenance overhead |
|
||||
| React hooks exhaustive-deps enforcement | Manual code review | `eslint-plugin-react-hooks` | Statically catches missing dependencies the type system can't |
|
||||
| Non-project file handling | `tsconfig.eslint.json` with `include: ['**/*']` | `disableTypeChecked` override | Simpler; avoids creating a catch-all tsconfig that degrades type checking |
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
| ----------------------------------------- | ----------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| Disabling formatting rules | Manual rule-off list | `eslint-config-prettier` | Prettier maintainers curate the list; it covers @typescript-eslint formatting rules too |
|
||||
| TypeScript project resolution in monorepo | Manual tsconfig glob list | `projectService: true` | v8 projectService auto-discovers all tsconfigs; no maintenance overhead |
|
||||
| React hooks exhaustive-deps enforcement | Manual code review | `eslint-plugin-react-hooks` | Statically catches missing dependencies the type system can't |
|
||||
| Non-project file handling | `tsconfig.eslint.json` with `include: ['**/*']` | `disableTypeChecked` override | Simpler; avoids creating a catch-all tsconfig that degrades type checking |
|
||||
|
||||
**Key insight:** The `eslint-config-prettier` package is the canonical way to integrate Prettier with ESLint. Without it, ESLint's `@typescript-eslint/indent` and similar formatting rules will conflict with Prettier's output, producing a feedback loop where `eslint --fix` and `prettier --write` undo each other.
|
||||
|
||||
@@ -313,53 +316,63 @@ The root `pnpm -r --if-present lint` then activates both. Note: the API's `tscon
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: ESLint 10 + eslint-plugin-react Runtime Error
|
||||
|
||||
**What goes wrong:** Installing `eslint@latest` (10.x) with `eslint-plugin-react@7.37.5` throws `TypeError: contextOrFilename.getFilename is not a function` at lint time.
|
||||
**Why it happens:** ESLint 10 removed legacy Context API methods that `eslint-plugin-react`'s version-detection utility still calls.
|
||||
**How to avoid:** Pin `eslint@9.39.4` (the maintenance tag). The plugin issue is tracked at jsx-eslint/eslint-plugin-react#3977 and is unresolved as of 2026-06-11.
|
||||
**Warning signs:** pnpm install succeeds but `pnpm lint` immediately throws a TypeError before reporting any actual lint errors.
|
||||
|
||||
### Pitfall 2: API test files excluded from tsconfig project
|
||||
|
||||
**What goes wrong:** `apps/api/tsconfig.json` has `"exclude": ["tests"]`. ESLint with `projectService: true` cannot type-check `apps/api/tests/*.ts` — you'll get "The file does not match your project config" errors, or type-aware rules silently won't fire on test files.
|
||||
**Why it happens:** `projectService` discovers tsconfig.json and respects its `exclude`. Test files outside the project scope can't receive type-aware linting.
|
||||
**How to avoid:** Add a dedicated `disableTypeChecked` override block in `eslint.config.js` for `apps/api/tests/**/*.ts`. Those files will still be linted with non-type-aware rules (syntax, `no-unused-vars`, etc.) — just not the type-aware ones. Alternatively, create an `apps/api/tsconfig.test.json` that includes the tests and verify `projectService` picks it up.
|
||||
**Warning signs:** "Parsing error: ESLint was configured to run on `apps/api/tests/foo.test.ts` but that file was not found in any of the provided project(s)."
|
||||
|
||||
### Pitfall 3: `vitest.config.ts` / `drizzle.config.ts` not in any tsconfig project
|
||||
|
||||
**What goes wrong:** Config files (`vite.config.ts`, `drizzle.config.ts`, `vitest.config.ts`, `playwright.config.ts`) sit outside the `include` arrays of all tsconfigs. Type-aware linting on them produces "file must be included in at least one of the projects" errors.
|
||||
**Why it happens:** These files are tool configs consumed by drizzle-kit / Vite / Vitest / Playwright, not by the TypeScript compiler. The tsconfigs correctly exclude them.
|
||||
**How to avoid:** Add these to the `disableTypeChecked` override block in `eslint.config.js` (see Pattern 1 skeleton above). They receive non-type-aware rules only.
|
||||
**Warning signs:** Lint errors only on config files, none on src/ — and the error references the project inclusion issue.
|
||||
|
||||
### Pitfall 4: `no-floating-promises` on setInterval callbacks
|
||||
|
||||
**What goes wrong:** The broker workers (`outboxWorker.ts`, `reminderScheduler.ts`, `poller.ts`) use the pattern:
|
||||
|
||||
```ts
|
||||
setInterval(() => {
|
||||
runOutboxDrain().catch((err: unknown) => { ... })
|
||||
}, 15_000)
|
||||
```
|
||||
|
||||
`setInterval` accepts `() => void`, so the arrow function IS void-returning (the `.catch()` return value is discarded). This pattern is correct and ESLint `no-floating-promises` should NOT fire here because the promise is handled via `.catch()`.
|
||||
**Why it matters:** If any worker uses `runX()` without `.catch()`, that IS a real floating promise that should be fixed (not suppressed).
|
||||
**How to avoid:** The `.catch()` on every worker's inner async call is already the correct fix. Verify no worker uses the bare pattern `setInterval(() => { runX() }, ...)` without `.catch()`.
|
||||
**Warning signs:** If the rule fires on a `.catch()`-chained promise, the rule is incorrectly applied — check that the `.catch()` callback is typed as `(err: unknown) => void`.
|
||||
|
||||
### Pitfall 5: `no-misused-promises` on event listener callbacks
|
||||
|
||||
**What goes wrong:** Pattern like `document.addEventListener('click', async () => { ... })` — the event listener callback is typed to return void but the async function returns `Promise<void>`. `no-misused-promises` flags this as a misuse.
|
||||
**Why it happens:** The type system marks `addEventListener` callbacks as void-returning, so passing an async function is technically unsound.
|
||||
**How to avoid:** Wrap the async logic: `document.addEventListener('click', () => { void asyncHandler() })` or extract to a named function. The `void` operator here is legitimate: it explicitly signals "I know this is a promise, I'm not awaiting it, and I accept that responsibility."
|
||||
**Warning signs:** Violations on event listener registrations in React `useEffect` hooks or service worker event handlers.
|
||||
|
||||
### Pitfall 6: `sw.ts` — service worker file type context
|
||||
|
||||
**What goes wrong:** `apps/pwa/src/sw.ts` declares `/// <reference lib="webworker" />` and `declare const self: ServiceWorkerGlobalScope`. The PWA tsconfig has `"lib": ["ES2023", "DOM", "DOM.Iterable"]`, which includes DOM but NOT ServiceWorker APIs. The SW file uses a separate lib reference to access SW types. ESLint may flag missing types for SW-specific globals.
|
||||
**Why it happens:** The SW file is in `apps/pwa/src/` which is in the PWA tsconfig's `include`. The webworker lib reference provides the types but projectService may need to see this correctly.
|
||||
**How to avoid:** Ensure the SW file is parsed under the correct tsconfig. If type errors arise specifically on `sw.ts`, consider adding it to a SW-specific `disableTypeChecked` override rather than fighting the lib reference.
|
||||
|
||||
### Pitfall 7: `eslint-config-prettier` import path in ESM flat config
|
||||
|
||||
**What goes wrong:** `import eslintConfigPrettier from 'eslint-config-prettier'` does NOT work in ESM flat config — it returns the full package with all sub-configs. The correct import for ESM flat config is `from 'eslint-config-prettier/flat'`.
|
||||
**Why it happens:** `eslint-config-prettier` exports different shapes for CJS legacy config vs ESM flat config.
|
||||
**How to avoid:** Always use `eslint-config-prettier/flat` in ESM `eslint.config.js`. [CITED: github.com/prettier/eslint-config-prettier#readme]
|
||||
**Warning signs:** `prettierConfig` resolves to an object with extra properties that don't match the flat config schema.
|
||||
|
||||
### Pitfall 8: `React` import in main.tsx and ErrorBoundary.tsx
|
||||
|
||||
**What goes wrong:** `eslint-plugin-react` with `react/jsx-runtime` preset or the `react/react-in-jsx-scope` rule will report `React` as unused when JSX transform is used. But in this codebase, `main.tsx` uses `<React.StrictMode>` (requires the `React` namespace) and `ErrorBoundary.tsx` extends `React.Component` (also uses the namespace directly). These are legitimate uses — the rule should NOT fire.
|
||||
**Why it happens:** The `react/react-in-jsx-scope` rule is disabled in the `flat.recommended` config since React 17+ JSX transform. But `React.StrictMode` / `React.Component` still require the import.
|
||||
**How to avoid:** No action needed — `reactPlugin.configs.flat.recommended` already disables `react/react-in-jsx-scope`. The `React` import stays because it IS used (namespace access, not just JSX).
|
||||
@@ -373,6 +386,7 @@ This section maps likely first-run violations to the correct fix per D-13-05/D-1
|
||||
### `@typescript-eslint/no-unsafe-*` family (no-unsafe-member-access, no-unsafe-argument, no-unsafe-assignment)
|
||||
|
||||
**Likely locations:**
|
||||
|
||||
- `apps/api/src/broker/sync.ts` — `ical.js` returns values typed as `any` via `getFirstPropertyValue()`. Multiple `as string` casts on the returned values.
|
||||
- `apps/api/src/broker/expand.ts` — similar ical.js property access.
|
||||
- `apps/pwa/src/sw.ts` — `event.data.json() as Record<string, unknown>` — already correctly cast.
|
||||
@@ -381,21 +395,25 @@ This section maps likely first-run violations to the correct fix per D-13-05/D-1
|
||||
**Correct fix:** Add type predicates or narrow via `typeof`/`instanceof` checks before accessing properties. For ical.js specifically (a library with weak typings), a targeted `eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // ical.js returns untyped property values` with justification comment is acceptable — this is a known external library typing limitation, not a bug.
|
||||
|
||||
**Distinction real-fix vs mask:**
|
||||
|
||||
- MASK: `// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment` with no explanation.
|
||||
- REAL FIX OR JUSTIFIED SUPPRESS: `// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // ical.js getFirstPropertyValue() returns 'any'; the caller immediately validates the value`.
|
||||
|
||||
### `@typescript-eslint/no-floating-promises`
|
||||
|
||||
**Likely locations:**
|
||||
|
||||
- Broker workers: all three `setInterval` blocks already use `.catch()`. The pattern `runX().catch(...)` should NOT trigger `no-floating-promises` because the promise is handled.
|
||||
- `apps/api/src/broker/outboxWorker.ts:237` — `dispatchEventChange(change, userId).catch(...)` — already handled.
|
||||
|
||||
**If the rule fires on a `.catch()` chain:** The return of `.catch()` is also a Promise. If the outer function doesn't return it, the chained promise itself is floating. In a `setInterval` callback that returns `void`, this is fine — but the rule may need the callback pattern `() => { runX().catch(...) }` to be recognized as void (not `() => runX().catch(...)`). Adjust by wrapping in `void`:
|
||||
|
||||
```ts
|
||||
setInterval(() => {
|
||||
void runOutboxDrain().catch((err: unknown) => { ... })
|
||||
}, 15_000)
|
||||
```
|
||||
|
||||
The explicit `void` operator here is a legitimate signal: "I know this returns a promise and I am intentionally not awaiting it because the setInterval schedule handles the next tick."
|
||||
|
||||
### `@typescript-eslint/require-await`
|
||||
@@ -408,11 +426,13 @@ The explicit `void` operator here is a legitimate signal: "I know this returns a
|
||||
|
||||
**What it catches:** Declared but never read variables/imports.
|
||||
**Convention:** The `_` prefix convention for intentionally unused params: `(_unusedParam: string) => ...`. Configure the rule to ignore `_`-prefixed names:
|
||||
|
||||
```js
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
}
|
||||
```
|
||||
|
||||
**Likely locations:** Destructured params in error handlers (`catch (err) { }` — empty catches don't have a binding, so no violation). React test files that import React explicitly.
|
||||
|
||||
### `react/display-name`
|
||||
@@ -463,13 +483,13 @@ Must be the LAST config object in the `tseslint.config(...)` array. Any config t
|
||||
|
||||
```js
|
||||
// eslint.config.js — the import:
|
||||
import prettierConfig from 'eslint-config-prettier/flat' // flat config import path
|
||||
import prettierConfig from 'eslint-config-prettier/flat'; // flat config import path
|
||||
|
||||
// In the config array — ALWAYS last:
|
||||
export default tseslint.config(
|
||||
// ... all other configs ...
|
||||
prettierConfig, // LAST
|
||||
)
|
||||
prettierConfig, // LAST
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -480,8 +500,8 @@ export default tseslint.config(
|
||||
|
||||
```yaml
|
||||
# Current order:
|
||||
- name: Lint # runs pnpm lint → pnpm -r --if-present lint → activates once package scripts exist
|
||||
- name: Typecheck # unchanged
|
||||
- name: Lint # runs pnpm lint → pnpm -r --if-present lint → activates once package scripts exist
|
||||
- name: Typecheck # unchanged
|
||||
- name: PWA unit tests
|
||||
```
|
||||
|
||||
@@ -491,7 +511,7 @@ export default tseslint.config(
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check # ← NEW step (D-13-07)
|
||||
- name: Format check # ← NEW step (D-13-07)
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Typecheck
|
||||
@@ -535,12 +555,12 @@ The root `package.json` already has `"lint": "pnpm -r --if-present lint"`. The `
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Node.js 22 | ESLint, typescript-eslint | ✓ | 22.x (CI: actions/setup-node@v4) | — |
|
||||
| TypeScript 5.x | type-aware linting | ✓ | ^5.5.0 (both apps devDeps) | — |
|
||||
| pnpm 11.5.1 | monorepo install | ✓ | 11.5.1 | — |
|
||||
| npm registry | package install | ✓ | — | — |
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
| -------------- | ------------------------- | --------- | -------------------------------- | -------- |
|
||||
| Node.js 22 | ESLint, typescript-eslint | ✓ | 22.x (CI: actions/setup-node@v4) | — |
|
||||
| TypeScript 5.x | type-aware linting | ✓ | ^5.5.0 (both apps devDeps) | — |
|
||||
| pnpm 11.5.1 | monorepo install | ✓ | 11.5.1 | — |
|
||||
| npm registry | package install | ✓ | — | — |
|
||||
|
||||
**Missing dependencies with no fallback:** None.
|
||||
**Missing dependencies with fallback:** None.
|
||||
@@ -551,40 +571,44 @@ The root `package.json` already has `"lint": "pnpm -r --if-present lint"`. The `
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Vitest (apps/api: node env; apps/pwa: jsdom env) |
|
||||
| Config file | `apps/api/vitest.config.ts`, `apps/pwa/vitest.config.ts` |
|
||||
| Quick run (api) | `pnpm --filter @familysync/api test` |
|
||||
| Quick run (pwa) | `pnpm --filter @familysync/pwa test` |
|
||||
| Property | Value |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| Framework | Vitest (apps/api: node env; apps/pwa: jsdom env) |
|
||||
| Config file | `apps/api/vitest.config.ts`, `apps/pwa/vitest.config.ts` |
|
||||
| Quick run (api) | `pnpm --filter @familysync/api test` |
|
||||
| Quick run (pwa) | `pnpm --filter @familysync/pwa test` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Behavior | Test Type | Automated Command | Notes |
|
||||
|----------|-----------|-------------------|-------|
|
||||
| `pnpm lint` exits non-zero on a deliberate violation | Smoke gate | Introduce a test file with a known violation, run lint, assert non-zero exit | Manual CI run required; not a vitest test |
|
||||
| `pnpm format:check` exits non-zero on unformatted file | Smoke gate | Stage an unformatted file, run `prettier --check`, assert non-zero | Manual verification |
|
||||
| CI lint step activates (not a no-op) | Integration | PR to main; observe CI step now reports violations (not silent pass) | CI run |
|
||||
| All first-run violations fixed — both apps lint green | End-to-end gate | `pnpm lint` exits 0 | Must pass before phase complete |
|
||||
| Prettier reformats files | End-to-end gate | `pnpm format && git diff --stat` shows changes | Visual inspection |
|
||||
| `pnpm format:check` green after reformat | End-to-end gate | `pnpm format:check` exits 0 | Must pass before phase complete |
|
||||
| Behavior | Test Type | Automated Command | Notes |
|
||||
| ------------------------------------------------------ | --------------- | ---------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `pnpm lint` exits non-zero on a deliberate violation | Smoke gate | Introduce a test file with a known violation, run lint, assert non-zero exit | Manual CI run required; not a vitest test |
|
||||
| `pnpm format:check` exits non-zero on unformatted file | Smoke gate | Stage an unformatted file, run `prettier --check`, assert non-zero | Manual verification |
|
||||
| CI lint step activates (not a no-op) | Integration | PR to main; observe CI step now reports violations (not silent pass) | CI run |
|
||||
| All first-run violations fixed — both apps lint green | End-to-end gate | `pnpm lint` exits 0 | Must pass before phase complete |
|
||||
| Prettier reformats files | End-to-end gate | `pnpm format && git diff --stat` shows changes | Visual inspection |
|
||||
| `pnpm format:check` green after reformat | End-to-end gate | `pnpm format:check` exits 0 | Must pass before phase complete |
|
||||
|
||||
### Deliberate-Violation Test (ROADMAP success criterion 1)
|
||||
|
||||
**What:** Introduce a temporary deliberate lint violation to prove the gate actually fails CI (not just passes silently). This is the ROADMAP's explicit first success criterion.
|
||||
**Where:** Create a throwaway file `apps/api/src/_lint-gate-test.ts` containing:
|
||||
|
||||
```ts
|
||||
// eslint-gate-test: proves no-floating-promises fires
|
||||
export function testFloatingPromise(): void {
|
||||
Promise.resolve(1) // deliberately unhandled — should trigger no-floating-promises
|
||||
Promise.resolve(1); // deliberately unhandled — should trigger no-floating-promises
|
||||
}
|
||||
```
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Install + config committed; run `pnpm lint` locally — should exit non-zero with violation reported.
|
||||
2. Delete the test file; `pnpm lint` exits 0.
|
||||
3. Commit without the test file.
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- No new vitest test files are needed for this phase — the validation is the lint gate itself.
|
||||
- The "deliberate violation proves gate fails" check is a manual one-time smoke test, not an automated spec.
|
||||
|
||||
@@ -596,21 +620,21 @@ export function testFloatingPromise(): void {
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | No | Not touched by this phase |
|
||||
| V3 Session Management | No | Not touched |
|
||||
| V4 Access Control | No | Not touched |
|
||||
| V5 Input Validation | Indirectly | `no-unsafe-*` rules enforce type-safe access to external data |
|
||||
| V6 Cryptography | No | Not touched |
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
| --------------------- | ---------- | ------------------------------------------------------------- |
|
||||
| V2 Authentication | No | Not touched by this phase |
|
||||
| V3 Session Management | No | Not touched |
|
||||
| V4 Access Control | No | Not touched |
|
||||
| V5 Input Validation | Indirectly | `no-unsafe-*` rules enforce type-safe access to external data |
|
||||
| V6 Cryptography | No | Not touched |
|
||||
|
||||
### Known Threat Patterns for This Phase
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| Masking a real bug with `eslint-disable` | Tampering (code quality) | D-13-06 constraint: no blanket disables; justification required |
|
||||
| Floating promise swallowing errors silently | Information Disclosure | `no-floating-promises` catches unhandled rejections that may hide security-relevant errors |
|
||||
| `no-unsafe-*` on user-controlled data paths | Tampering | Use proper type guards instead of `as any` casts on data from external sources |
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
| ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| Masking a real bug with `eslint-disable` | Tampering (code quality) | D-13-06 constraint: no blanket disables; justification required |
|
||||
| Floating promise swallowing errors silently | Information Disclosure | `no-floating-promises` catches unhandled rejections that may hide security-relevant errors |
|
||||
| `no-unsafe-*` on user-controlled data paths | Tampering | Use proper type guards instead of `as any` casts on data from external sources |
|
||||
|
||||
**Note:** This phase primarily improves code quality, not security posture directly. The security value is indirect: `no-floating-promises` and `no-unsafe-*` rules prevent the class of bugs (unhandled rejections, unsafe data access) that could propagate into security-relevant code paths.
|
||||
|
||||
@@ -618,12 +642,12 @@ export function testFloatingPromise(): void {
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | `reactHooks.configs.flat.recommended` is the correct flat config export path for eslint-plugin-react-hooks@7.1.1 | Architecture Patterns | Lint config fails to load; need to use manual plugin registration instead |
|
||||
| A2 | Prettier `.prettierrc` `singleQuote: true` matches the codebase's existing style | Prettier Configuration | Reformat commit will be significantly larger; may need `singleQuote: false` |
|
||||
| A3 | Type-aware lint adds 30-90 seconds to CI fast-checks job | Performance | Could be faster (no impact) or slower (may need investigation) |
|
||||
| A4 | `apps/api/src/broker/sync.ts` ical.js casts will trigger `no-unsafe-*` violations | First-Run Violations | May be fewer violations than expected; or may need targeted disables |
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
| --- | ---------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------- |
|
||||
| A1 | `reactHooks.configs.flat.recommended` is the correct flat config export path for eslint-plugin-react-hooks@7.1.1 | Architecture Patterns | Lint config fails to load; need to use manual plugin registration instead |
|
||||
| A2 | Prettier `.prettierrc` `singleQuote: true` matches the codebase's existing style | Prettier Configuration | Reformat commit will be significantly larger; may need `singleQuote: false` |
|
||||
| A3 | Type-aware lint adds 30-90 seconds to CI fast-checks job | Performance | Could be faster (no impact) or slower (may need investigation) |
|
||||
| A4 | `apps/api/src/broker/sync.ts` ical.js casts will trigger `no-unsafe-*` violations | First-Run Violations | May be fewer violations than expected; or may need targeted disables |
|
||||
|
||||
---
|
||||
|
||||
@@ -644,6 +668,7 @@ export function testFloatingPromise(): void {
|
||||
## Sources
|
||||
|
||||
### Primary (MEDIUM confidence — Context7 official docs)
|
||||
|
||||
- `/typescript-eslint/typescript-eslint` (Context7) — flat config, projectService, disableTypeChecked, monorepos
|
||||
- `/websites/typescript-eslint_io` (Context7) — projectService blog post, allowDefaultProject
|
||||
- `/prettier/eslint-config-prettier` (Context7) — flat config import path, plugin naming pitfall, placement
|
||||
@@ -651,12 +676,14 @@ export function testFloatingPromise(): void {
|
||||
- `typescript-eslint.io/troubleshooting/typed-linting/performance/` — cache incompatibility, performance guidance
|
||||
|
||||
### Secondary (LOW confidence — WebSearch verified against multiple sources)
|
||||
|
||||
- `github.com/jsx-eslint/eslint-plugin-react/issues/3977` — ESLint 10 runtime incompatibility (open issue confirmed)
|
||||
- `github.com/facebook/react/issues/35758` — react-hooks ESLint 10 peerDep issue
|
||||
- ESLint 9 maintenance release 9.39.4 confirmed via npm dist-tags
|
||||
- eslint-plugin-react-hooks flat config export names confirmed via npm package inspection + community docs
|
||||
|
||||
### Package Registry Verification
|
||||
|
||||
- All versions confirmed via `npm view <pkg> version` on 2026-06-11
|
||||
- Repository URLs verified for all packages via `npm view <pkg> --json`
|
||||
- Postinstall scripts: none detected on any package
|
||||
@@ -664,6 +691,7 @@ export function testFloatingPromise(): void {
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
|
||||
- Standard stack + versions: MEDIUM — confirmed via npm registry; ESLint 10 compat issue confirmed via GitHub issue
|
||||
- Architecture: MEDIUM — based on official typescript-eslint docs via Context7
|
||||
- First-run violations: LOW-MEDIUM — based on codebase inspection; actual violations may differ
|
||||
|
||||
@@ -18,13 +18,13 @@ created: 2026-06-11
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Vitest (apps/api: node env; apps/pwa: jsdom env) — already present, unchanged |
|
||||
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/vitest.config.ts` |
|
||||
| **Quick run command** | `pnpm lint` (root — runs ESLint across both apps) |
|
||||
| **Full suite command** | `pnpm lint && pnpm format:check && pnpm typecheck && pnpm test` |
|
||||
| **Estimated runtime** | ~30–60s (type-aware lint over ~91 src files + tests + e2e, no cache) |
|
||||
| Property | Value |
|
||||
| ---------------------- | ----------------------------------------------------------------------------- |
|
||||
| **Framework** | Vitest (apps/api: node env; apps/pwa: jsdom env) — already present, unchanged |
|
||||
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/vitest.config.ts` |
|
||||
| **Quick run command** | `pnpm lint` (root — runs ESLint across both apps) |
|
||||
| **Full suite command** | `pnpm lint && pnpm format:check && pnpm typecheck && pnpm test` |
|
||||
| **Estimated runtime** | ~30–60s (type-aware lint over ~91 src files + tests + e2e, no cache) |
|
||||
|
||||
---
|
||||
|
||||
@@ -39,15 +39,15 @@ created: 2026-06-11
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 13-XX-XX | TBD | 1 | SC-1 (gate fails on violation) | — | N/A | smoke gate | `pnpm lint` exits non-zero on deliberate violation | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 1 | SC-1 (format gate fails) | — | N/A | smoke gate | `pnpm format:check` exits non-zero on unformatted file | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-3 (baseline green) | — | N/A | end-to-end gate | `pnpm lint` exits 0 (both apps) | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-3 (format baseline) | — | N/A | end-to-end gate | `pnpm format:check` exits 0 | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-2 (CI gate blocks PR) | — | N/A | integration | CI `fast-checks` lint + format:check steps report violations (not silent pass) | ❌ W0 | ⬜ pending |
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
| -------- | ---- | ---- | ------------------------------ | ---------- | --------------- | --------------- | ------------------------------------------------------------------------------ | ----------- | ---------- |
|
||||
| 13-XX-XX | TBD | 1 | SC-1 (gate fails on violation) | — | N/A | smoke gate | `pnpm lint` exits non-zero on deliberate violation | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 1 | SC-1 (format gate fails) | — | N/A | smoke gate | `pnpm format:check` exits non-zero on unformatted file | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-3 (baseline green) | — | N/A | end-to-end gate | `pnpm lint` exits 0 (both apps) | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-3 (format baseline) | — | N/A | end-to-end gate | `pnpm format:check` exits 0 | ❌ W0 | ⬜ pending |
|
||||
| 13-XX-XX | TBD | 2 | SC-2 (CI gate blocks PR) | — | N/A | integration | CI `fast-checks` lint + format:check steps report violations (not silent pass) | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky. Task IDs assigned by planner.*
|
||||
_Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky. Task IDs assigned by planner._
|
||||
|
||||
---
|
||||
|
||||
@@ -56,17 +56,17 @@ created: 2026-06-11
|
||||
- [ ] No new vitest test files are required — the validation is the lint/format gate itself.
|
||||
- [ ] `eslint.config.js` + Prettier config + package-level `lint` scripts must exist before any gate assertion can run (this is the phase's own Wave 1 work, not a test scaffold).
|
||||
|
||||
*Existing vitest infrastructure is untouched; this phase adds no unit specs.*
|
||||
_Existing vitest infrastructure is untouched; this phase adds no unit specs._
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Lint gate fails CI on a real violation | SC-1 | One-time smoke proof; not a repeatable automated spec | Create throwaway `apps/api/src/_lint-gate-test.ts` with an unhandled `Promise.resolve(1)` (triggers `no-floating-promises`); run `pnpm lint` → assert non-zero; delete file → assert exit 0; do NOT commit the throwaway file |
|
||||
| Prettier reformats existing files | SC-3 | Visual confirmation of the mechanical reformat diff | `pnpm format && git diff --stat` shows the reformat; commit isolated from logic fixes for reviewability |
|
||||
| CI step activates (was a no-op) | SC-2 | Requires a real PR to main on the Gitea runner | Open PR; observe `fast-checks` lint + format:check steps now report/gate instead of silently passing |
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
| -------------------------------------- | ----------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Lint gate fails CI on a real violation | SC-1 | One-time smoke proof; not a repeatable automated spec | Create throwaway `apps/api/src/_lint-gate-test.ts` with an unhandled `Promise.resolve(1)` (triggers `no-floating-promises`); run `pnpm lint` → assert non-zero; delete file → assert exit 0; do NOT commit the throwaway file |
|
||||
| Prettier reformats existing files | SC-3 | Visual confirmation of the mechanical reformat diff | `pnpm format && git diff --stat` shows the reformat; commit isolated from logic fixes for reviewability |
|
||||
| CI step activates (was a no-op) | SC-2 | Requires a real PR to main on the Gitea runner | Open PR; observe `fast-checks` lint + format:check steps now report/gate instead of silently passing |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user