Repository navigation
Commit 56147a8
fix(metadata-protocol): duplicatePackage copies an object whose name lacks the source namespace prefix into the target namespace (#23525)
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_customer` becomes `dupdst_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. On `main` at `222293f60` it lands
in `failed[]` 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 `duplicatePackage` region only:
- **Every copied object takes a name in the target namespace.** A
prefixed name swaps its prefix, unchanged. A name without the source
prefix gets the target prefix in front of its whole name: `ticket`
becomes `dupdst_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.
- **References to such a name are rewritten where a property names an
object, and only on an exact match.** A bare word cannot be found safely
in free text. Measured: putting `ticket` into 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 called `ticket`. So a bare name is rewritten
only at the derived object-reference properties (`REFERENCE_SITES`,
target `object`: `reference`, `object`, `objectName`, `objects`,
`targetObject`). The rule reads the property wherever it occurs in any
item, because a flow binds its object on a node `config.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`.
- **A name that is already taken is refused up front, at the object.**
The name can be taken by a prefixed sibling's copy (`ticket` and
`dupsrc_ticket` both become `dupdst_ticket`) or by an object the source
package holds. In that case the object is listed in `failed[]` 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.
- **Bounded in-place fix, same defect class.** The record-key rule also
applies to prefixed names. Measured on `main`: a copied permission set
kept `objects: { dupsrc_customer, ticket }`, which granted on the
SOURCE's objects. It now holds `dupdst_customer` and `dupdst_ticket`. It
is the same walker in the same region, pinned by the references case
below, and it adds no gate surface. Every other `failed[]` cause and
every non-object item name is unchanged.
## The H2 reading: rename, and refuse only a claimed name
- **ADR-0070 D4 docblock** (the contract): it clones every ACTIVE item
"RE-NAMESPACING object names — … `sys_metadata` keys on (type,name,org),
so a same-name copy would collide with the source".
- **ADR-0129 D2** (the governing naming record): "A package declares its
prefix once (`manifest.namespace`), and every object it defines MUST be
named `${namespace}_${shortName}` (platform-reserved `sys_*` names
exempt)."
- **ADR-0028 is Deferred.** Its status line records the literal-prefix
rule as the enforced current state, and ADR-0129 D4 says "Until 0028's
recorded re-open trigger fires, this ADR is the governing record of
object naming." So the rule applied is ADR-0129's literal prefix,
`${targetNs}_${name}` with a single underscore. The `__` form is the
legacy FQN form, which the prefix rule refuses.
- **The target namespace is always known at the copy loop.** A missing
or invalid one is refused `400` before 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.
- **The "illegal name" arm is unreachable, so no code was added for
it.** The namespace declaration (`^[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.
- **"Existing names" is read as the names this copy and its source
hold.** A collision with a third package's registry entry is answered by
the save door's own gates, exactly as for a prefixed copy, and is
unchanged.
## Zone 2 hypotheses, measured on a real kernel (ObjectKernel +
ObjectQLPlugin, memory driver)
- **H1 holds as guessed.** At `222293f60` the 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 copied `dupdst_note.fields.ticket.reference` stayed
`ticket` (the source's object), and the copied permission set kept both
source keys.
- **H2:** see above. The PM route (the same map through the free-text
regex) is the measured prose corruption, so the bare name goes through
the structural rewrite instead.
- **H3 holds.** At `222293f60`, `ticket` is in `failed[]` 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.
- **H4 holds.** A record inserted through `dupdst_ticket` lands in the
`dupdst_ticket` table, and the `ticket` table holds only its own record.
At base, `dupdst_ticket` does 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 in
`protocol-package-lifecycle.test.ts`. The base column was read against a
`metadata-protocol` dist built at `222293f60`.
| case | at `222293f60` | at head |
|:--|:--|:--|
| a prefixed and a non-prefixed object both copy into the target
namespace, each owned by the target; no target row under the source's
name | red: `failed` holds `ticket` with the D9.9 text | green |
| references follow: lookup `reference`, view `object`, flow node
`config.objectName`, permission `objects` keys. NEGATIVE CONTROLS: the
prose and the lookup field called `ticket` (key and `name`) are
unchanged, and the source is untouched | red: no `dupdst_ticket` row |
green |
| the copy has its own table. NEGATIVE CONTROL: the source table holds
only its own record | red: `Object 'dupdst_ticket' not found` | green |
| a claimed name is refused at the object (asserts on named subjects:
the object, the name it would take, the holder, the rename). NEGATIVE
CONTROL: the prefixed sibling took the name with its own body, and one
history entry was written for it | red: the D9.9 text instead | green |
## Ablation (one, on the non-prefixed branch)
The mutation was `for (const name of unprefixed) {` replaced by `for
(const name of unprefixed.slice(0, 0)) {`, so no bare name is renamed or
refused. It ran through `scripts/ablation-replace.mjs`: anchor x1 to x0,
blob `c19415261` to `0de914530`. After a rebuild,
`ablation-dist-preflight` found 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 HEAD `c19415261`, `git diff HEAD` empty). 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: `olds` widened 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.
- Both suites ran at `c2430b9b4` (lock VERDICT command-exit 0).
`e9c2bc9ad` only rewords one code comment. After it, `metadata-protocol`
was rebuilt and the 14 duplicate pins reran green.
- `metadata-protocol` typecheck: exit 0.
- `objectql` typecheck: exit 0. `check:test-typecheck` OK 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 `--ran` at `e9c2bc9ad`: 67
derived, 66 run with exit 0, 1 NOT MEASURED, 0 unrun.
- `check:lean-entry-closure` first refused its prerequisite. It reran
green after an `objectql` build.
- `check:dual-build-cjs-loads` is NOT MEASURED: `PREREQUISITE NOT MET`,
because it needs the whole workspace built. It is left to CI.
- Origin/main has moved 7 commits since the branch point. One of them,
the read naming no package, touches other regions of `protocol.ts`. `git
merge-tree` against it is clean. No merge was made, and the PR's
merge-ref CI covers the joint state.
## Acceptance notes
- **A non-object item whose name lacks the source prefix still keeps its
name, as the docblock says.** Measured: the copy of the view container
`ticket_views` is stored as a second row bound to the target. Its served
views expand per object (`ticket.default` for the source,
`dupdst_ticket.default` for the copy), and a read naming no package
serves the source's row. No defect measured; noted.
- **A bare name inside free text (an expression, a description) is not
rewritten, by design.** A prefixed name still is, through the existing
regex.
- **Read-only inference, not measured:** the existing free-text regex
has a right identifier boundary but no left one.
- **Not measured:** physical identifier limits (63 bytes on PostgreSQL)
for a longer target name. No object-name length cap is declared, and the
prefixed path carries the same exposure.
- **Out of scope, handed to the seat:** a copied permission set's
`fields` map, keyed `OBJECT.FIELD`, keeps the source object's names.
Measured at head: `dupsrc_customer.full_name` stays as is while
`objects` follows. This is a composite key form the derived
reference-site index does not model, so it is not fixed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 989aa48 commit 56147a8
3 files changed
Lines changed: 439 additions & 12 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26290 | 26290 | | |
26291 | 26291 | | |
26292 | 26292 | | |
| 26293 | + | |
| 26294 | + | |
| 26295 | + | |
| 26296 | + | |
| 26297 | + | |
| 26298 | + | |
| 26299 | + | |
| 26300 | + | |
| 26301 | + | |
26293 | 26302 | | |
26294 | 26303 | | |
26295 | 26304 | | |
| |||
26419 | 26428 | | |
26420 | 26429 | | |
26421 | 26430 | | |
26422 | | - | |
26423 | | - | |
| 26431 | + | |
| 26432 | + | |
26424 | 26433 | | |
26425 | 26434 | | |
26426 | 26435 | | |
26427 | 26436 | | |
| 26437 | + | |
| 26438 | + | |
| 26439 | + | |
| 26440 | + | |
| 26441 | + | |
| 26442 | + | |
| 26443 | + | |
| 26444 | + | |
| 26445 | + | |
| 26446 | + | |
| 26447 | + | |
| 26448 | + | |
| 26449 | + | |
| 26450 | + | |
| 26451 | + | |
| 26452 | + | |
| 26453 | + | |
| 26454 | + | |
| 26455 | + | |
| 26456 | + | |
| 26457 | + | |
| 26458 | + | |
| 26459 | + | |
| 26460 | + | |
| 26461 | + | |
| 26462 | + | |
| 26463 | + | |
26428 | 26464 | | |
26429 | | - | |
26430 | | - | |
26431 | | - | |
26432 | | - | |
| 26465 | + | |
| 26466 | + | |
| 26467 | + | |
| 26468 | + | |
| 26469 | + | |
| 26470 | + | |
| 26471 | + | |
| 26472 | + | |
| 26473 | + | |
| 26474 | + | |
| 26475 | + | |
| 26476 | + | |
| 26477 | + | |
| 26478 | + | |
| 26479 | + | |
| 26480 | + | |
| 26481 | + | |
| 26482 | + | |
| 26483 | + | |
| 26484 | + | |
| 26485 | + | |
| 26486 | + | |
| 26487 | + | |
| 26488 | + | |
| 26489 | + | |
| 26490 | + | |
| 26491 | + | |
| 26492 | + | |
| 26493 | + | |
| 26494 | + | |
| 26495 | + | |
| 26496 | + | |
| 26497 | + | |
26433 | 26498 | | |
| 26499 | + | |
| 26500 | + | |
26434 | 26501 | | |
| 26502 | + | |
26435 | 26503 | | |
26436 | 26504 | | |
26437 | | - | |
| 26505 | + | |
| 26506 | + | |
| 26507 | + | |
26438 | 26508 | | |
26439 | 26509 | | |
26440 | | - | |
26441 | | - | |
26442 | | - | |
| 26510 | + | |
| 26511 | + | |
| 26512 | + | |
| 26513 | + | |
| 26514 | + | |
| 26515 | + | |
| 26516 | + | |
| 26517 | + | |
| 26518 | + | |
| 26519 | + | |
| 26520 | + | |
| 26521 | + | |
| 26522 | + | |
| 26523 | + | |
| 26524 | + | |
| 26525 | + | |
| 26526 | + | |
| 26527 | + | |
| 26528 | + | |
| 26529 | + | |
| 26530 | + | |
| 26531 | + | |
| 26532 | + | |
| 26533 | + | |
| 26534 | + | |
26443 | 26535 | | |
26444 | 26536 | | |
26445 | | - | |
| 26537 | + | |
| 26538 | + | |
| 26539 | + | |
| 26540 | + | |
26446 | 26541 | | |
26447 | 26542 | | |
26448 | 26543 | | |
| |||
26478 | 26573 | | |
26479 | 26574 | | |
26480 | 26575 | | |
26481 | | - | |
| 26576 | + | |
| 26577 | + | |
| 26578 | + | |
| 26579 | + | |
| 26580 | + | |
| 26581 | + | |
26482 | 26582 | | |
26483 | 26583 | | |
26484 | 26584 | | |
| |||
0 commit comments