Repository navigation
fix(objectql): a unique refusal's user-facing sentence names the object and field by label - #25021
Conversation
…ct and field by label Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
…eld-less variant and developerMessage Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
…PI name Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
…plicate-message-labels
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 18 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 817b5361916dca2cdc5b47ea54d67527598c3690 && git checkout 817b5361916dca2cdc5b47ea54d67527598c3690
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 73432299c0649af2318d137522d55cf444b3a869 2e76ecfc31ce49425c9f09f6ee39b73386a474d1 && git checkout -B drift-repro 73432299c0649af2318d137522d55cf444b3a869 && git merge --no-ff 2e76ecfc31ce49425c9f09f6ee39b73386a474d1
node scripts/docs-audit/affected-docs.mjs --json 73432299c0649af2318d137522d55cf444b3a869 |
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
Contract reviewServed-tier: Scope: the net diff against the merge base (7 files, +237/−14):
Read with them: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…plicate-message-labels
…or the label clause Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8RKtAGLpTgi5BmpDr2xeo
Seat delta note ·
|
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24951. 7 files, +244/−15. Not governed: check-governed-merges matched 0 of 7 paths; 259 changed lines. |
| Scope | A unique refusal's user-facing sentence names the object and the field by their declared labels, and by API name only where none is declared. developerMessage opens with the API names. Status 409, the engine's DUPLICATE_RECORD, the wire's UNIQUE_VIOLATION and No record was written. are unchanged. |
| Seat's own read | All five engine doors that raise the refusal pass this.getSchema(object): the single insert and its autonumber retry, the batch insert, update by id and update by predicate. The constructor's fourth argument is optional and module-local in type. No export is added. |
| Review | TIER_DEFAULT. Record 6305423780 is PASS on 49bf2c2d1d. The seat's delta note 6306381547 carries it to this head: fix round 2 moved only the checklist item's revision and history. |
| Declaration | One changeset, @objectstack/objectql patch. Its Clause-②: no (text only: …) line is the re-declared one (6305353681), the same on the changeset and the PR body. |
| Cross-lane | domain:cli: the meta-stored-legacy-organization integration pin (test only), declared on #6024 at 6305367998; the seat is vacant and no objection stands. domain:devx: the error-handling.mdx sentence and the records-forms clause, declared on #6023 at 6305373450. devx objected at 6305842305 that the clause change owed a revision bump. That is now met at revision 9 with a history row, which is the condition devx named for entering the queue. No other objection on the card, the PR or the seat posts at this read. |
| Gates | Dev at 697e25ef6: 104 derived, 104 run, all exit 0. The new pins pass 17/17. In the ablation 13 go red and the 4 API-name fallback pins stay green, as they describe the old behaviour. objectql passed 8524 tests; rest, runtime, mcp, plugin-hono-server, metadata-protocol, service-automation and cli suites passed. Fix round 2, on the merged tree: check:platform-checklist, --changed-from origin/main and the watchdog self-test all exit 0, and the objectql build and typecheck exit 0. |
| CI | At this read on 2e76ecfc31: ci-failure GREEN, all 42 check-runs completed, none failed; Check Changeset success. git merge-tree against today's origin/main (4d991588ff) is clean. |
Disposition:
open_questions: all three are taken as the dev settled them: A, keep the sentence frame and substitute labels; A, no localized catalog entry now; A, the default label, not a per-locale one.deviations: all accepted: the three cross-lane files (declared),developerMessage's new opening (re-declared), the optional constructor argument, and the worker restart (the suites were re-run to completion).out_of_scope_findings:driver-sql'srefuseUpsertConflictOutsideWrittenOrganizationkeeps API names in its copy of the sentence. It is reached only by direct driver callers, not by an engine write door, so it meets no filing gate. It is noted, not filed.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24951 then closes the card.
Generated by Claude Code
Fixes #24951
Clause-②: no (text only: the user-facing sentence names the object and field by label, and
developerMessageopens with the API names; no code, status, outcome or export moves)What changed
A unique refusal's user-facing sentence (
DuplicateRecordError.message) now names the object and the field by their declaredlabel. Every door keeps that message as thrown, so a refused record save and a screen flow both show it to the end user.Duplicate record refused on 'crm_account': a unique constraint on 'name' already holds this value. No record was written.Duplicate record refused on 'Account': a unique constraint on 'Account Name' already holds this value. No record was written.Duplicate record refused on 'and the closingNo record was written.do not move. The sentence still starts withDuplicate, not with a SQL verb, so the importer'ssanitizeRowErrorbackstop leaves it alone.Where the labels come from.
DuplicateRecordErroris built in one place,envelopeUniqueViolation. It is called from five sites inengine.ts, all inside engine methods with the registry in reach:createWithAutonumberResync);updateMany).Each site now passes
this.getSchema(object).envelopeUniqueViolationis module-internal (it is not in the package'sindex.ts), so its new third argument is not a public signature change. The labels reach the class through one optional fourth constructor argument,{ object?, field? }. Callers that pass three arguments, including every test and fake in other packages, get the API-name sentence. No new export.Diagnostics keep the API names.
objectandfieldstill carry them.developerMessagenow opens with them:Duplicate record refused on object 'crm_account', field 'name'. The driver refused …. Before this PR,developerMessagenamed neither (see Deviations).Not moved: the codes (
DUPLICATE_RECORDin the engine,UNIQUE_VIOLATIONon every wire route), the 409 status and the outcome.Census, read before the edit
envelopeUniqueViolationcall sites are listed above. There is no upsert or bulk site inpackages/objectqlbeyond these.new DuplicateRecordErrorappears in source only insideenvelopeUniqueViolation; every other occurrence is a test.git grepon both phrases across every package,qa/,docs/qa/platform-checklist/,content/docs/; CHANGELOGs excluded):rest:rest-duplicate-record-arm.test.ts,error-response-structured-arm-door-parity.test.ts;mcp:mcp-unique-violation-wire-code.test.ts;plugin-hono-server:handler-throw-declared-envelope.test.ts;metadata-protocol:protocol.batch-row-http-status.test.ts;runtime:batch-row-driver-text-real-driver.integration.test.ts.runtime'sseed-loader-driver-text-real-driver.integration.test.ts(dt_acct, unlabelled) stayed green.cli'smeta-stored-legacy-organization.integration.test.tsmatched/Duplicate record refused on 'sys_metadata'/, andsys_metadatais labelledSystem Metadata. It moved (see below).content/docs/protocol/kernel/error-handling.mdxanddocs/qa/platform-checklist/areas/records-forms.jsondescribed the sentence with an API name. Both are updated.driver-sql'srefuseUpsertConflictOutsideWrittenOrganization(sql-driver.ts) builds a copy of the same sentence with the object's API name. It has no registry and is outside this card's fence (see Acceptance notes).Moved pins and docs, by lane
packages/cli/src/commands/migrate/meta-stored-legacy-organization.integration.test.ts(domain:cli). The pin now matches the stable opening^Duplicate record refused on 'and assertserror.object === 'sys_metadata', so it no longer restates the label.content/docs/protocol/kernel/error-handling.mdx(domain:devx). The example reads'Account'(the object's label).docs/qa/platform-checklist/areas/records-forms.json(domain:devx). Theqa_uniqueclause now says the sentence names the object by its declared label, and by its API name only where none is declared. The itemrecords-forms.field-unique-enforcementmoves from revision 8 to 9, with onehistoryrow (ref#25021), as the ledger rule asks of a clause change (domain:devx, 6305842305).Verification record
Base:
origin/main7532ea3c08.origin/mainwas merged at697e25ef6(the incoming commits touch noobjectqlfile). After the merge, theobjectqldependency closure, theobjectqlbuild and theobjectqltypecheck were re-run: exit 0.New pins:
packages/objectql/src/engine-duplicate-record-label-sentence.test.ts. They drive the real write doors (single insert, batch insert, update by id, update by predicate) on a registry double:developerMessagenaming the API names.Result: 17/17 passed.
Ablation: I reverted
duplicate-record-error.tsandengine.tsto the base. The marker counts went from 2/5 to 0/0. 13 failed, 4 passed. The 4 that passed are the fallback pins, which describe the old behaviour. I then restored both files withgit checkout HEAD --. Each file's blob hash equals the one atHEAD(8830e86e…,247b9130…),git diff HEADis empty, and the re-run passed 17/17.@objectstack/objectql:typecheckexit 0, includingcheck:test-typecheckover the new file. Fulltestexit 0: 452 files, 8524 tests.Dependent full suites, each exit 0:
rest: 307 files;runtime: 432 files;mcp: 40 files;plugin-hono-server: 35 files;metadata-protocol: 251 files, 3 skipped;service-automation: 197 files, 2605 tests.cli:--project unit: 315 files green. Two files first failed only becausepackages/clihad not been built; they passed after the build.--project integration: 165 files green and 1 skipped. Twelve files first failed only for the same build prerequisite and passed after the build. The moved pin passes 7/7.pnpm --filter @objectstack/core test:repo: exit 0, 6 files, 82 tests.Gates: I ran every command printed by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon697e25ef6(104 commands).check:skill-examples,check:dual-build-cjs-loads,check:i18n-coverage. After a fullturbo run build, all three exited 0.--ranreconciliation:104 derived famil(ies) accounted for — 104 run, 0 NOT-MEASURED (a DERIVED zero — all 104 recorded an exit code and none of them is 3).Deviations
developerMessagegained an opening clause naming the API names. The card and the dispatch say it "keeps" them, but onmainit named neither the object nor the field. Without that clause, the API names would only be onobjectandfieldoncemessageuses labels. The seat re-declared the claim'sClause-②line in 6305353681 (fix round 1), and the changeset and this body carry the re-declared line.DuplicateRecordError's constructor takes an optional trailing argument (labels). The class is exported, so this is an additive, optional signature widening. No export was added.Acceptance notes
driver-sql's upsert refusal outside the written organization (refuseUpsertConflictOutsideWrittenOrganization) still names the object by API name. It is a driver-side copy of the sentence and has no registry in reach. No engine write door callsdriver.upsert: its callers, such as the lifecycle service'scold.upsert, a seed or a connector, receive the error exactly as the driver throws it. So its text is unchanged here. No carrier.developerMessageis the engine'smessage, so it now names labels. The body'sobjectandfieldkeys still carry the API names, and the curatederrortext is unchanged.packages/spec's operation-message catalog (domain:spec) and is not part of this PR.Generated by Claude Code