Skip to content

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
claude[bot] merged 1 commit into
mainfrom
claude/issue-18193-error-catalog-wire-face
Sep 14, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-18193-error-catalog-wire-face

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #18193

Clause-②: no
Docs-only. One hand-written MDX page under content/docs/; no packages/** byte changes, no runtime surface widened, no new authorable key.

⚠️ The card's premise is falsified: main is NOT red — the failure belongs to PR #18191

The card diagnoses a latent red on main and rests that on one mechanism claim: that face.wireCodes is 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) reconciles StandardErrorCode with every other code a scanned page publishes a status for — and it scans two pages:

const DOC_HANDLING = 'content/docs/protocol/kernel/error-handling.mdx';   // :219
const DOC_CATALOG  = 'content/docs/api/error-catalog.mdx';                // :220

error-handling.mdx is 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

tree wireCodes.length docPublishedBeyondStandard wire codes with no catalog heading
origin/main @ 076bb975d (today) 51 INVALID_REQUEST, UNIQUE_VIOLATION (none)
main @ ba9b4981c (PR #18191's own base) 51 same two (none)
PR #18191 head 50d799c03 52 + VALIDATION_FAILED VALIDATION_FAILED
this branch 52 + VALIDATION_FAILED (none)

The provenance line the run prints for the 52nd code on the PR head names the writer outright:

claimed VALIDATION_FAILED = [[400, ["content/docs/protocol/kernel/error-handling.mdx:346"]]]

That line is in PR #18191's diff. And the test is green on main, twice measured:

BEFORE (unmodified tree, 076bb975d)   Test Files  1 passed (1) · Tests  5 passed (5)
AFTER  (this branch)                  Test Files  1 passed (1) · Tests  5 passed (5)

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 by git checkout HEAD -- PATHS reproducing the HEAD blob hashes and an empty git diff HEAD):

MUTATED handling=d174e08b8b93f10891366716e3677458f073944b catalog=6a529b3d11dd502d94c18c6c10dcd6f7f217e95b
        (handling hash == `git rev-parse probe-18191-head:content/docs/protocol/kernel/error-handling.mdx` — identity, not a grep)
catalog claim now: **51 error codes reachable on the wire**
catalog VALIDATION_FAILED headings: 0

AssertionError: wire code `VALIDATION_FAILED` has no catalog entry: expected false to be true
 ❯ src/api/error-catalog-docs.test.ts:95:82
AssertionError: expected 51 to be 52
 ❯ src/api/error-catalog-docs.test.ts:125:31
 Test Files  1 failed (1) · Tests  2 failed | 3 passed (5)

RESTORED both files == HEAD blobs · git diff HEAD empty

⚠️ One honest miss in that probe: the first swap also printed a grep -c marker 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:

  1. The page already told readers to branch on a code it never catalogued. INVALID_REFERENCE's Fix line says "branch on VALIDATION_FAILED + fields[].code === 'reference_not_found'", the dangling-reference callout publishes its full JSON body, and the MISSING_REQUIRED_FIELD callout names 400 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.
  2. It is green in both worlds, because the catalog page is itself one of the two scanned inputs. Adding the entry under ## Validation Errors (400) is what admits the code to the vocabulary, so the count moves 51 → 52 on main alone 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 with probe-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.
  3. It unblocks docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary #18191, which the seat deliberately did not arm for this reason.

⇒ 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:

Claim now on the page Writer it was read off
flat /data body is { error, code: 'VALIDATION_FAILED', fields: [...] }, fields a top-level sibling structuredCodeAnswer's arm, packages/rest/src/error-response.ts:1150-1159
flat fields is always present, [] when the producer named none same arm: fields: Array.isArray(error?.fields) ? error.fields : [] (:1156)
the nested door carries the list at error.details.fields instead validationFailureDetails returns { code, fields } (packages/types/src/validation-failure.ts:57-63); splitSemanticCode promotes code to error.code and leaves the rest as details (packages/runtime/src/error-envelope.ts:100-126)
a route that builds its own refusal sends the code with no fields at all sendError(res, 400, 'VALIDATION_FAILED', 'object and recordId are required'), packages/plugins/plugin-sharing/src/share-link-routes.ts:156
it is the code a producer names, unlike VALIDATION_ERROR which is status-derived HttpStatusErrorCodeMap[400] for the latter; the former is matched on err.code === 'VALIDATION_FAILED' || err.name === 'ValidationError'
status is 400 and only 400 the derivation's own emitted census: emitted[VALIDATION_FAILED] = [[400, [...]]]protocol.ts MalformedVersionTokenError, protocol.ts:2721, protocol.ts:13012, share-link-routes.ts:156
covers a batch row naming no record id rowRequiredIdError, packages/metadata-protocol/src/protocol.ts:2719-2724
fields[] entry keys (field, code, message, label, constraint, value, options) FieldValidationError, packages/objectql/src/validation/record-validator.ts:102-133; code from FieldErrorCode (ADR-0114)

The count was read off face.wireCodes.length after 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 --ran in the :: exit N form:

Run reconciliation — 41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN.
  EXIT CODES — all 41 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3).

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) through OS_VERIFY_LOCK_SLOT=issue-18193 bash scripts/pm/os-verify-lock.sh; the second printed VERDICT command-exit 0.

Lint — full, not narrowed. eslint . --no-inline-config --format json over 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-bytes exit 0.

skip-changeset — measured on the publish surface, not asserted. Of the 70 non-private workspace packages, zero list anything under content/ or docs/ in files[], and apps/docs is "private": true. Positive control: the same reader prints @objectstack/spec's real files[] (["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 from origin/main 076bb975d.

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:

  • Editing error-handling.mdx silently widens the wire face and reds a test about error-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 mechanism reconciledVocabulary was 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.
  • The count sentence is self-referential. Adding a catalog entry for a ledger code moves wireCodes.length by exactly +1 through the same edit, so "add the entry" and "fix the number" are one change, not two. Worth knowing before reading a 51 → 52 diff as two independent claims. Noted, not filed — no rule is violated and the gate is loud. Successor: none.
  • VALIDATION_ERROR and VALIDATION_FAILED are 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 in packages/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 on VALIDATION_FAILED」,而目录里根本查不到这个码。② 派单说 main 红了,这一条经实测不成立:main 上这个测试是绿的(今天的 076bb975d 与 PR #18191 自己的 base ba9b4981c 都测过)。真正会变红的是 PR #18191 —— 它重写的 error-handling.mdx 恰好是 deriveWireFace 扫描的两个页面之一,该页新写的 VALIDATION_FAILED 400 把这个码带进了线上词表,目录页却没跟上。卡片认定「文档改动移不动词表」,这一点是错的。

风险与代价(含回滚):纯文档,零运行时影响,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

… 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>
@claude claude Bot added documentation Improvements or additions to documentation skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Sep 14, 2026
@claude
claude Bot marked this pull request as ready for review September 14, 2026 14:15
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 3f98c2a Sep 14, 2026
42 checks passed
@claude
claude Bot deleted the claude/issue-18193-error-catalog-wire-face branch September 14, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant