Files
familysync/.planning/phases/06-ux-polish/06-02-SUMMARY.md
T

6.4 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
06-ux-polish 02 api/broker
tdd
rrule
recurrence
serialization
ical.js
zod
requires provides affects
assembleRruleString helper in apps/api/src/broker/outboxWorker.ts
recurrenceUntil/recurrenceCount fields in outboxPayloadSchema + eventFieldsSchema
apps/api/src/broker/outboxWorker.ts
apps/api/src/routes/events.ts
apps/api/tests/broker/vevent.test.ts
apps/api/tests/broker/outboxWorker.test.ts
added patterns
ical.js ICAL.Recur.fromString + rruleProp.setValue for RRULE serialization
assembleRruleString count-wins-over-until mutual exclusion (RFC 5545 §3.3.10)
Series-edit Pitfall 3
strip UNTIL/COUNT via regex before re-applying new bound
created modified
apps/api/src/broker/outboxWorker.ts
apps/api/src/routes/events.ts
apps/api/tests/broker/vevent.test.ts
apps/api/tests/broker/outboxWorker.test.ts
assembleRruleString: COUNT takes precedence over UNTIL (mutual exclusion, RFC 5545 §3.3.10)
Timed UNTIL serializes as YYYYMMDDTHHMMSSZ (end-of-UTC-day T235959Z) per RESEARCH Pitfall 2
hasExplicitRecurrence check gates assembleRruleString; recurrence:'none' explicitly yields undefined (no RRULE)
Series-edit bound-only change: regex strips existing UNTIL/COUNT from preserved RRULE before re-applying new bound
duration_minutes completed_date tasks_completed files_modified
8 2026-06-10 2 4

Phase 06 Plan 02: RRULE UNTIL/COUNT Serialization + FREQ Persistence Summary

One-liner: RRULE UNTIL/COUNT serialization with value-type-matching (DATE vs DATETIME UTC) via assembleRruleString, wired into both create + update outbox branches, with a FREQ=DAILY regression lock.

Tasks Completed

# Name Commit Type
1 RED — failing tests for UNTIL/COUNT serialization + FREQ-persistence regression a59455a test
2 GREEN — assembleRruleString + Zod schema acceptance, wired into the write path d2abb91 feat

What Was Built

Task 1: RED

Added failing tests to two files:

vevent.test.ts — three new serialization assertions confirming ical.js 2.2.1 handles UNTIL/COUNT correctly via the existing ICAL.Recur.fromString path:

  • FREQ=WEEKLY;COUNT=5RRULE:FREQ=WEEKLY;COUNT=5
  • FREQ=DAILY;UNTIL=20260630 (all-day) → contains RRULE:FREQ=DAILY;UNTIL=20260630, does NOT contain T235959Z
  • FREQ=WEEKLY;UNTIL=20260630T235959Z (timed) → RRULE:FREQ=WEEKLY;UNTIL=20260630T235959Z

outboxWorker.test.ts — two new describe blocks:

  • assembleRruleString (D-06): 6 cases covering COUNT wins, UNTIL DATE/DATETIME, COUNT-wins-over-UNTIL mutual exclusion, base preset unchanged
  • FREQ persistence (D-07 regression): 1 case asserting daily-recurrence payload emits RRULE:FREQ=DAILY

RED confirmed: assembleRruleString is not a function (6 failing tests).

Task 2: GREEN

apps/api/src/broker/outboxWorker.ts:

  • Added recurrenceUntil: z.string().max(10).optional() and recurrenceCount: z.number().int().min(1).optional() to outboxPayloadSchema (T-06-02 mitigations)
  • Implemented and exported assembleRruleString(basePreset, until?, count?, allDay?) with JSDoc (D-06)
  • Wired assembleRruleString into both create and update dispatch branches
  • Fixed precedence: hasExplicitRecurrence checked first (covers recurrence:'none' → explicitly yields undefined); preservedRrule only used when no explicit recurrence
  • Series-edit Pitfall 3: when a bound-only change applies to a preserved RRULE, strips UNTIL/COUNT via /;(UNTIL|COUNT)=[^;]*/g before re-applying

apps/api/src/routes/events.ts:

  • Added recurrenceUntil: z.string().max(10).optional() and recurrenceCount: z.number().int().min(1).optional() to eventFieldsSchema

All 39 tests pass. The previously passing CR-01 (recurrence:'none' wins over _preservedRrule) was initially broken by the change and auto-fixed (Rule 1 bug: logic precedence error).

TDD Gate Compliance

Gate Status
RED commit (test(06-02):) a59455a — exists, confirmed failing
GREEN commit (feat(06-02):) d2abb91 — follows RED commit
Commit order test(06-02) precedes feat(06-02) — verified via git log

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Fixed hasExplicitRecurrence precedence for recurrence:'none'

  • Found during: Task 2 (GREEN)
  • Issue: Initial implementation used if (hasExplicitRecurrence && rruleFromPayload) — when recurrence:'none', rruleFromPayload is undefined, so the condition was false, incorrectly falling through to else if (preservedRrule) and emitting an RRULE even though the user explicitly selected 'none'. Broke existing CR-01: explicit recurrence:'none' wins test.
  • Fix: Changed to if (hasExplicitRecurrence) with an inner ternary: if rruleFromPayload is truthy, assemble with bound; otherwise undefined. Applied identically to both create and update branches.
  • Files modified: apps/api/src/broker/outboxWorker.ts
  • Commit: d2abb91 (folded into GREEN commit)

Verification Evidence

cd apps/api && pnpm vitest run tests/broker/vevent.test.ts tests/broker/outboxWorker.test.ts
Test Files  2 passed (2)
Tests  39 passed (39)
grep -n "recurrenceUntil" apps/api/src/routes/events.ts apps/api/src/broker/outboxWorker.ts
events.ts:111:  recurrenceUntil: z.string().max(10).optional()
outboxWorker.ts:84:  recurrenceUntil: z.string().max(10).optional()

Git log confirms test(06-02) precedes feat(06-02).

Known Stubs

None. All test assertions target exact ICS/RRULE strings verified against ical.js 2.2.1 in RESEARCH. No placeholder data.

Threat Flags

No new threat surface beyond what was planned in T-06-02 / T-06-02b. Both mitigations implemented:

  • z.string().max(10) on recurrenceUntil + z.number().int().min(1) on recurrenceCount at both route and outbox schema boundaries.
  • assembleRruleString uses .replace(/-/g,'') (digits only) + fixed templates — no raw passthrough to ICS.
  • Assembled string passes through ICAL.Recur.fromString (parse-rejects malformed RRULE).

Self-Check: PASSED

Item Status
SUMMARY.md created FOUND
RED commit a59455a FOUND
GREEN commit d2abb91 FOUND
39 tests passing CONFIRMED
recurrenceUntil in both schemas CONFIRMED