Repository navigation
fix(metadata-protocol): a by-name read naming no package wears the envelope of the package whose body it serves - #22055
Conversation
…velope of the package whose body it serves getMetaItem with no packageId merged the first-registered package's artifact envelope (_packageId, _packageVersion, _provenance) over the body it served, so a name two packages ship answered one package's stored copy, row or MetadataService item under the other package's _packageId. The envelope is now looked up at the served item's own package, the rule the list read already applies to each item (envelopePackageId, shared by both doors). A read naming a package, the lock and a package-less served item are unchanged. Pins: the measured case on both kernels, both registry orders and two ownerships; the read naming each package; list and by-name envelope parity; and a disposition ledger over every lookupArtifactItem( site in protocol.ts. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… and the list parity as separate cases Each of (a) the read naming no package, (b) the reads naming each package and (c) the list's envelope for the same body is its own case, so each can fail on its own under reverse verification. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…-package-envelope-pairs-body
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
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 dde477aa66a0e32977184173f1669c1fe486ce53 && git checkout dde477aa66a0e32977184173f1669c1fe486ce53
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7a48b784d5401908933c6cdeeb97adbfc88d1e2 96059eb7404e7cb68a566509df9940d9e6592b7c && git checkout -B drift-repro a7a48b784d5401908933c6cdeeb97adbfc88d1e2 && git merge --no-ff 96059eb7404e7cb68a566509df9940d9e6592b7c
node scripts/docs-audit/affected-docs.mjs --json a7a48b784d5401908933c6cdeeb97adbfc88d1e2
|
ACCEPT (seat review) — PR #22055 at head
|
Fixes #22024
Clause-②: no
What this changes
getMetaItemwith nopackageId(the method behindGET /api/v1/meta/TYPE/NAMEwhen no package is named) merges the registry artifact's protection envelope over the body it serves, throughmergeArtifactProtection. The fields are_packageId,_packageVersionand_provenance. With no package named, that envelope waslookupArtifactItem(type, name, undefined): the first composite, which is the first-registered package's. When two packages ship a name and the served body is the other package's (its stored copy of a container they both ship, its package-bound row, or its MetadataService item), the answer served that body under the first-registered package's_packageId. The answer's top-levelpackageId/provenance/packageVersionare read off the served item (servedLockState→extractProtection), so they carried the same wrong value.The envelope lookup is now
lookupArtifactItem(type, name, envelopePackageId(request.packageId, item)).envelopePackageIdisrequest.packageId ?? item._packageId, which is exactly the expression the list (readFlattenedMetaItems) already used for each item it serves. It is now one module-level function, and both doors call it. The list's call is a byte-equivalent refactor. Result: one served body wears one envelope on both doors.Unchanged:
resolveItemLock/artifactLockLayerAtare untouched, andgetMetaItempassesitemLock, somergeArtifactProtection's lock branch does not run. The_lock*family andlock/editable/deletableare as before.packages/specedit, no governed surface.Files:
packages/metadata-protocol/src/protocol.ts:envelopePackageId, thegetMetaItemenvelope merge, and the list's call site.packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts: block (i), in finding(metadata-protocol): a stored copy of a view container a package ships on another package's object expands under its own name (#21334's arm), so a form withdrawn in that copy does not reach the package's shipped form of that name #21980's harness.packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts: the enumeration pin..changeset/22024-no-package-read-wears-served-package.md(@objectstack/metadata-protocolpatch).H1: where the body and the envelope part (confirmed)
Measured in-process through the real
getMetaItemnaming no package, on both kernels and in both registry orders, at base8caa131e52and at the fix. The probe was not committed. Setup:pkg_aandpkg_bboth ship view containertask._packageId)state: 'draft'pkg_bpkg_b's rowpkg_b(rowpackage_id)pkg_awhen first)pkg_bpkg_b's stored copypkg_b(the container's package)pkg_bruntimeViewContainerPackage: first-registered)pkg_bpkg_bpkg_blookupArtifactItemitselfThe card's measured case, with
pkg_aregistered first: on base the read answeredIntake (pkg_b copy)with_packageId: pkg_aand_packageVersion: pkg_a@1, and the response's top-levelpackageIdwaspkg_a. Now all three saypkg_b. This holds for all 12 combinations (2 kernels × object owned bypkg_a/pkg_b/ no package × 2 orders). Withpkg_bregistered first, base answeredpkg_btoo, by coincidence.H2: the fix's shape (confirmed)
getMetaItem's envelope merge. Nothing else had to move.mergeArtifactProtectionfrom the one artifact:_packageId,_packageVersionand_provenance. With them move the response's top-levelpackageId/packageVersion/provenance._lock,_lockReason,_lockDocsUrland_lockSourcefamily. The item-lock resolution sets it.H3: the 18
lookupArtifactItem(lines, classifiedgit grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, prints 18 lines. That holds at the merge basea7a48b784d, whereprotocol.tsis byte-identical to8caa131e52; line numbers below are that file's. It still prints 18 at HEAD.packagedArtifactBase·type, namereadCodeLayerForCarryForward·type, name, pkgreadFlattenedMetaItems·request.type, itemName, itemPackageIdenvelopePackageId.getMetaItem·request.type, request.name, request.packageId(shipped-flow arm)getMetaItem· the envelope mergeenvelopePackageId(request.packageId, item).getMetaItemLayered·request.type, request.name, request.packageId(code layer)codelayer, body and envelope one artifact. The layered response reads its provenance off that layer by its own rule, and nothing is grafted ontoeffective.isArtifactBacked·type, nameisNestedArtifactField·'object', name.slice(0, sep)packagedArtifactOwner·folded.type, folded.nameshippedArtifactsOf·type, nameshippedArtifactsOf·type, name, packageIdhydrateOverlayIntoRegistry·type, (data as any).name, options.packageId ?? undefinedexpandRuntimeViewContainer·type, String(item.name), ownPackageIdshippedViewContainerOf·type, name, packageIdruntimeViewContainerPackage·type, container.name_packageId, and its own artifact lends the envelope (line 18617). The by-name read now looks up at that same package, so body and envelope agree.isShippedByAnotherPackage·type, nameviewContainerNameCollisionRefusal·type, nameTotals: one yes (fixed), 16 no (one of them with an open question), the definition.
H4: the list path (confirmed, now pinned)
The env-wide list already looked each item's envelope up at that item's own package. Measured: it serves
(Intake (shipped by pkg_a), pkg_a)and(Intake (pkg_b copy), pkg_b). #21980's block (d) pins the reads NAMING a package against the list's slots, and block (h) pins only the body of the read naming none. The no-package read's envelope against the list was not pinned. Block (i)(c) pins it now, and the rule is one function both doors call.Pins
In
protocol.org-scoped-write-refused.test.ts, block (i), reusing #21980's harness (harness,PLACEMENTS,MEMBERS,saveCopyOf). Each case runs on both kernels.pkg_aor by no package, 2 registry orders) and 2 cells of apkg_b-bound row of the name. In each, the read naming no package answers[Intake (pkg_b copy), pkg_b, package](title,_packageId,_provenance) and top-levelpackageId: pkg_b._packageIdand its own top-levelpackageId.[title, _packageId, _provenance]for the body the no-package read serves.protocol.lookup-artifact-item-call-sites.test.tsrecords everylookupArtifactItem(inprotocol.ts, keyed by its class member and argument text, with a disposition. The idiom is the repo'sreadFileSync(new URL('./protocol.ts', import.meta.url))caller-set pins (hydrateOverlayIntoRegistry,applyRegistryWriteThrough). It goes red when a call is added, removed or re-scoped without its row. Non-vacuity: one definition, no site outside a member, and the population equal to the ledger. It also asserts that both read doors callenvelopePackageId.Reverse verification (on committed HEAD, restore proven)
The mutation, through
node scripts/ablation-replace.mjs:Anchor
request.type, request.name, envelopePackageId(request.packageId, item),→request.type, request.name, request.packageId,(the base's lookup).Anchor x1 → x0, replacement x0 → x1; blob
05c2a69909ca→9efd4116e951.Restore: blob after restore
05c2a69909ca== the HEAD blob, andgit diff HEADis empty.The subject is imported from
./protocol.js(source), so nodist/is on the path and no rebuild applies.Run 1, on
ef02620dbd, where (a) and (c) still shared one case. Predicted 8 red, measured 8 red / 183 green: the 6 copy cells and the 1 row cell withpkg_afirst, plus the ledger. (c) was never reached behind (a), so the pins were split.Run 2, on
4c75f230e0. Predicted 15 red, measured 15 distinct red: (a) ×7, (c) ×7, ledger ×1.["Intake (pkg_b copy)", "pkg_a", "package"]wherepkg_bwas expected. In (c) that is the list holdingpkg_bwhile the read wearspkg_a.Clause-② (measured: no)
dist/index.d.tsanddist/index.d.ctsare byte-identical between a build of baseprotocol.ts(8caa131e52) and HEAD: sha256f91b87a1031d3aee39c721835976fcc846025fa30f185b216262a4d6be751916for all four files. Positive control:envelopePackageIdoccurs 3 times in HEAD'sdist/index.jsand 0 times in base's.Tests and gates (at HEAD
96059eb740, after mergingorigin/maina7a48b784d)pnpm --filter @objectstack/metadata-protocol exec vitest run: Test Files 220 passed | 3 skipped (223); Tests 28181 passed | 19 skipped (28200).pnpm --filter @objectstack/metadata-protocol run typecheck(tsc --noEmit): exit 0.--listFilesincludes both touched test files.node scripts/pm/dispatch-gates.mjs --commands(no paths) derived 64 commands. All 64 exit 0.check:dual-build-cjs-loadsandcheck:lean-entry-closurefirst answered PREREQUISITE NOT MET (exit 3) on a partly built tree. Afterturbo run buildover every package (71/71), both exit 0.--ran:✓ dispatch-gates --ran: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3).check:engine-double-contract,check:objectql-double-limit,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher. All were run.check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-pathsare NOT WIRED without a PR context (exit 2), so they are NOT MEASURED. Thepnpm check:forms of the same three are self-test only (exit 0).check:adr-symbol-anchors: 2167 anchors across 140 records resolve.check:scripts-symbol-anchors: 3763 anchors across 282 scripts resolve.check:spec-docblock-symbol-anchors: 5168 anchors across 1886 spec sources resolve.check:adr-anchors: OK, 42386 citations across 5271 files resolve.Acceptance notes
getMetaItemLayeredtakes itscodelayer from the no-package lookup (first-registered for a shared name). The response's top-levelprovenance/packageId/packageVersionare read offcode ?? overlay.pkg_b's copy served andpkg_afirst:codeispkg_a's shipped item,effective._packageIdispkg_b, and top-levelpackageIdispkg_a. This is unchanged by this PR.getMetaItem's item agrees with the layeredeffectiveon_packageId, which it did not on base. The two reads' top-levelpackageIdnow differ for that address, where on base both saidpkg_a.codelayer itself: which shipped item is the diff baseline and the reset reference. That is a body change, outside this card's envelope-only scope.pkg_b, is hydrated by write-through into the registry's bare slot.pkg_athen reaches step 3, andregistry.getItem(view, NAME, 'pkg_a')answers the bare slot first. It servespkg_b's row body (Intake (pkg_b copy)) under_packageId: pkg_a, while the list scoped topkg_aservespkg_a's shipped item._packageId(written before the derived-provenance strip at the write door) is served with that stamp, as before. The envelope is now looked up at it, exactly as the list already does for the same row. Not measured._packageIdbecomes false.content/docs/ui/doc-pages.mdx's "best-effort (first match)" is about which body a bare link resolves, and that is unchanged.Generated by Claude Code