Files
familysync/.planning/phases/06-ux-polish/06-03-PLAN.md
T

144 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 06-ux-polish
plan: 03
type: tdd
wave: 1
depends_on: []
files_modified:
- apps/api/src/broker/expand.ts
- apps/api/tests/broker/expand.test.ts
autonomous: true
requirements: []
must_haves:
truths:
- "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)"
artifacts:
- path: "apps/api/src/broker/expand.ts"
provides: "hasRrule:boolean field on CalendarOccurrence, populated from event.isRecurring()"
contains: "hasRrule"
- path: "apps/api/tests/broker/expand.test.ts"
provides: "hasRrule true/false assertions + bounded-RRULE occurrence-count assertion"
contains: "hasRrule"
key_links:
- from: "apps/api/src/broker/expand.ts"
to: "apps/pwa/src/api/client.ts (mirror, added in Plan 05)"
via: "CalendarOccurrence.hasRrule is the source-of-truth field the client mirrors"
pattern: "hasRrule"
---
<objective>
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`.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<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
</context>
<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>
<tasks>
<task type="tdd" tdd="true">
<name>Task 1: RED — failing tests for hasRrule population + bounded expansion count</name>
<files>apps/api/tests/broker/expand.test.ts</files>
<read_first>
- 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
</read_first>
<behavior>
- 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).
</behavior>
<action>
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`.
</action>
<verify>
<automated>cd apps/api && pnpm test -- run broker/expand 2>&1 | grep -E "hasRrule|COUNT" && echo "RED present"</automated>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>hasRrule + bounded-expansion tests written and RED; committed.</done>
</task>
<task type="tdd" tdd="true">
<name>Task 2: GREEN — add hasRrule to CalendarOccurrence and populate it</name>
<files>apps/api/src/broker/expand.ts</files>
<read_first>
- 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)
</read_first>
<action>
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`.
</action>
<verify>
<automated>cd apps/api && pnpm test -- run broker/expand</automated>
</verify>
<acceptance_criteria>
- `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).
</acceptance_criteria>
<done>hasRrule populated; bounded expansion verified; expand suite green; RED→GREEN order present.</done>
</task>
</tasks>
<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>
<verification>
- `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).
</verification>
<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>
<output>
Create `.planning/phases/06-ux-polish/06-03-SUMMARY.md` when done (RED/GREEN notes + commits).
</output>