Repository navigation
[finding] content/docs/protocol/kernel/error-handling.mdx documents DUPLICATE_RECORD as a 409 HTTP response code, while every route reports the conflict as UNIQUE_VIOLATION #15362
Description
Observation recorded while implementing #14723 (maintainer ruling A: a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION; the engine's thrown DuplicateRecordError.code stays DUPLICATE_RECORD in-process). Filed unassigned; not addressed by the #14723 PR, whose surface is code, pins and changesets.
What the page says
content/docs/protocol/kernel/error-handling.mdx, section ## Error Codes / ### Resource Errors, at line 349 on origin/main 2ed6be649:
#### DUPLICATE_RECORD— "HTTP Status: 409", "Meaning: Resource with unique constraint already exists", with an example HTTP body whoseerror.codeis"DUPLICATE_RECORD"(and adetailsobject carryingresource/constraint/field/value).- Line 950, in the rate-limiting section:
// Response: "DUPLICATE_RECORD" or "VALIDATION_ERROR"for a registration attempt.
What the platform does
content/docs/protocol/kernel/http-protocol.mdxlines 759 and 775 document the 409 constraint-violation body as"code": "UNIQUE_VIOLATION"— the spelling the ruling on rest/metadata-protocol: one unique conflict, two wire spellings on ONE route — a batch/import ROW reportsDUPLICATE_RECORDwhile the whole-request failure on the same route reportsUNIQUE_VIOLATION#14723 names as the published one.- Every REST route answers a unique-constraint refusal as
UNIQUE_VIOLATION: the single-record door (唯一约束冲突没有单一判别谓词:仓内四套各自为政的方言词表,REST 的 409 映射漏掉 MySQL(Duplicate entry落成 500 INTERNAL_ERROR) #6250 / REST: the UNIQUE_VIOLATION 409 message is hard-coded English and carries no field — and it is less informative than the bulk path's own message for the same constraint #7821 /mapDataErrorhas noDUPLICATE_RECORDarm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping thefieldkey and the user-facing conflict sentence #14389), the whole-request bulk / import / metadata / UI doors (Routes exiting throughhandleRouteErroranswer the engine'sDUPLICATE_RECORDenvelope fromresolveErrorResponse's.statuspassthrough —fieldandobjectdropped,mapDataError's structured 409 arms never consulted #14541), and after rest/metadata-protocol: one unique conflict, two wire spellings on ONE route — a batch/import ROW reportsDUPLICATE_RECORDwhile the whole-request failure on the same route reportsUNIQUE_VIOLATION#14723 the per-row reports ofPOST /api/v1/data/:object/batchand the import runner. DUPLICATE_RECORDis the engine's in-process identity (a hook or a flow node catchingDuplicateRecordErrorsees it —content/docs/automation/flows.mdxline 1124 documents that correctly), never an HTTP body'scode.
So the error-handling page describes a wire body no route emits, and the example's details.value echoes the offending value, which the platform sentence deliberately withholds (the driver's error stays on cause).
Not measured here
- Whether the
{ "success": false, "error": { ... } }envelope shape on that page matches today's ADR-0112 body at all — the whole section may predate the current envelope; this card records only the code spelling. - Whether
content/docs/api/error-catalog.mdxline 355 (### DUPLICATE_RECORD, "A record with the same unique key already exists") needs a note distinguishing the in-process code from the wire spelling; it is a catalog entry, not an HTTP example, so it may be fine as-is.
Suggested disposition
Docs-only: rewrite the DUPLICATE_RECORD entry on error-handling.mdx to say the 409 wire body carries UNIQUE_VIOLATION (cross-reference http-protocol.mdx), keep DUPLICATE_RECORD documented as the engine-envelope code an in-process caller branches on, and drop the value echo from the example. Leave for triage to grade.
Activity
os-zhuang commentedon Sep 4, 2026
分诊 ·
domain:devx/priority:p2/pm:queueAnchor read, not guessed. The deliverable is prose in
content/docs/protocol/kernel/error-handling.mdx, so it lands in the content-docs surface ⇒domain:devx.Re-measured on
origin/mainf1d7872(2026-09-04T23:36:33Z):DUPLICATE_RECORDinerror-handling.mdx:349(the####heading),:358(the example body'serror.code),:950(the rate-limit comment)UNIQUE_VIOLATIONinerror-handling.mdxUNIQUE_VIOLATIONinhttp-protocol.mdx:759,:775) ⇒ the zero above is a real absence, not a broken patternpackages/rest/srcUNIQUE_VIOLATION;error-response.ts:806and:870say in so many words that the in-process code isDUPLICATE_RECORD"instead of the wire'sUNIQUE_VIOLATION"So the page is not merely under-specified — the tree already carries the sentence that contradicts it, written into the door that does the translating.
Grade — p2, not p3. This is a protocol reference page, and its failure mode is silent on our side and loud on the integrator's: a client written against
#### DUPLICATE_RECORDinstalls a 409 handler that never fires, and the fallback path swallows a conflict as an unknown error. A stale sentence nobody derives from would be p3; a stale sentence a client branches on is p2.Boundary test — Bug, not Feature. Nothing here widens the accept set or the authorable surface; it restores documented = emitted. No manual floor. The filer's own disposition is the right one, with one addition:
#### DUPLICATE_RECORDto say the 409 wire body carriesUNIQUE_VIOLATION, cross-referencinghttp-protocol.mdx:759;DUPLICATE_RECORDdocumented as the in-process engine code a hook or flow node branches on (content/docs/automation/flows.mdx:1124already gets this right — copy its framing rather than inventing one);details.valueecho from the example, per the platform sentence that withholds the offending value;:950's comment in the same pass.Deliberately left open for whoever takes it (the filer flagged both; triage is not pre-empting either): whether the
{ success:false, error:{…} }envelope on that page still matches ADR-0112, and whethercontent/docs/api/error-catalog.mdx:355needs an in-process-vs-wire note. Both are in scope if the reader finds them wrong, but neither is required for this card to close.⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk, 2026-09-05T00:04Z — dispatching to anos-devsubagent on branchclaude/issue-15362-error-handling-unique-violation.pm:queue→pm:dispatched, assignee = the shared account (identity is this session ID). Hot-file check:content/docs/protocol/kernel/error-handling.mdxis in no open PR. Ruling: the page documents the WIRE spellingUNIQUE_VIOLATIONat 409 (http-protocol.mdx :759/:775 anderror-response.ts:806/:870are the record), withDUPLICATE_RECORDnamed as the in-process engine code only. Live devs after this batch: 5/5.Generated by Claude Code
docs(protocol): the 409 unique-constraint refusal is UNIQUE_VIOLATION…
baozhoutao commentedon Sep 5, 2026
os-dev-report
{ "issue": 15362, "status": "needs_decision", "branch": "claude/issue-15362-error-handling-unique-violation", "pr": "https://github.com/objectstack-ai/objectstack/pull/15634", "premise_still_valid": true, "summary": "Premise verified on the tree, not the card. On origin/main 6b8c67778, content/docs/protocol/kernel/error-handling.mdx had DUPLICATE_RECORD 3x (:349 heading, :358 example code, :950 comment) and UNIQUE_VIOLATION 0x; packages/rest/src/error-response.ts:992 translates the engine envelope to status 409 / code UNIQUE_VIOLATION, and no route answers DUPLICATE_RECORD on the wire unless a non-engine producer (a sandbox hook) throws that registered code itself, which is a different producer by design. Rewrote the entry as #### UNIQUE_VIOLATION (HTTP 409), replaced the example with the door's real flat body (no details object, no value echo — the page's nested envelope emits nowhere), added one sentence naming DuplicateRecordError / DUPLICATE_RECORD as the in-process identity translated at the boundary, and corrected the :950 rate-limit comment. The page has no error-code index row for this code (only error-catalog.mdx does, filed as #15631). One file, no code change, skip-changeset applied and read back. DECISION NEEDED: the correct docs claim reds check:error-status-conformance, whose deriver cannot see the UNIQUE_VIOLATION producer; the PR is draft and must not land until that is graded (#15633).", "tests": "Gate family DERIVED, not hand-listed: `node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack` (exit 0; 38 commands; it confirmed the repo assertion against this checkout's origin). All 38 run at commit 476535c9a = branch head, exit codes captured by redirect BEFORE any pipe. 37 exit 0, 1 exit 1. RED (caused by this diff): `pnpm check:error-status-conformance` exit 1 — its own --self-test passed first (`✓ ... 40 cases pass`), then `✗ UNIQUE_VIOLATION: documented with an HTTP status, but no producer declares one`; scope line moved from `51 code(s) reconciled ... + 1 ledger code(s) (INVALID_REQUEST)` on main to `52 ... + 2 (INVALID_REQUEST, UNIQUE_VIOLATION)` on the branch. Root cause derived, not guessed: R5b matches `status: NNN, body: { ... }` with a brace-free body group, and the real terminal at error-response.ts:992 uses conditional spreads, so the pair straddles a nested brace; driver-memory's producer (memory-unique-constraint.ts:504-505) declares code/status by assignment, which no rule matches. GREEN highlights (verdict lines as the gates printed them): check:doc-anchors `✅ check-doc-anchors: 307 internal #fragment link(s) across 410 source file(s) all resolve to a real heading`; check:docs-single-h1 `✓ ... 405 page(s) under content/docs/ carry no body-level heading`; check:nul-bytes `check-nul-bytes: OK (scanned 7565 text file(s) ... no raw ASCII control bytes)`; spec check:docs `✅ 230 generated files in sync with packages/spec`; check:doc-authoring `✓ ... 14808 customer-facing string(s) across 756 spec sources clean`; check:role-word `check-role-word: OK, no new occurrences of the reserved word`; check:doc-formula-expressions `✓ ... 22 record-scoped formula example(s) across 428 files / 1371 TS blocks judged clean`; check:doc-security-posture `✅ 27 ObjectSchema.create example(s) in 227 marked block(s) ... validate-clean`; check:skill-examples `✅ 257 prose examples type-check across 3 surface(s)`. NOT MEASURED on first pass, then measured: check:doc-formula-expressions and check:doc-security-posture exited 3 with `PREREQUISITE NOT MET` (unbuilt @objectstack/formula / @objectstack/lint) and check:skill-examples exited 1 on `packages/client-react/dist holds no .d.ts declarations`; after `turbo run build` for those three packages all three re-ran green (tabled above). NOT MEASURED and NOT this diff: check:docs-audit-scope first failed its self-test (`✗ ... every contract declaration admitted is a packages/spec API declaration`); cause derived by a trap-guarded probe — the scanner walks the gitignored artifact packages/spec/.examples-build/ and admits 5 transpiled doc examples there as contract declarations. Control: with that directory moved aside the gate is green on this branch (`✓ affected-docs self-test: 568 cases pass.` / `✓ check-audit-scope self-test: 32 cases pass.`, exit 0), and it is green in the older shared checkout, which has no such directory; already filed as #15446. The probe restored the script and the restore was PROVEN, not assumed: HEAD blob a41faf9c27433c54c78270acbc0720c5df55b97d == on-disk `git hash-object` after restore, `git diff HEAD` empty, marker grep count 1 after mutation and 0 after restore. NOT MEASURED by construction: 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). Grep controls: origin/main 6b8c67778 error-handling.mdx — DUPLICATE_RECORD 3 occurrences (:349, :358, :950), UNIQUE_VIOLATION 0 (`git grep` exit 1); branch 476535c9a — DUPLICATE_RECORD 1 occurrence (:364, inside the in-process sentence only), UNIQUE_VIOLATION 4 (:349 heading, :357 example, :365 sentence, :953 comment). Heavy runs went through `scripts/pm/os-verify-lock.sh` with OS_VERIFY_LOCK_SLOT=issue-15362; first sweep `VERDICT command-exit 0 · held the lock 298s · waited 158s`, second attempt `VERDICT queue-timeout (exit 99)` (slot kept, NOT MEASURED, re-acquired and completed on retry). No ablation was performed — this is a docs diff with no code path to ablate.", "mcp_calls": "6 — REST was the channel for all reads (repo-scoped GET probe returned 200) and for the label write; the auto-mode classifier blocked every write-shaped Bash invocation (issue creation via curl and the jq/python payload builders), so the three findings, the PR and this comment went through MCP github tools instead. Declared channel switch, writes only. GitHub's /search/issues is unavailable to this session (`sessions are bound to their configured repositories`), so the duplicate check ran on two pages of the repo-scoped open-issue list endpoint plus a local grep, covering #15223..#15628.", "open_questions": [ { "question": "Documenting the true wire status for UNIQUE_VIOLATION reds check:error-status-conformance, because its deriver cannot see the producer (R5b's body group forbids nested braces, and driver-memory declares code/status by assignment). The runtime does emit 409 UNIQUE_VIOLATION and tests pin it. How should this land?", "options": [ "A — extend the deriver in scripts/check-error-status-conformance.mjs (widen R5b to a brace-balanced body span, plus a rule for the `err.code = X; err.status = Y` assignment form), with a --self-test case per shape as the script's header requires; dispatch as its own card (#15633), then this PR goes green unchanged.", "B — maintainer adds UNIQUE_VIOLATION to scripts/error-status-unpinned-baseline.json. The script marks that path MAINTAINER-ONLY and ratchet-weakening, and the entry would record the opposite of the truth (the baseline means 'genuinely unemitted today').", "C — publish the entry without a `**HTTP Status:**` line so the gate parses no claim. Routes around the gate and strips the 409 from an error-code reference page; recorded only for completeness." ], "recommendation": "A. The gate's own header states the invariant this violates — 'The runtime side is DERIVED, never listed ... Where an identifier cannot be resolved the declaration is REPORTED as unresolved, never silently dropped — a deriver that goes quietly blind is the same failure one layer down.' A terminal skipped by the regex is neither derived nor reported, so B would ratify a blind spot with a false statement and C would hide it. A also closes the class: any mapper terminal with a conditional-spread body is invisible today, not just this one." } ], "out_of_scope_findings": [ "filed as #15631: content/docs/api/error-catalog.mdx:355 lists DUPLICATE_RECORD under Conflict Errors (409) and in the 409 quick-reference row with no note that the wire spelling is UNIQUE_VIOLATION — the card recorded this page as not measured", "filed as #15632: the `{ success, error: { code, message, details } }` envelope published across error-handling.mdx matches no route's body (routes emit a flat body; no arm emits success/details/requestId/timestamp) — the card recorded the envelope question as not measured", "filed as #15633: check:error-status-conformance cannot derive the UNIQUE_VIOLATION producer — R5b's body group forbids nested braces and the assignment form has no rule; this is the blocker above", "already filed as #15446 (not re-filed): check:skill-examples leaves packages/spec/.examples-build/ behind and check:docs-audit-scope then fails its own self-test — reproduced and controlled here" ] }Generated by Claude Code
claude commentedon Sep 5, 2026
Ruling — PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk.The docs change in PR #15634 (read against the three-dot diff:
#### UNIQUE_VIOLATIONat 409, the door's real flat body —error/code/field/object, nosuccess/detailsenvelope — one sentence namingDuplicateRecordError/DUPLICATE_RECORDas the in-process identity translated aterror-response.ts:992, and the:950comment corrected) is CORRECT and stays as written. It redscheck:error-status-conformanceonly because that gate's deriver is blind to both real producers — R5b's{ status, body }body group forbids nested braces (the arm aterror-response.ts:992builds its body with conditional spreads) and theerr.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-testcase both directions, the gate's live verdict movingunpinned: 33 (baselined 32)→ the baseline count withUNIQUE_VIOLATIONderived. ⛔ 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 commentedon Sep 5, 2026
The #15633 deriver fix is implemented and pushed on
claude/issue-15633-error-status-deriver(cd26dd1), but it is NOT yet a PR — it is parked on one decision, recorded in theos-dev-reporton #15633: the widened deriver pins six codes still listed inscripts/error-status-unpinned-baseline.json, so six rows have to be ratcheted down before the gate goes green, and that file was fenced off in the dispatch. Measured in a throwaway worktree: with that shrink, the docs diff on this PR applied on top goes GREEN andUNIQUE_VIOLATIONreconciles at 409 (scope 52 codes, 27 pairs) — so nothing on this card needs to change.Generated by Claude Code
claude commentedon Sep 5, 2026
#15633 has landed as draft PR #15656 (head
87ad25178) — the deriver now reads bothUNIQUE_VIOLATIONproducers and the six now-false rows have been ratcheted out ofscripts/error-status-unpinned-baseline.json, socheck:error-status-conformanceis green; re-measured on that exact head with this PR's docs diff applied on top, the gate is GREEN andUNIQUE_VIOLATIONreconciles at 409 (scope 52 codes, 27 pairs), so nothing on this card needs to change — mergeorigin/mainonce #15656 lands and CI should go green unchanged.Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 15362, "status": "done", "branch": "claude/issue-15362-error-handling-unique-violation", "pr": "https://github.com/objectstack-ai/objectstack/pull/15634", "premise_still_valid": true, "summary": "Resume-only run: no docs content was edited. origin/main (95d5cbb31) was MERGED into the branch — merge commit 082626dc1ab6145f61c4e51faadb516c93bed4aa, no rebase, no force-push, no conflicts (git diff --name-only --diff-filter=U empty). The three-dot diff vs main is unchanged: one file, content/docs/protocol/kernel/error-handling.mdx, 14 insertions / 14 deletions. The blocker is gone: with the #15633 deriver on main, check:error-status-conformance is EXIT=0 at the merged head and UNIQUE_VIOLATION is now a reconciled code rather than an unpinned documented one. PR body got ONE appended section titled 'Unblocked by #15633' via REST PATCH on /pulls/15634; the existing body, its 'Fixes #15362' first line and its footer are byte-identical on read-back — the only delta is that GitHub appended a second, bare '_Generated by [Claude Code](https://claude.ai/code)_' footer block of its own (the documented PATCH behaviour; not re-pasted, left as-is rather than looping). PR is still draft, still open, head 082626dc1; labels (documentation, size/s, skip-changeset) and assignee untouched, ready NOT flipped, auto-merge NOT enabled.", "tests": "All at merged head 082626dc1 (git rev-parse --short HEAD), worktree /home/user/objectstack-15362, tree clean (git status --porcelain empty) after every build. Exit codes captured BEFORE any pipe (each command redirected to its own log file first; never read through | head/| tail). (1) pnpm check:error-status-conformance -> 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.' — matches the PM's post-landing reading on origin/main (279 producer sites, baselined 26). (2) Derived family: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack, run on the merged head (its stderr confirms: gate list derived from the tree of objectstack-ai/objectstack at commit 082626dc1; --repo checked against this checkout's origin remote — it holds; change set 1 path vs merge base 95d5cbb31, three-dot). 39 commands emitted; ALL 39 EXIT=0. Four needed a build before they measured anything and were re-run to a real measurement: pnpm --filter @objectstack/lint run check:doc-formula-expressions and check:doc-security-posture first EXIT=3 PREREQUISITE NOT MET (unbuilt @objectstack/formula / @objectstack/lint); pnpm --filter @objectstack/spec run check:docs first EXIT=1 on the gitignored packages/spec/json-schema tree being absent (its text: 'Nothing was measured' class, not a finding); pnpm --filter @objectstack/spec run check:skill-examples first EXIT=1 on unbuilt spec dist, then again EXIT=1 on unbuilt @objectstack/client-react. Builds ran under the shared lock, OS_VERIFY_LOCK_SLOT=issue-15362 set before the first attempt: turbo build spec+formula+lint -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 213s (3m33s) · waited 0s'; turbo build client-react+client -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 250s (4m10s) · waited 0s' (both VERDICT lines carry the script's own SHARED-BOX SECONDS caveat: the lock excludes other LOCKED runs only, not unlocked check:* siblings). After the builds all four measured green: check:skill-examples -> '✅ 257 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them'; check:docs -> EXIT=0; both lint doc gates -> EXIT=0. pnpm check:docs-transcript-drift also first EXIT=3 (needs packages/lint/dist) and is EXIT=0 after the lint build. pnpm check:nul-bytes EXIT=0; pnpm check:doc-anchors, check:docs-single-h1, check:doc-authoring, check:corpus-claim-drift all EXIT=0. check:docs-audit-scope EXIT=0 on this checkout (the .examples-build artifact behind the earlier self-test noise, already filed as #15446, was absent this run). The gen:schema that ran as the spec build's first step left the tree clean — no regenerated baseline rode along. NOT MEASURED by name, 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 dispatch-gates does not enumerate; those are CI's. No test suites were run beyond the gate family — the diff is one docs file with no package code.", "mcp_calls": "0 — REST only (curl); no MCP GitHub tools were used this run", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 5, 2026
LANDED — PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk.PR #15634 merged 2026-09-05T04:51:28Z as
6c0813196onmain(squash, via the merge queue). Probed AND run on re-fetchedorigin/main(detached worktree; readings copied from the run output):Closures in the landing window: #15362 (this card) and #15497 (this seat's, 04:50Z). Stripping
pm:dispatched+ assignee; the card closed viaFixeson merge. The page's remaining envelope defect (#15632) and the catalog page (#15631) are this seat's next folded dispatch;error-handling.mdxis free for it.Generated by Claude Code
docs(protocol): the 409 unique-constraint refusal is UNIQUE_VIOLATION…