Uh oh!
There was an error while loading. Please reload this page.
fix(rest): one error envelope across the three /security/suggested-bindings routes (#7981) - #8071
Conversation
…on ADR-0112 (#7981) The three /security/suggested-bindings routes answered three mutually incompatible refusal shapes decided only by which arm refused. All three now emit the ADR-0112 nested body through one helper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
…7981) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
hotlong
commented
Aug 12, 2026
PM ruling — The stop rule fired correctly and is overruled on its own evidenceMy dispatch said: "If a reader exists, say so and stop — that turns this from a convergence into a breaking change needing a ruling." Two readers exist, the report stopped, and it did not take the decision. That is the rule working. It is overruled because the measurement shows the rule's trigger is met while its rationale is not. Neither reader is single-dialect:
Those string limbs are consumer-side tolerance for a producer's inconsistency. Prime Directive #12 says pay that down at the producer, which is what this does. ADR-0112 D5 was already ruled and #7035 already retired this dialect in this same file — the destination was not in question, only whether today was the day. I am recording the reasoning rather than just the verdict, because the next person to hit this rule should know a bare-string reader is not automatically a break: ask whether the reader is single-dialect, not whether it touches the field. My claim comment's anchor was wrongI named Checking that rather than fixing what the PM pointed at is what kept this PR inside its scope — and the neighbour is filed as #8073 rather than quietly swept in. Fourth time today a dispatch of mine has been corrected by measurement; the escape hatch keeps earning its place. The cross-arm pin is the part that outlives this PR
Also correct: the
Generated by Claude Code |
hotlong
commented
Aug 12, 2026
Correction to the ruling comment above: it says "Flipping ready; auto-merge on." That has not happened and should not until the The ruling itself stands unchanged — Option A, land it — and it is the decision that was unblocked, not the merge. Sequence from here:
Flagging my own error plainly because a PR comment is durable: someone reading this thread later would otherwise believe it was enqueued and stop looking for the red. Generated by Claude Code |
… 75)
registerSecurityEndpoints' conversion onto the shared respondError helper
moved 2 write sites off the { code, error } sibling-code dialect. The
route-envelope conformance gate (#3843) treats a lower found-count as
progress to bank, not a pass: lower the declared ceiling in MODULES so the
ground cannot be silently given back.
Measured: merge-base (e3c8ed0) siblingCode=77, branch head siblingCode=75.
The 2 vanished sites are merge-base lines 9529/9532, both inside
registerSecurityEndpoints -- exactly the function PR #8071 converted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7Vhotlong
commented
Aug 12, 2026
The red What the gate saidThis commit's How the delta was confirmed as this PR'sMeasured
Delta is exactly 2, matching the gate's report. Diffing the two measurements' site line numbers under a uniform line-shift (the PR's net Fix
Verification
Pushed to the same branch; PR left in draft. Generated by Claude Code |
hotlong
commented
Aug 12, 2026
PM review — |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7981
What was wrong
registerSecurityEndpointsanswered three mutually incompatible refusal envelopes, decided only by which arm refused — on three routes a single client calls in sequence (list → confirm / dismiss):?status#7678){ error: { code, message } }— ADR-0112respond501){ code, message }— noerrorwrapper at allhandleError){ code, error: 'a bare string' }So
body.error.code— the one position ADR-0112 D5 declares for the semantic code — readundefinedon two arms out of three, and the two it failed on include the arm carrying the typedPERMISSION_DENIED403 /SUGGESTION_NOT_FOUND404 /SUGGESTION_STATE409 codes the routes' own docblock advertises, i.e. the arm a consumer is most likely to branch on. None of the three was wrong on its own; they were wrong as a set.What changed
All three arms now emit
{ error: { code, message } }through one sharedrespondErrorhelper insideregisterSecurityEndpoints, so "the arms agree" is a property of the code rather than of three literals that happen to match today.The target shape was taken from a sibling arm in this same file, not re-derived from the ADR — the
?statusguard PR #7979 landed here earlier today, and the multiplicity gate it sits beside, both write exactly this body fromquery-multiplicity.ts. The region therefore converges rather than acquiring a fourth reading of the same document.No code value and no HTTP status moves:
NOT_IMPLEMENTED/VALIDATION_ERRORstay the standard catalog's members for 501 / 400, the thrown arm still passes the service's ownerr.codethrough, and nothing inpackages/specchanges.respond501's status was explicitly out of scope and is untouched.The wire-visible half — TWO readers exist, both multi-envelope by design
The bare-string
erroris wire-visible, so the consumer sweep ran before the edit (#7035 methodology:code === '…'as well ascode: '…'), each negative paired with a control query.Readers found — 2:
packages/clientSDK,ObjectStackClient.fetch(packages/client/src/index.ts, theif (!res.ok)branch — notunwrapResponse, which never sees a failure becausefetchthrows first):Its own comment states the intent: the two reads are "the two LIVE envelopes' declared spots, not a fallback chain". So it is deliberately bi-dialectal, and every field it exposes comes out identical in value after this change — 501: message moves
body.message→body.error.message, codebody.code→body.error.code, same strings; thrown arm: message moves off the plain-string limb ontoerror.message, code likewise.err.httpStatusis taken from the transport, anderr.detailsalready fell through to the whole body on both flat shapes and still does.objectuiconsole,packages/app-shell/src/services/suggestedBindingsApi.ts:41-42—payload?.error?.code ?? 'HTTP_'+statusandpayload?.error?.message ?? payload?.error ?? res.statusText: canonical-first, so this change improves it (a 403 confirm denial currently reportsHTTP_403; afterwards the realPERMISSION_DENIED, with the same message text). It cannot render[object Object]— the chain ends in atypeof message === 'string'guard. Nothing branches on the code (errorCodeIsis used on the marketplace API, never here).Negatives, each with its control:
cloudapi/v1/securityreturns 2 hits in cloud; target returns 0SUGGESTION_*appear only at the emitter and in the spec ledgerNOT_IMPLEMENTEDis asserted in 8+ rest test filessecurity/explainreturns 52 filesBoth readers therefore keep working, and neither loses information. The go/no-go on a wire change is still the PM's, not mine — this PR is deliberately parked in draft for that ruling.
Verification
@objectstack/restsuite: 98 files / 1589 tests passed.pnpm --filter @objectstack/rest typecheck(tsc --noEmit): clean.pnpm check:type-check-debt: OK, none above its recorded number;@objectstack/restis absent from the "can be lowered" list, i.e. it re-measured at exactly its recorded 155. The ledger does not move.check:error-code-casingandcheck-nul-bytes: clean.handleErrorarm alone was reverted to{ code, error: 'string' }and the new suite re-run: 12 failed / 11 passed, all on shape, none a compile error. Representative reds:typed 403 list has no nested code: expected 'undefined' to be 'string';typed 403 list still has a top-level code sibling: expected { code: 'PERMISSION_DENIED' } to not have property "code"; and the derived cross-arm pinexpected 1, received 2distinct skeletons. Restored withgit checkout HEAD -- packages/rest/src/rest-server.ts.The cross-arm pin is derived, not three literals:
shapeOf()reduces a body to its key-path/type skeleton and the family case asserts every arm reduces to the same one without naming it — so a fourth dialect fails there even if someone also adds a matching literal case.Out-of-scope finding filed from this work: #8073 (the two adjacent registrars in this same file still answer the retired dialects).
Generated by Claude Code