docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD - #15750
docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD#15750claude[bot] wants to merge 2 commits into
Conversation
… 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
|
ACCEPT — PR #15750 (head Per the #14723 ruling: the 409 entry is renamed Measured by this seat: the deriver fix Flip + arm when both required jobs report success on Generated by Claude Code |
|
ACCEPT WITHDRAWN — CI red is this PR's, and the fix is a contract decision, not a rework. Both required jobs are green ( Generated by Claude Code |
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 mergesorigin/main(no rebase) to pick up #15762 — the deriver fix for #15749 filed from this branch.e52bb6a44is an ancestor of HEAD (git merge-base --is-ancestorexit 0, self-certifying); the diff againstorigin/mainis still the one file.What changed
content/docs/api/error-catalog.mdxcatalogued 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.codestaysDUPLICATE_RECORDin-process only, translated at the REST door. A client branching on the catalogued constant never matched.## Conflict Errors (409)entry is renamed toUNIQUE_VIOLATION, Cause / Fix / Retry kept, with one cross-reference sentence naming the in-process spelling and where the translation happenspackages/restis the judge here, not the patient — nothing underpackages/**is touched.Per-example reconciliation
UNIQUE_VIOLATIONpackages/rest/src/error-response.ts:993— theDuplicateRecordErrorarm ofstructuredCodeAnsweranswersstatus: 409,code: 'UNIQUE_VIOLATION'; pinned key-for-key bypackages/rest/src/rest-duplicate-record-arm.test.tsUNIQUE_VIOLATIONhad 0 occurrences### UNIQUE_VIOLATIONentry + quick-reference rowDUPLICATE_RECORDpackages/objectql/src/duplicate-record-error.ts—readonly code = DUPLICATE_RECORD_CODE, thrown in-process; census ofcode: 'DUPLICATE_RECORD'acrosspackages/**finds no wire producer, only docblocks and that throwLive control for the "0 occurrences" reading: in the same run
UNIQUE_VIOLATIONmatched in six other files undercontent/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 inpackages/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/sendErrorinpackages/types/src/response-envelope.ts:83and:251write{ success: true, data }and{ success: false, error: { code, message, ...extra } }. 95 non-test call sites acrosspackages/rest,packages/services/*andplugins/*.details: YES, elsewhere. Carried insendError'sextra, typed as aPickofApiError's own optionals. Live example:packages/services/service-settings/src/settings-routes.ts:74sends{ details: { namespace: err.namespace } }.requestId: DECLARED, NOT EMITTED. It is onApiErrorSchemaand accepted bysendError'sextra, but no call site supplies one — the only read isresponse-envelope.ts:227, feedinglogServerFault. Nothing puts it on a response body today.timestamp: NOT EMITTED on any error body. Zero occurrences in botherror-response.tsandresponse-envelope.ts; positive control,successmatches 8 times in the latter. It exists asEnhancedApiErrorSchema.timestampand asmeta.timestamponBaseResponseSchema/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.tsreads BOTH envelopes' declared spots, deliberately and by name::6017—asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code):6020—Array.isArray(errorBody?.error?.details?.fields) ? errorBody.error.details.fields:6035—error.category = errorBody?.error?.category:6043—error.details = errorBody?.details ?? errorBody?.error?.details ?? errorBodyThe 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, flatandruntime dispatcher, wrapped.packages/client/src/shares-envelope-compat.test.tspins that dual read.So the nested envelope is not aspirational. It is declared in the spec (
BaseResponseSchema, andenvelopeViolationsREQUIRES the nested form for a failure body: "a failure body must carryerrorwith a stringcodeandmessage— 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.mdxdoes 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:536and:587already publish two nested examples, forGET /api/v1/meta/viewesandPUT /api/v1/meta/fieldz, and those examples are CORRECT for those routes.Rewriting
error-handling.mdxpage-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.mdxpublishes 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/objectstackwere re-derived and re-run on the merged head8888b3862, 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 pathcontent/docs/api/error-catalog.mdx(merge basee52bb6a44).39 of 39 green. The
check:error-status-conformancered is gone. No command reported exit 3 / PREREQUISITE NOT MET on this run;check:skill-examples, which refused on an unbuiltpackages/client-react/distbefore the merge, is green here because the dependency closures were built first.node scripts/check-error-status-conformance.mjs— EXIT=0:--self-test— EXIT=0,58 cases pass.pnpm check:nul-bytes— EXIT=0,scanned 7618 text file(s) ... no raw ASCII control bytes.DUPLICATE_RECORDis no longer counted as a wire producer: the deriver now reports it undertranslatedand 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-currentorigin/mainc2a336ca2— 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_RECORDheading absolved through the weakercoveredgrade) is moot and was never taken.Self-check greps
On
content/docs/api/error-catalog.mdxafter the change:DUPLICATE_RECORDoccurs exactly once, in the cross-reference sentence at:361."success": falseoccurs twice and"error": {twice — both are the/api/v1/meta/*examples at:536and: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": false17,"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
check:error-status-conformanceasDuplicateRecordErrordoes. Measured forDUPLICATE_RECORDonly.packages/client, so the census stopped at the SDK rather than being carried through every app.content/docs/references/api/error-code-ledger.mdxcarries the same in-process/wire confusion. It is GENERATED and out of fences; not read for this purpose.check-cross-package-test-inputs.mjs --union-into,check-shard-attestation.mjs --emit,check-test-completeness.mjs. Named by the deriver as NOT MEASURED.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code