Skip to content

docs(client-sdk): state the two-tier error-code vocabulary and add the VALIDATION_FAILED row - #12469

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12389-error-codes-table
Aug 26, 2026
Merged

docs(client-sdk): state the two-tier error-code vocabulary and add the VALIDATION_FAILED row#12469
yinlianghui merged 1 commit into
mainfrom
claude/issue-12389-error-codes-table

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #12389

What the table was, measured before writing a row

The card asks for a VALIDATION_FAILED row. Before adding one I established
what the ### Error Codes table is a table of, because if its contract were
"the StandardErrorCode enum" then adding a ledger-registered code would make
the table false and the honest deliverable would be the opposite finding.

Measured on this branch's base (68c5dbaab2):

Fact Value
StandardErrorCode members (packages/spec/src/api/errors.zod.ts) 50
Rows in the page's table 9
Rows that are enum members 9 / 9
VALIDATION_FAILED in StandardErrorCode no
VALIDATION_FAILED in ERROR_CODE_LEDGER yes (@objectstack/rest, @objectstack/objectql, …)
ERROR_CODE_LEDGER codes 245

9 of 50 refutes "the table is the enum." The table was a curated subset with
an unstated contract — which is exactly why a reader mistakes it for the
authority. So the fix is the contract plus the row, not the row alone; that
matches what the card itself calls "arguably the real fix."

Changes — confined to the ### Error Codes section

  1. State the two-tier vocabulary (ADR-0112): the closed StandardErrorCode
    catalog, the per-package ERROR_CODE_LEDGER, the exported ErrorCode schema
    as their union, and packages/spec as the authority for the full set. No
    ledger dump — that is the sync-drift shape this page keeps paying for.
  2. A Tier column, so every row says which set it comes from and the table
    can hold both tiers without lying.
  3. The VALIDATION_FAILED row — ledger, 400, carries fields[].
  4. Sharpened VALIDATION_ERROR's description to the distinction that
    actually holds, since a reader picking the wrong one of the two is the
    defect. Both codes are live and they are not synonyms:
    • VALIDATION_ERROR — request-shape refusals: repeated query parameter
      (query-multiplicity.ts:140, package-routes.ts:752), a filter outside
      the allowlist (query-allowlist.ts:196), malformed arguments
      (storage-service.ts:120, metadata-service-contract.ts:105).
    • VALIDATION_FAILED — record-level validation on a write, carrying
      fields[] (error-response.ts:631, rest-server.ts:9671 → 400).
  5. A note that Category/Retryable are semantic, not wire guarantees
    grep -rn "category:" packages/rest/src/error-response.ts returns nothing,
    so the per-field envelope sends neither. This closes the gap between the
    table's columns and the narrowing example's "optional — set only when the
    server sent it".

Every other code this page's examples use is also a ledger code

Counted across the whole page (dispatch Zone 2c). Codes appearing outside the
table: VALIDATION_FAILED (:619, :632, :655), ROLLED_BACK / NOT_ATTEMPTED
(:579-580, batch rows), FLOW_DISABLED / FLOW_FAILED / FLOW_NO_START_NODE
(:436-438, the automation example). All six are ledger-registered and none is
in StandardErrorCode
— which is itself the argument for the union note
rather than a row-by-row catch-up. Their omission is no longer misleading once
the table declares itself a subset, so no further rows were added.

⚠️ Nothing verifies this table

Stated plainly rather than implied: the table's rows are hand-authored prose.
There is no generated source for the Category/Retryable columns anywhere in
packages/spec, and this PR deliberately adds no os:check markers — that
is the subject of #11942, which is pm:blocked. The row is measured against the
spec by hand, and nothing in CI will notice if it drifts.

Verification

Docs-only; no changeset (this PR publishes nothing — skip-changeset).

Gate union re-derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
(23 families) and run at the final commit 5b76c2b049, plus
check-nul-bytes — 24/24 green, exit codes captured before any pipe. Verdict
lines from the gates themselves:

✓ check-docs-single-h1: 180 page(s) under content/docs/ carry no body-level `# ` heading
✅ check-doc-anchors: 278 internal fragment link(s) across 408 source file(s) all resolve to a real heading
✓ doc authoring guard: 390 files clean — no bare metadata literals.
✓ check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 403, content/blog 3.
✅ 229 generated files in sync with packages/spec        (spec check:docs)
   260 marked example(s) across 101 file(s), 3 surface(s)  (spec check:skill-examples, green)
TOTAL FAILED=0

Repo-wide pnpm lint was narrowed, and the narrowing is measured, not
assumed
: eslint.config.mjs contains the string mdx zero times and no
files glob covers .mdx, so ESLint's own population excludes the one file
this PR touches — pnpm exec eslint content/docs/api/client-sdk.mdx --no-inline-config --format json reports 1 file, errorCount: 0, message
"File ignored because no matching configuration was supplied." No type-aware
linting is configured, and this diff changes no ESLint config, so it cannot
move a verdict on any untouched file either. CI runs the full farm regardless.

Scope

Only the ### Error Codes section is touched (35 insertions, 11 deletions, one
file). The example blocks are untouched — the remaining one belongs to #12388
and content/docs/releases/** is not involved.

Generated by Claude Code


Generated by Claude Code

…e VALIDATION_FAILED row

The `### Error Codes` table listed nine codes, all members of the closed
`StandardErrorCode` enum, with no statement of what it enumerates. It omitted
`VALIDATION_FAILED` — the ledger-registered code both of the page's own
error-handling examples branch on — so a reader working from the table
concludes a per-field validation failure arrives as `VALIDATION_ERROR` and
writes a branch that never matches.

Measured first, because adding the row to a table of enum members would have
made the table false: the table is 9 of the enum's 50 members, so it was never
"the enum" — it is a curated subset with an unstated contract. The fix is
therefore the contract plus the row, not the row alone.

- State the two-tier vocabulary (ADR-0112): the closed `StandardErrorCode`
  catalog plus the per-package `ERROR_CODE_LEDGER`, with the exported
  `ErrorCode` schema as their union and `packages/spec` as the authority.
- Add a `Tier` column so each row says which set it comes from, and add the
  `VALIDATION_FAILED` row (ledger, 400).
- Sharpen `VALIDATION_ERROR`'s description to the distinction that actually
  holds on the wire: request-shape refusals (repeated query parameter,
  disallowed filter, malformed argument) versus record-level validation, which
  answers `VALIDATION_FAILED` and carries `fields[]`.
- Note that `Category`/`Retryable` are semantic classification, not wire
  guarantees — `packages/rest` sets neither on the per-field envelope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghui yinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Review — devx@objectstack seat (#6023), R10. Landing on the gate only.

Zone 2a asked you to read what the table is a table OF. You did, and the answer changed the fix

I warned that adding a row might make the table false: VALIDATION_FAILED is not in the StandardErrorCode enum, so if the table's contract were "that enum", the honest finding would have been the opposite one. You found the actual contract — a two-tier vocabulary — and verified at source by this seat:

error-code-ledger.zod.ts:668
export const ErrorCode = z.enum(
  [...StandardErrorCode.options, ...REGISTERED_ERROR_CODES] as [string, ...string[]]
) as z.ZodType<StandardErrorCode | RegisteredErrorCode>;
:672  export type ErrorCode = StandardErrorCode | RegisteredErrorCode;

Union at both the schema and the type. VALIDATION_FAILED appears in the ledger (positive control: other codes match the same search). So the row belongs — and it needed the tier distinction to belong honestly. You did both rather than picking one.

⭐ The correction you found is worth more than the row

The old table listed VALIDATION_ERROR as "Input validation failed". That description sends a reader branching on the wrong code:

what it actually is
VALIDATION_ERROR the request was refused before any record was validated
VALIDATION_FAILED a record failed validation on a write; carries fields[]

The card was filed as an omission. It was also a misdescription — the page's own examples branch on VALIDATION_FAILED while its table taught VALIDATION_ERROR as the validation code. A reader following the table would have written a branch that never fires. Finding that is the difference between filling a gap and reading the page.

The subset disclaimer is the right shape

Saying the table is "a hand-picked subset of the codes you are most likely to branch on, not either tier in full" stops the next reader from treating it as a census — the exact failure this seat keeps pricing elsewhere: a partial reading presented as complete. ⛔ It also means nobody will later "fix" the table by asserting it enumerates something it does not.

And the closing caveat — Category / Retryable are the semantic classification, neither guaranteed on the wire, with the REST per-field envelope sending neither — is consistent with what #12387 established about error.category being present only when the server sent it. Two docs pages that now agree.

Zone 1 held

⛔ No os:check markers added (#11942's subject, blocked). ⛔ Not content/docs/releases/**. ⛔ The example blocks are untouched — #12388 still owns the remaining one and is held behind you. One file, +35/−11.

Landing condition

Lint & Repo Gates completed + success, read by name. Nothing else outstanding.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 26, 2026 04:57
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Landing condition met — armed.

Lint & Repo Gates read by name on this PR's head commit: status: completed, conclusion: success. ⛔ Not from an aggregate, ⛔ not from check_suite.completed.

Marked ready for review and auto-merge enabled (SQUASH requested; the MERGE echo is cosmetic — verified against origin/main, whose last 30 commits are all single-parent).

Two notes from your report, both accepted as recorded and neither blocking:

  • The duplicated _Generated by [Claude Code]_ footer in the PR body is a platform artifact at creation time. Recorded, not corrected — rewriting the body to fix a cosmetic duplicate is not worth the churn on an armed PR.
  • The report comment's HTML marker was sanitized after landing, so the authoritative report is comment 5420737460, which opens with the literal text os-dev-report. Noted for whoever reads this card next.

⭐ Your Zone 1 Ruling 2 statement stands as the most useful line in the report: no os:check markers were added, there is no generated source for the Category/Retryable columns, and therefore nothing in CI verifies this table — it is hand-measured and CI will not notice if it drifts. That is the honest disclosure, and it is a better outcome than a table that looks checked and is not.


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

[finding] The Error Codes table on content/docs/api/client-sdk.mdx omits VALIDATION_FAILED — the one code the page's own examples branch on

2 participants