Skip to content

fix(metadata-protocol): filter the batch verbs' failed[].code through the error catalog (#8441) - #8505

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8441-batch-verb-driver-code
Aug 13, 2026
Merged

fix(metadata-protocol): filter the batch verbs' failed[].code through the error catalog (#8441)#8505
os-zhuang merged 3 commits into
mainfrom
claude/issue-8441-batch-verb-driver-code

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8441

#8333 landed the error limb at these collectors and deliberately left the sibling code alone — "a different field with a different rule (a closed union)". This is that field.

The premise, measured per site on current main

Driven in process with an engine failing sys_metadata the way a driver really does — Object.assign(new Error('SQLITE_ERROR: no such table: sys_metadata'), { code: 'SQLITE_ERROR', errno: 1 }):

sitebeforeafter
publishPackageDraftsfailed[]{ error: 'publish failed', code: 'SQLITE_ERROR' }{ error: 'publish failed', code: 'INTERNAL_ERROR' }
revertCommitfailed[]{ error: 'revert failed', code: 'SQLITE_ERROR' }{ error: 'revert failed', code: 'INTERNAL_ERROR' }
discardPackageDraftsfailed[]no code at allunchanged, untouched
deletePackagefailed[]no code at allunchanged, untouched

The issue reproduced only the first. The second is confirmed here; the last two are the ones that build the identical limb and turn out not to need it — see "Scope" below.

The rule, and why it is not #8333's

limbquestionpredicate
errordid the producer AUTHOR this sentence for a caller?4xx statusdeclaresClientRefusal
codeis this value a MEMBER of the catalog?StandardErrorCode ∪ ERROR_CODE_LEDGERclientFacingFailureCode

A message is free text, so no catalog bounds it and status-alone is the safe test. code writes ApiErrorSchema.code, a closed union (ADR-0112 D4) a driver's dialect must never enter. Applying #8333's rule here would be the wrong test on the wrong field in both directions at once, and the new pin file's discriminator case proves it: one error, 503, whose sentence is withheld and whose ledger-registered ERR_DATASOURCE_UNAVAILABLE is kept.

The membership predicate is not new — it is carryCatalogedErrorCode's, itself verbatim toRowApiError's. One rule, one place to change it. No error code was minted; ADR-0114's catalog already names every value this can return.

What passes through, and what is replaced

  • Catalogued: through, byte for byte.BATCH_ABORTED on the collateral rows, NOT_OVERRIDABLE, VERSION_NOT_FOUND, INVALID_METADATA, and ledger-registered engine codes such as ERR_DATASOURCE_UNAVAILABLE.
  • Uncatalogued: replaced by the code its declared status maps to, otherwise INTERNAL_ERROR. toRowApiError's derivation, reused rather than a second vocabulary.
  • No code at all: still no code key. Minting one where the wire never carried one is an addition to the response — a separate contract decision, the same posture carryCatalogedErrorCode takes toward status.

⛔ The limb is filtered, never dropped: code is the half a client branches on, and blanking it would trade a real authoring surface for a disclosure narrower than the one already dealt with — worse than doing nothing.

The positive control, green

#8333's mandatory control, re-run against the field it actually reads. A flow draft with genuinely broken CEL still answers with the offending draft, the field path flows[0].nodes[1].config.approvers[0].value, code: 'INVALID_METADATA', and the structured issues the Studio form highlights with.

Scope

discardPackageDrafts and deletePackage build the same limb and got no filter. Both wrap exactly one call, deleteMetaItem, whose two re-wrap exits already run carryCatalogedErrorCode — so no driver dialect survives to them. Measured under the same failing driver and pinned, so a later change to those exits is caught in this repo rather than on a customer's wire. Adding a filter there would be consumer-side tolerance over a producer that does not need it (Prime Directive #12) — the same call #8136 recorded at those sites for the message. The issues limb is likewise untouched: structured authoring feedback no driver error carries.

Verification

protocol.batch-verb-driver-code.test.ts — 12 cases in six sections: evidence (2), the catalogued pass-through guards (4, including the 503 discriminator), the positive control (1), the scope evidence for the two clean collectors (2), the substitution's exact shape (2), and the operator half (1).

⚠️On why #8333's own pins could not see this: its fake throws a bare Error with no code property. Its payload scan would have caught the disclosure — the fixture simply never carried the field under test. The harness here attaches a real driver code, which is what makes every assertion non-vacuous.

Reverse verification, both directions predicted before running.

  • (a) protocol.ts reverted to pre-fix — predicted 4 red / 8 green, measured 4 red / 8 green. Every prediction held, including the operator-half case, predicted red because it asserts the payload as well as the log.
  • (b) The over-broad "just drop the limb" variant — predicted 8 red / 4 green, measured 9 red / 3 green. ⚠️ One missed prediction, recorded in the file rather than tidied away: the operator-half case came back red because it asserts expectCataloged before the log, and under the drop variant there is no code to be catalogued. It is the same miss [finding] metadata-protocol's batch verbs still put caught error text on client-facing payloads — the 8 producers option C did not reach #8333 recorded for its own P7 log case, one card later in the same lane.

Local runs: @objectstack/metadata-protocol 82 files / 1208 tests green; downstream @objectstack/objectql 197 / 3539 and @objectstack/rest 112 / 1843 green. Gate families derived from lint.yml and re-derived with dispatch-gates.mjs against the actual changed paths, union run green: check:nul-bytes, check:error-code-casing, check:engine-double-contract, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure, check:type-check-coverage, plus the changeset scripts.

check:error-code-casing earned its place mid-run: it correctly rejected a lowercase pseudo-code in the off-catalog fixture (ADR-0112 D1), which now spells its dialect as the postgres SQLSTATE 42501 — the dialect the issue itself names, and a case where dialect and declared 403 agree on meaning so the substitution has to preserve it.

check:objectui-pin-fresh is red, pre-existing and unrelated: .objectui-sha is untouched here and main already carries 2018 changesets, so the gate fires identically on main. It asks the release manager to bump the pin.

Out of scope, filed

#8502deleteManyData's row errors carry raw driver text in errors[].message. Measured while working this card: the code half is already gated by toRowApiError, the message half is not, and the sink is a data batch row rather than a metadata verb, so neither #8136 nor #8333 enumerated it. Not ridden in here.


Generated by Claude Code

…gh the error catalog (#8441)
#8333 closed the `error` limb at these collectors and left the sibling `code`
alone as a different field with a different rule. Measured on the fixed branch,
`publishPackageDrafts` still answered `{ error: 'publish failed', code:
'SQLITE_ERROR' }` and `revertCommit` the same — response DATA no HTTP
boundary's withhold reaches.
`code` writes `ApiErrorSchema.code`, a closed union (ADR-0112 D4), so the rule
is CATALOG MEMBERSHIP — `carryCatalogedErrorCode`'s predicate, itself
`toRowApiError`'s — not the 4xx question. A catalogued code passes through byte
for byte; an uncatalogued one becomes the code its declared status maps to, else
INTERNAL_ERROR. No code was minted, and an error with no code still yields no
key.
`discardPackageDrafts` / `deletePackage` build the same limb and need no filter:
they wrap `deleteMetaItem`, whose re-wrap exits already gate `code`. Measured
and pinned rather than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
… a SCREAMING-free SQLSTATE fixture (#8441)
Direction (b), the drop-the-limb variant, was predicted 8 red / 4 green and
measured 9 red / 3 green. The miss is section 6's: the prediction reasoned
about the log half and forgot the case asserts the payload first
(`expectCataloged`), which is the SAME miss #8333 recorded for its own P7 log
case. Recorded rather than tidied — it makes section 6 evidence for the
substitution, not only for the logging.
The off-catalog fixture now spells its dialect as a postgres SQLSTATE
(`42501`, insufficient_privilege) instead of a lowercase pseudo-code:
`check:error-code-casing` correctly flags a lowercase literal in a code
position (ADR-0112 D1), and the SQLSTATE is both the dialect the card's issue
names and a case where dialect and declared 403 agree on meaning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 4:25pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 16:44
@os-zhuang
os-zhuang added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit c2c67bfAug 13, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8441-batch-verb-driver-code branch August 13, 2026 17:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A raw driver code (SQLITE_ERROR) still reaches client-facing payloads through the batch verbs' code limb — the sibling field #8333 did not cover

2 participants

@os-zhuang@claude