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

147 lines
9.6 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: 01
type: tdd
wave: 1
depends_on: []
files_modified:
- apps/pwa/src/lib/eventDateTime.ts
- apps/pwa/src/lib/eventDateTime.test.ts
autonomous: true
requirements: []
must_haves:
truths:
- "On any start change, the event end preserves its current duration (D-04)"
- "The end never strands behind the start day — at worst it snaps to the same day/+1h (D-04 floor)"
artifacts:
- path: "apps/pwa/src/lib/eventDateTime.ts"
provides: "computeNewTimedEnd + computeNewAllDayEnd pure end-tracking helpers"
contains: "computeNewTimedEnd"
- path: "apps/pwa/src/lib/eventDateTime.test.ts"
provides: "RED-then-GREEN unit coverage for duration preservation + floor rule"
contains: "computeNewTimedEnd (D-04"
key_links:
- from: "apps/pwa/src/lib/eventDateTime.ts"
to: "apps/pwa/src/lib/eventDateTime.test.ts"
via: "vitest unit assertions"
pattern: "computeNewTimedEnd|computeNewAllDayEnd"
---
<objective>
Deliver the duration-preservation math for event end-tracking (D-04) as two pure, fully-tested functions in `apps/pwa/src/lib/eventDateTime.ts`. These are the load-bearing logic behind the 999.7 fix: when a user moves an event's start, the end must follow so the event keeps its duration instead of stranding behind the start and producing absurd multi-month spans.
This plan ships the math ONLY (TDD: tests first). Wiring these helpers into the `EventForm` start `onChange` handlers happens in the EventForm integration slice (Plan 06), which depends on this plan's exported symbols.
Purpose: End-tracking math is deterministic input→output logic — the canonical TDD candidate. Isolating it from the React component keeps the cycle fast and the behavior verifiable without rendering.
Output: `computeNewTimedEnd` and `computeNewAllDayEnd` exported from `eventDateTime.ts`, green under `pnpm --filter @familysync/pwa test`.
</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
@.planning/phases/06-ux-polish/06-UI-SPEC.md
</context>
<artifacts_this_plan_produces>
NEW symbols introduced here (exclude from any drift/convergence check — they did not exist before this phase):
- `computeNewTimedEnd(newStartDate, newStartTime, oldStartDate, oldStartTime, oldEndDate, oldEndTime): { endDate, endTime }` in `apps/pwa/src/lib/eventDateTime.ts`
- `computeNewAllDayEnd(newStartDate, oldStartDate, oldEndDate): string` in `apps/pwa/src/lib/eventDateTime.ts`
- Any private date helpers these need (e.g. `dateDiffDays`, `addDaysISO`, `localDateISO`, `localTimeHHMM`) — add only if not already present in the file; reuse existing local-accessor helpers where they exist.
</artifacts_this_plan_produces>
<tasks>
<task type="tdd" tdd="true">
<name>Task 1: RED — failing tests for computeNewTimedEnd / computeNewAllDayEnd</name>
<files>apps/pwa/src/lib/eventDateTime.test.ts</files>
<read_first>
- apps/pwa/src/lib/eventDateTime.test.ts — copy the existing `import { describe, it, expect } from 'vitest'` header and `describe/it/expect` structure (the existing `serializeEventDateTime` block is the exact analog)
- apps/pwa/src/lib/eventDateTime.ts — existing `serializeEventDateTime`, `localWallClockToUtcIso`, and `parseDateTime` local-accessor pattern (RESEARCH §Focus 4; PATTERNS §eventDateTime.ts)
- .planning/phases/06-ux-polish/06-RESEARCH.md §"End-tracking pure functions (TDD target)" — exact signatures + the delta/floor algorithm
- .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 4" — the behavior contract (timed: newEnd = newStart + (oldEnd oldStart); floor snaps to +1h timed / same-day all-day)
</read_first>
<behavior>
- computeNewTimedEnd preserves a 1-hour delta: old 09:00→10:00 on a day, new start moved forward → new end is exactly 1h after new start, same date when within the day.
- computeNewTimedEnd preserves a multi-day timed delta (e.g. 26h) when start moves.
- computeNewTimedEnd floor rule: when oldEnd <= oldStart (already-invalid stale state), new end snaps to newStart + 1h (never behind start).
- computeNewAllDayEnd preserves a 0-day span (single-day all-day event) → new inclusive end equals new start date.
- computeNewAllDayEnd preserves a 3-day span → new inclusive end is newStart + 3 days.
- computeNewAllDayEnd floor rule: when oldEnd < oldStart, new end snaps to new start (same day).
</behavior>
<action>
Add a new `describe('computeNewTimedEnd (D-04 — end-tracking)', ...)` block and a `describe('computeNewAllDayEnd (D-04 — all-day end-tracking)', ...)` block alongside the existing tests. Import the two not-yet-existing functions from `./eventDateTime.js`. Write the six cases listed in the behavior block, asserting on returned `endDate` ('YYYY-MM-DD') and `endTime` ('HH:MM') strings. Use concrete dates (e.g. start 2026-06-11) so assertions are exact. Run the suite and CONFIRM RED — the import resolves to undefined and tests fail with a missing-export / call-of-undefined error (NOT a syntax/import-path error). Per CLAUDE.md WR-05: assertions must reflect LOCAL wall-clock dates, never UTC-sliced dates. Commit: `test(06-01): add failing tests for end-tracking duration math`.
</action>
<verify>
<automated>cd apps/pwa && pnpm test -- run lib/eventDateTime 2>&1 | grep -E "computeNewTimedEnd|computeNewAllDayEnd" && echo "RED block present"</automated>
</verify>
<acceptance_criteria>
- eventDateTime.test.ts contains both new `describe` blocks with the six named cases.
- Running the suite shows the new tests FAILING (RED) due to the missing exports, not due to an import-path typo.
- A `test(06-01): ...` commit exists.
</acceptance_criteria>
<done>Six new failing tests describe duration preservation + floor behavior for timed and all-day; RED confirmed and committed.</done>
</task>
<task type="tdd" tdd="true">
<name>Task 2: GREEN — implement the two end-tracking helpers</name>
<files>apps/pwa/src/lib/eventDateTime.ts</files>
<read_first>
- apps/pwa/src/lib/eventDateTime.ts — `serializeEventDateTime` export style + `parseDateTime` (lines ~107133) local-accessor pattern (getFullYear/getMonth/getDate/getHours/getMinutes) — WR-05 constraint
- .planning/phases/06-ux-polish/06-PATTERNS.md §"apps/pwa/src/lib/eventDateTime.ts" — the exact function bodies to mirror, including the 1h floor and `Math.max(0, dateDiffDays(...))` span
- .planning/phases/06-ux-polish/06-RESEARCH.md §"End-tracking pure functions" — algorithm and the `60 * 60 * 1000` floor constant
</read_first>
<action>
Implement `computeNewTimedEnd` and `computeNewAllDayEnd` exactly per the signatures in RESEARCH §"End-tracking pure functions (TDD target)". Timed: compute `oldStartMs`/`oldEndMs` via `new Date(\`${date}T${time}:00\`).getTime()`, set `deltaMs = oldEndMs > oldStartMs ? oldEndMs - oldStartMs : 60*60*1000` (the 1h floor), then `newEnd = new Date(newStartMs + deltaMs)` and return `{ endDate, endTime }` formatted through LOCAL accessors (do NOT use `toISOString().slice(0,10)` — that returns UTC date; this is the WR-05 trap called out in PATTERNS). All-day: `span = Math.max(0, dateDiffDays(oldStartDate, oldEndDate))`, return `addDaysISO(newStartDate, span)` — the `Math.max(0, …)` is the floor (a stale negative span collapses to same-day). If `dateDiffDays`/`addDaysISO`/`localDateISO`/`localTimeHHMM` helpers are not already in the file, add them as small private functions using local Date accessors. Run the suite to GREEN. Commit: `feat(06-01): implement duration-preserving end-tracking helpers`.
</action>
<verify>
<automated>cd apps/pwa && pnpm test -- run lib/eventDateTime</automated>
</verify>
<acceptance_criteria>
- `computeNewTimedEnd` and `computeNewAllDayEnd` are exported from eventDateTime.ts.
- All six new tests pass; the pre-existing eventDateTime tests still pass (no regression).
- Date formatting uses local accessors only (grep: no `toISOString().slice` in the new helpers).
- A `feat(06-01): ...` commit exists after the `test(06-01): ...` commit (RED→GREEN order).
</acceptance_criteria>
<done>Both helpers implemented; full eventDateTime suite green; RED→GREEN commit order present.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| (none new) | Pure client-side date arithmetic on already-trusted local form state. No network, no untrusted input, no persistence. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-01 | Tampering | computeNewTimedEnd / computeNewAllDayEnd | accept | Pure functions over local strings; no trust boundary crossed. Output is re-validated downstream by the existing serialize/write path (vevent.ts WR-04). UI/logic only — no new attack surface. |
</threat_model>
<verification>
- `cd apps/pwa && pnpm test -- run lib/eventDateTime` is green.
- Git log shows `test(06-01)` before `feat(06-01)`.
- No change to any file outside `eventDateTime.ts` / `eventDateTime.test.ts`.
</verification>
<success_criteria>
- D-04 duration-preservation and floor rules are encoded as passing unit tests.
- Two exported helpers are available for Plan 06 to wire into EventForm.
- No EventForm or write-path file touched (clean ownership for parallel Wave 1).
</success_criteria>
<output>
Create `.planning/phases/06-ux-polish/06-01-SUMMARY.md` when done (RED/GREEN/REFACTOR notes + commit list).
</output>