@@ -1777,8 +1777,31 @@ type ShippingPackages = {
17771777 * stands in for the overlay layer over `artifactItem` alone, as before: the
17781778 * registry's hydration and a previewed draft, neither of which is a read's
17791779 * lock answer.
1780+ *
1781+ * [#23074] An artifact lends its envelope only to a body of its OWN identity.
1782+ * For a type whose identity is a pair ({@link ITEM_KEY_DISCRIMINATORS}:
1783+ * `email_template`'s `(name, locale)`), that is the same bundle member, read
1784+ * by {@link bundleMemberOf}, the identity's one declaration; an artifact of
1785+ * another member is treated as no artifact at all. So an environment overlay
1786+ * of a member the package does not ship (a `zh-CN` template beside a shipped
1787+ * `en-US`) keeps the tenant authorship it was registered with, and is never
1788+ * read as packaged code: `isCodeArtifactBody` false, no `_packageId` /
1789+ * `_packageVersion` / `_provenance` stamp, no artifact `_lock*`. Grafted, it
1790+ * read as the bundle's own artifact of that member to the registry's artifact
1791+ * lookup and its overlay-entry retirement, and the read doors served it under
1792+ * the package's provenance. The shipped member's overlay is unchanged: each
1793+ * caller looks the artifact up at the body's own member
1794+ * (`lookupArtifactItem`'s `member`), so it still wears that member's
1795+ * artifact's envelope. A registry that ignores `member` can answer another
1796+ * member's artifact, and this check is what keeps that answer off the body.
1797+ * Every other type has no members, so `bundleMemberOf` is `undefined` on both
1798+ * sides and the merge is exactly what it was. ⛔ The lock family a read
1799+ * resolved (`itemLock`) is applied before this check and is not moved by it:
1800+ * a lock binds the NAME, at the read and at the `_lock` gate alike
1801+ * ({@link resolveItemLock} over every package that ships the name).
17801802 */
17811803function mergeArtifactProtection(
1804+ type: string,
17821805 item: unknown,
17831806 artifactItem: unknown,
17841807 itemLock?: ItemLock,
@@ -1788,6 +1811,7 @@ function mergeArtifactProtection(
17881811 if (artifactItem === undefined || artifactItem === null) return item;
17891812 const a = artifactItem as Record<string, unknown>;
17901813 if (typeof a !== 'object') return item;
1814+ if (bundleMemberOf(type, a) !== bundleMemberOf(type, item)) return item;
17911815 const out: Record<string, unknown> = { ...(item as Record<string, unknown>) };
17921816 if (itemLock === undefined && resolveItemLock({ artifact: [a], overlay: item }).layer === 'artifact') {
17931817 if (a._lock !== undefined) out._lock = a._lock;
@@ -1801,6 +1825,18 @@ function mergeArtifactProtection(
18011825 return out;
18021826}
18031827
1828+ /**
1829+ * [#23074] The bundle member a body IS, for a type whose identity is a pair
1830+ * ({@link ITEM_KEY_DISCRIMINATORS}): `itemDiscriminator` under the type's
1831+ * canonical spelling, so the canonical member for a body that declares none.
1832+ * `undefined` for every other type, which has no members. The member
1833+ * {@link mergeArtifactProtection} matches an artifact against, and the one
1834+ * each of its callers looks the artifact up at.
1835+ */
1836+ function bundleMemberOf(type: string, body: unknown): string | undefined {
1837+ return itemDiscriminator(canonicalMetaType(type), body);
1838+ }
1839+
18041840/**
18051841 * [#22024, ADR-0048, ADR-0010 §3.3] The package whose artifact envelope
18061842 * ({@link mergeArtifactProtection}: `_packageId`, `_packageVersion`,
@@ -9650,7 +9686,9 @@ export class ObjectStackProtocolImplementation implements
96509686 // takes priority; else the item's own `_packageId`.) [#22024]
96519687 // The by-name read takes the same rule ({@link envelopePackageId}).
96529688 const itemPackageId = envelopePackageId(packageId, it);
9653- const a = this.lookupArtifactItem(request.type, itemName, itemPackageId);
9689+ // [#23074] …and to the item's own bundle member, whose artifact
9690+ // alone may lend it an envelope ({@link mergeArtifactProtection}).
9691+ const a = this.lookupArtifactItem(request.type, itemName, itemPackageId, bundleMemberOf(request.type, it));
96549692 let itemLock: ItemLock | undefined;
96559693 if (typeof itemName === 'string' && (it as any)?._draft !== true) {
96569694 const address: ItemAddress = {
@@ -9671,7 +9709,7 @@ export class ObjectStackProtocolImplementation implements
96719709 // is the other exit a client reads field metadata from, and
96729710 // an overlay row wins over the (already-governed) registry
96739711 // entry in the merge above, so it carries the same lie.
9674- governed.push(this.governServedObject(request.type, mergeArtifactProtection(it, a, itemLock)));
9712+ governed.push(this.governServedObject(request.type, mergeArtifactProtection(request.type, it, a, itemLock)));
96759713 }
96769714 return {
96779715 type: request.type,
@@ -10436,8 +10474,11 @@ export class ObjectStackProtocolImplementation implements
1043610474 // serves. Before, a read naming no package took the first-registered
1043710475 // package's envelope, so a name two packages ship answered one
1043810476 // package's stored copy under the other package's `_packageId`.
10477+ // [#23074] …and at the served item's own bundle member, whose artifact
10478+ // alone may lend it an envelope ({@link mergeArtifactProtection}): a
10479+ // member the package does not ship is served as the environment's own.
1043910480 const artifactItem = this.lookupArtifactItem(
10440- request.type, request.name, envelopePackageId(request.packageId, item),
10481+ request.type, request.name, envelopePackageId(request.packageId, item), bundleMemberOf(request.type, item),
1044110482 );
1044210483 // [#21738] The lock is the one item-lock resolution's, over the layers
1044310484 // this read resolved from its address: [#21803] every installed
@@ -10458,7 +10499,7 @@ export class ObjectStackProtocolImplementation implements
1045810499 request.type,
1045910500 this.governServedObject(
1046010501 request.type,
10461- mergeArtifactProtection(item, artifactItem, itemLock),
10502+ mergeArtifactProtection(request.type, item, artifactItem, itemLock),
1046210503 ),
1046310504 );
1046410505 // ADR-0047 — list views additionally get reference-integrity
@@ -18698,7 +18739,15 @@ export class ObjectStackProtocolImplementation implements
1869818739 this.hydrateExpandedViewItems(type, data, options, registry);
1869918740 return true;
1870018741 }
18701- const artifact = this.lookupArtifactItem(type, (data as any).name, options.packageId ?? undefined);
18742+ // [#23074] The artifact of the row's OWN bundle member: an overlay of
18743+ // a member the package does not ship has none, so it registers as the
18744+ // tenant row it is ({@link mergeArtifactProtection}). It used to take
18745+ // the bundle's artifact canonical-first, and a `zh-CN` overlay beside
18746+ // a shipped `en-US` was registered wearing that envelope, which every
18747+ // registry reader then took for the package's own `zh-CN` artifact.
18748+ const artifact = this.lookupArtifactItem(
18749+ type, (data as any).name, options.packageId ?? undefined, bundleMemberOf(type, data),
18750+ );
1870218751 // [#16702] Say what this row IS before the artifact envelope is grafted
1870318752 // on top of it. Every body reaching this hydrator came out of a
1870418753 // `sys_metadata` write and is therefore tenant-authored by definition
@@ -18720,7 +18769,7 @@ export class ObjectStackProtocolImplementation implements
1872018769 // and its receipt rendered both as the package. Registered as the
1872118770 // tenant row it is, it is reported as one and never armed.
1872218771 const envelope = canonicalType === 'flow' ? undefined : artifact;
18723- registry.registerItem(type, mergeArtifactProtection(stateTenantAuthorship(data), envelope), 'name' as any);
18772+ registry.registerItem(type, mergeArtifactProtection(type, stateTenantAuthorship(data), envelope), 'name' as any);
1872418773 this.hydrateExpandedViewItems(type, data, options, registry);
1872518774 return true;
1872618775 }
@@ -18872,7 +18921,7 @@ export class ObjectStackProtocolImplementation implements
1887218921 // where the item's own artifact exists, its `_provenance` still
1887318922 // wins (ADR-0010 §3.3), as it does on the container.
1887418923 const authored = options.tenantAuthored ? stateTenantAuthorship(item) : item;
18875- out.push(mergeArtifactProtection(authored, ownArtifact) as Record<string, unknown>);
18924+ out.push(mergeArtifactProtection(type, authored, ownArtifact) as Record<string, unknown>);
1887618925 }
1887718926 return out;
1887818927 }
0 commit comments