Skip to content

fix(rest): put the /meta 501 refusals inside the ADR-0112 error envelope (#7035) - #7293

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7035-meta-501-envelopes
Aug 10, 2026
Merged

fix(rest): put the /meta 501 refusals inside the ADR-0112 error envelope (#7035)#7293
os-help merged 1 commit into
mainfrom
claude/issue-7035-meta-501-envelopes

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7035

Three adjacent /meta handlers in packages/rest/src/rest-server.ts answered the
same condition — "the protocol implementation lacks this method", a 501 — in three
different shapes. All of them now speak the ADR-0112 nested envelope,
{ error: { code, message } }, which is the shape POST /meta/_migrate-stored
already used.

Per the triage promotion ruling of 2026-08-09 this is the card's option 1;
option 3 (a shared envelope constructor across the whole file) is explicitly not
this card.

Premise: verified, and it moved

Both sites the card named are still non-conforming on origin/main. The card's
line numbers had drifted (:4697 / :4996 matched no revision of main at
filing time), so I relocated by content.

Relocating by content found three, not two:

route before shape
POST /meta/_migrate-stored { error: { code, message } } nested — conformant, the anchor
DELETE /meta/:type/:name { error: '…' } bare string, no code at all
PUT /meta/:type/:name { error: '…', code } code as a sibling of error
PUT /meta/:type/:section/:name { error: '…', code } code as a sibling of error

The card's table lists one PUT. The two PUT refusals were byte-identical,
and the file's own gate comment on the compound route calls the pair "WORD FOR
WORD the same mechanism, because it is word for word the same operation". So
"the sibling-key site" could not be resolved to one of them by content — and
fixing one while leaving its literal twin would have left the wrong template in
the file directly beside the right one, which is precisely the harm the card
describes ("下一个照抄相邻 handler 的人会照抄哪一种,取决于他滚到了哪一行").
I converged all three and am flagging the count deviation rather than burying it.

I did not touch the file's other ~44 bare-string error bodies — that is the
card's option 2, and it is not this card. See the finding below.

The catalog question the card left 未实测

Checked, and the catalog answers it: NOT_IMPLEMENTED is already a member of
the standard catalog (packages/spec/src/api/errors.zod.ts), and it is the code
the catalog maps 501 to — standardErrorCodeForHttpStatus(501) returns it, pinned
in packages/spec/src/api/errors.test.ts. So the code is kept as-is, nested, and
no catalog entry is minted; packages/spec is untouched.

The gate half: measured, and it is not a one-rule extension

The ruling asked to extend the error-code-casing / route-envelope gate family
to cover these sites if it does not already. Measured: neither gate sees this
defect, and closing that is a restructuring job rather than a rule.

  • check:route-envelope does not scan rest-server.ts at all. Its
    discover() collects only files ending -routes.ts, plus the one named
    i18n-service-plugin.ts. Running its own scanSource against rest-server.ts
    by hand reports responses: 208, ok: 2, err: 0, stringError: 44. Bringing the
    file into the scan therefore means a ratchet pinning 208 write sites in a file
    that changes several times a day — every unrelated res.json added or removed
    flips the number — and the gate's model ("route everything through the shared
    sendOk / sendError") is card option 3 for this file.
  • The sibling-code dialect is invisible to the scanner even so. scanSource
    counts stringError and privateOk; it has no counter for "top-level code
    as a sibling of error". Covering the second of this card's two dialects needs
    a new counter plus self-test cases, not a table row.
  • check:error-code-casing does scan rest-server.ts, but only for casing
    lowercase literals in code positions. Position is outside its thesis, and it is
    textual by design, which is exactly what check:route-envelope went AST to avoid.

Per the dispatch's own instruction for this outcome, filed as an out-of-scope
finding instead of built here. Both gates were run and are green on this branch.

Tests

New: packages/rest/src/meta-501-envelope.test.ts (7 cases). Each refusal case
asserts the ADR-0112 pair — status and body.error.code at the nested
position — plus both retired dialects being gone (body has no top-level code;
body.error is an object, not a string). No toThrow-shaped assertion: these
handlers send rather than throw, so "the promise resolved" could not separate
"refused with the wrong envelope" from "did not refuse at all", and the wrong
envelope is the defect.

The 501 branches are reachable only when the protocol lacks the method, so the
in-file stub deliberately omits saveMetaItem, deleteMetaItem and
migrateStoredMetadata, and isSystem clears the capability gate that fires
before the protocol is probed.

Two cases are deliberately not defect detectors and are labelled as such: the
_migrate-stored case pins the anchor shape so the convergence keeps its
reference point, and the twin-equality case pins the two PUTs as one contract.

Reverse verification (direction predicted before running: 5 red, 2 green).
Fix removed with git checkout origin/main -- packages/rest/src/rest-server.ts,
restored from a patch file:

× DELETE /meta/:type/:name — was a bare-string `error` with no code at all
× DELETE /meta/:type/:name?dropStorage=true — the destructive form answers the same envelope
× PUT /meta/:type/:name — was `code` as a sibling of `error`
× PUT /meta/:type/:section/:name — the compound twin, same dialect, same fix
× one code path reads every refusal — `err.error.code` on all four routes
AssertionError: expected undefined to be 'NOT_IMPLEMENTED' // Object.is equality
 Tests  5 failed | 2 passed (7)

The failure text is the card's argument verbatim: body.error.code read
undefined. The 2 green are the anchor and the twin-equality pin, as predicted.

Green with the fix in place:

pnpm --filter @objectstack/rest test
 Test Files  77 passed (77)
      Tests  1226 passed (1226)

pnpm --filter @objectstack/rest typecheck   → clean
pnpm check:route-envelope                   → ✓ 8 modules, 7 conformant, 0 ratcheted, 1 exempt
pnpm check:error-code-casing                → ✓ self-test 17 cases; no lowercase codes in 3378 files
node scripts/check-nul-bytes.mjs            → OK (6640 files)
pnpm check:type-check-debt                  → OK, 33 entries re-measured, none above its recorded number

@objectstack/rest TEST_DEBT: recorded 163, measured 155 on this branch, with
0 errors attributable to the new test file. The new file's first draft did add
one TS2835 (import … from './rest-server'); it is written './rest-server.js'
instead, matching three existing test files in the package. That matters because
#7248 lowers this entry to 155 — a zero-margin ceiling this branch stays clean
against whether or not #7248 is in the merge base.

Changeset

patch on @objectstack/rest. Wire-visible on three 501 branches: a caller running
a kernel without metadata writers sees the code move from err.code into
err.error.code, and the DELETE route's error becomes an object. No in-repo
or objectui consumer reads either retired position — grepped both repos for the
two message strings and for the routes' shapes, zero hits — so no tolerant
dual-read window is needed.

Out of scope, filed separately

  • The rest of rest-server.ts's non-conforming error bodies (44 bare-string
    error sites by the gate's own scanner) — card option 2, untouched here.
  • The dispatcher-side envelope in packages/runtime/src/domains/meta.ts — out per
    the card's own 未实测 fence.

Generated by Claude Code

…elope (#7035)

Three adjacent `/meta` handlers answered "the protocol implementation lacks
this method" in three different shapes. `POST /meta/_migrate-stored` sent the
ADR-0112 nested `{ error: { code, message } }`; `DELETE /meta/:type/:name` sent
a bare-string `error` with no code at all; and the two `PUT` save twins sent the
code as a *sibling* of `error`.

A client reading `err.error.code` — the position ADR-0112 declares — therefore
got `undefined` on three of the four routes, and `undefined` takes the "no code"
branch rather than an error branch. That is Prime Directive #12's
producer-is-the-contract broken in the shape that forces `??` chains on
consumers.

All four now send the nested shape. `NOT_IMPLEMENTED` is unchanged: it is
already the standard catalog's member for 501 (`spec/src/api/errors.zod.ts`,
`standardErrorCodeForHttpStatus(501)`), so no catalog entry is minted and
`packages/spec` is untouched.

The card's table sampled two non-conforming sites; measurement found THREE. The
two `PUT` refusals were byte-identical — the file's own gate comment calls them
"WORD FOR WORD the same mechanism" — so converging one and leaving its literal
twin would have left the wrong template beside the right one, which is the harm
this card is about.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 5:12am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

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

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/system-context.mdx (via packages/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

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-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
@os-help
os-help marked this pull request as ready for review August 10, 2026 05:43
@os-help
os-help added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 870f90c Aug 10, 2026
26 checks passed
@os-help
os-help deleted the claude/issue-7035-meta-501-envelopes branch August 10, 2026 05:57
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: rest-server.ts 里三个相邻 /meta handler 的错误信封是三种不同形状,其中两种不符合 ADR-0112

2 participants