Repository navigation
fix(metadata-protocol): duplicatePackage copies an object whose name lacks the source namespace prefix into the target namespace - #23525
Conversation
…source prefix into the target namespace An object whose name lacks the source namespace prefix kept its name in the copy, a second claim on the source's registry entry and table. It now takes the target prefix in front of its whole name (ADR-0129 D2's own rename), its references are rewritten where a property names an object, and a name already claimed is refused up front at the object with the source rename that clears it. Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…object Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…uplicate Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…ame was measured to do Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 60e26f78a7bf4cd943c7e11353ddf36604be0ba9 && git checkout 60e26f78a7bf4cd943c7e11353ddf36604be0ba9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0be162c75dc68d62820d7871f24e4b0af925f0f5 e9c2bc9ad64e981a67690914123336a00c86da70 && git checkout -B drift-repro 0be162c75dc68d62820d7871f24e4b0af925f0f5 && git merge --no-ff e9c2bc9ad64e981a67690914123336a00c86da70
node scripts/docs-audit/affected-docs.mjs --json 0be162c75dc68d62820d7871f24e4b0af925f0f5
|
席位核对ACCEPT · No contract face, so no review is owed beyond this one: no published schema, no governed text, 1. Shape
2. Premises measured
3. Gates
4. Deviations accepted
5. Out of scope
6. Landing: Tier S, ordinary queue, once the head is all green. Generated by Claude Code |
Fixes #23511
Clause-②: no
What this does
duplicatePackage(packages/metadata-protocol/src/protocol.ts, ADR-0070 D4) renamed a copied object only when its name started with the source namespace prefix (dupsrc_customerbecomesdupdst_customer). An object whose name lacks the prefix (ticket) kept its name. An object name is its registry key and its table name, so that copy was a second claim on the source's object. Onmainat222293f60it lands infailed[]with the ADR-0029 D9.9 ownership text ("bind the customization to …, or have … extend the object instead"), which names no way out for a duplicate. Every copied item that referenced it still named the source's object.Now, in the
duplicatePackageregion only:ticketbecomesdupdst_ticket. That is the rename the namespace-prefix rule itself prescribes (validateObjectNamespacePrefix: "Rename it to NAMESPACE_NAME"). The copy gets its own registry entry and its own table.ticketinto the existing free-text rewrite turns "A support ticket raised by a customer" into "A support dupdst_ticket raised by a customer". The same match would also rename the value of a lookup field calledticket. So a bare name is rewritten only at the derived object-reference properties (REFERENCE_SITES, targetobject:reference,object,objectName,objects,targetObject). The rule reads the property wherever it occurs in any item, because a flow binds its object on a nodeconfig.objectName, which the index does not walk. It covers the four shapes a name takes there: a string, an array element, a record key, or a.name.ticketanddupsrc_ticketboth becomedupdst_ticket) or by an object the source package holds. In that case the object is listed infailed[]before anything is written for it. The entry names the object, the name it would take, the holder of that name, and the rename on the source that clears it. No second name is invented.main: a copied permission set keptobjects: { dupsrc_customer, ticket }, which granted on the SOURCE's objects. It now holdsdupdst_customeranddupdst_ticket. It is the same walker in the same region, pinned by the references case below, and it adds no gate surface. Every otherfailed[]cause and every non-object item name is unchanged.The H2 reading: rename, and refuse only a claimed name
sys_metadatakeys on (type,name,org), so a same-name copy would collide with the source".manifest.namespace), and every object it defines MUST be named${namespace}_${shortName}(platform-reservedsys_*names exempt)."${targetNs}_${name}with a single underscore. The__form is the legacy FQN form, which the prefix rule refuses.400before anything is minted, so the copy is renamed whenever its name is free. The copy is a NEW object, so the rename costs nothing. Renaming the SOURCE object (the alternative way out) would move its table.^[a-z][a-z0-9_]{1,19}$) plus_plus a name the object declaration admitted (^[a-z_][a-z0-9_]*$) is always admitted. No length cap is declared on object names, so H2's length check has nothing to check.Zone 2 hypotheses, measured on a real kernel (ObjectKernel + ObjectQLPlugin, memory driver)
222293f60the rename is a prefix swap, and a name without the prefix is left unchanged. The rename map holds only the swapped objects. References are rewritten by one free-text regex over every string value, and record keys are never touched. Measured: the copieddupdst_note.fields.ticket.referencestayedticket(the source's object), and the copied permission set kept both source keys.222293f60,ticketis infailed[]with the D9.9 text "Cannot layer object 'ticket': the overlay is bound to package 'com.qa.dupdst', but the object is owned by package 'com.qa.dupsrc' …". With this change that cause moves for this case only. The same pin file's other assertions and the package suites show no other cause moved.dupdst_ticketlands in thedupdst_tickettable, and thetickettable holds only its own record. At base,dupdst_ticketdoes not exist (Object 'dupdst_ticket' not found).Pins
The new file is
packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts. It sits beside the existing duplicate tests inprotocol-package-lifecycle.test.ts. The base column was read against ametadata-protocoldist built at222293f60.222293f60failedholdsticketwith the D9.9 textreference, viewobject, flow nodeconfig.objectName, permissionobjectskeys. NEGATIVE CONTROLS: the prose and the lookup field calledticket(key andname) are unchanged, and the source is untoucheddupdst_ticketrowObject 'dupdst_ticket' not foundAblation (one, on the non-prefixed branch)
The mutation was
for (const name of unprefixed) {replaced byfor (const name of unprefixed.slice(0, 0)) {, so no bare name is renamed or refused. It ran throughscripts/ablation-replace.mjs: anchor x1 to x0, blobc19415261to0de914530. After a rebuild,ablation-dist-preflightfound the marker in 2 dist files. Result: 4 failed / 0 passed, for the base reasons (the D9.9 text, the missing copy,Object 'dupdst_ticket' not found). The restore was proven by the tool (blob equals HEADc19415261,git diff HEADempty). After a rebuild, the marker was absent from all 24 dist files, the tree was clean against HEAD, and the pin was 4 passed. The measurement of the PM route used the same discipline:oldswidened to every renamed name, then 1 failed / 3 passed (the prose case), restored, marker absent, tree clean.Tests and gates
pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 236 files passed, 3 skipped; 28397 tests passed, 19 skipped.pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2: 417 files, 8101 tests passed.c2430b9b4(lock VERDICT command-exit 0).e9c2bc9adonly rewords one code comment. After it,metadata-protocolwas rebuilt and the 14 duplicate pins reran green.metadata-protocoltypecheck: exit 0.objectqltypecheck: exit 0.check:test-typecheckOK with its debt unchanged, and the new file is in the test program (--listFilesOnly).node scripts/pm/dispatch-gates.mjs --commands(diff-derived, 67 families) was run locally and reconciled with--ranate9c2bc9ad: 67 derived, 66 run with exit 0, 1 NOT MEASURED, 0 unrun.check:lean-entry-closurefirst refused its prerequisite. It reran green after anobjectqlbuild.check:dual-build-cjs-loadsis NOT MEASURED:PREREQUISITE NOT MET, because it needs the whole workspace built. It is left to CI.protocol.ts.git merge-treeagainst it is clean. No merge was made, and the PR's merge-ref CI covers the joint state.Acceptance notes
ticket_viewsis stored as a second row bound to the target. Its served views expand per object (ticket.defaultfor the source,dupdst_ticket.defaultfor the copy), and a read naming no package serves the source's row. No defect measured; noted.fieldsmap, keyedOBJECT.FIELD, keeps the source object's names. Measured at head:dupsrc_customer.full_namestays as is whileobjectsfollows. This is a composite key form the derived reference-site index does not model, so it is not fixed here.Generated by Claude Code