| ID | Scenario | Result |
|---|---|---|
| HH-01 | Solo inquirer → applies → approved | PASS |
| HH-02 | Couple joint application, zero annotations (absorbed co-applicant) | PASS promoted |
| HH-03 | Primary + co-signer guest card, wrong hand-typed annotation | PASS |
| HH-04 | Primary + 2 co-signers + non-applying occupant | PASS promoted |
| HH-05 | On-behalf-of inquiry | PASS promoted |
| HH-06 | Two solo inquiries → one household (merge) | PASS promoted |
| HH-07 | Dual-role: co-applicant on deal A, primary of own deal B | PASS |
| HH-08 | Adversarial: no group id / no primary / duplicate guest cards | PASS |
| HH-09 | Reminder dedup on a 3-member household | PASS |
| HH-10 | List / tile / dashboard count divergence | PASS |
| HH-11 | One group id, two units → resolved by recency, loser on the audit | PASS new |
Plus: 10-trace real-conversion replay, reminder-path parity, manifest audit — 157 tests green across the household layer + harness; 1,618 across src/lib/domain/leasing. The harness projection drives the real production writers and store, not a parallel reimplementation. Every promoted check was mutation-tested (broken deliberately, confirmed red, restored). Not run: the prod-credential containment sweep (needs approved credentials; the in-suite containment assertions pass and nothing writes to a real table).
startedAt, silently re-electing the face of any tied household every sync tick.Validated against real data the same afternoon: the one "unit mismatch" in Camellia's groups was a stale guest-card unit (toured one unit, applied for another) — resolved correctly by rule 1.
| Item | Finding | Backfill action |
|---|---|---|
59 legacy PROP#/PROSPECT# rows | All matched deterministically by id (legacy key contains the authoritative row's id). 7 carry stale stages; authoritative row newer in every case. | 57 retire-links via the audited path (2 members of held groups sequence with their group) |
| Group 2 (3 solo cards) | Clean structural merge; absorbed members have zero conversations, zero sagas. | Merge → 1 household |
| Group 1 (apps 16 + 18) | Real joint group per live AppFolio: same unit 308, identical lease dates, approved the same minute, tenants converted. Dry-run "contradictions" were artifacts. af.coApplications absent only because the #5371 backfill skipped this group. | Merge-safe; timing per Decision 3 |
| Group 3 (apps 33 + 34) | Both applications on unit 405 (live AppFolio); the "101" was guest-card drift. Both Decision Pending with an active email thread. App 34's row carries a wrong co-signer flag — AppFolio has no co-signer annotation; they are peer co-applicants. | Merge-safe; role corrected to co_applicant; timing per Decision 3 |
| Renewal-saga exposure | Zero sagas on all 64 affected persons — no 2026-06-24 incident class exposure. | — |
Residual check folded into the backfill's own verification: confirm each group row's personId maps to a distinct applicant (differing email domains on both pairs already point that way). New bug class carded from this work: guest-card→application unit drift on ProspectInquiry.targetUnitId (masked when both members drift identically).
The gate this PR was explicitly held for is 11/0/0 against the real writers, with mutation-tested checks and the real-data unit rulings already encoded. Go-forward writes start producing households; Camellia data flows through the new fold on its next sync. Post-deploy verification (DB first, UI screenshots, Sentry sweep over the window) runs immediately.
Nothing concrete pending; the bot review + CI run on the current head either way. Costs a day against the cleanup that depends on it.
Fede's ruling, after the data showed same-tier divergence has never occurred in prod (all 15 Camellia applications agree on unit within their groups): the hold was over-engineering. The rule is now two lines — strongest tier wins (lease > application > tour); recency breaks every remaining tie. No hold arm in unit resolution at all (net −244 lines). The safety is HouseholdMergeAudit.overruledUnits: whatever the rule beat is recorded, so a wrong pick is visible and reversible from the audit rather than prevented by machinery for a case never observed. The identity hold (bare uncorroborated group id — are these even the same deal's people) is unchanged and now pinned by an end-to-end test.
Run now: group 2 merge + group 1 merge (its deal is concluded — both tenants converted, lease signed; nothing in-flight to strand) + all retire-links. Group 3 waits for its Decision-Pending window to close (or your explicit go), then merges with the corrected co_applicant role. Every write through the audited merge path; nothing deleted.
A household merge writes no conversation state, so the active-thread risk is indirect (UI regrouping mid-conversation). Defensible, but the deal is literally being decided right now.
Most conservative; leaves both verified-safe groups as known duplicates.
Superseded 2026-09-08: grouping is fully automatic; the "link?" suggestion described below was deleted. Current state lives on the onboarding plan page.
Two people applying together for the same unit — a couple, say — kept showing up as two separate prospects. That's because 31 different spots in the code were each deciding "same household or not" on their own, and 5 different screens (the prospect list, the funnel, the dashboard, the owner report, and reminders) did that math differently. One real couple could look like 2 pending applications on the dashboard, 1 signed lease on the owner report, and 1 unit on the trend line — three different answers to the same question.
An August 5 pull request marked "proof of concept — do not merge" got merged anyway. The piece that was supposed to read the grouping back out was left for later, with a one-line comment saying it could go in "when the other piece lands" — but that other piece had already landed 7 hours earlier. Nobody owned finishing the job, so later changes on August 17, 30, and 31 worked around the gap instead of closing it.
The engine, the suggestion feature, and a self-heal fix for a bug where application received-dates were silently never being saved (a gap in what fields were allowed to be written). Checked live: Camellia's unit-120 couple now shows correctly as a linked suggestion, still as two separate rows. The Willows — 450 records, zero changed by the new grouping, so no regression there. Sentry — zero new errors. Camellia's applications went from 0 to all 20 carrying a received date once the first sync ran.
Fede's ruling: PM application-review reminders are one per household while any member is still in review, and none once the whole household is decided. Reason: the Aug 5 household code was never wired into reminders — the live path ran one timer per applicant and suppressed "redundant" co-signers, so at Camellia unit 403 both co-signers got suppressed because the primary was already decided, leaving zero reminders for a still-open deal. Shipped and verified live: Camellia went from 3 to 6 reminders (the silent deal now gets one), The Willows stayed at 5. Old per-applicant timers retired themselves; the suppressor was deleted and fenced against regrowth.
Engineering refs: #7327.
The "Source" column now shows "AppFolio" for leads that came from the property system and have no marketing source of their own. This reverses an earlier decision that said the property system should never show up as a source — reversed because Fede asked for it directly.
Tenant-side co-tenancy (as opposed to prospect-side) still groups people by unit and lease-start date instead of the new Household record — that's the next thing to fold in. Separately, about 273 old workflow jobs are still running left over from the September 3 platform cutover; that's been flagged to whoever owns that cutover, not something this project needs to fix.
Engineering refs: #7299, #7312, #7316, #7271, #7275.
ProspectInquiry.targetUnitId) — mitigated at the household layer by ruling 1; source fix to be carded separately.Prepared Aug 4, 2026. Sources: harness run on fede/household-record (PR #5377), read-only Camellia DynamoDB dry-run, live AppFolio Data API pull (same day). No customer names appear in this doc or any referenced code; rows are referenced by ids.