Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
6.9 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 |
|
|
|
|
|
|
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=5→RRULE:FREQ=WEEKLY;COUNT=5FREQ=DAILY;UNTIL=20260630(all-day) → containsRRULE:FREQ=DAILY;UNTIL=20260630, does NOT containT235959ZFREQ=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 unchangedFREQ persistence (D-07 regression): 1 case asserting daily-recurrence payload emitsRRULE: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()andrecurrenceCount: z.number().int().min(1).optional()tooutboxPayloadSchema(T-06-02 mitigations) - Implemented and exported
assembleRruleString(basePreset, until?, count?, allDay?)with JSDoc (D-06) - Wired
assembleRruleStringinto both create and update dispatch branches - Fixed precedence:
hasExplicitRecurrencechecked first (coversrecurrence:'none'→ explicitly yieldsundefined);preservedRruleonly used when no explicit recurrence - Series-edit Pitfall 3: when a bound-only change applies to a preserved RRULE, strips
UNTIL/COUNTvia/;(UNTIL|COUNT)=[^;]*/gbefore re-applying
apps/api/src/routes/events.ts:
- Added
recurrenceUntil: z.string().max(10).optional()andrecurrenceCount: z.number().int().min(1).optional()toeventFieldsSchema
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)— whenrecurrence:'none',rruleFromPayloadisundefined, so the condition wasfalse, incorrectly falling through toelse if (preservedRrule)and emitting an RRULE even though the user explicitly selected 'none'. Broke existingCR-01: explicit recurrence:'none' winstest. - Fix: Changed to
if (hasExplicitRecurrence)with an inner ternary: ifrruleFromPayloadis truthy, assemble with bound; otherwiseundefined. 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)onrecurrenceUntil+z.number().int().min(1)onrecurrenceCountat both route and outbox schema boundaries.assembleRruleStringuses.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 |