fix(metadata-protocol): a hydrated view expansion carries its container's tenant marker, so an unscoped kernel answers an expanded view as env_local does (#21511) - #21603
Conversation
…er's tenant marker On an unscoped kernel, registry hydration registered each expansion of a stored environment-wide view container without the tenant-authorship marker its container carries. An expansion of a package-bound container then read as a code artifact through the registry's bare-key fallback: the by-name read answered resettable and the layers read answered the expansion as its code layer, where env_local answered neither. hydrateExpandedViewItems now asks expandRuntimeViewContainer for tenant-authored expansions: stateTenantAuthorship first, then the expansion's own artifact envelope, the order the container gets. The registry-free reads are unchanged. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…pansion-tenant-marker
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 50b8fc75a83028a9624ad1de6b39e7592a69eab3 && git checkout 50b8fc75a83028a9624ad1de6b39e7592a69eab3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b610eabf721672ab621dd1f45e2d1dafbf76a740 09033d877d6160e270b03192581264d80c2fad5d && git checkout -B drift-repro b610eabf721672ab621dd1f45e2d1dafbf76a740 && git merge --no-ff 09033d877d6160e270b03192581264d80c2fad5d
node scripts/docs-audit/affected-docs.mjs --json b610eabf721672ab621dd1f45e2d1dafbf76a740
|
Fixes #21511
Clause-②: no
What this changes
On an unscoped (control-plane) kernel, registry hydration registers every view a stored environment-wide view container expands, each under its own name.
hydrateOverlayIntoRegistryregisters the container with the tenant-authorship marker (stateTenantAuthorship, ADR-0010_provenance: 'org'), andhydrateExpandedViewItemsregistered the expansions without it. An expansion of a package-bound container therefore sat under its bare name, wearing that package's_packageIdand no marker, andSchemaRegistry.getArtifactItem's bare-key fallback took it for a view the package ships.This PR implements triage's ruling (comment 5964348889): each expansion inherits its container's authorship.
hydrateExpandedViewItemsasksexpandRuntimeViewContainerfor tenant-authored expansions (tenantAuthored: true).expandRuntimeViewContainerappliesstateTenantAuthorshipto each expansion BEFORE that expansion's own artifact envelope is merged over it. This is the order the container gets (mergeArtifactProtection(stateTenantAuthorship(data), envelope)), so where the container's own package ships a view of that name, the artifact's_provenance,_packageIdand_lockstill win (ADR-0010 §3.3).expandRuntimeViewContainer(the list read's expansion pass and the by-name read's step 1b, throughexpandStoredViewContainers) pass no such option and serve exactly what they served before.isArtifactBacked(whichresettablereads) and the layered read'scodearm already askisTenantAuthored; they now get the marker to read.Measured before the change
At base
a7ab047c, with the #21508 harness (showcaseHarness) and the card's probe (a containeros_qa_probeonshowcase_taskwithlistViews.in_progress, read asshowcase_task.os_qa_probe.in_progress):getMetaItem(...).resettablegetMetaItemLayered(...).codeenv_localcom.example.repairassets)falsenullenv_localfalsenulltruefalseAt the base the registry held the container as
{ name: 'os_qa_probe', _provenance: 'org' }and the expansion as{ name: 'showcase_task.os_qa_probe.in_progress', _packageId: 'com.example.repairassets' }, with no_provenance. For the package-less arm the expansion carried no_packageId, soresettablewas alreadyfalse. Itscodelayer was still wrong, through the layered read's runtime-onlygetItemarm, which drops only tenant-marked entries.After the change, both kernels give
env_local's answer for every member kind in both arms.The save door (ruling: no save-door rule change)
The fix changes what
isArtifactBackedanswers for an expanded name on the unscoped kernel, and the save door reads that predicate. The door's acceptance is pinned rather than assumed. After the container is saved, a write by the expanded name is accepted on both kernels and in both arms, stored once in the container's scope, and that row then answers the name on the by-name read and on the object door. The outcome is identical across the two kernels. The same probe was accepted on all four kernel and arm combinations at the base. Under reverse verification leg 1 below, the save-door pins stay green, so the acceptance does not move with the fix.Tests
packages/metadata-protocol/src/view-container-runtime-expansion.test.tsgets a new describe block,#21511 an expanded view of a stored container answers as tenant-authored on both kernels, with 15 tests in #21508's harness. It covers the package-bound and package-less environment-wide arms. Hydration never registers an organization-scoped row.codelayer on either kernel, and the unscoped kernel's whole answer equalsenv_local's (resettable,editable,deletable,lock,provenance,packageId,code). That is 10 tests.isCodeArtifactBody).showcase_taskcontainer. Its expansions,showcase_task.defaultandshowcase_task.in_progress, stay resettable with the packagedcodelayer on both kernels, and on the unscoped kernel the registered expansion keeps the artifact's envelope (_provenance: 'package',_packageId: com.example.showcase) over the marker.Reverse verification (both runs recorded)
Each run starts from the committed fix, and each leg restores with
git checkout HEAD -- ABSOLUTE_PATH. Each restore is proven by blob hash equal to the HEAD blob, an emptygit diff HEAD, and a cleangit status, all inside a script armed withtrap restore EXIT INT TERM. The subject is imported from source (./index.js), so nodist/is involved.Run 1, at
e8e00609(the fix commit, before merging main):protocol.tswas reverted to the base blob3ac2573f(git restore --source=a7ab047c). The landing was proven by the on-disk blob equalling the base blob; the base blob carries 0 occurrences oftenantAuthored: true, and HEAD carries 1. Result: 12 failed, 132 passed (144). The 10resettable/codepins failed withunscoped: no package ships it: expected true to be false(package-bound) andunscoped: no artifact, so no code layer: expected {…} to be null(package-less). The 2 marker pins failed withexpected undefined to be 'org'. The 2 save-door pins and the CONTROL stayed green.scripts/ablation-replace.mjs, with anchor 1→0, replacement 0→1, and blobb106f11e→5d6e6263. Result: 1 failed, 143 passed. Only the CONTROL failed:showcase_task.default: the artifact's envelope is merged over the marker, not under it: expected { _provenance: 'org' } … { _provenance: 'package' }.Run 2, at
09033d87(after mergingorigin/main6c5697df, which includes the landed #21545 aseb9ef791):24cd0629(6c5697df): 12 failed, 132 passed, with the same 12 tests and the same messages.Verification at
09033d87pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 207 passed, 3 skipped (210); Tests 3207 passed, 19 skipped (3226);VERDICT command-exit 0.pnpm --filter @objectstack/metadata-protocol typecheck(tsc --noEmit) exited 0. Its--listFilesreaches 210 of the package's 210 test files, including the edited test file.eslint --no-inline-config --format jsonover the two edited.tsfiles gives 2 files, 0 errors and 0 warnings. The changeset.mdis outside everyfilesglob ofeslint.config.mjs.--print-configshows noparserOptions.projectorprojectService(type-aware linting is not enabled), so this diff cannot move the verdict on any untouched file. The fullpnpm lintis CI's.node scripts/pm/dispatch-gates.mjs --commands(no paths) derived 64 families for this change set. 63 exited 0.pnpm check:dual-build-cjs-loadsis NOT MEASURED: it exited 3 (PREREQUISITE NOT MET), because it needs every package'sdist/and 67 had none. This diff changes no exports, entry points or build config.--ranreconciliation: 64 accounted, 63 run, 1 NOT MEASURED. Two first runs were prerequisite misses and were re-run green after the prerequisite was met.check-plugin-teardown-shape --self-testneeded its pinned fixture commit fetched into the shallow clone.check:lean-entry-closureneeded@objectstack/objectqlbuilt.Region and surface
protocol.tshunks: thestateTenantAuthorshipdocblock (its "ONE caller" sentence now names both callers),expandRuntimeViewContainer's options type and its merge line, andhydrateExpandedViewItems' call and docblock. The claim names thehydrateExpandedViewItemsregion.expandRuntimeViewContainersits in the same hydration block, and the stamp must go there so that it precedes each expansion's own envelope (the order above), without a second copy of the envelope rule. That is the one widening of the region, declared here. The save door and the data door's read region are not edited. #21545's hunks (landed aseb9ef791, merged here) are disjoint from these.Acceptance notes
codelayer was wrong at the base;resettablewas alreadyfalse(table above). Both values are pinned now.isArtifactBackednow answersfalse, as onenv_local. Reading the save path (not separately measured), the write intent it derives is therefore the runtime-only one rather than the artifact-override one. The ruling expects this ("layered by the corrected predicate on both kernels"). Acceptance is unchanged, as measured above./layersREST routes call.origin/mainat6c5697df. One later main commit (f6b75208, spec conformance-case notes and a lint test) is not merged. It touches none of this PR's files.Changeset:
.changeset/21511-expansion-tenant-marker.md,patchfor@objectstack/metadata-protocol.Generated by Claude Code