Twenty-three items shipped between 19–21 August. Every one merged, passed CI and cleared a blocking review. None has been looked at by anyone outside engineering. This page says what is true now — the tracker rows still describe what was broken, in the present tense, because that is when they were written.
“Done” here means merged and green
It does not mean confirmed correct by someone who knows what should have happened. For much of this work those are the same thing. For the derived Stage column and the parsers, they are not — and those sections say so.
Step zero — is the customer config stamped?
The deterministic parsers run on every property with an email integration, but the customer-specific half — which counsel, which assistance programmes, how unit numbers are written, the client id, the landlord entity on a court caption — lives in Property.collectionsCorrespondents.
Absent config is fail-closed: no parses at all. Not wrong parses — none. And that is deliberate, because a claimed parse replaces the model’s unit list, empty included. One property’s unit pattern applied to another’s mail deletes a correct unit reference rather than declining to add one. Lossy, not inert.
scripts/set-collections-correspondents.ts is dry-run by default — run it without --apply and it prints the current config and writes nothing. I believe this was stamped, but “I believe” is the standard this page exists to avoid. Treat it as unconfirmed until the dry run prints it back.
What you can click
/collections — renewals-shaped: aged buckets, then one table with Requires action · In progress · Resolved. Carries the zero-balance ratchet (~$43,684 of already-paid debt was frozen on the page) and the new derived Stage column.
The tenant collections panel — aged strip, lifecycle rail, and a chronology interleaving four sources: system, operator, law firm, assistance programme. It exists because those signals previously had zero readers — a defective demand or an assistance approval existed only as unread email.
What runs where you cannot see it
The auto-reply guard. Highest value per line of code in the phase. The firm’s ticketing leg was being classified as bulk mail and deleted — including an April “Defective Demand — Unable to File”. Zero signals had ever been extracted from that queue in four months. The fix added one domain to an allowlist that had one domain in it.
The parsers. 844 claims over 15,112 messages: 100% flow precision, 86.7% recall. The precision figure deserves explanation rather than applause — these are deterministic rules that refuse anything they do not recognise. Recall is the number with room in it: roughly one real message in eight is not claimed.
Hearing “stop the process”. 20 recorded “cancel the eviction, assistance is coming” messages, 4 of them after a writ was already at the sheriff. Nothing could hear any of them before. It now catches 17 of 21, and on all four post-writ units the approval queue refuses to send. Deliberately generous about phrasing (a question or a condition still counts) because the errors are not symmetric — a false positive means we decline to text someone about money; a false negative means we text someone whose cheque already cleared. A resident cannot trigger it; only counterparties can.
Payment plans. AppFolio’s column was read off the wire and thrown away, so anyone on an arrangement was invisible and fully dunnable. The catch: the column is money in one report and a flag in another — reading only one would have graded every real plan as absent, i.e. looked like it worked and been wrong every time.
Category drift. A $10,000 assistance payment confirmation was filed as a vendor cost; filtering on delinquency/eviction lost ~a quarter of collections traffic. A benchmarked prompt upgrade that scored better on clean gold was measured as a regression on real mail and deliberately not shipped.
The safety work
A cross-tenant path closed on the AppFolio trusted-sender gate — unit-number-only matching could land a notice on the wrong property’s resident.
A drift guard over 3,102 files and nine identifiers makes compiling a customer’s constants into shared source impossible. It found the exact hole it exists to close: a comment-stripper that made 546 lines invisible while reporting them as inspected.
Resident names out of source. The sweep kept widening — 32 distinct real people across seven directories, including some earlier passes called clean. Sharpest find: a fixture with two siblings of one household and two real phone numbers, written surname-first, which is why three prior sweeps missed it.
What “done” does not mean
The Stage column is 82.9% right — 63 of 76 measured units
Stage is derived, from the same lifecycle graph the tenant rail draws (one derivation, not two that can disagree). Of the 13 disagreements: 6 are a declared no-source floor the page states on screen, 7 are an office-channel blind spot — things that happened by phone or in person.
Three of those seven run in the dangerous direction: an active legal posture shown for an account whose assistance had actually cleared off-channel. That is the specific thing to look for.
Three filter options can never match anything, each labelled with why: Demand ready (no source yet), Evicted (no source yet), Fee eligible (no rows land here). Not bugs — the vocabulary is complete while the plumbing is not, stated rather than hidden. Whether an option that can never match should be shown at all is a product call nobody has made.
Four of the highest-value emails are pointers to data we do not have. The delinquency report, eviction filing report, results of cases and writs at the sheriff carry their per-resident detail inside PDF/XLSX attachments. We captured filenames, not contents.
32 of 878 phone numbers in source still match the corpus. Reported as the next worklist rather than quietly fixed — separating company lines from personal ones needs case-by-case judgement.
Nothing writes a stage automatically yet. The ADR permitting it was accepted on the 21st. Accepted is not built: automatic writes are permitted only on a case a human has explicitly linked, and that link did not exist anywhere. So the first piece is the link, not the write. It is in review now.
The shape of that design is what makes it safe, and it is worth stating plainly: anything that makes the product do less to a resident may happen automatically; anything that removes a restraint or moves an account further up the legal ladder is a proposal a human confirms one at a time; and an unlinked case writes nothing but still alerts — because the message most likely to land on a case nobody linked is the assistance approval arriving after a writ. Gating the alarm on the link would make the worst case the silent one.
What I would actually test
In this order. Tick as you go — these save for everyone.
Check
Why it matters
Confirm the config is stampedDry-run set-collections-correspondents.ts.
Everything downstream is inert without it — and inert looks identical to working-but-quiet.
Find an account whose stage you knowOn /collections. Is the Stage right?
This is the 82.9% made concrete. You are the only available instrument for it.
Hunt the risky-direction caseActive legal posture where assistance actually came through.
Three exist in the measured set. Finding one is the known failure reproducing, not a surprise.
Read a chronology with law-firm historyOn the tenant page.
Not “does it render” — does the story match what you remember happening?
Check someone on a payment planConfirm they are not being chased.
The naive version of this fix would have looked right and been wrong about every case.
Decide on the three impossible filter optionsShow with a reason, or do not show?
A product judgement, not an engineering one. Nobody has made it.
What is left in Phase One
The demand builder (Fede) — open PR, merge conflicts.
Signals → stage write path — the four-piece sequence above, first piece in review.
Everything else in Phase One is merged and running. Whether it is right is the question this page hands to you.