docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary - #18191
Merged
claude[bot] merged 5 commits intoSep 14, 2026
Conversation
…page Envelope section, auth codes and the validation codes, each measured against its writer. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…andling page Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…and security examples Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 14, 2026
claude
Bot
deleted the
claude/issue-15632-error-envelope-both-families
branch
September 14, 2026 15:10
This was referenced Sep 14, 2026
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 free
to 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 #15632
Clause-②: no
Docs-only. No
packages/**byte changes, no runtime surface widened, no new authorable key — the whole diff is one hand-written MDX page undercontent/docs/.What the ruling asked for
Decision batch #107 item 3 (comment 5596253788), option A, maintainer reply verbatim: 「其他同意」.
Routes are not changed here. Zero bytes outside
content/docs/.Every shape published, traced to its writer
The ruling's execution note requires each envelope claim to be measured against a writer before it is written. Each claim in the new page and the writer it was read off:
{ success: false, error: { code, message, ...extra } }sendError,packages/types/src/response-envelope.ts:228extrais exactlycategory · httpStatus · details · requestId · declaredCode · userMessagePick<ApiError, …>onsendError's signature, same file :209detailsis emitteddetails:extras across the 7sendErrorroute modules;buildApiError'sdetailssplit; the rate limiter's{ retryAfterSeconds, resetAt }httpStatusis written by the dispatcher writer, not bysendErrorbuildApiErrorsets it unconditionally (packages/runtime/src/error-envelope.ts:119-126); nosendErrorcall site passes one (measured over the same 7 modules that yielded the 9details:hits — same grep, same population, so the zero has a firing control)requestIdrequestIdpassed intosendError's extra orbuildApiError'sextra/detailsat any call site. Firing control:details:fires 167× in the same populationtimestamptimestamp:inerror-response.ts,response-envelope.tsorerror-envelope.ts{ error: <string>, code?, …siblings }structuredCodeAnswerarms + the generic passthrough return,packages/rest/src/error-response.ts:2384-2392codeis optionalthrownCodeFieldsreturns{}when the producer named no code (:493-498)field,fields,developerMessage,dependentObject,dependentCount,currentVersion,currentRecord,datasource,reason,object,issues,declaredCode,userMessagestructuredCodeAnswerarms (:1007-1270) +withDeclaredUserMessage(:708-715)UNIQUE_VIOLATIONflat-onlyDUPLICATE_RECORDarm, :1229-1245DELETE_RESTRICTEDflat-only, two sentences,dependentCountabsent when withheldpackages/objectql/src/engine.ts:13786-13825(discloseCountgates the key)INVALID_FIELDflat-only,field/objectas siblingsRATE_LIMIT_EXCEEDEDnested-only, withhttpStatus+detailspackages/runtime/src/security/inbound-rate-limit.ts:353-361; code derived viaHttpStatusErrorCodeMap[429]VALIDATION_FAILEDis the per-field code,fieldsa top-level siblingFieldValidationError(packages/objectql/src/validation/record-validator.ts:102-133);codevalues fromFieldErrorCode(packages/spec/src/api/errors.zod.ts:233-277)VALIDATION_ERRORis the status-derived 400 codeHttpStatusErrorCodeMap(errors.zod.ts:173)EXPIRED_TOKENis not emitted by anythingStandardErrorCoderegistration aterrors.zod.ts:71. Firing control:INVALID_TOKEN, same grep, same population, returns two real producers instorage-routes.ts/data403 for a shared-record write isFORBIDDEN, notPERMISSION_DENIEDpackages/plugins/plugin-sharing/src/sharing-plugin.ts:1337-1344CODE:prefix is stripped before the body is writtenwithoutDeclaredCodePrefix,error-response.ts:537INTERNAL_ERROR_MESSAGE='Internal server error',packages/types/src/error-leak.ts:38)packages/runtime/src/observability/instrument.ts:103-121(res.header(requestIdHeader, requestId), defaultX-Request-Id); inbound echo is validated first —extractRequestId,request-context.ts:40-52One measurement that corrects the brief
Zone 2's map said
/meta/*answers nested. That holds for the runtime dispatcher's/metadoor only. The@objectstack/rest/metaregistrars report every thrown error throughhandleRouteError→resolveErrorResponse, i.e. flat — 18handleRouteError(res, error)call sites insideregisterMetadataEndpointsInner's range, against zerosendErrorcalls there. So the sameINVALID_REQUESTspelling refusal (packages/metadata-protocol/src/protocol.ts:274-283, a bare throw with.code+.status) answers nested through the dispatcher — which is whaterror-catalog.mdx's/metaexamples publish,httpStatusand all — and flat through@objectstack/rest.The page states this as the rule rather than as a path list, which is what Zone 3 asked for: the envelope is decided by the door, never by the code; a refusal a writer builds is envelope-exclusive, a refusal that is thrown takes whichever door caught it. The
/metasplit is called out explicitly in its own callout.The page also discloses, without enumerating, that the
@objectstack/restdoor still carries tracked non-conforming bodies, and namespnpm check:route-envelopeas the authority — rather than publishing a ratchet count that goes stale.Verification
Gates — 41/41 green, derived with no path list (
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack), exit codes captured to disk before any pipe, reconciled with--ran:The first sweep produced five non-zero results, all of them
PREREQUISITE NOT MET, none a finding:check:doc-formula-expressionsandcheck:doc-security-postureexit 3 on an unbuilt@objectstack/lint;check:docs-transcript-driftexit 3 on the same;check:docsexit 1 on a missingpackages/spec/json-schema;check:skill-examplesexit 1 onpackages/spec/distand then again onpackages/client-react/dist. After building those packages the whole sweep was re-run, not just the five. Builds went throughOS_VERIFY_LOCK_SLOT=issue-15632 bash scripts/pm/os-verify-lock.sh, all threeVERDICT command-exit 0.Lint — full, not narrowed.
eslint . --no-inline-config --format jsonran over the whole repo in one foreground pass: exit 0, 6752 files, 0 errors, 0 warnings. No narrowing claim is needed, and the file this PR touches is not in eslint's population at all (.mdxmatches none of the config'sfiles:globs — 0 of the 6752 reported files end in.mdx).Tree. Derived and run on
50d799c03, which already mergesorigin/main(the derivation's earlierSTALE TREEwarning is gone, and the derived family list is byte-identical before and after the merge).Control characters.
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file: no match.pnpm check:nul-bytesexit 0.skip-changeset. Measured on the publish surface, not asserted: of the 70 non-private workspace packages, zero list anything undercontentordocsinfiles[], andapps/docsis"private": true. Positive control: the same reader prints@objectstack/spec's realfiles[]array, so the zero is a reading. Nothing in this diff ships in a tarball.Acceptance notes
Observed while measuring, not filed and not fixed here — none is a reproducible defect, a contract violation, or a metadata-authoring trap:
rest-server.ts'sregisterMetadataEndpointsInnermixes four response shapes on one prefix —handleRouteError's flat body, hand-built{ error: { code, message } }with nosuccess,{ code, message }with noerrorkey at all (respond501), andrespondError→ the shared nestedsendError. This is exactly the driftcheck:route-envelope'sdialectOnlyratchet already tracks against#9559, so it has a successor and needs no new card.query-allowlist.ts,query-multiplicity.ts) each write one{ error: { code, message } }body with nosuccessabove it. Already carried in the same gate asunenveloped: 1apiece, ratcheted to#9559./datadoor drops a thrown error'sdetailsbag (the sharing plugin sets one atsharing-plugin.ts:1343; the generic 4xx passthrough does not relay it). Noted, not filed — the flat envelope has nodetailsslot by design, so this is the design working, not a leak.EXPIRED_TOKENis a registeredStandardErrorCodewith no producer — the same declared-but-dead shapeQUOTA_EXCEEDEDalready carries on this page. Noted, not filed: the ADR-0049 enforce-or-remove question for the error-code catalog is apackages/specdecision, not a docs one, and no PR or person is heading for it right now. Successor: none.维护者速读(草稿)
改了什么:协议文档《错误处理》整页重写。此前开篇写「每个错误都长这样」并给出一个嵌套信封,页面上十几条 per-code 示例全部重复该嵌套形状 —— 而平台实际上有两套活的错误信封。现在页面如实描述两套:新增「信封由哪一道门写决定,不由错误码决定」这条规则、两节分别写明嵌套与扁平各自的真实字段、一张路由族对照表,并把每一条 per-code 示例放回它真正所属的那一族。
为什么改:裁决 A(决策批次 #107 第 3 项,维护者原话「其他同意」)。此前照这页实现的客户端在
/data一族上整条解包路径都是错的 —— 会去读body.error.code,而线上给的是body.code。页面还发布了三个任何写手都不发射的字段(details在嵌套上是真的,但requestId/timestamp两者皆无),以及一个零生产者的错误码EXPIRED_TOKEN。风险与代价(含回滚):纯文档,零运行时影响,
content/docs/**不进任何已发布包(已在发布面实测)。回滚 = revert 这一个 commit。真正的代价是这页现在更长(955 → 1330 行):两套信封、一条判别规则、逐码归属,写全就是这个体量。若嵌套与扁平将来收敛为一套,这页可大幅收缩 —— 但那是一张协议卡,本次裁决明确没有开(未给A+)。席位意见:(留空,待席位定稿)
你要做的:如果同意「两套信封如实公开」这个方向,直接 review 合并即可。如果你希望开一张「收敛为一套信封」的协议卡,那是独立的一张,本 PR 不阻塞它。
🤖 Generated with Claude Code
https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Generated by Claude Code