147 lines
9.6 KiB
Markdown
147 lines
9.6 KiB
Markdown
---
|
||
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 ~107–133) 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>
|