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.
9.1 KiB
phase, reviewed, depth, files_reviewed, files_reviewed_list, findings, status
| phase | reviewed | depth | files_reviewed | files_reviewed_list | findings | status | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-event-write-back-pwa-install | 2026-06-09T00:00:00Z | standard | 29 |
|
|
issues_found |
Phase 3: Code Review Report (Re-Review, Iteration 3 — final --auto pass)
Reviewed: 2026-06-09 Depth: standard Files Reviewed: 29 Status: issues_found (no blockers — remaining items are accepted v1 limitations)
Summary
Final re-review of the event write-back + PWA-install phase after the iteration-2 fix pass. I traced each iteration-2 fix end-to-end against its implementation and tests. All iteration-2 fixes are correct and introduce no regressions. The prior BLOCKER (CR-01: edit-as-move strips the RRULE) is now resolved and correct.
Iteration-2 fixes — verified
-
Move-path RRULE forwarding (CR-01) — VERIFIED FIXED.
events.tsnow selectsrawVeventin the edit lookup (events.ts:338) and, in the move branch, extracts the source RRULE and stashes it as_preservedRruleon the create payload only when the edit carried no explicit recurrence (events.ts:388-395). The worker create branch reads it back:hasExplicitRecurrenceis computed viahasOwnProperty(fields,'recurrence')(outboxWorker.ts:336), andrruleStringresolves topreservedRrule ?? rruleFromPayloadonly when there is no explicit recurrence (outboxWorker.ts:341-353). The two sides agree: an EDIT omitsrecurrence, sohasExplicitRecurrence=falseand the stashed RRULE is applied; an explicitrecurrence(including'none') still wins.JSON.stringifyon the move payload drops the absentrecurrencekey, sohasOwnPropertyis correctlyfalseafter the round-trip. Covered by events.test.ts:422-469 (route stashes RRULE) and outboxWorker.test.ts:311-358 (worker re-applies; explicit'none'still emits no RRULE). No regression to the same-calendarupdatepreserve path (outboxWorker.ts:281-285). -
Outbox payload re-validation (IN-03) — VERIFIED FIXED. Both the
updateandcreatebranches parse the stored JSON, thenoutboxPayloadSchema.safeParseit (outboxWorker.ts:231-235, 323-327). A schema-invalid row is hard-failed (no retry, no CalDAV dispatch). The schema mirrorseventFieldsSchemaand uses.passthrough()so_preservedRrulesurvives validation (outboxWorker.ts:70-82). Covered by outboxWorker.test.ts:288-306 (missing title → hard-fail, never dispatched). -
Sync-status failed-row ranking (WR-04) — VERIFIED FIXED.
sync-statusorders by a status-priority CASE (failed/dead=0,pending=1, else=2) thencreatedAt DESC(events.ts:549-552), so an earlier failed/dead row for a uid outranks a laterdonerow. Covered by events.test.ts:556-589, which also asserts the CASE expression is present in the ORDER BY chunks. -
Helper-text / all-day toggle clamp (WR-01/WR-02 UI) — VERIFIED FIXED. The recurrence
<select>is disabled in edit mode with explanatory helper text (EventForm.tsx:789-800), andhandleAllDayToggleclampsendDatetomax(startDate,endDate)on toggle-on and clears stale time errors (EventForm.tsx:296-305). -
All-day inclusive/exclusive DTEND symmetry (CR-03) — STILL CORRECT.
vevent.ts:116-123rolls the inclusive end forward one UTC day on write;EventForm.tsx:86-96rolls it back on pre-fill. Symmetric; covered by vevent.test.ts:140-160.
The two findings below are carried-forward, deliberately-accepted v1 limitations (documented in code), not regressions; they are recorded for completeness. There are no blockers in this phase.
Warnings
WR-01: Missing cached etag still produces an unconditional PUT/DELETE (D-08 gap)
File: apps/api/src/broker/write.ts:74-78, 103-107; apps/api/src/routes/events.ts:406, 432, 507
Issue: When eventRow.etag is null (event cached before an etag was captured, or Fastmail omitted it), the outbox row's etag is undefined, and write.ts maps null/'' to "no If-Match header" — an unconditional PUT/DELETE. That defeats D-08 conflict detection for exactly the rows most likely to be stale: a concurrent external edit is silently overwritten with no 412. The iteration-1 fix added a console.warn so the path is observable (write.ts:75-77, 104-106), but the unconditional write itself is unchanged — observability is not prevention. Only triggers when the cached etag is missing, so Warning, not Blocker.
Fix: When no etag is available, fetch the current etag (REPORT/GET) before writing, or skip the write and force a targeted re-sync so the next attempt carries a real etag. At minimum, document that the no-etag path is an accepted unconditional-write window for v1.
WR-02: Edit cannot change or remove an RRULE; "no recurrence field" is overloaded as "keep existing"
File: apps/pwa/src/components/EventForm.tsx:361-371, 768-800; apps/api/src/broker/outboxWorker.ts:281-285, 336-353
Issue: The recurrence <select> is hard-disabled on edit and the payload always omits recurrence on edit (EventForm.tsx:367). The server treats an absent recurrence as "preserve the stored RRULE" (both the same-calendar update and the move path). The consequence is that a user can never (a) change a recurring event's frequency, nor (b) intentionally make a recurring event non-recurring — there is no way to express "remove the RRULE" through the edit form, because "omit recurrence" is reserved to mean "unchanged." Helper text now surfaces the constraint (EventForm.tsx:789-800), which is the iteration-2 mitigation, so this is a documented v1 scope cut rather than a silent trap. Recorded because the overloaded semantics will need disentangling when recurrence editing ships (a sentinel distinct from "omitted" will be required to express "remove").
Fix: When recurrence editing lands, introduce an explicit "remove recurrence" signal distinct from an omitted field (e.g. recurrence: 'none' already overrides — wire the edit form to send it when the user clears the schedule), and parse-and-modify the stored RECUR in place rather than replacing it with a bare preset (see IN-01).
Info
IN-01: RRULE_PRESETS round-trip is lossy for any parameterized RRULE
File: apps/api/src/broker/vevent.ts:49-54; apps/api/src/broker/outboxWorker.ts:245-248, 337-340
Issue: RRULE_PRESETS maps only to bare FREQ=DAILY|WEEKLY|MONTHLY|YEARLY. extractRruleString correctly preserves the full stored RECUR (which may carry BYDAY/INTERVAL/COUNT/UNTIL), and both preserve paths keep that rich rule. But if a recurrence preset value is ever applied to a previously-rich rule, it collapses the rule to the bare preset — silently dropping qualifiers. This cannot happen in v1 (the picker offers only the four bare presets and is disabled on edit), so it is latent, not active. The limitation is now documented at the RRULE_PRESETS definition (vevent.ts:39-48).
Fix: When recurrence editing ships, parse the existing RECUR and modify it in place instead of replacing it with a preset.
IN-02: Move-path RRULE preservation depends silently on rawVevent being non-empty
File: apps/api/src/routes/events.ts:388-391
Issue: In the move branch, preservedRrule = payload.recurrence === undefined ? extractRruleString(eventRow.rawVevent ?? '') : undefined. If eventRow.rawVevent is ever null/empty (it is selected at events.ts:338 and calendar_events.rawVevent is notNull per schema.ts:106, so this is not currently reachable), extractRruleString('') returns undefined and the move silently drops the RRULE with no diagnostic. The schema NOT NULL constraint makes this safe today; the fragility is that the preserve path has no observability if that invariant ever changes (unlike write.ts:75-77 which logs the analogous no-etag gap).
Fix: Optional — log a warning when a move with no explicit recurrence finds no extractable RRULE on a recurring-looking source, so a future schema/contract change that empties rawVevent is diagnosable rather than silent.
Reviewed: 2026-06-09 Reviewer: Claude (gsd-code-reviewer) Depth: standard