Property is context, not a switch

A record with no building attached must never change what Clara does; who someone is comes from their role at the company. What breaks today, the design, the local proof, the migration.

2026-11-02 · Approved by Fede, Nov 2 (Option B); chain merged Nov 3; live proof pending · local POC and harness built, zero CI, nothing in src · claims audited Nov 2 (20 hard, 2 estimates labeled, 5 unverifiable noted) · rule: Fede, Nov 2 (~/.claude/CLAUDE.md) · related: coworker loop · Gera's portfolio model · testing playbook

On this page
  1. The short version
  2. What happened to Hugo, before and after
  3. How scope is carried today: one call, three answers
  4. How big it is: 107 places
  5. The ten worst, in plain words
  6. Identity: roles are the answer, phones are the gap
  7. The design: one resolver, org first
  8. Options and recommendation
  9. Local POC and harness
  10. Migration: six PRs, each proven by replay
  11. Decisions for Fede
  12. What this changes in existing rules
  13. Sources, overlap, caveats

The short version

Today 107 places in Clara's core logic quietly stop, or skip a safeguard, the moment a record has no building attached. Hugo's email reply was one. Nothing is wrong with the records: company-line calls, call-summary notices and staff threads have no building by design. The code treats "no building" as "no idea who or what this is". Fix: one shared answer to "which company, which building, which roles" that every flow uses, org first, building when known, roles always; a check that blocks new early exits; and the existing guards removed in six small PRs proven by replaying real records.

What happened to Hugo, before and after

Sunday Nov 1, 4:02 PM MT (record time 2026-11-01T23:02Z; all times Mountain, the company's zone), David called the Situs main line about garbage behind 1683 Alton, unit 110. His number matched no tenant, so the call sat on the company's placeholder "front door" row. At 4:04 PM Clara emailed a call summary to the shared leasing inbox. The notice carried the company, David and the call, and no building. Monday 7:43 AM MT Hugo, the Situs admin, replied from his own address.

Before (production, Nov 2)

The staff check asked "does this notice have a building?" first. It did not, so the answer was "not staff" and Hugo's role was never read. His reply fell to ordinary triage, which guessed a building from the word "Alton" and opened a brand-new thread there.

Clara: "I can't place calls, and I don't see who 'him' is on this thread."

conv_voice_950df6d1 (David) · tn_7e6e9f2b (notice, no property) · msg_cfda4895 (Hugo) · 14fcb780 (the blind thread)

After (rule applied; door fixed in #24489, merged Nov 2)

The check reads Hugo's role at the company first: org admin, active. He is staff. The reply lands on David's call thread, so "him" is David and the number, the address and the complaint are all in view.

Clara: "Got it. I'll text David that the city inspection is today and the area behind 1683 Alton needs to be cleared. I'll let you know when he answers."

The second half, relaying the team's answer to the renter, still needs a building today (PR 3 below). The "after" text is the target behavior, not a production record.

How scope is carried today: one call, three answers

The same call can be "about" three different buildings depending on which code path looks. Hugo's Oct 28 test call showed all three at once: the escalation email used the matched building (correct), the after-call emergency check ran only where the dialled line's Maintenance setting read Off, and on a company line that setting is the placeholder's, not the home's, so the on-call tech heard "Maintenance is off here" about a building where it was Supervised (Gera's words, #23497, merged Oct 29), and the daily hold report showed "Situs Group" with no unit.

Scope sources and readers today SOURCESREADERS (how many places read it directly) Row: conversation.propertyIdfront-door placeholder on text/email · empty on voice Pin: spokenHomeMatchwritten mid-call, undoable, never binds the row Turn home (person-first join)this message's building, when it differs Org stamp: conversation.organizationId"absent = we did not know" Notice: orgId required, propertyId optionalcall summaries never carry one 551 direct reads of the rowtools 142 · agent loop 101 · escalation 38 · maintenance 35 · voice 51 7 reads through the shared view (pin → turn → row)the view exists; almost nobody uses it 4 different "which company is this" resolverstwo share a name; a docblock says "collapse them later" 3 escalation-owner doors: no building → nobodythe org rung exists but is only reached through a building Staff roles: by email at the org (by phone exists too)a tenant or prospect match still beats a staff role Counts from grep on origin/main a369684b8a, Nov 2; full commands in the inventory file.

How big it is: 107 places

Three numbers, kept apart. A text search finds 1,591 lines that mention a missing property. Five read-only inspectors read about 2,300 lines and sorted each one. 25 of the worst were re-read by hand; all held.

ClassMeaningLinesVerdict
(a) bug classThe record has no building by design (company line, call summary, staff thread) and Clara stops or degrades107Fede's rule: fix
(b) integrity fenceA building id is present but the row could not be read260legitimate, keep
(c) building operationThe step is about one building (its unit list, its calendar)826legitimate, keep
(d) unclearNeeds a reader with more context29triage in PR 1

Inside the 107, two shapes. About 95 are drops (the split is an estimate: 8 inverse rows are tagged, about 4 more are described in prose): nobody paged, no notice sent, the team's answer not relayed, a tool refused. About 12 are the inverse: a missing building turns a safeguard off, for example the company's "maintenance is off" switch is not applied and property-less notices skip the Supervised hold. Where the 107 live: escalation 18, tools 14, voice integrations 12, agent loop 13, conversations 5, maintenance 5, leasing 5, the rest spread thin.

The ten worst, in plain words

  1. "Get me a person" reaches nobody on a company-line call or thread, although a company-level contact exists.
  2. Forward to the manager marks the thread escalated, so Clara goes quiet, and nobody is emailed.
  3. Hugo's next failure: now that he is recognized, relaying the team's answer to the renter goes through a step that needs a building. The company is computed three lines earlier in the same function and not used. Replay on Nov 2 (#24757) ruled this one out: David's call sits on Situs's front-door row, so the company and the line are the same before and after; the fix still applies to threads with no building at all (Fairhaven: 176 relays that were written and never sent now go out at the company).
  4. A new message on an escalated thread with no building is told to nobody, with no log line.
  5. Verified residents calling the company line are refused their balance, lease and work orders. Violeta (Situs, Oct 31) was greeted by name and still sat on the placeholder. Nov 3 correction (#24772 replay): her tools already worked at her building; she was lost at call start because the phone lookup drops co-tenant rows (162 of 534 Situs residents are co-tenants). Fixed as PR 4c.
  6. No turn-settled event on property-less threads: no classification, grade or maintenance signal.
  7. Clara's spoken promises on company-line calls are never checked.
  8. A tour agreed on a transferred call is never booked or recorded.
  9. Daily team note, staff texts, "message taken" text return "no inbox" silently.
  10. A guard-blocked reply on a property-less thread gets no review card, no page, no row: the hold queue is keyed by building.

A hidden second wall: the step that pages a human about a dead call or a broken promise gets the company only from the building. Fix the checks in front of it alone and the drop moves one step down, so PR 2 fixes both together.

Identity: roles are the answer, phones are the gap

Roles already live as rows scoped to the company, a group or a building, and the lookup by email at the company exists (it is what the Nov 2 fix uses). Two facts from production decide the design:

CompanyBuildingsActive staffActive staff with no phone on file
Situs Group53 (1 placeholder)88 of 8
Western Slope90 (1 placeholder)91 of 9

The design: one resolver, org first

Not a new concept: the pieces exist in five places. The design merges them, which is also the only form the repo's one-source-of-truth rule allows.

Proposed resolver any record (call, thread, notice) resolveScope(record) org: stamp → building's org → refuse building: pin → turn → row → none placeholder row reads as "none" roles: by email and by phone, at the org roles are recognition, never authorization who to tell: org owner if no building hold clock: company's zone if no building identity: staff role wins reports: matched building, else company Ratcheta build check fails any file that gains a new "if no building, stop" in core code; baseline 122 lines in 97 files today, shrinking with every PRcopied from the existing saga-read ratchet: per-file baseline, positive control, two mutation controls

Options and recommendation

OptionWhat it isProsConsVerdict
A. Resolver + ratchet, migrate as touchedMerge the five half-resolvers, add the check, fix guards only when a flow is otherwise editedSmallest first PR; no behavior change up frontThe 107 stay live for months; Hugo's next failure (PR 3) waits for someone to touch that filefloor, not enough
B. A, plus bind-on-confirm, plus a six-PR sweep by flow recommendedEverything in A, voice binds the matched building when confirmed, and the guards come out in six flow-sized PRs, each proven by replayFixes the ten worst in weeks; each PR is one user-visible effect; ratchet shrinks to zeroAbout 2,900 lines over six PRs; two groups need Fede's decision firstgo
C. Always stamp a placeholder buildingGive every record a "front door" row so a building id is never emptyNo code reads changeThis is what Situs has today: on the Oct 28 test call the placeholder's Maintenance setting answered for the home and the on-call tech was told "Maintenance is off here" (#23497). Hides the problem, breaks safeguardsthe foil, no

Local POC and harness

What it does: ten fixtures from real, scrubbed production records (David and Hugo; Hugo's Oct 28 call; Jay; Violeta and Ruth; three more Situs company-line callers; controls with a building attached). It runs the real decision functions as they stand today, then the same decisions through a resolver shim, and prints decision | today | with resolver | rule says. Controls with a building must be byte-identical. Two mutation controls: re-plant the pre-fix guard and Hugo must flip back to "left to triage"; remove the org fail-closed and the harness must fail.

What was replayedResult
Rows replayed (decision × fixture)182
Rows with a real building on the record40, all byte-identical today vs resolver
Outcomes that change on real records6
  notices with no building that now reach the company owner instead of nobody5
  hold-report label on a front-door call where the caller named a home: "Mayor Apartments" instead of "Situs Group"1 (new decision 7 below)
Outcomes that change on derived or probe rows (labelled, never asserted)31
Hugo's reply: pre-fix door / today's door / resolverleft to triage / routed to the notice thread / routed to the notice thread
Mutation control A: pre-fix guard planted back

1 violation · HUGO today (HEAD door): "left to triage", expected "routed to the notice thread" · exit 1

The resolver path still routes him, so the control isolates the guard.

Mutation control B: org fail-closed removed from the shim

3 violations · no-company inputs crash downstream (TypeError ×2, "org id is empty") instead of being refused with no_org · exit 1

Green run: 0 violations, three no-company inputs refused, one company-only input resolves.

Ratchet: baseline on main is 122 "if no building, stop" lines in 97 files, matching the inventory. Tree check green; a guard planted in a copied file, a guard in a new file, and a removed guard each turn it red.

Not replayable in the POC: 7 control reply rows had no matching notice in the pull; the "sender is one of our own addresses" check cannot pass on scrubbed senders; reply headers are synthesized; companies carry no time zone, so a property-less record has no hold clock on either side today (PR 1 adds a company zone, see decision 8). Results: scripts/scope-resolver-poc/results/ in the worktree.

Migration: six PRs, each proven by replay

PR1  one scope answer + org-level owner door + ratchet   (no behavior change)
 ├── PR2  a person is reached                              ─┐ parallel
 └── PR3  the team is told, and their answer gets back     ─┘
        └── PR4  recognized on the company line (residents, vendors, staff by role)
               ├── PR5  after the call (promises checked, transferred tour booked)
               └── PR6  turn settled + company context on text turns
PRPlain wordsEst. linesProof before merge
1One answer to "which company and building"; the org rung reachable without a building; the ratchet~540Replay of every live customer's records: zero differences
2"Get me a person" pages the company owner; forward-to-manager emails someone~510Replay: differences only on property-less records, listed in advance; Willows company line: an owner email arrives
3New message on an escalated thread is announced; the team's reply reaches the renter; daily note and staff texts go out~460Same; Willows: a staff reply reaches the test renter
4Residents, vendors and staff on the company line recognized by role; role beats a stale renter match~510Same; Willows: a known resident and a staff member call the company line
5Promises checked and transferred tours booked on company-line calls~510Same; Willows: tour agreed on a transferred call is booked
6Turn-settled event and company context on property-less text turns~400Same; Willows: a text turn logs the event

Estimates scale the Nov 2 fix (57 lines per guard including its test) and are estimates, not measurements. Six PRs instead of one because about 50 sites across six independent flows cannot fit under the 1,000-line gate; each PR is one user-visible effect. All six are fixes: no switch, live everywhere on merge.

Decisions for Fede

TL;DR: one yes unblocks the work: go on Option B, and PR 1 (no behavior change) opens within the hour. Decisions 2 to 8 are needed only before PR 3 or later and can come as the chain moves.

  1. Chain status (Nov 3, 2:45 AM MT): all 16 PRs merged. 1a #24692, 1b #24732, 1c #24695, 1d #24743, 2a #24742, 2b #24782, 3a #24757, 3b #24767, 4a #24764, 4b #24772, 4c #24790, 5a #24765, 5b #24763, 6 #24759, plus the live-run fixes 3c #24842 and 3d #24856 and the replay script #24828. Every change is a fix with no switch, proven by read-only replay of real records (11,6k conversations and 553 notices unchanged where a building exists). Live rung: not yet run. Willows testing is closed; the voice provider ran out of credits on Nov 3 (every call failing since 11:23 PM MT; purchase pending). A text proof at Fairhaven was tried and stopped before sending: every Fairhaven text or email thread gets a building stamped by the number map, so the no-building records there are only voice calls and video sessions. The live proof therefore runs as voice calls once credits are back. The staff-reply leg ran live on Nov 3 (after #24767 and #24746 deployed): a Fairhaven leasing agent who was not on the notice replied by email on the one no-building thread and was admitted by their company-level role; the note did not fit the call, so Clara held a one-line ask back to the agent and sent nothing to the caller, with no "I don't see who"; a second note with no question (a "?" only inside a signature link) got no reply. A third note that fit the call ("can you tell her what the next step is on that faucet?") got Clara's own answer in the same thread within 30 seconds, naming the caller and the faucet with no building on the record: the original Hugo/David failure, now passing live. Three defects found by the run, all filed: the hold was filed under the company's first building (#24835); marking a no-building call handled did not clear Clara's pending question (fixed, #24842); a teammate's follow-up on a handled call was dropped silently (fixed, #24856). Both fixes were proven live the same morning: "handled" closed the notice and withdrew the pending question, and a follow-up on the closed notice got Clara's answer in the same thread within 25 seconds. Voice proof (Nov 3 morning, credits restored, three robocalls to the Fairhaven company line): the owner page at company level, staff recognition with no building, and a tour booked at the home the caller named all passed live. One gap found: the after-call promise check only knows the "pinned home" that the maintenance tool writes, so a tour booked at a named home still left it saying "no home on the call"; fix PR 5c (#24873) merged and deployed Nov 3 17:00Z; its live call could not reach Clara because the new Clara front door (Gera, #24820) timed out waiting for ElevenLabs on every Fairhaven call after 15:07Z and rang the test staff line instead; handed to Gera's side, live proof pending. Not yet exercised live: a tour agreed during a transfer to a person, and a co-tenant calling in (no Fairhaven persona for either without new test data). Open follow-ups: #24697, #24698, #24744, #24760, #24769, #24770, #24773, #24774, #24793 (direct voice path skips co-tenant occupancy), #24822 (company-notice thread copy). Stamp: "Approved by Fede, Nov 2 (Option B); chain merged Nov 3; live proof pending."
  2. New decision from PR 6 (#24759): company-level context for Clara's prompt (settings, knowledge, hours) on front-door threads is NOT in the chain: it changes how the prompt is built and needs evals plus a choice of which company record stands in for a building. Recommendation: separate follow-up with evals, after the chain. Same for tagging front-door threads as 'front-door' in the settled event (today they report 'none' so live behavior is unchanged).
  3. Go on Option B? Fede, Nov 2: yes, "a clean refactor of this", "lots of testing". PR 1 (foundation, no behavior change, proven by a shadow diff of the new resolver against the four old ones over every live customer record) opens now.
  4. Company-level holds. Fede, Nov 2: "not sure if we need that"; no new queue surface is built. A property-less reply follows the company's own hold setting through the existing queue; revisited only if a real property-less held reply shows up. The review queue is keyed by building, so a property-less held reply has nowhere to go today. Options: (i) a company row in the queue, shown on the company page recommended; (ii) file it under the placeholder; (iii) never hold property-less replies, send or drop. Needed before PR 3.
  5. The 12 inverse cases. Closing them makes Clara do less on company-line threads (the company's "off" switches and Supervised holds start applying). Recommendation: yes, they are safeguards and should apply.
  6. Building-scoped staff on the company line. Today only company-scoped roles count as staff (the inbound-resolution code's note, attributed to your Oct 15 ruling). Recognizing a building-scoped leasing agent who calls the main line widens who counts. Recommendation: recognize them as staff of their building only, never company-wide.
  7. Admin visibility of property-less records is sign-in and permissions code, Gera's territory. Recommendation: hand to Gera via the bridge with this page.
  8. Company-level on-call. The emergency chain is per building on purpose. Recommendation: leave as is; PR 2 pages the company owner, not the emergency chain.
  9. Reports use the matched building. The daily hold report labels a front-door call by the row ("Situs Group"); the resolver labels it by the home the caller confirmed ("Mayor Apartments"). Recommendation: yes, reports read the same resolver as everything else.
  10. A time zone on the company. Companies carry none, so a property-less hold has no clock today. Recommendation: add an organization time zone in PR 1, defaulted from the company's buildings when they agree, set on the company page otherwise.

What this changes in existing rules

Sources, overlap, caveats

PropFlow Docs