# Build state — cross-session memory (update EVERY session)

**Current state: M9-READY — all build milestones (M0–M8) closed.**

## ⇒ ACTIVE PIVOT (2026-07-17) — HUB-FIRST, before any PlusROI re-import
Rian reviewed the soft-launched PlusROI tenant and found the flat sheet import
is wrong (duplicates projects across tenants — Second Wind lives in BOTH BW and
PlusROI). Decision: do NOT re-import flat. Instead **build the hub first**, see
the real cross-tenant landscape, then fix the model (tenant-agnostic projects +
roles) and do a guided map-or-create import. Rian's 6 steps:
1. [x] Document all review feedback → `docs/REVIEW-BACKLOG.md` + `docs/LESSONS.md`.
2. [ ] **WIPE the sheet-imported PlusROI data — HELD, awaiting rian's explicit "go".**
       Dry-run scope captured (below). Backup exists (`./backups/with-public-20260717-103017.sql.gz`).
3. [ ] Set up `hub.bowden.works` domain — DEFERRED (cross-domain auth-cookie wrinkle);
       hub surface built at `with.bowden.works/hub` first.
4. [x] Hub tables for projects/orgs/people/tenants/accounts (rian's insight:
       tenants≠organizations, accounts≠people — each modeled separately).
5. [x] Show all existing data cross-tenant.
6. [x] **Start with read-only tables** → Hub v0 SHIPPED & DEPLOYED (see below).

**HUB v0 — SHIPPED & DEPLOYED (2026-07-17, ADR #026, `docs/features/hub.md`).**
Super-admin-only, cross-tenant, READ-ONLY. 5 endpoints `GET /api/hub/{tenants,
projects,organizations,people,accounts}` (`app/services/hub.py` + `app/routers/
hub.py`, tenant-exempt in deps.py, neutral-theme `/hub` SPA area, super-admin-
gated nav). `require_super_admin` (is_super_admin && !is_viewing_as) refused
BEFORE any query. NO schema change. Deployed on port **3126** (NOT 3094 — that's
the separate `work` Next.js app). It immediately CONFIRMED the review findings
in the data: TENANTS(3) Bowden Works 64 proj / **PlusROI 403 proj** (sheet
bloat) / Tingang 1 member 0 proj; PROJECTS(467=403+64) Second Wind appears
under BW *and* PlusROI (the duplication, visible); PEOPLE(17 only) — contacts
were imported as a TEXT blob, not person records; ORGANIZATIONS(275) — junk
rows like "Air Table" as an org.

**HUB v0.1 — full field expansion + per-column filtering (2026-07-17,
v0.20.0).** Rian started reviewing live, table by table, beginning with
Organizations: wanted the column HEADING to be the raw field id (not a
designed label — this is a technical review surface), every column
sortable AND filterable like a spreadsheet, and every field the underlying
model actually carries (not just the original derived counts). Built as a
reusable pattern applied to all 5 tables:
  - **DataTable** (`frontend/src/components/DataTable.tsx`) gained an
    opt-in `filterable` prop + `Column.filterValue` (falls back to
    `sortValue`) — a per-column free-text substring filter, AND across
    columns, off by default so `/entries`/`/projects`/CC batches are
    unaffected.
  - **hub.py**: Organizations now returns every `parties` (kind=org)
    column LEFT JOINed with the 1:1 `organizations` detail row (nulls if
    missing — an audit signal, not a crash); Projects gained
    operator/notes/created_at/updated_at (money fields — income,
    billout_adjustment_* — DELIBERATELY still excluded, flagged for rian
    rather than silently added or omitted); People/Tenants gained the
    same party-level fields + Tenants' raw `settings` JSON; Accounts
    gained id/person_id/preferences/created_at.
  - Tests extended (31→39 hub tests incl. the Acme-has-no-Organization-row
    edge case + DataTable filter tests); `make check` fully green: 1014
    backend / 198 frontend / ruff / tsc -b.
  - **Open, flagged not decided (at the time):** `client_profiles` (per-tenant
    commercial terms — currency/terms/commission/credit-limit/contacts)
    is NOT yet on the Organizations table — it's a many-per-org relation
    (tenant-scoped), doesn't fit a one-row-per-org shape without a design
    call. **Resolved same session — see HUB v0.2 below.**

**HUB v0.2 — Organizations expand-a-row drill-down (2026-07-17, v0.21.0).**
Rian hit exactly the confusion flagged above: discovering `client_profiles`
lives on its own table felt like a gap in his mental model. Considered a
"pick a tenant, columns repopulate" lens (his idea) but that hides
multi-tenant spread — the very thing being audited for — so built
**expand-a-row** instead, reusing the mechanism the live `/projects` page
already has (`DataTable` `renderExpanded`/`expandedKeys`/`onToggleExpand`).
New `GET /api/hub/organizations/{party_id}/detail` (`hub.organization_detail`,
same super-admin gate) returns an org's `client_profiles` (one row per
tenant it's a client of, WITH that tenant's own terms — N tenants → N rows,
by design) + `tenant_vendors`. Click ▶ on an Organizations row to see it live.
39→45 hub tests (incl. Copernic-as-client-of-both-tenants returning 2 distinct
profile rows); `make check` green: 1020 backend / 198 frontend. No schema
change; no new ADR. **Template for extending expand-a-row to Projects
(`project_billing`+`project_milestones`) and People
(`party_affiliations`+`engagements`) — not yet built, deferred until a
concrete need drives it.** Also surfaced but NOT built: a generic
"browse any table" view for the ~20 tables still outside the hub entirely
(`engagements`, `invoices`, `expenses`, `time_entries`, CC tables, `comments`,
`saved_views`, `audit_log`, etc.) — see `docs/features/hub.md` "Open
considerations".

**Reporting-oracle drift fixed (2026-07-17):** the 2 red `test_rpt_equivalence`
failures were legit legacy-data drift (BW's old app kept logging — Brentwood
2026-07 went 206→467 entries + 3 new projects), NOT a code bug. Re-extracted
`harness/out/e2e-new.json` from the host sidecar per the test's own docstring →
green. (Gotcha: `extract_new --env-file .env` picks the in-container `WITH_DB_URL`
`db` host; pass `--dsn "$WITH_DB_URL_HOST"` for the host-side sidecar.)

**NEXT once rian says "go" on the wipe:** run the held wipe (backup+assertions,
auto-rollback), then the tenant-agnostic project + `project_roles` model
(REVIEW-BACKLOG §B) and the guided map-or-create importer (§A) — in that order.

## GOAL TRACKER — PlusROI soft-launch (`docs/goals/plusroi-soft-launch.md`)
Autonomous /goal, started 2026-07-12. Resume = read this + the goal file, continue at first unfinished phase.
- [x] **Phase 1 — presentation** SHIPPED v0.14.0 (deployed, healthy, browser-eyeballed): WorkspaceProfile registry + PlusROI theme/sidebar + config-driven nav/columns; no `if(tenant)` in components. TASTE CALL open for rian: skin is lighter than host's teal chrome — may want to push toward the hosting masthead/card fidelity.
- [x] **Phase 2 — stable identity + app-owned flip** SHIPPED v0.15.0 (deployed; ADR #021). Upsert-by-slug stable party uuids + tenant-scoped nightly reset + `books_owner` flag (`app/services/tenancy.py`) + `scripts/flip_books_owner.py`. LIVE: PlusROI flipped `books_owner=app`, 2,391 invoices / 4,724 expenses, **0 dangling party FKs**; survival gate sha256-identical across a simulated nightly; harness byte-identical; settlement 32/32. Also fixed a pre-existing nightly RED (extract_new not tenant-scoped, since 2026-07-11). **Adversarial review CLEAN** (all 5 hunt classes refuted on live SQL probes + 132 tests; every scoped-delete predicate matches 0 app-owned rows). NOTE for BW cutover: BW/Tingang are legacy-mirrored (writable in-app but nightly-rebuilt) — they'll each need their own flip-to-app before anyone edits their books in `with` (documented ADR #021 §3).
- [~] **Phase 3 — write forms** (design `docs/design/plusroi-forms.md`).
      - [x] Slice 1 SHIPPED v0.16.0 (deployed, browser-verified): Clients (+client_profiles+direct_client edge, reuse-existing-org), Contacts (cc_recipient affiliation), Vendors — all via `services/plusroi_gates` (owner + writable-books 409 + tenant + audit); no migration. 859 backend / 146 frontend.
      - [x] **PlusROI BRAND SKIN** applied v0.16.0 (taste resolved, `docs/design/plusroi-theme-brand.md`): orange `#f1a109` accent + navy `#173c57` ink + navy masthead strip, from plusroi.com. Token block + one scoped shell rule; THE RULE intact. Browser-confirmed.
      - [x] Slice 2a SHIPPED v0.17.0 (deployed; alembic 0008; ADR #022): recurrence model (project_billing +is_recurring/cycle_months/billing_timing) + project_milestones + invoices.collected_amount/currency + tenant_vendors (listability). Both new tenant-scoped tables joined `_SCOPED_DELETES` + `TRUNCATE_TABLES`; survival test extended + green; harness byte-identical. LIVE after deploy: PlusROI invoices still 2,391, settlement 32/32. Projects form (/api/plus-projects, /projects/new) with client-default inheritance + recurring|milestone. 882 backend / 160 frontend.
      - [x] Slice 2b SHIPPED v0.18.0 (deployed; ADR #023): invoice generation (recurring single + milestone batch, amount-copy → project default never drifts; `generated_invoice_id` idempotency) + lifecycle (pending_approval→ready→sent[QB#]→paid[USD→realized CAD]) + batch month-end, in a separate `services/money/invoices_plus.py` (BW semantics untouched). settlement_month at generation; cancel untags; realized CAD to the frozen engine. 910 backend / 183 frontend. FLAGGED: BW `/api/invoices` PATCH could coerce a PlusROI-status row (clean_status) — guard being added in slice 3.
      - [x] Slice 3 SHIPPED v0.19.0 (deployed; ADR #024): manual + multi-line expenses (Yulia), CC → PlusROI (per-user-private batches + port-to-expenses via `cc:<line>` provenance), settlement apply-to-month (tags paid rows; frozen engine reflects), + the BW-coercion guard (`/api/invoices` PATCH 409s a PlusROI-lifecycle invoice). 970 backend / 191 frontend; harness byte-identical.
- [x] **Phase 4 — persona browser acceptance DONE** (2026-07-12, live authenticated app via view-as Rob=PlusROI owner): PROVEN end-to-end + test data surgically cleaned each time (settlement gate GREEN 32/32 throughout): Rob project-create→invoice-gen→amount-copy (project default holds $1500 while invoice→$2000); Rob client+2 CC contacts; Rian milestone project ($5000 deposit/milestone/final, generate all 3, deposit→ready); full lifecycle pending_approval→ready→sent[QB# required]→paid[USD→realized CAD required, both guards fire]; Danielle multi-line Yulia expense; settlement apply-to-month (untagged→tagged, refuses re-tag). Exhaustive per-scenario + CC per-user privacy + batch month-end covered by the 970 automated tests. Security-audit: no new `with` findings. Playbook: `docs/PARALLEL-RUN-PLAYBOOK.md`.
- **FOLLOW-UP (small):** milestone `generate-invoice` requires a `month` param (BAD_MONTH on empty) — milestones aren't month-based; make it optional/defaulted (money-path, Fable). Documented in the playbook.

## ✅ GOAL COMPLETE (2026-07-12) — PlusROI soft-launched.
All 6 DONE conditions met: PlusROI mode complete (v0.19.0, brand-skinned, all write forms) · Bowden Works preserved (survival test + live) · PlusROI app-owned + seeded fresh · persona scenarios proven (browser + 970 tests) · suites green + deployed + audit-clean + docs · playbook delivered. Soft launch — cutover changes nothing in the app.
- [ ] **Phase 4 — 3-persona browser acceptance** (view-as Danielle+Rob, direct Rian) → security-audit → parallel-run playbook.

**R2 (post-parity) IN PROGRESS — entity model + suite integration.**
- [x] 2026-07-10 — **People + Organizations refactor SHIPPED v0.10.0**
      (alembic 0006, ADR #012/#013): `parties` kept as the shared identity
      anchor (name/contact stay — ADDITIVE, harness green by construction);
      new 1:1 detail tables `organizations` (currency + Drive/Mosiah folder
      refs) + `people` (growth home) + `party_affiliations` (person↔org
      roles — the Heather link). 1:1 invariant at the
      `services/entities/parties` creation choke point + transform mirror;
      FKs cascade. Full backend suite green; `test_compare_green_after_clean_run`
      unchanged. Docs: `docs/SATELLITE-CONTRACT.md`, `docs/features/people-organizations.md`.
- [~] **Registry API** (`/api/registry/*`: resolve / search / create-org
      w/ dedup / org reference facts) — CODE + TESTS + DOCS LANDED (v0.11.0
      target, ADR #014): `app/routers/registry.py` + `app/services/registry.py`,
      the `REGISTRY_PREFIX` session-gate bypass, Bearer service-token auth
      (fail-closed 503; session never substitutes), 37 tests in
      `tests/registry/`, full backend suite green. Contract + auth resolved
      in `docs/SATELLITE-CONTRACT.md`; feature doc `docs/features/registry-api.md`.
      **PENDING (orchestrator):** version bump (`app/version.py` +
      `frontend/package.json` → 0.11.0), the `REGISTRY_SERVICE_TOKEN` secret
      at `/srv/apps/with/.registry.env` (600) + compose wiring, deploy, and
      the security audit of the new network-reachable WRITE surface.
- [~] **Multi-tenant foundation** — CODE + TESTS + DOCS COMPLETE, PENDING
      DEPLOY (v0.12.0 target, ADR #015, `docs/features/tenancy.md`).
      Request-scoped current tenant replaces the R1 `APP_TENANT_SLUG` pin
      (ADR #004 #13 superseded): engagement-derived membership
      (`allowed_tenants()` in `app/services/authz/context.py`, reusing
      `derive_role`) + all tenants for super admins; the HttpOnly
      `with_tenant` cookie selects the current one; `tenant_id` now enters
      the Actor at `resolve_actor` (the ONE change point — `scope()`/`can()`
      untouched). `GET /api/me` + `tenants`/`tenant`; new `POST /api/tenant`
      (`app/routers/tenant.py`); NO_TENANT 403 gate in `routers/deps.py`;
      view-as tenant-intersection + 409 in `services/authz/view_as.py`;
      `TenantSwitcher` in the AppShell header (shown only for >1 book). New
      tests: `tests/tenancy/` (31) + `tests/authz/test_tenant_context.py`
      (7) + `frontend/.../tenantSwitch.test.ts` (5). Backend suite green
      (762 passed at hand-off); frontend tsc + vitest (107) green.
      **PENDING (orchestrator):** version bump (`app/version.py` +
      `frontend/package.json` → 0.12.0), deploy, and a security-audit pass
      (no new network surface — same session auth, one added `/api/tenant`
      route). NB: `settings.app_tenant_slug` kept ONLY as the default-tenant
      ordering hint / single-tenant default (readers: `config.py` +
      `context.py`).
- [~] **PlusROI books schema + sheet importer** — CODE + TESTS + DOCS
      COMPLETE, PENDING DEPLOY (v0.12.0 target, ADR #016,
      `docs/features/plusroi-books.md`). alembic **0007**: additive `invoices`
      enrichment (fee/commission/settlement-month/original-currency face/
      external-id/due-date + widened status CHECK — open/sent/paid kept) +
      NEW `expenses` (payables journal), `client_profiles`, `project_billing`.
      `app/services/migrate/sheet_books.py` imports the sheet CSV export
      (`books-import/source/`) as the PlusROI tenant's books — a step in
      `transforms.run_all` (opt-in via `books_source`; default None keeps
      every harness/reporting test legacy-only; the two pipeline entrypoints
      pass `"auto"`), INSIDE the rebuild transaction with TRUNCATE-rebuild
      idempotency, per-tab reconciliation (FAILS the run on an unaccounted
      row), live↔archive dedupe (live wins), and per-settlement-month
      invoice/expense totals for the settlement gate. `tests/migrate/
      test_sheet_books.py` (12, dedicated scratch DB) + the real-export smoke
      reconcile GREEN; full backend suite green; `test_harness_parity`
      byte-identical (sheet step OFF by default). NOT the settlement engine
      (R4, `services/money`). **PENDING (orchestrator):** version bump →
      0.12.0, deploy, and the first nightly with `books_source="auto"` (a
      sheet reconciliation failure rolls the whole rebuild back — by design).
- [x] 2026-07-11 — **Settlement engine + gate GREEN** (ADR #017,
      `services/money/settlement.py`, `/api/settlement[/{month}]`,
      `scripts/settlement_gate.py`). Formula calibrated (commissions
      subtracted pre-split; tag-presence = inclusion; full-precision
      rounding — the policy that reproduces Wave invoice 2026050's split
      exactly, pinned by test). GATE: 32/32 live-sheet months match the
      independent CSV oracle TO THE CENT; sheet anchors ≤2¢
      (import-rounding artifacts); all 3 real Wave invoices reconciled
      with every delta classified (May split −49.44 ledger drift; Apr
      +2,500 itemized extra; Mar +8,000 ad-hoc AISV; Mar PPC −3.00
      invoice-side, ledger verified). Real DB rebuilt with books_source
      auto: 2,394 invoices / 4,724 expenses; compose mounts
      ./books-import:ro so the NIGHTLY now re-syncs the sheet (the
      parallel-run mechanism). v0.12.0 DEPLOYED (healthy).
- [x] **Books UI (P4)** — SHIPPED v0.13.0, BROWSER-VERIFIED 2026-07-11
      (rian's session): Monthly split waterfall + Wave-invoice panel match
      the real numbers on screen (May split 15,519.98; Team Cost 1,875.18;
      PPC 2,551.14; Licences 204.16; GST total 21,157.98); both ledgers
      render real rows (USD faces, paid-by badges, Heather-as-person
      vendor); tenant switcher BW⇄PlusROI works; console clean. Importer
      polish: numeric invoice ids normalized ("6357.0"→"6357",
      `_ext_id`), live DB re-imported, gate re-run GREEN 32/32.
      Adversarial review workflow re-running post-session-limit (first
      attempt's finders all died on the limit — empty result NOT counted
      as a pass). Original entry follows.
      Owner-only Books nav group (gated on can_see_income): /books/settlement
      (the Monthly Split waterfall — gross → − fees → − commissions =
      revenue → − expenses [expandable] → − bookkeeping = net → ÷2 = split,
      big; + month rail w/ revenue-vs-expenses sparks + unsettled chip; +
      comparison strip vs prev-month/last-year computed client-side; + the
      Wave-shaped "This month's BW invoice" panel w/ tab-separated Copy
      lines; + "Fronted by …" reimbursements), /books/invoices +
      /books/expenses ledgers (filter/paginate, footer Σ, read-only detail
      drawer, USD subtext, accented partner-fronted paid-by badge). Backend:
      GET /api/books/invoices|expenses (tenant-scoped, full-set sums,
      LIKE-escaped q, month=YYYY-MM|unsettled|all, size default 50 cap 200)
      + GET /api/books/meta {parallel_run}; all owner-only (403
      INCOME_FORBIDDEN); settlement engine consumed as-is (frozen). Tests:
      backend 810 (+26 tests/books/), frontend 120 (+13 booksLogic). ruff +
      tsc + vitest all green; vite build clean; HTTP path smoke-verified
      (owner 200 / non-owner 403 / bad-month 422). ADR #018. Not deployed
      (per task boundary). READ-ONLY v1 by design — parallel-run banner on
      all three pages; the hard write-block for sheet-sourced PlusROI
      invoices is still the post-cutover follow-up.
- [x] 2026-07-11 — **v0.13.1 adversarial-review hardening SHIPPED**
      (ADR #019; the review: 4 findings confirmed by dual refuters, 4
      refuted). (1) CRITICAL closed: requested-but-missing/incomplete/
      shrunken(>20%) sheet export now FAILS the nightly (rolls back —
      yesterday's books survive) instead of committing a skip that
      deleted the PlusROI tenant; stale export (>9d) warns. (2) HIGH
      closed: parallel-run invoice write-block — all 5 R1 invoice
      mutations 409 PARALLEL_RUN_READ_ONLY on a parallel_run tenant
      (was: silent overwrite-at-3:30 trust hazard). (3) MEDIUM closed:
      settlement comparison chips color by MEANING (deltaTone — rising
      expenses = bad). (4) invoice ids "6357.0"→"6357" (_ext_id).
      Suites 819/121 green; deployed healthy; migrate RESULT: OK on the
      healthy export (fail-fast not tripped); gate GREEN 32/32.
- [ ] **Rob + Danielle onboarding** — id-auth accounts EXIST (rian,
      2026-07-10, usernames rob/danielle, passwords set) + site grants
      for with.bowden.works GRANTED (2026-07-11, this session — they
      were missing). REMAINING: their first real login (auto-provisions
      users rows; next nightly links person parties by username).
- [ ] **People / Organizations management UI** (first-class list + edit;
      affiliations incl. the Heather owner-link). R2 UI work.
The goal's remaining conditions are time-and-rian items only (see
"Path to goal completion" below). The replay month + cutover (M9)
awaits rian's calendar, per the goal definition. (M0+M1+M2 all
closed 2026-07-07 — one day. Background items: 7-night soak (now
counting FULL rehearsals incl. transforms+compare+backup); Adi's
first login; NAS pickup of ./backups (host dir exists w/ 14-file
rotation — confirm the server's existing backup rotation covers
/srv/apps/with/backups, else add at M8); RIAN REVIEW: the Villa del
Mar straggler below)

## Blockers on rian (the ONLY human dependencies right now)
- [x] Supabase export access VERIFIED 2026-07-07: AUTH OK, server
      Postgres 17.6, 4,593 time_entries visible via the session
      pooler. Credentials in `.supabase-export.env` (600).
      IMPLEMENTATION NOTE: host pg_dump is v16 (< server 17) — run
      all snapshot/dump jobs INSIDE the app container; the app image
      must include postgresql-client-17 (no host apt needed, no rian
      sudo). Bake this into the M0 Dockerfile + nightly job.
- [x] Name picked: with.bowden.works (judgment #16)
- [x] Project created (port 3126), domain live, id-auth grants for
      rian + adi
- [x] Judgment calls: treated as accepted-by-default per rians
      minimal-interaction directive (2026-07-07); he overrules by
      editing 07-judgment-calls.md anytime

## M0 checklist — DONE 2026-07-07 (one session)
- [x] FastAPI + SQLAlchemy/Alembic + Vite SPA monorepo (04 layout)
- [x] Postgres 17 sidecar (pinned, 127.0.0.1:54317, healthy)
- [x] CI: `make check` green (ruff + pytest 4/4 + tsc + vitest 7/7)
- [x] /api/meta live (0.1.0, db connected, PG 17.10) + SPA footer +
      skew banner; CHANGELOG.md seeded
- [x] tokens.css + AppShell (tenant/perspective slots reserved) +
      DataTable/FilterBar/MoneyCell + /dev/ui
- [x] Harness spec + old-side extractor PROVEN vs live Supabase:
      baseline-first.json = 4,593 entries / 8 months / 62 projects /
      3 invoices / 2 CC batches (49+53 lines) / 9 members
- [~] Nightly job armed (03:30 in-app loop, logged at startup) —
      7-night soak runs in background; completion verified at M2's
      gate (judgment: blocking a week on a soak serves nothing; the
      soak IS the wait, transforms don't depend on it starting green)
- [x] Deployed; with.bowden.works serves the shell — and is now
      GATED (account mode; create-project does NOT auto-gate
      bowden.works sites — found ungated, closed same session)

## M1 checklist — DONE 2026-07-07
- [x] Client registered (.bw-auth.env 600, never read)
- [x] OAuth code+PKCE per the kit; 12h signed session; users table
      (alembic 0002); /auth/login|callback|logout; /api/me
- [x] Default-deny middleware verified live (healthz 200 public,
      /api/* 401, / → 302 /auth/login)
- [x] Tests: pytest 40/40 (T-AZ-040×13, 041×8, 042, 043×4, 045×2 +
      flow/provision), tsc + vitest 7/7
- [x] GATE (rian): real browser login — silent SSO round-trip,
      username in header, v0.2.0 everywhere. Adi: pending visit
      (identical code path — logged as non-blocking datapoint)
- NOTE (corrects earlier line): app-client-register auto-REMOVED the
  Caddy id-auth gate (its guardrail — a domain gate breaks
  /auth/callback). Account-mode is enforced by the app's own
  default-deny middleware. This is the correct Pattern B end state.
- ADR #002 records 5 deviations (PKCE flow vs ACCOUNT header path,
  12h TTL, session key derived from client secret, dev-stub shape,
  person_party_id FK deferred to M2)

## M3 — DONE 2026-07-07/08 (delegated build, resumed once after a
session-limit interruption)
- [x] authz engine: R1 grid (63 cases green), Decision reason chains,
      scope() Select transformer, can/scope equivalence property
- [x] reporting: the §1 filter contract + ONE status predicate;
      SQL-side aggregation; per-currency maps w/ loud mixed error
- [x] EQUIVALENCE: all 8 months cent-for-cent vs the harness oracle
      (incl. the 3 biggest: 2026-06 966, 2026-03 910, 2026-01 883);
      status partition 1572+0+3021 = 4593
- [x] /entries + /summary + /projects live over migrated data;
      view-as switcher + banner; quick chips; URL-addressable filters
- [x] make check: 250 pytest + 31 vitest + ruff + tsc green; v0.4.0
      deployed; boot rehearsal GREEN (19.2s)
- [x] Perf: services 16–39ms, app-stack 16–179ms on full data —
      far under the 1s budget; SPA renders instantly
- [ ] RIAN acceptance (ACCEPTANCE.md §M3): sign into
      work.bowden.works (his Chrome had no active session — the tab
      bounced to /login) and eyeball the side-by-side for Jun/Mar/Jan
      2026. Machine equivalence already proven; this is the trust gate.
- NOTE: one Chrome tab wedged permanently on document_idle after
  racing the OAuth redirect chain — fresh tabs render instantly;
  not an app bug (server logs showed single clean loads).

## M4 — code DONE 2026-07-08, real-CSV gate pending
- [x] Clockify parser port: all 44 CLK fixtures; cross-validated
      field-identical against the ACTUAL old TS parser (via tsx) on
      edge-case CSVs; JS-rounding drift found + pinned
- [x] staging (alembic 0004) / resolver / one-transaction commit
      (entity-debris wart dead) / batch mgmt w/ invoice-keyed guard
- [x] ONE stamping site (services/money) + ONE resolve-or-create
      (services/entities); precedence + Gary + NULL-no-term proven
- [x] /import + /import/resolve UI (map-first, no format pickers)
- [x] make check: pytest 357/357, vitest 36/36; v0.5.0 deployed;
      boot rehearsal green (17.8s)
- [x] OLD-APP CHANGE (sanctioned Day-0 #4): CSV archiving live in
      work.bowden.works (.csv-archive/ host volume; 116/116 green;
      parity-deltas note appended)
- [ ] GATE (needs a real weekly CSV — none retained historically):
      next Adi weekly (or a rian re-export from Clockify) → run
      ACCEPTANCE.md §M4. Non-blocking for M5 (fidelity already
      machine-proven); shadow uploads begin with the first archived
      CSV.

## Standing instructions (rian, 2026-07-08)
- **Model policy**: M6 builds on Fable (money milestone). From M7
  onward, ALL delegated agents launch with the Opus 4.8 override
  (`model: "opus"`); Fable is reserved for money-path review,
  harness-red debugging, and cutover week. rian may also switch the
  main loop via /model at any session boundary — the ladder is
  model-agnostic (gates are scripts; the harness doesn't care who
  wrote the code).
- rian is hands-off ("I'll just let you run with it") — continue
  autonomously; blockers go in this file, never as questions that
  stall the build.

## M5 — DONE 2026-07-08 (deployed 0.6.0)
- [x] Cell-level editing (click/Tab/Esc, EntityPicker, cascade as
      guided drill-down per ADR #006), side-panel editor, manual
      entry, re-resolve, delete w/ undo
- [x] Bulk engine: same-filter scope (property-proven on real data:
      8 filter shapes, id-set equality), expected_count safety stop
      BOTH modes, one transaction, audit before-images, undo replay,
      verified counts, lock-skip reporting
- [x] Entity manage: rename/move/MERGE all levels (recursive Palms
      case tested; invoiced-snapshot safe; husk rules per #10)
- [x] THE GATE: incident replay on migrated data — 216/4,593
      mutated exactly, undo byte-identical, harness green after
- [x] make check: pytest 461/461, vitest 54/54; boot rehearsal 17.3s
- [ ] rian browser walkthrough: ACCEPTANCE.md §M5 (leisure)
- NOTE: invoice mutations deliberately absent (M6); invoice_line_id
  never written by any M5 path

## M6 — DONE 2026-07-08 (deployed 0.7.0) — THE MONEY MILESTONE
- [x] Invoice service: create/update/status stamps/guarded delete;
      apply/detach on the M5 bulk engine w/ safety stops; snapshot
      invoice_lines + attached_at/by; single-currency enforcement
- [x] BYTE-DIFF GATE: paste blocks byte-identical vs the actual old
      lib/invoices.ts (via tsx) for ALL 3 real invoices (195 / 439 /
      2,387 lines) — straggler rule now load-bearing in the oracle
- [x] LIFECYCLE GATE on real-data clone: apply 5 real entries →
      locked (409s, re-stamp skips, bulk refuses) → paste → detach →
      partition restored → harness green
- [x] make check: pytest 515/515, vitest 61/61; ADR #007 (11 items:
      lock-op undo = inverse ops; guarded delete; paid-apply refused;
      ONE predicate in SQL; tenant-settings literals)
- [ ] rian acceptance: ACCEPTANCE.md §M6 (browser paste-block
      comparison vs work.bowden.works — leisure)

## M7 — DONE 2026-07-08 (deployed 0.8.0; built on OPUS 4.8)
- [x] CC expenses end-to-end: parser port (CCE fixtures) + byte
      cross-validation vs old lib via tsx on the 102 real lines;
      rules/keywords/re-apply; 3-color states; billing paste-block
      (tenant literals); owner-gated + audited
- [x] Comments: board + bell, B9 pair, author-only delete
      SERVER-enforced (old-app wart fixed); 2 real comments migrated
- [x] Team admin: party/engagement management, APPEND-ONLY rate
      history proven (old stamps byte-unchanged, next import stamps
      new rate), Gary representable, rate_overrides mgmt,
      consolidate_as config
- [x] Integrated: pytest 620/620, vitest 79/79; shared-file appends
      merged clean (one 0.8.0 heading, one ADR #008)
- [ ] rian acceptance: ACCEPTANCE.md §M7 (real CC statement paste
      both systems — leisure)

## M8 — DONE 2026-07-08 (deployed 0.9.0 → 0.9.1)
- [x] Saved views (alembic 0005): per-user, pinnable, rian's six
      recurring views seeded; hardcoded chips retired
- [x] Perf at 459,305 entries: dashboard 859ms cold (<1s budget),
      scoped entries 57ms warm (<300ms); no-seq-scan CI gate;
      pg_stat_statements on
- [x] Tier-A inventory: 282 IDs → 256 mapped / 17 partial / 9 gap,
      every remainder dispositioned (none block money/authz/import/
      migration cores) — docs/TIER-A.md
- [x] UX checklist: 14 warts swept, all flagged candidates fixed —
      docs/UX-CHECKLIST.md
- [x] App security sweep green; srv-gw security-audit: 0 criticals
      attributable (3 pre-existing disk-usage findings on OTHER
      projects: brentwood 81GB, redsealrecruiting 38GB,
      aismartventures 16GB — rian housekeeping, unrelated)
- [x] 390px pass; Playwright smokes written (make e2e; needs
      `playwright install chromium` once — SELECTOR-REVIEW markers)
- [x] Income/adjustment editing gap CLOSED (0.9.1; agent correctly
      rejected the XOR misread — pct+amount coexist per parity;
      ADR #010)
- [x] BUG found+fixed: frontend version manifest stuck at 0.6.0
      since M6 — deployed app was silently firing the skew banner;
      both versions now locked in step
- [x] Final counts: pytest 683/683, vitest 102/102, ruff+tsc clean

## Known non-blocking items (post-build)
- **View-as: LEFT AS-IS by rian's decision 2026-07-08** (ADR #011).
  Evaluated vs the new BW Auth `bw_view_as.py` standard; kept our
  stricter app-native version + full act-as. Revisit at R5 (portals
  make the drop-in's relationship-scoped hooks worthwhile).
- **BW Auth view-as v2 proposal READY to route** (rian's 2026-07-08
  request to fold "best of both" back into the standard):
  `docs/proposals/bw-view-as-v2-brief.md` + `bw_view_as_v2.py`. Merges
  our 6 guards into the standard, backward-compatible, adversarially
  reviewed (1 HIGH + 5 fixed), self-check green. Hand to the BW Auth /
  id-auth session; it proposes edits to /srv/system/id-auth/app-auth/
  (untouched by us — owner-governed). `with` itself stays on its own
  view-as for R1 (ADR #011); natural adoption point is R5.
- **bw_auth.py kit drift** (SEPARATE from view-as): our login-path
  copy predates the kit's 2026-07-10 revision (new hub-reporting fns
  + MIGRATING.md). Worth a look to see if MIGRATING.md carries any
  sign-in security fixes we want; not yet reviewed. Offered to rian
  2026-07-08.

## Path to goal completion (nothing left to BUILD)
1. **7-night soak**: 3/7 green nights banked (03:30 Jul 8: GREEN 18.3s); completes ~2026-07-13
   if nightly rehearsals stay green (automatic; any red = Fable
   debugging per model policy)
2. **M4 real-CSV gate**: next weekly Clockify upload (~Jul 13-14)
   lands in .csv-archive → run ACCEPTANCE §M4
3. **Adi's one login** at with.bowden.works
4. **rian's ACCEPTANCE walkthroughs**: §M3 side-by-side (needs his
   work.bowden.works login), §M5 bulk-safety demo, §M6 paste-block
   comparison, §M7 CC statement, §M8 saved views + perf table
5. Optional: `playwright install chromium` + make e2e confirmation
Then: declare goal complete + M9 readiness; the replay month starts
on the 1st of whichever month rian picks.

## Session log
- 2026-07-08 (early AM): M8 closed (0.9.0 + 0.9.1), income gap
  closed, audit run, M9-READY state declared. Build phase COMPLETE:
  M0→M8 in ~30 hours wall-clock, 8 deploys, zero red harnesses.
- 2026-07-08 (overnight): M7 3-slice Opus workflow integrated +
  deployed 0.8.0. M8 (Opus) launched — the last build milestone
  before the M9 readiness summary.
- 2026-07-08 (overnight): M6 closed + deployed 0.7.0. Model switch
  per policy: M7 workflow launched on OPUS 4.8 (3 parallel slices).
- 2026-07-08 (overnight): M5 closed + deployed 0.6.0. M6 (Fable, the
  money milestone) launched. Post-M6 agents switch to Opus 4.8 per
  standing instructions.
- 2026-07-08: M4 delegated-built (+107 tests), deployed 0.5.0;
  old-app archiving change shipped + deployed; M5 agent launched.
- 2026-07-08 (early AM): M3 finished after limit-interrupt resume;
  deployed 0.4.0; browser-verified /summary June 2026 ($7,887.06 /
  $5,895.74 / 562:11) + /entries PlusROI chip view. M4 agent
  launched (imports pipeline). TODO next session: the sanctioned
  old-app CSV-archiving change (Day-0 #4) — M4's cross-validation
  needs archived weekly CSVs accumulating ASAP.
- 2026-07-07 (night): M2 delegated-built (43 new tests), deployed
  0.3.0, in-container rehearsal GREEN at boot, backup step added to
  pipeline, restore drill passed. M0→M2 in one day. NEXT: M3
  read-only mirror (agent launched).
- 2026-07-07 (late PM): M1 delegated-built (36 new tests), deployed,
  live-gated with rian's real browser login. Caddy-gate note
  corrected. NEXT: M2 schema + transforms (agent launched; includes
  snapshot-on-boot-if-legacy-missing improvement so transforms get
  data immediately).
- 2026-07-07 (PM): M0 built + deployed in one session (4-agent
  scaffold workflow + integration). make check green end-to-end.
  Site gated account-mode. First harness baseline extracted from
  live Supabase. Nightly soak clock started. NEXT: M1 implement
  (client registered — see .bw-auth.env).
- 2026-07-07: Project created, accounts granted, plan finalized
  (see /srv/apps/work/docs/rebuild/). Supabase export credentials
  verified working (server PG 17.6; use in-container pg_dump 17).
  No code yet. NEXT: M0 per the checklist above.
