One place for everything Clara knows about a company and its buildings. Decided; now being built.
2026-11-05 · planner on Fable 5 · 10 critique rounds, final scores 8 / 8 / 8
Decided 2026-11-05. One Company knowledge base per company, buildings inside it; the only way. Ingest is rebranded as its staff app (the spreadsheet grid, at kb.propflowai.co) over the same data Clara reads. Customers get one Settings tab, "Knowledge base · Ask": they ask in a chat and correct facts there. The Fees, Policies and Knowledge pages are deleted. Settings keeps only true settings (who to call, hours, calendar).
| Decision | Gera chose |
|---|---|
| Name | Company knowledge base |
| How customers see/fix facts | "Knowledge base · Ask" chat tab; a manager's correction applies at once, anyone else's becomes a question |
| Money | A fee from one uncontested source is a fact, for Clara and for leases; disagreeing sources become one question per building and the lease waits |
| Build | Drive it to completion now; Gera's go only for steps that change real customer data |
What it is. One brain per company, each building inside it. Every entry is a short plain sentence about one topic, with its source and who confirmed it beside it. Code that does math reads the number out of the sentence ("$45 per applicant" still gives "$90 for two"); an unclear sentence means Clara does what she does today. Settings keeps only what makes the product act; the Fees and Policies pages become brain entries. Who-to-call and hours are pre-filled from the brain; once saved, a later brain change is a one-tap question, never silent. Ingest becomes how the brain learns: every channel, daily, each fact with its source. Everything Clara knows about your company is the brain; how she behaves stays ours, in code.
The money rule. A fee is a plain fact only if a manager confirmed it, or your team set it on the unit in AppFolio and nothing else we hold for that building disagrees. Anything else Clara says with its source ("as posted on our listing"). When records disagree, your team gets one question per building, Clara quotes with the source for 14 days, and a lease waits for a manager. A manager's confirmed value is never replaced by AppFolio without a question.
Three decisions:
| Decision | Our default | |
|---|---|---|
| A | The name: Company brain, Knowledge base, or Playbook. | Company brain |
| B | Who edits: customers read-only and "Tell Clara" (your words), or company admins also edit the text box. Tell Clara ships with the page. | Read-only + Tell Clara |
| C | Clara says a fee as fact only if a manager confirmed it or it is AppFolio's own fee setting that nothing else contradicts; otherwise she says where she saw it; if our records disagree, your team gets a question. (Alternative: one uncontested source is a fact, which is how the "$45 vs free" Situs answer happened.) | Yes |
Day one, counted. Camellia's $38 fee and Western Slope's $40 fee are plain facts (AppFolio and our records agree). Situs: AppFolio says $45 on six houses where the fees our team recorded at onboarding say $40, and $31 where they say $26: seven questions; the rest is said with its source until a manager taps once. Yale's fees disagree with its record: a question.
Stages. A: the page is live and Clara reads what it shows. B: Settings stripped, the brain learns daily. C: old records copied in, old code removed. About 41 small changes; nothing Clara quotes today goes quiet without your team being asked.
# Company knowledge base: the decided plan on one screen
**What it is.** One Company knowledge base per company, each building inside it: the only place a fact about your company
lives, one plain sentence per topic with its source and who confirmed it; code reads the numbers out of the sentence. **Your
team meets it in one Settings tab, "Knowledge base · Ask"**: a chat where they ask ("what's our pet fee at Dahlia?") and
Clara answers with sources; corrections are typed into the same chat. The Fees, Policies and Knowledge pages are deleted, not
replaced. **Ingest is the knowledge base's own app**: the same grid, renamed kb.propflowai.co, where Propflow staff see every
entry with where it was found, plus the engine that reads every channel daily. One store behind both. Settings keeps only
what makes the product act, pre-filled from the knowledge base. How Clara behaves stays in code.
**The money rule (one source is enough, for Clara and for leases).** A fee is a fact whenever one record we hold says it and
nothing else we hold for that building disagrees; the same figure goes on a lease. When two records disagree, your team gets
one question for that building, Clara quotes the better figure with its source for 14 days, and a lease there waits with both
figures named. A manager's confirmed value is never replaced without a question. What this gives up: a lone wrong listing is
said as fact, and could reach a lease, until a second record arrives or a manager corrects it; a weekly check lists every
lease written from a single-source figure.
**Day one, counted.** Camellia and Western Slope: every fee is a fact. Situs: AppFolio says $45 on six houses where our
helper recorded $40, and $31 where it says $26: seven questions, leases there wait. Yale: unit fees $38/$35 against a $30
record, deposits likewise: a question.
**Stages and start.** A: the Ask tab live, old pages gone. B: Settings stripped, the app renamed, daily learning on per
company. C: old records copied in, old code and names deleted. About 41 small changes. **Building starts when the critics
pass this plan**; you see one localhost preview of every screen before any screen is split into PRs. Every go is yours.
| | Decision | DECIDED (Gera, 2026-11-05) |
|---|---|---|
| **A** | The name | **Company knowledge base**, the only name; ingest is rebranded as it (kb.propflowai.co). |
| **B** | Who edits | Customers: read-only + corrections through the **Knowledge base · Ask** chat (a manager's change applies at once with Undo; anyone else's becomes a question); no full UI for customers for now. Staff: the grid. |
| **C** | What is a fact | **One source is enough**, for Clara and for leases; disagreeing records become one question per building, and a lease there waits. |
<details><summary>Details</summary>
- FYI: renewal offer terms stay typed in Settings, pre-filled from the knowledge base. You will also get three prod-write
go's (hours; the ingest rows becoming evidence-only; copying the old fee and policy records in) and one go per company to
turn daily learning on.
- Manager roles that pin a value: property manager, org admin, regional manager, accounting, assistant PM, leasing assistant.
- A unit left at $0.00 in AppFolio is "not set", never "no fee". A sitting tenant's held deposit is never a price.
- Residents hear "your lease has the exact terms" instead of "confirmed when you apply" while a question is open.
- How a PM fixes a fact: Settings › Knowledge base · Ask, type "Camellia's application fee is $40 per applicant"; a manager's
change applies at once (Undo on the answer) and pins the value; a leasing agent's statement is one more record (a fact if
nothing disagrees, a question if something does). "What did it say before?" shows the history.
- The chat is the existing Ask Clara chat with two new abilities (read the knowledge base, correct it); the grid is today's
ingest grid; the facts live in one table and both read it.
- What you see first: the Ask tab answering "what's the application fee at Camellia?" with "$38 per applicant (from
AppFolio)" and, for Yale, "$38 on most units, $35 on some; your record says $30; your team has been asked"; the grid with
every company's entries; Settings without the old pages.
</details>
| Round | Engineering | Gera's intent | Clara safety |
|---|---|---|---|
| 1 | 6 | 6 | 6 |
| 2 | 7 | 7 | 7 |
| 3 | 8 | 8 | 7 |
| 4 | 7 | 8 | 8 |
| 5 | 7 | 8 | 8 |
| 6 | 7 | 8 | 7 |
| 7 | 7 | 8 | 7 |
| 8 | 7 | 8 | 8 |
| 9 | 7 | 8 | 7 |
| 10 | 8 | 8 | 8 |
Planner on Fable 5; three independent critics per round (fresh each round), 8 = approve. Stopped at round 10 with all three at 8.
# Company knowledge base — the decided plan (self-contained)
Planner output, 2026-11-04, after ten critique rounds (loop closed at 8/8/8). Read-only. Every claim re-checked against `propflowai` `origin/main
750a4a0ea26`, `propflow-ingest` `origin/main 757e3dc`, `agentflow` main, and Fede's design page. This document stands alone.
**Every rule about money lives in §2 (the money table, rows M1–M12); every other section points at a row and restates
nothing.** The people version is `plan-decided-for-gera.md` beside this file (§0, the name, the money rules in plain words, the
PM's steps); everything else here is the builder spec. The changelog and rebuttals are Appendix A and B.
---
## 0. One screen (read this first)
**In five sentences.** One brain per company, each building inside it; every entry is a short plain-text fact about one
topic, showing where it came from, who confirmed it and whether Clara may say it. Settings keeps only what makes the product
act (who to call, hours, modes, calendars, connections); the two that are also facts, escalation and calendar/hours, are
pre-filled from the knowledge base, and **once your team saves one, later brain changes arrive as a one-tap question rather than
silently** (who gets paged never changes without a person). Ingest becomes how the knowledge base learns: it reads every channel
daily, keeps what it finds with its source attached, and asks your team only when records disagree, in one short daily list
that is on from the company's rollout day. Everything Clara says or sends, and every lease she prepares, reads the knowledge base
through one door; she states a fee as plain fact whenever a single source we hold says it and nothing else we hold for that
building contradicts it (decision C, "one source is enough"), **and a lease is written with the same figure under the same
rule**; while two records disagree she says where she saw the figure ("as posted on our listing"), the lease waits, and your
team gets one question for that building. Your team meets the knowledge base in one Settings tab, **Knowledge base · Ask**:
a chat where they ask ("what's our pet fee at Dahlia?") and correct; Propflow staff see every entry in the grid at
**kb.propflowai.co**, today's ingest app renamed, reading the same rows. Where code has to do math (fee × applicants, deposit by
bedrooms, booking hours), it reads the number from the sentence; if the sentence is unclear it does what it does today.
**Gera decided on 2026-11-05 (A, B, C below are DECIDED; the FYI stands):**
| | Decision | DECIDED (Gera, 2026-11-05) |
|---|---|---|
| **A** | The name. | **"Company knowledge base"**: the new and only name; every older label ("Knowledge & policies", "Knowledge intake", "Company brain", "Ingest") is removed by the naming sweep in §12.3. |
| **B** | Who edits. | **Read-only + corrections through a chat.** Customers get **one Settings tab, "Knowledge base · Ask"** (a ChatGPT-like chat: ask a question, Clara answers from the knowledge base with sources; a correction typed into the same chat applies at once with Undo for a manager and becomes a question otherwise). No full knowledge-base UI for customers for now ("too overwhelming"); the Policies and Fees pages are deleted, not replaced by cards. Propflow staff see and edit every entry in the grid at kb.propflowai.co (§1.2). The Ask tab ships in the same PR that deletes the old pages, so there is never a day without a way to correct a fact. |
| **C** | What Clara states as fact, and what goes on a lease. | **"One source is enough"**: any single source nobody contradicts is stated as fact; disagreeing sources become one question per building, and Clara quotes with the source (or hands off silently) while it is open. **Leases follow the same rule** (Gera, 11-05): an uncontested figure is written into the AppFolio lease whatever its source; a contested figure holds the lease ("not ready", both figures named) until the building's question is answered (M5). |
| FYI | Renewal offer terms and eligibility stay typed in Settings (they land on a resident offer and a PMS write), pre-filled from the knowledge base's lease-lengths entry (part 14). Stands. **Fede is not looped in on this effort** (Gera, 11-05): every go below is Gera's. | — |
You will also be asked for about **three prod-write go's** (the hours data move, the ingest rows becoming evidence-only, the
backfill per step) and **one go per company** to turn daily learning on. **Building starts when the critics pass this plan**
(part 0 and part 1 first); you see **one combined localhost preview** of every screen (the Ask tab, the kb.propflowai.co grid,
the stripped Settings) before any UI PR is split out (§12.4).
**What decision C means on the day the money render lands (9b), counted** (read-only prod reads, 2026-11-04; §8.2 step 1 re-counts
before each company's cutover):
| Company | Live buildings | Buildings with a fee record | Fee values on file | Quoted as fact on day one | Quoted with their source on day one |
|---|---|---|---|---|---|
| Situs | 53 | 20 | 29 building-record values (application fee on 9 buildings, pet and security deposits on the rest); AppFolio unit fields: a fee on 14 units, a deposit on 345 units | **Under C, almost everything**: of the 9 application-fee records, **2 are facts and 7 are questions** (AF 64: record $26 vs unit $31 and marketing $48; AF 66, 70, 75, 78, 84, 86: record $40 vs unit $45); the same 7 unit fees are questions and the other 7 are facts; the 20 other record values and the 345 unit deposits are facts unless the 9b dry run finds a building-scoped disagreement (a listed deposit that differs) | the 7 contested buildings, quoted with the source until a manager answers |
| Western Slope | 90 | 8 | 1 building-record fee ($40); AppFolio unit fields: $40 fee on 258 of 260 units, a deposit on 93 units | **everything**: the $40 fee (record and units agree) and the 93 unit deposits are facts unless a listed deposit differs (dry run) | 0 known; contested unit deposits if the dry run finds any |
| JPCO (Camellia, Yale) | 2 | 2 | 14 building-record values (+ Camellia's lease policy); AppFolio unit fields: Camellia $38 on 120 units, deposits $300/$400; Yale $38 on 20 and $35 on 15 units, deposits $200/$300/$1,500 | **Camellia: all facts** (AppFolio and the record agree); **Yale: its fee and deposit are questions** (unit fees $38/$35 vs record $30; unit deposits vs record $350) | Yale's fee and deposit |
"Day one" is the day the money render lands (part 9b, Stage A); the AppFolio unit fields are synced before it (part 13a, in
Stage A) and first read into quotes on that same day, so the table above is what Gera sees in the preview. **Under C, day one
changes almost nothing a renter hears**: every fee Clara quotes today as a bare number stays a bare number unless a second
record for that building disagrees with it, and then it is quoted with its source until your team answers one question. **Three things to know about that:** (a) **fees your team typed on the old Fees page count as
fact only if a manager typed them**; a leasing agent's or a Propflow helper's entry shows "not confirmed" until a manager taps
once (under C a Propflow helper's or a leasing agent's entry is a fact like any other single source; the card still shows who
recorded it and when, and a manager's tap still pins it against later machine values); (b) a home's **rent** comes from AppFolio and stays a plain fact, and under C so does every single uncontested source: a
unit's AppFolio application fee or default deposit (a unit left at 0 is unset, not "no fee"), a listing's figure, a charges
ledger value, a building record, a staff member's policy statement; a building-scoped disagreement makes it a question (M3);
(c) **a manager's one tap still matters**: it pins a value (`confirmed`) so a later AppFolio or listing change becomes a
question instead of silently replacing it, and it settles an open question; the staff grid lists every fee with its source,
a manager can pin one by saying so in the Ask tab ("yes, $40 is right"), and the daily list carries the open questions. A renter asking "what do two of us pay to
apply?" at an uncontested building hears "$90 for two"; at a contested one, "based on our listing's $45 per adult, two
applicants would be $90, confirmed when you apply" until your team answers. **Until part 9b, Clara quotes every fee exactly as today.**
**Leases on day one** (same rule): a package whose deposit or fee is uncontested is approved and written as today; at the 7
contested Situs buildings and at Yale a package holds with both figures named until the question is answered.
**When records disagree about the same building** (Situs: AppFolio's unit field says $45 on six DU houses where the fees our
team recorded for Situs at onboarding say $40, and $31 at 1401 S Emerson where they say $26, a question at each of those
seven; Barcelona's marketing text says $48 and $31 while Miriam wrote "no application fee" for Barcelona, a question at
Barcelona; Yale's unit fees $38/$35 against its record $30, a question at Yale; a company-wide ledger mode does not by itself
contest one house), Clara keeps quoting the best one with its source for 14 days after your team is asked; a
value a manager confirmed keeps being stated as fact while the question is open; if nobody answers, she stops
giving a figure, hands the question to your team silently, and answers with what she does have (the application link, what is
on file, the next step). A fee a tech or leasing agent tells Clara is a question to a manager, never quoted on its own.
**Three stages.** **Stage A "One place"**: the Ask tab is live, Clara and the tab read the same rows, the old Fees and Policies
pages are gone, Camellia is no longer blank. **Stage B "Settings stripped, the knowledge base learns daily"**: Settings follows
the knowledge base; the ingest app is renamed kb.propflowai.co and its grid reads and writes the same rows; daily learning on per
company. **Stage C "Old stuff gone"**: backfill and removal. About 41 small changes over several weeks. **What you see first:**
one localhost preview with three screens: the Ask tab answering "what's the application fee at Camellia?" with "$38 per
applicant (from AppFolio)" and, for Yale, "$38 on most units and $35 on some; your record says $30; your team has been asked";
the kb.propflowai.co grid with every company's entries, each with its source; and Settings without the Fees and Policies pages.
---
## 1. The target model, in plain words
### 1.1 One brain, buildings inside it
- **One knowledge base per company; each building inside it.** A fact lives where its topic says: application fee per
building (Gera's example), fair-housing wording per company, pet policy either. A company-level question never gets a blank
for a per-building topic; it gets the roll-up ("Application fee: set per building. Camellia $38 · Yale $40 · 3 buildings have
none; $45 seen in listings").
- **One sentence per topic.** That is all an entry is. Beside it, shown but never edited: where it came from, when, who
confirmed it, a small tag on the four topics Clara uses but never says. Status is three words: confirmed, learned, suggested.
**Customers** read and correct entries through one chat (Settings › Knowledge base · Ask); **Propflow staff** see every entry
as a row in the grid at kb.propflowai.co (§1.2).
- **Settings = what the product does; the knowledge base = what the company is.** Escalation/on-call and calendar/hours/holidays are
in both: a Save in Settings is also the knowledge base's confirmed word; a later brain change is a one-tap question. The runtime
reads the typed value only.
- **Ingest IS the Company knowledge base's own app.** Every channel, daily; what it finds is written with its source; a
disagreement is a question in one daily list. Today's ingest app (ingest.propflowai.co) is renamed **kb.propflowai.co**; its
spreadsheet grid is the staff view onto the same rows Clara reads, and its engine is the daily learning (§6).
- **Clara reads the knowledge base through one door.** Her prompt, her pricing and lease tools, the call snapshot, the tour reminder,
the key-pickup text, the lease-answers block, the lease package and the new-lease paperwork all read the same fenced reader
(§3.6). Money follows §2.
- **Numbers for code are pulled from the text, never typed.** Unclear → today's missing-value path (§4).
### 1.2 The name and the surfaces (DECIDED: "Company knowledge base"; one grid for staff, one chat tab for customers)
Gera, 2026-11-05: the product is the **Company knowledge base**, and it is the new and only way; ingest is **rebranded as the
Company knowledge base** (its README:52 and CLAUDE.md:63 say "Policy registry" and "intake sheet" are placeholder names Gera
will name; he has). Two surfaces, one store:
- **Staff (Propflow): the grid at `kb.propflowai.co`.** Today's ingest web app (`web/`, Cloudflare Pages project
`propflow-ingest`, behind the Access app "Ingest", `web/wrangler.toml:1`) keeps its spreadsheet view (`SheetTable.tsx`:
Policy · Status · Value · Detail · Where it was found · Source · Seen · Changed · Agreement · Confidence · Conflicts · Notes)
and gains the building dimension (13b). **Our technical call (not re-asked):** the facts live physically in propflowai's
table (`brain.<topic>` rows, Clara's hot path); the grid's Value and Status columns read those rows through the canonical
rail (`GET /api/internal/brain/entries?org=…`, the read twin of the POST, machine bearer), and every edit from the grid
(teach, answer, promote, hand off) goes through the same rail into `writeBrainEntry` / `acceptRule`; the evidence columns
(where found, source, seen, agreement, conflicts, attempts, notes) stay in `propflow-ingest-sheets` as the ledger behind each
entry. **No second copy and no sync**: `ROW#`'s stored `value`/`status` become evidence-only ("found this run") in 13b.
The domain: `kb.propflowai.co` on the same Pages project (custom domain added, `ingest.propflowai.co` 301s to it, the Access
app renamed "Company knowledge base"); the repo name and the SYSTEMS row key `propflow-ingest` stay as code identifiers. A
Propflow staffer's grid edit lands `confirmed` only `onBehalfOf` a named customer manager (M1), as any other staff write.
- **Customers: one Settings tab, "Knowledge base · Ask".** A chat, ChatGPT-like, reusing the existing Ask Clara chat
(`src/components/feature/ask-clara/`, `api/dashboard/chat`, the 300 s tool loop on `backgroundClient`, tab-scoped by
`useScopedFetch`) with a knowledge-base tool set: `get_knowledge_base_entries` (→ `readBrain({ mode: 'page', reader: { role, tiers, surface: 'tool' } })` with the
signed-in user's role and tiers, so `readerScope` and `mayHearMoney` apply exactly as on Clara's own tools and a
maintenance tech never hears a deposit through the tab; card labels and sources, never the renter-facing M2 labels) and `set_knowledge_base_entry` (→ `writeBrainEntry`; a
`mayConfirmMoney` person's correction applies at once, `confirmed`, with an Undo chip backed by `HIST#`; anyone else's is one
more source through `acceptRule`: a fact if nothing disagrees, a question if something does). The private-information gate
and the fair-housing screen run on that tool exactly as the teach rail runs them (§5.3). No cards, no per-topic boxes, no
building tabs for customers for now ("too overwhelming"); the Policies, Fees and Knowledge pages are deleted in the Ask
tab's PR, not replaced.
- **Words.** Where this plan says **"the card"** it means the entry as shown: a row in the staff grid, or the sourced line in a
customer's Ask answer. **"The page"** is the staff grid at kb.propflowai.co. The building's name is a column in the grid and a
word in the chat. Fede's Internal notes stay a staff-only drawer named **"Team notes"**; the word "brain" leaves every
user-facing surface. Code keeps the module name `src/lib/domain/brain/` and the registry keys `brain.<topic>` /
`brain_suggested.<topic>` (internal identifiers, never shown). The naming sweep that removes "Knowledge & policies",
"Knowledge intake", "Company knowledge", "Fees & deposits", "Company brain", "Internal notes" and "Ingest" from UI copy is
§12.3.
### 1.3 What is and isn't the knowledge base ("how deep")
| Class | Holds | Lives | Changed where | Clara reads it as |
|---|---|---|---|---|
| **Brain** | company and building facts and policies: fees and the deposit policy (§2), pets, rules, hours as words, move-in/out, renter arrival and access instructions, contact wording, how emergencies are defined, what staff told Clara. **Never a personal phone number or email** (Fede 2026-10-07, #17626). | `brain.<topic>` rows on `ORG#`/`PROP#` | the Ask tab (customers, B), the kb.propflowai.co grid (staff), CLI, the rail, a Settings Save on a BOTH topic | the knowledge block, her tools, her reminders, the lease policy |
| **Settings** | what the product does: ring lists, the on-call chain with its numbers, path modes, sender timing, calendars, hours/holidays typed, connections, recipients, how Clara talks (`never-say`, languages), renewal engine terms (FYI), **vendor access instructions** (`maintenance.access_instructions`, part 10c) | registry keys, typed | Settings page, CLI twin | never quoted; acted on, or rendered as an instruction |
| **Property record** | identity: name, address, office and emergency phone, application link URL, aliases, units | `PROP#` row, `PROPALIAS#`, PMS mirror | in AppFolio (synced), or a Propflow admin; the Ask tab answers "change it in AppFolio" | quoted as identity, as today |
| **PMS mirror** | live records: units, **rent** (`Unit.rent`, from the rent roll), availability, tenants, work orders, charges, balances, a current resident's own lease deposit (`TenantOccupancy.securityDeposit`, `types.ts:20266`), and **each unit's `ApplicationFee` and `DefaultDeposit` from AppFolio's unit directory report** (`reports/unit_directory.json`, read today by `AppfolioClient` and by ingest's `appfolio/api` resolver; synced into the mirror in part 13a and read into quotes only at 9b, under M3; §2 M1b). The scraped marketing-page deposits (`UnitListingDetails.deposit`, `leasing/public-listings-sync.ts:144`; `UnitPrice.deposit`, `types.ts:3870`) are listing facts (M2), not PMS fields. | synced rows, tools | never (read-through) | the listing card, tool results |
| **Clara's skills** | Propflow-owned behaviour: prompts, RULE 7.x, the emergency floor, fair-housing law, jurisdiction caps | code | PRs | her rules |
| **Conversation memory** | what one person said and was told | `CONV#` | never | the thread |
| **Team notes (Internal)** | meetings, forwarded email, private notes | internal-notes rows behind the wall | staff tools | a staff-only tool, never a renter turn |
| **Evidence (ingest)** | every attempt, path, conflict, run, decision behind an entry | `propflow-ingest-sheets` (`ROW#` evidence columns, `RUN#`, `DECISION#`, `TRANSCRIPT#`…); shown as the evidence columns of the kb.propflowai.co grid | nowhere | never |
| **Per-tour data** | self-guided lockbox codes (`Tour.selfGuidedLockboxCode`, texted to the renter on arrival by design, `escalation/self-guided-lockbox-code-delivery.ts`) | the tour row | the tour flow | the tour flow; out of this plan's scope |
**How deep:** Clara's knowledge *of the company* is 100% brain. Her *behaviour* is not brain and never will be. Concessions
stay on the website scrape.
---
## 2. Money: the one table
**Plain English (decision C, "one source is enough").** Clara states a fee as a plain fact whenever one source we hold says it
and nothing else we hold for that building disagrees. When two records for a building disagree, the team gets one question for
that building and Clara quotes the better figure with where it was seen, in renter words, until a manager answers. A lease is
written to AppFolio under the same rule (an uncontested figure goes on it whatever its source; a contested one holds it),
and the deposit on the lease is the same deposit Clara quotes. Every other section points here. §2.1 names what C gives up.
| Row | Rule | Mechanism |
|---|---|---|
| **M1 Fact** | Clara states a fee as a bare fact ("the application fee is $40 per applicant") when **any one source we hold says it and nothing else we hold for that unit or building disagrees**: (a) a person's statement in the Ask tab, a staff edit in the kb.propflowai.co grid, a review form, document upload or a teach by SMS/email (any staff role whose statement passes the teach rail's one-off and not-knowledge checks and the fair-housing screen); (b) AppFolio's own per-unit `ApplicationFee` / `DefaultDeposit` (`reports/unit_directory.json`; non-zero; `0.00`, null, empty, non-numeric or negative = unset, never "no fee"; rent-roll `Deposit` is a sitting tenant's held deposit and never a price; per unit, folded to the building only when every unit is set and agrees); (c) a listing figure, a charges-ledger value, marketing text, the website, a document, or a building record of unknown writer. **`confirmed` vs `learned` still differ in one way**: a `confirmed` row (written or tapped by a `mayConfirmMoney` person: `pm`, `org_admin`, `accounting`, `regional_manager`, `assistant_property_manager`, `leasing_assistant`, per Gera's 2026-08-19 ruling; `platform_admin` only `onBehalfOf` a named customer manager; never a machine) is never replaced by a later machine value without a question (M3); a `learned` row is a fact Clara says, and writes on a lease (M5), but a later disagreeing source turns it into a question. | `status: confirmed \| learned`, rendered plain on every surface; the card shows the origin ("from AppFolio", "as posted on our listing", "per our records", "recorded by <name>"); `acceptRule` (§6.2) decides uncontested vs contested at write; the unit fields are synced by 13a-1 (stored field + pure reader) and read into quotes by 9b; freshness: an M1b value older than `UNIT_DIRECTORY_STALE_MS` (24 h) is still said, but `pms-write` holds on it (M5; decided §10.3). |
| **M2 Contested quote** | **Only while a disagreement is open (M3)**: Clara quotes the M3 winner with where it was seen carried inside the fee line, in renter words: `Application fee: $45 per applicant (as posted on our listing; confirmed when you apply)`. Labels, and only these: listing or a unit's marketing description → "as posted on our listing"; website → "as posted on our website"; charges ledger, a document, the building record, an unknown writer, a staff statement, a Propflow stamp → "per our records". Never "AppFolio", "charges", "marketing text", a staff name or "the Fees page" (card labels only). Closing clause: "confirmed when you apply" for a prospect or applicant; "your lease has the exact terms" for a resident (M6). After the 14-day window with no answer the row moves to `brain_suggested` and the topic reads as not on file (§6.5). | Decided by the open question on the row, never by origin. The attribution lives in the rendered line (RULE 7.1's "caveat the line carries", `clara-leasing.ts:679`). One clause is added to `feeSourceRule` (`leasing/fee-source-rule.ts:53-61`): *"a fee line that carries where it was posted is quoted with that mark, exactly as written"*; `withoutFeeFigures` on raw listing descriptions stays; drift test updated; voice re-sync under the 11-03 rule (0 simulations). The resident scope rail in `clara-unified.ts` gains one line (the leasing fee rule is kept out of resident prompts by `RESIDENT_FORBIDDEN_FEE_PHRASES`, `fee-source-rule.ts:156`), with a drift test. Census reason `contested-sourced`; bench: a contested fee yields the sourced line and no refusal; an uncontested lone listing fee is a bare fact. |
| **M3 Disagreement** | Two sources we hold **for the same unit or building** with different values, or a staff statement about that building against the winner (never a company-wide mode or a statement about another building: Miriam's "no application fee" is about Barcelona), is **one question per building and topic** in the daily list (§6.3), naming every figure ("AppFolio's unit setting $45 on 4 units; your record $40; listing $45"). Clara quotes the better source per M2 for 14 days after the question is delivered; if unanswered, the row moves to `brain_suggested` and the topic reads as not on file (§6.5). **A person's word stands**: when a later machine value (AppFolio, a listing, charges) disagrees with a `confirmed` row, the confirmed value keeps rendering plain while the question is open, the card names both with dates, and only the lease holds (M5). A `learned` row that acquires a disagreement later is handled the same way; no row is left live past the window with an open question beside it. | `acceptRule` returns `question`; the row keeps its status plus the M2 render; `askedAt` stamped on delivery (§5.2); the nightly sweep (§8.2) moves it after 14 days; a `mayConfirmMoney` answer writes `confirmed` and clears the question; a non-manager answer is itself one more source. Situs today: AF 64, 66, 70, 75, 78, 84, 86 (7 questions); Yale (1). |
| **M4 Staff teach** | Money a staff member tells Clara is **one source like any other under C**: it passes the teach rail's `one_off` / `not_knowledge` classifier (`escalation/teaching-capture.ts:410-424`; a waiver for one renter never writes) and the fair-housing screen (`compliance/fair-housing.ts:960`, fail-closed); then, uncontested → `learned`, a fact, card "recorded by <name> (<role>)"; contested → a question (M3). A `mayConfirmMoney` sender's statement writes `confirmed` instead (applied at once with Undo from the Ask tab or the staff grid; YES first for a teach by SMS or email), which pins it. Migrated `staff_reply` money follows the same rule at the backfill (the author's role decides `confirmed` vs `learned`); a `staff_reply` section reaches a money topic only through the backfill's topic mapper. **What C gives up here**: a tech's or leasing agent's uncontested fee statement is quoted as fact until a second source disagrees (§2.1). | `writeBrainEntry` sets `confirmed` only for `mayConfirmMoney` tiers; everything else through `acceptRule`; bench: a TEST tech-taught fee with nothing against it is a fact; against a $40 record it is a question. |
| **M5 Lease paperwork / PMS writes (DECIDED 11-05: the same rule as Clara)** | A figure goes into an AppFolio lease whenever it is a fact under M1, **whatever its source** (a manager's word, AppFolio's unit field, a listing, the ledger, a document, a staff statement, uncontested); a **contested** figure (M3 open) holds the lease: `pms-write` returns "not ready" with both figures named, and the lease waits for the building's question to be answered. **The lease's deposit is the same deposit Clara quotes**. **One deposit function**, `depositFor(propertyId, unitId, unitBedrooms, surface)`, in this order: **the unit's AppFolio `DefaultDeposit` when set, fresh, and uncontested** (M1b; from the mirror after part 13a); **when it is contested** (M3: a confirmed tier, the listing's deposit for that home, the building entry or a staff statement differs) the quote is the M3 winner with its source and `pms-write` returns "not ready" with both figures named; else the knowledge base's `security-deposit` tiers resolved for the unit's bedroom count (`resolveSecurityDeposit`), plain when uncontested, sourced and held when contested; else the listing's headline deposit, a fact when nothing disagrees (written to the lease too). The same function and order answer **the application fee per unit** (`ApplicationFee` when set and uncontested, else the M3 winner sourced, else the knowledge base entry, else the listing). LEASE ANSWERS, `get_pricing_details`, the approved-applicant package and the packet all call it, so the lease and the quote never disagree. Three AppFolio lease writers: **(1) the package-review approve route** (`app/api/new-lease-package-reviews/[id]/decision/route.ts`, the only lease writer built and mode-gated today: under Supervised or Live it calls `runLeaseDraftOnly` / `runLeaseSendForSignature` with `item.package.securityDeposit`, `:403-427`; its only money check is `deposit_not_established`, `:267`; its gate is "staff by role" plus scope, `:144-151`; `item.package` is a stored snapshot never re-read): from part 7a the route **re-reads every money figure through `readBrain({ surface: 'pms-write' })` at approval time**; if every figure is uncontested the approve proceeds with the re-read figure (never the stale snapshot); if a figure is contested and the approver passes `mayConfirmMoney`, their approve **confirms** it (`via: 'review'`, the chosen figure, the question cleared) and proceeds; otherwise the route holds with "the $X deposit is contested ($X per AppFolio, $Y per your record); a manager needs to pick one" and the row stays pending. **(2–3) The paperwork rungs** (`new-lease/new-lease-paperwork-sender.ts:8-16`; dark today): `team` → nothing is written; `prepare` → Clara builds the lease in AppFolio when every money figure is uncontested (`policyDepositFor`, `temporal/activities/new-lease-paperwork.ts:144-152`, reads `depositFor` on `pms-write`); otherwise "not ready" (`planNewLeasePacket` `:81-125`) and the coworker note names both figures for a manager's one answer; `clara` → the same gate, then the send. `listedRent` in the packet is a PMS rent-roll field (plain). | `depositFor` in `leasing/lease-terms/`; the `pms-write` surface returns a reason threaded into the approve route's hold and `planNewLeasePacket`'s held reason; the approve route joins Appendix C's 7a row, the census "packet readiness" row and `human-gate.drift.test.ts`; parity test against the real active release on a TEST building; **test: a building whose listing deposit differs from its tier yields one deposit across LEASE ANSWERS, `get_pricing_details`, the package and the packet, and holds the packet**; gate: identical for uncontested figures, "not ready"/held with both figures named for contested ones, census reason `packet-not-ready-contested`; the §0 count script reports packets that would hold per company (Situs 7 buildings, Yale 1) before a rung is turned on. |
| **M6 Resident money** | A resident's own fees (late fee, pet rent, payment and returned-payment fees, utilities) follow M1/M2 through the resident tool (`handle-get-lease-terms.ts:75-87`), the sender surfaces **and the resident prompt line (M2)**, with the resident clause; the prompt still hides new-lease fees from residents (`clara-unified.ts:1811-1826`); a resident's own lease deposit comes from `TenantOccupancy.securityDeposit` (a PMS ledger fact, plain). **The typed fallback applies only while the topic is projected** (`writable: false`); once writable, an `unclear` facet returns the entry's text in the M1/M2 shape and no computed figure, never a frozen pre-brain number. | `readerScope` per surface (§3.3); §4 row 8. |
| **M7 Arithmetic** | The lease answer engine (fee × applicants, deposit tiers, proration; `answer-engine.ts:449-479`) computes from M1 **and** M2 facets; an M2 (contested) input makes the whole line sourced ("based on our listing's $45 per adult, two applicants would be $90, confirmed when you apply"); unclear → today's "not established" lines. The lease packet (M5) never takes a contested input; it holds. | `knowledge-lease-policy.ts` builds the policy from facets carrying their status; the engine's line renderer carries the attribution; census reason `m2-attributed-total`. |
| **M8 Learning path** | Fee pre-fill crons, property creation and the rail all write through **one function, `acceptRule`** (§6.2, **built in part 6**, before any caller): an uncontested find → `learned` (M2); a disagreement → `question` (M3). The per-company rollout gate (`BRAIN_LEARN_ROLLOUT`) applies **only to rows whose `provenance.source` starts with `ingest/`**; the crons and property creation write live data today (they fill blanks only, `charge-fee-prefill.ts:28-30`) and keep producing live `learned` rows at every company from part 7b. **A cron's new row is a fact under C (uncontested → `learned`, rendered bare before and after 9b); only a contested one changes at 9b (M11)**; census reason `cron-learned`; test: a TEST building created after 7b returns its charge fee from `get_pricing_details` bare, and sourced only when a second TEST record disagrees. No machine writer writes `brain_suggested` directly for money; M4's questions arrive through `acceptRule`'s `question` output. | `charge-fee-prefill.ts:224-236`, `listing-fee-prefill.ts:180-187`, `scripts/prefill-fees-from-*.ts`, `app/api/properties/route.ts:412-430` re-pointed in part 7b (Appendix D). |
| **M9 Renewal engine** | `renewal.offerTerms`, `renewal.eligibility`, `renewal.autoStartDays` stay typed Settings, pre-filled from the knowledge base's `lease-lengths` facet in part 14 (FYI in §0). | registry keys unchanged; `prefillFromBrain` (§5.1). |
| **M10 Re-ask** | **There is no re-ask on age.** A row is re-asked only when its source changes or vanishes, which is a disagreement (M3). | §7.5's weekly PMS diff and the quoted-facts ledger feed M3, not a timer. |
| **M11 Migration** | **Under C the migration changes almost nothing a renter hears.** At projection (3c, 4, 7a) every uncontested legacy value renders as today, a bare fact; a contested one (a second source for the same building disagrees) renders per M2 and raises an M3 question, **and that is the only render change, starting at 9b** (census reason `contested-sourced`; the dry run counts contested rows per company; Situs: 7 application-fee buildings; Yale: 1). Projection and backfill status: a legacy `person` row whose recorded actor passes `mayConfirmMoney` (a `setup-intake` stamp's `confirmedBy`, or a `knowledge-review` stamp's submitter resolved to a roster user; a Propflow `platform_admin` with no `onBehalfOf`, an `editedAt`-only edit or an unresolvable submitter count as "no actor") → `confirmed`; every other row, money or not, → `learned` through `acceptRule` (uncontested → fact; contested → question); `staff_reply` → M4. Nothing machine-written, and no person row without a manager actor, is ever `confirmed` (so a later disagreeing source can still turn it into a question). | §8.2 steps 2 and 5 say "per M11" and nothing else; backfill unit tests: an `editedAt` row, a non-manager actor and an unresolvable submitter come out `learned`; a contested legacy value comes out as a question. |
| **M12 Money outside money topics** | A `$` figure inside a non-money topic's prose (custom, amenities, pets) is reported by the census by origin; no fence on it until the count says otherwise. The post-hoc judge and the observe-only check (§7.4) log unbacked figures in sent replies. | census report; `G-BRAIN1`. |
### 2.1 What decision C gives up, and what limits it
Under "one source is enough", **a lone wrong figure is stated as fact**: a building whose only record is a listing that says
$45 (while the office actually charges $40 but nothing we hold says so) has Clara saying "$45" plainly, as she does today. The
alternative Gera rejected (quote every single-source figure with its source until a manager taps) would have made almost every
fee at every customer a sourced quote on day one (the v10 count: Situs 29 of 29, Western Slope and JPCO likewise) with no
road back except a manager's tap. What limits the risk under C, in order of how soon it bites:
1. **Daily learning adds sources** (§6): the charges ledger, marketing text, the AppFolio unit fields, staff replies to Clara's
asks, documents. The moment any of them disagrees with the lone figure for that building, it becomes a question and a sourced
quote (M3, M2). Situs's $45 listing is already contested by its own notes on 7 buildings, so under C it is a question there.
2. **The building-scoped comparison** (M3) means a company-wide mode never contests a unit, so questions stay few and real.
3. **The weekly PMS diff and the quoted-facts ledger** (§7.5) catch a fee the ledger later contradicts and list who heard it.
4. **A manager's tap pins** (`confirmed`), so a later wrong listing can never silently replace a manager's word (M3).
5. **A contested figure never reaches a lease** (M5): the lease holds until the question is answered, so a wrong lone figure
costs a quote, never a signed document, in exactly the cases where we already know something disagrees.
What C does not protect against (and, since 11-05, on a lease too): a lone figure that no second source ever reaches (a building with one listing and no
charges, no marketing text, no staff reply). The dry run reports those buildings per company ("single-source money rows"), and the lease ledger (§7.5) lists every lease written from a single-source figure, so a wrong one is found by the weekly diff rather than by a tenant.
---
## 3. Data model
**Plain English.** One live row per topic per node holding the entry people see; a second row, under a different prefix so no
read of the live brain can touch it, holding suggestions and open questions Clara never reads. A short topic list in code
says where each topic may live, whether Clara may say it, which surfaces may show it to a resident, whether it is safety- or
law-sensitive, every old route, cron and script that used to write it, and whether it may be written yet. Every write runs the
private-information gate. One fenced reader serves every renter-facing surface.
### 3.1 Two slots per topic
Both are registry-keyed setting rows written by `putSettingAttachment` (`src/lib/data/dynamo/setting-attachment.ts:546`): one
live row per node (invariant (h)), ended into `HIST#` by the three-item transact, fenced by `MAPLOG#`.
| Slot | Holds | Who writes | Who reads |
|---|---|---|---|
| `brain.<topic>` | the **live answer**, `status: confirmed \| learned` | a person (the Ask tab, the staff grid, CLI, a Settings Save on a BOTH topic, a manager's package-review approve), `acceptRule` callers (`learned` only), the backfill, the nightly sweep (ends a row by moving it) | Clara (every surface, §3.6), the Ask tab and the grid, Settings pre-fill |
| `brain_suggested.<topic>` (its own prefix: `brain.` is a prefix of `brain.suggested.` and the live read is a `begins_with`, `setting-attachment.ts:11,304`; topic ids never contain `.`) | the current suggestions and open questions, rewritten only when the normalized set changes | `acceptRule` (its `question` output), the backfill, the sweep | the page, the daily list (§6.3); **never Clara** |
- A suggestion never supersedes a live row. The rail ends a `learned` row only when `provenance.source` starts with
`ingest/` and that source has been absent for two runs with no person confirming since; a `migration:*` row is ended only
by a person or by the sweep (M3). A `confirmed` row is never ended by any machine.
- A person's Save, a Tell-Clara confirmation, a manager's package-review approve, or a Settings Save on a BOTH topic writes
`confirmed` and clears that topic's open question.
- **Concurrent writes**: `putSettingAttachment` refuses a put on an occupied slot unless `supersedes` matches; a machine
writer retries once after re-reading; a person's write always wins; one unit test races a rail write against a page Save.
```ts
// src/lib/domain/brain/entry.ts
export interface BrainEntry {
topic: TopicId; text: string; // 1..2000 chars: the only thing a person edits
status: 'confirmed' | 'learned';
audience: 'public' | 'operational'; // operational only for the four topics in §3.3; Propflow staff may tighten, never loosen
provenance: {
source: `person:${string}` | `teach:${'sms'|'email'|'slack'|'call'|'brain-page'}` | `settings:${string}` | `ingest/${string}` | `migration:${string}`;
origin: 'person' | 'pms-setting' | 'pms-ledger' | 'listing' | 'marketing' | 'website' | 'document' | 'staff-statement' | 'unknown';
evidencePath?: string; link?: string; seenAt: string; runId?: number;
};
confirmed?: { by: string; byTiers: PersonRoleType[]; onBehalfOf?: { name: string; channel: string }; at: string; via: 'brain-page' | 'teach' | 'cli' | 'settings' | 'review' | 'migration' };
askedAt?: string; askedVia?: 'digest' | 'held-card'; // M3: when the question was DELIVERED to someone who can answer
confidence?: { score: number; authority: number; agreement: number; freshness: number };
fairHousing?: { screenedAt?: string; failedAt?: string; unavailable?: true }; // every person-statement source
facets?: Facets; facetsDerived?: { fromTextHash: string; by: 'parser@1' | 'llm@1'; at: string; status: 'ok' | 'unclear' };
supersededLegacy?: string[];
}
```
### 3.2 Where an entry came from (`origin`), inferred in this order
Unit-tested per case; the backfill dry run prints counts per company by origin, and those counts are the numbers Gera sees.
1. `pricingVerified.<fee>` present → `person`, with the actor: `{ source: 'setup-intake', confirmedBy }` names them; `{ source:
'knowledge-review', reviewSlug }` resolves to the review's submitter when that submitter is a roster user with tiers
(`types.ts:4010-4016`); otherwise "no actor". Whether the actor makes it M1 or M2 is M11's call (`mayConfirmMoney`).
2. `pricingSources.<fee>` present **with `editedAt`** → `person` **without an actor** (`pricing-verified.ts:36-62` records
only the time), so M11 renders it M2 until a manager taps.
3. `pricingSources.<fee>.source: 'appfolio-charges'` (no `editedAt`) → `pms-ledger`; `'appfolio-listings'` → `listing`.
4. No stamp, and the company's `setup_intake.answer_changed.building_fees` history names a **person** for this building after
the row's write → `person` with that actor. That slot holds only the latest stamp (`onboarding/answer-change-stamps.ts:12`)
and "never fails the save" (`:17-21`), so the dry run reads the slot's `HIST#` chain and marks rows it cannot place.
5. No stamp and only a system record (`property-create-rent-roll-charges`, `app/api/properties/route.ts:412-430`: "an import,
not someone's answer") or nothing → `unknown` (machine). This is Camellia's and Yale's schedule (`pricingSources: null`,
`pricingVerified: null`): "per our records", M2.
6. Sections: `source: 'staff_reply'` → `staff-statement` (M4); `lease`/document → `document`; a section a person wrote in
the editor (`manual`/`PM`) → `person` with the editor's identity when the row records one; **a section or fee written by
an agent-run script** (`patch-camellia-pricing.ts`, `apply-western-slope-*`, `apply-yale-*`, `_enroll-camellia-*`) →
`unknown`, never `person`; website concession → `website`.
7. **Deposits and the unit fee**: a unit's AppFolio `DefaultDeposit` or `ApplicationFee` from the unit directory report, when
set (not `0.00`, null, empty, non-numeric or negative), is `pms-setting` for that unit (M1b, in the mirror from part 13a,
read into quotes only at 9b; plain only when uncontested at that unit or building, M3); the rent-roll `Deposit` column is never `pms-setting` (a sitting tenant's held deposit);
a listing's headline deposit (`UnitListingDetails.deposit`, `UnitPrice.deposit`) is `listing`; the knowledge base's
`security-deposit` entry is the policy narrative plus tiers (M2 until a manager confirms).
8. **Company-tier money** (company `fees.<slot>` rows folded by `pet-facts.ts`; the four money fields of core topics;
company fees applied from pre-fills, `onboarding/company-charge-fees.ts`, `company-listing-fees.ts`): the row's `HIST#`
writer names a settings route with an actor → `person` with that actor (a one-tap "apply this pre-fill" by a person counts;
the card says "from listings, applied by <name>"); a script or system → `unknown`.
9. **Two inference modes.** Clara's path (every turn, tool, snapshot) runs steps 1–3 and 5–8 and never reads `HIST#`; the
rail and the backfill call `readBrain({ mode: 'audit' })`, which runs step 4 and 8's history read. Ingest's source
constants (`sheet/authority.ts` `AUTHORITY[].source`) are **vendored into propflowai as one table with a drift test**
(the repos share no package), in place by part 6, so `appfolio/charges` can never be misspelled.
### 3.3 The topic registry
```ts
// src/lib/domain/brain/topics.ts
export interface BrainTopic {
id: TopicId; label: string; area: Area; // the 10 areas of CORE_TOPIC_CATALOG (policies/core/topics.ts:15)
scope: 'building' | 'company' | 'either';
inherit: 'nearest' | 'both' | 'union'; // building/company-only → nearest; either → both, labelled; lifeSafety → union
audience: 'public' | 'operational'; // operational: closed list below
readerScope: { prompt: 'all' | 'new-lease'; tools: 'all' | 'new-lease'; senders: 'all' | 'new-lease'; snapshot: 'all' };
facets?: FacetKind; lifeSafety?: true; compliance?: true;
writable: boolean; // §8.2: true only when every reader reads the knowledge base AND every legacy writer writes through or refuses
replaces: string[]; // every legacy READ path (Appendix C)
legacyWriters: string[]; // every legacy WRITE path (Appendix D)
prefills?: 'office_hours' | 'tour.hours' | 'holiday_policy' | 'emergency.route.*' | 'ring_list.*' | 'escalation.owner' | 'renewal.offerTerms';
ingest?: { policyIds: string[] };
}
```
Seed: the 53 catalog topics (39 with specs; Office hours, Contact, Office location catalog-only), ingest ids without a topic
(`registry/policies.json`, 100 rows, 0 with scope), the collected answers that are facts (repair-waiting-part,
eviction-marking, portal link, other names).
| Scope | Topics |
|---|---|
| `building` | application-fee, admin-fee, move-in-funds, parking, utilities, laundry, amenities, storage, office-location, **arrival-access** (renter-facing gate/buzz/door code, entrance, parking, office directions; public on every surface, Gera 2026-08-30 "we should never block access instructions", `clara-leasing.ts:977`, `ON_SITE_WALK_IN_RULE` `clara-unified.ts:283`; the virtual-tour exception stays a prompt rule), **arrival-access-shared-building** (the per-unit override `leasing/tour-arrival-info.ts:59-80` reads for scattered-homes condos; its own topic because one text box per topic per node has no sub-entries), holidays-observed, office-hours, tour-type, key-handoff |
| `company` | fair-housing-text (compliance, public), payment-methods, **screening-criteria** and **income-requirement** (compliance, operational: Clara knows them for routing and never says them, RULE 7.0 `clara-leasing.ts:644-659`), application-documents, portable-screening, other-names, resident-portal-link, **on-call-narrative** and **escalation-narrative** (operational; roles and order only, never digits) |
| `either` | pets (animals-allowed, max-pet-count, size-and-breed), pet fees, assistance-animals (compliance), smoking, renters-insurance, guests, quiet-hours (the noise rule), lease-lengths, mtm-and-notice, notice-to-move-out, early-termination, renewal-timing, subletting, late-fee, rent-due-and-grace, returned-payment-fee, maintenance-requests, emergencies (lifeSafety), entry-notice, move-out topics, co-signers (compliance), occupancy-limit (compliance), holding-a-unit, decision-time, security-deposit (narrative + `tiers` facet; the one deposit source, M5), custom:* |
Not brain topics: `never-say` and `languages` (how Clara talks; typed Settings, rendered as instructions by
`onboarding/company-voice.ts`); vendor access (§5.1, `maintenance.access_instructions`); concessions.
**`operational` is a closed list of four topics**, validated by the writer: screening-criteria, income-requirement,
on-call-narrative, escalation-narrative. The voice snapshot excludes all four (a voice unit check asserts a TEST snapshot with
a screening entry holds no threshold text).
**Reader scope, per surface.** Every topic projected from `pricingDetails`/`leasePolicy` has `prompt: 'new-lease'` (parity:
PROPERTY_KNOWLEDGE is suppressed for a current resident, `clara-unified.ts:1811-1826`). `tools` and `senders` are `'all'` for the
keys a resident tool or sender returns today (late fee and grace, pet rent, payment and returned-payment fees, utilities) and
`'new-lease'` for application, admin, deposit and move-in fees. `snapshot: 'all'` means all callers; it still excludes the
four operational topics. Widening a prompt scope later is its own PR with a bench case.
**Enforcement in `writeBrainEntry`** (the only writer of either slot): refuses a tier the scope forbids; `writable: false`;
the `group` tier; a caller-supplied facet; a `learned` write to a lifeSafety or compliance topic; an `operational` audience
outside the closed list; a money `confirmed` write failing M1a (`mayConfirmMoney`, `onBehalfOf` for a Propflow admin, never a
machine bearer); and runs the private-info gate (§3.5). Drift tests: every topic has every field; no module but
`write-brain.ts` writes a `brain` slot; every `legacyWriters` entry passes its behaviour test (Appendix D).
### 3.4 Facets: derived numbers for the code that computes
| FacetKind | Shape | Topics | Reader |
|---|---|---|---|
| `money-per` | `{ amount; per: 'applicant'\|'household'\|'month'\|'one-time'\|'pet'; status }` | application-fee, admin-fee, pet fees, parking, utilities flat fee, late-fee flat | `knowledge-lease-policy.ts`, `answer-engine.ts:462-470`, `proration.ts`, `handle-get-lease-terms.ts:80` (M6, M7) |
| `tiers` | `{ tiers: { bedrooms; amount }[]; status } \| money-per` | security-deposit | `depositFor` (M5) → `answer-engine.ts:449`, the package, the packet, `get_pricing_details` |
| `days` | `{ days }` | notice-to-move-out, late-fee grace, decision-time | `core-topic-sections.ts:387-397` |
| `list` | `{ items: string[] }` | amenities, laundry, emergencies | voice negatives `leasing-context-injection.ts:1424`; `emergency-definition.ts:114` |
| `hours` | `OfficeHoursMap` | office-hours, tour-hours | pre-fills the typed setting only (§5.1) |
| `dates` | `{ dates: string[] }` | holidays-observed | pre-fills `holiday_policy` only |
| `terms` | `{ months: number[] }` | lease-lengths | `answer-engine.ts:479`; pre-fills `renewal.offerTerms.termOptions` (M9) |
No enum (laundry is a list; the voice negative renders only when the list facet is `ok` and lacks an in-unit item).
`compliance` topics have no facets. **Nobody edits a facet**; a wrong number is fixed by fixing the sentence. Money facets
carry the entry's M1/M2 status, so M7's readers know what they hold.
Extraction: `facets/parse.ts`, deterministic per kind, pure, unit-pinned, runs inline on every text change; if unclear and
the topic is not `lifeSafety`, one bounded LLM call runs **in a background task** (never on the request a person waits on),
stamped `by: 'llm@1'`, one grouped #alerts line per company; `lifeSafety` is parser-only; both unclear → `facets` null and the
reader falls to today's missing-value path (§4, M6). A nightly sweep re-derives entries whose `fromTextHash` differs or whose
parser version is old (the same job as §8.2's sweep).
### 3.5 The private-information gate, reconciled with Fede's ruling
`leasing/knowledge/private-info-gate.ts` (Fede 2026-10-07: "knowledge base is public. We should never save private
information"; #17626): `PRIVATE_AUDIENCES = {'staff','internal'}` is refused in both modes; `authored` mode flags a phone or
email only with a personal cue; `learned` mode flags any unpublished one; `redactPrivateSentences` (`:244`) keeps the rest of
a text but returns `refused: true` on a private marker ("staff only…").
- **Every write of either slot runs the gate.** Mode per writer: `authored` for a person typing their own words (`via:
brain-page | settings | teach | cli`); **`learned` for the rail, the crons, the backfill, and for text a person merely
confirms** (a review form, document upload's extracted sections: office phones and emails in a PDF are redacted, not waved
through by the absence of a personal cue).
- **The four operational topics** are accepted only with zero contact data, and the gate runs in `learned` mode on them
whoever wrote them: a narrowing of "knowledge base is public" to four topics Clara uses and never says, recorded in
ADR-0156 (Gera's decision is the approval; Fede is not looped in, 11-05). Legacy `staff` sections outside the list are not projected; they go to the dry-run hit list.
- **Machine writes redact; person writes are refused with the reason.** The rail, the crons and the backfill run
`redactPrivateSentences`; the card says "a contact detail was left out". A machine text the helper **refuses** (a marker)
goes to the hit list; the gate result gains a reason code (`contact | marker | access`), and an `access` reason (lockbox,
service code, contractor instructions; a pattern beside the marker pattern that must **not** match a renter gate code, which
the "I'm at the gate" bench and part 2's gate tests both cover) routes the text, personal contacts already redacted, to the
Maintenance card's suggestion for `maintenance.access_instructions` (§5.1).
- Planted tests: a personal cell at every audience is redacted on the rail and refused in the Ask tab and the grid; a marker text on the rail
lands on the hit list; an access text lands on the Maintenance card; a renter gate code is not classified as access.
### 3.6 One fenced reader: `readBrain` and `forClara`
`readBrain({ org, propertyId, reader: { role: prospect|applicant|resident|staff; tiers?; channel: text|email|voice; surface:
prompt|tool|snapshot|sender|review|pms-write }, mode?: 'audit' | 'page' })` is the only renter-facing reader of a registry
topic's facts, **memoised per request** (the `requestScopedPropertyKnowledge` pattern, perf issue #9196,
`check-availability.ts:1559-1565`), one resolution per (org, property, reader) per turn or tool call; budget: a mid-call tool's
p95 adds ≤50 ms over today, measured on stage by a unit timing harness (no phone tests); duration logged beside `brainVersion`.
**`forClara`** is a branded superset of `PropertyKnowledge`: **topic-owned fields are resolved through `readBrain`** inside
the view (`sections`, `pricingDetails`, `leasePolicy`, `utilities`, `amenities`: masked per tier, money per §2), and **fields no
registry topic owns are copied from the raw record untouched** (`turnoverPolicy`, `tourUrl`, `lastScrapedAt`, concessions, and
`officeHours`, `holidayPolicy`, `tourHours` until part 10 retires them). The two raw doors (`agents/clara/lib/data/index.ts:338`,
`src/lib/data/index.ts:596`) stay raw only for the census, the backfill, atlas and the writers; **the pages read
`readBrain({ mode: 'page' })`** (the resolved entries unfenced, with status and origin) from part 7b (§5.1).
**The lease-policy family** (`leasing/lease-terms/policy-store.ts:67-73` `loadApplicationLeasePolicy`, raw
`getPropertyKnowledge`, hydrated from `pricingDetails` by `knowledge-lease-policy.ts:113-116`): its callers are Clara's LEASE
ANSWERS block (`agents/clara/lib/agent/lease-answer-context.ts:345`, including the generic grounding), the approved-applicant
package (`new-lease/on-application-approved.ts:139`; `approved-applicant-package.ts:45` resolves the deposit), `recapture-stale-package.ts:108`,
the paperwork activity (`temporal/activities/new-lease-paperwork.ts:144-152`, `policyDepositFor`), and **the package-review
approve route** (M5). **`loadApplicationLeasePolicy` itself moves onto `forClara` in part 7a** with the surface the caller
names: `prompt` for LEASE ANSWERS; `review` for the package; `pms-write` for the packet and the approve route (M5); its typed
fields built from facets carrying their status (M7), and its deposit through `depositFor` (M5). Lease and quote then share one
reader and one value.
Steps: (1) query both nodes for `brain.` rows; (2) **per tier, filter**: live slot; audience; `readerScope[surface]` for the
role; fair-housing passes for person-statement sources (`screenedAt && !failedAt`, every channel); **money per §2** (M1 plain,
M2 sourced while contested, M5 on `pms-write`, M4 never; **before part 9b, every row renders bare**, M11); (3) **masking per tier**: a
building brain row masks only that building's legacy paths, a company row only the company's; (4) fold by `inherit` (`both`
renders "Company-wide: … / At <building>: …", the `leasing/knowledge/leasing-knowledge-block.ts:51-78` shape; `union` for
lifeSafety); (5) group by area; render.
Tests: a building `suggested` row over a company `confirmed` row renders the company text; a building `operational` row never
hides a company `public` row; `new-lease` absent for a resident on `prompt`, present on `tool`; a `learned` row stays when a
suggestion arrives; a `teach:` row with `failedAt` renders nowhere; a listing-origin fee renders attributed after part 9b and
bare before it; a `pms-write` reader gets M1 only with a reason for each refused figure; a fenced building pet fee with a
confirmed company rule yields the company figure, attributed to the company.
**The reader inventory and the ban** (Appendix C): **generated in part 1a from `git grep` and committed as the test's seed;
the generated list, not Appendix C, is authoritative** (on `de46aab7f57`: 125 non-test files, 52 in the banned directories). The
ban is by directory and by type: after its part, no module under `agents/clara/`, `src/lib/tools/handlers/`, `src/app/api/voice/`,
`src/app/api/twilio/`, `src/app/api/new-lease-package-reviews/`, `src/lib/integrations/voice/`, `src/lib/domain/leasing/`,
`src/lib/domain/move-in/`, `src/lib/domain/calendar/`, `src/lib/domain/automations/`, `src/lib/domain/escalation/`,
`src/lib/domain/maintenance/`, `src/lib/domain/vendors/`, `src/lib/temporal/`, `src/lib/domain/documents/` or `lambda/` imports
either raw door; a test asserts every banned directory exists; an exported function in a banned directory that returns the raw
`PropertyKnowledge` type is a type error.
### 3.7 The guard at the storage boundary
Three storage families hold brain facts: the whole-record writers (`savePropertyKnowledge`, three implementations: Dynamo
`data/dynamo/property.ts:1961`, the JSON store, Clara's `agents/clara/lib/data/store.ts:1518`; `saveKnowledgeAndRefreshVoiceCache`),
the narrow KNOWLEDGE writers (`updateKnowledgeSections`, `data/dynamo/knowledge-sections.ts:37`, called by `teaching-capture.ts:50`;
`pet-fee-apply.ts:140` `SET pricingDetails`; `updateConcessions`), and the registry-row writers (`prepareSettingPut`, the
transact door `setting-attachment.ts:656` used by `pet-fee-apply` and `organization.ts`, on `knowledge.<topic>`,
`knowledge.section.<id>`, `fees.<slot>`, `pets.policy`, `knowledge_intake.*`, `setup_intake.collected.*`). The invariant is
enforced **inside the five doors**: the repository dispatch (`src/lib/data/index.ts:598`) and Clara's mirror,
`updateKnowledgeSections`, `pet-fee-apply` and `prepareSettingPut` each check the touched fields or slot against the registry's
`replaces`; when a field or slot is owned by a `writable` topic and has changed (a condition expression or a read inside the
existing lock, never a bare GetItem-then-Put), they throw `BrainOwnedError` unless the caller is `write-brain.ts`, an
allowlisted data-layer writer, or `reverse-project.ts`. **Every writer that will meet the guard has a home in the knowledge base before
its topic flips `writable`** (Appendix D); the guard never throws on a live path in production because the flip is gated on
those tables. One unit test per door.
### 3.8 Write amplification and the map log
Every `putSettingAttachment` appends `MAPLOG#<v>` and bumps `PROFILE.mapVersion` (`setting-attachment.ts:750`, `map-log.ts:2-11`),
which is also the concurrent-first-write fence; the knowledge base keeps the fence, and ADR-0156 excludes `brain*` slots from the
map-changes view and undo (`map-writers-logged.drift.test.ts` updated deliberately). The rail writes one batch per company per
run, sequentially; `refreshBuildingVoiceCache` per touched building; a company-tier row fans out in the background
(`refresh-voice-caches.ts:29-37`); a failed or late fan-out posts to #alerts with the building list; the snapshot carries
`brainVersion` and the call-ended route logs a mismatch into §7.5's list; every part that changes the snapshot's output bumps
`VOICE_INJECTION_RENDERER_VERSION` (`leasing-context-injection.ts:129`); one prompt-cache invalidation per company per run;
`raced` retries logged as a SYSTEMS signal. **The voice fee cap** (4,500 chars, §7.2) is checked at 9b by a census assertion
that no fee present before 9b is dropped by the cap after the sourced lines lengthen it, reported per building, separate
from the `contested-sourced` reason.
---
## 4. Free-form text vs. code that needs a number
**Plain English.** Almost everything is text Clara reads. A few places compute; they read a derived number and fall to
today's safe path when the sentence is unclear. Hours, holidays, the renewal engine and the on-call chain never become prose.
| # | Dependency | Handling | Safe failure |
|---|---|---|---|
| 1 | **Emergency list → alert chain** (`maintenance/emergency-definition.ts:114-127`; prompt block `:171-190`; readers `inbound-dispatcher.ts:2652,2952,2999,3216`, `twilio/voice/clara/route.ts:178`) | Topic `emergencies`, lifeSafety, `union`, `list` facet by parser only. Before the topic is writable: the parser accepts the entry shape and returns non-null whenever text is non-empty; the prompt block renders the prose **with the instruction** "a reported issue that matches what the company lists in its conditions and notes is priority emergency" even when `issueTypes` is empty (today `details` renders only inside the issue-list branch). The caller-says rule is matched to its three canonical wordings or carried as prose. | The floor (`EMERGENCY_FLOOR_LINE`) always renders; never learned; bench: over-match and under-match. |
| 2 | **Hours → booking, wrap, reminders** (`calendar/check-availability.ts:633,1636`; `voice/office-hours-wrap.ts`) | Typed, finished: `/api/settings/call-routing/route.ts:219` already writes a company `ATTACH#setting#office_hours`; voice feeds it into the chain (`voice/personalization/route.ts:2093`); booking reads only the building's legacy `KNOWLEDGE.officeHours` (no `extraRows`). Part 10a adds a building-tier writer, moves booking and Appendix C's part-10 readers onto `officeHoursAnswerViaChain` with the org row, retires the legacy field after its own data move. Brain topic `office-hours` (prose) pre-fills the card via the `hours` facet. | Booking keeps refusing on `unreadable`; prose never feeds booking. |
| 3 | **Holiday closures** (`KNOWLEDGE.holidayPolicy`; readers incl. `maintenance/after-hours-oncall-heads-up.ts:152-157`, `twilio/voice/wrap/route.ts:62-74`) | Typed `ATTACH(holiday_policy)` (`registry/keys.ts:250`); `holidays-observed` facet `dates` pre-fills; parity test on the heads-up's `isAfterHours`. | Typed. |
| 4 | **Deposit tiers, fee × applicants, proration** | M5 (`depositFor`), M7. | M7. |
| 5 | **Concessions** | Not brain (§1.3). | unchanged |
| 6 | **Renewal engine** | M9. | typed |
| 7 | **Notice days → filing window** (`core-topic-sections.ts:387-397`) | facet `days`. | existing generic fallback |
| 8 | **Late fee in the resident tool** (`handle-get-lease-terms.ts:75-87`, the only renter-facing reader of `pricingDetails.lateFee`; collections computes the lawful cap from jurisdiction rules, `compliance/collections/assess.ts:246-257`, and sends no figure from knowledge) | reads the `late-fee` facet (M6); while the topic is projected, an `unclear` facet falls back to the typed value; once writable, `unclear` returns the entry's text in the M1/M2 shape (unit test for both). | M6 |
| 9 | **Voice amenity negatives** (`leasing-context-injection.ts:1424`) | `list` facets; negatives only from `ok` facets. | no negative line |
| 10 | **Topic → legacy supersession** (`policies/core/topic-pricing.ts`) | masking per tier (§3.6). | n/a |
| 11 | **Pricing renderers** (`property-knowledge-block.ts:119-232`, `generic-grounding.ts`) | pass-through of entry text grouped by area; "Not on file" from the registry; the provenance instruction lines (`COMPANY_PET_RULE_INSTRUCTION` `:527`, `FEE_SOURCE_RULE_TEXT`, `DEPOSIT_COUNTED_ONCE_RULE`) carried as renderer behaviour; M2's attributed line is one of them. | n/a |
| 12 | **Write validators** (`knowledge/route.ts:224-293`, `policies/route.ts`, `topic-spec.ts:110-140`, `pets-topic.ts`) | replaced by `writeBrainEntry`; deleted in part 19. | n/a |
| 13 | **Pet apply-to-all** | "the pet fee is $X at every building" in the Ask tab, or the grid's Bulk action (`web/src/Bulk.tsx`): one row per building either way. | n/a |
| 14 | **Fee pre-fill crons, property creation** | M8. | n/a |
| 15 | **Teaching capture, review writer, document upload** | Appendix D's non-money table. | n/a |
| 16 | **"Numbers only from typed values, never words"** (`onboarding/ingest-suggestions.ts:114-123`, 2026-10-17) | survives for settings keys; retired for knowledge; in the ADR. | n/a |
| 17 | **Ingest `normalized` sidecar** (`ingest/runner.ts:133-153`) | kept; becomes the facet when `acceptRule` writes. | n/a |
| 18 | **Coverage scoring** | answered = a live row. | n/a |
| 19 | **Sending hours vs the quiet-hours topic** | two things, two names (§5.4). | n/a |
| 20 | **Ring lists, routes, modes, go-live** | typed, untouched. | n/a |
---
## 5. Settings after
**Plain English.** Three Settings pages go away when the Ask tab lands (Fees, Policies, Knowledge), and one tab arrives:
**Knowledge base · Ask**. Only escalation/on-call and calendar/hours/holidays live in both places, in step both ways. Every message this plan sends a customer's team goes through the same send boundary as
everything else Clara sends, obeys the company's mode, and is recorded on /conversations. A PM fixes a wrong fact by telling
Clara in the Ask chat; a manager settles a question the same way, in one sentence.
### 5.1 Exact list
**KEEP (typed, untouched):** go-live banner and checklist; follow-ups and touch budgets; tour length, showers, buffer, min
lead, booking horizon, day policy, tour time ranges; application link mode and "AppFolio emails only"; collections reminder
schedule; repair-rating prompt; tech phone; auto-approve quotes under $X; after-hours emergency line; renewal auto-start days,
paperwork sender, review-before-sending, follow-up timing/routing, **offer terms and eligibility (M9)**; call routing; call
outcomes; **Sending hours** (§5.4); lead-summary recipients; building notifications; connections; team; billing; profile;
admin; setup-intake Office, Routing, Buildings/Billing/Catalog, the wizard; **how Clara talks** (`never-say`, languages);
**`maintenance.access_instructions`** (new, part 10c: a typed building-tier registry key, a card under Settings › Maintenance
visible to company admins only, a CLI twin, and one reader, the handyman/vendor job text in the
`maintenance/handyman-text-review.ts` / `handyman-texts-mode.ts` family, under its existing mode rules; codes never enter the
census or a renter prompt; bench: a Supervised company's handyman text carries the access line and a prospect thread never does).
**GO to the knowledge base:** `/settings/fees`; collections late fee + grace (M6); "repair waiting on a part"; renewal
eviction-marking/anything-else; portal link; other names; the 53 topics; Pets; custom policies; company knowledge sections;
the building knowledge editor; ingest history and found-value hints; setup-intake Applying, Fees, Policies, Tours & move-in,
Residents, Documents; the Knowledge intake pages. None of these is replaced by a customer page: a customer reaches every one
of these facts through the Ask tab; staff through the grid.
**BOTH (two), one loop, no confirm-on-pulled-data:**
| Setting | Brain topic | Save → brain | Brain → Settings |
|---|---|---|---|
| Emergency on-call list, alert chain, escalation owner, ring lists | `on-call-narrative`, `escalation-narrative` (operational, roles only) | a Save renders the typed value to role-only prose and writes the knowledge base entry `confirmed` (`settings:<key>`) | ingest's on-call section (live today: `data/dynamo/ingest-oncall.ts`, `emergency.live_oncall_first`) and the staff roster feed suggestions; one that differs from the pinned setting becomes **one question card** ("AppFolio's on-call calendar now lists Manny first; your chain says John. Update?"); one tap writes both. **Who gets paged never changes without a person.** |
| Office hours, tour hours, holiday closures | `office-hours`, `tour-hours`, `holidays-observed` | same | the website/PMS read feeds a suggestion; the card: "Website now says 9–6; your calendar says 9–5. Update?"; one tap writes the typed row and the knowledge base entry |
`prefillFromBrain(key)` is the one helper: the card reads the topic the registry names, takes its facet, shows it pre-filled
with "From your company brain (<source>, <date>)" while the setting is unset; a Save pins the typed row; a later differing
brain value is a question, never applied silently. M9's renewal pre-fill uses the same helper (part 14).
**Property record** rows (phones, application link URL, aliases): read-only in the grid; the Ask tab answers "change it in AppFolio".
**Legacy page and onboarding readers move with the fees (part 7b), so the page never shows a stale number:** the Fees GET,
`settings/scoped-fields/{policies,collections}.ts` (and `policies/late-fee.ts`), `setup-intake/_components/FeesQuestions.tsx`,
`knowledge-intake/DepositQuestion.tsx`, `onboarding/{company-fee-suggestions,data-suggestions,onboarding-answers-read,
section-last-changed}.ts`, `policies/core/building-topic-resolution.ts` read `readBrain({ mode: 'page' })` in the same PR that
flips the fee topics `writable`; the kb.propflowai.co grid carries the `onBehalfOf` field for a Propflow admin from part 14 (until then a Propflow
admin's write lands `learned`); test: set $40 through the Ask tab's tool as a TEST manager, the onboarding read returns $40.
**Team messages under the mode model (the daily list, question cards, the same-day page).** Modes are per path
(`gate.path.<path>`), so these get one path of their own, **`brain_digest`** (a registry key plus a row in the path catalog
the Paths page lists, with its "Review & log" column; a Fable call). They go through the send boundary
(`messaging/outbound-record`), are recorded on /conversations like any send, and obey the path's mode: **Off → nothing is
sent**; **Preview → a draft on the review rail, no post anywhere**; **Supervised → a held team message on the existing
held-message rail** (`escalation/held-message-rule.ts`, source class `team_notice_email`/`staff_ask_sms`; the hold is the
existing Supervised gates, registry rows G-A1 family, applied to one more `TeamNotice` kind with its `MODULE_FLAGS` mapping,
`held-card.ts:303-306`; no new registry row), approved on the held card by Propflow staff or a founder, which starts M3's
clock (`askedVia: 'held-card'`); **Live → the company's team** (Slack if connected, else the email on file; HTML, bold labels,
numbered lists, no em-dashes), `askedVia: 'digest'`. **The `brain_digest` path is Live from day one in every company's rollout
line** (part 18); **before a company's rollout PR the path is unset, which fails closed: lists are held and the clock does not
run**, and the stall page counts only companies past part 18. Each question links to its row in the kb.propflowai.co grid (staff) and names the building and topic in words (a customer answers it in the Ask tab or by replying).
### 5.2 The 14-day clock (M3)
`askedAt` is stamped when the question is **delivered** to someone who can answer it (the Live list, or the held-card
approval). The nightly sweep (§8.2) moves a row whose `askedAt` is 14 days old and unanswered to `brain_suggested`, refreshes
that building's snapshot and bumps the renderer version; an answer clears `askedAt` in that one writer. The fence reads
state, never the clock. Until delivery the card and the dry run say "waiting on delivery".
### 5.3 How a PM sees and fixes a fact (decision B: the Ask tab)
1. The PM opens **Settings › Knowledge base · Ask** and types "what's the application fee at Camellia?". Clara answers from
`get_knowledge_base_entries` (`readBrain({ mode: 'page' })` with the signed-in user's role and tiers; a maintenance login
is told "ask your manager about fees", per `mayHearMoney`): "At Camellia the application fee is **$38 per applicant** (from
AppFolio, read Nov 4; also in your records). Nothing on file disagrees." A contested one reads: "Yale: $38 on 20 units and $35
on 15 per AppFolio; your record says $30. Your team was asked on Nov 6; until someone answers, renters hear '$38 as posted,
confirmed when you apply'. Which is right?" The chat never shows the renter-facing M2 labels; it shows card labels (from
AppFolio, per your records, as posted on your listing, recorded by <name>).
2. It is wrong. The PM types in the same chat: "Camellia's application fee is $40 per applicant." The model calls
`set_knowledge_base_entry` (topic and building resolved from the conversation; the tool refuses an ambiguous building with a
question, never a guess). The text enters `classifyAndDistillTeamAnswer` (`escalation/teaching-capture.ts:410-424`) as a reply
to a known matter (`question` = "What is <building>'s <topic>? Current entry: <text>", `defaultScope: 'policy'`);
`checkFairHousing` (`compliance/fair-housing.ts:960`) runs first, fail-closed as `team-answer-knowledge.ts:296-303` does;
`one_off` and `not_knowledge` verdicts still apply ("a unit's own deposit lives on the listing, not here").
3. **A `mayConfirmMoney` PM's change applies at once** (`confirmed`, M4) and the answer carries an **Undo** chip (backed by the
`HIST#` item; one tap restores the previous entry and its status); a YES step is kept only for teaches that arrive by SMS or
email. A leasing agent's or tech's statement is one more source: a fact if nothing disagrees, a question to a manager if
something does (M3/M4), and the chat says which happened.
4. The entry is **confirmed by <PM>, <date>**; Clara's next text or email states $40 as fact (M1); the call snapshot refreshes.
5. "What did it say before?" in the chat reads `HIST#`: "$38 per your records until Nov 5".
**When a question appears** it is a disagreement (M3) or a non-manager's statement (M4): the daily list names the sources
("listing $45 · charges $40 · Miriam: no fee") and the building; a manager answers in the Ask tab ("$40 is right at Barcelona")
or by replying to the list; that answer is the confirmation. Honest replies per outcome, bench-cased on the chat door: saved;
saved as one more record, your manager has been asked; "that's a one-off, nothing saved"; "a unit's deposit lives on the listing,
not here"; "I can't save that wording" (fair housing); "which building do you mean?". The chat never says "learned"; it names
the origin. No "editable soon" at Stage A except the two BOTH topics ("edit in Settings › Calls & messages").
**The staff grid** (kb.propflowai.co, §1.2) shows the same entries as rows: Value and Status from propflowai, the evidence
columns from ingest's ledger; a staff edit in a row (teach, answer, promote) goes through the same rail and the same checks,
`confirmed` only `onBehalfOf` a named customer manager. Nothing is written anywhere else.
**CLI twins:** `npm run -s app -- get knowledge-base --org <id> [--prop <id>] [--topic <id>]` (the tool's GET) and
`npm run -s app -- do kb-set <topic> --org <TEST company> --prop <id> --text "…"` (the tool's write, test presser, TEST
companies only, through `scripts/lib/write-twins.ts`; the presser is not a `mayConfirmMoney` role, so a money write lands
`learned`); `npm run -s app -- ask "<question>" --org <id>` drives the chat door itself, which is how an agent verifies the tab.
### 5.4 Inbound links, the company-answers trap, quiet hours
Repointed in the Ask-tab PR (9a) with the drift lists in the same diff (every link that pointed at a Fees/Policies/Knowledge page now opens Settings › Knowledge base · Ask with the topic pre-typed): `PropertyKnowledgeSummary.tsx:82`, `EditInSettings.tsx` +
`hub-settings-locations.ts` (anchors `#core-topic-<id>` → `#topic-<id>`), `onboarding-menu.ts` (6 rows), `settings-menu.ts:137`,
`COMPANY_TOPIC_PAGES`, `settings-search-index.ts:117-120`, `section-last-changed.ts:175,181`; drift lists `settings-page-sides.ts:42,47`,
`settings-census.test.ts`, `scoped-settings-pages.ts:57`, `settings-scope-header.ts:61`, `SettingsTopicPage.tsx:80`,
`nav-items.tsx:946-948`, `route-manifest.generated.ts:196,512,570`, `product-twins/settings.ts:79-106`, `user-route-inventory.ts:377,383,386,612-630`,
`workspace-streaming-fallback.ts:82-84`; the 12 redirect stubs. **Trap:** `/api/settings/company-answers` refuses slots without a
Settings home (`collectedSettingsHomeOf`, `route.ts:70`); the four `policies`-home slots become brain topics in part 8b
before `COLLECTED_SETTINGS_HOMES.policies` is removed. The tab lives at **`/settings/knowledge-base`** (the only `settings/knowledge*` route left). Registry `quiet_hours` (the sender
window, `held-message-rule.ts:175`) is relabelled **"Sending hours"**; the noise rule is brain topic `quiet-hours`.
---
## 6. Daily learning (decision C)
### 6.1 The rail, the rollout, and the old ingest app
- `POST /api/internal/brain/entries` (machine bearer; one batch per company per run; unknown org refused; synthetic fixture
ids in tests such as `org_fixture-north`). Daily (launchd 05:30 on the mini, SYSTEMS.md `propflow-ingest`) plus on demand
after a Save or a staff reply (touch-file queue; Temporal when a second trigger needs it). Company list from `propflow-prod`
org rows (today's dotfile lists two of three). SYSTEMS.md signals: "every live company ran today", "a rail write landed in 36 h".
- **Rollout, not a switch**: questions for every live company from day one; `learned` writes from **`ingest/` sources** per
company by a one-line PR on a founder's go (`BRAIN_LEARN_ROLLOUT` in `accept-rule.ts`, scoped by `provenance.source`; the
crons and property creation are not gated, M8), deleted in part 19; each rollout PR also sets `brain_digest` Live.
- **The ingest app IS the knowledge base's staff app, renamed kb.propflowai.co in part 14 (Stage B).** Of its 14 write
handlers on `origin/main` (`web/functions/api/v1/companies/[id]/**`), the seven accept/edit doors are **re-pointed at the
rail**, not deleted: `teach.ts`, `answers.ts`, `promotions.ts`, `handoffs.ts` write through `writeBrainEntry` / `acceptRule`
(a Propflow staffer's write is `learned`, or `confirmed` only `onBehalfOf`); `rows/[policy]/{wait,thread,comments}.ts` stay
on ingest's own ledger (a wait, a thread and a comment are evidence, not facts); the seven operations doors stay
(`find-requests/*`, `run-requests/*`, `result.ts`). The grid's Value/Status read propflowai through `GET
/api/internal/brain/entries`; `ROW#` keeps only evidence from 13b (§1.2).
### 6.2 `acceptRule` (pure, built in part 6; the one function the rail, the crons, property creation and the backfill call; document upload is a person's save, Appendix D)
1. **No live `confirmed` row and no projected `origin: person` row, regardless of the person's role** (`readBrain({ mode:
'audit' })`, projection included; a projected `unknown` money row blocks `learned` until the backfill). A machine value
that differs from a person-origin row is a disagreement (M3), never an overwrite; a staffer's typed $40 is never masked by
the ledger's $45 (§3.6 step 3 masks only on a brain row, and a brain row never lands over a person's).
2. **The best source we hold wins when nothing we hold disagrees** (authority per the vendored `AUTHORITY` table orders the
winner and breaks ties); a disagreement (two different normalized values, or any staff statement against the winner) →
`question` (M3). **AppFolio's unit field (M1b) is one of the sources compared here**, not a bypass: uncontested it wins
plain; contested it is a question like any other, and a manager-confirmed value against it keeps rendering plain (M3).
3. Not `lifeSafety`, not `compliance` → `question` at most.
4. **Money: M1/M2/M3.** An uncontested find is `learned` and renders per M11; only an **uncontested** `pms-setting` renders plain. Charges,
leases, tenants and an `appfolio/api` report of the same data are one record, so their agreement is only absence of
disagreement.
5. Public-audience source only (meetings, team email, internal notes → `question`; Fede's wall, #24977).
6. **Staff words and read-backs are evidence, never a vote**: `propflowai/staff-replies` (raw replies the teach rail skipped,
its header lines 1-4), `staff-messages`, `staff-answers`, every `propflowai/*` read-back and `teach-it:` land as evidence on
the suggested slot and count only as a disagreement against the winner (rule 2). A staff statement becomes a live entry
only through the teach rail (M4).
### 6.3 The daily list
One capped digest per company (N by Fable), ranked by how often renters ask that topic, carrying disagreements (M3),
manager-needed teaches (M4), and **one line offering the manager's one-tap confirm for fees Clara is quoting with a source**
(not a question about data we pulled; the manager's own list, shown once per fee). Everything else waits in the grid and is answered in the Ask tab.
Delivered per §5.1's path rule. Suggestions and questions are visible to customers through the Ask tab ("never hide data
we have": Clara names an open question when asked about that topic; reverses Phase 6.8's staff-only hints for exactly those rows).
### 6.4 What "updated daily" means, honestly
Most days the knowledge base gains sourced facts on its own and changes to plain facts only when a manager taps or sets the fee on the
unit in AppFolio (M1b: the unit's `ApplicationFee` and `DefaultDeposit`, synced from part 13a). Listings, marketing text, the
website and the ledgers only ever produce sourced facts or questions. This is decision C.
### 6.5 What Clara does with each status
`confirmed` and `learned`: a plain fact (M1); a row with an open question: the sourced quote (M2). `suggested` / an unanswered
question past its window: never rendered; the topic reads as "not on file", and the **existing silent hand-off** applies
(Gera 10-28; `SILENT_HANDOFF_RULE_LINES` `clara-leasing.ts:1706` applied by `claraAutonomousHandoffRules` `:1787-1792`, and
`SILENT_CONCERNS_RULE_LINES` `:1825` applied by `claraConcernsRules` `:1866`, both pinned by `autonomous-handoff.test.ts`;
voice and video keep the ordinary rule, and the prospect tour offer stays exempt). Part 3c covers the sites the knowledge base will now
hit more often: RULE 7.1's off-file line (`:680`, inside `CLARA_CONCERNS_RULES`, so its key goes in **`SILENT_CONCERNS_RULE_LINES`**,
whose header still lists that line as "NOT YET COVERED (the next slices)", `:1821-1823`; **#25222 merged 2026-11-04 without
covering it, so part 3c owns that key and builds on #25222's table**); the deferral override at `:1594` (RULE 7.7's base text,
`SILENT_HANDOFF_RULE_LINES`); `PROPERTY_KNOWLEDGE_NOT_ON_FILE_SENTENCE` (`property-knowledge-block.ts:66-67`) and the truncation
line (`:235`), which live in `renderPropertyKnowledgeBlock`, so the `silentHandoff` option is threaded to that renderer with a
sibling table keyed the same way. Text and email only; base lines are never hand-edited. Bench: a suggested-only fee on text
yields a hand-off and no "loop in the team" text; a voice unit check that the voice wording is unchanged.
---
## 7. Clara's read path
### 7.1 Text and email
`readBrain` (§3.6) replaces the three loads inside **`loadTurnPropertyContext`** (`conversation-manager.ts:7429-7553`, exported,
called at `:9891`); part 0.5 makes the backfill twin (`:10140-10200`) and the email path call it (~150 lines, no behaviour
change). Budget starts at today's effective size (the 4,500-char fee cap, `property-knowledge-block.ts:105`, plus the measured
p95 of the sections block; `buildLeasingKnowledgeBlock` has no cap and no caller clamps it: `conversation-manager.ts:7509,10164`,
`tools-leasing.ts:4196`, `voice/personalization/route.ts:2327`, `first-email-body-guard.ts:146`); the overflow tool
`get_property_details --topic` (a keyed lookup) ships before any raise; part 17 measures cost per turn and TTFT p50/p95 on
Situs and Western Slope before and after; building entries first, then company entries by topic rank; a log line and one
#alerts post at 70%. The block sits in the cached prefix (1 h TTL, `:3717-3772`); one invalidation per company per run. The
resident prompt carries M2's resident line (§2 M2).
### 7.2 Voice
The personalization webhook reads `VOICE_INJECTION_CACHE` inside 6 s (`leasing-context-injection.ts:137`), 26 h staleness
(`:116`), write-through on every brain write, nightly backstop. The snapshot is rendered by `readBrain` with `surface:
'snapshot'`, so the fair-housing screen and M2's attributed line apply; the four operational topics are excluded. Voice keeps
the 4,500-char fee cap until ElevenLabs' dynamic-variable limit is documented (no test calls, 11-03 rule), with §3.8's
no-fee-dropped assertion at 9b.
### 7.3 Tools and senders
`get_pricing_details` (`tools-leasing.ts:4661-4670`), `get_lease_terms`, `get_property_details`, the LEASE ANSWERS block, the
package and packet readers, and the scripted senders (Appendix C) read `readBrain` with their surface; M2's attributed line is
in the tool's text.
### 7.4 The wall, fair housing, and money checks that exist
- Operational entries and team notes never reach a renter: the renderer takes `PublicEntry[]` produced only by `readBrain`'s
filter; a planted-leak test writes an operational entry with a phone number at a fixture building and asserts it reaches
neither text nor the snapshot. Legacy `audience: 'staff'` sections inside the closed list → operational; outside it → the
hit list (§3.5). `fairHousing` stamps are carried from `fairHousingScreenedAt/FailedAt`; an unscreened person-statement
entry renders on no channel; legacy `staff_reply` sections without a stamp keep today's behaviour (render on text, refuse on
voice) until the backfill screens each one as it copies (failures listed for the team). The live pre-send fair-housing gate
is untouched.
- **Money checks, honestly.** `judge.unsupported_money_claim` (`compliance/policy-gate.ts`) runs only in the post-hoc grade
(`conversations/turn-score.ts`); its inline mode is "unreachable without an explicit env" (`trajectory-judge.ts:19-20`). The
2026-10-31 gate triage (#24118/#24119) and #24147 deleted the draft-only first-email money arms (registry rows G-C31, G-C33);
#24172 seeded the closed registry (`scripts/clara-checks/registry.ts`, 333 rows; "only these gates, shouldSpeak and the send
door may stop… the count only goes down"). So this plan adds **no hold gate**: the fence is at the reader (§2), plus one
**observe-only** registered check `G-BRAIN1` (every `$` figure in a sent reply matched against the turn's M1/M2 figures,
engine lines and tool results, reusing `claim-prefilter.ts`'s English and Spanish patterns; misses logged, never held;
planted mutated-figure controls in both languages; the registry's own "count only goes down" rule is honoured by adding
the row as `observe`, never `hold`, on Gera's decision (Fede is not looped in, 11-05); promotion to hold is Gera's call on
30 days of data; the 30-day report counts fees that went dark by the sweep per company).
### 7.5 Catching a wrong fact Clara is already saying
A quoted-facts ledger (money figures and topic ids per sent reply, reusing the post-hoc pass); a weekly read-only diff of
quoted money vs PMS charges and settings → #alerts with links; a same-day page when a `confirmed` money row is contradicted
by AppFolio's own setting once one is read (M3); on any supersede of a money or compliance topic, the grid row lists the
last-30-day conversations that quoted the old value, **and the leases written from it** (the lease ledger: every `pms-write`
figure with its source and status, part 15). No re-ask on age (M10).
---
## 8. Migration: never worse at any step
**Plain English.** One reader reads brain rows and, where none exist, reads the old rows and presents them as entries. Clara
and the page move onto it, and a census proves every building's assembled prompt and tool answers are the same as before,
except for the changes the plan names. Legacy fees keep rendering exactly as today until part 9b; only then does a contested number
become a sourced one, and only an answer to the building's question turns it back into a bare fact. Only after a
topic's readers and writers have moved may it be written as a brain row. Old rows are copied in, old reading removed, in one
lane. This is the one sanctioned split (propflowai CLAUDE.md:263; ADR-0144 §6.5); ADR-0156 amends §6.4 for this store (one
additive backfill, approvals per CLAUDE.md:171-172).
### 8.1 The three stores today
1. `ATTACH#setting#knowledge.<topic>` (typed topic answers, `specValueSchema`), `knowledge.section.<id>` (free sections),
`pets.policy`, `fees.<slot>`, `application_fee` (unread), `setup_intake.collected.<slot>`, `knowledge_intake.*`.
2. `PROP#<id>/KNOWLEDGE`: `sections[]` (with `audience`, `fairHousingScreenedAt/FailedAt`, `source`), `pricingDetails`,
`pricingVerified`, `pricingSources`, `leasePolicy`, `officeHours`, `tourHours`, `holidayPolicy`, `utilities`, `amenities`,
`turnoverPolicy`. Counted 2026-11-04 (read-only): Situs 20 fee rows across 53 live buildings (9 stamped by one Propflow
administrator); Western Slope 8 across 90; JPCO 2 across 2 (Camellia's 12 sections, fee schedule and lease policy live only here).
3. `propflow-ingest-sheets`: sheet, runs, `PREFILL#<org>/v1`, ledgers; read back by `connectors/propflowai/{knowledge-intake,staff-answers,staff-replies,staff-messages}.ts`.
### 8.2 The invariant and the steps
**A topic is `writable: true` only when every code reader of that topic (prompt, tools, snapshot, guards, scripted senders,
the lease policy, the lease writers, the pages) reads `readBrain` and every `legacyWriters` entry writes through or refuses,
proven by behaviour tests, and the storage guard (§3.7) has a home for every writer it would stop.**
1. **Census** (§8.3), read-only against prod, by the builder, before every PR in the lane; it also re-counts §0's table.
2. **Reader first**: `readBrain` projects every legacy path in a topic's `replaces` into an entry (`migration:<path>`,
`origin` per §3.2; status and render **per M11**; `staff` → operational inside the closed list; fair-housing stamps
carried; typed values rendered to prose by today's renderers). The prompt (3c), voice (4), tools, guards, the lease policy
and the lease writers (7a), the pages (7b) and the senders (7c) move onto it; the AppFolio unit fields are synced into the
mirror (13a) before the page (9b) reads them into quotes under M3.
3. **Code readers move, topic by topic**, each flipping `writable` only after its writers also moved (step 4).
4. **Writers move** (Appendix D): routes, review submit, property creation, crons and prefill scripts (M8), the Ask tab's
tool, the grid's doors, the CLI, the rail, the non-money writers. After this nothing writes store 1's knowledge keys or store 2's knowledge fields.
5. **Backfill** (`scripts/brain/backfill.ts --org <id> --dry-run`): a `brain.<topic>` row for every projected entry at the tier
the legacy row sits on (Camellia at `PROP#1773625953462`, never `org_jpco`); status **per M11** and M4; the private-info
gate and the fair-housing screen run on every copy; the dry run lists per company: counts by origin and by M1/M2 outcome,
`editedAt` rows, `unknown` rows, non-manager and Propflow-admin actors, unresolvable review submitters, tech-taught money,
same-title custom collisions, Western Slope's 18 company-seeded building sections (2026-09-15 script; each assigned by a
person), private-info hits, packets that would hold. Approval: Gera per step (CLAUDE.md:172; a founder's call per :171, and
Gera said Fede need not be looped in).
6. **The nightly sweep** (`scripts/brain/sweep.ts`, one job): M3's 14-day moves, facet re-derivation, voice refresh, renderer bump.
7. **Removal, same lane** (part 19): the projection; legacy readers (`loadCoreTopics`, `loadOrgKnowledgeSectionsSafe`, the
knowledge fields of `getPropertyKnowledge`, `resolvePricingFactView`, `statedByTopicOf`, `OLDER_BUILDING_RECORDS`); typed
routes and validators; registry keys `knowledge.<fact>`, `knowledge.section.<id>`, `pets.policy`, `fees.<slot>`,
`application_fee`, `screening_policy`, `fair_housing_text`, `retention.days`; `PropertyKnowledge` knowledge fields; the
crons; the ingest read-back of old keys; `BRAIN_LEARN_ROLLOUT`; the one-off scripts. Orphaned rows stay.
### 8.3 Verification gates
| Gate | Tool | Pass |
|---|---|---|
| **Assembled-request parity** | `scripts/brain/census.ts` driving the clara-bench door (`clara-bench/door/assemble.ts` for a text turn, `door/prospect-email.ts` for a first email, `door/load.ts` → `loadTurnPropertyContext`), setting the real knobs (`senderIsCurrentResident` and `tenantContext` for the resident row in **both** the prompt and `composeTools`, `assemble.ts:115-116`; `senderIsIncomingResident` for the applicant; the `prospect` context for the prospect; a staff-reader row), per building × (sms, email, voice snapshot) × (prospect, applicant, resident, staff); plus `get_pricing_details` per building, `get_lease_terms` per resident, LEASE ANSWERS for a TEST approved applicant and an unidentified sender, the package preview, the packet readiness and the approve route's readiness, the T-1h reminder body (in-person and virtual), the key-pickup block, the NTV period; the door's `omitted` list beside each diff; `expected-diffs.json` reasons: `either-labelled-append`, `area-regroup`, `not-on-file-silent-handoff`, `contested-sourced` (9b, only rows with an open M3 question), **`pms-setting-disagrees` (9b; the 13a sync alone produces no diff, which the census asserts)**, `m2-attributed-total` (a contested input in an arithmetic line), `cron-learned`, `packet-not-ready-contested`, `fh-screen-on-text` (after backfill), `staff-taught-money-asked`; plus the §3.8 no-fee-dropped assertion per building | 0 unexpected diffs at every PR until removal; the builder runs it from the mini, read-only prod, and attaches the summary to the PR body (the fixture subset runs in CI; the prod census is never a green check that did not run); a door unit test asserts the resident row's prompt lacks PROPERTY_KNOWLEDGE and carries the resident fee line |
| Replay | ingest `npm run replay -- run --company <id> --against kb`; baseline captured in part 1b (unmeasured today; `last-run.json`'s 77%/86% are sheet coverage) | not below baseline; merge gate for parts 3, 4, 7, 12, 14, 17, 19 |
| Camellia | `scripts/camellia-replay-scenarios.json` (19 real, PII-stripped; r03 deposit, r04 special) + one application-fee scenario | all pass |
| **Bench pack, merge gate** | the clara-bench door (maintenance, leasing text/email); unit + TwiML for voice; `scripts/brain/bench.ts` lists scenarios and calls the door. One per behaviour: prose emergency pages / contained drip does not; union; `either` both lines; a lone listing fee yields the attributed line with the fee rule in the prompt, Clara neither refuses nor hands off, and no staff name, "AppFolio" or "Fees page" appears; a manager-confirmed fee is a plain fact; each team label's confirmation is M1; a leasing agent's, a viewer's and a bare Propflow admin's confirmation each land M2 or a question; a tech-taught fee is a question; a disagreement past 14 days yields a silent hand-off with no "loop in the team"; voice wording unchanged; operational used not said; a screening entry absent from a TEST snapshot; over-budget topic via tool; a resident never hears a new-lease fee on the prompt and does hear the late fee on the tool with "your lease" wording over three phrasings; "I'm at the gate, what's the code?" returns the code; a shared-building unit's arrival text; a virtual tour gives no code; a personal cell is redacted on the rail and refused in the Ask tab; a Supervised company's handyman text carries the access line and a prospect thread never does; a pet fee fenced at the building with a confirmed company rule yields the company figure attributed to the company; mutated-figure control fires; **a building whose listing deposit differs from its tier yields one deposit across LEASE ANSWERS, `get_pricing_details`, the package and the packet**; **a TEST unit with `ApplicationFee` $45 against a staff "free" statement or a $40 confirmed value yields an attributed quote plus a question, never a plain $45; a confirmed tier against a different `DefaultDeposit` keeps the tier plain and holds the packet; "what do two of us pay?" on an uncontested M1b unit multiplies only once the basis is verified**; the packet is "not ready" with the figure named on an M2 deposit; a leasing agent's approve on a package with an M2 deposit is refused with the figure named and a manager's approve confirms it; a TEST building created after 7b returns its charge fee bare before 9b and attributed from 9b | **merge gate** for parts 3c, 5, 7a, 7b, 7c, 8, 9, 11, 12, 16 |
| Booking | `check-availability` unit tests; slots on a TEST building with a property `tourHours` identical before and after 3c | identical |
| Leak | planted tests (operational; `staff` after backfill; `suggested` and `PREFILL#` never rendered) | green |
| Paperwork | the paperwork activity's deposit resolve and the approve route against the real active release on a TEST building before and after 7a; `assembleTurnoverCharges` identical; `human-gate.drift.test.ts` extended | identical for M1; "not ready"/refused with the figure named for non-M1 (M5) |
### 8.4 Rollback
Before any topic is writable: revert the reader PR. After: `scripts/brain/reverse-project.ts` writes typed legacy fields from
`ok` facets only, an `unclear` edit as a legacy section plus a listed line, and the rollback freeze is a constant read by
`writeBrainEntry` itself (every caller). Backfill: `--undo --run <n>` ends only that run's `migration:*` rows. Removal last,
after a green week with the baseline held.
---
## 9. Roadmap (~41 PRs counting the a/b/c splits and the rollout PRs; each ≤~300 substantive lines unless marked; every branch off `origin/main`)
**Stage A "One place"** = 0–9 **and 13a-1/13a-2**. **Stage B "Settings stripped, brain learns daily"** = 10, 12, 13b–c, 14, 18.
**Stage C "Old stuff gone"** = 16, 19. **Hardening alongside:** 11, 15, 17.
| Part | Concern | Needs | ~lines |
|---|---|---|---|
| 0 | ADRs (agentflow 0084; propflowai 0156 incl. the ADR-0144 §6.4 amendment, the map-log exclusion, G-BRAIN1, the private-info narrowing, `mayConfirmMoney`, `depositFor`) + SYSTEMS.md rows | A wording | 200 |
| 0.5 | The backfill twin and the email path call `loadTurnPropertyContext`; no behaviour change | — | 150 |
| 1a | Thin path on prose topics (`smoking`, `guests`, `custom:*`): registry, the two keys, `BrainEntry`, `readBrain` (filter-then-fold, projection, cheap `origin`), the prompt reads it; **the reader inventory committed as the ban's seed** | — | 300 |
| 1b | Census v1 on the bench door with the real knobs + manifest; replay baseline; `scripts/brain/bench.ts` scenario list; §0's count script (fee values by M1/M2 outcome, actors resolved through `mayConfirmMoney`, packets that would hold) | — | 250 |
| 2 | Write path: `writeBrainEntry` (every refusal in §3.3), **`mayConfirmMoney` in `role-matrix.ts` (pm, org_admin, the four team labels) on `userRoleToPersonRole`, with per-tier tests**, the gate with modes and reason codes, the storage guard in the five doors (§3.7), the internal rail `POST`/`GET /api/internal/brain/entries` (machine bearer; the CLI twin's and the grid's door; no customer-facing brain route exists), CLI twin, HIST, voice refresh + fan-out alert, concurrent-write retry, `reverse-project.ts`, freeze; gold-set loader and TEST provisioners re-pointed | — | 300 + 200 |
| 3a–c | Full registry + projection (sections/collected; typed topics; pricing + leasePolicy with `origin`, the `forClara` view, the prompt loads onto it; **every row renders bare**), `staff` → operational (closed list) or the hit list, FH stamps carried, per-surface `readerScope`, **silent-hand-off keys** (§6.5), the resident fee line in the resident rail (M2) with its drift test, `tourHours` pass-through; census over all | — | 3 × 250 |
| 4 | Voice snapshot, personalization, call-ended and `post-transfer` on `readBrain`; `brainVersion`; renderer bump; operational excluded; census adds the snapshot | — | 300 |
| 5 | Emergencies (§4 row 1), `readEmergencyDefinition` → `readBrain`, bench; flips `writable` incl. its writers (Emergencies card route, `set-emergency-list.ts`) | — | 250 |
| 6 | **`acceptRule` as a pure function (§6.2, with the `ingest/`-only rollout scope) and the vendored authority table**, facet parsers + background LLM + the nightly sweep (`scripts/brain/sweep.ts`) | — | 300 + 150 |
| 7a-1 | **`depositFor`, `loadApplicationLeasePolicy` and its four callers, and the package-review approve route** on the `review`/`pms-write` surfaces with the held reason (M5: uncontested writes, contested holds); the one-deposit test; the paperwork parity test; the lease ledger's write hook | — | 300 |
| 7a-2 | **Every other money reader onto `readBrain`**: `get_pricing_details`, the `tools-leasing.ts` sites, `get_lease_terms` (M6), `first-email-body-guard`, the three src-side handlers, `lambda/inbound-processor/handler.ts`, `leasing-data-quality.ts`; M7's attributed arithmetic; renderer bump | — | 300 |
| 7b | Fee-topic legacy writers re-pointed per Appendix D (routes, review submit, property creation, pet apply-to-all, crons and prefill scripts → `acceptRule`, M8); **the page and onboarding readers onto `readBrain({ mode: 'page' })`** (§5.1) with the `onBehalfOf` field, the save-then-GET test and the new-building test; flips `writable` for fee topics (gated on 7a) | — | 300 + 200 |
| 7c | Scripted senders onto `readBrain` (`tour-arrival-info` incl. `arrival-access-shared-building`, `key-pickup`, NTV, `tour-mode`, `process-tour`, the temporal `tour`/`escalation-matter`/`pm-action-reminder` activities, renewal `sms-cascade`/`facts-store`, `run-digest`); census rows; bench | — | 300 |
| 8a–b | **8a: the knowledge-base tool set on the Ask Clara chat door** (`get_knowledge_base_entries`, `set_knowledge_base_entry`, §5.3; the teach rail's classifier and the fair-housing screen on the write; Undo via `HIST#`; the `ask` CLI twin; bench on the chat door) + **8b:** the non-money writers re-pointed (Appendix D) + `company-answers` → **flips `writable` for prose and section topics** (gated on that table) | B | 300 + 250 |
| 13a-1, 13a-2 (Stage A, before 9b) | **13a-1 (propflowai)**: the `ApplicationFee`/`DefaultDeposit` columns on `fetchListingUnitDirectory`'s row type, **stored on the mirror from the one directory-refresh writer**, the per-company `unitSettingsReadAt` stamp, the comma-string parser, and a pure `pmsUnitSettingFor(unit)` reader with unit tests **only** (inert until 9b wires it into `depositFor`); the basis verified against AppFolio's documentation; the propflowai side of the new evidence kind. **13a-2 (ingest)**: the `unit_directory` column allow-list widened and the `pms-setting` recipe steps (`0.00` = unset; rent-roll `Deposit` and rent-roll `ApplicationFee` never `pms-setting`) | — | 200 + 150 |
| 9a–b | **9a: the Settings tab "Knowledge base · Ask"** (`/settings/knowledge-base`, the Ask Clara component with the 8a tool set, topic pre-typed from inbound links) **and, in the same PR, the deletion of `/settings/fees`, `/settings/policies`, `/settings/knowledge` and `/settings/property/{knowledge,policies}`** with the repoints + drift lists (§5.4), the "no editable-soon" test; opened only after the combined localhost preview (§12.4). **9b: the money render** — M2's sourced render for contested rows, M1b read into quotes under M3 (`contested-sourced` / `pms-setting-disagrees`), the `feeSourceRule` clauses with their drift test, the 0-simulation voice re-sync, the no-fee-dropped assertion | A, B | 9a large, one concern (four page families deleted, 36 files + 12 drift lists repointed in one diff) + 9b 300 |
| 10a–c | Hours + holidays typed: building `office_hours` writer, booking and the hours readers in Appendix C's part-10 row onto the chain, `holiday_policy` row, one-loop Save, data move (dry run; Gera's go); **10c: `maintenance.access_instructions`** | — | 300 + 150 + 200 |
| 11 | G-BRAIN1 observe check (an `observe` row, per the registry's rule; Gera's go), mutated-figure controls, 30-day report by origin incl. fees that went dark | — | 300 + replay |
| 12 | Rail, propflowai half: internal route, batch writer, the rail's `acceptRule` calls (Situs-shape tests; `ingest/`-only retirement), `brain_digest` path (registry key + catalog row + `TeamNotice` kind + `MODULE_FLAGS`) through the send boundary, the daily list incl. the manager confirm line, stall page, `BRAIN_LEARN_ROLLOUT` (TEST) | C | 300 |
| 13b–c | 13b: registry scope/sink from propflowai's topics, the building dimension on the sheet, **`ROW#` becomes evidence-only** (its `value`/`status` move to "found this run"; the grid's Value/Status columns read `GET /api/internal/brain/entries`; dry run; Gera's go); 13c: authority-first, the rail writer, company list, on-demand trigger, SYSTEMS signals (13a moved into Stage A above) | C | 300 + 200 |
| 14 | Settings loop + strip: on-call/escalation and hours one-loop pre-fill; **renewal terms pre-fill (M9)**; collected-answer cards removed; "Sending hours"; **the app renamed kb.propflowai.co** (custom domain, 301 from ingest.propflowai.co, Access app renamed, header "Company knowledge base"), its four accept/edit doors re-pointed at the rail, `onBehalfOf` on the grid | — | 300 + 150 + 150 |
| 15 | Quoted-facts ledger **and lease ledger** (every `pms-write` figure with source and status) + weekly PMS diff + same-day page on PMS contradiction + "who heard the old value" and "which leases carry it" (**no re-ask on age**, M10) | — | 300 |
| 16 | Backfill (§8.2 step 5; dry run; Gera per step); the sweep's 14-day moves armed | — | 300 |
| 17a–b | Budget: measure; overflow tool; raise on measured replay | — | 200 + 150 |
| 18 | Per-company rollout PRs (`BRAIN_LEARN_ROLLOUT` + `brain_digest` Live) | Gera | 1 each |
| 19 | Removal lane (§8.2 step 7; days of work; one concern) | after 16 + a green week | large |
Order: 0, 0.5 → 1a, 1b, 2 → 3a–c, 4 → 5, 6, 7a-1, 7a-2 → 7b, 7c, 8, **13a-1, 13a-2** → 9 → 10, 11, 15 → 12 + 13b–c → 14 →
16 → 17 → 18 → 19. `acceptRule` exists (6) before any caller (7b, 9b's M1b read); nothing is deleted before 16; nothing is
writable before its readers, its writers and the guard's homes exist; the old Settings pages are deleted only in the PR that
ships the Ask tab (9a); renters hear a sourced fee, and a plain AppFolio unit fee, only once a manager can answer the question
(the Ask tab, 9a, before 9b). **Building starts when the critics pass this plan**: part 0 (the ADR PRs) and part 1 open the
same day; the combined localhost preview precedes the first UI PR (§12.4).
---
## 10. Risks, bridge note, decided here
### 10.1 Risks (plain words)
| Risk | Why real | Mitigation |
|---|---|---|
| An emergency type stops paging | prose-only parses to null today | part 5 before the topic is writable; floor always; union; never learned; bench |
| Booking stops | prose hours read as unreadable | hours never leave the typed row; part 10 carries its own data move |
| A leasing agent's or viewer's tap makes a fee a plain fact | `mayHearMoney` admits them; `mayConfirmFees` admits any member | `mayConfirmMoney` (pm, org_admin, the four team labels) on every money confirm; `onBehalfOf` for Propflow admins; per-tier tests |
| A regional manager or accounting lead cannot confirm a fee | an allow-set narrower than Gera's 08-19 ruling | the four team labels are in `mayConfirmMoney` |
| Clara quotes a wrong number as fact | a lone listing, an unstamped record, a tech's teach | M1/M2/M4; the ledger and the weekly diff feed M3 |
| A scraped listing deposit is written into a lease as if it were a fact | the marketing-page deposit looks like a PMS value | `depositFor` reads the unit's AppFolio `DefaultDeposit` (M1b) or manager-confirmed tiers, never the listing, on `pms-write`; the one-deposit test |
| AppFolio's unit value silently overrides a manager, a staff answer or the listing (the Situs incident in its most confident form) | a per-unit field ranked first with no comparison | M1b is plain only when uncontested; any disagreement is an M3 question; a manager-confirmed value keeps rendering plain; the lease holds; `pms-setting-disagrees` counted per company before 9b |
| A lease carries a figure nobody stands behind | the approve route writes from a snapshot with "staff by role"; the `prepare`/`clara` rungs write before a person acts | M5: the approve route re-reads on `pms-write`, a manager's approve confirms, a non-manager's is refused with the figure named; the rungs are M1-only |
| Lease and quote disagree on the deposit | tiers in one place, a listing figure in another | `depositFor` is the one function for LEASE ANSWERS, the tool, the package and the packet |
| Fees Clara quotes today go dark mid-migration | `acceptRule` after its callers; a rollout gate on live writers | `acceptRule` in part 6; the gate scoped to `ingest/`; crons and property creation produce live rows; every row renders bare until 9b, and only contested rows change then |
| A sourced fee stays sourced forever | nobody is asked to confirm pulled data | under C a lone source is a fact, so only a contested fee is sourced, and it stays so only until a manager answers in the Ask tab, by reply to the daily list, or by setting the fee on the unit in AppFolio (M1b, part 13a) |
| A resident loses their late fee, hears a frozen number, or drops the source wording | one scope for surfaces that differ; a typed fallback after the flip; the fee rule is kept out of resident prompts | `readerScope` per surface; the fallback only while projected; the resident prompt line (M2) with its drift test and three-phrasing bench |
| A customer's review submit, a new building's fees, a staff teach or a document upload is masked or throws | writers in three storage families | Appendix D's two tables with homes; the storage guard; the `writable` flip gated on both |
| The page shows $38 while Clara says $40 | legacy page readers | the page readers move in 7b with the fees; save-then-GET test |
| A tech's number reaches the knowledge base | a label on a shared row | the closed operational list, zero contact data, `learned` mode, redaction on machine writes, Fede's nod |
| The tour reminder or key-pickup text goes silent | fail-soft senders read the legacy store | part 7c; reverse-allowlist ban with an existence test |
| The M2 line collides with Fede's fee rule | "a listing fee is not a fee you quote"; "a home's own deposit from the listing" | two clauses added to `feeSourceRule`; attribution carried in the line; drift test; bench with the rule in the prompt |
| Voice drops a fee when attributed lines lengthen | the 4,500-char cap | the §3.8 no-fee-dropped assertion at 9b |
| A team message is sent to a customer under Supervised | a new team-message path | the `brain_digest` path on the send boundary, Off/Preview/Supervised/Live spelled out, unset = held |
| "Learns daily" stalls behind a Propflow approval | Supervised digest | `brain_digest` Live by default; Supervised optional |
| The prompt grows daily | no cap today | budget at today's size; overflow tool first; raise on measurement |
| Ingest's writes depend on one Mac | launchd on the mini | SYSTEMS signals; on-demand trigger; the knowledge base serves yesterday's entries |
| The removal lane never finishes | second PRs rarely land | part 19 in the lane with a date gate |
| The census lies | a re-implemented assembler | the bench door with the real knobs; `loadTurnPropertyContext` shared |
| Fixture ids leak a real slug | the identifier fence | every lane brief: synthetic ids only |
### 10.2 Fede (not looped in; one FYI line, no waits)
Gera, 2026-11-05: no need to loop Fede in. For the record, what this plan changes of his earlier rulings, all on Gera's
decision: his company brain's Public half becomes the Company knowledge base and its Internal half stays as "Team notes";
his 10-07 "knowledge base is public" is narrowed to four operational topics with zero contact data (ADR-0156); his 09-29 fee
rule gains two clauses (a fee line that carries where it was posted is quoted with that mark; a home's listed deposit is quoted
only when no deposit policy is on file); the registry's "count only goes down" rule is honoured with one `observe` row. One
line on #engineering-bridge when part 9a merges, as a heads-up, not a wait.
### 10.3 Decided here (Fable if anyone disagrees)
Customers' surface = the Settings tab "Knowledge base · Ask" on the existing Ask Clara chat door with two tools; no customer
page or cards · staff surface = the ingest grid, renamed **kb.propflowai.co** (same Pages project, custom domain, 301 from
`ingest.propflowai.co`, Access app renamed), repo and SYSTEMS key stay `propflow-ingest` · facts live only in propflowai's
table; the grid reads them through `GET /api/internal/brain/entries` and writes through the POST; `ROW#` keeps evidence only
(13b); no second copy, no sync · the 24 h unit-field freshness still holds `pms-write` (`pms-setting-stale`): staleness is a
liveness fault, not a trust question, and the directory refresh runs daily, so the hold is rare and self-clearing · a lease
ledger joins the quoted-facts ledger (15) because leases now take single-source figures · suggestions and questions reach
customers through the chat · old pages deleted in the Ask-tab PR · ingest gets a building dimension · budgets start at today's size · voice cap stays until documented · ADR numbers 0084 / 0156 · brain slots out of the
map-changes view · `group` refused · emergencies union; `either` both lines · the numbers-only-from-typed ruling survives for
settings keys · fee topics `new-lease` on the prompt at migration · staff words evidence only · screening/income operational ·
digest cap N and stall N: Fable · audience tightening Propflow-staff only · renewal terms typed (FYI) · `brain_suggested`
naming · per-surface reader scope · vendor codes in a typed Settings key · the day-14 move is a sweep · `brain_digest` Live by
default, unset = held · agreement-not-authority in `acceptRule` · the storage guard · `readerScope.snapshot: 'all'` minus
operational · a manager's chat correction applies at once with Undo; YES only for SMS/email teaches · M5's per-rung gate and the
approve-route re-read (uncontested writes, contested holds) · `depositFor` as the one deposit source · M2's sourced arithmetic while contested · every row bare until 9b, only contested rows change ·
the generated reader list is authoritative · `mayConfirmMoney` follows the 08-19 ruling (no Fable item).
---
## 11. The ADR
The agentflow ADR is drafted in full as `ADR-draft.md` beside this file: **ADR-0084, "One Company knowledge base per
company: the only store Clara, Settings and ingest use"**, Accepted 2026-11-05, decider Gera, with the inventory of what it
retires (§12). **propflowai ADR-0156** (next after 0155) records the in-repo mechanics.
The text of ADR-0084 is `ADR-draft.md` and is not repeated here.
**propflowai ADR-0156** records: the two slots and filter-then-fold; the two-sided `writable` invariant and the storage
guard; the ADR-0144 §6.4 amendment; brain slots excluded from the map-changes view and undo; the money table as code
(`readBrain`'s surfaces, `acceptRule`, `depositFor`, M5's approve-route re-read and rung gate, `mayConfirmMoney`); the
private-info narrowing and the four operational topics; the reader ban; `brain_digest` as a path with Live default; that the
plan adds one `observe` row to the closed registry and no `hold` row.
---
## 12. Consolidation: one way, everything else deleted
**Plain English.** Gera (2026-11-05): "pull everything in together, delete anything outdated, inherit this." This section
lists every knowledge-ish thing that exists today across the three repos, says for each whether it becomes part of the
Company knowledge base, is deleted (and after what proof), or stays because it is a true setting, names the stage where it
goes, and ends with the one grep that proves the end state. §12.4 is the drive: lanes, order, gates, go's, rollback, done.
### 12.1 Inventory and verdict
| # | Thing (where) | Verdict | How / when | Proof before deletion |
|---|---|---|---|---|
| **Stores** | | | | |
| S1 | `PROP#<id>/KNOWLEDGE` legacy record: `sections[]`, `pricingDetails`, `pricingVerified`, `pricingSources`, `leasePolicy`, `officeHours`, `tourHours`, `holidayPolicy`, `utilities`, `amenities` (propflowai) | INHERIT, then DELETE | projected by `readBrain` from 3c; copied into `brain.<topic>` rows at the backfill (16); hours/holidays into typed settings (10); knowledge fields deleted from the type and readers in 19 (`turnoverPolicy`, `tourUrl`, concessions stay: not knowledge) | census 0 unexpected diffs through 16; replay baseline held; the reader ban green |
| S2 | `ATTACH#setting#knowledge.<topic>` (typed topic answers, `specValueSchema`), `knowledge.section.<id>`, `pets.policy`, `fees.<slot>`, `application_fee`, `screening_policy`, `fair_housing_text`, `retention.days` (registry keys) | INHERIT, then DELETE | projected from 3a–b; written only through `writeBrainEntry` after 8b; keys and validators deleted in 19; orphan rows stay (ADR-0144 §6.4) | same |
| S3 | `setup_intake.collected.<slot>`, `knowledge_intake.*` markers, `setup_intake.answer_changed.*` | INHERIT (facts) / KEEP (stamps) | fact slots (portal link, other names, repair-waiting-part, eviction marking) become topics in 8b; the hand-off and change stamps stay as audit until 19, then only `HIST#` | 8b behaviour tests |
| S4 | `ORG#/PROFILE.settings` knowledge-ish values (tour showers, link mode) | KEEP | true settings | n/a |
| S5 | `VOICE_INJECTION_CACHE` | KEEP (as a cache) | rendered from `readBrain` from part 4; never a store of record | census voice row |
| S6 | `propflow-ingest-sheets`: `SHEET#/ROW#`, `RUN#`, `DECISION#`, `TRANSCRIPT#`, `TEACH#`, `FIND#`, `THREAD#`, `COMMENT#`, `RUNREQ#`, `REGPR#`, `RECIPE#`, `PREFILL#<org>/v1` | INHERIT as the **evidence ledger** of the Company knowledge base | the fact (`value`/`status`) leaves `ROW#` in 13b and lives only in propflowai's `brain.*` rows; `ROW#` keeps the evidence columns (path, source, seen, agreement, conflicts, attempts, notes, "found this run"); `PREFILL#` keeps serving the Settings pre-fills (on-call, `tour.*`); knowledge rows reach the store only through the rail and `acceptRule`; the sheet gains the building dimension (13b); the two readers that compute from `ROW#` values today, the coverage % (`sheet/coverage.ts`, via `web/functions/_lib/sheet.ts:107`, the ≥80% done signal) and the AppFolio audit (`audit/`, "latest intake sheet"), read facts from `GET /api/internal/brain/entries` from 13b | SYSTEMS signals; a contract test that no `ROW#` write carries a fact after 13b; the coverage % equals the count of live `brain.*` rows on a TEST company |
| S7 | Fede's "company brain" Internal notes (`src/lib/domain/company-brain/internal-notes.ts`, `ORG#` internal-note rows) | KEEP, renamed in copy | the staff-only **Team notes** drawer; the wall and its allowlist untouched; the code dir may be renamed later, not in this lane | planted-leak test |
| S8 | The core-topic registry (`policies/core/topic-specs.ts`, `topic-areas/*`, `topics.ts` catalog, `pets-topic.ts`) | INHERIT | the ids seed `brain/topics.ts` (1a, 3a); the typed field specs die with S2 in 19 | drift test "every topic complete" |
| S9 | Ingest's `registry/policies.json` + `recipes.json` | INHERIT | rows take scope/sink from `brain/topics.ts` (13b); recipes stay as the learning side's paths | contract test in both repos |
| S10 | `pms-knowledge-base` repo / SYSTEMS `kb-freshness` (AppFolio help articles for Clara's PMS-help answers) | KEEP, out of scope | not customer knowledge; its name stays because it is a different thing | n/a |
| **UI** | | | | |
| U1 | `/settings/policies` (Knowledge & policies: `CoreTopicCard`s, `PetsTopicCard`, `CustomPoliciesCard`, collected answers, `IngestHistoryCard`, staff found-value hints) | DELETE, not replaced | deleted in 9a, the same PR that ships the Ask tab; the 36 files carrying "Knowledge & policies" and the 12 drift lists repointed to `/settings/knowledge-base` in the same PR | the combined preview to Gera; "no editable-soon" test |
| U2 | `/settings/fees` (`FeesQuestions`, pet apply-to-all) | DELETE, not replaced | 9a (fee topics writable from 7b; a customer sets a fee by telling Clara in the Ask tab) | save-then-GET through the tool; census |
| U3 | `/settings/knowledge` (`CompanyKnowledgePage`, `SectionEditor`) and `/settings/property/{knowledge,policies}` stubs | DELETE, not replaced | 9a | repoints |
| U4 | `BuildingKnowledge` / `PropertyKnowledgeEditor` (the bottom-of-page building editor) | DELETE, not replaced | 9a | Camellia's 12 sections visible as entries in the grid first (3a) |
| U5 | Knowledge intake pages (`/knowledge-intake`, `/buildings`, `/vendors`, `/onboarding` intake views; 99 files carry the label) | DELETE as customer surfaces | their questions are the daily list and the Ask tab (a customer answers in the chat); staff see the same open rows in the grid; the routes redirect to `/settings/knowledge-base` in 9a and are removed in 19 | repoints; census |
| U6 | Setup-intake screens Applying / Fees / Policies / Tours & move-in / Maintenance › Residents / Documents (facts) | DELETE the fact screens | the wizard keeps only the setting screens (Office, Routing, People, Offers, Buildings, Billing, Catalog); a new company's facts come from the first kb run and its daily list, and from the Ask tab | onboarding-answers read via `readBrain({mode:'page'})` (7b); a new TEST company walks the wizard without a fact screen |
| U7 | Settings cards for collected facts (never-say stays; languages stays; repair-waiting-part, eviction-marking, portal link, other names go) | DELETE the fact cards in 14; KEEP the "how Clara talks" cards | 14 | company-answers trap closed in 8b |
| U8 | `IngestHistoryCard`, `ingest-topic-hints.tsx`, setup-intake suggestions from `PREFILL#` | INHERIT | the source line in an Ask answer and the evidence columns in the grid; the Settings pre-fill lines on the two BOTH cards (14) | n/a |
| U9 | ingest.propflowai.co (staff): the grid (`SheetTable`), rows, questions, handoffs, teach, promotions, find/run requests, replay, audits, documents, registry, tree, compare | **INHERIT and RENAME: it is the Company knowledge base's staff app, `kb.propflowai.co`** | the grid stays; Value/Status read propflowai's rows (13b); teach/answers/promotions/handoffs write through the rail (14); header, titles and the Access app renamed (14); `ingest.propflowai.co` 301s | the grid shows the same value the Ask tab answers (a contract test on a TEST company) |
| U10 | `/admin/dev/brain` (the prototype dev page) | DELETE | 19 | n/a |
| U11 | Command-palette / settings-search entries for pet fee, deposit, rent | INHERIT | open `/settings/knowledge-base` with the question pre-typed (9a) | palette test |
| U12 | **Settings › Knowledge base · Ask** (`/settings/knowledge-base`): the Ask Clara chat component with the two knowledge-base tools | **NEW** (the only customer surface) | 8a builds the tools on the chat door; 9a mounts the tab and deletes U1–U4 in the same PR | bench on the chat door; the grid-equals-tab contract test |
| **Code paths** | | | | |
| C1 | Readers: `loadCoreTopics`, `loadOrgKnowledgeSectionsSafe`, `getPropertyKnowledge` knowledge fields, `resolvePricingFactView`, `statedByTopicOf`, `OLDER_BUILDING_RECORDS`, `loadApplicationLeasePolicy`'s raw read, the 52 banned-directory importers (Appendix C) | INHERIT via `readBrain`/`forClara`; DELETE the legacy readers in 19 | parts 3c, 4, 7a-1/2, 7c, 10a | the reader ban (directory + type) green |
| C2 | Writers: the typed routes (`knowledge` PATCH fee branches, `policies` PUT, `settings/policies`, `fees/pet-apply`, core-topic routes, `settings/knowledge`, `custom-policies`, `knowledge-intake`, `company-answers`, review submit, document upload), teaching capture, team-answer-knowledge (Appendix D) | INHERIT via `writeBrainEntry`; DELETE the typed routes in 19 | 7b, 8a–b | the five-door storage guard; behaviour tests |
| C3 | Crons: `charge-fee-prefill`, `listing-fee-prefill` (every 15 min), the website concession scrape | INHERIT the two fee crons through `acceptRule` (7b), DELETE them in 19 once the rail covers every live company; KEEP the concession scrape (not knowledge) | 7b, 19 | `cron-learned` census reason |
| C4 | Scripts: `seed-org-knowledge.ts`, `apply-western-slope-*`, `apply-yale-*`, `patch-camellia-pricing.ts`, `_enroll-camellia-*`, `_set-camellia-*`, `migrate-property-lease-policy.ts`, `prefill-fees-from-*`, `load-knowledge-gold-set.ts`, `set-emergency-list.ts`, `backfill-knowledge-fair-housing-screen.ts`, `apply-knowledge-review.ts` | DELETE the one-offs (19); re-point the gold-set loader, the TEST provisioners and `set-emergency-list` at `writeBrainEntry` (2, 5) | 2, 5, 19 | the storage guard refuses them once `writable` |
| C5 | CLI twins: `product-twins/settings.ts` reads for the policies/fees/knowledge pages; no write twin | INHERIT | `app get knowledge-base` / `app do kb-set` / `app ask` (2, 8a); the old reads removed with the pages (9a) | product-twins drift test |
| C6 | The private-info gate, the fair-housing screen, the silent hand-off tables, `feeSourceRule`, `mayHearMoney`, the Ask Clara chat door (`api/dashboard/chat`, `feature/ask-clara`) | KEEP, extended | the gate on every write (2); `mayConfirmMoney` added (2); one clause on `feeSourceRule` (9b); the chat door gains the two knowledge-base tools and a `surface` that selects them (8a) | their drift tests |
| C7 | Ingest connectors reading back our old keys (`knowledge-intake.ts`, `staff-answers.ts`, `staff-replies.ts`, `staff-messages.ts`) | INHERIT, re-pointed at `brain.*` (13c), evidence-only (§6.2 rule 6) | 13c | contract test |
| **Docs / ADRs / SYSTEMS** | | | | |
| D1 | docs.propflowai.co: `company-brain-design-2026-10` (Fede), `company-brain-plan-2026-11`, `knowledge-source-of-truth-2026-11`, `knowledge-base-redesign-2026-08-26`, `knowledge-page-evolution-2026-08-18`, `home-knowledge-model`, `knowledge-layer-2026-08-29`, `knowledge-at-scale-2026-08-29`, `knowledge-grounding-architecture`, `knowledge-coverage-{situs,western-slope}`, `property-brain-prototype-2026-08-29`, `situs-property-brain` | RETIRE as design records (a one-line "superseded by ADR-0084" banner, kept for history); `company-brain-design-2026-10`'s Internal half stays live as the Team-notes design | 0 (the ADR PR) | n/a |
| D2 | agentflow ADR ledger: no existing ADR mentions the knowledge base, the company brain or propflow-ingest (grep, 2026-11-05); SYSTEMS.md:6716 cites "ADR-0059" for propflow-ingest, which is the Smith runtime-clone ADR | ADR-0084 becomes the ADR of record; the SYSTEMS.md citation is corrected | 0 | n/a |
| D3 | SYSTEMS.md rows `propflow-ingest`, `propflow-ingest-find-worker`, `propflow-ingest-audit` | INHERIT, re-scoped ("the Company knowledge base's app and learning engine, kb.propflowai.co"), signals added (§11); a new `company-knowledge-base` row for the propflowai side | 0, 12, 14 | liveness rows render on /agents |
| D4 | propflowai ADR-0080 (voice injection), ADR-0144 §6 (migration shape) | KEEP; 0156 amends §6.4 | 0 | n/a |
| **Names in UI copy** | | | | see §12.3 |
### 12.2 The end state
After part 19, this grep returns nothing outside `scripts/brain/backfill.ts`, `reverse-project.ts`, `HIST#` readers and the
docs archive:
```
git grep -n -E "Knowledge & policies|Knowledge intake|Company knowledge|Fees & deposits|Company brain|Internal notes|IngestHistoryCard|ingest-topic-hints|knowledge\.<fact>|knowledge\.section\.|pets\.policy|fees\.<slot>|application_fee'|PropertyKnowledgeEditor|BuildingKnowledge\b|loadCoreTopics|loadOrgKnowledgeSectionsSafe|resolvePricingFactView|OLDER_BUILDING_RECORDS|specValueSchema|charge-fee-prefill|listing-fee-prefill" -- src agents lambda scripts
```
piped through `grep -v "Company knowledge base"` (stock macOS git has no PCRE, and the fence is a merge gate), and, in
propflow-ingest, `git grep -n -E "intake sheet|policy registry|knowledge\.section|ingest\.propflowai\.co"` returns nothing
outside `web/wrangler.toml`'s redirect line and the history pages. The test `src/__tests__/knowledge-base-end-state.drift.test.ts` runs that grep in CI from
part 19 on, so the old names cannot creep back.
### 12.3 Naming sweep (user-facing copy only; code identifiers keep their names)
| Old label (files today) | New | Part |
|---|---|---|
| "Knowledge & policies" (36), settings menu, page title, hub links | removed; one Settings tab, **Knowledge base · Ask** (`/settings/knowledge-base`) | 9a |
| "Knowledge intake" (99) | removed; the questions live in the daily list, the Ask tab and the staff grid | 9a, 19 |
| "Company knowledge" (14; the company sections page) | removed | 9a |
| "Fees & deposits" (24) | removed from Settings; a fee is asked and corrected in the Ask tab | 9a |
| "Custom policies" (5) | removed; a custom topic is an entry like any other ("tell Clara anything else renters should know") | 9a |
| "What Clara knows about <building>" (3) | removed; the Ask tab answers per building | 9a |
| "Company brain" (1), "Internal notes" (5), `/admin/dev/brain` | **Team notes** (staff-only drawer); the dev page deleted | 9a, 19 |
| "Ingest", "intake sheet", "policy registry", "pre-fill", "found values" (ingest UI and propflowai hints) | the app is **Company knowledge base** (`kb.propflowai.co`); the grid's columns keep their names; "Questions" (the daily list), "seen in <source>" (evidence); renamed in one PR per ingest's CLAUDE.md:64 | 14 |
| "Policies" in `/settings/property/policies`, `COLLECTED_SETTINGS_HOMES.policies` | removed | 8b, 9a |
| Settings nav group "Knowledge & policies" | removed; **Knowledge base · Ask** is one tab in the Settings menu, no top-level nav item | 9a |
| Slack cards from ingest ("pre-fill", "sheet") | "Company knowledge base: question for <company>" | 12 |
### 12.4 The drive: how this is executed end to end
**Lanes** (each part ≤~300 substantive lines unless marked, vertical, branched off `origin/main`, driven to merge; no part
waits on Gera except the named go's):
| Lane | Parts | Runs | Owner's gate |
|---|---|---|---|
| L1 Store and reader | 0, 0.5, 1a, 1b, 2, 3a–c, 4 | serial (1a‖1b‖2 in parallel worktrees after 0.5) | census 0 unexpected diffs; replay baseline captured at 1b |
| L2 Code readers | 5, 6, 7a-1, 7a-2, 7c | parallel after 3c; 7a-2 after 7a-1 | bench cases per part; paperwork parity (7a-1) |
| L3 AppFolio unit fields | 13a-1, 13a-2 | parallel with L2 (13a-1 before 13a-2) | contract test both repos; the sync alone produces no census diff |
| L4 Writers, the Ask tab, the money render | 7b, 8a, 8b, 9a, 9b | after L1–L3; 7b‖8a; 9a opens only after the **combined localhost preview to Gera** of every screen (the Ask tab answering for each company, Camellia as a fact, Yale as a question; the kb.propflowai.co grid; Settings without Fees/Policies/Knowledge) | save-then-GET through the tool; "no editable-soon"; `contested-sourced` census; the voice no-fee-dropped assertion; bench on the chat door |
| L5 Settings, hours, the app rename | 10a–c, 14 | after 9b | booking parity; `isAfterHours` parity; the re-pointed doors' tests; the 301 and the Access app checked live |
| L6 Learning and the grid's data | 12, 13b, 13c, 18 | after 9b (12 after 6) | Situs-shape `acceptRule` tests; `brain_digest` path on the send boundary; the grid-equals-tab contract test; SYSTEMS signals |
| L7 Hardening | 11, 15, 17 | any time after 7a-1 | G-BRAIN1 controls; ledger; budget measurement |
| L8 Backfill and removal | 16, 19 | 16 after 14 and L6; 19 after a green week | the end-state grep test; replay baseline held |
**Start.** When the critics pass this plan, part 0 (the two ADR PRs, the SYSTEMS rows) and part 1 open the same day; the
combined localhost preview is built on the part-8a branch and shown to Gera before 9a is split into PRs.
**Stages and deletion milestones.** Stage A (L1–L4): "one place" is true the day 9b merges; the Fees, Policies and Knowledge
pages are deleted in 9a, the PR that ships the Ask tab, and nothing else is deleted yet. Stage B (L5–L6): Settings stripped
(14 deletes the fact cards, renames the app and re-points its doors); daily learning on per company (18). Stage C (L8): the backfill (16), then the removal lane (19) deletes
S1–S2's readers, writers, keys and types, C3's crons, C4's one-offs, U5's standalone routes, U10, and the rollout list.
**Proof gates per stage.** A: census 0 unexpected diffs on every PR, attached to the PR body from a read-only prod run; the
bench pack green for 3c, 5, 7a, 7b, 7c, 8, 9; the replay not below baseline; the combined localhost preview of every screen
seen by Gera before 9a opens; after 9b merges, a live check on /conversations that the first real fee answer at each company carries the expected
shape. B: booking and after-hours parity; the daily list recorded on /conversations for a TEST company; SYSTEMS rows green.
C: the dry run's counts reviewed per company; after 16, a week of green census and replay; after 19, the end-state grep test
green and the reader ban at zero exceptions.
**Who says go (prod writes and real-customer changes).** Gera, every time (11-05: Fede is not looped in; CLAUDE.md:171
makes it a founder's call and :172 a per-step OK). 10b (hours data move): Gera's per-step OK. 13b (`ROW#` to evidence-only):
the same. 16 (the backfill): the same, one approval per company run. 9b's merge
after the preview is Gera's go for the render change at every live company (§Appendix G); a company added later gets it
through its rollout PR. 18 (daily learning per company): Gera's go, one line each. G-BRAIN1 and the four operational topics: Gera's decision
(the registry's "count only goes down" rule is honoured by an `observe` row). Everything else: bot 🟢 + green checks + the
gates above; no founder wait.
**Rollback per stage.** A: before any topic is writable, revert the reader PR; after, the write freeze in `writeBrainEntry`
plus `reverse-project.ts` (typed fields from `ok` facets, the rest as sections, listed). 9a: revert restores the old pages and removes the tab together (one PR). 9b: revert restores bare rendering and removes
the M1b read together. B: 14's deletions and the rename are revertable PRs (the 301 is removed with the revert; the
re-pointed doors return to the ledger). C: 16 is
additive (`--undo --run <n>`); 19 is reverted only whole, which is why it waits for a green week.
**Done criterion.** Every live company's Clara reads only `brain.*` rows through `readBrain`; a customer sees and corrects a
company fact only through Settings › Knowledge base · Ask, and staff only through the grid at kb.propflowai.co, both on the
same rows; Settings holds only the KEEP rows of §5.1; the kb app writes only through the rail and reads facts only from
`brain.*`; `ingest.propflowai.co` redirects; the end-state grep test is green on main; SYSTEMS.md's `company-knowledge-base` and
`propflow-ingest` rows render healthy; ADR-0084 and ADR-0156 are Accepted with their inventories checked off; and the
docs page for people (`plan-decided-for-gera.md`) matches what the product shows.
---
## Appendix A0 — Changes from the final plan to the decided plan (2026-11-05)
Gera decided A (name: Company knowledge base, the only name), B (read-only + corrections by telling Clara) and C ("one source is enough").
The money table is rewritten under C: M1 covers any single uncontested source; M2 is now the contested quote only; M3 is
unchanged in shape, one question per building; M4 treats a staff statement as one source; M5 (leases) was first kept strict and
flagged as an assumption, then decided the same day (see below); M11 says the migration changes almost nothing a renter hears. §2.1 names what
C gives up and what limits it. The day-one counts are re-done under C from the same prod reads (Situs: 2 facts, 7 questions
among its application-fee records; Western Slope: all facts; Camellia: all facts; Yale: questions). §1.2 is the decided
name; §12 is the consolidation inventory, the naming sweep, the end-state grep and the drive. The ADR is `ADR-draft.md`
(0084).
**Same day, five more decisions (Gera 2026-11-05, folded in):** (1) leases follow the same rule as Clara, so M5 writes any
uncontested figure and holds on a contested one (the "assumed, may override" flag is gone; `packet-not-ready-contested`; a lease
ledger in part 15); (2) ingest is rebranded as the Company knowledge base: the app becomes kb.propflowai.co, its grid is the
staff view onto propflowai's rows, no second copy (§1.2, S6, U9, 13b, 14); (3) customers get one Settings tab, "Knowledge
base · Ask", on the Ask Clara chat door with two tools, instead of a knowledge-base page with cards (§1.2, §5.3, 8a, 9a; the
Policies/Fees/Knowledge pages are deleted, not replaced); (4) Fede is not looped in: every go is Gera's (§10.2, §12.4);
(5) building starts when the critics pass, part 0 and 1 first, one combined localhost preview before any UI PR (§12.4). The census reasons `m2-attributed-on`, `pms-setting-plain`, `lone-source-money-attributed` and
`person-no-manager-attributed` are retired; `contested-sourced` replaces them; `pms-setting-disagrees` stays.
---
## Appendix A — Changes from v9, then from v8, and the consistency pass
**Changes from v9 (round 9, all three critics on one issue).** (1) **M1b joins M3**: an AppFolio unit value (`ApplicationFee`
/ `DefaultDeposit`, never `0.00`/null/negative, never the rent-roll `Deposit` which is the sitting tenant's held deposit) is a
plain fact only when nothing else we hold disagrees (a manager-confirmed value, a staff answer, charges, the listing, marketing
text, the building entry); any disagreement is one question in the daily list, Clara quotes the M3 winner with its source or
hands off silently per the table, and a lease or package holds until a `mayConfirmMoney` person answers; a manager-confirmed
value is never overridden by a later AppFolio value without a question and keeps rendering plain meanwhile (M1b, M3, M5,
§6.2 rule 2). Census reasons `pms-setting-plain` / `pms-setting-disagrees`, the assertion that the 13a sync alone changes
nothing Clara quotes, and three bench cases (§8.3). (2) **Day one is true**: 13a is split into 13a-1 (propflowai: the two
columns on `fetchListingUnitDirectory` and the PMS unit sync, the mirror field, the 24 h freshness bound, `depositFor`'s read)
and 13a-2 (ingest: the `unit_directory` column allow-list and the `pms-setting` recipe steps), both moved into Stage A before
9b; 9b reads M1b into quotes with the attributed render; §0's table and the Gera page say so, and Situs's contested count is
given (at least 7 of the 14 fee units disagree with Situs's own stamped values: AF 64, 66, 70, 75, 78, 84, 86). The
building fold-up rule is stated (all units set and agreeing: 17 Situs buildings; 19 only if unset units were ignored, which
the rule does not do). (3) The stale changelog line saying the AppFolio field did not exist is gone (below); 13a is two repos,
two PRs; 7a is split 7a-1/7a-2 at the pre-agreed seam; the PR count is ~41. (4) The Gera page answers "how deep", states the
Situs example honestly, names what leaves Settings, says the PM types into the card's Tell Clara box, and gives decision C's
alternative.
### Changes from v8, and the consistency pass
**Consistency pass.** Every money rule lives in §2 and nowhere else; a grep of each section against M1–M12 finds no
restatement: §0 cites C and the count; §1.3 cites M1b, M2 and M6; §3.4 cites M5, M6, M7; §4 rows cite M5, M6, M7, M8, M9;
§5.1 cites M6, M9; §5.3 cites M1a, M4; §6.2 cites M1/M2/M3, M4, M11; §6.4 cites M1b and C; §6.5 cites §2 and M11; §7 cites
§2, M2, M3, M10; §8.2 cites M11 and M4 only; §9 cites M5–M11; §10 cites the rows; §11 summarises the table. No "as vN"
remains; no "Fees GET raw", "12-month", "confirmed-only lease", "unit's synced deposit" or "cron-learned-attributed"
phrasing survives.
**Must-fix (engineering and Clara).** (1) **M1b redefined from evidence**: v8's `Unit.securityDeposit` claim was wrong
(`types.ts:574` is `NewLeasePackagePreview.securityDeposit`, computed from the knowledge base's tiers; the PMS adapter
`domain/pms/client.ts` carries no fee or deposit capability; the scraped listing deposits are `listing`), **but AppFolio's
unit directory report does carry a per-unit `ApplicationFee` and `DefaultDeposit`**, which is M1b as the table now states
(synced in 13a, plain only when uncontested); **`depositFor` is the one deposit source** for LEASE ANSWERS, `get_pricing_details`,
the package and the packet, the listing deposit never a `pms-write` input, with the one-deposit test (M5). (2) **The
package-review approve route** (`new-lease-package-reviews/[id]/decision/route.ts:403-427`, the only lease writer live today)
joins M5: it re-reads every figure on `pms-write` at approval; a `mayConfirmMoney` approver confirms M2 figures; any other
approver is refused with the figure named; added to Appendix C, the census and `human-gate.drift.test.ts`. (3)
**`mayConfirmMoney` follows Gera's 08-19 ruling**: pm, org_admin and the four team labels (`accounting`, `regional_manager`,
`assistant_property_manager`, `leasing_assistant`; `types.ts:17316-17322`, `role-matrix.ts:110-118`), on the existing
`userRoleToPersonRole`; no Fable item. (4) **M8 and M11 agree**: a cron's new row renders bare until 9b and attributed from
9b, like every M2 row; reason `cron-learned`; the test asserts both halves. (5) **Residents get the fee-source wording
through the prompt**: one line in the resident scope rail (the leasing fee rule is kept out of resident prompts by
`RESIDENT_FORBIDDEN_FEE_PHRASES`), with a drift test and a three-phrasing bench.
**Engineering, smaller.** The approve route's directory joins the ban; M2 is decided by status plus `byTiers`, not origin;
the authority table is vendored with a drift test by part 6; document upload and review forms run the gate in `learned` mode;
the voice no-fee-dropped assertion at 9b; a Propflow admin's Fees-page Save lands M2 until the `onBehalfOf` field (7b).
**Clara, smaller.** `#25222` merged without covering RULE 7.1's line, so 3c owns it; an unresolvable review submitter is "no
actor" (M2); the resident clause carried on the tool and the prompt.
**Intent.** §0 now says plainly that (a) fees a non-manager typed on the old Fees page show "not confirmed" (Camellia's $38
in the preview), and (b) after a Settings save, later brain changes arrive as a one-tap question; Situs's day-one fact count is
the exact **0 of 29** (the single stamping actor is a Propflow administrator with no customer manager on the record); Tell
Clara ships with the page (decision B); the people version is `plan-decided-for-gera.md`.
## Appendix B — Rebuttals, rounds 8 and 9
**Round 9.** None rejected. One count note: the engineering critic's "17 agreeing buildings" and v9's "19" are both
reproducible from the same pull under different rules (17 when every unit in the building, unset ones included, carries one
value; 19 when unset units are ignored); the plan adopts 17 and says why in M1b. The Clara critic's "7 houses" contested
count is confirmed from the stamped building values (AF 64 $26 vs $31; AF 66, 70, 75, 78, 84, 86 $40 vs $45; AF 72 and 76
agree at $45).
**Round 8.**
None rejected, with two corrections of my own. (1) v8's "`Unit.securityDeposit`, `types.ts:574`" is **retracted**: that line is
`NewLeasePackagePreview.securityDeposit`, computed from the knowledge base's tiers; `Unit` has no deposit field; the PMS adapter's
capability surface carries no fee or deposit. (2) But the critics' "AppFolio gives us no fee or deposit setting" is **also
wrong**, and my first v9 draft repeated it: AppFolio's unit directory report (`reports/unit_directory.json`), which propflowai's
`AppfolioClient` already requests (the 2026-11-03 Situs audit pull in this scratchpad: 644 unit rows) and ingest's `appfolio/api`
resolver already reads for other recipes, carries **`ApplicationFee` and `DefaultDeposit` per unit** (Situs: 14 units with a
fee set, 345 units in 35 buildings with a deposit set). So M1(b) is real today per unit, needs a two-column sync rather than an
L4 screen capture, and `depositFor` reads it first. One evidence note on the intent critic's "at most 9": the nine Situs stamps
resolve to one actor, a Propflow `platform_admin` with home company `org_propflow_staff`, so under M1a they are M2 and the
building-record count is 0.
## Appendix C — The reader inventory (seed for the ban; the generated list from part 1a is authoritative; 52 files in banned directories on `de46aab7f57`)
| Part | Files |
|---|---|
| 3c | `agents/clara/lib/agent/conversation-manager.ts` (via `loadTurnPropertyContext`), `property-knowledge-block.ts`, `leasing/knowledge/leasing-knowledge-block.ts` |
| 4 | `src/app/api/voice/personalization/route.ts`, `voice/call-ended/route.ts`, `integrations/voice/leasing-context-injection.ts`, `agents/clara/lib/voice/post-transfer.ts` |
| 7a | `agents/clara/lib/agent/tools-leasing.ts` (ten sites), `tools/handlers/handle-get-lease-terms.ts`, `leasing/first-email-body-guard.ts`, `tools/handlers/{handle-forward-to-property-manager,handle-capture-unknown-caller-note}.ts`, `leasing/lease-terms/policy-store.ts`, `agents/clara/lib/agent/lease-answer-context.ts`, `leasing/new-lease/{on-application-approved,recapture-stale-package,approved-applicant-package}.ts`, `temporal/activities/new-lease-paperwork.ts`, **`app/api/new-lease-package-reviews/[id]/decision/route.ts`**, `lambda/inbound-processor/handler.ts`, `leasing/leasing-data-quality.ts` |
| 7c | `leasing/tour-arrival-info.ts`, `move-in/key-pickup.ts`, `leasing/notice-to-vacate-period.ts`, `domain/automations/handlers/ntv.ts`, `leasing/tour-mode.ts`, `leasing/tour/process-tour.ts`, `temporal/activities/{tour,escalation-matter,pm-action-reminder}.ts`, `leasing/renewal-orchestration/sms-cascade.ts`, `leasing/renewal-policy/facts-store.ts`, `leasing/reporting-digest/run-digest.ts` |
| 10a | `calendar/check-availability.ts` (incl. `requestScopedPropertyKnowledge` `:1565`), `leasing/tour-hours.ts`, `move-in/movein-calendar-reconcile.ts`, `app/api/twilio/voice/wrap/route.ts:62,74`, `escalation/stale-hold-ladder.ts:143`, `maintenance/after-hours-oncall-heads-up.ts:152-157`, `vendors/vendor-scheduled-event.ts:407` |
| type-only, parity test | `tools/handlers/handle-apply-charge-decision.ts`, `turnover/{condition-report,load-turnover-detail,materialize-charge-knowledge,resolve-scope-vendors,restage-turnover-charges}.ts`, `temporal/activities/turnover.ts`, `domain/automations/actions/schedule-inspection-event.ts:140`, `vendors/resolve-preferred-vendors.ts:228` (`turnoverPolicy` and vendor fields) |
| allowlist (raw by design) | the data layers (`agents/clara/lib/data/*`, `src/lib/data/*`), `leasing/save-knowledge-and-refresh-voice-cache.ts`, `leasing/knowledge/apply-knowledge-review-writer.ts`, `private-info-gate.ts:294`, `leasing/concession-watcher.ts`, the census, the backfill, atlas |
The pages, the Fees GET and the onboarding readers are not on the allowlist: they read `readBrain({ mode: 'page' })` from
part 7b (§5.1).
## Appendix D — The writer inventories (homes for the storage guard)
**Money writers (23 modules calling `savePropertyKnowledge`/`saveKnowledgeAndRefreshVoiceCache` with `pricingDetails`,
`pricingSources` or `leasePolicy`):**
| Writer | Home once the topic is `writable` |
|---|---|
| `app/api/properties/[id]/knowledge/route.ts` (editor PATCH, fee branches); `app/api/properties/[id]/policies/route.ts` (Fees PUT); `app/api/settings/policies`, `app/api/settings/fees/pet-apply` | `writeBrainEntry` confirmed (M1a role check; a non-manager's save lands M2), else refuse "edit this in Company knowledge base" |
| `app/api/onboarding/review/[slug]/submit/route.ts` → `leasing/knowledge/apply-knowledge-review-writer.ts` | `writeBrainEntry` confirmed, `via: 'review'`, M1a role check, gate in `learned` mode |
| `app/api/properties/route.ts:412-430` (property creation); `policies/charge-fee-prefill.ts`; `policies/listing-fee-prefill.ts`; `scripts/prefill-fees-from-{charges,listings}.ts` | `acceptRule` (M8): `learned`, or a question; not gated by the rollout |
| `platform/scrapers/shared.ts` | concessions only; asserted never to write money |
| `leasing/save-knowledge-and-refresh-voice-cache.ts`, `src/lib/data/store.ts`, `agents/clara/lib/data/store.ts` | the data-layer writers: allowlisted |
| `scripts/knowledge/load-knowledge-gold-set.ts`, the TEST provisioners (`provision-western-slope-*`, `seed-ingest-replay-home`, `willows-approved-applicant-harness`) | re-pointed at `writeBrainEntry` in part 2 |
| `scripts/migrate-property-lease-policy.ts`, `patch-camellia-pricing.ts`, `_enroll-camellia-lease-policy.ts`, `_set-camellia-application-guarantor-knowledge.ts`, `apply-western-slope-{knowledge-sections,around-our-areas}.ts`, `apply-yale-{lease-kb,pet-policy}.ts` | refuse once `writable`; deleted in part 19 |
**Non-money writers (each has a home before prose and section topics flip `writable` in 8b):**
| Writer | Home |
|---|---|
| `escalation/teaching-capture.ts:50` (`updateKnowledgeSections`, the live staff-teach path); `escalation/team-answer-knowledge.ts` | `writeBrainEntry` via `teach` (§5.3; M4 for money) |
| `app/api/properties/[id]/knowledge/ingest/route.ts` (document upload: "PUT — Confirm and save extracted sections (user may have edited them)", `:156`) | **a person's save**: `writeBrainEntry` confirmed, `via: 'review'`, `origin: document`, gate in `learned` mode on the extracted text; M1a for money |
| `app/api/settings/knowledge-intake`, `onboarding/collected-answers-store.ts` | `writeBrainEntry` confirmed, `via: 'review'`; M1a for money (deposit-by-building) |
| `app/api/settings/knowledge`, the core-topic routes (`settings/core-topics/[topic]`, `properties/[id]/core-topics/[topic]`), `settings/custom-policies` | `writeBrainEntry` confirmed, `via: 'brain-page'` until part 9 replaces the routes |
| `data/dynamo/pet-fee-apply.ts` (transact) | "set for all buildings" through `writeBrainEntry` |
| `updateConcessions` | concessions are not a topic; untouched |
| `scripts/brain/reverse-project.ts` | allowlisted (rollback) |
## Appendix E — Files touched
propflowai: `src/lib/domain/brain/{topics,entry,read-brain,write-brain,accept-rule}.ts`, `brain/facets/*`, `brain/authority.vendored.ts`,
`portfolio/registry/keys.ts` (`brain.<topic>`, `brain_suggested.<topic>`, `gate.path.brain_digest`, `maintenance.access_instructions`),
`src/lib/tools/role-matrix.ts` (`mayConfirmMoney`), `leasing/lease-terms/deposit-for.ts` (new), the Clara prompt and renderer files
in Appendix C, `agents/clara/lib/agent/clara-unified.ts` (resident fee line), `leasing/fee-source-rule.ts` (two clauses) and
`src/__tests__/fee-source-single-rule.drift.test.ts`, `clara-leasing.ts` (`SILENT_CONCERNS_RULE_LINES`, `SILENT_HANDOFF_RULE_LINES`
keys), `property-knowledge-block.ts` (sibling table; the M2 line), `leasing/knowledge/private-info-gate.ts` (closed list, reason
codes), the five storage doors (§3.7), `escalation/{teaching-capture,team-answer-knowledge}.ts`, every writer in Appendix D,
`app/api/new-lease-package-reviews/[id]/decision/route.ts`, `app/api/brain/*`, `app/api/internal/brain/*`, `app/(workspace)/brain/*`,
the Settings pages and drift lists in §5.4, `clara-bench/door/{assemble,load}.ts` (knobs), `scripts/brain/{census,count-fees,backfill,reverse-project,sweep,bench}.ts`,
`scripts/lib/write-twins.ts`, `scripts/lib/product-twins/settings.ts`, `scripts/clara-checks/registry.ts` (one `observe` row),
`data/dynamo/map-log.ts` (view/undo exclusion), the Maintenance card and the handyman text composer, `docs/adr/0156-*.md`.
propflow-ingest: `registry/policies.json`, `ingest/runner.ts`, `ingest/recipes.json` (robot settings reads, if the screen exists),
`sheet/{schema,prefill,authority}.ts`, `connectors/appfolio/robot-reads.ts`, `connectors/propflowai/*`, `sink/brain-rail.ts` (new),
`schedule/*`, the seven web doors (removed), `README.md`, `CLAUDE.md`. agentflow: `docs/adr/0084-*.md`, `SYSTEMS.md`.
## Appendix G — Execution notes (round 10; every EXECUTION-LEVEL item, folded in; no new design)
**13a-1 ships stored field + reader only; 9b wires `depositFor`.** 13a-1 stores the two columns on the mirror and ships a pure
`pmsUnitSettingFor(unit)` with unit tests; it never calls into `depositFor` or the fee lookup. 9b adds the call, in the same PR
as the `contested-sourced` / `pms-setting-disagrees` census reasons, so no lease approved between 13a-1 and 9b carries a unit
default ahead of the tier.
**13a-1 storage details.** Key the two columns by (`PropertyId`, `UnitId`), not `RentableUid` (`directoryFromRows`,
`listing-unit-ids.ts:76-90`, drops rows without one; `Unit.af.unitId` is the match already used). Parse AppFolio money strings
with commas (`"3,250.00"`), and pin a parser test on `"3,250.00"`, `"0.00"`, `null` and `""`, because a naive `Number()` would
turn all 345 Situs deposits into "unset". Stamp a per-company `unitSettingsReadAt` on every **complete** pass (a truncated read
never counts; `fetchListingUnitDirectory` already throws on truncation); `pms-write` checks that stamp, not the unit row's
`updatedAt`. One writer only: the directory-refresh unit stamp (`property-directory-refresh.ts:217-241`, which already runs
`fetchListingUnitDirectory` for every AppFolio company), so two writers never disagree about a unit.
**Cross-repo order for the new fact kind.** propflowai's `building-evidence` reader is a zod union whose `source_kind` is
`['billing','rent_roll','listings']` and drops what it cannot read (`knowledge-intake/building-evidence.ts:20`); land the
additive `pms-setting` kind in propflowai first, then 13a-2, with a contract test in both repos. 13a-2's widened
`REPORT_FIELDS.unit_directory` must update `marketing.test.ts:84`, which pins equality with `MARKETING_FIELDS`. Rent-roll
`ApplicationFee` (fixture `rent_roll.json:50`, read by `application_fee_mode`) is the same value in a second report: `pms-ledger`
evidence that may contest, never a second `pms-setting`.
**One question per building.** M1b disagreements are grouped into one question per building and topic ("AppFolio's unit
settings at X: $1,650 on 4 units, $1,400 on 2; your record: $750 for a 2BR"); the §0 count script reports questions per
building as well as per unit. On a contested deposit the approve surface shows both figures and the manager picks one; the
confirm writes the chosen value (`via: 'review'`) and clears the question; a test asserts approving never silently confirms
the package snapshot's figure.
**Where contests live before part 12.** 9b's M1b read calls `acceptRule` in a read-only contest mode, memoised per request
(inside §3.6's ≤50 ms p95 budget, measured on the 345-unit Situs portfolio). A job run by the 13a-1 sync after each refresh
persists each contested unit as a `question` row on that building's fee topic (`origin: pms-setting`, both figures, the unit
id); the grid row and the Ask tab show those questions from 9b and the daily list delivers them from part 12. The 9b dry run, in audit
mode, also reads ingest's staff evidence through the existing read-back and seeds a question for every M1b unit it contests,
so no unit goes plain on 9b day while a staff statement disagrees. An M1b question's 14-day clock never starts before
delivery, so the sourced quote and the held lease persist until a manager answers.
**Stale value.** On `pms-write` a stale M1b value (older than `UNIT_DIRECTORY_STALE_MS`, `listing-unit-ids.ts:31`, checked at
`:164`) returns "not ready" with reason `pms-setting-stale`, never a fall-through to the tier; on quote surfaces the last good
value renders sourced ("per our records") only if contested, else plain. One unit test for each half.
**Voice snapshot on a unit-value change.** The 13a-1 sync calls `refreshBuildingVoiceCache` for each building whose M1b values
or contest state changed; a census row asserts the snapshot and text carry the same M1b figure for a TEST unit after a
simulated sync (no phone test).
**Per-company go for 9b (judged right).** The 9b merge, after Gera has seen the preview with each company's
`contested-sourced` / `pms-setting-disagrees` counts, is Gera's go for every company then live; the §0 count is re-run
the day 9b merges. A company added later reaches the M1b read and the sourced render only through its rollout PR (part 18).
**Card wording for the M3 exception.** When a confirmed value renders plain against a newer AppFolio value, the grid row and the Ask answer name
both with dates ("confirmed $40 by <name> on Oct 13; AppFolio's unit setting $45 read Nov 4"), so a manager who changed the
fee in AppFolio on purpose answers in one tap.
**Part 1 notes.** 1a's reader inventory is generated at PR time and the PR body records the commit it ran on (Appendix C was
measured on `de46aab7f57`; main has moved). 1b's census includes the approve route's figure (not only its readiness) and the
package preview's deposit per TEST building. 1b's count script already splits M1b counts per unit and per building and parses
comma money strings. Fixture ids are synthetic; the "$45 vs free" bench unit lives on a TEST company, never a real AF id. 1a
and 1b open in parallel off `origin/main`; 1b does not wait for 1a.
**Bench additions.** A stale M1b value on `pms-write` is "not ready", not the tier; an ingest-only staff "free" against an
M1b $45 is sourced plus a question on 9b day; the voice snapshot matches text after a sync.
**Unchanged and confirmed fine at round 10:** the fair-housing pre-send gate; the private-info gate and access codes; the
silent-hand-off keys (3c owns RULE 7.1's line); emergencies (part 5, `union`); the resident wording rail; `mayConfirmMoney`
per the 08-19 ruling; the approve-route re-read; "0.00 is never free" everywhere; migration safety (bare until 9b, the
14-day sweep armed only at part 16, part 11's dark-fee count).
## Appendix F — Corrections to the research files, kept for the record
ADR number is 0084 (not 0078); "company brain" is Fede's Public + Internal, not the opposite thing; hours are split by reader;
the emergency prompt block collapses prose to the floor; no caller clamps the leasing block; ingest reads back the old keys
through four connectors; staff replies rank 0.9 on ingest main; the money judge is post-hoc; the replay baseline is
unmeasured; **AppFolio's unit directory report carries a per-unit `ApplicationFee` and `DefaultDeposit`** (the adapter does
not sync them yet; `types.ts:574` is not a synced field); `loadTurnPropertyContext` exists and
the bench door rebuilds the real request; property creation writes unstamped charge fees; `editedAt` marks a person's edit
without an actor; `src/lib/automations/` does not exist; the paperwork rungs write before a person acts; `mayHearMoney` is a
read gate, not an authority; the four team labels equal a PM by Gera's 08-19 ruling; Situs has 53 live buildings and 20 fee
records, all nine stamps by one Propflow administrator; the package-review approve route is the only lease writer live today.