Repository navigation
refactor(plugin-hono-server,runtime): the Hono doors and the dispatcher read UNIQUE_VIOLATION off the shared arm instead of stamping the literal (#23714) - #23760
Conversation
…code off the shared arm instead of stamping the literal Both doors that translate the engine's DuplicateRecordError to the ruled wire spelling now read that code off structuredCodeAnswer's arm (@objectstack/types), as @objectstack/mcp's wireCodeOf does, so neither package spells a copy of UNIQUE_VIOLATION. The dispatcher's arm-agreement check compares against the envelope's decided code, as the Hono doors' does. The wire answer is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…ique-violation-arm-relay Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 47c6fce64718d82f4fd9a733e0ff586bf9fa49bb && git checkout 47c6fce64718d82f4fd9a733e0ff586bf9fa49bb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 140238a47b892ef9234052e44267ec3739fa79c2 ea8db26da26920af1f10f43f6f154ee9a25951d4 && git checkout -B drift-repro 140238a47b892ef9234052e44267ec3739fa79c2 && git merge --no-ff ea8db26da26920af1f10f43f6f154ee9a25951d4
node scripts/docs-audit/affected-docs.mjs --json 140238a47b892ef9234052e44267ec3739fa79c2
|
✅ ACCEPT: PR #23760 at
|
Fixes #23714
Clause-②: no
The two doors that translate the engine's
DuplicateRecordErrorinto the ruled wire spelling now read that code off the shared classification's arm (structuredCodeAnswer,@objectstack/types), as@objectstack/mcp'swireCodeOfalready does. Neither package's non-test source spells'UNIQUE_VIOLATION'any more, as a stamp or as a comparison. The wire answer does not move. It was compared byte for byte at every door, before and after.Governing text
Triage's grade on the card (
6201012432):The ruling it serves (recorded in commit
65846bc46): one wire spelling on every route,UNIQUE_VIOLATION.What changed
packages/plugins/plugin-hono-server/src/adapter.ts,declaredEnvelopeForThrow. For the engine's class (the unchanged keyisEngineDuplicateRecordEnvelope), the envelope'scodeis nowstructuredCodeAnswer(thrown)?.body.codewhen that is a string, and the thrown code otherwise. It was the literal'UNIQUE_VIOLATION'.declaredArmContextis unchanged.packages/runtime/src/http-dispatcher.ts,HttpDispatcher.errorFromThrown: the same read.duplicateArmContext's agreement check compared the arm's code with the literal. It now compares it with the envelope's decided code, which is the Hono doors'declaredArmContextrule. Once the code is read off the same arm, the literal comparison is redundant, so it goes too. The status half of the check is unchanged.patchchangeset (see "Changeset" below).packages/specedit (no ledger row, no gate change), nopackages/typesedit, no export added or changed, and no test file added. Two call sites in two packages, each three lines, so no shared helper.Literal census (non-test source, both packages)
Read at
origin/main3b46855fb(this branch's base) and at this head, withgit grepfor the quoted literal and comment lines dropped.plugin-hono-server/src/adapter.ts:362,declaredEnvelopeForThrowisEngineDuplicateRecordEnvelope(thrown) ? 'UNIQUE_VIOLATION' : resolved.coderuntime/src/http-dispatcher.ts:406,duplicateArmContextarm.body.code !== 'UNIQUE_VIOLATION'runtime/src/http-dispatcher.ts:1235,errorFromThrownengineDuplicate ? 'UNIQUE_VIOLATION' : thrown.codeH1 holds: those are the only two stamps, and there is no third. After the change both packages' non-test source has zero quoted occurrences outside comments. The remaining mentions are prose in docblocks (
adapter.ts,http-dispatcher.ts,sandbox/quickjs-runner.ts:1463). The rebuiltdist/of both packages has zero quotedUNIQUE_VIOLATIONtoo (index.mjsandindex.jsofplugin-hono-server,index.jsandindex.cjsofruntime).The fallback branch (H2): measured unreachable
The seat asked whether
structuredCodeAnswercan answerundefinedfor a throwisEngineDuplicateRecordEnvelopeaccepts. By reading: the predicate requirescode === 'DUPLICATE_RECORD'andname === 'DuplicateRecordError'. The three arms ahead of the duplicate arm key onDELETE_RESTRICTED, onCONCURRENT_UPDATEor theConcurrentUpdateErrorname, and onMETADATA_CONFLICT, so none of them fires. The duplicate arm keys on the same two-part predicate inline and answers the literalUNIQUE_VIOLATIONat 409.Measured with a scratch probe over the built
@objectstack/types(not committed). The arm answered409/"UNIQUE_VIOLATION"for all 9 accepted shapes: the engine's class with and withoutfield, a plain object, a null-prototype object, a frozenErrordeclaringstatus: 400, and shapes also carryingcurrentVersion(string andnull),dependentObject/dependentCount, or a non-stringfieldwith an empty message and object. The one shape that reaches the fallback is contrived: acodegetter that answersDUPLICATE_RECORDto the predicate's read and something else to the arm's reads. So the thrown-code fallback (as inwireCodeOf) is unreachable for any value whosecodeandnameare stable.Measured before and after: each door's duplicate-record answer
Rig (scratch, not committed): a real
ObjectQLoverSqlDriver(better-sqlite3:memory:), a realObjectStackProtocolImplementation, and the BUILTdist/of@objectstack/hono,@objectstack/runtimeand@objectstack/plugin-hono-server. The doors are the realcreateHonoApp(app.request) and a realHonoHttpServer(a route throughwrap(), and a raw mount throughonError). The identity step (HttpDispatcher.prototype.timedResolveExecutionContext, answering a member) is the one stubbed seam. The objectudocdeclaresemailunique, andr1holds the value.dist/built at the base3b46855fb(pnpm turbo run build --filter=@objectstack/hono..., all 32 tasks cache hits for that source).dist/rebuilt ate5ffc8248(26 of 32 cached; the hono, plugin-hono-server and runtime builds re-ran).node scripts/ablation-dist-preflight.mjsfound the new identifierduplicateArmCodein 2 built files of each package.errorkeysdetailskeyscreateHonoAppPOST /api/v1/data/udocrepeatingemail(adapter)UNIQUE_VIOLATIONcode,message,detailsdeveloperMessage,objectHonoHttpServerroute,engine.insertrepeatingemail(adapter)UNIQUE_VIOLATIONcode,message,detailsdeveloperMessage,objectHonoHttpServerroute, the engine's class withfield: 'email'(adapter)UNIQUE_VIOLATIONcode,message,detailsdeveloperMessage,field,objectHonoHttpServerraw mount,engine.insertrepeatingemail(adapter)UNIQUE_VIOLATIONcode,message,detailsdeveloperMessage,objectcreateHonoAppPOST /api/v1/actions/udoc/add_duplicate, a handler write (dispatcher)UNIQUE_VIOLATIONcode,message,httpStatus,detailsdeveloperMessage,objectcreateHonoAppPOST /api/v1/actions/udoc/take_email/r2, a declarative update (dispatcher)UNIQUE_VIOLATIONcode,message,httpStatus,detailsdeveloperMessage,objectcreateHonoAppPOST /api/v1/actions/udoc/throw_named_duplicate, the class withfield(dispatcher)UNIQUE_VIOLATIONcode,message,httpStatus,detailsdeveloperMessage,field,objectHonoHttpServerroute, a producer speakingDUPLICATE_RECORDDUPLICATE_RECORDcode,messagecreateHonoAppactions door, a producer speakingDUPLICATE_RECORDDUPLICATE_RECORDcode,message,httpStatuscreateHonoAppactions door,DELETE_RESTRICTEDDELETE_RESTRICTEDcode,message,httpStatusTwo of the bodies, verbatim (the same bytes before and after):
createHonoAppPOST /api/v1/data/udoc:409 {"success":false,"error":{"code":"UNIQUE_VIOLATION","message":"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written.","details":{"developerMessage":"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written.","object":"udoc"}}}createHonoAppPOST /api/v1/actions/udoc/add_duplicate:409 {"success":false,"error":{"code":"UNIQUE_VIOLATION","message":"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written.","httpStatus":409,"details":{"developerMessage":"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written.","object":"udoc"}}}The whole 12-line record (the 10 rows above plus two store checks: one row holds the value, and
r2keeps its own) is byte-identical before and after:cmpexit 0, sha2563694fa2b2f02on both files. No ablation was run, because no tested behaviour changed. The landed pins below, which were each measured red against the untranslated doors by their own PRs, are the discrimination.Provenance gate (H3)
pnpm --filter @objectstack/spec check:error-code-provenance:3b46855fb): exit 0.scanned 2985 files; 379 registered-code stamp site(s): 360 listed, 19 waivedandOK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver (10 waiver(s), all live).--report, before and after: theUNIQUE_VIOLATIONsites are the same four listed ones (@objectstack/driver-memory,@objectstack/driver-sql, and@objectstack/typestwice). The only difference in the whole report is one@objectstack/runtime → PROJECT_MEMBERSHIP_REQUIREDsite, whose line moved fromhttp-dispatcher.ts:1759to:1775because of the added comment lines.The gate could not see the two ternaries before, and there is nothing for it to see after. Its blindness is not changed here.
Tests
At the final head
ea8db26da. Before opening,origin/main2906cd8d2was merged in (no conflict). Its three commits touchcli,driver-sqlandmetadata-protocoland none of the changed files here. Then the workspace was rebuilt (pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2:75 successful,32 cached),packages/runtime/.objectstackwas removed, and every line below was re-run there. The first pass at20fb7e3d9(on the base3b46855fb) read the same counts.pnpm --filter @objectstack/plugin-hono-server test:Test Files 29 passed (29),Tests 366 passed (366). This includes PR fix(plugin-hono-server): a Hono door answers the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with the arm's fields (#23587) #23682's pins inhandler-throw-declared-envelope.test.ts(Tests 49 passed (49)in a verbose re-run). All 12 rows of its[#23587] a repeated unique value answers 409 UNIQUE_VIOLATION on a Hono doorblock are green, unchanged: three doors, each with the engine's class with and withoutfieldand the two controls.pnpm --filter @objectstack/plugin-hono-server typecheck: exit 0 (tsctwice andcheck:test-typecheck: OK, 0 debt).pnpm --filter @objectstack/runtime test:Test Files 395 passed (395),Tests 5918 passed | 19 skipped (5937). That includes PR fix(runtime): the dispatcher answers the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with the arm's fields (#23686) #23700'sdispatcher-unique-violation-wire-code.test.ts, whose 7 rows are green, unchanged (Tests 7 passed (7)in a verbose re-run).pnpm --filter @objectstack/runtime test:repo:Test Files 3 passed (3),Tests 808 passed (808).pnpm --filter @objectstack/runtime typecheck: exit 0 (tsc --noEmitandcheck:test-typecheck: OK, the debt ledger unchanged).pnpm exec eslint --no-inline-config packages/plugins/plugin-hono-server packages/runtime: exit 0, no output. A JSON-format run over the two changed files shows both linted, with 0 errors and 0 warnings. Repo-widepnpm lintis CI's.Both pin files import the door under test from source (
./adapter,./http-dispatcher.js), so they ran against this change without a rebuild.Gates
At the final head
ea8db26da:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths: 60 commands derived from the 3 changed paths (merge base2906cd8d2). All 60 ran, each exit code was recorded before any pipe, and all 60 exited 0.--ranreconciliation:60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN(a derived zero: all 60 recorded an exit code and none is 3).pnpm check:dispatcher-error-vocabulary,pnpm check:route-envelope,pnpm check:error-status-conformance,pnpm check:test-source-alias,pnpm check:cross-package-test-inputs,pnpm check:nul-bytes,pnpm check:issue-citations,pnpm check:doc-authoring,pnpm check:published-files,pnpm check:dual-build-cjs-loads,check-adr-0087-registration,check-empty-changesetandcheck-changeset-no-major.20fb7e3d9,pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET, no workspacedist/). After a cache-backed full build under the verify lock it exited 0, and so did the four otherdist/-reading gates re-run beside it. At the final head all 60 exited 0 on the first pass.node scripts/check-changeset-fixed.mjs,pnpm check:authz-resolver,pnpm check:error-code-casing,pnpm check:filter-alias-parity,pnpm check:route-ledger-census.pnpm --filter @objectstack/spec check:error-code-provenanceat the final head: exit 0,scanned 2986 files; 380 registered-code stamp site(s): 361 listed, 19 waivedand the sameOKline. The counts moved by one file and one site from the base reading above because ofmain's incomingdriver-sqlchange. TheUNIQUE_VIOLATIONsites are still the same four listed ones.createHonoAppover the rebuiltdist/) were measured directly.Changeset
.changeset/23714-unique-violation-arm-relay.md,patchfor@objectstack/plugin-hono-serverand@objectstack/runtime. It says there is no behaviour change and describes the source-shape change. The dispatch left the call to theCheck Changesetgate, and under this repo's rules the gate needs one:files[]shipdist/, and the rebuiltdist/changed (the new identifier is in the built files, and the literal is gone). So this PR publishes changed bytes from two released packages, andskip-changeset(for a diff that publishes nothing) does not apply.check-empty-changesetrefuses an added empty-frontmatter changeset, so an empty one is not an option.The changeset describes no behaviour change, because none happened.
Acceptance notes
structuredCodeAnsweris now asked twice for the engine's class: once for the code, and once by the context helper. It is a pure function on an error path only. Passing the arm between them would changedeclaredArmContext's signature, which is out of this card's surface.declaredArmContext(Hono) andduplicateArmContext(dispatcher) now carry the same agreement rule, status and code against the decided envelope. They stay two module-local functions, one per package, as the dispatch directed.content/docs/releases/is untouched. No docs edit: no published sentence names the literal at these sites.Generated by Claude Code