Skip to content

gate: reconcile the documented HTTP status of an error code against the status the runtime emits (#8967) - #9023

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-8967-error-status-conformance
Aug 16, 2026
Merged

gate: reconcile the documented HTTP status of an error code against the status the runtime emits (#8967)#9023
os-project-manager merged 1 commit into
mainfrom
claude/issue-8967-error-status-conformance

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#8967

content/docs/api/error-catalog.mdx and content/docs/protocol/kernel/error-handling.mdx publish an HTTP status per error code; the doors decide it. Nothing reconciled the two, so MISSING_REQUIRED_FIELD was documented 400 in both pages while five controlled_by_parent refusal paths answered 422, on a public error contract, with CI green throughout.

scripts/check-error-status-conformance.mjs + pnpm check:error-status-conformance, wired into .github/workflows/lint.yml.

Lead evidence — RED on the pre-#8963 tree, not on a synthetic fixture

Checked out PR #8963's merge parent (90197e15e) as a second worktree, dropped the gate in, ran it against the real pre-fix pages and the pre-fix runtime:

✗ MISSING_REQUIRED_FIELD: the runtime can emit HTTP 422, and no doc publishes
that status for it (documented: 400). Emitted at
packages/plugins/plugin-security/src/errors.ts: class MasterReferenceMissingError.
✗ check:error-status-conformance — 4 finding(s). # exit code 1

The A/B isolates it to the two doc files. On that same pre-fix tree, git checkout 02042bef2 -- content/docs/api/error-catalog.mdx content/docs/protocol/kernel/error-handling.mdx (only #8963's docs, runtime untouched) ⇒ 3 findings, the MISSING_REQUIRED_FIELD line gone; restoring the pre-fix docs brings it back. The three remaining are the TIMEOUT/NOT_IMPLEMENTED findings this PR corrects.

That is a demonstration, so it is also pinned: the pre-fix **HTTP Status:** 400 line and the pre-fix quick-reference 400 row are byte-copied out of 90197e15e into the --self-test as a permanent red case, with the landed post-#8963 text as its green twin.

Positive control for the zero-hit case. A green run that reconciled nothing is a blind run, not a clean one. The gate publishes reconciled: N code(s) … M (code, status) pair(s) matched, exits 1 if that count is ever 0, and a --self-test case asserts the green fixture reports exactly 2 matched pairs while a producer-free fixture reports 0.

(a) The runtime side is derived, never listed

No code → status table exists in this PR. Every runtime status is read out of source:

sourcewhat it reads
error classesreadonly code + readonly status / readonly statusCode in one brace-matched class body
@objectstack/types doorsendError(res, status, code, message) literal arguments
@objectstack/rest doorsendError(res, { code, …, status })
REST mapper terminals{ status: N, body: { …, code } }
the door's own mapHttpStatusErrorCodeMapexplicit entries only, never standardErrorCodeForHttpStatus's bucket fallback, the same bound standardSynonymOf draws in error-code-ledger.zod.ts

Identifiers resolve through a repo-wide constant index (scalars, object members, MAP[OBJ.key], and computed-key status tables written as const satisfies … — the external-errors.ts shape). A name bound to two different literals is refused, not guessed. An unresolvable declaration is reported, never dropped — the run prints an unresolved: block; it is currently empty, and the three declarations that were in it are now read.

Comments are stripped first, and that is load-bearing. This repo documents envelopes in prose, and two of the gate's very first findings on main were docblocks: quickjs-runner.ts narrating a bug it fixed, and protocol.ts naming a shape it exists to prevent. Read as producers, both manufacture a disagreement out of a sentence saying the opposite. Pinned as a --self-test case using those two real sentences.

"No in-package declaration" is a stated finding, not a pass.#8880 recorded that ValidationError declares no status, so nothing pinned the doc's claim on either side. Every documented standard code with zero derived producer lands in scripts/error-status-unpinned-baseline.json — 37 today, shrink-only: a new one fails, and a row that becomes pinned fails until ratcheted down. The baseline-expanding remedy carries the ⛔ MAINTAINER-ONLY marker per the existing convention, and a --self-test case holds the marker on that message and off the ratchet-down one.

(b) One code may carry more than one status

The assertion is a set comparison in both directions — A: every status the runtime can emit is documented; B: every status the docs claim is reachable (asserted only for codes with a derived producer, which is what the census above bounds).

Doc statements are graded, deliberately:

  • claimed (both directions) — error-handling.mdx's **HTTP Status:** … line, every 4xx/5xx integer on it, which is exactly how a documented exception is expressed; and error-catalog.mdx's quick-reference rows.
  • covered (direction A only) — ## … Errors (NNN) section headings. ## Validation Errors (400) mirrors ErrorCategory.validation → 400: it states the category's status, not each member's. It can absolve an emitted status (a 405 under ## Request Errors (405/428) is documented) but never demand reachability. Grading it up would have the gate manufacture findings from a heading that never claimed them.

Two more of the same defect class, found by the new instrument and corrected here

Docs only — no runtime status moved, and #8880 direction 2 is untouched. Both are in error-catalog.mdx's quick reference, both mechanical and pinned by source:

  • NOT_IMPLEMENTED is served 501, by packages/services/service-storage/src/storage-routes.ts:625, 674, 692, 734, plus HttpStatusErrorCodeMap[501]. The page filed it under ## Server Errors (500) and named 501 nowhere. ⇒ added a 501 row.
  • TIMEOUT is the code a 504 carries (HttpStatusErrorCodeMap[504]), and nothing emits TIMEOUT at 500. The 500 row claimed it. ⇒ moved to a new 504 row (both directions fired on this one).
  • The section heading became ## Server Errors (5xx) with one sentence saying why, since those five codes are served at 500/501/503/504 — otherwise the corrected table contradicts the heading above it. No page links that anchor (checked here and in objectui); check:doc-anchors is green.

Scope covered / not covered — the gate says so itself, on every run

Covered: the 53 StandardErrorCode members, the vocabulary both pages publish statuses for. Not covered: the registered ERROR_CODE_LEDGER codes — 42 are derived and counted, but neither page publishes their status, so there is nothing to reconcile them against; documenting them is a separate card. The run prints both numbers, so a bounded gate cannot read as a complete one.

Verification — union re-run at bb0bc3397, the final commit

check:error-status-conformance PASS # 24 self-test cases + the real check
check:role-word PASS
check:docs-audit-scope PASS
check:doc-anchors PASS
check:doc-authoring PASS
check:node-version PASS
check:required-contexts PASS
check:shard-attestation PASS
check:workflow-status-functions PASS
check:nul-bytes PASS
check:error-status-conformance — documented HTTP status ⇄ runtime-emitted status
scope: 53 StandardErrorCode members reconciled; 1801 source files scanned; 112 producer
site(s) derived; 42 registered ledger code(s) derived but NOT reconciled
reconciled: 16 code(s) with a derived producer, 17 (code, status) pair(s) matched
unpinned: 37 documented code(s) with no derivable producer (baselined: 37)
✓ every derivable runtime status is documented, and every documented status is reachable.

packages/spec tests that read these pages: error-catalog-docs.test.ts, error-code-ledger.test.ts, errors.test.ts — 3 files, 46 tests, all pass.

check:type-check-debt appears in this PR's derived gate set via the .github/workflows/lint.yml glob; it needs the whole workspace build closure and re-measures per-package tsc errors. This diff contains no TypeScript (one .mjs, one .json, package.json, a workflow comment + step, one .mdx), so nothing it measures can have moved; left to CI. check:type-check-coverage's own half ran green here.

skip-changeset: a scripts/ gate plus workflow wiring publishes no package source.

Generated by Claude Code


Generated by Claude Code

…he status the runtime emits (#8967)
`content/docs/api/error-catalog.mdx` and
`content/docs/protocol/kernel/error-handling.mdx` publish an HTTP status per
error code; the doors decide it. Nothing reconciled the two, so
`MISSING_REQUIRED_FIELD` was documented 400 in both pages while five
`controlled_by_parent` refusal paths answered 422, with CI green throughout.
`scripts/check-error-status-conformance.mjs` closes that. The runtime side is
DERIVED, never listed: error classes' own `readonly status` / `readonly
statusCode`, both `sendError` doors' literal arguments, the REST mapper's
`{ status, body }` terminals, and `HttpStatusErrorCodeMap`'s explicit entries
(never the bucket fallback). Identifiers resolve through a repo-wide constant
index including computed-key status tables; an unresolvable declaration is
reported, not dropped; comments are stripped first, because this repo narrates
envelopes in prose and two of the gate's first findings were docblocks
describing a bug that had been fixed.
The assertion is a set comparison in BOTH directions, so a code may carry more
than one documented status: every status the runtime can emit is documented,
and every status the docs claim is reachable. Doc statements are graded —
per-code lines and quick-reference rows CLAIM (both directions), `## … (NNN)`
section headings only COVER (direction A), because a category heading is not a
per-code assertion.
Verified red on PR #8963's merge parent (90197e1), where the real pre-fix
pages filed the code under 400 with no exception:
x MISSING_REQUIRED_FIELD: the runtime can emit HTTP 422, and no doc
publishes that status for it (documented: 400). Emitted at
packages/plugins/plugin-security/src/errors.ts: class
MasterReferenceMissingError.
Swapping in ONLY #8963's two doc files on that same tree clears it. The real
pre-fix text is embedded as a `--self-test` red case so the property is pinned,
not demonstrated once.
Two more of the same defect class, found by the new instrument on `main` and
corrected here in `error-catalog.mdx`'s quick reference (docs only, no runtime
status moved): `NOT_IMPLEMENTED` is served 501 by four storage routes while the
page filed it under 500, and `TIMEOUT` is the code a 504 carries while the 500
row claimed it and nothing emits it at 500.
Refs #8967
@vercel

vercelBot commented Aug 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 16, 2026 6:39am

Request Review

@os-project-manageros-project-manager added tooling skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 16, 2026 — with Claude
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file labels Aug 16, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 16, 2026 06:41
@os-project-manager
os-project-manager added this pull request to the merge queueAug 16, 2026
Merged via the queue into main with commit 6468df1Aug 16, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-8967-error-status-conformance branch August 16, 2026 07:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

2 participants

@os-project-manager@claude