Uh oh!
There was an error while loading. Please reload this page.
fix(rest): type the REST door's author-side error responder to the closed ErrorCode union - #9222
Merged
Merged
Conversation
…osed ErrorCode union The REST door exported `sendError(res, error: any, object?)` — a name that collided with the strict shared writer `sendError(res, status, code: ErrorCode, message)` in `@objectstack/types`. A cross-door parity note cited the strict one's closed parameter as the reason the REST door could not put an unregistered code on the wire; route modules used the loose one. The door read as closed while any handler could emit a fresh unregistered code silently. Split the two responsibilities by name and type the author-side one: - `sendThrownError` (renamed) keeps `error: any` deliberately — narrowing what a CAUGHT error may carry is an ADR-0112 public-contract decision, not an internal typing one. - `sendDeclaredFault` is new: `code: ErrorCode`, for refusals this repo DECIDES. It delegates to `sendThrownError`, so the wire answer is byte-identical — same #5437 5xx prose-withholding, same #5423 4xx truncation, same flat `{ error, code }` dialect (#7035 is untouched). All five author-declared emissions now route through it and are checked by `tsc --noEmit`. `check:dispatcher-error-vocabulary` gains a structural door-typing half so the narrowing cannot be widened back silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) reference the affected code — list omitted above 15 rows. Re-derive: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.
|
This was referenced Aug 17, 2026
os-project-manager
marked this pull request as ready for review
August 17, 2026 03:23
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9098
The vocabulary half of this card was discharged by #8885 (PR #9177) and is not re-done here: no code is registered, the ledger is untouched. Verified on
origin/main@88f97d528before starting —FIELD_VISIBILITY_UNRESOLVEDhas its ledger row and its tier-3 emission is pinned. What was still standing is the structural half, and that is all this PR does.The hole
packages/rest/src/error-response.tsexportedsendError(res, error: any, object?). That name collided with the strict shared writer in@objectstack/types—sendError(res, status, code: ErrorCode, message). The collision was not cosmetic. The cross-door parity note inpackages/runtimecited "sendError's closedErrorCodeparameter" as the reason the REST door could not put an unregistered code on the wire. That sentence names a real, strict function and draws a false conclusion from it: route modules reached for the other one. The door read as closed while any handler could emit a fresh unregistered string silently.Measured evidence that prose had already failed here:
rest-server.tsimports the shared writer aliased assendEnvelopeError, and carried four separate hand-written comments each warning the reader off the same conflation.The fix — split the two responsibilities by name, type the author-side one
sendThrownError(renamed fromsendError) — the CLASSIFICATION door.error: anyis kept deliberately: this parameter binds a caught value, and narrowing what a thrown error may carry is what the card flags as the fork.sendDeclaredFault(new) — the AUTHOR-side door,code: ErrorCode, for refusals this repo decides. It delegates tosendThrownError, so the wire answer is byte-identical.All five author-declared emissions now route through it:
sendFieldVisibilityFault(503) and the fourUNAUTHENTICATED/PERMISSION_DENIEDliterals inrest-server.ts. Every one sits in compiled source, sotsc --noEmitchecks them.The stale citation in
packages/runtime/src/package-door-error-parity.test.tsis corrected, and theCodeDoornote indispatcher-error-vocabulary.tsnow records the split. That citation is what made the hole invisible; leaving it would leave the next reader the same trap.Fork clause — the public contract is NOT narrowed
Deliberately untouched:
resolveErrorResponse's passthrough. A thrown error carrying an unregisteredcodestill reaches the wire exactly as before. Narrowing that is an ADR-0112 public-contract decision (the dispatcher's equivalent needed a maintainer ruling); this PR narrows only what an author in this repo can hardcode, which is internal typing. No behaviour a client can observe changes.Why not the two shapes the card offered, verbatim
The card's first shape — route
sendFieldVisibilityFaultthrough the closed-ErrorCoderesponder in@objectstack/types— cannot be taken as written, and this is the main measurement of the round:{ success: false, error: { code, message } }. The REST door emits the flat{ error, code }. Switching moves the envelope POSITION — which is finding finding:rest-server.ts里三个相邻/metahandler 的错误信封是三种不同形状,其中两种不符合 ADR-0112 #7035, deliberately out of scope and still open.rest-field-visibility-fault-envelope.test.tspin that rest emits two error codes outside the closed ADR-0112 vocabulary — FIELD_VISIBILITY_UNRESOLVED has no ledger entry and no test anywhere #8885 just landed.So the vocabulary narrowing and the dialect move are two separate decisions, and this PR takes only the first.
sendDeclaredFaultgets the closed union without touching the dialect.The card's second shape (extend the #8087 gate) is taken, as the anti-regression half rather than the fix itself.
Gate extension
check:dispatcher-error-vocabularygains a structural door-typing half. The vocabulary scan answers "is this code registered"; it cannot answer "could an unregistered one be written here tomorrow". Three assertions, each pinned in both directions by--self-test:sendDeclaredFaultexists and narrowscodetoErrorCode;sendErroragain;anydoor.Assertion 3 is the one that earns its keep:
FIELD_VISIBILITY_UNRESOLVEDis registered now, so the vocabulary scan is silent about it — yet reverting only its routing still reddens the gate. An anchor the gate cannot find is a finding, never a pass.Verification — all at
592bd5e30(final commit, clean tree)pnpm --filter '@objectstack/rest^...' buildpnpm --filter @objectstack/rest typecheckpnpm --filter @objectstack/rest testpnpm --filter @objectstack/runtime typecheckpnpm --filter @objectstack/runtime testcheck:dispatcher-error-vocabulary(+--self-test)check:route-envelope·check:authz-resolver·check:cross-package-test-inputscheck:nul-bytes·check:error-code-casingcheck:engine-double-contract·check:where-matcher·check:query-options-erasurecheck:type-check-coverage·check:type-check-debt --re-measurescripts/docs-audit/check-affected-docs.mjsFamilies derived with
node scripts/pm/dispatch-gates.mjsagainst the actual changed paths. Thetype-check-debtratchet refuses without a built workspace closure, so the full closure was built first and it is genuinely measured, not skipped.Reverse verification — direction decided before running, and all three landed as predicted (red):
sendDeclaredFaultcall site:error-response.ts(1137,9): error TS2820: Type '"FIELD_VISIBILITY_UNRESOLVED_X"' is not assignable to type 'ErrorCode'. Did you mean '"FIELD_VISIBILITY_UNRESOLVED"'?code: ErrorCodetocode: string— gate exit 1, names the widening;sendThrownError— gate exit 1, names the bypass.Each was run from the committed state and restored with
git checkout; the tree was confirmed clean afterwards.Changeset
None —
skip-changesetlabel applied. The rename is entirely package-internal:packages/restpublishes a single.entry built fromsrc/index.ts, which exports neithersendErrornor anything else fromerror-response.ts. Wire answers are byte-identical. Nothing a consumer can observe changes.Out of scope, untouched
rest-server.ts里三个相邻/metahandler 的错误信封是三种不同形状,其中两种不符合 ADR-0112 #7035 (envelope position, flat dialect) remains open and is deliberately not absorbed.Generated by Claude Code