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

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 01 tdd 1
apps/pwa/src/lib/eventDateTime.ts
apps/pwa/src/lib/eventDateTime.test.ts
true
truths artifacts key_links
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)
path provides contains
apps/pwa/src/lib/eventDateTime.ts computeNewTimedEnd + computeNewAllDayEnd pure end-tracking helpers computeNewTimedEnd
path provides contains
apps/pwa/src/lib/eventDateTime.test.ts RED-then-GREEN unit coverage for duration preservation + floor rule computeNewTimedEnd (D-04
from to via pattern
apps/pwa/src/lib/eventDateTime.ts apps/pwa/src/lib/eventDateTime.test.ts vitest unit assertions computeNewTimedEnd|computeNewAllDayEnd
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.

<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 @.planning/phases/06-ux-polish/06-UI-SPEC.md

<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>
Task 1: RED — failing tests for computeNewTimedEnd / computeNewAllDayEnd apps/pwa/src/lib/eventDateTime.test.ts - 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) - 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). 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`. cd apps/pwa && pnpm test -- run lib/eventDateTime 2>&1 | grep -E "computeNewTimedEnd|computeNewAllDayEnd" && echo "RED block present" - 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. Six new failing tests describe duration preservation + floor behavior for timed and all-day; RED confirmed and committed. Task 2: GREEN — implement the two end-tracking helpers apps/pwa/src/lib/eventDateTime.ts - 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 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`. cd apps/pwa && pnpm test -- run lib/eventDateTime - `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). Both helpers implemented; full eventDateTime suite green; RED→GREEN commit order present.

<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>
- `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`.

<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>
Create `.planning/phases/06-ux-polish/06-01-SUMMARY.md` when done (RED/GREEN/REFACTOR notes + commit list).