@@ -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