AUTHOR’S WORD — 2 rows here show a completed status that nothing derived
The pill on these rows was typed, not measured: a0-spine, a0-how-chapters.
Every other status on this page comes from that row’s own PR, commit or decision links; these name nothing a machine can read, so nothing can contradict them when the work changes.
To make the same claim checkable, write one of these on the row and it derives on the next tick: prs (a PR ref — read from GitHub), completion_evidence (owner/repo@<full 40-char sha> — checked against the default branch), shipped_without_pr (why no PR can exist), or decision (the id that settled it). A row that is not owed a build says so with unbuilt; one a decision retired says so with canceled.
Organization architecture — the HOW
Organization architecture · Organization Core. Names updated September 16 to match the existing ORG# boundary. “Portfolio” means a collection of properties. Earlier recordings and quotations retain their wording; existing links and technical IDs remain stable.
The implementation half of the Sep 6 study, drawn for a person first — the rows, the one resolver, the wall, the changes that never migrate, and the road — with the full reference record one click away for an agent.
2026-09-07 · Fable for Gera — the implementation half of the Sep 6 study · status: proposed, pending Gera and Fede · updated Sep 7 with the Slack decisions and Fede's three-week frame · updated Sep 8 with the founders' standup — the org model, who owns what, and where we are · updated Sep 8 (evening) with Gera's three decisions — the property owner's grant widens to the portfolio set with conversations grantable and off, delegated administration with a privilege ceiling, and "owner" split into org_admin / property owner / PMS credential holder · every section deep-linkable (#/model, #/change, …)
The verdict, in plain English— where we are, and what the design changes
The short version. Right now the system works well for one building. It does not work well for a company with many. Everything — the phone, the calendar, the mail, the money, the rules — is tied to a single building. And one company’s things are kept separate from another’s by a rule in our code, not by a wall in the database. This plan adds the company itself as that wall, with buildings underneath it.
The verdict, in plain English. PropFlow is clean for one building and half‑built for a customer with many. Today the phone line, the calendar, the mailbox, the rents and the settings all hang off one building, and what keeps one customer’s data away from another’s is a filter in our code rather than a wall the database enforces — which is how a brand‑new company could briefly see every property. The design keeps what is clean and adds one thing above it: the customer, which is the wall and the bucket everything belongs to, with the building underneath, and every line, calendar, login and rule attached to whichever of the two it belongs to.
The verdict. PropFlow is clean for one building and half‑built for a customer with many. The separation between customers is a filter in code, not a wall the database enforces. The design adds the customer above the building as that wall, and attaches every line, calendar, login and rule to whichever of the two it belongs to. Today: 113 of the Implementation board’s 179 ladder rows read merged or shipped (counted 2026-09-13; chapter 13 carries the method), the customer exists as an id on rows but not as an enforced wall, the calendar and policies still live on the building, a person belongs to one customer, and one portfolio’s scattered homes sit under a single placeholder building. Every today‑claim here is cited row by row in A1.
The verdict, in plain English. PropFlow is clean for one building and half-built for a client with many. Today a phone line, a calendar, a mailbox, the rents and the settings all hang off one property, and the separation between clients is a filter in code rather than a wall the database enforces — which is how a brand-new org could see every property until Fede's fix. The design on this page keeps what is clean and adds one thing above it: the org, which is the client, the wall and the bucket everything else belongs to, with the building underneath, and every line, calendar, login and rule attached to whichever of the two it belongs to — so a change is a new row next to the old one, never a migration. What we are up against: the ladder is under way and not finished — 113 of its 179 rows on the Implementation board read merged or shipped, counted 2026-09-13 (chapter 13 carries the method); the org exists as an id on rows but not as an enforced wall; the calendar and the policies still live on the property; a person belongs to one org; and Western Slope's sixteen homes sit under one placeholder building. The three weeks to Oct 2 put the wall, the one lookup and the test harness in place in the dark; the flip that lets one line answer for many buildings is a November item at one engineer. Chapter 00 is the whole thing in plain words, with eight worked examples; A4 is the plan of attack — what we do, in what order, and what proves each phase. We attacked the one-nameplate-per-number design six ways: the idea held, but its key was one notch too fine and is being repaired at zero migration cost — A2. Chapter 12 is the architecture in the standup's words; chapter 13 is the row-by-row gap between today and the design. The assessment runs A1 what we have · A2 what we want · A3 the gap · A4 the plan · A5 the stress bench · A6 the screens — and every today-claim in this paragraph is cited row by row in A1. For where the work actually stands today, the live board is the Implementation board, which carries A4’s ladder and A6’s UI lanes in one place, grouped by phase.
Each customer is one house: what everybody shares hangs on a hook in the front hall, what one room uses hangs in that room, and the front door has a nameplate so a caller never walks into the wrong house.
Read this page at
How to read a row
The labels you’ll see. Further down, this page shows lines of PropFlow’s own shorthand. One rule reads all of them: every line has a slash in the middle. Before the slash is what the line is about. After the slash is what kind of fact it is.
Here is one line, cut in half:
What it is about
What kind of fact
PROP#a1
META
a building
its own facts
That is the whole trick. Nothing below this line needs it — the rest of this level is in plain words.
Every row on this page is written the same way: a name, a slash, then a label. Read the left half as a noun and the right half as “… and here is a fact about it.”
The part
How to read it
everything before the /
What the row is about — a company, a building, a group, a phone number, a person.
everything after the /
What kind of fact it is — the labels below.
the #
Nothing clever. It only separates the parts of a name: ADDR#phone#+1000… reads “an address, of kind phone, this one”.
The label after the slash
Means
META · PROFILE
Its own facts. The row that says this thing exists, and describes it.
ATTACH#…
Something hung on it — a phone line, a calendar, a setting, a schedule. Dated, so it has a history.
REL#…
A relationship to something else, with a date on it.
HEAD
The one true record of a thing — one row per phone number across the whole platform.
SECRET
A stored login, kept once and pointed at by id from wherever it is needed.
Every row on this page is written the same way: a name, a slash, then a label. Read the left half as a noun and the right half as “… and here is a fact about it.”
The part
How to read it
everything before the /
What the row is about — a company, a building, a group, a phone number, a person.
everything after the /
What kind of fact it is — the labels below.
the #
Nothing clever. It only separates the parts of a name: ADDR#phone#+1000… reads “an address, of kind phone, this one”.
The label after the slash
Means
META · PROFILE
Its own facts. The row that says this thing exists, and describes it.
ATTACH#…
Something hung on it — a phone line, a calendar, a setting, a schedule. Dated, so it has a history.
REL#…
A relationship to something else, with a date on it.
HEAD
The one true record of a thing — one row per phone number across the whole platform.
SECRET
A stored login, kept once and pointed at by id from wherever it is needed.
In database terms the half before the slash is the partition key and the half after it is the sort key, which is why history sorts itself: …#t0 at the end of an ATTACH# label is the date it started.
Every row on this page is written the same way: a name, a slash, then a label. Read the left half as a noun and the right half as “… and here is a fact about it.”
The part
How to read it
everything before the /
What the row is about — a company, a building, a group, a phone number, a person.
everything after the /
What kind of fact it is — the labels below.
the #
Nothing clever. It only separates the parts of a name: ADDR#phone#+1000… reads “an address, of kind phone, this one”.
The label after the slash
Means
META · PROFILE
Its own facts. The row that says this thing exists, and describes it.
ATTACH#…
Something hung on it — a phone line, a calendar, a setting, a schedule. Dated, so it has a history.
REL#…
A relationship to something else, with a date on it.
HEAD
The one true record of a thing — one row per phone number across the whole platform.
SECRET
A stored login, kept once and pointed at by id from wherever it is needed.
In database terms the half before the slash is the partition key and the half after it is the sort key, which is why history sorts itself: …#t0 at the end of an ATTACH# label is the date it started.
If you only read this: one customer is one house, and their buildings are the rooms. Everything the customer owns — a phone number, a calendar, a plumber, a rule — hangs on a hook: in the front hall, where every room can reach it, or inside one room. The first worked example below walks the whole idea end to end.
If you only read this: one customer is one house, its buildings are the rooms, and everything a customer owns — a number, a calendar, a plumber, a rule — hangs on a hook in the hall or in one room. Eight worked examples follow, each one of Gera’s: three buildings sharing a number, a calendar added later, a person in two orgs, a vendor at two buildings, a warehouse and a dentist, a property owner who is not the operator, inviting the first person, and — new on Sep 8 — handing someone the job of administering people without handing them the power to appoint their own successor.
If you only read this: one customer is one house, its buildings are the rooms, and everything a customer owns — a number, a calendar, a plumber, a rule — hangs on a hook in the hall or in one room. Eight worked examples follow, each one of Gera’s: three buildings sharing a number, a calendar added later, a person in two orgs, a vendor at two buildings, a warehouse and a dentist, a property owner who is not the operator, inviting the first person, and — new on Sep 8 — handing someone the job of administering people without handing them the power to appoint their own successor.
Three words carry the whole page: the nameplate (one row per phone number, which turns a dialled number into a customer), the hook (one attachment, hung on exactly one node — org, group, building or person), and the walk (building → group → org, nearest wins, and every answer names the row that set it).
If you only read this: one customer is one house, its buildings are the rooms, and everything a customer owns — a number, a calendar, a plumber, a rule — hangs on a hook in the hall or in one room. Eight worked examples follow, each one of Gera’s: three buildings sharing a number, a calendar added later, a person in two orgs, a vendor at two buildings, a warehouse and a dentist, a property owner who is not the operator, inviting the first person, and — new on Sep 8 — handing someone the job of administering people without handing them the power to appoint their own successor.
Figure S1. Three Buildings, One Number, One Calendar. Solid = the settings walk (every building reads itself, then the org); dashed = who a number reaches. The shared nameplate hangs on a line-set tag covering a1 and a2, so a call on it never reaches a3 — but a1's settings still walk a1 → the org, never through the tag. inserted 18 · updated 0 · moved 0
Read it at
A company is a house with a front door. Its phone numbers, calendars and rules are hooks inside — some in the hall, some in one room — and a call reads the nameplate on the door to find the house, so nobody outside can see in.
Think of each customer as one house. The house is the org. Every building the customer runs is a room in that house. Things the whole customer shares — a phone number, a calendar, a plumber, a rule — hang on a hook in the front hall. Things only one building uses hang on a hook in that room. When a call comes in, PropFlow reads one nameplate on the front door to learn whose house it is, then walks room → hall to find the nearest hook for each thing it needs.
Nobody can see into another house. If a room later needs its own calendar, you hang one new hook in that room. Nothing else moves. Three words do the work here: the nameplate on the front door (which house is this), a hook (one thing hung on one room or on the hall), and a line set (a label saying which rooms one phone number rings).
The org is the customer, the contract and the wall: one org's rows are never readable from another org's context (DF §3.1 L179-191; §5.1 L496-521). Under it sit its buildings — any built facility, an apartment block, a scattered home, a warehouse, an office — each a PROP# row with an asset_type (DF §3.1 L187; §4.2 L384); a group may sit between when several buildings share something (DF §3.1 L191). Everything configurable is an attachment: a dated row hung on exactly one node — org, group, building or person — for a line, mailbox, calendar, credential, setting, vendor roster or schedule, one live row per slot (DF §3.2 L196-234). A number or mailbox is claimed once, platform-wide, at ADDR#phone#<e164>/HEAD — one row per physical number, carrying channels:['voice','sms'] — written only if no row exists, so a second customer's claim is refused by the database, not by code (DF §3.3 L239-246, as repaired by the stress verdict HV §3 rows 1–2). There is one lookup: address → nameplate → org → the buildings in reach → the person; then each setting reads building → group → org, nearest wins unless the org locked it, and every answer names the row that set it (DF §4.1 L350-355; §4.4 L419-475). Nothing under a building ever moves; change is inserts and ends (DF §7 L579-602). Adapters are leaves: the voice platform, the calendar provider, the mail provider and the PMS each sit behind one seam, named on the attachment row, never on the trunk (DF §3.2 L220-223; §11 L801).
ORG#/PROFILE → GROUP#{kind:'chain'} → PROP#/META{organizationId} is the walked chain (a line_set tag is a membership, never a rung — HV §3 row 9); <node>/ATTACH#<kind>#<slot>#<startedAt> are the facts; ADDR#phone#<e164>/HEAD{organizationId, scope, channels[]} (mailboxes: ADDR#email#<addr>/HEAD) is the conditional-put truth per physical address, read as one consistent partition Query with its FN# rows (HV §3 rows 1, 10); resolveEffective folds building → chain → org by key class and returns {value, setAt, locked, maskedBy?}; provider identity lives on the attachment value, never on the trunk.
One house per customer, and the wall is the database's, not a filter in code — a foreign row is unreadable, not merely unlisted.
One nameplate per physical number, voice and text on the same row; a second customer's claim in any spelling is refused at write.
Nearest hook wins — building, then group, then org — and every answer names the row that set it.
A change is one new row beside the old one: inserted · updated · moved, and moved is always 0.
Anything vendor-shaped — the PMS, the calendar provider, the phone vendor, the voice platform — is a value on a leaf row, never a branch of the trunk.
Eight worked examples
How to read these. Each example runs the same four beats: the situation, the setup as rows, what a caller experiences, what changes later. Names are the stress bench’s fixtures, and every target row is proposed.
E1 · One org, three buildings; two share a number; one calendar for everything
A company called Park West runs three buildings. Two of them share one phone number. The third has a phone number of its own. One calendar covers every booking, for all three.
When somebody rings the shared number, PropFlow knows two things before anybody speaks: whose company this is, and that the call is for one of those two buildings and never the third. So Clara asks which of the two it is, and offers times from the one calendar. A text message to that number is read exactly the same way as a call.
None of this is a special arrangement built for Park West. It is the ordinary way this design writes down “this number rings these two buildings”. If the third building later joins the shared number, that is one small change and nothing else moves.
Dashed lines are which buildings a phone number reaches. Plain lines are where a building looks when it needs a rule or a setting: at itself first, then at the company. The shared number reaches the first two buildings and never the third.
The situation. Park West runs three buildings. Two of them answer on one shared phone number; the third has its own. One calendar covers everything.
The company is a row of its own — written ORG#org_st_a. Each building is a row of its own too — PROP#a1, PROP#a2, PROP#a3. Read them the way the key above says: PROP#a1 is “this building”, and whatever follows its slash says what kind of fact the row carries.
The shared number is also a row of its own — written ADDR#phone#+10004000101. There is exactly one such row per phone number across the entire platform, which is what makes it impossible for a second company to claim the same number.
That number does not point at a building. It points at a label covering the two buildings that answer on it — written GROUP#g_ab, a line set. The label says one thing only: which buildings this number rings. It gets no say in settings — when the first building needs a setting it still looks at itself, then at Park West, never at the label. The third building’s number points straight at that building. The calendar hangs on Park West itself, so all three reach it.
Four of the rows, names only:
ORG#org_st_a/PROFILE Park West itself
PROP#a1/META the first building
GROUP#g_ab/PROFILE the label — "this number rings the first two"
ADDR#phone#+10004000101/HEAD the shared number itself
What a caller experiences. The nameplate says Park West, and the number reaches the first two buildings, so Clara asks which one and offers slots from Park West’s calendar. A text reads the same row as a call — one row carries voice and SMS together.
What changes later. Put all three on one number by re-pointing the nameplate at the company: one update, no rows moved.
The situation. Park West runs a1, a2 and a3. a1 and a2 answer on one number; a3 has its own. One calendar covers everything.
The setup, proposed — every row, and what each one does. Eighteen rows go in; none moves.
Park West itself. mapVersion counts structure changes and valuesVersion counts value changes. purpose:'customer' marks this a customer org rather than a sandbox, an internal-staff org or an owner party. The timezone and state live here and are read only through the registry’s setting keys.
One row per building. organizationId is the wall — it names the only company whose context may read the row. ROSTERPK is an index stamp so “Park West’s buildings” is one query instead of a platform-wide scan. lifecycle and answerable say the building is live and takes leasing and maintenance. a1 and a2 also carry groupIds:['g_ab'] on that stamp — an attribute on the row that already exists, not a new row.
The label. system:true means the number’s claim minted it rather than a person. It is marked never walked: it groups a phone line and nothing else, so it has no say in which setting value wins.
Two dated edges putting a1 and a2 in the label. The payload is empty on purpose — no precedence. Only an edge into a kind:'chain' group carries one, and a chain group is the only kind the settings walk goes through.
The phone number itself — the nameplate. One row per physical number across the whole platform, written only if no row exists, so a second company’s claim is refused by the database and not by a branch in code. scope says who the number reaches: here the label, so a1 and a2 and never a3. channels puts voice and text on the one row.
The line hung on the label. direction:'both' — it receives and sends. primaryForScope — it is the number this scope sends from. function:'any' — not narrowed to leasing or maintenance. route:{kind:'clara'} — Clara answers it.
The one calendar — hung on Park West rather than on a building, which is exactly what “one calendar for everything” means: each building walks up and finds it. The provider is a value on this row, never a branch of the trunk.
Four setting rows — the four pickup keys the claim refuses to run without.
inserted 18updated 0moved 0
What a caller experiences. The nameplate says Park West, and the number reaches a1 and a2, so Clara asks “a1 or a2?” and offers slots from the org calendar. A text reads the same nameplate as a call — one row carries voice and sms.
What changes later. → E2. Or put all three on one number: rebind the nameplate to the org, one conditional update (HV §3 row 13).
The situation. Park West runs a1, a2 and a3. a1 and a2 answer on one number; a3 has its own. One calendar covers everything.
The setup, proposed. The shared number hangs on a line set — a label that says “this number rings a1 and a2”. It is a label on two rooms, not a new floor of the house: when a1 needs a setting it still walks a1 → Park West, never through the label. A call on it reaches those two and never a3, but a1's settings still walk a1 → Park West (HV §3 row 9). a3's number hangs on a3; the calendar hangs on the org (shape S3 L633).
What a caller experiences. The nameplate says Park West, and the number reaches a1 and a2, so Clara asks “a1 or a2?” and offers slots from the org calendar. A text reads the same nameplate as a call — one row carries voice and sms.
What changes later. → E2. Or put all three on one number: rebind the nameplate to the org, one conditional update (HV §3 row 13).
The rows, for the builder — skip to the ledger if you only want the shape.
ORG#org_st_a/PROFILE {mapVersion, valuesVersion, purpose:'customer', timezone, jurisdictionStateCode}
PROP#a1/META · PROP#a2/META · PROP#a3/META
{organizationId:'org_st_a', ROSTERPK:'PROPORG#org_st_a', lifecycle:'active',
answerable:{leasing:true, maintenance:true}}
— a1 and a2 also carry groupIds:['g_ab'] on the roster stamp (an attribute, not a row)
GROUP#g_ab/PROFILE {kind:'line_set', system:true, organizationId:'org_st_a'} — a TAG the claim minted; never walked
PROP#a1/REL#in_group#GROUP#g_ab#t0 {} — no precedence: only a kind:'chain' edge carries one
PROP#a2/REL#in_group#GROUP#g_ab#t0 {}
ADDR#phone#+10004000101/HEAD {organizationId:'org_st_a', scope:'GROUP#g_ab', channels:['voice','sms'],
claimedAt, claimedBy, GSI1PK:'ADDR_ORG#org_st_a'} — ONE row per number, platform-unique
GROUP#g_ab/ATTACH#phone_line#+10004000101#t0
{direction:'both', primaryForScope:true, function:'any', route:{kind:'clara'}}
ADDR#phone#+10004000103/HEAD {organizationId:'org_st_a', scope:'PROP#a3', channels:['voice','sms']}
PROP#a3/ATTACH#phone_line#+10004000103#t0 {direction:'both', primaryForScope:true, function:'any'}
CRED#c_a/SECRET {provider, accessToken, refreshToken, organizationId:'org_st_a', version}
ORG#org_st_a/ATTACH#calendar#primary#t0 {provider, externalCalendarId, credentialId:'c_a', readMode:'full', writeMode:'write'}
ORG#org_st_a/ATTACH#schedule#tour#r1#t0 {kind:'calendar_owner', layer:0}
ORG#org_st_a/ATTACH#setting#{org.timezone | org.jurisdiction | emergency_phone | maintenance.intake_route}#t0
— the four pickup keys the claim refuses without (DF §4.4 rule (i) L455)
inserted 18updated 0moved 0
Today, in the code: a number lives on one building row and the last row scanned wins, silently the builder’s footnote · every row also in A1
Today. A number lives on the property row's own phone fields (src/lib/data/types.ts:2919-2934) or in a hand-kept code map (src/lib/domain/properties/phone-lookup.ts:140-148); both are single-valued, and when two rows hold the same number the last row in scan order wins, silently (phone-lookup.ts:300-306). A calendar is one field per property row (types.ts:3060), so "one calendar for three" is the same token copied three times and a refresh on one row clobbers the siblings (scripts/portfolio-harness/results/latest.md:25). Nothing can hang a number on an org or a group, or a calendar on a person — gauntlet T6 and T9 are CANNOT-EXPRESS (scripts/portfolio-harness/gauntlet/results/latest.md:14,17). There is no org writer at all (src/lib/data/constants.ts:18-21; src/lib/data/interfaces/organization.ts:15).
E2 · Later, a2 gets its own calendar
The situation. Months in, a2 hires a leasing agent who keeps a2's showings on her own calendar. a1 and a3 stay on Park West's.
The change, proposed. One credential row and one calendar#primary row on a2 — calendar is writable at the building (DF §4.3 L411). A calendar is a value, so valuesVersion ticks. The nearest row now wins for a2.
What does not change. Both nameplates, both line rows, the line set and the org's tour rule; booked tours keep the calendar snapshot that hosted them (DF §6 L564).
What a caller experiences. Same number, same agent: "a2" now offers a2's calendar, "a1" still offers the org's. If it is her calendar, it is three rows instead — one on the person, an assignment and a tour rule on a2 (DF §7 #10 L592).
The rows, for the builder — skip to the ledger if you only want the shape.
+ CRED#c_a2/SECRET {provider, accessToken, refreshToken, organizationId:'org_st_a', version}
+ PROP#a2/ATTACH#calendar#primary#t1 {provider, externalCalendarId, credentialId:'c_a2', readMode:'full', writeMode:'write'}
+ ORG#org_st_a/MAPLOG#<v> {actor, at, source, items:[3], reverse:[3]} — the log line every transact writes; what Undo replays
~ ORG#org_st_a/PROFILE ADD valuesVersion :1 — one attribute update on the profile, not a row
inserted 3updated 1moved 0
Today, in the code: a2 can get its own calendar, but the shared number still resolves to exactly one building the builder’s footnote · every row also in A1
Today. a2 can get its own calendar — the connect flow writes one field on a2's row (types.ts:3060) — but the shared number still resolves to exactly one property, so a caller on it who wants a2 books wherever the map pointed: the intake property is returned unconditionally and the tour lands on that property's calendar (agents/clara/lib/agent/tools-leasing.ts:835-903; src/lib/domain/leasing/listing-resolution.ts:187-193; src/lib/domain/leasing/tour/apply-tour-intent.ts:507-530). Only one calendar provider works at all: a second one is written by its callback and then rejected at read (src/lib/domain/calendar/provider-guard.ts:44, 78-80). A person's own calendar field (types.ts:12658) is read by no scheduler (gauntlet T9).
Figure S2. One Row Later. The same picture as Figure S1 with one calendar hook added inside a2 — outlined in green as the only insert. The nameplate, the line set, the hall's calendar and a1's and a3's rows are byte-identical; a2 simply finds a nearer hook first. inserted 3 · updated 1 · moved 0
E3 · A person who works for two customers
The situation. One leasing agent runs leasing at Park West and covers weekends at Bellwether, a different customer.
The setup, proposed. Two Person rows, one inside each wall, joined by an admin-only link row neither customer can read. One login, two membership rows, an org picker a single-membership person never sees (DF §5.4 L560). Two calendar rows through two credentials; Bellwether's is free/busy only, so it sees busy blocks and never Park West's tours.
What she experiences. At Bellwether she sees Bellwether's building and nothing of Park West's. The one cross-wall question ever asked is "does this person exist elsewhere — yes or no", at invite time.
What changes later. She leaves Bellwether: end the role; the cascade ends her assignments in one transaction. Park West's rows do not change.
The rows, for the builder — skip to the ledger if you only want the shape.
Today, in the code: a Person’s org is single, and a login’s home org is whichever role scans first the builder’s footnote · every row also in A1
Today. A Person's org is required and single (types.ts:14973-14974), so two customers already means two Person rows — but a login's home org is whichever role scans first (src/lib/domain/identity/accessors.ts:327-335, the comment at :540-547; the approval route logs the same ambiguity at src/app/api/admin/waitlist/[email]/approve/route.ts:126-136), and the home-org role list includes tenant and prospect, so a tenant role somewhere else can become a manager's request-time org (accessors.ts:275-293). There is no membership row and no picker: gauntlet T8, multi-org login, fails (gauntlet/results/latest.md:16).
E4 · A vendor used at two buildings and refused at the third
The situation. A plumbing firm works a1 and a2, not a3 — used first at a1 and a2, and never offered at a3.
The setup, proposed.Identity is platform-unique — one card per real company, no org, no building — claimed like a number. Reach is Park West's own engagement row, whose coverage names a1 and a2. Order is one vendor_roster#plumbing row on a1 and on a2. The roster orders; the engagement permits (VS §2, §5).
What a caller experiences. A leak at a2 reaches the plumber — coverage includes a2 and a2's roster names it first. At a3 it never does: a roster row there naming it is refused at write, not only at read (VS §3).
What changes later. Promote to the whole org: drop coverage, insert one org roster row — three items, zero moves. A second customer engages in its own partition, invisible to Park West (VS §4, §8).
The rows, for the builder — skip to the ledger if you only want the shape.
○ VENDOR#v_plumb/META {company, trade:'plumbing', publicPhone, origin:{kind:'pms'|'manual'}}
— the card: no org, no building, no display override
○ VID#phone#+10004000701/HEAD {vendorCompanyId:'v_plumb', kind:'phone', claimedAt, source} — identity, platform-unique
● ORG#org_st_a/REL#engages#VENDOR#v_plumb#t
{organizationId:'org_st_a', vendorCompanyId:'v_plumb', grants:['dispatch','work_orders'],
coverage:{kind:'buildings', propertyIds:['a1','a2']}, isInHouse:false, origin:'manual', setBy}
— coverage absent = the whole org; there is no empty-array sentinel
● PERSON#p_va/MEMBERSHIP#m1 {organizationId:'org_st_a', vendorId:'v_plumb', role:'owner'} — the human only: no buildings on it
● PROP#a1/ATTACH#vendor_roster#plumbing#t {vendorIds:['v_plumb']}
● PROP#a2/ATTACH#vendor_roster#plumbing#t {vendorIds:['v_plumb']}
● ORG#org_st_a/MAPLOG#<v> — one ADD item on ORG#org_st_a/PROFILE
— not — PROP#a3/ATTACH#vendor_roster#plumbing#t (refused: coverage does not include a3)
— not — GROUP#g_ab/ATTACH#vendor_roster#plumbing#t (g_ab is E1's line-set TAG and holds no settings)
inserted 5updated 1moved 0
Ships later than E1–E3. The two roster rows ride step 2b as already priced (DF §10 L707); the engagement row, the identity heads and the picker are the vendor slice after step 6a — roughly 10–13 engineer-days, counts × judgment, outside the 83–121 (VS §12). Today. Reach is org-wide only — any membership in the org lists the vendor and buildings are ignored (src/lib/data/dynamo/property.ts:916-930), ended memberships included (src/lib/data/dynamo/memberships.ts:368-380). The building link is four overlapping homes with three precedence rules (types.ts:3057, types.ts:12464, types.ts:13237, types.ts:4569; the handyman list beats the preferred list in src/lib/tools/handlers/handle-create-work-order.ts:143-159 while the turnover walk reads only preferred, src/lib/domain/turnover/resolve-scope-vendors.ts:451-474). Per-customer facts sit on the shared card, so a display override is seen by every org (types.ts:5661), and a company with no PMS link cannot be saved at all (src/lib/domain/vendors/vendor-write-invariants.ts:66-73).
E5 · The same trunk for a warehouse operator and a dental practice
The situation. One customer runs warehouses; another is a dental practice. Both want one number, one mailbox and after-hours emergencies; neither leases anything.
What is the same, proposed. Both are orgs; each facility is a PROP# row. "Property" stays the word — a property is a built facility. The trunk does not change: the same hooks, on any building.
What differs is data, not structure. An asset_type; leasing switched off, so a rent question hears a named refusal instead of "not found"; a vocabulary key so Clara says "suite"; one registry line for engineering work (DF §4.2 L384; §9 L685; L390; L402).
What a caller experiences. The same shared agent, maintenance and emergency routing; "the warehouse or the office?" only when both answer that function; leasing refused by name.
The rows, for the builder — skip to the ledger if you only want the shape.
ORG#wh/PROFILE · PROP#x_wh/META {organizationId:'wh', asset_type:'industrial', answerable:{leasing:false, maintenance:true}}
ORG#wh/ATTACH#setting#capability_stage.leasing#t {v:'off'}
ORG#wh/ATTACH#setting#maintenance.intake_route#t {target:'clara'}
ORG#dds/PROFILE · PROP#x_dent/META {organizationId:'dds', asset_type:'office', answerable:{leasing:false, maintenance:true}}
ORG#dds/ATTACH#setting#vocabulary.office#t {unit:'suite', lease:'agreement'} (a reserved key)
ORG#dds/ATTACH#pms_credential#any#t {credentialId:'c9', pmsType:'<practice system>', accountId}
— the adapter, named on the row and nowhere else
ADDR#phone#+10004000801/HEAD {organizationId:'dds', scope:'PROP#x_dent', channels:['voice','sms'], agentId}
PROP#x_dent/ATTACH#phone_line#+10004000801#t {direction:'both', primaryForScope:true, function:'any'}
inserted 4 warehouse · 6 practiceupdated 0moved 0
Honest limit. "Dentist" appears nowhere in the design record; this example is built from asset_type:'office' plus a leasing capability switched off. Shapes for non-dwelling spaces — rooms, bays, chairs — are a stretch item the record neither contains nor prices. Today. The trunk cannot store either customer: unit counts, floors and units-per-floor are required on a property (types.ts:2904-2906); bedrooms, bathrooms and square feet are required on a unit (types.ts:4682-4684); a work order requires a unit (types.ts:6190-6191); the property type is a free string read for display only (types.ts:2908); and billing is units × $10 (src/app/api/billing/checkout/route.ts:41-46, 64), so a dental practice pays the floor. The PMS seam is already the right shape — a client interface with declared capabilities (src/lib/domain/pms/client.ts:133-273, registry.ts:32-71) — but its vocabulary is residential (src/lib/domain/pms/types.ts:300-315) and credentials are keyed by user, not by org (src/lib/data/dynamo/pms-credential.ts:10).
E6 · A third-party manager running a property owner's building
The situation. Park West manages a1, which a property owner owns. The property owner wants to see how their building is doing; Park West answers the phone.
The setup — the rows proposed, the grant list decided. The runner is Park West — one attribute on the building; there is no property-owner field. The property owner is an org row with no login of Park West's; ownership is a dated relationship, and its grants list is decided (Gera, 2026-09-08): reports · financials · occupancy · work_orders · documents by default when a manager adds a property owner — the surface Gera calls portfolio intelligence — read in the property owner's own house, scoped to the buildings that row names (DF §3.4 L252-259, widened by DEC §5). conversations is grantable and off by default: a legal member of the list that takes a deliberate grant, with tenant privacy as the reason for the default rather than a prohibition. One plain setup question — "may pooled hosts show this building?" — and a "no" changes who hosts tours, never who is answered.
What each side experiences. Callers get exactly E1 — the property owner is never a rung. The property owner signs in to their own house and reads the granted projections for the buildings they own. The grant list widens; the wall does not. Each projection is assembled into the property owner's own partition by Park West's workflow — the one write that crosses a wall — and a read the other way is still refused: getProperty from that context returns the same answer as for a building that does not exist.
What changes later. The owner fires Park West: a transfer — a new building row under the new runner, the old marked transferred (DF §7 #8 L590).
The rows, for the builder — skip to the ledger if you only want the shape.
PROP#a1/META {organizationId:'org_st_a', lifecycle:'active', answerable:{…}} — the runner, unchanged
ORG#org_st_o/PROFILE {purpose:'owner_party'} (no login)
PROP#a1/REL#owned_by#ORG#org_st_o#t0 {partyOrganizationId:'org_st_o', share:1, poolingConsent:['leasing_line']?,
grants:['reports','financials','occupancy','work_orders','documents']} — the decided default;
'conversations' is legal here but only on a deliberate grant
ORG#org_st_o/REPORT#a1#2026-09 {occupancy, rentRoll, woCounts, periodTo, writtenBy:'org_st_a',
setBy:{source:'party_push', party:'ORG#org_st_a'}}
(later) PROP#a1/REL#transferred_to#PROP#b3#cut {toOrganizationId:'org_st_b'} · PROP#a1/META.lifecycle:'transferred'
(owner runs it and hires a manager instead) PROP#a1/REL#managed_by#ORG#org_st_a#t {grants:['operational'], acceptedAt}
inserted 2 (+1 report per period)updated 0moved 0
The ledger, recounted honestly. Still 2: the grant list is an attribute on one relationship row, not a row per grant, so widening the default from one entry to five inserts nothing. Granting conversations later is the dated form — END the row, INS the next — inserted 1 · updated 1 · moved 0, exactly E2's and E4's shape. Revoking is ending the row, so the periods already pushed stay readable. Honest limit: which row carries each of financials, occupancy, work orders and documents beside REPORT# is not settled by the decision and is the builder's to shape.
Today, in the code: the runner is one optional string on the building row; there is no owner entity at all the builder’s footnote · every row also in A1
Today (every cite in this footnote re-opened at 58bf665820). The runner is one optional string on the property (types.ts:2887-2890); there is no relationship row, no owner entity and no report push, so runner ≠ property owner is CANNOT_EXPRESS in the fixture scorecard (scripts/portfolio-harness/results/portfolio-fixtures.json; the loader only parses, scripts/portfolio-harness/fixtures/portfolio/load.ts:9-11). The only "owner" the code knows is the PMS credential holder — Property.ownerId, "a Better Auth userId for PMS credentials rather than a landlord identity" in the type's own words (src/lib/data/types.ts:3371, the field at :3780 on Property; credentials keyed by user at src/lib/data/dynamo/pms-credential.ts:10) — which is neither a property owner nor an administrator.
E7 · A leasing agent invited before the first building exists
The situation. Park West signs on Monday; the sync has not run, so the org has zero buildings. That afternoon the admin invites a leasing agent — the standup's case: "there's no properties right now."
The setup, proposed. Her job is a role on the org, not a list of buildings: one role row scoped to the org, one membership row on her login. A roster of zero buildings is a legal answer (DF §3.1 L187-189); "all buildings" is the absence of a subset. The invite form asks the org first, then which buildings.
What she experiences. One membership, so no picker. Tuesday: an empty workspace. Wednesday the sync writes three building rows and her next roster returns three — zero rows touched on her.
What changes later. Limit her to a1: one assignment row. She leaves: end the role, and the cascade ends her assignments (DF §6 L564).
The rows, for the builder — skip to the ledger if you only want the shape.
ORG#org_st_a/PROFILE {purpose:'customer', mapVersion, valuesVersion, timezone, jurisdictionStateCode}
— pre-exists; ZERO PROP# rows; the roster Query returns 0 items, which is legal
+ PERSON#dana_a/PROFILE {organizationId:'org_st_a', displayName, source}
+ PERSON#dana_a/CLAIM#email#<addr> {type:'email', primary:true, verifiedBy:'self_declared'}
+ PERSON#dana_a/ROLE#r1 {tier:'leasing_agent', scope:{organizationId:'org_st_a'}, active:true, startedAt} — THE JOB
+ USER#u1/MEMBERSHIP#org_st_a {personId:'PERSON#dana_a', organizationId:'org_st_a', invitedBy:'PERSON#admin_a',
acceptedAt: absent until she signs in}
+ ORG#org_st_a/MAPLOG#<v> {actor, at, source, items:[…4 INS…], reverse} — a role is structure, so mapVersion +1
(no building row · no property-id list on the login · nothing on her names a building)
(later, optional) PROP#a1/ATTACH#assignment#dana_a#leasing#t {coverage:[], calendarIds:[], priority:0}
— the subset; absent = every building on the roster
inserted 5updated 1moved 0Wednesday's three buildings: 0 · 0 · 0 on her
Today, in the code: the invite form has no org on it, so the first person of a new customer cannot be invited the builder’s footnote · every row also in A1
Today. Two failures, decided by who clicks Send. (1) PropFlow staff invite the customer's first person: the property checkbox list renders only when properties exist (src/app/(workspace)/admin/users/page.tsx:585), so the body carries no property ids (:307), the derived org stays null and the route refuses — "Assign at least one property (with a stamped organization)" (src/app/api/admin/users/route.ts:204-224). (2) The customer's own admin invites her: accepted, and the spine link already writes an org-scoped role (src/lib/platform/auth/link-user-to-spine.ts:253-259) — but her reach is a property-id list on the auth row that is undefined, i.e. "no properties" (types.ts:12659; src/lib/platform/auth/scope.ts:47-52), so Wednesday's buildings never reach her until an admin edits the list by hand (route.ts:374-382). Only the org-admin role escapes, through a per-request pre-fill (src/lib/platform/auth/helpers.ts:340-344) — which is also why an org admin with a real subset is impossible today.
E8 · A delegate who administers people, and the ceiling above her
The situation. Park West's org_admin does not want to be the only person who can add a leasing agent. He hands the office manager the job of administering people — and nothing above it. She must not be able to hand that same job to anyone else, and she must not be able to grant a permission she does not hold herself.
The rules, decided (Gera, 2026-09-08). Role administration is itself a grant — manage_roles — held by people, not a special class of user. Five rules: no escalation, you can never grant what you do not hold; peers can edit each other; only the top level may grant manage_roles — only an org_admin hands the administration job on, so a delegate administers people but cannot mint another administrator; at least one live administrator at all times, and the last one cannot be removed or step themselves down; and a property owner is not an exception — a landlord is a person whose grants arrive from an ownership relationship, assigned by this same path, not a rung of their own. The rules are decided; the row that encodes them is proposed like every other row on this page.
What each side experiences. The office manager opens the team screen, invites a leasing agent, corrects a phone number, ends a role — all inside Park West's wall. She opens the same screen on a peer and it works. She tries to hand someone the administration job and is refused by name: only an org_admin may. She tries to grant a permission she does not hold and is refused by name. The org_admin, while he is the only one, tries to step himself down and is told the company needs at least one administrator first.
What changes later. Revoking is one END on the role row, never a delete; a second org_admin makes the last-administrator refusal go quiet on its own. Adding a property owner runs down this same path (→ E6), and hands out no rung.
The rows, for the builder — skip to the ledger if you only want the shape.
PERSON#office_mgr/ROLE#r2 {tier:'property_manager', scope:{organizationId:'org_st_a'},
grants:['manage_roles'], grantedBy:'PERSON#admin_a', active:true, startedAt}
— the grant sits on the role row; manage_roles is never a tier
ORG#org_st_a/MAPLOG#<v> {actor:'PERSON#admin_a', at, source:'team', items:[1 INS], reverse}
— a grant is structure, so mapVersion +1
— refused — PERSON#agent_x/ROLE#r3 {…, grants:['manage_roles'], grantedBy:'PERSON#office_mgr'}
— only an org_admin may hand manage_roles on; the refusal names the rule
— refused — any grant on r3 that r2 does not itself hold
— refused — ending the last live manage_roles holder, or that holder ending themselves
— same path — a property owner's grants arrive on PROP#a1/REL#owned_by (E6), never as a rung
inserted 2updated 1moved 0
Today, in the code: three of these five rules already ship; what is missing is the separable grant and its ceiling the builder’s footnote · every row also in A1
Today (re-opened at 58bf665820). Peers already edit each other:canInviteRole returns inviterLevel <= targetLevel for management rungs (src/lib/platform/auth/permissions.ts:199-209, the peer rule at :208), and assignableRoles filters through it (src/lib/domain/team/manage-teammate.ts:108-110) — so Gera's ruling confirms shipped behaviour and books no work. The last-administrator guard already ships:isLastActiveAdmin (manage-teammate.ts:49-50) is enforced on removal (:79-80) and on step-down (:101-102), with the sentence "Your company needs at least one admin. Make someone else an admin first."; self-edit is already refused at :58-59. No escalation holds for roles:canManageTeammate refuses any subject outside the actor's invite reach (:66-68) and canChangeTeammateRole re-checks the new role (:98-99). What does not exist is manage_roles as a separate grant — grep -rn manage_roles src/ returns zero hits — so "administers people but is not an administrator" cannot be expressed, and the ceiling has nothing to sit on. Both are rows added to manage-teammate.ts, the one decision module, never a second writer beside it. The ladder itself carries no seat for a landlord: ROLE_HIERARCHY holds platform_admin at level 0 (cross-org PropFlow staff) and org_admin at level 1 (permissions.ts:52-57), and a property owner appears nowhere in it.
What this design refuses
Three things this design will not do, on purpose:
It will not run two different property-management systems behind one phone number. If a customer arrives that way, they tidy up first, then come on.
It will not give one person two logins. One person, one login — and if they work for two companies, a switcher between them.
It will not set a rule that a building is not allowed to argue with. A building can be held to a rule by the company above it, or by the law, and when that happens the answer says out loud which one did it.
Refused
Why
Two property-management systems behind one number
A building has one runner, and a management system is a credential hung on one node. “Two Claras on one address” is two building rows, never one. Refused as a client offering — clean the house first. Two management systems inside one company stays a later row, not a refusal.
A person with two logins
One login, one membership row per company, an explicit active company and one picker. Somebody who belongs to just one company never sees the picker. It was the team’s own ask: admins switch between companies without keeping two logins.
A platform-wide setting a building cannot override
There is no platform tier. A setting is writable at the company, a group, a building or a person, and a default is just a constant in code. A building is stopped only by its own company’s lock — which is named in the answer and reversed with one update — or by a legal floor. A policy written below the company is refused loudly, never quietly ignored.
Refused
Why, in rows
Two PMSs behind one number
A building has one runner (one attribute) and a PMS is a credential slot on a node (DF §3.2 L223); “two Claras on one address” is two building rows, never one (shape S10 L643). Refused as a client offering — clean the house first. Two PMSs inside one org stays a later row, not a refusal (DF §12 M21 L842).
A person with two logins
One auth identity, one membership row per org, an explicit active org and one picker; a single-membership human never sees it (DF §5.4 L560; §5.1 L513). The standup’s own ask: admins switch between orgs without keeping two logins.
A platform-wide setting a building cannot override
The registry has no platform tier — a key is writable at org, group, building or person (DF §4.2 L363) — and a default is a code constant. A building is stopped only by its own org’s lock, which is named in the answer and reversed with one update (DF §7 #16 L598), or by a statute floor. A policy written below the org is refused loudly, never quietly ignored.
Switch to College to see what today’s code does instead, with file:line.
Refused
Why, in rows
Today
Two PMSs behind one number
A building has one runner (one attribute) and a PMS is a credential slot on a node (DF §3.2 L223); "two Claras on one address" is two building rows, never one (shape S10 L643). Refused as a client offering — clean the house first. Two PMSs inside one org stays a later row, not a refusal (DF §12 M21 L842).
The PMS is one string per property (types.ts:3451) and credentials are per user, not per org (src/lib/data/dynamo/pms-credential.ts:10).
A person with two logins
One auth identity, one membership row per org, an explicit active org and one picker; a single-membership human never sees it (DF §5.4 L560; §5.1 L513). The standup's own ask: admins switch between orgs without keeping two logins.
Home org is whichever role scans first (src/lib/domain/identity/accessors.ts:327-335); gauntlet T8 fails.
A platform-wide setting a building cannot override
The registry has no platform tier — a key is writable at org, group, building or person (DF §4.2 L363) — and a default is a code constant. A building is stopped only by its own org's lock, which is named in the answer and reversed with one update (DF §7 #16 L598), or by a statute floor. A policy written below the org is refused loudly, never quietly ignored.
One settings row serves every org (src/lib/data/dynamo/settings.ts:5, 40), writable from the settings page (src/app/api/settings/route.ts:182-225).
Proof: three fitness functions carry this pane — second-claim-refused-by-store (a second customer claiming the same number, on either channel or in any spelling, gets a conditional-check failure from the database, not a code branch; closes at step 1), every-effective-value-has-provenance (every setting on every building names the row that set it — a chain node, a default or a floor, with no property.x ?? org.x left in code; step 2), and rebind-line-inserts-only (after every change on this page each pre-existing row is byte-identical and PK/SK moved counts 0; step 3b). E3 and E6 add isolation-gauntlet T1–T19 for the wall; E7's bench fixture is "Park West, day one" with zero buildings; and E8 adds no-escalation-in-grants — a delegate's grant of a permission she does not hold and her attempt to mint an administrator are both refused by name, with the peer-edit and last-administrator behaviours held as regressions (ST-141–ST-145; step 6a/P7).
The same trunk for a warehouse and a dentist
What never changes
The trunk: org → building → person, with an optional group between. One nameplate per number. One attachment per hook, on exactly one node. One resolver, nearest wins, provenance on every answer. The wall around each customer. A change is inserts and ends — moved is 0. PROP#x_wh/META {organizationId:'wh'} is the same row shape as PROP#a1/META.
What is an adapter at the leaf
The PMS — any kind, or none — is a credential slot whose pmsType is a value on the row. The calendar provider, the mail provider, the phone vendor and the voice platform are the same: a provider field on one attachment, behind one seam, with the SDKs imported from a single pinned directory (DF §3.2 L220-223; §11 L801). A new vertical is data — an asset type, a vocabulary key, a capability switched off — not a branch.
An example row
ORG#dds/ATTACH#setting#vocabulary.office#t {unit:'suite', lease:'agreement'} makes Clara say "suite" for a dental practice, and ORG#wh/ATTACH#setting#capability_stage.leasing#t {v:'off'} makes a rent question at a warehouse hear a named refusal instead of "not found". Two rows, no new code paths, no fork of the trunk.
Each example ends with the literal rows, a ledger, and a footnote for the builder saying what today’s code does instead, with file:line. Only decision G1 (one shared agent set on every line), the items the page’s banners list, and the three Gera decided on Sep 8 — the property owner's widened grant list (E6), delegated administration with its ceiling (E8), and the three-way split of the word “owner” — are decided; everything else is a proposal, the rows in E6 and E8 included. The fixture names are Park West (org_st_a) with buildings a1 · a2 · a3, Bellwether (org_st_b) and the owner party org_st_o; every number is an impossible one the harness’s tripwire requires and every mail address ends .invalid, so nothing here can reach a real person.
Read the contract, implementation pattern and articles
Direction authorized by Gera on September 16: add a final verification phase to the existing Phase Dock and an Organization Core tab to this HOW page, including supporting articles. Organization Core names the shared identity, scope, resource and action-integrity contracts across the existing architecture. It is an umbrella for existing components, not a new service. The implementation and verification pattern below is proposed engineering detail under that direction. It does not certify current code or reverse ALIGN1–16.
PC1 — one contract, enforced at every boundary
Use the existing canonical identity writers, explicit memberships/grants, settings registry/resolver, scoped repositories, resource attachments, reservations and durable effect/workflow machinery. Name the authoritative implementation for each responsibility before changing it. A shared contract may have several enforcement points; it must not acquire separate rule copies in a page, tool, workflow and provider adapter.
The proposed logical envelope carries a verified actor or workflow identity; authorized org/property scope; this request's selected scope where applicable; canonical target/resource IDs; custody, attachment and served-property coverage; relevant rule/data revisions and freshness basis; and, for writes, an operation identity and explicit outcome. These are responsibilities to map onto existing types, not an instruction to add a duplicate universal context type or store. Minimize logged data; never include credentials or customer message bodies in receipts.
For staff business reads, effective scope is authorized scope intersected with selected scope, applied before the business query. Explicit authorized org-level records remain distinct from property records. Org membership never silently expands property grants. Selection is independent per browser tab; it cannot grant access. Background ingestion and durable workflows use their own authorized operation scope, not a viewer's tab. Pinned workflow identity preserves provenance, but does not preserve revoked permission to perform future effects.
Roles remain descriptive. Ordinary in-scope settings edits retain validation, locks and provider constraints without role hierarchy denials. The platform Admin boundary remains the explicit stable-identity exception; the Settings sections remain Admin, Property, Organization and Profile. A successful transport response or an absent exception is not an authorization decision or proof of an external effect.
PC2 — fresh decisions with a stated guarantee
Reuse the existing scope/version machinery. For every access or effect path, document the authoritative rows read, cache/index behavior, revocation boundary and final validation point. An eventually consistent index plus an unchanged version read does not prove a complete current roster. Individually strong reads also do not automatically form an atomic multi-row snapshot. Exercise stale indexes, delayed cache invalidation and an access change between resolution and use.
The proposed acceptance boundary is explicit: after an access/coverage revocation is committed and acknowledged, subsequent protected operations cannot reuse the revoked grant. An operation already submitted to a provider before that boundary may complete; record that ordering and reconcile its actual outcome. Operations paused before submission must revalidate as required by the existing mutation/fence contract. If the present protocol cannot establish that ordering, record a failure and a same-phase correction rather than claiming instantaneous global revocation. This is a consistency requirement to implement and measure on our store, not a claim of the reference paper's guarantees.
PC3 — resources and outcomes mean the same thing everywhere
Resource custody, attachment subject, served-property coverage, provider capability and customer activation are separate facts. Multiple person/property/org calendar endpoints do not multiply a human's capacity. Resolve busy sources and write destinations explicitly, then reserve against the actual shared person/resource identity. If a required reservation or connection cannot be established, preserve the existing documented refusal/pending/manual disposition; never label it a protected booking.
An external action has explicit pending, confirmed, failed or unknown semantics mapped onto the existing operation states. A local update cannot stand in for a provider receipt. Timeout after submission is unknown until the existing workflow reconciles it; retrying uses the same operation identity and must not create duplicate effects. Rescheduling preserves old and new provenance and capacity until the outcome is known. Reuse current workflow timers and recovery; the reference outbox pattern does not authorize a second queue, scheduler, booking engine or success log.
PC4 — the final phase verifies the existing phases
Phase 10 — Organization Core: end-to-end verification lives only on the existing Implementation board. Its rows contain the dispatch instructions and receipts. The contract/inventory task can start now. Scenario tasks can begin when their named dependencies are available; Phase 10 is not a barrier that postpones fixes or requires unrelated future features to finish first. A required dependency still open means an honest unmet prerequisite, not an invented founder decision.
The named risk fixture is two organizations with two properties each, asymmetric grants, two independently selected browser tabs, a shared human with multiple endpoints, an org-held resource serving a finite property subset, and two source accounts reusing a native ID. These are synthetic acceptance fixtures derived from Gera's examples and the review's named risks, never production seed instructions. Add real, scrubbed workflow history through the existing harness. Exercise the real internal chain; stub only the external boundary for injected failures, then obtain the existing sanctioned bench's real-provider proof where required.
Verification covers backend query scope and denied writes; revocation and stale readers; identity and source mappings; capacity and resource coverage; partial effects and retry; meaningful metric aggregation; Settings and Admin enforcement; and Atlas relationship/outcome explanations. Atlas remains read-only, bounded and lazy. Canonical breadcrumb revisits collapse to the existing entity; browser-style Back restores the previous view. Legitimate relationships may form cycles in storage. Atlas shows missing, revoked, unsupported and unknown connections without exposing secrets or private cross-org records.
PC5 — evidence and correction discipline
Each scenario receipt identifies the inspected/deployed commit, environment, fixture provenance, entry point, operation ID where applicable, expected and observed result, executable command/run link, and relevant read/write/provider evidence. A positive control must reach the intended path. A denied query returns no unauthorized business data and causes no unauthorized write. Empty test discovery, skipped steps, mocked internal subjects, an HTTP 200 alone and a zero counter without exercised traffic are not evidence of completion.
A failed integration assertion links to an existing owning row, or creates a corrective follow-up inside that original phase if completed work is wrong. Preserve completed receipts. Phase 10 owns the cross-component proof and re-run, not a parallel implementation backlog. Do not attach the documentation PR as an implementation/completion receipt. Required unsupported or unverified behavior keeps the relevant scenario unfinished; record any explicitly deferred scope and its existing decision separately.
The final row closes only after all required scenario receipts agree on the release under review, affected checks have been rerun after intervening changes, correction links are resolved and the existing rollout/bake/deletion conditions are satisfied for the claimed scope. Bench proof is not customer activation. Planning approval and a merge grant do not activate real customer features, authorize a live backfill or waive provider consent. This is a bounded portfolio acceptance milestone, not a new universal policy engine or the future contractor privilege matrix.
PC6 — articles and the implementation lessons we take from them
These primary sources explain the pattern; they do not replace the current product decisions. Reviewed September 16, 2026. Technical titles and vendor names below are bibliographic references.
Zanzibar: Google's Consistent, Global Authorization System — full paper: explicit relationships and consistency between authorization and protected content changes. Adopt a stated freshness contract and adversarial checks; do not claim its distributed guarantees from our relationship graph alone.
Open Policy Agent — deployment and enforcement: separate the policy decision from the application's enforcement boundary. Shared rules need not require a remote central service. OPA is a candidate implementation, not an adopted dependency.
OPA — external data: decisions depend on the data supplied and refreshed by the integration. Cached grants require a deliberate freshness and revocation strategy.
OPA — data filtering: authorization also shapes which records a query may return. Apply the principle to existing scoped key/query builders; its SQL example is not a drop-in adapter for our store.
OPA — policy testing: test both allowed and denied cases and fail on an empty test selection. Use our existing harness unless an engine is deliberately adopted.
DynamoDB — read consistency: global secondary indexes are eventually consistent; strong base-table reads do not prevent a later concurrent change. Verify the exact access protocol, not a generic "strong read" label.
AWS — transactional outbox pattern: durable state and downstream effects must survive partial failure; duplicate delivery needs idempotent handling. Reuse the existing durable workflow/effect design. An outbox does not itself prove that a provider accepted a booking.
Implementation choice: establish and verify the shared contract first. Consider a policy-engine dependency only through a bounded comparison against the current resolver and repository boundary, with measured operational benefit. This phase does not authorize that dependency or a broad rewrite.
01 · The problem today
Every phone number in the office is scrawled on one sticky note on the fridge, and whoever wrote on it last wins.
Figure 1. The Sticky Note. Today an inbound call, text or email finds its building through five hand-kept guesses, none of which is a database fact — so nothing can refuse a duplicate, name a property owner, or keep sixteen homes apart.
Read it at
Right now, when someone calls, the computer looks at a list somebody typed by hand to guess which building they mean. Sometimes the list is wrong and the wrong building answers.
Today a call, a text or an email finds its building through five separate hand-kept lists and guesses. Two of those lists can disagree, one silently throws leads away, and an unknown number gets answered as if it were ours. Western Slope's sixteen homes are squeezed into one pretend building because there is nowhere else to put them.
The routing is five independent lookups — a hand-edited number file, an in-memory map where the last write wins, a first-match mailbox matcher, a fail-open for unknown numbers, and an "umbrella" property standing in for a whole company. None of them is a database fact, so nothing can refuse a duplicate, and nothing can say who a number belongs to. That is a correctness problem before it is a scaling one.
The inbound path resolves a number through PHONE_TO_PROPERTY_MAP + env overrides and a last-writer-wins Map, mailboxes through first-match matchers that drop on >1, and unmapped voice through a fail-open in the personalization route; a company with no building of its own lives in PROP#western-slope-proto. There is no row whose existence is the fact "this address belongs to this company", so uniqueness is unenforceable, isolation is a post-read fence, and every Western Slope lead is born under a key that must be re-keyed later (the K16 one-way door). The rest of this page replaces those five guesses with one row read once.
Five lookups, five owners, zero database facts — nothing can say "no".
The same number can be claimed twice; the winner is whichever file loaded last.
Unknown numbers are answered as ours instead of refused.
Western Slope's 16 homes are one umbrella building; every lead written there needs a re-key — Sep 12 ships on these rails on purpose, with a counter watching.
Proof: from today a counter shows how many leads were born in the wrong box, and an unknown number is refused instead of guessed — the tests are umbrella-tripwire (a dated count of rows born under the umbrella, from now) and unclaimed-address-refused-zero-reads (red today, closes at 3b/4).
A family runs a few houses on one street, and every phone, mailbox and calendar hangs on exactly one hook — at a house, on the block's gate, or at the family's front office — never in two places.
Figure 2. One Street, Many Houses. One company runs its buildings through an optional group; every phone, mailbox, calendar and setting is a hook on exactly one node of that chain; doorbells at the edge point at a node; tags, property owners, the vault and the registry sit beside the chain and are never walked.
Read it at
A company runs some buildings, and every phone number and mailbox hangs on one hook — at a building or at the company. When a phone rings, the doorbell says whose hook it is.
The company is the customer and the wall around its data; its buildings sit under it, sometimes through a group in between. Phone lines, mailboxes, calendars, logins and settings are hooks on one of those three places, and a doorbell row at the edge says which company each address belongs to. Property owners and tags sit beside the chain and are never walked.
Three node types form a chain — company → optional group → building — and everything a customer configures is an attachment on one node of it. An address head at the edge maps a phone number or mailbox to exactly one company and one node. Owners and managers are dated relationships on the building, not rungs; areas and regions are tags, not rungs. One resolver walks the chain; there are no modes.
ORG#<id>/PROFILE → GROUP#<id>/PROFILE {kind:'chain'} (walked, ordered by in_group.precedence) → PROP#<id>/META {organizationId}; capabilities and values are <node>/ATTACH#<kind>#<slot>#<startedAt> rows; ADDR#<channel>#<address>/HEAD {organizationId, scope} is the single conditional-put truth for an address; PROP#/REL#owned_by|managed_by|in_group#…#<validFrom> are dated relationships; GROUP# {kind:'area'|'region'|'brand'|'line_set'} are tags. The invariant: one node runs a building (Property.organizationId, one attribute), and every fact hangs on exactly one node — nothing under PROP# ever moves.
Three new kinds of row — attachment, address head, relationship — and four new prefixes (ADDR#, GROUP#, CRED#, PLINK#); everything else is a row we already have with two extra fields.
One building, one company that runs it — that is one attribute, so the database cannot store a second.
Camellia is this same picture with the doorbell pointing at the building; the hybrid operator has one doorbell on a group and two on buildings.
The org map the operator edits is a view over these rows, never a second truth — in 2026 a fixture file checked by validate-fixture, not a screen (decision 14).
Proof: Camellia loads as this exact picture with no groups and no relationships, and its calls come out byte-identical at every step — fixture F01 (Camellia), camellia-replay-byte-identical.
Each hook has a label saying what hangs there, who hung it and when — one thing per hook at a time, with the old label kept in a drawer.
Every proposed thing is a row on an existing shelf — a new sheet, never a moved drawer. Two pictures open here: the bookcase the rows live in, and three sheets up close. Four more are folded below for when you want them: the six literal rows as key strips, which keys are pinned and which are free, how a value is found, and which door answers which question.
Figure 3. The Bookcase. One table, seven kinds of shelf: four on the walked chain, three platform shelves. Every shelf starts with its own card and everything else is a sheet slid in behind it; the only line in the picture is the front door naming the company — every other reference is written inside the row, which is literally how the table works.
Read it at
Everything about a company, a building or a person lives in its own drawer. To change something, you put a new sheet in the drawer; you never move the drawer.
The database is one big cabinet with seven kinds of drawer: company, group, building, person, phone number, password, and "same person twice". Each drawer starts with its own card; a phone line, a setting, an owner or a calendar is a sheet behind that card. Changing who answers a number means adding one sheet, not moving anything.
One table; every node (ORG#, GROUP#, PROP#, PERSON#) is a partition whose first row is its profile and whose other rows are sibling rows — attachments (capabilities and values), relationships (dated edges) — exactly the pattern the property partition already uses for its settings rows. Three platform partitions sit outside the chain: the address head (which company answers this number), the credential secret, and the same-human link. The building partition is storage: conversations, tours and work orders are born there and stay there.
Seven prefixes, seven row shapes: <node>/PROFILE|META, <node>/ATTACH#<kind>#<slot>#<startedAt>, PROP#/REL#<kind>#<party>#<validFrom>, ADDR#<ch>#<addr>/HEAD, …/FN#<function>, CRED#<id>/SECRET, PLINK#<id>/LINK. PROP#/META carries organizationId + the PROPORG#<org> roster stamp on a sparse keys-and-stamps index — the one truth for who runs a building; ORG#/PROFILE carries mapVersion (structure) and valuesVersion (values), bumped in every map transact. Attachment and relationship rows under PROP# derive their org from META and never store it; ORG#/GROUP#/PERSON# rows store it. The invariant: zero partition moves — every mutation in the catalog is inserts plus attribute updates on existing shelves.
Seven prefixes: four chain shelves (ORG#, GROUP#, PROP#, PERSON#) and three platform shelves (ADDR#, CRED#, PLINK#); ORG#, PROP#, PERSON# exist today — the other four are new prefixes in the same table.
PROP#/META says who runs the building (organizationId + the PROPORG# stamp); no second row says it.
Today's rows (CONV#, TOUR#, WO#, UNIT#) are untouched and never move.
The registry (code, not a row) decides which sheets are allowed on which shelf.
Proof: move a phone line building → group → building on a test portfolio and not one row changes its key — rebind-line-inserts-only on fixture F05 (inserts + attribute updates only, PK/SK moved == 0). Depth: the rows.
{kind:'clara'}Clara answers; external = a line that is never pointed at the voice platform
value.language
['en','es']what the line can speak
startedAt
2026-09-15in the SK, so history keeps its order
endedAt
absentabsent = live
setBy
admin_ui · a personwho put it here
version
1optimistic bump
One live sheet per slot. A second is refused unless it names this one; then END + INS ride one transact.putAttachment → {status:'conflict', live} · opts.supersedes = startedAt
Written with the head. This row, the address head, the company's mapVersion bump and a MAPLOG# line are one transact.transactWrite [Put ATTACH · Put HEAD cond attribute_not_exists(PK) · Update PROFILE ADD mapVersion :1 · Put MAPLOG#]
address headPK ADDR#voice#+10004000970SK HEAD
organizationId
wswhich company answers
scope
ORG#wswhich shelf the number hangs on
claimedAt · claimedBy
2026-09-15 · a personwhen, and who
platformNumberId · agentId
…the voice platform's ids, written back by claimAddress
One company per number, refused by the database. The second claimant gets {status:'conflict', heldBy}.Put HEAD · ConditionExpression: attribute_not_exists(PK)
Refused until the pickup keys resolve. A claim on a fresh company lists the keys it needs — timezone, jurisdiction, emergency phone, maintenance route — before anything rings.claimAddress → {status:'refused', keys:[…]}
read
GetItem · never cacheda released number cannot answer for the old company for a second
SK FN#maintenance
scope · route
ORG#ws · {kind:'external', target}narrows one function of the number; read only once the function is known
Only the holder may narrow.ConditionCheck HEAD.organizationId = :ctxOrg
['leasing_line']may the company line's pooled hosts show this home — it changes who hosts, never who is answered
acceptedAt
2026-01-03the property owner's admin said yes; its view exists only after
validFrom
2026-01-01in the SK — a change of property owner is a new row
endedAt
absentabsent = live
organizationId
derived from PROP#/METAnever stored on a PROP# row — the runner is the building's one truth
index
GSI1 REL_PARTY#owned_by#ORG#situs / PROP#372-ember#2026-01-01the owner's own door (figure 8)
Only the runner writes it.Property.organizationId must equal the writer's company; an owner or manager cannot self-grant.putRelationship refuses otherwise · a party's view derives only after acceptedAt
Figure 4. Three Sheets. The three rows most of the design is made of, with every attribute explained once and every conditional write drawn as a lock: one live row per slot, one company per number, one runner per building. Rows are dated and signed; a wrong first guess is END + INS, never an edit that loses history.
Read it at
Every sheet says what it is, when it started and who wrote it. The drawer won't take a second sheet that says the same thing.
A row is a small form with a fixed header: what kind, which slot, when it started, who set it. Some rows carry a rule the database checks before accepting them — like "only one company may own this phone number". If the rule fails, the write is refused with a reason, not silently overwritten.
Three row types cover most of the design: an attachment (a capability or value on a node, dated, with setBy), an address head (one number → one company, unique across the whole platform), and a relationship (owner or manager, dated, with grants). Each carries its conditional write: a slot holds at most one live attachment; a number holds at most one head; a relationship is written only by the company that runs the building. Refusals are named (conflict, heldBy), never masked.
ATTACH#<kind>#<slot>#<startedAt> with endedAt absent = live; invariant (h) — ≤1 live row per (node, kind, slot) — is enforced in putAttachment (supersedes ⇒ END + INS in one transact). ADDR#<ch>#<addr>/HEAD is written attribute_not_exists(PK) in the same transact as the attachment and ADD mapVersion :1; FN#<function> rows carry a ConditionCheck on HEAD.organizationId. PROP#/REL#<kind>#<party>#<validFrom> stores partyOrganizationId and derives the runner from META; the writer rule is Property.organizationId === ctx.organizationId, and a party's view derives only after acceptedAt. The Update item in transactWrite (the first commit of step 1) is what makes every "same TX" claim above true.
Every row is dated (startedAt in the SK, endedAt absent = live) and signed (setBy), so a wrong first guess is END + INS, never an edit that loses history.
Conditional writes are database rules, not code checks: one head per number, one live row per slot, one runner per building.
A refusal comes back named — {status:'conflict', heldBy} — and the caller decides; nothing is silently overwritten.
The FN# sub-row exists only where one number carries several functions; most numbers have a head and nothing under it.
Proof: a second claim on the same number is refused by the database, and superseding a row leaves exactly one live — second-claim-refused-by-store (a second head on +10002000142 → ConditionalCheckFailed) and one-live-row-per-slot (nightly; supersedes yields exactly one live row). Depth: the rows.
The Label on the Hook → the six literal rows as key strips, and END + INS in one transaction
Figure 5. The Label on the Hook. Six literal rows: the building's META carries who runs it; attachments carry their kind, slot and start date in the key; the address head is written only if absent; relationships are dated; the secret is referenced, never copied — and a change of value is END + INS in one transaction.
Read it at
Every fact is a card with a name and a date. When something changes, we write a new card and mark the old one finished — we never throw cards away.
A row has a key that says which node it belongs to and what it is, plus its values. The phone line's row lives under the company; the building's row says which company runs it; the doorbell's row at the edge says which company owns the address and can only be written once. Changing a value means ending one row and inserting another in the same instant.
Six row families cover the whole design: the building's META (carries the runner), attachments (the hook's kind, slot and start date are all in the sort key), the address head (a conditional put — the database refuses a second claim), relationships (dated, on the building), and credentials (one secret, referenced by many calendar rows, refreshed by exactly one writer at a time). Attribute updates and inserts are the only writes; nothing is moved between partitions.
PROP#<pid>/META {organizationId, lifecycle, ROSTERPK: PROPORG#<org>} is the wall's one truth; <node>/ATTACH#<kind>#<slot>#<startedAt> gives every hook an identity so END + INS of a slot is legal; ADDR#<channel>#<address>/HEAD is written with attribute_not_exists(PK) in the same transactWrite as the attachment and the mapVersion bump; PROP#/REL#<kind>#<party>#<validFrom> derives its org from META and never stores it; CRED#<id>/SECRET is referenced, never copied, and rotated under a conditional update on version plus a single-flight lease. Invariant (h): at most one live row per (node, kind, slot), enforced in the write (supersedes names the row it ends), never in the read.
Four new prefixes (ADDR#, GROUP#, CRED#, PLINK#); everything else is a sibling row in a partition that exists today.
The sort key carries the start date, so every "wrong first guess" in chapter 07 reverses with END + INS.
The doorbell row is written only if absent — uniqueness is the database's job, not a file's.
History is kept, not piled: an ended row moves to a HIST# sibling in the same partition on END, so the live prefix holds exactly one row per slot.
Proof: the database itself refuses a second company on a number, and a nightly check finds exactly one live row per hook — second-claim-refused-by-store (a second HEAD → ConditionalCheckFailed) and one-live-row-per-slot.
Pinned or Free → which keys belong to one building and which inherit
Figure 6. Pinned or Free. The x-axis is where a key may be written; a chip that spans one column is FIXED, a chip that spans three is DYNAMIC. The four bands are the classes: POLICY (company only, locked from above), PARAMETER (anywhere, nearest wins), IDENTITY (building only, pinned), and the compliance floor under everything.
Read it at
Some facts belong to one building and can't be borrowed, like its address. Other facts can be set for the whole company and every building follows, unless one building sets its own.
Every setting has a label saying where it may be written: only on the building, or on the building, its group or the company. "Only the building" is fixed — a building never borrows a neighbour's address or tenants — while "anywhere" is dynamic: office hours set once at the company apply everywhere, and one building can override its own. A few company rules are locked so buildings can't change them, and the law sits under everything.
The registry classifies every key. IDENTITY keys (address, timezone, tenants, units) are writable at the building only and never inherited — the real incident was a building borrowing a sibling's identity. PARAMETER keys (hours, fees, vendors, schedules, escalation owner) are writable at building, group or company, and the nearest row wins. POLICY keys (send tiers, fair-housing text, retention) are company-only with a write below refused, and the compliance floor (TCPA hours, notice periods) is written by nobody and clamps last.
KeySpec { class, writableAt, direction, merge, absentMeans, lifeSafety?, inheritOnlyWhen?, lookupVia?, condition? } in code, drift-pinned: DYNAMIC = writableAt.length ≥ 2, FIXED = exactly ['property']. Tenants, units and leases are not keys but KINDS rows kind:'entity', writableAt:['property'] — "you don't want tenants in companies" as a refused write. org.timezone / org.jurisdiction / emergency_phone are PARAMETER with inheritOnlyWhen:'home_unknown', so the G1 company-line pickup does not refuse and a pinned building never borrows them. Narrowing a class after a customer has authored against it costs a scripted sweep — which is why the class list is the one irreversible artifact and Gera signs it against the step-2a PR.
Four classes, one axis: where a key may be written decides FIXED vs DYNAMIC; the class decides who wins when two rows exist.
IDENTITY never inherits; a building without its own application_link refuses rather than borrowing one.
POLICY is written at the company (or a chain group on acquisition); a write below is refused at write time, not masked at read time.
escalation.owner ships DYNAMIC (reversing the Sep 1 classing) — the row Gera signs.
Proof: writing tenants at the company, an application link at the company, a policy below the root or over a lock is refused every time, with the conflict named — registry-closed-and-typed; KEYS ∪ KINDS ∪ REL_KINDS ∪ FUNCTIONS enumerates 100% of the literals in code and on this page. Depth: the key registry.
The Walk → where a value comes from: nearest wins, locks walk down
Figure 7. The Walk. Four stops, two walks on one key: nearest wins for a parameter (the arrow stops at the first live row), ancestor wins for a policy or a lock (the arrow reaches down and the building's row is named as masked). Every answer is a tuple that names the node that set it.
Read it at
To find a setting, look at the building first, then its group, then the company; the first one that has it wins. Some company settings are locked, and then the company wins even if the building wrote something.
The resolver walks up: building, group, company, and finally a built-in default from the registry. For most settings the nearest row wins, so a building can override its company. For locked or policy settings the walk runs the other way — the company's row wins and the building's row is named as overruled, never silently ignored — and every answer says which place set it.
One fold per key with a fixed order: PARAMETER = nearest live row wins (replace, never deep-merge); a locked ancestor inverts that and the resolver reports maskedBy[]; POLICY rows below the company cannot exist because the write was refused; IDENTITY reads the building only; the compliance floor clamps last. No row anywhere means the registry's absentMeans — a stated constant or a named refusal. There is no property.x ?? org.x anywhere in code; the fold is the only place precedence lives.
resolveEffective(ctx, keys, at) loads live ATTACH#setting# rows for the chain [PROP#, GROUP#…(precedence asc), ORG#] with ≤3 parallel consistent Queries cached per (node, valuesVersion), then per key: hoisted inheritOnlyWhen check (refuse 'home_known' on a pinned building), lookupVia shortcut (branded_as | line), tier-illegal → loud tier_illegal_since_v<N>, then the class switch — PARAMETER nearest, farthest locked ancestor wins with maskedBy; POLICY the org row; IDENTITY chain[0] if it is a building; floor via clamp(FLOOR(spec, jurisdiction), value). Output Effective<K> = { value, setAt: NodePK | 'default' | 'floor', via?, locked, class, maskedBy?, refused? }. Invariant: every effective value has provenance, and a lock or reclassification is visible on the first read, never masked.
Two directions, one function: nearest wins for parameters, ancestor wins for policies and locks.
Every answer is a tuple naming the node that set it (setAt) — or 'default' or 'floor', never "somewhere".
A lock names what it masks (maskedBy[]); a policy write below the company was refused before it could exist.
The floor clamps last, from the building's jurisdiction — or the stricter of the company's and any candidate home's while the home is unknown.
Proof: every setting on every test building says which place set it — never "somewhere" — and the pickup keys resolve at the company while the home is unknown: every-effective-value-has-provenance (every key × every fixture building resolves to a chain node, 'default' or 'floor'; no ?? org.x in code) and pre-pin-keys-resolve-at-org (a pinned building refuses home_known). Depth: the walk-order pseudocode.
The Doors → which index answers which question; no "every building" door
Figure 8. The Doors. Each question has its own door with the key you knock with written on the plate; the company is in the key on most doors, the owner's door is keyed by the owner, the front door is the one door with no company on it on purpose — and there is no "every building" door for application code.
Read it at
Each question has its own door, and the company's name is on the door. You can only open doors with your own name on them.
The database answers questions through indexes, and each one is built so the company is part of the key. "Which buildings do we run?" is one knock on our door; another company's buildings are behind a different door we cannot open. The phone-number door is the one exception — it has no company on it, because the number itself decides which company answers.
Five index keys and two direct reads: the sparse roster index PROPORG#<org> (ids and three small stamps, never a token), GROUP_ORG#<org>, GROUP_MEMBERS#<gid> (org checked on the group's profile first), REL_PARTY#<kind>#ORG#<party> (the grantee reads its own key), CONVPROP#<pid> (a home's threads), the ADDR#…/HEAD GetItem (the front door, context-less and uncached), and the node's own partition (begins_with ATTACH#). There is no platform-wide "all buildings" query in application code; isolation is what exists behind your door, not a filter applied after the fact.
The roster lives on a new sparse GSI whose projection is INCLUDE {lifecycle, answerable, asset_type} — so a cold mint on a 400-home company reads ids and stamps, ≈ 50 KB, and derives reach from the answerable stamp with zero per-building reads; saveProperty's own item literal stamps it, so no full save can erase it. GROUP_ORG#/REL_PARTY#/GROUP_MEMBERS# are GSI1 stamps with ProjectionType: ALL; CONVPROP# is a new sparse GSI8 added online (four precedents). GROUP_MEMBERS#<gid> carries no org copy on purpose — the reader checks GROUP#<gid>/PROFILE.organizationId === ctx.organizationId before the Query, so a merger re-stamps nothing. Conversation GSI1/5/6 are re-keyed org-first (PHONE#<org>#<e164>, CONV_RECENCY#<org>, <org>#<personId>) in the step-5a bulk stamp. Invariant: every Query or Scan in a tenant context returns zero items from another org's rows — asserted by Count, not by trust.
Most doors have the company in the key; the rest check the company before knocking (GROUP_MEMBERS#) or are keyed by the party that may read them (REL_PARTY#).
The front door (ADDR#…/HEAD) is the only context-less read in the system: one GetItem, never cached, and a miss refuses with zero further reads.
The roster door returns ids and stamps, never a calendar token: a 400-home cold mint is ≤6 reads ≈ 50 KB, and reach costs zero reads keyed by a building.
Adding a door is an online index add with four precedents — no table rebuild anywhere in the ladder.
Proof: two look-alike companies are seeded and every query is counted — zero rows from the other company, every time — isolation-gauntlet T1–T19 (Count on every Query/Scan), roster-index-carries-no-secret, and the step-0 parity drift test getProperties(orgId) === Query PROPORG#<org> for every org. Depth: the wall · the indexes.
04 · One resolver
When the doorbell rings you check one nameplate to know whose house it is; when you need the house rules you walk from the room to the house to the family office, and the nearest posted rule wins unless the parents locked it.
Figure 9. The Nameplate and the Walk. Stage 1 goes up: one uncached nameplate read decides the company, a miss refuses, and if the company has more than one home the same Clara asks and binds it once. Stage 2 goes down: building → group → company → registry default, nearest hook wins unless a locked ancestor overrides it — and the answer names who set it.
Read it at
First the computer reads one nameplate to know which company the call is for, and if there is no nameplate it politely hangs up. Then it looks for the rules, starting at the building and walking up to the company.
Stage one goes up: read the doorbell row once, refuse if there is none, and if the company has more than one home, Clara asks which one and writes it down once. Stage two goes down: for each setting, take the nearest hook — building, then group, then company — unless the company locked it, in which case the lock wins and the hidden value is named. Before step 4 a company line answers company facts and takes a message; the question, the tour and the work order arrive when the shared agent gets tools.
One function, two stages. Up: the address head decides the company with one uncached read; a miss refuses with zero further reads (the life-safety lane is a separate branded context, not an if); if the reach is one building the home is known at pickup, otherwise the same agent asks — area first above six homes, then the available homes in that area — and bindHome is the only writer of the home. Down: the chain rows are folded per key by class — identity never inherits, policy is ancestor-wins, parameters are nearest-wins with ancestor locks — and every answer names the node that set it.
mintFromAddress → ADDR#/HEAD GetItem (never cached) → TenantContext {organizationId, roster, reach, propertyId | null, mapVersion} where roster and reach both come from the one sparse roster Query (reach = a filter over the answerable stamp, zero per-building reads); |reach| === 1 ⇒ propertyId at mint, else home_state:'unknown' and bindHome writes conversation.propertyId + GSI8PK. resolveEffective(ctx, keys, path) loads ≤3 parallel consistent ATTACH#setting# Queries cached per (node, valuesVersion), hoists inheritOnlyWhen:'home_unknown', refuses any row outside writableAt loudly, folds by class (IDENTITY | POLICY | PARAMETER | COMPLIANCE_FLOOR), and returns {value, setAt, locked, class, maskedBy[]} or refused: {reason}. Invariant: every effective value has provenance — setAt is a chain node, 'default' or 'floor', never a property.x ?? org.x in a caller.
A cold mint on a company line is ≤6 reads ≈ 50 KB — nameplate, profile, the roster's ids and stamps, and ≤3 chain Queries — with zero reads keyed by a building; cost is bounded by chain depth, never by company size.
A missing nameplate refuses — no guessing; the only carve-out is life safety, and it is a type.
Locks are data, not read-time masks: a locked ancestor wins and the resolver names what it hid.
Owner is never a rung; owner consent changes who hosts a tour, never who is answered.
Proof: every answer names who set it, the company line works before a home is known, and the read count is on a ledger — every-effective-value-has-provenance, pre-pin-keys-resolve-at-org (the G1 pickup keys resolve at the company while the home is unknown), cache-exact, answerable-stamp-exact.
Every family has its own locked front door; a neighbour who owns a house you run gets the monthly report slid under their door, never a key to yours.
Figure 10. Every Family Its Own Front Door. Three companies behind three walls. The only things that cross a wall are an invited membership scoped to one building (inside the runner's wall) and a report row pushed into the reader's own partition; the middle gutter is empty on purpose.
Read it at
Each company has its own room, and nobody can look into another company's room. If someone else owns a building, we put the numbers about their building into their room — we do not let them into ours.
The company is the wall, and every login sees only what is inside its own company's wall. A manager who runs a building for its property owner is invited in as a member for that one building. A property owner who is a different company never reads our rows; what they were granted is written into their room by our workflow, and that is what they see. Since Sep 8 that grant is wider than a single report — the numbers, the occupancy, the work orders and the documents for the buildings they own — and it is still their room, not ours.
Isolation is a type, minted in one module: a TenantContext carries the company, the roster it runs and the reach a caller may name; a property owner gets a narrower ReportsContext over rows pushed into its own partition — the grant list on the relationship says which projections are assembled there, and no member of that list is a read of the runner's rows; admins get nothing until they mint an audited AdminContext. The view a party gets is the rows that exist on its side, not a filter over ours. Opt-outs cross the wall by law; identity and opt-ins do not.
TenantContext {organizationId, roster, reach, propertyId|null, actor?, mapVersion} and four siblings (AdminContext, ComplianceContext, LifeSafetyContext, ReportsContext) are branded interfaces from mint.ts; every PROP# read is gated on ctx.roster.has(pid) before the key is queried and returns the byte-identical 404 shape; ORG#/GROUP# partitions are gated on organizationId === ctx.organizationId; conversation GSI1/5/6 are org-stamped. REL#managed_by {acceptedAt} authorises MEMBERSHIP# {invitedForParty} + PersonRole{pm, {propertyId}} inside the runner's org; REL#owned_by {grants} — defaulting since 2026-09-08 to ['reports','financials','occupancy','work_orders','documents'], with 'conversations' legal but absent unless deliberately granted — authorises IReportsRepository.push for the projection asked for (ConditionCheck on the live row, the projection ∈ grants); it authorises no read of the runner's partition, so getProperty from a party context stays the byte-identical 404. That ConditionCheck on the live relationship row is what lets the runner's workflow write ORG#<owner>/REPORT#<pid>#<period>; which row carries each of the other granted projections beside REPORT# is an honest limit, not settled by the decision. Invariant: no login reads another company's partition — isolation-gauntlet T1–T19 counts every Query's items. Admin bypasses are on the delete list; a tester is an ordinary admin inside the real company.
Yale 25 stays under JP&Co; nothing moves. ConAm's people are members of JP&Co scoped to Yale — one mechanism.
A property owner sees exactly the projections its owned_by row grants — by default reports, financials, occupancy, work orders and documents (decided Sep 8) — assembled in its own house and never as a read of ours; the push is authorised by that row, not by trust. Threads are grantable and off by default, so "no conversations" is a default a manager may deliberately change, not a wall; people and settings are not on the list at all.
Consent is split: an opt-out follows the human; an opt-in stays with the company that captured it.
The same human at two customers is two Person rows and one admin-only link row (rec; decision 1).
Proof: a test dials every company and counts what leaks — the number must be zero — isolation-gauntlet (T1–T19: zero foreign items per call, counted), consent-split (T11a/T11b), reports-push-authorised-by-row.
The tour host is chosen like chores on a fridge chart — who is on today, whose calendar is free, closest first — and the chart is rules on paper, not somebody's memory.
Figure 11. The Chore Chart. People own their calendars (one secret each, in the vault); the building holds who covers it and when, and a rule for choosing between them; one pure function reads those rows and the calendars and offers a slot with a named host and the reason.
Read it at
Each person keeps their own calendar, and the building has a chart of who works when. A small program reads the chart and the calendars and picks who gives the tour.
A person's calendar is a hook on the person, not on the building, with its secret kept in one vault. The building holds two kinds of rows: who covers it and when, and a rule for choosing between them. One piece of ordinary code reads those rows and the calendars and offers a slot with a named host and the reason — Clara never guesses at rotas.
Calendars are attachments on PERSON# referencing one credential row; assignments (coverage, calendar ids, priority) and schedule rules (fixed, round-robin, primary/secondary, calendar-owner) are attachments on the building; an override is a higher-layer rule row with a date window. findTourCandidates, whoIsOnCall and nearbyAlternatives are pure functions with unit tests and no evals; a building with none of these rows degenerates to today's availability check; ending a person's role ends their assignments in the same transact, so the on-call list never pages a departed employee.
PERSON#<pid>/ATTACH#calendar#primary#t {provider, externalCalendarId, credentialId → CRED#, readMode, writeMode}; PROP#<pid>/ATTACH#assignment#<personId>#<function>#t {coverage[], calendarIds[], priority, homeAreaId}; PROP#<pid>/ATTACH#schedule#<function>#<ruleId>#t {kind, members[], window, layer, fairnessScope}; HOLD#/LRB# rows for holds and round-robin fairness; the tour snapshots ruleId. Resolution order is fixed — chain → hours keys → hosts at the slot instant → rule → consent filter → free/busy ∩ holds → travel buffer → rank — and poolingConsent on owned_by is consumed here (hosts, never reach). CRED# refresh is a conditional update on version under a single-flight LEASE. Invariant: no prompt contains a calendar id, a rota or a distance (scheduling-is-pure). The same person's calendar at a second customer is free/busy only; tour rows never cross the wall.
One secret per calendar, in a vault row; rotating it touches one row, and only one refresher runs at a time.
Situs's three leasing people have no calendar today (a whiteboard), so they start on a PropFlow-hosted one (provider:'propflow') — the fallback, not the goal: Gera's Sep 7 answer is an adapter that plugs into whatever calendar a person already uses, and moving them onto their own later changes the provider on the same row (decision 8). A declined invite opens an operator task; the hold stays until re-hosted.
A host above the building is dropped unless every property owner of the building consented to pooling — the home is still answered.
Camellia has none of these rows and behaves exactly as today.
Proof: the host is picked by plain code you can unit-test, and no prompt ever sees a calendar — scheduling-is-pure (T1–T6, E1/E2 as unit tests; nearbyAlternatives on F01 = []), role-end-cascades, cred-refresh-single-flight, and fixture F02/F06 (Yale, the floating assistant).
Moving the phone from the house to the front office is moving it between hooks — nothing inside the house gets packed into boxes.
Figure 12. Moving the Phone Between Hooks. Pick a change above and the exact rows light up; the card below says what was written, what it cost and how it reverses — + inserted, ended, ~ updated. Moves is zero on every row; a row whose key changes is what would make this a migration, and there are none.
#1 · Line moves building → company
Rows written
Moves · fields · deploys
Operator clicks
Wrong first guess
Reverse
Ledger
Boundary, once: a new placement or value is data; a new key or kind is code. Every change also writes a MAPLOG# row, so Undo is one click (catalog #18). In 2026 "clicks" means a line in the fixture file checked by validate-fixture; the org-map screen is priced for 2027 (decision 14).
Read it at
When something changes, we add a new card and mark the old one done. We never carry the building's stuff to a new room.
Every change in the list is inserts plus a few updated fields, in one transaction. Nothing under a building is ever moved to a different key, and no new columns are needed. The two big ones — merging companies, or a building changing who runs it — are scripts rather than clicks, and even those move zero building rows. Every change writes a log line, so undoing the last one is a click.
Because every capability is a dated attachment on a node and every address is a head row at the edge, re-binding is an attribute update plus END + INS. Ten real changes are drawn: each lights the rows and states moves (always zero), schema fields (always zero), the operator's clicks and what a wrong first guess costs. Every transact also writes a MAPLOG# row with its reverse items, so "Recent changes → Undo" replaces hand-written inverses. Once step 3b lands, the two config files become dumps of the rows, so numbers stop costing PRs.
Catalog #1: UPD ADDR#voice#…/HEAD.scope → ORG#ws (cond scope = PROP#old AND organizationId = :ctxOrg) + END PROP#old/ATTACH#phone_line#…#t0 + INS ORG#ws/ATTACH#phone_line#…#t1 + ADD mapVersion + Put MAPLOG#<v> in one transactWrite (the Update item is step 1's first commit; a transact holds ≤48 descendants, above that a named chunked sweep). #7 is the merger: chain group + re-hung attachments + in_group {precedence} + Property.organizationId re-stamp + the identity-pool rehome script. #8 is the transfer: a new PROP# for the incoming runner, the old frozen lifecycle:'transferred'. Invariant: zero partition moves, zero schema fields for every row in the catalog — rebind-line-inserts-only hashes every pre-existing row before and after.
Eighteen changes in the reference catalog; the ten an operator asks for most are in the picker.
"Moves" is zero on every row — a row whose key changes is what would make this a migration, and there are none.
The merger and the transfer are scripts with a dry-run, a pre-image and a revert by run id; both happen every year in this domain.
Gera's question — "can they just update one thing?" — is answered per row in the card's clicks cell.
Proof: move a line away and back on a test portfolio and every original row is still there, byte for byte, with zero keys moved — rebind-line-inserts-only (seed F05: before ⊆ after byte-identical, PK/SK moved = 0), transfer-is-two-partitions, map-dump-stable, maplog-undo-exact.
The same hooks work whether the family runs three houses, a shopping arcade, a warehouse, a dorm or four hundred cottages — the size is a number, not a different kind of house.
Topology · vertical
Commercial
Industrial
Short-term rental
Student
Affordable
Mixed-use
Retail
Senior · housing
Senior · care
Co-living
Build-to-rent
HOA / condo
Mobile-home park
Corporate
Self-storage
One company → many buildings
Many property owners → one manager
One property owner → many managers
Hybrid — some own numbers
JV + manager + brand (N:N)
Franchise / brand above operators
Asset manager above N managers
Co-management by function
Master lease / sublease
A building changes operator
A company splits
stretch 1 = fits — the rows existstretch 2 = registry lines — new keys or values, no new rowsstretch 3 = needs a named primitive — the chain and the wall holdrefused by design — senior care; Clara does not do care
Figure 13. The Same Hooks, Any Building. Eleven portfolio topologies against fourteen verticals (senior living shown as its housing half and its care half), Gera's six first. Click a cell for the literal rows, the verdict and the fixture; the card below the figure fills in. The topologies cost 1–2 everywhere; what stretches is what sits below the building (unit ownership, unit program, the person's role on the home) and the closed lists written for one vertical.
Scattered homes, one line × Multifamily
Topology rows
Vertical delta
Verdict
Fixtures
Read it at
Big or small, houses or shops, the same shelves and hooks work. Some kinds of places need one new kind of card, but the shelves stay.
The chain, the doorbells and the hooks do not care what kind of building it is, so most portfolios fit as they are. Commercial and warehouse buildings need a few new settings, and short-term rentals and affordable housing need one new kind of record — a stay, a unit's program — because the fact lives below the building; a brand above independent operators needs one new primitive, the push. Nothing needs a second copy of the chain, and the one honest "no" is care: Clara does not do it.
Eleven topologies (one-to-many at every size; many owners; one owner, many managers; hybrid; a JV with a brand; a franchise above operators; an asset manager above managers; co-management by function; master lease; an operator change; a split) times fourteen verticals. Green cells need nothing; blue cells need registry lines (keys with defaults, an enum value, no rows); yellow cells need one property-bound entity or one reserved primitive the residential model lacks — unit-level ownership, a stay, a program, the cross-org push. The one red column is deliberate: senior care is a refused function, because a refused function is safer than a staged one; senior housing beside it is yellow, one named primitive.
Verticals enter the model through asset_type (IDENTITY), capability_stage.<function> (PARAMETER) and answerable(), through FN#<function> {route} on the address head, and through registry keys with absentMeans; a COMPLIANCE_FLOOR clamp and the reserved condition {program | leaseFormId | unitType | assetTypes} attribute cover regulated stock. Entities are KINDS rows kind:'entity', writableAt:['property'] — a stay or bed kind is a registry entry plus a partition-local row family, never a change to the chain, the head or the contexts. Five reservations now keep every door open at zero code: owned_by at UNIT#, a FUNCTIONS table, the push tokens (brand_party, setBy.source:'party_push', franchised_under), occupancy.role, and structure as a group kind. Invariant: the trunk (chain + head + contexts) is invariant under vertical; only keys, kinds and entities vary — registry-closed-and-typed fails on any literal outside KEYS ∪ KINDS ∪ REL_KINDS ∪ FUNCTIONS.
165 cells, eleven red — all in one column, senior care: Clara does not do care, by construction. Senior housing is the yellow column beside it.
Blue means keys (stretch 2): commercial, student and retail are new defaults, an enum value or one role, not new rows.
Yellow (stretch 3) is one named primitive the residential model lacks, in exactly four places — a brand above operators (the push), short-term rental (a stay), affordable (a unit's program), senior housing (a responsible party) — and the chain and the wall are untouched.
"Adaptive enough to expand quickly" is a claim about the registry, not the storage — which is why Gera signs the class list (decision 2) and the five reservations ship with it.
Proof: every shape on the grid loads as plain rows with no setup script — fixtures S1–S10 + F10 today (seeding-calls-zero-scripts), F11–F35 as the permutation register, and registry-closed-and-typed.
You rewire the house room by room while the family keeps living in it — each room is switched over only after its lights are proven to work on the new wiring.
Fede's frame, Sep 7. Week 1 — by Fri Sep 12 — ships the smallest leasing increment that already works in the centralized setup: the Western Slope leasing line takes leasing inbound on voice and text, Clara captures the guest card, the card lands on the listing's property. The next three weeks, Mon Sep 14 to Fri Oct 2, build on that. On this ladder's own arithmetic that window holds the dark steps 0–1 and the Western Slope agent on real PMS data; the fan-out flip keeps its November dates. How the two fit is D10.
Figure 14. Rewiring While They Live In It. 16 steps in 12 cards (2a + 2b, 5a + 5b and 6a + 6b share a card), 83–121 engineer-days summed from the table (cards 5 and 6 carry a split chip, so 0–6a sums to 50–73 on the picture); the accent pins are the proof — each fitness function at the rung where it closes, none after 3b; the first four rungs touch no live path. Every date is a working-day count from Tue Sep 15, low / high. Sep 12 is on today's rails and needs none of this; the commitment stops at 6a (50–73 days), and 5b, 6b and 7–10 are the road, not the promise.
Read it at
We build the new way in small steps, and each step is checked before we switch it on. The first steps change nothing anyone can see.
16 steps in 12 cards: the first four write new rows that nothing reads yet and prove the new lookup gives the same answers as the old one. Then each door is switched one address at a time, Camellia last, with a test that Camellia's calls stay identical. Friday's launch uses today's rails on purpose, texting on (decided Sep 7); the new rails carry Western Slope's shared line from mid-November to mid-December with one engineer, and the promise stops at step 6a — the rest is the road.
Expand → offline parity replay → one facade cut per key or per address behind a flag → strip. Steps 0–2 are dark (rows written, read by nothing); 3a–5a flip the edge, the voice state and the conversation rows; 6a adds relationships and the pushed reports and closes the commitment; 5b, 6b, 7–10 are keys, calendars, workflow pins and deletions. 83–121 engineer-days summed from the table; committed = 0–6a at 50–73, backlog 33–48. The dates on the rail are the only dates claimed: Sep 12 on existing rails, Oct 13–23 for 0–2, Nov 18–Dec 17 for the shared line at one engineer (Nov 5–27 voice-first, Nov 6–Dec 1 with many hands), Nov 19–Dec 21 for Situs's maintenance line.
Step 0 mints Property.organizationId, the sparse PROPORG# roster index with its {lifecycle, answerable, asset_type} stamps, and ORG# rows; 0.5 gives the harness expect:'RED'|'GREEN' + closesAt, a read ledger, the parity replay and validate-fixture; 1 adds the Update item, ClientRequestToken and CancellationReasons to transactWrite, the row families, MAPLOG#, the mirror tree; 2a ships KEYS/KINDS/REL_KINDS/FUNCTIONS with every key PARAMETER + today's constant, 2b the fold in offline replay; 3a/3b privatise the helpers and flip HEAD per address (the claim refusing until the pickup keys resolve); 4 lands home_state, the two-step area ask, bindHome, mintFromConversation and the shared agent's tools; 5a stamps conversations; 6a relationships, views and the authorised report push. Invariant: at no step do two live read paths exist, and every step's PR ships the mirror tree (mirror-tree-parity, green on every step); rollback at every step is a flag while writes stay on the old rows.
Sep 12 is on today's rails, texting on (decided Sep 7), and needs none of this — the tripwire counts what lands in the umbrella from now (Gera, Sep 10), and the re-key script's dry-run is on file by Sep 19.
The never-migrate proof is complete at 3b, not at 10: the fitness functions that are the proof close at 1, 2a, 2b and 3b, with Camellia green at every step — which is why the commitment stops at 6a.
The Western Slope shared line is 3a → 3b → 4 → 5a; Situs's maintenance line is 3a–4 plus a 5–8-day slice pulled forward from 7 and 8.
"A second engineer" is not the parallelism this team has: the many-hands steps (3a, 5b, 6b, 7, 10) fan out to agent workers under Gera's review; a hire is productive from Fri Nov 13 and helps only the high bound (decision 16). Days are counts × judgment, not measurements.
Proof: Camellia's calls come out byte-identical at every step, and the never-migrate proof is complete by step 3b — camellia-replay-byte-identical (the control) and mirror-tree-parity; the proof itself is map-dump-stable (1), second-claim-refused-by-store (1), registry-closed-and-typed (2a), every-effective-value-has-provenance (2b), rebind-line-inserts-only (3b).
Seventeen forks in the road; each has a suggested way, one person whose call it is, and a stretch of road that opens once they choose.
What is decided and what is not. As of 10 Sep, G1, five further rulings (A10 · The review) and the eleven answers in the green banner below are decided; on the older cards below, only G1 was decided at the time of writing: voice runs one shared agent set, and a company-bound line makes Clara ask which building (Fede, Sep 5). Four adjacent items are decided on their own surfaces and cited as such: the v1 front-door agent is one coupled agent with no transfers; Western Slope leasing goes live Fri Sep 12 on existing rails; the Western Slope leasing number is bought; the mail-provider integration is paused. Five more were decided on Slack on Sep 7 (Gera and Fede, #agent-smith) and are cited here as such: the Western Slope leasing line stays on, texting included; scope is leasing only — guest cards from prospects, no tenant support; the fan-out rails are built in parallel with the onboarding work and tested on extra PMS properties Gera creates; the portfolio test line is bought (PR #7285); number purchases are pre-approved. Three more were decided by Gera on the evening of Sep 8 and this page states them as decided, not proposed: (1) the property owner's grant widens from a report stub to reports · financials · occupancy · work_orders · documents by default — portfolio intelligence, read in the property owner's own house, scoped to the buildings they own — with conversations grantable and off by default, tenant privacy being the reason for the default rather than a prohibition; (2) delegated administration with a privilege ceiling — manage_roles is a grant held by people, no escalation, peers may edit each other, only an org_admin may hand manage_roles on, and at least one live administrator always, the last of whom cannot self-remove (E8); (3) the vocabulary — "owner" is retired as a bare word in favour of org_admin, property owner and PMS credential holder (the limits pane). The rules are decided; the rows that encode them are proposed like every other row here. Fede's frame for the work: the week-1 increment must itself work in the centralized-leasing setup, and everything in the next three weeks — Mon Sep 14 to Fri Oct 2 — builds on that foundation. What that does to the ladder is D10 (the road). Every card below is a proposal — recommendation first, the question second, on purpose. Answers live on the study's decision table and the decisions surface; this page keeps no second store.
Ruled 2026-09-09/10 — eleven answers, and where each one landed. Each was raised as a block and answered in the decision ledger, so this page states them as decided rather than proposed. (1) Isolation before renewals (b88986177): the wall — P1’s roster gate flipped from observe to refuse before the first foreign login — precedes any renewals work for the first signed client (A4). (2) The vocabulary switch is a hard cutover (b89001763): one clean switch inside a maintenance window, not a bake period in which both vocabularies read (P00). (3) The acting-operator window (b89000051, design shape C): mint the operator’s org now with no accounts under it; the building is one property row in that org’s house; the stand-in administers it under a dated mandate; every value the stand-in writes is attributed to them, and the customer confirms those values at handover. Superseded in part, 2026-09-10:D-0910-1 keeps the house and dated mandate, replaces this fallback with PropFlow defaults, and leaves default eligibility open. And DISPUTED since 2026-09-10 18:01 CDT: the founders’ standup that evening talked the acting-operator circumstance down (permalink); the dated-mandate / handover / vocabulary half is contested and open at b89279042. The house and the named admin are not in dispute. (4) Pre-wall cohort visibility is allowed until the roster gate flips from observe to refuse (b89007120) — and ends there. It is a dated window, not an open-ended exemption. (5) and (6) Two properties may share one street address (b89007320, b89007922): they are told apart by proving their scopes do not overlap, never by editing the address into a different string (A10). (7) Life safety for a caller with no residency pages the company whose line it is (b89007521), not the platform pager (the carve-out). (8) Default role permissions (b89007944): PropFlow ships a conservative set and each customer widens it; the detailed contents are deliberately deferred until the features they cover land. (9) The building’s own property-system login wins (b89009726) over the company’s job-specific one. (10) Archiving is PropFlow staff only, and it is a soft state that retains the data — whether an archived org’s addresses are released is still open (N6, A4). (11) The picker-and-link rules are written during the phases, alongside the wall, rather than settled up front. Four screen-level rulings ride with them: the dashboard shows two separate numbers and never blends buildings we run with report-only ones; company selection caps at eight; observe mode belongs on a building or a company, never on a phone number; and two buildings behind one line may use different property-system accounts (A6).
Founder rulings · 2026-09-10. These five rulings supersede conflicting proposals below, including shape C’s second half. The older material stays as the record of how the design got here; a ruling is not evidence that its implementation or stress tests have passed.
D-0910-1 · A fourth shape: the operator’s house, PropFlow defaults second half disputed
DISPUTED — and this page does not settle it. Read this before the ruling below. D-0910-1 was raised 00:21 CDT 2026-09-10 (b89017673) and answered 00:35. At 18:01 CDT the same day — about seventeen and a half hours later — the founders’ standup talked the circumstance down, all three agreeing (#transcripts, 2026-09-10 18:01 CDT). Two of them, verbatim:
“This hybrid access is not gonna exist in the real world… we’re acting as the temporary custodian of the Yale account until we onboard ConAm. We don’t need to be solving for that circumstance. It’s not gonna really exist in reality.”
Sean · founders’ standup, 2026-09-10 18:01 CDT
“I would just move what we have for Yale Station to a new org for ConAm. Dara be the admin. Instead of… all the fancy permissions and hierarchies. We’re building this like an enterprise solution that we don’t need yet.”
Fede · same standup
What is NOT in dispute — the design’s first half. “Mint the operator’s house now, put the building inside it, name the admin” is almost word-for-word Fede’s own proposal in that standup. It stands.
What IS in dispute — the second half. The dated mandate, the stand-in attribution, the single named handover mechanism (D-0910-5), the vocabulary that makes “acting operator” permanent product language (p00-operator-vocabulary), and the eight stress cases built on it. That is apparatus for a circumstance three founders agreed will not exist once Yale moves.
The open question, and whose it is. Is the Sep 10 standup the later ruling — in which case the machinery collapses to “create the org, name the admin, move on” — or is it cheap insurance for the next customer handover, in which case this page should say so and cite the standup it is overriding? That is Gera’s to answer, and it is open on the decisions page as block b89279042, still open as of 2026-09-13. Until he answers, treat the second half as proposed and contested rather than settled: do not build on it, and do not delete it. Anyone reading the card below without this banner would believe the whole of it was decided. It is not.
Decided — and contested in part seventeen hours later. Offered three shapes, the founder chose a fourth. Keep shape C’s first half: not in dispute — mint the operator’s house now and put the building inside it; disputed, see the banner above — that the acting operator holds the login under a dated mandate.
“Anything will always just be a PropFlow default until the operator overrides it.”
Founder, 2026-09-10 · D-0910-1
A missing value falls to a PropFlow default, not to a value the stand-in wrote and attributed to the absent operator. Transcription survives as machinery for a value genuinely transcribed; it is no longer the mechanism that answers Yale. This supersedes the attribution-and-confirmation second half of shape C in answer (3) above and the older P00/Yale descriptions.
The PRINCIPLE is settled. The LIST is not. Do not read this as one open question. Product behaviour, such as an agent contact window, may carry a default. A business term, such as a pet fee, may not: a platform default could make Clara quote a resident a fee the operator never agreed to, unattributed and indistinguishable from the operator’s own answer. That is worse than the transcription it replaced. RULED — the founders drew exactly that line out loud on the standup of 2026-09-02 (#transcripts permalink), Fede stopping Sean on the word and Sean answering it: “Policies would be like, what fees do you charge” — policies are the customer’s to set, settings are the ones PropFlow pre-fills and the customer tunes. This banner used to say “proposed, not decided”, and that was never ours to propose: the meeting had ruled the boundary nine days before the question was raised here, and nothing routed it to this page. STILL OPEN — the enumeration, not the principle. Which keys fall on which side, and whether to derive that candidate list now for Gera’s signature, is open on the decisions page as block b89278033 (open as of 2026-09-13). Source: the standup digest §2.1 (propflow-docs #185, #187).
D-0910-2 · Quiet hours mean “may Clara speak FIRST”
Decided. Quiet hours stay one key, quiet_hours. They gate direction, not contact: block an agent-initiated message during quiet hours; allow a reply to a resident who wrote first. Do not split this into separate initiation and reply keys or interpret it as a blanket silence window.
D-0910-3 · Named buildings only
Decided. Targets name the buildings explicitly. The all_managed wildcard target is not adopted; no automatic expansion to every building a manager manages is authorised by this ruling.
D-0910-4 · The owner requests, the manager applies
Decided. The property owner requests a change; the manager applies it. The proposed fourth, landlord-written authority domain is not adopted. A request from the owner does not itself write an effective setting.
D-0910-5 · One handover mechanism; name still open disputed
Whether there should be a named handover mechanism at all is DISPUTED.Half of this is DISPUTED — the founders’ standup at 2026-09-10 18:01 CDT talked the acting-operator circumstance down. The dated mandate, the stand-in attribution, the handover mechanism and the vocabulary are contested and open on Gera’s decisions page as b89279042 (open as of 2026-09-13); this page does not settle it. See D-0910-1.
Decided: use one handover mechanism for both the acting-operator handover and a subsequent operator change. OPEN: which name survives. This page does not pick a name.
Unreconciled test record. The founder’s review identifies eight stress cases still naming TRANSFER and ST-900 still asserted “post-TRANSFER”, a mechanism shape C removed. Those references need reconciliation with the single handover mechanism; they are not proof that it is settled or tested. Preserve the historical assertions until they are explicitly superseded; do not silently rename them or claim they passed.
Figure 15. Seventeen Forks. Eight gates on the road, dated by the ladder's own working-day calendar and labelled "needed by", never "≈": the test line this week; the Sep 12 sentence and texting on Sep 12 (decided: on, Sep 7); the class list before step 2a; the voice-first reorder and hire-vs-fan-out before step 3a; the one-number question before step 3b; the tour host and the tools ruling before step 4; the Person ruling and the meaning of "company" before step 6a; the re-key after step 5a. Five are wording rulings or priced options with no gate — the ninth card.
Read it at
There are seventeen choices only Gera and Fede can make. Each one comes with our best suggestion first, and says what can start once it is made.
Nothing on this page is decided except how voice works (one shared agent that asks which building). The seventeen rows are the choices the design cannot make on its own — some this week, some before a named step, some just wording. Each row leads with the recommendation, then the question and the owner; open it for what it unblocks.
The gates on the rail are dated by the ladder: the test line this week, the class list before step 2a (Oct 6–14), the voice-first reorder and hire-vs-fan-out before step 3a (Oct 13–23), the one-number question before step 3b (Oct 19–Nov 2), the tour host and the tools ruling before step 4 (Nov 2–23), the Person ruling and the meaning of "company" before step 6a (Nov 18–Dec 17), the re-key after step 5a. Five are wording rulings with no gate. Every row cross-references the study's D-numbers, the onboarding plan's X-numbers and the catalog's Q-numbers so nothing is answered twice.
D2 is the one irreversible artifact — the KEYS class list — because narrowing a class after a customer authors against it costs a scripted END sweep per key (~½ day each); D1 chooses between two PERSON# rows + PLINK# and one Person + per-org roles, with PLINK# disappearing under the alternative and every other row unchanged; D3 fixes Property.organizationId for Yale; D15 is the line that gives the shared agent tools at step 4 and not before; D16 and D17 govern the calendar (many hands vs a hire; 3b′ before 3a); D9 and D10 govern PROP#western-slope-proto's retirement and the ladder's start. Invariant: only G1 is decided on this surface; every other sentence is proposed (the page's own grep in the critique log), and this page keeps no second store of answers.
Recommendation first on every row — the question second, on purpose.
Twelve decisions sit on one of eight dated gates; five are wording rulings or priced options, and say so.
D2 (the class list) is the only one that gets more expensive the longer it waits.
Answers are recorded once, on the study's table and the decisions surface — a row here links to its home rather than asking again.
Proof: each open decision keeps a named test red until it is made — every row's Unblocks names the ladder step whose closesAt fitness function is waiting (D2 → registry-closed-and-typed, D1 → isolation-gauntlet T7/T8, D9 → umbrella-tripwire, D15 → same-agent-id-every-line).
Seventeen rows, recommendation first; tap a row for the full recommendation, what it unblocks and where the answer is recorded. Decision owner as a label; the gate is the ladder step it must precede.
D1
Keep two Person rows and one admin-only link row
the same human at two customers.
Fedebefore 6a
Keep A2 — two Person rows and one admin-only link row in a platform partition. The one-Person alternative is on record; the F06 fixture passes either way, so this is a wording ruling, not a schema fork.
Unblocks: step 6a (the link row's writer); the floating-assistant fixture's wording. · Cross-refs: Q4 · study D5 · plan X5 · A2. · Home: study D5 · answer
D2
Sign the class list as written, escalation.owner DYNAMIC
which keys are fixed to a building and which inherit.
Gerabefore 2a
Sign the class list as written, including escalation.owner as DYNAMIC (reversing the Sep 1 identity classing); every existing key ships as a nearest-wins parameter with today's constant, so the only irreversible choices are the policy and identity rows.
Unblocks: step 2a — the one irreversible artifact; every key move in step 7. Needed by Oct 6–14. · Cross-refs: deepest Q3 · Z24 · K12. · Home: answer · the class list
D3
A and A — Yale stays under JP&Co; nothing moves
what "company" means, and Yale now.
Fedebefore 6a
A and A — a company is the operator whose Clara answers; Yale stays under JP&Co with owned_by JP&Co and managed_by ConAm (act-as inside JP&Co's wall); nothing moves.
Unblocks: step 6a (relationship rows; ConAm's memberships). Needed by Nov 18–Dec 17. · Cross-refs: study D1, D2. · Home: study D1/D2 · answer
D4
Maintenance line first for Situs
Situs phase-1 scope.
Fedeno gate
Maintenance line first, on these rails after steps 3a–4 plus the route keys and the Situs slice; after-hours leasing is the same rows one step later.
Unblocks: Situs's onboarding order; what is pulled forward from steps 7 and 8. No gate. · Cross-refs: Q8 · Q10. · Home: the onboarding plan · answer
D5
Decided Sep 7 — texting stays on; leasing only
texting on the shared Western Slope line.
Fede + Geradecided Sep 7
On — decided on Slack, Sep 7 (Fede: "we need to make it work on channels"; Gera: "let's just leave it on"). Scope is leasing only, so a text about a listing carries its own property and the umbrella's re-key debt stays small. The safeguard that replaces "off": the tripwire counts rows born under the umbrella per prefix, daily, and the re-key script fails loudly on any row kind it does not handle (~3 h, this week). The life-safety lane split (3a/3b) still gates tenant texting, which is out of scope while the line is leasing-only.
Unblocks: nothing on the ladder; it protects Sep 12. · Cross-refs: Q8 · N26 · K13. · Home: answer
D6
Done Sep 7 — the portfolio test line bought and registered
buy the test line and place one Willows call.
Fededone Sep 7
Bought Sep 7 under Fede's standing approval ("always auto approve buying numbers"): a Denver 720 voice + SMS line, registered as kind prototype with no property and no agent (PR #7285). Still open: the voice-platform import against a sandbox agent, after which the six L3 scenarios unblock on their own; the Willows call gates E8 and the mid-call question.
Unblocks: steps 3b and 4's live proofs. This week. · Cross-refs: Q25 · Q22 · Z33. · Home: answer
D7
Answered (Sep 7): keep their numbering as it is — we work around their system
would Western Slope and Situs accept one number per property?
Fedebefore 3b
Answered by Gera, Sep 7 2026: “lets keep them as they are, we have to work around them, not the other way around, this is our big value as a company, we work around their system.” So the flip does not ask a customer to renumber: whatever numbering they already run is the input, and a doorbell per building is something they may choose, never something we require.
Unblocks: the shape of step 3b's flip for those two customers. Needed by Oct 19–Nov 2. · Cross-refs: Q18. · Home: answer
D8
Nearest-first by calendar, 30-minute buffer; the calendar itself is an adapter over whatever they already use (Sep 7)
tour assignment and availability source.
Fedebefore 4
By-calendar nearest-first with a 30-minute buffer for Western Slope week 1; the PMS “ready to show” mark as the availability source. On the calendar itself, Gera answered Sep 7 2026: “we want to have an easy to plug-n-play outlook/g-calendar/propflow, so the goal is an easy adapter that does all/any.” So a person keeps the calendar they already have and the adapter reads it; a PropFlow-hosted calendar is the fallback for someone who has none today (Situs’s three leasing people run a whiteboard), and swapping them onto their own calendar later is a change of provider on the same attachment row, not a migration.
Unblocks: Sep 12 behaviour; step 8's rows for Situs. Needed by Nov 2–23. · Cross-refs: Q23 · Z13. · Home: answer · the rows
D9
Accept the umbrella with the tripwire; re-key once after 5a
the umbrella's one-way door.
Fede + Geraafter 5a
Accept it for Western Slope's first weeks with the tripwire (Gera, Sep 10) and the re-key script on file (Gera, by Sep 19); run the re-key once, after step 5a.
The three-week cut: week 1 on today's rails, texting on; Sep 14–Oct 2 holds steps 0–1 dark plus the Western Slope agent on real PMS data; the fan-out flip keeps Nov 5–Dec 17
the Sep 12 sentence, Fede's three-week frame and the ladder start.
Fede + GeraSep 14
Fede's frame (Slack, Sep 7): "the minimum work to ship an increment for leasing in the next week that works in the portfolio / centralized leasing setup. Everything else in the next 3 weeks builds on that foundation." Week 1 (by Fri Sep 12) is therefore: the Western Slope leasing line answers leasing inbound on voice and text, Clara captures the guest card, the card lands on the listing's property — on today's rails, texting on, plus the tripwire and the re-key script on file by Sep 19.
What the ladder's own arithmetic says fits Mon Sep 14 – Fri Oct 2 (working day 14 from Tue Sep 15, one engineer): steps 0 and 0.5 done and step 1 nearly done at the low bound (cumulative 5 · 9 · 15), steps 0 and 0.5 at the high bound (7 · 13) — the wall's one truth, the harness, the claim row, all dark. What does not fit: the fan-out flip for the shared line (3a → 3b → 4 → 5a) — earliest Nov 5 voice-first, Nov 6 with many hands, Nov 18 in page order (high: Nov 27 / Dec 1 / Dec 17). Those dates are unchanged: steps 0–2 Oct 13–23; the committed scope (0–6a) Nov 24–Dec 25; Situs maintenance Nov 19–Dec 21.
Proposed reconciliation — the cut is Gera and Fede's call, needed by Mon Sep 14: (a) the Western Slope agent's real PMS data and tour tools do not wait for the ladder — the line is registered today as a property line on the umbrella row, so tools ride the existing per-property path with the home known at pickup, the same path Camellia's line uses; that is Fede's half of the three weeks, and every tour it books lands under the umbrella where the tripwire counts it; (b) "fan-out built in parallel" means steps 0–1 (2a if the days allow) built dark against the test company — the bought line plus the PMS properties Gera seeds — proving the new lookup gives today's answers, so the later flip is a flag, not a migration; (c) the flip itself keeps the dates above. One tension to name: Fede's Sep 7 note that the agent needs "the tools to create tours" reads against the Sep 5 v1 sentence (one coupled agent, no tools); which he means is his call, and the rows do not change either way.
Unblocks: the whole ladder's calendar. Needed by Mon Sep 14. · Cross-refs: Q10 · Z34 · AS1 · Slack #agent-smith, Sep 7. · Home: answer · the calendar
D11
None — nearest-wins with the company's lock is enough
which knowledge keys, if any, are policy under "portfolio wins".
Fedeno gate
None — nearest-wins with the company's lock available is "portfolio wins" when the company chooses it and "the building knows better" when it does not.
Unblocks: step 7's knowledge keys. No gate. · Cross-refs: Q11 · AS3 · plan X9. · Home: answer
D12
Keep the third-party-manager rows, weight them lower
owner-operators vs third-party managers as the segment.
Fedeno gate
Keep the third-party-manager rows in the harness, weight them lower — the rows cost nothing.
As recorded: D3-A, D4-A, D6-A, D7-A, D8-A, D9-A; W11-A, X5-A, X6 superseded, X9-A — none is settled by this page; each stays on its own table under its own number, and this page opens no new fork.
No org-map screen in 2026; the fixture file is the map
does the org map exist as a screen in 2026?
Fede + Gerano gate
No org-map screen in 2026 — the row is typed in the fixture notation, checked by validate-fixture, loaded by bin/load-fixture; the clicks column and this page's chip pictures describe the 2027 screen and say so. If wanted: read-only map 3–4 days + six write actions as forms 8–12 days = a step 6.5 of 11–16 days, outside the 83–121.
Unblocks: how the two onboardings are typed. No gate. · Cross-refs: P2 · §7.1. · Home: answer · the clicks column
D15
Yes — the shared agent gets tools at step 4, not before
does the shared agent get tools, and when?
Fedebefore 4
Yes, at step 4 and not before — step 4 gives the shared agent tools, superseding the v1 "no tools" rule for the front door; until then a company line answers company facts and takes a message, and this page says so. home_picker:'search' and every home-taking tool on a shared line wait for this line.
Unblocks: step 4; the bind, the tour and the work order on a company line. Needed by Nov 2–23. · Cross-refs: A8 · AS1. · Home: answer · a call on a company line
D16
Fan out now under Gera's review; a hire helps only the high bound
hire, or fan out the many-hands steps now?
Fedebefore 3a
Fan out now under Gera's review — the Head / hands column names which steps can take it (3a, 5b, 6b, 7, 10, the mirror tree); a hire productive from Fri Nov 13 helps only the high bound and is a reviewer's cost in October. If the hiring line is opened anyway, it is for the backlog (7–10), not the shared line.
Unblocks: the "with help" date (Nov 6–Dec 1). This week. · Cross-refs: P8. · Home: answer · head / hands
D17
Price 3b′ and decide on the number
voice-first reorder — 3b′ before 3a?
Gerabefore 3a
Price 3b′ (the one-address flip at the three voice edges, estimated 5–7 days) and decide on the number — if it holds, the shared line lands Nov 5–27 instead of Nov 18–Dec 17, at the named cost of two weeks in which the Western Slope leasing line's wall is the edge mint plus the open-site count, not the compile-time guarantee; texting stays after 3a either way.
Unblocks: the shared line's date. Needed by Oct 13–23. · Cross-refs: P7. · Home: answer · the reorder
12 · The org model — the architecture, in the standup's words
One client, one house: the org is the house, the buildings are its rooms, and every key, calendar and rule hangs on the hook of the room — or the front hall — it belongs to.
Who owns what (founders' standup, Sep 7). This page is Gera's architecture effort: the org model, the rows, the wall, the one lookup, the road. Gera's other item this week — pulling the four settings levels apart (personal · property · org · admin) — is a UI that sits on this model and is deliberately not on this page. Fede owns onboarding and leasing: the add-a-customer flow, the org-level calendar connection, guest cards, the Western Slope leasing line's prompt. Where his work and this design touch, the row is named here and the wiring is his. The word is org, not portfolio and not global — the client, general enough for a warehouse or a commercial book later (Gera).
Figure 16. One Client, One House. The org is the client and the wall. A building is a room inside it. Whatever is shared by the whole client — Western Slope's one number, its one calendar, its fees — hangs on the org and every home inherits it; whatever belongs to one building — Camellia's own line — hangs on that building. A person who works for two clients is one login with two membership rows, and an org switcher, never two accounts.
Read it at
Each customer gets its own house. The rooms are the buildings. Things the whole family shares go in the front hall; things one room needs go in that room. A person can have a key to two houses.
The org is the customer — the top bucket, the thing we bill, the wall nobody else sees through. Its buildings sit underneath. A phone number, a calendar, a mailbox, a fee or a rule is attached to the org when the whole customer shares it and to a building when only that building uses it; a building without its own answer inherits the org's.
The PMS is a detail inside the org — the whole org on one system, or one building on another — and a person who works for two customers is one login that can switch orgs. That is the whole model; the rest of this page is the rows that make it true.
Two node kinds carry everything: ORG# (the customer, the isolation boundary, the billing unit) and PROP# (a building, the storage partition for what is born on its line). Lines, mailboxes, calendars, PMS logins, schedules and settings are attachment rows on one of those two nodes, read by one resolver that walks building → org and answers nearest-wins, with a lock the org can set. Membership is a PersonRole scoped to an org, a group or a building; the same human at two customers is two Person rows and one admin-only link. What Fede is shipping this week — a store-level check that the requester may read what it asked for, and an org-level calendar connection — are rows 2 and 4 of chapter 13, landing ahead of the ladder.
Property.organizationId required-at-write plus a sparse PROPORG#<org> index stamp make the roster one Query (step 0); <node>/ATTACH#<kind>#<slot>#<startedAt> rows on ORG# or PROP# carry the calendar, the lines, the mailbox and the credentials; ADDR#<channel>#<address>/HEAD is the platform-unique claim that a number or a mailbox belongs to one node; the key registry classifies every setting (POLICY / PARAMETER / IDENTITY) with absentMeans = today's constant; TenantContext {organizationId, roster, reach} is minted once per request and every PROP# read is gated on roster.has(pid) — which is what "the wall is a row, not a filter" means below.
Figure 17. The Wall Is a Row, Not a Filter. Today a request can read every property and rely on code to filter — one missing check and a new org sees everything, which is the bug Fede found. In the design the login's context is minted once with its org's roster, and every building read is gated on that roster before the key is touched. Fede's store-level check on every request is this gate, shipped first.
Four ways a client is shaped — and the one we refuse
Shape
On the org
On the building
Rows
Verdict
Owner-operator, direct — one company runs its own buildings (JP&Co, Camellia)
fees, policies, the mailbox
each building's own line and calendar
ORG# + PROP# per building; attachments on the building
supported today's shape, as rows
Centralized leasing — one number and one calendar for many homes (Western Slope)
the Western Slope leasing line, the one calendar, fees
nothing of its own
attachments on ORG#; a claim row per address; the caller names the home
the design's centre; fan-out is steps 3a–5a
Third-party manager — a property owner's building run by another company (ConAm running Yale)
the property owner's org gets the projections its owned_by row grants, pushed into its own partition
the runner's line; the manager's people invited to that building
REL#owned_by / managed_by; a membership scoped to one building
supported as rows; nothing moves
Hybrid — some buildings on the shared line, some with their own; a manager promoted to two buildings
the shared line and calendar
an override line where a building has one
nearest-wins: the building's attachment beats the org's; a second PersonRole scope
one rule, no exceptions
Two PMSs behind one number
—
—
—
refused as a client (Fede, Sep 7): clean your house before you add AI
The org is the customer, the contract and the wall; two orgs stay separate even when one owner sits behind both (ConAm and JP&Co).
Attach to the org what the whole customer shares; attach to the building what only that building uses; a building without its own answer inherits the org's.
One person, many orgs: one login, a membership row per org, an org switcher, visibility per user and per building. Multi-select across orgs is the same roster read wider — parked, not blocked.
The PMS is a detail inside the org. Testing is driven through PropFlow's own rows and Fede's harness, not through the PMS.
The map Gera described in the standup — a card on each org showing its calendar, its people and what is per-building — is these attachment rows rendered; whether it gets a screen this year is decision 14.
Proof: dial every org and count what leaks — zero — and try to claim a number twice — refused by the store: isolation-gauntlet, second-claim-refused-by-store, reports-push-authorised-by-row.
The house has good bones and no interior walls yet; this is the punch list, room by room, with who is holding the hammer.
This pane is the DESIGN RECORD, not the board. Day-to-day phase tracking lives at the Organization Architecture Implementation board, which is grouped by phase and opens on the phase we are actually in. Update that page, not this one — two copies of the ladder drift, and this pane going stale is what the dock was built to answer. What stays here is the reasoning: the thirteen-row gap below, what each row costs, and the test that turns it green. A4 carries the same notice; this chapter is where a newcomer starts, so it carries it too.
How far, in one breath — a dated number, not a standing sentence. The ladder is well under way: 113 of the 179 ladder rows on the Implementation board read merged or shipped — 86 merged, 27 shipped, 3 wip, 62 open, 1 blocked, counted 2026-09-13 from artifacts/portfolio-architecture-phases.html at commit d2f502ab with bin/tracker_rows.py, an anchored parse of that page’s tracker-data island over rows whose group matches ^Phase \d. Re-count before you quote this. The figure ages every day, and a whole-file "status" search is not the instrument: it reads narrative prose as rows and answers several too high (local-bin#179). Until 2026-09-13 this banner opened “Nothing on the ladder has started” — true on the day it was written, false by the time it was read, and believed for as long as it was because it carried no date and no method. It is replaced rather than caveated. The spine described next — a Person row per human, an org id on every property, org-scoped person reads — and the Western Slope prototype on a placeholder building are what the original writing recorded; the dock, not this pane, is the live answer. The committed scope is 50–73 engineer-days; the three weeks to Oct 2 hold steps 0 and 0.5 and most of step 1 at one engineer. Seven of the seventeen decisions are taken. Two rows below are landing this week ahead of the ladder, in Fede's hands, and they are the same rows the design wants — so they count.
Figure 18. The Punch List. Thirteen rows, three piles. Seven of the seventeen decisions are answered and feed the ladder; four are still needed and the ladder did not wait for them (dated count in the banner above); two rows — the store-level check and the org calendar — are arriving ahead of the ladder and are read as done by the same fitness function, not as a fork. Solid = the walked chain · dashed = arriving early.
Read it at
The house is standing and the rooms are there. What is missing are the walls between the families and the hooks in the front hall. This is the list of what to fix, room by room, and who is holding the hammer for each one.
Thirteen rows, each one a thing we have today and the thing we want instead. The ladder has started and is a long way in — 113 of its 179 rows on the Implementation board read merged or shipped, counted 2026-09-13 (the banner above says how). Seven of the seventeen questions are answered, four still need an answer, and the work did not wait for them; two rows are landing early because Fede is already building them. The table below is the punch list; the last column names the test that turns each row green.
Each row pairs a today-claim with the target row shape and the ladder step that pays for it. The spine already exists — a Person row per human (src/lib/data/dynamo/persons.ts:405-441), a customer id on the building row (src/lib/data/types.ts:2885-2890) — but the roster is one index query over every building followed by an in-memory filter (src/lib/data/dynamo/property.ts:154-165) and the settings row is one global row (src/lib/data/dynamo/settings.ts:28-56). Two of the thirteen rows are Fede’s and land before their ladder step; the rest wait on the four answers named under “Needed first”. Proposed.
The committed slice is steps 0–6a at 50–73 engineer-days of an 83–121 total; the three working weeks to Fri Oct 2 (working day 14) hold steps 0 and 0.5 and most of step 1 at one engineer. Row 2 is TenantContext minted once per request with every PROP# read gated on roster.has(pid); row 4 is ORG#/ATTACH#calendar# read by the one resolver instead of leasingCalendar on the building row. Both close under the same fitness functions the ladder already names (map-dump-stable, second-claim-refused-by-store, isolation-gauntlet), so arriving early costs no rework. Every today-claim in the table carries its file:line; the full citation set is A1. Proposed.
What
Today, in the code
Where we want to be (this design)
Work
Status
The org is the top bucket, the client, the wall
Property.organizationId exists on rows and ORG# rows exist, but a property list is a full read filtered in code afterwards (property.ts:154–164)
organizationId required at write plus a sparse PROPORG# index stamp; the roster is one Query and reach a filter over it
step 0 · 5–7 days
not started
Isolation between clients, at the store
person reads already refuse across orgs (persons.ts:437); property and unit lists do not — a new org saw everything (Fede, Sep 7)
TenantContext minted once per request; every PROP# read gated on the roster; the isolation gauntlet counts zero leaks
one org per login; no switcher; admins juggle accounts
two Person rows + one admin-only PLINK# (decision 1, taken); PersonRole.scope per org, group or building decides visibility
step 6a · 4–6 days
decided, not started
The calendar at the org
leasingCalendar lives on the property row — Western Slope's one calendar sits on the placeholder building
ORG#/ATTACH#calendar# read by the resolver; one calendar for one customer is one org row (Western Slope confirmed one calendar for two leasing people)
Fede: the org-level calendar connection now (his #1) · design: steps 3a–4
in flight (Fede)
Policies, fees and rules at the org
property-level settings pages; the five OrganizationSettings twins are inert
the key registry: every key PARAMETER, nearest-wins with the org's lock; no POLICY knowledge keys (decision 11)
step 2a · 1–2 days, then step 7 · 10–16 days
not started (Fede's #2 ask)
One line for many buildings (fan-out)
PHONE_TO_PROPERTY_MAP plus the placeholder; the Western Slope leasing line books real tours under it (proved Sep 7)
ADDR#/HEAD claim per number, home_state → bindHome; the shared agent asks which home
steps 3a → 5a · 26–39 days after 0–2 · Nov 5–Dec 17 at one engineer
not started · built dark in parallel (Sep 7)
Western Slope's sixteen homes under one placeholder
16 UNIT# rows, real tours and inquiries under PROP#western-slope-proto
one PROP# per home; the re-key script runs once after 5a; the tripwire counts what lands meanwhile
re-key 5–7 days · tripwire ~3 h · script on file by Sep 19
accepted (decision 9), not started
The PMS is a detail inside the org
pmsSource per property; one PMS per line in practice
pms_credential#<role> slots on the node; two PMSs behind one number refused as a client
step 8 · part of 8–12 days
decided by refusal (Sep 7)
Entities created two ways
the PMS Read API creates properties, units and tenants at onboarding; Gera is creating Situs and Western Slope entities directly from the three databases we can read
both paths stamp organizationId and PROPORG#; seeding-calls-zero-scripts and map-dump-stable hold either way
Gera, this week
in flight (Gera)
The org map
the Atlas page, with stale entities (Ulysses, "Riverbend") to clean
no map screen in 2026; the fixture file is the map (decision 14) — but the standup's card-per-org is these attachment rows rendered
fixture + validate-fixture · a screen is unpriced
open question (decision 14 vs the standup)
Multi-select across orgs (Ask Clara over five orgs)
single select everywhere
reach over the roster stamps, so every chart and Ask Clara read the selected set
unpriced
parked (Fede: build if you want)
Guest cards both ways
PropFlow is the CRM; phone-call guest cards are PropFlow rows only
Fede's lane — not on this page
Fede
in flight (Fede)
One Settings entry (Admin · Property · Organization · Profile)
one settings page with everything mixed; admin controls in profile settings
Gera's second item — a UI over this model, not on this page
Gera, after the architecture
not started
Taken (7 of 17)
1 the same human at two clients · 4 Situs's first line · 5 texting on the Western Slope leasing line · 6 the test line · 7 one number per building, ask · 8 tour assignment and the calendar adapter · 9 the placeholder, accepted with the tripwire.
Needed first
10 the three-week cut (Mon Sep 14) · 2 sign the registry class list (before 2a) · 14 the org map (the standup reopened it) · 15 the shared agent's tools (step 4, and the Western Slope leasing line already has them).
Not on the ladder, landing anyway
Fede's store-level isolation check and the org-level calendar are rows 2 and 4 of this table. They are the design's rows arriving early, not a fork: the fitness functions that close at steps 1 and 3a would read them as done.
Proof: each row above names the fitness function that turns it green — the ladder's tests are the status column's future: map-dump-stable, second-claim-refused-by-store, isolation-gauntlet, registry-closed-and-typed, rebind-line-inserts-only, camellia-replay-byte-identical.
Each customer is a house — but today the house has no front door: the customer is a sticker stuck inside one room, and every phone, mailbox, calendar and login hangs on a hook in that same room.
If you only read this: the customer exists as an id on rows, not as a wall the database enforces — so a phone line, a calendar, a mailbox and the settings all hang off one building, and the separation between customers is a filter in code. Twenty rows below, each with the file:line that proves it.
Figure A1. The House Today. One hook — the building row — holds the phone line, the mailbox, the calendar, the PMS login and the flags, and the customer is only a sticker on that hook. Solid = the call’s path today · dashed = a reference that is only a string · outside the house = not a row at all.
Read it at
The customer is a sticker on one building, and everything — phone, mailbox, calendar, login — hangs on that building. There is no hook for the company itself.
Think of each customer as a house. Today PropFlow has no front door for the house: the customer is only a name written on a sticker, and the sticker is stuck inside one room — a building. Every phone number, mailbox, calendar, PMS login, plumber list and staff list hangs on a hook in that one room, because there is no hook anywhere else.
A call finds the room by the number it dialed, then reads the sticker to learn whose house it is. Some things do not hang in the house at all: they sit in a code file, in an environment variable, or in one shared box that every customer opens. That is why a second customer, a second building on one number, or a second calendar all break — there is one hook, and it is in the wrong place.
The organization is a string on the property row, optional, with no writer and usually no row of its own (src/lib/data/types.ts:2885-2890; src/lib/data/interfaces/organization.ts:15-29); the property row is the only node a line, mailbox, calendar, PMS credential, setting or vendor list can attach to, so every one of those is a column on PROP#<id>/META. Inbound routing goes number → property → org, through a code map and a last-writer-wins cache, and voice fails open to the caller's own property when the number is unknown (src/lib/domain/properties/phone-lookup.ts:140-148, 300-306; src/app/api/voice/personalization/route.ts:568-586). Settings for every customer live in one global row (src/lib/data/dynamo/settings.ts:28-56). A person's reach is a mutable list of property ids on the auth row from which the org is derived (src/lib/data/types.ts:12659). Six spine repositories enforce the org at the store; everything else filters after the read, when the route remembers to.
Property.organizationId? is optional (src/lib/data/types.ts:2885-2890), IOrganizationRepository has no save (src/lib/data/interfaces/organization.ts:15-29), every leaf is a PROP#/META column or a USER#/CONFIG row, resolvePropertyIdForPhone reads a 60-second cache ∪ a code map (src/lib/domain/properties/phone-lookup.ts:140-148, 346-364), and getProperties(org?) is one entity-type index query over every building, then an in-memory filter — there is no customer index on the building row (src/lib/data/dynamo/property.ts:154-165). The one place the wall is enforced at the store is the person spine's conditional writes (src/lib/data/dynamo/persons.ts:405-441, 485-510).
The verdict, area by area
Fede's rule for the week — “if you see the skeletons, you gotta rewrite it from scratch… this is a deep clean” — applied to every area we opened. SOLID keep it and copy it · HALF-BAKED the posture is right, the coverage or the writer is not · SKELETON a leaf that never had a node to hang from.
Area
Today (file:line)
Verdict
Person spine
org required on every row, conditional writes, cross-org error — src/lib/data/dynamo/persons.ts:405-441, 485-510; helpers.ts:297-307
SOLID
Spine link rows (occupancy, inquiry, household, vendor link)
same discipline, org-sharded index — src/lib/data/dynamo/inquiries.ts:351-376; memberships.ts:24-27
SOLID
Text-message door
fails closed twice on an unknown address — src/app/api/<phone-vendor>/webhook/route.ts:453-486
SOLID (posture)
Vendor rows (card, link, contact point)
shape is right and walled per engagement — src/lib/data/types.ts:16560-16584, 2499
SOLID (rows)
Person roles
the scope union is right, but the home org is first-found — src/lib/data/types.ts:15203-15206; src/lib/domain/identity/accessors.ts:275-336
HALF-BAKED
Building ↔ customer (the roster)
optional stamp; one index query over every building then an in-memory filter, with 159 bare callers; create writes no customer and checks no session — types.ts:2885-2892; src/lib/data/dynamo/property.ts:154-165; src/app/api/properties/route.ts:88-110
HALF-BAKED
Route-level permission envelope
right posture, opt-in coverage (130 of 431 routes); the org_admin list is a self-described stopgap — src/lib/platform/auth/org-scope.ts:17-47; scope.ts:23-51
HALF-BAKED
Voice agent selection
agent-per-number lives on the platform, not in rows; one dedicated fleet for one building — agents/clara/lib/agent/specialists/fleets.ts:65-78
HALF-BAKED
Email lane A (connected inbox)
refuses on more than one claim — the right instinct — but walks every building and keeps tokens on the row — src/lib/integrations/email/webhook-processors.ts:398-425
HALF-BAKED
Email lane B (relayed domain)
first match in scan order; a platform catch-all address — agents/clara/lib/email/match-property.ts:87-104, 125-130
HALF-BAKED
Vendor reach and preference
one fact with four homes and three precedence rules — types.ts:3057, 12464, 13237; src/lib/domain/vendors/resolve-preferred-vendors.ts:111-160
HALF-BAKED
Staff membership (the invite)
building checkboxes only; the org is derived from them and refused when they disagree — src/app/(workspace)/admin/users/page.tsx:585-600; src/app/api/admin/users/route.ts:204-224; types.ts:12659
HALF-BAKED
The Atlas
the org is a list entry, never a focus; vendors flat and un-scoped — atlas/_components/focus-blocks/index.ts:45-56; atlas-tree.ts:1171-1195
HALF-BAKED
The proof harness
22 of 22 cases reproduce RED on a seeded company, but there is no GREEN polarity, no loader and no free/busy stub — scripts/portfolio-harness/results/latest.md:9-30
HALF-BAKED
The customer row
no create/save writer — only a targeted preferred-vendor field update; two key spellings; approval mints an id and no row — src/lib/data/interfaces/organization.ts:15-29; src/app/api/admin/waitlist/[email]/approve/route.ts:104-107
SKELETON
Phone routing
code map + seven environment overrides + a last-wins cache + a forever customer memo + an inversion map + one fallback line — phone-lookup.ts:140-148, 300-306, 403-419, 465-469, 471-478
SKELETON
Voice fail-open
an unknown number adopts the caller's own building — src/app/api/voice/personalization/route.ts:568-586
SKELETON
Calendar
one calendar per building with its tokens on the row; the first refresh writes the rotated token back and invalidates the copies; the second provider is written then refused — types.ts:3060; src/lib/domain/calendar/sync-tour.ts:141-175; provider-guard.ts:78-81
SKELETON
PMS credential
keyed by the person who connected it; the building points at that person — src/lib/data/dynamo/pms-credential.ts:10-11; types.ts:3759
SKELETON
Settings (all four levels)
one global row holds kill switches, modules, the subscription and per-person interface state; the org twin is read by nobody — src/lib/data/dynamo/settings.ts:28-56; src/app/api/settings/route.ts:24-50; types.ts:12422-12436
SKELETON
The analogy, word by word
So the picture and the rows never drift apart, here is what each word in the lede actually names.
the house
the customer — a string on the building row, optional, with no writer; approval mints an id and no row src/lib/data/types.ts:2885-2890 · src/lib/data/interfaces/organization.ts:15-29 · approve/route.ts:104-107
the sticker
the customer id on the building row — written by nobody on create src/app/api/properties/route.ts:88-110
the one room
PROP#<id>/META — found by asking the index for every building, then filtering in memory src/lib/data/dynamo/property.ts:154-165
the hooks in the room
the building's number list, its mailbox integration, its leasing calendar, its PMS credential holder, ~40 flags, its preferred vendors types.ts:2934, 3060, 3061, 3759, 3057
the code file
the number map plus seven environment overrides; outbound is its inversion, else one fallback line src/lib/domain/properties/phone-lookup.ts:140-148, 465-499
the shared box
one settings row for every customer src/lib/data/dynamo/settings.ts:28-40
the staff list
a list of building ids on the auth row; the customer is derived at invite time types.ts:12659 · src/app/api/admin/users/route.ts:204-224
the plumber card outside
a vendor row with no customer, reached through per-customer link rows — ended links still count property.ts:916-930
What breaks — three stories from the same shape
The story
What happens today (file:line)
Why
A second customer approved from the waitlist
Approval writes no row (approve/route.ts:104-107). A building added by hand carries no customer, so their own dashboard cannot show it (properties/route.ts:88-110; property.ts:161-164). Every staff surface that enumerates customers misses them, and the Atlas lists their buildings as dangling. Flipping one flag flips the first customer's kill switches, modules and subscription (settings.ts:28-56). Their chosen ticker is refused because the roster is global (property.ts:199-205).
The customer is a sticker, so nothing can hang on it and nothing can be walled by it.
A second building on one number — one customer, buildings A and B
The writer script refuses the share, and the code map is one key to one value, so B cannot be added under it at all (phone-lookup.ts:140). If both rows carry the number, whichever the scan returns last wins — no log, no test, flipping every 60 seconds (phone-lookup.ts:300-306). Voice resolves A only; B's tenant becomes an unknown caller; the tour lands on A's calendar and A's hours (listing-resolution.ts:178-193). Outbound to B falls back to one shared line (phone-lookup.ts:465-499).
The number can only point at a building, and there is exactly one column to hold it.
A second calendar — the customer-level calendar one client actually has
Connecting a calendar needs a building to write it onto, so the company calendar goes on a placeholder building. Putting the same calendar on every real building means copying the tokens; the first refresh writes the rotated token back onto one row and invalidates the siblings (sync-tour.ts:141-175). A leasing person's own calendar has no home a scheduler reads (types.ts:12658). A second provider is written by the callback and refused by the read path (provider-guard.ts:78-81).
One calendar per building, with its secret on the building — there is no node above the building and no vault beside it.
The customer has no writer: approving a waitlist signup mints an id and writes no row, so a self-serve customer exists only as a string (approve/route.ts:104-107).
The building row is the only hook, and creating one writes no customer and checks no session (properties/route.ts:88-110).
Routing is a code map with a last-wins cache and a fail-open on voice — two buildings on one number degrade to one building, silently (phone-lookup.ts:140-148, 300-306).
Settings are one row for everybody: a second customer flipping a flag flips the first customer's kill switches, modules and subscription (settings.ts:28-56).
Staff reach is a mutable list of building ids on the auth row — the code itself calls it “mutable; corruption or misconfiguration could let it leak” (src/lib/platform/auth/org-scope.ts:166-171).
What this proves: gauntlet T6 (a line bound to a customer) and T9 (a person's own calendar) come back CANNOT-EXPRESS — the row kinds do not exist (scripts/portfolio-harness/gauntlet/results/latest.md:14, 17); harness cases 15–17 (outbound falls back to the global line, a shared number is not expressible, a shared calendar token clobbers its siblings) are RED on a seeded company (scripts/portfolio-harness/results/latest.md:23-25). Fixture F01 (Camellia) is the one shape this picture handles cleanly — one building, one number, one calendar.
What this assessment could not check. Every claim above is read from the code at one commit; nothing was read from live data, by rule. So: whether any self-serve customer id exists in production today (that is, whether the first story is latent or already live); which of the 159 bare building reads are later filtered by the route helper (a route-by-route trace was not done); whether creating a building is reachable without a session in production, since the route itself checks nothing; and whether the three newer indexes are provisioned in production at all — the table-creation script defines four (scripts/create-dynamo-table.sh:53-82).
The same house with a front door: the customer is a real room of its own, every phone and calendar is a dated hook in the hall or in one room, and a nameplate at the kerb says whose house each address is.
If you only read this: the org becomes a real row and the wall the store enforces; every line, calendar, mailbox, credential and setting becomes a dated hook on the org or on one building; one lookup walks building → org and names the row that answered. Gera’s head-level question is answered here: the level was right, the key was one notch too fine.
Figure A2. The House We Want. The org is a row and a wall, its buildings hang under it, and every line, mailbox, calendar and rule is a dated hook on the hall, on a room or on a person. Solid = a claim or the walked chain · dashed = a reference or a tag, never walked · the band = the one lookup, in order.
Read it at
The customer gets a real front door. Phone numbers, calendars and rules are hooks in the hall or in one room, a nameplate on each number says whose house it is, and nobody can see into another house.
Give the house a front door. The customer becomes a real row — the org — and it is the wall: nothing inside one org can be read from another. Every building is a room in that house. Anything a customer sets up — a phone number, a mailbox, a calendar, a PMS login, a rule, a plumber list — is a hook, and a hook can hang in the front hall, in one room, or on a person.
A number or a mailbox gets one nameplate for the whole platform that says whose house it is, and the database itself refuses a second nameplate. When a call comes in there is one lookup: read the nameplate, find the house, ask which room if the nameplate names the whole house, then walk room → hall to find the nearest hook for each thing. Nothing under a room ever moves; a change is one hook added or one retired.
The org is a profile row written by one registered writer, carrying two version counters and a purpose. A building is a property row with the customer required at write and a roster stamp, so “which buildings does this customer run” is one query instead of a scan. Everything configurable is an attachment row on exactly one node — hall, group, room or person — one live row per slot, with history written on retirement. An address is claimed once, platform-wide, on the physical address, with the channels listed on the claim, and a second claim is refused by a conditional write. One resolver walks up (address → customer → function → person → home) then down (room → group → hall, nearest wins, the hall may lock), and every answer names the row that set it. Every read enters through a context minted once at the seam and gated on the roster before the key is queried. The PMS, the calendar provider, the voice platform and the mail provider are named on the attachment row, never on the trunk. Proposed.
Three trunk nodes — ORG#/PROFILE {mapVersion, valuesVersion, purpose} → optional GROUP#{kind:'chain'} → PROP#/META {organizationId, ROSTERPK}; the facts are <node>/ATTACH#<kind>#<slot>#<startedAt> rows; ADDR#phone#<e164>/HEAD {organizationId, scope, channels[]} is the attribute_not_exists(PK) truth per physical address; resolveEffective returns {value, setAt, locked, maskedBy?}; TenantContext.roster gates every PROP# read before the key. Proposed throughout.
The analogy, word by word
the front door / the wall
ORG#<id>/PROFILE {mapVersion, valuesVersion, purpose, timezone, jurisdiction} written by one registered writer
ADDR#phone#<e164>/HEAD · ADDR#email#<addr>/HEAD {organizationId, scope, channels[]} — one per physical address, written with attribute_not_exists(PK)
the one lookup
up: address → customer → function → person → home; then down: room → chain groups → hall → what absence means
the vault
CRED#<id>/SECRET — one secret referenced by many calendar and PMS hooks, never copied; a lease makes the refresh single-flight
nobody sees in
TenantContext {organizationId, roster, reach} minted from the nameplate; a building outside the roster reads as missing, not as forbidden
a person on a hook
a role scoped to the customer, plus that person's own calendar attachment
Every capability, the row that makes it true, the test that proves it
Nothing here is decided beyond what the page's banners already list. The last column names who runs the test — H the harness, D a drift test, R a replay — and the ladder step at which it is allowed to go green. The steps are the phases in A4.
Capability we want
The row that makes it true
Fitness function
Who runs it · when it can go green
The customer exists as a row; its roster is one query
ORG#/PROFILE + PROP#/META with the customer required and a sparse roster index
Camellia is byte-identical before and after every step
the live rows replayed against a fresh fixture across six surfaces
camellia-replay-byte-identical
H+R · every step
Where a vendor lives — the open question, answered as rows
Gera asked it three ways: an extra field? a dynamic mapping at the org level? is the org the head? The vendor judge scored three designs (49 · 39 · 52) and the directory design won on zero-move mutations, a one-query org card, and how few rows a person has to hold. Proposed.
Gera's option
Verdict
The row instead
An extra field on the vendor
No
Per-customer facts on a shared card are the bug today — a display-name override on the company is seen by every customer (types.ts:5661) and the PMS pointer is one number per company (types.ts:5613-5626). Every customer-specific fact moves onto the engagement row in that customer's own partition.
A dynamic mapping at the org level
Yes — as a dated row, not a map attribute
The map attribute exists today (types.ts:12464) and is one of four homes for one fact. Replace it with two rows in the record's own grammar: an engagement row for reach and a roster hook per trade for order.
Is the org the head?
Two heads, deliberately
The customer is the head of the relationship — “who may use this company” is a wall question and lives in the customer's partition. The platform is the head of identity — “which company is this” is a dial-the-same-number question and lives on an identity claim, the vendor twin of the address nameplate.
So the three shapes Gera named all fall out of the same rows: a vendor “part of an organization” = a card plus an engagement; “tied to one property” = an engagement whose coverage names that building; “a collection we grouped ourselves, belonging to no org” = a card with zero engagements — that is the directory. The card, the link row and the contact point are kept verbatim (types.ts:16560-16584, 2499); four preference homes collapse to one hook per trade (types.ts:3057, 12464, 13237). Size: the first key rides step 2b as already priced (4–5 d); the rest is a slice after step 6a, ≈10–13 engineer-days, counts × judgment.
The one decision Gera and Fede owe: is a vendor's identity platform-unique — one card per real company across every customer — or per customer, with no cross-customer dedupe and therefore no directory? Recommendation: platform-unique with three guards — reuse only on exactly one candidate whose name agrees, any second candidate or name mismatch is a named refusal and an admin task, and the “engaged by N” count stays off for every tenant context. Cards pulled from the internet stay a reserved value until Fede names who curates them.
Is the head-level mapping the best design?
Answer. Yes on the level, and nobody found better. Six attackers built the strongest alternatives they could — a routing table in the customer’s partition, a per-number document, the org map as a version-control-style file, a claim per function, a plain index on the line row. Every one loses the same way: at ring time the only thing the platform knows is the address, so the one row that turns an address into a customer has to be keyed by the address and written with a condition the database checks. That is exactly what the head is.
The flaw. The record’s key was one notch too fine. It put the channel in the key, so what the database actually refused was “two customers on one channel of a number”, not “two customers on one number” — weaker than the writer script we already run. Five of six attackers found this independently. The repair keys the head on the physical address and lets channel and function narrow underneath it.
The cost. Half a day of record edits and half a day of page edits. Migration cost is zero, because no such row exists yet. Tally: 0 of 6 broke the level, 5 of 6 broke the key the same way, and the two alternatives that tie are the head with its narrowings inlined — equal is not better. Proposed.
The customer is a row with a writer, not a string — and it is the wall, minted once at the seam rather than filtered after the read.
Every leaf is an attachment on exactly one node, one live row per slot, history written when it retires.
One nameplate per physical address, refused by the store, with the channels listed on it rather than in the key.
One lookup, and every answer carries the row that set it — no property.x ?? org.x anywhere.
The PMS, the calendar provider, the voice platform and the mail provider are named on the hook, never on the trunk.
What this proves:second-claim-refused-by-store (a second nameplate comes back as a conditional-check failure, step 1), every-effective-value-has-provenance (room two's calendar answers “set at room two”, room one's answers “set at the hall”, step 2) and rebind-line-inserts-only (moving room three's number to the hall is attribute updates and inserts, moved = 0, step 3b) — on bench A-3own + A-calB, cases ST-13 and ST-17.
Still open in the target — named, not hidden
Still open
Why it matters
Owner · default
The billing counterparty
The page calls the customer the billing unit, but today's subscription is one field on the global settings row; who the invoice is addressed to is not settled.
Fede + the JP&Co principal · not on the ladder
The customer map as a screen
The standup asked for a card per customer — like a repository's own metadata file — showing the calendar, the managers, and what is per-building versus per-customer. The record says no screen this year; the Atlas focus leaf is the cheap half.
Gera · a full screen is 11–16 d outside the ladder
Adapter contracts at the leaf
The record names the calendar seam and the voice-platform assignment call; it names no PMS adapter interface, no lead-source adapter, and no idempotency rule for the platform's side of a rebind or a release.
either · counts × judgment
Facilities that are not housing
A clinic, a warehouse, an office suite. The nearest rows we have are an asset-type identity key plus a capability stage turned off, which reads as “known, out of scope” rather than as a second product.
either · stretch; the trunk does not change
Reach: wall or menu?
A caller on one line names a home that is only in another line's set — refuse, or bind and say so? The wall is unchanged either way.
Fede · default: menu inside the customer
The platform catch-all address
Retire it and make every customer claim its own inbound address, or keep a platform-scoped nameplate resolved by sender — which is exactly the name-guessing the wall rules forbid.
A renovation, not a demolition: walls that stay, half-built rooms to finish, and rotten beams to pull out rather than paint over.
If you only read this: most of the house stays. Six spine repositories, the text-message door and the vendor rows are already the right shape — that is keep. The roster, the settings and the invite are half-built — that is finish. Phone routing, the voice fail-open and the calendar on the building row never had anything to hang from — that is rip out.
Figure A3. Keep, Finish, Rip Out. Twelve pieces of today’s house sorted by what happens to each: four stay as the pattern, four are finished in the direction they already point, four are pulled out and replaced with rows. Green = keep · amber = finish · red = rip out (a leaf that never had a node to hang from).
Read it at
Keep the walls that already work, finish the rooms that are half built, and pull out the rotten beams instead of painting over them.
Not everything has to be torn down. The parts of PropFlow that already treat the customer as a wall — the person rows, the tenant and vendor link rows, the text-message door that refuses an unknown number — stay exactly as they are and become the pattern for everything else: keep. Some parts are half-built in the right direction — the building's link to its customer, the per-request permission check, the login, the two email doors, the staff invite — and need finishing, not replacing: finish.
And some parts are skeletons: a phone map in a code file, a calendar with its password copied onto a building, a PMS login tied to a person instead of a customer, one settings box shared by every customer, a customer that is only a name. Those get pulled out and replaced with rows, never patched: rip out. The order is fixed by what the database can check first — the customer row and the building's stamp, then the nameplate and the rulebook, then the wall.
The overlap is real: six spine repositories already require the customer on every row and refuse cross-customer writes at the store (persons.ts:405-441, 485-510), the text webhook already fails closed (src/app/api/<phone-vendor>/webhook/route.ts:453-486), the vendor rows are already platform counterparties (types.ts:16560-16584), and the harness already reproduces the failures. The finish list is coverage, not posture: an optional customer stamp becomes required with a roster index; an opt-in route filter becomes a context minted once at the seam; a first-found home customer becomes a membership row; the email lanes and the staff invite move onto the same nameplate and role rows. The rip-out list is every leaf that never had a node to hang from: phone routing, the voice fail-open, the calendar on the row, the PMS credential by person, the global settings row, the customer as a string. Nothing found in three stress rounds needs a row kind the record lacks; what is missing is executors and fourteen sentences. Proposed.
Keep K1–K15 (the store-enforced discipline of persons.ts becomes the template); finish F1–F10 (required organizationId + a sparse roster index, TenantContext before the key, MEMBERSHIP# + a customer-scoped PersonRole, email heads); rip R1–R11 onto ADDR#…/HEAD, ATTACH#calendar|pms_credential|setting and one registered createOrganization. Ladder order 0 → 0.5 → 1/2a → 2b → 3a/3b → 4/5a → 6a/6b. Committed band 0–6a = 50–73 engineer-days inside the full 83–121.
Area by area — today, target, the move, the size, the owner, the case that proves it
Size names the ladder step and its day range where one exists, else c×j (counts × judgment — a shape and a count, not a priced estimate). “Proves it” names the stress case whose flip to GREEN closes the row; on a narrow phone that column folds away — every row’s proving case is in A5, by id.
Area
Today (file:line)
Target
Move
Size
Owner
Proves it
The customer row
no writer; approval mints an id and no row — approve/route.ts:104-107; two key spellings
one registered writer, a profile row with two version counters and a purpose
rip
step 0 · 5–7 d
Gera
ST-97, ST-49, ST-52
Roster (building ↔ customer)
optional stamp, no customer index (one query over every building, then filter), 159 bare callers, no session check on create — property.ts:154-165; properties/route.ts:88-110
required at write + a sparse roster index + lifecycle + a derived answerability stamp
number → agent lives on the platform; one dedicated fleet — fleets.ts:65-78
the one shared agent set (G1, decided); the claim writes the platform ids back onto the head; config files become dumps
finish
3b (inside the 10–15 d); the platform contract c×j
Gera
ST-01, ST-13, ST-08, ST-61, ST-64
Email lane A (connected inbox)
walks every building, tokens on the row, refuses more than one — webhook-processors.ts:398-425
a mailbox hook + an email nameplate + the vault
finish
step 3b
Gera
ST-11
Email lane B (relayed domain)
first match in scan order; the header, not the envelope; a platform catch-all — match-property.ts:87-104, 127-130
a nameplate on the delivery address; the envelope recipients carried on the payload; the catch-all retired
finish
relay fix now · ½ d c×j; flip at 3b
Gera (the catch-all is a decision)
ST-03
Calendar
one per building with tokens; the refresh writes back and kills the copies; the second provider refused; tours land on the intake building — types.ts:3060; sync-tour.ts:141-175; listing-resolution.ts:187-193
a calendar hook on person / room / group / hall → the vault and a single-flight lease; schedule and assignment rows; holds keyed by the calendar, not the building
rip
the row lands this week (P0); the scheduling at step 8 · 10–13 d
Gera · Fede (rota facts)
ST-04, ST-16–ST-23, ST-65, ST-98
PMS credential
keyed by the connecting person; the building points at that person — pms-credential.ts:10-11; types.ts:3759
a credential hook per role → the vault; the sync iterates credential rows, never connections
rip
step 8 (inside the 10–13 d) + a Situs slice · 5–8 d (not in the total)
Gera
ST-37, ST-38, ST-100, ST-117–119
Settings (four levels)
one global row including the subscription; an unread customer twin; ~40 building columns — settings.ts:28-56; types.ts:12422-12436
a closed key registry + a setting hook on the chain + a resolver that returns provenance; locks and sweeps
rip
2a · 1–2 d + 2b · 4–5 d + 7 · 10–16 d
Gera (signs the class list first)
ST-07, ST-45, ST-54, ST-57–59
Vendors — rows
card, link row, contact point — types.ts:16560-16584, 2499
kept; five per-customer fields move onto the engagement row
keep
0 d
—
ST-32, ST-34
Vendors — preference and reach
four homes, three precedence rules; a link minted once per connection for the first customer — types.ts:3057, 12464, 13237; resolve-preferred-vendors.ts:111-160
one roster hook per trade; one engagement row per (customer, vendor); a platform identity claim with three guards
rip + finish
step 2b · 4–5 d (priced) + a slice after 6a · ≈10–13 d c×j
Gera + Fede (the identity decision)
ST-33, ST-35, ST-89–91, ST-120
Relationships and groups
none; the management company is a string; ownership is not modelled
dated ownership / management / grouping rows; a party context and pushed rows for the projections the owned_by row grants (reports · financials · occupancy · work orders · documents by default since Sep 8; conversations grantable, off)
add
step 6a · 4–6 d
Gera (views) · Fede (permission rows)
ST-27–ST-31, ST-68, ST-95
Conversations
no customer on the row; property-first thread match — types.ts:8983
a customer stamp + an immutable scope + one writer for the home; a building index
add
5a · 4–6 d + 5b · 3–4 d
Gera
ST-42, ST-56, ST-47, ST-85
The Atlas
the customer is never a focus; vendors flat — focus-blocks/index.ts:45-56; atlas-tree.ts:1171-1195
a customer focus leaf over the map dump: a trade × building grid, engaged vendors, nameplates with a disagreement chip
finish
after step 1; vendor blocks ≈2 d; the rest c×j; a full map screen is 11–16 d outside the ladder
either (Gera owns the rows)
ST-35, ST-46
The proof harness
RED-only, no loader, no free/busy stub; three of the four lanes never run in CI — .github/workflows/portfolio-harness.yml:97-101
a GREEN polarity with a closes-at step, a fixture loader and validator, a read ledger, a parity replay
finish
step 0.5 · 4–6 d (many hands)
Fede (contract, fixtures) · Gera (instruments)
ST-14, ST-20, ST-38, ST-55, ST-02
The placeholder building
one fake building holds the customer's line, calendar, homes and escalation owner
sixteen real buildings plus customer-level hooks; drained by the re-key after 5a
rip by design
script 5–7 d, on file by Sep 19; the tripwire ½ d (Sep 10)
Gera
ST-44, ST-83–85, ST-116
Billing counterparty
the sum of units on the global row
open — who the invoice is addressed to is not settled
open
not on the ladder
Fede + the JP&Co principal
ST-76 (kept open)
Verticals / adapters at the leaf
unit shape and work-order unit are required fields; a closed PMS enum; vendor-named columns — types.ts:4677-4684, 6190-6191
an asset-type identity key + a vocabulary per asset type + capability stages; unit-shape fields optional per asset type; external refs inside the adapter's own row
finish (stretch)
c×j — one registry line + one type change per assumption
either
ST-39–41, ST-75, ST-78, ST-105–109
Interface (switcher, multi-select)
a logo top-left; a building selector only; the invite form has no customer
the customer switcher is the wall picker, listing memberships; multi-select over customer × building with split-by; every chart reads the selected set
add — a spec pane
c×j (the build is Fede's / Gera's)
Fede / Gera
ST-24, ST-60, ST-71
Further than they look · closer than they look
Further than they look
“Connect the customer's calendar” — the row lands this week, but scheduling on it is step 8 (10–13 d): without a shared secret and a single-flight lease the first rotation clobbers every sibling; two leasing people need schedule and assignment rows even when there is only one calendar; and the hold must move from the building to the calendar or two buildings double-book one customer calendar.
“Isolation at the store” — steps 0 + 3a + 3b + 5a, 23–34 engineer-days, many hands: a store gate can only check a stamp that exists, and conversations carry no customer at all today (types.ts:8983). What is cheap is step 0's roster.
“Decouple the four settings levels” — the page split is interface work; the data split is a registry (1–2 d) + a resolver (4–5 d) + 10–16 d of moving keys one at a time, with sweeps and locks. One key alone is read at 68 sites in 25 files.
Closer than they look
The head-level address claim — zero migration and about a day: no such row exists yet, the number canonicaliser already ships, today's writer script already refuses at the number level, and the backfill is seven numbers to five nodes. Six attackers and three sweeps found no better level.
The customer row + roster (step 0) — 5–7 d, and half the rows exist: six profile rows already carry the target key, the save literal needs two attributes beside one it already stamps, and the customer-scoped role is already typed. It is the only step that fits the three-week window whole.
Vendors — the rows are the ones we already have, the first key is already priced with an eighteen-assertion oracle to check it against, and the rest is a slice that gates neither the shared line nor Situs's maintenance line. The one thing that is not close is the identity decision.
Six repositories already enforce the wall at the store — every “finish” is a copy of what persons.ts:405-441 already does.
Three skeletons are replaced wholesale, not patched: phone routing, the calendar on the row, the settings box.
Settings and vendors are both “four homes for one fact”; the deep clean is one row per fact, and the vendor case has an eighteen-assertion oracle to prove byte-equality.
Nothing found in three stress rounds needs a row kind the record lacks — the delta is executors, fourteen missing sentences, and five decisions.
The order is fixed by what the database can check first: the customer row and the roster stamp (step 0), then the nameplate and the key registry, then the wall.
What this proves:roster-stamp-complete (step 0) turns the first amber card green; second-claim-refused-by-store (step 1) is the first red card's replacement proven by the store itself; isolation-gauntlet T1–T19 (steps 3b/6) closes the whole finish column at once; and camellia-replay-byte-identical, run at every step, is the promise that the green column never moved — fixture F01 (Camellia).
Five decisions this gap cannot close for you. Gera signs the settings class list before the registry is written (one key alone reverses a Sep 1 call) — but the principle behind that line is no longer open: the founders drew the settings-versus-policies boundary out loud on the standup of 2026-09-02 (permalink; Sean: “Policies would be like, what fees do you charge”), and that ruling governs. What is still open is the enumeration — whether to derive the candidate list now for signature, block b89278033, open as of 2026-09-13. Gera and Fede settle whether a vendor's identity is platform-unique, and Fede names who curates cards that came from the internet. Gera decides whether the platform catch-all address is retired. Gera prices the one-address voice slice (5–7 d) before step 3a is scheduled. Gera decides whether the customer map is a screen this year or an Atlas card. Two stress legs also stay open rather than unwritten: the explicit building subset for a customer admin, and what happens to a staff subset when a building changes customer.
We fix the house in a builder's order — foundation, walls, rooms, paint — and no room opens until the last one's test is green.
The live tracker moved — this pane is now the DESIGN RECORD, not the board. Day-to-day phase tracking lives at the Organization Architecture Implementation board, which is grouped by phase, carries the same 109 ladder rows plus the fifteen UI phases A6 prices beside the ladder (switcher · dashboard · atlas, ≈48–53 days) that never appeared here, and opens on the phase we are actually in. Update that page, not this one — two copies of 109 rows drift, and this pane going stale is what the dock was built to answer. What stays here is the reasoning: the ordering finding, the rulings, the rail arithmetic and the per-phase cards below.
Read this before the ladder — the Sep 8 standup battle test found the plan's ORDERING wrong, and it is the one finding on this page with a date attached. The defect is not “isolation is last”. P0 already carries an isolation slice this week, but its roster check lands in observe mode — it logs every miss and refuses nothing, which is an instrument, not a wall. The first date on which a building read is refused rather than filtered is inside P1, which starts six calendar days after onboarding starts. And onboarding is not exposure: during onboarding week the only logins are Gera's and Fede's. Exposure begins at the first foreign customer login — go-live, mid next week — and that, not Sep 9, is the deadline. Ruled 2026-09-10 (b89007120): the pre-wall cohort’s shared visibility is allowed only until the roster gate flips from observe to refuse — the orgs already on today’s rails keep today’s login until that flip, and their window closes with it; it is a dated window, not an open-ended exemption. The minimum cut is five items: stamp the customer on every building row and add the roster index; make the property-picker path a Query; give getProperty(id) a required customer with a 404-shaped refusal; flip the roster gate from observe to refuse before the first foreign login; keep the tripwire. 3.5–5 engineer-days in Gera's lane, none of it in Fede's files, all of it inside P1's existing 5–7 day budget — a REORDER of P1, not new work. The priced table, what is consciously left at the 20%, and the block raised for a ruling (b88910993) are on A9 · The standup, battle-tested.
Ruled 2026-09-09 (b88986177, Gera): isolation first, renewals after. The first signed client asked whether renewals could come first; both claimed week one. Decided: the wall — P1’s roster gate flipped from observe to refuse before the first foreign login — precedes any renewals work for that client. The harness shows 21 of 22 isolation cases still failing today; every feature built before the wall is built on rows any new company can still read. What moves out to make room is named, not assumed: the read boundary’s early window is priced at 13½–22 working days beyond P1, and P0’s Atlas polish and the early calendar-connection work move out of the early phases to pay for it — a re-ordering that prices nothing is not a plan change.
If you only read this: the work is a ladder of sixteen steps in eleven phases, 83–121 engineer-days in all, of which 50–73 are committed. As of 2026-09-13, 113 of the Implementation board’s 179 ladder rows read merged or shipped (86 merged, 27 shipped, 3 wip, 62 open, 1 blocked; chapter 13 carries the method) — this pane said “Nothing has started” until that date. The first four steps write rows nothing reads yet and prove the new lookup gives the old answers; the flips come after. The tracker below is one row per piece of work, with its owner, its phase and the test that turns it green.
If you only read this: we fix the house in a builder's order — foundation, walls, rooms, paint — and every room has a test that must be green before the next one opens. Nothing this assessment found adds a step before the November flip, so the flip dates stand; the one number that moved is the re-key of the sixteen homes, 5–7 days, and it sits beside the ladder, not inside the 83–121 — and on the night of Sep 8 the team relaxed the constraint that number existed to honour, so the re-key comes off the critical path altogether and those homes are seeded as real buildings now (A9).
Figure A4. The Plan, as Rooms. Ten rooms on a rail that wraps into two rows — the top row is September to October, the bottom row November onward — each naming who, how many days, and the test that must be green before the next room opens. Plain = dark (read by nothing) · accent = a flip · dashed (P8) = priced beside the ladder · the rail is working days from Tue Sep 15.
Read it at
First the foundation, then the walls, then the rooms — this week we fix the phone, the next weeks we build in the dark, and in November we flip the switch, with a test before every step.
We fix the house in the order a builder would: foundation, walls, rooms, paint. This week Fede makes Western Slope's one phone line work on what exists, and the two new pieces he needs anyway — the org calendar and the “you only see your own stuff” check — are built in the new shape so they never have to move.
For three weeks Gera pours the foundation in the dark: every customer becomes a row that cannot be skipped, and every building says whose it is in a way the database can check. October adds a nameplate on each phone number and a rulebook for settings. November is the flip: one door for every read, one number for many buildings, and Clara asks which one. Every step has a test that must turn green first, and every step has an off switch.
We are going to fix the building in the same order a builder would: first the foundation, then the walls, then the rooms, then the paint. This week Fede gets Western Slope's phone line working the way it already can, and the two new things he needs anyway — one calendar for the whole org, and a check that one customer can never see another customer's stuff — get built in the exact shape the new design wants, so we never have to move them later. The next three weeks Gera builds the foundation in the dark: every customer becomes a real row in the database that cannot be skipped, every building says which customer it belongs to in a way the database itself can check, and the test bench learns to say “this now works” instead of only “this is broken.” October adds the two rows that make one phone number safe to share — a claim on the number that only one customer can hold, and a rule book for settings that knows whether a rule came from the org or from one building — still read by nothing. November is the flip: one door for every read, one number answering for many buildings, and the sixteen homes stop being sixteen units of a placeholder. Then property owners and logins, then vendors, then the long road of keys, calendars and deletions.
P0 (W0, today's rails, K16-tagged) → P1 step 0 createOrganization + PROPORG (5–7 d) ∥ P2 step 0.5 expect:'GREEN' + closesAt (4–6 d) → P3 steps 1 + 2a ADDR#/HEAD, ATTACH#, KEYS dark (7–10 d) → P4 step 2b resolveEffective in byte-parity (4–5 d, Oct 13 / 23) → P5 3a + 3b mint.ts, getRepository(ctx), HEAD live per address (14–21 d, Nov 2 / 23) → P6 4 + 5a the shared line + the re-key (12–18 d + 5–7, Nov 18 / Dec 17) → P7 6a + 6b (7–10 d, Nov 24 / Dec 25) → P8 the vendor slice (≈10–13 d c×j) → P9 5b, 7–10 + the Situs slice (Jan 8 / Mar 3).
Nothing this assessment found adds a step before the flip, so Nov 18 / Dec 17 stands (Nov 5 / 27 if the voice-first reorder is taken, Nov 6 / Dec 1 if the mechanical steps fan out).
The one number that moved is the re-key of the umbrella — 5–7 days, not 1 — and it is priced beside the ladder, never inside the 83–121.
Fede's two week-1 rows (the org calendar, the isolation check) land in the design's shape, so step 1 never has to lift them.
The registry class list unsigned when step 2a's PR is ready moves Oct 13 / 23 and everything after it one day for one day.
Every phase's rollback is a flag or a row END — no phase rolls back by deleting a customer's data.
What this proves: each card's exit test is a named fitness function whose closesAt equals that card's ladder step, and the rail is the ladder's own working-day arithmetic — so a reader can recompute every date from Tue Sep 15 and the day range on the card. Fixture F01 (Camellia) stays byte-identical at every card (camellia-replay-byte-identical).
The rows below are the phases. Status is derived by bin/refresh-trackeronly for the rows that carry a PR, decision or production-evidence link — 104 of the 109 carry none, so they read exactly as AUTHORED and no refresh can move them. Treat a status here as a claim by whoever typed it, not as a measurement. The stamp below is the last time a status actually MOVED, not the last time this page was checked: bin/refresh-trackers sweeps every 15 minutes and discards a stamp-only rewrite, so an old date here means nothing derivable has changed, not that nobody looked. Before you edit a row here, check whether it belongs on the Implementation board instead — that page is the one kept current, and a row changed only here will read as the newer of the two without being it. Last status change 2026-09-10 14:47Z.
Overall
—
—
Shipped
—
Merged · untested
—
In flight
—
Open
—
Waiting on you
The phases, in order — click one to open it
StatusOwner
—
A row that carries a PR, decision or production-evidence link has its status re-derived from that link by bin/refresh-tracker; a row that carries none keeps the status its author typed, and 104 of the 109 rows below are in that second group today. The script is the one the bin/refresh-trackers rotation runs for every page carrying the id=refreshed stamp above. Merged is not shipped — a merged row stays amber and out of the hero number until a permalink proves it was exercised in production. A row that is waiting on a human should carry a decision id and link raised with blocked raise; the rows below that say they are waiting have no block raised yet, so they read as open rather than as a link that lands nowhere.
The phases in full
Ten keys per phase, so a designer can lay them out as one card and a builder can read the lands cell as a PR list. Days come only from the ladder; anything else is marked c×j (counts × judgment) and is not inside the 83–121.
Phase
Goal
Who
Runs on
Days
Lands
Exit criteria
Gated by
Deletes
Rollback
P00 The clean slate — the operator vocabulary switch
Before P0’s week-one work, one vocabulary switch so every later phase uses the final words from day one. Retire firm and custodian. operator = the company running a building day to day, whose rule book governs — an org-level role; a property points at exactly one operator org, and there is no property-level operator entity. acting operator = the stand-in holding the login until the operator onboards (Yale: JP&Co, “transcribed, not authored”) — and the window itself is now ruled (b89000051, shape C): the operator’s org is minted now with no accounts under it, the building is one property row in that org’s house, the stand-in administers it under a dated mandate, and every value the stand-in writes is attributed to them and confirmed by the customer at handover — but that second half is DISPUTED: the founders’ standup of 2026-09-10 18:01 CDT talked the circumstance down, and the question is open at b89279042 (see D-0910-1). The house and the named admin are not in dispute. staff = the human using PropFlow (replaces the code’s operator sense). org_admin = the top-level person inside a customer. owner-operator is not a role — it is owner org == operator org.
Gera
Opus 5 + subagents — a mechanical rename behind a protect-list, needing a test run and a CI gate, not a design argument. Gera owns the go/no-go and the messaging pause.
Proposed — no dates, no rail span. Ruled 2026-09-10 (b89001763): a hard cutover in a maintenance window, not a bake period in which both vocabularies read — one clean switch with a short messaging pause, because “we haven’t onboarded the bigger clients, so this is the best time.”
No firm and no custodian left in the words we use; every phase from P0 on is written in the final vocabulary.
— (it gates P0 rather than being gated).
firm and custodian.
The expand/contract plan on the migration page — the old words keep reading until the contract step.
P0 Week 1 on today’s rails
Western Slope (the sixteen-home customer) has leasing working on its own line this week without borrowing anything from the ladder, and the two rows Fede is building anyway land in the design’s shape.
Fede (the increment) · Gera (the rails, the seed)
Opus 5 (this session + subagents) — code on today’s rails, tests in CI. No design work in this phase, so no reviser.
W0, Sep 8–12 (follow-ups to Sep 19). Gera’s lane ≈6–8 d at the high bound, c×j
The Western Slope leasing line; guest cards; the org calendar as an org attachment plus a credential row; the isolation check in observe mode; the tripwire; the enumerator; the record edits; the Atlas org card; the add-a-customer writer; both clients’ entities seeded.
A call and a text land under the placeholder and the tripwire counts them; the callback writes an org calendar row and a credential row and no token lands on any building; the isolation check logs zero misses for a new org’s login on stage; the seed script’s dry-run of the import reports created 0 for both clients.
Nothing live. The empty singleton tile in the Atlas tree and its stale cross-org label.
End the calendar attachment; the isolation check is observe-only; the Atlas PR is a view; the seeded client rows roll back by archiving from the seed manifest, never by deleting.
P1 The org row and the roster
The org exists as a row nobody can skip, and every building names its org in an index the store can query.
Gera (rows, stamps, index, loader) · Fede (the F01 fixture)
Opus 5 + subagents — rows, stamps, the index and the loader are code. The custody and roster-index questions it reads close in an Astra gap run first.
5–7 → Sep 21 / Sep 23
The one registered org writer and its archive sibling; the sentinels as rows; the approval route; the org stamp required at write; the roster index and its backfill; one query per org; the create-building route under a login; the guard on the building’s own row.
Listing an org’s buildings equals the index query for every org; the index carries no secret; a full round-trip keeps the stamp; the dangling and unassigned Atlas buckets read zero.
The org as a string: the repository with no save, the two-key split, the approval route that writes no row.
Drop the stamp reader. Dark reads, live attribute writes.
P2 The proof apparatus — runs beside P1
The bench can say GREEN, seed any setup through production writers without the PMS, count reads, and refuse a bad fixture before writing.
Fede (the contract and fixtures, by Sep 26) · Gera (the store-side instruments first)
Opus 5 + subagents, one agent per fitness function — the functions are independent and parallelise cleanly. Fable 5.1 reviews the contract polarity once.
4–6, two owners in parallel → W1 ends Fri Oct 2 with steps 0 and 0.5 done at both bounds
The expected-verdict contract; the fixture loader; a bench per fixture; the read ledger; the conditional-put twin; validate-fixture; the parity replay; the recording calendar stub and voice-platform adapter; three more lanes in CI; the nine benches; the five staff fixtures.
Every fitness-function name has an executor that exists (today none of the 35 is a file); the validator prints its refusal; the run exits zero with 22 cases still expected RED; a bench seeds with a real org row.
The RED-only contract; the import-graph checker in favour of source-grep drift tests.
None needed — tests and fixtures only.
P3 Rows in the dark
Every row kind the design needs exists, is written from today’s fields, and is read by nothing — with the address head keyed on the physical address.
Gera, one head. Fede’s only touch: the dated last-mapping-edit announcement.
Opus 5 — the registry, the head and the attachment are code. Any residual registry-shape gap goes to Astra before the phase opens.
6–8 + 1–2 → Oct 7 / Oct 15
The widened transaction helper; the code tables; the attachment and history rows; the address head, the normaliser, the credential and its lease; the map log and undo; group and relationship shapes; the backfill and its preflight; the envelope-recipient fix.
New lookup equals old for every address in the five lockstep files, zero mismatches; a second head on any address is refused by the store; a retried claim reads as reused, never as a conflict; the working tree is clean after a claim.
Nothing live (dark). In the record: the per-channel head key and every “and the same for text”.
Delete the rows.
P4 The resolver in offline parity
One function answers what the effective value of a key is at a building and who set it, byte-equal to today on the first two keys.
Gera, one head
Opus 5 — the resolver in offline parity. It must return whether this rung may write, not only the value — a shape Astra owes on the record first.
4–5 → steps 0–2 done Oct 13 / Oct 23
The chain walk; the resolver with provenance and its refusal shape; the nearest-live-row walk for calendars and credentials; key one (the vendor roster) and key two (who gets escalated to).
Byte-equal on every real org and building for both keys, with the mismatch count published; every answered and every refused value names the nodes it looked at.
Nothing live (dark). The three precedence rules for one vendor fact stop being the truth for readers that opt in.
— (read by nothing).
P5 The wall
Every read enters through a context minted once at the seam, the raw primitives are unreachable, and inbound and outbound resolve through the head per address — Camellia last.
Gera (the flip, the facade, the minter) · many hands under review · Fede (the per-address flip order)
Opus 5 — the wall. The gate is the harness’s open L2 cases, not a reviewer’s opinion.
3a 4–6 → Oct 19 / Nov 2; 3b 10–15 → Nov 2 / Nov 23
The 52 importers behind repository methods; the minter and the five contexts; the facade; the per-address flip; the platform assignment on claim; outbound resolution at 55 sites; the adapter contract; the mail lane; the catch-all retired; texting gated; the config files become dumps.
The importer count equals the repository files only; the open-call-site list only shrinks; the isolation gauntlet T1–T19 green; a rebind inserts and never moves a row; an unclaimed address is refused with zero reads; the cache is exact (warm 2, cold at most 6).
The number-to-building map and its seven overrides, the last-wins map, the forever cache, the outbound inversion and its environment fallback; the two writer bypasses; the first-match mailbox matchers; the shared catch-all list; the week-1 hotfixes.
Per-address flag; a line with no head resolves the old way by construction.
P6 The shared line
One number answers for many buildings: Clara asks which building, binds the thread once, and the sixteen homes stop being sixteen units of a placeholder.
Fede (prompt, fixtures, sandbox agent) · Gera (webhook, tool contract, the stamp, the re-key)
Opus 5 — the shared line and the conversation org stamp. Fede owns the prompt and fixtures in parallel, as the Who cell says.
step 4 8–12; step 5a 4–6 → Nov 18 / Dec 17 (Nov 5 / 27 voice-first, Nov 6 / Dec 1 many hands) + the re-key 5–7 beside the ladder
Home state and the ask block; the area ask above six homes; the home reference on ten tools; the single writer of the bound home; the per-call scope row; the fail-open deleted; tools on the shared agent; the conversation stamp; the four fan-outs rerouted; the re-key script and its one run.
Camellia’s line is always known; the rendered prompt and variable set are byte-identical modulo the additive allowlist; the tool-call corpus re-validates; zero partition moves at 5a; the robot call picks a home in two turns on the bought test line.
The voice fail-open and the post-call chain; the placeholder row, its provisioning script and its six documented hacks; the prototype agent outside every registry and its one dedicated fleet; the two unrouted birth paths; the four fan-outs.
Keep the building required on the agent; readers fall back to the old field; the re-key reverts by run id from its pre-image, rows born after are left in place.
P7 Property owners, groups, login, staff
Property owner and operator are rows, not walls; a person logs in once and picks an org; a staff job is an org-scoped role with an optional building subset.
Gera (views, login, the link row) · Fede (permission rows, the invite form, the announcement)
Opus 5 — relationships, groups, login, staff. Waits on the acting-operator ruling (b89000051) landing first.
6a 4–6 → Nov 24 / Dec 25 (committed scope complete); 6b 3–4, many hands
The five dated relationship kinds; groups, of which only chains are walked; the actor view; org admins lose the whole-org view; reports authorised by a live owner row; membership rows and an explicit active org; the person link; the staff job and the invite form.
Gauntlet T7 / T8 / T12 / T13 / T16 green; the six permutations become rows; a report push with no live owner row is refused by the store; the bypass roles are gone; the switcher lists exactly the memberships.
The first-found home-org resolver; the bypass roles; the pre-populated admin stopgap; automatic provisioning from an email list; the building list as the only membership; the org as a string.
6a is an announced flip for admins; 6b’s login is a flip; both keep the old rows until the guard counts read zero.
P8 Vendors
One card per real company on the platform, one engagement row per org and vendor, one ordered roster key per trade — and a vendor off the internet is storable without being reachable.
Gera (rows, the picker, the sync) · Fede (names the directory’s curator; co-owns the identity decision)
Opus 5 — the vendor slice.
≈10–13, one head, c×j, after 6a — not in the 83–121
The card minus the five per-org fields; the identity head; the engagement row; the roster key as the only order; the picker replacing four dispatch sites; the sync minting per org every run; the one-contact rule; the directory that only suggests.
Roster and dispatch order byte-equal on the replay window; gauntlet T20–T23; a diff of the card across two orgs’ fixtures is empty; no any-org vendor readers outside the admin repository.
The four homes for one fact; the per-org fields on the shared row; the membership’s building list and in-house flag; the any-org contact readers; the in-memory web-search vendors.
The engagement rows are dark until parity; the four dispatch sites flip behind the facade; the backfill is one row per distinct org and vendor and is re-runnable.
P9 The road
Finish what the proof does not need but the product does: every setting is a key with provenance, every calendar and login is a credential row at the node it belongs to, every workflow survives a rebind, and the skeletons are gone.
Gera (one head per step) · many hands under a drift-test gate · Fede (rota facts, the Situs calendars, scheduling)
Opus 5 — the road. Astra first on anything §10.x still carries as unwritten.
30–44 → Jan 8 / Mar 3; the Situs slice 5–8 beside it → Nov 19 / Dec 21
The per-building conversation index; the keys moving one at a time; the six twins; the calendar finished; the hold keyed by the calendar; scheduling as pure functions; workflow pins; the deletions; the Situs slice.
Each key a no-op by construction and the settings dump diff-empty per key; two concurrent refreshes make one provider call; the tour corpus byte-identical at the rendered layer; a workflow started before a rebind completes after it with the same outcome; a write whose PMS instance does not match the building refuses.
The calendar tokens on buildings and logins; the credential keyed by user and the PMS credential holder's id as its key; the forty flag columns and the operating-mode arm; the six twins; the stopgap settings route; the twenty open call sites.
Per-key flag; a per-building switch on the calendar read path; readers fall back to the partition.
Runs on follows one standing rule (plan §1a): authoring the design record goes to Astra; independent critique to Fable 5.1, sparingly, because its allowance is weekly; code, refactors and tests to Opus 5 and its subagents; parallel read-only work to Claude subagents, because Astra cannot spawn subagents; and a decision with two defensible answers to Gera or Fede.
The sum is the ladder’s own: P1–P9 = 5+4+7+4+14+12+7+30 at the low bound and 7+6+10+5+21+18+10+44 at the high — 83 / 121. P0, P8 and the Situs slice sit beside it. Read P9 as 30–44, not 33–48: the record’s backlog figure counts the login step twice, and P7 already carries it.
Decisions this plan needs
Existing numbers keep the page’s status. “Needed by” is the phase whose first PR cannot merge without it. None of these rows carries a decision link yet — a decision is raised with blocked raise, which hands back the link; a hand-built one looks exactly like a real one until somebody clicks it and lands nowhere. Until they are raised, the tracker rows above that wait on them read as open.
#
Decision
Whose
Recommendation, as recorded
Status
Needed by
D1
The same human at two customers
Fede
Two person rows plus an admin-only link
taken
P7
D2
Sign the registry class list; the escalation key’s class
Gera
Sign as written
open — needed first
P3 (the 2a PR)
D3
“Org” means the operator; Yale sits under its runner
Fede
A and A
open
P7
D4
Situs phase-one scope
Fede
The maintenance line first
taken
P9 (the Situs slice)
D5
Texting on the shared line
Fede + Gera
ON
decided
P0 (on) · P5 (the gate)
D6
Buy the test line and place one call
Fede
Yes
line done; the platform import still open
P2 · P6
D7
Ask both clients about one number per building
Fede
Answered Sep 7: keep their numbering as it is; we work around their system
decided
P5
D8
Tour assignment and where availability comes from
Fede
By calendar, nearest first; answered Sep 7: an adapter over whatever calendar they already use — ours only where a person has none
decided
P9
D9
The placeholder’s one-way door
Fede + Gera
Accept, with a tripwire and a re-key after 5a
taken
P0 · P6
D10
The three-week cut
Fede + Gera
Week one on today’s rails; Sep 14–Oct 2 dark; the flip keeps its November dates
needed by Mon Sep 14
P0 / P1
D11
Which knowledge keys are policy
Fede
None
open
P9
D12
Owner-operators versus third parties as a segment
Fede
Keep, weight lower
open
—
D13
Study the customer questions; plan the four open items
Fede
All A
open
—
D14
The org map as a screen this year
Fede + Gera
No map screen in 2026; a read-only map + six write forms priced at 11–16 d if wanted (§17 #14) — this cut proposes taking the read-only half now
open — the standup reopened it
P0
D15
The shared agent gets tools at step 4
Fede
Yes, at 4 and not before
needed first
P6
D16
Hire, or fan out the mechanical steps
Fede
Fan out now, under one review
open
P5
D17
Voice-first reorder: the one-address flip before the compile-time wall
Gera
Price it (5–7 d) and decide
open
P5
N1
Vendor identity is platform-unique, with three guards
Gera + Fede
Platform-unique with the guards
new
P8
N2
The head key repair — key on the physical address, carry the channels
Gera
Take the repair: zero migration cost now
new
P0 (the record edit) · P3
N3
The Atlas card, split read-only now / edit forms later
Gera + Fede
Read-only yes, edit no this year
new
P0
N4
Retire the platform catch-all mailbox
Gera
Retire it; every org claims one inbound address
new
P5
N5
The trade list is the code’s 27 slugs, not the record’s 11
Gera
27
new
P3
N6
The archive writer, and what happens to two dormant orgs
Gera · Fede
Ruled 2026-09-10: archiving is PropFlow staff only and is a soft state that retains the data — the rows stay, the org stops being reachable. Whether an archived org’s addresses are released is still open.
decided — the address-release half still open
P0 (hide) · P1 (writer)
N7
The hold row is keyed by the calendar, not the building
Gera
Yes; retire the clashing prefix first
new
P9 (record edit in P0)
N8
A calendar defaults to free/busy only
Gera + Fede
Yes; full only by the credential’s owning org
new
P0 · P9
N9
Reach: a wall, or a menu inside the org
Fede
A menu inside the org
new
P6
N10
The week-1 calendar row’s shape
Fede + Gera
The record’s shape; the credential interface first; the row and the read this week, the scheduling at step 8
new
P0 · P9
N11
Fede’s database-layer check is the roster gate
Fede + Gera
One module, observe first, refuse at P1
new
P0
N12
Where the 5–7 day re-key lands
Gera + Fede
Fan out its mechanical half; Gera owns the apply run
new
P6
N13
A same-org replay of a claim reads as reused
Gera
Reused
new
P3
N14
Do scattered operators rotate numbers between vacancies
Fede
Ask with D7
new
P5
N15
Does the consent filter drop an inherited org calendar
Gera + Fede
Hosts only; the org’s own calendar still books
new
P9
N16
The billing counterparty
The JP&Co principal + Fede
—
open
not on the ladder
N17
The mailbox subscription pointer: a row or a field
Gera
The row
new
P3
N18
Can the phone vendor split voice and text of one number across accounts
Gera
Key on the number regardless
new
P3
N19
The week-1 shape of the add-a-customer writer
Fede + Gera
The record’s rows, through the interface Gera lands first
new
P0, this week
N20
Western Slope’s 16 homes as real buildings in week 1
Gera + Fede
Now, hidden by stamp, so the re-key merges instead of minting
new
P0 · P6
What would change the plan
#
If this happens
What moves
1
The registry class list is unsigned when the 2a PR is ready (about Oct 5)
P3 stalls at its second commit, and every day of delay moves Oct 13 / 23 and everything after it one for one.
2
Fede’s week-1 rows land in today’s shape instead of the record’s
Nothing breaks; step 1 lifts one more row and the wall moves one more importer. About 1–2 days, one more hotfix in the collision table, and the token-clobbering case stays live for a real customer until step 8.
3
The re-key really is 5–7 days, not 1
Serially in Gera’s lane it pushes the shared line’s high bound by up to a week; fanned out, it costs Gera the apply run only.
4
The voice-first reorder is taken
Nov 5 / Nov 27. For about two weeks the Western Slope leasing line’s wall is the edge mint plus the open-call-site list, not the compile-time guarantee; texting waits for the wall regardless.
5
A booked tour’s reminder must survive the re-key
The workflow pin moves up out of P9 into P6: 2–3 days, one head, before the apply run.
6
A second org on a shared PMS account appears before P8
Vendor reach breaks first. The cheapest bridge is one sync change — mint an engagement for this org every run — pulled into P5, about a day, ahead of the rest of the slice.
7
The test line’s import into the voice platform stays open
Every live-call leg stays blocked, and P6 exits on the offline legs and the variable re-baseline only. Say so on the page rather than claim the proof.
8
The phone vendor cannot split voice and text of one number across accounts
Nothing changes in the rows — the key stays on the number. One case’s other-channel arm becomes store-only, with no carrier twin.
9
Fede prefers the customer’s real number for tests
The bought line sits idle and every isolation case with a live leg runs against a customer-facing number. The bench forbids that; keep the 970 out of the harness.
10
The escalation key is signed as an identity key rather than a dynamic one
Step 7’s first key changes to hours and tours, and the unbound-lead case loses its org-level home and needs a task row instead.
11
A customer asks for the org map as an edit surface this year
Plus 8–12 days outside the ladder. The read-only half is unaffected.
12
The mechanical steps fan out to more than one head
The phase order does not change — only the wall-clock of P5 and P9. The page already prices this as Nov 6 / Dec 1 for the shared line.
A crash-test lab: a pretend town of customers built out of our own database rows, where every story is driven into the wall on purpose.
If you only read this: every case here is driven through PropFlow’s own rows, never the PMS — a pretend org called Park West with three buildings, one shared number, one mailbox and one org calendar, a neighbour called Bellwether behind its own wall, and a twin whose resident has the same phone digits as one of ours. 213 case ids in 232 rows, each naming a bench, an executor and the ladder step that turns it green — and not one of them is proven correct by anything that runs today.
Figure A5. The Test Bench. Park West — three buildings, one shared number, one org calendar — with Bellwether behind its own wall and a twin whose resident shares digits with Park West’s. Solid = a claim or the walked chain · dashed = a reference or a coincidence · a bordered box is another wall · the band is the harness.
Read it at
We build pretend customers out of real database rows and run every “what if” story against them until each one passes — without ever using the PMS.
Before we trust the new design we try to break it — and we do it with PropFlow’s own rows, never by touching the PMS. We build a pretend customer called Park West: one org, three buildings, one shared phone number, one mailbox, one calendar for the whole org, three staff and two residents. Next door we build Bellwether, with its own number and calendar, and a twin customer whose resident has the very same phone digits as one of ours.
Every test is a story on those rows: two buildings share a number; one building later gets its own calendar; a caller from next door dials our number; a manager works for two customers; a plumber is used at two buildings but never a third. Each story says what must happen, which rows prove it, and which step of the plan turns it green.
Every case names a bench, a leg with its executor — the harness on a local store in a throwaway directory, a drift test over the source, or a replay against stage — and the ladder step that closes it. The families are the ones the ask named: routing one org to many buildings and many numbers to one building, the hybrid of a shared line plus one building’s own, calendars at the org, the building or both, people in two orgs, vendors at the org and at the building, PMS mixes, other kinds of property, operator mutations, isolation, failure modes, time, scale, consent and life safety. The head-level claim was attacked six ways and stood; the key moved from per-channel to per-address, at zero migration cost. Today the harness can only say RED: the contract has no expected verdict, the local store has no org row, and three of the four proof lanes do not run in CI.
Benches A / A-3own / A-calB / A-0 / A-0-legacy / A-3mail / B / B×3 / Z / O / 400 — plus Y and Y-mgr, the Sep 8 standup’s hard case: two CUSTOMER orgs sharing one building, which no earlier bench could seed because the second org in the parties bench has no login — seeded through production writers into a per-fixture local store; PortfolioCase {expect:'RED'|'GREEN', closesAt?, redBecause?, run()} — today RED-only (scripts/portfolio-harness/cases/types.ts); executors H (harness), D (drift test), R (replay), GAUNT, FIX, UNIT; every leg carries its fitness-function name and its closesAt step. 122 cases from rounds 1–3 with 130 refutations, plus 24 minted in this cut (the five staff cases, the two-paths case, four gap cases, seven UI cases, the six head-verdict lines and the packaging case), plus 5 from the delegated-administration decision, 17 from the Sep 8 standup and 45 from the red team’s five lenses (60 cases, 15 of which merged into an existing id rather than double-numbering) — 213 ids, 232 rows, re-derived off the catalog file at ingest and never carried in prose. Nothing was renumbered: the standup’s cases are ST-146 to ST-162 and the red team’s are ST-163 to ST-207.
Every case names a bench, a leg, an executor and the step that closes it — a case with no executor is not a test, it is a wish.
The head-level claim stood against six alternatives; the key moved to the physical address at zero cost, because no head row exists yet.
Gera’s calendar permutations are ST-17 / 18 / 20 / 21 / 22 — one org calendar, two leasing people, a calendar added later, one removed, one credential behind three rows.
The staff cases the invite form raised are ST-123 to ST-127, on the org-before-its-first-building bench and its unstamped twin.
Today the harness can only say RED; P2 is what gives it an expected verdict, and until then no case can prove a fix.
What this proves:ST-09 (Bellwether claims Park West’s number and the store refuses it), ST-13 (the hybrid: a3’s own line and the shared line resolve to different scopes with the same agent), ST-17 (a2’s calendar answers at the building, a1’s at the org), ST-24 (one login, two orgs, the switcher lists exactly the memberships) and ST-46 (zero rows over every family from the neighbour’s context) — all on this one bench. Today only ST-09’s refusal holds, and only inside one number-writing script with two bypasses; everything else is RED or cannot yet be expressed.
How to run these without the PMS
The seeding path. Every bench is written through production writers — the same functions the sync calls — so a fixture can never become a second way to create a row: saveProperty, saveUnits, saveTenants, saveProspect, saveLeases, savePropertyKnowledge, savePropertyLeasingSettings, saveVendor, saveVendorMembership, savePersonRole, saveUser, saveConversation, saveOccupancy, writeSyncLog, ensurePersonByClaim. None of them needs the PMS. The one hole is the org row: there is no saveOrganization (src/lib/data/interfaces/organization.ts:15-29), the local store returns nothing for an org (json-repository.ts:586-590), and the stage seeder writes the row at one sort key while the reader looks at another (scripts/seed-stage-orgs.ts:63-73 against src/lib/data/dynamo/organization.ts:44-46). That is P1’s first row and P2’s first blocker.
The fixture notation. A case is two files. The fixture (fixtures/portfolio/F-ST-nn.yaml) is what a designer edits — orgs, groups, relationships, properties, lines: [{number, binds_to:{org|group|property}}], mailboxes, calendars: [{id, binds_to:{person|property|group|org}}], staff, residents, prospects, vendors, knowledge, settings, pms_accounts, expect. The case is what a builder edits. Underneath, the same setup is five row shapes: HEAD(channel, address → node[, function]), att(node, kind#slot, {value}), rel(building, kind → party {attrs}), role(person, tier @ scope), tag(building → group{kind}). Every seed marks itself as a test: impossible area codes (+1000…), .invalid mail, sandbox orgs, and a drift test that refuses a real name.
The commands.npm run test:harness:portfolio (and :red, :list, :stamp) is the only lane in CI today. npm run test:harness:portfolio:fixtures, :gauntlet and :migration-proof exist and do not run in CI — three lanes that are green by never running. bin/load-fixture, validate-fixture and scripts/scope-parity-replay.ts do not exist at all; P2 writes them. The live robot-call lane has never dialled: it is blocked on the test line’s import into the voice platform, and the seeded org lives only in a throwaway directory where a real inbound call cannot find it.
The head-level mapping under stress. Six attackers built the strongest alternatives they could, and none of them beat the head on any of the five criteria; nought of six broke the level, five of six broke the same thing about the key. The full answer, with the repair and its cost, is on A2. The cases that exercise it here are ST-01 L1 and ST-13 (one uncached read from address to org and scope), ST-08b / ST-08d (the function sits underneath the address, not beside it), ST-09 (a second org’s claim refused by the store, plus the new sibling arm: the other channel of the same number), ST-10 (a retry that already committed reads as reused), ST-11a/b/c (one mailbox, two orgs; two spellings; the provider notification), ST-14a/b (a rebind is updates and inserts), ST-15b/c (a reply keeps its number) and ST-06 L4 (outbound never borrows a line).
The catalog
Every case, by family. 213 case ids in 232 rows — a case with lettered legs is one id and several rows, and nothing was renumbered or dropped. Shape is the setup in one phrase; seed is the bench it runs on; assert is what must be true; harness is the executor that says so; gates is the ladder step that turns it green. Cells are trimmed to fit the page — the full text of every row lives in the catalog record, and a cell is also cut short where the source names a vendor by name. The tag in the id cell is the refutation verdict: NR survived both lenses, RW is the refutation’s recut, NEW was minted in this cut, REGRESSION is an existing case re-asserted where the standup put it, buying no phase work and required green before and after.
The nineteen families, and where each one is. Every family below opens on a tap; nothing is hidden, only folded, so the pane reads as one page with a catalog behind it.
§ Routing — one org, many buildings, one address11 cases · 11 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-01
The org line rings once, answers for three
1 org : 3 buildings : 1 number
BENCH-A (with B1–B3). Call +10004000101 from unknown +10004000999; call +10004000201 on BENCH-B beside it.
L1 (mint): a call on a line whose HEAD names ORG# mints {organizationId: org_st_a, scope: ORG#org_st_a, propertyId: null, reach: {a1,a2,a3}}, home_state:'unknown'; both layers of the unpinned home asserted — mint propertyId: null (DF §9 L679) …
describe-table shows the PROPORG index ProjectionType: INCLUDE with exactly {lifecycle, answerable, asset_type}; one Query returns 400 items ≤200 bytes each ≈ 50 KB ≈ 6 RCU; no leasingCalendar, no CRED#, no token in the result.
FF roster-index-carries-no-secret (R on stage + H)
0
ST-03a
One mailbox, a lead names the building
— bound · 1 org : 3 buildings : 1 mailbox
BENCH-A. A lead relay to leasing@park-west.invalid with a2's address in the subject. Scan-order control (F4): run with a1 seeded first, then …
3b (inbound flips to HEAD per address, DF §10 row 3b); the INQUIRY# half needs §2.12
ST-03b
A lead names a building outside reach
same
BENCH-A. Relay subject names an address that is not a1/a2/a3.
bindHome → unknown, indistinguishable from a home that exists nowhere (§9 L685 — the same answer ST-47 asserts for a foreign building); the lead lands with propertyId absent.
same cases; NEW st-03b-address-outside-reach
3b
ST-03c
A portal lead with no address line at all
same
BENCH-A. A portal lead on the same mailbox, no address line.
Never reaches bindHome; lands unbound by construction, not by refusal — the only leg that proves the mailbox is bound to the ORG rather than to a subject-parsing trick. The unbound outcome has a key: one message to …
NEW st-03c-portal-lead-unbound; FF pre-pin-keys-resolve-at-org (escalation.owner two-phase)
7 — or "the step that re-homes the mail gate to the org" (68 reads / 25 files, DF §10 row 7)
ST-05a
A resident of building two calls the org line
— ring · resident on a shared line
BENCH-A + BENCH-Z (the twin resident on the same digits). r_a2 (+10004000121, occupancy at a2) calls +…101.
Recognition is never line-conditional: resolveIdentity → {personId: r_a2 of org_st_a, role: from occupancy.role, homeRef: a2} (§9 L686); never the twin r_z1; the gate keys on reach, so "the gate moved up to the org" is distinguishable from "the …
H-L2 voice-ring-resident-of-home-b-discarded (cases/voice.ts:170) → GREEN (re-pin its stale auditRows:174-177, which cite :457-466 / :487-500) …
4
ST-05m
The same resident on a maintenance line
resident on a shared function line
BENCH-A + HEAD(voice,+10004000102 → ORG#, fn: maintenance) + att(ORG#, phone_line#+…102 {function:'maintenance'}) (per §2.4 the function is on …
home_state:'suggested' with homeRef: a2, role from the in-org occupancy (§9 L681; FF L799); same agent id on both lines (§2.6).
NEW st-05m-resident-suggested-on-maintenance-line; FF same-agent-id-every-line ('suggested' clause)
4
ST-05b
The caller confirms; the thread binds
resident on a shared line
BENCH-A. After ST-05a, the tool webhook confirms a2.
bindHome on the tool webhook (§9 L685, L345): ORG#org_st_a/CONV#<id> gains propertyId: a2 + GSI8PK: CONVPROP#a2; ledger inserted 0 · updated 1 · moved 0; a bindHome on z1 from this context refuses.
NEW st-05b-confirm-binds-thread; FF bind-home-single-writer
5a (index itself 5b)
ST-06
The receipt text leaves on the org line
outbound address on a shared line
BENCH-A; BENCH-A-3own for L2/L3; BENCH-A with the org phone_line attachment absent for L4. Pure resolver, no person, no send (L1–L4); L5 needs no …
Assert the pair, not the address (§2.5): L1 BENCH-A, path(a3), cold → {address:'+…101', setAt:'ORG#org_st_a'} · L2 A-3own, path(a3) → {address:'+…103', setAt:'PROP#a3'} · L3 A-3own, path(a1) → still setAt:'ORG#…' (the …
Both lines mint {scope: PROP#b1, |reach|=1}; same mint tuple, same rendered DV set except function.
NEW H-L2 st-08a-two-lines-one-building — GREEN day 0 for routing (the one topology today expresses: N entries in …
0 (positive control, kept green at every rung)
ST-08b
A line carries a function; reach is function-scoped
same
BENCH-B as above; twin with capability_stage.maintenance flipped to off.
ctx.function is read off the line's own attachment — zero FN# rows, zero extra reads (§2.4) · negative twin: flip capability_stage.maintenance to off → +…202 mints |reach|=0 and known_out_of_scope while +…201 on the same building is …
NEW H-L2 st-08b-line-carries-a-function; FF answerable-stamp-exact (the 'any' clause)
3b
ST-08c
Same agent, both lines
same
as ST-08a.
§2.6 (a)(b)(c) on +…201 / +…202; agent id read from HEAD.agentId.
NEW st-08c-same-agent-both-lines; FF same-agent-id-every-line
Function known from the branch header → the FN# row narrows scope/route (§3.3 L240); a cross-org narrowAddress(voice, +…201, maintenance, {scope: PROP#a1}) under org_st_a → refused by ConditionCheck HEAD.organizationId = :ctxOrg with zero FN# rows …
NEW st-08d-one-did-two-branches; FF second-claim-refused-by-store (FN# clause)
1 (ConditionCheck is supported in the seam today, helpers.ts:2489-2498)
ST-09
A second org claims a number the first holds
2 orgs → 1 number (refuse)
BENCH-A + BENCH-B (B10: both orgs carry the pickup set, seeded before the claim — else the claim refuses for the wrong reason, §4.4 (i), and …
Arms, not one assert:(a)claimAddress('voice','+10004000101',{scope: ORG#org_st_b}) under org_st_b → {status:'conflict', heldBy:'other'} (§2.9); the rendered phrase "held by another org" is a separate copy check, never a gate on the FF …
FF second-claim-refused-by-store (H = arms a–f as outcomes; R = the staged real cancel at the HEAD item index, §2.7); GAUNT T14 'arbitrary home' (FAILS → …
1 (H and R); T14 flips at 3b
ST-10
The same org claims twice (a retry that already committed)
1 org, 1 number, 2 writes
BENCH-A (pickup set seeded; the input names scope: ORG#org_st_a + an explicit function). No 10-minute wait: a new token is outside the window by …
L0 pickup set unseeded → {status:'refused', keys:[…]} — makes the seed's own adequacy an assertion; cross-checks FF claim-refuses-until-pickup-keys · L1 seeded; claimAddress → 'created'; HEAD 1, ATTACH#phone_line 1, MAPLOG# 1, mapVersion…
L1/L3/counts 1 (H + D) · L2/L4 1 (R only — the twin has no token concept — grep-attested: no ClientRequestToken in src/lib/data/store.ts) · L0 3b; re-run …
{status:'conflict', heldBy:'other'} — not{status:'refused', keys:[…]}; one HEAD byte-identical before/after; zero rows / zero MAPLOG# under org_st_b (ST-09 arm (f) form).
H-L2 email-two-properties-one-mailbox-dropped (cases/messaging.ts:117) → GREEN; FF second-claim-refused-by-store (email channel; H + R per §2.7)
1 (H, R); the inbound flip 3b
ST-11b
Two orgs, one mailbox — the spelling variants
2 orgs → 1 mailbox (refuse)
BENCH-A + BENCH-B. org_st_b claims Leasing@Park West.invalid, leasing+ws@park-west.invalid, and a proxy/alias for the same account.
Every variant normalises to the one PK (normalizeAddress('email', raw): strip display name → NFC → lowercase → strip +tag on receiving-domain addresses only, HV §3 row 7) → the same conflict; an alias is never a HEAD (HV §3 row 8 delivery-address rule).
BENCH-A with a mailbox attachment carrying a subscription handle; an unknown subscription beside it.
A mailbox-provider notification for the address never fans to two buildings; the hop is one GetItem on a pointer written in the same transact as the claim — proposed SUB#<platformSubscriptionId>/PTR {channel:'email', address, organizationId, scope} …
NEW st-11c-subscription-pointer; FF unclaimed-address-refused-zero-reads (email)
1 (pointer row) / 3b (flip)
ST-12a
Many orgs, one building — refused by construction
— the store guard · 2 runners → 1 building (refuse)
GAUNT's own org_a/org_b seed (gauntlet/seed.ts:99-102; seedOrgC() precedent :107-110). Ordinary writer under org_b re-stamps …
The runner attribute cannot be re-stamped in place by an ordinary writer, by store condition — reuse the conditional put already shipping on Person (attribute_not_exists(PK) OR organizationId = :orgId → CrossOrgIdentityError, persons.ts:495-513) on …
NEW GAUNT T18 "foreign saveProperty refused" (§2.13; H + one staged real ConditionalCheckFailed with CancellationReasons, the ST-09 pattern)
RED → GREEN at 0; the "no other writer" clause 3a
ST-12a-D
— the D probes
none (rg).
Exactly one org-typed attribute on Property; exactly two writers may set organizationId on an existing PROP#/META (today: none); @ts-expect-error on an admin writer typed as a tenant writer (DF §11 L797).
D (rg-backed, DF §11 L772)
0 / 3a
ST-12b
JV — two owners, one runner, reach unchanged
JV allowed
BENCH-O. Two owned_by rows {share:0.5} each on a1; org_st_b's login beside.
Two owned_by rows are legal; two-sided: org_st_a still resolves reach={a1,a2,a3} with META byte-identical (the rebind-line-inserts-only hash shape); org_st_b's mint has a1 ∉ reach, getProperty(ctx_b, a1) null, reports return no Property/PERSON#…
NEW st-12b-jv-two-owners; FF isolation-gauntlet (act-as writes only org-stamped rows); FF reports-push-authorised-by-row
CANNOT_EXPRESS → GREEN 6a
ST-12c
"Two Claras on one address" = a transfer
runner changes
BENCH-O. Run §7 #8 (L590) on a1.
Folded into FF transfer-is-two-partitions (L804): after the cut A reads zero post-cut rows, B zero pre-cut rows; ScopeMoved not 404 (L717; FF workflow-survives-rebind L802). The real risk is the runner silently changing, not two at once.
NEW st-12c-transfer; FF transfer-is-two-partitions
6a
§ Hybrid — a shared line plus one building’s own7 cases · 7 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-13
Three buildings, two on the shared line, one on its own
BENCH-A-3own on a private bench (M4); hash every row (§2.8). rebindAddress('voice','+…103', PROP#a3 → ORG#), then back.
Every pre-existing row still readable (live, or under HIST# at the same startedAt with only endedAt/endedBy added); PK moved == 0; delta = inserts + attribute updates on ADDR#/ATTACH#/HIST#ATTACH#/PROFILE/MAPLOG# only; a3's CONV# …
FF rebind-line-inserts-only (seed BENCH-A-3own as its second seed — the FF seeds F05, which moves a line to a chain group and has no org-scoped …
3b
ST-14b
Undo the rebind
hybrid rebind (E3)
as 14a; after rebind 1, undoMapChange(v).
Re-Puts the t0 row from HIST# byte-identical (§7 #18 L600) — the only leg maplog-undo-exact (L793) actually covers; negative: a touching later write ⇒ named refusal ("v43 changed a3's line after this").
FF maplog-undo-exact
1
ST-15a
Outbound picks the nearest line
— cold · hybrid outbound
BENCH-A-3own. Text about a3, then about a1.
= ST-06 L2 + L3 on the same seed: {address:'+…103', setAt:'PROP#a3'} / {'+…101', 'ORG#'}. RED today because both return the global env sender.
NEW sibling today-case outbound-hybrid-own-line-invisible (do not reuse case 16 — 3b's green must not be claimed on a predicate the hybrid never moved); FIX …
3b
ST-15b
The reply leaves on the address it arrived on
hybrid outbound, reply leg (the collision)
BENCH-A-3own. Inbound SMS to +…101; bindHome(a3); Clara replies. Writes one CONV# row — the case asserts its own precondition (the bench is …
Reply leaves from +…101, sourced from the conversation's arrival address via mintFromConversation (DF §5.1) and not from the cold walk (which would say +…103) — §2.5. GREEN today (process-envelope.ts:463), so a positive control kept green at …
NEW st-15b-reply-on-arrival-address; FF thread-keeps-its-number (HV §7)
every step (control); the arrivalAddress field itself 5a
Writes no HEAD (§3.2 L232 "outbound-only rows take no claim"); a foreign claim on +…104 is not refused (nothing to collide with); an inbound to +…104 refuses (E12) with zero further reads. HV §3 row 16: an inboundphone_line row written by raw …
NEW st-15c-outbound-only-row; FF unclaimed-address-refused-zero-reads; FF one-inbound-row-per-address (HV)
1 (row) / 3b (refusal)
ST-130[NEW]
Hybrid mailbox — the org inbox plus one building's own
the email twin of ST-13/ST-15 (critic §7 #31; GAP F4/F5; HV §3 rows 2, 6, 8, 16, 17, 21)
BENCH-A-3mail (§1, B17). Two leads: one to leasing@park-west.invalid naming a1 in the subject; one to a3@park-west.invalid with no address …
L1 (inbound, 3b): the leasing@ lead mints {org_st_a, scope: ORG#, reach {a1,a2,a3}} and binds a1 from the subject (ST-03a); the a3@ lead mints {org_st_a, scope: PROP#a3, |reach|=1} and is bound by the address it arrived on — read ledger …
H-L2 email-shared-mailbox-first-match-wins (cases/messaging.ts:74) kept as the today witness for L1's org half; NEW H-L2 st-130-hybrid-mailbox-inbound…
§ Calendars — at the org, at the building, or both14 cases · 14 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-04a
One org calendar, a tour at building two
1 org calendar : 3 buildings
BENCH-A (B5: zero live leasing assignment/schedule on the chain, asserted at seed time). a2 has no owned_by, or one with …
findTourCandidates(ctx, {propertyId: a2}) reads free/busy for externalCalendarId of att(ORG#, calendar#primary)once, not three times (slots alone cannot tell the org-scoped answer from three buildings seeded with the same external id) …
NEW F-ST-04 probe + the recording free/busy stub at the provider seam (note 05 M5 — only the token endpoint is stubbed today, setup-env.ts:71-75); UNIT …
today: CANNOT_EXPRESS (not RED — calendar@org is an absence probe, tokens.ts:144-148); expect …
ST-04b
The owner did not consent to pooled hosts
same, consent confound
BENCH-A + rel(a2, owned_by → org_st_o {share:1, grants:['reports']})withoutleasing_line; plus an assignment at ORG# for agent_a1 (a …
The org-level host is dropped by the §6 consent filter (A2) and the case does not silently fall through to office hours. What happens next is open (§7 O2): (reading 1, ST-04's) a refusal naming the building and the consent; (reading 2, ST-17's …
NEW F-ST-04b; UNIT scheduling-is-pure (F07 negative); FF consent-never-gates-reach
8 (precondition 6a for the owned_by row)
ST-16a
Per-building calendars (the control)
— seeded · 1 calendar per building ×3
BENCH-B ×3 with three independent credentials.
L1 every key resolves setAt:'PROP#b*' or absentMeans · L2 negative control: one PARAMETER key at ORG#org_st_b with no building row answers setAt:'ORG#org_st_b' on all three; one IDENTITY key (writableAt:['property'], §4.2) written at ORG# …
FF camellia-replay-byte-identical (H half); FF cred-refresh-single-flight; UNIT scheduling-is-pure. FIX column corrected: F01/F08 pass today and …
L1 2b · L2 2a · L3 8 · L4 8 (CRED#)
ST-16b
Per-building calendars — the parity replay
same
The Camellia tour-replay corpus, no seed, gated at the shipped src/__tests__/tour-replay/ oracle. The propertyId → b1..b3 projection written …
Byte-identity at (i) the AvailabilityStageOutput projection and (ii) the rendered prompt block — rendered text plus the decoded{propertyId, date, time} claims, never the token string; nearbyAlternatives = [] on F01. Re-pin DF §6 to …
FF camellia-replay-byte-identical (R half). Executor R does not exist (scripts/scope-parity-replay.ts, note 05 M11); closesAt:'every step' = N …
every step
ST-17
Mixed — org calendar, one building with its own
1 org calendar + 1 building calendar
BENCH-A-calB. Tour at a2; tour at a1.
a2 hosts on c_a2 (provenance.node === 'PROP#a2' — the nearest node with a live calendar row, §2.3); a1 hosts on c_a (provenance.node === 'ORG#org_st_a'); a2's row does not mask a1's; the two provenance tuples are equal except node and the …
NEW F-ST-17 probe + the recording free/busy stub (shared with ST-04); UNIT scheduling-is-pure; drop every-effective-value-has-provenance (settings …
BENCH-A + CRED#c_p1, CRED#c_p2 · att(PERSON#agent_x, calendar#primary), att(PERSON#agent_y, …) (R1: renamed from agent_a1/a2 so the seed …
L1 availability = union of the two persons' free/busy ∩ holds; the org calendar is not consulted while a host exists (§2.3) · L2 (R4, split three ways) a counter assert (LRB# advances by exactly one per booking), a determinism assert (same seed …
FIX F04 (F04-C2-M4, F04-E6-within-org) NOT_RUN → PASS; UNIT scheduling-is-pure T3; FF cred-refresh-single-flight (ST-18b only); NEW F-ST-18 probe + …
L1–L4 8 · L5 8 (CRED#) · seed-time precondition B5 (zero live leasing assignment/schedule on the chain …
ST-20[NR]
A calendar added later for one building
— Gera's second panel (BRIEF §2) · org calendar, then a2 gets its own
BENCH-A at mapVersion v; then CRED#c_a2 + att(PROP#a2, calendar#primary). Snapshot dumpOrgMap and every key × building resolve before and …
L1 after: a2's tours book c_a2 (needs M5 — the row is NOT runnable before the free/busy stub, R2; stress-r1.md:188's M5 row gains ST-20) · L2 (the falsifier, R1) a1/a3 resolves byte-identical before/after is vacuous both ways (today reads one …
FF map-dump-stable (1); FF cache-exact "a value-only edit leaves the roster cache untouched" (3b); NEW F-ST-20; validate-fixture prints the phase_1 …
L3 1 · L5 3b · L1/L2 8
ST-21[NR]
A calendar removed
endAttachment on a building calendar
BENCH-A-calB with two booked TOUR# at a2 and one live HOLD#. endAttachment(PROP#a2, calendar, primary, t0); then undoMapChange(v) (R4).
L1 the row moves to PROP#a2/HIST#ATTACH#calendar#primary#t0 in the same transact, zero live rows for the slot (Query begins_with 'ATTACH#calendar#' = 0; HIST#…#t0 = 1 with endedAt/endedBy) · L2 new tours at a2 book c_a (org) under §2.3 …
FF one-live-row-per-slot (1) for L1; FF maplog-undo-exact (1) for L6; UNIT scheduling-is-pure (8) for L2 — notevery-effective-value-has-provenance…
L1/L6 1 · L2–L4 8 · L5 8 (CRED#)
ST-22[NR]
One credential, three calendar rows, two refreshers
credential rotation
BENCH-A + att(PROP#a1, calendar#primary {credentialId:c_a}) + the same at a3 (three rows → one CRED#). Expire the token; fire two refreshes …
One provider call (the recording stub counts), one LEASE, both callers end with the same token; a SECRET write with a stale version → ConditionalCheckFailed and the loser re-reads; no row holds a copied refresh token (the PROPORG projection carries …
H-L2 calendar-shared-token-refresh-clobbers-siblings (cases/messaging.ts:274) → GREEN at 8; FF cred-refresh-single-flight (8); FF …
8 (CRED#/LEASE); the index half 0
ST-23[NR]
A leasing person's role ends
role-end cascade
ST-18 bench. savePersonRole(agent_x, active:false). Owns only the cascade + candidate-list arms — the booked-tour arm moved to ST-63 …
L1 in one transact her assignment rows move to HIST# and every live schedule row naming her carries out:true · L2findTourCandidates lists agent_y only — asserted once at the org node, not three times at a1/a2/a3 (R8: ST-18's assignment is …
FF role-end-cascades T6′ (8; H + unit).
8
ST-63[NR·MERGED]
A leasing role ends between the hold and the tour
— the booked-row arm of ST-23 · role-end vs a booked row
ST-18 bench + PROP#a1/REL#owned_by#ORG#<owner> {grants:['reports'], poolingConsent:['leasing_line']} (R3) + arm A a seeded PROP#a1/TOUR#<id> …
arm A (pure, runs the moment M2/M3 land): the cascade ENDs her assignments, schedule out:true; the booked TOUR# keeps its calendarIds[]/assignedPersonId snapshot (DF §6 L564, §7.1 row 10); a row opens — ORG#<org>/TASK#<id> …
arm A: FF role-end-cascades (T6′) extended + UNIT scheduling-is-pure; NEW F-ST-63. arm B: note 05 M5 + a booking stub (add ST-63 to stress-r1.md:188's …
8 (H + unit)
ST-65[NR]
The org calendar's credential is revoked at 06:00
one credential, N buildings, blast radius
BENCH-A (c_a) + ST-22's fan-out (c_a referenced from a1 and a3 → three attachments, one CRED#) + a building with its OWN credential …
All three c_a buildings degrade identically and loudly: holdSlot/bookSlot refuse with a named reason; availability is returned marked calendarReadFailed, never as clean office-hours slots; one task row per CRED# (the TASK# kind from ST-63 …
FF cred-refresh-single-flight (8); H-L2 calendar-shared-token-refresh-clobbers-siblings → GREEN at 8; NEW F-ST-65 with the 401 stub mode.
8
ST-98[NR]
The hold that outlived its TTL
a TTL'd row is not gone at expiresAt
BENCH-A. Caller 1 holdSlot Tue 14:00 at a2 (CAL#c_a/HOLD#<start> {expiresAt: now+10m, token: tk1}); the store's TTL sweeper has NOT deleted the …
S1–S5:holdSlot/bookSlot compare expiresAt to now() in the ConditionExpression themselves (attribute_not_exists(PK) OR expiresAt < :now OR token = :mine) — the TTL attribute is garbage collection, never the lock; at minute 11 the slot is …
NEW FF hold-expiry-in-condition (H + R, 8, encodes E33); the read-filter clause also in UNIT scheduling-is-pure; the json twin's conditional-put emulation …
8
ST-100[RW]
The connector's calendar was the leaver's
credential principal vs role end
BENCH-A-calB + USER#u_agent_x/MEMBERSHIP#org_st_a {personId: agent_x} (a5 — the join the case never seeded) · CRED#c_a/SECRET {kind:'oauth_user' …
The finding keys on the credential's principal (whose account dies), never on connectedByUserId (who clicked): c_a (principal = the leaver) → one TASK#CRED#c_a {state:'reconnect_before_expiry'} naming every attachment that references it; c_a2 …
FF role-end-cascades (T6′, 8) gains the credential clause, reading SECRET.principal (DF stores connectedByUserId and reads it nowhere — this gives the …
8
§ People — a person in two orgs, the third-party manager, the property owner, the switcher22 cases · 22 rows — ST-141–ST-145 added 2026-09-08 with the delegated-administration decision
id
Title
Shape
Seed
Assert
Harness
Gates
ST-19[NR]
A manager promoted to two buildings
staff covers a second building
BENCH-A as it is — every leasing agent already at: org (R-list §3.2: the round's pre-state "assigned at a1 only" contradicted BENCH-A and …
L1findTourCandidates(a2) now lists agent_z; L2 (the discriminating negative, §3.1 of the refutation)findTourCandidates(a3) lists the org pool and not agent_z — falsifiable only because z is building-scoped and the org pool is present · L3…
NEW F-ST-19 probe; UNIT scheduling-is-pure (this case = its building-node control for the F07 negative); FF registry-closed-and-typed (the assignment …
8 (L1/L2, UNIT) · 6a (L3, actor.buildings)
ST-24[NR]
The switcher: one login, two orgs, explicit pick
person in two orgs
BENCH-A + BENCH-B, both humans seeded (R1): the existing one-Person/two-roles staff for T7 and T8's today observation, and the target pair …
(a)mintFromSession(session,'org_st_b').roster == {b1,b2}; …'org_st_a' == {a1,a2,a3} · (b) negativemintFromSession(session,'org_st_c') with no MEMBERSHIP#refuses (DF §5.1 "membership must exist") · (c) order control re-seed with the …
ST-24 bench + CRED#c_A (org_st_a), CRED#c_B (org_st_b, connected with B's consent), att(PERSON#h_a, calendar#primary {externalCalendarId: X …
ST-25a (wording-neutral; F06 carries it): no read under org_st_b's TenantContext returns any org_st_a identifier — no TOUR# from a1, no PROP#a1, no ORG#org_st_a, no PERSON#h_a, no LRB# naming an org_st_a node · ST-25b (the discriminator) …
GAUNT T9 rewritten to a behavioural probe → PASSES at 8 (R6); FIX F06 = add credentials: + read_mode:/write_mode: (R5), scored under both §17 #1 …
8 (calendar rows) · 6b (the MEMBERSHIP#/PLINK# precondition)
ST-26[NR]
Staff at org A texts org B's line
staff on a foreign line
BENCH-A + BENCH-B. pm_a's phone texts +…201; then calls it. F1 precondition throw: resolveActor({phone: PHONE_PM_A, propertyId: PROP_A …
On SMS and on voice the sender is unknown at org_st_b: staffTiers=[], no org_st_a HomeSpec.address string in any prompt block (F3 — the behavioural closing assert; the anchoredRead source probe stays as the RED reproducer only), no tier admitted; a …
GAUNT T3 (FAILS → PASSES at 3b); H-L2 isolation-portfolio-person-on-other-co-line (cases/isolation.ts:137); FF isolation-gauntlet "PM at org A on org …
3b
ST-27[NR]
The manager's staff see one building inside the runner's wall
managed_by
ST-27a: BENCH-M minus the membership row; m_pm given the property-scoped role directly. ST-27b: full BENCH-M with …
27a:mintFromSession yields {organizationId: org_st_a, actor.buildings: {a3}} — one wall, not two; a1/a2 read 404 byte-identical to missing; org-level grantee keys (vendor_roster, escalation.owner) hidden (visibleToGrantee false); every row she …
FIX F02 → PASS at 6a; GAUNT T13 (ADR-0120 envelope) at 6b; FF isolation-gauntlet "the manager's act-as context writes only rows stamped with the runner org".
27a 6a · 27b 6b
ST-28[NR]
Invite before the relationship is accepted
managed_by not yet live
BENCH-A only (no managed_by). Try USER#m/MEMBERSHIP#org_st_a {invitedForParty: ORG#org_st_m} and role(m_pm, pm @ {propertyId: a3}).
savePersonRole refuses while no accepted managed_by row for that party is live on a3; the refusal names the missing row; zero rows written — including the User row: today the User row written at admin/users/route.ts:252 survives a later refusal …
NEW GAUNT row T16 "invitedForParty without managed_by" — id collision: ST-46 R8 and ST-47 also claim T15/T16 (§7 O12); FF registry-closed-and-typed …
L1 in the same transact m_pm's property-scoped roles on a3 end — computable only after R2: stamp the party on the role row, PersonRole.grantedUnder?: 'ORG#<party>', written by the invite path (DF §5.2 L271/L528 names the cascade; MEMBERSHIP# is …
NEW F-ST-29 (phase_1 = END only); FF role-end-cascadesrelationship variant — a different cascade from T6′ (R8: the §11 FF is role → …
6a
ST-30[NR]
The owner reads reports and nothing else
owned_by + pushed reports
ST-30a (owner-as-customer): a login at the owner org, acceptedAt set (the record's customer-owner instance, DF L636). ST-30b (owner-party …
mintReports yields {organizationId: org_st_o, buildings: {a1,a2}}; IReportsRepository returns the pushed rows and never a Property, PERSON#, CONV#, KNOWLEDGE or ATTACH# — the forbidden families are physically present in the same partition …
FF reports-push-authorised-by-row (6a); FF isolation-gauntlet "property-owner login reads exactly ORG#<owner>/REPORT# rows and zero …"; FIX F07 (S6).
6a
ST-31[NR]
Owner consent gates hosts, never reach
poolingConsent
ST-31a (reach, step 4): one org line, three homes, two owned_by rows on one owner with different consent; eight writes in a matrix …
31a: both homes stay in reach and are answered; a bind returns known_out_of_scope for the home that is known and out of scope and unknown for the foreign one; home_options shrinks with the answerable stamp; the today-column names the key whose …
31a: FF consent-never-gates-reach (H, 4). 31b: UNIT scheduling-is-pure "a host above the building is dropped without leasing_line consent (F07 …
31a 4 · 31b 8
ST-60[NR]
The wrong org in the switcher
a legitimate write in the wrong wall
BENCH-A + BENCH-B, one human with MEMBERSHIP# in both (ST-24's login). Pick org_st_b, then write three rows meant for org_st_a …
Every row is stamped org_st_b and is valid — not an isolation failure, no gauntlet row fires; the cure is a second edit, never undoMapChange (MAPLOG# is per org, L304); the map dump for org_st_b names the three rows with setBy.userId + timestamp …
GAUNT T8 (FAILS → PASSES at 6b) for the pick half; FF map-dump-stable (1) + FF maplog-undo-exact negative half for the three-row half; NEW GAUNT …
L1 a read at instant T returns exactly the row live at T (REL#owned_by#ORG#<party>#<validFrom> keyed by validFrom; the ended row is never a candidate) · L2/L3 the October report is pushed at periodTo and the ConditionCheck runs against the …
FF reports-push-authorised-by-row (6a) gains L2/L3 + the D sentence; NEW F-ST-68{expect:[L1..L4], requires:[rel.owned_by, rel.dated, report.owner_view]} …
6a
ST-71[RW]
Twelve walls, one admin
the picker at scale
BENCH-12. h_admin signs in (12 MEMBERSHIP# rows + platform_admin in org_propflow_staff); pick org 7; end the membership in org 3 while the …
mintFromSession(session, activeOrganizationId) is explicit and the picker lists exactly the 12 accepted memberships (never getOrganizations()); a stale activeOrganizationId for the ended membership is refused at mint — never "the first one" …
GAUNT T8 + T12 (→ PASS at 6b); FF isolation-gauntlet (AdminContext uncompilable as TenantContext); NEW GAUNT row "ended membership is not a pickable …
6b
ST-123[NEW]
Invite into an org with zero buildings
the job is attached at the org; there is no building to hang it on
BENCH-A-0 (§1, B16): ORG#org_st_a/PROFILE + the pickup set, properties: [], no staff. Act (1) inviteStaff({org: org_st_a} …
L1 one transact, five rows: PERSON#p_ag/PROFILE · PERSON#p_ag/CLAIM#email#ag@… · PERSON#p_ag/ROLE#{leasing_agent, scope:{organizationId:'org_st_a'}} (no subset = the whole roster, §2.15) · USER#<authId>/MEMBERSHIP#org_st_a · ORG#/MAPLOG# with …
NEW fixture F-ST-123 on BENCH-A-0 (B16: the loader must accept properties: [] and staff with no building); NEW H-L2 st-123-invite-into-empty-org…
L1 the second invite adds a secondUSER#u/MEMBERSHIP#org_st_b and a second PERSON#…/ROLE# beside the first — one auth row, two walls; no second login, no row rewritten in org_st_a · L2 the two Persons are joined by PLINK#<id> readable …
NEW fixture F-ST-124; NEW H-L2 st-124-one-person-two-orgsexpect:'RED'; D pin exists-elsewhere-returns-boolean at 0.5; GAUNT T8 + T12 (the …
L3 D pin 0.5 · L4 6a · L1/L2/L3 6b
ST-125[NEW]
A building moves org — the subset must not leak
the grant is a row, so a move is an END
BENCH-A + BENCH-B. pm_x invited into org_st_a with buildings:['a1','a2'] → two ROLE#{pm, scope:{propertyId}} rows and no org-scoped row …
L1 her next mint's actor.buildings excludes a1 on the roster read alone (the union over live role rows ∩ the roster, §2.15) — a1 reads 404 byte-identical to missing from org_st_a; this leg holds whichever way §7 O12 is answered · L2 org_st_b …
NEW fixture F-ST-125 (phase_1 = TRANSFER only); NEW H-L2 st-125-transfer-shrinks-the-grantexpect:'RED'; GAUNT T27 (§4 step 6a already names …
L1 the write is accepted — requireScopeForRole widened to take {propertyId} for every workspace tier (§2.15) and the index key is ROLE_SCOPE#org_admin#PROP#a1 (src/lib/data/dynamo/helpers.ts:936-944, the builder already handles all three …
NEW fixture F-ST-126; NEW H-L2 st-126-org-admin-with-a-subset — expect:'RED' on L1/L3/L5's subset arm, the org-wide arm expect:'GREEN' in the …
L1/L3/L5 6a · L4 6b · L2 blocked on §7 O13
ST-127[NEW]
A building with no organizationId stamp
an unstamped row is on nobody's roster and hires nobody
BENCH-A-0-legacy (§1): BENCH-A-0 plus one PROP#x_legacy/META written by the raw probe put — no organizationId, no ROSTERPK …
L1 the write-time invariant: after step 0 an unstamped PROP# can be created only by the raw put — saveProperty refuses property_unstamped naming the id (T18, §2.13); the legacy row survives as a fixture, not as a thing the product can make …
NEW fixture F-ST-127 on BENCH-A-0-legacy; NEW H-L2 st-127-unstamped-building-blocks-the-inviteexpect:'RED'; GAUNT T18 (§2.13's …
BENCH-A. PERSON#office_mgr/ROLE#r2 {tier:'property_manager', scope:{organizationId:'org_st_a'}, grants:['manage_roles'], grantedBy:'PERSON#admin_a'}; a target with a leasing role. Act: she writes a grant she does not hold, and a rung above her own.
L1 refused by name, naming the unheld grant · L2 the role half already holds and must not regress (manage-teammate.ts:66-68, :98-99) · L3 the refused write leaves zero rows and no map-log line · L4 the grant set a delegate may hand is the intersection of what she holds with what her rung may invite, asserted as a set …
NEW UNIT st-141-no-escalation-in-grants; FF no-escalation-in-grants; D rg — one writer
6a / P7
ST-142[NEW]
A delegate tries to mint another administrator
the ceiling
ST-141's seed. She holds manage_roles and is not org_admin; she writes grants:['manage_roles'] onto a teammate. Control arm: the org_admin writes the same grant.
L1 refused by name — only an org_admin may hand it on · L2 the control arm is created, so it is a ceiling and not a lock · L3 holding the grant is not transitive · L4platform_admin is the only caller above the ceiling and writes its audit row first …
NEW UNIT st-142-only-top-level-grants-manage-roles; FF no-escalation-in-grants
6a / P7
ST-143[REGRESSION]
Two peers edit each other
confirmed, already true
BENCH-A. Two property_manager rows at the same rung, both org-scoped; each edits the other's name, phone and assignments, then the other's role within reach.
L1 both succeed — this is what already ships: canInviteRole returns inviterLevel <= targetLevel (permissions.ts:208, in :199-209), filtered by assignableRoles (manage-teammate.ts:108-110) · L2 across the wall it is 404 Teammate not found (:61-65) · L3 self-edit stays refused (:58-59) · L4 buys no phase work …
Existing UNIT extended with the peer pair; D rg (no second writer)
already green — regression through P7
ST-144[REGRESSION]
The last administrator tries to remove themselves
at least one live holder
BENCH-A with exactly one ACTIVE org_admin. Arms: (a) self-removal · (b) self step-down · (c) a delegate holding manage_roles removes them · (d) a second admin made ACTIVE, then (a) and (b) retried.
L1 (a) and (b) refused with the shipped sentence — isLastActiveAdmin (manage-teammate.ts:49-50) enforced at :79-80 and :101-102 · L2 (c) refused by the same guard, so the ceiling opens no side door · L3 (d) both succeed once a second holder exists · L4 only ACTIVE holders count …
Existing UNIT extended with legs (c)/(d); FF no-escalation-in-grants (last-holder arm)
L1/L3/L4 already green · L2 gates 6a / P7
ST-145[NEW]
A property owner is assigned by the same admin path
not an exception, not a rung
BENCH-O. The org_admin adds a property owner to a1 through the admin path: ORG#org_st_o/PROFILE {purpose:'owner_party'} + owned_by {share:1, grants:['reports','financials','occupancy','work_orders','documents']} — the decided default, conversations absent. Then a delegate adds a second; then a grant of conversations; then endRelationship.
L1 the grants arrive on the owned_by row, not as a rung — no PersonRole for the landlord, and no landlord level in ROLE_HIERARCHY (permissions.ts:52-57) · L2 the delegate may add one but still cannot hand manage_roles on · L3 the wall does not move — pointer, not a restatement: ST-30 and ST-138 own those asserts · L4conversations only on a deliberate grant; adding it later is END + INS, revoking is ending the row · L5Property.ownerId is the PMS credential holder (types.ts:3371) and appears in no roles matrix …
NEW UNIT st-145-owner-grants-arrive-from-the-relationship; NEW D st-145-ownerid-never-in-a-roles-matrix; FF reports-push-authorised-by-row
An org vendor is reachable at every building of that org and nowhere else
org vendor
BENCH-V (ids per the bench repair; rowsTouched = VENDOR#vendor_ …
org_st_a reaches {v_plumb, v_local}, org_st_b {v_plumb}; vendor_roster.plumbing at a1 and a3 = [v_plumb]setAt: ORG#org_st_a; at b1 = absentMeans []; zero VMEM#org_st_a items to an org_st_b context — stated per reader (R6) …
NEW H-L2 st-32-org-vendor-reach — polarity: today's L2 file is RED-only (cases/types.ts:1-13); the GREEN day-0 reach half needs the step-0.5 contract …
reach 0 (control, after 0.5) · roster 2b
ST-33[NR]
A property-preferred plumber replaces the org list at one building only
property-preferred
BENCH-V defined in the case (R1): one org; a1, a2, a3 via saveProperty; v_plumb, v_local with VMEM# for both, plus v_orphan with no…
a2 → [v_local]setAt: PROP#a2 (replace, never merge — v_plumb absent); a1 → [v_plumb]setAt: ORG#; a roster entry naming a vendor with no live VMEM# is refused at write — new putAttachment refusal (k) naming the vendor id (R4) — and …
Executors fixed (R7): H-L2 on the json bench (R cannot run it — scope-parity-replay.ts is read-only vs stage/prod where v_local, a vendor with no PMS …
2b
ST-34[NR]
One directory vendor, two orgs, two contacts
directory vendor across two orgs
BENCH-V with one human behind p_va/p_vb — the same phone and email claimed in both orgs (R1: with different contact values no reader …
ST-34a contact-resolution symmetry — org_st_a's dispatch email and dial both reach PERSON#p_va's claims, never p_vb's; org_st_b's reach p_vb; the phone lane's chosen Person pinned by name under a fixed claim ordering (R5) · ST-34b dial …
34a: NEW H-L2 st-34a-vendor-contact-org-scopedexpect:'RED'; 34b: expect:'GREEN' day 0; 34c: NEW GAUNT row — not T17 (spent …
34a 3b · 34b 0 · 34c 3b (HEAD)
ST-35[NR]
A ghost vendor belongs to no org
unattached directory row (the Atlas flaw, BRIEF §4)
BENCH-V + v_ghost (zero memberships), written through saveVendor only. Read from every TenantContext; then from AdminContext.
a1–a4: zero items from every TenantContext; resolvePreferredVendors drops it; the dial refuses "no phone → no call"; only AdminContext lists it · a5′ re-pointed at the join, not the page (R-mandatory): the Atlas org card is asserted through the …
NEW GAUNT row (the round's T18 → renumbered T20 in the refutation; T18 is spent by ST-12a's foreign saveProperty refused, §2.13 — §7 O12): a1/a2/a3 …
0 (control) · a5′ P8 (the org card)
ST-36[NR]
The same vendor at two orgs is two Persons, one vendor firm
identity across orgs
BENCH-V + an email claim on bothp_va and p_vb with the same normalized value (R2 — phone-only claims trip the email precondition at …
36a refused — asserted at the rows (R3): today mergePersons deprecates claims, re-mints them, deactivates roles and moves references beforesoftRedirectPerson throws (merge-persons.ts:169-231, 271, 311-317, 348), so "refused at the throw" …
GAUNT T10 (PASSES for the merge refusal; the rehome half is grep-attested only, gauntlet/rows.ts:617 — rewrite to a behavioural probe, R4); FF …
the second account's vendors — polarity flipped, defect swapped
BENCH-P. Vendor discovery runs acct-1, acct-2, acct-1 again (three passes) with two real plumbers who share one PMS vendor id across the two …
(a) GREEN today — the control:4480 and 4490 both appear after pass 2 — the wall is the org, not the credential (property.ts:916-930); keep as a positive control, not a red · (b) RED today — identity: after pass 2 there are two live cards for …
NEW H-L2 isolation-two-accounts-one-vendor-roster (three passes; expect:'RED' on (b)/(c) with closesAt = the vendor slice; (a) the GREEN control in the …
(a) 0 · (b)/(c) P8 (the vendor slice) · key shape OPEN (M21)
ST-89[RW]
The building's plumber is promoted to the org
a roster row moves up one node
BENCH-V after ST-89 phase_0 = no org-level roster row (the round's INS collided with a live slot on the bench it named, D1). v_local is …
(i) after the TX all three answer [v_local] with setAt: ORG#; a2's value byte-identical, provenance differs (setAt, contributors); listAttachments(ORG#, 'vendor_roster') = exactly one live plumbing row + two HIST# rows · (ii) negative …
NEW H-L2 st-89-promote-roster-one-transact (json bench + read ledger, three arms); NEW F-ST-89 (phase_0/phase_1); executor R dropped …
2b (the key) · the seam op 1
ST-90[RW]
Dropped by one org, kept by the other
the directory vendor's membership ends
BENCH-V addendum (R1): p_va.startedAt = T0+1d, p_vb.startedAt = T0 (order decides today's any-org winner), both role:'owner'; one WO at a1 and …
ST-90a reach — getVendors(_, org_st_a) on both backends, scopeVendors's input set and resolvePreferredVendors at a1 all exclude v_plumb; expect:'RED' · ST-90b WO assignment — create_work_order at a1 with assignedVendorCompanyId …
NEW H-L2 st-90-ended-membership-drops-reach (RED today); GAUNT T25 reworded to the isolation half and folded with ST-34's row under one id (§7 O12); FF …
unassigned — the sub-step of 2b that owns "reach = live VMEM#" (§7 O14); 90d 2b
ST-91[RW]
The vendor's only contact leaves
a reachable org nobody can reach
BENCH-V (bench repair) + the card's public contacts declared per arm; END the owner-role membership carrying PERSON#p_va, leaving org_st_b's …
ST-91a card {publicPhone:'', publicEmail:''} → every channel refuses with the same reason and one task row (ORG#<oid>/TASK#vendor_unreachable#<woId> {vendorCompanyId, trade, channels, workOrderId}, idempotent on the key — R5, the TASK# kind …
H-L2 st-91a-no-contact-refuses-every-channelexpect:'RED'; st-91bexpect:'GREEN' day 0 (regression pin); FF complete-mediation-rg gains one clause …
91a 2b (the retirement sub-step) · 91b 0 · 91c 2b + the TASK# kind
ST-121ahandleCreateWorkOrder({property_id:a3, unit_number:'4'}, conv, {senderId:+…131}) under the router's per-turn handyman flag → a WO under PROP#a3 with personId = p_hm, no handymanCreate rationale; a second call for a1 unit 2 (in propertyIds…
121a: H (handler-level, json) + UNIT; 121b: H-L2 ring() + a NEW json call-ended drive; 121c: UNIT + D; 121d: H + FF every-effective-value-has-provenance.
one vendor firm, per-org isInHouse, two dispatch paths
BENCH-V. v_plumb.trade = 'general', category general (R3 — tier 1 hard-codes isInHouse:true regardless of the membership; a1 must pick on tier …
ST-122a a1 dispatches in-house from the membership row; ST-122b-i (day 0) external at b1 parks with estimatedCost:null — the fail-closed rule "no invented money decides spend" (:1303-1308), a GREEN regression pin; ST-122b-ii the cap as a key …
122a/122b-i/122c/122d: H (json; but the json twin's MAINTENANCE_SETTINGS are stubs, json-repository.ts:178-179 — a step-0.5 item, R4); 122b-ii: H …
L1 (reach is the tag's membership, never the walk):pickVendor(ctx,{propertyId:a1, trade:'plumbing'}) → v_pod (via: vendor_roster.plumbing@ORG#, first id ∈ E, VS §5 step 3); at a3 → v_plumb with skipped:[{vid:'v_pod', reason:'not_engaged'}] …
NEW H-L2 st-131-group-coverage-pickvendor (json; needs GROUP# rows + the in_group tag writer in the twin — the ST-95 unblocker, 6a) + NEW UNIT …
L1/L5 2b (pickVendor, the (k) rule) after 6a (the rows exist) · L2–L4/L6 6a · L7 P8 (the org card)
§ PMS mixes — two accounts one wall, two types, per-role slots, the sandbox inside7 cases · 7 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-37[NR]
One org, two PMS accounts on different buildings — allowed
ST-37a (the Situs slice, DF L719): write ① refuses with the E17 code, the recording adapter empty; write ② passes, the recorder holds {acct-2, db-2}; read ledger shows zero PMSCRED# reads on either path · ST-37b (step 8 + R1): …
37a: NEW behavioural L2 case + recording fake adapter + the DynamoDB lab (M7) + read ledger (M10) → closes F07-page-P3-overlap, F09-E10. 37b: → closes …
37a Situs slice · 37b 8
ST-38[NR]
Two PMS accounts behind one number — refused as a client
(Fede, Sep 7) · 2 accounts behind 1 line
ST-37 bench with capability_stage.leasing = live at both a1 (acct-1) and a3 (acct-2) under the one shared line. Run validate-fixture. Negative …
validate-fixture prints a named finding multi_pms_behind_line listing acct-1, acct-2 and the line, and refuses to load it as a customer fixture — never a silent accept. One case, one closesAt = 8 (the refutation's §4: the predicate needs the …
NEW F-ST-38 + F-ST-38-neg, expect:'RED', closesAt: 8; validate-fixture (M11).
The claim sentinel is keyed (org, account, type, value) — or pms_external_id values are namespaced by accountId before they become claims — so two humans never collapse into one Person; the second write lands on its own row; existsElsewhere still …
NEW GAUNT T24 "two accounts, same external id, two Persons" — seedable today as a FAILS row, flips to PASSES when the Situs slice lands the signal + …
Situs slice
ST-88[RW]
The sandbox account inside the live wall
a test building on a real line
BENCH-P + p_sbx{isTest:true, provenance:'test'} with att(PROP#p_sbx, pms_credential#any {accountId:'acct-sandbox'}), same ORG#, same line. …
Four rules the record lacks (the refutation: 3 of 4 asserts had no rule to hold them): R1provenance ∈ live|test is a META attribute (IDENTITY, FIXED, absentMeans:'live'), stamped by saveProperty's literal beside ROSTERPK and projected into …
FF answerable-stamp-exact (0/4) with R2; FF isolation-gauntlet; NEW F-ST-88; the step-0 backfill isTest:true → provenance:'test' (B1).
R1/R2/B1 0 · R3 3b · R4 8 · S1 5/6a
ST-117[RW]
One org, two PMS types
nearest pms_credential wins; capability differs
BENCH-Q + PROP#q4 (no row) + mock adapter kinds mockA ⊇ {messaging}, mockB ⊅ {messaging}. Whether q1/q2 share a line must be stated (if yes …
ST-117a credential-fold-nearest-then-any — the fold DF never defines for a KIND (§4.4 walks ATTACH#setting# only, L424): (q1,'any') → {c_A, setAt: ORG#}; (q2,'any') → {c_B, setAt: PROP#q2}; (q4,'any') → {c_A, setAt: ORG#} (inherited — stated …
117a: H (json) + unit; 117b: H; 117c: H over a seam-backed sync runner with fake adapters keyed by credentialId; 117d: D + R; 117e: H + unit; NEW FIX …
118a: unit on resolveCredential (pure over dumpOrgMap) + H; 118b: H (json) with a fake read client per credential; 118c: unit on putAttachment + H …
rows 1 · fold 8 (pms_credential unread until 8, DF L412, L716) — but 118a's any-only map must …
ST-119[RW]
Two walls, one PMS database — a test-setup case, not the standup's
one account behind two customer walls
BENCH-W; pms_accounts recorded CANNOT_EXPRESS on H until step 1's twin; vendor discovery = syncAccountVendors ×2 with one injected source. …
A1 two CRED#/SECRET, each organizationId = its wall, the sameconnectedByUserId (the dedicated user per database, Q21), two ORG#/ATTACH#pms_credential#any keyed (orgId, acct-W); falsifier = the E10 set-diff ∅ across walls — closes at 1 …
NEW H-L2 st-119-known-vendor-second-wall (expect:'RED', redBecause above, closesAt = the rung chosen); a step-1 twin case for the two CRED# rows; FF …
A1 1 · A2 3b or 8 (O17) · A3 6b · A4 3b
§ Other kinds of property — the trunk is invariant, the keys and entity facts change13 cases · 13 rows
39a (D, at the type strip): seeding succeeds with no bedroom/bathroom/sqft — the unit is a space, not a dwelling · 39b (H):bindHome(x_wh) on the leasing function → known_out_of_scope {route} (not not_found); on maintenance → bound; reach …
NEW FIX F-ST-39a/F-ST-39b (stretch 1 per HOW F23); FF import-classifier; FF registry-closed-and-typed (asset_type closed).
39a 0 (type strip, D) · 39b 4
ST-40[NR]
A dental office with rooms
office, no leasing, maintenance only, "rooms"
BENCH-X x_dent: asset_type:'office', UNIT#Room-1..4, att(ORG#org_st_x, setting#vocabulary.office {…}), capability_stage.leasing='off' at …
The WO binds to x_dent with unitId === <UNIT#Room-3 id> and unitNumber === 'Room-3' (R5: unitRef is a noun no layer defines — adopting it leaks the vocabulary key into the schema); the nouns come from vocabulary.<asset_type> — proven as a …
NEW FIX F-ST-40; FF registry-closed-and-typed; FF same-agent-id-every-line (differential arm).
type strip 0 · routing 4 · vocabulary key 7
ST-41[NR]
Nightly turnover (short-term rental)
STR, event-triggered schedule, occupancy calendar per unit
BENCH-X x_str: 3 units, calendar {purpose:'occupancy'} per unit, att(PROP#x_str, schedule#housekeeping#r1 {trigger:'event:checkout'}) …
The checkout event opens a housekeeping turnover WO keyed PROP#x_str with unitId = unit 2 and hosts from the schedule; the guest is occupancy.role:'guest' never tenant; FUNCTIONS gains guest_services/housekeeping as rows; "trunk unchanged" is …
NEW FIX F-ST-41 (stretch 3 per HOW F25); FF registry-closed-and-typed; scheduling-is-pure with an event trigger.
— a trunk fact, filed under a stretch vertical by mistake · WO with no unit
ST-75a (trunk; BENCH-A, not BENCH-X): a1 (4 units, resident r_a1) — the boiler room. r_a1 reports "the boiler in the basement is out", then …
75a: the common-area WO binds to PROP#a1 with unitIdabsent and is not refused for that reason; the faucet WO carries her unit; "no unit" and "no home" are different refusals (unit_unknown vs home_required); the intake-classifier half …
BENCH-X x_dent (4 rooms, capability_stage.leasing='off', no leases) under org_st_x; BENCH-A beside it. Run the billing quantity computation …
Kept as the OPEN pointer the record already has (DF §12 M16; §15 "the billing counterparty … not designed here"), plus the two isolation clauses that ARE assertable: two orgs on one deployment hold two subscriptions (the CONFIG singleton is a shared …
NEW GAUNT T22 "two orgs, two subscriptions" — CANNOT-EXPRESS today on (b); ships with a positive control (a non-test property in a throwaway json store …
— (open, M16)
ST-77[RW]
The nightly guest calls at 23:10
STR tiers + quiet hours on a shared line
BENCH-X x_str sharing ORG#org_st_x's line with x_dent; an active-reservation guest at 23:10 local; a past guest a week later. Needs …
Kept as a permutation-register fixture (expect:'RED', no closesAt, redBecause:'F25 primitives (RESERVATION#, occupancy.role guest, calendar purpose, guest_services/housekeeping) are stretch-3 and unpriced'). Four clauses corrected: the guest is …
F-ST-77 (RED, no closesAt); NEW FF calendar-purpose-never-crosses (the adapter count, H); the clock = a harness change, priced c×j.
— (stretch 3; §7 O18: is STR in scope at all — Fede + Gera)
ST-78[RW]
Two buildings over one address
mixed use, bindHome → ambiguous — the only case that exercises ambiguous at all
78a: a1 with the record's reading (i): bindHome(ctx, A) → {outcome:'ambiguous', options:[…two…]}, propertyId absent, home_state'unknown', options labelled by name + asset_type vocabulary (never the shared address); negative control: …
78a: NEW FF bind-home-resolves-roster-wide (4; the negative control is its falsifier) + consent-never-gates-reach for a4; 78b: NEW F-ST-78b; FF …
78a 4 · 78b 6a
ST-105[RW]
The storage tenant asks for the gate code
identity basis × fact class × function
BENCH-X2 x_stor + renter r_stor1 (claim +10004000621); the fact PROP#x_stor/ATTACH#setting#knowledge.access.gate_code {v:'4471'} with …
P1: the DV set carries no access value; the read returns ask_factor {requiredFactor:'otp_to_claimed_number'}; after the factor allow with policy.setAt = ORG#org_st_x · P2: identify_caller → {basis:'asserted', role:'storage_tenant'} and no …
NEW H-L2 st-105-name-and-unit-is-not-a-tenant (P2, RED, 4) · st-105-sms-never-soft-verifies (P4, GREEN, 0 — pin it so the voice fix does not loosen text) …
A1–A3 refused, code no_tier (the sixth named refusal), one operator line, MAPLOG# untouched, valuesVersion unchanged; A4 succeeds ×3 (the seam is alive); A5 {value:'off', setAt:'default', locked:false} and answerable[care] === false …
FF registry-closed-and-typed D (fixture ⊆ tables with the [] entry) + H (2a, no_tier); FF answerable-stamp-exact (0/4); FF …
A1–A5 2a · A6 4
ST-107[RW]
The guarantor calls the org line
occupancy.role gates DISCLOSURE and privileged WRITES, never intake and never reach
BENCH-X2 x_stuwithout beds (no BED# exists anywhere — the round's positive control needed a bed-move tool): PROP#x_stu (2 units) …
P1 ring (g): DVs home_state:'suggested', home_ref: u1, caller_role:'guarantor' (new DV on the N5 additive allowlist) · P2 read get_tenant_balance: resolves the household's balance (BAL#o_r via the occupancy's unit + lease window, not the …
NEW H-L2 st-107-role-gates-disclosure (kind:'voice-tool', 5 probes, 4); UNIT the roleGrants fold (pure); FF every-effective-value-has-provenance; FF …
4 · the roleGrants key 7
ST-108[RW]
Eighty-eight doors, one tenant org
master lease: occupant vs counterparty
BENCH-X2′ x_ml; PROP#x_ml/LEASE#… {counterparty: ORG#org_st_ml, noticeDays: 60} (the notice term and the counterparty are fields no row …
ST-108′a the lessee is an entity fact every renewal entry point reads: 30 replay days, replayLog.starts contains no tenantId at x_ml (today: 88 starts — RED); skipped.lesseeNotPerson = 88 · ST-108′b the occupant at door 41 is a home, not a …
108′a: H7 renewal replay gains one master-leased building (expect 0 sends) + NEW FF saga-refuses-non-person-lessee (unit); 108′b: H-L2 (json); 108′c: H + …
the NNN responsibility matrix as a per-lease entity fact
BENCH-X2″: x_wh with two leases — bay-1 (+…621, responsibilityMatrix {roof:'tenant', hvac:'landlord'}) and bay-2 (+…622 …
ST-109″a responsibility is a lease fact and a key by that name is refused everywhere: setting#responsibility_matrix at ORG#, GROUP#, PROP#x_wh → all three unregistered (§4.4 (a)); rg finds zero setting#responsibility literals · ST-109″b …
109″a: D (rg) + H (2a); 109″b: H-L2 (needs ST-75a's unit-less WO at 0 and a TRADES-typed category at 2a) + NEW UNIT the create_work_order responsibility …
109″a 2a · 109″b 4
ST-129[NEW]
A work order with no unit, at a facility that has no units
asset_type decides whether "which unit?" is asked (critic §7 #30; SF E5; WANT §6 A1; ST-75a's sibling …
BENCH-X x_dent (asset_type:'office', ST-40's occupant p_dent_om with authority:['report_issue']) seeded with zero UNIT# rows (ST-40 …
L1 (the type strip, D, 0):Unit.bedrooms/bathrooms/sqft (types.ts:4682-4684), Property.totalUnits/floors/unitsPerFloor (types.ts:2904-2906) and WorkOrder.unitId/unitNumber (types.ts:6190-6191) become optional in one dated exception row of …
§ Operator mutations — rebind, undo, lock, reclassify, and the placeholder under traffic23 cases · 23 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-42[NR]
Move a number from a building to the org, mid-thread
§7 #1
BENCH-B variant: +…201 starts on PROP#b1; a prospect is mid-thread (CONV#c1 under PROP#b1, bound to b1). rebindAddress('sms','+…201' …
ST-42a the transact: seven named items, both HEAD conditions (scope = :from AND organizationId = :ctxOrg), one MAPLOG#; undoMapChange(v) restores the pre-image byte-for-byte · ST-42b her next text matches CONV#c1 org-first (same org ∧ same …
ST-42 after the rebind. undoMapChange(v); then an unrelated edit at v+1 and undoMapChange(v) again. Needs M4 (per-fixture bench — the hashes …
Four legs with distinct causes + a positive control (R1): (i) the first undo restores the pre-image at v+1 — "byte-for-byte" defined as a scoped diff over the rows in reverse[], not a partition diff (R2); (ii) the second undo is refused by name …
FF maplog-undo-exact (1, widened per R11).
1
ST-44[NR]
Re-key the placeholder (the umbrella)
K16 / §9 retirement — the one scripted move
The umbrella shape on the bench = BENCH-U (stress-r2.md §0): PROP#u/META {type:'scattered_site', isTest:true} + 16 UNIT# whose number = …
The dry run enumerates every CONV#/TOUR#/INQUIRY# born under PROP#u (57 candidate families, 3 populated) with a pre-image and a revert runId; after apply: 16 PROP#, sms threads re-keyed to their home, the 9 unbound voice threads parked under ORG# …
FF umbrella-tripwire (5a; cron + H); the script's own dry-run diff; FF transfer-is-two-partitions shape. Price: q19 5–7 engineer-days, not DF's 1 …
5a
ST-45[NR]
Lock a key at the org over live building rows
§7 #16
ST-45a BENCH-A with a divergent seed (R2: a1 08–16, a2 10–18, org 09–17, a3 none — the assert is vacuous on equal values); lock office_hours…
45a one transact: locked:true + END both descendants (Delete + HIST# Put each, endedBy.source:'lock' — R5) + one MAPLOG#; the next resolve at a1 = {value: org's, setAt: ORG#, locked: true, maskedBy: []} (rows ended, not masked); a later write …
2 (needs step 1's transactWrite Update arm + the json all-or-nothing twin)
ST-56[NR]
The line claimed at the wrong node, found forty calls later
undo vs an immutable scope
BENCH-A, but claimAddress('voice','+10004000101', {scope: PROP#a1}) — a first claim (C1: there is no earlier HEAD at ORG#). Take 40 calls …
undo:MAPLOG#<v>.reverse[] for an INS deletes the HEAD → the address is unclaimed (§7.1 #3's three-click story, L621), the phone_line row on PROP#a1 *goes to* HIST#; the correct binding needs the forward edit · rebind: UPD HEAD.scope + …
FF maplog-undo-exact (1) + FF umbrella-tripwire (5a, the same cron shape); NEW H-L2 st-56-undo-leaves-scoped-threads (expect:'GREEN', 5a).
undo 1 · strand count 5a
ST-57[NR]
Two operators, one slot, the same second
concurrent supersede
BENCH-A. Two admins both read att(ORG#, setting#office_hours#t0); on H a sequential proxy (write, then replay the second with the stale …
Exactly one transact commits (Delete #t0 + Put HIST# + Put #t1); the loser's refusal names the new live row (t1) and setBy — not t0, which no longer exists: 57a → slot_live, 57b → version_conflict naming the version that moved and who …
FF one-live-row-per-slot (1; H nightly + D json shape) gains the adversarial row; the log clause rides FF maplog-undo-exact (strengthened); NEW GAUNT row …
1
ST-58[NR]
A lock over four hundred buildings, and a write during the sweep
lock > 48 descendants — "the strongest case in the r2 batch"
F-ST-58 = one org, 120 buildings, one key via load.ts:63-93 (R1 — drop BENCH-400; "writes, not reads"), office_hours at each. Lock at ORG#…
ST-58a (the hole, no barrier): after chunk 1, putAttachment(PROP#<in chunk 1>, office_hours)succeeds (no locked ancestor yet); after the last chunk the resolve returns locked:true, maskedBy:[PROP#…] — a masked live row · ST-58b (the barrier …
FF no-masking-nightly (2) at every chunk boundary; FF one-live-row-per-slot (1); NEW F-ST-58 (the only way to exercise the chunk boundary).
2
ST-59[NR]
A key reclassified while a building still holds a row at the old tier
registry reclass mid-flight
BENCH-A with att(PROP#a2, setting#escalation.owner) live. Reclass escalation.owner PARAMETER → IDENTITY (registry edit, deploy); resolve at a1 …
a2 answers from its own row unchanged (under the target class the building row is the one that survives; the org row is the stranded one); a1's resolve returns refused:{reason:'tier_illegal_since_v<N>'}loudly — never a silent fall-through to the …
BENCH-A. releaseAddress('voice','+10004000101') (S12: DEL HEAD + FN, attachment → HIST#); the phone vendor's number is returned/re-sold in the …
The undo is refused by name — held_by if another org's HEAD now exists, and a NEW refusal platform_detached when the adapter reports the platform no longer holds platformNumberId; the reverse of a release is a claim, which goes through claimAddress…
FF maplog-undo-exact (1) extended with a platform pre-check; FF second-claim-refused-by-store (1); the adapter stub (build at 0.5, closes 3b).
3b (needs S14's adapter contract)
ST-62[NR]
The building is marked sold while its line is still live
lifecycle vs a claim — "the highest-yield case in the r2 batch"
BENCH-A-3own. Two front doors (R2): (a) call +…101 and name a3; (b) call +…103 (a3's own line) — the undefined state, scope = PROP#a3 and …
(a) bindHome → known_out_of_scope {reason, route} (DF §9 L688) · (b) mintFromAddress on a dark scope → known_out_of_scope {reason:'lifecycle'|'capability_off', route} from the org's transfer.*/escalation.owner — never not_found, never a silent …
NEW F-ST-62 (H-L2, expect:'GREEN', 4) + NEW FF no-live-head-on-a-dark-scope (nightly, 4); FF answerable-stamp-exact (0/4) covers the stamp half only…
4 (plan.md gates it at 10 with the deletions — both are honest: the FF at 4, the last dark-scope deletion …
ST-92[RW]
One number, two spellings
normalization at the head
BENCH-A. Every claim names scope. The round's two spellings were two different numbers under the house normaliser ((100) 400-0101 ≠ …
A{voice, '(000) 400-0101', ORG#} — same org, same scope → {status:'created'}, store writes == 0 (transact cancelled, HEAD unchanged), exactly one ADDR#voice# row and its PK is ADDR#voice#+10004000101 · B{voice, '+1 000 400 0101', PROP#a2} …
NEW H-L2 st-92-normalize-at-head (A–D at 1; E at 3b); FF second-claim-refused-by-store (the same-org, other-scope clause); FF map-dump-stable (dump …
A–D 1 · E 3b
ST-93[RW]
Sends for one, receives for another
an outbound-only row on a foreign number — the one real hole in three rounds
BENCH-A + BENCH-B. Order A: org_st_a holds +…101; under org_st_b putAttachment(ORG#org_st_b, phone_line#+10004000101 {function:'any' …
The HEAD is the claim to use, not only to answer — one address, one org, any direction: A: write 2 → ConditionalCheckFailed → {status:'conflict', heldBy: 'other'} "held by another org"; B: write 2 (the claim) → the same — an outbound-only …
NEW GAUNT row "outbound-only never crosses the wall, either order" (CANNOT-EXPRESS → PASSES 3b; halves A+B; kinds phone_line + mailbox; channels voice + …
FF 1 · gauntlet 3b (plan.md GAP S2)
ST-94[RW]
The last primary is ended
zero primaryForScope on a chain — and the half-state the round hid
BENCH-A-3own. Act corrected (R1):releaseAddress('voice','+…103'), releaseAddress('sms','+…103'), then the same for +…101 — endAttachment…
ST-94′-aresolveOutboundAddress(ctx,'sms', path(a3)) walks a3 → ORG# and finds no live primaryForScope → refused no_primary_address at the send seam ("Park West has no texting line; attach one at the org or at a3") + the operator line in …
H-L2 st-94-zero-primary-refuses (expect:'RED', 3b — cannot be authored until step 1's rows exist: endAttachment/releaseAddress/ADDR# = 0 hits, so …
3b (all four)
ST-95[RW]
Two chains, one building
chain precedence and depth
BENCH-C. Reading stated (R1): depth = live chain edges per building, cap 3. Three chain groups (g_north 1, g_brand 2, g_region 3). Act (1) …
st-95a (1) refused precedence_tie naming g_north and g_brand, zero rows changed (the tie refusal belongs to the relationship writer, L272 — DF puts it on putAttachment at L323/L455, a record edit, R6) · st-95b (2) refused chain_depth…
Four H-L2 rows, each expect:'RED' today with a NOT_RUN line naming the unblocker (GROUP# rows + the in_group writer in the json twin — step 6a, DF …
leg 1 + control: H-L2 st-96-external-route-on-platform-number (json, 3b, no stub); legs 2–3 gated on the adapter stub (assigned/unassigned/detached …
1–3 3b · 4 2b
ST-97[RW]
The org seeded twice under two keys
one org, two profile rows — three narrow rows, not one
Seed org_st_a through the stage seeder shape (SK:'META') and through createOrganization (SK:'PROFILE'). The round's three asserts could not …
ST-97a org-row-single-writer (D, rg): every ORG# put in src, scripts, agents goes through createOrganization; rg "SK: 'META'" within 3 lines of ORG# = 0; IOrganizationRepository exposes exactly one create; seed-stage-orgs.ts calls …
97a: D (0); 97b: H (0/1); 97c: R + nightly (1) — FF seeding-calls-zero-scripts (1), map-dump-stable (1); notroster-stamp-complete (a building count …
97a 0 · 97b 0/1 · 97c 1
ST-128[slot]
Two paths, one building
— a building born twice (Gera's hand seed, then the PMS import), plus the foreign-org twin and the umbrella …
BENCH-A + BENCH-B; the "PMS export" is a fixture file of directory rows under the no-real-names rules. Executor H once extra-7.md §3 #9 …
Seven arms, written in extra-7.md §4: (a) id carried → created 0, updated 1, organizationId byte-identical — GREEN today (import/route.ts:286-293), the control · (b) id unknown → today a twin (nameAddressKey mismatch); GREEN when the key is …
FF import-classifier (DF §11 L808) gains the case (HV B5); ST-97a org-row-single-writer (the seed script writes no ORG# put of its own).
(a)(d)(e-first)(f) W0 as asserts in the seed script's dry-run · (b)(c) the step HV B5 lands (HV says 4 …
ST-132[NEW]
The org is archived
archiveOrganization — HEAD released, E ended, the card becomes the directory, the picker forgets it …
BENCH-B (b1 with its own line +…201, mailbox, calendar c_b1, pm_b) + BENCH-V's v_plumb engaged by both orgs + VENDOR#v_only_b engaged …
L1 (one transact, ≤48 items else the sweep with E last — VS §8 #5's shape):ORG#org_st_b/PROFILE {status:'archived', archivedAt}, mapVersion +1, one MAPLOG#; everyADDR#…/HEAD whose scope ∈ subtree(ORG#org_st_b) is deleted with its FN# …
NEW H-L2 st-132-archive-org-releases-and-ends (json; needs the writer + releaseAddress in the twin, 1) · st-132-archived-org-not-pickable (6b) …
L1 1 (writer + HEAD release; the E/M END half at 6a, the card/shelf at P8) · L2 3b (text) / 4 (voice) · L3 …
ST-83[RW]
A call lands mid re-key
rows born after the snapshot
BENCH-U + a traffic generator; rekey-umbrella-rows.ts --apply with the pause_at: loader key (shared with ST-84) — one inbound sms and one voice …
Mechanism re-cut: §7 #1 re-points the umbrella's HEADs to ORG#wsbefore the script runs; the script takes no writer-stop window; it converges by re-census until the HANDLED-prefix count under PROP#u is 0 and the born-after set is empty …
FF umbrella-tripwire (DF §11 L809) as the standing verdict — re-specified as the S1 triple (HANDLED-prefix count · born-after set · in-flight …
5a (row half) · HEAD half 3b on top of 1
ST-84[RW]
The re-key dies at row nine of sixteen
resumability and the revert
BENCH-U with fail_at: in the loader (stress-r2.md:123) + a run ledger, modelled on rehome-org-jpco-apply.ts (--resume, writeAuditRow) and …
(a) re-run is idempotent — skips what exists by natural key and finishes; (b) revert restores the pre-image for exactly the rows this run touched and refuses, naming them, any row a later writer touched (the clause the jpco prod run needed — 58 …
NEW F-ST-84 (fails on purpose at a named row); FF umbrella-tripwire (5a) for (a); dropmaplog-undo-exact (a different mechanism) and …
5a · (d) R or step-1 json twin
ST-85[RW]
The day sixteen units became sixteen buildings
reach and the DV set change under a live, unbound thread
BENCH-U + att(ORG#u, setting#geo.areas [4 towns]); the re-key derives META.city from each unit number and writes the in_group area tag. …
Her thread is found by GSI1PK = PHONE#org_u#<e164> and matches org-first with both sides unbound; the reply leaves on +…301 resolved from ORG#u; the run ledger shows exactly onemoved for her thread with a pre-image (the named exception to …
FF umbrella-tripwire (5a) · FF same-agent-id-every-line (4) · maplog-undo-exact/ST-84's ledger for the one move · NEW H-L2 st-85-parked-thread-matches …
5a (thread) · 4 (DV set)
ST-115[RW]
The tour reminder after the re-key
a workflow pinned to the umbrella — one true finding, everything around it failed
BENCH-U. A tour under PROP#u on the umbrella's calendar; tourWorkflow arms T-24h/T-1h (timers anchored to scheduledStartAtIso …
The finding: DF names the tour cadence as a checkpoint (§4.6 L486, step 9 L717) and the re-key's row families (§9 L695, Q19 §4 L109) and never joins them — no row, step or FF says a *running* tourWorkflow learns the new home; one line closes it. …
FF workflow-survives-rebind (9) gains the tour cadence and a same-org move clause; FF umbrella-tripwire (5a: the moved TOUR# leaves the umbrella …
9 (workflow pins) · 5a (the count)
ST-116[RW]
Revert the re-key after a day of traffic
reverse over rows born after the pre-image — four narrow rows
BENCH-U, org org_st_u, line +10004000301. Re-key R; clock +24 h; traffic (6 new persons text → 6 CONV# under ORG#org_st_u; 2 TOUR# + 1 …
ST-116a rekey-revert-refuses-touched-rows--revert R dry-run lists every pre-image row and marks the three touched ones stale by PK/SK; apply restores the untouched byte-for-byte and REFUSES the three by name (exit ≠ 0, none re-Put) …
116a/b: H (seam-backed script; json ledger) + dry-run; 116c: H (undoMapChange) + D; 116d: cron + H — FF umbrella-tripwire; FF maplog-undo-exact (the …
116a/b/d 5a · 116c 1 + 3b
§ Isolation — every read from the wrong org is zero rows5 cases · 5 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-46[NR]
The count is the verdict, over every new row family
isolation gauntlet extended
BENCH-A + BENCH-B + BENCH-M + BENCH-V (twin digits everywhere). From an org_st_b context run every repository method once. Eight of twelve families …
Every Query/Scan returns Count == 0 for org_st_a's rows across PROP#, ORG#, GROUP#, ATTACH#, REL#, ADDR# (context-less read is mint.ts only), CRED#, REPORT#, MAPLOG#, HOLD#, VMEM#, CONV#; any item with organizationId ≠ …
FF isolation-gauntlet T15–T19 per R8; GAUNT T2′ beside T2.
T15/T16/T2′ 3b · T17 5b · T18 6a · T19 8 (DF §11's "3b / 6" was two numbers for five families)
ST-47[NR]
Caller on A's line names B's building
E2 / T4 — the cheapest cross-org case
BENCH-A + BENCH-B. On +…101 the caller asks for "20 Bellwether Way" (b1); the same on text. Two literals to add (R6).
bindHome → unknown ("I don't have that home"), byte-identical to a home that does not exist anywhere; conversation.propertyId never re-pointed; asserted against the deleted fill-in, not the surviving refusal — the empty-property fill-in at …
GAUNT T4 (PASSES, keep as the today row, re-graded at 5a); NEW T15 + T16; FF bind-home-single-writer (5a); FF consent-never-gates-reach (4) for the oos row.
T4 0 · T15/T16 5a
ST-48[NR]
Opt-out crosses, opt-in does not
T11a/b
BENCH-A + BENCH-B. +10004000999 texts STOP to +…101 (org_st_a); org_st_b later tries to text it; separately +10004000998 grants consent at a1 …
Both org_st_b sends are refused — the second with no_grant_for_org (the T11b code ST-80d and ST-110′ reuse), not consent_missing; isRevoked answers a boolean; hasGrant is org-stamped; no method returns consent rows to app code. Repairs …
GAUNT T11 (keep as-is — ST-80's refutation: do not "name the org" in T11); FF consent-split (3b).
3b
ST-49[NR]
The platform admin sees nothing until mintAdmin
anchor org / bypass
BENCH-A-staff (R1) = BENCH-A + (i) one INQUIRY# under PROP#a1; (ii) org_st_z minted the *approval* way — an id only, no ORG#/PROFILE …
Three positives, two negatives at the seam (R3): getProspect(inq_a1) for the staff caller resolves from a1's anchor org (getOrgIdForProperty → org_st_a) and returns the row; the same for inq_z1 resolves org_st_zeven though it has no ORG# row…
GAUNT T12 (kept, unconjoined from its doc grep); FF isolation-gauntlet (OPEN_SITES_TODAY only shrinks); FF complete-mediation-rg (+ no …
sentinel-as-row half 0 · mintAdmin half 6b
ST-104[RW]
Two walls, one ticker
display ids across orgs — four narrow rows
BENCH-A a1 ticker:'ELM', BENCH-B b1 ticker:'ELM', then a2 ticker:'ELM' in org_st_a. One WO at a1 and one at b1 via handleCreateWorkOrder → …
ST-104a ticker-unique-per-org saves 1 and 2 succeed; save 3 refused naming a1; the roster the check read = the PROPORG#org_st_a Query (read ledger), never the platform GSI3 set; a property with no organizationId refuses before the ticker check …
104a: H (json) + unit on the caller (assertTickerUnique(property, roster(org)), one signature change at assert-ticker-unique.ts:32); 104b: H; 104c: D (rg …
104a/b/c 10 (the roster from step 0; the mailbox mint 3b) · 104d 6b + a §17 decision
§ Failure modes — the race, the stale stamp, the org-less building, the unclaimed line8 cases · 8 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-50[NR]
A claim written twice, concurrently, by two orgs
race on HEAD — the staged race falsifier (ST-09 is the deterministic CI case, §2.7)
BENCH-A + BENCH-B. Two claimAddress for +10004000777 fired within the same 100 ms from the two contexts; on stage, capture the real …
Exactly one HEAD exists; the loser gets {status:'conflict', heldBy:'other'} (opaque under TenantContext, §2.9); the winner's MAPLOG# is the only one; the staged run's CancellationReasons payload — Code === 'ConditionalCheckFailed' at the index of …
FF second-claim-refused-by-store (1; H + R "one observed real cancel on stage", encodes E11, Z27, Z17, A13).
1 (R; needs M7 + the widened transactWrite)
ST-51[NR]
A stale roster stamp
index drift — the only proposed case that tests the safe stamp
BENCH-A. (a) a full putItem literal for PROP#a2 omitting ROSTERPK — on H modelled as reserved fields on the seeded object (R3: ROSTERPK …
(a) roster-stamp-complete is a behavioural round-trip (R5: a full saveProperty round-trip of a fixture building keeps ROSTERPK/ROSTERSK/answerable — the token grep passes on a comment and fails on a rename); answerable is the unsafe stamp (R1):…
ST-52a (refusal): seeds nothing — the attempted saveProperty({…, organizationId: undefined} as never)is the observation; positive control …
52asaveProperty refuses without organizationId (the construction invariant) andPOST /api/propertiessupplies it from the branded context (R3, both sides of the seam) · 52b rows written before step 0 persist until the backfill exits …
52a: D + H; 52b: H; 52c: H + D; FF pinned-types-gain-no-required-field (0 — Property.organizationId is the dated exception); GAUNT T5 → re-pointed at …
52a 0 · 52b 0/3b · 52c 5
ST-53[NR]
An unclaimed address rings
HEAD miss — two acts, two ledgers
BENCH-AB-twin (R1) = BENCH-A + BENCH-B with one sender phone carrying an active residency in both orgs (PERSON#r_x{org_st_a} at a2 and …
Act 1 (plain text + plain call, no lexicon match): reads == 0 after the HEAD miss, writes == 0, persons minted 0 · Act 2 (the "gas leak" text): the LifeSafetyContext lane reads residencies and pages every active residency for that phone …
FF unclaimed-address-refused-zero-reads (3b/4; H over the webhook + voice seam); inbound-dispatcher.test.ts once R4's seam exists; GAUNT T5; NEW F-ST-53 …
3b (text) · 4 (voice)
ST-54[NR]
The same slot written twice
invariant (h)
BENCH-A. putAttachment(ORG#, setting#office_hours) twice, the second without supersedes; then with supersedes: t0; then (A5) two writers both …
A1 second write → {status:'conflict', live:{startedAt, setBy}} rendered slot_live · A2 with supersedes → Delete + HIST# Put + new row — atomicity on R (one staged real cancel) and D (a source-shape fence that the supersede path is one …
FF one-live-row-per-slot — H (counts, conflict shape, non-refusal arm, nightly with its positive control) + D (single-transact source fence, json …
1
ST-55[NR]
A claim while the pickup keys refuse
the X2 gate
Fresh org_st_n with none of the pickup keys. Filed twice on one seed (R2): claimAddress('voice', '+10004000501', ORG#org_st_n …
L1 the refused set is computed from the registry, never a literal (R1):keys == {k ∈ PICKUP_SET(fn) : KEYS[k].absentMeans === 'refuse'} (§2.1), with the literal expectation pinned separately · L2 the four controls (R3): all keys set → created …
FF claim-refuses-until-pickup-keys — TODAYD (a grep-backed drift test: no gate between PHONE_TO_PROPERTY_MAP/Property. …
3b (H) · today D + R
ST-64[NR]
The number is ported away
HEAD outlives the platform row
BENCH-A. The phone vendor releases +…101 while the HEAD and the attachment stay live — modelled at two adapters (R2): …
drift-check folds both adapters into one per-address verdict — live (either holds it), detached (neither), assignment_drift (the carrier holds it, the platform lost the assignment) — and writes a named artifact (R5: the leg has no oracle …
NEW FF address-detached-refuses-outbound (3b); FF complete-mediation-rg (3a, the adapter fence — not the oracle); the RED control is not …
3b (needs S14's two-adapter contract)
ST-73[RW]
The roster does not fit one page
pagination vs "one Query" — the falsifier for DF §15's "still one Query"
F-ST-73 — its own org, one org line, generate: {count: 1200, pad_id_to: 512} so the PROPORG Query returns ≥2 pages on DynamoDB (≈1.3 MB — the …
a1 the roster loader is an unbounded paginated Query — follows LastEvaluatedKey to exhaustion, passes no Limit; the read ledger prints roster: {queries: 1, pages: N, items: M, bytes} and M equals the PROPORG#<org> item count counted by …
FF answerable-stamp-exact read-ledger half (0/4) with the one-word edit; FF cache-exact (3b); FF roster-stamp-complete (the M = count sibling); NEW …
ledger 0.5 · roster 0 · page boundary R (M7)
§ Time and workflows — a saga across a rebind, the clock under a cadence, the nightly sync3 cases · 3 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-66[NR]
A renewal saga crosses a rebind
workflow vs a moved scope — two rows, one earns ScopeMoved
ST-66a (the filed seed, corrected): BENCH-B + a saga pinned {org_st_b, b1, v}; between two outbound sends rebindAddress('sms','+…201' …
66a at the declared checkpoint the snapshot is refreshed: from = the address the HEAD now names, the thread still matches (org-first), outcome identical to the unmoved run, noScopeMoved (same org, same building — the round asked ScopeMoved to …
BENCH-A with a queued outreach cadence (3 touches) for a prospect in reach and not yet pinned; two more edits (R1):quiet_hours → 07:00–22:00 …
t1 provenance setAt: ORG#, t2 setAt: PROP#a2, and an org.timezone edit after the pin changes t3 by nothing (inheritOnlyWhen:'home_unknown'); the send at 07:15 local is refused/deferred (the COMPLIANCE_FLOOR clamped last) and the stricter …
FF no-masking-nightly (2, the clamp ordering); UNIT scheduling-is-pure (window arithmetic); NEW H-L2 st-67-timezone-edit-defers-never-widens (the RED …
7 (the keys) · control 0
ST-102[RW]
The nightly sync that must not move the map
400 row rewrites, zero version bumps — two rows, one positive control
BENCH-400 at mapVersion: v, valuesVersion: w. Act 1 = the real sync: 400 × applyEntitySync with synthetic RentRollUnit[] → UNIT# …
ST-102a st-102-sync-leaves-versions after acts 1+2, mapVersion = v, valuesVersion = w; the read ledger — which gains {pk_prefix, index?} so it can see an index (R5) — shows PROPORG Queries == 0 and PROFILE GetItems == resolves during the …
102a: H over the read ledger (RED-by-unrunnable until step 0; closesAt: 1, values half re-checked at 2b); 102b: FF cred-refresh-single-flight clause (8) …
102a 1 (values half 2b) · 102b 8
§ Scale — thirty numbers, four hundred homes, one recency partition4 cases · 4 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-70[RW]
Thirty numbers, one org
many addresses on one wall
BENCH-12's 400-home org (named) + an additive block of 30 phone_line rows: 1 ORG#{function:'any', direction:'both', primary} · 4 …
a1 exactly one primaryForScope:true per (node, channel, function) — a second is refused primary_exists naming the holder; two primaries on one node differing only by function are legal, two differing only by direction are refused …
FF cache-exact (3b); FF map-dump-stable (1, this use lands at 3b); NEW H-L2 st-70-thirty-lines-one-primary-per-function; NEW H-L2 …
a1/a2/a4 3b · a3 3b after O28
ST-72[RW]
Four hundred homes, and a town with seven
the two-step area ask (X6) with the caller's own third constraint
BENCH-400 = F11 generate: with towns:[4] and one town's vacancy tuned to 7 available; geo.areas = the 4 towns; line/mailbox/calendar at ORG# …
H-L2 st-72-area-ask-payload:home_picker=area_menu; DVs property_id:'', home_state:'unknown', home_areas = 4 towns and 0 addresses, available_homes carries the 7-home town with all 7 (grouped area → bedrooms, sorted by the F1 rule) …
FIX F11 seeded via the loader (f11-generates-400-buildings expands but does not seed, probes.ts:532-545); NEW H-L2 + H-L3 as named; H1 …
a1–a3 (UNIT, pure over rows): a missing pair is named in the answer's provenance and falls back to a declared constant — the design names no fallback at all for a missing adjacency pair inside nearbyAlternatives, and the 30-minute value the …
UNIT scheduling-is-pure (a1–a3, a5); FF consent-never-gates-reach (a4, 4); NEW F-ST-74 = F11 + the rows above (expect:'RED', closesAt: 8).
a1–a3/a5 8 · a4 4
ST-103[RW]
Every text in the org lands on one index key
one org's recency partition — four narrow rows
The round's seed could not reach its own assert (texts ≠ index items; the "measured throttle" is ~7,000× away, so the branch under test was dead) …
ST-103′a one builder, one reader branch (D, no store): GSI5_PK/GSI5SK/CONV_RECENCY_PK/GSI_CONV_RECENCY importers = {src/lib/data/dynamo/conversation.ts, its agents/clara mirror, helpers.ts (declaration), the backfill script, this drift …
103′a/d: D (rg); 103′b: H (json) + M7; 103′c: R (stage, after 5a); FF building-conversations-via-gsi8 (5b) for the per-building list; NEW D row …
103′a/d now (D) · 103′b 5a (key) / 5b (GSI8) · 103′c R after 5a
§ Consent, identity and life safety on a shared line12 cases · 12 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-69[RW]
One org line, two states
the floor before the home is named — ST-111 is its re-issue and is folded here
BENCH-A + PROP#a2/ATTACH#setting#jurisdiction='UT', PROP#a1|a3/…='CO', ORG#/PROFILE.jurisdictionStateCode='CO' (read as org.jurisdiction …
a1 pre-pin fees.* resolves {setAt: ORG#, class: PARAMETER}and carries floor: {jurisdictions: ['US-CO','US-UT'], strictest: 'US-UT', support: 'unsupported'} — a new member of Effective (O1: the "strictest" combinator does not exist; propose …
FF pre-pin-keys-resolve-at-org (4) gains the floor clause; FF every-effective-value-has-provenance extended with the floor member (2b); NEW D …
a1/a5/a6 2b · a2–a4 4 · the roster stamp 0
ST-111[RW·MERGED into ST-69]
The org line spans two states
— what survives after the fold: the drive and four clauses ST-69 does not carry
BENCH-S (org_st_s, s1 CO, s2 UT, line at ORG#). Drive: (1) unknown number: "how much notice does a tenant have to give?"; (2) r_a2 (a …
c1 (1) resolves knowledge.notice_to_vacate (PARAMETER — add to §4.2 or fold into knowledge.<fact>) at setAt: ORG#; E7 fires only if a building-level row differs; no FLOOR key is consulted for this question — the round's drive was the …
c1/c2: NEW H-L2 st-111-tenant-notice-is-a-lease-fact (4); c3/c4: NEW H-L2 st-111-floor-clamps-a-send-not-an-answer (2 registry / 8 collections …
c1/c2 4 · c3/c4 2 / 8 · c5 8 · c6 now
ST-79[RW]
"I smell gas" before the home is named
life safety with |reach| = 3, on a claimed line — three rows
ST-79a (text): BENCH-A + A-rota (att(ORG#, schedule#emergency#r1)); arrivals: (1) unknown +10004000999 texts "I smell gas" to +…101; (2) …
The brand has exactly two minters (DF §4.5 L477): the text lane after classifyLifeSafety, and the ring webhook on a refused/unclaimed voice line — so on a claimed line, voice has no deterministic life-safety path: whether Clara asks "which …
BENCH-A (+ the seam repair: isRevoked(ctx, …) at the 12 checkSuppression sites). r_a2 texts STOP to +…101; Clara has an a1 matter and an a3 …
ST-80a the phone, everywhere: isRevoked(ComplianceContext{channelOrgId: org_st_a}, {kind:'phone', value}) → true and nothing else; outreach for a1/a2/a3 → refused suppressed; outreach from org_st_b → refused suppressed (same key, no org in it …
NEW H-L2 st-80-stop-stops-the-phone-start-regrants-the-org (expect:'GREEN' for a/b/c at HEAD once the seam repair lands; d/e close at 3b); GAUNT T11 kept …
a/b/c 0 (after the seam repair) · d/e 3b
ST-81[RW]
The two-party caller on a one-party line
recording consent, home unknown
BENCH-A with a2 in a two-party-consent state (att(PROP#a2, setting#jurisdiction='WA')), org one-party; the line row carries record: true; a …
Before the home is known the recording decision uses the strictest jurisdiction in reach (the disclosure is played), not the org's; the line row's record flag can be narrowed by the floor, never widened; the durable home of the decision is the …
intake_route:'external' + life safety — split in three
BENCH-A + att(ORG#, setting#maintenance.intake_route={target:'external', number:'+10004000900'}) + two buildings overriding to {target:'clara'}; …
ST-82a the ordinary maintenance text gets the scripted hand-off with the target number and zero intake turns (a turn counter, F3); the two overriding buildings intake normally from the same line with setAt: PROP#; absentMeans:'refuse' on …
82a: NEW H-L2 st-82a-external-intake-scripted-handoff (expect:'RED', closesAt: Situs slice; needs M1 · M2 · 2a · 3b · 7 + the turn counter); 82b: NEW …
82a Situs slice · 82b 3a · 82c 7
ST-99[RW]
The resident's number goes to a stranger
reassigned phone number — the thread is held, not transferred
BENCH-A. r_a2's phone +10004000121 is a Tier-1 (PMS-verified) claim with an occupancy at a2. The carrier reassigns it; a stranger texts …
ST-99′-1 the thread is held, not transferred: after deny_identity(conversationId) (the tool the design must name — a model-interpreted "no, I'm looking to rent" is not L2-assertable) the conversation meta carries personId = p_session_<id> (a …
NEW H-L2 st-99-suggested-is-not-bound (4) — driving one resolver + one tool, never a model turn (ring()cases/isolation.ts:35-46; the resolver …
99′-1/99′-2 4 · 99′-3 0 (true today) · the deny_identity tool: a §9 sentence (§7 O33)
ST-101[RW]
The resident who moved from a1 to a2
two occupancies, one ended — two turns, two closesAt
BENCH-A with dated occupancies (a residents: [{…, from, to}] token — BENCH-A cannot mint the shape today): r_a1 has OCCUPANCY#o1 …
turn 1 (4):home_state:'suggested'; the suggested ref = a2 (the occupancy live at the call instant — the ended one is never suggested: DF never says so; one clause), unitId a2's; a1 appears in no DV; conversation.propertyId absent (a …
NEW H-L2 st-101-live-occupancy-suggests (turn 1, 4); UNIT resolveIdentity from the occupancy live at at; FF bind-home-single-writer (turn 2, 5a); NEW …
turn 1 4 · turn 2 5a
ST-110[RW]
START after STOP: which doors reopen
opt-in scope after a crossing opt-out — the one consent case today FAILS outright
BENCH-A + BENCH-B + r_a2 also exists at org_st_b (PERSON# under ORG#org_st_b with claim +…121, a prospect at b1 — gauntlet/seed.ts:233-241…
START on +…101 lifts SUPPRESS#PHONE#+…121 (the lift is identifier-only today and DF §5.4 says nothing about reinstatement fan-out — the round's "revocation is per human, so the lift is too" contradicted its own rows; state the rule: the phone suppression …
FF consent-split (T11a/b, 3b) gains the START leg; NEW H-L2 st-110-start-scopes-to-org; GAUNT T11.
3b
ST-112[RW]
The plumber reports a gas leak
lane order: vendor lane vs life-safety lane
BENCH-A + BENCH-V. p_va (org_st_a's plumbing contact) texts the org line from his claimed number: "at a1, I smell gas in the basement …
The life-safety classifier runs FIRST on every inbound text regardless of sender class; the LifeSafetyContext lane pages whoIsOnCall(ORG#org_st_a,'emergency') for a2 (resolved inside reach), records the escalation under org_st_a, answers with the …
NEW H-L2 st-112-vendor-text-life-safety-first (expect:'RED', closesAt:'3a'; a new kind:'sms-dispatch' calling dispatchInbound on json; the pager …
3a (the text-lane split, DF §10)
ST-113[RW]
The leak at the building where maintenance is off
reach vs answerable ≠ unknown — four narrow rows
BENCH-M (a3 with an owner, r_a3 at a3, r_a1 at a1) + intake_route external at ORG#, clara at a1 and a2; capability_stage.maintenance = off…
ST-113a off-needs-a-doorputAttachment(PROP#a3, capability_stage.maintenance=off) while maintenance.intake_route resolves clara at a3 → refused {reason:'no_route_for_off', keys:[maintenance.intake_route]}, zero rows — "off" must have a door …
113a: UNIT on putAttachment + H (json); 113b: H-L2 sms-maintenance-off-hands-off-not-unknown; 113c: H-L2 (same bench); 113d: UNIT on bindHome + H; FF …
one address, a HEAD and a claim — four narrower cases, one new and real
BENCH-O (stress-r3.md §0). Two of the round's probes ((2) From == To, (3) the owner texting a resident from the org line) describe carrier …
ST-114a her number is the org line: claimAddress and addClaim both succeed in either order (neither key sees the other); a prospect's text on +…701 mints under ORG#org_st_o2 (L679); +…701is in the suppress set (it is an address the …
114a/b: H-L2 (json; cases/messaging.ts:146 imports the resolver — no webhook, no carrier, so the synthetic From == Tomust be asserted as dropped, not …
114a/b 3b · 114c 3a (text) / 4 (voice) · 114d 4
§ The head verdict’s stress lines that had no case id6 cases · 6 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-H1
An alias carries a function
maintenance@customer in aliases[] on the org mailbox row
BENCH-A + att(ORG#, mailbox#leasing@park-west.invalid {aliases:[{address:'maintenance@park-west.invalid', function:'maintenance'}]}). Mail …
The mint reads the mailbox HEAD (one head per delivered-on address, HV row 8) and the alias's function:'maintenance' narrows the turn → the maintenance.intake_route fallback fires (ST-82's script) instead of the leasing lane; the alias itself has no …
FF alias-function-narrows (HV §7; plan.md P6)
4
ST-H2
The catch-all is refused with one read
mail to clara@… after the flip
BENCH-A. A message to an address no org holds, delivered on the org's domain catch-all.
Refused; read ledger = 1 (the HEAD miss), writes 0, persons minted 0 — the mailbox twin of ST-53 Act 1.
FF catch-all-refused-zero-reads (HV §7; P5)
3b
ST-H3
Four numbers on 400 homes: two overlapping line_set tags on one home
a number's set of homes is a tag, never a chain (HV W2; §2.11)
BENCH-400 + four lines at ORG# each with a line_set tag over a subset; one home in two sets.
Accepted with no precedence tie (tags are not walked); moving a home between sets touches 0 HEAD rows (the set is on the tag row, not the head); cold reads stay ≤6 (the tag is read from the roster projection, not per home).
two buildings share one provider:'propflow' calendar; holdSlot on both for 10:00
BENCH-A-calB variant: a1 and a3 both attach c_a (ST-22's fan-out); two callers hold 10:00 at a1 and a3.
The second hold is refused held_by — because the hold row is keyed by the calendar (CAL#<attachmentId>/HOLD#<start>, HV B1 = ST-04a/ST-18 R3), not by the building; the ST-98 expiresAt < :now clause applies.
FF one-hold-per-calendar-slot (HV §7; P9)
8
ST-H5
A guest-card reply from the second PMS account in one org
PMS-relayed mint
BENCH-P. A guest-card notification arrives from acct-2 for a lead on p2; then one from an account no pms_credential row names.
mintFromPmsRecord → PROP#p2 (the credential row's scope, ST-117c's unit of work); an unattached account refuses with zero further reads — the relay never resolves by scan order.
isolation gauntlet + NEW case (HV §8; P5 3a)
3a
ST-H6
An agent's cell recorded route:external at two orgs
a route:'external' row is a map fact with no claim (HV W5; ST-96)
BENCH-A + BENCH-B. The same human's cell recorded as phone_line {route:{kind:'external'}} at both orgs; then a foreign claimAddress on it.
Accepted at both; no HEAD written at either (an external row is not a delivered-on address); a foreign second claim returns heldBy:'other' only if some org holds an answering HEAD — here none does, so the claim is created and both external rows …
grants = {org_st_a: [a1,a2,a3], org_st_b: [b1,b2]} for one login (ST-24's human); proposals: (i) url ?org=org_st_a,org_st_z&prop=a1,b2,z1 · …
(i) → orgIds ['org_st_a'], propertyIds ['a1'], refusedOrgCount 1 (z counted, never named), b2 dropped silently (in the grant, outside the selected orgs — not a refusal, W6); (ii) → the default (home org), refusedOrgCount 0 (a cookie never shows …
NEW UNIT st-133-selection-within-grants (property-based on deriveSelection); NEW D st-133-no-header-reader (rg "x-pf-selection" src/app outside the …
ST-133's grants; the picker open on the org panel with one org checked; then the property panel with one building checked; then T7's empty grant (a …
propose({org: []}) → the default, never []; unchecking the last checked org / building is refused at the row (aria-disabled, Space is a no-op, the chip has no ×) and deriveSelection still returns ≥1 of each when the grant is non-empty (UI §1.2 …
NEW UNIT st-134-never-empty (property-based: for every non-empty grant and every proposal, orgIds.length ≥ 1 ∧ (propertyIds.length ≥ 1 ∨ every selected …
ST-133's grants; a fixture of proposals covering: an org at its default, a strict subset, two orgs one at default, ids in random order, a duplicated …
serialize(deriveSelection(g, parse(url))) === canonical(url) and deriveSelection(g, parse(serialize(s))) === s (a fixed point after one hop); an org at its default rides as one *@<orgId> token — 400 scattered homes never appear in a URL or a fetch …
NEW UNIT st-135-url-fixed-point (property-based, two grants fixtures); NEW component test on SelectionProvider (push vs replace, the one-time …
UI S-A (deriveSelection, serialisation) · S-B (the provider)
ST-136[NEW]
Two orgs on one dashboard is two walls, not one wide read
one-context-per-org (UI §4 "Carried underneath", W4; UI §2.4 "Run one read … across two orgs")
ST-133's login with orgIds {org_st_a, org_st_b}, everyOrgAtDefaultRoster; GET /api/dashboard/stats?org=org_st_a,org_st_b; then the same with nine orgs …
The read ledger shows twoTenantContext mints (one mintFromSession(session, o) per org), each with exactly one PROPORG#<o> Query and reads keyed only by that org's prefixes; zero reads keyed by a list of orgs and zero unscoped getProperties() …
NEW H st-136-two-contexts-two-rosters (json read ledger; the stats route); NEW D st-136-no-org-list-context (rg: no organizationId: string[] on any …
UI DB-D1
ST-137[NEW]
A surname is not a person across the wall
merge-rows-not-payloads, restated to the fold (UI §4 "Carried into the fold"; UI D-12)
ST-136's set + the shared-surname fixture: r_a2 "Garza" at org_st_a and r_b1 "Garza" at org_st_b, each with one active eviction signal …
activeEvictions.combined === 2 — the fold Σ_o num_o over two per-org computes — never 1 (a last-name union-find across the wall, which is what a union of rows would produce); problemUnits keyed (propertyId, unitNumber) — a1's '2' and b1's '2' …
NEW UNIT st-137-fold-not-union (the two computes + the fold, pure); NEW H on the stats route over the fixture; FF isolation-gauntlet (the eviction …
UI DB-D1
ST-138[NEW]
An owner's building is on the dashboard and never in the compute
reports-ids-never-reach-compute (UI §4 "Refused for a reports projection", W13; DF §5.3; ST-30)
BENCH-O: ORG#org_st_o {purpose:'owner_party'} with a login (ST-30a's owner-as-customer instance) holding a1 through …
deriveSelection → reportsIds ['a1'], propertyIds [] (a1 has projection:'reports', viewOrgId: org_st_o, organizationId: org_st_a); the dashboard renders the reports projection from IReportsRepository (ORG#org_st_o/REPORT#a1#<period>) and the …
NEW UNIT st-138-reports-ids-split (deriveSelection over a grant with a reports row); NEW H st-138-owner-dashboard-zero-prop-reads (json ledger); NEW D …
6a (the owned_by row, ReportsContext) + UI DB-D1
ST-139[NEW]
Org-level pages need exactly one org
org-level-needs-one-org (UI §4 "Refused for every write", W15/W16; UI §1.3 "Org-level surfaces"; UI §5 S-E)
ST-133's login with orgIds {org_st_a, org_st_b}; then {org_st_a} only. Requests: org settings, Team, billing, vendor create (POST …
With two orgs: every org-only route → 400 needs_one_org naming the set, before any store read (ledger 0); with one org: each proceeds under requireOneOrg(scope) and body.organizationId is ignored or refused (org_from_body_refused), never …
NEW H st-139-org-level-refuses-two-orgs (json; the six routes); NEW D st-139-one-door (rg: every file in the 45-route org-only list imports …
UI S-E
§ The back-office writer and packaging2 cases · 2 rows
id
Title
Shape
Seed
Assert
Harness
Gates
ST-97d[NEW]
A second writer of the org row is refused
the back-office form is a caller of createOrganization, never a put of its own (extra-6.md §0, §7 …
BENCH-A-0 (§1, the org before its first building) for the store legs; the source tree at 8941d763fa for the drift leg. Fede's route (POST …
L1 (D, source) the handler imports no store primitive — rg "from '@/lib/data/dynamo/helpers'" src/app/api/admin/organizations = 0 — and every PK: \ORG# put outside createOrganization / archiveOrganization = 0, on ST-97a's W0 census (§3.10: six …
Dst-97d-second-org-writer (the l4-core-no-propflow-imports.drift.test.ts shape) · NEW H-L2 st-97d-second-org-writer-refused (json; needs M1's org …
L1 W0 — armed the day Fede's route merges, not at step 0 (a second live writer turns the row red on the …
ST-140[NEW]
The modules a client pays for are one row on the org, not one row for everyone
modules-per-org (extra-6.md §7 ST-M1, F2: "this customer just has leasing")
BENCH-A-0 + BENCH-B (two orgs; no building needed). putAttachment(ORG#org_st_a, setting, modules.enabled …
L1 (resolution)resolveEffective(['modules.enabled'], {organizationId:'org_st_a'}) → setAt:'ORG#org_st_a'; for org_st_b → setAt:'default' with absentMeans = today's constant (Object.keys(DEFAULT_ENABLED_MODULES) …
NEW H-L2 st-140-modules-per-org (json; the W0 one-function shim, then resolveEffective) · Dst-140-one-modules-writer (exactly one writer of …
L1/L2 W0 through the shim, re-run at 2b on resolveEffective · L3 2a (the registry) · L4 0/1…
§ The Sep 8 standup — the permutations the architecture discussion raised17 cases · 17 rows
ST-146 to ST-162, added 2026-09-08 late. Nothing was renumbered — ST-145 was the previous maximum. Six of the seventeen exist because the battle test changed the design (A9, changes 1–6); the change each one proves is named in its assert. Two run on today’s rows with nothing new seeded. One new bench, BENCH-Y, is what four of them need and no earlier bench provides: two CUSTOMER orgs sharing one building — the second org in the parties bench is a no-login party, so it cannot test a second party that can itself write. Its inverse arm, BENCH-Y-mgr, swaps the ownership edge for a management edge and is the discriminating negative for change 1.
id
Title
Shape
Seed
Assert
Harness
Gates
ST-146[NEW]
One building, two paying customers
1 building : 1 runner : 1 customer party — not the Sep-9 arrangement: while only one party is a customer that party is the runner and today’s stamp is correct; this is the state after the second party signs
BENCH-Y. A manager at each org signs in and opens the dashboard, the property list, the settings page for the shared building, and a conversation list.
L1 the runner’s portfolio is one roster Query and zero relationship-keyed reads · L2 the party’s portfolio is two Queries, never a scan — its own roster plus the reverse party index, with both halves non-empty, which no earlier case tests · L3 nothing about the building changes, Sean’s own test sentence · L5 the one-customer arrangement asserted as the contrast …
NEW FIX F-ST-146 (the two-customer fixture) + NEW H-L2 st-146-two-customers-one-building (json, read ledger) + NEW UNIT over the selection split; FF roster-stamp-complete, reports-push-authorised-by-row, isolation-gauntlet; L5 is an absence assert, not a behaviour
L1 0 · L2 6a + UI · L3–L5 6a · L6 3b/6a
ST-147[NEW]
A policy written by a non-custodian is refused by name
change 4 · the party axis at write time
BENCH-Y. The holder customer’s context writes a pet fee of 40 at the shared building while the runner holds 50. Control arms: the runner writes the same row; then the runner delegates that keyspace on the party edge and the holder retries.
L1 refused not_custodian, and the refusal names the runner — never a bare 403, never a silent tie broken by precedence · L2 the refused write leaves zero rows: no attachment, no history, no log line, version unchanged · L3 the discriminating negative: precedence stays party-blind, and the resolver’s signature takes no party argument at all …
NEW UNIT st-147-not-custodian-refusal (pure, no store, runnable the moment the refusal exists); NEW D st-147-resolve-takes-no-party; FF every-effective-value-has-provenance gains the party clause; NEW FF custodian-writes-policy
L1/L2/L5 2a after 6a · L3 2b · L4/L6 6a · the delegation field is a record edit, W0
ST-148[NEW]
Custody transfers mid-tenancy — the runner flips, the building does not move
change 1’s dated half
BENCH-Y under live traffic: two open threads, a booked tour with a live hold, an open work order, two occupancies, a building-scoped role, one settings row at the runner. Act: transfer custody at a cut time — same building id, new runner.
L1 one transact: the stamp and roster key move, the outgoing runner is inserted as a party (not exiled), the incoming party’s ownership edge ends, one log row in each org, and PK moved == 0 — which is what distinguishes custody transfer from a new building id · L2 the settings cliff is named, not hidden: the script prints what stops answering before it commits …
NEW H-L2 st-148-custody-transfer-same-id (json, ledger, PK moved == 0); NEW FF custody-transfer-enumerates-settings; FF role-end-cascades, maplog-undo-exact; NEW FIX F-ST-148 (a before phase and an after phase)
L1/L6 1 · L2 2b · L3 5a / 8 · L4/L5 6a
ST-149[NEW]
A manager’s region spans three walls
change 2 · a party’s grouping across runners — Sean’s Rocky Mountain
BENCH-Y + a third customer; the manager holds accepted management edges on two foreign-run buildings. Its context creates a region group carrying createdByParty and tags one of its own plus the two foreign ones. Control arm: it tags a building it holds no edge on.
L1 the three tags are created and the members Query returns all three — one Query across three walls, which is the entire point · L2 the control is refused no_live_relationship naming the building: a party may group only what it holds · L3 the trap stays refused — a settings row on a tag group is refused tier_illegal, and resolution at a foreign building is byte-identical before and after the tag exists …
NEW H-L2 st-149-party-group-across-walls (json); NEW UNIT on the tag refusals; FF registry-closed-and-typed; NEW FF party-group-never-walked; FF disengage-cascades
L1/L2/L5/L6 6a · L3 2b · L4 0 · the field and the widened reader check are W0
ST-150[REGRESSION + one new arm]
Nested groups with a value at both rungs — the refusal, re-asserted where the standup put it
a parent-child pair expressed as two flat chain edges, which is how “Rocky Mountain = Denver plus Salt Lake City” must be written
BENCH-C (the two-chains bench) + a new arm: two chain groups at ascending precedence whose members nest, with the same key on both.
L1/L2 (regression, already specified): the scope path is nearest-first at depth 2 under the cap of 3; the farther group appears in neither the value nor the masked list; an equal-precedence third edge is refused precedence_tiewith both groups named, a fourth at the depth cap · L3 (new): the group profile has no parent pointer and the walk reads no group-to-group edge — the ordering is precedence on the building’s own membership rows …
the existing scope-path unit extended with the parent-child arm; NEW D st-150-no-parent-pointer; FF registry-closed-and-typed, every-effective-value-has-provenance
A sub-market override under a manager-wide default
the $35 suburban / $50–60 urban split · A8
BENCH-Y extended so the runner runs six buildings: three tagged suburban, three tagged urban (disjoint, so no tie). A manager-wide default at the customer, a suburban group value, and one urban building overriding at the building.
L1 three rungs, three answers, one key: six buildings, three distinct provenances, three rows written — “I don’t want them to go into 50 properties” as a row count, not a promise · L2 the write economics asserted on the ledger: inserted 3 · updated 0 · moved 0, because six per-building rows give the same six answers and only the ledger tells them apart · L3 change 1’s dependency stated as a miss by name: run the same seed with a foreign-run building in the group and the walk never reaches it …
NEW H-L2 st-151-submarket-override (json + read ledger); NEW FIX F-ST-151; FF every-effective-value-has-provenance (three provenances, one key); FF cache-exact
L1 2b · L2 0.5 (the ledger) · L3 2b after 6a · L4 2a
ST-152[NEW]
The greeting is the size of the reach set
change 6 · reach 3 vs reach 1 · runs on existing benches
BENCH-A (shared line, reach of three) and BENCH-A-3own (own line, reach of one) side by side, with no greeting row on either; a third arm adds an explicit greeting at the customer.
L1 reach > 1 resolves to the company’s name with via:'derive' · L2 reach == 1 resolves to that building’s name — same key, same absent row, two answers, and the difference is a set size the mint already computed · L3 zero extra reads: the ledger is identical to the same bench without the key, and a fixture that adds a read fails · L4 an explicit row still wins by nearest-wins …
NEW UNIT st-152-greeting-derives-from-reach (pure — runnable the day the derivation is written, no store); NEW H-L2 st-152-greeting-ledger; FF every-effective-value-has-provenance (derive is a provenance the shape must admit); FF answerable-stamp-exact
A building gets its own number and the greeting follows
change 6 + the rebind, together · “a simple new line… without having to delete, or reroute”
BENCH-A-set (shared line over two buildings, the third on its own). Act, in one operator gesture: claim a new number for the second building. Then the inverse rebind.
L1 one row family, no delete, no reroute — the claim’s ledger is inserted 3 · updated 1 · moved 0, zero rows deleted and the old claim byte-identical: the one line is a delta whitelist · L2 the greeting flips with it and no second edit: the new line derives its building’s name, and the old line’s reach shrinks to one so it flips too. Two greetings changed by one write and no greeting row anywhere …
NEW H-L2 st-153-new-line-one-write (json, delta whitelist); ST-152’s unit reused; FF rebind-line-inserts-only (PK moved == 0); FF claim-refuses-until-pickup-keys; FF maplog-undo-exact
L1/L4/L6 1 then 3b · L2/L3 2b · L5 3b
ST-154[NEW]
A tour is reassigned between three leasing calendars
the hole in A12 — “they can move tours around between them and reassign them”
ST-18’s bench extended to three hosts, each with a personal calendar and credential, round-robin over all three. Book a tour, then reassign it. Arms: reassign to a busy host; to a person with no assignment; after the tour’s start time.
L1 the hold moves, it is not duplicated: one transact deletes the source hold and puts the target under the existing not-exists-or-expired condition, and exactly one live hold exists for the slot across all three calendars — the hold is keyed by the calendar, so a reassignment is a cross-partition move and a partial failure double-books or frees the slot · L2 the fairness counter is not advanced: a manual move is not a turn …
NEW UNIT st-154-reassign-is-pure (the decision, extending scheduling-is-pure); NEW H-L2 st-154-reassign-moves-the-hold; NEW FF one-hold-per-calendar-slot extended with the move arm; NEW FIX F-ST-154 (three hosts — the existing fixture has two)
8 throughout · L5 also the task-row family
ST-155[REGRESSION + one new arm]
A calendar disappears with a live hold — “are we able to still persist and not break”
ST-21 extended one rung up: the customer calendar, not the building’s
ST-21’s bench for the building rung, which is already written. The new arm ends the customer-level calendar on BENCH-A, where three buildings inherit it and none has its own.
L1–L4 (regression): end into history in one transact, new tours fall to the next live rung, booked tours keep their calendar snapshot, the live hold is not silently freed, undo re-puts the row · L5 (new): the blast radius is named — three buildings left with no calendar on any rung, degrading to office-hours-only and doing it loudly, one task row naming the customer and all three. Gera’s word is persist, and persisting silently is the failure · L6 the hold outlives its own calendar …
ST-21’s harness for L1–L4; NEW H-L2 st-155-org-calendar-ended-degrades-loudly; NEW FF credential-not-orphaned; the task-row family
L1–L4 1 / 8 · L5/L6 8 · L7 8 (nightly)
ST-156[NEW] extends ST-25
Free-busy across two customers, and the verdict on “we can drop that one”
A13 — judged and kept, against the standup’s own instinct to drop it
ST-24’s pair (one human, a person row in each customer, one link) + ST-25’s calendar rows, the second connected free-busy-only with its own consent. New arm: a tour booked in one customer at 14:00, then a caller on the other’s line asks for 14:00 the same day.
L1–L3 (owned by ST-25): the link is free/busy only; the second customer reads busy, never subject, attendee, building or thread · L4 (new): the block is observed, not assumed — zero slots at 14:00, a non-empty set at 15:00, and the call log shows exactly one free/busy call made with the second credential. “It has to know about this other organization without spilling the details” is one adapter call and a boolean · L5 the leak assert in the shape that can fail …
ST-25’s harness extended; NEW H-L2 st-156-free-busy-blocks-across-the-wall (json + a recording stub, one call log); NEW D on the projection; the isolation gauntlet’s calendar row flips CANNOT-EXPRESS → PASS here; FF scheduling-is-pure
L4/L7 8 · L5 8 (the drift arm earlier) · L6 is a record line, W0
ST-157[NEW] extends ST-136
One dashboard with one wall operational and one a portfolio projection
A14 — the one untested permutation of the two-customer fold
BENCH-Y + one login holding a membership in both orgs: a manager role in the runner, a viewer role in the holder. Selection names both.
L1–L3 (owned by ST-136): two context mints, one per customer, each with its own roster; the fold is a sum over per-customer computes; the selection is capped at eight · L4 (new): a mixed selection — operational ids from both rosters plus a reports-only id from the projection, with the shared building appearing in both lists, once per context, and the dashboard not double-counting it: two rows, one identity …
ST-136’s harness extended with BENCH-Y; NEW UNIT on the mixed selection; NEW H on the two-context ledger; FF one-context-per-org, merge-rows-not-payloads, reports-ids-never-reach-compute
L4/L6 6a + UI · L5 6a · L7 6b
ST-158[NEW] extends ST-19
The regional manager’s domain vs the site person’s one building
A16 — two sets, both named, neither assumed
ST-149’s bench (a region spanning three walls) + a group-scoped regional role in the runner and a building-scoped leasing role.
L1 the regional actor’s buildings are members of the group intersected with the roster — and the intersection is the assert: her operational set is the one building her own customer runs, while her visible set includes the two foreign-run ones only if a live accepted management edge exists on each · L2 the discriminating negative: the site person’s set is one building, and he is listed as a tour candidate there and not elsewhere · L3 the combination rule …
ST-19’s harness extended; NEW UNIT st-158-actor-buildings-three-shapes (a property test over the combination rule); FF registry-closed-and-typed (the role scope widened to a group); NEW D asserting the actor’s building set is computed in exactly one module
L1/L3/L5 6a · L2 8 · L4 6a
ST-159[NEW]
The in-house handyman at both rungs — who do you call
A15 — the tie nobody had asserted
BENCH-V on BENCH-A. A customer-wide in-house engagement with no coverage, and a second in-house engagement covering one building only; a vendor roster row at the customer and another at that building. Work orders at two buildings. Arms: both ids on one roster; the roster row ended; the engagement ended while the roster still names it.
L1 (the rule, stated once): the winner is decided by the roster attachment’s nearest-wins walk — not by the in-house flag and not by coverage. Coverage gates reach (may this vendor be dispatched here at all); the roster decides order. Both mechanisms must fire and be separable · L2 with both ids on one roster the answer is the first in the list, and the provenance names it …
NEW H-L2 st-159-two-rung-inhouse-pick + NEW UNIT st-159-roster-order-is-the-tiebreak; FF engagement-gates-vendor-reach gains the two-rung clause; FF roster-names-only-engaged run at two rungs; NEW D st-159-inhouse-is-not-a-tiebreak
L1–L3 2b after 6a · L4 2b · L5 6a · L6 P8
ST-160[NEW]
A building-scoped role administrator
change 5 · A9 — “who is essentially the admin… one of that property”
BENCH-A. A site manager holds manage_roles on a role row scoped to one building, granted by the account owner. Targets: a leasing agent scoped to that building and one scoped to another. Arms: granting a rung above her own; handing manage_roles on; the org-scoped administrator doing all of it.
L1 (the reach is the scope): she may invite, edit and end roles scoped to her building; the same call against the other building is refused out_of_reach in the 404 shape the code already uses — indistinguishable from “no such person”, so a building administrator cannot learn the customer’s roster by probing · L2 no escalation, asserted against the scope rather than the tier · L3 the decided ceiling holds unchanged …
NEW UNIT st-160-building-scoped-admin-reach (a property test over the teammate-management decision with a building-scoped grant); FF no-escalation-in-grants (scoped arm); the one-writer drift assert reused
6a / P7 — the grant and its scope
ST-161[NEW]
The intern’s override, bounded
change 3 · A10 — “that’s too customizable” against “you need to be able to go in”
BENCH-A + a bound of 25–75 on the pet-fee key in the registry + a customer-level value of 50. A leasing agent writes at one building: 40 · 500 · 0 · 40 with a lock above · then the org_admin writes 500 at the building · and changes the customer’s own row outside its own bound.
L1 narrowing is allowed, and the tuple says which ancestor bounded it · L2 exceeding is refused outside_bound, naming the ancestor and the bound, zero rows written — this is the leg that distinguishes a ceiling from a lock, because 40 and 500 get different answers and no lock flag can produce that · L3 a lock still beats a bound: the two instruments compose, lock first, and a bound never weakens a lock …
NEW UNIT st-161-bound-refusals (a property test over a key-spec fixture — runnable with no store the day the field exists); NEW FF bounded-keys-refuse-outside; FF every-effective-value-has-provenance; FF registry-closed-and-typed; NEW H on narrowing a bound
L1–L6 2a + 2b · L7 2b · the field itself is a record edit, W0
ST-162[NEW]
The wall is a Query, not a filter
the pre-onboarding cut’s own falsifier · runs today, no new rows
BENCH-A + BENCH-B (two customers, twin digits). From the neighbour’s context: the property list, the property-by-id read and the cached list; then count call sites by census.
L1 the list is one Query on the roster key and zero table-wide index queries — asserted on the read ledger, not on the returned rows, because the returned rows are already correct today and that is exactly why the defect is invisible. The whole polarity lesson: the current answer is right and the current mechanism is wrong. · L2 the single-row read returns the byte-identical 404 of a missing building and reads zero rows under it · L3 the call-site census, a number that must only shrink …
the existing isolation-gauntlet row extended with the ledger arm and the census; NEW D st-162-bare-getproperties-census (arms today); NEW H-L2 st-162-list-is-a-query; FF roster-stamp-complete, complete-mediation-rg
L3/L6 arm at W0 · L1/L4 0 (the roster index — items 1–2 of the pre-onboarding cut) · L2 0, complete at 3a · L5 0
§ Round 4 — the red team against the Sep 8 standup45 cases · 45 rows
ST-163 to ST-207, added 2026-09-08 night. Five reviewer lenses read this page and the standup transcript independently and returned 104 findings and 60 cases. Each lens numbered its cases from ST-146 with no cross-talk, and the standup family above had already spent ST-146–ST-162, so all sixty are renumbered here from ST-163, in lens order — L1 authority · L2 visibility · L3 inheritance geometry · L4 control and delegation · L5 edges. Nothing already published is renumbered. Sixty cases become forty-five ids: eleven duplicate a case the standup family already carries and fifteen collapse in all — the table under this one names every merge and the leg it adds, because a second id for one case splits its evidence and inflates the count. No new bench: the manager bench behind ST-27/28/29 carries the third-party-manager cases, BENCH-Y carries the two-customer ones, and the twenty-party case is a generator arm on the parties bench. All forty-five are RED or cannot yet be expressed — see the executed-vs-paper audit beside the fitness table.
The red team wrote
Surviving id
The leg it adds
L2 — twenty ownership rows on one building
ST-146
exactly twenty push transacts, one ConditionCheck each; ending one row fails that one and the other nineteen commit — no shared transact
L1 — who may write a setting on a building I manage
ST-147
four actors × one key as a set assertion, so a new rung fails the test rather than widening it; plus the in-wall manager scoped to a different building, refused for scope and not for tier
L1 — the manager signs: the transfer, itemised
ST-148
the dry-run prints residents needing re-consent, open work orders, booked tours and workflows to drain before anything is written — and prints zeros over the custody flip, which is the two numbers side by side
L1 — the manager’s region label is refused
ST-149
the refusal is readable by an operator, not a code; and resolveEffective is byte-identical before and after the tag — the falsifier that the change added visibility and not a precedence rung
L2 — a region that spans two runner walls
ST-149
zero rows written and mapVersion unchanged on both walls when the tag is refused; the supported path named inside the refusal
L3 — move a sub-market under a different region
ST-150
the group-to-group parent edge, proposed by L3 and refused by ST-150’s own assert. The row wins: hierarchy stays ordered flat edges, and this is now the record of what we chose not to build, with the cost line stated
L5 — a booked tour is reassigned
ST-154
the hold is keyed by calendar, so it moves or the slot leaks; the round-robin counter must not advance; the prospect already holds a confirmation naming the first host; a half-moved provider pair lands in a named pending state, never half-moved silently
L5 — reverse the retraction on the person at two customers
ST-156
a record-level drift leg: the catalog may not carry ST-25 as dropped, because F06, ST-124 and the isolation gauntlet all model that human as a normal shape
L1 — the regional across two runners
ST-158
the assert is the count of contexts (2), not a screenshot; the union is of rows and never of contexts
L1 — who administers the managed building
ST-160
the cross-wall arm: a party’s person administers one building without thereby reaching the rest of the wall — today the only seat that works hands them every other building
L4 — the building’s administrator mints a wall-wide one
ST-160
subset containment asserted beside the tier test: a role write is refused unless the target’s scope is a subset of the writer’s
L1 + L4 — the runner locks the key the manager owns
ST-163
one id, two lenses: same seed, same act, same proposed refusal — L1 states it as the business inversion, L4 as the row transact
L1 + L3 + L4 — a party’s policy cannot reach the building it manages
ST-164
one id, three lenses. Kept apart from ST-149, which is the tag across walls and explicitly refuses a value on that group — folding the value case into it would answer an accepted finding by citing the row that refuses it
L3 + L4 — every node that differs, in one list
ST-183
one id, two lenses: the same projection over the same dump, one fitness function
id
Title
Shape
Seed
Assert
Harness
Gates
ST-163[NEW]
The runner locks the key the manager owns
L1 + L4, one case · the lock as a weapon rather than nearest-wins
BENCH-M / F02.rel(a3, managed_by → ORG#org_st_m {grants:['operational'], acceptedAt}) live; att(PROP#a3, setting#pets.policy = 50) written by m_pm. Act: the runner's org_admin writes att(ORG#org_st_a, setting#pets.policy = 35, locked:true).
L1 the lock write is refused, naming the live managed_by row — proposed refusal (k) beside (c). · L2 the control arm (no live managed_by row on any building of the wall) succeeds, so it is a relationship gate and not a lock ban. · L3a3's row is still live and resolveEffective returns {value:50, setAt:'PROP#a3', locked:false}. …
NEW UNIT st-163-lock-refused-over-a-managed-row (property test over putAttachment, no store); NEW H-L2 st-163-lock-ends-the-managers-row (L6, json + HIST# count); FF custodian-writes-policy (ST-147's, extended to the lock)
L6 2 (the lock exists) after 6a (the relationship row) · L1–L5 new work, proposed 2b …
ST-164[NEW]
A party's policy has no rung on a building it does not run
L1 + L3 + L4, one case · the retype cost as a number
BENCH-M + a second building m1 that org_st_mruns under ORG#org_st_m, with att(ORG#org_st_m, setting#quiet_hours='07:00-22:00'). Resolve quiet_hours at m1 and at a3.
L1m1 resolves setAt:'ORG#org_st_m'; a3 resolves the runner's value or absentMeans, never the manager's — today's correct-per-design answer, pinned as a RED reproducer so the day it changes it changes loudly. · L2 the case prints the number: the count of DYNAMIC keys a party must retype per managed building, so the cost is a figure in the results file and not an argument. …
NEW H-L2 st-164-party-policy-has-no-rung (json, read ledger, the key count printed); NEW UNIT st-164-chain-top-is-the-runner over getScopePath; FF every-effective-value-has-provenance; NEW D st-164-dump-is-bounded-by-the-wall …
L1/L3/L4 6a · L2 2a (it counts the registry) · L5 new work, D3-gated — it cannot be written until the authority ruling lands
ST-165[NEW]
The runner reads the conversations the manager wanted withheld
L1 · Gera's question, as a reproducer
BENCH-M / F02 with real CONV# rows on a3. Act: the runner's org_admin, holding noconversations grant of any kind, opens a3's conversation list.
L1 every conversation is returned — the RED reproducer of Gera's question, pinned so the answer is a recorded fact rather than an inference. · L2 the same login on a building the wall does not run returns the 404 shape byte-identical to missing — the wall still works; this is not an isolation defect. …
L1–L3 6a · L4 6a, and only if the authority ruling picks the manager-as-runner arm
ST-166[NEW]
The party that grants itself
L1 · a grant whose grantor and grantee are the same wall
BENCH-M. Act: the runner's org_admin writes PROP#a3/REL#owned_by#ORG#org_st_a {grants:[…,'conversations']} — grantor and grantee the same org.
L1 today it succeeds with no counter-signature — the reproducer. · L2 under the proposed change it is refused without acceptedBy: ORG#org_st_m while a live managed_by row exists. · L3 with no managed_by row the self-grant succeeds, so the rule binds the managed case only. …
NEW UNIT st-166-self-grant-needs-a-countersignature (property test over putRelationship); FF reports-push-authorised-by-row (the grant half)
L1 6a · L2–L4 new work, proposed 6a
ST-167[NEW]
Two effective values for one building
L1 · visibleToGrantee is prose, not a field
BENCH-M.att(ORG#org_st_a, setting#vendor_roster.plumbing) and att(ORG#org_st_a, setting#escalation.owner). Read the same key three ways: (a) the channel context on a3's line; (b) the party's acting-org walk; (c) the runner's own walk.
L1 (a) and (c) agree. · L2 (b) differs today for visibleToGrantee:false keys — the reproducer, and its plain sentence is that the manager cannot see what the assistant tells their prospects. · L3 under the change (b) returns {refused:{reason:'not_visible_to_grantee'}} naming the node — a refusal, never a different value …
NEW UNIT st-167-grantee-walk-refuses-never-differs; FF registry-closed-and-typed (the new field); FF no-masking-nightly (the grantee arm)
L4 2a (the registry field) · L1–L3 6a (the acting-org walk)
ST-168[NEW]
The missing ownership row on a self-run building
L1 · cross_sell.same_owner_only has no operand
F01 (the control fixture, zero relationships) + F02. Act: resolve cross_sell.same_owner_only for the pair.
L1 the record's two statements are reconciled by the fixture, not by prose: either F01 gains rel(f01_building, owned_by → its own runner {share:1}) or the sentence that says every building of that principal carries an ownership row is corrected. …
NEW UNIT st-168-same-owner-is-total (pure, over the predicate); FIX F-ST-168 (the one added row on F01); FF every-effective-value-has-provenance
L2/L4 2a (the predicate) · L1/L3 6a (the row)
ST-169[NEW]
The thirty-percent holder
L1 · share is stored and read by nothing
BENCH-O + ST-12b's shape: rel(a1, owned_by → ORG#inv {share:0.3, grants: the decided default}) beside a {share:0.7} runner-held row. Act: the minority party reads everything its grants name; then the runner grants it conversations.
L1share is read by nothing — asserted as a read-ledger zero over the whole projection path, so "share gates access" is disproved rather than assumed. · L2 the 30 % party's projections are byte-identical to a 100 % party's. …
NEW UNIT st-169-share-is-read-by-nothing (read-ledger zero); NEW D st-169-one-share-consumer (rg: exactly one site reads share); FF reports-push-authorised-by-row (L3)
L1/L2/L4 6a · L3 6a and it needs the L2-D1 ruling on grant grain
ST-170[NEW]
The maintenance spend cap has no key
L1 · Gera's own worked example of the control problem
BENCH-V + BENCH-M. Act: set a spend cap at the wall, at a group and at PROP#a3, then read it as the assistant would before dispatching a $450 repair on a3.
L1 today no key exists, so all three writes are refused as unregistered — the reproducer for Gera's own example. · L2 once registered PARAMETER / org, group, property / absentMeans:'refuse', the $450 dispatch parks fail-closed when no cap resolves, consistent with the shipped pin that no invented money decides spend. …
NEW UNIT st-170-spend-cap-registered (over KEYS); FF registry-closed-and-typed; FF no-masking-nightly (the new tier set)
L1/L4 2a (registry) · L2 2b (resolution) · L3 after the authority ruling — gates D2
ST-171[NEW]
The holder party asks what is happening right now
L2 · latency is a period, not an event
BENCH-O read at a date one month after the last push. rel(a1, owned_by → ORG#org_st_o {share:1, grants: the five decided projections}). One OPEN work order on a1 raised after the period closed, one live CONV#, one tour booked for today. Last push = the previous month's REPORT#.
L1mintReports + the reports repository return exactly one row, the previous period's; this month's work order, the live thread and today's tour are absent by construction, not filtered — asserted as zero reads keyed PROP#a1. · L2 the holder surface prints the period and its periodTo date beside every number — never an undated figure. …
NEW H-L2 st-171-holder-sees-a-period-not-a-present (json read ledger); NEW D st-171-no-undated-holder-figure (rg: every holder-surface number renders with its periodTo)
6a
ST-172[NEW]
The granted projection with no carrier row
L2 · the Sep 8 grant list against the row model
BENCH-O. The five-projection default grant row on a1. Ask each granted projection in turn through the reports repository.
L1reports resolves to the period row. · L2 the other four each resolve to a named refusal citing the honest limit, never an empty array and never a zero — an ungranted-shaped answer and an unbuilt-shaped answer must be distinguishable, or the surface reports "no work orders" when it means "no carrier". · L3 the limits pane names which of the five are live today. …
NEW UNIT st-172-unbuilt-projection-refuses-by-name; NEW D grant-list-matches-carrier-list (the grant enum and the carrier map are the same set, or the build fails)
6a
ST-173[NEW]
A conversation grant is not a period
L2 · the grant is decided; the carrier is not
BENCH-O.grants gains 'conversations' on the live ownership row — the deliberate, off-by-default grant Gera decided on Sep 8. Three threads on a1: one open, one closed yesterday, one closed inside the last period.
L1 today the grant is accepted onto the row and changes nothing readable — the contradiction is made visible rather than hidden. · L2 ST-30's bar is on the reader: the reports repository returns no CONV#. This case asserts that a pushed digest row under its own prefix is not forbidden by ST-30, so the gap is a missing carrier and not a prohibition. …
NEW UNIT st-173-conversations-grant-has-no-carrierexpect:'RED'; extends FF reports-push-authorised-by-row; NEW D st-173-no-second-cross-wall-reader
6a · blocks on the grant-grain ruling
ST-174[NEW]
The work order the holder can count but cannot open
L2 · a count is not an object
BENCH-O with work_orders granted. Two work orders on a1 in the period: one closed, one open with three vendor messages.
L1 the holder reads woCounts and cannot name a single work order. · L2 zero vendor message bodies exist anywhere in the holder's partition — a Count assert, not a sample. · L3 with the proposed projection row, id, status, category, openedAt and spend-to-date are present and no message body is — the privacy line is at the body, not at the object. …
NEW H-L2 st-174-wo-projection-carries-no-bodies (json); NEW D no-message-body-crosses-a-party-push (rg on the push writer's payload type)
6a · gated on the grant-grain ruling
ST-175[NEW]
One number across buildings run and buildings only held
L2 · the roll-up across a projection boundary
org_p runs d1–d3 and its principal is an org_admin there; org_other runs g1–g2 with ownership rows naming org_p and no membership in org_other. Control arm: the same person given a membership in org_other, which must fold to one live headline over five …
L1 today deriveSelection yields propertyIds {d1,d2,d3}, reportsIds {g1,g2}; the combined headline covers three of five and the projection tile covers two. · L2 the scope line must therefore say "3 properties · 1 organization + 2 reported", never "5 properties" — a headline that silently means three is a fabricated number, and this leg binds whichever way the fold ruling goes. …
NEW UNIT st-175-reports-leg-in-the-fold (pure, over the two-leg fixture); NEW H st-175-headline-counts-what-it-says; extends FF merge-rows-not-payloads
6a + UI DB-D1 · needs the fold ruling
ST-176[NEW]
The fold has no context
L2 · the one function that legitimately holds two walls' numbers is the only one with no branded type
ST-136's login across two organizations. Instrument the stats route.
L1 two mintFromSession calls, each producing its own tenant context. · L2 the function that holds both results is typed with none of the five branded contexts — asserted as a type-level @ts-expect-error on any attempt to hand the folded object to a repository. …
NEW D st-176-no-org-list-anywhere (extends ST-136's); NEW UNIT st-176-foldcontext-accepted-by-no-repository
UI DB-D1
ST-177[NEW]
A saved selection is the label, done as a view
L2 · visibility as a view, never a row on the building
ST-133's grants across two walls. Save PERSON#<p>/VIEW#<slug> {orgIds, propertyIds}. Then revoke the person's role in the second wall and re-open the view.
L1 the saved view is re-derived through deriveSelection(grants, saved) on every open, so after revocation it opens as the first wall only with refusedOrgCount 1 — a saved label can never widen a grant. · L2 the view writes zero rows on any PROP# and zero on any ORG# other than the person's own. …
NEW UNIT st-177-saved-view-narrows-only (property-based, reuses ST-133's generator); NEW D st-177-view-row-is-person-scoped; FF selection-never-wider-than-grants
L1 the end bumps mapVersion. · L2 the next mint gives actor.buildings = {a2} — asserted with a cold and a warm process, because the record names a cache key for the roster and for chain rows and none for group-derived actor.buildings. · L3 the negative control: a value-only edit bumps valuesVersion and must not change actor.buildings, proving the key is the map version and not a TTL. …
NEW H st-178-group-scope-invalidates-on-mapversion (two-process, cold/warm); NEW D st-178-actor-buildings-cache-key-named (rg: exactly one cache site, keyed (personId, mapVersion)); FF cache-exact
L1–L4 6a (the {groupId} scope) · L5 UI S-E
ST-179[NEW]
Shares that do not sum to one
L2 · share? is optional and unconstrained
BENCH-O extended: (a) two rows at 0.5 and 0.5; (b) two rows at 0.6 and 0.6; (c) one row with share absent; (d) twenty rows at 0.05.
L1 today (a)–(d) are all writable — asserted, so the absence of a rule is a recorded fact and not an assumption. · L2no number a person reads may be divided by share unless every live row on that building carries one and they sum to 1 within tolerance; otherwise the figure prints an em dash with "ownership split incomplete". This is the no-fabricated-numbers rule applied to a divisor. …
NEW UNIT st-179-share-never-divides-an-incomplete-set; NEW D st-179-one-share-consumer (shares its rg site with ST-169)
6a · L3 needs a ruling
ST-180[NEW]
The ninth wall in the switcher
L2 · the selection cap against twenty parties
A person with logins across twenty walls (ST-181's parties, each with one). Open the picker; select all.
L1 the cap refuses the ninth selection, by name, and says the cap — it never silently drops one. · L2 the cap is on *selection*, never on *holding*: all twenty rows are listed and searchable. · L3 with twenty selected-or-capped, deriveSelection still yields at least one wall — ST-134's never-empty. …
NEW UNIT st-180-org-cap-refuses-by-name; NEW H st-180-capped-fold-says-so
UI S-A / S-B · needs the cap decision
ST-181[NEW]
Event-grained party push at twenty parties
L2 · the amplification the freshness fix creates, measured before it is built
BENCH-O generated to N=20: PROP#a1 with twenty live ownership rows, twenty distinct party walls, share:0.05 each, the five-projection default. One work order on a1 moves through six status transitions in a day.
L1 naive: 6 × 20 = 120 cross-wall transacts for one work order — measured and printed, never estimated. · L2 the damper is named before the writer ships: coalesce per (party, building, entity) on a bounded interval, so the same day costs ≤20 pushes and the party's row carries the latest status plus updatedAt. …
NEW H st-181-event-push-coalesces (json, counted, generated seed); NEW D st-181-no-cross-party-batching; extends FF reports-push-authorised-by-row to N
after the grant-grain ruling
ST-182[NEW]
"basis: mixed" means something
L2 · the undefined string on the headline
ST-175's set: three live legs + two reported legs.
L1basis is a typed value on the stats payload, 'live' | 'reported' | 'mixed', never a free string. · L2 a fold that crosses a projection boundary must emit mixed, and the rider names the reported legs' periodTo. · L3 an all-live fold emits live and no rider — so the rider's presence is information rather than decoration. …
NEW UNIT st-182-basis-typed; NEW D st-182-basis-not-a-free-string
UI DB-D1
ST-183[NEW]
Every node that differs from the customer, in one list
L3 + L4, one case · the instrument half of the intern question
BENCH-A extended to eight buildings, one key (fees.pet): the customer row = 35, three buildings holding 50 / 50 / 45, one of them set by a person whose role has since ended, and one building holding 35 of its own (the redundant row).
L1keyDivergence(dumpOrgMap(ctx), 'fees.pet') returns the customer's row first, then the three divergent buildings each with {value, setBy.userId, setBy.source, since, effectiveIfRemoved:35}; the inheriting buildings are a count, never rows. · L2 the redundant building appears as redundant, not as divergent — without this leg the assert is vacuous against the migration failure ST-185 names. …
NEW UNIT st-183-key-divergence-projection (pure, over the dump); NEW FF overrides-inventory (nightly, delta-triggered); FF map-dump-stable; FF every-effective-value-has-provenance
2b — with the resolver, not at the map-screen item the record dates to 2027
ST-184[NEW]
Two buildings that disagree about which group is nearer
L3 · the tie refusal is intra-building only
BENCH-A + two chain groups g_region and g_submkt, both holding fees.pet; building a1 edges {g_region:2, g_submkt:1}, building a2 edges {g_region:1, g_submkt:2}.
L1 the second building's edge write is refusedprecedence_inconsistent, naming both groups and the building that already fixed the order — today both writes succeed and a1/a2 resolve different values from the same two rows with no signal anywhere. · L2 control: two groups that share no key still write freely — the refusal is about order, not co-membership. …
NEW UNIT st-184-precedence-consistent-across-buildings; FF registry-closed-and-typed (the new refusal); FF map-dump-stable
6a — the refusal belongs to the relationship writer, the same site as ST-95a
ST-185[NEW]
The migration invents the inconsistency
L3 · the step-7 backfill manufactures per-building divergence
BENCH-400 (120 buildings is enough) with escalationOwnerEmailidentical on all of them.
L1 after the per-key move, count(PROP# rows for escalation.owner) is 0, not 120: identical values hoist to one customer row and the descendants END with endedBy.source:'hoist'. · L2 an operator edit at the customer then changes the resolved value at all 120 — the assertion that catches the redundant-row failure, which the step's own "diff-empty per key" proof cannot, because diff-empty is exactly …
NEW H-L2 st-185-migration-hoists-identical-values (json, counted); NEW FF backfill-hoists-identical (the step-7 per-key gate's second assertion); FF maplog-undo-exact
7
ST-186[NEW]
Make them all the same, and let one change tomorrow
L1collapseKey(ORG#, 'office_hours') ENDs a1's and a2's rows with endedBy.source:'collapse', writes nolocked flag, one log row. · L2 every building then resolves {value: the customer's, setAt: ORG#, locked:false}. …
BENCH-A + GROUP#urban {kind:'chain'} over a1, a2; ORG#/ATTACH#setting#fees.pet {v:35, locked:{belowTier:'property'}}.
L1 the write of fees.pet = 50 on GROUP#urbansucceeds. · L2 the write of fees.pet = 60 on PROP#a1 is refused, naming the locking node and the tier the lock stops at, so the operator sentence is one line: "the pet fee is locked below the sub-market — set it on the sub-market or leave it." · L3a1 resolves {value:50, setAt:'GROUP#urban', locked:true}. …
NEW UNIT st-187-lock-below-tier (pure, over the lock line of the fold); FF registry-closed-and-typed; FF no-masking-nightly
2 — D2-gated: the lock lives on the value row, so the shape change is a registry edit now and a scripted sweep later
ST-188[NEW]
A fourth rung
L3 · the depth cap, priced instead of assumed
a building already in an LLC-set, a cluster and a line-share group — the shape one customer already needs — to which a sub-market group is then added.
L1 refused chain_depth, and the refusal names the three groups already on the building so the operator knows which to fold; today ST-95b asserts only that it refuses. · L2 with the cap raised to five, loadChainRows still issues one round trip and the read ledger shows at most five parallel Queries — so raising the cap is priced, not assumed, and the cap stays a bound on the read rather than a mood. …
Existing UNIT over getScopePath extended; NEW H st-188-chain-depth-ledger (json read ledger); FF cache-exact
L1 6a · L2 3b (the ledger)
ST-189[NEW]
Two conditioned rows in one slot
L3 · the reserved field the live-row invariant forbids
ORG#/ATTACH#setting#fees.pet written twice on one node, once conditioned on one asset type and once on another.
L1 the second write is refused by invariant (h) with {status:'conflict'} — which is the point: the case exists to pin that the reserved condition field cannot express multi-armed policy under the current slot grammar, because the slot is the key. …
NEW UNIT st-189-conditioned-rows-share-a-slot (over putAttachment); FF one-live-row-per-slot
2 (the refusal is live the day the writer ships) · the widening is the reserved milestone
ST-190[NEW]
One key, one node, one person — the delegation row
L4 · "relinquish control" gets an encoding
BENCH-A + PERSON#onsite7/ROLE#{apm, scope:{propertyId:'a3'}} and PERSON#pm_org/ROLE#{property_manager, scope:{organizationId}}.
L1 the org_admin writes att(PROP#a3, delegation#setting.quiet_hours {to: PERSON#onsite7, grantedBy}) → created, one log row, valuesVersion unchanged. · L2onsite7 writes setting#quiet_hours at a3 → created. · L3pm_org writes the same key at the same node → refused by name, naming the delegate — proposed refusal (k). …
NEW AttachmentKind 'delegation' + one KINDS row; NEW UNIT st-190-delegation-refusal (property test over putAttachment, no store); NEW FF delegation-names-one-person-one-key (a live delegation row admits exactly one writer …
2a (the kind) + 2b (the refusal); L7 runnable at 2a
ST-191[NEW]
A lock with nothing to say
L4 · you cannot forbid a value without publishing one
BENCH-A, pets.policy set at PROP#a1 and PROP#a2, no customer row.
L1 the org_admin tries to forbid building overrides without publishing a value. Today value is required, so the only path is to write a number with locked:true — assert the transact ENDs both building rows and the next resolve returns the invented number with setAt: ORG#. …
NEW H-L2 st-191-lock-invents-a-number (json, L1/L2); NEW UNIT st-191-lock-row-without-a-value (L3); FF every-effective-value-has-provenance
L1/L2 2 · L3/L4 new work
ST-192[NEW]
Two overrides between two map edits
L4 · the log is keyed on a counter the write did not move
BENCH-A at mapVersion v, valuesVersion w.
L1putAttachment at a1 then at a2, with no structural write between them. · L2 assert two distinct log rows exist — today both transacts write MAPLOG#<mapVersion> at the same unchanged v, so this is the falsifier and it prints whatever the store actually does: a collision, an overwrite, or a cancelled transact. …
NEW H-L2 st-192-two-value-edits-two-log-rows (json, row count); FF maplog-undo-exact (extended to value edits)
1 — the sort key must be decided before the first row is written
ST-193[NEW]
Who set this, and were they allowed to?
L4 · authorship is optional and authority is unrecoverable
BENCH-A; three writes of setting#application_fee at a1: one through the admin path with a userId, one through the onboarding path with none, one by a person whose role is ended afterwards.
L1 all three are accepted today — setBy.userId is optional and no writer refuses its absence; the defect stated as a passing test. · L2 for the second, "who set it" resolves to nothing: the provenance tuple carries a source and no human. …
NEW UNIT st-193-setby-required-on-the-admin-path; NEW D st-193-setbyrole-is-a-copy (rg: no join at read); FF every-effective-value-has-provenance — which today asserts the node, never the person
1 — cheap before rows exist
ST-194[NEW]
The org_admin is told when a building overrides a policy
L4 · a successful write that should not have happened has no signal
BENCH-A + a watch-list row at the customer naming pets.policy.
L1 a write of a watched key below the customer emits exactly one notification carrying {building, key, from, to, actor, at}. · L2 it fires on a successful write — the case that has no signal today, because the design's only near-real-time signals are the write-time refusal (which by construction fires when the write fails) and the version-conflict sentence (which fires on a concurrent edit). …
NEW H st-194-watched-key-emits-one-signal (stream reader, json); FF maplog-undo-exact (the same row); FF overrides-inventory (the nightly half, ST-183)
2b — one stream reader on a row that already exists
ST-195[NEW]
A delegate hands the landlord the conversations
L4 · the ceiling is stated on the row kind, not on the grant value
BENCH-O + a delegate holding manage_roles at the customer scope who is not an org_admin.
L1 the delegate writes owned_by.grants += 'conversations' on a1 → assert what happens. Today nothing refuses it: the decided ceiling and the intersection rule are both about role grants; ownership grants are a different field on a different row kind with no stated ceiling, and ST-145 L2 affirmatively asserts "the delegate may add one". Silence here is permission, not omission. …
NEW UNIT st-195-grant-ceiling-on-the-value; NEW D st-195-grants-registry-closed (rg, mirrors registry-closed-and-typed); FF reports-push-authorised-by-row
6a / P7 — the ceiling must land with the grant, not after
ST-196[NEW]
A refusal that names a node you cannot read
L4 · the grantee-facing wording
BENCH-M; att(ORG#org_st_a, setting#office_hours {locked:true}); the party's person acting under the acting-org context.
L1 declare visibleToGrantee — assert it exists on KeySpecor on the attachment; today it is in neither (this is ST-167's L4 from the other side, and the two share one registry assert). · L2 assert its scope: every key, or only the two the prose names? If every key, resolve the whole chain under the acting-org context and assert what the party's person gets for office_hours, greeting and …
NEW UNIT st-196-grantee-refusal-names-the-relationship; NEW D st-196-no-setat-outside-reach (rg + a shape assert); FF registry-closed-and-typed
2a (the declaration, shared with ST-167) · 6a (the behaviour)
ST-197[NEW]
Three words, and only three
L4 · Fede's litmus as a drift test
F10 + BENCH-A, every key class represented.
L1 for each (key, node) the settings surface offers exactly one of *"Everyone here uses this"* · *"Buildings may differ"* · *"<Name> decides this here"* — or renders one of two greyed facts, *"This building's own"* (IDENTITY) and *"The law decides"* (COMPLIANCE_FLOOR). …
NEW UNIT st-197-three-words-total (pure, over KEYS); NEW D settings-vocabulary-closed (rg over the settings tree's strings)
the rg test at 2a · the projection at 7, as keys move
ST-198[NEW]
The new line greets with the building's name, and the cadence does not change numbers
L5 · the outbound half ST-153 does not pin
BENCH-A-3own, plus a live CONV# on the shared line bound to a3 and one already-queued outbound (a tour reminder) for it.
L1 after the claim, the greeting at a3 resolves setAt:'PROP#a3'and the rendered string names a3 — assert the string, never the tuple alone. · L2 the shared line's rendered greeting is byte-identical before and after, and its reach still contains the buildings it kept. …
NEW UNIT st-198-rendered-greeting-names-the-building (reuses ST-152's pure derivation); NEW H-L2 st-198-queued-outbound-keeps-its-address (json, the recording adapter's call log); FF same-agent-id-every-line
L1/L2 3b · L3/L4 5a — and the arrival-address field ST-15b names is defined in no row, which this case pins
ST-199[NEW]
The third leasing hire silently drops the building's own calendar
L5 · a correct-looking write re-arms the failure
the three-calendar shape: PROP#/ATTACH#calendar#primary plus two seeded assignments that each list the building's calendar in calendarIds; then INS a third assignment whose calendarIds omits it.
L1 (the control, green by the seed) a tour that goes to one of the first two hosts does land on the building's calendar, because her list carries it. · L2 (the falsifier, must fail today) a tour that goes to the third host lands on nothing shared: the building's own calendar receives no event, no write is refused and no warning is raised — the building's record of what happens there stops being complete, …
NEW H-L2 st-199-third-host-drops-the-building-calendar (json + the recording calendar stub); NEW FF building-calendar-never-silently-dropped; FF one-live-row-per-slot
8
ST-200[NEW]
The floating manager shows a home whose holder never consented
L5 · an undecided branch that fires on ordinary stock at go-live
the scattered-site shape: one staff calendar at the customer, one credential; PROP#gj-home/REL#owned_by#ORG#<party>#t {share:1, grants:['reports']}withoutpoolingConsent:['leasing_line'].
L1findTourCandidates today drops the customer-level host by the consent filter and the building has none — the case must assert which of the two open readings fires, by name, never "open". · L2 (the recommendation) an ownership row whose party wall carries purpose:'owner_party' — a non-customer — defaults poolingConsent to ['leasing_line'], because a passive holder has no staff and no …
NEW H-L2 st-200-pooling-default-for-a-non-customer-party (json); FF consent-never-gates-reach (the host half); FF scheduling-is-pure
6a (the ownership row) — the ruling is needed before onboarding, not at step 8
ST-201[NEW]
Who is allowed to read her free/busy across the link
L5 · ST-25 pins the content and never the authority
ST-25's bench: one human, a person row in each wall, one admin-only link, one calendar row per wall behind one external calendar.
L1 the tour scheduler in the second wall, running as code, reads free/busy across the link and books around it. · L2 a human in that wall — any tier, administrator included — reading the same surface through the UI or an API route gets nothing: no busy blocks, no "she has something at 2", no count. …
NEW UNIT st-201-freebusy-grant-is-held-by-the-schedule-row; NEW D st-201-no-human-reader-of-the-link (rg: no route reads the cross-link path); FF isolation-gauntlet (a new row)
8 (calendars) · 6b (the link)
ST-202[NEW]
The building's handyman does not pick up
L5 · replace is the wrong verb for a dispatch list
BENCH-V on BENCH-A: a customer-level in-house roster entry and a building-level one at a2, both with live memberships. Companion to ST-159, which pins the same-roster tie; this pins the cross-rung tail ST-159 does not carry.
L1 (today, the control) the pick at a2 returns the building's entry only; the customer's is absent, not ranked last — assert the absence, so the case fails the day a merge is introduced silently. · L2 (the change) with mode:'prepend' on the building row the answer is [building's (via PROP#a2), customer's (via ORG#)], in that order, each carrying its via. …
NEW UNIT st-202-roster-tail-mode; FF roster-names-only-engaged; FF engagement-gates-vendor-reach
2b — ST-33's own step
ST-203[NEW]
A line in observe for twenty-four hours
L5 · the back-test the ramp assumes
BENCH-A + a line attachment carrying route:{kind:'observe', target}.
L1 (must be CANNOT-EXPRESS today) there is nowhere to write "this line is observing": capability_stage is writable at the customer, a group and a building, and the address is not a node — assert the absence at the registry, not at a call. …
NEW UNIT st-203-observe-is-a-line-not-a-node; NEW H-L2 st-203-observing-line-sends-nothing (json + the recording adapter, send count 0); FF registry-closed-and-typed (the widened enum); FF unclaimed-address-refused-zero-reads
3b (the row) · 4 (the drafted-reply capture)
ST-204[NEW]
Break-glass: give the line back to the humans in under forty minutes
L5 · both existing levers end at the assistant
BENCH-A with a live claimed line and traffic on it.
L1 measure what each existing lever does to a caller: setting the capability stage off at the customer leaves the caller reaching the assistant and hearing the out-of-scope line; releasing the address yields the platform refusal with zero further reads. Neither reaches a human — assert that, it is the finding. …
NEW H st-204-break-glass-reaches-nobody (L1, json, the refusal shape); NEW R st-204-rehearsed-repoint (the measured wall-clock, an artefact); NEW D st-204-reconciler-does-not-undo (over sync-phone-numbers.yml)
L1 now (it is a read of today's behaviour) · L2 on the bought test line, this week · L4 3b
ST-205[NEW]
A building's own line goes live while a thread about it is in flight
L5 · the first claim, which ST-42's family does not cover
BENCH-A-3own + a live CONV# bound to a3 that arrived on the shared line; then the first claim of the building's own number.
L1 her next text to the old number matches the same thread org-first — same wall, same person, same channel — and no second thread is born. · L2 her next text to the new number matches the same thread, not a new one: the match must not hard-scope on the arrival address any more than it hard-scopes on the building, which is the shipped defect one level up. …
Existing H-L2 thread-lost-after-mid-conversation-bind extended (the RED half); NEW H-L2 st-205-first-claim-keeps-the-thread (json); FF bind-home-single-writer
5a — and it must be added to ST-42's family, which today covers a rebind and a rebind-and-back, never a first claim
ST-206[NEW]
"One new edge" costs two merges — the deploy falsifier
L5 · the operator catalog's zero-deploy claim, made falsifiable
BENCH-A-3own on today's tree.
L1 count the artefacts a hybrid building's new number actually requires today: the rows (4 inserts + 1 update) plusconfig/phone-registry.jsonplus the number-to-agent file plus two merges — assert the count is greater than zero deploys, i.e. that the operator catalog's "0 / 0 / 0†" is footnote-true and headline-false until the config dumps land. …
Existing D phone-registry.drift.test.ts extended; NEW D st-206-new-line-deploy-count (rg + the file list, arms today); FF seeding-calls-zero-scripts
1 (L1 — a drift test over today's tree, runnable now) · 3b (L2/L3)
ST-207[NEW]
The building's own calendar blocks nothing
L5 · a write target that is not a read source
ST-199's bench with a busy block on the building's own calendar — a resident event, a fire drill, an office closure — at a weekday afternoon slot.
L1 (the falsifier)findTourCandidates offers that slot to a prospect, because the building's calendar is a write target only: availability is the union of the hosts' free/busy intersected with holds, and no leg reads it. That is a double-book that reaches a real person. · L2 (the change) any calendar that appears in a live assignment's calendarIds[] is also a free/busy source; the slot disappears. …
NEW H-L2 st-207-building-calendar-is-a-freebusy-source (json + the recording calendar stub); FF scheduling-is-pure
The picker is a pair of reading glasses, not a door key — it changes what you are looking at, never what you are allowed to see.
If you only read this: three screens change — the top-left corner becomes an org picker with checkboxes, the dashboard learns to add two orgs up correctly, and the Atlas becomes a tree you can read. The 26 questions below are historical proposals; current dated rulings above govern before any of it is built.
Three screens change, and they all change for the same reason. Today the app remembers one building — a single id in a cookie (filter.tsx:108-113) — and it has no idea which org you are working for; it guesses from the first role it finds (accessors.ts:327-336). Proposed: the top-left corner stops showing a logo and starts showing the org, with checkboxes so you can hold one, two or three at once; the dashboard adds a "⋯" menu that splits every number by org or by building; and the Atlas becomes a map you can actually read, one tree per org, with a what-if switch that rehearses a change and prints the rows it would write. Nothing here is decided — this is the spec, and the twenty-six questions at the bottom are what Gera and Fede have to answer before it is built.
1 · The org switcher — and checkboxes on both pickers
Figure U1. Two Pickers, One Address. The org picker takes the logo's place with the collapse button beside it; the building picker in the top bar becomes checkboxes grouped by org; both commit once on close into one sorted, shareable address that every page and every route re-reads for itself.
Read it at
The top-left corner stops showing our logo and starts showing which org you are working for. You check the orgs you want — one, two or three — and you can never tick none.
Where the logo sits now, an org picker takes its place, with the collapse-the-sidebar button on its right. It opens a small panel with a search box and checkboxes; the count at the bottom says how many you have. The building picker in the top bar becomes checkboxes too, grouped under each org. Ticking edits a draft; closing the panel commits it once, so three ticks are one page reload, not three. Everything you picked goes into the web address, so a link you paste to Fede opens on exactly what you were looking at — trimmed to what he is allowed to see.
Both pickers are one primitive used twice: checkbox rows, tri-state group headers, a footer with a count and an Only / All action, one keyboard map, one ARIA contract (listbox, aria-multiselectable, aria-selected per row). A selection is a branded value with one constructor, deriveSelection(grants, proposal), run on the server at layout time and re-run by the same pure function in the browser on every address change — so the browser can only ever narrow what the server minted. An org at its full default roster serialises as one *@<orgId> token rather than four hundred building ids. An address naming an org you are not in is trimmed and counted, never named. Pages that genuinely need one building — the renewals board (renewals/(list)/page.tsx:39-43), a mass send, per-building settings — refuse a set of two and list the set; they never flip the picker back to single-select.
One shared module, lib/domain/scope/selection.ts, exports Selection {orgIds, propertyIds, reportsIds, everyOrgAtDefaultRoster, allOfOrg, refusedOrgCount, emptyGrant, sets, cappedOrgIds}, ScopeSet, scopeKey() and the pure deriveSelection. The flag is everyOrgAtDefaultRoster, and it was called allProperties until #7890 renamed it at review — because the old name read as unscoped, which is the widening this module exists to kill. It is true when every selected org rides its own DEFAULT ROSTER, and that roster is still the VIEWER’s: for a building-scoped pm it is one building, for an org that grants nothing it is none — so it is true while the selected set is narrow or empty. It is DISPLAY-ONLY (it is what lets the picker say “All of Acme” and what serialize turns into *@<orgId>); read propertyIds / sets[].propertyIds to know what to query. sets (the per-org resolved sets the fold consumes) and cappedOrgIds (orgs the viewer holds that did not fit under MAX_SELECTED_ORGS, refused BY NAME) are part of the shape too — this list named seven fields when the module exported nine. One edge file is proposed in src/middleware.ts — its matcher already runs on every app route (src/middleware.ts:930-940) — to copy the query into header x-pf-selection, overwriting any client value, and refresh cookie propflow-selection; the layout calls a zero-argument, react.cache()d getSelection(); API routes never read that header — the 33 selection-driven ones re-mint from their own URL through readScope(req, user), and the 29 job/admin routes keep one id under an explicit allowlist. Options are minted with the grants from that org's own role rows (never from the request user's single-column role / assignedPropertyIds, scope.ts:47-52), over WORKSPACE_TIERS = STAFF_TIERS ∪ {viewer}, dropping the PropFlow staff org (src/lib/data/constants.ts:27) and non-active orgs. Caches key on scopeKey(scope) plus the route's own params and carry no viewer in the body.
The next three blocks are the build spec — Fede’s half. Nothing below adds a fact the paragraphs above have not already said.
The contract
State
Where it lives
Who reads it
What you picked
the URL — ?org= and ?prop=, ids sorted so two people's links are byte-equal; an org at its default rides as one *@<orgId> token
every page and every route, each re-minting from its own URL
What you may pick (grants)
minted on the server per request from your role rows in each org; never sent up from the browser
deriveSelection(grants, proposal) — the only constructor
The relay
proposed: one edge file copies the query into a request header (overwriting any client value) and refreshes cookie propflow-selection; the matcher that would carry it already runs on every app route — src/middleware.ts:930-940
the page render's mint only; an API route reads its own URL, never the header
Your last selection
the cookie, as a proposal — it can be refused
the mint, when the address says nothing
A refused org
a count on the selection (refusedOrgCount), never a name
one info banner: "2 organizations in this link aren't in your account"
Today, instead of all of that
one building id in cookie propflow-property-filter (filter.tsx:108-113), read as a bare string (filter.tsx:23-26)
62 API routes read searchParams.get('propertyId') (scoping-params.ts:24-26); 36 more take the org from the first role found (org-scope.ts:36-47)
The rules
You cannot select nothing. The last ticked row is disabled with a reason on hover; a filter that empties the picture keeps the org card and offers "Clear filter".
A selection is never wider than your grants. It is minted on the server and the browser re-runs the same pure function over the same grants — the client can only narrow.
Ticking edits a draft; closing commits once. Three ticks would otherwise be three recomputes, and a warm dashboard compute is ~2.4–3.3 s today (DashboardDataProvider.tsx:189-191).
One org is not a special mode. A set of size one is the single case; an org-level screen (settings, Team, billing, creating a vendor) refuses two and says which two.
The invite form gets the org first. Today it shows a building checkbox list only (admin/users/page.tsx:585-600) and the route derives the org from the ticked buildings, refusing when they disagree (api/admin/users/route.ts:204-224). The refusal is narrower than it looks: the invitee's org is seeded from the CALLER (:204) and the ticked buildings only override it, so a customer's own admin inviting with nothing ticked lands in their own org and passes; it is PropFlow staff inviting into a customer with no stamped buildings that gets refused, and Fede's open PR #7313 adds a staff route that takes the org from the URL instead (api/admin/companies/[id]/people/route.ts). Proposed: pick the org (defaulted from the switcher), then "which buildings — all by default", where "all" is the absence of a list, not a pre-filled one (scope.ts:25-45 calls its own pre-fill a stopgap).
What it must not do
Render a logo, wordmark or mark in the side-nav header at any width — that corner is the org now.
Ship a single-select mode, a radio list, or an "All organizations" row for anyone, staff included. The wall is per org; staff pick, and each pick is read under its own context.
Hold an org id in localStorage, in a cookie the browser may write, or in a request body — vendors/route.ts:71-79 takes body.organizationId from a caller with no org of its own today, and that goes.
Let '' or [] keep meaning "all". The two meanings of empty were the bug (scope.ts:54-57 already reads an empty set as nothing).
Let a write consult the picker. A write is addressed — row → building → org (mass-sends/route.ts:186 already does it right).
2 · The dashboard under a selection
Figure U2. The Headline That Does Not Move. The scope line carries a chip per org and a "⋯" menu; splitting adds small multiples under the same headline, a second series on the same axis and a column group on the table — and the catalogue's one rule per number decides whether the set is summed, weighted, recomputed or simply refused.
Read it at
The dashboard shows the numbers for exactly what you ticked. A "⋯" button can break each number apart by org or by building, and the big number at the top stays the same either way.
Pick two orgs and the dashboard adds them up correctly for you. A "⋯" menu offers combined, split by org, split by property or both: splitting draws a small tile and a line per org under the same headline, and adds a column group to the table.
Percentages are the part that goes wrong most easily — a 100 % occupied four-unit building next to a 50 % occupied 120-unit building is 51.6 %, not 75 % — so every number carries its own rule for how a set is folded. Where the answer really is unknown the screen prints a dash and says why; it never prints a zero.
Six fold rules live as one proposed column on MetricSpec (src/lib/data/metric-catalog.ts:89): sum, weighted ratio (Σ numerator / Σ denominator, dash at zero denominator), mean weighted by count, recompute (the server ships {num, den} per org because the answer cannot be derived from rates), merge (top-N over a union, keyed with the building), and setting / latest / one ("varies · N values", a relative timestamp, or a refusal). The unit of recompute is the org: one read and one compute per org, folded above — never one read whose org is a list. The two differ wherever a key is a bare name; today evictions are union-found on last names (compute.ts:202-246), so a family surnamed the same at two clients would be one root in a union and two in a fold, and a name is not a person. Colour comes from one registry (lowest free slot on first appearance, held for the session) rather than by position in the series array as today (insight-specs.ts:296-306).
GET /api/dashboard/stats?org&prop&source&period&segments answers {scope, combined, byOrg?, bySegment?}, where ResolvedScope.orgs[].total is the viewer's visible roster and therefore never enters a cache body; keys are scopeKey(scope) + the route's own params (today the server key embeds the RBAC scope, stats/route.ts:55-59). computeDashboardStats (which loads and computes in one body, compute.ts:353-376) splits into loadDashboardInputs(ctx_o) + a pure computeDashboardStatsFrom, run once per S_o and folded. The deployment-wide PORTFOLIO roll-up (handler.ts:326-341, served ungated at metric-history/route.ts:233-244) and the PROP#GLOBAL/INSIGHT# fallback (analytics.ts:152-164) stop being values a client may send; a per-org ORG#<id>/METRIC#<date> row replaces them, carrying daily weights, num/den for recompute keys and memberIds[], and served only when memberIds == S_o — one comparison that covers both the narrowed manager and the explicitly named sandbox. ChartSeriesSpec.key is the entity (org:… / prop:… / set / other), never an index.
The next three blocks are the build spec — Fede’s half. Nothing below adds a fact the paragraphs above have not already said.
The contract
State
Where it lives
Who reads it
The split
?split=org|property|org+property in the URL, canonical
the "⋯" menu and the inspector's own Split toggle (Toolbar.tsx:227-237) — one state, two mirrors
The numbers
one server answer {combined, byOrg?, bySegment?} at the grain the menu asked for
the KPI row, every chart and the table — one fetch, not one per tile
How a set is folded
one proposed setRule column on MetricSpec (src/lib/data/metric-catalog.ts:89)
the fold, and the unit test per rule that proves it
Which colour is whose
one tone registry per session in sessionStorage (never localStorage — these are org and building ids), cleared on sign-out and on impersonation
every chart, the building cards, and the Atlas root accent, which only reads
Cache key
scopeKey(scope) + the route's own params; the body carries data only
the server stats cache and the in-tab cache (today keyed by one building id, session-cache.ts:43-56)
Today, instead
colour by position in the series array (insight-specs.ts:296-306); the hero cards carry no change figure (TopMetricStrip.tsx:70-74); savings are deflections × mean cost (compute.ts:645-653)
— so the savings figure moves when this ships, and the page should say so
The rules
A ratio is never summed and never averaged. Occupancy over a set is Σ occupied ÷ Σ units; a zero denominator prints "—" with "no tenants in scope", never 0 %.
The headline never moves when you split. A split adds small multiples beneath the same number, N series on one axis, a column group on the table.
One read and one compute per org, folded above. A route may never issue one read whose org is a list.
A gap is a gap. A date where a member has no row makes the line null for that date with a rider, never a flat zero; a change figure is the line's ends, so no line means "— MoM".
Splitting is enabled by the set, and a disabled item still says why. Split by org needs two orgs (an org with nothing ticked still counts); split by property needs 2 to 50 buildings.
What it must not do
Colour by rank, generate a ninth hue, or reuse a status colour as a series colour.
Serve a deployment-wide roll-up as one customer's "all", or serve a whole-org roll-up to somebody narrowed inside that org.
Put a dual axis on one chart, or set a live single-building figure beside a set figure — the live legs stay null at any set (compute.ts:1275-1285).
Merge a list on a key that drops the building (problemUnits keys on unit number alone today, compute.ts:700-704), or print a 0 where the answer is "no data".
Let Ask Clara answer over your whole grant while the page shows your selection — the question carries the same scope, one context per org.
3 · The Atlas as a map
Figure U3. One Tree Per Org. One rooted tree per selected org: addresses on the left each claim exactly one node, the org runs its buildings through one fan-out, chips say where each line, mailbox, calendar, login and policy really lives — and the what-if switch rehearses "give this building its own calendar" on a test building, printing the exact rows it would write.
Read it at
The Atlas becomes a picture: the org at the top, its buildings under it, and little tags showing which phone, mailbox and calendar belongs where. A switch lets you try a change on a test building and see what would happen, without changing anything.
One tree per org you have ticked. The org sits at the top with tags for the things it holds — its mailbox, its tour calendar, its office hours, the vendors it uses, its login to the property system. Its buildings hang under it, each with tags of its own: a solid tag means the row really lives there, a dashed one means it is borrowed from higher up. Two things you flagged are fixed here. Vendors stop being a flat folder off to the side — a vendor shows as a tag on the org that engages it, and again on each building that ranks it.
And the second org tile stops reading as a second list of companies. It is not redundant — it is the only place you can browse every building, and every person, across all the companies at once, which a single company's page cannot do — so it keeps that job and loses the disguise: it is now called All records and filed under Ops. Only real orgs are roots, and the staff org, archived orgs and unassigned rows drop into a side list under a line that says what they are. Phone numbers sit on the left and each one points at exactly one place. Then the useful part: focus a test building, flip what-if, and the map shows the change and lists the rows it would write — three inserted, one updated, nothing moved — with an Undo. Nothing is saved.
Eight things a person must be able to read off it: how many trees (one per selected org, with the staff org, unassigned rows, dangling ids and archived orgs pushed into a side list — the "two organizations show" confusion is closed — propflowai#7674 renamed the second tile All records rather than deleting it, and propflowai#8048 put a named divider above the side list (atlas-tree.ts:484-500)); one-to-many as a fan-out; many-to-one as several addresses converging on one node; a calendar at the org, the building, or both; what a change would do; vendors inside the org rather than the flat folder they sit in today (atlas-tree.ts:1171-1195); one pill per address claim; and what this org holds elsewhere. The chip vocabulary is deliberate: solid = here, dashed = inherited, ● = overrides, ▒ = your own row masked by a lock farther up, ⚠ = an honest gap. The picture already draws one such gap — a1 and a2 answer on the shared number but hold no line of their own on the chain, so a fresh outbound message has nothing to send from.
Layout is ours, not a library's — the app carries no graph library, and "build our own so it is more customizable" is the ask. Fixed lanes, a packed column per lane, a bus path for fan-outs and elbows for the rest, one inline <svg> overlay with pointer-events:none and one marker per tree, plus a second pass on ResizeObserver — about 200 lines of layout over a ~120-line pure projector projectOrgMap(dump, KEYS, opts), snapshot-tested on the bench fixtures with a layout test asserting no two cards overlap and every edge's endpoints sit inside their cards. At the target the map is fed by dumpOrgMap(ctx): one profile read, one roster query, one group query, one address query, roles by scope, engagements, party relationships and one attachment query per drawn node — roughly seventy bounded reads for a four-hundred-home org, against seventeen index scans in today's page (atlas/page.tsx:109-164). A what-if is ?whatif=<catalog>:<targetPK> — a catalogue number and a target, never hand-written rows in a URL — gated to a test building, and its ledger comes from the one counting function this page's own change catalogue already uses.
The next three blocks are the build spec — Fede’s half. Nothing below adds a fact the paragraphs above have not already said.
The contract
State
Where it lives
Who reads it
Which trees are drawn
selection.orgIds — one rooted tree per selected org, one context per tree
the map; it dims what is outside the selection and never writes the selection back
The map itself
one dump per org at the target (dumpOrgMap(ctx)); today, the columns the page already ships
projectOrgMap(dump, KEYS) — a pure function, snapshot-tested
Where a row really lives
resolved client-side over the key registry: here · inherited · overrides · masked · absent
the chip's look — solid, dashed, ●, ▒, ⚠ — and the focus drawer's row list
Focus and filters
?path=organizations/<orgId>&focus=<node>&mode=tree|grid|table|raw, validated against the drawn document only
the right drawer; a phone gets the accessible table by default
The what-if
?whatif=<catalog>:<targetPK> — a catalogue number, never rows
the overlay, the ledger and the Undo; the store is never touched
Today, instead
one org list plus the renamed All records cross-org browse, and a flat vendor folder (atlas-tree.ts:484-500, :1171-1195); the ring-time truth for a number is a code map (phone-lookup.ts:140-148)
staff only — the page is notFound() for everyone else in production (atlas/page.tsx:93-96)
The rules
One tree is one wall. No edge ever crosses between two customers; an owner or manager from outside is a card at the edge, never a door.
Solid where the row lives, dashed where it is borrowed. ● where a nearer row overrides, ▒ where a lock farther up masks your own row, ⚠ where the record has an honest gap.
An identity key is never drawn as inherited. A phone line or a mailbox belongs where it belongs; only policy and parameter keys walk the chain.
What-if writes nothing, runs only on a test building, and always prints inserted · updated · moved — with moved at zero by construction.
The map reads the selection and never writes it. Following a link out of the map keeps your selection; a building outside it opens with a rider, not a silent re-scope.
What it must not do
Use a graph or force-layout library. The lanes are a sort we own, so we can keep changing them.
Draw a vendor as a rung of the chain, or invent a "serves" edge — a vendor is a card plus a dated engagement, mirrored as a chip where it is on the roster.
Render the staff org, unassigned rows, dangling ids or archived orgs as roots — those go to a side list. A selected sandbox is a root, with its badge.
Show a secret on a chip or in a popover, ever — display strings only.
Keep a second copy of the ledger arithmetic. One counting function, shared with this page's own change catalogue, or the two drift.
4 · The phased build
Fifteen phases in three lanes that run in parallel once the contract lands. Days are counts × judgment, engineer-days, and they are not on the 83–121 ladder — this is UI work beside it. Owners are proposed (D-01). The critical path to the demo Gera described — switcher, multi-select, a set-aware dashboard — is S-A → S-B → DB-D1 → DB-D2, about 16–19 days against the 14 working days to Fri Oct 2, so it fits only if S-B and DB-D1 overlap; the sweeps do not fit the frame.
#
Phase
Lane
What lands
Days
Owner
What proves it
1
S-A the contract
switcher
the one shared module, grants per org, the server mint, the edge relay and cookie, the provider, readScope, scopeKey, addressed writes, org-level guard; two gates before merge — zero unstamped buildings, and the org reader agreeing with the seeder on stage
one picker primitive used twice, tri-state headers, the org switcher as a slot on the header, the logo removed, collapsed rail tile, phone sheet, palette links carrying the selection, the invite form taking the org first
3
Gera
every panel and trigger state reachable; the phone touch-target suite green as written; never-empty-by-click; new st-invite-org-first
3
ATL-A tidy
atlas
the duplicate org tile renamed, not deleted — deleting it takes the only click-path to four cross-org lists with it, so it keeps its job and loses its disguise (All records, under Ops; propflowai#7674) — an org becomes a focus leaf, buckets after a divider (propflowai#8048), archived hidden by default
2
Gera
the existing Atlas path test extended: root tile count == real orgs
4
DB-D0 the rule table
dashboard
setRule on every catalogue entry; the missing denominators on the stats payload and the daily row; problem units re-keyed with the building
2
Fede
a unit test per rule on a two-building fixture: {100 %, 4} + {50 %, 120} = 51.6 %
5
DB-D1 the set query
dashboard
six routes re-minting from their own URL; compute split into load + pure fold, run per org; combined / by-org / by-building at the asked grain; caches on the scope key; the four side-fetches on one abort signal
5–6
Fede
ST-46 (a query from the wrong org returns zero) · ST-26; one-context-per-org, merge-rows-not-payloads, search-and-chat-take-the-set
6
ATL-B the read-only tree
atlas
the projector over today's columns, the layout module, the edge overlay, node tiles, dashed pills, legend, accessible table, roots from the selection, the conflict badge on a number held by two buildings
4
Gera
projector snapshot on the bench fixtures; layout test (no overlap, endpoints inside cards); ST-09 / ST-50 in their today shape; ST-35
7
DB-D2 the ⋯ menu and the chart contract
dashboard
the split state in the URL, the menu, the series spec and tone registry, comparison views on series, hero small multiples, table column groups, legend and keyboard, empty states, scope-line chips
4
Gera
removing one entity from a three-set leaves the other two tones unchanged; the headline string identical across all four modes; a rider wherever coverage is partial
8
S-D the consumer sweep
switcher
five client filters, seven server pages, five list loaders, the two URL mirrors deleted, and 45 org-only routes plus three detail loaders moved onto addressed reads and the one-org guard
7–8
Fede
the existing scope suites rewritten and green; no-home-org-reader, addressed-write-gated-by-grant; the migration rider never renders; ST-47 as the addressed-read control
9
S-E navigation and refusers
switcher
selection-preserving links and the back chevron; needs-one prompts that list the set; Vendors reading the set; org-level screens refusing two
2–3
Fede + Gera
three orgs → Only → back → three; org-level-needs-one-org; ST-32
10
DB-D3 the lines
dashboard
history on the set, composed server-side with per-date gaps; rates from the stored daily weights; the per-org roll-up row written by the nightly job with weights, parts and member ids, served only under the member-ids gate. Touches infrastructure — name the risk before it ships (D-14)
3
Fede
a set occupancy line == Σ occupied ÷ Σ units per date and null where a member has no row; a manager granted 2 of 5 never receives the org row; ST-02's shape
11
ATL-C the side lanes, lazily
atlas
people, vendors and parties fed through the detail route; the org focus panel's blocks
2
Fede
detail-route contract test; a person with only a vendor role now appears; ST-34's today shape
12
DB-D4 the rest of the page
dashboard
funnel segments, stacked categories, both-axis small multiples, the four mini tables, the insights dock on merged rows, the Ask Clara bridge, coverage and setting riders, the owner-party empty state
3
Gera
every chart on the dashboard in all four modes against the bench fixtures; no legacy single-id hook left in the dashboard tree; ST-30
13
S-F delete the shim
switcher
the single-id compatibility exports and the old cookie removed; eight pinned suites rewritten
1–2
Fede
the drift test no-single-id-reader green — exactly the 29-file allowlist
14
ATL-D the projector on the record's rows
atlas — waits on the record's steps 1–2
attachments, address heads, relationships, engagements and groups through the dump; the group lane; provenance per key class; one pill per claim; vendor cards; the head-versus-attachment banner
2
Fede
map-dump-stable; ST-13 · ST-16 · ST-17 · ST-18 · ST-33 · ST-37 · ST-45; a fixture with one dangling claim and one headless inbound row
15
ATL-E what-if
atlas — waits on ATL-D
the catalogue items, the overlay, the diff, the ledger count, the recent-changes rail, refusal banners, the test-building gate, the version pill, the URL form
4
Gera
the ledger equals this page's own catalogue literal for every numbered change on the bench fixture; ST-14 · ST-42 · ST-20 · ST-23 · ST-55
Total
switcher 17–21 · dashboard 17–18 · atlas 14
the demo is phases 1, 2, 5 and 7; everything after phase 9 is the debt that makes "future-proof" true
≈ 48–53
One more row that costs no app days: this page's own left rail gets a collapse toggle (BRIEF ask 6) — a button as the first child of the section nav, a class on the shell, the state remembered in local storage, and nothing rendered at all below 901 px so the phone strip is untouched by construction. Fitness function how-rail-toggle-desktop-only.
5 · What Gera and Fede have to decide
Twenty-six of them, each with the default this spec takes if nobody answers. None of these is decided.
D
The question
Default if unanswered
Who
D-01
Owners per phase — Fede the seam, routes, loaders and the nightly job; Gera the visible components
as proposed
both
D-02
Commit on close, or react to every tick the way the picker does today
commit on close; Only is the one-click path
Gera
D-03
What a staff person with no home org starts on
the cookie, else the first active customer by name — never "all"
Gera
D-04
The cap on selected orgs — the brief says "one, two or three"; eight is a guess
Ruled 2026-09-10: eight, counted into the refusal banner
Gera
D-05
Whether a sandbox is ever folded into a default or an "All"
only when named; a sandbox-only roster folds and labels the trigger
Gera
D-06
A customer with exactly one org — a plain label, and the split item disabled with a reason
static label; disabled row, never hidden
Gera
D-07
Old single-building links in bookmarks and messages — rewrite for one release, or forever
one release, then a drift test
Gera
D-08
Who gets a workspace at all — a read-only viewer keeps one; a resident or vendor contact role never grants an org in the switcher
viewer included; a named accessor for the grants read
Fede
D-09
Whether a property owner's owned buildings — the ones their owned_by grants reach, never the runner's rows — appear in the picker under a label, or get their own surface
in the picker, labelled; zero rows until ownership ships
both
D-10
The stage org row's sort key disagrees with the reader (organization.ts:45 vs seed-stage-orgs.ts:67-73) — fix here or in the record's writer
here, one edit, so the switcher can be tested on stage at all
Fede
D-11
A link to a building outside your selection — open with a rider, or re-scope your picker as today's sync does
open, do not re-scope
Gera
D-12
Union or fold. They differ only on a name-keyed number, where a union crosses the wall on a surname
the fold; the shared-surname fixture asserts 2, not 1
Fede
D-13
The per-building split cap — 50 is judgment; the shape that must never fan out is 400 scattered homes
50, one constant beside the org cap
Gera
D-14
The per-org roll-up row is a write-side change to the nightly job (handler.ts:326-341) — infrastructure blast radius. Without it, split-by-org over a daily line is N queries per org on every open
ship it in DB-D3 behind the member-ids gate — after naming the risk
Fede
D-15
Seven named series, or eight — the token file has eight slots
seven, so seven + Other never needs a ninth
Gera
D-16
Does Ask Clara answer over your selection, or over your whole grant, said aloud
the selection; a granted-but-unselected building says so
Gera
D-17
Which denominators may widen the stats payload and the daily row — each is a permanent column on ~400 rows per building per year
the DB-D0 list as written
Fede
D-18
The live browser-agent gauge — staff-only, or a per-building row so a customer sees it
staff-only
both
D-19
The ledger rule: a map edit and a values edit bump different counters, so either a values change logs under a key it did not bump, or Undo does not cover it
count the log row as inserted and the version bump as updated; Fede picks the log key
Gera · Fede
D-20
Two rehearsals the record has no numbered change for — a building's own calendar, and sharing a number with a sibling
offer them, labelled "not on the catalogue" until the numbered rows exist (½ day to add)
Gera
D-21
Found by drawing the brief's own example: a building that rings on a shared number cannot send on it. Either outbound also consults the shared-number tag, or the setup gains an org line
consult the tag — whoever shared the number meant "this is their number" (½ day)
Gera · Fede
D-22
Who the map is for. It ships behind today's staff gate; does a customer's own admin get the same picture of their own org, and where
out of scope here; the projector and renderer are reused as-is when it is answered
Gera
D-23
Two labels the record does not carry — the party's name on a relationship row, and the building's name on a pushed report row
show the id until they land; never a cross-partition read to fill a label
Fede
D-24
Map rendering choices — the phone default, the empty group lane, and whether "moved 0" is drawn
the table on a phone; collapse the empty lane; draw the zero in green, red if it is ever non-zero
Gera · Fede
D-25
Does the invite form belong to this lane or to the settings lane (BRIEF §9)
S-B owns the form; the record's membership step owns deleting the bypass list
both
D-26
The docked-chat and setup-checklist state is platform-wide today ((ws)/layout.tsx:63-68); with a switcher it should be per org
left global, and named here so it is not mistaken for a switcher bug
Fede
Proof: the switcher is proven by ST-24 (one login, two orgs, an explicit pick — today's gauntlet fails it), ST-19, ST-52 and st-invite-org-first; the dashboard by ST-46 (a query from the wrong org returns a count of zero, not a filtered list), ST-26 and the two-building rule fixture {100 %, 4} + {50 %, 120} = 51.6 %; the map by ST-16 / ST-17 / ST-20 (a calendar at the org, at the building, and both), ST-13, ST-14 / ST-42, ST-32 / ST-35 and ST-30, all on the B6 bench fixtures. The standing fitness functions are selection-never-wider-than-grants, selection-never-empty, never-empty-by-click, url-roundtrip-stable, one-context-per-org, merge-rows-not-payloads, addressed-write-gated-by-grant, org-level-needs-one-org, map-dump-stable and how-rail-toggle-desktop-only.
A smoke alarm wired backwards: it beeps while the fire is burning and goes quiet when someone puts it out. Silence is the alarm — either the fire is out, or the battery died, and you are not allowed to guess which.
Yes — it is scripts/portfolio-harness/, merged as PR #7164 on 2026-09-06 (“reproduce every one-channel-one-property assumption on a seeded company”), extended by #7251 (the twelve customer-shape fixtures, the isolation gauntlet, the migration proof) and #7285 (the test line). It is a fake company written through the real writers into a real data layer, and twenty-two cases drive today’s production code against it. We are using it as the test suite, and the bench below says exactly where it already covers our stress catalog and where it does not.
Read the polarity before anything else — it is the one thing in this directory that misreads.npm run test:harness:portfolio is green while the product is broken. Every case asserts “the one-channel-one-property failure is still there, for the reason we wrote down”, so a green run means the bug reproduced, not that the product works. The twin, :red, is the same twenty-two cases written as assertions of correct behaviour — and it genuinely fails, 22 of 22. That inversion is deliberate: a case that stops reproducing, because someone fixed the product or because the case rotted into a typo, fails loudly instead of passing silently (scripts/portfolio-harness/README.md:29-39). The mechanism is positive-controlled, not assumed: a mutation control breaks the verdict assignment on purpose and confirms a case that stops reproducing comes back WRONG-REASON (README.md:162).
Figure A7. The Seeded Company. Eight scattered-site homes across three invented towns, every one of them listing the same line, the same mailbox and the same calendar; a second company with its own two homes beside it; the twin-digit phone that identifies a different human at each company; and one number listed nowhere. Solid = the routing the bench drives · dashed = the collision the case is built on. Homes at scripts/portfolio-harness/seed/company.ts:103-112, the channels at :52,54,56, Other Co at :62-63, 115-118, the twin digits at :139-147, the two vacants at :130.
Read it at
Fede built a pretend company with eight houses that all share one phone number, one mailbox and one calendar. Then he wrote twenty-two little tests that each say “this is still broken, and here is exactly why.” When the tests pass, the product is still broken. When they fail, someone fixed it — or the test broke.
The harness builds a fake company — Test Portfolio Co, eight rented houses spread over three towns — and gives the whole company one phone number, one mailbox and one calendar, which is exactly how a scattered-site landlord really runs. Then it puts a second company next door, Other Co, with two houses of its own, and deliberately gives one of Other Co’s residents the same phone digits as one of Test Portfolio’s. That collision is the isolation fixture: if the code ever confuses the two people, the bench sees it.
Twenty-two cases then run today’s real code against that company — the ring, the tools, the text lane, the mail lane, the outbound sender, the calendar, the thread, the work order. Every one of them reproduces a failure and writes down why. The confusing part is the scoreboard: the everyday command is green while the product is broken, because “green” means “the bug is still there for the reason we recorded”. The second command flips every case round to assert the right behaviour, and all twenty-two fail. That failing output is the reproduction environment.
The suite seeds two organizations through production writers (saveProperty, saveUnits, saveTenants with the spine stamp and occupancy signals, savePropertyKnowledge, savePropertyLeasingSettings) against the json backend with the data directory pointed at a fresh temp directory; nothing about the code under test is mocked, and the single declared stub is the token endpoint used to model refresh-token rotation (README.md:45-51, 138-141). Every fixture phone uses the repo’s impossible area code convention +1000… — a North American area code can never begin with zero, so no fixture number can be assigned or dialled — and every domain is .invalid, both enforced file by file by a drift test (README.md:60-64). The known limitation is itself a finding: the json backend cannot store an organization row at all, so the company exists only as Property.organizationId, which is how every runtime reader resolves a customer today anyway (README.md:72-77).
Polarity is the contract. The parent suite carries one polarity for all twenty-two cases; the isolation gauntlet chooses it per row from a four-value vocabulary — PASSES (a red means a regression), FAILS (a red means someone fixed the product, or the row rotted), CANNOT-EXPRESS (the hit count is the verdict), NOT-RUN (gauntlet/README.md:16-28). Both rest on falsifiability: every verdict must be able to stop being true, and a guard resolves local bindings through the TypeScript syntax tree and grades the expanded expression, because both defects that actually shipped hid the offending term in a const one level up where a line-level check cannot see it. Three review rounds each found a permanent green in a red costume; the guard now walks the cases, the fixtures, the gauntlet and the migration proof, and a reach test drives the three registries themselves so a new probe it cannot read fails there rather than shipping unguarded (README.md:163).
The seeded company, in rows
Test Portfolio Co
org_portfolio_harness — 8 scattered-site single-family homes across 3 invented towns, every one listing the same line, the same mailbox, the same calendar and company-level office hours seed/company.ts:103-112
its one front door
line +10002000142 · mailbox leasing@testportfolioco.invalid · calendar shared-tours@testportfolioco.invalid · office line +10002000140 · emergency +10002000141seed/company.ts:52-59
two homes vacant
G and H are seeded on the market with a fresh listings and rent-roll heartbeat — without real inventory the leasing-injection case could only ever observe an empty block seed/company.ts:130 · README.md:66-70
Other Co
org_other_harness — 2 homes, its own line +10002000188 and mailbox, and two people whose numbers are the same digits as Test Portfolio residents seed/company.ts:115-118, 139-147
the collision
+10002000102 identifies Fixture02 Bramblewick at Test Portfolio and Fixture07 Grimsby at Other Co — reused on purpose; that pair is the cross-company isolation fixture results/latest.md:28
written how
through production writers against the json data layer in a fresh temp directory — nothing is a mock of the code under test’s inputs README.md:45-51
safe how
every property isTest: true in an invented org; every credential deleted before any app module loads; an outbound kill switch armed even though every case is a read or a local write README.md:130-141
The twenty-two cases
Ids and titles are taken verbatim from results/latest.md:9-30 and cases/*.ts. The polarity column is uniform, so it is stated once instead of repeated: all 22 are RED — the declared failure reproduced, for the declared reason; the CI-safe suite is green on all 22 and the red view fails on all 22 (results/latest.md:43; README.md:156). The column below carries the case’s own kind vocabulary instead (cases/types.ts:47-58). Two ids name the PMS vendor and are written here as <pms> per this page’s naming rule; the literal ids are at the cited lines.
#
Case (file:line)
Surface
What it asserts
What it proves about the architecture
1
voice-ring-unmapped-company-linevoice.ts:59
voice-ring
A company line with no home behind it still names its company.
There is no number→company resolver. The company is emitted only through a building, so a company front door cannot name its own company — and the empty building id is the live error that started this.
A number shared by eight homes does not silently pick one.
The number→building cache is one key to one value. Eight writes leave one arbitrary winner — whichever home the roster returns last — with no duplicate detection anywhere.
A resident of home B is recognised on the shared line.
The caller is dropped to “unknown” whenever their home differs from the one the dialled number resolved to. Recognition is gated on a building, not on a company.
A voice tool can be called with a company but no building yet.
Six leasing tool schemas hard-bind the building id to a ring-time variable and none exposes a company-level reference; an empty building id is a hard failure the moment the ring emits it.
5
voice-tool-context-silently-unboundvoice.ts:240
voice-tool
An unbound tool call signals, rather than guessing.
It returns an undefined building with no error and no company fallback; the resolver has no company field at all, so the handler downstream is left to guess or fail.
“Search within the caller’s company” works before a home is known.
The company is derived only through a building, and the conversation row has no company field — so the state “company known, home not yet” cannot be written down anywhere.
7
voice-create-work-order-no-propertyvoice.ts:310
work-order
Maintenance intake on a company line lands somewhere.
Creation refuses outright without a building id. There is no company-level “home not yet known” bucket a work order can be filed into.
8
knowledge-no-company-tiervoice.ts:364
knowledge
A company fact is asked for once, at the company.
Knowledge is keyed by building only, with no company variant and no fallback chain, so the same company fact is duplicated onto all 8 home rows and can only be asked for by building.
9
voice-office-hours-are-one-homesvoice.ts:394
voice-ring
Hours, transfer number and emergency number are the company’s.
All three are read off the single arbitrarily-bound home, so a shared line inherits one home’s hours and numbers for every caller. There is no company record to read them from instead.
Both vacant homes are offerable on the company line.
Availability is built for exactly one building id — the arbitrarily-bound home — so the company’s other vacant home is invisible for the whole call. This is why two homes are seeded vacant.
11
sms-shared-number-last-writer-winsmessaging.ts:24
sms-inbound
Eight buildings claiming one number raise an ambiguity.
Same one-key-one-value cache as the ring: eight claims, one silent arbitrary winner, no ambiguity signal. Reorder the roster and the answer changes.
A shared mailbox does not resolve to the first match.
The matcher returns the first building whose inbound addresses contain the recipient, with no second-match check — silently, unlike the other mail lane.
A mailbox claimed by two homes routes to the company.
The other mail lane refuses outright on a multi-claim and drops the message. Correct for a cloned test building; wrong for a company that genuinely has one mailbox and many homes.
The address in a lead subject is used for routing.
The subject is never passed into the parser, so the one deterministic address signal is discarded before anything can route on it. Redundant on a one-building mailbox; the only routing key there is on a company mailbox.
An unmapped building silently falls back to one global line — a different number from the one the caller knows. Three named senders (prospect cadence, tour confirmation, post-call receipt) all go through this one resolver.
The outbound map is the inversion of the inbound map, and inverting a many-to-one map is not a function: 0 of 8 homes resolve the company line, and the other seven get the global fallback.
One shared calendar credential survives its own refresh.
Rotation is persisted onto the single building row that triggered it; every sibling still holds the now-invalid credential, so the next home to sync a tour fails. There is no company-level calendar record to write it to.
Thread matching hard-scopes on the destination building, so once a thread is bound to a home, the next inbound on the shared line opens a second thread. Same person, same channel, same company — only the building differs, and that alone excludes.
A work-order write can name which PMS database it belongs to.
The queue envelope carries the account id, the handler never forwards it, and the handler context type has no field to carry it in — structurally impossible to reach the runner. With two PMS databases live, the write cannot name its own.
Nothing of the other company is spoken on this line.
No leak on this path — but nothing anywhere asserts “this channel belongs to this company”. The company-scoped lookup facade has zero production call sites, so today’s separation is a side effect of which arbitrary building the number resolved to.
The wall between two companies is a wall, not a coincidence.
No content leak — but separation rests on a building-equality gate written for two buildings in one company, which fails open whenever either side’s company is unknown. The customer stamp is optional, so “unknown” is a normal state.
Cross-company: a line listed on no home’s row bound the call to the other company’s home — the code’s own comment calls it a known fail-open. It cannot simply be closed, because outbound legs would break without it.
Six of the twenty-two refuse to report a verdict at all unless their own precondition holds (results/latest.md:45). Six more scenarios exist for a live robot call on a real line — written, parsed and schema-validated, and none has been dialled: every called number is a literal placeholder, and the validator refuses any scenario that names a real line (README.md:81-89; results/latest.md:36-41).
The isolation gauntlet — T1 to T14
A second bench, two seeded companies with one building and one line each plus a third onboarded at runtime, for the question “does the wall hold?” Its polarity is chosen per row, because some of these pass today and some fail, and asserting the same thing about both would make half the suite meaningless (gauntlet/README.md:16-28). At 0c2da03a86: 6 PASSES6 FAILS2 CANNOT-EXPRESS, all MATCHED; the red view is 8 failed / 6 passed, which is exactly the six FAILS plus the two CANNOT-EXPRESS — the polarity contract’s own positive control (gauntlet/README.md:30-34).
T
What it checks
Verdict
Reading
T1
One phone at two companies mints two people; each company’s lookup returns its own
PASSES
The identity spine already walls per customer. Keep it.
T2
Company B’s conversation candidates contain no company A thread
PASSES
Correct outcome, but by a filter applied after a global read — the partitions are still read.
T3
Staff of company A texting company B’s line gets no staff tier there
FAILS
The text lane fences on the customer; the ring’s identity call still passes no customer at all.
T4
A caller on A’s line naming B’s building gets not-found
PASSES
Scoped roster, refusal when the customer is unresolvable, and a refused cross-building re-point.
T5
A dialled number mapping to no building refuses to adopt the caller’s
FAILS
The fail-open block is present, self-labelled, and still adopts. Same defect as case 22.
T6
A line bound to a company resolves to that company with no building
CANNOT-EXPRESS
The row kind does not exist. The one resolver returns a building id; the customer is only ever derived through a building.
T7
A batch person read returns a person minted at A who holds an active role at B
FAILS
Two readers disagree about the same person on the same customer — one checks reachability, the batch one checks origin.
T8
A human on staff at two companies gets a deterministic, user-selected active company
FAILS
The login lands wherever the first role row happens to be; an explicitly selected active customer is absent.
T9
A person’s own calendar is readable from another company as free/busy only
CANNOT-EXPRESS
The row kind does not exist. Calendars are building-attached; the field on the login row is consumed by no scheduler.
T10
A cross-company identity merge is refused
PASSES
Refused, with a same-customer merge succeeding in the same run as the control — so duplicates minted per customer are permanently unhealable.
T11
An opt-out via A’s number suppresses B’s outbound to the same phone
PASSES
Consent is platform-keyed by phone, by decision, not by defect — and it is why “reset from scratch” wording is unshippable as written.
T12
A platform admin impersonating uses the anchor building’s customer, never the staff sentinel
PASSES
The anchor rule is what stops a staff session erasing every thread or writing into the wrong customer’s partition.
T13
A cross-customer request returns 404 byte-identical to missing; list reads empty
FAILS
The envelope is per-route, not at the store; the single-row reader takes no customer. The registry still carries 22 legacy markers and zero of four stages have shipped.
T14
Adversarial: onboard a third company with a company-level line and two buildings, rows only
FAILS
Onboarding succeeds with zero schema change; the line half fails — the company line silently becomes one home’s line, chosen by roster order.
Source: scripts/portfolio-harness/gauntlet/results/latest.md:9-22, evidence per row at :26-115. T6 and T9 are the two rows our design notes cite as CANNOT-EXPRESS — a red T6 or T9 would not be a bug, it would mean the row kind shipped and the real assertion now has to be written (gauntlet/README.md:23-28).
The twelve fixtures — F01 to F12
Twelve customer shapes, each a small YAML file: companies, groups, relationships, buildings, lines, mailboxes, calendars, staff, residents, vendors, knowledge, settings, PMS accounts, and an expect: list. Each expectation is graded against six columns — five candidate designs plus “today” — giving 74 expectation rows × 6 = 444 cells: 18 PASS · 0 FAIL · 283 CANNOT‑EXPRESS · 143 NOT‑RUN (results/portfolio-fixtures.md:86-91).
F01
Solo Building Co — one building, its own line and calendar. The control: the shape today already fits.
F02
Owner is not the manager — one building, an owning company and a managing company, a scoped grant.
F03
Centralized Co — the merged 22-case seed extended with two apartment buildings; one number, one mailbox.
F04
A rotating leasing pod — three agents with their own calendars, plus the alternative of one company calendar.
F05
Hybrid cluster — three buildings sharing a line through a group, two with their own, one mailbox for all five.
F06
A floating assistant on staff at two customers, with one external calendar behind both roles.
F07
A third-party manager with three property owners, per-owner reporting, and a per-building “what may be pooled” consent.
F08
An owner-operator acquires building #2 — phased, like F09; its data-layer half is the other row that scores PASS, beside F01’s control.
F09
Two companies, same owners, then an acquisition — kept as two with a roll-up, or truly merged.
F10
Knowledge and region overrides — company greeting and fees, regional managers over building groups.
F11
Scattered site at scale — 400 homes, generated from a generate: block, never hand-listed.
F12
A joint venture — two owners with 0.60/0.40 shares, one operator, and a second operator that must be refused.
What a fixture buys over a hand-written case. A case is one company, one assertion, one answer. A fixture is a customer shape stated once and graded against every candidate design at once, so “which design survives this customer?” is a column, not an argument — and 283 CANNOT-EXPRESS cells are a measured answer, not an opinion. The generate: block means F11 cannot rot away from the 400 it claims, because the loader expands it. The honest limit: the loader parses, validates and expands — it does not seed (fixtures/portfolio/load.ts:9-11); only five of its probes write rows at all, and all of them through one building writer (fixtures/portfolio/probes.ts:120-136, called at :198, 303-307, 416-417, 470-472, 500). So F02–F07, F09, F10 and F12 are shapes on paper today, and the twenty-two cases are the only thing with runtime behind it.
Where his suite meets our bench
Our stress catalog (A6) numbers its cases ST-*. Some of them cite his case by id as the “today” half they intend to flip from RED to GREEN; others cover the same ground without naming it, and that is our judgment, marked as such. Everything ST-* is proposed; everything in his columns is what runs today.
His case
Our id
Relation
Same ground
voice-ring-shared-number-last-writer-wins
ST‑01 · ST‑13 · ST‑79
cited by id
The company line rings once and answers for three; the hybrid where one building keeps its own; life safety before the home is named.
voice-ring-unmapped-company-line
ST‑52
cited by id
A building with no customer stamp — three polarities, three rows.
voice-ring-resident-of-home-b-discarded
ST‑05a
cited by id
A resident of building two calls the company line.
knowledge-no-company-tier
ST‑07
cited by id
Knowledge asked before the home is named — and refused when it is a building-only fact.
voice-office-hours-are-one-homes
ST‑45
cited by id
Locking a key at the company over live building rows.
voice-leasing-context-one-home-inventory
ST‑72
cited by id
Four hundred homes and a town with seven — the two-step area question.
No ST‑* row cites any of these five by id. The tool-schema and PMS-account halves are a real hole in our catalog, not a covered case. Named here so it is not read as coverage.
What his suite does not yet seed — and what it already does
Checked shape by shape against the twelve fixtures and both benches, so “missing” means missing and not unlooked-for.
The customer shape
In his suite?
Ours
What we found
One person on staff at two customers
covered
ST‑124
Already his. F06 states it as a fixture, and the gauntlet executes it for real: T7 and T8 mint a person with a role at each of two companies and drive both the batch read and the login. We should reuse this, not rebuild it.
A vendor engaged by two customers
not seeded
ST‑34 · ST‑36
Exactly one fixture seeds a vendor at all (F01, one maintenance vendor at one company, no building carve-out), and no case in either bench exercises a vendor. F07 names a shared vendor roster only as a pooling consent, never as rows.
A building that moves customer
adjacent only
ST‑125
F07 moves a building between property owners inside one manager (F07.yaml:70) and F09 merges two companies — neither moves a building across the customer wall, which is where a staff subset could leak to the old customer.
A facility with zero units
not seeded
ST‑129
Every seeded building has units — single-family homes with one, apartments with 12 to 120. Nothing seeds an office, a warehouse or a dentist’s suite, so “which unit?” is never asked of a building that has none.
A calendar added later, for one building
adjacent only
ST‑17
F08 and F09 are the two phased fixtures; F08’s later calendar arrives with a new building and F09’s phase is a company merge. Neither is Gera’s example: an existing building gaining its own calendar while the company keeps the shared one. F05 has the mixed shape but no phase and no probe.
Staff invited before the first building
not seeded
ST‑123
Every fixture and both benches start from buildings; even the adversarial T14 onboards its third company with two homes. The invite-with-zero-buildings path is never reached, and that is the path today’s invite form cannot express at all.
How to run it
Verbatim from scripts/portfolio-harness/README.md:17-20:
Command
What it does
npm run test:harness:portfolio
CI-safe: passes while every case still reproduces
npm run test:harness:portfolio:red
THE RED VIEW: 22 failures, one per broken assumption
npm run test:harness:portfolio:list
what is in here, including the L3 scenarios
npm run test:harness:portfolio:stamp
re-stamp the fixture manifest + hash lock
Eight more front doors exist beside those four — :fixtures, :gauntlet, :migration-proof and their :list / :red twins (package.json:79-90). Only the CI-safe suite and its :list run in CI, on their own lane keyed to the 20 production surfaces its cases drive (.github/workflows/portfolio-harness.yml:97,101); the gauntlet, fixture and migration-proof results are regenerated by hand and committed. The harness directory is deliberately excluded from the required unit-test lane, so a red-by-design suite can never be folded into it (README.md:161).
Green means broken. The everyday command passes only while all 22 failures still reproduce for their recorded reason; the red twin is the reproduction environment (README.md:29-39).
It is a real company on the real data layer — production writers, a temp-directory store, every credential stripped, and one declared stub at the token endpoint (README.md:45-51, 138-141).
Every number is impossible to dial. The +1000… convention beats the fiction block, because an area code can never begin with zero (README.md:60-64).
The gauntlet chooses polarity per row, so its 6 / 6 / 2 split is a claim about the product, not about the suite (gauntlet/README.md:16-34).
The fixtures are shapes, not runs — the loader parses and expands but does not seed, so 283 of 444 cells are CANNOT-EXPRESS by construction (load.ts:9-11; results/portfolio-fixtures.md:86-91).
What this proves: the committed result file records 22 of 22 L2 cases reproduced their declared failure mode, with 6 L3 scenarios written, parsed and validated and 0 dialled (scripts/portfolio-harness/results/latest.md:43). That number is trustworthy because the mechanism it rests on is positive-controlled rather than assumed: the harness/portfolio-wrong-reason control breaks the verdict assignment on purpose and confirms a case that stops reproducing comes back WRONG-REASON, and the harness/portfolio-falsifiable control replays byte-exact copies of the two verdicts that really shipped and requires both to be reported by case id (README.md:162). Six of the twenty-two additionally refuse to report a verdict unless their own precondition holds (results/latest.md:45).
What this pane could not check. No suite was run for this page — every count above is read from the result files as committed, and each was generated at a different commit than today’s tree: the 22 cases at 7c400a035e, the gauntlet at 0c2da03a86, the fixture grid at 835dc310fc, against a checkout at 8941d763fa. So the numbers are the last recorded run, not a live one. Also unverified here: whether the red view still fails 22 of 22 at today’s commit; whether the live-call leg is any closer to dialling than the blocker recorded in the results table; and whether the seeded company could survive a real inbound call at all, since it exists only in a per-process temp directory and a real call reads the cloud store.
Moving in is a delivery. The customer’s records arrive in crates, and PropFlow must write on every crate whose house it belongs to and which door the answer goes back out of. Today it writes the room, inherits the house from whoever signed for it, and leaves the door blank.
If you only read this: three customer databases — two connected, one still being onboarded — and the proposed architecture holds for all three — but only because it moves two things the code does not have yet: a customer-scoped key so a re-import lands on the building that exists, and the outbound door as a row written at onboarding rather than by hand, one building at a time. The import writes neither. Everything below carries the file:line that proves it.
Figure A8. How a Customer Gets In. Three doors on the left, one write path across the middle, and one fork at the bottom. Solid = the path a real import takes · dashed = a reference resolved elsewhere · the fork is drawn below the path because nothing on the path writes it.
Read it at
Getting a new customer in means copying their buildings into PropFlow. Today the copy names the building but not the company, and it never says which door to answer through. A person has to fill that in by hand, one building at a time.
When a new customer joins, we copy their buildings in. The copy asks their system for a list, then, for each building on the list, looks for a row we already have. It looks two ways: first by the id their system gave it, then by the name and the street. If it finds one it updates it; if not it makes a new one.
Two things go wrong. The second look reads every customer’s rows, not just this one’s — so two customers with a building on the same street can collide. And the copy never writes down which of the customer’s databases an answer should go back to. That one blank is deliberately safe: with it blank, nothing leaves at all. But it means a customer is not really moved in until a person has typed one command per building.
Onboarding calls the PMS with a per-person credential and upserts one skeleton building per record (import/route.ts:245-273, 359-369). Idempotency is the PMS pointer first, then a normalized (name, address) fallback built over a bare, deployment-wide roster read (:70-78, :284-297, :328-330; dynamo/property.ts:154-165). The customer stamp is the importer’s session org, blanked when the importer is our own staff (:306-315), and preserved on re-import (:160). Units and people arrive on the nightly pass; the person writer refuses a building with no customer (dynamo/property.ts:663-671). The outbound account is resolved per building and fails closed (resolve-account.ts:50-69), but nothing on the import writes it — one operator script does (set-appfolio-account.ts:152-161).
Two independent stores answer “which database”: PROP#<id>/META.pmsAppfolioAccount on the business table (types.ts:3613-3649) and PK=APPFOLIO_ACCOUNT / SK=ACCOUNT#<slug> on the jobs table (multiTenantConfig.ts:24-32, 242-246), each written by its own operator script and neither by any runtime path. A building may therefore name a slug that has no config row: the app-side resolve passes and the runner-side context lookup fails. Below both, the browser runner itself still resolves one deployment-wide base URL regardless of the account the caller resolved (l4-core.ts:3199-3207). The proposed shape collapses all three onto one dated attachment placeable at customer, group or building, nearest wins, with a refusal when a write’s instance is not the building’s (DF §3.2 L223, §8 L719 “E17”).
The three accounts, as shapes
Slugs are the code’s own (resolve-account.ts:32-35; types.ts:3638; provision-western-slope-proto-property.ts:180). Building counts and the answering arrangement are not in the code — they come from the design record, and where the record disagrees with itself that is said here rather than smoothed over.
1 · the founding customer jpco
Two buildings. A number per building, a mailbox per building, a calendar per building — one of the two carries three calendars (the manager’s, a floating assistant’s, the building’s). Nothing is centralized; the record calls it the control shape, where every setting resolves at the building or falls to a constant (DF §8 S1). One building is run day to day by a third-party manager whose staff sit inside this customer’s wall. The same database also holds our own test bench under a different PropFlow customer (rehome-org-jpco-core.ts:67-73) — so a database is already not one customer.
2 · the first external client situsgroup
Somewhere between fourteen and sixty-two buildings, and the record does not settle it. Seventy-one source records; the stated identity is records minus eleven zero-unit shells, three placeholders and one do-not-use entity (DF §8 S2, §11 L808) — which is fifty-six, while the prose beside it says about sixty-two and two other passages say fourteen and about thirty. Unreconciled, and the arithmetic does not close. One public number with four menu branches, each forwarding to its own line; two emergency lines split by asset type. One mailbox at the company. No leasing calendar at all today — the product has to create it. Three in-house leasing people answer; maintenance intake goes to an outside call centre at all but two buildings. Ownership is thirty-three investor entities over one hundred and forty-two links, with forty-five buildings in two or three at once.
3 · the scattered-site operator westernslopepm
Fourteen source records, sixteen homes, four towns (DF §8 S3) — and the record says plainly that the export itself was never read. One number, one mailbox, one calendar, all at the company; maintenance stays with an outside call centre. Part of its stock is owned by customer 2. In the code today it is not a customer at all: one prototype building marked test, its own sandbox company row written by a raw put, and the sixteen homes stored as unit rows under that one building, fed from a public listings page rather than the PMS (provision-western-slope-proto-property.ts:126-199, 201-226).
The code and the record disagree, and the code is behind. Four places in the code still say two databases are live (resolve-account.ts:9-10; types.ts:3617-3619; set-appfolio-account.ts:4-5; l4-core.ts:3180-3181, 3408-3410). The third slug appears in the code only as a listings URL and a docs entry — there is no property with that account, no credential, no config row. So “three accounts” is true of the business and not yet of the database, and the third one’s ingestion has never run.
How data gets in today, in order
#
Step
What actually happens (file:line)
Fails closed?
1
The customer row
Approving a signup mints an id and writes no row; the customer interface has no save — so the customer exists as a string before any building does (interfaces/organization.ts:15-29; approve/route.ts:104-107).
no
2
The credential
Stored per PERSON, per PMS — PK=PMSCRED#<userId> SK=APPFOLIO. The building points at that person through ownerId, and a building without one is skipped by the sync entirely (dynamo/pms-credential.ts:9-11; types.ts:3753-3759).
skips
3
The buildings
One skeleton row per record the credential can see, upserted through saveProperty. Units, people and leases are explicitly NOT created here (import/route.ts:22-24, 359-369).
n/a
4
The match
Primary key = the PMS pointer; fallback = normalized (name, address). Both indexes are built from a bare roster read that returns every customer’s buildings (import/route.ts:70-78, 284-297, 328-330; dynamo/property.ts:154-165).
no
5
The customer stamp
New rows take the importer’s session customer; an existing row keeps its own. When our own staff run it the stamp is deliberately left undefined and a backfill script is expected to fix it later (import/route.ts:155-160, 306-315).
defers
6
Ownership guard
A matched row owned by someone else is refused — but only when it already has an owner, and a platform admin bypasses it by design (import/route.ts:187-192, 344-357). An unowned legacy row is claimed silently.
partly
7
Staff reach
The same call pushes every imported building id onto the importer’s own reach list, and persists their deselections as a denylist (import/route.ts:389-405). Ingestion is also a permissions write.
n/a
8
Units and people
The nightly pass writes them. The person writer throws the whole batch if any building has no customer, and throws again if a person has neither phone nor mail (dynamo/property.ts:663-671, 691-696). This is the one place the wall is enforced at the store.
yes
9
Vendors
Swept once per customer, first building wins, and a building with no customer is skipped without claiming it (appfolio-sync/handler.ts:1511-1544). The vendor card itself carries no customer and is refused without a PMS link (types.ts:5613-5626; vendor-write-invariants.ts:66-70).
partly
10
The door out
Every write resolves the account from the building and throws when it is missing — there is no fallback subdomain (resolve-account.ts:50-69), and the send path turns that throw into a named refusal rather than a stack trace (appfolio/message-sender.ts:83-108). Nothing in the import writes the field. One operator script does, dry-run by default, one building per run (set-appfolio-account.ts:1-38, 152-161).
yes
Why step 10 is a fork and not a setting: a deployment-wide default here would send one customer’s renewal offer, move-out draft or conversion into another customer’s database, with no error at all (types.ts:3617-3622). It has happened in a smaller form — a test building was seeded pointing at the founding customer’s real database and a harness dispatched a real request into it for a fake tenant; the fix was a second operator script that repoints a test building at a recording writer (set-property-pms-sandbox-mode.ts:1-30).
There is a second door, and it is undocumented as a path. One live customer has no PMS connection at all: its buildings and homes were written by a one-off provisioning script, and its rent roll is a public listings page re-read on a cron that overwrites the unit rows every fifteen minutes (provision-western-slope-proto-property.ts:169-182; public-listings-sync.ts:228, 340). That door writes units and never people, never vendors, and never an account — so it produces a building that can answer a caller and cannot complete a tenancy. It is the shape a fourth customer without a PMS would arrive in, built once for one prototype rather than designed.
The figure, word by word
So the picture and the rows never drift apart, here is what each box in Figure A8 actually names.
the PMS
three databases behind one login — the reason a deployment-wide default is a cross-customer write and not a convenience types.ts:3617-3622
a listings page
the door for a customer with no PMS connection: a public page read on a cron, with the rent roll as its source of truth provision-western-slope-proto-property.ts:169-182
the credential
one encrypted row per PERSON per PMS; the building points at that person, and a building without one is skipped by the sync dynamo/pms-credential.ts:9-11 · types.ts:3753-3759
the import
one skeleton building per record; units, people and leases are deliberately not created here import/route.ts:22-24
the match
the PMS pointer, then normalized name-and-address over a roster that spans every customer import/route.ts:70-78, 284-297
the building row
the one hook — number, mailbox, calendar, credential pointer and the account field are all columns on it types.ts:2934, 3060, 3061, 3637, 3759
the customer stamp
optional on the type, inherited from the session, blank when staff imported, preserved on re-import types.ts:2887-2890 · import/route.ts:155-160
the rows
people and occupancies — the only writer on this path that enforces the wall at the store, by throwing the batch dynamo/property.ts:653-671
the fork
the account field: read by every write path, written by no import, set one building at a time by an operator resolve-account.ts:50-69 · set-appfolio-account.ts:152-161
Fails closed, or silently guesses
The difference matters more than the count. A path that fails closed produces a named refusal an operator can fix; a path that guesses produces a row that looks correct and is not. Both kinds are on the ingestion path today.
The moment
Behaviour
Where (file:line)
The building names no database
FAILS CLOSED throws a non-retryable error at every write path; the send leg turns it into a named refusal that says which script fixes it, rather than a stack trace on a message that looked delivered
FAILS CLOSED the whole batch of people throws before any row is written, so a partial write cannot leave orphans
dynamo/property.ts:653-671
A person has no phone and no mail
FAILS CLOSED refused as un-stampable rather than written without an identity
dynamo/property.ts:691-696
The account is set on a building whose source is a different PMS
FAILS CLOSED the script refuses it outright as a paste error, and dry-runs by default
set-appfolio-account.ts:20-22, 124-141
Two customers have a building on the same street
GUESSES the fallback index is built over every customer’s rows and returns whichever it holds; the import then writes the pointer onto the other customer’s building
import/route.ts:284-297, 328-330, 160
Our own staff run the import
DEFERS the customer stamp is left undefined on purpose and a backfill script is expected to assign it later
import/route.ts:306-315
A matched building has no owner yet
GUESSES the ownership guard only fires when an owner is already set and differs; an unowned legacy row is claimed by whoever imports next
import/route.ts:344-357
A second customer wants a display code the first already uses
REFUSES WRONGLY the uniqueness check runs over every customer’s buildings, so a legitimate second customer is turned away; the fix is pinned in the code as a TODO waiting on the wall
dynamo/property.ts:199-206
A vendor exists only in PropFlow
REFUSES WRONGLY a vendor with no PMS link cannot be written at all, so a customer with no PMS — or one whose plumber is not in it — has no way to record a vendor
vendor-write-invariants.ts:62-73
Two buildings carry the same number
GUESSES whichever the roster scan reaches last wins the cache entry, with no log and no test, and it can flip on the next refresh
phone-lookup.ts:300-306
The two stores that must agree
“Which database” is answered twice, in two tables, by two operator scripts, and by nothing at runtime. A building can therefore name a database that has no configuration behind it: the first read passes and the second fails, far away from the import that created the building.
the field on the building
a slug and a subdomain on PROP#<id>/META; requires the building’s source to be that PMS; the only writer is one operator script types.ts:3613-3649 · set-appfolio-account.ts:152-161
the config for the slug
a row on a second table under a fixed partition, holding the session context and the template names the runner needs — explicitly read-only from application code, written only by its own seed script multiTenantConfig.ts:24-32, 242-246
the runner underneath
still resolves one deployment-wide base URL whichever account the caller resolved — named in the code as a scoped follow-up, not a closed gap l4-core.ts:3199-3207
what the proposal replaces all three with
one dated attachment placeable at the customer, a group or one building, slot = system role, nearest wins, and a write whose instance is not the building’s refuses DF §3.2 L223 · §8 L719
The verdict — does the proposal hold for all three, and for the next one
Columns 1 · 2 · 3 are the three accounts above, and each cell answers one question only: “does the PROPOSED architecture hold for this customer’s actual shape?” — today’s behaviour is the second column and the two tables above, not these dots. HOLDS the proposed rows express it as they stand and a case pins it · NEEDS expressible, but account-specific work must land first — named in what it needs · NO the record has no row and no case for it yet · n/a not this customer’s shape.
Concern
What happens today (file:line)
1
2
3
What it needs
Proved by
Which database a write lands in
Resolved per building, fails closed, no default (resolve-account.ts:50-69) — but written only by hand (set-appfolio-account.ts:152-161), and a second store must agree (multiTenantConfig.ts:24-32, 242-246). The runner below still takes one deployment-wide URL (l4-core.ts:3199-3207).
HOLDS
HOLDS
NEEDS
The credential and account as one dated attachment at customer, group or building — nearest wins — with a refusal when a write’s instance is not the building’s (DF §3.2 L223; §8 L719). Account 3 has no building to hang one on until the re-key lands.
ST-37 · ST-119 · ST-88
One number, many buildings
Number → one building, in a source file plus a last-writer-wins cache (phone-lookup.ts:140-148, 300-306). Two buildings on one number degrade to one, silently.
HOLDS
NEEDS
NEEDS
The claim at the head names the customer, not the building. Account 2 needs the four menu branches as function rows under one head — and the record marks that branch-to-line mapping as an ASSUMED shape, not a read one. Account 3 needs the re-key first.
ST-01 · ST-13 · ST-38
One mailbox, many buildings
The mailbox is a column on the building (types.ts:3061); the matcher takes the first match in scan order on one lane and refuses more than one claim on the other.
HOLDS
HOLDS
NEEDS
The mailbox as an attachment on the customer, with the building named by the lead and bound, or refused when it is outside reach. Account 3 needs the re-key first.
ST-03a · ST-03c · ST-11a
The calendar
One calendar per building with its secret on the building row; the first token refresh writes the rotated value back onto that row (types.ts:3060; sync-tour.ts:141-178). A company calendar has nowhere to live but a placeholder building.
NEEDS
NEEDS
NEEDS
The calendar as an attachment on the customer or on a person, one token in one place. Account 1 needs the person-level attachment plus a coverage schedule for its floating assistant; account 2 has NO calendar to migrate — the product must create three, plus a round-robin; account 3’s single company calendar moves with the re-key.
ST-04a · ST-16a · ST-17
Staff, and who can see what
The invite is a checkbox list of buildings; the customer is derived from them and refused when they disagree (admin/users/route.ts:204-226; types.ts:12659). The import itself writes onto that same list (import/route.ts:389-405).
NEEDS
HOLDS
NEEDS
The job attached at the customer with an optional building subset; “all buildings” is the absence of a subset. Account 1 also needs the third-party manager’s staff to sit inside its wall through an accepted running relationship, not a hand-typed list.
ST-123 · ST-125 · ST-126
Vendors
The card carries no customer and cannot be written without a PMS link (types.ts:5613-5626; vendor-write-invariants.ts:66-70); reach is a per-customer link row with an empty-array “all” sentinel (types.ts:16564-16584); the sweep is one per customer, first building wins (appfolio-sync/handler.ts:1511-1544).
HOLDS
NEEDS
NEEDS
An engagement scoped to the customer, a group or one building, plus a directory card that belongs to no customer. Account 2 needs the company roster with per-building overrides and a property owner who declines to share; account 3 has no vendors ingested at all and its maintenance intake is a route, not a vendor.
ST-32 · ST-33 · ST-35
Re-import and duplicates
Primary key is the PMS pointer; the fallback is (name, address) over an unscoped roster (import/route.ts:70-78, 284-297, 328-330). Name is in the key, so a renamed record is a new building; the roster is global, so another customer’s row can be the match.
NEEDS
NEEDS
NEEDS
The key is the customer plus the normalized street address — (organizationId, normalizeAddress('postal', street)) — and a second account seeing the same address adds a per-account pointer, never a second building (HV B5; CAT ST-128b).
ST-128 · ST-97 · ST-87
A building that moves customer
Re-import preserves the row’s existing customer and never moves it (import/route.ts:160); nothing ends the old staff subset, and the display-code uniqueness check is deployment-wide with the fix pinned as a TODO (dynamo/property.ts:199-206).
HOLDS
HOLDS
NEEDS
The running relationship is a dated row, so a move is an end plus an insert — the old customer’s staff subset ends in the same transaction, and in-flight work is told its scope moved. Account 3 is the live instance: part of its stock is owned by account 2, and the re-key is itself a move.
ST-125 · ST-66 · ST-104
A customer with no PMS at all
The manual create route writes no customer and checks no session (properties/route.ts:27-108); people then refuse to be written onto it (dynamo/property.ts:663-671) and vendors are refused outright. The one live example is modelled as a test building with its homes as unit rows (provision-western-slope-proto-property.ts:126-199).
n/a
n/a
NO
Customer first, then buildings through the same classifier, then channels — the self-serve flow becomes step two of one sequence rather than a second path, and the PMS is one optional adapter slot (DF Catalog N8 rec). Marked NO because the record answers this as a product sequence and mints no stress case for it.
no case — nearest ST-123 · ST-97d
Why column 3 carries almost every NEEDS. They are one fact said eight ways: the proposal holds for that customer only after its sixteen homes stop being unit rows under one test building. Until the re-key, there is no building to hang a number, a calendar, a staff subset or an account on. The design record calls that placeholder a one-way door — fifty-seven row families can be born under it, the re-key is priced at five to seven engineer-days, and unbound voice conversations cannot be re-keyed at all. That is the cost of ingesting a customer before the shape exists, and it is the strongest argument in this assessment for doing the trunk first.
The idempotency rule
A re-import must land on the building that already exists. The record names one key — the customer plus the normalized street address, (organizationId, normalizeAddress('postal', street)) (HV B5; CAT ST-128 arm b) — with a single named normalizer and a per-account pointer map so a second account seeing the same address adds a pointer instead of a second building. Today’s code has neither half: the fallback key includes the name (import/route.ts:70-78) and reads a roster that spans every customer (:284-297; dynamo/property.ts:163). The matcher is also inline in a route handler, so no test can drive it — extracting it into a pure function is the half-day that makes the rule provable at all.
“And others” — what a fourth customer could be that today cannot express
A different PMS, or none
Per-building PMS type is already expressible; the credential is not — it is keyed by the person who connected it (dynamo/pms-credential.ts:9-11), so “accounting on one system, work orders on another” has nowhere to sit. A customer with no PMS gets a building with no customer stamp and no people (properties/route.ts:27-108; dynamo/property.ts:663-671), and cannot have a vendor at all (vendor-write-invariants.ts:66-70). Fixed by: the credential as an attachment with a slot per system role, nearest wins, and the PMS as one optional adapter at the leaf. Proved by ST-117 · ST-118.
Buildings already inside another customer’s data
Two of the three live customers already overlap — one owns stock the other runs. Import the second one and the unscoped fallback key can return the first one’s building; the import then stamps its PMS pointer onto that row and keeps the other customer’s stamp (import/route.ts:160-163, 284-297), so the new customer’s building never comes into existence. Fixed by: the customer-scoped key, and a store rule that one building belongs to exactly one running customer while ownership is a separate dated link. Proved by ST-128c · ST-12a.
Their own number per building
Expressible today only in the degenerate sense that the map holds one number per building; the reverse — several buildings on one line, or several lines on one building with different functions — is not. Note this one honestly: the record has no dedicated stress case for “a number per building” and treats it as an open question to ask the two clients (Q18), because either answer still needs the claim and the resolver. Fixed by: a claim per address naming a node at any level, with function rows underneath. Proved by ST-08a · ST-13 · ST-70.
Staff who work for two customers
Already true across the three live accounts — the same principals appear at two of them, and one floating assistant works two days a week at a fourth company. Today the invite form has no customer on it at all: the customer is derived from the buildings ticked, refused when they disagree, and refused when none of them carries a stamp (admin/users/route.ts:204-226). So one human working for two customers is two logins, and a customer with no buildings yet can hire nobody. Fixed by: one person, two role rows, one login with an explicit pick — the person is not the wall. Proved by ST-124 · ST-123 · ST-127.
A facility with no units
A warehouse, a clinic, a storage yard. Today totalUnits, floors and unitsPerFloor are required on a building and bedrooms/bathrooms/sqft are required on a unit, and a work order requires a unit id — so the shape is written as zeroes and lies. One live customer already has two office towers in scope. Fixed by: an asset-type fact that decides whether “which unit?” is even asked, and a capability switched off by name rather than a building that answers wrongly. Proved by ST-39 · ST-129 · ST-40.
The outbound door is right and unwritten: resolution is per building and fails closed (resolve-account.ts:50-69), but the import never sets it and one operator script does, one building at a time (set-appfolio-account.ts:152-161).
The de-duplication key spans customers: the fallback match reads every customer’s buildings, so two customers on one street collide by construction (import/route.ts:284-297; dynamo/property.ts:163).
Ingestion silently writes permissions: every imported building id lands on the importer’s own reach list in the same call (import/route.ts:389-405).
The wall is enforced in exactly one place on this path — the person writer, which throws the whole batch rather than write an unstamped row (dynamo/property.ts:663-671).
Two stores must agree about one account and neither is written at runtime, so a building can name a database that has no configuration behind it (types.ts:3637-3648; multiTenantConfig.ts:24-32, 242-246).
What this proves: the proposed architecture holds for all three live accounts on the trunk — customer, building, person — and the failures are concentrated in two named repairs, not in the model. HV B5 (the import key becomes the customer plus the normalized address, half a day plus a fixture) turns ST-128 arms (b) and (c) from RED to GREEN, and arm (a) is already GREEN today, which is what makes “two paths, one row” a number rather than a hope: created 0. The account fork is pinned by ST-37 and ST-119; the no-units customer by ST-39 and ST-129. What is not proved by any case is a customer with no PMS at all — the record answers that as a product sequence, not a test, and the nearest cases (ST-123, ST-97d) run on a customer that has no buildings yet.
What this pane could not check. Nothing was read from live data, by rule — so which buildings actually carry an account field today, whether the third slug has any row at all, and whether the second store holds a config for each slug are all unknown here. The building counts for accounts 2 and 3 come from the design record, which contradicts itself (fourteen, about thirty, and about sixty-two for the same customer) and states that one export was never read. Nothing in the code names a building count for any customer. The answering arrangements — who picks up, what is centralized — are from the record’s notes of client conversations, not from rows.
Yale supersession · 2026-09-10. The older custody account below is superseded by D-0910-1: the operator’s house exists now, the building is inside it, and the acting operator holds the login under a dated mandate. Missing values use PropFlow defaults, with eligible settings still OPEN; transcription no longer answers Yale. One handover mechanism covers both cases; its name is undecided — and whether the acting-operator apparatus is needed at all is DISPUTED: the founders’ standup of 2026-09-10 18:01 CDT talked the circumstance down, and the question is open at b89279042 (see D-0910-1). The house and the named admin are not in dispute.
The team drew the model on the wall in their own words. We drove ours into it, ask by ask — and where theirs was the better model, ours changed and the change is named.
If you only read this: the team's labels are our relationship rows, so the graph argument is settled in one line — partition by custodian, index by party. Of the twenty asks, nine are answered, two half-answered, six are gaps, one is wrong and two are answered with a correction. The wrong one is the ordering, and it is the most important thing on this page tonight: the first read PropFlow refuses rather than filters lands inside P1, and exposure begins at the first foreign customer login. The five-item cut below moves it in front of that login for 3.5–5 engineer-days, all of it already inside P1's budget.
The ordering finding, stated precisely. It is not “isolation is last”. P0 already carries an isolation slice this week — requireInRoster(user, propertyId) at the repository facade — but in observe mode: it logs every miss and refuses nothing, which is an instrument, not a wall. The first date on which a building read is refused rather than filtered is inside P1, which starts six calendar days after onboarding starts. And onboarding is not exposure: during onboarding week the only logins are Gera's and Fede's. Exposure begins at the first foreign customer login — go-live, mid next week — and that, not Sep 9, is the deadline the cut below is priced against. Raised for a ruling as block b88910993.
And this pane was itself red-teamed, the same night. Five reviewer lenses read it and the transcript independently — 104 findings, 60 stress cases — and hit one seam three times over: the design gates where a key may live and never who may write it, so on a building whose landlord and manager are different companies the levers the standup assigned to the manager all sit with the runner. That is accepted and raised for a ruling. Three of their claims are refuted with a row — most usefully that a region default plus a sub-market override already resolves, because a building may sit in several walked chains at once, so the defect was this page’s sentence and not the design. The cases are ST-163–ST-207 on A5; the dispositions, refutations and cites are Round 4 in the findings record.
Figure A9. One Runner, Many Labels. Sean's own sentence separates the two halves the design needs: the labels “determine who has visibility”, while “the property manager will set the policies for the property”. Solid = the asymmetric edge that makes a Query possible · dashed = a label read through the reverse index · the band underneath is the alternative, drawn because it is already what the code does.
Read it at
The team said: let a building be one thing, and put labels on it saying who is involved. That is what we already do. The one thing we had to add is which label gets to change the rules — exactly one, and everybody else can only look. We also found that the day a stranger's login can reach our data comes before the day we stop letting them, so we moved a few days of work earlier.
Sean drew a building with labels hanging off it: this company owns it, that company runs it, this region contains it. Fede called it a graph. Both are right, and both describe rows we already have — a label is a dated line saying “this party, this kind of tie, from this date”, and there is already an index that reads them backwards so a party can list everything it holds.
One word had to change. We had been letting the company that holds the deed be the one that owns the building's settings. It should be the company that runs the building through PropFlow — the one whose staff answer its phone. Nothing has to be re-stamped tomorrow, because today the company that pays is also the company that answers. It matters the day the other company signs up too, and that day it is one dated row, not a migration.
The team's labels are the design's REL# rows: PROP#<pid>/REL#<kind>#ORG#<party>#<validFrom> is a labelled dated edge, and its reverse index is what turns “everything this party holds” into a Query. The correction is the identity of the custodian: it is the runner, the customer org that operates the building through PropFlow, not the deed holder and not the real-world manager. Making the runner the operator removes three simultaneous breaks — no portfolio for a non-runner party, no region that can span walls, no manager-wide default on a chain rooted at somebody else's org — without adding a rung. A symmetric-label graph cannot do it: with no distinguished edge there is no partition key, so the portfolio read is a predicate over every building on the platform, which is src/lib/data/dynamo/property.ts:154 with a different column name.
Control has three halves and the design answered one and a half. Who may write is answered in principle — the resolver is deliberately party-blind, the chain is [PROP#, GROUP#(chain, precedence asc), ORG#], and the record's own four words are “Owner is never a rung” (read as property owner, the landlord) — but the ten write refusals name no party at all, which is fine only while the non-custodian is a no-login party. How a wrong write is detected is answered well: every effective value carries its provenance, a masked row names its masker, and every map edit writes a reversible log row. What is missing is a bound: the only instruments above a value are a statutory floor and a binary org lock, and a lock forbids the legitimate $40 exactly as hard as the illegitimate $500. That bound is already decided — “a building may narrow, never widen” — and simply has no row to live in, which is the cheapest kind of gap and the easiest to lose. Citation basis for every file:line on this pane: app checkout assess-main @ 58bf665820; the L numbers in quotes are lines of the architecture half of the Sep 8 thread (replies 28–46), which the brief's own transcript file omitted.
The pre-onboarding cut — five items, priced
Gera's own bar is 80%, not perfect. This is the smallest set that reaches it, in priority order, and it must fit beside Fede's week rather than inside it — none of it touches Fede's four files. Items 1, 2 and 5 are P1 and P0 verbatim; item 3 is P1's getProperty half pulled forward from P5; item 4 is a flag.
#
The cut
Why it is in
Price
1
Stamp the customer on every live building row, and add the roster index plus its backfill. No reader changes.
Nothing downstream can be a Query until the index exists, and the stamp is the one thing that gets harder every day a customer writes rows. Two creation paths exist and only one stamps: the Add Property route builds a sixteen-field building literal with no customer on it at all (src/app/api/properties/route.ts:88-106, :108), while the onboarding import does stamp (import/route.ts:160, sourced from the session at :313-314).
1–1.5 d — the script, the writer literal (src/lib/data/dynamo/property.ts:208-217) and the backfill; P1's first third
2
The property-picker path becomes a Query.getProperties(organizationId) reads the roster index, and the workspace's own cached path takes the customer (get-properties-cached.ts:20 feeding visibility.ts:26, :29, :31).
This is what a customer's browser actually touches, so it is the right first cut. Be precise about what it does not do: the 109 bare getProperties() callers do not go through the cached path — they call the reader directly and still hit :163 return all.
1 d — two files plus a parity drift test
3
getProperty(id) gains a required customer argument at the repository, with a 404-shaped refusal. The 404 shape already exists.
This is the single-row read of another customer's building — the one defect on this list a curious go-live user could trip by editing a URL. The gauntlet's T13 names it: getProperty(id) takes no org and reads the other customer's row fine, so the envelope is per-route.
1–2 d, judgment — the call-site count is what the compiler says, and it is the one number the battle test could not get without a build
4
Flip P0's roster gate from observe to refuse before the first foreign login, not after.
It is already being written this week. Flipping a boolean the day before go-live costs nothing, and it is the whole difference between an instrument and a gate.
0 d of new work, ½ d of watching the observe log first
5
The tripwire, kept — P0's per-prefix daily count of rows born under the placeholder.
Cheap, and it is the only thing that tells you the cut held.
½ d, already in P0
Total: 3.5–5 engineer-days in Gera's lane, none of it in Fede's files, all of it inside P1's existing 5–7 day budget — so this is a REORDER of P1, not new work. One sentence for the standup: before the first foreign customer login, a building read must be a Query on that customer's roster, not a filter after a full read — three data-layer changes, three to five days, already inside the budget we had for the week after.
What is consciously left at the 20%, named so nobody thinks it is covered. The 109 bare getProperties() call sites in src (+5 in lambda, +9 in agents) against two genuinely customer-scoped ones — item 2 does not touch them; item 4's roster gate stands in front of them and src/__tests__/unscoped-fleet-read.drift.test.ts:57-86 is the ratchet that proves the number did not grow. Threading a context through them is P5. Also left: the 22 unverified-legacy routes and 4 held envelope gaps of ADR-0120 (per-route envelopes stay per-route); the conversations post-read filter, whose outcome is right today and whose partition assertion is not; the batch-versus-single person reader disagreeing about the same person; every address and claim row — phone and mail routing stay on today's rails through go-live, and that must not be rushed; and the fail-open in the voice personalization route that silently becomes the caller's own building (personalization/route.ts; the harness's own case 22 records that the branch is deliberately kept open because outbound legs would break without it), which is P6 work with a prompt re-baseline attached.
The graph — the verdict, in plain words
Sean and Fede are right, the design already agrees with them, and the disagreement is smaller than it looked: it is about one word. Sean's own sentence separates the two halves — the labels “determine who has visibility” while “the property manager will set the policies for the property” — and that separation is the custodian invariant: exactly one party writes, and every other party is a labelled edge that reads. Fede's “you're just carrying a graph, with nodes and relationships” is a storage metaphor for the same thing, and it is literally what our rows are.
their labels are our rows
PROP#<pid>/REL#<kind>#ORG#<party>#<validFrom>is a labelled, dated edge; the reverse index GSI1PK = REL_PARTY#<kind>#ORG#<party> is how a party reads everything it holds. The only thing the design adds is naming which label confers write.
partition by custodian
The building is one partition keyed by the org that runs it. Sean's test sentence — “Yale could have 20 owners with 20 labels, but what happens in Yale stays the same… the only thing that changes is who has visibility” — is exactly true of these rows: 20 edges, 20 index entries, one partition, one runner, one set of policies.
index by party
The manager's portfolio is one Query on its own roster; the landlord's is its roster plus one Query on the reverse index — two Queries, both keyed, neither a scan.
the collapsed case survives it
Where one company is both landlord and manager, runner and landlord are the same org and the ownership edge is optional and self-referential. The invariant is stated as “exactly one custodian, and the custodian is the runner”, never as “the manager” — which is why it reads the collapsed case better than the current rows do.
why a symmetric graph fails
A graph with no distinguished edge has no partition key. “Every building labelled with this company” becomes a predicate evaluated after reading candidates — which is src/lib/data/dynamo/property.ts:154 verbatim today: a table-wide index query, then :163if (!organizationId) return all; and :164 an in-memory filter. It also makes the isolation assertion untestable (a post-read filter that drops a foreign row is itself a failure, and it fails while returning the right answer), and it leaves precedence with no total order to refuse a tie at.
and nothing to re-stamp tomorrow
The qualifier is load-bearing: the runner is whoever operates the building through PropFlow, not whoever manages it in the real world. On Sep 9 the paying customer's staff answer Yale's line, so today's stamp is correct as written. Real-world management is a fact carried by an optional managed_by label naming a party with no login and no roster. The day the other company signs and takes the line, custody moves by one dated transact on the same building id.
The twenty asks — answered, gap, or wrong
One line each, in the team's own numbering — A1 to A20 below are the standup's asks, not this page's tabs.Tally as the battle test counts them: 9 answered · 2 half-answered · 6 gaps · 1 wrong · 2 answered-with-a-correction. A row whose verdict is compound is counted under the half it leads with. Of the six gaps, one — A10's ceiling — is a decision already taken that has nowhere to live.
#
The ask
Verdict
In one line
A1
A full table scan filtered on the front end (Gera)
corrected
Right in substance and it needs sharpening, not correcting: the property list itself is narrowed server-side (src/lib/domain/properties/visibility.ts:26, :29, :31), but there is a real browser-side customer derivation on the vendors surface (VendorsClient.tsx:171-173, :184 into api/vendors/route.ts:108, :110), and the bigger exposure is the census: 109 bare reads against 2 scoped ones.
A2
The browser agent hardcodes one customer (Fede)
answered
Structurally: after step 0 one function is the only writer of a customer row, and a drift test pins the count at zero elsewhere. Worth carrying back — the repo already has a fence for exactly this, and the parametrized path already exists beside the hardcoded one: multiTenantConfig.ts:44, :47 model two customer databases and lambda/agent-runtime/handler.ts:131 literally says “do NOT default”, while one layer down a subdomain is a literal on a live path (vendors/…/apiClient.ts:198, :223).
A3
The graph: a building as the atomic node, every party a labelled relationship (Sean → Fede)
corrected
The rows, the reverse index and a worked party-to-buildings read already exist. One definition changes: the custodian is the runner, not the deed holder.
A4
Region / cluster as a real rung, per customer, possibly nested (Sean, Fede)
gap
“Whose group” is already answered — groups carry a customer. Nesting is structurally impossible and never said out loud: only a building is ever a member, so “Rocky Mountain = Denver plus Salt Lake City” is two ordered flat edges or two tags, never a tree. What is genuinely missing is a non-runner party's grouping: a manager cannot group across walls.
A5
Policy precedence — who wins, customer vs building vs property manager (Gera's question, Sean's rule)
gap
The deepest one. Precedence is level-only and party-blind by design and that is correct; but not one of the ten write refusals names a party, so Sean's rule and Gera's rule are both un-encodable. Today there is not even a level axis to hang one on (src/lib/platform/settings-resolver.ts:4, :51: two rungs, a building row then a compiled constant) and no writer identity at all (src/lib/data/dynamo/settings.ts:149-157 is a bare put).
A6
Propagate defaults without retyping across fifty buildings (Fede)
answered
Nearest-wins over the chain with an explicit “absent means” is exactly Fede's “80-90% is at the top, and then everyone kind of inherits, and then you can override at the bottom”. One row at the customer answers for the whole roster. True only if that customer is the runner — see A5.
A7
Quiet hours differ per community (Fede)
answered
A parameter writable at customer, group or building, nearest wins. “Some communities have 7am, some 8am” is one row per exception and nothing else.
A8
Sub-market pricing inside one manager (Sean)
gap
Answered as a mechanism — one chain group per sub-market carrying the fee, one row rather than one per building — but no case exercised a sub-market override under a manager-wide default, and the case is only expressible at all once the custodian is the runner. Now ST-151.
A9
Who is the property admin — a building-scoped role administrator (Gera)
gap
A building-scoped role is expressible (PersonRole.scope already types a building, src/lib/data/types.ts:15203-15206); a building-scoped administrator is not — manage_roles was decided with no scope field. Undecidable in the code too: the permission model has no role-administration action at all (permissions-source.ts:54) and the team-management decision carries no building dimension at all — its actor and subject types carry id, role, customer and status and nothing else (src/lib/domain/team/manage-teammate.ts:29-42). Cheap to fix while it is still ink.
A10
Too-customizable is its own risk (Gera)
half
Detection is answered and is one of the strongest parts of the design: every effective value names who set it and at which node, a masked row names its masker, and each map edit is one reversible log row. The ceiling is not — and the honest finding is sharper than “missing”: it is already decided (“a building may narrow, never widen”) and has no row.
A11
The phone-to-building count changes the greeting; adding a number must be one line (Fede, Gera)
gap
The one line is answered and is one of the design's best rows — a claim mints under a not-exists condition and a rebind is one conditional update, so “you just add one line without having to delete, or reroute” is the row. The greeting is not derived: it is a stored string nobody computes, and today it cannot be, because a number maps to exactly one building by type (src/lib/domain/properties/phone-lookup.ts:140) and the greeting interpolates that one name (triage-greeting.ts:190, :225).
A12
Calendars — three at one building, three reassignable at another, and losing one must not break (Fede, Gera)
half
Losing one is already written in full, and the brief was wrong to call this a gap. The hole is reassignment: nothing moves a booked tour between hosts, and a tour carries no host field at all today. And “lose a calendar” has a worse answer than breaking — a revoked credential returns the same null as no-calendar (sync-tour.ts:164-168 vs :145-148), so the flag built for exactly this never fires (check-availability.ts:531) and availability is served with an empty busy list. It does not break. It quietly double-books.
A13
Cross-customer free-busy without leaking (Gera)
answered
And Gera was wrong to drop it: Fede's own two-login case eleven minutes later is the counter-example, and tonight's gauntlet puts a number on it — the binding type has zero hits in the repo and no scheduler consumes the one field that exists. The design does answer it: nothing crosses the link at runtime except a calendar pointer, through two credentials, the second free-busy-only and connected with its own consent.
A14
One person, two customers, one dashboard (Fede)
answered
Two contexts, one per customer, each with its own roster, folded client-side, capped at eight. Today it is broken in a nameable way: the login resolves to whichever home-org role the iterator surfaces first, not to a choice (src/lib/domain/identity/accessors.ts:327-334), and a repo-wide search for a switcher returns five hits, all in one vendors client and all derived from the selected building rather than a choice of company (VendorsClient.tsx:171-173).
A15
The in-house handyman at the customer level and the building level — who do you call (Gera)
gap
Coverage and in-house are both modelled and the picker names what it skipped, but no case asserts the winner of a two-live-rows tie. Today the two rungs give different answers and one ignores the building entirely: preferred vendors are building-over-customer per trade, while the in-house designation is customer-wide only (src/lib/domain/vendors/in-house.ts:40 has no building reference anywhere in the file) — so a crew member assigned to one building reads in-house for another's work order.
A16
A regional manager sees her domain; a site person sees one building (Sean)
answered
Answered in-wall — a group-scoped role and a building-scoped role are both written down. Across walls it needs the custodian change plus the party grouping. Today the regional label has no scope at all: its data scope is identical to a site manager's and its entity matrix is shared verbatim (src/lib/platform/auth/permissions.ts:32, :58-65, :396-398 → scope.ts:51; permissions-source.ts:61-63), and there is no grouping entity of any kind in the data layer — a search over src/lib/data returns two hits, both a recurring-visit series id.
A17
The litmus test: the UI must make sense to a non-technical person (Fede)
answered
Answered as a pane, unproven as a fact. Twenty-one fitness functions are the machine-checkable half; the human half has no instrument and should not pretend to. The one thing worth saying: the four-word model is “one runner, many labels”, and if that sentence does not survive contact with Sean tomorrow, the design has a naming problem, not a rows problem.
A18
A migration is now acceptable (Fede)
answered
And it buys 5–7 engineer-days off the critical path: the placeholder re-key script disappears, because the scattered-site customer's sixteen homes can be seeded as real buildings now, through production writers, before that line carries traffic. It also turns the customer stamp from a dated exception into a required field with a real backfill, which is item 1 of the cut. It does not buy an earlier address claim (that was already zero migration) and it does not compress P5, whose cost is code.
A19
The clock — onboarding starts Sep 9, go-live mid next week (Gera, Fede)
wrong
The plan's ordering is wrong, and it is the most important finding on this page. See the banner above and the priced cut.
A20
Method: permutations, not unit tests (Gera, Fede)
answered
151 case ids before tonight, 168 after this battle test and 213 after the red team read it back; every case names a bench, an executor, the ladder step that closes it and a “breaks today” file:line. Fede's “here's 25 permutations, build a graph that passes” is the catalog on tab A5, and the catalog is six times bigger than the ask.
The six design changes
Each is a row or a field, each names its reason and its cases. These are places the design changed rather than defended itself.
#
The change
Why — what it replaces
Cases
1
The custodian is the RUNNER — the customer org that operates the building through PropFlow, whose staff answer its line and write its policy. Exactly one at a time, dated, transferable by a row. Not the deed holder and not the real-world manager. Stated as an invariant sentence beside the customer field; the worked example gains “until custody transfers” rather than being rewritten. Raised for a ruling as block b88912676.
Not a wrong row — a missing definition. The current shape (a manager's staff as guests inside the landlord's wall) is correct while only one party is a customer, and breaks A4, A5, A6, A8 and A16 simultaneously the day both are, with nothing in the record saying which arrangement applies when.
ST-146 · ST-148
2
A non-runner party may group the buildings it holds a live accepted edge on. The group profile gains createdByParty; such groups are restricted to the tag kinds (area, region, brand) and are never walked by the resolver, so no precedence trap is reintroduced. The membership reader's check widens to “…or the context is the group's createdByParty”, with no per-building check beside it: ending the party edge ends the tag in the same transact, so the tag's existence is the proof.
The current rule, under which a manager's “Rocky Mountain” cannot span the ten walls it manages.
ST-149 · ST-150
3
Carry the already-decided ceiling into the registry. The key spec gains bound?: {min?, max?, enum?} beside locked; a descendant may write inside the bound and is refused outside it — a new refusal outside_bound naming the ancestor node and the bound — and the effective value gains boundedBy.
This is not a new idea. “A building may narrow, never widen” is decided, and the register already lists “ceiling or default” as a registry field — but the key spec, the class table and the refusal list carry none of it, and the registry has now been folded into P3, so the field lands there or a decision Gera already took is simply lost. It replaces the binary lock, which forbids the legitimate $40 exactly as hard as the illegitimate $500.
ST-161
4
A write-time party gate. The attachment write refuses when the writing context's org is not the building's runner, unless the runner has delegated that keyspace on the party's edge: the ownership and management edges gain delegatedKeys?: string[]. New refusal not_custodian, naming the runner. Provenance is free — the attachment row already carries a party field. Precedence stays party-blind: the party axis lives entirely at write time, never in the walk.
Ten write refusals, not one of which names a party. This is Gera's “if they want to relinquish control to a local property or to the owner, they have to decide that” — the property owner, the landlord — made a row — and it makes the landlord's competing $40 a named refusal rather than a value that quietly loses a tie.
ST-147 · ST-151
5
manage_roles gains a scope, and the held-open question closes. The grant is written on a role row whose scope may be the customer or one building; a building-scoped holder administers people only within that building's grants, and the decided ceiling — only the account owner hands manage_roles on — is unchanged. May a building-scoped administrator write customer-level rows? No, by the existing writable-at refusal, which needs no new rule.
The customer-only reading, which cannot express Gera's “who is essentially the admin… one of that property”. This closes the “who is the property admin” question outright. Free to fix: the grant has zero occurrences in the code today, and the permission model has no role-administration action at all.
ST-160
6
The greeting becomes derived by default — absent means: one building in the reach set gives that building's name, more than one gives the company's. The precedent is already in the design: the same set size already decides whether a caller hears a menu or an area menu, and a reach of one already sets the building at mint. Zero extra reads; an explicit greeting row still wins by nearest-wins.
A stored string on the line attachment that nobody derives, and today's interpolation of one arbitrary building's name onto a shared line. Adding a number to one building therefore flips its greeting with no second edit — Fede's rule and Gera's one line become the same row.
ST-152 · ST-153
And one decision taken rather than changed. The plan's open item on the scattered-site customer — do its sixteen homes become real building rows now, or at P6? — is settled now, on the authority of A18. That removes the re-key script (5–7 d) from P6 and four hazard cases from the critical path; they stay in the catalog as regression rows but stop gating a phase. And two things checked and deliberately NOT changed: groups already know whose group they are (the brief said otherwise and the brief was wrong), and nested groups are already refused — the refusal is right, because a tree of settings rungs is the precedence trap already refused once, and the region ask needs a party, not a parent.
What the design should stop claiming
Found while checking. Being wrong out loud is cheaper than being quietly wrong — the battle test corrects its own first pass in four places, and these are the record's.
“a setting a building cannot override” is refused
The refusal is right about a platform tier and wrong about a customer bound. There is no platform tier and there should not be one; but “a building may narrow, never widen” is a decided customer bound, and the blanket refusal has already been overruled for at least one key. Change 3 is where it lands.
the walkthrough's pickup set
The onboarding walkthrough says a claim refuses and lists four keys it needs; the catalog pins a set that also includes the greeting and office hours. Three lists already disagreed and this is a fourth. One-line record edit.
“read by nobody”
Now false. The customer-settings row's module list is read (src/lib/domain/modules/get-org-enabled-modules.ts:30) and written (src/lib/data/dynamo/organization.ts:115); its own header says it was previously a dead field. The other five fields remain unread. So there is exactly one working customer-to-platform settings cascade today, and the catalog cell that says otherwise needs the same correction.
a customer named in an example
A vendor note renders a real customer's name in an example inside a document that bans customer names outside fixtures. Use fixture orgs.
the plan's weekday labels
W0 is labelled Mon–Fri over dates that are a Tuesday and a Saturday, and one contract date lands on a Saturday. Sep 15 as day 1 and every date from Sep 14 onward check out. One line, but a date somebody will schedule against.
the plan never names the onboarding date
The plan that A19 is about has no row for the event A19 is about. The cut above should be written into P0 explicitly rather than inferred.
and the polarity, said plainly
Both harness suites exit 0 while the product is broken — a green run means every case failed in exactly the way it said it would, and a case that started passing for real would turn the suite red. Tonight: 242 passed / 22 skipped on the case suite, 32 passed / 14 skipped on the gauntlet, with all fourteen gauntlet rows matching their declared expectation. Nobody should read “242 passed” as “isolation works”.
What this proves: the two models are the same model, and the evidence is Sean's own test sentence — twenty labels on one building change nothing about that building — which is exactly true of the rows as written. What the battle test changed is one word (the custodian is the runner) and five fields, each with a case id; what it found is one ordering defect, priced at 3.5–5 engineer-days inside an existing budget. Four gauntlet rows carry the argument and all four ran tonight at 58bf665820: T8 (the login picks a customer by iterator order, not by the user), T9 (cross-customer calendar binding cannot be expressed at all), T13 (the single-row building read takes no customer, so the envelope is per-route) and T14 (one company line listed on both homes, the customer coming out right only by side effect). What is not proved by anything here is the human half of A17: whether the hierarchy makes sense to a non-technical person has no instrument, and this pane does not pretend it does.
S · Scenarios — play-button walkthroughs
Eleven walkthroughs, each on its own small figure. Press play and the rows light up step by step; the strip under the caption prints the literal rows read or written. Every scenario ends with the same three-line ledger — inserted · updated · moved — and moved is always 0. Captions come in the same four levels as every chapter (Elementary · Middle · High · College); the page-wide reading-level control picks one.
One receptionist answers the whole building's front desk and asks 'which apartment?' before opening a folder.
Figure 19. The Doorbell. Someone dials Western Slope's one number. Sixteen homes share it. Clara asks the town, then reads the free homes in it. … Resolve on the full path, building → company. Tours and work orders are born on the home.
1 / 9
A person calls the company. Clara picks up.
Proof: the same Clara answers the company line and still ends up on one home, and only bindHome ever writes that home — pre-pin-keys-resolve-at-org · same-agent-id-every-line · bind-home-single-writer · fixture S3 / M24 Western Slope. Ledger:inserted 3 · updated 1 · moved 0 — the scope row, the conversation and the tour are inserted; bindHome is one attribute update.
Proof: a number claimed twice is refused by the database, and the shared line's rows are untouched — rebind-line-inserts-only · second-claim-refused-by-store · seeding-calls-zero-scripts · fixture S5 / F05 Hybrid (E3). Ledger:inserted 4 · updated 1 · moved 0 — two heads, one line row and the MAPLOG# line inserted; mapVersion updated; the release is 2 deletes + 1 update.
03
The principal logs in — and so does the manager's staff
A building's key ring: the principal holds keys to every door his company runs; a hired manager gets one key, cut only after the principal signs the work order.
Figure 21. The Wall (views from rows). The JP&Co principal signs in. One membership row → JP&Co, no picker. … End the managed_by row → the roles end in the same transact. Nothing ever moved.
1 / 7
The JP&Co principal logs in and is inside his company.
Proof: the manager's login cannot see, or even detect, the building she was not invited to — isolation-gauntlet T7/T8/T12 · transfer-is-two-partitions (negative) · fixture S4 / F02 Yale-shaped · §5.3 permutations 1–2. Ledger:inserted 3 · updated 2 · moved 0 — the managed_by row, the membership and the role inserted; the two ENDs are updates.
04
The same human at two customers texts the wrong line
S7 / F06 (passes under D5-A and under the one-Person reading alike) · chapter wall
Two employee badges in one wallet: each badge opens its own office, and neither office's door knows the other badge exists.
Figure 22. Two Doors. One human, two Person rows — one per company — and one admin-only link row. … The only shared thing is a pointer: two calendar rows, one calendar; B sees free/busy.
1 / 7
She has one record at each company, like two separate name badges.
Proof: a text on the wrong company's line reads zero rows across the wall — isolation-gauntlet T7–T10 · consent-split T11a/T11b · unclaimed-address-refused-zero-reads · fixture S7 / F06 (passes under D5-A and under the one-Person reading alike). Ledger:inserted 0 · updated 0 · moved 0 · reads across the wall 0 — nothing is written by the wrong-line text; the two Person rows and the link row existed before it.
05
An acquisition becomes a group (zero value changes)
Two families merge households: the smaller one's house rules move onto a shelf inside the big house, word for word — only the surname on the mailbox changes.
Figure 23. The Merger. Phase 0, for months: two companies, a few owned_by rows, one login with two memberships. … Dissolve: end the in_group rows and re-insert the values on a new company.
1 / 8
At first nothing changes: two companies, and the boss can switch between them.
Proof: after the merge the map dumps identical rows and every value still names who set it — map-dump-stable · every-effective-value-has-provenance · isolation-gauntlet · fixture S9 / F09 Acquisition (phase 0 → phase 1). Ledger:inserted 4 · updated 4 · moved 0 — on F09 as drawn (one setting, two buildings, one number): the group, the re-hung setting and two in_group rows inserted; the ended setting, two runners and one doorbell updated. It scales as 1 + settings + buildings inserted, settings + buildings + heads updated; the identity rehome re-stamps attributes; no partition moves.
06
We got it wrong: move the line back to the building
You moved the doorbell wire from one flat to the whole house on Monday; on Wednesday you move it back. The wire's history is in the logbook, and Tuesday's visitors are still in the visitor book.
Figure 24. The Undo. Monday's move left three rows: head → company, a live company row, an ended building row. … Next call: head → building, reach 1, home known. Camellia-shaped again; 3 rows touched.
1 / 7
On Monday the number was handed from one building to the whole company.
Proof: there and back, every original row is byte-identical and no key moved — rebind-line-inserts-only (there and back) · one-live-row-per-slot · cache-exact · fixture S5 / F05 (E3) · §7 #1 reversed. Ledger:inserted 2 · updated 4 · moved 0 — the building row and the MAPLOG# line inserted; two heads, the END and mapVersion updated.
A locked office building at night: the front desk won't buzz you in, but the fire alarm on the wall works for anyone.
Figure 25. The Fire Exit. 02:00. A call lands on a number no company has claimed. … Recorded in the resident's company, tagged cross-org. No channel-side thread. Fixed reply.
1 / 8
Someone calls a number that isn't on anyone's list, in the middle of the night.
Proof: an unclaimed number is refused with zero reads, and the emergency lane still pages someone — unclaimed-address-refused-zero-reads (T17 + T17-voice) · complete-mediation-rg (two pinned minters) · fixture isolation-gauntlet twin-digit orgs · +10002000199. Ledger:inserted 1 · updated 0 · moved 0 · reads after the miss 0 — one escalation row in the resident’s partition; no conversation on the unclaimed channel.
08
A tour is booked at Yale — code picks the calendar
A restaurant host stand: the seating chart and the servers' shifts are on paper, and the host — not the guest — decides which server gets the table.
Figure 26. The Calendar Desk. A prospect on Yale's own line asks for Wednesday at 2pm. The home is already known. … No prompt saw a calendar id, rota or distance. Camellia, with no rows, is byte-identical.
1 / 7
Someone wants to see the Yale apartment on Wednesday afternoon.
Proof: the host is chosen by plain code, and Camellia's text stays byte-identical — scheduling-is-pure (T2 Yale layering) · camellia-replay-byte-identical (T1 control) · fixture S4 / F02 + Fig. 4 · understand-scheduling-people §4 T2. Ledger:inserted 2 · updated 1 · moved 0 — the hold and the tour inserted; the round-robin counter updated; no prompt saw a calendar id.
09
A commercial tenant calls the same company line as residents
One hotel switchboard: 'press 1 for rooms, press 4 for the conference centre' — and if a conference guest presses 1, the operator hands them across rather than pretending the centre doesn't exist.
Figure 27. The Switchboard. One public number, four branches: a DID per branch → one head each; shared DID → FN# rows. … Wrong guess: a mis-labelled branch is one FN# update; opening a tower to Clara is one row.
1 / 7
One phone number, with a menu: homes, repairs, offices.
Proof: the office branch is recognised as out of scope by name, and a foreign function row is refused — import-classifier (known_out_of_scope) · second-claim-refused-by-store (FN# by another org) · same-agent-id-every-line · fixture S2 / F03 Situs · §7 #14 · PMS shape 1.9. Ledger:inserted 0 · updated 0 · moved 0 — a read-only walk; a wrong guess is one FN# update or one capability row.
10
The 400-home ring — a cold mint at four hundred homes
S8 / M24 scattered homes · red team A1 / A4 · chapter stretch
Finding out who is home on a street of four hundred houses: knock on every door twice, or read the one list pinned to the corkboard.
Figure 28. The Ring. The same call, twice. Left, reach is computed per building and the roster carries every token: 805 reads, about 2 MB, before the greeting. Right, the roster is ids plus three stamps and reach is a filter over them: at most six reads, about 50 KB. The number is the argument — "the size is a number" becomes one.
Proof: the cold mint on 400 homes is six reads, and no secret is in the index — roster-index-carries-no-secret · answerable-stamp-exact · consent-never-gates-reach · the S8 read ledger (_readLedgerForTests, step 0.5) · fixture S8 / M24. Ledger:inserted 0 · updated 0 · moved 0 — a read-only mint; the only thing that changes with size is the byte count of one page.
11
Onboard Western Slope in one sitting — seven screens
Moving into a new office in one afternoon: the folder, the staff list, the landlords' letters — and the phone company refuses to connect the line until the four blanks on the form are filled in.
Figure 29. One Sitting. Seven screens, about two hours; the 2026 screen is the fixture notation plus validate-fixture, the 2027 screen is the org map with the same seven actions as forms. The claim on screen 4 refuses and lists the four keys it needs, so nobody ever hears a half-configured Clara; screen 7 is the same claim, accepted. Thirty-one rows, none of them moved.
Proof: the whole onboarding loads as rows with no setup script, and the claim refuses until the four keys exist — claim-refuses-until-pickup-keys · seeding-calls-zero-scripts · import-classifier · validate-fixture on F01 prints the screen-4 refusal · fixture S3 / F01 Western Slope. Ledger:inserted 31 · updated 0 · moved 0 — every row born in its final place; the second claim is the only retry.
P · Podcast
A recipe card, not a meal: we write down exactly what goes in, a person cooks it once, and the kitchen never has to be in the room when you eat.
Figure P1. The Recipe Card. The two written sources and the chapter titles exist today and ship with the page; a person makes the recording once; transcription, waveform and the upload are mechanical, and the page picks the result up with no edit of its own.
Read it at
There is a talk-show version of this whole page. The script is already written and you can read it now; the recording is made by a person later, and when it arrives the play button just starts working.
One Nameplate on the Door is a two-host episode that walks the whole design in about the time it takes to drive somewhere. It covers the org as the house, buildings as rooms, hooks in the hall and in the rooms, one nameplate on the front door that turns a dialled number into a customer, and one walk from room to hall for every setting.
Then the honest half — what today's code does instead, which of it is solid and which is a skeleton, the six attacks written against the nameplate and the one flaw five of them found, the vendor answer, the plan in phases, and the bench each phase has to turn green. It ends where every example ends: rows inserted, rows updated, rows moved — and moved is zero.
The episode is one source brief of 5,962 words and a 2,503-word focus outline that carries no facts of its own, both readable and copyable on the episode page today. Fourteen chapters run the argument in order: the four customer shapes and the one decided item; the org / building / group trunk and dated attachments; the platform-unique address claim and the single resolution walk; the three-building worked example and its ledger; a person at two customers and a hire before the first building; the vertical stretch and its honesty line; today's code as one repeated mistake; the solid / half-baked / skeleton verdicts; the six head-level attacks and the free key repair; vendors as two heads; staff, the org switcher and the map; the phase plan against the working-day calendar; the seven open decisions; and the stress bench. Every claim about today is carried in the design record's provenance table with a file:line; every claim about the target names its row.
The episode page is artifacts/portfolio-architecture-how-podcast.html, pf-surface="podcast", tagged architecture, decisions and joined to this project by pf-project="portfolio-architecture"; it carries the four mandatory tabs (Episode · Transcript · Source doc · Outline) with an honest pending state in two of them. The player contract is #ep (the <audio>, src relative so the build routes it to the bucket rather than bundling it), #pl (the transport renders into it), #noaudio (un-hidden by the player's own error handler while the file is missing) and, later, #tx and the .vtt the data-vtt attribute already points at. The brief and outline live on disk at episodes/portfolio-architecture-how.md and -outline.md; the seven-step runbook is episodes/README-portfolio-architecture-how-podcast.md. This pane deliberately hosts no player: a pf-surface="project" page gets neither the injected player assets nor the R2-only audio rule.
#
Chapter
What it lands
01
What this is about
Four customer shapes, one question: what do we sell to, and how does everything else hang off it. One decided thing.
02
One customer, one house
The org is the customer, the contract and the wall. Rooms, hallways, hooks, and one secret per credential — referenced, never copied.
03
The nameplate and the one lookup
One claim per number, platform-wide, refused by the database rather than by code. Address → org → reach → person, then room to hall.
04
Park West, a1, a2 and a3
Two buildings share a number through a line set — a label, not a floor. Then one of them gets its own calendar; the undo, and the rebind.
05
A person at two customers, and a hire before the first building
One login, two walls, free-busy across the link. And a leasing agent invited into an org with zero buildings — which today's form refuses on the staff path, though the customer's own admin can do it.
06
A warehouse and a dentist's office
The same trunk, different data — plus the honesty line about what the record does not yet say and what the building row cannot yet store.
07
What today's code does instead
One mistake in nine costumes: a thing that belongs to the company, written onto one building. Ending on the placeholder and its tripwire.
08
Solid, half-baked, skeleton
Keep the spine, finish the half-built, rip out the skeletons. Three things further away than they look, three closer.
09
The nameplate was attacked six ways — and held
Zero of six broke the level; five of six broke the key one notch too fine. The repair is free, and the tour hold moves to the calendar.
10
Vendors: two heads, on purpose
The platform heads identity, the org heads the relationship. Card, head, engagement, roster — and the one decision left open.
11
Staff, the org switcher and the map
The invite derives the org from checkboxes today; the job becomes a role on the org. The switcher is a lens, not a door.
12
The plan, in the order a builder builds a house
This week on today's rails, three weeks of foundation in the dark, October's rows, November's wall and flip, then the road.
13
What is still a decision
Seven items with owners, ending on the one that is a question for the customers rather than for the design.
14
How we will know
Driven through PropFlow's own rows, never the PMS. Three executors, four assertions, and a catalog where a rewritten case is still the test.
How it gets made — three lines.
1 · Written here, first
The brief and the outline are on the episode page now, copyable and downloadable, and the page ships in a truthful “audio pending” state before any recording exists.
2 · Recorded by a person
A person generates the audio overview in a browser from those two sources plus the chapter titles as the focus prompt. There is no API for it, and synthesising it with a paid voice service is not allowed.
3 · Then it is mechanical
Transcribe for the follow-along, measure the waveform, upload the file to the object store that answers range requests. The player picks it up with no edit to the page.
One episode, one brief — the outline rides beside it as the second Source. Sources are peers with no supersession, so a second corrected brief would argue with the first instead of replacing it.
Nothing on the episode page claims to be decided that is not: the one decided item is named in chapter 1, and the open decisions get chapter 13 to themselves.
No outside system is named in the brief — the PMS, the phone vendor, the calendar provider and the voice platform appear by role only, and the transcript gets the same pass by hand.
The fixture names in the examples (Park West, Bellwether, a1, a2, a3, Dana) are the stress bench's, with impossible numbers and invalid mail domains, so nothing said aloud can reach a real person.
The .m4a never enters git: the build treats audio on an episode page as store-only, and a committed copy would ship unseekable.
Proof: the page is publishable today and the audio lands later with no structural edit — the player's own error handler is what shows the pending block, so the “missing” state is produced by the file being missing rather than by anyone remembering to write it; and the brief’s printed count (5,962) is inside the 4,000–6,000 the template asks for, with the 2,503-word outline a focus document rather than a second brief.
Recommendation, findings disposition, the bar as tests
This is the full record, written for agents; the short read for a person is the first group of sections.
Recommendation. Keep the study's WHAT verbatim — the company is the customer and the wall, a building has exactly one company that runs it (optionally through one walked group), and every line, mailbox, calendar, PMS login, schedule and setting is an attachment row on a node of that chain, read by one resolver, with property owners and managers as dated relationship rows and every login's view derived from them. Mint one truth for the wall at step 0 (Property.organizationId required-at-write plus a PROPORG#<org> stamp on a new sparse INCLUDE index that carries lifecycle, answerable and asset_type — the roster Query returns ids and three stamps, never a token, and reach is a filter over that one Query), one truth for every address (ADDR#<channel>#<address>/HEAD, a conditional put the database enforces, with optional per-function narrowing rows underneath), and give every attachment an identity in its sort key so every "wrong first guess" in the catalog reverses with END + INS. The registry of keys and kinds is code and is the one irreversible artifact — Gera signs its class list before a customer authors against it. Isolation is a type (TenantContext and four narrow siblings) minted in one module, never an object literal. Every mutation in the catalog is inserts plus attribute updates: zero partition moves, zero schema fields, a MAPLOG# row per transact so undo is one click, and — once the config dumps land at 3b — zero config PRs. The ladder is 16 steps and 83–121 engineer-days, two numbers: committed to the never-migrate proof and Western Slope's shared line = steps 0–6a, 50–73 days; backlog after the proof 33–48. Sep 12 ships on existing rails, steps 0–2 run Tue Sep 15 → Oct 13–23, and Western Slope's shared line (3a, 3b, 4, 5a) lands Nov 18–Dec 17 at one engineer in the page order — Nov 5–27 under the voice-first reorder, Nov 6–Dec 1 with many hands on the mechanical steps; Situs maintenance Nov 19–Dec 21, proposed.
What is decided and what is not. As of 9 Sep, G1 plus five further rulings are decided (see A10 · The review); on the older cards below, only G1 was decided at the time of writing: voice runs one shared agent set, and a company-bound line makes Clara ask which building (Fede, Sep 5). Four adjacent items are decided on their own surfaces and are cited here as such: the v1 front-door agent is ONE coupled agent with no transfers and no tools; Western Slope leasing goes live Friday Sep 12; the Western Slope leasing number is bought; the mail-provider integration is paused. Five more were decided on Slack on Sep 7 (Gera and Fede, #agent-smith) and are cited here as such: the Western Slope leasing line stays on, texting included; scope is leasing only — guest cards from prospects, no tenant support; the fan-out rails are built in parallel with the onboarding work and tested on extra PMS properties Gera creates; the portfolio test line is bought (PR #7285); number purchases are pre-approved. Three more were decided by Gera on the evening of Sep 8 and this page states them as decided, not proposed: (1) the property owner's grant widens from a report stub to reports · financials · occupancy · work_orders · documents by default — portfolio intelligence, read in the property owner's own house, scoped to the buildings they own — with conversations grantable and off by default, tenant privacy being the reason for the default rather than a prohibition; (2) delegated administration with a privilege ceiling — manage_roles is a grant held by people, no escalation, peers may edit each other, only an org_admin may hand manage_roles on, and at least one live administrator always, the last of whom cannot self-remove (E8); (3) the vocabulary — "owner" is retired as a bare word in favour of org_admin, property owner and PMS credential holder (the limits pane). The rules are decided; the rows that encode them are proposed like every other row here. Fede's frame for the work: the week-1 increment must itself work in the centralized-leasing setup, and everything in the next three weeks — Mon Sep 14 to Fri Oct 2 — builds on that foundation. What that does to the ladder is D10 (the road). Every other sentence on this page is a proposal for Gera and Fede, numbered in the decisions table (17 of them, in Answers) and cross-referenced to the study's D1–D9, the onboarding plan's X5/X6/X9 and the catalog's Q1–Q28 (PR #7209). Synthesis rule: the three judges split three ways (the engineer took graph-map-resolver at 62, the architect took relationship-tuples at 66, Gera's seat took study-hardened at 66) and agreed that ~80% of the storage is shared and the mechanics decide — so this page commits to one design: the study's WHAT, graph-map-resolver's trunk mechanics, relationship-tuples' line and label vocabulary, and every cross-design fix all three judges listed as a commit blocker. Code anchors are the attackers' pins at 45dac64eb9 (HEAD 7d4038f9ed); a few types.ts lines drift, so cite by symbol.
Findings disposition — every adversarial round, in order (round 2’s 44 findings · round 3’s 39 plus five doors · round 4’s red team against the Sep 8 standup)
The judges' must-fix lists, reconciled. "Fixed" names the section on this page that carries the repair; "rejected" says why the finding's proposed fix was not taken.
Finding (source)
Disposition
Where / why
transactWrite has no Update; "same TX" bumps impossible (impl-G F3, impl-R F4, mut-G F8)
fixed
step 1's first commit: Update item + CancellationReasons returned (The rows; Getting there)
Address uniqueness per (address, function), not per address (iso-G M1, impl-G F2, mut-G F7)
fixed
HEAD + FN# rows; GMR's CLAIM#<function> SK rejected
Attachment SK has no identity; END→INS refused (impl-B F2, mut-G F3)
PROPORG# stamp at step 0 is the only roster source — on a new sparse INCLUDE GSI since round 3 (A4), stamped by saveProperty itself (A3); REL#runsrejected
GSI2PK-only stamps index nothing (mut-B F4, impl-B M6)
fixed
sparse GSI8 CONVPROP# (online, four precedents); GSI2 overload rejected
the key table lists every key; the kind table every kind; a drift test greps design text and code
Locks/reclass mask at read (mut-G F4, mut-B F6, mut-R F4)
fixed
walk order and locks; FF-P3 rewritten as no-masking-nightly
Single-parent group collides (dom-G F1, dom-B F8, dom-R F1)
fixed
walked chain with precedence + tags; GMR's walked regionrejected
Operator change leaks history (dom-G F5, dom-B F3, dom-R F3)
fixed
new-PROP# transfer (catalog #8); era-scoped reads from dated runs rows rejected — there is no runs row, and a date filter on every PROP# read is a read-time mask
EFFECTIVE# snapshot broken as specified (mut-R F2)
rejected
no snapshot; ≤3 parallel per-node Queries cached per (node, valuesVersion) (round 3, A5 — was (orgId, mapVersion)); revisit at a measured p95
One Person across orgs reinterprets Fede's A2 (iso-B M1, dom-B F6)
rejected as a silent pick
D5-A restored (two rows + PLINK# under AdminContext); the alternative is parked for Fede (decision 1)
pinned-types-gain-no-required-field red on step 0 (mut-G F5, impl-G F4)
fixed
dated allowlist ratchet
home_known breaks DV set-equality (impl-B M3, impl-R F1)
fixed
home_state emitted on every line, N5 redefined at the rendered layer with an additive allowlist, one admitted re-baseline at step 4
re-summed in the repair pass: 76–112; steps 0–2 Oct 12–22, shared line Nov 17–Dec 15 at one engineer — superseded in round 3: 83–121, Oct 13–23, Nov 18–Dec 17, with a printed working-day calendar (P1/P6 below); three honest lines next to the ladder
GMR's 12 attachment kinds ahead of any dilemma (impl-G F11)
fixed
area*/cross_sell_rule are not kinds: the catalogue, the minutes and the rule are registry keys (geo.areas, geo.adjacency, cross_sell.rule) unread until step 8; schedule_override is a higher-layer schedule row
IAM LeadingKeys per-tenant enforcement
not in this design
the branded context + pre-read gate is the wall for application code (Honest limits)
Function reaches the edge; Spanish off the function axis (dom-G F9)
fixed
FN# rows + phone_line.function; language[] on the row and preferredLanguage on Person
No org condition on release/rebind (iso-G M2)
fixed
every HEAD/FN mutation carries organizationId = :ctxOrg
putRelationship has no writer rule (iso-G M3)
fixed
runner-only writer + acceptedAt by the party; REL_KINDS
Workflow under a runner change (iso-G M8)
fixed
mintFromSnapshot → ScopeMoved; open workflows drained before a transfer
PMSCRED# shared by two orgs (iso-G M9)
fixed
copied once into CRED# per attachment; (credentialId, pmsType) set diff
Attachment.organizationId on PROP# rows = a second truth (mut-G F2)
fixed
derived from the node on PROP# rows, stored only on ORG#/GROUP#/PERSON# rows; PROP#/REL# rows likewise
One runner / one parent not DB-enforced (mut-B F3)
fixed by construction
Property.organizationId is one attribute; a second runner is unrepresentable
Estimates invented (impl-R F7)
fixed
re-derived from the HEAD counts in the ladder preamble; "counts × judgment"; summed in the repair pass (76–112; 83–121 after round 3)
≤1 live row per slot not an invariant; history unbounded under the walked SK (impl-R F8)
fixed (repair pass)
invariant (h): END + INS in one transact or a named refusal; nightly one-live-row-per-slot; history under HIST# — round 3 (A11) moved the row there in the END transact itself, so there is no compaction threshold and no compaction script
Schedule layers/overrides in the SK (dom-B F4)
fixed
schedule#<function>#<ruleId> rows with layer; an override is a higher-layer row with a window
Asset manager above N managers has no runs (dom-B F12)
fixed
ORG#udg {customer} with capability_stage.* = off; the managers run the buildings; the asset manager's view is ReportsContext
Round 3 — red team 2 (rebuild/redteam-2.md, 2026-09-07) and the permutations' five doors (rebuild/permutations.md §D)
Severity in the source; every row is fixed in place or rejected with its reason. 18 architecture, 10 plan and 11 experience findings plus five doors.
Finding
Sev
Disposition
Where / why
A1 read amplification hidden in reach (400 + 400 Queries at mint)
fatal
fixed
answerable is a derived stamp on META projected into the roster index; reach = a filter over one Query; the cold count at 400 homes is printed: HEAD 1 · PROFILE 1 · roster 1 · chain ≤3 = ≤6 reads, ≈50 KB, ≈6 RCU; nightly recount is the falsifier (§3.1, §4.6, §5.1, §11). The sub-suggestion "compute reach lazily" is rejected: it is a set filter over the same single Query, so laziness saves nothing
A2 absent owner consent has two meanings
fatal
fixed
ONE meaning: consent gates pooled hosts and roster inheritance (§6), never reach; "not answered on the shared line" is capability_stage.<fn> = off on the building; S3 rows annotated; harness consent-never-gates-reach (§5.1, §6, catalog #4, §8 S3, §11)
A3 saveProperty erases the roster stamp
major
fixed
saveProperty's item literal stamps the roster index keys next to GSI3 (property.ts:208-216 verified: GSI3 only today) and requires organizationId; nightly FF roster-stamp-complete is a count (§3.1, §10 step 0, §11)
A4 roster Query returns full rows with OAuth tokens
major
fixed
PROPORG# moves to a new sparse GSI, projection INCLUDE {lifecycle, answerable, asset_type} (online, four precedents); GSI1 keeps REL_PARTY#/GROUP_MEMBERS#/GROUP_ORG#; FF roster-index-carries-no-secret (§3, §3.1, §4.6, §16 row 8)
A5 one org-wide zookie for structure and values; version read before rows
major
fixed
two counters on PROFILE (mapVersion structure, valuesVersion values), roster/reach cached on mapVersion, chain rows per (node, valuesVersion); base-table Queries ConsistentRead: true; the roster (GSI, eventually consistent) reads the version after the rows and retries once on change; cache-exact rewritten (§3.1, §4.6, §11). The alternative "state a ≤1 s window" is rejected — the retry makes the version a real lower bound
A6 route: external on a shared DID is a transfer; SMS has no FN#
major
fixed
v1 rule: an external branch is a DID never pointed at the platform; FN# {route:'external'} rows are reserved until transfers exist or §16 row 3 proves ring-time redirect; sms.function_fallback: a text whose intent maps to a function with intake_route: external gets the scripted hand-off reply, zero intake turns (§3.2, §3.3, §4.2, catalog #14, §9, §14.2)
A7 one CRED# row, N concurrent refreshers
major
fixed
rotation is a conditional update on version; CRED#<id>/LEASE {owner, expiresAt} single-flight with TTL; losers re-read and use the winner's token; harness cred-refresh-single-flight (§3.6, §11)
A8 the company-line pickup needs a tool the v1 agent lacks
major
fixed
said plainly: before step 4 a company line answers company facts and takes a message; bindHome, the tour and the WO arrive with step 4, which gives the shared agent tools — its own decision line, decision 15 (Fede); home_picker:'search' waits for tools; the v1 ask is the two-step area menu (§9, §10 step 4, §17)
A9 per-building conversation listings that Query the partition are incomplete
major
fixed
rule stated once: a building's conversations = GSI8 CONVPROP#<pid>, never Query PROP#<pid> begins_with CONV#; drift pin; priced into step 5b (§3.5, §10, §11)
A10 the reports push is refused by the wall
major
fixed
IReportsRepository.push(ctxRunner, ownerOrg, report): the transact carries a ConditionCheck on the live owned_by row (grants contains reports) — the relationship row is the authorisation; added to the minter list and complete-mediation-rg (§3.6, §5.1, §5.2, §11)
A11 the hot-path Query pays for history until compaction
major
fixed
END = the same transact Deletes the ATTACH# row and Puts it under HIST# (two items); the live prefix holds exactly one row per slot; attach-compact.ts is never written (§3.2, §3.6, §4.4, §11, §15)
A12 the 100-item transact cap
major
fixed
≤48 descendants per transact (Delete + HIST Put each; the lock UPD, the PROFILE ADD and the MAPLOG# row spend three); above that scripts/lock-sweep.ts in chunks of 48, the lock UPD in the last chunk (§4.4, catalog #16)
A13 no ClientRequestToken; a committed-then-timed-out claim reports "held by another company"
minor
fixed
every seam transact sends ClientRequestToken (10-minute idempotency window); on cancel the seam compares HEAD.organizationId to ctx.organizationId before reporting heldBy (§3.3, §3.6)
A14 the code/data boundary's cost is not on the page; trade list open; cross-repo grep
minor
fixed
one sentence for the human page (§4.2); TRADES closed in KINDS; the drift grep reads the design's key list exported as registry-keys.fixture.json, not this page (§4.2)
A15 GSI5 per-org key at call-centre volume
minor
fixed
shard suffix CONV_RECENCY#<org>#<yyyymm> reserved in the key builder, used only on a measured GSI throttle; the front-door partition is meta-only (messages live under CONV#<id>, conversation.ts:5-6) (§3.5)
A16 a prefix-less Query PK=ORG#<org>
minor
fixed
rule + drift grep queryByPK(orgPK( without a prefix = 0 (§3.5, §11)
A17 the platform claim GSI has no app-path job; invite dedupe absent
minor
fixed
the claim GSI serves admin search and merge suggestions only; existsElsewhere(ComplianceContext, claim) → boolean for invites (§5.4)
A18 open WOs/tours at a transfer are not handed over
minor
fixed
the transfer script exports open WO#/TOUR# rows as a dated hand-over document listed in the dry-run, and emits a re-consent task for TCPA grants (catalog #8)
P1 decision deadlines not re-derived after G1
major
fixed
every "by when" re-derived from the §10 cumulative days, low and high printed, labelled "needed by" (§16)
P2 the org map as an edit surface has zero engineer-days
major
fixed
rec (a), pending decision 14: no map UI in 2026 — the row is typed in the B6 fixture notation, checked by validate-fixture, loaded by bin/load-fixture; the clicks column describes the 2027 screen; (b) priced (read-only map 3–4 days, six write forms 8–12) as decision 14 (§2, §7, §7.1, §10, §15, §17)
P3 same-file collisions Sep 8–Oct 22 unnamed
major
fixed
the collision table collision table: file · who · rule (the collision table)
P4 the two Sep 12 "on file" deliverables have no owner
major
fixed
tripwire cron = Gera, ½ day, Sep 10; rekey-umbrella-rows.ts enumerator = Gera, 1 day, on file by Sep 19 (§1 #12, §9, §10, decision 9)
P5 step 1's backfill needs the registry that is step 2
major
fixed
step 2 split: 2a = the code tables (lands as step 1's second commit, before the backfill's setting writes), 2b = getScopePath + resolveEffective + the replay (§10)
P6 the proof completes at 3b; 25–38 days can leave the commitment
major
fixed
two numbers: committed 0–6a = 50–73, backlog = 33–48; step 1 trimmed (accountId → the Situs slice, config dumps → 3b's flip); step 5 split 5a/5b (GSI8 after Western Slope voice is live); step 6 split 6a/6b (§1 #12, §10, §11 closesAt)
P7 3a is not on the voice critical path
major
fixed as a priced option
the voice-first reorder (0 → 0.5 → 1 → 2 → 3b' → 4 → 5a, 37–53 days → Nov 5–27) is on the page with its cost (two weeks in which the Western Slope leasing line's wall is the edge mint + OPEN_SITES_TODAY, not the compile-time guarantee); Gera prices 3b' (§10, decision 17). Texting after 3a is unchanged
P8 "a second engineer" priced as productive in October
major
fixed
the §10 table gains a Head / hands column; the "with help" date is re-derived from fanning out the hands steps (Nov 6–Dec 1); the hire-vs-fan-out decision is decision 16 (§1 #12, §10, §15, §17)
P9 step 0.5's polarity sits on Fede's plate in go-live week
minor
fixed
Gera's store-side instruments first; Fede's contract + polarity by Sep 26; step 1's "done means" says "under the GREEN contract" (§10)
P10 no Situs date printed
minor
fixed
Nov 19–Dec 21, proposed, next to the Western Slope line (§1 #12, §10, §13 Q10)
X1 "Add owner" removes a home from the shared line
fatal
fixed
= A2; the form shows consent as what it does ("pooled leasing hosts may show this home: yes/no"), never as a token list (catalog #4, §7.1)
X2 a claimed line whose pickup keys refuse has an undefined ring
major
fixed
claimAddress and the capability_stage.<fn> = live flip refuse while a pickup-set key refuses at the scope node, naming the keys; endAttachment on a key a live line depends on is a named conflict; until (1) lands the third state is defined: emergency-only DV set + a page to ops (§4.4 rules (i)/(j), §9, §11)
X3 the walk-in "what do you have in Fruita?" has nothing to hear
major
fixed
available_homes data block on company/group lines: answerable homes with an available unit, grouped by area tag, capped and sorted — code, no tool; the pin is a normal bindHome (§9)
X4 no "what changed at v41", no undo
major
fixed
ORG#<org>/MAPLOG#<mapVersion> {actor, at, items[], reverse[]} written in every map transact; "Recent changes" and "Undo" read it; catalog #18 (§3.6, §3.7, §7, §7.1)
X5 ending a role does not end assignments
major
fixed
ending a PersonRole ENDs the person's assignment rows and marks them out in live schedule rows in the same transact, and the pure functions drop hosts without an active role at the slot instant; harness T6′ (§6, §11)
X6 sixteen addresses read aloud is not a menu
major
fixed
two-step ask above six: area first (geo.areas + the area tags), then the homes in that area; flat menu below six; search waits for tools (§9, §8 S8, §12 M24, §16)
X7 "one sitting" undefined
major
fixed
validate-fixture <file> ships before the form and prints every refusal the loader would raise; expected sitting printed: Western Slope ≈ 2–3 h, Situs ≈ a day (§7.1, §10 step 0.5, §13 N8)
X8 "silently rejoins"
minor
fixed
reworded: the line chip moves back to the company card; a MAPLOG# line (catalog #3)
X9 refusal messages undesigned
minor
fixed
one line per refused.reason/conflict in the operator's words (§4.4)
X10 thread continuity across the umbrella → company move is never shown
minor
fixed
play scenario named in §9
X11 a declined provider invite has no path
minor
fixed
opens an operator task; the hold stays until re-hosted (§6)
Perm-1 owned_by per unit
door
fixed (reserved)
SK shape REL#owned_by#UNIT#<u>#ORG#<party>#<validFrom> in REL_KINDS; per-unit consent gates pooled hosts for that unit, never reach; REPORT# gains unitIds[] (§3.4)
Perm-2 function as a registry, commercial out of it
door
fixed
fourth code table FUNCTIONS {id, class, lifeSafety, defaultStage}; commercial becomes an asset_type value read by emergency.route.* and FN# scope (§3.3, §4.2, §4.3, catalog #14, §8 S2)
Perm-3 the cross-org PUSH primitive named, three tokens reserved
door
fixed (reserved)
purpose:'brand_party', setBy.source:'party_push' + setBy.party, REL_KINDSfranchised_under / mandated_by; a pushed row is ended only by its party or AdminContext; REPORT# push (A10) is the first instance (§3.4, §3.6)
Perm-4 person→home needs a role
door
fixed (reserved)
occupancy.role closed enum in the entity registry; resolveIdentity → {personId, role, homeRef}; home_state:'suggested' reads it (§4.3, §9)
Perm-5 a fact below the building
door
fixed
rec (ii): one PROP# per asset class tagged GROUP#{kind:'structure'}; condition gains unitType/assetTypes reserved; honourable mentions reserved in one line (§3.1, §4.2, §4.3)
§ numbers in the two tables above cite the record (design-final.md) and map onto these panes: §1–§2 Recommendation · §3 The rows · §4 The resolver · §5 Isolation and views · §6 People · §7 The mutation catalog · §8 Every shape · §9 A call on a company line · §10 The ladder · §11 Fitness functions · §12 Scorecard · §13, §17 Answers · §14 the figures · §15, §16, §18 Honest limits · §D rebuild/permutations.md.
Repair pass (late evening Sep 6) against the critic's 24 gaps — 1 fatal (the ladder arithmetic), 8 major, 15 minor — every one fixed, none rejected; the fixes are folded into the sections below rather than logged here. The one retraction worth naming: an earlier draft's "Oct 6–17" for the shared line was an arithmetic error; no parallelism assumption reaches October.
Round 4 — the red team against the Sep 8 standup (2026-09-08)
Five reviewer lenses, run independently at Gera’s request against this page and the standup transcript — 104 findings, 60 stress cases, 12 decisions for a human; synthesis at portfolio-redteam-standup-2026-09-08, the cases ingested as ST-163–ST-207 on A5. This block is this design’s disposition, because “we reviewed it” is not a disposition: a finding is closed when it is fixed, or when a row proves it wrong. Three are refuted here with the row that refutes them; the rest are accepted and one is accepted with a correction that makes it worse.
Read this before the tables. The reviewers read the design record, not the product repo — their own “Honest limits” says so. That is the right way to red-team a design, and it is also where the three refutations come from: those claims are true of the prose and false of the rows. Where a claim is true of the rows it is accepted here without softening, including the one that is the finding of the night.
The headline five
#
What the red team found
Disposition
1
The authority inversion. The recommended arrangement puts the shared building under the property owner (landlord) as the runner — and in this design the runner holds the policy root, the lock, the administrator’s seat and every conversation. The standup said the opposite three times: the property manager sets policy and the landlord cannot force it. The design gates WHERE a key may live, never WHO may write it. Three of five lenses hit this seam independently.
accepted— and it is the finding of the night. Verified in our own rows: the ten refusals the attachment writer specifies are unregistered key · tier outside writableAt · below a locked ancestor · POLICY below the root · chain tie · second primary · missing pooling consent · live slot · pickup keys · dependent line (want.md:144, DF §4.4 L455) — not one of them names a party, exactly as they say. Raised for a ruling as the settings-authority decision; the runner question is the design’s existing D3 and is not re-raised. Cases: ST-147, ST-163, ST-164
2
No relinquish row. Nothing in the model carries key + node + person. Scoping a person to a building hands her every dynamic key there and excludes nobody above her; the only exclusion primitive points the wrong way.
accepted. setBy {…, party?} (want.md:70) already carries the party on the written row, so the provenance is free — what is missing is the grant, not the record. Their proposed delegation row rides the dated / history / log / undo machinery unchanged, which is why it is cheap: one attachment kind, one refusal, one chip. Queued to raise. Cases: ST-190, ST-191; fitness function delegation-names-one-person-one-key
3
A booked tour cannot be reassigned between leasing people — the daily need of a pod customer — and ST-63 states the opposite: a booked tour’s assignment is frozen.
accepted with a correction — and the correction makes it worse, not better. They say there is “no row for what happens to the tour’s assignee and calendars”. The row exists: ST-63 seeds the assignee, the calendar list, the rule id, the date and the start time, and its assert opens a re-host task — so a path exists and the fields exist in the target. What genuinely does not exist is an operator-initiated reassignment mutation and the calendar event moving with it. And the sharper half is one they did not make: today’s live Tour type carries no assignee field at all — no assignee, no calendar list, no host — verified this round at origin/mainsrc/lib/data/types.ts:10878. So this is not a missing mutation on an existing row; it is a missing field on the live type. Their change 3 is bigger than they priced it and more clearly worth pulling forward into the calendar slice. Case: ST-154
4
Landlord visibility is monthly. Five projections were granted on Sep 8; exactly one has a carrier row. The widened grant is a wider promise over the same one narrow row. Their remedy: drop conversations from the grantable list until it has a row.
accepted on the gap · refuted on the remedy. The gap is real and this design already admitted it in writing — the carrier row for the four new projections is named as an open limit rather than invented. The remedy is refused: see R3 below. The correct action is theirs minus that clause — build the carrier rows and rename the grants to what they actually deliver. Cases: ST-171, ST-172, ST-173, ST-174; drift test grant-list-matches-carrier-list
5
No override view for the intern question. The data exists and the reader does not; the change log keys on the structure counter, which a value edit does not bump; authorship is optional; the step-7 backfill manufactures per-building divergence before a customer touches it.
accepted. This is the instrument half of Gera’s intern question, and the point that a values-only edit logs under a counter its own transact did not move is specific and checkable — the design’s own UI decision D-19 says the same thing and is undecided. Their hoist verb also closes the “don’t retype across fifty” ask from the opposite end. Cases: ST-183, ST-185, ST-186, ST-192, ST-193, ST-194; fitness function overrides-inventory
Refuted — with the row that refutes it
These are the point of the block. A refutation that does not cite a row is an opinion.
#
Their claim
The row that refutes it
R1
“Regions are tags” — so a manager’s region defaults cannot express a sub-market override.
Refuted by their own third lens, and by the row. A group profile carries kind ∈ chain · area · region · brand · line_set · structure, and kind:'chain' is walked, ordered by the edge’s precedence, ties refused at write, depth cap 3 (want.md:20, DF §3.1 L191 · §3.4 L259). A building may sit in several walked chain groups at once, so region-default plus sub-market-override resolves today, with no new concept and no new node kind. Their own lens says it — “better than the page’s own prose says”. The defect was the page’s sentence, not the design, and the sentence is fixed rather than argued: the model’s concept row now carries the promotion clause it was missing. Cases: ST-149, ST-184, ST-187, ST-188
R2
The catalog says “146 ids in 165 rows” in one place and 151/170 in another — the page disagrees with itself.
Already fixed before their read landed, corrected to 151 ids / 170 rows the same evening, and the number moved again with the standup’s seventeen. Their audit is right that the page had drifted; it is reading a version that was superseded hours earlier. The standing fix is the one worth keeping: the number is derived from the catalog file on ingest and never carried over in prose — which is how this round’s 213 ids in 232 rows was produced, and why it is restated identically in every place on the page that says it
R3
conversations should be dropped from the grantable list as unimplementable.
Refused — superseded by a ruling made the same day. Gera decided it on 2026-09-08, in both halves: “whether the wider owner grant set is right, and whether a property owner may ever be granted conversation access” → yea — recorded as grantable, off by default, on a deliberate grant, tenant privacy being the reason for the default rather than a prohibition. A reviewer may not un-decide a ruling made the same day, and a stress case may not either: ST-173 carries the fork as “build the digest carrier or rename the grant”, and states in its own text that dropping the grant is not one of its arms
Where the red team and our own battle test agree — independent confirmation, worth more than either alone. Two reads, run the same evening from different starting points with no contact, reached the same three conclusions: that the standup’s label graph and this design are the same structure; that a symmetric label graph reproduces the very bug the standup opened with, because a predicate over every edge is a table scan with a new column name; and that the design’s real error is fusing the isolation wall, the billing unit and the policy root into one node. Both landed on the same fix — authority follows the management relationship. Two independent reads reaching one seam is the strongest signal in either document, and it is why finding 1 is raised for a ruling this week rather than filed.
Numbering, stated once. The five lenses each numbered their new cases from ST-146 with no cross-talk, and our own battle test had already spent ST-146–ST-162. Ingest renumbers the red team’s sixty from ST-163 upward in lens order, nothing already published is renumbered, and fifteen of the sixty merged into an existing id rather than being double-numbered — every merge named in A5’s Round 4 family, with the leg it adds. Handled here: the findings. The twelve consolidated decisions belong on the decisions ledger and are not raised by this ingest — and when they are, the design’s existing D3 is adopted and coordinated rather than re-raised.
The bar, as tests
Gera's twelve acceptance criteria from the Sep 6 Zoom, each a test rather than a description. Every one is wired to a named fitness function further down.
AC1 — One-row mutation. A hybrid building getting its own number, a mailbox moving to the company, a calendar attaching to a person, property #2: each is attachment/claim rows written in one transact, zero partition moves, zero schema fields, zero source-file edits, zero deploys once step 1 lands; measured by a row-count diff and an empty git diff --stat (42:25 "change, like, one thing").
AC2 — Tuple on the answer, edge in the store. Every key at every node resolves to {value, setAt, class, locked}; no reader ever writes property.x ?? org.x (22:27 "tuples… at every layer", 23:25 "that field doesn't exist").
AC3 — One dumpable per-org map.dumpOrgMap(ctx) renders the company, every building, attachment, claim and relationship as one document; the resolver's answers are a pure function of that document plus the registry; the map is a view, never a second truth (24:12 "a mapping", 40:01 "one giant file").
AC4 — Labeled key registry, DYNAMIC vs FIXED. Every customer-fact key is declared once with writableAt and absentMeans; a new fact column without a declaration fails CI (37:11–37:47 "you don't want tenants in companies").
AC5 — Deterministic code owns choice; the LLM owns intent. Which calendar, which host, which neighbours are pure functions over rows with unit tests and no evals; no prompt contains a distance, a rota or a calendar id (32:50 "algebraically", 35:39 "some sort of function", 39:06 "less stuff to do evals with").
AC6 — Provider-decoupled credentials. One credential row that N scopes reference; provider SDK calls only behind one adapter per capability; rotation writes one row (41:34 "hardcoded to [the calendar provider]").
AC7 — Isolation is structural, verifiable from a customer seat. Every store read takes a tenant context; no global phone/email lookup in application code; admins are an audited bypass; a per-org test account exists and the harness signs in as it (26:38 "nothing from Camellia spilling in", 27:23 "give them maybe JPCO only").
AC8 — Authority is a role × scope × tool matrix.PersonRole.scope accepts org, group or property; role-matrix.ts decides read vs write per tool; the trunk ships no if (role === …) outside the matrix (28:31–29:20, 29:35 "should already be there… double check").
AC9 — Byte-identity on fresh fixtures, per step. Every dark step replays a fresh Camellia-shaped fixture byte-identical across the settings dump, the voice DV set, the outbound address and the prompt; the control is never the Willows; each ladder row names one owner and one red-before/green-after row (43:59 "an owner to each one", 46:28 "somehow not done").
AC10 — Hybrid by capability, no mode. One org holds a line at the company, a line at a building, a mailbox at the company, a calendar on a person, a maintenance branch outsourced per building — as rows only; Property.operatingMode is deleted (22:01 "the hybrid is… on the fields themselves", 40:11).
AC11 — No read-time masking. A stored value the resolver would ignore cannot exist: POLICY below the org is refused at write, a row outside writableAt is a loud named refusal at read, a lock names what it masks (23:06 "empty… or does it not have a mailbox?").
AC12 — Replay is the review loop. The maintenance and Camellia corpora replay against every trunk step in CI; a standing round-robin reviewer diffs outcomes and files gaps as harness rows; the harness, not a demo, decides "done" (47:41–48:22; Fede 48:27 "tens of thousands of examples… replay the conversations").
The rows and types
The model in one picture
Figure 1. The Attachment Model. Western Slope's own rows. The number's head row at the edge names the company with one GetItem and is the wall; the walked chain is company → optional group → building, and every capability or value is an ATTACH# row on one of those three nodes. The resolver walks up for nearest-wins parameters and honours locks and policies top-down, naming what they mask. Areas, regions and brands are groups too, but tagged, never walked. Credentials, same-human links and pushed owner reports are platform rows outside the chain. The registry is code and validates every row; the org map is a view over all of it. Camellia is the same picture with the line on the building; the hybrid operator has one line on a group and two on buildings.
Customer
Where the line sits
What that makes true at pickup
Rows that differ
Camellia (single building)
ADDR#voice#+1844…/HEAD → PROP#cam
|reach| = 1, home known, home_state:'known'; nothing about today's call changes
zero groups, relationships, assignments, rules — the N5 control
Western Slope / Situs (centralized)
ADDR#voice#+1000…/HEAD → ORG#ws
reach = 16 answerable homes; propertyId: null; the same Clara asks which home
line, mailbox, calendar, schedule and hours as ORG# attachments; one PROP# per home
Hybrid (the LA operator)
one HEAD → GROUP#downtown; two HEADs → PROP#la4, PROP#la5
a call on 504 is Camellia-shaped; on 501 it asks which of three; same agent id
a kind:'chain' group with three in_group rows and one line; no mode flag anywhere
Eleven concepts, each earning its place
Each concept closes a dilemma from the Zoom (Z-numbers), the study (D-numbers) or the catalog (Q-numbers). Everything else is a row attribute or a registry line.
#
Concept
One line
New?
Row shape
Closes
1
Company
the customer, the wall, the identity pool, the operator whose Clara answers; purpose gains owner_party | manager_party | brand_party; carries mapVersion (structure) and valuesVersion (values)
existing (+3 attrs)
ORG#<id>/PROFILE
Z8, Z14, D1
2
Building
one property or one scattered home; the storage partition for everything born on its line; organizationId required; lifecycle IDENTITY; answerable a derived stamp
existing (+3 attrs, a roster index stamp)
PROP#<id>/META
D9, Z18, Z19
3
Group
one row kind, two jobs: kind:'chain' is walked (ordered by the edge’s precedence, ties refused, depth cap 3 — and a building may sit in several walked chains at once, which is what makes a region default plus a sub-market override resolve today); area | region | brand | line_set are tags. The word is not the job: a region that must hold a value is a kind:'chain' group with a precedence — the honest promotion cost, and the sentence round 4 (R1) caught this page getting wrong
new
GROUP#<id>/PROFILE {kind}
Z4, Z31, Q3, Q9
4
Person
the human spine; one row per company (D5-A); PersonRole.scope accepts {groupId} and staff {propertyId}
existing (validation widened)
PERSON#<id>
Z9, Z13, Z22
5
Attachment
a capability or value on a node; dated; locked; setBy; kinds closed by the registry
new
<node>/ATTACH#<kind>#<slot>#<startedAt>
Z1, Z2, Z5, Z6
6
Address head
one address → one company, conditional put; per-function narrowing with route
one secret, referenced by N calendar / PMS attachment rows; rotation writes here only — by exactly one refresher at a time (a conditional update on version + a LEASE row, A7)
new
CRED#<id>/SECRET
Z1, Z36, AC6
9
Key + kind registry
the DYNAMIC/FIXED label, defaults stated once, drift-pinned
the wall as a type; reach ≠ roster; minted in one module
new (code)
a branded type
Z8, Z9, Z26, Q14, Q15
11
Org map
a derived, versioned document over rows 1–8; the operator's edit surface (in 2026 that surface is the B6 fixture notation + validate-fixture; the screen is 2027, decision 14); every edit writes a MAPLOG# row; never truth
new (view)
dumpOrgMap(ctx)
Z3, AC3
Extended, not counted:Conversation gains organizationId, scope, propertyId? and org-stamped index keys; Tour gains optional assignedPersonId, calendarIds[], ruleId; PLINK# and REPORT# are platform/pushed rows under concepts 4 and 7. Not concepts on purpose:mode, Owner entity, Team, Area, Assignment, Rotation, Region, Brand, EffectiveSnapshot, PersonLink entity, Seat (reserved) — each is a row attribute, a tag group, or a registry line.
Entity and relationship, as a table
From
Cardinality
To
Carried by
Organization
1 → N
Group
GROUP_ORG# index; kind:'chain' walked, area | region | brand | line_set tags
Organization
1 → N
Property
Property.organizationId + PROPORG# on a new sparse INCLUDE index — the ONE truth for "runs"
Group
1 → N
Property
REL#in_group {precedence}; chain edges ordered, tag edges unlimited
Organization / Group / Property / Person
1 → N
Attachment
ATTACH#kind#slot#startedAt; on PROP# rows the org is derived from the node; Person holds calendar and assignment only
Attachment
N → 1
Credential
credentialId → CRED#/SECRET; one secret, N references
Attachment (phone_line, mailbox, inbound or both)
1 → 0..1
Address head
ADDR#ch#addr/HEAD; outbound-only rows take no claim
Address head
1 → N
Address function row
FN#function {scope, route}, ConditionCheck on the org
All rows are siblings under partitions that exist today (ORG#, PROP#, PERSON#) plus four new prefixes (ADDR#, GROUP#, CRED#, PLINK#) and one new row family under an existing prefix (REPORT# under ORG#). Nothing under PROP# ever moves. Index facts the attackers verified: seven GSIs, all ProjectionType: ALL (scripts/create-dynamo-table.sh:53-83) — which is exactly why the roster does not ride on GSI1 (A4: a projection-ALL Query returns every building's META row, leasingCalendar tokens included, until step 8 moves them to CRED#); GSI1 is free on PROP#/META (property.ts:208-216 stamps only GSI3 — re-verified 2026-09-07: the literal is {PK, SK:'META', GSI3PK, GSI3SK, entityType, ...property} and a full putItem, so any stamp not in that literal is erased by the next save, A3); GSI1/GSI2 are composite, so a writer must stamp the range key or the row is unindexed (helpers.ts:503-504); adding a GSI online has four precedents (scripts/add-*-gsi.ts).
Row kind
PK / SK
Attributes
Index stamps
Conditional write
Replaces / mirrors today
Building META (existing)
PROP#<pid> / META
+ organizationId (required at write), lifecycle ∈ active | lease_up | placeholder | legal_shell | sold | transferred, asset_type, answerable: {leasing, maintenance} — a DERIVED stamp (A1): lifecycle ∈ {active, lease_up} ∧ effective capability_stage.<fn> ≠ off, written in the same transact by saveProperty (lifecycle) and by putAttachment/endAttachment on capability_stage.<fn> at any chain node (≤48 buildings per transact, above that scripts/answerable-restamp.ts in chunks); owner consent is not an input (A2); no areaId — area is a tag
ROSTERPK = PROPORG#<org>, ROSTERSK = PROP#<pid> — a new sparse GSI PROPORG, ProjectionType: INCLUDE {lifecycle, answerable, asset_type} (~120 bytes per building, no token, A4)
writer invariant (A3): saveProperty's item literal stamps ROSTERPK/ROSTERSK next to GSI3PK/GSI3SK; the step-0 backfill uses updateItemFields once; nightly FF roster-stamp-complete is a count
getProperties()'s platform-wide filter as the roster source
Company PROFILE (existing)
ORG#<oid> / PROFILE
+ two counters (A5): mapVersion — the structure (nodes, heads, relationships, roster, lifecycle, groups, roles) — and valuesVersion — attachment values; purpose ∈ customer | sandbox | internal_staff | owner_party | manager_party | brand_party, timezone, jurisdictionStateCode (read only through the registry keys)
—
ADD mapVersion :1 for a structure transact, ADD valuesVersion :1 for a value transact, both in one Update item when a transact changes both — unconditional
the self-serve path that mints an org id with no row
Group
GROUP#<gid> / PROFILE
{id, organizationId, kind: 'chain' | 'area' | 'region' | 'brand' | 'line_set' | 'structure', name, createdAt} (structure = the tag that ties the PROP# rows of one physical building split by asset class, Perm-5)
GSI1PK = GROUP_ORG#<org>, GSI1SK = GROUP#<gid>
—
nothing (zero rows for today's customers)
Attachment
<NodePK> / ATTACH#<kind>#<slot>#<startedAt>
id, kind, slot, value, locked?, condition?, startedAt, endedAt?, setBy {userId?, source}, version; organizationId stored on ORG#/GROUP#/PERSON# rows only, derived from META on PROP# rows
invariant (h): a row in the slot refuses unless opts.supersedes names it; then END by moving — Delete the ATTACH# row + Put it under HIST# + Put the new row, three items, one transact (A11) — so the live prefix holds exactly one row per slot
LEASING_SETTINGS, KNOWLEDGE, RENEWAL_POLICY# siblings (settings.ts:143-267) — the same shape
none (an org-less PK on purpose — an address PropFlow answers on is platform-unique)
attribute_not_exists(PK), in one transact with the node's attachment and the mapVersion bump, sent with a ClientRequestToken (A13); on cancel the seam compares HEAD.organizationId to ctx.organizationId before reporting heldBy; every later mutation carries organizationId = :ctxOrg
PHONE_TO_PROPERTY_MAP (phone-lookup.ts:140-148), config/phone-registry.json, the number→agent file
Address function row
ADDR#<channel>#<normalized> / FN#<function>
{organizationId, scope: NodePK, route, language?: string[]}; function ∈ FUNCTIONS (leasing | maintenance | emergency | any at ship — commercial is an asset_type, not a function, Perm-2); route:'external' rows are reserved in v1 (A6)
none
ConditionCheck HEAD.organizationId = :ctxOrg
the IVR branch a caller pressed, today unrepresentable
Relationship
PROP#<pid> / REL#<kind>#<party>#<validFrom>
see the REL_KINDS table
GSI1PK = REL_PARTY#<kind>#ORG#<party> or GROUP_MEMBERS#<gid>; GSI1SK = PROP#<pid>#<validFrom>
Property.organizationId === ctx.organizationId (the runner grants); equal-precedence chain ties refused
Property.ownerId as an owner (renamed pmsCredentialUserId)
none — the property owner Queries its own partition
the writer is NAMED (A10): IReportsRepository.push(ctxRunner, ownerOrg, report), whose transact carries a ConditionCheck on the live PROP#<pid>/REL#owned_by#ORG#<owner>#… row with grants contains 'reports' — the relationship row IS the authorisation to write into another org's partition; no other writer may Put under a foreign ORG#
a property-owner login reading the runner's rows through the roster gate
written by invite/link only, never discovered through the claim GSI; invitedForParty legal only while an accepted managed_by row for that party is live on the scoped building
the GSI key builder widened at step 6 — helpers.ts:858-866 throws on {groupId} today
writer savePersonRole; refuses an invitedForParty role while no accepted managed_by row is live
the login-record list of assigned buildings
Least-recently-booked
PERSON#<pid> / LRB#<node>#<function>
{lastBookedAt, count}
none
written inside bookSlot's transact
nothing (round-robin fairness is new)
Hold
PROP#<pid> / HOLD#<calendarId>#<start>
{slotId, token, expiresAt (TTL)}
none
written by holdSlot, consumed by bookSlot or expired by TTL
the in-memory hold
Attachment history
<node> / HIST#ATTACH#<kind>#<slot>#<startedAt>
{…the row, endedAt, endedBy} — the ended row, moved here by the END transact itself (A11); never under the walked prefix; read only through listAttachments({history:true})
none
written by the END transact (Delete + Put); no compaction script exists
nothing — the resolver's Query returns exactly one row per slot
Credential lease
CRED#<id> / LEASE
{owner: runId, expiresAt (TTL ≈ 30 s)}
none
single-flight (A7): a conditional Put — attribute_not_exists(PK) OR expiresAt < now — taken by the ONE refresher; losers wait, re-read SECRET and use the winner's token; the refresher writes SECRET with version = :read and deletes its LEASE in one transact
PR #7164's clobber between copies, now between refreshers
written in the SAME transact as every map edit (X4) — the transact already enumerates its items, so the row is bounded; "Recent changes" and "Undo" read it; dumpOrgMap's header links the latest
nothing — the who-changed-what trail an owner or auditor asks for
The attachment, typed
type ScopeRef = { organizationId: string } | { groupId: string } | { propertyId: string } | { personId: string }; // person: calendar/assignment only
type NodePK = `ORG#${string}` | `GROUP#${string}` | `PROP#${string}` | `PERSON#${string}`;
interface ScopePath { propertyId?: string; chain: NodePK[] /* [PROP#, GROUP#…(precedence order), ORG#] */; organizationId: string }
// EXACTLY ONE row per (node, kind, slot) under ATTACH#, and it is live — invariant (h): END moves the row to HIST# in the same transact (A11)
type AttachmentKind =
| 'phone_line' | 'mailbox' | 'calendar' | 'pms_credential' // capabilities with an external identity
| 'setting' // slot = registry key
| 'vendor_roster' // slot = trade
| 'schedule' // slot = <function>#<ruleId> (layers/overrides are rows)
| 'assignment'; // slot = <personId>#<function>
// NO 'area' / 'area_adjacency' / 'cross_sell_rule' kinds: an area is a GROUP#{kind:'area'} tag; the catalogue,
// the minutes and the rule are registry KEYS (geo.areas, geo.adjacency, cross_sell.rule) — one home per fact
interface Attachment<K extends AttachmentKind = AttachmentKind> {
id: string; kind: K; slot: string;
organizationId: string; // REQUIRED on ORG#/GROUP#/PERSON# rows; on PROP# rows DERIVED from the node (never stored)
value: AttachmentValue[K];
locked?: boolean; // descendants may not hold this key (refused at write; honoured top-down at read)
condition?: { leaseFormId?: string; program?: string; unitType?: string; assetTypes?: string[]; effectiveFrom?: string }; // reserved at step 2, unread until M26; unitType/assetTypes reserved for the unit overlay (Perm-5)
startedAt: string; endedAt?: string; // dated, append-only; the SK carries startedAt
setBy: { userId?: string; source: 'admin_ui' | 'script' | 'backfill' | 'pms_sync' | 'onboarding' | 'lock' | 'party_push'; party?: `ORG#${string}` }; // party_push = a row written across the wall by an accepted relationship (Perm-3, reserved; the REPORT# push is the first instance)
version: number;
}
type AttachmentValue = {
phone_line: { address: string; function: FunctionId; language?: ('en'|'es')[]; direction: 'inbound'|'outbound'|'both';
primaryForScope: boolean; route: { kind:'clara' } | { kind:'external'; target: string };
record?: boolean; greeting?: string; displayName?: string }; // greeting/displayName = the per-line override read by lookupVia:'line'
// route.external in v1 = a DID that is NEVER pointed at the platform (recorded for the map, resolveOutboundAddress and the SMS hand-off reply) — never a ring-time redirect (A6)
mailbox: { address: string; direction: 'inbound'|'outbound'|'both'; primaryForScope: boolean; credentialId?: string; displayName?: string };
calendar: { provider: CalendarProvider /* the two external vendors, or 'propflow' for a Clara-owned calendar */;
externalCalendarId: string; credentialId: string; readMode: 'full'|'free_busy_only'; writeMode: 'write'|'hold_only'; timeZone?: string };
pms_credential: { credentialId: string; pmsType: string; accountId: string; database: string }; // slot = system role: leasing | accounting | maintenance | any
setting: { v: unknown }; // validated by KEYS[slot].schema
schedule: { function: 'leasing'|'maintenance'|'emergency'; kind: 'fixed'|'round_robin'|'primary_secondary'|'calendar_owner';
members: { subject: `PERSON#${string}`; order: number; coverage?: Window[]; calendarIds?: string[]; priority?: number; homeAreaId?: string; out?: boolean }[];
window?: Window[]; layer: number; fairnessScope: 'person@node@function' };
assignment: { function: string; coverage?: Window[]; calendarIds: string[]; priority?: number; homeAreaId?: string };
vendor_roster: { vendorIds: string[] }; // slot ∈ TRADES, a closed list in KINDS (A14)
};
Why sibling rows and not a NODE# prefix: the property partition already is sibling rows, and Query(PK=node, begins_with 'ATTACH#') returns a node's whole map in one call, bounded by depth, not org size. Outbound-only rows take no claim ("receives for forty, sends for one", Z28); putAttachment enforces one primaryForScope per (node, channel, function).
Invariant (h) — one row per slot under the live prefix, enforced in the write, not the read (A11).putAttachment on a slot that already holds a row refuses {status:'conflict', live: {startedAt, setBy}} unless opts.supersedes names that startedAt; then the same transactWriteENDs by moving: Delete <node>/ATTACH#<kind>#<slot>#<t0> (condition: the row still exists) + Put <node>/HIST#ATTACH#<kind>#<slot>#<t0> {…row, endedAt, endedBy} + Put the new row at t1 — three items, one transact, never two calls, so two live rows cannot exist for a second and the fold never needs a tie rule. endAttachment (a plain detach) is the same Delete + HIST Put, two items. The SK already carries startedAt, so history has no reason to sit under the walked prefix: the resolver's begins_with 'ATTACH#setting#' Query returns exactly one row per slot (~30 rows per node), history is read only through listAttachments({history:true}), and there is no compaction script. Nightly one-live-row-per-slot asserts it over every partition.
Address head — uniqueness across every tenant, function underneath
HEAD is written with attribute_not_exists(PK) in one transact with the node's phone_line/mailbox attachment and the mapVersion bump — the addClaim sentinel shape (persons.ts, two-item transact; transactWrite returns false on cancel, helpers.ts:2361-2373). On cancel the seam reads HEAD, compares HEAD.organizationId to ctx.organizationId first — equal means this org's own earlier attempt committed (a transact that timed out after committing) and the answer is {status:'created'}, not "held by another company" (A13) — and only then surfaces {status:'conflict', heldBy}; with the Update item, CancellationReasons distinguish "held by another company" from a version conflict. Every seam transact sends a ClientRequestToken (transactWrite sends none today, helpers.ts:2361-2408, verified 2026-09-07) so a retry inside the 10-minute idempotency window is the same write, not a second claim.
The v1 rule for an external branch, stated plainly (A6): a number that has rung the platform has already rung Clara; "RouteOut, zero Clara turns" from the personalization webhook would be a transfer, and the v1 agent has none (AS1). So in v1 an external branch is a DID that is never pointed at the platform — the call centre's number stays the call centre's; PropFlow records it on the map as route:{kind:'external', target} so resolveOutboundAddress, the hand-off reply and the org map know it, and never as a ring-time redirect. FN# {route:'external'} rows are reserved until transfers exist or the dated check proves the platform can redirect at ring time. What that means for a text: SMS has no FN# and no RouteOut; a text whose classified intent maps to a function whose intake_route is external gets the scripted hand-off reply (the target number, from maintenance.intake_route.number) and zero intake turns — the sms.function_fallback key.
FN# rows carry a ConditionCheck that HEAD.organizationId = :ctxOrg — one org per address and one node per (address, function) are both DB-enforced. The FN# row is read after the function is known (voice: the DID the platform delivered on, an IVR branch header, or discovered in conversation; SMS/email: HEAD only). Whether Situs's IVR forwards each branch to a distinct DID is assumed (B4 row 8 records the four branches, not the DIDs); if it does, the common case is one HEAD per DID and zero FN# rows; if a branch shares a DID, FN# is the fallback — the rows are the same either way.
Every HEAD/FN mutation (rebindAddress, releaseAddress) carries organizationId = :ctxOrg; cross-org rebind needs AdminContext; archiving a building releases every claim whose scope is that node in the same batch (nightly guard: every HEAD's scope node exists). releaseAddress deletes HEAD and FN rows and ENDs the attachment — a recycled number carries nothing of the previous org.
The two config files become dumps of these rows at 3b's flip (moved out of step 1, P6 — the leasing number is already mapped):scripts/sync-phone-numbers.ts dump (:11-13) is repointed at ADDR#; claimAddress calls the voice-platform adapter to assign the G1 shared agent and writes platformNumberId/agentId back on HEAD; drift-check compares table vs platform, never file vs platform; phone-registry.drift.test.ts gains a company-line kind. Until that PR lands, catalog rows that touch a number read "+ 2 PRs".
REL_KINDS — the relationship-kind registry
Closed and typed like KEYS/KINDS: putRelationship takes kind: keyof typeof REL_KINDS, and the drift grep covers REL# literals. The organizationId of a PROP#/REL# row is derived from the building's META, never stored, so the merger re-stamp inventory does not grow by a row family the design just removed for attachments.
Kind
Partition / SK and attributes
Party
Writer rule
Index
Unread until
owned_by
PROP#<pid>/REL#owned_by#ORG#<party>#<validFrom>{share?, grants:['reports','financials','occupancy','work_orders','documents'], poolingConsent?: ('leasing_line'|'vendor_roster'|'cross_sell')[], acceptedAt?} — the grant list is the decided default (Gera, 2026-09-08); 'conversations' is a legal member, absent unless deliberately granted; the list is per row, so one property owner may hold more than another
an ORG# (customer or owner_party)
the runner; a customer party's view derives only after its admin sets acceptedAt
the runner; equal-precedence chain ties refused with both named; tag groups unlimited; depth cap 3
GSI1 GROUP_MEMBERS#<gid> / PROP#<pid>#<validFrom>, read only after the group PROFILE's org check (no org copy in the key — nothing to re-stamp at a merger)
step 6
represents
ORG#<customer>/REL#represents#ORG#<party>#<validFrom>{grants, acceptedAt, organizationId} — the decision names only owned_by; this row is read here as mirroring that list, which is the page's reading and not Gera's words
an owner_party org with no login (Situs's LLCs)
AdminContext at onboarding, or the represented party's registered writer; consumed only by mintReports — the reports view is the union over the customer's own represents rows and its direct owned_by rows
as owned_by; per-unit consent gates pooled hosts for that unit only (X3's available_homes marks such a unit building-hosted), never reach (A2); REPORT# rows gain unitIds[]
GSI1 REL_PARTY#owned_by#ORG#<party>
no writer until a per-unit-ownership customer
franchised_under / mandated_by (reserved, Perm-3)
PROP#<pid> or ORG#<member> / REL#<kind>#ORG#<party>#<validFrom>{grants:['brand_keys','reports'], acceptedAt}
an ORG#{purpose:'brand_party'} (or a customer)
the member org's admin accepts; the party then pushes locked rows into the member's partition with setBy.source:'party_push', setBy.party — the ONE cross-org write primitive, of which the REPORT# push (A10) is the first instance; a pushed row is ended only by its party or AdminContext
GSI1 REL_PARTY#<kind>#ORG#<party>
no writer until a brand-above-operators customer
Owner parties that are not customers are ORG#{purpose:'owner_party'} rows (Situs: 33 property groups, almost all named after LLCs, 142 memberships across its 71 PMS records). No Owner entity, no *ownerId field anywhere; Property.ownerId → pmsCredentialUserId by the B9 R-code mapper, with R-attr as its removal trigger.
Doors kept open in one line each (permutations §D, all reservations, zero rows):Organization.purpose gains brand_party; PersonRoleType becomes a registry table like FUNCTIONS (board member, compliance specialist, community manager, engineer, broker are additions, not code branches); COMPLIANCE_FLOOR specs gain assetTypes[] applicability (fair-housing text does not clamp an office lease); a vocabulary.<asset_type> PARAMETER ("your lease" / "your membership" / "your unit") is reserved so the prompt's nouns are data; the transfer script (catalog #8) emits a re-consent task because TCPA grants do not survive an operator change; the chain cache is keyed per node (A5), so the org-wide hot key at 40,000 buildings does not arise.
Conversation — two fields, one index, four re-stamped keys, zero moves
PK = convMetaPK(scope) — PROP#<pid> when the HEAD names a building (Camellia: unchanged); ORG#<oid> / GROUP#<gid> for a company/group line
SK = CONV#<id>
+ organizationId (required at write) + scope: NodePK (required, IMMUTABLE — the meta writer refuses a scope that disagrees with REF)
+ propertyId? ("the home, once named" — written only by bindHome)
GSI8PK = CONVPROP#<pid> GSI8SK = <lastMessageAt>#<id> (sparse, new online index; stamped on every meta row with a known home, born-in-PROP# rows included)
GSI1PK = PHONE#<org>#<e164> GSI5PK = CONV_RECENCY#<org> GSI6PK = <org>#<personId> (re-stamped in the step-5 bulk stamp)
CONV#<id>/REF gains { PK } (lazy backfill; readers accept either shape)
The four queryAllPropertyPartitions('CONV#') fan-outs (conversation.ts:159, 205, 225; insights/themes/load.ts:49) are rerouted through GSI5/CONVPROP# or extended with the org's front doors from dumpOrgMap; spine-reconcile.detector.ts learns the new partitions in the same PR. UNASSIGNED (conversation.ts:55) and UNROUTED_INBOUND_ORG (:433-450) births are deleted; existing rows are archived with a count.
Rule, stated once (A9): the list of a building's conversations is GSI8 CONVPROP#<pid>, never Query PROP#<pid> begins_with CONV#. A thread born on the company line and bound to that home lives under ORG#<org>/CONV# and appears only through GSI8; GSI8 is stamped on born-in-PROP# rows too, so it is the complete index. That covers the per-property readers the four fan-outs do not name — getConversations(propertyId)-shaped readers (conversation.ts:156 and its callers on the property's Conversations tab, the PM inbox, per-building insights). Drift pin: outside src/lib/data/dynamo/conversation.ts, queryByPK(propPK(…), 'CONV#') = 0. Priced into step 5b.
Two shape rules (A15, A16): the ORG# front-door partition holds meta rows only — messages live under PK=CONV#<id>/SK=MSG#… (conversation.ts:5-6), so the partition grows by one small row per thread and is not a hot key; the key builder reserves a shard suffix CONV_RECENCY#<org>#<yyyymm> behind one function, used only when a measured GSI5 throttle says so (today GSI5 is one platform-wide key, helpers.ts:620, so per-org is strictly better). And every ORG# Query names an SK prefix (PROFILE, ATTACH#…, REL#, CONV#, REPORT#, MAPLOG#, today's follow-up override rows) — a prefix-less Query PK=ORG#<org> would pay for every family at once; none exists today, and the drift grep queryByPK(orgPK( without a prefix = 0 keeps it so.
transactWrite gains an Update item and a ClientRequestToken (helpers.ts:2361-2408 handles Put | Delete | ConditionCheck only and sends no token — verified 2026-09-07) — about 40 lines plus two unit tests, the first commit of step 1; the json twin gains conditional-put emulation (store.ts:5126addClaim re-implements the invariants in code today), in its own file store-conditional.ts. The service cap is 100 items / 4 MB per transact (A12): every seam transact spends up to three on bookkeeping (the PROFILE version ADD, the MAPLOG# row, a lock or HEAD update), so a fan-out transact carries ≤48 two-item descendants; larger sets are chunked sweeps with a named script. Every "same TX" UPD/END/version bump on this page depends on it.
The repository seam
Proposed signatures; src/lib/data/dynamo/{attachments,address-heads,relationships,groups,credentials,reports}.ts + interfaces + the JSON twin + the agents/clara/lib/data/dynamo mirror in the same PRs, gated by scripts/check-data-layer-drift.sh.
// the facade — callers change one line, not one argument per call (the spine's lesson #7: one facade cut, not a per-signature sweep)
getRepository(ctx: TenantContext): ScopedRepository // every method below is on the scoped instance; legacy readers are gated at the four mint sites
// attachments
putAttachment(a, opts?: { expectVersion?: number; supersedes?: string }): Promise<Attachment> // refuses per KEYS/KINDS writableAt, class, locked ancestor (names it),
// equal-precedence ties, and a live row in the slot unless `supersedes` names it
endAttachment(scope: ScopeRef, kind, slot, startedAt, endedAt?): Promise<void>
listAttachments(scope: ScopeRef, kind?, opts?: { history?: boolean }): Promise<Attachment[]> // Query PK begins_with ATTACH#[kind] (one row per slot); {history:true} reads HIST#
// address heads (transactional with the attachment + mapVersion bump)
claimAddress({ channel, address, scope, attachment, function? }): Promise<{status:'created'} | {status:'conflict'; heldBy: string}> // also assigns the shared agent via the platform adapter
narrowAddress(channel, address, function, { scope, route, language }): Promise<void> // FN# row, ConditionCheck HEAD.org = ctx.org
rebindAddress(channel, address, from: NodePK, to: NodePK): Promise<void> // one TX: UPD HEAD.scope (cond scope=:from AND org=:ctxOrg), END old, INS new, ADD mapVersion
releaseAddress(channel, address): Promise<void> // DELETE HEAD + FN rows, END attachment, bump — one TX
resolveAddress(channel, address): Promise<{ organizationId; scope: NodePK } | null> // the ONE context-less read (mint.ts only); null ⇒ refuse
// relationships + groups
putRelationship(rel): Promise<Relationship>; endRelationship(propertyId, kind, partyKey, validFrom, to): Promise<void>
listRelationshipsForProperty(propertyId, kind?): Promise<Relationship[]>
listRelationshipsForParty(kind): Promise<Relationship[]> // GSI1 REL_PARTY#<kind>#ORG#<ctx.org> — the party is ALWAYS the context
pushReport(ownerOrg, report): Promise<void> // IReportsRepository, runner ctx; ConditionCheck on the live owned_by row (A10)
listConversationsForProperty(propertyId, opts?): Promise<ConversationMeta[]> // GSI8 CONVPROP#<pid> — the ONLY per-building conversation listing (A9)
recentMapChanges(limit?): Promise<MapLogEntry[]>; undoMapChange(mapVersion): Promise<{status}> // MAPLOG# rows; undo = the reverse[] items as one transact at a new version, refused if a later change touched the same rows (X4)
createGroup(g): Promise<Group>; listGroups(kind?): Promise<Group[]> // GROUP_ORG#<ctx.org>
// scope + resolution (src/lib/domain/scope/)
getScopePath(propertyId): Promise<ScopePath> // META (org) + Query PROP#/REL#in_group# → chain groups by precedence
resolveEffective<K extends RegistryKey>(keys: readonly K[], at?: ScopePath): Promise<{ [k in K]: Effective<k> }>
resolveOutboundAddress(channel, at: ScopePath, function?): Promise<{ address; setAt: NodePK } | null>
dumpOrgMap(): Promise<OrgMapDocument> // profile + groups + buildings + attachments + heads + relationships; mapVersion as a header
bindHome(conversationId, homeRef): Promise<{ outcome: 'bound' | 'ambiguous' | 'unknown' | 'known_out_of_scope'; options?; reason?; route? }> // the ONLY writer of conversation.propertyId + GSI8PK
The resolver and the registry
Superseded in part · 2026-09-10.D-0910-1 makes the fallback a PropFlow default until the operator overrides it; which keys may default remains OPEN. The business/legal no-default rule itself is ruled, not proposed — the founders drew the settings-versus-policies line on the standup of 2026-09-02; what is open is the list of keys (b89278033, open as of 2026-09-13), so historical defaults below do not settle eligibility. D-0910-2 keeps quiet_hours as one key meaning “may Clara speak FIRST”: agent-initiated messages are blocked in the window; replies to a resident who wrote first are allowed.
One resolver
One function, two stages, two directions. Stage 1, scope, upward: address → company (fail-closed, zero further reads on a miss) → function (the FN# row, only once known) → person inside that company (the in-org CLAIM#<org>#<TYPE>#<value> sentinel, persons.ts:17-18, one GetItem — never the platform claim GSI) → home discovered by conversation (bindHome). Stage 2, settings, downward: building → chain groups (precedence order) → company → registry absentMeans, per key by class, direction, lock and merge, clamped by the compliance floor last. The property owner is never a rung (Q7).
Figure 2. One Resolver. The left column is stage 1, upward: the address head decides the company with one uncached GetItem and a miss refuses with zero further reads; the life-safety lane is a separate branded context, not an if. A function row routed external is reserved in v1 — an external branch is a DID never pointed at the platform (A6). If the reach is one building the home is known at pickup; otherwise the same agent asks (area first above six homes, with available_homes as data), and bindHome is the only writer of the home. The right column is stage 2, downward: the chain rows are read once per (node, valuesVersion) — one row per slot, consistent — and folded per key in a fixed order, and every answer is a tuple naming the node that set it — or a named refusal.
The key registry — FIXED vs DYNAMIC, the week-one keys classified
Proposed; Gera signs the class list before a customer authors against it (deepest question 3). The registry is code, which is why a new key is a deploy by design (AC4) and a new placement or value is data.
interface KeySpec<V> {
key: string; class: 'POLICY' | 'PARAMETER' | 'IDENTITY' | 'COMPLIANCE_FLOOR';
writableAt: ReadonlyArray<'org' | 'group' | 'group:brand' | 'property' | 'person'>; // DYNAMIC = ≥2 levels; FIXED = exactly ['property'] (or ['person'])
// 'group' = a kind:'chain' group; 'group:brand' = a brand TAG group (tierOf(GROUP#) reads the profile's kind)
// there is NO 'line' tier — a per-line value lives on the line's own attachment row, read through lookupVia:'line'
lookupVia?: 'branded_as' | 'line'; // consulted BEFORE the chain, for this key only
direction: 'nearest' | 'ancestor'; merge: 'replace' | 'append';
absentMeans: V | 'refuse'; // the ONE place a default lives; today's constant at ship time
lifeSafety?: true; // resolvable through LifeSafetyContext with a null chain
inheritOnlyWhen?: 'home_unknown'; // an org value legal ONLY while at.propertyId is null (never borrowed by a known building)
conditionKinds?: ('leaseForm' | 'program')[]; // reserved; no key uses it at step 2
schema: ZodType<V>;
}
Key
Class
writableAt
Dir / merge
absentMeans (today's constant)
Note
escalation.owner, escalation.cc
PARAMETER (DYNAMIC)
org, group, property
nearest / replace
today's value + ops alarm at the flip; per-building refuse after the operator confirms (two-phase)
reverses the Sep 1 IDENTITY classing (entity-model.md:542) on Zoom 24:01 "who to escalate to… attached anywhere" and Situs's no-per-property contact; Gera to sign
never inherited (the brand tag is consulted before the chain, never as a rung)
the nearest binding's identity
contradiction #34 resolved: binding (org-writable address) ≠ display identity; a brand tag group is the one tag the resolver reads, for lookupVia keys only
greeting, brand.display_name
PARAMETER, lookupVia:'branded_as' | 'line'
org, group, group:brand, property
nearest / replace
derived buildTriageGreeting
order: the line row's own greeting? (via:'line') → the brand tag (via:'brand') → the chain; setAt names the node, via the shortcut
live (an explicit live row written for every existing building at step 7, then absentMeans flips to observe — no deploy-date constant)
P12; slot ∈ FUNCTIONS, the same axis as answerable() and the line row's function; a write re-stamps answerable on the node's subtree in the same transact (A1); the flip tolive refuses while a pickup-set key refuses at the node, naming the keys (X2)
menu below six answerable homes, area_menu above (X6) / — / today's per-property switch
search is a tool by definition and waits for the step-4 tools decision (A8, decision 15)
sms.function_fallback
PARAMETER, lifeSafety
org, group, property
nearest / replace
scripted_handoff
A6: a text whose classified intent maps to a function whose intake_route is external gets the scripted hand-off reply carrying the target number, zero intake turns; life-safety still goes to the lane, never to the script
vocabulary.<asset_type> ({lease, unit, rent, …} nouns)
PARAMETER
org, group, property
nearest / replace
the residential nouns
reserved (permutations honourable mention): HOA/storage/office wording is data, not a prompt fork
not keys — KINDS rows kind:'entity', writableAt:['property']
property
—
—
"you don't want tenants in companies" (37:32) as a refused write
Drift guard (P14, A14): an rg test enumerates every setting#<key> / ATTACH#<kind> / REL#<kind> / FN#<function> literal in src, agents, scripts and the fixtures; any absent from KEYS/KINDS/REL_KINDS/FUNCTIONS fails CI; a new customer-fact column on Property/Organization fails without a registry entry. This page is not grepped from CI (a cross-repo grep is impractical): the design's key list is exported once as registry-keys.fixture.json in the product repo, and the drift test asserts the code tables ⊇ that fixture — the fixture is regenerated when this page changes.
The fourth code table — FUNCTIONS (Perm-2).{id, class: 'resident' | 'ops' | 'refused', lifeSafety?: true, defaultStage: 'live' | 'observe' | 'off'}, four rows at ship: leasing, maintenance, emergency {lifeSafety}, any. Every place that keys on a function — FN#<function>, capability_stage.<function>, schedule.function, assignment.function, answerable(pid, function), whoIsOnCall(function), transfer.<role> — types on FunctionId = keyof typeof FUNCTIONS, so office adds engineering, STR adds guest_services/housekeeping, affordable adds compliance, HOA adds violations, and senior's care is a row with class:'refused' — additions to a table, never a union edit and never an if. commercial leaves the function axis: it is an asset_type value read by emergency.route.* and by FN# scope, which is what it always was.
The cost of the boundary, for the human page (A14):a new value or placement is a click (in 2026: a fixture line and a loader run); a new kind of fact — a new key, kind, function or relationship — is a pull request, about a day.vendor_roster.<trade> slots come from TRADES, a closed list in KINDS (plumbing, electrical, hvac, appliance, locks, doors, roofing, landscaping, cleaning, pest, general), extended by the same pull request.
The kind registry
Kind
writableAt
Claim?
Unread until
phone_line, mailbox
org, group, property
HEAD for inbound | both; none for outbound
step 3b
calendar
person, property, group, org
—
step 8
pms_credential (slot = system role)
org, group, property
—
step 8 (accountId end-to-end at step 1)
setting (slot = key)
per KEYS
—
step 2 (first key)
vendor_roster (slot ∈ TRADES, closed)
org, group, property
—
step 2
schedule (slot <function>#<ruleId>), assignment
org, group, property (assignment also person)
—
step 8
no area / area_adjacency / cross_sell_rule kinds
—
—
the catalogue, the minutes and the rule are the keys geo.areas, geo.adjacency, cross_sell.rule; an area is a GROUP#{kind:'area'} tag; Camellia, Western Slope week 1 and Situs phase 1 write zero such rows
entity (tenants, units, leases, WOs, tours)
property only
—
a write elsewhere is refused; occupancy.role is a closed enum on the tenant/occupancy entity (tenant_of_record | occupant | guarantor | authorised_contact | responsible_party | owner_occupant | landlord_owner | board_member | guest | storage_tenant, Perm-4) and resolveIdentity returns {personId, role, homeRef} — home_state:'suggested' reads the role, so a guarantor is never quoted next year's rent as a prospect
Walk order, locks, reclassification
async function resolveEffective(ctx, keys, at = ctx.path) {
const chain = at.chain; // [PROP#?, GROUP#…(precedence asc)?, ORG#] — ≤5 nodes
const rows = await loadChainRows(ctx, chain); // ≤3 parallel Query(PK=node, begins_with 'ATTACH#setting#', ConsistentRead: true) on a per-(node, valuesVersion) cache miss; the prefix holds exactly one row per slot (A11) — ~30 rows per node, no history
const out = {};
for (const key of keys) {
const spec = KEYS[key]; // closed; unknown key = compile error
if (spec.inheritOnlyWhen === 'home_unknown' && at.propertyId) { out[key] = refuse(key, 'home_known'); continue; }
// HOISTED above the class switch: a KNOWN building never borrows the interim org value, whatever the class;
// the pinned path reads the IDENTITY twin (timezone, jurisdiction)
const via = spec.lookupVia ? await lookupVia(ctx, spec, at) : undefined; // 'branded_as': the brand tag's live row; 'line': the line attachment's field
if (via) { out[key] = { value: via.value, setAt: via.node, via: via.kind, locked: false, class: spec.class }; continue; }
const hits = chain.map(n => rows.live(n, key)).filter(Boolean); // nearest first; live(n, key) is singular by invariant (h)
const illegal = hits.filter(h => !spec.writableAt.includes(tierOf(h.node)));
if (illegal.length) { out[key] = refuse(key, 'tier_illegal_since_v' + REGISTRY_VERSION, illegal); continue; } // LOUD, never skipped (AC11)
let eff, maskedBy = [];
switch (spec.class) {
case 'IDENTITY': eff = hits[0]?.node === chain[0] && isProp(chain[0]) ? hits[0] : undefined;
if (!eff && spec.inheritOnlyWhen === 'home_unknown') eff = hits.find(h => isOrg(h.node)); break; // reachable only while unpinned
case 'POLICY': eff = hits.find(h => isOrg(h.node)) ?? hits.find(h => isAcquiredGroup(h.node)); break; // lower rows cannot exist (refused at write)
case 'PARAMETER': { const lock = [...hits].reverse().find(h => h.locked); // farthest locked ancestor wins, top-down
if (lock) { maskedBy = hits.filter(h => nearer(h.node, lock.node)).map(h => h.node); eff = lock; }
else eff = spec.merge === 'append' ? appendUp(hits) : hits[0]; break; }
case 'COMPLIANCE_FLOOR': eff = floorFor(at, ctx); break;
}
let value = eff ? eff.value : spec.absentMeans;
if (value === 'refuse') {
if (ctx.brand === 'life_safety' && spec.lifeSafety) value = PLATFORM_PAGER; // the declared carve-out, not an if in a handler
else { out[key] = refuse(key, at.propertyId ? 'absent' : 'home_unknown'); continue; }
}
out[key] = { value: clamp(FLOOR(spec, jurisdictionOf(at, ctx)), value), setAt: eff?.node ?? 'default', locked: !!eff?.locked,
class: spec.class, maskedBy, contributors: spec.merge === 'append' ? hits.map(h => h.node) : undefined };
}
return out;
}
type Effective<K> = { value: Value<K>; setAt: NodePK | 'default' | 'floor'; via?: 'brand' | 'line'; locked: boolean; class: KeyClass;
maskedBy?: NodePK[]; contributors?: NodePK[]; refused?: { reason: 'absent' | 'tier_illegal_since_v<N>' | 'home_unknown' | 'home_known' } };
Write-time refusals in putAttachment: (a) unregistered key/kind; (b) tier outside writableAt — tierOf(GROUP#) is group for kind:'chain' and group:<kind> for a tag, so a brand key on a brand group passes and a vendor_roster row on an area tag is refused; (c) PARAMETER below a locked ancestor — the refusal names the locking node; (d) POLICY below its root; (e) two live chain edges with equal precedence; (f) a second primaryForScope per (node, channel, function); (g) assignment/vendor_roster at org/group when a building underneath lacks the matching poolingConsent (names the building); (h) a second row for the same (node, kind, slot) unless opts.supersedes names the live one — then Delete + HIST Put + INS ride one transact (invariant (h)); (i) claimAddress, and the capability_stage.<function> = live flip, refuse while any key in that function's pickup set (org.timezone, org.jurisdiction, greeting, office_hours, emergency_phone, maintenance.intake_route, transfer.*) resolves refuse at the scope node — the refusal names the keys, so the go-live gate is a sentence the operator reads, not a DV the caller hears (X2); (j) endAttachment on a key a live line's pickup set depends on is a named conflict ("000-400-… depends on org.timezone") unless the line is released in the same transact (X2). Until (i) lands, the third state is defined: a claimed line with a refused pickup key rings the emergency-only DV set and pages ops — never a half-configured Clara.
Lock after the fact = one transact that sets locked:true and ENDs every live descendant row for that key (Delete + HIST# Put each, endedBy.source:'lock'), or refuses while descendants are live and names them; ≤48 descendants per transact (the lock UPD, the PROFILE version ADD and the MAPLOG# row spend three of the 100), above that scripts/lock-sweep.ts <node> <key> ends descendants in chunks of 48 (dry-run, pre-image, revert runId) and writes the lock UPD in the last chunk, so no descendant is ever masked while live (A12). Narrowing a class (PARAMETER → IDENTITY/POLICY) = registry edit + scripts/registry-reclass-sweep.ts <key> (dry-run, pre-image, revert runId, about half a day per key); widening = registry edit, zero rows. A skipped sweep is visible on the first read as the loud refusal, never masked.
The refusals in the operator's words (X9)
Effective.refused.reason and the seam's conflict codes map 1:1 to these lines; the fixture validator prints the same text.
Code
What the operator reads
absent
"Ember Lane has no timezone. Set it on the home — it is never borrowed."
home_unknown
"This caller has not named a home yet; application link differs by home, so Clara asks first."
tier_illegal_since_v<N>
"Escalation owner stopped being writable at a group in registry v41. This row at Downtown needs the sweep."
locked_by (c)
"Office hours are locked at Situs since v41 by Fede. Unlock there or leave it."
policy_below_root (d)
"Fair housing text is a company rule; it cannot be set on a home."
precedence_tie (e)
"Two chain groups tie at precedence 1: Downtown and North — order them."
primary_exists (f)
"Fruita already sends texts from 000-400-…; make that one secondary first."
consent_missing (g)
"Two homes owned by Smith LLC have not agreed to pooled hosts: 11 Elm, 14 Elm."
slot_live (h)
"Quiet hours is already set here (by the Director of Ops, Sep 3). Replace it or end it."
pickup_keys_refuse (i)
"000-400-… cannot go live: timezone, emergency phone and maintenance route are unset at Western Slope."
line_depends (j)
"000-400-… depends on timezone; release the line or set it elsewhere first."
version_conflict (expectVersion)
"The map changed (v41, Fede locked office hours) while you were editing. Reload — nothing was saved over."
held_by (HEAD)
"000-400-… is held by another company." (never shown when the holder is you — A13)
Fail-closed, and the life-safety carve-out
resolveAddress → null ⇒ the platform refusal with zero further reads, zero writes, no Person minted (E12). The carve-out is a brand, not an if: LifeSafetyContext {channelOrgId: string|null, address} has exactly two minters, both importer-pinned — (1) inbound-dispatcher.ts after classifyLifeSafety (:279-290, the text lane the drift test's own author asked for at inbound-org-scope.drift.test.ts:149); (2) the ring webhook on a refused or unclaimed voice line, which emits an emergency-only DV set (constant greeting, every customer block blank, property_id:'', one tool escalate_life_safety) whose handler mints the brand. The lane may read findResidentsByPhoneAnyOrg → Array<{organizationId, propertyId, personId}> (plural; channel org first; on collision page every active residency, one audit row each), whoIsOnCall(LifeSafety, 'emergency') resolved with absentMeans → platform pager, and recordEscalation into the resident's org partition tagged crossOrgChannel; it creates no channel-side conversation row and answers with the constant reply. Ruled 2026-09-10 (b89007521): a caller with no residency pages the company whose line it is, not the platform pager — when the digits match no active residency, the escalation goes to the on-call of the org that holds the address head the call arrived on, and absentMeans falls through to the platform pager only where there is no company to page, on a line nobody has claimed.
Caching, invalidation, reads — exact
The ADDR# HEAD GetItem is never cached and reads consistently; a released number cannot map to the old org for a second.
Two versions, two caches (A5).ORG#/PROFILE is read per resolve (one cheap consistent GetItem) so a warm runtime sees both counters on the next call — no TTL window on the map. The roster + reach (one PROPORG GSI Query, ids + stamps) is cached on (orgId, mapVersion); chain rows are cached per node on (node, valuesVersion), read with ConsistentRead: true (base-table Queries support it). A taught knowledge fact bumps valuesVersion and re-reads one node's ~30 rows on the next call; it never touches the roster.
The zookie is a real lower bound. Chain rows are consistent, so version-before-rows is safe. The roster GSI is eventually consistent, so its loader reads PROFILE after the Query and, if mapVersion moved during the read, re-Queries once (a second stale read is counted and surfaced by the read ledger, never served silently). FF cache-exact asserts reflects the rows within one consistent re-read.
Cost, printed (A1/A4/A5): warm = HEAD 1 + PROFILE 1 = 2 round trips; cold = HEAD 1 + PROFILE 1 + roster 1 + chain ≤3 = ≤6 round trips, ≈50 KB at 400 homes (400 items × ~120 bytes ≈ 48 KB, one page, ≈6 RCU), ≈120 KB at 1,000 homes — still one Query. reach is computed from the answerable stamp inside that one Query result — 0 per-building reads. "The size is a number" is now a number the harness's read ledger asserts.
mapVersion is bumped by every writer that changes the structure — claimAddress/rebind/release, putRelationship/end, createGroup, saveProperty (create/archive/organizationId/lifecycle change), savePersonRole, and the transfer/rehome scripts; valuesVersion by putAttachment/end on value kinds; a writer that changes both (a capability_stage write re-stamping answerable; a line claim with its attachment) adds both in one Update item — all inside the same transact, unconditional, so unrelated edits never cancel each other. Every such transact also writes its MAPLOG#<mapVersion> row (X4). The forever cache propertyOrgCache (phone-lookup.ts:407-419) is deleted.
Workflows pin {organizationId, propertyId, mapVersion} at start and re-resolve only at declared checkpoints, named for the four in flight: the renewal saga before each outbound send and at offer acceptance; the tour cadence before each reminder and at booking confirmation; turnover at each vendor dispatch; collections before each notice. Any other workflow declares its checkpoint activities in its own PR. mintFromSnapshot re-reads Property.organizationId and returns a named ScopeMoved outcome (never a silent 404) when the building changed runner; the transfer script lists open workflows per building and drains them first.
dumpOrgMap reads one Query per row family bounded by the org (GROUP_ORG#, PROPORG (ids + stamps), then per-node ATTACH# and REL#, and the latest MAPLOG# for the header); no whole-org GSI is invalidated by a single building edit. No EFFECTIVE# snapshot; revisit only if a measured p95 says so.
Isolation and views
Isolation and views
Figure 3. Views from Relationships. Solid arrows are "runs" (Property.organizationId, the one truth); dashed arrows are dated relationship rows. Yale 25 stays under JP&Co; JP&Co's principal sees Camellia and Yale because JP&Co runs both; ConAm's people see Yale because the accepted managed_by row lets JP&Co invite them as members scoped to Yale — an ordinary runner-org context with an actingOrg label, not a second wall. Situs sees the Grand Junction homes it owns only as report rows Western Slope's workflow pushed into Situs's own partition. No login reads another company's partition; the view a party gets is the rows that exist on its side, not a filter.
The context at the seam — five brands, one minting module
declare const TENANT: unique symbol;
export interface TenantContext {
readonly [TENANT]: true;
readonly organizationId: string;
readonly propertyId: string | null; // null on a company/group line until bindHome
readonly path: ScopePath; // chain in precedence order
readonly roster: ReadonlySet<string>; // every propertyId this org RUNS — from the sparse PROPORG GSI (step 0; INCLUDE {lifecycle, answerable, asset_type}, A4), never getProperties()'s platform-wide filter, never managed_by
readonly reach: ReadonlySet<string>; // buildings a caller on THIS line may name = subtree(scope) ∩ {pid : answerable[function]} — a FILTER over the same roster Query result, zero per-building reads (A1); |reach| === 1 ⇒ propertyId set at mint
readonly origin: 'channel' | 'session' | 'workflow' | 'conversation';
readonly function?: LineFunction; readonly address?: string;
readonly actor?: ActorView; // session origin only: { personId, tier, projection: 'operational'|'read-only', buildings: ReadonlySet<string>, actingOrg?: string }
readonly mapVersion: number;
}
export interface AdminContext { readonly [ADMIN]: true; actorUserId; reason; auditId } // writes the audit row FIRST
export interface ComplianceContext { readonly [COMPLIANCE]: true; channelOrgId: string } // booleans only
export interface LifeSafetyContext { readonly [LIFE_SAFETY]: true; channelOrgId: string | null; address: string }
export interface ReportsContext { readonly [REPORTS]: true; organizationId: string; buildings: ReadonlySet<string> } // from owned_by rows; accepted ONLY by IReportsRepository
mintFromAddress(channel, address) // HEAD GetItem → PROFILE → one roster Query → reach as a filter; an FN row routed external is reserved in v1 (A6)
mintFromSession(session, activeOrganizationId) // membership must exist; explicit, never first-found (accessors.ts:327-336 deleted); managed_by grants become actor.buildings + actingOrg inside the RUNNER's org
mintFromSnapshot(snapshot) // workflows; ScopeMoved on a runner change
mintFromConversation(conversationId) // tool webhooks, call-ended, outbound legs: VOICECALL#<id>/SCOPE (ring-time) or CONV#<id>/REF.PK → organizationId
mintAdmin({actorUserId, reason}); forCompliance(ctx); mintLifeSafety(address, channelOrgId|null); mintReports(session, ownerOrg)
// the reports PUSH runs under the RUNNER's TenantContext (mintForJob(runnerOrg, {runId, reason:'owner-report'})) and reaches the owner's partition only through IReportsRepository.push, whose ConditionCheck on the live owned_by row is the authorisation (A10)
mintForJob(orgId, {runId, reason}) // backfill/scripts, audited — no object-literal contexts anywhere (a drift test)
answerable(pid, function) = lifecycle ∈ {active, lease_up} ∧ effective capability_stage.<function> ≠ off — and nothing else. It is the answerable stamp on META, read from the roster index, never recomputed at mint. Owner consent is not in it (A2, one meaning, stated once): poolingConsent gates which hosts may show a home and which rosters a building inherits; it never removes a home from reach. A property owner who withholds pooling gets building-scoped hosts, not silence; a customer who genuinely wants "this property owner's homes are not answered on the shared line" sets capability_stage.leasing = off on those buildings — a key that already exists, set by the runner, visible on the map, and the stamp follows. Asset type is not a reach filter — reach is what a caller may name; asset_type ∈ rule.assetTypes belongs to nearbyAlternatives only. Legacy PROP# partitions never move: the scoped repository gates every PROP# read on ctx.roster.has(pid)before the key is queried; a miss returns the 404 shape byte-identical to "missing" (ADR-0120 D2). ORG#/GROUP# partitions are gated on organizationId === ctx.organizationId. Raw getItem/putItem/queryByPK/queryGSI (helpers.ts:880-2262) become module-private to src/lib/data/dynamo/ (the memberships.ts:16-19 pattern); the 52 non-dynamo importers move behind repository methods first (step 3a); OPEN_SITES_TODAY (20, inbound-org-scope.drift.test.ts:145-222) can only shrink. org-boundary.ts stays as defense-in-depth.
one mechanism, stated once: the accepted managed_by row is what authorises the runner to issue MEMBERSHIP# {invitedForParty: ORG#<mgr>} invites and PersonRole{pm, {propertyId: yale}} rows for that party's staff inside the runner's org; savePersonRole refuses such a row while no accepted managed_by row is live, and ending the row ENDs those roles in the same transact. mintFromSession then mints an ordinary runner-org context {organizationId: runner, actor.buildings:{yale}, actingOrg: mgr} — one Clara, one identity pool on Yale; the manager's staff are members of the runner's org scoped to the building (what exists today, AS4). A walk under an actingOrg context never reads org-level vendor_roster/escalation.owner values: keys carry visibleToGrantee (default false)
REL#owned_by#ORG#<owner> {grants:['reports','financials','occupancy','work_orders','documents']} — the decided default, 2026-09-08
the portfolio-intelligence projections the row grants — reports, financials, occupancy, work orders and documents by default, conversations grantable and off — scoped to the buildings that row names
ReportsContext over pushedORG#<owner>/REPORT#<pid>#<period> rows written by the runner's reporting workflow through IReportsRepository.push(ctxRunner, ownerOrg, report) — the one writer allowed to Put under a foreign ORG#, because its transact's ConditionCheck on the live owned_by row (grants contains reports) is the authorisation (A10; the same shape as this table's managed_by row); IReportsRepository never returns a Property, PERSON#, CONV#, KNOWLEDGE or ATTACH# row; getProperty(ctx, pid) returns null for a building the party does not run, whatever its grant list holds — the grant list widens, the wall does not; which row carries each granted projection beside REPORT# is an honest limit
administers people inside the customer: invite, edit, change role, end a role — and nothing above what the holder already holds
A grant on a person, not a class of user. Four rules and a ceiling: no escalation (you cannot grant what you do not hold); peers can edit each other; only an org_admin may hand manage_roles on, so a delegate administers people but cannot mint another administrator; and at least one live holder at all times, the last of whom cannot be removed or step themselves down. A property owner is not an exception — their grants arrive from an owned_by row by this same path, and the ladder holds no rung for a landlord (ROLE_HIERARCHY: platform_admin 0, org_admin 1, src/lib/platform/auth/permissions.ts:52-57). Three of these already ship at 58bf665820 — peer editing (permissions.ts:199-209, the peer rule :208; src/lib/domain/team/manage-teammate.ts:108-110), the last-administrator lock (manage-teammate.ts:49-50, enforced :79-80 and :101-102) and no-escalation for roles (:66-68, :98-99) — so the new work is the separable grant and its ceiling only (grep -rn manage_roles src/ = 0), added to manage-teammate.ts, the one decision module, never a second writer beside it.
whole roster / the group's buildings (GROUP_MEMBERS#) / that building
actor.buildings
PersonRole{viewer | accounting}
read-only
⚠️ CORRECTED 2026-09-13 — this cell said “excluded from every write set (role-matrix.ts:166-189)” and that was never true. On origin/mainsrc/lib/tools/role-matrix.ts:117-122 defines TEAM_LABELS as ['accounting', 'regional_manager', 'assistant_property_manager', 'leasing_assistant'], and STAFF_WRITE at line 176 — inside the very range the old cell cited — spreads it: ['pm', 'org_admin', 'maintenance', 'platform_admin', ...TEAM_LABELS]. Six other write sets spread it too (lines 135, 143, 155, 163, 173, 183, 186). So an accounting role can write today.
What is true as of propflowai#8337 (merged 9ffd5a30b961): owner decision b89329919 — “Accounting is read-only” — is encoded and guarded. READ_ONLY_TIERS is ['viewer', 'accounting'] in ONE place (session-view.ts:121), grants.ts now derives WRITE_TIERS = WORKSPACE_TIERS − READ_ONLY_TIERS from it rather than holding a second literal, and a guard asserts the writer is never called for an accounting row (deleting the tier reds it: 4 failed / 5). But authorizeAddressedWrite has zero callers under src/app and no write path reads ActorView.projection — the rule is decided ahead of its readers.
STILL OWED, and it needs a person:role-matrix.ts is on the dangerous-diff list, so no lane may change it. Until an attended human removes accounting from TEAM_LABELS (or from the write sets that spread it), the live tool gate still lets an accountant write. The dashboard matrix (ROLE_PERMISSIONS_SOURCE) remains a second edit after that.
platform_admin
nothing until mintAdmin(reason)
ORG_BYPASS_ROLES/getUserOrgScope → null (org-scope.ts:18-20, 37), the ADMIN_EMAILS auto-provision (auth/server.ts:40-46), identity-admin.ts:24-28 "NO org filtering" are on the delete list
"Send through Clara" (Z10) = checkToolAuthorization (role-matrix.ts:511-526) + entry.scope enforced against ctx.actor.buildings (the ADR-0029 Phase 3 flip, its own PR) + the POLICY key clara.send_tiers; channel-origin contexts derive actor only through withActor(ctx, view) after in-org identity resolution. Not a trunk blocker (Fede 29:39); the trunk only widens PersonRole.scope and the GSI key builder.
The six Zoom permutations as rows → view
Login
Rows
View
The JP&Co principal (25:24)
Camellia.organizationId = jpco, Yale.organizationId = jpco, owned_by#ORG#jpco on both
operational on both
ConAm's team — the ConAm on-site manager, the ConAm regional manager (25:42)
PROP#yale/REL#managed_by#ORG#conam {operational, acceptedAt} (the authorisation) → ConAm's staff = MEMBERSHIP# {invitedForParty: conam} + PersonRole{pm, {propertyId: yale}} in org_jpco (refused without the row, ended with it)
Yale only, inside JP&Co's wall; Camellia does not exist; nothing moves (D2-A)
her two buildings; may send on them (clara.send_tiers includes regional_manager)
The Situs President (27:48)
14 buildings run by Situs; owned_by#ORG#situs on the GJ homes (or on the LLC owner_party orgs Situs "represents" — ORG#situs/REL#represents#ORG#<llc>, so the reports view is the union); REPORT# rows pushed by Western Slope's workflow
operational on 14; reports on the Western Slope homes Situs owns; zero Western Slope residents, threads, knowledge or settings — by what exists in ORG#situs's partition, not by a filter
The Western Slope principal (28:40)
PersonRole{viewer, {organizationId: ws}}
read-only, every write tool denied
PropFlow admin / "give them JP&Co only" (27:09–27:31)
none in any customer org; a tester = an ordinary PersonRole{org_admin, {organizationId: jpco}} in the real org (not a sandbox org), audited as staff
nothing until mintAdmin; the JP&Co tester sees exactly what a JP&Co admin sees (T13)
Compliance carve-outs — the stores that lawfully cross the wall
isRevoked(ComplianceContext, {kind, value}) → boolean; recordOptOut is inside the brand
TCPA consent CONSENT#<phone> (compliance.ts:5-9) — grants are per property today (pick-consent-for-property.ts:42-52)
opt-out crosses; opt-in does not
hasGrant(ctx: TenantContext, e164, scope) → boolean reads grants stamped organizationId (backfill from the grant's property → org; null-property grants scope to the capturing org); T11a/T11b
SMS_STATUS#<sid> (compliance.ts:5)
carrier callbacks carry no claimed address
organizationId stamped at send time; mintFromCarrierCallback(sid) (a narrow fifth minter)
Identity-claim GSI (persons.ts:16-18)
admin search and merge suggestions only (A17) — with two Person rows per human now correct, "dedup" has no app-path job
the app path is the in-org sentinel; the cross-org fallback (persons.ts:912-921) moves behind AdminContext; the one app-path question — "does this invitee already exist at another customer?" — is existsElsewhere(ComplianceContext, claim) → boolean (a yes/no with no org named), used by the invite flow to offer the admin PLINK# instead of a duplicate
VendorCompany / VMEM# (ADR-0033)
vendors are platform counterparties
the vendor inbound lane mints from the called address first, then reads VMEM#<ctx.org> — closes the two OPEN sites in vendors/inbound-resolution.ts
Life safety
the gas-leak page
the brand above
Rule, once: identity may not leak; opt-out must; opt-in must not.
Same human at two customers (D5-A restored): two PERSON# rows, one per company, and one admin-minted PLINK#<id> in a platform partition readable only through IAdminRepository — never under either PERSON# partition, because an in-org partition read would reveal "this human exists at another customer". Nothing crosses the link at runtime except a calendar pointer: two ATTACH#calendar rows naming one externalCalendarId through two CRED# rows (org B's is readMode:'free_busy_only', connected with B's consent). Login is org-first: auth identity → written USER#<authId>/MEMBERSHIP#<org> rows → explicit activeOrganizationId; single-membership humans never see a picker (M12 falls out). Prospects and tenants are walled by the mint (no sentinel in the channel's org → a new Person there; T1/T15). The competing wording (one Person + per-org roles, ADR-0020 Q1) is parked for Fede as decision 1 — the harness's F06 is written so either passes.
People, calendars, scheduling
People, calendars, scheduling
Deterministic rows the LLM never reasons over. A calendar belongs to a person (or to a building, as today) through a credential row it references; assignment and schedule are rows on a node; the model calls one "schedule a tour" interface and never picks a calendar (Fede 39:17 "scheduling a tour is the interface"; Gera 35:39 "some sort of function").
Figure 4. People Own Calendars, Code Owns Choice. Yale 25 with its manager and the floating assistant. Each person's calendar is an attachment row on the person referencing one credential row (the OAuth callback writes CRED#, never the property row). The building holds two assignment rows and two schedule rows; the second schedule row is an override on a higher layer with a date window. findTourCandidates folds those rows in a fixed order and returns slots with the chosen host and its provenance; holds and bookings are rows; the tour snapshots the rule it was booked under. Camellia has none of these rows and degenerates to today's availability check, pinned by the tour-replay corpus at the rendered layer.
Rows.PERSON#<pid>/ATTACH#calendar#primary#t {provider, externalCalendarId, credentialId → CRED#, readMode, writeMode}; provider:'propflow' is a Clara-owned calendar (holds are the truth, invites go out) — the row Situs's three leasing people get on day one, because they have no calendar (B4 row 12). <node>/ATTACH#assignment#<personId>#<function>#t {coverage[], calendarIds[], priority, homeAreaId} (write-time check: an active PersonRole in this org — and the mirror at the end (X5): savePersonRole(active:false) ENDs that person's live assignment rows and marks the person out:true in every live schedule row that lists them, in the same transact; belt and braces, findTourCandidates / whoIsOnCall also drop any host without an active role at the slot instant, which is the half the harness asserts — T6′: techA's role ended → [techB, emergencyPhone]). <node>/ATTACH#schedule#<function>#<ruleId>#t {kind, members[], window, layer, fairnessScope} — an override (sick day, swap, a pure out) is another schedule row with a higher layer and a date window. PERSON#<pid>/LRB#<node>#<function> is the least-recently-booked counter, written inside bookSlot's transact. PROP#<pid>/HOLD#<calendarId>#<start> (TTL) is the hold row. Tour gains optional assignedPersonId, calendarIds[], ruleId (absent = today).
Resolution steps are fixed: chain → hours keys → hosts (assignments active at the slot instant; zero rows ⇒ the node's own calendar = today) → rule at the slot instant (zero rules ⇒ fixed/calendar_owner) → consent filter — the ONE place owner consent acts (A2) (a host whose assignment node is above the building is dropped unless every live owned_by of the building carries leasing_line; vendor-roster inheritance stops at the building without vendor_roster consent; a per-unit owned_by row without consent drops pooled hosts for that unit only, Perm-1 — and none of this touches reach) → per-host free/busy ∩ holds → travel buffer from geo.adjacency (empty ⇒ tour.travelBufferDefaultMinutes = 30) → rank (rule order → in-area bonus → time proximity → priority → personId; never randomness). Cross-sell (Z12): nearbyAlternatives reads cross_sell.rule + cross_sell.same_owner_only, the geo.areas catalogue, geo.adjacency minutes, the kind:'area' tag per home, asset_type per home and owned_by rows for sameOwnerOnly; rule absent = off. The scheduling report's six cases pass on these rows: T1 control, T2 Yale layering, T3 round-robin (the LRB# row), T4 in-area bonus, T5 Western Slope travel gap, T6 override (out:true member on a higher layer); E1 cross-company double booking (write-then-verify), E2 mid-week rotation (the Tour snapshot). Clara-owned calendars, what the person sees (X11): an invite in their own calendar for every booked tour and a hold they cannot edit; a declined provider invite opens an operator task ("re-host Tue 14:00 at Ember Lane") and the hold stays until it is re-hosted or the tour is cancelled — a decline never silently frees the slot. Credential rotation under N references (A7): the refresher takes CRED#<id>/LEASE, refreshes once, writes SECRET conditionally on version, and every calendar row that references the credential reads the new token on its next use; two Lambdas refreshing the same expiring token make one provider call, not two, and never clobber each other's refresh token (PR #7164's failure, now between refreshers instead of copies, is closed by the lease).
Customer
Calendar rows
Assignment / schedule rows
What the resolver does
Camellia
att(PROP#cam, calendar#primary {credentialId}), lifted from leasingCalendar
none
degenerates to checkAvailabilityRange (check-availability.ts:674-747); provenance ['building_calendar_default']; byte-identical at the rendered layer
the assistant first on her days, then the manager; the building calendar is a member's calendarIds entry
Situs (three leasing people, no calendars today)
3 × att(PERSON#L*, calendar#primary {provider:'propflow'}) — PropFlow-hosted because they have none; the same row takes an external provider when they do (D8, Sep 7)
by-calendar nearest-first with a constant travel buffer; the same rows the umbrella property uses today, on the org node
M6 — the assistant at a second customer
a second PERSON#asst_B in org B + PLINK# (AdminContext) + att(PERSON#asst_B, calendar#orgB {externalCalendarId: same, readMode:'free_busy_only', credentialId: c_B})
org B's own assignment rows
org B's scheduler sees busy blocks only; tour rows never cross; write-then-verify catches a cross-company double booking
The mutation catalog
Current rulings · 2026-09-10.D-0910-3: named buildings only; all_managed is not adopted. D-0910-4: the owner requests, the manager applies; no fourth landlord-written authority domain. D-0910-5: one handover mechanism for both cases, with its name still OPEN. Older transfer/mutation recipes below remain historical proposals wherever they conflict.
The HOW: the mutation catalog
The invariant. Every row below is inserts plus attribute updates in one transact: zero partition moves, zero schema fields, and — once the config dumps land at 3b — zero deploys. Every transact also writes ORG#/MAPLOG#<version>, so "wrong guess" has one universal answer — Undo (#18) — and the column on the right gives the domain-specific one. A new placement or value is data; a new key or kind is code. The two node mutations that are scripts rather than clicks (the merger, #7, and the transfer, #8) are priced and are still zero building-partition moves. Gera, 42:25: "can they just update one thing?" — yes, and the column on the right says what a wrong first guess costs.
Notation: INS insert · UPD attribute update · END set endedAt · DEL delete · TX one transactWrite (with the Update item from step 1) · every TX bumps ORG#/PROFILE.mapVersion. "Moves" = a row whose PK changes. "Clicks" = what the operator does on the org map — in 2026 that is a line in the B6 fixture notation, checked by validate-fixture and loaded by bin/load-fixture; the clicks column describes the 2027 screen (P2, rec (a); the screen is priced in decision 14). Rows marked † touch a phone number: 0 deploys once the config dumps land at 3b; "+ 2 PRs" until then (config/phone-registry.json + the number→agent file; sync-phone-numbers.yml:187-190 re-points drifted numbers to the file nightly).
#
Change
Rows written
Moves / fields / deploys
Operator clicks
Wrong first guess costs
1 †
Line moves building → company (Western Slope's leasing line leaves the umbrella)
TX: UPD ADDR#voice#+1000…/HEAD.scope → ORG#ws (cond scope = PROP#old AND organizationId = :ctxOrg) + the same for ADDR#sms#…; END PROP#old/ATTACH#phone_line#+1000…#t0; INS ORG#ws/ATTACH#phone_line#+1000…#t1 {direction:'both', primaryForScope:true, function:'any'}; ADD mapVersion. Conversations/WOs/tours under PROP#old untouched; the next inbound mints {org, propertyId:null, reach:16} → "ask which home"
0 / 0 / 0†
drag the line chip from the home card to the company card; confirm
reverse = the same TX with from/to swapped (the ended row's SK carries t0, so the re-INS at t2 is legal); conversations born under ORG#ws meanwhile stay there, indexed by home
2
Property #2 added to a single-building company (F08)
INS PROP#new/META {organizationId, lifecycle:'active', ROSTERPK: PROPORG#org, answerable} + UNIT#… (+ KNOWLEDGE); optional INS HEAD + PROP#new/ATTACH#phone_line if it has its own line, else it inherits by walking; the org row already exists (step 0); the N=2 surfaces unhide
0 / 0 / 0
"Add property"; optionally attach a line
none: property #1's rows never change (N5); IDENTITY keys on #2 refuse until set (Z25 — the clicks column says so)
3 †
Hybrid building gets its own number (Gera's exact question)
TX: INS ADDR#voice#<new>/HEAD {org, scope: PROP#pid} (attribute_not_exists — a number claimed anywhere on the platform is refused) + ADDR#sms#…; INS PROP#pid/ATTACH#phone_line#<new>#t {primaryForScope:true}; the shared line's rows untouched; nearest-wins makes inbound arrive with the building known and outbound use the new number; claimAddress assigns the shared agent at the voice platform
0 / 0 / 0†
"Attach number" on the home card; type the E.164
releaseAddress (DEL HEAD, END the row) — the line chip moves back to the company card and "Recent changes" shows it (X8); the building answers on the shared line again
4
Owner added (JP&Co owns Yale; ConAm manages it; Situs owns a Grand Junction home Western Slope runs)
INS PROP#yale/REL#owned_by#ORG#jpco#t {share:1, grants:['reports','financials','occupancy','work_orders','documents']} (the decided default; 'conversations' only on a deliberate grant); INS PROP#yale/REL#managed_by#ORG#conam#t {grants:['operational']}; if the property owner is not a customer, INS ORG#<owner>/PROFILE {purpose:'owner_party'} via the registered writer; Property.organizationId unchanged. reach is unchanged whether or not poolingConsent is given (A2/X1): a caller naming the home is still answered; without leasing_line consent the home's tours are hosted by building-scoped hosts only
0 / 0 / 0
"Add relationship" on the home card; pick the party; one question in plain words: "pooled leasing hosts may show this home — yes / no" (never a token list)
END the row (a dated to keeps history for reports at periodTo); a wrong share is one UPD; a wrong consent answer is one UPD and costs nothing a caller heard
5
Manager changes (the property owner fires the fee manager)
END REL#managed_by#ORG#old; INS REL#managed_by#ORG#new — when the manager is not the runner. If the runner changes → #8 (transfer), never a rehome
0 / 0 / 0
two clicks
reversible by dates
6
Staff person covers a second building
INS PERSON#pid/ROLE#new {scope:{propertyId:B}} (validation widened at types.tsPersonRoleScope + savePersonRole + the GSI key builder — a one-time trunk change, from step 6); INS PROP#B/ATTACH#assignment#pid#leasing#t {calendarIds:[her calendar]}
0 / 0 / 0
"Assign person" on building B
END both rows
7
Acquisition → group (F09; Everly buys Placeholm) — the merger case
first, for months: nothing — two companies + owned_by/managed_by rows (free). Then, only when they want one Clara and one pool: INS GROUP#plhm/PROFILE {kind:'chain', organizationId: everly}; for every ORG#plhm/ATTACH#… INS the same row under GROUP#plhm + END the old (PARAMETER values unchanged; POLICY keys now resolve to the acquirer's — listed in the dry-run); per building INS REL#in_group#GROUP#plhm#t {precedence: n+1} + UPD Property.organizationId → everly + re-stamp GSI1PK; UPD every ADDR#…/HEAD.organizationId (AdminContext); Placeholm's own chain groups survive as lower-precedence chain rows
0 building-partition moves; one scripted identity rehome: Person.organizationId, CLAIM#<org>#… sentinels, OCC#/INQ#/VMEM#/HH#<org> GSI stamps, Conversation.organizationId + the three org-stamped GSI keys, ORG#plhm/CONV# rows' organizationId, CRED#/GROUP#/ORG#-node attachments' organizationId — the full re-stamp inventory, with the (before, after) set-diff falsifier (precedent scripts/rehome-org-jpco*.ts: 2,502 persons, 58 stragglers in an 8-minute writer-stop window)
a script, not a click: dry-run, pre-image, revert runId
dissolve = END in_group + re-INS the values on a new org; the identity merge is the one-way half — said before running it
8
Building changes the company that runs it (mid-lease takeover; a normal-year event, PMS §3 row 7) — the transfer case
INS PROP#new/META {organizationId: B, lifecycle:'active'} + UNIT# rows imported from B's PMS; UPD PROP#old/META.lifecycle → 'transferred' (+ capability_stage.* = off); rebindAddress every HEAD whose scope was PROP#old → PROP#new (AdminContext); INS PROP#old/REL#transferred_to#PROP#new#t. Residents re-mint under B on first contact (T1's rule); A keeps read-only history under its own wall; B starts clean. The script also exports every open WO#/TOUR# under PROP#old as a dated hand-over document (data, not a move) listed in the dry-run, and emits a re-consent task because TCPA grants are per org and do not follow the building (A18)
0 / 0 / 0† (a script for the HEAD re-points)
scripted
the domain's own shape: after the cut A reads zero post-cut rows, B reads zero pre-cut rows (FF transfer-is-two-partitions)
9
PMS account changes (Situs's 14 situsgroup stamps become one; a second database inside one org)
INS ORG#situs/ATTACH#pms_credential#any#t {credentialId → CRED#, accountId, database}; a second database for a subset = the same row on a chain GROUP# or a PROP#; per-system-role slots (#leasing, #accounting) for Yale; END old + INS new to change; PMSCRED#<userId> rows are copied once into CRED# per attachment (K14 closed)
0 / 0 / 0
"Connect PMS" at the company
swap the row; falsifier E10: the (credentialId, pmsType) set over every property identical
10
A person's calendar replaces the building calendar (Yale's assistant)
INS CRED#c1/SECRET (the OAuth callback writes here instead of Property.leasingCalendar); INS PERSON#asst/ATTACH#calendar#primary#t {credentialId:c1}; INS PROP#yale/ATTACH#assignment#asst#leasing#t {coverage:[Mon,Wed], calendarIds:[asst, yale], priority:0}; INS PROP#yale/ATTACH#schedule#leasing#r1#t {kind:'primary_secondary', members:[asst, pm]}; the building calendar row stays as a member's calendarIds entry
0 / 0 / 0
"Connect calendar" as the person; "who shows tours here" on the building
END the assignment/schedule rows → the building calendar default returns; booked tours keep their Tour.ruleId snapshot
11
Brand / front door changes (Carr Street gets its own greeting and site inside Situs)
INS GROUP#carr/PROFILE {kind:'brand'} + PROP#carr/REL#in_group#GROUP#carr#t (a tag); INS GROUP#carr/ATTACH#setting#greeting#t, #identity.senderDomain#t; brand keys resolve via the tag before the chain; a separate mailbox = PROP#carr/ATTACH#mailbox#carr@…#t + HEAD
0 / 0 / 0 (mailbox: 0†)
"Brand" panel
END the tag; the greeting falls back to the chain
12
Region added (the regional manager's two buildings)
INS GROUP#north/PROFILE {kind:'region'}; INS REL#in_group#GROUP#north#t ×2 (tags); INS PERSON#rm/ROLE# {regional_manager, scope:{groupId: north}}; if the region must hold a setting (F10 south quiet hours) it is a kind:'chain' group with a precedence — one profile attribute plus N in_group chain rows, refused on any equal-precedence tie with the buildings' other chain groups (the honest promotion cost)
0 / 0 / 0
"New group", drag two homes in, assign the manager
dissolve = END three rows; no resolver rung was created for a tag, so nothing re-resolves
13 †
Western Slope adds a maintenance line (press 3 stops going to the call centre)
TX: INS ADDR#voice#<new>/HEAD {scope: ORG#ws} + ORG#ws/ATTACH#phone_line#<new>#t {function:'maintenance', route:{kind:'clara'}}; INS ORG#ws/ATTACH#setting#maintenance.intake_route#t {target:'clara'}; INS ORG#ws/ATTACH#schedule#maintenance#r1#t {kind:'primary_secondary', members:[…], window:'17:00-08:00'}; leasing line untouched
0 / 0 / 0†
"Add line: maintenance" on the company
END the route key → press 3 goes back to external
14 †
Situs function-first IVR (one public number, four branches, outsourced on 28 of 30, two emergency lines by asset type)
assumed (B4 row 8 lists the branches, not the DIDs; the dated check below decides): the IVR forwards each branch to a distinct DID → one HEAD + attachment per DID that lands in PropFlow (leasing, maintenance); a branch that shares a DID = FN#<function> rows under one HEAD — the same rows either way; the commercial branch in v1 is a DID that is never pointed at the platform (A6), recorded as route:{kind:'external', target} on the map — commercial is an asset_type, not a function (Perm-2); ORG#situs/ATTACH#setting#maintenance.intake_route#t {target:'external', number: the call centre} + two PROP#/…intake_route {target:'clara'} overrides; ORG#situs/ATTACH#setting#emergency.route.residential#t, …commercial#t; asset_type=office + capability_stage=off on the two towers
0 / 0 / 0†
one routing table on the company: one row per (function, language)
a branch that turns out to be a different function = UPD the FN# row; moving one building from the call centre to Clara = one override row
15
Cross-sell switched on for a cluster (Z12)
INS GROUP#lakewood/PROFILE {kind:'area'}, GROUP#arvada/PROFILE {kind:'area'}; INS ORG#situs/ATTACH#setting#geo.areas#t {v:[{areaId:'lakewood', name:'Lakewood'}, …]} (append); INS ORG#situs/ATTACH#setting#geo.adjacency#t {v:[{a:'lakewood', b:'arvada', minutes:20}]} (append); INS PROP#/REL#in_group#GROUP#lakewood#t tags; INS GROUP#<chain>|ORG#situs/ATTACH#setting#cross_sell.rule#t {v:{enabled:true, maxMinutes:30, assetTypes:['apartment'], maxResults:3}}; cross_sell.same_owner_only stays the org POLICY row — keys, never kinds
0 / 0 / 0
draw areas; set minutes
END the rule row → absentMeans {enabled:false}
16
Lock a key at the company (office hours locked after three buildings set their own)
TX: UPD ORG#/ATTACH#setting#office_hours#t.locked = true + END the three descendant rows (an audit row each, setBy.source:'lock') — or refuse and name them
0 / 0 / 0 (≤48 descendants in the one transact — Delete + HIST# Put each, three items of the 100 spent on the lock UPD, the PROFILE ADD and the MAPLOG# row; above 48, scripts/lock-sweep.ts in chunks, lock last — A12)
toggle the lock icon; confirm the named descendants
unlock = one UPD; the ended rows are re-INSertable from HIST#
17
Reclassify a key (PARAMETER → IDENTITY)
registry edit (code, a deploy — by design, AC4) + scripts/registry-reclass-sweep.ts <key> ENDing every row now outside writableAt (dry-run, pre-image, revert runId); widening = registry edit only
0 / 0 / 1 deploy (the registry is code)
none — engineering
a skipped sweep is a loudtier_illegal_since_v<N> refusal on the first read, never a mask
18
Undo the last map change (X4 — the universal reversal)
read ORG#/MAPLOG#<v>; one TX applies its reverse[] items at v+1 (a Deleted row is re-Put from HIST#, an INS is moved to HIST#, an UPD restores the pre-image) with a ConditionCheck that no later MAPLOG# touched the same keys — else a named refusal ("v43 changed Ember Lane's line after this"); the undo writes its own MAPLOG# line
0 / 0 / 0
"Recent changes" → the row → Undo
undo of an undo is the same click
Boundary stated once: a new placement or value is data; a new key or kind is code. Every other row above is writable through getRepository(ctx) with no deploy.
What the operator clicks — three worked examples (the experience lens)
The 2026 "screen" is the B6 fixture notation plus validate-fixture and bin/load-fixture, typed by Fede for the two onboardings (P2); the 2027 screen is the org map with the same actions as forms. The three examples read the same on either — a text form has the same rules, with worse feedback, which is why the validator ships first (X7).
1. Onboard Western Slope in one sitting — seven screens, about two hours (≈ a day for Situs; the classifier questions are the long pole).
Create the company.createOrganization → ORG#ws/PROFILE {mapVersion:1, valuesVersion:1}; MAPLOG#1.
Import the PMS export, answer the classifier. 14 records → the split is shown ("record 7 is two homes: 11 Elm and 14 Elm — one building each?") → 16 × PROP#/META {organizationId: ws, lifecycle:'active', ROSTERPK: PROPORG#ws, answerable:{leasing:true, maintenance:true}}. The roster exists before any line does.
Property owners. Situs and the LLCs as parties; per property owner one question in plain words — "pooled leasing hosts may show these homes: yes / no" — writes owned_by {grants:['reports','financials','occupancy','work_orders','documents'], poolingConsent?} — the decided default, with 'conversations' left out unless asked for. A "no" changes who hosts, never who is answered (A2/X1).
Claim the Western Slope leasing line and the mailbox on the company card. The claim refuses and lists the four keys it needs: timezone, jurisdiction, emergency phone, maintenance route (rule (i), X2). Nothing rang; nobody heard a half-configured Clara.
Set the four keys (org.timezone, org.jurisdiction, emergency_phone, maintenance.intake_route = {external, 000-400-…}) → four ATTACH#setting# rows on ORG#ws; valuesVersion 1 → 5.
Connect the one calendar as the person who owns it → CRED#c1/SECRET + PERSON#/ATTACH#calendar#primary + ORG#ws/ATTACH#schedule#tour#r1 {calendar_owner}.
Claim again — created.ADDR#voice#+1000…/HEAD {ws, ORG#ws}, the phone_line row, mapVersion++, MAPLOG#; go-live gate green. The ledger at the bottom of the screen reads inserted 31 · updated 0 · moved 0.
2. The Fruita call — Clara on a company line, start to tour (what runs at step 4; before step 4 the same call answers company facts and takes a message — A8).
Ring → HEAD (ORG#ws) → PROFILE → roster Query (16 ids + stamps; reach = 15, one home is lease_up with capability_stage.leasing = observe) → DVs: home_state:'unknown', home_picker:'area_menu' (|reach| > 6, X6), available_homes grouped by area (X3). Clara: "Grand Junction, Fruita, Clifton or Ridgway?" — "Fruita." — "Two are available in Fruita: 372 Ember Lane, 3 bed, and 18 Peach Court, 2 bed." — "Ember Lane." → tool get_available_units{home_ref:'372 Ember Lane'} → mintFromConversation (VOICECALL#/SCOPE) → bindHome inside reach → one attribute update on ORG#ws/CONV#<id> (propertyId, GSI8PK) → slots from findTourCandidates (the one calendar, 30-minute buffer) → booked. Camellia's call beside it is the same agent, two frames shorter, byte-identical at the rendered layer. Ledger: inserted 3 (CONV, TOUR, HOLD) · updated 1 · moved 0.
3. The wrong drag — a mistake and its undo.
The operator attaches the new number to Ember Lane instead of Elm Street. The map shows the chip on the wrong card; "Recent changes: v42 — 000-400-… attached to 372 Ember Lane (Fede, 10:14)". Click Undo → v43: one transact applies MAPLOG#42.reverse[] — Delete HEAD (cond scope = PROP#ember), move the phone_line row to HIST#, mapVersion++, MAPLOG#43 {undoOf: 42} — refused, by name, if anything touched those keys since. Then attach to Elm Street → v44. Three lines in the log, every row keeps its startedAt, nothing masked, nothing lost. Ledger across the three clicks: inserted 4 · updated 3 · moved 0. This is invariant (h) and P3 as one story.
The version stamp as a story (for the human page): Fede locks office hours at the company at v41 while the Director of Ops is typing Yale's hours against v40; her save is refused naming v41 and the lock ("locked at Situs since v41 by Fede"); nothing is masked, nothing is lost, she reloads and sees the lock icon. Elementary: two people cannot both hold the pen. College: expectVersion + ADD mapVersion + invariant (h).
Every shape as rows, and the 20 PMS shapes
Every shape as rows
Notation: HEAD(ch, addr → node[, fn]) · att(node, kind#slot, value) · rel(prop, kind → party {attrs}) · role(person, tier @ scope) · tag(prop → GROUP#g{kind}). Every fixture loads from the B6 notation with zero setter scripts and git status clean.
Shape
Literal rows
Refused?
S1 / F01 / M1 Camellia (the N5 control)
PROP#cam/META {organizationId: jpco, ROSTERPK: PROPORG#jpco, answerable} · HEAD(voice,+1844… → PROP#cam), HEAD(sms), HEAD(email, leasing@cam → PROP#cam) · att(PROP#cam, phone_line#+1844…, {both, primary, function:'any'}), att(PROP#cam, mailbox#…), att(PROP#cam, calendar#primary {credentialId}) (lifted from leasingCalendar), att(PROP#cam, setting#escalation.owner) (lifted from the column) · zero groups, relationships, assignments, rules. Every key resolves at PROP#cam or absentMeans = today's constant; |reach| = 1 ⇒ home_state:'known'
no
S2 / F03 / M3 Centralized (Situs shape)
ORG#situs · one HEAD per IVR DID (leasing, maintenance) → ORG#situs; the commercial branch is a DID never pointed at the platform (A6; commercial is an asset_type, Perm-2), recorded as route:external on the map — an FN# row only if the dated check proves ring-time redirect · att(ORG#situs, mailbox#leasing@…), att(ORG#situs, pms_credential#any {database: situsgroup}), att(ORG#situs, setting#office_hours), att(ORG#situs, setting#maintenance.intake_route='external') + 2 building ='clara', emergency.route.{residential,commercial}, org.timezone, org.jurisdiction · 3 × att(ORG#situs, assignment#L{1,2,3}#leasing) + 3 × att(PERSON#L*, calendar#primary {provider:'propflow'}) · att(ORG#situs, schedule#tour#r1 {round_robin, inAreaBonus}) · areas Lakewood/Denver/Arvada as kind:'area' tags + geo.areas/geo.adjacency key rows · the import classifier mints one PROP# per home/community/tower = records − shells − placeholders − notUse (the Situs export reads 71 records = 37 homes + ~23 communities + 2 towers + 11 zero-unit LLC/fund shells + 1 NOT USE entity + 3 "DO NOT USE" placeholders → ~62 buildings with the towers among them; the catalog's "59" is a raw record count and is not asserted), 33 ORG#{owner_party} rows (Situs's 33 property groups) + 142 owned_by {share} (the 142 memberships), 0 shells and 0 placeholders as buildings; the two towers asset_type=office, capability_stage=off
towers/shells: known_out_of_scope, not not_found
S3 / M24 Western Slope (14 PMS properties / 16 homes / 4 towns)
ORG#ws · HEAD(voice,+1000-400-0970 → ORG#ws), HEAD(sms), HEAD(email, leasing@… → ORG#ws) · att(ORG#ws, phone_line#+1000… {primary}), att(ORG#ws, mailbox#…), att(ORG#ws, calendar#primary {credentialId}) (one staff calendar, one token, plan G5), att(ORG#ws, schedule#tour#r1 {calendar_owner}), att(ORG#ws, setting#tour.travelBufferDefaultMinutes=30), org.timezone, org.jurisdiction, maintenance.intake_route={external, 000-400-…} (press 3 stays with the call centre) · areas GJ/Fruita/Clifton/Ridgway as tags (adjacency empty week 1) · 16 × PROP#/META (one home = one building, D9-A; the umbrella drained) each with asset_type ∈ sfh|duplex|townhome|apartment · rel(<GJ homes>, owned_by → ORG#situs {reports}) — no poolingConsent, and the homes stay in reach (A2: the caller who names one is answered; its tours are hosted by building-scoped hosts until Situs says yes — harness consent-never-gates-reach), others → ORG#<llc>{owner_party} · att(ORG#ws, setting#cross_sell.rule {enabled:true, maxMinutes:30}) + att(ORG#ws, setting#cross_sell.same_owner_only=true) (W7-A) · att(ORG#ws, setting#listings.feed_url) (C15 → data). Commercial/industrial/medical and the third-party Ridgway stock imported lifecycle or capability_stage=off
as S4's last line: two Person rows, one PLINK#, two calendar rows to one external calendar; org B's scheduler reads free/busy only; tour rows never cross; T7–T10 hold
no
S8 / M24 400 scattered homes
400 × PROP#/META {…, answerable}; four org attachments (line, sms, mailbox, calendar); GROUP#<town> {kind:'area'} tags (+ a chain group only where a town shares a line); geo.areas filled so the two-step ask has towns to offer (home_picker resolves area_menu above six, X6; search waits for tools, A8) — "the size is a number": cold mint = 6 reads, ≈50 KB; reach from stamps, 0 per-building reads
no
S11 The 400-home ring — the read count as a picture (human surface)
two columns, same call. Naive: HEAD 1 · PROFILE 1 · roster 400 rows with tokens · 400 stage lookups · 400 owner lookups · chain 3 = 805 reads, ~2 MB. As designed (A1/A4/A5): HEAD 1 · PROFILE 1 · roster 1 Query, 400 ids + stamps · chain ≤3 · reach from stamps 0 = ≤6 reads, ≈50 KB. The number is the argument
—
S12 The number that came back (human surface)
a company releases 000-400-…: one TX = DEL ADDR#voice#…/HEAD + DEL FN# rows + move the phone_line row to HIST# + mapVersion++ + MAPLOG#; ADDR# holds no ended rows, so the next company's claimAddress lands in an empty partition — the edge carries nothing of the previous org
second claim before release: refused
S13 The provenance tuple, rendered (human surface)
one building, one key (office_hours), three rows on the page: company (locked) · group · building (masked, greyed, "masked by Situs since v41") → {value: 9–6, setAt: ORG#situs, locked: true, maskedBy: [GROUP#downtown, PROP#yale]}; the same key on a building whose company has no lock → {value: 10–7, setAt: PROP#yale, locked: false}
—
S9 / F09 / M9 + M12 Acquisition; one login, two companies
phase 0: two orgs + owned_by ×4; the Situs President and the Director of Ops: PERSON#x_situs + PERSON#x_ws, two MEMBERSHIP# rows → activeOrganizationId picker; two pms_credential attachments, two CRED# rows (K14 closed). Phase 1: catalog #7
no
S10 / M15 Joint venture
rel(bldg, owned_by → ORG#a {share:0.6}), rel(bldg, owned_by → ORG#b {share:0.4}); one runner (Property.organizationId); a second runner is refused by construction (one attribute); "two Claras on one address" = two PROP# rows over one address (the transfer primitive, catalog #8)
second runner refused
F04 Rotating pod
S2's three assignments + schedule#tour#r1 {round_robin, fairnessScope:'person@node@function'} + three PERSON#/ATTACH#calendar#primary (providers may differ; hold_only for the read-only provider) + LRB# rows
no
F08 Owner-operator adds #2
catalog #2; phase_0 = F01 rows exactly; phase_1 = INS only; organizations.json exists on the json backend (step 0)
no
F10 Knowledge + region overrides
att(ORG#np, setting#greeting|application_fee=45|screening_policy), att(ORG#np, setting#fair_housing_text {locked:true, POLICY}) → a write at PROP#r3 refused at write; GROUP#south {kind:'chain', precedence:1} (it holds settings) + att(GROUP#south, setting#quiet_hours='22:00-07:00'), att(GROUP#south, setting#escalation.owner=south-ops@…); GROUP#north {kind:'region'} (a label); att(PROP#r4, setting#application_link) IDENTITY — r1 without one refuses, never inherits; role(rm_south, regional_manager @ {groupId: south}) sees r4, r5 only
The ledger every scenario ends with (human surface, experience lens):inserted · updated · moved — and moved is always 0. Seeing the zero appear at the end of every play-button walkthrough is what "never migrate" looks like to a person; the harness's rebind-line-inserts-only is the same three numbers as an assertion.
condition {leaseFormId} reserved on value rows; lease terms live on the Lease row; notice_period floor is a floor
deferred
1.12 HUD / LIHTC
COMPLIANCE_FLOOR + condition {program} reserved
deferred
1.13 student
—
silent (no customer)
1.14 portfolio transfer
catalog #8 (new PROP#, old frozen)
no
1.15 firm acquires firm
catalog #7 staged
no
1.16 one human, two firms
two rows + PLINK#; org-first login
no
1.17 two front doors
kind:'brand' tag with greeting/identity.senderDomain/identity.callerIdName
no
1.18 asset manager above N managers (UDG)
ORG#udg {customer} with capability_stage.* = off; managers as manager_party orgs (or their own customer org) that run the buildings; UDG's primary view is ReportsContext over pushed rows
no
1.19 multi-PMS
pms_credential#<role> slots + pms_external_id map by account; accountId end-to-end
no (rows deferred until Yale's second PMS is wired)
1.20 sandbox in prod
purpose:'sandbox' org; provenance:'test' key; the lab lives inside the customer's org that owns the credential
no
1.21 lifecycle / capital event
lifecycle IDENTITY consumed by answerable() and nearbyAlternatives
no
A10 · The adversarial review — twelve rounds against a 9.0 bar
An outside model was pointed at this design and told to break it. It could not overturn the shape — it made the specification five times larger, and the founders answered five questions along the way.
What this section is, and what it is not. The scores below are one model’s independent judgement against an absolute bar, not a measurement. Each round was graded by a fresh critic that was never told any previous score, so the series is honest but noisy — roughly ±0.2. No round reached 9.0, and none was manufactured. The design set lives on the branch astra/arch-review; this page is the readable half of it.
The series
Round
1
2
3
4
5
6
7
8
9
10
11
12
Score
6.1
7.8
8.2
8.2
8.2
8.4
8.2
8.4
8.3
8.6
8.6
8.4
Read the flat middle correctly. It is not a plateau of the same defects — the finding set rotates. Round 11’s critic opened by crediting the design with deterministic ordering on the calendar‑selection and outbound‑sender questions round 10 had raised — then raised three new findings a level deeper. It did not return to round 10’s third finding (export progress) at all, so that one is unrepeated rather than confirmed closed. The stronger evidence for the pattern is that no finding recurs across the twelve reports, and that the eleven revision rounds added 5,913 lines while removing 6. Each round genuinely closes what it was given and the critic descends. On a set of 539 stress cases a cold critic will nearly always find three things, which is why 9.0 may be unreachable by construction — and why the honest stopping point is “no blocker survives a round”, which has been true since round 10.
What the review actually changed
Before
After
The three founding ideas — the customer org is the wall, everything attaches to the building, nearest‑wins resolution
Unchanged. Twelve adversarial rounds and an independent second review failed to overturn the shape. The second review’s words: “not one of the eleven cases required a different model.”
The target rows (want.md)
361 lines
1,962 lines
The record
967 lines
3,266 lines
The stress bench
272 cases
548 cases
Who sets policy
Whoever holds the building in our system — in practice, whoever pays us
The managing firm, and as of 9 Sep it sits below the building on a management scope
That last row is the one genuine reversal in the whole effort, and it came from the founders, not the reviewer. The reviewer’s contribution was proving the old model inverted the team’s own rule at a real property: the owner could lock a policy and the manager was refused at write.
Where the design stands, stated honestly — and why the headline number is not the number to read. Two full adjudication rounds have now run over the 548-case bench. The second returned 430 PASS, 112 PARTIAL and 6 FAIL, with zero control breaches — not one case in which a control this design claims was found broken. The split is what makes those counts mean anything. Case ranges whose cases were written alongside the rules they test come back at 0–8% non-PASS; ranges written before this design existed come back at 40–51%. So the headline rate is inflated by construction, and the independent ranges — not the aggregate — are the real signal. That is why this page prints the counts and the split rather than one overall pass percentage: quoted bare, such a number would flatter the design by a wide margin. What the failures do not do is block the near work: no FAIL gates P0, P1 or P2. All six land in later phases, and each is carried on the bench with the step that closes it.
The five rulings the founders made
Ruling
In the founder’s words
What it changes
D‑0909‑1
“Authority follows the manager.”
The managing side sets the building’s policies. The owner cannot force one.
D‑0909‑2
“The operator — whoever runs day‑to‑day through PropFlow.”
When two companies both pay and both work one building, the operator is custodian.
D‑0909‑3
“It should be baked into the strategy from day one … put it in the early phases.”
The read‑side isolation work moves early, ahead of go‑live.
D‑0909‑4
“Strictly just one property management firm behind a new property … that firm will determine who takes precedence … the firm should be able to kind of delegate those rules.”
The firm is the rule book: it sets policy and which fields a building may override.
D‑0909‑5
“Keep one prop and then add a management scope beneath it … another property in the same building would be a separate property row.”
One property with split management is one row with scopes beneath; two co‑located properties are two rows.
All five are direction, not specification — the founder’s own framing was that he is “not very confident” in them and wants the structure derived so the answers fall out of the architecture rather than being bolted on as special cases.
What the firm rule book added
Five new contracts came out of those rulings. MANAGING_CUSTODY — one firm appointment, independent of payment. FIRM_GRANTS — permission precedes every lower policy value, so no value can exist below the firm’s rung without the firm’s explicit grant. FIRM_REQUEST — a locked field can start a real request to the firm rather than dead‑ending. FIRM_ADDRESS — one firm command controls phone allocation and reach. DEPOSIT_TERMS — typed formulas, with the decision explicitly still owed.
One consequence reaches the screen. For a property page to show which fields are unlocked, the resolver must return not just the value but whether this rung may write it — and it splits that into two signals, because collapsing them would make the padlock lie: rungOverrideAllowed (does the firm permit this rung to override, independent of who is looking — the green indicator) and mayWrite (that, and the actor’s own grants).
Still open — and deliberately so
One item left this list on 2026-09-10 (b89007320, b89007922). The discriminator’s own edge — two co‑located properties told apart by address, where mixed‑use routinely shares one street address — is answered: two properties may share one street address, and they are told apart by proving their scopes do not overlap. The address is the label a person reads, never the identity key, so writing “100 Main St (retail)” changes no identity and creates no row. Units inside a property keep the discrimination they already have; this ruling is about the layer above them.
Deposit value shape. A deposit is $200 for a one‑bedroom and $300 for a two‑bedroom in one building, a percentage of rent in another, and higher for single‑family homes than apartments. The settings model holds one flat value per building and cannot express any of it — verified, not assumed: the mechanism that would (condition) is marked reserved, used by no key, and its declared kinds are lease form and program, not unit type. On the decisions surface, unanswered.
Does a split building share its phone number, and whose quiet hours reach a resident who can hear both? Nobody has answered either.
The reviewer was told the score is not the goal, in the founder’s words: “they might get built 100% the best plan in the wrong direction … we want the best plan in the RIGHT DIRECTION.” It took the instruction — round 12 argued against the founders’ own fresh ruling, showing that the one‑firm rule was being enforced over physical buildings rather than management scopes, so a mixed‑use block run by two firms was refused outright. That objection became ruling D‑0909‑5 above.
A call on a company line
A call on a company line
G1, the only decided item, expressed on the rows. Western Slope's leasing number rings; Camellia's line follows the same code path with the home known at pickup.
Figure 5. A Call on a Company Line. The ring-time edge is one uncached GetItem on the address head; a miss refuses with zero further reads. The company's profile, roster and chain rows follow, the call's scope is written as a TTL row, and the dynamic variables carry property_id = '', home_state = 'unknown', the four areas and then the homes, and available_homes as data — the same agent id on every line, no second agent, no branch in code. The tool webhook mints from the scope row rather than trusting anything the model filled; bindHome resolves the named home inside the reach and is the only writer of the conversation's home, as an attribute update, never a move.
Born where
mintFromAddress('voice', '+1000…') → HEAD → PROFILE → one roster Query (16 ids + answerable stamps) → {organizationId: ws, scope: ORG#ws, propertyId: null, reach: 16 answerable homes, roster: 16} — six reads cold, two warm, none per building (A1); the ring webhook writes VOICECALL#<system_conversation_id>/SCOPE {organizationId, scope, function, mapVersion} (TTL 24 h). The conversation is born in the front-door partitionORG#ws/CONV#<id> with organizationId, scope: ORG#ws (immutable), propertyId absent, GSI1PK = PHONE#ws#<e164>, GSI5PK = CONV_RECENCY#ws, GSI6PK = ws#<personId>; CONV#<id>/REF {PK}. Camellia's line: HEAD names PROP#cam, |reach| = 1, born in PROP#cam/CONV# exactly as today.
The unknown-home state as injected data
The personalization webhook emits, on every line, organization_id, property_id ('' when unknown), home_state: 'unknown' | 'suggested' | 'known' ('known' for Camellia; 'suggested' when the in-org caller-ID occupancy names a home on a maintenance line — confirmed by the caller, overridden by an asserted address, never asked cold), home_options (the reach as [{ref, display}] when |reach| ≤ 6; above six, the two-step ask (X6): home_areas from geo.areas + the area tags first — "Grand Junction, Fruita, Clifton or Ridgway?" — then the homes in the chosen area; home_picker:'search' waits for tools, A8), available_homes (X3): the answerable homes that have an available unit, from the listings feed / UNIT# rows, grouped by area tag, capped and sorted — code, no tool, no LLM choice — so the most common company-line call, "what do you have in Fruita for two bedrooms?", has something to hear, and the pin is a normal bindHome on the home she picks (the leasing twin of nearbyAlternatives), and company-level context from resolveEffective(ctx, [office_hours, org.timezone, greeting, transfer.*, emergency_phone, maintenance.intake_route, geo.areas], ORG#ws) — every one of which resolves at the org path (FF pre-pin-keys-resolve-at-org). The "ask which home" block is keyed on property_id === '' and rendered as data; the same agent id on every line, no second agent, no branch in code (study assertion #6). N5 for voice is redefined at the rendered layer: rendered prompt identical; DV values identical for every pre-existing key; an explicit additive allowlist (organization_id, home_state:'known'); one admitted re-baseline of the contract test's set-equality (voice-personalization-contract.test.ts:20-30) at step 4, run against a sandbox agent before the fleet.
Which agent, and when (A8, said plainly)
The v1 front-door agent (AS1) has no tools, so before step 4 a call on a company line answers company facts from the injected blocks and takes a message — no home is bound, no tour, no work order.bindHome, the tour and the WO arrive with step 4, which gives the shared agent tools; that supersedes AS1's "no tools" for the front door and is its own decision line (decision 15, Fede). Mailbox leads (W11) carry an address and are unaffected.
Tool contract
Every home-taking tool gets home_ref (name or address, model-filled) and organization_id (a cross-check only); property_id leaves required[] on the ten leasing schemas (src/lib/tools/leasing.ts:71-1168; POC-A did two) and becomes optional-and-ignored-for-authority; a drift guard fails CI on any voice schema exposing a model-fillable opaque id (P19, K15). The tool webhook mints with mintFromConversation (VOICECALL#/SCOPE → else REF.PK); tool-helpers.ts:88-92, 116-128 ("parameters outranks the dynamic variable for property_id") is deleted in the same PR. bindHome(ctx, homeRef) resolves inside ctx.reach — exact-after-normalization, ambiguous → ask, unknown → "I don't have that home" (indistinguishable from foreign), known_out_of_scope {reason, route} for a tower on the leasing branch (transfer, zero Clara turns about leasing) — and is the only writer of conversation.propertyId + GSI8PK (an attribute update on the existing meta row; a drift pin on its importers). identify_caller returns identity and never binds (voice/tools/[tool]/route.ts:708-721 fill-in deleted); resolveIdentity returns {personId, role, homeRef} with role from occupancy.role (Perm-4), so home_state:'suggested' carries who this person is at that home — a guarantor or an authorised contact is never treated as the tenant of record. WO/tour rows born from the thread are keyed PROP#<home> as today (Fede 30:17 "you can't tour a portfolio"); create_work_order refuses without a home and files a message to escalation.owner at the org.
What Clara may do before the home is known
Answer org-level facts from org scope (E7: property-varying facts ask to pin or say "it differs by building"); save a lead with propertyId absent (N11 — prospect@{organizationId} roles are legal, person-roles.ts:161-163 widened); text a receipt from the org's primary address (resolveOutboundAddress); quiet hours and office-closed on org.timezone until pinned (AS7 says the property's clock — the org key is the interim, inheritOnlyWhen:'home_unknown'); the compliance clamp uses the stricter of org.jurisdiction and any candidate home's; emergency → whoIsOnCall(ORG#, 'emergency') / emergency_phone at the org, emergency.route.residential until the asset type is known; work orders and tours refuse until pinned.
Thread matching, text and email
canonical-conversation.ts:114 (c.propertyId !== ctx.propertyId → false) becomes org-first: same organizationId ∧ same person ∧ (same property ∨ either side unbound); candidates come from GSI1PK = PHONE#<org>#<e164>, so foreign threads never enter the process (case 18 green); grouping key (personId, organizationId) with the home as an attribute (Q13). call-ended re-derivation reads VOICECALL#/SCOPE (never the address) so it cannot undo a mid-call bind (E22); a legacy meta row without organizationId is not a candidate (fail-closed) and is counted by the K16-style tripwire until the backfill completes. OutboundThreadLog gains the org-level address as a key.
Same mint for text and email: mintFromAddress('sms', to) (HEAD only — SMS carries no function; the line's function:'any'; a text whose classified intent maps to a function whose intake_route is external — "my toilet is overflowing" to a company whose maintenance is the call centre's — gets the scripted hand-off reply with the target number and zero intake turns, sms.function_fallback (A6); a life-safety verdict still takes the lane first), mintFromAddress('email', toMailbox) — the SES lane finally has a channel identity to claim (N12); the shared mailbox resolves the home from the "New Lead for <address>" line inside ctx.reach (W11-A; parseLeadEmail must pass subject), unmatched → a lead under the person with propertyId absent + a human task; the >1 → drop rule (webhook-processors.ts:398-427) and first-match matchers (match-property.ts:48-65, 87-104) die with the HEAD row. Outbound: resolveOutboundAddress(ctx, channel, path, function) = nearest primaryForScope walking up (a building with its own number sends from it; else the company line); a reply goes out on the address it arrived on.
A play scenario the design has right and never showed (X10): a prospect texting mid-thread on the day the Western Slope leasing line moves from the umbrella to the company (catalog #1) keeps her thread — same org, same person, one side unbound — and Clara's reply leaves on the same number; the conversation row never moved, the match is org-first. And the per-building list (A9): the property's Conversations tab, the PM inbox and per-building insights read GSI8 CONVPROP#<pid> — the thread above appears there the moment it is bound, although it lives under ORG#ws.
The umbrella's retirement (K16)
PROP#western-slope-proto (16 UNIT# rows; provision-western-slope-proto-property.ts:6-10) stays the prototype's home until step 5 lands. Rule (P18): no lead is written into it without a re-key plan on file — scripts/rekey-umbrella-rows.ts (dry-run, pre-image, revert runId; enumerates CONV#, TOUR#, PROSPECT# SK prefixes) is written by Gera, 1 day, on file by Sep 19 (P4 — "Sep 12" is not real for a script nobody has started in a week both founders are on the onboarding) and is a rehearsal until the ORG#ws front door has readers (step 5a) — "written before the first lead" ≠ "runnable before"; a dated count of rows born under the umbrella per day runs from now (one cron, the K16 tripwire — Gera, ½ day, Sep 10). Texting on the shared line stays off until the life-safety lane split has landed (step 3) and the SMS lane's subset of OPEN sites is 0 — not the global count. Week-1 leads are mailbox leads and land under the person already; tours land TOUR# rows on the one calendar attached to the umbrella row, enumerated by the same script. Then: releaseAddress the umbrella's HEADs (re-pointed to ORG#ws by catalog #1), lifecycle:'placeholder', archive.
The ladder, and what it deletes
Getting there
Each step: expand (rows written from today's fields, read by nothing) → parity gate (an offline replay over recorded inputs, mismatches = 0 published in results/<sha>.md) → one facade cut per key or per building behind the existing fail-closed switch shape, Camellia last → TS strip + tombstone drift guard. Rollback at every step is a flag while writes stay on the old rows. Days are engineer-days derived from the attackers' recounts at HEAD (IDataRepository-family surface 294 methods; 383 delegates in data/index.ts; 52 non-dynamo raw-helper importers; 144 bare getProperties(); 68 escalationOwnerEmail reads in 25 files; 51 getPropertyKnowledge; 55 getPropertyPhone; 22 getPMSClient; 30 convMetaPK sites; 4 CONV# fan-outs; a 23-file agents/clara/lib/data/dynamo mirror; 182 Property literal constructors; personalization/route.ts 2,305 lines). The parity gate is an offline replay over recorded inputs (scope-parity-replay.ts, read-only against stage or prod snapshots); it never serves a production read, and at no step do two live read paths exist. "Dark" is labelled honestly: dark = no reader changes; dark reads, live attribute writes = stamps on live rows; flip = a declared per-building or per-address switch.
Sep 12, stated honestly. (1) What ships Friday = Western Slope leasing on existing rails — W11 mailbox address routing (a parser change + exact address match), one calendar on one property row (the umbrella), the Western Slope leasing line answering leasing inbound on voice and text (texting on, decided Sep 7; leasing only, guest cards from prospects, no tenant support) — plus the K16 born-row tripwire (one cron — Gera, ½ day, Sep 10) and rekey-umbrella-rows.ts on file (the dry-run enumerator — Gera, 1 day, by Fri Sep 19; a rehearsal until step 5a) (P4). (2) Nothing from the ladder is required or helpful for Friday. (2b) Fede's frame, Sep 7: week 1 must itself work in the centralized-leasing setup, and the next three weeks — Mon Sep 14 to Fri Oct 2, working day 14 on the calendar below — build on it: steps 0 and 0.5 done and 1 nearly at the low bound, 0 and 0.5 at the high; the shared line's flip keeps its November dates; the Western Slope agent's PMS data and tour tools ride the umbrella row's per-property path meanwhile (decision 10). (3) The calendar below, every date a working-day count from Tue Sep 15 (weekends skipped, no holidays; low bound / high bound), printed once so nobody re-derives it by hand again — the retracted "Oct 6–17" happened because nobody drew it. Steps 0–2 (20–28 working days) end Oct 13–23; Western Slope's shared line needs 3a, 3b, 4, 5a (26–39 more days) → Nov 18–Dec 17 at one engineer in the page order, Nov 5–27 under the voice-first reorder (a proposed option with its cost named, decision 17), Nov 6–Dec 1 with many hands on the mechanical steps under Gera's review (decision 16). Situs maintenance needs 3a–4 + the Situs slice (maintenance.intake_route / emergency.route.* pulled forward from step 7, whoIsOnCall from step 8, accountId end-to-end) → Nov 19–Dec 21, proposed (P10). Camellia is the control at every step and is flipped last.
Figure 6. The Dark Ladder. Sixteen steps (0, 0.5 for the proof apparatus, 1, 2a/2b, 3a/3b, 4, 5a/5b, 6a/6b, 7–10 — the split halves share a rung in the picture), 83–121 engineer-days summed from the table below; committed to the proof and the shared line = 0–6a (50–73), backlog 33–48. Steps 0–2 touch no live path and run Tue Sep 15 → Oct 13–23. Western Slope's shared line needs 3a through 5a and lands Nov 18–Dec 17 at one engineer in the page order, Nov 5–27 under the voice-first reorder, Nov 6–Dec 1 with many hands on the mechanical steps. The bar is the low bound drawn to scale (11 px per engineer-day); the high bound is 121. The Sep 12 line at the top is what Friday actually needs, and it is none of this.
#
Step
Owner
Done means
Proof it changes nothing
Rollback
Head / hands
Days
0
ORG# rows for every org id in use, ONE registered writer (ADR-0079) exposed as the product path createOrganization — the first screen of org-first onboarding (N8); Organization.purpose gains owner_party | manager_party | brand_party; Property.organizationId required-at-write (construction invariant in the writer) + the new sparse PROPORG GSI (INCLUDE {lifecycle, answerable, asset_type}, scripts/add-proporg-gsi.ts, the fifth precedent) + ROSTERPK/ROSTERSK stamped BOTH by the one-time updateItemFields backfill AND in saveProperty's item literal next to GSI3 (A3/A4); Property.lifecycle + the answerable stamp (A1); json backend organizations.json; mapVersion + valuesVersion on PROFILE; the F01 fixture loader
Gera (rows, stamps, the GSI, the loader) · Fede (F01's content — the fresh Camellia-shaped fixture, never the Willows)
getProperties(orgId) result set === Query PROPORG#<org> for every org (parity drift test); a full saveProperty round-trip keeps the stamp; the index carries no leasingCalendar attribute (FF roster-index-carries-no-secret); F01/F03 load on json
nothing reads differently — dark reads, live attribute writes
drop the stamp reader
one head
5–7
0.5
The proof apparatus: PR #7164's contract gains expect: 'RED' | 'GREEN' + closesAt and a GREEN verdict (exit 0 iff every case's verdict equals its expect; flipping expect is the PR that lands the step); a module-private read ledger in the json store keyed by PK prefix (_readLedgerForTests()); scripts/scope-parity-replay.ts --step N with its own credential posture (the pms/coverage.ts:158-167 shape; read-only against stage/prod); the json twin's conditional-put emulation (in its own file, store-conditional.ts, so Gera's emulation and Fede's fixtures never share a diff — P3); dependency-cruiser replaced by rg-backed drift tests in the house shape (l4-core-no-propflow-imports.drift.test.ts); validate-fixture <file> (X7): prints, without writing, every refusal the loader would raise — unregistered key, pickup keys unset for a claimed line, consent unanswered, a shell about to become a building — and the counts import-classifier asserts
Fede — the harness contract, GREEN polarity, fixtures F01–F10 and the sandbox org (Zoom 28:21 "I'm building a harness right now that we can reuse"; next-steps "Build a harness/testing framework" and "Create a new, clean set of properties"; dilemmas §3 W-G) · Gera — the store-side instruments only: _readLedgerForTests, scope-parity-replay.ts, store-conditional.ts, the rg drift tests, validate-fixture. Sequenced (P9): Gera's instruments land first (they block nothing of Fede's); Fede's contract + polarity by Fri Sep 26, after Western Slope's first week
every §11 row names an executor that exists; validate-fixture on S3 prints the X2 refusal
—
—
many hands (two owners, two files)
4–6
1
transactWrite gains Update, CancellationReasons and ClientRequestToken (first commit, A13); step 2a's registry tables are the second commit (P5 — the backfill's setting rows validate against them); attachment (with the HIST# move on END, A11) + HEAD + CRED#/LEASE (A7) + relationship + group + MAPLOG# (X4) row kinds, interfaces, JSON twin, and the agents/clara/lib/data/dynamo mirror (check-data-layer-drift.sh); backfill from the carrier-numbers column + PHONE_TO_PROPERTY_MAP → HEAD/phone_line, propertyEmail/inboundEmailAddresses → mailbox, leasingCalendar → CRED# + calendar, escalationOwnerEmail → setting#escalation.owner, ownerId+the PMS account column → pms_credential#any (+ CRED#) — read from the two config files only after Fede's last mapping edit (a dated hand-off, P3); dumpOrgMap. Moved out (P6): the config-file dumps + platform adapter → 3b's flip (the leasing number is already mapped; the first † mutation says "+ 2 PRs" until then, honestly); accountId end-to-end → the Situs slice
Gera
under the GREEN contract (P9): drift test: new lookup vs old for every address in the five lockstep files = 0 mismatches; a second HEAD on any address → ConditionalCheckFailed (json shape + one real cancel on stage); a retried claim with the same token → created, not held_by; git status clean after a claim
dark (read by nothing)
delete the rows
one head
6–8
2a
The code tables — KEYS/KINDS/REL_KINDS/FUNCTIONS with every existing key PARAMETER + today's constant (a provable no-op), condition reserved, TRADES closed; lands as step 1's second commit, before the backfill writes a setting row (P5 — the write path validates against KEYS[slot].schema, so the registry cannot come second); Gera signs the class list against this PR (decision 2)
Gera
registry-closed-and-typed green on the fixture export; zero rows change
dark
—
one head
1–2
2b
getScopePath (precedence order); resolveEffective with the two caches (A5) in offline parity replay on key #1 = vendor_roster against resolvePreferredVendors's 18 pinned assertions, then escalation.owner (two-phase absentMeans)
Gera
byte-equal on every real company and building; mismatch count published
dark
—
one head
4–5
3a
Helpers go private: the 52 non-dynamo importers of dynamo/helpers move behind repository methods; raw primitives module-private with an importer-count pin; mint.ts with the six minters; LifeSafetyContext split (text lane). Not on the voice critical path (P7): the structural guarantee, needed before texting
Gera
importer count = the repository files only; OPEN_SITES_TODAY unchanged
dark
—
many hands (52 importers are mechanical under the importer-count pin) · one head for mint.ts
4–6
3b
getRepository(ctx) facade (callers change one line); the four new repos require ctx; legacy readers gated at the four mint sites; inbound + outbound flip to HEAD per address (a building/company line with no HEAD resolves the old way), Camellia last; the two config files become dumps + claimAddress performs the platform assignment (from step 1, P6); claimAddress refuses until the pickup keys resolve, endAttachment names a dependent line (rules (i)/(j), X2); resolveOutboundAddress replaces the inversion (55 sites); machine-sender suppression re-derived from HEAD rows; caches: ADDR# uncached, PROFILE per resolve, propertyOrgCache deleted; the vendor lane and pm-call-context.ts mint first; OPEN_SITES_TODAY shrinks per directory (SMS lane subset → 0 first). 3b′ (the voice-first slice, P7): the flip for ONE address at the three voice edges — ring webhook, tool webhook, call-ended — plus resolveOutboundAddress for Western Slope's lanes; 5–7 days, Gera to price
Gera (Fede: the per-address flip order)
T5/T6/T11a/T11b/T17 green; OPEN_SITES_TODAY only shrinks; validate-fixture and claimAddress agree on the X2 refusal
flip per address
per-address flag
one head (the flip, the facade) · many hands (the 55 resolveOutboundAddress sites)
10–15
4
Unknown-home state for voice: home_state on every line, ask block on property_id === '', the two-step area ask above six homes + available_homes (X3/X6), home_ref on ten tools + drift guard, bindHome, mintFromConversation + VOICECALL#/SCOPE, tool-helpers.ts precedence and the identify_caller fill-in deleted, the ring-time LifeSafetyContext minter + emergency-only DV set, the fail-open (personalization/route.ts:540-578) deleted with leg direction explicit (E24); the one admitted DV re-baseline against a sandbox agent. This is the step that gives the shared agent tools (A8, decision 15)
Fede (prompt, fixtures, sandbox agent) · Gera (webhook, tool contract, CI provisioning)
Camellia's line always home_state:'known'; rendered prompt + DV values byte-identical; the Camellia tool-call corpus re-validates against the additive schema; the S8 robot call picks a Fruita home in two turns
flip on the shared agent (declared)
keep property_id required on the agent
one head (the contract) · Fede in parallel from the day 3b lands
8–12
5a
Conversation.organizationId + scope + propertyId?; convMetaPK(scope) only, scope immutable, REF carries PK; org-stamped GSI1/5/6 in one bulk attribute stamp (the meta-row count is taken from a GSI3PK = Conversation count at dry-run time — the attackers' ~1,900 is an estimate, not a measurement; spine-backfill discipline: dry-run, --mode=primary on stage); the four CONV# fan-outs rerouted; the reconcile detector taught; org-first canonical match; the two unguarded re-pointers stop (tools-leasing.ts:924-945, canonical-conversation.ts:114); rekey-umbrella-rows.ts becomes runnable; the CONV_RECENCY#<org> key with its reserved shard suffix (A15)
Gera
zero partition moves counted; "no meta row at two PKs" (GSI3 vs partition count); case 18 green
dark reads, live attribute writes
readers fall back to propertyId
one head
4–6
5b
GSI8 CONVPROP# (online) and every per-building conversation listing on it — getConversations(propertyId)-shaped readers (conversation.ts:156 and callers: the property's Conversations tab, the PM inbox, per-building insights) — with the drift pin queryByPK(propPK(…), 'CONV#') = 0 outside conversation.ts (A9); ORG# Queries name a prefix (A16). After Western Slope voice is live (P6): a week-3 need, not a go-live need
Gera
every per-building list on F05/S3 equals the GSI8 set; a company-line thread bound to a home appears on that home's tab
dark reads
readers fall back to the partition (incomplete, counted)
many hands (readers under a drift test)
3–4
6a
Relationship rows (Yale gets owned_by/managed_by); GROUP# node + in_group + precedence walk (zero rows created); ActorView; PersonRole.scope accepts {groupId} and staff {propertyId} + the GSI key builder; company admins stop seeing every building (a flip with an announcement — the JP&Co principal loses the Willows bench); ReportsContext + IReportsRepository.push with the owned_byConditionCheck (A10) in the owner-report workflow
Gera (views) · Fede (permission rows, the announcement)
T7/T8/T12/T16 green; permutations 1–5; a push without a live owned_by row → ConditionalCheckFailed
flip (admins)
—
one head
4–6
6b
Org-first login with written MEMBERSHIP# rows and explicit activeOrganizationId; PLINK# + existsElsewhere for invites (A17); the JP&Co tester account; admin bypasses deleted
Gera
T13 green; permutation 6; ORG_BYPASS_ROLES gone
flip (login)
—
many hands (deletions under the guard counts)
3–4
7
Settings keys move to the resolver one at a time, each absentMeans = today's constant: escalation.owner first (68 reads / 25 files, two-phase), then hours/tour keys (14 settings files), transfer/emergency numbers (org.timezone/org.jurisdiction with them — the G1 pickup keys; the X2 claim gate is armed on this set the day they move), capability_stage (explicit live written per building first; the answerable re-stamp on write), structured knowledge keys (51 sites, D6-A), greeting; the five inert OrganizationSettings twins become real keys
Gera
each key a no-op by construction; the settings dump for every live property diff-empty per key; F10 POLICY/IDENTITY refusals
dark per key
per-key flag
many hands (one key per worker under the per-key diff)
10–16 (spread)
8
Person calendars on CRED# (the OAuth callback writes one row; getCalendarToken's persist-onto-property dies) with the LEASE single-flight refresh (A7), assignments, schedule rules + layers, the role-end cascade (X5), the declined-invite task (X11), LRB#/HOLD# rows, Tour snapshot fields, areas/adjacency/cross-sell rule, findTourCandidates / whoIsOnCall / nearbyAlternatives with unit tests; provider:'propflow'; pms_credential#<role> slots; ownerId → pmsCredentialUserId R-code mapper
Gera (deterministic code) · Fede (rota facts, Situs calendars)
zero group/assignment rows for today's customers; tour-replay corpus byte-identical at the rendered layer; T1–T6 + T6′, E1/E2; two concurrent refreshes → one provider call; E10 (credentialId, pmsType) set diff empty
flip (sync-tour.ts read path)
per-building switch
one head (the pure functions) · many hands (per-key moves)
10–13
9
Workflows pin {org, propertyId, mapVersion} at start; declared checkpoints per workflow (renewal saga: before each outbound send, at offer acceptance · tour cadence: before each reminder, at booking confirmation · turnover: at each vendor dispatch · collections: before each notice — owner: Gera, one PR each names its checkpoint activities); ScopeMoved outcome; the rg drift test forbids src/lib/temporal/** importing the resolver
Gera
E18: a workflow started before a re-bind completes after it with the same outcome; a runner change yields ScopeMoved, never a 404
dark
—
one head
2–3
10
Deletions (What it deletes); per-company ticker uniqueness together with the org-scoped WO lookup; OPEN_SITES_TODAY = 0; the umbrella drained by the re-key script; ownerId R-attr; ADR-0120 superseded in the same PR series
accountId end-to-end (ToolCtx.accountId, runWorkOrder, CreateWorkOrderL4Input.accountId, E17) + maintenance.intake_route / emergency.route.* pulled forward from 7 + the whoIsOnCall slice from 8
Gera
E17: a write whose instance ≠ the property's refuses; corpus replay lands tickets on the right home + database
dark
—
one head
5–8 (not in the total; runs after step 4)
Total (summed from the rows above: lows 5+4+6+1+4+4+10+8+4+3+4+3+10+10+2+5, highs 7+6+8+2+5+6+15+12+6+4+6+4+16+13+3+8) — committed to the proof + Western Slope's shared line = 0–6a: 50–73 · backlog after the proof = 5b, 6b, 7–10: 33–48 (P6)
83–121
The proof is done at 3b, not at 10 (P6). The fitness functions that are the never-migrate proof close early: map-dump-stable (1), seeding-calls-zero-scripts (1), second-claim-refused-by-store (1), registry-closed-and-typed (2a), every-effective-value-has-provenance (2b), rebind-line-inserts-only (3b), camellia-replay-byte-identical (every step). Steps 7–10 are key moves, calendars, workflow pins and deletions — valuable, not the proof. The commitment therefore stops at 6a; 5b, 6b, 7–10 stay on the page as the road.
The working-day calendar from Tue Sep 15
Weekends skipped, no holidays; low bound / high bound; printed once so nobody re-derives it by hand again. Every "by when" on this page is one of these lines, "needed by", never "≈".
Milestone
Steps
Days (cum., low / high)
Date, low / high
Steps 0–2 done (dark; the registry signed)
0, 0.5, 1, 2a, 2b
20 / 28
Tue Oct 13 / Fri Oct 23
3a done (helpers private; texting's prerequisite)
+3a
24 / 34
Mon Oct 19 / Mon Nov 2
3b done (HEAD live per address)
+3b
34 / 49
Mon Nov 2 / Mon Nov 23
4 done (unknown-home voice, tools)
+4
42 / 61
Thu Nov 12 / Wed Dec 9
Western Slope's shared line, page order, one engineer
+5a
46 / 67
Wed Nov 18 / Thu Dec 17
Committed scope complete (proof + 6a)
+6a
50 / 73
Tue Nov 24 / Fri Dec 25
Western Slope's shared line, voice-first reorder (P7, proposed option)
0 → 0.5 → 1 → 2a/2b → 3b′ (5–7) → 4 → 5a
37 / 53
Thu Nov 5 / Fri Nov 27
Western Slope's shared line, many hands (P8) — 3a and the mechanical halves fanned out under Gera's review, 4's Fede half in parallel with 3b
0 → 0.5 → 1 → 2 → 3b → 4 (Gera's half 4–6) → 5a
38 / 55
Fri Nov 6 / Tue Dec 1
Situs maintenance line (P10, proposed)
after 4 + the Situs slice (5–8)
47 / 69
Thu Nov 19 / Mon Dec 21
Texting on the shared line (decision 5)
after 3a and the SMS lane's OPEN subset = 0 (inside 3b)
34 / 49
Mon Nov 2 / Mon Nov 23
Backlog complete (5b, 6b, 7–10)
all
83 / 121
Fri Jan 8 / Wed Mar 3
The voice-first reorder (P7), as a proposed option with its cost named. What voice on the Western Slope leasing line needs is the mint at three edges (ring webhook, tool webhook, call-ended), HEAD live for one address, the unknown-home DV set and the voice life-safety minter — that is 3b′ (the per-address flip for one number at the three voice edges + resolveOutboundAddress for Western Slope's lanes, 5–7 days, Gera to price) plus step 4. 3a (privatising primitives, moving 52 importers) is the structural guarantee and is dark; the texting gate ("life-safety lane split landed and the SMS lane's OPEN subset = 0") binds texting, not voice. Reordered: 0 → 0.5 → 1 → 2a/2b → 3b′ → 4 → 5a ≈ 37–53 days → Nov 5–Nov 27; then 3a and the rest of 3b for texting. The cost: for about two weeks the Western Slope leasing line's wall is the edge mint plus OPEN_SITES_TODAY, not the compile-time guarantee — T5/T6 (the edge tests) still pass; T19 (the importer pin) lands with 3a. "Texting after 3a" is unchanged. Decision: decision 17.
"A second engineer" is not the parallelism this team has (P8). On a 294-method repository surface, 383 delegates, a 23-file mirror tree and a drift-test culture, a new human is a reviewer's cost for the first month; a hire productive ≈ Fri Nov 13 (working day 43) arrives after the low-bound step 4 has ended and helps only the high bound. The parallelism that exists is in the Head / hands column: the steps that need one head are 1, 2a/2b, 3b's flip and 4's contract; the mechanical sweeps that have a drift test as their gate — 3a's 52 importers, 5b's readers, 7's per-key moves, 10's deletions, the mirror tree — are what this team already fans out to agent workers under one reviewer. The real decision is hire (productive from Fri Nov 13) vs fan out the many-hands steps now under Gera's review — decision 16. Situs maintenance needs 3a–4 + the Situs slice (maintenance.intake_route / emergency.route.* pulled forward from step 7, whoIsOnCall from step 8, accountId end-to-end) → Nov 19–Dec 21, proposed (P10). Camellia is the control at every step and is flipped last.
Same-file collisions, Sep 8 – Oct 22 — nine files, two names, three rules (P3)
Fede ships Western Slope on the umbrella (Sep 8–12 and the follow-ups after) while Gera's steps 0–2 touch the same files; step 3b later deletes lines Fede is hot-fixing in week 1. Written down once:
split into two files: store-conditional.ts (Gera) and the fixtures (Fede) — no shared diff
scripts/portfolio-harness/*
the contract (expect, closesAt, GREEN polarity)
the read ledger, validate-fixture
the contract PR (Fede) lands before the read-ledger PR (Gera), or the two rebase onto each other exactly once, on Sep 26
What each customer gets, and when
Western Slope
Sep 12 (existing rails): the shared mailbox routes by the lead's address line (W11-A: parseLeadEmail passes subject, exact address match, human fallback); one calendar on the umbrella row; the Western Slope leasing line answers leasing inbound on voice and text — texting on, decided Sep 7, leasing only; Fede's prototype agent gets the PMS's real data and the tour tools on the umbrella row, the existing per-property path (his half of the three weeks to Oct 2, proposed); the K16 tripwire cron counts rows born under PROP#western-slope-proto daily; rekey-umbrella-rows.ts is on file as a rehearsal. The shared line on the new rails: steps 3a → 3b → 4 → 5a — Nov 18–Dec 17 at one engineer in the page order, Nov 5–27 voice-first (3b′ before 3a), Nov 6–Dec 1 with many hands. Texting stays on through the flip; the life-safety lane split (3a/3b) gates tenant texting, out of scope while the line is leasing-only. The umbrella is released, re-keyed and archived after step 5a.
Situs (maintenance line first)
Steps 3a–4 for the HEAD-per-DID edge, the minters and the unknown-home state; maintenance.intake_route and emergency.route.* pulled forward from step 7 (org external to the call centre + two building clara overrides; residential and commercial emergency routes); whoIsOnCall pulled forward from step 8 for the after-hours window; accountId end-to-end (moved out of step 1 into this Situs slice, P6) so a write whose PMS instance ≠ the building's refuses — the slice is 5–8 days after step 4 → Nov 19–Dec 21, proposed. The import classifier mints ~62 buildings, 33 owner-party orgs and 142 ownership rows from the 71-record export; shells and placeholders never become buildings. Phase-1 scope is Fede's one-line call (decision 4).
Yale 25 and ConAm
Nothing moves. Yale's owned_by and managed_by rows, ConAm's memberships scoped to Yale and the act-as context: step 6. The floating assistant's three calendars, the assignment rows and the primary/secondary schedule: step 8. A second PMS for accounting: the pms_credential#accounting slot, rows deferred until it is wired.
Camellia
Byte-identical at every step at the rendered layer — settings dump, rendered prompt, DV values, outbound address, canonical match, slot text, the tool-call corpus — flipped last on every per-address and per-key switch, and the control fixture (a fresh F01, never the Willows) in the harness.
What it deletes
PHONE_TO_PROPERTY_MAP + env overrides (phone-lookup.ts:140-148, :23-66), the last-writer-wins Map (:296-306), the PROPERTY_TO_PHONE_MAP inversion + two hand pins + getPropertyPhone env fallback + resolvePropertyFromNumber + claraNumberForProperty (:385-395, :465-499), the forever propertyOrgCache (:407-419).
The voice unmapped-number fail-open (personalization/route.ts:540-578); tool-helpers.ts:88-92, 116-128 parameter precedence; the identify_caller fill-in (voice/tools/[tool]/route.ts:708-721).
getCalendarToken persisting onto the property row (sync-tour.ts:141-180) and the dead User.calendarIntegration writer in the calendar OAuth callback route (:260).
settings-resolver.ts:1-6 "No global fallback" header; Property.operatingMode/connectionMode + OPERATING_MODE_ARM (types.ts:3337; settings.ts:118); the six unread OrganizationSettings twins (types.ts:12203-12215).
escalationOwnerEmail/CcEmail, the carrier-numbers column, leasingCalendar, propertyEmail-as-routing, inboundEmailAddresses, the PMS account column, ownerId (renamed) as META columns — TS-stripped, attributes left cold (lesson #16).
ORG_BYPASS_ROLES / getUserOrgScope → null (org-scope.ts:18-20, 37), the ADMIN_EMAILS auto-provision (auth/server.ts:40-46) → a one-time bootstrap script, identity-admin.ts:24-28 "NO org filtering".
UNASSIGNED (conversation.ts:55) and UNROUTED_INBOUND_ORG (:433-450) births; the four queryAllPropertyPartitions('CONV#') fan-outs (conversation.ts:159, 205, 225; insights/themes/load.ts:49); canonical-conversation.ts:114 hard property match.
The umbrella PROP#western-slope-proto + provision-western-slope-proto-property.ts + its six documented hacks (after re-key); config/phone-registry.json and the number→agent file as sources (they remain as generated dumps); the 20 OPEN sites (inbound-org-scope.drift.test.ts:145-222) → 0; ADR-0120 superseded.
Fitness functions and the scorecard
Handover reconciliation OPEN · 2026-09-10.D-0910-5 adopts one mechanism for both handover cases, but does not decide its name. The review flags eight stress cases still naming TRANSFER and ST-900’s “post-TRANSFER” assertion after shape C removed that mechanism. These are outstanding reconciliation work, not a green proof for the new ruling.
Fitness functions
The executable tests, in the harness contract: PortfolioCase { expect: 'RED' | 'GREEN'; closesAt?: LadderStep; redBecause?; run() }; the runner exits 0 iff every case's verdict equals its expect; unfixed rows keep PR #7164's inverted polarity (P18). Three executors, named per row: H = scripts/portfolio-harness (json backend, read ledger); D = src/__tests__/*.drift.test.ts (rg-backed, no store); R = scripts/scope-parity-replay.ts --step N (read-only vs stage/prod). Each test file's header cites this page. "Red today?" says whether the assertion fails on today's code — the harness's whole point is that it does until the named step lands.
Name
Assertion
Encodes
Where it runs
Red today?
closesAt
rebind-line-inserts-only
seed F05; move p_la4's line to the cluster and back on a private bench: hash every pre-existing row; before ⊆ after byte-identical; delta = inserts + attribute updates on ADDR#/ATTACH#/PROFILE rows only; PK/SK moved == 0; git diff --stat on types.ts empty; git status clean; spawned scripts == 0; after the move resolveAddress names an active attachment
P2, P3, E3, AC1
H
red
3b
camellia-replay-byte-identical
fresh F01 (never the Willows): settings dump per key, rendered voice prompt, DV values for every pre-existing key (additive allowlist: organization_id, home_state:'known', declared once at step 4), outbound address per lane, canonical match, slot text; the Camellia tool-call corpus validates against the additive schema — identical before/after every step
N5, P1, AC9
H + R
green (the control)
every step
pre-pin-keys-resolve-at-org
F03/F05/S3 at propertyId:null: every key the pickup reads (office_hours, org.timezone, org.jurisdiction, greeting, transfer.*, emergency_phone, maintenance.intake_route) resolves non-refused with setAt: ORG#; a known building never receives an inheritOnlyWhen:'home_unknown' value — the resolver refuses reason:'home_known' before the class switch, whatever the class
snapshot of Property/Organization/Conversation/Person member optionality; exceptions: Property.organizationId (step 0, removeWhen: strip), Conversation.organizationId + scope (step 5); any other new non-optional member fails
P2-b
D
green
0
seeding-calls-zero-scripts
load any F01–F10/S3/S2 with the source tree read-only; git status clean; spawned setters == 0; claiming an address leaves git status clean
P2-c, P6, E20
H
red
1
unclaimed-address-refused-zero-reads
inbound on +10002000199: store reads == 0 after the HEAD miss (outside the LifeSafetyContext lane, entered only on a lexicon verdict), writes == 0, persons minted == 0; the gas-leak text pages every active residency into the resident's partition, no channel-side conversation; the same on voice (emergency-only DV set, escalate_life_safety mints the brand)
P9, E12, E29, T17 + T17-voice
H + inbound-dispatcher.test.ts
red
3b / 4
second-claim-refused-by-store
HEAD for +10002000142 by a second org → ConditionalCheckFailed (json shape) + one observed real cancel on stage; an FN# row by a different org → refused by the ConditionCheck; SMS on a function-claimed number still answers
E11, Z27, Z17
H + R
red
1
one-live-row-per-slot
nightly: for every (PK, kind, slot) exactly one row under ATTACH# and it carries no endedAt; every ended row is under HIST#ATTACH# (A11); putAttachment without supersedes on a live slot → {status:'conflict'} naming the live row; with it → one transact, the old row under HIST#, the new one live; the resolver's per-node Query returns ≤ the slot count (~30), never history
impl-R F8, P3, A11
H nightly + D (json shape)
red
1
roster-stamp-complete
nightly count: every PROP#/META with organizationId carries ROSTERPK = PROPORG#<org> and ROSTERSK = PROP#<pid>; a full saveProperty round-trip of a fixture building keeps both (the literal, not the backfill)
A3
H + D (the literal is grepped)
red
0
roster-index-carries-no-secret
describe-table shows the PROPORG GSI ProjectionType: INCLUDE with exactly {lifecycle, answerable, asset_type}; a roster Query on S8 returns no leasingCalendar, no CRED#, no token; bytes per item ≤ 200
A4
R (stage) + H
red
0
answerable-stamp-exact
nightly recount: for every building, answerable[fn] on META equals lifecycle ∈ {active, lease_up} ∧ resolveEffective(capability_stage.<fn>) ≠ off; flipping capability_stage.leasing = off at the org re-stamps every building in one transact (≤48) or the named sweep; S8 cold mint = HEAD 1 + PROFILE 1 + roster 1 + chain ≤3 reads on the ledger, 0 reads keyed by a building
A1
H nightly + H (read ledger)
red
0 / 4
consent-never-gates-reach
add an owned_by row without poolingConsent to a Grand Junction home on S3 → the home is still in reach, a caller naming it is answered, its tour hosts are building-scoped only; capability_stage.leasing = off on the same home → out of reach, known_out_of_scope
A2, X1
H
red
4
cred-refresh-single-flight
two concurrent refreshes of one CRED# → one provider call (the adapter counts), one LEASE, both callers end with the same token; a SECRET write whose version is stale → ConditionalCheckFailed, the loser re-reads
A7
H
red
8
reports-push-authorised-by-row
push into ORG#<owner>/REPORT# with a live owned_by row whose grants hold the projection → written; the row ended, or the projection not granted → ConditionalCheckFailed; no other method can Put under a foreign ORG# (importer pin on push)
A10
H + D
red
6a
building-conversations-via-gsi8
a thread born on ORG#ws/CONV# and bound to Ember Lane appears in listConversationsForProperty(ember); outside conversation.ts, queryByPK(propPK(…), 'CONV#') = 0; every ORG# Query names an SK prefix (queryByPK(orgPK( without one = 0)
A9, A16
H + D
red
5b
claim-refuses-until-pickup-keys
on a fresh org, claimAddress for a leasing line → {status:'refused', keys:[org.timezone, org.jurisdiction, emergency_phone, maintenance.intake_route]}; set them → created; endAttachment(org.timezone) while the line is live → line_depends naming the line; validate-fixture prints the same refusal without writing
X2, X7
H
red
3b
role-end-cascades (T6′)
savePersonRole(techA, active:false) → techA's assignment rows under HIST#, out:true in every live schedule row naming techA, in one transact; whoIsOnCall(maintenance, 02:00) → [techB, emergencyPhone]; a fixture that skips the cascade still yields the same answer (the pure functions drop role-less hosts)
X5
H + unit
red
8
maplog-undo-exact
every map transact writes exactly one MAPLOG#<mapVersion>; undoMapChange(v) restores the pre-image byte-for-byte on a private bench and is refused by name when a later log touched the same keys; the three clicks of operator example 3 leave moved == 0
X4
H
red
1
every-effective-value-has-provenance
for every key × every fixture building: setAt is a chain node, 'default' (declared absentMeans) or 'floor'; no reader does property.x ?? org.x (grep)
Z5, AC2
H + D
red
2
registry-closed-and-typed
unregistered key/kind, disallowed tier (tenants@company, application_link@org), locked-ancestor write, POLICY below root, equal-precedence chain tie → refused with the conflict named; KEYS ∪ KINDS ∪ REL_KINDS enumerates 100% of setting#/ATTACH#/REL# literals in src, agents, scripts, fixtures and this page
Z6, Z24, P13, P14
D
red
2a
no-masking-nightly
for every live node and key: every live row is at a tier in writableAt and is not masked by a lock; after reclassifying escalation.owner PARAMETER → IDENTITY and running the sweep, zero live rows outside writableAt; a skipped sweep is a loud tier_illegal_since_v<N> on the first read
AC11, P3 (the rewritten FF-P3)
H nightly
red
2
isolation-gauntlet T1–T19
two seeded orgs, twin digits: zero items returned from another org's rows per call (Count on every Query/Scan; any item with organizationId ≠ ctx.organizationId fails); PM at org A on org B's line is unknown on SMS and voice; property-owner login reads exactly ORG#<owner>/REPORT# rows and zero PERSON#/CONV#/KNOWLEDGE/ATTACH#; ConAm's act-as context writes only rows stamped org_jpco; AdminContext uncompilable as TenantContext (@ts-expect-error); OPEN_SITES_TODAY only shrinks; no *ownerId on any row (grep)
P8, N3, N7, T1–T19
H + D
red
3b / 6
consent-split (T11a/T11b)
OrgA STOP → OrgB send refused; OrgA grant → OrgB send refused; isRevoked returns a boolean only; no method returns consent rows to app code
P8 carve-out, E15
H
red
3b
same-agent-id-every-line
every fixture line resolves the same agent id; only injected data differs; home_state:'known' on Camellia, 'unknown' on company/group lines, 'suggested' for a known resident on a maintenance line
G1
H
red
4
bind-home-single-writer
conversation.propertyId/GSI8PK are written only by bindHome (importer pin); naming the home mid-thread changes one row's attributes; the next inbound on the shared line matches the same thread; call-ended cannot undo the bind; no meta row exists at two PKs (GSI3 vs partition count)
Q13, E22, case 18
H + D
red
5a (the index itself 5b)
complete-mediation-rg
only src/lib/domain/scope/** imports attachment/settings readers; src/lib/temporal/** never imports the resolver; src/app/api/voice/** never reads Property settings columns; dynamo/helpers primitives have exactly the repository files as importers; mint.ts importers pinned; no object-literal TenantContext; provider SDK importers pinned to src/lib/domain/{calendar,mail,voice}/provider/ — the calendar seam exists (provider/types.ts:170-176), the mailbox and voice-platform adapters take the same shape at steps 1 and 4; importers elsewhere = 0
P9, Z30, AC6, AC7
D
red
3a
mirror-tree-parity
agents/clara/lib/data/dynamo matches src/lib/data/dynamo for every repository file a step touches (check-data-layer-drift.sh exits 0)
K22
D, on every step PR
green
every step
workflow-survives-rebind
a renewal saga / tour cadence started under one binding completes after E3 with the same outcome; a runner change yields ScopeMoved, never a 404
E18
H (temporal harness)
red
9
map-dump-stable
dumpOrgMap byte-equal before/after any single-row edit except that row (the version stamp is a header); the resolver's answers are a pure function of the dump + registry
AC3, Z3
H
red
1
transfer-is-two-partitions
after catalog #8 on F09: A's login reads its pre-cut rows and zero post-cut rows; B's reads zero pre-cut rows; residents re-mint under B on first contact; zero partition moves; the hand-over document lists every open WO#/TOUR# and the re-consent task exists (A18)
dom fact 5, M23, E19
H
red
6a
owner-id-retired
field-drop pin; access-shape fence; meaning fence /\bowner(Id|Ref|OrganizationId)\b/ on Property/Organization/scope (allowlist: the PMS WO-owner type's ownerId); (credentialId, pmsType) set diff empty (E10/E16)
N7
D + R
red
8 / 10
scheduling-is-pure
T1–T6, E1/E2 of the scheduling report as unit tests; no prompt contains a calendar id, rota or distance; nearbyAlternatives on F01 = []; a host above the building is dropped without leasing_line consent (F07 negative)
AC5, Z13, dom fact 2
unit, no eval
red
8
cache-exact
ADDR# HEAD read count per resolve == 1 and uncached; PROFILE read per resolve; after N random attach/detach on a fixture the next resolve reflects the rows within one consistent re-read — chain rows on the first resolve (consistent Queries), the roster within one retry when the GSI lagged, the retry counted on the ledger; the roster cache is keyed by mapVersion only and a value-only edit (a taught knowledge fact) leaves it untouched; chain caches are keyed per (node, valuesVersion)
K12, AC3, A5
H nightly
red
3b
import-classifier
the Situs export is classified before any node is minted and the per-category counts are computed at seed: buildings === records − shells − placeholders − notUse (71 − 11 − 3 − 1 on the current export), ownerParties === 33, memberships === 142, and the roster holds 0 shells and 0 placeholders; a caller naming a fund or a tower on the leasing branch gets known_out_of_scope {route}; reach excludes lifecycle ∉ {active, lease_up}
dom fact 4, E28, Q19
H
red
4
custodian-writes-policyRound 4
a settings write whose context’s wall is not the building’s runner is refused not_custodian, naming the runner — never a bare 403 and never a tie broken by precedence — with zero rows, zero HIST#, zero log line and valuesVersion unchanged; the refusal is lifted only by delegatedKeys on that party’s edge, and the written row then carries the party in setBy; resolveEffective’s signature takes no party argument, so the party axis cannot leak into the walk (grep); and a lock at an ancestor over a live management edge is refused the same way, naming the relationship rather than the node
a live delegation#<key> row at a node admits exactly one writer for exactly one key at that one node: every other writer of that key there is refused by name and the refusal names the delegate; the delegate writing a different key at that node, or the same key at another node, is refused; ending the row restores the prior write set in one transact; and the row carries no value, so “buildings may not change this” is sayable without publishing a number. Falsifier: with no delegation kind registered both refusals must fail — that is the defect, asserted rather than described
Round 4 finding 2 (“relinquish control” has no row) · ST-190, ST-191
unit (no store)
no runner yet
2a (the kind) · 2b (the refusal)
overrides-inventoryRound 4
nightly, per wall per key: the count of live rows whose setAt is not the wall’s own node, failing on a delta above a per-wall threshold and never on a level — a deliberate fifty-building divergence is not noise, a Tuesday-afternoon spike is. The same pure projection over dumpOrgMap + the registry serves the operator’s list: which nodes differ, what they hold, who set it and when, with effectiveIfRemoved beside each; a row equal to the wall’s value is reported redundant, not divergent; IDENTITY keys never appear. Zero extra reads — the purity claim map-dump-stable already makes
AC11’s other half · Round 4 finding 5 (the intern instrument) · ST-183, ST-186, ST-194
H nightly + unit
no runner yet
2b
greeting-derives-from-reachRound 4
with no greeting row anywhere: a line whose reach set holds exactly one building renders that building’s name, a line whose reach holds more renders the customer’s — and the assert is the rendered string, not the provenance tuple. An explicit greeting row at any rung beats both. The derivation is pure over (reach, customer name, building name) and its provenance value is the derived one, so it is admitted by the shape rather than smuggled past it; the same set-size test already decides whether a caller hears a menu or a list, so no second rule enters the model. A building leaving a shared line flips two greetings with one write and no greeting row anywhere
change 6 · ST-152, ST-153, ST-198
unit (pure) + H (read ledger)
no runner yet
2a (absentMeans:'derive') · 2b
umbrella-tripwire
a dated count of rows born under PROP#western-slope-proto per day (the cron: Gera, Sep 10); the re-key script's dry-run enumerates every one of them (on file by Sep 19); after step 5a the count is 0
K16, P18, P4
cron + H
counting from now
5a
What runs behind this table — the honest answer is “almost nothing”. Round 4’s central structural finding is not any single defect: it is that no case in the catalog is proven correct by anything that executes. Read the status column above literally and it says so — 3 of 39 rows are green, and none of the three asserts a behaviour of the resolver, the wall or the row model: two are drift guards over the source tree (pinned-types-gain-no-required-field, mirror-tree-parity) and one is a byte-identity control (camellia-replay-byte-identical, labelled as such). One row is a cron counting from now with no threshold, and the four Round 4 rows are marked honestly. The remaining 31 are red, and “red” on this table conflates two different states — an assertion that runs today and reproduces the defect, and an assertion nobody has written yet. A reader who takes red as “a test exists and fails” is reading a status the column cannot carry, which is why the four Round 4 rows say no runner yet instead of borrowing a colour they have not earned.
The audit that separates them, reproduced rather than paraphrased. The red team’s fifth lens counted the catalog as it stood at 151 ids — before the Sep 8 standup’s seventeen and before Round 4’s forty-five — using a stated rule: a case is executed only if its harness column names a probe that exists (one of the CI suite’s twenty-two case ids, one of the isolation gauntlet’s rows T1–T14, or an existing unit test). Everything whose executor begins “NEW” is paper, however confident its prose.
Bucket
Count
What it means, said plainly
Executed — green
8
gauntlet or unit rows that pass today’s product. Six of the eight rest on the gauntlet lane, which CI never runs: its results are regenerated by hand and committed, at a commit that is not today’s tree
Executed — red
28
not “a test is failing”. These are green tests asserting the bug is still there — the suite’s polarity is inverted, so exit 0 means every case reproduced its declared failure mode
Paper
114
a named executor that does not exist yet
Open
1
the one row whose gate is still an open milestone
Zero
0
cases proven correct by anything that runs. That is the finding, and it does not move when the catalog grows: the Sep 8 standup’s seventeen and Round 4’s forty-five are all RED or cannot-express, so the count of proven-correct cases is unchanged at 213 ids
Two qualifications that belong with the number. (a) Only the twenty-two-case suite and its listing run in CI; the fixture, gauntlet and migration-proof lanes exist and do not — three lanes that are green by never running, which is why six of the eight greens are weaker than they look. (b) The counts above were read from committed result files, each generated at a different commit from the tree they describe. Our own battle test ran the two lanes it could on 2026-09-08 and printed 242 passed / 22 skipped on the inverted suite and 32 passed / 14 skipped on the gauntlet, every row matching its declared expectation — numbers that must never be quoted as progress, because on an inverted contract a suite that started passing for real would turn red. The phase that builds the GREEN contract is P2; until it lands, no case can prove a fix.
The Camellia byte-identity oracle, concretely: control = a fresh F01 loaded on the json backend (never the Willows) and the live Camellia rows read by scope-parity-replay.ts --step N in read-only mode; surfaces = (a) the settings dump (every registry key → {value, setAt}), (b) the rendered voice prompt and the DV values (set-equality re-baselined once, at step 4, against a sandbox agent), (c) outbound address per lane, (d) canonical-match decisions for the Camellia SMS corpus, (e) tour slot text from the tour-replay corpus, (f) the Camellia tool-call corpus validated against the tool schema; it runs in CI on every PR for (a)–(f) on F01 and by the replay script before each step's flip for the live rows. Two measured numbers the page should carry once stage has them (red team 2): the read ledger over the S8 fixture printing reads per cold mint and RCU per cold mint (the ≤6 / ≈50 KB count, as digits), and the real TransactionCanceledException with its CancellationReasons from one staged second-claim attempt — the falsifier for E11 as a payload, not a promise. Every play scenario ends with the same three-line ledger: inserted · updated · moved — and moved is 0.
The standing reviewer (AC12's second half; Zoom 47:41–48:22).results/<sha>.md is not only a CI artefact: a scheduled reviewer — the round robin Gera described ("actively, maybe randomly, or through round robin, picking a different system") — diffs each run against the previous sha and against the maintenance-corpus replay (Fede 48:27 "tens of thousands of examples… replay the conversations"), and files every new mismatch as a harness row (expect:'RED', an owner, a closesAt), so a gap becomes a case before it becomes a demo failure. The harness, not a demo, decides "done".
Scorecard
The B6 skeleton rows, one column for this design. pass = rows exist and the assertion holds by construction · pass-with-rows = holds once the named rows/step land · refused-by-design = the design refuses it on purpose · open = not settled here.
Row
Verdict
Rows / reason
M1 single property, line = property
pass
F01; zero fixture keys beyond properties[0]; every replay byte-identical (step 0 stamps organizationId as an attribute)
M2 single property under a multi-property manager
pass-with-rows
owned_by/managed_by (step 6); the identity pool is the runner's, one explicit fact
M3 centralized, one number, one mailbox, qualify-first
pass-with-rows
HEAD → ORG#; home_state:'unknown'; same agent id; no 1008 (steps 3b–4)
M4 N leasing agents, own calendars, org-tied
pass-with-rows
CRED# + person calendars + schedule#tour {round_robin} (step 8)
M5 hybrid, per-line binding
pass-with-rows
S5: three HEADs on three nodes, no mode flag (step 3b)
M6 floating assistant across orgs
pass-with-rows
two Person rows + PLINK#; two calendar rows to one external calendar; no cross-org Person read (steps 6, 8) — wording parked as decision 1
absentMeans per key; IDENTITY never inherited; unarmable without recipients (two-phase for escalation.owner)
E28 scattered site scope; caller-asserted address
pass-with-rows
one home = one building; known_out_of_scope; assertion policy per org (Q17)
E29 life-safety on an unscoped line
pass-with-rows
text and voice minters
E30 owner-that-is-a-customer; equal-precedence groups refused
pass-with-rows
ReportsContext; tie refused at write with both named
E31 resident on the company line recognised; Spanish by explicit signal
pass-with-rows
home_state:'suggested'; preferredLanguage on Person (N19)
E32 multi-org staff login
pass-with-rows
MEMBERSHIP# rows; no cross-org Person read (two rows + PLINK#)
E33 operational edges (holds, staff-reply stop, per-account reply routing, N emails to one team)
pass-with-rows / open
HOLD# rows; the staff-reply stop rule is a shipped property capability (G9) kept; per-account reply routing = pms_delivery_mode key; the team-inbox fan-out is open (F16)
prior rows (Camellia today; one number + one email; hybrid; floating assistant; isolation; view by property owner; property changes hands; new if/else risk)
= M1 / M3 / M5 / M6 / E1 / M7 / M9+E9
if/else risk low — zero call sites hand-roll precedence: resolveEffective is the one fold (complete-mediation-rg)
M13-B6 / M14-B6 / M15-B6 / E11-B6 … E15-B6
= M24 / M15 / M13 / E11 / E12 / E18 / E20
D7-A: a staff reply teaches the building; promotion is an explicit act with author + reason
Answers — decisions, Q/D/Z, the deepest questions
Decisions for Gera and Fede
The seventeen decisions only they can make, recommendation first, numbered as the design numbers them (14–17 were added in round 3). Each cross-references the study's D-numbers, the onboarding plan's X-numbers and the catalog's Q-numbers so no question is answered twice.
#
Decision
Whose
Recommendation
Cross-refs
1
The same human at two customers
Fede
Keep A2 — two Person rows + one admin PLINK# in a platform partition. The alternative (one Person + per-org roles, ADR-0020 Q1) is on record with B8's construction argument; the harness's F06 passes either way, so this is a wording ruling, not a schema fork.
Q4, study D5, plan X5, A2
2
Sign the registry class list, including escalation.owner DYNAMIC (reversing the Sep 1 IDENTITY classing)
Gera
Sign as written; every existing key ships PARAMETER + today's constant, so the only irreversible choices are the POLICY and IDENTITY rows.
deepest Q3, Z24, K12
3
What "company" means, and Yale now
Fede
A and A — company = the operator whose Clara answers; Yale stays under JP&Co with owned_by JP&Co + managed_by ConAm (act-as inside JP&Co's wall); nothing moves.
study D1, D2
4
Situs phase-1 scope (AS12 vs the atlas vs the buyer)
Fede
Maintenance line first as INIT said, on these rails after steps 3a–4 + the route keys; after-hours leasing is the same rows one step later.
Q8, Q10
5
Texting on the shared Western Slope line
Fede + Gera
Decided Sep 7 (Slack): on. Leasing only, so a listing text carries its own property; the replacement safeguards are the per-prefix daily tripwire count and the re-key script's loud failure on an unhandled row kind (~3 h). The lane split (3a/3b) still gates tenant texting, which is out of scope. The earlier lean — off until the split and the SMS lane's OPEN subset is 0 — is withdrawn.
Q8, N26, K13
6
Buy the portfolio test line and place one Willows call for hand-off variable survival
Fede
Done Sep 7 — the portfolio test line, kind prototype, no property, no agent (PR #7285); the voice-platform import against a sandbox agent is still open and gates the six L3 scenarios; the Willows call gates E8 and the mid-call question.
Q25, Q22, Z33
7
Ask Western Slope and Situs whether they would accept one number per property
Fede
Ask before step 3b; either answer keeps the trunk — a "yes" fills carrier-number-shaped HEADs per building and skips the ask block.
Q18
8
Tour assignment and availability source
Fede
By-calendar nearest-first with a 30-minute constant buffer for Western Slope week 1; Situs's three leasing people on Clara-owned calendars (provider:'propflow') replacing the whiteboard; the PMS "ready to show" mark with a date as the availability source.
Q23, Z13
9
The umbrella's one-way door
Fede + Gera
Accept it for Western Slope's first weeks with the tripwire (Gera, Sep 10) and the re-key script on file (Gera, by Sep 19); run the re-key once, after step 5a, under the P2 carve-out.
deepest Q10, K16, P18
10
The Sep 12 sentence and the ladder start
Fede + Gera
Sep 12 = umbrella + W11 + tripwire + script on file (by Sep 19); steps 0–2 start Tue Sep 15 and end Oct 13–23; the shared line (3a, 3b, 4, 5a) lands Nov 18–Dec 17 at one engineer in the page order, Nov 5–27 voice-first (#17), Nov 6–Dec 1 with many hands (#16); the committed scope (0–6a, 50–73 days) ends Nov 24–Dec 25; Situs maintenance Nov 19–Dec 21 — stated on the decisions page as proposed working-day dates, "needed by", never "week 2–3" or "≈". Sep 7 update (Slack): texting on; leasing only; Fede's three-week frame — the week-1 increment must itself work in the centralized setup, and Mon Sep 14 – Fri Oct 2 (working day 14) holds steps 0 and 0.5 done and 1 nearly at the low bound, 0 and 0.5 at the high; the flip's dates are unchanged; the Western Slope agent's PMS data and tour tools ride the umbrella row's per-property path meanwhile. The cut is Gera and Fede's, needed by Sep 14.
Q10, Z34, AS1
11
Which knowledge keys, if any, are POLICY under AS3 "portfolio wins"
Fede
None — PARAMETER nearest-wins with the org's locked bit available is "portfolio wins" when the company chooses it, and "the building knows better" when it does not.
Q11, AS3, plan X9
12
Owner-operators vs third-party managers as the segment
Fede
Keep M7/M17 in the harness, weight them lower; the rows cost nothing.
R9
13
Study D3, D4, D6, D7, D8, D9 and plan W11, X5, X6, X9
Fede
D3-A, D4-A, D6-A, D7-A, D8-A, D9-A; W11-A, X5-A, X6 superseded, X9-A — none of them is settled by this page; each stays on the study's or the plan's decision table under its own number, and this page opens no new fork for any of them.
study D3–D9, plan W11/X5/X6/X9
14
Does the org map exist as a screen in 2026? (P2)
Fede + Gera
(a) no — the row is typed in the B6 fixture notation, checked by validate-fixture, loaded by bin/load-fixture; the clicks column and the human page's chip pictures describe the 2027 screen and say so. (b) is priced if wanted: read-only map 3–4 days + the six write actions of catalog #1–#4, #6, #16 as forms 8–12 days = a step 6.5 of 11–16 days, outside the 83–121.
P2, N8
15
Step 4 gives the shared agent tools — superseding AS1's "no tools" for the front door (A8)
Fede
Yes, at step 4 and not before; until then a company line answers company facts and takes a message, and the human page says so. home_picker:'search' and every home-taking tool on a shared line wait for this line.
A8, AS1, Q22
16
Hire, or fan out the many-hands steps now? (P8)
Fede
Fan out now under Gera's review — the Head / hands column of the ladder names which steps can take it (3a, 5b, 6b, 7, 10, the mirror tree); a hire productive ≈ Fri Nov 13 (working day 43) helps only the high bound and is a reviewer's cost in October. If the hiring line is opened anyway, it is for the backlog (7–10), not the shared line.
P8, Zoom 12:41
17
Voice-first reorder: 3b′ before 3a? (P7)
Gera
Price 3b′ (the one-address flip at the three voice edges, estimated 5–7 days) and decide on the number — if it holds, the shared line lands Nov 5–27 instead of Nov 18–Dec 17, at the named cost of two weeks in which the Western Slope leasing line's wall is the edge mint plus OPEN_SITES_TODAY, not the compile-time guarantee; texting stays after 3a either way.
P7, decision 5
Every question in the catalog, the study, the plan and the Zoom — answered in one line each
Catalog Q1–Q28 and N8
Q
rec
Q1
Org = the customer = the portfolio; no new top-level entity; the group is an optional node with kind; ingestion's portfolioId is renamed or documented as a label; N10 (org as a real tier) is step 0, not a consequence.
Q2
Bindings; operatingMode/connectionMode + OPERATING_MODE_ARM deleted, never a branch point; a line row carries function, language[], direction, route.
Q3
Ship GROUP# with kind now, zero rows; only kind:'chain' is walked, ordered by in_group.precedence, ties refused at write (Gera's brief taken now — Situs already needs LLC-set + cluster + line-share on one building); region/area/brand/line_set are tags; owner is never a group.
Q4
Two Person rows + one admin PLINK# in a platform partition (study D5-A, Fede's A2 wording); the one-Person alternative is decision 1 — the harness's F06 passes either way.
Q5
Property.ownerId → pmsCredentialUserId (R-code mapper now, R-attr at step 10, allowlist the PMS WO-owner type's ownerId); the owner is an Organization{purpose:'owner_party'} + owned_by rows; no *ownerId anywhere else.
Q6
Soft — an ORG# party + dated owned_by {share, grants, poolingConsent, acceptedAt}, the list defaulting since 2026-09-08 to reports · financials · occupancy · work_orders · documents with conversations grantable and off / managed_by {grants:['operational']}; grants derived; no rung; consent consumed by the scheduler and roster inheritance; ONE wording for Fede: plan X6-A is superseded.
Q7
One function, two stages — scope upward (address → org → function → person → home), settings downward (building → chain groups → org → absentMeans), per-key class/direction/lock/merge, floor last; owner not in the chain.
Q8
Onboard the next customer on the new rails with reduced scope once steps 3a–4 land; the leak gate = life-safety split landed + the SMS lane's OPEN subset = 0 (N26 re-scoped).
Q9
Staged — two companies + owned_by/managed_by for months (free), then re-parent under a kind:'chain' group with the full re-stamp inventory; P2 carves the identity merge out explicitly.
Q10
Sep 12 = umbrella + W11 + tripwire (Gera, Sep 10) + re-key script on file (Gera, by Sep 19); steps 0–2 start Sep 15 and end Oct 13–23; Western Slope shared line = 3a, 3b, 4, 5a (Nov 18–Dec 17 at one engineer, page order; Nov 5–27 voice-first; Nov 6–Dec 1 with many hands); Situs phase-1 scope is Fede's one-line call (AS12 lean, atlas dissent noted) and needs 3a–4 + the Situs slice → Nov 19–Dec 21, proposed. Sep 7: texting on, leasing only, Fede's three-week frame to Oct 2 (decision 10).
Q11
The key registry ships at step 2 with every existing key PARAMETER + today's constant; binding ≠ display identity; clara.send_tiers POLICY; AS3 "portfolio wins" = PARAMETER nearest-wins on structured knowledge keys with locked available at the org — ask Fede whether any knowledge key should be POLICY (decision 11).
Q12
Platform defaults are code constants (absentMeans), never per-customer flags; the five OrganizationSettings twins become real org keys at step 7; prompt principles and runtime knobs stay platform for week 1.
Q13
(A) never move — convMetaPK(scope) only, scope immutable, home as a field + GSI8; grouping (personId, organizationId); re-homing only as the one-shot umbrella script.
Q14
Partition, then fence — the branded context + pre-read gate + org-stamped index keys; org-boundary.ts stays as defense-in-depth; ADR-0120 superseded in the trunk's PR series.
Q15
Data reads fail closed; LifeSafetyContext with two importer-pinned minters (text, voice); TCPA revocation + suppression platform-wide, grants org-stamped; the ADR-0126 ledger per (org, E.164); delete the voice fail-open with E24's leg flag.
Q16
Written MEMBERSHIP# rows + explicit activeOrganizationId (default = the org of the property last touched); Person.organizationId documented as the row's org (one row per company under D5-A).
Q17
A per-org PARAMETER identity.assertionPolicy (default caller-asserted for WO intake with PM confirmation for privileged actions); {asserted, inferred, confidence} never collapsed; money/private data/access refuse on assertion alone.
Q18
Ask the Western Slope principal and the Situs President before step 3b; either answer still needs HEAD + the resolver; a "yes" fills carrier-number-shaped HEADs per building and skips the ask block.
Q19
A home is a building; the classifier mints shells as owner_party orgs and placeholders not at all; out-of-scope records carry asset_type + capability_stage=off + lifecycle and answer known_out_of_scope.
Q20
Western Slope is its own org with owned_by rows for the Situs-held stock; brand = kind:'brand' tag + three keys (greeting, identity.senderDomain, identity.callerIdName) — the shortcut in week 1 with a real node later; the cross-brand resident is harness case E33-B3.
Q21
pms_credential#<role> attachment at org (group/property override) referencing CRED#, keyed (orgId, accountId); pms_external_id a map by account; a dedicated PropFlow user per client database; the lab lives inside the customer org that owns the credential; a write whose instance ≠ the property's refuses and alerts.
Q22
Same-leg bind (G1); one Willows test call settles hand-off variable survival before anything relies on it; company-level pickup context + per-home tool reads until then.
Q23
By-calendar nearest-first with travel buffer (findTourCandidates); availability = the PMS "ready to show" mark with a date (TTL + conflict rule for Fede).
Q24
Per-org rollup rows now; owner group when F4 is built; verify ADR-0082's key first.
Q25
Yes, buy the portfolio test line (Fede’s go); the live Western Slope leasing number stays forbidden in the harness.
Q26
Both labs — json harness (L2/CI, with organizations.json) and a seeded sandbox org on stage; the B6 fixture notation is the onboarding input shape.
Q27
One attachment row kind with the existing scope union ({propertyId}|{groupId}|{organizationId}|{personId}), SK ATTACH#<kind>#<slot>#<startedAt>; the Org writer registered as PR #1 of the trunk.
Q28
D6-A + D7-A restated as "never surfaces outside the scope it was taught at"; the Aug 13 hard wall is superseded by AS3 and the types.ts:4279 comment rewritten; one home per fact stays.
N8
The scrappiest onboarding path is org-first, and the org map is the form — createOrganization (the ONE registered writer of step 0) is the product path that mints ORG#; "add properties" = PROP#/META rows through the import classifier; "bind channels" = claimAddress; today's per-property self-serve flow becomes step 2 of that sequence, not a parallel path. The B6 fixture notation is exactly this form — typed by Fede for the two onboardings (checked by validate-fixture, which prints every refusal the loader would raise without writing, X7; expected sitting: Western Slope ≈ 2–3 hours, Situs ≈ a day), by the operator's admin UI afterwards (2027, decision 14). Go-live = D22's gate expressed as absentMeans:'refuse' keys that stop refusing (every fee key set, terms answered, one tour booked → confirmations), and the prototype line's deletion (K16) is the last acceptance row.
Study D1–D9 · plan W11 / X5 / X6 / X9
D1 rec: A. · D2 rec: A (Yale stays; two relationship rows; ConAm's staff are JP&Co members scoped to Yale). · D3 rec: A, hardened (the conversation section). · D4 rec: A (group variant + resolver rung now, zero rows; precedence walk). · D5 rec: A (two rows + admin link, in a platform partition) — confirm with Fede. · D6 rec: A. · D7 rec: A (promotion = END + INS at the org with author + reason). · D8 rec: A (managed_by → act-as operational on that building; owned_by → pushed reports). · D9 rec: A (one building per home; the umbrella re-keyed and retired).
W11 rec: A — resolve the home from the lead's address line inside ctx.reach, human fallback; ships on existing rails this week. · X5 rec: A — two rows per company, linked by admin action, each company's lookups see only its own rows. · X6 rec: superseded — the property owner is a party org with a relationship row and a pushed portfolio view, never a grouping inside the manager org. · X9 rec: A — company facts shared by key class, home facts isolated.
Zoom Z1–Z37
Z
rec
Z1
1:N by attachment rows on any node; the 1:1 columns TS-stripped, attributes left cold.
Z2
Capability = an attachment kind closed by the registry, not a node kind; no mode field survives.
Z3
The map is dumpOrgMap(ctx), a derived versioned document; rows are truth; every map edit = one row write in one transact with a mapVersion bump.
Z4
HEAD names the company (the wall); the node it names is Fede's association set; reach = subtree ∩ answerable() — a filter over the answerable stamp on the one roster Query, never a per-building read; N=1 ⇒ no question; a "15 of 30" set = a chain group.
Z5
Severed in the store (no row), tuple on the read {value, setAt, locked, maskedBy}; no ?? org.x anywhere (drift-guarded).
Z6
writableAt labels FIXED vs DYNAMIC as code; tenants/units/leases are kind:'entity' rows refused elsewhere.
Z7
Owner vs operator = owned_by vs runs/managed_by; the JP&Co principal sees both, ConAm sees Yale inside JP&Co's wall.
Z8
A branded context + pre-read gate + org-stamped keys; two lawful cross-org lanes named; insights within an org span actor.buildings.
Z9
Admins mint an audited AdminContext; a JP&Co-scoped tester is one role row in the real org; the regional manager = a region tag + {groupId} role; the Situs President = ReportsContext over pushed rows.
Z10
Role × scope × tool at the dispatcher + clara.send_tiers POLICY; the scope-column flip (ADR-0029 Phase 3) is its own PR; not the trunk.
Z11
HEAD at the edge; home_state + ask block as data; home_ref validated inside reach by bindHome; tours/WOs refuse until pinned; same agent.
Z12
Areas/adjacency/asset type/owned_by as rows; nearbyAlternatives is code; rule absent = off.
Z13
Calendars on people via CRED#, assignments + schedule rules as rows, deterministic findTourCandidates/whoIsOnCall; the schedule key writable at company or property.
Z14
The mutation catalog is the HOW — one transact, zero moves, zero fields, zero deploys once step 1 lands; the two node mutations named and priced.
Z15
F01–F10 + S2/S3 on the json backend + a sandbox org; the harness contract with GREEN polarity is the gate — owner Fede for the contract, fixtures and sandbox org, Gera for the store-side instruments (step 0.5); a scheduled reviewer diffs results/<sha>.md and files gaps as rows (AC12); maintenance "done" = corpus replay lands tickets on the right home + database.
Z16
Size is a number — 400 homes = 400 PROP#, four attachments, a search picker; the third customer onboards through the fixture notation.
Z17
Function is a row field on the line attachment and an optional FN# narrowing row under HEAD; language is a list on the attachment and a per-person fact, never on the function axis.
Z18
A building (D9-A); the umbrella is drained.
Z19
Born in the front-door partition with organizationId/scope, indexed by home, never moved.
Z20
Delete.
Z21
A party org + dated many-to-many owned_by {share}; audience on the party profile; billing counterparty open.
Z22
Two rows + PLINK# (Fede to confirm, decision 1).
Z23
Structured inherits by class, free-form appends, a reply teaches the building, promotion explicit.
Z24
Four classes + lock + merge as the key registry; locks honoured top-down with maskedBy[]; narrowing = a sweep; Gera signs the class list (decision 2).
Z25
absentMeans per key; explicit live per existing building then observe; unarmable without recipients.
Z26
A brand with two pinned minters and lifeSafety:true keys resolved with a null chain.
Z27
HEAD conditional put; FN#ConditionCheck on the org.
Z28
direction + primaryForScope on the row; outbound-only rows take no claim.
Z29
The credential is an org attachment referencing CRED#, slotted by system role; pms_external_id a map by account; accountId end-to-end at step 1.
Z30
Pin mapVersion, declared checkpoints per workflow (owner Gera, one PR each), ScopeMoved.
Z31
A walked chain ordered by precedence with refused ties + unlimited tags — Gera's brief and the study reconciled.
Z32
home_ref only; bindHome inside reach; a drift guard on opaque ids; the tool-helpers.ts precedence deleted.
CRED# once, referenced by N; rotation writes one row, by one refresher at a time (LEASE, A7); provider:'propflow'; provider SDK calls live only behind one adapter per capability, importer-pinned by complete-mediation-rg.
Z37
json harness + a purpose:'sandbox' org on stage; fresh properties inside the customer org that owns the credential with provenance:'test'.
The ten deepest questions
#
rec
1
The map is a view; rows are truth; mapVersion in every map transact makes it a versioned edit surface.
2
A home is a building; bindHome returns a building id with a fourth outcome known_out_of_scope; one org may hold a community (UNIT#) and scattered homes.
3
Gera signs the class list before step 2; escalation.owner is DYNAMIC (reversing Sep 1, with the reason).
4
"What Clara may do before the home is known", made legal by inheritOnlyWhen:'home_unknown'.
5
The renewal saga (before each outbound send; at offer acceptance), the tour cadence (before each reminder; at booking confirmation), turnover (at each vendor dispatch) and collections (before each notice) pin mapVersion and re-resolve only there; any other workflow declares its checkpoints in its own PR; owner Gera; ScopeMoved named.
6
A walked chain with precedence and refused ties; cluster is a tag unless it holds a setting.
7
Two rows + PLINK#; the Situs President's session at Situs asserts a ReportsContext over pushed rows, never membership; the active org is explicit.
8
A brand (LifeSafetyContext) with a per-key flag (lifeSafety:true) and two minters; an unmapped 2 a.m. caller gets the emergency-only DV set on voice and the pager on text.
9
The Camellia oracle (fresh F01 + read-only replay; six surfaces; CI per PR, replay per flip).
10
The re-key script is on file by Sep 19 (Gera) and is a rehearsal until step 5a; texting stays off; week-1 leads are mailbox leads under the person; the tripwire (Gera, Sep 10) counts the debt daily.
Honest limits, what would change our mind, sources
Honest limits
Two node mutations are scripts, not clicks, and are priced: the merger (catalog #7 — identity-pool rehome with a writer-stop window, the full re-stamp inventory, revert by runId) and the operator transfer (catalog #8 — a new PROP#, HEAD re-points under AdminContext). The domain says both happen every year; neither is free; both are zero partition moves.
Failing closed is a visible regression in week one (K13): every number and mailbox that used to guess a winner will refuse; the operator is told before each per-address flip, and the two-phase absentMeans on escalation.owner exists so the first key moved does not throw on the building that already had the gap.
The registry class list is the one irreversible decision (K12): narrowing after a customer authors against a key costs a sweep; that is why escalation.owner ships at the widest class and why Gera signs the list before step 2.
Mid-call context refresh on the voice platform is unproven (Z33): the design does not depend on it (company-level pickup + per-home tool reads), but "the prompt still describes the company after the home is named" is the cost until one Willows call settles it; whether the platform passes unknown tool parameters through after property_id leaves the schema is assumed conservatively (hence the handler-side delete, not only the schema guard).
IAM per-tenant LeadingKeys enforcement is not in this design; legacy PROP# keys carry no tenant prefix and the policy size for 400 homes is unmeasured. The branded context + pre-read gate + org-stamped index keys is the wall for application code; a credential-scoped bridge could be added behind the context, never instead of it.
Lease-form / program policy inside one building (M26), seats for call-centre queues, the billing counterparty, and multi-PMS rows are reserved in the registry (condition, kind:'seat', pms_credential#<role>) and not designed here; 88 Situs doors are under a master lease on day one and their notice period lives on the Lease row until the overlay exists.
The carrier row for each granted projection beyond REPORT# is not designed here. Gera's Sep 8 decision widens the owned_by grant list to reports · financials · occupancy · work_orders · documents (with conversations grantable and off), and that list is decided; which row in the property owner's own partition carries financials, occupancy, work orders and documents beside ORG#<owner>/REPORT#<pid>#<period> is the builder's to shape, and no row on this page claims otherwise. What is fixed either way: the projections are assembled into the property owner's house, the wall does not move with the grant list, and a read of the runner's partition from a party context stays the byte-identical 404.
Only one of the five delegated-administration rules is new build. Peer editing, the last-administrator lock and no-escalation-for-roles already ship (src/lib/platform/auth/permissions.ts:199-209; src/lib/domain/team/manage-teammate.ts:49-50, 66-68, 79-80, 98-99, 101-102, 108-110, re-opened at 58bf665820), so Gera's ruling confirms them rather than commissioning them. manage_roles as a separable grant and the org_admin-only ceiling above it do not exist (grep -rn manage_roles src/ = 0) and are the whole of the new work — rows added to manage-teammate.ts, which is already the single decision module, never a second writer beside it.
Free-form knowledge sections append and are shown at both levels; they never merge (D6-A). Travel time starts as a hand-filled adjacency table with a constant buffer.
Read cost: 2 round trips warm (HEAD + PROFILE), ≤6 cold (HEAD, PROFILE, one roster Query of ids + stamps, ≤3 chain Queries of one row per slot); history never sits under the walked prefix (invariant (h), A11). At 400 homes the roster page is ≈50 KB and ≈6 RCU; at 1,000 ≈120 KB, still one Query — the number grows with the roster, the round-trip count does not; the whole-org dump is a view. Not yet measured: the S8 read ledger on stage is the digit that replaces this sentence.
No map UI in 2026 (P2). The operator's edit surface this year is the B6 fixture notation, validate-fixture and bin/load-fixture, typed by Fede for the two onboardings; the drag-a-chip pictures on the human page describe the 2027 screen and say so. The screen is priced (decision 14: read-only map 3–4 days, the six write forms of catalog #1–#4, #6, #16 8–12 days) and is not in the 83–121.
A hire is not October parallelism (P8). The dates that beat one engineer come from fanning out the many-hands steps under Gera's review, not from a second full-time human; a hire productive from Fri Nov 13 helps only the high bound.
Before step 4 the company line is company facts and a message (A8). The v1 agent (AS1) has no tools; no home is bound, no tour or work order is taken on a shared line until step 4 lands and decision 15 is made.
The read-only calendar provider's calendars are hold-only (provider/types.ts:170-176); a double-booking there is detected on the next read, not prevented; fixtures assert unverified_on + an operator task.
Estimates are counts × judgment, re-derived at HEAD by the attackers, not measured; the founders' own 30/70 split says the mutation catalog and the fitness table matter more than the day counts. Two data-layer trees must change together (K22) — every step ships the mirror in the same PR or does not ship.
config/phone-registry.json and the number→agent file remain as generated dumps; number purchase and carrier webhook pointing stay PropFlow ops outside the row (a provision-number script), which is fine; what is not fine — and is fixed at step 1 — is a hand-edited file being the source of truth for a customer line.
What would change our mind
Check
By when
If it comes out the other way
Fede reaffirms A2 ("two linked rows, never one shared read") or amends to ADR-0020 Q1 for app reads
needed by the start of 6a: Wed Nov 18 (low) / Thu Dec 17 (high) — re-derived from the ladder (P1)
one Person + per-org roles + the in-org sentinel; PLINK# disappears; every other row unchanged; F06 passes either way
Gera signs the key registry class list, including escalation.owner DYNAMIC
needed by the start of 2a: Tue Oct 6 (low) / Wed Oct 14 (high) — against the 2a PR itself (P5)
a key reclassified after signing costs a sweep (catalog #17), priced ~½ day per key
The voice platform delivers an IVR branch/function at ring time on a shared DID, and can redirect at ring time from the personalization webhook (Q22's Willows call)
needed by the start of 4: Mon Nov 2 (low) / Mon Nov 23 (high) in the page order, Tue Oct 20 / Tue Nov 3 under the voice-first option
if yes, FN# rows are read at ring time (still one org per address) and route:'external' rows leave "reserved"; if no, one HEAD per DID is the only voice shape, an external branch stays a DID never pointed at the platform (A6), and FN# serves SMS/email discovery only
A single number must carry N functions at ring time for a customer
before Situs phase 1
the FN# rows are already the shape; nothing changes
A measured p95 on the cold path exceeds 300 ms at ≥300 buildings, or the S8 read ledger on stage disagrees with the read count (≤6 reads)
at step 3b's replay and again at customer ~20
the chain cache is already per node on valuesVersion (A5); the next move is a per-building EFFECTIVE# row rebuilt on write — a read-path change, no data change
A customer needs the same key set differently on two overlapping walked groupings with equal precedence
first occurrence
the tie refusal names both; the operator orders them; if ordering is unacceptable, a key-level precedence override — a registry line
The identity rehome (catalog #7) proves unaffordable at the first real merger
first merger
residents' identities move to a property-anchored pool and the merger becomes cheap — a spine decision, not this design's
A prod describe-table shows GSI projections differ from the create script, or the new PROPORG GSI cannot be created INCLUDE online
step 0
the new sparse GSI is the design (A4), not a fallback; if INCLUDE is refused, KEYS_ONLY with answerable/lifecycle folded into ROSTERSK (PROP#<pid>#<lifecycle>#<answerable bits>) — same one Query, same zero per-building reads
The Western Slope principal / Situs President accept one number per property (Q18)
before step 3b
HEADs per building, no ask block; the trunk is unchanged
The 400-home fixture shows the two-step area ask (X6) is unusable by voice — the caller cannot name a town, or a town holds > 6 available homes
step 4's robot call (Q25)
a third step (bedrooms / price band from available_homes), still code; home_picker:'search' only once the shared agent has tools (A8, decision 15)
A live incident where an inherited escalation.owner paged the wrong building's staff
any
reclassify to IDENTITY with the sweep — the P3 path, one registry line + ½ day
The answerable stamp drifts from the rows (the nightly recount finds a mismatch)
any
the stamp is rebuilt by the recount and the writer that missed the re-stamp is the bug; the roster index never lies for longer than a night
A customer with per-unit ownership, a brand above operators, or a guarantor-shaped resident signs (permutations §D)
first occurrence
the reserved REL_KINDS rows, brand_party/party_push and occupancy.role get their first writer — additions, not reshapes
Where this came from
Inputs: the catalog and its contradictions, loop kickoff and B1–B9 (PR #7209: points-catalog.md B10, 568 lines — §1–§15, M1–M28, C1–C29, K1–K25, F1–F18, Q1–Q28, E1–E33; points-contradictions.md, 50 rows; points-B6.md §3.0, §4, §5); the harness (PR #7164's diff); the Zoom of Sep 6 (zoom-2026-09-06.md, 845 lines) and the study (study.txt); six research lenses (understand-codebase-how, understand-pms-domain, understand-practice-2026, understand-roles-isolation, understand-scheduling-people, understand-dilemmas — the last carries Z17–Z37, AC1–AC12 and the ten deepest questions); three designs (design-graph-map-resolver.md 621 lines, design-study-hardened.md 584, design-relationship-tuples.md 522); twelve attack reports (each design × mutation, isolation, implementability, domain-fit); three judges (judge-engineer-4-weeks, judge-architect-customer-50, judge-gera-the-how); the critic's 24-gap pass that produced the repaired final; and, on Sep 7, the second red team (rebuild/redteam-2.md: 18 architecture, 10 plan, 11 experience findings) and the permutations red-teamer's five doors (rebuild/permutations.md §D), whose dispositions are in the Round 3 table under the findings disposition. Read through the designs and attacks, not re-opened: points-B1…B9, live.txt, the audit directory, entity-model.md. Every file:line above is an attacker's or a designer's pin at 45dac64eb9/7d4038f9ed, cited as such; no product code was re-opened for this synthesis.
The Sep 8 decisions. The three items this page states as decided on the evening of 2026-09-08 — the property owner's widened grant list, delegated administration with its ceiling, and the three-way split of the word "owner" — come from Gera's own answers on the settings decision block, recorded in the design record's decided-2026-09-08-settings-register.md and cited on this page as DEC §n (§5 the grant list, §6 delegated administration and the term collision). Every file:line cited for them was re-opened at 58bf665820 (assess-main, 2026-09-08) at the app checkout, which is a later sha than the rest of this page's basis and is named at each cite.
Provenance of the grafts. From study-hardened: one HEAD per address with function underneath, the in-org claim sentinel, lifeSafety:true, the registry grammar and the kind table, "not concepts on purpose", owner_party/manager_party, the per-step owner column, locked-ancestor-wins at read. From graph-map-resolver: Property.organizationId + PROPORG# as the single truth, reach ≠ roster, mapVersion in every map transact, GSI8, kind:'entity' registry rows, maintenance.intake_route as the "N of M" pattern, absentMeans:'refuse' as the arming gate, the mutation-catalog columns, the "what may Clara do before the home is known" paragraph. From relationship-tuples: the line row's {function, language, direction, primaryForScope, route}, RouteOut before a Clara turn, the label-kind split with one walked kind, in_group {precedence} with refused ties, startedAt in the SK, CRED# once referenced by N, provider:'propflow', the new-PROP# transfer, home_state:'suggested', partyOrganizationId and the party-keyed index. From the attack reports: the consent split, pushed owner reports + ReportsContext, org-stamped conversation keys, mintFromConversation + VOICECALL#/SCOPE, bindHome as the single writer, the act-as reading of managed_by, the no-sweep facade, config files as dumps, harness GREEN polarity + the read ledger + the replay script, the dated allowlist ratchet, the import classifier + lifecycle + known_out_of_scope, PLINK#, the admin-bypass deletions, the exact cache statements, the two-phase escalation.owner, the voice life-safety minter, the three-line Sep 12.
Re-verified for round 3 (2026-09-07, read-only at ~/code/PropFlow/wt/portfolio-architecture-points):src/lib/data/dynamo/helpers.ts:2361-2408 — transactWrite maps Put | Delete | ConditionCheck only, sends no ClientRequestToken, returns false on TransactionCanceledException (A12/A13 act on this); src/lib/data/dynamo/property.ts:200-220 — saveProperty builds {PK, SK:'META', GSI3PK, GSI3SK, entityType, ...property} and putItems it, stamping GSI3 and nothing else (A3 acts on this). The TransactWriteItems cap (100 items / 4 MB) and "GSI Queries are eventually consistent" are the service's documented limits from memory, not re-fetched.
Not read / uncertain: no prod describe-table (GSI projections and GSI1 vacancy on PROP#/META come from the create script and the add-scripts); DynamoDB transact/BatchGet limits from memory; whether the voice platform delivers a function hint at ring time or passes unknown tool parameters through; whether Fede's A2 admits the one-Person reading; the study's adversarial review may still move D1–D9; engineer-day figures are re-derived counts × judgment, not measurements; the exact split of the dispatcher's eight OPEN sites between life-safety and data rungs (≈2/6 assumed); Western Slope's "14 properties / 16 homes" and every Situs count carry the atlas's one-witness marks.
Three words, never the bare one (decided Sep 8, and the ladder's own shape confirms it). org_admin is the top-level administrator of a customer org — level 1 in ROLE_HIERARCHY, under platform_admin at level 0 for cross-org PropFlow staff (src/lib/platform/auth/permissions.ts:52-57, re-opened at 58bf665820) — and it is the only rung that may hand manage_roles on. A property owner (landlord) is the external party holding the deed, an ORG#{purpose:'owner_party'} reached by a dated owned_by row; it holds no rung in ROLE_HIERARCHY at all, which is why "only the top level may grant access" can only mean the org_admin. The PMS credential holder is the third and easiest to mistake for either: Property.ownerId, "a Better Auth userId for PMS credentials rather than a landlord identity" in the type's own words (src/lib/data/types.ts:3371, the field at :3780), which keys PMSCRED# and must never appear in a roles matrix.
DECIDED — Gera, 2026-09-16: use the ten distinctions below throughout the current architecture and implementation work. Keep Organization architecture and Organization Core as the design and contract names. This is a vocabulary decision, not a finding that every old term occurs in the code. The code inventory and the adoption sequence below are proposed engineering work; the implementation board holds dispatch state and evidence.
N1 — one name for each meaning
Ambiguous wording
Preferred term
Meaning and retained exceptions
Company / firm / customer / operator used for the same entity
Organization; Operator for its property relationship
Organization is the entity and existing isolation boundary. Operator means an organization operating a property, not a staff person. Customer, payer and legal company remain valid when those distinct commercial or legal facts are intended. Portfolio remains a collection of properties.
Platform administrator is the explicitly assigned platform exception. Organization administrator is an organization staff label, not platform access or a new ordinary permission tier. Keep Admin as the navigation label.
Owner
Property owner, Credential holder, Task owner
State what is owned. A landlord, a provider credential holder and an accountable task assignee are different facts; none automatically grants platform administration or data access. Existing wire fields can retain their spelling with an explicit meaning map.
Technical tenant isolation / TenantContext
Organization isolation / Organization context
Use organization for the technical customer boundary. Retain Tenant for the rental relationship; Person, Tenant and User remain different concepts. Do not globally replace tenant tokens, vendor terminology or persisted names.
Role / permission / entitlement
Staff role, Access grant, Module entitlement
Staff role describes work. Access grant authorizes the relevant organization/property boundary. Module entitlement identifies product availability. Do not collapse all uses of permission into access grants: provider consent and platform exceptions remain distinct.
For viewer requests, effective query scope is authorized scope intersected with this tab's selected scope, enforced before business reads. Background workflows have their own authorized operation scope; a viewer's selection does not redefine it.
Calendar / org calendar / combined calendar
Connected calendar / Organization schedule view
A connected calendar is a provider endpoint. An organization schedule view combines visible scheduling information; it does not prove a provider calendar exists. Name availability source and booking destination separately. Several endpoints do not multiply one person's capacity.
Custody / attachment / coverage
Controlling organization, Attached to, Serves properties
These are plain-language labels for three different resource relationships. Keep precise schema relationships and provenance distinct; attachment is not control and serving a property is not permission to read all of it. No mandatory schema-field rename follows from these labels.
Provider support, product availability, execution mode and a phone action setting answer different questions. Rename only after identifying which fact a field represents. These labels introduce no new switch, authorization layer or activation.
Phase Dock / ladder / board / plan for the execution tracker
Implementation board
Use this for the existing tracker and its current links. A plan can still mean a plan. Preserve historical quotations, completed receipts, URLs, anchors, row IDs and control IDs.
D10 applied to this page, 2026-09-16. Prose here now says Implementation board; Phase Dock is deliberately kept where D10 itself says to keep it — the ambiguous-wording cell above, the dated statement of authorized direction in Organization Core, and every completed receipt, tracker row context, podcast transcript, URL, anchor, row ID and script identifier elsewhere.
N2 — prioritize safe adoption; classify before renaming
Update current explanatory text and UI labels early. A private variable, type or filename can move in its owning implementation change after checking all consumers. A filename can also be a route, worker export, generated input or dynamic lookup contract: its appearance alone does not make the change safe. Retain an accurate existing name; no bulk replacement of organization, operator, tenant, owner, scope or portfolio is authorized by this contract.
The inventory records the inspected commit, file and symbol, actual meaning, proposed name, whether the target name already means something different, and the owning board row. Classify each occurrence as current prose/UI, internal-only code, persisted/public/durable contract, frozen prompt/evaluation text, or retained history. Store the inventory in the existing row's dropdown or linked implementation PR evidence, not a new competing architecture document. A sample establishes examples, not completion of the inventory.
For an internal rename, follow static imports, barrel exports, dynamic references, scripts, generated artifacts and relevant tests. Preserve behavior, branded types and distinct context shapes. Reuse the existing vocabulary guard and protected-literal list; strengthen narrowly where needed, without a new whole-repository banned-word framework. Frozen prompt wording follows its existing evaluation process instead of an incidental naming sweep.
For each serialized or externally consumed spelling, explicitly choose retain technical spelling with a meaning map or bounded compatibility migration. Keeping a stable contract is a valid completed naming decision. Migration work identifies readers and writers, payloads, stored keys/values, API clients, caches, URLs, exports, environment/CLI consumers and rollback before changing the value. Use reader compatibility and writer cutover only where the inspected contract calls for them; aliases and dual writes are not mandatory boilerplate. Name any retirement evidence and owner. Schema migrations, production backfills and workflow restarts are not authorized by this planning update.
N3 — durable workflow names are contracts
Inspect the existing workflow naming standard and registrations before renaming local symbols or files. The audit covers registered workflow and activity types, workflow IDs, task queues, signal/query/update names, schedules, search attributes, history payload fields and deduplication keys. Default to retaining their existing external spellings. A clearer explanation or local helper name does not require changing a running workflow's identity.
If a durable name truly needs to change, add a bounded corrective item in the same implementation phase with the old/new contract, worker/client rollout order, affected open executions, representative history replay, old-client compatibility, retry/idempotency checks, rollback and retirement criteria. Replay alone does not prove that workers register the required types or clients address the right queue and execution. Patching, worker versioning and workflow-type cutover solve different problems; pick only the mechanism required by the actual change. Never cancel, restart or reassign live executions merely to make their names prettier. Safe docs and UI work can proceed while this audit retains durable spellings.
N4 — execution and closure
The existing implementation board owns these eight open tasks, in their existing phases:
Phase
Row
Deliverable
00
p00-terminology-inventory
Inventory meanings, consumers, collisions and rename risk for all ten distinctions.
00
p00-terminology-contracts
Record retain-or-migrate decisions for stored and external contracts.
00
p00-terminology-internal
Safely rename residual internal helpers/files outside the domain-specific rows.
3
p3-terminology-resources
Align organization/resource/calendar/product-setting names without changing relationships.
5
p5-terminology-access
Align identity, role, grant and scope names without changing access behavior.
7
p7-terminology-surfaces
Align Settings, Admin, Atlas, current docs and actionable board titles with their meanings.
9
p9-terminology-workflows
Audit durable workflow names and retain them or explicitly plan a necessary migration.
10
p10-terminology-consistency
Verify the vocabulary and behavioral contracts together before the Core exit.
Inventory first; independent labels and clearly internal changes follow their inspected subsets. Rows must identify the subset they consumed, not wait on unrelated migrations. Work shared with an existing completed item gets a linked correction in that original phase, never a rewritten receipt. The Phase 10 exit consumes naming acceptance as well as its existing integration receipts. Every term gets a concrete implemented, intentionally retained, or still-open disposition. A necessary unresolved migration remains open; an intentionally retained wire name with a meaning map is not an unfinished migration.
Acceptance uses affected type/import checks, contract fixtures, interface screenshots and representative workflow replay/registration tests only where applicable. Tests must prove meaning and behavior, not merely an absence of old tokens. Any search used as evidence has a known-present and known-absent control. Retained Tenant, historical vocabulary and stable external names are expected. Documentation approval does not complete these implementation rows or certify production behavior.
N5 — references for implementers
The following primary documentation informs the proposed workflow handling; it does not add a dependency or mandate a versioning mechanism:
Workflow versioning: distinguish changes compatible with existing histories from patching, worker versioning and workflow-type cutovers. Existing executions require a deliberate compatibility path.
Workflow definition: deterministic workflow execution and registered workflow types constrain which apparent renames are only local changes.
Testing workflows: use representative replay alongside the application's existing tests; registration, routing and client compatibility need their own checks.
Start with the repository's existing workflow naming standard and vocabulary safeguards. These references explain the risk; the inspected code and contracts determine the actual work.