Clara choke-point design

A proposal for one decision function that every Clara send passes through, with the inventory of send sites reconciled against the PRs already claiming them. For Gera's decision.

30 Oct 2026 · scope: Clara's outbound paths (email, text, call, customer Slack, AppFolio writes) · status: proposed, not started · source: origin/main 02a2004

The decision

Recommendation: Option A, staged. One function, decide(module, building), returns record, hold or send. Every sender and every consumer goes through it, and a send cannot be built without a module. This is the migration the repo already sanctions.

A. One decision function, callers migrated, then the escape hatch closed recommended

Step 1: the existing capabilitiesFor(mode, subject) at src/lib/domain/modules/module-mode.ts:210 is already the one rule. Make it the only decider, and migrate the readers that narrow the mode themselves (about 10 of them).

Step 2: type the send calls so module is required, and migrate the roughly 35 'none' and 'team_alert' sites to their real modules.

Step 3: a lint rule modelled on propflow/no-settings-bypass with a shrink-only exemption file, in the style of scripts/check-settings-reads-allowlist.ts, so raw senders are importable only through the door.

Each step is an old/new-equivalence split under ADR-0144 §6 (docs/adr/0144-the-gatekeeper-is-the-keeper-of-every-gate.md:145), dark until replay shows byte-identical output.

Rejected alternatives B and C

B. A new gatekeeper module that replaces everything in one change

Fewer intermediate states, but it is the parallel-implementation shape ADR-0144 §6 forbids, across about 60 sites. It can't be replayed against real books before it lands. Not recommended.

C. Keep patching sites and rely on sweeps

Four sweeps in four weeks (7 Oct, 26–29 Oct, 28–30 Oct, 30 Oct) each patched sites, and none removed the bypass. Not recommended.

Dependencies still with Fede (not decided here)

D1. Does Preview open an approval request at all? The audit recommends no. Step 1 can ship without it. It lands in the Preview hold front door: holdEscalationEmail and holdEscalationSms.

D2. Does a per-path Live override beat Preview? The audit recommends Preview absolute. It touches path-mode.ts:169-216, twilio/client.ts:1240 and process-envelope.ts:1739. Until D2 is answered, step 1 keeps today's behaviour.

Inventory, reconciled against claimed work

Inventory, reconciled against claimed work

Rows are the audit's five cause groups, not single sites. Status comes from PR titles on origin/main and the open PR list on 30 Oct. "Claimed" means a PR names the sites; "open" means none does.

#CauseWhere (examples)StatusClaimed by
1A hold does work after it opens: the Slack card, the bump reminder, approve routespostHeldEmailForReview, postHeldTextForReview, bumpStaleHolds, bake-gate 'none' arms (:826, :1486), approve-held-pms-write, turnover-gatepartly claimedReviewer email asks the mode: #23805 (merged). Approve a Preview email: #23823 (merged). Collections owner alert: #23813 (merged). Renewal notes and PM page: #23841 (open). Emergency bounce fallback: #23846 (open). Card, bump and PMS-approve routes: open, no PR
2Staff notices declared 'none' or 'team_alert', about 35 sitesReply-release notes, gated-inbound team notice, promises pm-delivery, forward-to-PM, tour confirmations, renewal office notices, renewal_escalate, PM reminders, vendor outcome, move-out mismatch, staff welcome text, unknown-caller notemostly claimedStaff email asks its module: #23809 (merged). Emergency and caller-note locked paths: #23836 (merged). Bounce fallback and renewal call recap: #23838 (merged). Unreadable-mode text hold: #23796 (merged). Staff or vendor text under Supervised: #23850 (open). Vendor-lane disclosure: #23853 (open). Caller-wants-callback type: #23844 (open). Team quiet hours: #23828 (open). Sites with no PR: open
3Gaps in the gatesdecisionRequestModuleOf knows 3 modules (decision-request-hold.ts); supervised-reply-rule.ts; team-notify/engine.ts:527; new-lease-gate.ts; writer-registry.ts:219 has no Preview branchpartly claimedVoice under Supervised: #23806 (merged). PMS writes at Preview and on an unreadable mode: #23827 (open). The rest: open, no PR
4Slack asks to a customer's channel with no mode checkpostOpsEvent action cards, new-lease review alert, fair-housing hold notify, reply-release :2551openNo PR names these. #23847 is Live's Slack switch, a different change.
5A per-path override beats Previewtwilio/client.ts:1240; pathRuled skips previewRefusesSend; process-envelope.ts:1739depends on D2None. This is a decision, not a bug.

Parallel track

The anchor per-building fix for mixed Preview and Supervised buildings (scan-batch.ts:131, build-batch.ts:79-82) does not depend on this design and proceeds on its own PR. Smith's session holds it.

PropFlow Docs