Files
familysync/.planning/milestones/v1.0-phases/06-ux-polish/06-03-PLAN.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
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.
2026-06-11 20:35:18 -04:00

9.6 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-ux-polish 03 tdd 1
apps/api/src/broker/expand.ts
apps/api/tests/broker/expand.test.ts
true
truths artifacts key_links
Each expanded occurrence exposes hasRrule, true for occurrences of a recurring series and false otherwise (D-08)
A bounded RRULE (e.g. COUNT=3) expands to exactly the bounded number of occurrences within a wide window, each with start→end duration (D-06 verify)
path provides contains
apps/api/src/broker/expand.ts hasRrule:boolean field on CalendarOccurrence, populated from event.isRecurring() hasRrule
path provides contains
apps/api/tests/broker/expand.test.ts hasRrule true/false assertions + bounded-RRULE occurrence-count assertion hasRrule
from to via pattern
apps/api/src/broker/expand.ts apps/pwa/src/api/client.ts (mirror, added in Plan 05) CalendarOccurrence.hasRrule is the source-of-truth field the client mirrors hasRrule
Expose `hasRrule` on the server-side `CalendarOccurrence` so the PWA can detect "this occurrence belongs to a recurring series" — the signal that gates the whole-series-edit confirmation prompt (D-08/D-09). Today the field is absent from the type on both sides, so the PWA cannot tell a recurring occurrence from a single event.

This plan owns the SERVER source-of-truth (expand.ts). The PWA mirror field on client.ts's CalendarOccurrence is added in Plan 05 (sole owner of client.ts); the prompt UI that consumes it is built in Plan 06. Per PATTERNS, both interfaces are hand-mirrored — expand.ts is authoritative.

This plan also locks D-06's expansion invariant: a bounded RRULE terminates at COUNT/UNTIL and each occurrence's duration derives from DTSTART→DTEND (not the recurrence span) — RESEARCH verified no expand.ts logic change is needed for bounding, so this is an assertion to prevent regression.

Purpose: hasRrule population is deterministic transform logic over a parsed VEVENT — a TDD candidate. Isolated from the PWA, it is verifiable purely against expandOccurrences. Output: hasRrule on CalendarOccurrence + population in expandOccurrences, green expand.test.ts.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-ux-polish/06-RESEARCH.md @.planning/phases/06-ux-polish/06-PATTERNS.md

<artifacts_this_plan_produces> NEW symbols introduced here (exclude from drift/convergence checks):

  • hasRrule: boolean field added to the CalendarOccurrence interface in apps/api/src/broker/expand.ts
  • New assertions in apps/api/tests/broker/expand.test.ts (hasRrule true on recurring, false on non-recurring, bounded-RRULE count) </artifacts_this_plan_produces>
Task 1: RED — failing tests for hasRrule population + bounded expansion count apps/api/tests/broker/expand.test.ts - apps/api/tests/broker/expand.test.ts — existing `expandOccurrences` tests + ICS fixtures (mirror the fixture-injection style for a recurring vs non-recurring VEVENT) - apps/api/src/broker/expand.ts — `CalendarOccurrence` interface (lines 3767), the two `occurrences.push({...})` sites (non-recurring ~241256, recurring ~287302), and `event.isRecurring()` usage (~line 223) - .planning/phases/06-ux-polish/06-RESEARCH.md §"Focus 3 — hasRrule" + §"Focus 1" (bounded expansion verified: COUNT=3 → 3 occurrences, complete=true) - .planning/phases/06-ux-polish/06-PATTERNS.md §"apps/api/tests/broker/expand.test.ts" — copy-ready assertion patterns - expandOccurrences on a recurring VEVENT (has RRULE) → every returned occurrence has `hasRrule === true`. - expandOccurrences on a non-recurring VEVENT (no RRULE) → the single occurrence has `hasRrule === false`. - expandOccurrences on a VEVENT with `FREQ=WEEKLY;COUNT=3` over a wide window → exactly 3 occurrences, and each occurrence's (end start) equals the DTSTART→DTEND duration (not the recurrence span). Add assertions to (or alongside) the existing recurring-expansion test: assert `occs[0].hasRrule === true` for a recurring fixture and `occs[0].hasRrule === false` for a non-recurring fixture. Add a `FREQ=WEEKLY;COUNT=3` fixture and assert `occs.length === 3` within a multi-month window plus a per-occurrence duration assertion. Run the suite and CONFIRM RED (the `hasRrule` property is absent → TypeScript/runtime undefined on the assertion). Commit: `test(06-03): add failing tests for hasRrule + bounded expansion`. cd apps/api && pnpm test -- run broker/expand 2>&1 | grep -E "hasRrule|COUNT" && echo "RED present" - expand.test.ts asserts hasRrule true (recurring) and false (non-recurring), plus the bounded-count case. - Tests FAIL because `hasRrule` is undefined (not because of fixture/import errors). - `test(06-03): ...` commit exists. hasRrule + bounded-expansion tests written and RED; committed. Task 2: GREEN — add hasRrule to CalendarOccurrence and populate it apps/api/src/broker/expand.ts - apps/api/src/broker/expand.ts — `CalendarOccurrence` interface (3767) and both `occurrences.push({...})` sites; `event.isRecurring()` already called near line 223 - .planning/phases/06-ux-polish/06-PATTERNS.md §"apps/api/src/broker/expand.ts" — capture `const isRecurring = event.isRecurring()` once before the branch; pass `hasRrule: isRecurring` into both push sites - .planning/phases/06-ux-polish/06-RESEARCH.md §"Pitfall 4" — update server type here; the client mirror is Plan 05's job (do NOT touch client.ts) Add `hasRrule: boolean` to the `CalendarOccurrence` interface in `expand.ts`. In `expandOccurrences`, capture `const isRecurring = event.isRecurring()` once before the recurring/non-recurring branch, then add `hasRrule: isRecurring` to each `occurrences.push({...})` (it is `false` in the non-recurring branch, `true` in the recurring branch — using the single captured value keeps them consistent). Do NOT change the DB query or `client.ts` (the client mirror is added atomically in Plan 05). Run the suite to GREEN. Commit: `feat(06-03): expose hasRrule on expanded occurrences`. cd apps/api && pnpm test -- run broker/expand - `CalendarOccurrence` in expand.ts includes `hasRrule: boolean`. - Both push sites set `hasRrule` from the single `isRecurring` capture. - All new + existing expand tests pass; bounded `COUNT=3` fixture yields exactly 3 occurrences. - `feat(06-03): ...` commit follows the `test(06-03): ...` commit (RED→GREEN). hasRrule populated; bounded expansion verified; expand suite green; RED→GREEN order present.

<threat_model>

Trust Boundaries

Boundary Description
(none new) Read-side transform of already-cached, already-trusted calendar data. No new input crosses a boundary; hasRrule is derived from a parsed VEVENT the server already holds.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-03 Information Disclosure hasRrule on CalendarOccurrence accept hasRrule is a boolean derived from data already returned to the authenticated, access-scoped caller (existing /api/events ownership filter unchanged). It reveals no new information beyond "this event recurs", which is already visible from rendered occurrences. No new trust boundary.

</threat_model>

- `cd apps/api && pnpm test -- run broker/expand` green. - `grep -n "hasRrule" apps/api/src/broker/expand.ts` shows the field on the interface and both push sites. - `client.ts` untouched by this plan (ownership belongs to Plan 05).

<success_criteria>

  • D-08: the recurring-series detection signal exists server-side.
  • D-06 expansion invariant (bounded count, start→end duration) is locked by test.
  • Clean file ownership: only expand.ts + its test touched. </success_criteria>
Create `.planning/phases/06-ux-polish/06-03-SUMMARY.md` when done (RED/GREEN notes + commits).