Skip to content

docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD - #15750

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-15631-15632-error-docs-wire-shape
Draft

docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD#15750
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-15631-15632-error-docs-wire-shape

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15631

Card #15632 is measured but NOT changed on this branch — its premise did not survive the measurement it asked for, and it stays open for grading. Details under "Measurement 2" below.

Docs-only. Verified at 8888b3862, which merges origin/main (no rebase) to pick up #15762 — the deriver fix for #15749 filed from this branch. e52bb6a44 is an ancestor of HEAD (git merge-base --is-ancestor exit 0, self-certifying); the diff against origin/main is still the one file.

What changed

content/docs/api/error-catalog.mdx catalogued the 409 unique-constraint refusal under the engine's in-process spelling. Per the maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION; DuplicateRecordError.code stays DUPLICATE_RECORD in-process only, translated at the REST door. A client branching on the catalogued constant never matched.

  • the ## Conflict Errors (409) entry is renamed to UNIQUE_VIOLATION, Cause / Fix / Retry kept, with one cross-reference sentence naming the in-process spelling and where the translation happens
  • the HTTP Status Quick Reference 409 row follows

packages/rest is the judge here, not the patient — nothing under packages/** is touched.

Per-example reconciliation

Code Status Judge Before After
UNIQUE_VIOLATION 409 packages/rest/src/error-response.ts:993 — the DuplicateRecordError arm of structuredCodeAnswer answers status: 409, code: 'UNIQUE_VIOLATION'; pinned key-for-key by packages/rest/src/rest-duplicate-record-arm.test.ts entry absent from the page; UNIQUE_VIOLATION had 0 occurrences ### UNIQUE_VIOLATION entry + quick-reference row
DUPLICATE_RECORD none on the wire packages/objectql/src/duplicate-record-error.tsreadonly code = DUPLICATE_RECORD_CODE, thrown in-process; census of code: 'DUPLICATE_RECORD' across packages/** finds no wire producer, only docblocks and that throw catalogued as a 409 wire code (entry + quick-reference row) named once, in prose, as the in-process spelling only

Live control for the "0 occurrences" reading: in the same run UNIQUE_VIOLATION matched in six other files under content/docs/ (http-protocol.mdx, error-handling.mdx, contract.mdx, error-code-ledger.mdx, drivers.mdx, releases/v17.mdx) — the zero was the page, not the instrument.

Measurement 1 — does any producer emit success / details / requestId / timestamp?

Asked of the judge first, then repo-wide.

  • structuredCodeAnswer / resolveErrorResponse: NO. Zero occurrences of all four in any emitted body in packages/rest/src/error-response.ts (the four hits in that file are prose in docblocks). Positive control: code: matches 37 times in the same file, so the grep was not blind.
  • success: YES, elsewhere. sendOk / sendError in packages/types/src/response-envelope.ts:83 and :251 write { success: true, data } and { success: false, error: { code, message, ...extra } }. 95 non-test call sites across packages/rest, packages/services/* and plugins/*.
  • details: YES, elsewhere. Carried in sendError's extra, typed as a Pick of ApiError's own optionals. Live example: packages/services/service-settings/src/settings-routes.ts:74 sends { details: { namespace: err.namespace } }.
  • requestId: DECLARED, NOT EMITTED. It is on ApiErrorSchema and accepted by sendError's extra, but no call site supplies one — the only read is response-envelope.ts:227, feeding logServerFault. Nothing puts it on a response body today.
  • timestamp: NOT EMITTED on any error body. Zero occurrences in both error-response.ts and response-envelope.ts; positive control, success matches 8 times in the latter. It exists as EnhancedApiErrorSchema.timestamp and as meta.timestamp on BaseResponseSchema / ErrorResponseSchema, both unset by any producer found.

Measurement 2 — does a real consumer read the nested shape? YES, and it stops the #15632 rewrite

This was the dispatch's stop condition, and it is met.

packages/client/src/index.ts reads BOTH envelopes' declared spots, deliberately and by name:

  • :6017asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code)
  • :6020Array.isArray(errorBody?.error?.details?.fields) ? errorBody.error.details.fields
  • :6035error.category = errorBody?.error?.category
  • :6043error.details = errorBody?.details ?? errorBody?.error?.details ?? errorBody

The comment above them states the reason verbatim: "The two reads below are the two LIVE envelopes' declared spots, not a fallback chain — the flat shape's retirement belongs to the envelope-convergence line (#3843)." It names both dialects explicitly: @objectstack/rest, flat and runtime dispatcher, wrapped. packages/client/src/shares-envelope-compat.test.ts pins that dual read.

So the nested envelope is not aspirational. It is declared in the spec (BaseResponseSchema, and envelopeViolations REQUIRES the nested form for a failure body: "a failure body must carry error with a string code and message — the nested form"), it has one shared writer, ~95 producer call sites, a gate ratchet (check:route-envelope), and a real SDK consumer.

This falsifies #15632's premise. The envelope published on error-handling.mdx does not match "no route's response body" — it matches the whole dispatcher and services route family. The strongest single piece of evidence is on the OTHER page in this dispatch: content/docs/api/error-catalog.mdx:536 and :587 already publish two nested examples, for GET /api/v1/meta/viewes and PUT /api/v1/meta/fieldz, and those examples are CORRECT for those routes.

Rewriting error-handling.mdx page-wide to the flat body would therefore have replaced one wrong claim with another. No bytes of that page are touched here.

What #15632 actually reports, restated from the measurement: the platform has TWO live error envelopes on different route families, and error-handling.mdx publishes one of them under the universally-quantified heading "Every error follows this structure". That is a contract-and-docs-architecture question, not a docs typo — which is why it comes back for grading rather than as a rewrite.

Gate verdicts

All 39 commands derived by node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack were re-derived and re-run on the merged head 8888b3862, exit code captured before any pipe. The derived command list is byte-identical to the pre-merge derivation, and the change set is still the single path content/docs/api/error-catalog.mdx (merge base e52bb6a44).

39 of 39 green. The check:error-status-conformance red is gone. No command reported exit 3 / PREREQUISITE NOT MET on this run; check:skill-examples, which refused on an unbuilt packages/client-react/dist before the merge, is green here because the dependency closures were built first.

node scripts/check-error-status-conformance.mjs — EXIT=0:

  scope: 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) a doc page publishes a status for (INVALID_REQUEST, UNIQUE_VIOLATION); 2246 source files scanned; 314 producer site(s) derived; 83 further ledger code(s) derived but NOT reconciled — no scanned page publishes a status for them, so there is nothing to reconcile them against.
  reconciled: 25 code(s) with a derived producer, 26 (code, status) pair(s) matched against the docs.
  unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
  translated: 1 class declaration(s) a door translates away before HTTP, so the thrown code is an in-process contract and NOT a wire producer (reported, and kept out of the unpinned census — the door pins the answer under its wire spelling) —
      DUPLICATE_RECORD (declared 409 at packages/objectql/src/duplicate-record-error.ts: class DuplicateRecordError) → UNIQUE_VIOLATION at packages/rest/src/error-response.ts:992
✓ every derivable runtime status is documented, and every documented status is reachable.

--self-test — EXIT=0, 58 cases pass. pnpm check:nul-bytes — EXIT=0, scanned 7618 text file(s) ... no raw ASCII control bytes.

DUPLICATE_RECORD is no longer counted as a wire producer: the deriver now reports it under translated and names the door that performs the translation. That is exactly the distinction this branch's docs change asserts, so the gate and the page now agree instead of the gate requiring the page to publish a status for a code that never crosses HTTP.

For the record, since it is what #15749 was filed on: before the merge this same gate read ✗ DUPLICATE_RECORD: the runtime can emit HTTP 409, and no doc publishes that status for it (documented: nothing). and ✗ check:error-status-conformance — 1 finding(s). on this branch, against exit 0 on the then-current origin/main c2a336ca2 — i.e. the gate was green only because the catalog published the falsehood this PR removes. #15762 fixed the deriver rather than the docs bending to it, and the workaround weighed in the earlier revision of this section (a bare ### DUPLICATE_RECORD heading absolved through the weaker covered grade) is moot and was never taken.

Self-check greps

On content/docs/api/error-catalog.mdx after the change: DUPLICATE_RECORD occurs exactly once, in the cross-reference sentence at :361. "success": false occurs twice and "error": { twice — both are the /api/v1/meta/* examples at :536 and :587, which are correct nested bodies for those routes and are deliberately left alone (see Measurement 2).

On content/docs/protocol/kernel/error-handling.mdx: unchanged on this branch — "success": false 17, "error": { 22, all still nested, pending the #15632 grading.

The dispatch's expected self-check was "zero occurrences on both pages after the rewrite". That expectation belonged to a rewrite that the measurement stopped, so the real counts are reported instead of the expected ones.

NOT MEASURED

  • Whether other in-process-only error classes sit in the same relation to check:error-status-conformance as DuplicateRecordError does. Measured for DUPLICATE_RECORD only.
  • Whether the Console / Studio apps or the examples read the nested shape. The stop condition was already met by packages/client, so the census stopped at the SDK rather than being carried through every app.
  • Whether content/docs/references/api/error-code-ledger.mdx carries the same in-process/wire confusion. It is GENERATED and out of fences; not read for this purpose.
  • Three families that take a value from the workflow and have no value outside a CI run: check-cross-package-test-inputs.mjs --union-into, check-shard-attestation.mjs --emit, check-test-completeness.mjs. Named by the deriver as NOT MEASURED.
  • The always-runs tail (workflows with no path filter) is not in the derived list and was not run locally; CI covers it.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

… wire (#15631)

`content/docs/api/error-catalog.mdx` catalogued `DUPLICATE_RECORD` under
`## Conflict Errors (409)` and in the HTTP Status Quick Reference. Per the
maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE
wire spelling on every route, `UNIQUE_VIOLATION`; `DuplicateRecordError.code`
stays `DUPLICATE_RECORD` in-process only, translated at the REST door
(`packages/rest/src/error-response.ts`, the `DuplicateRecordError` arm of
`structuredCodeAnswer`). A client branching on the catalogued constant never
matched.

The entry is renamed to the wire code with a one-sentence cross-reference to
the in-process spelling, and the quick-reference row follows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Brings in #15762 (#15749), the error-status-conformance deriver fix that stops
counting a door-translated in-process class as a wire producer.

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

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #15750 (head 8888b3862, content/docs/api/error-catalog.mdx +7/−2) reviewed against the GitHub three-dot diff on the merged tree, not the report.

Per the #14723 ruling: the 409 entry is renamed UNIQUE_VIOLATION with a one-sentence cross-reference (engine throws DuplicateRecordError with in-process DUPLICATE_RECORD; the REST door translates at the boundary), and the quick-reference row follows; DUPLICATE_RECORD occurs once on the page (the cross-reference), no packages/** bytes. Fixes #15631 only — #15632 stays in the decision box with zero bytes of error-handling.mdx touched.

Measured by this seat: the deriver fix e52bb6a44 (#15762) is an ancestor of the head; on a detached probe of the branch, node scripts/check-error-status-conformance.mjs → EXIT=0, DUPLICATE_RECORD (declared 409 at packages/objectql/src/duplicate-record-error.ts: class DuplicateRecordError) → UNIQUE_VIOLATION at packages/rest/src/error-response.ts:992 under the translated: census and ✓ every derivable runtime status is documented, and every documented status is reachable. — the red the earlier head carried is gone by the deriver, not by a bare heading; git merge-tree --write-tree origin/main <branch> → EXIT=0; governed test per the earlier report NOT governed (content/docs/api/**). NOT MEASURED here: the 39-command derived family the dev re-ran on the merged head (39/39 exit 0, closures built through the lock) — the report is the record.

Flip + arm when both required jobs report success on 8888b3862. On landing #15631 closes.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT WITHDRAWN — CI red is this PR's, and the fix is a contract decision, not a rework. Test Core (1/6) on 8888b3862 fails in packages/spec/src/api/error-catalog-docs.test.ts (the ADR-0112 D7 guard: every ### \CODE`heading is aStandardErrorCode` member and every member has a heading), read from the job log:

docs heading `UNIQUE_VIOLATION` is not in StandardErrorCode: expected false to be true
StandardErrorCode member `DUPLICATE_RECORD` has no docs entry: expected false to be true

Both required jobs are green (TypeScript Type Check success; Lint & Repo Gates still running at withdrawal), and Test Core (1/6) is green on main a84e1ced6 and on the other queued heads, so the red is not the base's. It is the D7 guard meeting the #14723 ruling: the enum keeps DUPLICATE_RECORD as an in-process member the door translates away, and the wire spelling UNIQUE_VIOLATION is a ledger code, not an enum member — the guard's premise ("the page and the enum can never disagree about which codes exist") no longer holds for a translated code. The existing INVALID_REQUEST ledger headings escape the guard only because they carry a suffix the regex does not match, which is an accident, not a design. The PR is HELD (draft) — nothing to re-run until the decision below is taken. The bare ### DUPLICATE_RECORD heading remains refused (#15631's ruling).


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/xs 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] content/docs/api/error-catalog.mdx lists DUPLICATE_RECORD under Conflict Errors (409) with no note that the wire spelling is UNIQUE_VIOLATION

1 participant