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