# Build plan — R0/R1 milestone ladder to work.bowden.works cutover

The execution plan rian asked for: from empty repo to feature-parity
cutover, in session-sized vertical slices, each ending **deployed to
the temp domain + tests green + a named review script rian runs**.
Produced from the 7-lens plan critique (2026-07-07, 68 improvements
adopted/adapted — the judgment-call deltas are in 07 #30–#33).

Companion docs: parity bar = 01-current-system/; target model = 02;
architecture = 04; migration mechanics = 05; tests = 06 (now tiered);
roadmap beyond cutover = 00.

---

## Operating principles

1. **Milestones are gates, not aspirations.** A milestone is done when
   its exit gate passes — the gate is a script or checklist, never
   "looks good." No milestone starts until its predecessor's gate is
   green (except the marked parallel tracks).
2. **Migrated real data before feature UI.** The transform scripts run
   from M2 onward, nightly; every screen is built and demoed against
   rian's full production history, not seed data. Perf and edge cases
   (blocked rows, baseline entries, the currency mix) surface where
   they live.
3. **The diff harness is the acceptance mechanism, not a cutover
   ritual.** Old-side extractor lands in M0; the new-side emitter
   grows per milestone, each slice adding its harness section and
   turning it green (entity inventories at M2, per-month entry
   aggregates at M3, invoices at M6, CC at M7).
4. **Demo-or-die cadence.** Every milestone ends with something rian
   can click on the temp domain. Solo side projects die in invisible
   months; this ladder makes progress visible at every rung.
5. **Single-writer rules for dangerous ground.** Any number of Claude
   sessions may run in parallel EXCEPT: exactly one session at a time
   owns (a) alembic migrations, (b) `services/money/`, (c) the
   migration transform scripts. Parallel-safe: frontend/design-system
   work, read-only pages, CC expenses, comments, docs.
6. **Old-app change control.** The current app is bugfix-only for the
   build window. Any change that does land updates the matching
   01-current-system spec + 06 test rows in the same session, and
   appends to a "parity-bar deltas since 2026-07-07" list in
   01-current-system/README.md.

## Day-0 owner checklist (do these now — each is a silent-stall risk)

| # | action | unblocks |
|---|---|---|
| 1 | Review + sign off 07-judgment-calls.md (33 calls) | everything |
| 2 | ~~Pick the name~~ **DONE: with.bowden.works** (judgment #16); `srv-gw create-project --name with --type custom --domain with.bowden.works` | M0 |
| 3 | **Prove the Supabase snapshot path this week**: `pg_dump` against the connection string (session-pooler host — direct host is IPv6-only; match pg_dump major version), run the auth-scrub, archive one scrubbed baseline to the NAS. Record tier + pause/retention policy in 05. | M0, all rehearsals, cutover |
| 4 | **Start archiving raw Clockify CSVs** (tiny old-app change, bugfix-class: save every uploaded CSV to a server folder — today nothing retains them past the 24h staging window) | M4 parser cross-validation |
| 5 | Stage the Supabase read-only key as a mode-600 env file (`REPLACE_WITH_` placeholder). (The BW Auth client secret needs NO staging — `app-client-register --secret-out` writes it directly at M1.) | M0, M1 |
| 6 | `srv-gw id-user-create` for rian + Adi (usernames frozen — transforms key on them from the first rehearsal) | M1, M2 |
| 7 | Currency: rehearsals proceed on the committed all-CAD default (07 #21); the reconciliation report flags per-currency counts until you give the final yes/no | M2 |
| 8 | **Recommended: patch the current app's income-exposure now** (07 #20 — reversed to fix-now; the window is realistically 6+ months, and the fix is one view/column-split) | — |

## The ladder

Effort is in **sessions** (a focused Claude working session), not
calendar time — you set the pace. (S) = parallel-safe with the next
milestone once its gate is green.

### M0 — Walking skeleton + the oracle's old side (3–5 sessions)

Prove the whole pipe end-to-end before anything real is built.

- Project scaffold per 04: FastAPI + SQLAlchemy/Alembic + Vite SPA,
  Postgres sidecar (**bound 127.0.0.1:<port>** — the decided access
  path for psql/harness/pg_dump; never 0.0.0.0, never bridge-exposed),
  CI (pytest, vitest, tsc, ruff), `srv-gw deploy --build` working.
- Version + changelog surfaced (`/api/meta` + SPA footer + skew
  banner) — the deploy-confirmation rule, live from commit one.
- **Diff harness old side**: the canonical-JSON spec + extractor
  reading live Supabase AND (same queries) a landed legacy schema.
  Baseline capture = production row counts recorded.
- Nightly job: snapshot → scrub → land in `legacy` schema → extractor
  runs. Unattended for a week = part of the gate.
- Design tokens file + app shell + the 3 primitives the first screens
  need (DataTable, FilterBar, MoneyCell). The full vendored-primitive
  set arrives as screens need it, not up front.
- **Gate:** temp domain serves the shell with live version footer; CI
  green; nightly snapshot job has run 7 consecutive nights; harness
  old-side emits canonical JSON for all history.

### M1 — Sign in with BW (1–3 sessions)

The Pattern B kit **shipped 2026-07-07** (`/srv/system/id-auth/
app-auth/`: README, PATTERN-B.md, CHECKLIST.md, bw_auth.py,
example-flask). This milestone follows it verbatim — no speculative
grant headers (07 #31):

- Owner runs `srv-gw app-client-register --name <slug> --redirect-uri
  https://with.bowden.works/auth/callback --secret-out <envfile>`; domain
  mode = **account** (`srv-gw id-gate --site with.bowden.works --mode
  account`).
- Port bw_auth.py's flow into the FastAPI middleware (OAuth code +
  PKCE, audience-bound id_token verification, the app's own session,
  `app_accounts` keyed on BW username → our `users` table). Reserved
  route: `/auth/callback`.
- **Gate:** rian AND Adi each complete a real login on the temp
  domain via auth.bowden.works; a forged/expired token is rejected
  (T-AZ negative tests); sign-out works. **This retires the plan's
  only hard external dependency in week one.**

### M2 — Schema + core migration transforms (4–6 sessions)

- Alembic initial schema per 02 as amended (07 #32/#33): parties,
  engagements, compensation_terms (with the `kind` = cost|billout
  worker-terms model), rate_overrides, tenants, projects
  (uniqueness **(tenant_id, client_party_id, lower(name))**),
  import_batches, time_entries (+ raw_*, + source_timezone,
  + currency stamps), invoices + invoice_lines, cc_* tables,
  comments (tenant_id + nullable anchors), users, audit (the
  unified audit/undo table: full before-images on bulk mutations).
  **No sets/tasks tables** — R3 creates them from the Airtable spec.
- Transform scripts 05 steps 2.1–2.6 (parties→tenant→edges→terms→
  projects→batches→entries), idempotent, nightly against the
  snapshot. Person parties from team_members only (05's pinned rule);
  all-CAD default stamping.
- Backup regime live from this milestone (sidecar now holds real
  books data): nightly pg_dump into the server rotation + NAS; one
  restore drill executed.
- **Gate:** harness green on entity inventories + per-month entry
  aggregates (counts, seconds, cost/billout sums incl. stamps
  copied verbatim) for ALL history; reconciliation report shows
  every legacy row accounted for; restore drill passed.

### M3 — The read-only mirror (4–6 sessions) — first big demo

The highest-risk parity areas (reporting semantics, authz scoping)
with zero risk to money invariants:

- `services/reporting/` ports of the RPC semantics — **SQL
  aggregation in Postgres is the only sanctioned pattern** (04 as
  amended); scope()-transformed Select statements.
- `services/authz/`: the R1 capability grid ONLY (owner /
  worker+imports / super-admin; importer-batch scoping; invoice-lock
  awareness) — the full engagement-visibility derivation table is
  R2+, explicitly not implemented now. scope() is a Select
  transformer; the can/scope equivalence property test runs on the
  seed graph.
- Read-only /entries (filters, pagination, sorting, summary card),
  /summary, /projects + expand — over migrated real data.
  View-as (read-only context swap) included — it exercises scoping.
- **Gate (scripted):** side-by-side of /summary + a bookmarked
  filter view vs the live app for the 3 biggest months — rian
  confirms numbers match (the harness already says so; this is the
  human trust gate). T-AZ grid green. Perf: cold dashboard <1s on
  the migrated full history.

### M4 — Import pipeline (4–6 sessions)

- `services/imports/`: the clockify.ts port with all CLK-*/parse
  fixtures translated 1:1; staging (pending imports), the resolver
  (map/create/skip + incomplete buckets), commit with stamping,
  chunked insert + rollback; batch auto-naming; batch delete guard
  (re-keyed to invoice attachment).
- **Parser cross-validation harness**: every archived weekly CSV
  (Day-0 #4) runs through BOTH parsers (TS via a thin script, and
  Python); field-level diff must be empty. Standing check until
  cutover.
- **Shadow uploads begin**: from now on rian re-uploads each real
  weekly CSV to the new system (~10 min/week); harness compares the
  week. Adi's plan role shrinks to "log in + use it and complain"
  — he is NOT a data-entry dependency (05 as amended).
- **Gate (scripted):** re-upload of a real recent weekly CSV →
  resolver → commit; harness green for that week; CLK suite +
  cross-validation green.

### M5 — Entries editing + the safety architecture (4–6 sessions)

- Inline cell-level editing (the "Google Sheet with data validation"
  model — primary interaction; the full-row form survives as a
  side-panel for whole-row surgery), cascading entity inheritance,
  manual entry, re-resolve, delete (toast-with-undo).
- `services/money/stamping.py` + explicit re-stamp actions (NULL
  semantics per 07 #19).
- **Bulk-mutation safety** (07 #11): scope-from-display-query,
  expected-count precondition, undo via the audit before-images.
  Mutation-feedback policy per 04 as amended: money/lock mutations
  are pessimistic with verified counts; only cosmetic edits are
  optimistic.
- Entity manage: rename/move/**merge** (07 #10) + resolve-or-create.
- **Gate (scripted):** replay of the 3,274-row incident shape —
  select 353 rows via filter, bulk edit, verify expected-count
  guard + verified count + undo restores exactly; harness still
  green after an edit+restamp cycle (only intended rows changed).

### M6 — Invoices + locks (3–5 sessions)

- Invoice CRUD (R1 shape per 07 #24), apply/detach with locks via
  invoice_lines (snapshot-at-attach, attached_at/by audit), margin
  view, **paste-block export**, transform step 2.7 (from/to-party
  seeding per 05's pinned conditional algorithm) + step-2 judgment
  rules.
- **Gate (scripted):** rian produces last month's PlusROI invoice
  paste block in BOTH systems and diffs the text **byte-for-byte**;
  harness invoice section green (per-invoice ids/counts/totals);
  lock tests green (edit-locked-row rejected end-to-end).

### M7 — CC expenses + comments + team admin (3–5 sessions, parallel-safe)

Three independent slices; interleave freely after M5:

- CC expenses end-to-end (batches, paste parse, rules, re-apply,
  color semantics, billing paste-block) + transform 2.8.
  **Detachable tail** (07 contingency): if CC becomes the long pole,
  cutover proceeds for time+invoices and CC follows within one
  statement cycle (legacy stays writable on /tools/cc-expenses only).
- Comments (global board parity + bell; anchors are schema-only).
- Team/engagement admin (member CRUD as engagements + terms,
  rates UI, consolidate_as mapping, Gary).
- **Gate:** one real CC statement pasted in both systems, identical
  assignments + export text; comments round-trip; harness fully
  green across ALL sections for all history.

### M8 — Hardening + UX debt paydown (3–4 sessions)

- The consolidated **R1 UX-fixes checklist** (built during M3–M7
  from the specs' recorded warts: paging restores, filter-context
  preservation, debounce, ?invoice= summary scope, no-op-save fix,
  progress feedback everywhere) — every wart gets an explicit
  port-or-fix decision.
- Perf pass on the 100x seeded dataset (latency budgets; no-seq-scan
  check on hot paths; pg_stat_statements enabled).
- `srv-gw security-audit` clean; Playwright smoke suite (the 3
  critical flows); 390px pass on the mobile-first pages (Summary,
  drill-downs, comments — bulk/import/invoice construction is
  explicitly desktop-first).
- Saved views (name-this-view, pin to nav/palette; seeds rian's
  recurring views) — promoted into R1 as the "easier to navigate"
  centerpiece; the hardcoded quick-chips retire.
- **Gate:** tier-A tests all green (06 §0); security
  audit clean; Playwright green; rian signs the UX checklist.

### M9 — Parallel month + cutover (calendar-quantized)

Per 05 as amended: entry criteria green by the 25th of the preceding
month or the whole thing slips a month — treat that date as the real
deadline. The month is **replay-based**, not double-entry (07 #23
amended): Adi uploads to the old app only; same-day replay mirrors
the artifacts into the new system; CC pastes + invoices mirrored at
month-end from a checklist. Mid-month divergence = fix + replay the
recorded inputs — the calendar month does NOT restart (only a
vacuous-comparison gap extends it). Freeze is enforced by flipping
the old app's read-only flag BEFORE the final snapshot. Cutover in
days 1–5 of the following month, after both systems complete the
month's invoicing. Old app stays warm-standby (un-parkable) until
one full month closes clean; the re-entry pack export stands ready.

**Total: ~30–45 sessions** plus the calendar month. At 3–4 sessions a
week that's roughly 2.5–4 months of building before the parallel
month starts.

## The UX program (runs inside the ladder, not after it)

Parity is defined on **behaviors, not screens** — the IA is
deliberately redesigned (the current 10-item nav grew by accretion):

- **M0–M2, IA design**: one working session producing the IA map —
  workflow groups (Time: entries/import/blocked-triage · Billing:
  invoices/CC/adjustments · Directory: projects+manage merged ·
  Admin: team/tools/audit), a per-role IA (Adi's app is ~4
  surfaces), and the app shell with **R2+ slots reserved** (tenant
  switcher placeholder, perspective indicator unifying view-as with
  R5 portals). Plus the one-page visual-language spec (density,
  type hierarchy, the state-color vocabulary migrated from today's
  load-bearing hexes, dark-mode yes/no decided now) and a /dev/ui
  styleguide route rendering every primitive in every state.
- **Every milestone gate doubles as a UX review** on real data —
  feedback lands as issues immediately, not during the parallel
  month.
- **Signature interactions** (in-ladder, not polish): cell-level
  editing (M5), blocked-entry triage as a grouped workflow surface
  with nav badge (M4/M5), saved views (M8).
- **Post-cutover polish backlog** (explicitly NOT cutover-gating):
  command palette, motion, skeleton loaders, keyboard shortcuts
  beyond table editing.

## Contingencies

| risk | trigger | response |
|---|---|---|
| BW Auth issue on temp domain | M1 gate fails | The kit shipped today — file the defect against id-auth (you own both sides); M2–M3 proceed (they don't need login for nightly rehearsals); M4+ blocks until fixed |
| Parser drift | cross-validation diff non-empty | Fix Python side; fixture added; never ship a divergent week |
| CC long pole | M7 slips past M8 | Detachable-tail plan (above) |
| Parallel-month bug | harness red mid-month | Fix + replay recorded inputs; month continues |
| Missed entry criteria | not green by the 25th | Slip one month, keep shadow-uploading; no partial starts |
| Post-cutover money bug | found weeks later | Warm-standby old app + re-entry pack (05 rollback horizon) |

## After R1 — the roadmap (details in 00)

- **R2 — engagement visibility on BW's real data** (rescoped): the
  visibility levels enforced for Adi/Gary/view-as, permission debug
  view, cross-tenant authz design (the Adi-works-on-PlusROI chain
  traversal), the inter-tenant transaction rule (07 #30). PlusROI
  *onboarding* moves to R3, where its data is first consumed.
- **R3 — port the Airtable system** + PlusROI tenant + registries
  migration + sets/tasks/assignments schema (created here, from the
  live spec), worker payables **reusing invoices+invoice_lines**,
  files/attachments + notifications land here (they're load-bearing
  for the SOP being ported), support blocks.
- **R4 — the money engine**: recurring generation, billing runs,
  fees/commissions, realized FX, the settlement (acceptance test:
  reproduce the sheet to the cent), bookkeeping exports. Retires
  the last Google Sheet.
- **R5 — external tenants + portals**: invite flow (id-auth
  dependency), client/commission views, per-tenant admin boundaries
  incl. the cross-tenant super-admin policy, hostile-user hardening.
