docs(api): catalog the VALIDATION_FAILED wire code — and the card’s main-red premise is falsified (the failure is PR #18191’s own) - #18196
Merged
Conversation
… advertised count
`error-catalog.mdx` told readers to branch on `VALIDATION_FAILED` in three
places — the `INVALID_REFERENCE` entry, the dangling-reference callout and the
`MISSING_REQUIRED_FIELD` callout — while the catalog itself carried no entry for
it. This adds the entry, traced to its writers rather than filled in to satisfy
a count:
- the flat `/data` answer is built by `structuredCodeAnswer`'s arm in
`packages/rest/src/error-response.ts`, which writes `fields` as a top-level
sibling and defaults it to `[]` when the producer named none;
- the nested answer comes from the runtime dispatcher, where
`validationFailureDetails` (`packages/types/src/validation-failure.ts`) hands
`{ code, fields }` to `buildApiError`, whose `splitSemanticCode` promotes the
code and leaves `fields` under `details`;
- a route that builds its own refusal (`share-link-routes.ts`) sends the code
and the sentence with no `fields` at all — so the entry tells clients to read
the sentence and treat `fields` as a refinement that may be absent.
The advertised count moves 51 -> 52 because the entry ITSELF is what admits the
code to the wire face: `deriveWireFace` reconciles `StandardErrorCode` with the
codes a scanned page publishes a status for, and this page is one of the two it
scans. The number was read off `face.wireCodes.length` after the edit, not
transcribed.
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
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 #18193
Clause-②: no
Docs-only. One hand-written MDX page under
content/docs/; nopackages/**byte changes, no runtime surface widened, no new authorable key.mainis NOT red — the failure belongs to PR #18191The card diagnoses a latent red on
mainand rests that on one mechanism claim: thatface.wireCodesis derived from CODE only, so "no docs edit can move it". That claim is wrong, and it is the whole diagnosis.deriveWireFace(scripts/check-error-status-conformance.mjs) reconcilesStandardErrorCodewith every other code a scanned page publishes a status for — and it scans two pages:error-handling.mdxis exactly the page PR #18191 rewrites. The test file itself never reads it — which is what the card measured — but the derivation it imports does, one frame down.reconciledVocabulary's own docblock calls that second half "the whole point": a ledger code is reconciled because the docs publish a status for it.Measured, four trees, same script
wireCodes.lengthdocPublishedBeyondStandardorigin/main@076bb975d(today)INVALID_REQUEST,UNIQUE_VIOLATIONmain@ba9b4981c(PR #18191's own base)50d799c03VALIDATION_FAILEDVALIDATION_FAILEDVALIDATION_FAILEDThe provenance line the run prints for the 52nd code on the PR head names the writer outright:
That line is in PR #18191's diff. And the test is green on
main, twice measured:Ablation — the card's two assertions, reproduced byte-for-byte, only with #18191 present
Both files swapped under a trapped restore (absolute paths; each mutation proven on disk by a changed
git hash-object; restore proven bygit checkout HEAD -- PATHSreproducing the HEAD blob hashes and an emptygit diff HEAD):grep -cmarker count of 0. The marker string was mine, invented from the PR's summary rather than read off its diff, so it never existed. The binding evidence is the blob-hash identity above, which fired; the grep proved nothing and is reported rather than quietly dropped.So why land this anyway, rather than returning a falsified-premise report with no PR
The ruling's intent is served and the repair is correct on its own terms — it just is not a
main-red stanch:INVALID_REFERENCE's Fix line says "branch onVALIDATION_FAILED+fields[].code === 'reference_not_found'", the dangling-reference callout publishes its full JSON body, and theMISSING_REQUIRED_FIELDcallout names400 VALIDATION_FAILED. Seven prose mentions, zero entries. That hole is real with or without docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary #18191.## Validation Errors (400)is what admits the code to the vocabulary, so the count moves 51 → 52 onmainalone and stays 52 once docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary #18191 lands. Verified on a throwaway merge of this branch withprobe-18191-head:wireCodes.length = 52, zero wire codes missing a heading, and the real test run 5 passed (5) with docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary #18191's page swapped in.⇒ If the intent was specifically "stanch a red on
main", there is nothing to stanch and this can be closed unmerged at no cost. Everything below stands on its own.The entry, traced to its writers
⛔ Not a placeholder row added to satisfy a count. Every claim measured on this tree:
/databody is{ error, code: 'VALIDATION_FAILED', fields: [...] },fieldsa top-level siblingstructuredCodeAnswer's arm,packages/rest/src/error-response.ts:1150-1159fieldsis always present,[]when the producer named nonefields: Array.isArray(error?.fields) ? error.fields : [](:1156)error.details.fieldsinsteadvalidationFailureDetailsreturns{ code, fields }(packages/types/src/validation-failure.ts:57-63);splitSemanticCodepromotescodetoerror.codeand leaves the rest asdetails(packages/runtime/src/error-envelope.ts:100-126)fieldsat allsendError(res, 400, 'VALIDATION_FAILED', 'object and recordId are required'),packages/plugins/plugin-sharing/src/share-link-routes.ts:156VALIDATION_ERRORwhich is status-derivedHttpStatusErrorCodeMap[400]for the latter; the former is matched onerr.code === 'VALIDATION_FAILED' || err.name === 'ValidationError'emitted[VALIDATION_FAILED] = [[400, [...]]]—protocol.tsMalformedVersionTokenError,protocol.ts:2721,protocol.ts:13012,share-link-routes.ts:156rowRequiredIdError,packages/metadata-protocol/src/protocol.ts:2719-2724fields[]entry keys (field,code,message,label,constraint,value,options)FieldValidationError,packages/objectql/src/validation/record-validator.ts:102-133;codefromFieldErrorCode(ADR-0114)The count was read off
face.wireCodes.lengthafter the edit (52) and not transcribed from the card, per its acceptance criterion 2.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--ranin the:: exit Nform:Exit-code census: 41 × exit 0, zero non-zero, zero exit 3.
pnpm check:error-status-conformance— the gate most exposed to this edit, since it is the same derivation — is one of them. Two prerequisite builds ran first (@objectstack/spec+@objectstack/lint, then the@objectstack/client-react...closure) throughOS_VERIFY_LOCK_SLOT=issue-18193 bash scripts/pm/os-verify-lock.sh; the second printedVERDICT command-exit 0.Lint — full, not narrowed.
eslint . --no-inline-config --format jsonover the whole repo in one foreground pass: exit 0, 6752 files, 0 errors, 0 warnings. No narrowing claim is needed. This file is not in eslint's population at all — 0 of the 6752 reported files end in.mdx.Control characters.
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file: no match (grep exit 1, zero lines).pnpm check:nul-bytesexit 0.skip-changeset— measured on the publish surface, not asserted. Of the 70 non-private workspace packages, zero list anything undercontent/ordocs/infiles[], andapps/docsis"private": true. Positive control: the same reader prints@objectstack/spec's realfiles[](["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]), so the zero is a reading and not an empty walk. Nothing in this diff ships in a tarball.Tree. Derived and run on
6f3523462, branched fromorigin/main076bb975d.Acceptance notes
Observed while measuring, not filed and not fixed here — none is a reproducible defect, a declared-contract violation, or a metadata-authoring trap:
error-handling.mdxsilently widens the wire face and reds a test abouterror-catalog.mdx. A docs author on the protocol page has no local signal that publishing a status for a ledger code enlarges the catalog's advertised count. It is loud where it matters (the assertion names the code and the arithmetic) and it is the mechanismreconciledVocabularywas deliberately built to have, so this is the design working rather than a defect. Successor: PR docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary #18191, which is the PR that hit it and now has the repair landing alongside.wireCodes.lengthby exactly +1 through the same edit, so "add the entry" and "fix the number" are one change, not two. Worth knowing before reading a51 → 52diff as two independent claims. Noted, not filed — no rule is violated and the gate is loud. Successor: none.VALIDATION_ERRORandVALIDATION_FAILEDare adjacent in the catalog and easy to confuse; the new entry states the distinction (status-derived vs producer-named) rather than leaving it to the reader. This is fixed here, not deferred — noting it only because the same confusion is visible inpackages/cli/src/utils/format.ts:236's comment, which is correct and needs nothing.维护者速读(草稿)
改了什么:错误码目录页
content/docs/api/error-catalog.mdx补上VALIDATION_FAILED这一条(此前它在页面正文里被提到七次,却没有自己的条目),并把页头「51 error codes reachable on the wire」改成实测的 52。为什么改:两件事,要分开看。① 这一页本来就有洞 ——
INVALID_REFERENCE条目明写「请改为 branch onVALIDATION_FAILED」,而目录里根本查不到这个码。② 派单说main红了,这一条经实测不成立:main上这个测试是绿的(今天的076bb975d与 PR #18191 自己的 baseba9b4981c都测过)。真正会变红的是 PR #18191 —— 它重写的error-handling.mdx恰好是deriveWireFace扫描的两个页面之一,该页新写的VALIDATION_FAILED400 把这个码带进了线上词表,目录页却没跟上。卡片认定「文档改动移不动词表」,这一点是错的。风险与代价(含回滚):纯文档,零运行时影响,
content/docs/**不进任何已发布包(70 个非私有包的files[]实测,带正控)。回滚 = revert 这一个 commit。因为目录页本身就是词表的输入之一,这个改动在「#18191 已合」和「#18191 未合」两种世界里都是绿的 —— 两种都实跑过。席位意见:(留空,待席位定稿)
你要做的:如果你认同「目录页该有这一条」,直接 review 合并即可,它同时解开 PR #18191 的阻塞。如果你当初批的是「去把 main 的红止住」,那没有红可止 —— 这张 PR 可以零代价关掉,证据都在上面。
🤖 Generated with Claude Code
https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Generated by Claude Code