docs(06): create ux-polish phase plan (6 plans, 2 waves)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d52acad54c
commit
456121969f
@@ -0,0 +1,143 @@
|
||||
---
|
||||
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 37–67), the two `occurrences.push({...})` sites (non-recurring ~241–256, recurring ~287–302), 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 (37–67) 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>
|
||||
Reference in New Issue
Block a user