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

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-real-lint-gate-eslint 02 execute 2
13-01
apps/api/src/**
apps/api/tests/**
apps/pwa/src/**
apps/pwa/e2e/**
true
truths artifacts key_links
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)
path provides
apps/api/src/broker/sync.ts ical.js unsafe-access findings resolved (narrowed or justified-suppressed)
path provides
apps/pwa/src/components/EventForm.tsx no-explicit-any on the occurrence.recurrence cast handled via the pre-existing justified suppression
from to via pattern
pnpm lint (root) apps/api + apps/pwa lint scripts exit code 0 after fixes pnpm -r --if-present lint
Run `pnpm lint` (real gate from Plan 01) and fix EVERY first-run violation across both apps until `pnpm lint` exits 0. Fixes must address violations, not mask them (D-13-06). Reformatting (Prettier) is explicitly NOT in this plan — Plan 03 owns the mechanical reformat as an isolated commit (D-13-08). Keep this plan's diff to genuine lint fixes so the reformat diff stays reviewable.

Purpose: A lint gate that never passes is as useless as one that never fails. D-13-05 requires the codebase to actually be clean; real bugs (floating promises, misused promises, unsafe access) get genuinely fixed, not silenced. Output: lint-clean source across apps/api and apps/pwa. pnpm lint exits 0.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md @.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md @.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md @.planning/phases/13-real-lint-gate-eslint/13-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 @apps/api/src/broker/poller.ts @apps/api/src/broker/reminderScheduler.ts @apps/pwa/src/components/EventForm.tsx

<artifacts_this_phase_produces> No NEW files. This plan MODIFIES existing source to remove lint violations. New eslint-disable-next-line comments (each with a justification) may be added to broker files and EventForm.tsx. The deliverable is a state change: pnpm lint exit 0.

Downstream: Plan 03 reformats everything with Prettier (isolated commit) and asserts the green baseline + CI format step. </artifacts_this_phase_produces>

<context_handoff> Read 13-01-SUMMARY.md FIRST. It contains the per-rule violation inventory pnpm lint produced in Plan 01. Use that inventory as the authoritative worklist — do not rediscover it from scratch. The classes RESEARCH anticipated (and how to fix each) are below; the SUMMARY tells you which actually fired and where. </context_handoff>

Task 1: Fix apps/api violations (broker async paths + ical.js unsafe access + api tests) apps/api/src/**/*.ts (notably broker/sync.ts, broker/expand.ts, broker/outboxWorker.ts, broker/poller.ts, broker/reminderScheduler.ts, routes/**), apps/api/tests/**/*.ts - 13-01-SUMMARY.md — the violation inventory (authoritative worklist). - 13-RESEARCH.md "First-Run Violations: Expected Findings and Correct Fixes" (no-unsafe-* family, no-floating-promises, no-misused-promises, require-await, no-unused-vars) + Pitfalls 4/5. - 13-PATTERNS.md "apps/api/src/broker/*.ts" section (the existing setInterval+.catch pattern, the `void runX().catch(...)` legitimate form, the ical.js suppression idiom) + "eslint-disable-next-line with justification comment" shared pattern. Iterate `pnpm --filter @familysync/api lint` until it exits 0. Fix by rule class, applying RESEARCH's right-fix-vs-mask guidance:
no-floating-promises / no-misused-promises (broker workers, route handlers): The three workers (outboxWorker.ts, poller.ts, reminderScheduler.ts) already use `setInterval(() => { runX().catch((err: unknown) => {...}) })`. If the rule fires on the `.catch()` return value, prepend `void`: `void runX().catch(...)` — legitimate fire-and-forget within a self-scheduling setInterval (Pitfall 4 / PATTERNS). HARD CONSTRAINT (D-13-06): before applying `void` to ANY promise on the push/outbox/reminder dispatch paths (e.g. dispatchEventChange, reminder fan-out, web-push send), review whether the promise should actually be `await`ed — a missing await there is a real error-swallowing bug, not noise. Only `void` a promise that is genuinely fire-and-forget, and only with that intent stated in a comment. For event-listener / addEventListener async callbacks (Pitfall 5), wrap as `() => { void asyncHandler() }` or extract a named function.

no-unsafe-* family (broker/sync.ts, broker/expand.ts): ical.js `getFirstPropertyValue()` returns `any`. Prefer narrowing via typeof/instanceof or an `ICAL.Time` cast where the value is then validated. Where narrowing is impractical because the library lacks types, use a targeted `// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access` (or the specific rule) WITH a justification comment in the EventForm.tsx:271-275 idiom — multi-line comment block stating "ical.js getFirstPropertyValue returns 'any'; caller immediately validates", then the disable line, then the code line, no blank lines between. sync.ts already has an explanatory comment at line 99 — only the disable line is the addition there.

require-await: remove `async` if the function has no await and returns a non-Promise, OR add the intended await. no-unused-vars: prefix intentionally-unused params/vars with `_` (the config already ignores `^_`); otherwise delete the dead binding.

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.
cd /home/luc/Projects/familysync && pnpm --filter @familysync/api lint; test $? -eq 0 && echo API_LINT_GREEN `pnpm --filter @familysync/api lint` exits 0. Every broker floating/misused-promise finding was reviewed for a real missing-await bug before being voided; any `void` applied is genuine fire-and-forget with a documented intent. ical.js unsafe-access findings are either narrowed or carry a justified eslint-disable-next-line comment. No blanket disables; no reformatting. The verify gate prints API_LINT_GREEN. Task 2: Fix apps/pwa violations (React/hooks rules + explicit-any + e2e specs) apps/pwa/src/**/*.{ts,tsx} (notably components/EventForm.tsx, main.tsx, ErrorBoundary.tsx, sw.ts), apps/pwa/e2e/**/*.ts - 13-01-SUMMARY.md — the violation inventory (pwa half). - 13-RESEARCH.md First-Run Violations (react/display-name, no-explicit-any EventForm.tsx:275 case) + Pitfalls 6 (sw.ts webworker lib) and 8 (React import in main.tsx/ErrorBoundary.tsx is legit — rule should not fire). - 13-PATTERNS.md "apps/pwa/src/components/EventForm.tsx" section — the suppression at lines 271-275 is PRE-EXISTING and already justified; verify it sits on the line immediately before the cast and that no other `as any` exists in the file. Iterate `pnpm --filter @familysync/pwa lint` until it exits 0. By rule class:
no-explicit-any (EventForm.tsx ~line 275): the `(occurrence as any)?.recurrence` cast already has a justified `// eslint-disable-next-line @typescript-eslint/no-explicit-any` immediately above it (PATTERNS). Verify the disable comment is correctly positioned and that the rule no longer fires; do NOT add a second suppression. If any OTHER `as any` exists, narrow it with a proper type guard rather than suppressing.

react/display-name: add a displayName or convert anonymous memo/forwardRef components to named function expressions (`memo(function Foo(){...})`). react/prop-types: should be OFF already (Plan 01 disabled it for React 19) — if it still fires, confirm the off-rule landed in the pwa-react block. React namespace usage in main.tsx (`<React.StrictMode>`) and ErrorBoundary.tsx (`extends React.Component`) is legitimate (Pitfall 8) — `react/react-in-jsx-scope` is already disabled by flat.recommended; do not delete the React import.

react-hooks/exhaustive-deps: for each finding, ADD the genuinely-missing dependency (the real fix). Only suppress with `// eslint-disable-next-line react-hooks/exhaustive-deps` + a justification if adding the dep would cause an intentional one-shot effect to re-run — and state that reason (D-13-06). Do not blanket-disable hooks rules.

sw.ts (Pitfall 6): if type-aware rules error on ServiceWorker globals due to the `webworker` lib reference, prefer keeping it in-project; only if it genuinely cannot resolve, add sw.ts to a narrow disableTypeChecked override in eslint.config.js with a comment — but try narrowing first.

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.
cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa lint; test $? -eq 0 && echo PWA_LINT_GREEN `pnpm --filter @familysync/pwa lint` exits 0. EventForm.tsx uses the single pre-existing justified suppression (no duplicate). exhaustive-deps findings got the real missing dependency where correct, suppressed only with a stated reason. The React import stays in main.tsx/ErrorBoundary.tsx. e2e missing-await findings fixed, not voided. No reformatting. The verify gate prints PWA_LINT_GREEN. Task 3: Assert whole-repo lint green + typecheck/tests unbroken by the fixes (verification only — no new edits expected) - 13-VALIDATION.md "Sampling Rate" + Vitest passes-while-tsc-fails note (run tsc --noEmit, not just vitest). Final guard for the wave. Run `pnpm lint` (root, both apps) and confirm exit 0. Then confirm the lint fixes did not break compilation or tests: run `pnpm typecheck` (tsc --noEmit for both apps — esbuild/vitest can stay green on type errors, so tsc is the real check) and `pnpm test` (apps/api) + `pnpm --filter @familysync/pwa test`. If any fix introduced a type error or test failure, correct it here (a fix that breaks the build is not a fix). Do NOT reformat — Plan 03 owns that. cd /home/luc/Projects/familysync && pnpm lint && pnpm typecheck && pnpm --filter @familysync/api test && pnpm --filter @familysync/pwa test && echo LINT_TYPECHECK_TESTS_GREEN `pnpm lint` exits 0 across both apps; `pnpm typecheck` clean (both apps, incl. tsconfig.e2e.json); api + pwa test suites pass. No reformatting performed in this plan. The verify gate prints LINT_TYPECHECK_TESTS_GREEN.

<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
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_model>

- `pnpm --filter @familysync/api lint` exit 0; `pnpm --filter @familysync/pwa lint` exit 0; root `pnpm lint` exit 0. - `pnpm typecheck` clean (both apps), `pnpm test` (api) + pwa test green — fixes did not regress build/tests. - Diff review: no blanket `eslint-disable` (file-level); every `eslint-disable-next-line` has an adjacent justification comment; no `void` on a push/outbox/reminder promise without a documented fire-and-forget rationale.

<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>
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).