# Significant judgment calls — rian's review list

Calls I made while assembling this plan where the conversation record
didn't fully dictate the answer, or where I'm extending something you
said. Each has the decision, why, and what changes if you overrule it.
Ordered by how much I'd like your eyes on it.

## 1. "BW Auth" = the unified id-auth system (Pattern B)

You said "the new BW Auth system currently in development." Nothing in
this project names "BW Auth," so I read it as the unified-auth plan
(`/srv/.logs/planning/unified-auth-2026.md`) — id-auth, login at
auth.bowden.works, with `work` already scheduled as a Wave 3 Pattern B
migration ("app logins REPLACED"). The rebuild keeps no password
store and owns all app-level roles internally.
**Reading confirmed + mechanism superseded (2026-07-07):** the
Pattern B app-auth kit shipped (see #31) — the integration is the
OAuth2+PKCE "Sign in with BW" flow, not the earlier speculative
signed-header design this call originally described.

## 2. Stack: React SPA + FastAPI + Postgres; Supabase fully retired

Your standards designate this stack for exactly this app profile, and
the auth plan locks "No Supabase in the auth path." I chose full
retirement — database too, not just auth — because the app becomes the
books of record for multiple businesses (data sovereignty + backups on
your own hardware) and because the relationship-based permission model
belongs in one tested Python module, not in RLS policies (we already
hit RLS's ceiling with the much simpler current model). Details:
03-stack-decision.md.
**If you'd rather keep Supabase as hosted Postgres only:** possible,
but you lose self-hosting and keep a cloud dependency; the code barely
changes (SQLAlchemy either way).

## 3. The rebuild is a new app with a data migration, not an in-place refactor

The current app keeps running untouched; the new one is a separate
project (new name/port/database); cutover happens after a parallel-run
period where both see the same month and the diff harness proves the
numbers match. More work than refactoring in place, but the only way
"nothing breaks while we rebuild the foundations" is credible.

## 4. New schema on day one — deliberate deviation from your standards

`react.md` says "don't improve a mature schema during a port." The
whole point of this rebuild is the new party/engagement model, so I'm
invoking the rule's own escape hatch (schema refactor as a separate,
deliberate change) and compensating exactly the way the standard
prescribes: the **data-export diff harness is built before anything
else** and proves computed outputs (invoice totals, summaries,
aggregates) are identical across old and new even though tables differ.

## 5. Ratifying the domain terminology as schema vocabulary

Your domain docs proposed (and asked for pushback on): **Party**,
**Engagement**, engagement types (subcontract/freelance/partnership/
referral/referral-pass-through/direct-client/employment/
official-partnership), comp models (hourly/per-unit/fixed-price/
profit-share/commission/gross-percentage/salary), visibility levels,
and Client→Project→**Set**→Task. I ratified all of it unchanged —
it's the best-developed part of the whole record, and "Set" and
"Prime" are your locked terms. One addition: **tenant** for "a party
whose books live in this system" (Bowden Works now; PlusROI, Heather's,
Summer's later), because "organization" is already overloaded.

## 6. R1 scope line: parity first, empire later

R1 rebuilds what exists today (minus phase-outs) on the new
foundations; multi-tenant visibility (R2), sets/tasks (R3), and the
money engine — profit share, commissions, Danielle's gross-percentage,
Hartling thirds-splitting, bookkeeping exports (R4) — come after
cutover. **Amended 2026-07-07 (plan critique):** the sets/tasks
tables are CUT from the day-one schema — Alembic makes adding tables
later free, judgment #28 already superseded their guessed columns
with the Airtable decode, and empty speculative tables violated this
call's own "speculative tables are how schemas rot" rationale. R3
creates them from the live spec (see #32). The engagements tables and
party-based entry attribution DO stay day-one (that part protects the
biggest table from a second migration). I also deliberately did NOT
design the commission/profit-share computation engine now: those need a dedicated
design pass against real R2-era usage, and speculative tables are how
schemas rot. The one thing R1 DOES take from the future model:
entries are attributed to **parties via engagements** (not
team_members), because retrofitting attribution later would be a
second migration of the biggest table.

## 7. View-as becomes app-layer impersonation again — and that's now correct

You were emphatic that view-as must be "exactly as if I logged in as
Adi," and the app-layer version failed you twice, which forced the
JWT hack. That failure was specific to the old architecture: RLS made
the *database* permission-aware, so only a database-level identity swap
was faithful. In the rebuild there is no RLS — every permission
decision happens in the app's authz engine — so impersonating at the
context layer IS logging in as Adi, bit-for-bit, by construction. The
audit log records both identities; credential-ish actions stay blocked
while impersonating.
**Watch item:** if any future feature adds DB-level identity (it
shouldn't), view-as fidelity must be re-verified.

## 8. Toggl removal includes the *import* path

You said "I no longer have any need for toggl" — the export/transfer
side is unambiguous. I'm extending it to the Toggl CSV *import* path
too (your historical Toggl-imported entries migrate as plain data;
the new importer speaks Clockify + manual entry only).
**If you still occasionally get Toggl CSVs from someone:** say so and
the parser keeps both dialects — it's small.

## 9. Invoices snapshot their line data at attach time

Open inconsistency in the current app: bulk-renaming a client left an
invoice displaying stale names (you flagged it 05-25 and it was never
fully resolved). I'm resolving it in favor of **snapshot**: when an
entry/expense is attached, the invoice line copies the display fields
(client, project, description, amounts) as they were at that moment.
Consistent with your lock-at-write invariant — an issued invoice is a
historical document; renaming an entity later shouldn't rewrite what
you already billed. The live entities remain linked by FK for
reporting.
**Alternative** (live-reference): invoices always show current names —
rejected because it makes past invoices unstable.

## 10. Entity merge becomes a first-class operation

The 06-23 session ended with unresolved pain: delete-with-reassign
couldn't merge "PlusROI / The Palms" into "Bowden Works / The Palms"
(unique-constraint collision), and you said wrong-operator assignments
"will happen often." R1 ships real merge (project→project,
client→client, operator→operator): move children, re-point entries,
snapshot-safe for invoiced rows, delete the husk — plus Move for the
no-collision case.

## 11. Bulk-mutation safety is a hard requirement, not polish

After the 3,274-rows-updated-from-a-353-row-selection incident (twice),
R1 bakes in: mutation scope is derived from the *same* query object as
the display scope (one code path), an expected-count precondition
(mismatch aborts before commit), and post-action undo for bulk edits.
This was the single biggest trust breach in the app's history; it gets
architectural treatment.

## 12. Rates: per-source-hour, engagement-scoped; "Gary the AI" is a
first-class case

Carried from ADR #034: no proportions, ever. Cost and billout become
compensation terms — in R1 as two `kind` rows on the worker's own
engagement (02 §2, resolved by the plan critique); recomposing
billout onto the BW→PlusROI partnership hop is R4's design question.
Same numbers you have today, in the model that scales to PlusROI's
books.
Zero-cost billable workers (Gary: cost $0, billout $25) are just a
term with rate 0 — supported natively, tested explicitly. The 140h
Tingang target becomes a setting on the engagement, not a hardcode.

## 13. The dev-mode identity stub — LARGELY SUPERSEDED by #31

Pattern B SHIPPED on 2026-07-07 (`/srv/system/id-auth/app-auth/`), so
the rebuild integrates the real "Sign in with BW" flow in its first
milestone (09-build-plan M1) instead of waiting behind a stub. The
`AUTH_DEV_USER` stub survives only as a local-test convenience
(still barred from production-flagged envs).

## 14. Native time tracking stays out of scope (Clockify remains the
ingest path)

You've said the long-run goal is replacing Clockify too, but Adi's
workflow lives there today and the import pipeline is your most
battle-tested code. The rebuild ports it faithfully (with its 44-test
suite) and defers native tracking to a post-R4 phase. The entry model
doesn't care where entries come from, so nothing blocks it later.

## 15. The last Google Sheet (PlusROI invoicing) survives until R4

Paste-block exports (invoice lines, CC expenses — your preferred
tab-separated-text pattern) carry over in R1 unchanged. Eliminating
that final sheet is the explicit target of the R4 money engine, not
R1 — doing PlusROI's books natively before the engagement/commission
model has burned in would be building the hardest part first on the
least foundation.

## 16. Where the new app lives — RESOLVED: with.bowden.works
(rian, 2026-07-07)

Project `with` (`/srv/apps/with`), domain **with.bowden.works** —
rian's pick: one syllable, collaborative ("working with Bowden
Works"), fits the multi-party future where every agency, freelancer,
and client works WITH Bowden Works. Own Postgres sidecar. Unlike the
earlier draft, with.bowden.works is the PERMANENT home (not a temp
domain) — work.bowden.works stays the live system until cutover,
then redirects. The old app's project is archived, not deleted,
after a full quarter closes clean in the new system.

## 17. Timestamps stay naive wall-clock in R1

The current app stores `start_at`/`end_at` as timezone-less local
wall-clock (preserving the source CSV exactly), and every month
boundary and RPC window assumes that. Changing to UTC/tz-aware during
the migration would silently shift month boundaries — exactly the
diff-harness failure we built the harness to catch. Call: R1 keeps
naive wall-clock (documented as an ADR); the multi-timezone question
reopens at R2 when a second business with non-Pacific workers
onboards, as its own migration with its own verification.
**Amended 2026-07-07:** R1 additionally records a nullable
`source_timezone` column (stamped at import where known) — zero
behavior change, harness-neutral, and it makes the future migration
possible at all: the timezone a naive wall-clock was recorded in is
forensically unrecoverable later.

## 18. Multi-business onboarding depends on an id-auth invite flow
that doesn't exist yet

id-auth has no self-signup or invite mechanism — every account is
`srv-gw id-user-create` by you at the CLI. Fine for rian+Adi (R1).
For R2 (Heather's/Summer's businesses, PlusROI staff, maybe client
logins) that's an operational bottleneck. Call: R1 proceeds on the
CLI runbook; an invite/self-service flow gets raised against the
id-auth roadmap (it's the auth system's job, not this app's — the
app must NOT grow its own account creation, per your standards).
Flagging now so it lands on the id-auth backlog before R2 needs it.

## 19. NULL means "never priced," zero means "priced at zero"

Current inconsistency: the import path leaves billout NULL when no
rate exists, but the re-stamp RPCs coalesce missing rates to 0 —
after a recalculate you can't tell "never priced" from "priced at
$0". Since Gary (the $0 AI) makes zero a *legitimate* rate, the
rebuild pins the semantic: NULL = no applicable compensation term
(remains Blocked-adjacent, surfaced in filters), 0.00 = a real
zero-rate term. Re-stamps preserve NULL when no term matches.

## 20. Security note on the CURRENT app (fix ships with the rebuild)

Found during extraction, flagging per paranoid-mode duty: when the
`projects` RLS was relaxed on 2026-06-09 so Adi's imports could
create projects, `income_usd` and the billout adjustment columns
became **readable (and writable) by any org member through the
Supabase API** — the owner-only protection is UI-layer only now. Adi
has never seen those screens, the API isn't documented anywhere, and
he'd need to construct authenticated PostgREST calls to reach it —
low practical risk, nobody malicious in the org — but the original
design goal ("income never leaks to a manager even through a UI bug")
no longer holds at the DB layer.
**Amended 2026-07-07 (plan critique): recommendation reversed to
fix-now.** The original "accept until cutover" priced the window as
short; the realistic build is 4–6 months. The fix is one view/
column-split in the current app — it's on the Day-0 owner checklist
(09-build-plan). Say the word and I'll patch it.

## 21. Currency is first-class from R1 (RESOLVED by rian 2026-07-07:
"most of my clients are in CAD")

Superseding the earlier USD-only draft of this call. Every
money-bearing row carries an ISO currency code from day one; entries
stamp cost_currency and billout_currency separately (the two billing
hops can differ); invoices and CC batches are single-currency
documents; **no cross-currency arithmetic exists in R1** — aggregates
group by currency and margin only computes within one currency. FX
conversion (needed for cross-currency profit-share math) is designed
in the R4 money-engine pass, not before. Full design:
02-domain-model.md §3 "Currency".

**The existing-data sub-question, mostly RESOLVED (2026-07-07):**
Adi bills in CAD (rian confirmed), PlusROI's books are CAD, and the
Airtable's default currency is CAD — so the working assumption is
**everything currently in the work app is CAD** and the `_usd`
column suffix was simply a misnomer from day one. The migration
stamps all historical work-app amounts CAD. Remaining confirmation
(one yes/no from rian at migration time): no work-app rate, income
figure, or CC statement is secretly USD.

## 22. Legacy "transferred" rows: extend the existing sentinel, never abort

Rows locked by the old transfer flow but not on any invoice should
number exactly zero (the 2026-05-25 invoice migration backfilled them
onto per-org "Legacy transfers" invoices). The migration asserts
zero; any stragglers attach to that same existing invoice with a
warning for your review. One lock mechanism, no new sentinel
invoices, no abort. (This resolves a contradiction between two
package docs that said "abort" and "new per-operator sentinel"
respectively — both readings are dead.)

## 23. The parallel-run month is a rehearsal, not a ledger

**Amended 2026-07-07: the month is REPLAY-based, not dual human
entry.** Adi uploads to the old app only (zero extra work for him);
a same-day replay step mirrors the identical artifacts (CSV files,
CC paste text, an invoice checklist) into the new system — bit-same
inputs, no human memory dependence. A mid-month divergence is fixed
and re-verified by replaying the recorded inputs; the calendar month
does NOT restart. During the month the new system's copy is
for verification only. At cutover the final migration rebuilds the
new database from the old app's data wholesale — the replayed
native month is discarded and replaced. Cost: any comments/settings
created only in the new app that month are lost (enumerated for
explicit acceptance first). Benefit: the cutover is deterministic —
no merge heuristics, no dedupe logic, no "which copy wins" bugs in
the books. The old app was source of truth all month anyway.

## 24. R1 keeps today's invoice shape; the fancy vocabulary waits

The domain model's richer invoice design (numbers, draft/issued/void
states, due dates, engagement-driven generation) is deferred to R4.
R1 invoices are exactly today's: name, open/sent/paid, invoice_date,
manual total override — plus the new lines table and party links.
Reason: the parity tests pin the current behavior, the migration
becomes column-for-column, and re-designing invoicing while also
re-platforming is how books get subtly wrong.

## 25. Rate overrides keep their own table in R1

Project-wide overrides ("every member on this project bills $20")
and percentage overrides ("Prompt Victoria −20%") are resolution-time
rules that don't flatten into static per-engagement terms without
changing semantics for members added later. R1 carries the override
table near-verbatim with the same specificity precedence; R4's money
engine decides whether compensation terms subsume it.

## 26. PlusROI's books are CAD with realized-rate FX — adopted as the
R4 pattern

The bookkeeping sheet keeps all totals in CAD; USD invoices convert
at the actual realized rate per invoice (observed 1.10–1.26), with
the original-currency face value retained alongside. Call: the R4
money engine adopts this exact pattern — a tenant has a books
currency; foreign-currency invoices record (original amount, original
currency, converted amount, realized rate); settlements and margins
compute in books currency. This refines R1's segregation rule rather
than contradicting it: R1 still never *invents* conversions; R4
records them when they actually happen (at payment).

## 27. The settlement formula is R4's acceptance test

The monthly BW↔PlusROI split is now fully specified from the live
sheet: `(Σ after-fee revenue − Σ expenses − 3% bookkeeping) ÷ 2`,
grouped by settlement-month tag, expenses creditable to whichever
partner paid them. Call: R4 is done when the app reproduces the
sheet's settlement to the cent on historical months — same
diff-harness discipline as R1's cutover, applied to the money engine.

## 28. R3 ports the LIVE Airtable model: percentages define budgets,
units are the working grain

Superseding this call's first draft (which treated units as legacy):
the Airtable "Production" base shows Sheet22's new model already
running, and units and percentages are complementary, not competing.
Percentages of Net (defaults: Project Lead 8%, Labour 40%, Client
Owner 0%, all overridable per project AND per set) define unit
budgets at **$5/unit**; workers are assigned and paid in units
(assignment amount = units × $5); duty roles are auto-created
assignments; workers invoice by attaching assignments
(Pending/Sent/Paid). R3's spec is therefore a PORT of the live
Airtable system onto the party/engagement foundations — including
the three-level defaults cascade (Company → Project → Set), per-set
overrides, recurring-set autocreation, the 11-state assignment
workflow, the resourcing state machine, and the worker-invoice
payables loop. Full decode: 08-current-tooling.md §8.

## 29. External tenants and portals are R5, after the PlusROI
absorption

Rian's rollout order (2026-07-07): current work app first, then PM +
bookkeeping for PlusROI and Bowden Works, then offering the system to
people like Summer, then client/commission-partner portal views. The
roadmap now ends with an explicit R5 for external tenants + portals.
Consequence: R2–R4 design decisions may assume all users are
rian-trusted insiders; R5 is where hostile-user hardening (rate
limits per tenant, tenant data isolation audits, portal-scoped
capabilities) gets its own pass.

## 30. Cross-tenant transactions are ONE row, owned by the issuing
tenant

When BW and PlusROI are both tenants (R2+), the BW→PlusROI invoice is
revenue in one ledger and the "Bowden Works Team" expense in the
other — one economic event. Call: a cross-tenant document is a single
row owned by the issuing tenant; the counterparty tenant's
payables/receivables views DERIVE from from/to_party_id (scope()
grants the counterparty tenant read on documents addressed to it).
No mirrored rows — mirrors drift, and the R4 settlement nets exactly
these rows. Consequence for R2's authz design: cross-tenant document
visibility + the engagement-chain traversal (Adi seeing PlusROI
projects through Adi→BW→PlusROI) are the two scope() extensions R2
must design together.

## 31. Pattern B shipped (2026-07-07) — the speculative grant header
is dead

`/srv/system/id-auth/app-auth/` now carries the real app-side
contract: OAuth2 authorization-code + PKCE against auth.bowden.works,
audience-bound id_token, the app keeps its own session +
`app_accounts` keyed on BW username, client registered via
`srv-gw app-client-register --secret-out` (secret straight to a 600
env file). The rebuild's M1 integrates THIS (04-architecture is
updated accordingly); the previously-specced per-request
`X-Auth-Grant: user|exp|hmac` header design is retired. This removes
the plan's only hard external dependency — auth lands in week one,
not at the parallel run.

## 32. Sets/tasks tables are created by R3, not R1

(The operative half of the #6 amendment, recorded separately for
visibility.) R1's schema contains no sets/tasks/assignments DDL and
no task_id column; R3 creates them from the Airtable "Production"
decode (08 §8), where the real column set lives. Adding tables and a
nullable FK later is a metadata-only Alembic operation — nothing is
protected by shipping guessed versions early.

## 33. Project uniqueness is tenant-scoped: (tenant, client, name)

The carried per-client uniqueness was tenant-blind, which mechanically
collides when R3 migrates PlusROI's ~400 sheet projects while R1
already migrated BW-tenant rows for PlusROI-operated projects. Call:
uniqueness = (tenant_id, client_party_id, lower(name)), plus the
canonical-project rule for R3's design: when a project's operator
party is itself a tenant, that tenant's row is the canonical one and
other tenants reference it through the engagement graph rather than
duplicating it. The R3 migration dedupes against this rule.
