Date. 2026-08-14 · Item. [B-95] (build list v0.73) — the commit ceremony's catch block manufactures a false cause.
Mode. READ-ONLY. Nothing built, nothing fixed. Surface read at 00ed2f2, engine at f011c80 (both = origin/main).
Ordered questions. (1) How many catch blocks manufacture a cause; (2) what the server sends that a client could branch on; (3) whether any guard asserts a rendered error matches the server's cause.
Twenty-three catch blocks in the surface set a user-facing message. Classified against the ruled distinction — naming a cause vs claiming failure without one:
Cause-manufacturing (the defect class): 4 sites, worst first.
| # | Site | What it asserts | What can actually come back |
|---|------|-----------------|------------------------------|
| 1 | CommitCeremony.tsx:52-60 | "Couldn't commit — try your passkey again." for EVERY non-401 ApiError | 409 not_converged with the true cause in the body — the two-day site |
| 2 | useConversation.ts:448 | "Couldn't reach the Companion." for every non-401 failure | The converse route answers 4xx/5xx WITH causes — e.g. 503 no_credential (a stable code) renders as unreachability. The server was reached; the message says it wasn't. The app's main artery. |
| 3 | ChatView.tsx:535 | Same "Couldn't reach the Companion," creation-chat surface | Same class as #2 |
| 4 | auth/page.tsx:73 | "Sign-in failed (N). Check the person id." for any status | Dev-auth only — unreachable in production (CR-187); lowest stakes |
Honest by construction (no action needed), for contrast: signin/page.tsx and claim/page.tsx — their catch wraps ONLY the WebAuthn ceremony call and branches on WebAuthnUnavailableError, so "passkey failed" is true where it renders. FallbackAffordance.tsx:67 and BatchFallbackAffordance.tsx:84-91 render the server's detail — the in-repo precedent the B-95 fix can follow. WorkspaceSelector.tsx:86 branches on status (assumes 409 = duplicate name — near-honest, pattern-adjacent).
Cause-discarding but not cause-inventing (second tier, ~9 sites): InboxView's three ("Couldn't record your approval / decline / dismiss. Try again."), useEngagementUpload, DocumentCreateFlow ×2, create-engagement/page.tsx, useVoiceListening, signin-flow's generic. These lose the server's message but invent no cause — honest ignorance under the ruled distinction. Reported, not defects of the B-95 class.
Per the Operator's instruction: this is a finding, not a licence. No general error-rendering mechanism is proposed. The fix target is site 1; sites 2–4 are filed-adjacent facts for the Operator to rule on (a candidate follow-on item covering the two "couldn't reach" sites, which share one sentence and one shape).
ApiError already carries the parsed response body (detail field, api.ts:115) — every catch site holds the server's cause at the moment it discards it.
Two server vocabularies exist:
api/errors.py): body shape {"error": "<code>", "message": "<prose>"}. not_converged is one of them. So is no_credential (503) on the converse path.HTTPExceptions: body shape {"detail": "<string>"} — prose, BUT the commit path's three 401s carry stable machine prefixes: step_up_required, commit_challenge_invalid: …, commit_attestation_failed: ….
Conclusion: the B-95 fix branches on err.detail.error === "not_converged" — a stable code, no prose parsing, no server-side prerequisite. The CR is surface-only as drafted.
One wrinkle found for the CR, named now: api() redirects ALL 401s to /signin (except bypassUnauthorizedRedirect callers), and CommitCeremony deliberately shows nothing on 401. A genuine failed passkey tap (401 commit_attestation_failed) therefore doesn't show "try your passkey again" — it bounces the Operator to sign-in mid-ceremony. The one case the current message would be TRUE for is the one case it never renders in. The fix's 401 handling needs bypassUnauthorizedRedirect on the instantiate call (or code-aware handling in api()) so a failed tap can be told apart from an expired session — a scope decision for the CR draft, flagged not decided.
Nothing anywhere asserts that a rendered error corresponds to the server's stated cause. CommitCeremony.test.tsx's only error-path test mocks a generic Error("user cancelled") — no ApiError, no status, no body, and it can only ever exercise the fallback message. No surface test references not_converged or reads err.detail. The suites verify that an error renders (not-silent), never that the right error renders — exactly the gap the standing note's third revision names: failure messages are composed claims, and no fence composes them against the server's cause. The B-95 fix's ruled test (drive the 409 body through the component; assert the passkey sentence does NOT render) is the first such guard; whether a sweep-wide fence belongs anywhere is the Operator's call, not this note's.
DUNIN7 — Done In Seven LLC — Miami, Florida — B-95 scoping note — v0.1 — 2026-08-14 Four sites manufacture a cause; the body already carries the truth; no guard anywhere checks the correspondence. Fix the one; the rest are findings.