docs(04): create phase plan — 6 plans across 5 waves for shared lists + live sync
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d89eb47483
commit
38fa6f448b
@@ -0,0 +1,260 @@
|
||||
---
|
||||
phase: 04-shared-lists-live-sync
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/pwa/package.json
|
||||
- apps/api/package.json
|
||||
- apps/api/src/db/schema.ts
|
||||
- apps/api/src/db/migrations/0002_lists_schema.sql
|
||||
- apps/api/test/setup.ts
|
||||
- apps/api/vitest.config.ts
|
||||
- apps/api/src/routes/lists.test.ts
|
||||
- apps/api/src/lib/listEmitter.test.ts
|
||||
- apps/pwa/src/hooks/useListSSE.test.ts
|
||||
- apps/pwa/src/routes/ListDetail.test.tsx
|
||||
- apps/pwa/src/App.tsx
|
||||
- apps/pwa/src/components/BottomTabBar.tsx
|
||||
- apps/pwa/src/routes/ListsIndex.tsx
|
||||
- apps/pwa/src/store/listsStore.ts
|
||||
autonomous: false
|
||||
requirements: [LIST-01, LIST-02, LIST-03, LIST-04]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can tap a 'Lists' tab in a bottom tab bar and land on a /lists route"
|
||||
- "The /lists route renders an empty state when no lists exist"
|
||||
- "The new lists/list_items/list_shares tables exist in MariaDB after migration"
|
||||
- "API test harness runs and the Phase 4 RED test stubs execute (failing, not erroring on import)"
|
||||
artifacts:
|
||||
- path: "apps/api/src/db/schema.ts"
|
||||
provides: "lists, listShares, listItems Drizzle tables"
|
||||
contains: "export const lists"
|
||||
- path: "apps/api/src/db/migrations/0002_lists_schema.sql"
|
||||
provides: "additive CREATE TABLE migration for the three list tables"
|
||||
contains: "CREATE TABLE"
|
||||
- path: "apps/pwa/src/components/BottomTabBar.tsx"
|
||||
provides: "Calendar | Lists bottom tab navigation"
|
||||
min_lines: 25
|
||||
- path: "apps/pwa/src/routes/ListsIndex.tsx"
|
||||
provides: "Lists surface with empty state"
|
||||
min_lines: 20
|
||||
- path: "apps/api/src/routes/lists.test.ts"
|
||||
provides: "RED test stubs for LIST-01/02/03/04 API behavior"
|
||||
contains: "describe"
|
||||
key_links:
|
||||
- from: "apps/pwa/src/App.tsx"
|
||||
to: "/lists"
|
||||
via: "react-router Route + BottomTabBar NavLink"
|
||||
pattern: "lists"
|
||||
- from: "apps/api/src/db/schema.ts"
|
||||
to: "MariaDB"
|
||||
via: "drizzle-kit generate + migrate"
|
||||
pattern: "mysqlTable\\('lists'"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Establish the Phase 4 foundation as a thin, runnable end-to-end shell: install the four new npm dependencies, add the three list tables to the Drizzle schema and apply them via a generated migration, scaffold the API test harness with the Phase 4 Wave-0 RED test stubs, and add react-router + a bottom tab bar so the user can navigate to a (currently empty) Lists surface.
|
||||
|
||||
This is the MVP first slice: after this plan a real user can tap "Lists" and see the Lists surface render (empty state). No list data yet — later slices fill it in. Wave 0 test stubs are created here so every downstream task has an `<automated>` target per 04-VALIDATION.md.
|
||||
|
||||
Purpose: De-risk the transport/routing/schema/test plumbing before any list feature is built, and satisfy the [BLOCKING] generate+migrate schema constraint once for all later DB-dependent work.
|
||||
Output: New deps installed; three tables migrated; API vitest harness + 4 RED stub test files; router + BottomTabBar + ListsIndex empty state; listsStore (UI-only).
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** household member, **I want to** create and manage shared named lists with real-time co-edit sync, **so that** my partner and I see each other's list edits appear within seconds without refreshing. (This plan delivers the navigable shell; later plans fill in CRUD, reorder, and live sync.)
|
||||
</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/04-shared-lists-live-sync/04-CONTEXT.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-RESEARCH.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-PATTERNS.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-VALIDATION.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 1: Package legitimacy gate for the SUS-flagged react-router</name>
|
||||
<files>apps/pwa/package.json</files>
|
||||
<read_first>
|
||||
- .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Package Legitimacy Audit"
|
||||
</read_first>
|
||||
<what-built>Nothing yet — this gate precedes the install in Task 2.</what-built>
|
||||
<action>
|
||||
Per the Package Legitimacy Audit, three packages (@dnd-kit/core, @dnd-kit/sortable, fractional-indexing) are verdict OK and auto-approved. `react-router` is flagged SUS only because version 7.17.0 was published 2026-06-04 (version-recency false positive); the package is the canonical React Router (remix-run, ~12 yrs, 47.5M/wk). Surface this to the operator for a one-time confirm before installing, since legitimacy checkpoints are never auto-approvable.
|
||||
</action>
|
||||
<how-to-verify>
|
||||
1. Open https://www.npmjs.com/package/react-router and confirm publisher is `remix-run`/`react-router` org with multi-year history and ~47M weekly downloads.
|
||||
2. Confirm version 7.x is the current major.
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- Operator types "approved" (or names a pinned version) before Task 2 runs.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" to proceed with the install, or specify an alternate version.</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Install new dependencies + scaffold API test harness with Wave-0 RED stubs</name>
|
||||
<files>apps/pwa/package.json, apps/api/package.json, apps/api/test/setup.ts, apps/api/vitest.config.ts, apps/api/src/routes/lists.test.ts, apps/api/src/lib/listEmitter.test.ts, apps/pwa/src/hooks/useListSSE.test.ts, apps/pwa/src/routes/ListDetail.test.tsx</files>
|
||||
<read_first>
|
||||
- apps/api/vitest.config.ts
|
||||
- apps/api/src/db/client.ts
|
||||
- apps/pwa/src/api/client.test.ts (existing PWA test convention)
|
||||
- .planning/phases/04-shared-lists-live-sync/04-VALIDATION.md §"Wave 0 Requirements" and §"Per-Task Verification Map"
|
||||
- .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Standard Stack" → "New Dependencies"
|
||||
</read_first>
|
||||
<action>
|
||||
Install PWA deps: react-router@7, @dnd-kit/core, @dnd-kit/sortable, fractional-indexing via `pnpm --filter @familysync/pwa add`. Install API dep fractional-indexing via `pnpm --filter @familysync/api add` (needed server-side for rank generation). Do NOT install ioredis — fan-out is in-memory EventEmitter per RESEARCH discretion (justified in Plan 02).
|
||||
|
||||
Scaffold the API test harness: the API currently has zero test files. Create `apps/api/test/setup.ts` and reference it from `apps/api/vitest.config.ts` (`test.setupFiles`). The setup file must establish how DB-backed route tests connect — point at the local MariaDB via the existing `apps/api/src/db/client.ts` pool (DB_HOST/DB_NAME from env), and provide a per-test cleanup (truncate lists/list_items/list_shares between tests). Pure-logic tests (listEmitter, fractional rank) do NOT need the DB.
|
||||
|
||||
Create the four Wave-0 RED stub test files listed in 04-VALIDATION.md, each with `describe`/`it.todo` or `it(... )` blocks that compile and FAIL (red) rather than error on import — they import the not-yet-existing modules behind a guard or use `it.todo` placeholders that downstream plans convert to real assertions:
|
||||
- apps/api/src/routes/lists.test.ts — LIST-01/02/03/04 API behavior stubs
|
||||
- apps/api/src/lib/listEmitter.test.ts — scoped fan-out correctness (D-04) stubs
|
||||
- apps/pwa/src/hooks/useListSSE.test.ts — D-11 bounded backoff (mock EventSource) stubs
|
||||
- apps/pwa/src/routes/ListDetail.test.tsx — D-07 optimistic update + rollback stubs
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/api test 2>&1 | grep -Eiq 'todo|fail|no tests|passed' && pnpm --filter @familysync/pwa exec vitest run src/hooks/useListSSE.test.ts src/routes/ListDetail.test.tsx 2>&1 | grep -Eiq 'todo|fail|passed'</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `react-router`, `@dnd-kit/core`, `@dnd-kit/sortable`, `fractional-indexing` appear in apps/pwa/package.json dependencies.
|
||||
- `fractional-indexing` appears in apps/api/package.json dependencies.
|
||||
- `ioredis` is NOT added to either package.json.
|
||||
- `apps/api/test/setup.ts` exists and is referenced by `setupFiles` in apps/api/vitest.config.ts.
|
||||
- All four Wave-0 test files exist and run (todo/red), not import-error.
|
||||
</acceptance_criteria>
|
||||
<done>New deps installed (no ioredis), API test harness runs, four RED/todo stub files present and executing.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add list tables to schema and apply via generate+migrate [BLOCKING]</name>
|
||||
<files>apps/api/src/db/schema.ts, apps/api/src/db/migrations/0002_lists_schema.sql</files>
|
||||
<read_first>
|
||||
- apps/api/src/db/schema.ts (full file — table conventions)
|
||||
- apps/api/src/db/migrations/0001_calendars_user_url_unique.sql (prior migration shape)
|
||||
- apps/api/drizzle.config.ts
|
||||
- .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Database Schema Design"
|
||||
- .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"apps/api/src/db/schema.ts"
|
||||
- $HOME/.claude/projects/-home-luc-Projects-familysync/memory/drizzle-mariadb-push-unsafe.md
|
||||
</read_first>
|
||||
<action>
|
||||
Append three tables to apps/api/src/db/schema.ts following the exact conventions in 04-RESEARCH §Database Schema Design and the analog patterns in 04-PATTERNS:
|
||||
- `lists`: int autoincrement PK, `ownerId` int('owner_id') references users.id onDelete cascade notNull, `name` varchar(255) notNull, `isShared` boolean('is_shared') default true notNull (D-01), `createdAt` timestamp defaultNow notNull, `updatedAt` timestamp defaultNow onUpdateNow; index idx_lists_owner_id on ownerId.
|
||||
- `listShares` (D-02, member-count-agnostic join table): int PK, `listId` int('list_id') references lists.id onDelete cascade notNull, `userId` int('user_id') references users.id onDelete cascade notNull, `createdAt` timestamp defaultNow notNull; unique('uniq_list_share') on (listId, userId), index idx_list_shares_user_id on userId.
|
||||
- `listItems`: int PK, `listId` int('list_id') references lists.id onDelete cascade notNull, `text` varchar(500) notNull, `checked` boolean default false notNull, `rank` varchar(255) notNull (D-13 fractional-indexing string), `createdAt`, `updatedAt`; index idx_list_items_list_id_rank on (listId, rank), index idx_list_items_list_id_checked on (listId, checked).
|
||||
|
||||
Then generate and apply the migration. This is [BLOCKING]: run `pnpm --filter @familysync/api db:generate` to produce `apps/api/src/db/migrations/0002_lists_schema.sql` (+ journal/snapshot). REVIEW the generated SQL — it MUST be additive (CREATE TABLE only, NO DROP/TRUNCATE of existing tables). Then run `pnpm --filter @familysync/api db:migrate` to apply. NEVER run `db:push` / `drizzle-kit push` — it emits a false destructive diff on populated MariaDB (hard project constraint). Build/type checks pass without the live migration, so this task is mandatory and must complete before any DB-dependent verification in later plans.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "mysqlTable('lists'" apps/api/src/db/schema.ts && grep -q "mysqlTable('list_shares'" apps/api/src/db/schema.ts && grep -q "mysqlTable('list_items'" apps/api/src/db/schema.ts && test -f apps/api/src/db/migrations/0002_lists_schema.sql && grep -iq 'CREATE TABLE' apps/api/src/db/migrations/0002_lists_schema.sql && ! grep -iE 'DROP TABLE `?(users|calendars|calendar_events|calendar_outbox|member_credentials)' apps/api/src/db/migrations/0002_lists_schema.sql && pnpm --filter @familysync/api typecheck</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Three tables present in schema.ts with the column/index/FK shapes above.
|
||||
- 0002_lists_schema.sql exists, contains CREATE TABLE for lists/list_items/list_shares, and contains NO DROP/TRUNCATE of any pre-existing table.
|
||||
- `db:migrate` applied successfully (migration recorded in drizzle journal).
|
||||
- `pnpm --filter @familysync/api typecheck` passes.
|
||||
</acceptance_criteria>
|
||||
<done>list/list_items/list_shares tables exist in MariaDB via additive generate+migrate; typecheck green; no push used.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Add react-router + BottomTabBar + empty ListsIndex shell</name>
|
||||
<files>apps/pwa/src/App.tsx, apps/pwa/src/components/BottomTabBar.tsx, apps/pwa/src/routes/ListsIndex.tsx, apps/pwa/src/store/listsStore.ts</files>
|
||||
<read_first>
|
||||
- apps/pwa/src/App.tsx (current one-liner)
|
||||
- apps/pwa/src/components/CalendarShell.tsx (state-branch + data-fetch conventions)
|
||||
- apps/pwa/src/components/AppNav.tsx (nav/active-state + CSS token conventions)
|
||||
- apps/pwa/src/store/calendarStore.ts (Zustand shape convention)
|
||||
- apps/pwa/vite.config.ts (confirm navigateFallback already covers /lists/*)
|
||||
- .planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md §"Layout: App Shell Changes", §"BottomTabBar", §"ListsIndex", §"ListsEmptyState"
|
||||
- .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"apps/pwa/src/App.tsx", §"BottomTabBar.tsx", §"ListsIndex.tsx", §"listsStore.ts"
|
||||
</read_first>
|
||||
<action>
|
||||
Transform App.tsx into a BrowserRouter shell (react-router declarative mode, NO data router/loaders): routes `/` → Navigate replace to `/calendar`, `/calendar` → CalendarShell, `/lists` → ListsIndex, `/lists/:listId` → ListDetail. ListDetail does not exist yet — for this plan render a temporary placeholder route element (a stub component that says the list view is coming) so the route resolves; Plan 04 replaces it. Render BottomTabBar as a sibling of `<Routes>`.
|
||||
|
||||
Create BottomTabBar.tsx: fixed-bottom 56px + env(safe-area-inset-bottom), background var(--color-surface-dim), border-top var(--color-border), two equal NavLink tabs (CalendarDays→/calendar, List→/lists) with isActive callback applying accent var(--color-member-0) to icon+label and a 2px active indicator; inactive var(--color-text-muted); 13px label; ≥44px touch target; z-index 200. On desktop (≥768px) the existing AppNav sidebar remains; per UI-SPEC add a "Lists" NavLink there too (sidebar) — do this without breaking the existing AppNav signature.
|
||||
|
||||
Create ListsIndex.tsx: full-height scrollable column, "Lists" heading, useQuery(['lists'], fetchLists) where fetchLists is imported from a minimal listsClient (create only the fetchLists function + List type here if listsClient does not yet exist; Plan 03 expands it). Render ListsEmptyState ("No lists yet" / "Tap + to create your first shared list…") when there are zero lists; render a placeholder card stack otherwise. Wire isLoading/isError/success branches mirroring CalendarShell. Include the "+ New List" FAB affordance (non-functional placeholder is acceptable here; Plan 03 wires CreateListSheet).
|
||||
|
||||
Create listsStore.ts (Zustand, UI-only): activeTab and createListSheetOpen state with setters, following calendarStore conventions (no persist, no immer).
|
||||
|
||||
Confirm vite.config.ts navigateFallback ('/index.html') + denylist already cover SPA deep-links to /lists/* (it does per Phase 3 config) — if a denylist entry would block /lists, fix it; otherwise leave unchanged and note in SUMMARY.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/pwa exec vitest run src/components/CalendarShell.test.tsx 2>&1 | grep -Eiq 'passed' && pnpm --filter @familysync/pwa exec tsc --noEmit && grep -q "BrowserRouter" apps/pwa/src/App.tsx && grep -q "to=\"/lists\"" apps/pwa/src/components/BottomTabBar.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- App.tsx wraps the app in BrowserRouter with /calendar, /lists, /lists/:listId routes; existing CalendarShell still mounts at /calendar.
|
||||
- BottomTabBar renders Calendar and Lists NavLinks with active-state accent and ≥44px targets.
|
||||
- ListsIndex renders the empty state copy from UI-SPEC when no lists exist.
|
||||
- listsStore exports activeTab/createListSheetOpen with setters (no server data).
|
||||
- PWA typecheck passes; existing CalendarShell test still green.
|
||||
- Browser check (project convention): `playwright-cli` navigates to /lists and observes the "No lists yet" empty state and the bottom tab bar with an active "Lists" tab. Record the observation in SUMMARY.
|
||||
</acceptance_criteria>
|
||||
<done>User can tap the Lists tab and land on the empty Lists surface; calendar still works; router + tab bar in place.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser → /api/* | All list/SSE requests cross here; untrusted client input |
|
||||
| API → MariaDB | Drizzle parameterized queries only |
|
||||
| drizzle-kit → MariaDB (migration) | DDL applied to a populated production-shaped DB |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-01 | Tampering | drizzle-kit push truncating populated tables | mitigate | generate+migrate ONLY; verify 0002 SQL has no DROP/TRUNCATE of existing tables before applying (Task 3 gate) |
|
||||
| T-04-02 | Information Disclosure | scoped-fan-out leak (D-04) — foundational | mitigate | Schema models access via list_shares join table + owner_id (this plan); enforcement lands in Plans 02/03/06; negative test seeded in lists.test.ts here |
|
||||
| T-04-SC | Tampering | npm installs (react-router SUS, dnd-kit, fractional-indexing) | mitigate | Legitimacy audit in RESEARCH; blocking human checkpoint (Task 1) for the SUS react-router before install |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/api typecheck` and `pnpm --filter @familysync/pwa exec tsc --noEmit` both pass.
|
||||
- `pnpm --filter @familysync/api test` runs (Wave-0 stubs red/todo, not erroring).
|
||||
- 0002_lists_schema.sql is additive; migration applied; three tables queryable.
|
||||
- `playwright-cli` confirms /lists renders the empty state with the bottom tab bar.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- New deps installed (no ioredis); API test harness operational.
|
||||
- Three list tables migrated additively (no push).
|
||||
- Router + BottomTabBar live; Lists tab navigates to an empty Lists surface.
|
||||
- Four Wave-0 RED stub test files exist and execute.
|
||||
</success_criteria>
|
||||
|
||||
<artifacts_produced>
|
||||
**Symbols/files this plan creates (exclude from drift verification — they are new):**
|
||||
- Tables: `lists`, `list_shares`, `list_items` (apps/api/src/db/schema.ts)
|
||||
- Migration: `apps/api/src/db/migrations/0002_lists_schema.sql` (+ journal/snapshot)
|
||||
- API test harness: `apps/api/test/setup.ts`; setupFiles wiring in `apps/api/vitest.config.ts`
|
||||
- RED stub tests: `apps/api/src/routes/lists.test.ts`, `apps/api/src/lib/listEmitter.test.ts`, `apps/pwa/src/hooks/useListSSE.test.ts`, `apps/pwa/src/routes/ListDetail.test.tsx`
|
||||
- Components: `BottomTabBar` (apps/pwa/src/components/BottomTabBar.tsx), `ListsIndex` (apps/pwa/src/routes/ListsIndex.tsx), temporary ListDetail placeholder route element
|
||||
- Store: `useListsStore` (apps/pwa/src/store/listsStore.ts) with activeTab/createListSheetOpen
|
||||
- App.tsx now exports a BrowserRouter-wrapped App + AppShell
|
||||
- (Possibly) initial `apps/pwa/src/api/listsClient.ts` with `fetchLists` + `List` type
|
||||
</artifacts_produced>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/04-shared-lists-live-sync/04-01-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user