Skip to content

Commit abf8655

Browse files
fix(metadata-protocol): the artifact envelope is stamped only on the bundle member the package ships (#23074) (#23094)
Fixes #23074 Clause-②: no ## What was wrong An `email_template` is identified by `(name, locale)`, so its locales form a bundle. When the environment saves an overlay of a member the package does **not** ship (a `zh-CN` version of a template the package ships in `en-US` only), that overlay is the environment's own item. `mergeArtifactProtection` grafted the bundle's artifact envelope onto it anyway: every caller looked the artifact up by name, canonical-first, so the `zh-CN` overlay was registered and served wearing the `en-US` artifact's `_packageId`, `_packageVersion` and `_provenance: 'package'`, and `isCodeArtifactBody` read it as packaged code. PR #23070 (stage 2 of #22877, `038d4c9588`) routed around one reader: it lifted the member's runtime shadow on ANY member's artifact in `removeRuntimeShadow`. This PR changes the graft itself. ## Measured on `origin/main` `62ce45d140` (before) and on this branch (after) The fixture is the stage 1/2 harness `packages/objectql/src/environment-overlay-email-locale-restart.test.ts`: a real `ObjectQL` engine, the real protocol, and a versioned package that ships `acme.digest` in `en-US` only. Each reading comes after `en-US` and `zh-CN` overlays are saved and the kernel restarts, so both overlays are hydrated. `isCodeArtifactBody` appears as "code". | reader of the unshipped `zh-CN` overlay | before | after | |:---|:---|:---| | registry entry (`getItem(type, name, undefined, 'zh-CN')`) | `_packageId: com.acme.mail`, `_packageVersion: 1.0.0`, `_provenance: package`; code **true** | `_provenance: org`, no package stamps; code false | | registry artifact lookup (`getArtifactItem(..., 'zh-CN')`) | answers **the overlay itself** as the package's `zh-CN` artifact | `undefined` | | by-name read, `GET ?member=zh-CN`: body and envelope | body wears the envelope; `provenance: package`, `packageId`, `packageVersion: 1.0.0` | no envelope; `provenance` / `packageId` / `packageVersion` absent | | by-name read: `lock` / `editable` / `deletable` | `none` / true / true | `none` / true / true (unchanged, see below) | | list, served and for execution | the `zh-CN` item wears the envelope | no envelope | | overlay-entry retirement (`removeOverlayEntry(..., 'zh-CN')`) | **refused** as code | retires it | | shadow heal (`removeRuntimeShadow(..., 'zh-CN')`) | lifts it (the #23070 name-level test) | declines: no `zh-CN` artifact (member level, below) | | `DELETE ?member=zh-CN`, end state | entry gone, receipt `reset: true` | same | CONTROL, measured at the same readers: the overlay of a shipped member (`acme.welcome` in `zh-CN`, and the canonical `en-US`) wears its artifact's envelope, is answered by its artifact, is served under its package, is refused by the retirement and un-shadowed by the heal. Nothing about it changes. The other `isCodeArtifactBody` readers never see an `email_template` member, because each is gated on its own type: `runtime/src/domains/actions.ts` (actions), `runtime/src/flow-clone.ts`, `service-automation`'s flow precedence and the protocol's flow write refusal (flows), `plugin-security/src/packaged-permission-set-lock.ts` (permission sets), the protocol's declined-row predicates (shipped flows, code datasources, sealed types; `email_template` is overlay-enabled, so never sealed), and the registry's object branch and collision warning (objects, and composite artifacts only). ## The change - **`mergeArtifactProtection` (`metadata-protocol`)** takes the type and grafts an artifact only onto a body of the **same bundle member**, read by `itemDiscriminator` (`bundleMemberOf`), the identity's one declaration in `ITEM_KEY_DISCRIMINATORS`. An artifact of another member counts as no artifact. Every type without members reads `undefined` on both sides, so its merge is byte-for-byte what it was. - **Its four call sites** pass the type, and the three that can serve a bundle member look the artifact up at the body's own member (`lookupArtifactItem`'s `member`): the boot and write-through hydration (`hydrateOverlayIntoRegistry`), the by-name read (`getMetaItem`) and the list (`readFlattenedMetaItems`). Without this second half, a shipped non-canonical member would lose its envelope, because the canonical-first lookup answers the `en-US` artifact for a `zh-CN` body (ablation A2 below). The view expansion (`expandRuntimeViewContainer`) only passes the type: a view has no members. - **`removeRuntimeShadow` (`objectql`) returns to member level** for a named member: a shadow is lifted only where an artifact **of that member** takes its place. An unshipped member's overlay now declines there, and the heal's last tier (`removeOverlayEntry`) retires it, because it is no longer grafted. Naming no member keeps the group sweep, as before. - No exported signature changes: `mergeArtifactProtection` and `bundleMemberOf` are module-private, and `removeRuntimeShadow` keeps its signature. ### Deliberately unchanged - **The lock binds the name.** The read's `lock` and the `_lock` gate both come from the one item-lock resolution over every package that ships the NAME (`artifactLockLayerAt`). The merge applies the read's resolved lock before its member check, so a template the package locks stays locked in every locale, at the read and at the gate. Making the lock per member would loosen protection, and that is a contract decision this card does not carry. - **`resettable` and the delete receipt** come from `isArtifactBacked(type, name)`, which is name-level and not the graft. An unshipped member still reads `resettable: true` and its reset answers `reset: true`. See the acceptance notes. ## Pins (real doors) All in `packages/objectql/src/environment-overlay-email-locale-restart.test.ts`, new block `[#23074]`, with each CONTROL as its own case: - the boot hydration: the unshipped member registers as the tenant row it is (no stamps, not code, no `zh-CN` artifact); CONTROL: the shipped members' overlays wear their artifacts' envelopes; - the by-name read `?member=zh-CN`: served as the environment's item (no envelope, `lock: none`, editable, deletable); CONTROL: the shipped member is served under its package; - the list, served and for execution: the same split; - the reset `?member=zh-CN`: the unshipped member is declined by the shadow heal and retired by the overlay-entry tier, at once and after a restart; CONTROL: the shipped member is un-shadowed onto its artifact and refused by the retirement; - a registry that ignores `member` (answers the canonical artifact): the graft's own check keeps that artifact off the `zh-CN` body; the canonical member still wears it; - CONTROL: a type with no members (`translation`, which carries a `locale` but is identified by its name) keeps its artifact's envelope when the overlay's `locale` differs from the artifact's. Also updated: the `registry-i18n-bundle-key.test.ts` case that pinned the name-level lift for an unshipped member now pins the member-level decline and the retirement. The two call-site ledgers (`protocol.lookup-artifact-item-call-sites.test.ts`, `protocol.register-item-call-sites.test.ts`) record the new argument text. ## Ablation Run from committed state with `scripts/ablation-replace.mjs`, under a `trap` restore. Each `metadata-protocol` leg was rebuilt and checked with `scripts/ablation-dist-preflight.mjs`, because objectql's suite reads that package's `dist/`. The two legs that delete the guard fail the DTS step (TS6133: the new `type` parameter is unread), but ESM and CJS are emitted, and the preflight proves the ESM the suite consumes carries the mutation. Every restore was proven by blob hash equal to HEAD and an empty `git diff HEAD`, then rebuilt and preflighted present again. The final run was 29/29 green. | leg | mutation | red | green | |:---|:---|:---|:---| | A3 | the unconditional graft back: guard removed, canonical-first lookups | 6: all five unshipped cases, plus #22877's door case for a member no artifact ships | every CONTROL (23) | | A1 | the guard alone removed | only "a registry that answers another member's artifact" | 28 | | A2 | the member-addressed lookups alone removed | exactly the four shipped-member CONTROLs | 25 | | A4 | `removeRuntimeShadow` back to the name-level test (registry source, no build) | only the unshipped reset case | 28 | A3 also shows the decision about `removeRuntimeShadow`: with the graft back, the member-level heal leaves the grafted overlay registered (the #22877 door case turns red). So the member-level test is sound **because** of the graft fix, and not without it. One leg is declared void: a first A3 run coincided with an unlocked `check:type-check-debt`, whose full build rewrote `metadata-protocol/dist/index.js` mid-leg. The preflight caught it (`marker still present`), and the run was redone with nothing in parallel (the readings above). ## Evidence - Real door: `packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts`, the showcase file that found the defect (real SQLite, real `sendTemplate`, real MetadataService), passes 4/4 on the fixed `dist/`: `DELETE ?member=zh-CN`, then the next `zh-CN` send uses the admin's `en-US` wording at once, and `?member=zh-CN` is 404 after a cold boot. - `pnpm --filter @objectstack/metadata-protocol typecheck` exit 0; `pnpm --filter @objectstack/objectql typecheck` exit 0 (its `check:test-typecheck` held the ledger). - `@objectstack/metadata-protocol` `vitest run`: 228 files passed, 3 skipped; 28203 tests passed. `@objectstack/objectql` `--project local`: 408/408 files, 7984 tests; `--project repo`: 1/1 file, 5 tests. All at `5d156f7044`. - Lint, a proven narrowing: `eslint --no-inline-config --format json` over the six changed TS files (`eslint.config.mjs` applies to all six, and none is reported as ignored) gives 6 files, 0 errors, 0 warnings. This repo's config enables no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot change the verdict on any untouched file. - Gates: the derived union `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `5d156f7044` (the dispatch's 58 commands plus 11 changeset families this diff adds): **69 commands, 69 exit 0**, each exit code captured before any pipe. Reconciled with `--ran`: `69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN`. `check:dual-build-cjs-loads` and `check:type-check-debt` were measured after a full `turbo run build` of `./packages/*` (72 tasks) at that head. Every heavy run went through `scripts/pm/os-verify-lock.sh`, and each result is its `VERDICT command-exit 0` line. ## Acceptance notes - `resettable: true` on the by-name read, and `reset: true` on `DELETE ?member=`, for a member the package does not ship: both come from `isArtifactBacked(type, name)`, by name. After that reset the `zh-CN` sends fall back to the package's `en-US` wording (the send ladder), while `GET ?member=zh-CN` answers 404. So "reset to the package default" is true at send time and not at the by-name read. #22877 pinned `reset: true` for this case on purpose. Recorded, not changed here. Carrier: none. - `getMetaItemLayered` takes no member, so its code layer is always the canonical artifact. That is unchanged and outside the graft. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0f1c5b commit abf8655

7 files changed

Lines changed: 305 additions & 30 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/objectql': patch
4+
---
5+
6+
A language the environment added to an email template, which the package does not ship, is no longer treated as packaged code
7+
8+
Clause-②: no
9+
10+
An `email_template` is identified by its name and its `locale`, so the languages of one template form a bundle. When an administrator customizes a template in a language its package does not ship (for example a `zh-CN` version of a template the package ships in `en-US` only), that version is the environment's own item. Until now it was registered and served wearing the package's protection envelope, taken from the shipped `en-US` version:
11+
12+
- the registry's artifact lookup answered the environment's `zh-CN` version as the package's own `zh-CN` artifact;
13+
- `GET /meta/email_template/:name?member=zh-CN` and the `/meta/email_template` list served it with `provenance: 'package'`, the package's id and its version;
14+
- the registry heal that runs after `DELETE ?member=zh-CN` refused to retire it as packaged code.
15+
16+
- `@objectstack/metadata-protocol`: the artifact envelope (`_packageId`, `_packageVersion`, `_provenance`, and an artifact lock) is copied only onto a version of the same language as the shipped artifact. Each place that copies it looks up the artifact of the body's own language first. A language the package does not ship is registered and served as the environment's item. A language the package does ship keeps its envelope, its lock and its reset exactly as before. Types without languages are unchanged.
17+
- `@objectstack/objectql`: `SchemaRegistry.removeRuntimeShadow(type, name, member)` lifts a named language's shadow only when the package ships that language. For any other language it declines, and the heal's last step (`removeOverlayEntry`) retires the entry. Naming no language still sweeps the whole bundle, as before.
18+
19+
A lock still binds the template name, at the read and at the write gate alike: a template the package locks stays locked in every language.

‎packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ type Disposition = 'definition' | 'served-package envelope' | 'no';
7070
const SITES: ReadonlyArray<Site & { readonly noPackage: boolean; readonly disposition: Disposition; readonly why: string }> = [
7171
{ member: 'lookupArtifactItem', args: 'type: string, name: string, currentPackageId?: string, member?: string', noPackage: false, disposition: 'definition',
7272
why: 'the definition itself' },
73-
{ member: 'getMetaItem', args: 'request.type, request.name, envelopePackageId(request.packageId, item)', noPackage: true, disposition: 'served-package envelope',
74-
why: 'the by-name read\'s envelope: naming no package, the package of the item it serves (the list\'s rule)' },
75-
{ member: 'readFlattenedMetaItems', args: 'request.type, itemName, itemPackageId', noPackage: true, disposition: 'served-package envelope',
76-
why: 'the list\'s envelope: each item\'s own package, the rule the by-name read shares' },
73+
{ member: 'getMetaItem', args: 'request.type, request.name, envelopePackageId(request.packageId, item), bundleMemberOf(request.type, item)', noPackage: true, disposition: 'served-package envelope',
74+
why: 'the by-name read\'s envelope: naming no package, the package of the item it serves (the list\'s rule); [#23074] at the served item\'s own bundle member' },
75+
{ member: 'readFlattenedMetaItems', args: 'request.type, itemName, itemPackageId, bundleMemberOf(request.type, it)', noPackage: true, disposition: 'served-package envelope',
76+
why: 'the list\'s envelope: each item\'s own package, the rule the by-name read shares; [#23074] at each item\'s own bundle member' },
7777
{ member: 'getMetaItem', args: 'request.type, request.name, request.packageId', noPackage: true, disposition: 'no',
7878
why: 'the shipped-flow arm: the served body IS this artifact, and the envelope is looked up at its own package' },
7979
{ member: 'getMetaItem', args: 'request.type, request.name, request.packageId, wantedMember', noPackage: true, disposition: 'no',
@@ -94,8 +94,8 @@ const SITES: ReadonlyArray<Site & { readonly noPackage: boolean; readonly dispos
9494
why: 'the lock\'s artifact layer (the one item-lock resolution), never an envelope on a served body' },
9595
{ member: 'shippedArtifactsOf', args: 'type, name, packageId', noPackage: false, disposition: 'no',
9696
why: 'scoped to a named package, and kept only when it is that package\'s own' },
97-
{ member: 'hydrateOverlayIntoRegistry', args: 'type, (data as any).name, options.packageId ?? undefined', noPackage: true, disposition: 'no',
98-
why: 'scoped to the row\'s own package; undefined only for a package-less row, whose body names no package' },
97+
{ member: 'hydrateOverlayIntoRegistry', args: 'type, (data as any).name, options.packageId ?? undefined, bundleMemberOf(type, data)', noPackage: true, disposition: 'no',
98+
why: 'scoped to the row\'s own package; undefined only for a package-less row, whose body names no package; [#23074] at the row\'s own bundle member' },
9999
{ member: 'expandRuntimeViewContainer', args: 'type, String(item.name), ownPackageId', noPackage: false, disposition: 'no',
100100
why: 'not called without a package, and kept only when it is the container\'s own package\'s' },
101101
{ member: 'shippedViewContainerOf', args: 'type, name, packageId', noPackage: false, disposition: 'no',

‎packages/metadata-protocol/src/protocol.register-item-call-sites.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const SITES: ReadonlyArray<Site & {
9595
+ 'and a row bound to another package is refused (D9.9, OBJECT_OVERLAY_PACKAGE_MISMATCH)',
9696
},
9797
{
98-
member: 'hydrateOverlayIntoRegistry', args: 'type, mergeArtifactProtection(stateTenantAuthorship(data), envelope), \'name\' as any',
98+
member: 'hydrateOverlayIntoRegistry', args: 'type, mergeArtifactProtection(type, stateTenantAuthorship(data), envelope), \'name\' as any',
9999
disposition: 'yes, view only: skips a name another package ships',
100100
path: 'the list\'s hydration and the write-through on an unscoped kernel; the boot\'s loadMetaFromDb on every kernel',
101101
why: 'a stored row: a view row bound to a package is not registered where another package ships its name, and the '

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

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,8 +1830,31 @@ type ShippingPackages = {
18301830
* stands in for the overlay layer over `artifactItem` alone, as before: the
18311831
* registry's hydration and a previewed draft, neither of which is a read's
18321832
* lock answer.
1833+
*
1834+
* [#23074] An artifact lends its envelope only to a body of its OWN identity.
1835+
* For a type whose identity is a pair ({@link ITEM_KEY_DISCRIMINATORS}:
1836+
* `email_template`'s `(name, locale)`), that is the same bundle member, read
1837+
* by {@link bundleMemberOf}, the identity's one declaration; an artifact of
1838+
* another member is treated as no artifact at all. So an environment overlay
1839+
* of a member the package does not ship (a `zh-CN` template beside a shipped
1840+
* `en-US`) keeps the tenant authorship it was registered with, and is never
1841+
* read as packaged code: `isCodeArtifactBody` false, no `_packageId` /
1842+
* `_packageVersion` / `_provenance` stamp, no artifact `_lock*`. Grafted, it
1843+
* read as the bundle's own artifact of that member to the registry's artifact
1844+
* lookup and its overlay-entry retirement, and the read doors served it under
1845+
* the package's provenance. The shipped member's overlay is unchanged: each
1846+
* caller looks the artifact up at the body's own member
1847+
* (`lookupArtifactItem`'s `member`), so it still wears that member's
1848+
* artifact's envelope. A registry that ignores `member` can answer another
1849+
* member's artifact, and this check is what keeps that answer off the body.
1850+
* Every other type has no members, so `bundleMemberOf` is `undefined` on both
1851+
* sides and the merge is exactly what it was. ⛔ The lock family a read
1852+
* resolved (`itemLock`) is applied before this check and is not moved by it:
1853+
* a lock binds the NAME, at the read and at the `_lock` gate alike
1854+
* ({@link resolveItemLock} over every package that ships the name).
18331855
*/
18341856
function mergeArtifactProtection(
1857+
type: string,
18351858
item: unknown,
18361859
artifactItem: unknown,
18371860
itemLock?: ItemLock,
@@ -1841,6 +1864,7 @@ function mergeArtifactProtection(
18411864
if (artifactItem === undefined || artifactItem === null) return item;
18421865
const a = artifactItem as Record<string, unknown>;
18431866
if (typeof a !== 'object') return item;
1867+
if (bundleMemberOf(type, a) !== bundleMemberOf(type, item)) return item;
18441868
const out: Record<string, unknown> = { ...(item as Record<string, unknown>) };
18451869
if (itemLock === undefined && resolveItemLock({ artifact: [a], overlay: item }).layer === 'artifact') {
18461870
if (a._lock !== undefined) out._lock = a._lock;
@@ -1854,6 +1878,18 @@ function mergeArtifactProtection(
18541878
return out;
18551879
}
18561880

1881+
/**
1882+
* [#23074] The bundle member a body IS, for a type whose identity is a pair
1883+
* ({@link ITEM_KEY_DISCRIMINATORS}): `itemDiscriminator` under the type's
1884+
* canonical spelling, so the canonical member for a body that declares none.
1885+
* `undefined` for every other type, which has no members. The member
1886+
* {@link mergeArtifactProtection} matches an artifact against, and the one
1887+
* each of its callers looks the artifact up at.
1888+
*/
1889+
function bundleMemberOf(type: string, body: unknown): string | undefined {
1890+
return itemDiscriminator(canonicalMetaType(type), body);
1891+
}
1892+
18571893
/**
18581894
* [#22024, ADR-0048, ADR-0010 §3.3] The package whose artifact envelope
18591895
* ({@link mergeArtifactProtection}: `_packageId`, `_packageVersion`,
@@ -9703,7 +9739,9 @@ export class ObjectStackProtocolImplementation implements
97039739
// takes priority; else the item's own `_packageId`.) [#22024]
97049740
// The by-name read takes the same rule ({@link envelopePackageId}).
97059741
const itemPackageId = envelopePackageId(packageId, it);
9706-
const a = this.lookupArtifactItem(request.type, itemName, itemPackageId);
9742+
// [#23074] …and to the item's own bundle member, whose artifact
9743+
// alone may lend it an envelope ({@link mergeArtifactProtection}).
9744+
const a = this.lookupArtifactItem(request.type, itemName, itemPackageId, bundleMemberOf(request.type, it));
97079745
let itemLock: ItemLock | undefined;
97089746
if (typeof itemName === 'string' && (it as any)?._draft !== true) {
97099747
const address: ItemAddress = {
@@ -9724,7 +9762,7 @@ export class ObjectStackProtocolImplementation implements
97249762
// is the other exit a client reads field metadata from, and
97259763
// an overlay row wins over the (already-governed) registry
97269764
// entry in the merge above, so it carries the same lie.
9727-
governed.push(this.governServedObject(request.type, mergeArtifactProtection(it, a, itemLock)));
9765+
governed.push(this.governServedObject(request.type, mergeArtifactProtection(request.type, it, a, itemLock)));
97289766
}
97299767
return {
97309768
type: request.type,
@@ -10489,8 +10527,11 @@ export class ObjectStackProtocolImplementation implements
1048910527
// serves. Before, a read naming no package took the first-registered
1049010528
// package's envelope, so a name two packages ship answered one
1049110529
// package's stored copy under the other package's `_packageId`.
10530+
// [#23074] …and at the served item's own bundle member, whose artifact
10531+
// alone may lend it an envelope ({@link mergeArtifactProtection}): a
10532+
// member the package does not ship is served as the environment's own.
1049210533
const artifactItem = this.lookupArtifactItem(
10493-
request.type, request.name, envelopePackageId(request.packageId, item),
10534+
request.type, request.name, envelopePackageId(request.packageId, item), bundleMemberOf(request.type, item),
1049410535
);
1049510536
// [#21738] The lock is the one item-lock resolution's, over the layers
1049610537
// this read resolved from its address: [#21803] every installed
@@ -10511,7 +10552,7 @@ export class ObjectStackProtocolImplementation implements
1051110552
request.type,
1051210553
this.governServedObject(
1051310554
request.type,
10514-
mergeArtifactProtection(item, artifactItem, itemLock),
10555+
mergeArtifactProtection(request.type, item, artifactItem, itemLock),
1051510556
),
1051610557
);
1051710558
// ADR-0047 — list views additionally get reference-integrity
@@ -18751,7 +18792,15 @@ export class ObjectStackProtocolImplementation implements
1875118792
this.hydrateExpandedViewItems(type, data, options, registry);
1875218793
return true;
1875318794
}
18754-
const artifact = this.lookupArtifactItem(type, (data as any).name, options.packageId ?? undefined);
18795+
// [#23074] The artifact of the row's OWN bundle member: an overlay of
18796+
// a member the package does not ship has none, so it registers as the
18797+
// tenant row it is ({@link mergeArtifactProtection}). It used to take
18798+
// the bundle's artifact canonical-first, and a `zh-CN` overlay beside
18799+
// a shipped `en-US` was registered wearing that envelope, which every
18800+
// registry reader then took for the package's own `zh-CN` artifact.
18801+
const artifact = this.lookupArtifactItem(
18802+
type, (data as any).name, options.packageId ?? undefined, bundleMemberOf(type, data),
18803+
);
1875518804
// [#16702] Say what this row IS before the artifact envelope is grafted
1875618805
// on top of it. Every body reaching this hydrator came out of a
1875718806
// `sys_metadata` write and is therefore tenant-authored by definition
@@ -18773,7 +18822,7 @@ export class ObjectStackProtocolImplementation implements
1877318822
// and its receipt rendered both as the package. Registered as the
1877418823
// tenant row it is, it is reported as one and never armed.
1877518824
const envelope = canonicalType === 'flow' ? undefined : artifact;
18776-
registry.registerItem(type, mergeArtifactProtection(stateTenantAuthorship(data), envelope), 'name' as any);
18825+
registry.registerItem(type, mergeArtifactProtection(type, stateTenantAuthorship(data), envelope), 'name' as any);
1877718826
this.hydrateExpandedViewItems(type, data, options, registry);
1877818827
return true;
1877918828
}
@@ -18925,7 +18974,7 @@ export class ObjectStackProtocolImplementation implements
1892518974
// where the item's own artifact exists, its `_provenance` still
1892618975
// wins (ADR-0010 §3.3), as it does on the container.
1892718976
const authored = options.tenantAuthored ? stateTenantAuthorship(item) : item;
18928-
out.push(mergeArtifactProtection(authored, ownArtifact) as Record<string, unknown>);
18977+
out.push(mergeArtifactProtection(type, authored, ownArtifact) as Record<string, unknown>);
1892918978
}
1893018979
return out;
1893118980
}

0 commit comments

Comments
 (0)