---
phase: 02
slug: calendar-display
status: ready
nyquist_compliant: true
wave_0_complete: false
created: 2026-06-04
---

# Phase 02 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | vitest |
| **Config (API)** | `apps/api/vitest.config.ts` (environment: node — exists, Phase 1) |
| **Config (PWA)** | `apps/pwa/vitest.config.ts` (environment: jsdom — created in Plan 01 Task 1) |
| **Quick run command (API)** | `cd apps/api && pnpm test -- <test-file>` |
| **Quick run command (PWA)** | `cd apps/pwa && pnpm test -- <test-file>` |
| **Full suite command** | `pnpm -r test` (runs both workspaces) |
| **Type gate** | `pnpm exec tsc --noEmit` per workspace |
| **Estimated runtime** | ~25 seconds full suite (no live network; broker mocked) |

PWA harness (vitest + jsdom + @testing-library/react + @testing-library/jest-dom) is installed in **Plan 01 Task 1** — until that task completes, all PWA test rows are blocked on the harness (`❌ W0`).

---

## Sampling Rate

- **After every task commit:** Run the task's quick run command (the `<automated>` in that task)
- **After every plan wave:** Run `pnpm -r test`
- **Before `/gsd-verify-work`:** `pnpm -r test` green + `tsc --noEmit` clean in both workspaces
- **Max feedback latency:** 30 seconds

---

## Per-Task Verification Map

| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 02-01-01 | 01 | 1 | CAL-07 | T-02-SC | schema + RED stubs + fixtures (no behavior yet) | unit/grep | `grep -q has_rrule + ICAL.parse fixtures` (3 grep/node checks) | ✅ creates RED stubs | ⬜ pending |
| 02-01-02 | 01 | 1 | CAL-02 | T-02-01 | dev-bypass hard-disabled when NODE_ENV=production | unit | `cd apps/api && pnpm test -- tests/auth/devBypass.test.ts` | ❌ W0 (this task creates it) | ⬜ pending |
| 02-01-03 | 01 | 1 | CAL-07 | T-02-02 | live MariaDB has has_rrule + is_shared (no false-green) | integration | `node SHOW COLUMNS calendar_events/calendars` | N/A (DB assertion) | ⬜ pending |
| 02-02-01 | 02 | 2 | CAL-07 | T-02b-04 | DST wall-clock preserved; all-day no shift; EXDATE excluded | unit | `cd apps/api && pnpm test -- tests/broker/expand.test.ts` | ✅ RED stub from 02-01 | ⬜ pending |
| 02-02-02 | 02 | 2 | CAL-02, CAL-07 | T-02b-01/02 | start/end zod-validated before SQL; 90-day cap; color+isShared join | unit | `cd apps/api && pnpm test -- tests/routes/events.test.ts` | ✅ RED stub from 02-01 | ⬜ pending |
| 02-02-03 | 02 | 2 | CAL-02 | — | operator marks shared calendar (is_shared=1) | manual | human-verify checkpoint (see Manual-Only) | N/A | ⬜ pending |
| 02-03-01 | 03 | 2 | CAL-02 | T-02c-SC | token layer + Schedule-X var overrides; Temporal-first import | grep/node | `grep --color-shared-family + node require deps` | N/A (style/deps) | ⬜ pending |
| 02-03-02 | 03 | 2 | CAL-02 | — | firstDayOfWeek 0→7; per-member + 'shared' config | unit | `cd apps/pwa && pnpm test -- src/lib/colorUtils.test.ts src/lib/calendarConfig.test.ts` | ✅ RED stub (calendarConfig) from 02-01 | ⬜ pending |
| 02-03-03 | 03 | 2 | CAL-07 | T-02c-02 | all-day→PlainDate guard; calendarId routed by isShared/ownerUserId | unit | `cd apps/pwa && pnpm test -- src/lib/hydrateEvents.test.ts` | ✅ RED stub from 02-01 | ⬜ pending |
| 02-04-01 | 04 | 3 | CAL-02, CAL-03 | T-02d-01 | Schedule-X renders real windowed occurrences; 4 views; token-only | grep/type | `grep ScheduleXCalendar/hydrateEvents + tsc --noEmit` | N/A (wired in 04) | ⬜ pending |
| 02-04-02 | 04 | 3 | CAL-03 | T-02d-01 | render smoke: 4 views + timed+all-day through hydrate→eventsService | unit | `cd apps/pwa && pnpm test -- src/components/CalendarShell.test.tsx` | ❌ W0 (this task creates it) | ⬜ pending |
| 02-05-01 | 05 | 4 | CAL-03 | T-02e-01 | popover renders fields as text (no dangerouslySetInnerHTML); Escape closes | unit | `cd apps/pwa && pnpm test -- src/components/EventDetailPopover.test.tsx` | ❌ W0 (this task creates it) | ⬜ pending |
| 02-05-02 | 05 | 4 | CAL-02, CAL-03 | T-02e-02 | legend/nav/toolbar + skeleton/empty/error; EventProof removed | grep/type | `grep SkeletonCalendar/EmptyState + tsc --noEmit` | N/A (wired in 05) | ⬜ pending |
| 02-05-03 | 05 | 4 | CAL-02, CAL-03, CAL-07 | — | visual + functional verification of 4 success criteria | manual | human-verify checkpoint (see Manual-Only) | N/A | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

Tasks without a unit-test `<automated>` (02-01-01 grep/fixture, 02-01-03 DB, 02-03-01 grep/deps, 02-04-01 grep/type, 02-05-02 grep/type) each carry an automated grep/node/tsc check, and none of them appear in 3-consecutive sequence without a unit test between them: the expand/events/colorUtils/hydrateEvents/popover/smoke unit tests interleave every wave.

---

## Wave 0 Requirements

Wave 0 = Plan 01 Task 1, which creates the failing-but-present test stubs and fixtures the later waves turn green:

- [ ] `apps/api/tests/broker/expand.test.ts` — RED stub for `expandOccurrences` (CAL-07). Contract: weekly `America/New_York` RRULE across the March 2026 DST boundary keeps 10:00 local wall-clock on both sides; all-day birthday → `allDay:true` + `'YYYY-MM-DD'` start; EXDATE-excluded occurrence absent. Fails at import (`../../src/broker/expand.js` not yet built).
- [ ] `apps/api/tests/routes/events.test.ts` — RED stub for windowed `/api/events` (CAL-02). Contract: occurrences carry `color` + `isShared`; bad/oversized params → 400.
- [ ] `apps/pwa/src/lib/hydrateEvents.test.ts` — RED stub (CAL-07). Contract: all-day→`Temporal.PlainDate`; timed→`Temporal.ZonedDateTime`; **Schedule-X `calendarId` = `occ.isShared ? 'shared' : String(occ.ownerUserId)`** (shared occurrence → `'shared'`; personal occurrence → `String(ownerUserId)`).
- [ ] `apps/pwa/src/lib/calendarConfig.test.ts` — RED stub (CAL-02). Contract: `WEEK_START_DAY=0` → Schedule-X `firstDayOfWeek=7`.
- [ ] `apps/api/tests/fixtures/{weekly-dst,allday-birthday,exdate-series}.ics` — fixture corpus; `weekly-dst.ics` carries a full VTIMEZONE (STANDARD + DAYLIGHT) for the DST assertion.
- [ ] `apps/api/tests/auth/devBypass.test.ts` — created and made green within Plan 01 Task 2 (not a cross-wave RED stub).
- [ ] `apps/pwa/vitest.config.ts` + PWA test deps (vitest, @testing-library/react, @testing-library/jest-dom, jsdom) — installed in Plan 01 Task 1; without this the PWA RED stubs cannot run.
- [ ] `apps/pwa/src/components/CalendarShell.test.tsx` and `apps/pwa/src/components/EventDetailPopover.test.tsx` — created in-wave by Plan 04/05 (not Wave 0 stubs; depend on components built in the same plan).

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Designate the shared-family calendar (`calendars.is_shared=1`) | CAL-02 | Which calendar is "shared-family" cannot be derived deterministically from data (broker exposes "Calendar" + "USA Holidays"; members' calendars arrive under their own credential) — operator must designate it. A different identification rule (e.g. displayName-pattern matching) is a **plan revision, not a resume-from-checkpoint**. | Plan 02 Task 3: list calendars, `UPDATE calendars SET is_shared=1 WHERE id=<chosen>`, re-list, curl `/api/events` confirms isShared:true + color #F25C7A on marked rows. |
| DST boundary correctness in the rendered UI | CAL-07 | Automated expand.test.ts asserts the wall-clock contract, but visual confirmation that Schedule-X paints the occurrence at the right hour across March 2026 requires a human eye on the grid. | Plan 05 Task 3 step 4: find a recurring event, navigate across the March 2026 DST boundary, confirm time does not jump ±1 hour. |
| All-day event renders as a full-day banner with no date shift | CAL-07 | PlainDate guard is unit-tested, but the actual Schedule-X all-day banner placement (correct date, no off-by-one) is a render-path visual check. | Plan 05 Task 3 step 5: find an all-day event (birthday/holiday), confirm it appears as a full-day banner on the correct date, not a day early/late. |
| Color-coded ownership legible at a glance + legend decode | CAL-02 | "Reads at a glance" is a subjective slick-constraint judgment. | Plan 05 Task 3 steps 2–3: confirm each member's events render in their color, shared in rose, legend decodes ownership. |
| Phone bottom-sheet popover + agenda default view | CAL-03 | Responsive breakpoint behavior (D-05) needs a real phone-width render. | Plan 05 Task 3 step 7: resize to phone width, confirm default view is Agenda and popover is a bottom sheet. |

---

## Validation Sign-Off

- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references (expand, events, hydrateEvents, calendarConfig stubs + PWA harness)
- [x] No watch-mode flags (`vitest run` / `pnpm test -- <file>`, never `--watch`)
- [x] Feedback latency < 30s
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** approved 2026-06-04
