--- phase: 13-real-lint-gate-eslint plan: 02 type: execute wave: 2 depends_on: - 13-01 files_modified: - apps/api/src/** - apps/api/tests/** - apps/pwa/src/** - apps/pwa/e2e/** 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)" 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" 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" --- 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. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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 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. 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. 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 (``) 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. ## 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. | - `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. - `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). 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).