--- phase: "06-ux-polish" plan: "03" subsystem: "api/broker" tags: ["tdd", "expand", "hasRrule", "ical", "recurrence", "d-08", "d-06"] dependency_graph: requires: - "apps/api/src/broker/expand.ts (CalendarOccurrence interface)" - "apps/api/tests/fixtures/*.ics (existing fixtures)" provides: - "CalendarOccurrence.hasRrule: boolean (server source-of-truth)" - "weekly-count3.ics test fixture (bounded RRULE, COUNT=3)" - "expand.test.ts hasRrule + bounded-RRULE assertions" affects: - "apps/api/src/broker/expand.ts (CalendarOccurrence consumers — routes/events.ts)" - "apps/pwa/src/api/client.ts (mirror field added in Plan 05)" tech_stack: added: [] patterns: - "TDD RED→GREEN: test-only commit followed by implementation commit" - "Capture event.isRecurring() once before branch, pass to both push sites" - "epochMilliseconds (not epochSeconds) for Temporal duration arithmetic with temporal-polyfill" key_files: created: - "apps/api/tests/fixtures/weekly-count3.ics" modified: - "apps/api/src/broker/expand.ts" - "apps/api/tests/broker/expand.test.ts" decisions: - "D-08: hasRrule derived from event.isRecurring() — no DB query change needed (already available on the parsed ICAL.Event)" - "Captured isRecurring once before the non-recurring/recurring branch (single capture pattern from PATTERNS.md)" - "epochMilliseconds used for Temporal duration math — temporal-polyfill returns number not BigInt for this property" - "weekly-count3.ics uses UTC DTSTART/DTEND (no VTIMEZONE needed) for simplicity in the bounded test fixture" metrics: duration: "11m" completed: "2026-06-10" tasks_completed: 2 files_modified: 3 --- # Phase 06 Plan 03: hasRrule Server-Side Exposure Summary Added `hasRrule: boolean` to the `CalendarOccurrence` interface in `expand.ts` and populated it via `event.isRecurring()` — the server-side signal that gates the whole-series-edit confirmation prompt (D-08). ## Tasks Completed | # | Task | Type | Commit | Outcome | |---|------|------|--------|---------| | 1 | RED — failing tests for hasRrule + bounded expansion | TDD test | 593302e | 3 hasRrule failures + duration test confirmed red | | 2 | GREEN — add hasRrule to interface and populate it | TDD impl | 44d336c | 10/10 expand tests pass | ## What Was Built ### `apps/api/src/broker/expand.ts` Added `hasRrule: boolean` field to `CalendarOccurrence` interface with JSDoc. Captured `const isRecurring = event.isRecurring()` once before the non-recurring/recurring branch. Both `occurrences.push({...})` sites now include `hasRrule: isRecurring` — false in the non-recurring branch, true in the recurring branch. ### `apps/api/tests/broker/expand.test.ts` Added two new `describe` blocks: **`hasRrule field — D-08`** (2 tests): - `weekly-dst.ics` (recurring): all occurrences have `hasRrule === true` - `single-duration.ics` (non-recurring): the single occurrence has `hasRrule === false` **`Bounded RRULE (COUNT=3) — D-06 invariant`** (3 tests): - `COUNT=3` within a 6-month window returns exactly 3 occurrences - Each bounded occurrence duration = 1 hour from DTSTART→DTEND (not recurrence span) - Bounded occurrences have `hasRrule === true` ### `apps/api/tests/fixtures/weekly-count3.ics` New fixture: `FREQ=WEEKLY;COUNT=3`, `DTSTART:20260601T090000Z`, `DTEND:20260601T100000Z` (1-hour UTC events). Used for the bounded expansion invariant test. ## TDD Gate Compliance - RED commit (`test(06-03): ...`) at `593302e` — tests written first, confirmed failing due to absent `hasRrule` field - GREEN commit (`feat(06-03): ...`) at `44d336c` — implementation added, all 10 tests pass ## Deviations from Plan **1. [Rule 1 - Bug] Duration test using `epochMilliseconds` instead of `epochSeconds`** - **Found during:** Task 1 test writing - **Issue:** `Temporal.ZonedDateTime.epochSeconds` returns `NaN` in the `temporal-polyfill` package used in the test suite; `epochMilliseconds` returns a regular `number` - **Fix:** Duration assertion uses `endZdt.epochMilliseconds - startZdt.epochMilliseconds` and compares to `3_600_000` (1 hour in ms) - **Files modified:** `apps/api/tests/broker/expand.test.ts` - **Commit:** 593302e (incorporated into RED commit before final form) No other deviations. Plan executed as written. ## Verification ``` npx vitest run tests/broker/expand.test.ts Test Files 1 passed (1) Tests 10 passed (10) ``` ``` grep -n "hasRrule" apps/api/src/broker/expand.ts 68: hasRrule: boolean 224: // Capture once — used in both branches to populate hasRrule. 261: hasRrule: isRecurring, // always false in the non-recurring branch 308: hasRrule: isRecurring, // always true in the recurring branch ``` `client.ts` untouched — mirror field is Plan 05's responsibility. ## Known Stubs None. `hasRrule` is fully populated from `event.isRecurring()` — no placeholder values. ## Threat Flags No new threat surface. `hasRrule` is a boolean derived from data already returned to the authenticated caller. T-06-03 accepted in plan threat model. ## Self-Check: PASSED - `apps/api/src/broker/expand.ts` exists and contains `hasRrule` - `apps/api/tests/fixtures/weekly-count3.ics` exists - `apps/api/tests/broker/expand.test.ts` contains `hasRrule` assertions - Commits 593302e and 44d336c exist in git log