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

<objective>
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.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-real-lint-gate-eslint/13-CONTEXT.md
@.planning/phases/13-real-lint-gate-eslint/13-RESEARCH.md
@.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
@.planning/phases/13-real-lint-gate-eslint/13-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
</context>

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

<tasks>

<task type="auto">
  <name>Task 1: Fix apps/api violations (broker async paths + ical.js unsafe access + api tests)</name>
  <files>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</files>
  <read_first>
    - 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.
  </read_first>
  <action>
    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.

  </action>
  <verify>
    <automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api lint; test $? -eq 0 && echo API_LINT_GREEN</automated>
  </verify>
  <done>`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.</done>
</task>

<task type="auto">
  <name>Task 2: Fix apps/pwa violations (React/hooks rules + explicit-any + e2e specs)</name>
  <files>apps/pwa/src/**/*.{ts,tsx} (notably components/EventForm.tsx, main.tsx, ErrorBoundary.tsx, sw.ts), apps/pwa/e2e/**/*.ts</files>
  <read_first>
    - 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.
  </read_first>
  <action>
    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.

  </action>
  <verify>
    <automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa lint; test $? -eq 0 && echo PWA_LINT_GREEN</automated>
  </verify>
  <done>`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.</done>
</task>

<task type="auto">
  <name>Task 3: Assert whole-repo lint green + typecheck/tests unbroken by the fixes</name>
  <files>(verification only — no new edits expected)</files>
  <read_first>
    - 13-VALIDATION.md "Sampling Rate" + Vitest passes-while-tsc-fails note (run tsc --noEmit, not just vitest).
  </read_first>
  <action>
    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.
  </action>
  <verify>
    <automated>cd /home/luc/Projects/familysync && pnpm lint && pnpm typecheck && pnpm --filter @familysync/api test && pnpm --filter @familysync/pwa test && echo LINT_TYPECHECK_TESTS_GREEN</automated>
  </verify>
  <done>`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.</done>
</task>

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

<verification>
- `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.
</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>

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