docs(03): add code review fix report (--auto, 2 fix passes)
Auto-fix loop converged after 2 fix iterations + a final verifying re-review: - Pass 1: 13/14 findings fixed (3 Critical, 6 Warning, 4 Info). - Re-review surfaced 1 new Critical (move-path RRULE data loss) + 4 lower. - Pass 2: 8/8 fixed, including the move-path RRULE forwarding. - Final re-review: 0 Critical. Remaining 2 Warning / 2 Info are documented v1 scope cuts (recurrence-editing deferred), not defects. Test suites green throughout: api 108, pwa 145; both tsc --noEmit clean. Per-iteration REVIEW/REVIEW-FIX snapshots retained as audit trail.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
---
|
||||
phase: 03-event-write-back-pwa-install
|
||||
fixed_at: 2026-06-09T00:00:00Z
|
||||
review_path: .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md
|
||||
iteration: 1
|
||||
findings_in_scope: 14
|
||||
fixed: 13
|
||||
skipped: 1
|
||||
status: partial
|
||||
---
|
||||
|
||||
# Phase 3: Code Review Fix Report
|
||||
|
||||
**Fixed at:** 2026-06-09
|
||||
**Source review:** .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md
|
||||
**Iteration:** 1
|
||||
|
||||
**Summary:**
|
||||
- Findings in scope: 14 (fix_scope: all — Critical + Warning + Info)
|
||||
- Fixed: 13
|
||||
- Skipped: 1
|
||||
|
||||
**Note on recovery:** a prior `--fix` run was interrupted (orphan worktree
|
||||
`/tmp/sv-03-reviewfix-uxjhc1` + branch `gsd-reviewfix/03-53993` + recovery sentinel).
|
||||
That run's 3 commits had mismatched finding labels and its branch had diverged from the
|
||||
current branch tip (which had advanced with docs commits, making a fast-forward
|
||||
impossible). Per the recovery protocol the orphan worktree/branch/sentinel were cleaned
|
||||
up and all fixes were re-applied fresh from the current branch tip. All 13 commits below
|
||||
are new.
|
||||
|
||||
**Verification environment:** the isolated worktree had no `node_modules` (gitignored,
|
||||
not carried into a fresh worktree). `node_modules` from the main repo were symlinked in
|
||||
so `tsc --noEmit` could resolve dependencies for Tier-2 syntax/type checks. The symlinks
|
||||
are gitignored and were never committed. Every fix was Tier-2 verified (full
|
||||
`tsc --noEmit` per affected package, clean).
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
### CR-01: Event edit/delete ownership check resolves an arbitrary member's row (uid-only lookup)
|
||||
|
||||
**Files modified:** `apps/api/src/routes/events.ts`
|
||||
**Commit:** 54addb1
|
||||
**Status:** fixed: requires human verification (ownership/authorization logic)
|
||||
**Applied fix:** Both the PATCH `/:uid/edit` and DELETE `/:uid` lookups now scope the
|
||||
`calendarEvents` → `calendars` join to the acting member's writable set
|
||||
(`or(calendars.userId = currentUserId, calendars.isShared)`), add
|
||||
`orderBy(sql\`(calendars.userId = currentUserId) desc\`)` so the user's own row ranks
|
||||
ahead of a shared/other copy, and `limit(1)` for determinism. This stops `[0]` from
|
||||
resolving to another member's calendar row for a shared-account uid (D-16).
|
||||
|
||||
### CR-02: Outbox WR-02 "freshest etag" re-read also queries uid-only
|
||||
|
||||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
|
||||
**Commit:** a596f52
|
||||
**Status:** fixed: requires human verification (etag-selection logic)
|
||||
**Applied fix:** The pre-PUT freshest-etag re-read now joins through `calendars` and
|
||||
filters on the outbox row's own `userId` + `calendarUrl` with `limit(1)`, so the etag
|
||||
used in `If-Match` belongs to the writing member's calendar instead of an arbitrary
|
||||
shared-account row. `calendars` added to the schema import.
|
||||
|
||||
### CR-03: All-day end date exclusive on write but inclusive on edit pre-fill
|
||||
|
||||
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
|
||||
**Commit:** f645644
|
||||
**Status:** fixed: requires human verification (date-arithmetic / data-correctness)
|
||||
**Applied fix:** Added `exclusiveEndToInclusiveDate()` (DST-safe UTC-component
|
||||
subtraction) and apply it when pre-filling the end-date input for all-day occurrences —
|
||||
both in the initial `useState` and the open/reset effect. Keeps `occurrence.end`
|
||||
exclusive everywhere (reviewer option a); `buildVeventString` still rolls forward to
|
||||
exclusive at the ICS boundary, so a re-edit no longer grows the span by a day.
|
||||
**Note:** the reviewer also suggested a regression test (edit an all-day multi-day event
|
||||
twice, assert the span is stable). Not added — flagged for the developer.
|
||||
|
||||
### WR-01: Recurrence silently reset to `none` on every edit — data loss
|
||||
|
||||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`, `apps/api/src/broker/vevent.ts`, `apps/pwa/src/api/client.ts`, `apps/pwa/src/components/EventForm.tsx`
|
||||
**Commit:** 02aa407
|
||||
**Status:** fixed: requires human verification (data-loss-prevention logic)
|
||||
**Applied fix:** Coordinated change so an edit no longer strips a recurring series:
|
||||
- `vevent.ts`: new `extractRruleString()` parses the existing RRULE from a stored VEVENT.
|
||||
- `outboxWorker.ts` (update path): when the payload carries no explicit `recurrence`, the
|
||||
freshest-etag query also reads `rawVevent` and preserves the existing RRULE; an explicit
|
||||
recurrence value (including `'none'`) still overrides.
|
||||
- `client.ts`: `CreateEventPayload.recurrence` made optional (matches the API Zod schema,
|
||||
which already had it optional).
|
||||
- `EventForm.tsx`: on edit, `recurrence` is omitted from the payload (signals "unchanged")
|
||||
and the recurrence `<select>` is disabled — editing recurrence is deferred until the
|
||||
occurrence contract exposes it.
|
||||
|
||||
### WR-02: Default-calendar selection on create is non-deterministic
|
||||
|
||||
**Files modified:** `apps/api/src/routes/events.ts`
|
||||
**Commit:** 5499f83
|
||||
**Applied fix:** Added `.orderBy(calendars.id).limit(1)` to the default-calendar query in
|
||||
POST `/create`, giving a stable insertion-order default instead of an arbitrary `[0]`.
|
||||
|
||||
### WR-03: `parseDateTime` all-day check uses the raw `iso`, not the cleaned string
|
||||
|
||||
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
|
||||
**Commit:** d34edec
|
||||
**Applied fix:** The all-day regex test and early return now use `clean` (IANA-suffix
|
||||
stripped) instead of the raw `iso`, matching the documented strip intent.
|
||||
|
||||
### WR-04: Worker cron schedules start on bare module import — pollutes the test process
|
||||
|
||||
**Files modified:** `apps/api/src/index.ts`
|
||||
**Commit:** 7bc129f
|
||||
**Applied fix:** `startBrokerPoller()` and `startOutboxWorker()` moved out of top level
|
||||
into the `isMainModule()` entrypoint guard, so importing `./index.js` in route tests no
|
||||
longer registers real `node-cron` schedules or leaks open handles.
|
||||
|
||||
### WR-05: `index.ts` direct-run guard is fragile and can mis-fire
|
||||
|
||||
**Files modified:** `apps/api/src/index.ts`
|
||||
**Commit:** 22d1bc2
|
||||
**Applied fix:** Replaced the basename-tail `endsWith` heuristic with
|
||||
`isMainModule()` comparing `fileURLToPath(import.meta.url)` against
|
||||
`realpathSync(process.argv[1])` (symlink-resolved), guarded by try/catch.
|
||||
|
||||
### WR-06: Edit-as-move create-412 dead-ends the move with no retry path
|
||||
|
||||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`, `apps/pwa/src/components/SyncStateToast.tsx`
|
||||
**Commit:** 1c71f8c
|
||||
**Status:** fixed: requires human verification (UX/conflict-flow logic)
|
||||
**Applied fix:** When a create row carrying a `groupId` (edit-as-move) hits 412, the
|
||||
worker now writes a distinct `move-failed:` `lastError` (no `'412'` substring).
|
||||
`SyncStateToast` detects it (`error.startsWith('move-failed')`), routes it away from the
|
||||
etag-conflict copy, and shows "Couldn't move the event. Open it and save again." No
|
||||
contract change — surfaced via the existing `sync-status` `error` field.
|
||||
|
||||
### IN-01: `triggerTargetedResync` re-loads and re-decrypts the credential per row
|
||||
|
||||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
|
||||
**Commit:** 95f9d8c
|
||||
**Applied fix:** `triggerTargetedResync` accepts an optional per-drain-cycle
|
||||
`Map<number, FastmailClient>` cache; `runOutboxDrain` creates one per cycle and passes it
|
||||
to both call sites, so each member's credential is decrypted at most once per cycle
|
||||
(narrows the decrypted-password-in-memory window, T-03-13). Cache is discarded when the
|
||||
drain returns.
|
||||
|
||||
### IN-02: Unknown-status responses retried for the full backoff window before giving up
|
||||
|
||||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
|
||||
**Commit:** e29d6c1
|
||||
**Status:** fixed: requires human verification (error-classification logic)
|
||||
**Applied fix:** `dispatchRow` now classifies any unmapped 4xx (status 400–499, after the
|
||||
explicit 408/429 transient set and 400/401/403 hard-fail set are handled) as a hard fail,
|
||||
so permanent client errors (405/409/422) settle immediately instead of burning the retry
|
||||
budget. 5xx, network, and truly unknown statuses still fall through to transient.
|
||||
|
||||
### IN-03: `InstallPrompt` reads `localStorage` synchronously without try/catch
|
||||
|
||||
**Files modified:** `apps/pwa/src/components/InstallPrompt.tsx`
|
||||
**Commit:** 7e4ea71
|
||||
**Applied fix:** Added guarded `readDismissed()` / `persistDismissed()` helpers
|
||||
(try/catch, mirroring `calendarStore.ts`) used by the `useState` initializer and
|
||||
`dismiss()`, so a throwing `localStorage` (private mode / SSR) degrades to "not dismissed"
|
||||
instead of crashing the component on mount.
|
||||
|
||||
### IN-04: `resolveUserId` typed as `any`
|
||||
|
||||
**Files modified:** `apps/api/src/routes/events.ts`
|
||||
**Commit:** 6d2fd79
|
||||
**Applied fix:** Parameter typed as Hono's `Context` (imported as a type) instead of
|
||||
`any`, removing the eslint-disable. `c.get('user')` resolves through the existing
|
||||
`ContextVariableMap` augmentation in `auth/devBypass.ts` and `getAuth(c)` accepts a
|
||||
`Context`. Used `Context` rather than the reviewer's literal
|
||||
`Context<{ Variables: { user?: { id: number } } }>` because the latter would conflict
|
||||
with the global `ContextVariableMap` augmentation (which types `user` non-optionally as
|
||||
the DEV_USER shape).
|
||||
|
||||
## Skipped Issues
|
||||
|
||||
### IN-05: `deleteCalendarEvent` relies on tsdav ignoring `data: ''`
|
||||
|
||||
**File:** `apps/api/src/broker/write.ts:93`
|
||||
**Reason:** skipped: reviewer specifies "None required for v1; note on the tsdav upgrade
|
||||
checklist." No source change is warranted — the finding asks for a process/checklist note,
|
||||
not a code fix. The existing inline comment already documents the dependency on tsdav
|
||||
internals. Flagged here so the developer can add a tsdav-upgrade-checklist entry.
|
||||
**Original issue:** Passes an empty `data` placeholder because tsdav requires the
|
||||
`DAVCalendarObject` shape. Relies on tsdav internals; a future version validating `data`
|
||||
would break this silently.
|
||||
|
||||
---
|
||||
|
||||
_Fixed: 2026-06-09_
|
||||
_Fixer: Claude (gsd-code-fixer)_
|
||||
_Iteration: 1_
|
||||
Reference in New Issue
Block a user