Skip to content

docs(protocol): the 409 unique-constraint refusal is UNIQUE_VIOLATION on the wire; DUPLICATE_RECORD is the engine's in-process code - #15634

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15362-error-handling-unique-violation
Sep 5, 2026
Merged

docs(protocol): the 409 unique-constraint refusal is UNIQUE_VIOLATION on the wire; DUPLICATE_RECORD is the engine's in-process code#15634
baozhoutao merged 2 commits into
mainfrom
claude/issue-15362-error-handling-unique-violation

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15362

Docs-only. content/docs/protocol/kernel/error-handling.mdx published DUPLICATE_RECORD as a 409 wire code. No route emits that on the wire: the engine's DuplicateRecordError is translated at the REST door and every route answers UNIQUE_VIOLATION. The entry is renamed, the example body is replaced with the one the door actually emits, one sentence names the in-process code so a reader who sees DuplicateRecordError knows why it never appears on the wire, and the rate-limit snippet's comment is corrected the same way.

Blocked on one decision — please read before reviewing

pnpm check:error-status-conformance goes RED on this branch, and the remedy is outside a docs-only PR. Its verdict line:

✗ UNIQUE_VIOLATION: documented with an HTTP status, but no producer declares one — nothing pins the doc's claim on either side, so it can drift with no test to update. Wire a producer that declares status/statusCode (or a sendError door with a literal status). Only if the code is genuinely unemitted today, admit it into scripts/error-status-unpinned-baseline.json — that path WEAKENS this ratchet and is MAINTAINER-ONLY, not a co-equal remedy.

The runtime does emit 409 UNIQUE_VIOLATION — the gate's deriver cannot see it. Rule R5b in scripts/check-error-status-conformance.mjs matches a mapper terminal with a body group whose contents are a negated character class excluding both brace characters, so nested braces disqualify the match. The real terminal in packages/rest/src/error-response.ts (the DuplicateRecordError arm, line 992) builds its body with conditional spreads — ...(field ? { field } : {}) — so the pair straddles a nested brace and R5b skips it. driver-memory's producer is invisible for a different reason: it assigns err.code = UNIQUE_VIOLATION_CODE; err.status = UNIQUE_VIOLATION_STATUS; (packages/drivers/driver-memory/src/memory-unique-constraint.ts:504-505) rather than declaring an object literal, which no rule matches either.

Neither remedy the gate offers belongs in this PR: extending the deriver is a gate-script change (and needs its own --self-test case), and the baseline path is maintainer-only and would record the opposite of the truth. Filed with the full derivation as #15633. This PR stays draft until that is graded.

Tree evidence

The translation site — packages/rest/src/error-response.ts, structuredCodeAnswer's DuplicateRecordError arm:

if (error?.code === 'DUPLICATE_RECORD' && error?.name === 'DuplicateRecordError') {
    const field = typeof error?.field === 'string' && error.field.length > 0 ? error.field : undefined;
    const refused = typeof error?.object === 'string' && error.object.length > 0 ? error.object : object;
    return {
        status: 409,
        body: {
            error: field
                ? `A record with this ${field} already exists`
                : 'A record with this value already exists',
            code: 'UNIQUE_VIOLATION',
            ...
        },
    };
}

Its docblock states the ruling this page now matches:

Maintainer ruling (2026-09-03, #14723): a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION ... the ENGINE's thrown identity is unchanged: DuplicateRecordError.code is still DUPLICATE_RECORD in-process; only what crosses the HTTP boundary spells UNIQUE_VIOLATION.

The route test that pins the 409 body — packages/rest/src/rest-duplicate-record-arm.test.ts, section 1:

it('the body is exactly these keys — nothing from the envelope rides that is not named here', () => {
    const env = envelope('duly_note', sqliteRaw());
    expect(mapDataError(env, 'duly_note').body).toEqual({
        error: CURATED_NAMED,
        code: 'UNIQUE_VIOLATION',
        developerMessage: env.message,
        field: 'email',
        object: 'duly_note',
    });

That is why the example body on the page is now flat with error / code / field / object rather than the page's nested success/error/details envelope with a details.value echo: no route emits details, and the door deliberately withholds the offending value (the driver's error stays on cause). The same flat body is already published at content/docs/protocol/kernel/http-protocol.mdx:757-762.

Grep controls

where DUPLICATE_RECORD UNIQUE_VIOLATION
origin/main (6b8c67778) error-handling.mdx 3 (:349 heading, :358 example code, :950 comment) 0 (git grep exit 1)
this branch (476535c9a) error-handling.mdx 1 — :364, inside the in-process sentence only 4 — :349 heading, :357 example, :365 sentence, :953 comment

Verification

Gate family derived from the tree, not by hand: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack — 38 commands, all run at commit 476535c9a (the branch head; exit codes captured before any pipe).

gate exit verdict
pnpm check:error-status-conformance 1 RED — the blocker above; the self-test itself passed (40 cases)
pnpm check:doc-anchors 0 check-doc-anchors: 307 internal #fragment link(s) across 410 source file(s) all resolve to a real heading
pnpm check:docs-single-h1 0 405 page(s) under content/docs/ carry no body-level heading
pnpm check:nul-bytes 0 OK (scanned 7565 text file(s) ... no raw ASCII control bytes)
pnpm check:doc-authoring 0 14808 customer-facing string(s) across 756 spec sources clean
pnpm check:role-word 0 OK, no new occurrences of the reserved word (236 files, 2 roots)
pnpm --filter @objectstack/spec run check:docs 0 230 generated files in sync with packages/spec
pnpm --filter @objectstack/lint run check:doc-security-posture 0 27 ObjectSchema.create example(s) in 227 marked block(s) ... validate-clean
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 22 record-scoped formula example(s) across 428 files / 1371 TS blocks judged clean
pnpm --filter @objectstack/spec run check:skill-examples 0 257 prose examples type-check across 3 surface(s)
pnpm check:docs-audit-scope 0 see note below
the other 27 derived commands 0 no findings

Notes on the results that were not a first-pass measurement:

  • check:doc-formula-expressions, check:doc-security-posture and check:skill-examples first exited 3 / 3 / 1 with PREREQUISITE NOT MET (unbuilt @objectstack/formula, @objectstack/lint, @objectstack/client-react). Those were NOT MEASURED, not findings; after turbo run build for the three packages all re-ran green, as tabled.
  • check:docs-audit-scope first reported ✗ self-test "every contract declaration admitted is a packages/spec API declaration". Not this diff: the scanner walks the gitignored build artifact packages/spec/.examples-build/ and admits five transpiled doc examples there as contract declarations. With that directory absent — the state CI checks out in — the gate is green on this branch: ✓ affected-docs self-test: 568 cases pass. / ✓ check-audit-scope self-test: 32 cases pass. (exit 0, measured both ways). Already filed as check:skill-examples leaves packages/spec/.examples-build/ behind, and check:docs-audit-scope then fails its own self-test in the same working tree #15446.

scripts/pm/dispatch-gates.mjs also named 3 families whose argv carries a CI-only variable — NOT MEASURED here by construction: check-cross-package-test-inputs.mjs --union-into ..., check-shard-attestation.mjs --emit ..., check-test-completeness.mjs ....

Scope

One file, content/docs/protocol/kernel/error-handling.mdx. No code change, no changeset (skip-changeset: nothing is published from a package). Two adjacent items the card itself recorded as not measured are left alone and filed instead: the page-wide success/error/details envelope, which no route emits either (#15632), and content/docs/api/error-catalog.mdx:355, which still lists DUPLICATE_RECORD under Conflict Errors (409) with no note that the wire spelling differs (#15631). Out of scope here: #14723 is not re-litigated, and the deriver gap (#15633) is left open.

Unblocked by #15633

The deriver gap that reddened check:error-status-conformance on this branch landed on main on 2026-09-05 (PR #15656, f99dbcc45). origin/main (95d5cbb31) is merged into this branch — merge commit 082626dc1ab6145f61c4e51faadb516c93bed4aa, a plain merge, no rebase and no force-push. The three-dot diff against main is unchanged by the merge: still the one file, content/docs/protocol/kernel/error-handling.mdx, 14 insertions / 14 deletions. Nothing in the docs prose was edited in this update.

pnpm check:error-status-conformance is now GREEN at the merged head (exit code captured before any pipe: EXIT=0). Its own lines, verbatim:

scope: 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) a doc page publishes a status for (INVALID_REQUEST, UNIQUE_VIOLATION); 2244 source files scanned; 279 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: 26 code(s) with a derived producer, 27 (code, status) pair(s) matched against the docs.

unpinned: 26 documented code(s) with no derivable producer (baselined: 26).

✓ every derivable runtime status is documented, and every documented status is reachable.

UNIQUE_VIOLATION is now inside the reconciled vocabulary rather than an unpinned documented code, which is exactly what the earlier RED verdict asked for and what this page's rename depends on.

Derived family at the merged head

Re-derived on the merged tree, not carried over: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack at commit 082626dc1 — 39 commands. All 39 exit 0. Exit codes were captured before any pipe; each command's output was redirected to its own file first.

gate exit verdict
pnpm check:error-status-conformance 0 ✓ every derivable runtime status is documented, and every documented status is reachable.
pnpm check:doc-anchors 0 no findings
pnpm check:docs-single-h1 0 no findings
pnpm check:doc-authoring 0 no findings
pnpm check:corpus-claim-drift 0 no findings
pnpm check:docs-transcript-drift 0 measured after building @objectstack/lint (first run exited 3, PREREQUISITE NOT MET — not a finding)
pnpm check:nul-bytes 0 no findings
pnpm check:docs-audit-scope 0 green on this checkout — the packages/spec/.examples-build/ artifact noted in the section above was absent this run
pnpm --filter @objectstack/spec run check:docs 0 measured after gen:schema ran as the first step of the spec build (first run exited 1 on the missing gitignored packages/spec/json-schema tree — NOT MEASURED, not a finding)
pnpm --filter @objectstack/spec run check:skill-examples 0 257 prose examples type-check across 3 surface(s) — measured after building @objectstack/client-react and @objectstack/client
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 measured after building @objectstack/formula
pnpm --filter @objectstack/lint run check:doc-security-posture 0 measured after building @objectstack/lint
the other 27 derived commands 0 no findings

Every prerequisite exit (3, and the two gates that spell theirs as 1) was re-run to a real measurement after the missing build artifact was produced; none is reported here as a green it did not earn. The working tree was clean (git status --porcelain empty) after every build, so no generated artifact rode along in this branch.

Still NOT MEASURED here, by construction: the 3 families whose argv carries a CI-only variable (check-cross-package-test-inputs.mjs --union-into ..., check-shard-attestation.mjs --emit ..., check-test-completeness.mjs ...), plus the always-runs workflow tail that dispatch-gates does not enumerate. Those are CI's to report.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code


Generated by Claude Code

… on the wire; DUPLICATE_RECORD is the engine's in-process code (#15362)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

HOLD (mirror of the ruling on #15362) — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

The docs change in PR #15634 (read against the three-dot diff: #### UNIQUE_VIOLATION at 409, the door's real flat body — error / code / field / object, no success/details envelope — one sentence naming DuplicateRecordError / DUPLICATE_RECORD as the in-process identity translated at error-response.ts:992, and the :950 comment corrected) is CORRECT and stays as written. It reds check:error-status-conformance only because that gate's deriver is blind to both real producers — R5b's { status, body } body group forbids nested braces (the arm at error-response.ts:992 builds its body with conditional spreads) and the err.code = …; err.status = … assignment form (driver-memory/src/memory-unique-constraint.ts:504–505) has no rule at all. The gate's own header says the runtime side is DERIVED, never listed, and an unresolvable declaration is REPORTED, never dropped — a terminal the regex skips is neither.

Option A. #15633 (the deriver gap) is graded domain:devx / priority:p2 (it blocks a p2) and dispatched now as this lane's card: widen R5b to a brace-balanced body span and add a rule for the assignment form, each with a --self-test case both directions, the gate's live verdict moving unpinned: 33 (baselined 32) → the baseline count with UNIQUE_VIOLATION derived. ⛔ Not B (a baseline entry would record "genuinely unemitted" about a code two tests pin as emitted — a maintainer-only ratchet weakening in the wrong direction). ⛔ Not C (stripping the status from a reference page hides the claim the page exists to make).

PR #15634 is HELD as a draft until #15633 lands; then its dev (or a resumed one) merges origin/main, CI re-runs, and this seat accepts the docs diff unchanged. #15631 (error-catalog.mdx) and #15632 (the published envelope matches no route) stay bare for triage — the second is the larger docs defect this card uncovered.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at 082626dc1 (1 file, +14/−14), not the report. Closing keyword: Fixes #15362 only; body first line unchanged; one ## Unblocked by #15633 section appended.

What the diff does (unchanged since the HOLD — the docs were correct all along): content/docs/protocol/kernel/error-handling.mdx renames the 409 entry DUPLICATE_RECORDUNIQUE_VIOLATION, replaces the nested { success, error: {…} } example with the flat wire body (error, code, field, object), adds the sentence that DuplicateRecordError's in-process code is DUPLICATE_RECORD and the REST door translates it at the boundary (with the best-effort field pointer), and fixes the client-snippet comment. The HOLD's reason — the gate's deriver could not see the two UNIQUE_VIOLATION producers — was #15633, landed via #15656 (f99dbcc45); origin/main was MERGED into the branch (merge commit 082626dc1, no rebase, no conflicts).

Measured by this seat on the PR head (plain node, detached worktree; readings copied from the run output):

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); 2244 source files scanned; 279 producer site(s) derived; …
  reconciled: 26 code(s) with a derived producer, 27 (code, status) pair(s) matched against the docs.
  unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
  ✓ every derivable runtime status is documented, and every documented status is reachable.

That is the exact reading the #15633 dev's scratch leg (b) predicted and this seat's post-landing reading on main plus this diff. The other docs gates in the derived family (39 commands, all exit 0 per the dev after building the lint/spec/client closures) are CI's to confirm; TypeScript Type Check already success, Lint & Repo Gates in progress.

Fences held: one docs file; check-governed-merges.mjs --test0 of 1 path(s) hit the register; no other open PR holds it; git merge-tree origin/main clean; skip-changeset (docs only). Flip + arm once both required jobs read success.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 03:47
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 03:48
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 6c08131 Sep 5, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15362-error-handling-unique-violation branch September 5, 2026 04:51
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

2 participants