Repository navigation
fix(metadata-protocol): the artifact envelope is stamped only on the bundle member the package ships (#23074) - #23094
Conversation
…bundle member the package ships An environment overlay of an email_template member the package does not ship (a zh-CN beside a shipped en-US) was hydrated wearing the bundle's artifact envelope, so isCodeArtifactBody read it as packaged code. mergeArtifactProtection now grafts an artifact only onto a body of its own bundle member (itemDiscriminator), and its callers look the artifact up at the body's own member. removeRuntimeShadow's named-member test returns to member level. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…rate cases Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 236a432854c7da09868ddf3e64958a113412a359 && git checkout 236a432854c7da09868ddf3e64958a113412a359
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6662e3f590e9790a28f598d3eb6f665e46f10703 5d156f704487d2a23106e639b7933a43db3b8cea && git checkout -B drift-repro 6662e3f590e9790a28f598d3eb6f665e46f10703 && git merge --no-ff 5d156f704487d2a23106e639b7933a43db3b8cea
node scripts/docs-audit/affected-docs.mjs --json 6662e3f590e9790a28f598d3eb6f665e46f10703
|
Fixes #23074
Clause-②: no
What was wrong
An
email_templateis identified by(name, locale), so its locales form a bundle. When the environment saves an overlay of a member the package does not ship (azh-CNversion of a template the package ships inen-USonly), that overlay is the environment's own item.mergeArtifactProtectiongrafted the bundle's artifact envelope onto it anyway: every caller looked the artifact up by name, canonical-first, so thezh-CNoverlay was registered and served wearing theen-USartifact's_packageId,_packageVersionand_provenance: 'package', andisCodeArtifactBodyread 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 inremoveRuntimeShadow. This PR changes the graft itself.Measured on
origin/main62ce45d140(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 realObjectQLengine, the real protocol, and a versioned package that shipsacme.digestinen-USonly. Each reading comes afteren-USandzh-CNoverlays are saved and the kernel restarts, so both overlays are hydrated.isCodeArtifactBodyappears as "code".zh-CNoverlaygetItem(type, name, undefined, 'zh-CN'))_packageId: com.acme.mail,_packageVersion: 1.0.0,_provenance: package; code true_provenance: org, no package stamps; code falsegetArtifactItem(..., 'zh-CN'))zh-CNartifactundefinedGET ?member=zh-CN: body and envelopeprovenance: package,packageId,packageVersion: 1.0.0provenance/packageId/packageVersionabsentlock/editable/deletablenone/ true / truenone/ true / true (unchanged, see below)zh-CNitem wears the enveloperemoveOverlayEntry(..., 'zh-CN'))removeRuntimeShadow(..., 'zh-CN'))zh-CNartifact (member level, below)DELETE ?member=zh-CN, end statereset: trueCONTROL, measured at the same readers: the overlay of a shipped member (
acme.welcomeinzh-CN, and the canonicalen-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
isCodeArtifactBodyreaders never see anemail_templatemember, 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_templateis 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 byitemDiscriminator(bundleMemberOf), the identity's one declaration inITEM_KEY_DISCRIMINATORS. An artifact of another member counts as no artifact. Every type without members readsundefinedon both sides, so its merge is byte-for-byte what it was.lookupArtifactItem'smember): 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 theen-USartifact for azh-CNbody (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.mergeArtifactProtectionandbundleMemberOfare module-private, andremoveRuntimeShadowkeeps its signature.Deliberately unchanged
lockand the_lockgate 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.resettableand the delete receipt come fromisArtifactBacked(type, name), which is name-level and not the graft. An unshipped member still readsresettable: trueand its reset answersreset: 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:zh-CNartifact); CONTROL: the shipped members' overlays wear their artifacts' envelopes;?member=zh-CN: served as the environment's item (no envelope,lock: none, editable, deletable); CONTROL: the shipped member is served under its package;?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;member(answers the canonical artifact): the graft's own check keeps that artifact off thezh-CNbody; the canonical member still wears it;translation, which carries alocalebut is identified by its name) keeps its artifact's envelope when the overlay'slocalediffers from the artifact's.Also updated: the
registry-i18n-bundle-key.test.tscase 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 atraprestore. Eachmetadata-protocolleg was rebuilt and checked withscripts/ablation-dist-preflight.mjs, because objectql's suite reads that package'sdist/. The two legs that delete the guard fail the DTS step (TS6133: the newtypeparameter 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 emptygit diff HEAD, then rebuilt and preflighted present again. The final run was 29/29 green.removeRuntimeShadowback to the name-level test (registry source, no build)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 rewrotemetadata-protocol/dist/index.jsmid-leg. The preflight caught it (marker still present), and the run was redone with nothing in parallel (the readings above).Evidence
packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts, the showcase file that found the defect (real SQLite, realsendTemplate, real MetadataService), passes 4/4 on the fixeddist/:DELETE ?member=zh-CN, then the nextzh-CNsend uses the admin'sen-USwording at once, and?member=zh-CNis 404 after a cold boot.pnpm --filter @objectstack/metadata-protocol typecheckexit 0;pnpm --filter @objectstack/objectql typecheckexit 0 (itscheck:test-typecheckheld the ledger).@objectstack/metadata-protocolvitest 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 at5d156f7044.eslint --no-inline-config --format jsonover the six changed TS files (eslint.config.mjsapplies 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 (noparserOptions.project, no typed rules), so this diff cannot change the verdict on any untouched file.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat5d156f7044(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-loadsandcheck:type-check-debtwere measured after a fullturbo run buildof./packages/*(72 tasks) at that head. Every heavy run went throughscripts/pm/os-verify-lock.sh, and each result is itsVERDICT command-exit 0line.Acceptance notes
resettable: trueon the by-name read, andreset: trueonDELETE ?member=, for a member the package does not ship: both come fromisArtifactBacked(type, name), by name. After that reset thezh-CNsends fall back to the package'sen-USwording (the send ladder), whileGET ?member=zh-CNanswers 404. So "reset to the package default" is true at send time and not at the by-name read. [finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877 pinnedreset: truefor this case on purpose. Recorded, not changed here. Carrier: none.getMetaItemLayeredtakes no member, so its code layer is always the canonical artifact. That is unchanged and outside the graft.Generated by Claude Code