Skip to content

Commit 681ace4

Browse files
committed
fix(metadata-protocol): duplicatePackage names an object without the 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>
1 parent 222293f commit 681ace4

2 files changed

Lines changed: 410 additions & 12 deletions

File tree

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26235,6 +26235,15 @@ export class ObjectStackProtocolImplementation implements
2623526235
* with the source — and rewriting every intra-package reference (lookup
2623626236
* `reference`, view `object`, expressions, etc.) to the new names. Per-item
2623726237
* best-effort; one failure never aborts the whole clone.
26238+
*
26239+
* Every copied object is named in the target namespace: `iojn_customer`
26240+
* becomes `iojn2_customer`, and an object whose name lacks the source
26241+
* prefix (`ticket`) becomes `iojn2_ticket` — the prefix rule's own rename
26242+
* (ADR-0129 D2). Such a name is rewritten where it is REFERENCED (an
26243+
* object-reference property, exact value), never inside free text; and
26244+
* when its target name is already claimed (the source also holds
26245+
* `iojn_ticket`) the object is refused in `failed[]`, before anything is
26246+
* written for it, with the source rename that clears it.
2623826247
*/
2623926248
async duplicatePackage(request: {
2624026249
sourcePackageId: string;
@@ -26364,30 +26373,115 @@ export class ObjectStackProtocolImplementation implements
2636426373
// [#21911] The explicit system opt-in — see findServedOverlayRow.
2636526374
const rows = (await this.engine.find('sys_metadata', { where, context: { isSystem: true } })) as any[];
2636626375

26367-
// Map only OBJECT names that carry the source namespace prefix; views/etc.
26368-
// are renamed by the same prefix swap and reference-rewritten via the map.
26376+
// Views/etc. are renamed by the source-prefix swap alone; a name without
26377+
// the prefix keeps it (the docblock re-namespaces OBJECT names).
2636926378
const renameName = (name: string): string =>
2637026379
sourceNs && typeof name === 'string' && name.startsWith(`${sourceNs}_`)
2637126380
? `${targetNs}_${name.slice(sourceNs.length + 1)}`
2637226381
: name;
26382+
26383+
// ⭐ EVERY OBJECT GETS A NAME IN THE TARGET NAMESPACE (ADR-0070 D4, read
26384+
// with ADR-0129 D2). An object name IS its table name and its registry
26385+
// key, so a copy that keeps the source's name is a second claim on the
26386+
// source's object: before PR #23510 it stored a target row the registry
26387+
// refused, over the source's table, and after it the copy failed with
26388+
// the ADR-0029 D9.9 ownership prescription, which names no way out for
26389+
// a duplicate. ADR-0129 D2 says what the name is: every object a
26390+
// package defines is `${namespace}_${shortName}`, and the prefix rule's
26391+
// own prescription for a name that lacks the prefix
26392+
// (`validateObjectNamespacePrefix`) is "rename it to
26393+
// `${namespace}_${name}`". The copy is a NEW object, so that rename
26394+
// costs nothing here — while renaming the SOURCE object would move its
26395+
// table. A prefixed name swaps the source prefix for the target's;
26396+
// a name WITHOUT the source prefix (a direct-publish save, which no
26397+
// prefix gate reads, or a package that had no namespace) takes the
26398+
// target prefix in front of its whole name.
26399+
//
26400+
// ⛔ REFUSED, NEVER RE-INVENTED: when that name is already claimed — by
26401+
// the copy of a prefixed sibling (`ticket` and `iojn_ticket` both name
26402+
// `iojn2_ticket`) or by an object the source package holds — the object
26403+
// is reported in `failed[]` before anything is written for it, at its
26404+
// own name, with the rename that clears it. A second invented name
26405+
// (a numeric suffix, a third prefix) would be a name nobody wrote.
26406+
const objectNames = new Set<string>(
26407+
rows.filter((row) => row?.type === 'object' && typeof row.name === 'string').map((row) => row.name as string),
26408+
);
2637326409
const renameMap = new Map<string, string>();
26374-
for (const row of rows) {
26375-
if (row?.type === 'object') {
26376-
const nn = renameName(row.name);
26377-
if (nn !== row.name) renameMap.set(row.name, nn);
26410+
const prefixed: string[] = [];
26411+
const unprefixed: string[] = [];
26412+
for (const name of objectNames) {
26413+
if (sourceNs && name.startsWith(`${sourceNs}_`)) prefixed.push(name);
26414+
else unprefixed.push(name);
26415+
}
26416+
for (const name of prefixed) {
26417+
const nn = renameName(name);
26418+
if (nn !== name) renameMap.set(name, nn);
26419+
}
26420+
// Every name a prefixed object's copy takes (itself, when the target
26421+
// namespace IS the source's), and every name the source holds.
26422+
const claimedBy = new Map<string, string>(prefixed.map((name) => [renameName(name), name]));
26423+
const refusedObjects = new Map<string, string>();
26424+
for (const name of unprefixed) {
26425+
const nn = `${targetNs}_${name}`;
26426+
const holder = claimedBy.get(nn) ?? (objectNames.has(nn) ? nn : undefined);
26427+
if (holder !== undefined) {
26428+
const lacks = sourceNs
26429+
? `lacks the '${sourceNs}_' prefix of package '${request.sourcePackageId}' (ADR-0129 D2: `
26430+
+ 'every object a package defines carries its namespace as a name prefix)'
26431+
: `carries no namespace prefix (package '${request.sourcePackageId}' declares no namespace)`;
26432+
const taken = holder === nn
26433+
? `'${nn}' is an object package '${request.sourcePackageId}' already holds`
26434+
: `'${nn}' is already the copy name of object '${holder}'`;
26435+
refusedObjects.set(
26436+
name,
26437+
`Object '${name}' is not copied: its name ${lacks}, so its copy is named '${nn}' in `
26438+
+ `namespace '${targetNs}', and ${taken}. Rename '${name}' on the source to `
26439+
+ `${sourceNs ? `a '${sourceNs}_' name` : 'a name'} no other object in the package uses, `
26440+
+ 'then duplicate again.',
26441+
);
26442+
continue;
2637826443
}
26444+
renameMap.set(name, nn);
26445+
claimedBy.set(nn, name);
2637926446
}
26447+
2638026448
// Longest-first, identifier-boundary rewrite so `iojn_task` never corrupts
2638126449
// `iojn_task_log`, and `iojn_x` inside `record.iojn_x`/`iojn_x.view` matches.
26382-
const olds = [...renameMap.keys()].sort((a, b) => b.length - a.length);
26450+
// Prefixed names only: a prefixed name is distinctive enough to find in
26451+
// free text, a bare one is not (below).
26452+
const olds = prefixed.filter((name) => renameMap.has(name)).sort((a, b) => b.length - a.length);
2638326453
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2638426454
const re = olds.length ? new RegExp(`(${olds.map(esc).join('|')})(?![A-Za-z0-9_])`, 'g') : null;
26385-
const deepRewrite = (v: any): any => {
26386-
if (typeof v === 'string') return re ? v.replace(re, (m) => renameMap.get(m) ?? m) : v;
26387-
if (Array.isArray(v)) return v.map(deepRewrite);
26455+
// ⭐ AT AN OBJECT-REFERENCE PROPERTY, A NAME IS REWRITTEN WHOLE — the
26456+
// only place a BARE name is rewritten. Measured: a free-text rewrite of
26457+
// `ticket` also renames a lookup FIELD called `ticket` (`fields.ticket.
26458+
// name`) and the word in "A support ticket raised by a customer". So a
26459+
// value is rewritten only where it sits under a property that names an
26460+
// object — the derived site index (`REFERENCE_SITES`, target `object`:
26461+
// `reference`, `object`, `objectName`, `objects`, …), read by property
26462+
// wherever it occurs in any item, because flow node configs (`config.
26463+
// objectName`) are declared by node descriptors the index does not
26464+
// walk — and only when it equals a renamed name exactly, in the four
26465+
// shapes a name arrives in there: a string, an array element, a record
26466+
// KEY (`permission.objects` is keyed by object name) or a `.name`. The
26467+
// record key holds for prefixed names too: a copied permission set
26468+
// otherwise kept `objects.<source object>`, a grant on the SOURCE.
26469+
const objectReferenceProperties = new Set(
26470+
(REFERENCE_SITES.byTarget.get('object') ?? []).map((site) => site.property),
26471+
);
26472+
const deepRewrite = (v: any, atReference = false): any => {
26473+
if (typeof v === 'string') {
26474+
const whole = atReference ? renameMap.get(v) : undefined;
26475+
if (whole !== undefined) return whole;
26476+
return re ? v.replace(re, (m) => renameMap.get(m) ?? m) : v;
26477+
}
26478+
if (Array.isArray(v)) return v.map((element) => deepRewrite(element, atReference));
2638826479
if (v && typeof v === 'object') {
2638926480
const o: any = {};
26390-
for (const [k, val] of Object.entries(v)) o[k] = deepRewrite(val);
26481+
for (const [k, val] of Object.entries(v)) {
26482+
const key = atReference ? (renameMap.get(k) ?? k) : k;
26483+
o[key] = deepRewrite(val, objectReferenceProperties.has(k) || (atReference && k === 'name'));
26484+
}
2639126485
return o;
2639226486
}
2639326487
return v;
@@ -26423,7 +26517,12 @@ export class ObjectStackProtocolImplementation implements
2642326517
const canonicalizeFlow = this.resolveFlowCanonicalizer();
2642426518

2642526519
for (const row of rows) {
26426-
const newName = renameName(row.name);
26520+
const refusal = row?.type === 'object' ? refusedObjects.get(row.name) : undefined;
26521+
if (refusal !== undefined) {
26522+
failed.push({ type: row.type, name: row.name, error: refusal });
26523+
continue;
26524+
}
26525+
const newName = row?.type === 'object' ? (renameMap.get(row.name) ?? row.name) : renameName(row.name);
2642726526
const rawType = String(row.type);
2642826527
const singular = PLURAL_TO_SINGULAR[rawType] ?? rawType;
2642926528
let body: unknown;

0 commit comments

Comments
 (0)