App flow — one chart, top to bottom
Ingestion pipeline — Drive sync · manual upload · inbound email
for each discovered file (Drive crawl · upload · email attachment):
hash + dedupe → already processed? older version stays reference-only (UI: "reference only"), skip
classify(file) → rent_roll | t12 | gl_export | pm_package | pm_budget | loan_docs | closing_statement | ... | other
if classify confidence < threshold:
raise AgentQuestion(doc_type) → THIS file pauses; everything else continues
if financial type:
extract → structured DRAFT proposals (status = pending, nothing commits)
run gates (cross-doc ties · printed subtotals = SoT · unit-count vs property record)
else:
chunk + embed → property context index (RAG) for Tower answers, memos & Property Wiki narrative sections
provenance logged · email-sourced tagged "email"
email copies filed into Drive (Content manager tier) dedupe by content hash → same entry, not a new doc
family records (loan · closing · budget · monthly) assemble only when first pass completes
expected foundation docs (budget · reno plan) missing after first import → a suggested Ground Control
task ("Upload a budget"), never a silent gap or a blocker
nightly sync + inbound email keep this loop running — Inbox fills as results land
phase 2: PMS sync (Entrata · AppFolio) becomes a fourth source — skips extraction, never skips review
Routing — where login lands you
on login: session → org → properties = the org's officer-managed list
if properties.count > 0:
land on Inbox of the last-active property (localStorage)
else:
Choose a path → New Inbound (stub) | New Hangar Deal (setup)
property status routes it: owned/under-contract → Hangar · prospect → Inbound (future) · sold → archive (read-only)
stale/invalid stored property (another org) → reset to first property
— never render another org's name
Connect & validate — Drive access + tier detection
on Connect: probe folder via service account → confirm access
detect granted tier:
Content manager → full: file inbound docs · archive processed · share-folders
Viewer → read-only mode; write features prompt when touched
count files + subfolders as a preview
crawl does NOT start here — it kicks off on Continue
gate: Continue enabled iff connected OR ≥1 manual file — no skip
Inbound email — Email Routing → quarantine → pipeline
Cloudflare Email Routing on in.flightdeckcre.ai → worker email() handler
address = <property-slug>-<4hex>@… (rotatable · unknown address = SMTP reject · 25MB cap)
store FIRST: raw .eml + attachments → R2 (content-hash keys) · metadata → D1
sender allowlist is PER-PROPERTY, keyed on envelope sender
(From header = spoofable → display only)
unknown sender → quarantined: stored, NOT processed → "approve sender?" Agent Question
approve → trust for that property only + un-quarantine ALL their held mail → process
processing (Durable Object):
body → captured to the searchable record (RAG corpus) + light AI read proposes draft tasks
bodies are NEVER treated as financial documents — numbers come from documents, not prose
attachments → same classify → extract → proposal pipeline as Drive (ZIPs expand one level)
filing: R2 is canonical (always) · Content manager tier ALSO files a copy
into Drive under "Inbound email/YYYY-MM/" · Viewer = R2 only (targeted upgrade prompt)
crawler re-discovery of the filed copy dedupes by content hash → same document, never a duplicate
attribution check while reading each attachment (content vs receiving property):
property name/address · unit count & unit numbers vs roster · loan number · PM name
mismatch → "wrong property?" question, reroute = Recommended — never silent (same-org only, org boundary absolute)
one email can feed multiple properties — attachments route independently
single doc spanning properties → choose primary, file as reference on the other (split = future)
Review lifecycle — drafts, approvals, autonomy
every extraction lands as a DRAFT proposal (status = pending) — nothing commits itself
approve → audited commit: allowlisted tables · org-scoped · INSERT-only · logged with approver
reject → reversible (Undo); a misclick never costs a re-extraction
autonomy: 3 consecutive clean approvals in a category → it auto-commits
(shown in the Inbox Autonomy line · the month close itself is never on Autopilot)
ambiguity (doc type · sender · source-of-truth · odd value)
→ Agent Question with a Recommended one-click answer; answering resumes the blocked file
family records (loan · closing · budget · monthly) stay locked until first pass completes
— locked surfaces point to the PINNED catch-up row; the Inbox itself never gates
closed months are immutable — restatement is question-gated, never silent
approved items LEAVE the Inbox — they live on in their records and Views
the default view shows only what needs you · empty = the goal state
suggested GC tasks (from documents · emails · meetings) queue in the Inbox — adding is the only write
Monthly close — the backbone workflow
ARRIVAL — a (property, month) close assembles when its documents land
GL export and/or PM package arrive (Drive · upload · email) → one Inbox row per month
either doc alone opens the workflow labeled "waiting on the other" — the missing side is named
cross-document records gate until the first import pass completes
first import: catch-up runs in the background and PINS to the top of the Inbox — never a
blocking step; questions and single-document reviews flow immediately (see the Catch-up node)
operator adjustments are first-class: value + reason, visible in the record, included in totals with a marker
a T12 arriving is corroboration, never its own review: catch-up uses it in Tier 1;
in steady state it cross-checks closed months — drift → restatement question
STEP 1 — RENT ROLL (the roster is the spine)
the month's roll reviews as a grouped CHANGE REPORT vs the approved roster:
move-ins · move-outs · renewals (+trade-out %) · no-event changes (always flagged) · admin
property record owns unit count — a roll that disagrees raises a question, never silently wins
one approval saves every field (rents · dates · tenants · statuses) · full roster one click away
on Autopilot a clean roll approves itself — the report publishes to Views, the briefing gets a line
ANY flagged group forces this review · roster approval UNLOCKS Step 2
a month may bring several rolls (snapshots · corrected re-sends) — ALL fold into this one close
freshest as-of = the month-end roster · earlier snapshots save as history · a corrected re-send re-flags only what changed
STEP 2 — INCOME STATEMENT (GL vs PM vs Budget, three columns)
GL rows → FlightDeck classes; account code decides P&L membership FIRST:
1xxx–3xxx (cash/balance-sheet) excluded BEFORE classification — receipts can't double-count income
operator reclasses inline · every override audited · the P&L recomputes live as classes change
every statement line drills down to its GL transactions — filter, inspect, reclass at the transaction level
PM package parsed to its stated lines, matched line-level to GL accounts where names align
Budget column = the chosen source's figures FOR THAT MONTH — monthly budgets pull the month as written
annual-only sources spread even ÷12 across the confirmed range, labeled "spread ÷ 12" — never silently
THE RECLASS RULE: unit-turn work the PM booked as R&M → capex BELOW the NOI line
renovation program spend sits below the line too — NOI reflects operations, not the turn program
every reclass drafts a PM-correction task: ask the PM to rebook it in THEIR ledger
corrections also queue into the next PM Question Pack — future statements arrive clean
THE TIE-OUT (the month's honesty check)
NOI is EXPECTED to differ: GL NOI $15,638 vs PM-stated $10,316 — the $5,322 gap IS the reclass
NET INCOME is what must tie: EGI − opex − capital vs the PM's bottom line · must tie EXACTLY — to the penny
vs-Budget deltas >10% flag amber for review
a failing tie blocks the close until the operator explains or corrects it
a near-tie is the tip of an iceberg
CLOSE — "Approve month"
batch-approves BOTH documents' suggestions through the same audited approval path — no special write
partial approvals keep reconciling (saved rows still feed the tie-out) · rejected rows drop out
re-uploaded documents: freshest doc per side becomes the tie-out primary, extras stay reviewable
every approval reversible (Undo) · after 3 clean closes a category goes to Autopilot
closing the month is always a human act — Autopilot can approve categories, it never closes a month
CLOSED — a month locks when approved
a closed month is immutable — late documents queue with a question:
"May is closed — re-open to restate, or file as reference?"
RE-OPEN is deliberate: operator action · logged with who/when/why · original close preserved
restate: add or correct documents → review → close again = close #2 (versioned, never overwritten)
memos, views, and the wiki read the LATEST close and mark restated months as restated
Authority merge — per-field source of truth across 13 documents
each of the 13 docs → one column
authority tiers: T1 executed > T2 commitment > T3 quote / term sheet
per FIELD: source of truth defaults to the highest-tier doc that STATES it
(a quote only wins fields nothing better supplies)
conflicts use canonical compare (dates → ISO · money/rates → numeric)
so "2025-10-10" vs "October 10, 2025" is NOT a conflict
each field shows WHICH document won — election is per-field, not per-document
on commit: obligation tasks derive from the merged record
(first payment · reserves · O&M · refi window · maturity)
dedupe by stable source keys — re-approve creates nothing
OBLIGATIONS — verify before suggesting
satisfied at closing (closing statement) or evidenced in the GL → marked done, never suggested
past-due with no evidence → question, not a stale task
only future / unevidenced obligations become suggestions
Budget source — candidates, annualize, one write
up to 5 most-likely candidates load as columns; every budget-shaped doc/tab detected
newest file gets the recommended badge — never auto-selected
columns normalize to annual FOR COMPARISON ONLY (×12/N, labeled);
storage keeps the source's native months; the close pulls the month
choosing writes the selection — the ONLY write here
unchosen candidates park as not_budget_source (restorable)
the chosen source becomes the Budget column in every monthly close
expected foundation docs (budget · reno plan) missing after first import → a suggested Ground Control
task ("Upload a budget"), never a silent gap or a blocker
Closing tie-out — sources = uses + refund
sources = uses — exact, reconciling via the refund line
negative cash-to-close = REFUND TO BUYER = asset transfer, not a use
$3,521,652 = $3,392,928 + $128,724 refund ✓
"Use as closing record" locks the record
purchase price confirm-gates from here — never typed
Rent rolls — as-of ordering + flag rules
pending rolls sort by as-of date · committed roster = read-only context column
flags per cell:
rent drop ≥ $5 · jump > 20% AND ≥ $100
vacancy flips both ways · unit missing / appearing mid-series
physical occ = occupied / total · economic occ = collected / market GPR
rolls carry NO standalone approval — they ride inside whatever unit is being approved:
the year during catch-up, the month forever after
steady state = ONE roll/month — the grouped change report IS Step 1 of the monthly close
(move-ins · move-outs · renewals + trade-out · no-event changes flagged · MTM · admin)
catch-up matrix (side-by-side rolls) is an INSPECTION lens only — trend flags across the
series, never an approval surface — lives on after catch-up as the roll-history lens
expiration outlook (30/60/90) surfaces rollover risk alongside the diff
Autopilot: clean month skips the Inbox — roll saves, report publishes to Views, briefing gets one line
any flagged group forces Inbox review regardless — routine → report; exceptions → decision
Ask the Tower — context, tools, skills
CONTEXT FED ON EVERY MESSAGE
the active property record (units · acquisition · loan summary) + your org scope
the page you're on + what's open (an open review month rides along → "this month" resolves)
conversation history for the session · the property wiki's key facts on demand
THREE TIERS, ALL VISIBLE, NEVER BLENDED: the record (approved numbers — the default basis) ·
pending suggestions (seen and reasoned about conditionally: "as approved it's X — approve what's
in review and it becomes Y", with a link to the review) · reference material (documents · emails ·
meetings — searched and cited). every answer states its basis
TYPED TOOLS (every action logged · org-scoped · no free-form writes)
query_financials — GL · T12 · rent roll · budget-vs-actual · loan record; group by month/category/account/unit
search_records — every document, email body, meeting note; answers cite their source
market_comps — live rent + expense benchmarks for the submarket
build_view / update_view / set_default_view — dashboards created conversationally (views follow the house design rules — see view library)
calc library — occupancy · trade-out · DSCR · annualization · loss-to-lease (pure, golden-tested)
create_task — drafts a Ground Control task (a suggestion, never auto-created)
run_skill — kicks any skill below on demand
code_execution — last resort for bespoke math/table shaping
get_pending_review — what's awaiting approval + the deltas (powers conditional answers)
what_if — scenario math (market rents · refi rates · turn pacing) — always labeled hypothetical
unit_history — any unit's full dossier, queryable
audit_query — the INSERT-only audit ledger, filterable property/actor/date · READ-ONLY: the Tower can
never write audit entries, only the write pipeline lands them · answers cite entries + link the Audit log
save_wiki_fact — proposes a wiki entry from a durable chat fact · operator-attributed · approval-gated
draft_email — PM question packs · lender notes — drafted for the operator, never sent by the agent
watch — "tell me if…" standing conditions that surface in the Weekly Briefing
OPERATOR STATEMENTS (design decision 2026-07-21 — chat is a gated ingestion path)
a durable fact stated in chat triggers a save-to-wiki proposal — the officer NEVER silently learns from chat
saved facts cite [Operator via Tower · date] · write-gated per the write policy · audited · undoable
they rank BELOW document evidence — a doc that later contradicts one raises a conflict card; the doc doesn't auto-win
sibling paths: cc the property intake email (bodies vectorized · attachments classified · new senders trust-gated once)
· Fireflies transcripts cover recorded calls
SOLUTION-FIRST RULES
build or replace the view, offer Set-as-default · no caveat walls
one aviation idiom max · numbers cite sources · writes always gate through your approval
pending items are part of the answer, not a disclaimer — say what changes if they're approved
SKILLS (click a chip on the Tower screen for each one's card)
live: AM Memo · Weekly Briefing · Investor Update · Rent-Roll Change Digest · Audit Review
proposed: Lender Reporting Package · Variance Explainer · Renewal Push List · Delinquency Watch · Refi Window Monitor · PM Question Pack
AM Memo
produces the monthly close narrative, modeled on institutional AM reporting:
snapshot · summary · market · operations · financials vs plan · capital · debt · outlook · actions
reads a CLOSED month (GL, rent roll, loan record — financials must already tie out)
plus HelloData comps for the market section
run control = "Run for [month ▾]" — closed months only, newest first, default latest closed
NOT an as-of date; mid-month status is the Weekly Briefing's job
budget-vs-actual leads the financial section; the PM reclass is a variance note, never the headline
runs automatically after each close · any closed month re-runnable on demand
re-running a prior month = a NEW VERSION stamped "regenerated <date> — reflects <what changed>"
prior versions kept — memos are dated artifacts, never silently mutated
every figure cites its source inline [GL May 2026] [Rent Roll 2026-05-31] [HelloData]
includes charts built to the house view design rules — positioning band, bridge, decomposition, milestones
read-only artifact — nothing to approve before it runs, it lands in Artifacts done
Weekly Briefing
produces the weekly email: new this week · open items with their age · standing watchlist
· the week in numbers · what's coming
scheduled weekly, Mondays 7:00 AM · delivered by email
NEW items appear exactly once, ever — never re-narrated in a later issue
reminders repeat ONLY while unresolved — always with their age or countdown, never their re-explanation
every number cites its source · visuals follow the house view design rules
reply to the email and the Tower answers — same agent, same citations
read-only artifact — no approval needed, nothing it generates writes anywhere
Investor Update
produces a quarterly LP-ready performance summary
reads the quarter's AM Memos plus the rent roll trend
runs quarterly, or Run now
operator edits the draft before sending — it never sends itself, there's no send action in the skill
Rent-Roll Change Digest
produces the week's move-ins/outs, renewals + trade-outs, and flags (rent drops, vacancy flips)
reads the latest committed rent roll against the prior one
runs weekly
publishes straight to Views — no approval gate, it's a read of already-committed data
Weekly Briefing surfaces one summary line from it
Lender Reporting Package · proposed
produces the lender's monthly/quarterly deliverable — financials, rent roll, DSCR calc — as a share-folder copy set
reads the closed month's GL, rent roll, and loan record
runs monthly or quarterly, matching the loan's reporting covenant
needs the Content-manager Drive tier to write the share folder
operator approves the package before the folder is shared with the lender
the cover note drafts via draft_email — you send it, never the agent
Variance Explainer · proposed
produces a budget-vs-actual explainer with a GL line-item drill-down
names the specific transactions behind every variance over 10%
reads the month's GL against the PM budget
runs on demand, or automatically after each close
read-only artifact — operator reviews the explanation, no writes involved
Renewal Push List · proposed
produces a proposed renewal rent per unit with a trade-out target
cross-references leases expiring 60/90 days out against live market_comps
reads the rent roll's lease-expiration dates + submarket comps
runs weekly, or on demand
feeds Ground Control tasks — operator approves each one before it's created
Delinquency Watch · proposed
produces an aging list of delinquent balances with suggested follow-ups
reads NSF/late patterns across recent rent rolls
runs weekly
flags repeat offenders and balance trends, not just a point-in-time total
operator approves any follow-up task before it's created
Refi Window Monitor · proposed
produces the DSCR trend, yield-maintenance countdown, and rate scenarios for the active loan
reads the loan record + trailing financials
runs on demand
pairs with the loan record's refi-planning task — this is the analysis behind that task, not a replacement for it
read-only artifact — operator decides if and when to act on it
PM Question Pack · proposed
produces the email to the PM covering that close's open items: unexplained variances, no-event rent changes, missing docs
reads the just-closed month's review flags and outstanding docs
reclasses from each close auto-append to the pack
runs after each close, or on demand
drafts the email via draft_email — operator reviews and sends it, Tower never emails the PM directly
Audit Review
answers who/what/when against the audit ledger: approvals · mappings · commits · auto-actions · undos · restatements
filterable by property · actor (you / officer / member) · date range
every answer cites its audit entries [Audit · Jun 2] and links the Audit log page
read-only over an INSERT-only ledger — the Tower can never write audit entries; only the write pipeline lands them
on demand from the Tower or Missions — nothing scheduled by default
One skill registry, two surfaces
skill = prompt + tool allowlist + output template — defined once
a Mission is that skill + a cadence + a delivery target (schedule row)
Ask the Tower's run_skill invokes the exact same registry, on demand
either path produces the identical artifact — same content, same inline citations
runs continue server-side if you close the tab
scheduling any catalog skill (live or proposed) just adds a schedule row — no separate build
artifact (md / xlsx) stored + listed with provenance, whichever surface triggered it
Ground Control — where tasks come from
tasks arrive from: loan obligations (merged record — verified against closing statement + GL first;
satisfied obligations never surface) · monthly-close reclasses · meeting triage · email triage
· missing-document suggestions ("Upload a budget" / "Upload or confirm a renovation plan") · manual
dedupe by stable identity keys (obligation / section refs)
same obligation cited in 2 docs = ONE task ("cited in N documents")
recurring tasks create templates on approval
lists: per property + Admin + Marketing
THE TASK RECORD — every task carries the full set
title · property/list · category (loan obligation | repair & maintenance | reporting |
compliance | financial | marketing | manual) · status · priority (the flag) · owner
due date + optional recurrence (recurring approval creates a template) · source links
(documents / email / meeting / Tower / manual) · one-line description
created-by (suggested-by-agent vs manual) + timestamps
suggestions arrive with every field pre-filled — approve, or fix a field, never start from blank
View library — what asset managers keep at their fingertips
ships in the design (frames on this canvas):
portfolio overview — how is the whole book doing, and what needs me first
economics by floor plan — what does each plan earn vs market ✦
collections — did the billed cash arrive? month by month, what's still out ✦
renovation & capex — is the reno program on budget, and where is every project ✦
market rent benchmark — are my rents priced right vs the comp band (built-in, HelloData)
rent roll changes (May) — this month's change report, mounted as a view after close ✦
rent roll history — every roll ever, side by side (built-in)
delinquency aging — who owes, how old is it, is it growing ✦
lease expiration / rollover — where renewals stack up, month by month ✦
budget variance YTD — where actuals broke from budget, and why ✦
T12 NOI trend — is NOI compounding or eroding, month over month ✦
catalog · buildable on request via the Tower:
leasing funnel — leads → tours → apps → leases, where the funnel drops
DSCR / covenant watch — how much cushion is left over the loan covenants
any of these lands as a saved ✦ view — ask the Tower: "build me a view showing …"
consolidated 2026-07-21: per-view full designs collapsed into the Views GALLERY —
only Portfolio Overview and the Units pages keep dedicated frames; the gallery cards
+ this data contract (formulas + these rules) are the build spec for everything else
VIEW DESIGN RULES
every view — shipped or Tower-built — follows the house style:
understandable in 10 seconds · data-ink first, zero chartjunk · direct labels, no legends ·
the headline states the answer · color only as signal · dense aligned tables over big charts
Unit dossier — assembly + insight patterns
assembly — two feeds, one dated log per unit:
structured events ← rolls (rent / status / tenant changes) · GL (unit-tagged txns)
· leases (start / expiry / renewal) · the acquisition record
corpus sweep ← every email, meeting transcript, inspection, and work order is
searched for the unit token ("202", "unit 202", "#202") →
dated entries, each citing its source document
photos ← attach automatically from inspections, work orders, and email
insight patterns — recomputed when the dossier rebuilds, pinned on top:
same component serviced ≥3× in 12 mo → replacement suggestion with the cost math
(Σ service spend vs a replacement quote)
value changed with no recorded event → flag (rent, deposit, status)
charge inconsistent with unit state → verify (a turn billed to an occupied unit)
un-renovated unit vs market spread → turn ROI (est. cost vs rent lift at renewal)
rules:
insights are ALWAYS suggestions — "Create the task" drafts a Ground Control task
for approval; nothing executes automatically. a dismissed insight only returns
on new evidence. the log is append-only; every entry keeps its citation.
insights roll up to the Units index — the flagged units surface without opening each dossier
every unit number app-wide renders as a link to /units/<unit> — and unit
links always open in a NEW TAB, so a review is never lost mid-approval.
Build contract — data model
properties (id, prod) — Supabase — the physical asset, acquisition_date + total_units are SoT
officer_properties (property_id, D1) — officer — registry of officer-managed commit targets
corpus_files (id, D1) — officer — crawl/classify state machine over Drive+email docs
proposals (id, D1) — officer — draft facts awaiting commit, grouped per document
agent questions (derived, none stored) — officer — live-derived blocking ambiguity, no parallel table
family_selections (org,type,property,key, D1) — officer — one authoritative loan/closing record per property
property_email_addresses/emails/senders (D1) — officer — per-property inbound trust + quarantine
tasks / task_templates (id, D1) — officer — Ground Control, never touches prod
prod allowlist (11 tables, Supabase) — officer writes — actuals, budgets, gl_transactions, property_loans, uploads, parsed_rent_roll_units, property_closing_statements, account_mappings, balance_sheet_entries, line_items, properties
org_id required on every tenant row and every query, D1 and prod alike
content-hash dedupe on vault snapshots and attachments, never re-snapshot unchanged bytes
prod writes are INSERT-only through one policy file, except the single audited total_units UPDATE
Build contract — formulas
Physical occ. = occupied / total (occupied|notice|mtm|model|employee)
Economic occ. (RR) = Σ in-place(occ+notice+mtm+employee) / Σ market rent(ALL units) — SCHEDULED basis, labeled as such in UI
Economic occ. (actuals) = rental revenue actual / GPR
EGI = residential_rent + other_income = -Σ(4xxx income rows, credit-normal)
OpEx = Σ opex rows (5-7xxx, non-reno/capex); NOI = EGI - OpEx
Capital (below line) = Σ reno/capex rows (8xxx+, or class=renovation/capex)
Net income = NOI - Capital ← THE tie-out gate (NOI delta is informational; as the PM states it — before debt service; PM books don't carry the mortgage)
GL bucket: code<4000 excluded(BS) | 4xxx income | 5-7xxx opex | 8xxx+ capital
Tie = EXACT ($0) — Net Income to the penny; any variance blocks the close until explained
('a near-tie is the tip of an iceberg')
1% tolerance survives ONLY for cross-system corroboration (GL vs PM-produced T12)
S&U tie: exact — sources = uses, OR reconciles exactly via refund = -cash_to_close (if cash_to_close<0)
Purchase price gate: gross ties record, or (gross - seller_credits) ties record
Loss to lease = Σ(market-in place, occupied), floored at 0
GPR = Σ market_rent × (days_owned/days_in_month)
Vacancy loss = Σ(vacancy_days × market_rent/dim); 0 if no prior roll
Budget/mo = the real stored month (basis native_monthly | annual_spread, labeled "spread ÷ 12"); NULL (not 0) when the month is outside the confirmed range
Compare-grid: ×12/N if N<12 mo AND all lines dated, else unscaled (comparison display only)
Rent-drop: prev-next ≥ $5; Rent-jump: next-prev ≥$100 AND >20%
Vacate/new-lease/unit_missing/appeared: vs last PRESENT column observation
Reno yield=(lift×12)/cost; payback=cost/(lift×12); lift=renov-classic in-place
DSCR = annual NOI / ADS; ADS = payment×12 (stated) else amortized P&I + IO
Worked ex (May, Alder Creek): EGI $24,021; GL NOI $15,638 vs PM NOI $10,316
(Δ$5,322 opex↔capital reclass); both tie Net income $(434)
DSCR: NOI-basis, runs rich vs lender NCF-basis (reserves+turns inside NCF) — app shows BOTH, labeled
Loss-to-lease: two named figures — floored P&L total vs signed rent-gap KPI; MVP uses distinct names
Build contract — extraction
rent_roll units[] w/ rents,status → property record owns unit COUNT (hard refuse)
t12 actuals[] by month/CoA → printed totals are tie SoT (1% — cross-system corroboration, capex excluded)
gl_export transactions[] signed → ledger has BOTH sides of dbl-entry; bucket by acct prefix
pm_package actuals+units+bal_sheet → plug lines make sums match printed totals (low-conf)
pm_budget actuals[] (budget kind) → annual doc: sum×12/N ties; no plug lines
loan_docs loan{} terms+escrows → monthly_payment=P&I only; balance needs as-of date
closing_statement closing{}+su_lines[] → sources/uses printed totals are tie SoT; price nets seller credits
sources_uses su_lines[] only → never forced to balance; off schedule waits for human
uw_model actuals+su+assumptions → budget lines ONLY from model's own plan column, never comp tabs
comparison_model budget_candidates[] only → T12/actual columns NEVER extracted (dup of real source docs)
bank_statement (corpus-only) → CoA spread beats bank-signal; never gl_export
other (corpus-only) → mid-band 'other' auto-resolves, safe default
Model tiering (evals/BASELINES.md): opus-4-8 for all extraction except
comparison_model (sonnet-5, exact-match); haiku-4-5 for classification (14/14).
sources_uses + uw_model have NO eval fixtures — tier is assumed, not evidenced.
Build contract — write policy
Every write funnels through commitProposal/createPropertyRow (write-policy.ts).
Checks, in order:
1. Caller-org scope: proposal.org_id vs callerOrgId, checked BEFORE commitProposal
2. Atomic claim: pending/commit_failed -> committing (no double-commit race)
3. resolveCommitTarget: no target -> operator org's FO property only; else
property must be in ctx.orgId (RLS-bypass guard) AND registered in
officer_properties (Add Property flow) - else refused
4. auditedInsert: table in ALLOWLISTED_TABLES + row.org_id === ctx.orgId or throw
5. Payload validation (required fields, rent-roll unit count = property SoT)
6. Dedupe: per-table semantics (actuals/budgets: property+line+month; GL:
ordinal-aware + one-doc-per-month; rent roll: one per as-of date, with
orphan-upload adoption; closing stmt/balance sheet: one per property[/month])
Allowlist (11 tables): properties, line_items, actuals, budgets,
gl_transactions, property_loans, uploads, parsed_rent_roll_units,
property_closing_statements, account_mappings, balance_sheet_entries.
(uw_assumption_sets, su_budget_sets, tasks = D1-only, never prod.)
Audit: every insert -> D1 audit_log(ts, action, detail JSON w/ proposal_id,
table, id). Approver only reachable via join to proposals.decided_by (gap).
Structurally impossible: no UPDATE except one walled-off column
(properties.total_units); no writes outside the policy functions; Drive
never deletes, archive = move.
Refusal taxonomy: benign (isBenignDuplicateRefusal - "duplicate:"/"already
exists/committed/in prod") never blocks Monthly Close; actionable (unit-count
mismatch, month collision) surfaces but also doesn't block - only genuinely
pending rows block. commit_detail = outcome.detail/reason, verbatim.
PENDING (rebuild should do day one): DB-level officer_ro/officer_writer
roles + RLS. Today one service-role key bypasses RLS entirely; every guard
above is app-code only, no DB backstop.