Uh oh!
There was an error while loading. Please reload this page.
refactor(rest): extract the ADR-0112 error/fault-classification prologue out of rest-server.ts - #8887
Conversation
…gue out of rest-server.ts The module-level code ahead of `RestServer` is ADR-0112's concern — how a thrown thing becomes an HTTP answer — not the REST server's. It reads no class state and lived in that file for historical rather than structural reasons. Moved verbatim to `packages/rest/src/error-response.ts`: mapDataError, resolveErrorResponse, DATA_STORE_FAULT, UNCLASSIFIED_FAULT, isScriptFaultMessage, declaredHttpStatus, truncateClientMessage, sendError, sendFieldVisibilityFault, logWithheldServerFault, isExpectedDataStatus, isExpectedQueryRejection, isExpectedRouteError, logUnexpectedRouteError, handleRouteError, droppedFieldsHeaderValue, applyDroppedFieldsHeader, missingRelationIsObject, plus their module constants. The console shim both files need moved to `packages/rest/src/log.ts` rather than being copied. No behaviour change: every moved line is byte-identical, the only edits being six `export` prefixes and mapDataError's docblock reattached to the function it describes (it had drifted above an unrelated import). `mapDataError` and `zodIssuesToFields` are still exported from `./rest-server.js` unchanged. Not the ADR-0076 D11 decomposition, which the 2026-08-15 ruling on #5949 closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
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:
|
…oss the prologue extraction
Two declarations, both made necessary by the file move in the previous commit and
neither changing what any gate ENFORCES.
check-route-envelope.mjs: `error-response.ts` emits REST bodies (the sendError /
handleRouteError doors) but sits outside the `*-routes.ts` discovery convention,
so it would have been audited by nothing. Named in OFF_CONVENTION_MODULES with a
`{ responses: 2, ok: 0, err: 0 }` entry measured at extraction. rest-server.ts's
own ratchet is untouched and still reads stringError 44 / siblingCode 69.
check-durability-degradation-log-level.mjs: the two saveMetaItem route catches
qualified on main as `loud (error@120 via handleRouteError())` — the gate followed
handleRouteError as a SAME-FILE helper down to logError. Moving that function to
its own module made the inference unavailable (helper resolution is file-scoped by
construction), so the gate lost visibility while the behaviour did not move a line.
Declared handleRouteError in FAILURE_PROPAGATION_CALLEES as `via: 'effect'`: it
answers the caller unconditionally, which is a STRICTER claim than the loudness it
replaces, since it logs only for unexpected errors. Seam count is 25 before and
after — nothing left the gate's view.
No baseline touched, no ratchet raised, no threshold moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53TnUh oh!
There was an error while loading. Please reload this page.
Fixes#8850
Fixes#8884
The module-level code ahead of
RestServeris ADR-0112's concern — how a thrown thing becomes an HTTP answer — not the REST server's. It reads no class state and sat in that file for historical rather than structural reasons. Moved topackages/rest/src/error-response.ts.⛔ Not the ADR-0076 D11 decomposition, which the maintainer ruling of 2026-08-15 on #5949 closed (option B). No
registerXxxEndpointsmethod moved, no line inside theRestServerclass body was touched, and no line count is offered as justification.Both dispatch premises verified by symbol, not by line number
"Module-level functions only, ending before the first class declaration." Holds. The first class is
export class RestServer— still at line 1981 oncc5c07b80, but re-derived by symbol rather than inherited. Everything above it is module-level."Zero coupling to the
RestServerclass." Holds, and it was checked rather than assumed. No moved function reads class state, and the coupling runs strictly the other way: the class calls seven of them (sendError5,sendFieldVisibilityFault5,handleRouteError36,logUnexpectedRouteError7,isExpectedRouteError2,applyDroppedFieldsHeader2,mapDataError9), and those seven are the module's entire export surface beyondmapDataError. The other eleven symbols had zero call sites in the class and stay module-private, exactly as they were.One correction to the card, offered because it is measurable and does not change the verdict: the extracted region is 404 non-comment code lines, not the ~732 the card estimated. The region was fixed by the card's own symbol list, not by that number.
What moved
The card's eighteen symbols plus their module constants (
CLIENT_MESSAGE_MAX,NATIVE_ERROR_NAME_RE,RELATION_DOES_NOT_EXIST), as one contiguous block. The console shim both files need went topackages/rest/src/log.tsrather than being copied — the alternative was two definitions of the same two lines.droppedFieldsHeaderValue/applyDroppedFieldsHeaderare the one pair in that list that is not error classification: they decorate a successful write (#3431). They travelled with the block because they are the rest of "what this boundary puts on the wire", and splitting them out would have meant a third module for two functions. Flagged here so the choice is reviewed rather than discovered.Evidence that this is a move
exportprefixes and one comment relocation, below.rest-server.ts's diff is 16 insertions / 1296 deletions across 4 hunks — three import-block edits and one re-export line. Nothing else in the file was reformatted or re-ordered (CharacterizeregisterMetadataEndpointsInner(2,328 lines, 20% of rest-server.ts) — read and propose, do not split blind #8851 is reading this file concurrently).rest-server.tsexports the same 13 names onorigin/mainand on this branch, none added, none removed —mapDataErrorand thezodIssuesToFieldsre-export included.packages/rest/src/index.tsis byte-identical. The three test files thatimport { mapDataError } from './rest-server.js'and the one that importszodIssuesToFieldsare untouched.The one deliberate non-identical edit:
mapDataError's docblock had drifted away from its function — it was stranded above an unrelatedimportroughly 300 lines up, presumably left behind whenzodIssuesToFieldsmoved to spec in #8124, andmapDataErroritself carried no docblock at all. It is reattached to the function it describes. Comment only; recorded here so a reviewer does not have to find it. The other pre-existing docblock drift in the region (missingRelationIsObject's block sitting abovedeclaredHttpStatus) was deliberately left as-is — both moved to the same file in the same relative order, so nothing about it got worse, and repairing it would have added a second unreviewable comment edit.Two gate declarations — second commit
Both are consequences of the move, and neither changes what any gate enforces. No baseline touched, no ratchet raised, no threshold moved.
check-route-envelope.mjs— closing #8884error-response.tsemits REST bodies (thesendError/handleRouteErrordoors) but sits outside the*-routes.tsdiscovery convention, so it would have been audited by nothing — the exact cost the checker's own header records paying once already forrest-server.ts. Named inOFF_CONVENTION_MODULESplus a{ responses: 2, ok: 0, err: 0 }entry, measured at extraction rather than chosen.responses: 2is the first non-zero write-site count in that table. It is pinned rather than waived because this module is the opposite ofrest-server.tson the axis that made that filedialectOnly: its write sites are the two doors, so the count is structurally stable and moves only if a third door appears — which is precisely what deserves review. It carries aratchetfor the same reasonrest-server.tsdoes:responses: 2is outstanding drift by the table's own definition, with #7035 option 1 as its end state.0 / 0on both dialects for what it is: both sites writeresolved.body, an identifier, and the dialect counters only see object literals — so the flat shapesmapDataErrorbuilds are not counted here, and were not counted inrest-server.tsbefore the move either. What the zeros pin is that no new literal body appears at this boundary unnoticed.rest-server.ts's own ratchet is untouched: stringError 44 / siblingCode 69, before and after.check-durability-degradation-log-level.mjs— a visibility regression, diagnosed before it was fixedCI went red on this gate at the first commit: two
saveMetaItemroute catches "degrade quietly". The diagnosis, in order:Is
maingreen? Yes — measured on a cleanorigin/mainworktree atcc5c07b80: exit 0, 25 seams. So this PR caused it.How? Not assumed — read off the gate's own
--listoutput onmain:error@120waslogError. The gate reached it by followinghandleRouteErroras a same-file helper (collectLoggedLevelsresolves callees through a per-filefunctionBodiesmap). Moving that function to its own module made the inference unavailable. The gate's visibility regressed; the behaviour did not move a line.The fix is the declaration the gate's own failure message prescribes, not a log:
handleRouteErroris declared inFAILURE_PROPAGATION_CALLEESasvia: 'effect'. It resolves the error once and then always writesres.status(resolved.status).json(resolved.body)— no branch returns without answering the caller — and it is the only definition of that name in the repo, so the callee-scoped declaration is unambiguous.Worth stating plainly: this classification is stricter than the one it replaces.
handleRouteErrorlogs only whenisExpectedRouteError()is false, so "unconditionally loud" was always slightly generous — a durability failure mapping to an expected status printed nothing. What is unconditional is the answer to the caller, and that is what the entry claims.catchDeliversFailure()still proves every path out of the catch reaches it. Seam count is 25 onmainand 25 here — nothing left the gate's view.Verification — all at final head
294788317Union re-derived after the final commit with
node scripts/pm/dispatch-gates.mjsover all five changed paths. It now namescheck:durability-log-level(it matches on the script path, which the first derivation could not see) and matchescheck:route-envelopethrougherror-response.tsitself.pnpm --filter '@objectstack/rest^...' build(closure)pnpm --filter '@objectstack/rest' typecheckpnpm --filter '@objectstack/rest' testpnpm --filter '@objectstack/rest' buildpnpm check:route-enveloperest-server.tsunmoved at 44 / 69pnpm check:durability-log-levelmainpnpm check:authz-resolverpnpm check:cross-package-test-inputspnpm check:nul-bytesReverse verification — for a move, the meaningful question is whether the suites exercise the new module rather than a stale copy. Ablated
UNCLASSIFIED_FAULT's status 500 → 599 inerror-response.tsand predicted RED. Observed RED across five suites (rest-5xx-status-passthrough,rest-expected-error-logging,rest-hook-refusal-status-passthrough,rest-hook-script-fault-envelope,rest-unclassified-fault-status). Restored from the commit and verified byte-identity bygit hash-objectagainstHEAD, with a cleangit status --porcelain.Suites covering the moved code (19 in
packages/rest):rest-unclassified-fault-status,rest-unknown-object-heuristic,rest-5xx-status-passthrough,rest-5xx-message-sanitization,rest-4xx-message-truncation,rest-hook-refusal-status-passthrough,rest-hook-script-fault-envelope,rest-expected-error-logging,rest-meta-outage-vs-miss,rest-unique-violation-dialects,external-write-forbidden-envelope,package-door-5xx-message-sanitization,rest-dropped-fields,rest-update-path-id-not-a-dropped-field,zod-union-fields,rest.test.ts, and three more.Honest gap, reported rather than papered over:
sendFieldVisibilityFault— the ADR-0106 D6 tier-3 refusal — has no test anywhere in the repo. Recorded in #8885, which is out of scope here and stays open.Finding filed and left open
#8885 remains open and is not addressed by this PR:
FIELD_VISIBILITY_UNRESOLVEDandRECORD_NOT_ACCESSIBLEare put on the wire but appear in neitherStandardErrorCodenorERROR_CODE_LEDGER, and the first has zero coverage while being documented to operators inproduction-readiness.mdx. Blocked-by #8846, and a wire-code question rather than a move.No changeset
skip-changesetapplied: no user-facing published change. No export added, removed or narrowed; no status, code, body, header or log line differs for any caller. The two gate edits are build-time declarations that ship to nobody.Generated by Claude Code