You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(metadata-protocol): getMetaItem naming no package grafts the first-registered package's artifact envelope onto another package's served view expansion, so a no-package by-name read can carry the wrong _packageId #22024
Filing gate: ① a reproducible defect, class (a), by-name read provenance (the envelope a served item wears). Filed from #21980's dev report (PR #22023, out_of_scope_findings[0]) by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). ⛔ Not graded or routed here. ⛔ Not a claim.
What is measured (by #21980's dev, at origin/mainaa09db58c9 and at PR #22023's head ca60b61d7b)
The read was made in-process, through getMetaItem with no packageId (the method behind GET /api/v1/meta/view/NAME when no package is named), on both kernels. It was not made over HTTP.
Setup: two packages ship container task, no code package owns the object, and pkg_b stores an env-wide copy.
Result: with pkg_a registered first, the no-package read answers Intake (pkg_b copy) with _packageId: pkg_a. This holds on base and at head, on both kernels.
So the body and the envelope can come from two different packages.
Direction (for triage)
With no package named, graft the artifact envelope of the package whose expansion or row was served. Do not graft the first-registered package's. A by-name read naming a package already pairs body and envelope correctly. This is a by-name read change, not a change to the withdrawal family. ⛔ No new key.
Reader who acts
Triage grades it. It is in metadata-protocol's protocol.ts (domain:engine). Serial: PR #22023 (#21980) edits protocol.ts in other regions.
Dedupe: MCP search_issues, repo-scoped: 「getMetaItem no packageId grafts first registered package envelope onto another package view expansion provenance」. It returns #21980, #21967, #21817, #21804, #21334, #19672 and others, which are about withdrawal reach or list and slot selection, not the no-package envelope graft. None is this.
Dedupe words: by-name read naming no package grafts first registered package envelope · getMetaItem no packageId _packageId mislabel view expansion · lookupArtifactItem without package wrong provenance served view
Path: fleet decision — a by-name read's envelope names the package whose body it serves | 缺项 | none
Triage: first grade, bug · priority:p3 · domain:engine · area:api · pm:blocked behind #21980 (finding removed). This is the closing card for no-package envelope grafts, with an enumeration pin
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T17:59Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/metadata-protocol/src/protocol.ts (getMetaItem's envelope merge, and lookupArtifactItem called with no package) ⇒ domain:engine; rationale: the lane table puts packages/metadata* there, and it is #21980's file.
Why p3: the reproduction is in-process, through getMetaItem, the method behind the meta read. No reach: was measured at a public door, and it is not a data-disclosure class. The body served is right; the provenance it wears is wrong. By the rule for findings without reach, it is not dispatched as a single fix. ⇒ It becomes the closing card for this mechanism.
The enumeration pin (the closing card's scope):
git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src gives 19 hits on main at this write (tests excluded), the definition included.
The PR body classifies every call that passes no package, or an optional package that can be undefined: does its _packageId reach an answer whose body came from a different source?
Each "yes" takes the envelope of the package whose expansion or row was served, as the card's direction says. ⛔ No new key.
This amends my grade 6022310461: the enumeration pin's count is 18 hits, not 19. That is git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, on main, and it includes the definition at protocol.ts:17261. I re-counted at this write. The scope and the rule are unchanged.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U.
The premise holds on main:getMetaItem still merges lookupArtifactItem(request.type, request.name, request.packageId) over the served body (protocol.ts:10431). With no package named, that is the first-registered package's envelope.
The enumeration pin, re-counted by the command at this write:git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, prints 18 lines, the definition included. The scope is the grade's (6022310461, corrected by 6022376040): each no-package call whose _packageId reaches an answer whose body came from elsewhere takes the envelope of the package whose expansion or row was served. ⛔ No new key.
Grade unchanged: bug · priority:p3 · domain:engine · area:api. The Blocked-by: #21980 line is spent.
It is the only eligible pm:queue card in this lane. The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」), and no dev slot is in use.
getMetaItem's envelope merge: lookupArtifactItem(request.type, request.name, request.packageId) at :10431, fed to mergeArtifactProtection (:1770).
The enumeration: git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, prints 18 lines at this write, the definition (:17261) included.
The rule (triage's direction): each call that passes no package, or an optional package that can be undefined, and whose _packageId reaches an answer whose body came from a different source, takes the envelope of the package whose expansion or row was served. ⛔ No new key.
⛔ The lock is not this card's: resolveItemLock over artifactLockLayerAt stays as it is.
The measured case: two packages ship container task, pkg_b stores a copy, and pkg_a registers first. The no-package by-name read wears pkg_b's _packageId, the package whose body it serves, on both kernels and in both registry orders, with the object owned by pkg_a and by none.
A read naming a package keeps its pairing, unchanged.
The PR body tables all 18 lines with a disposition each. An enumeration pin keeps the classified set honest when a call is added.
.changeset/22024-*.md (@objectstack/metadata-protocol patch).
Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate).
Clause-②: no
Provisional. The change corrects the provenance an answer wears, and no request is accepted or refused differently.
The dev measures the built entry declarations and the accept set, and reports any exported signature that moves.
Thread-read: 6029982038
Serial constraints cleared: at 2026-10-07T03:31Z:
os-dev-report
{
"issue": 22024,
"status": "done",
"branch": "claude/issue-22024-no-package-envelope-pairs-body",
"pr": "#22055",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent's (PM's) session id; identity is the branch",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 8caa131. getMetaItem naming no package merged lookupArtifactItem(type, name, undefined), the first-registered package's artifact envelope, over the served body. With pkg_a registered first, the measured case answered 'Intake (pkg_b copy)' with _packageId pkg_a and _packageVersion pkg_a@1, and the response's top-level packageId pkg_a. That held on both kernels and with the object owned by pkg_a, pkg_b or no package. The same mislabel occurred for a pkg_b-bound stored row of the name and for a MetadataService item stamped pkg_b. The fix looks the envelope up at envelopePackageId(request.packageId, item), i.e. request.packageId ?? the served item's own _packageId. That is the expression the list (readFlattenedMetaItems) already used; it is now one module-level function both doors call, and the list's call is a byte-equivalent refactor. A read naming a package, the lock (resolveItemLock / artifactLockLayerAt untouched) and a package-less served body are unchanged. Pins (a)-(d) were added and reverse-verified. Draft PR #22055 is open with Fixes #22024 and Clause-②: no.",
"tests": "At HEAD 96059eb (after merging origin/main a7a48b7). (1) pnpm --filter @objectstack/metadata-protocol exec vitest run: 'Test Files 220 passed | 3 skipped (223); Tests 28181 passed | 19 skipped (28200)'. (2) typecheck (tsc --noEmit): exit 0; --listFiles includes both touched test files. (3) New pins: block (i) in protocol.org-scoped-write-refused.test.ts has 42 cases: (a) 14, (b) 12, (c) 14, control 2. protocol.lookup-artifact-item-call-sites.test.ts has 4 cases. Both files green, 217/217 together. (4) Reverse verification on committed HEAD 4c75f23, through node scripts/ablation-replace.mjs. The anchor 'request.type, request.name, envelopePackageId(request.packageId, item),' was replaced by 'request.type, request.name, request.packageId,' (anchor x1->x0, replacement x0->x1, blob 05c2a69909ca -> 9efd4116e951). Predicted 15 red; measured 15 distinct red: (a) x7 and (c) x7, the pkg_a-first cells, plus the ledger x1. Each (a)/(c) case received ['Intake (pkg_b copy)','pkg_a','package'] where pkg_b was expected (the base's mislabel). All (b) cases and both controls stayed green. Restore: 'blob after restore 05c2a69909ca == HEAD, git diff HEAD empty'. The subject is imported from ./protocol.js (source), so no dist/ is involved and no rebuild applies. An earlier run on ef02620 measured 8 red / 183 green, as predicted; (c) was unreached behind (a) in the same case, so the pins were split and re-run. (5) Clause-② build measurement: base and head dist/index.d.ts and index.d.cts are byte-identical (sha256 f91b87a1031d3aee39c721835976fcc846025fa30f185b216262a4d6be751916). Positive control: envelopePackageId occurs 3 times in head dist/index.js and 0 times in base.",
"mcp_calls": "0 — no MCP GitHub tools used",
"api_writes": "3 — REST proxy writes, each a POST /repos/objectstack-ai/objectstack/dispatches to the fleet-write relay: (1) pr_create, which created #22055 (draft, read-back 17134 bytes sent = 17134 stored); (2) label-write --issue 22055 --assign os-project-manager (relay assign = POST /issues/22055/assignees; read-back MATCHES); (3) this os-dev-report comment on #22024 via post-stamped.mjs (POST /issues/22024/comments). Outside the REST count: git push x4 to the branch (the empty-branch probe, the fix, the pin split, the origin/main merge). Reads only otherwise (gh api single-card REST reads of #22024 and PR #22055).",
"open_questions": [
{
"question": "Line 10764, getMetaItemLayered's code layer: should the layered read naming no package follow the served overlay's package for a name two packages ship? Measured with pkg_b's copy served and pkg_a registered first: code = pkg_a's shipped item; effective._packageId = pkg_b; top-level packageId/provenance (read off code ?? overlay) = pkg_a. This PR leaves it unchanged, so getMetaItem's top-level packageId (now pkg_b) and the layered read's top-level packageId (pkg_a) differ for that address. On base both said pkg_a. getMetaItem's item now matches layered effective's _packageId, which it did not on base.",
"options": [
"A — scope the layered code layer to the served row's or expansion's package when none is named, so the no-package layered read equals the layered read naming that package. Cost: this moves the code layer's BODY (the diff baseline and the reset reference) for shared names, needs the overlay read before the code read, and goes beyond the claim's 'only the envelope moves'.",
"B — leave it. The code layer is the no-package code layer, which is what the no-package read serves once the overlay is removed, and the layered envelope is that layer's own by its declared rule. Cost: the two reads' top-level packageId can differ for a shared name with no package named.",
"C — B in this PR, with a separate card for the layered read's code-layer selection under a shared name."
],
"recommendation": "C. This card's ruling moves only the envelope. In the layered read the envelope IS the code layer's own body, so moving it is a body change with its own consequences (Studio's diff baseline and reset reference). That needs its own ruling, not a rider on this fix. It is a p3 in-process divergence, not a data-disclosure class. Fixes #22024 stays correct under B/C. If the seat picks A inside this card, the PR's first line should become 'Part of #22024'."
}
],
"out_of_scope_findings": [
"class: a · reach: in-process through saveMetaItem (packageId pkg_b) then getMetaItem (packageId pkg_a), the methods behind PUT and GET /api/v1/meta/view/NAME?package=..., on an unscoped kernel. Measured in both registry orders; not measured over HTTP. · evidence: a pkg_b-bound stored view-item row of a name both packages ship is hydrated by write-through into the registry's bare slot. The read naming pkg_a misses steps 1 and 1b and reaches step 3, where registry.getItem(view, NAME, 'pkg_a') answers the bare slot first. It serves pkg_b's row body ('Intake (pkg_b copy)') under _packageId pkg_a, while the list scoped to pkg_a serves pkg_a's shipped item. The environment-scoped kernel answers correctly. It is pre-existing: named reads take the identical path with this PR's change reverted. #21980 closed the same bare-slot shadow for container expansions, not for a row stored under the name itself. · dedupe words: 'unscoped kernel hydrated bare slot by-name read naming package serves other package row' · 'getMetaItem packageId registry getItem bare key shadows composite package-bound row' · 'write-through hydrateOverlayIntoRegistry package-bound row of a shared view name'",
"carrier: 承接者:无 · noted, not filed — a legacy stored row whose body still carries its own _packageId (from before the write door's derived-provenance strip) keeps that stamp, and the envelope is now looked up at it, exactly as the list already did for the same row; not measured (PR Acceptance note 3)",
"carrier: 承接者:无 · noted, not filed — line 8399 getPackagedViewBase/getPackagedDashboardBase answers the first-registered package's shipped item as the i18n comparison base for a shared name, whatever package's body is served; envelope-free, observation only, not measured"
],
"gates": "At HEAD 96059eb. dispatch-gates --commands (no paths, --repo objectstack-ai/objectstack; tree check 'it holds'; change set of 4 paths vs merge base a7a48b7, 321 changed lines) derived 64 commands, and all 64 exit 0. check:dual-build-cjs-loads and check:lean-entry-closure first answered PREREQUISITE NOT MET (exit 3) on a partly built tree; after turbo build of all packages (71/71 successful) both exit 0. --ran (exit-annotated record): '✓ 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)'. Six families beyond the order's list came from the real change set: check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt (run under the lock: '1 ledger entr(ies) re-measured ... none above its recorded number') and check:where-matcher. Artifact-roster block (53 families, outside the total): 53/53 exit 0. Three of them (check-closing-target-claim, check-single-claim-paths, check-partof-closing-keyword) answered NOT WIRED (exit 2) until PR #22055 existed, then exit 0 with PR context: '✓ check:closing-target-claim: PR #22055 closes #22024, and each carries a Claim: whose Branch: line names claude/issue-22024-no-package-envelope-pairs-body'. Symbol-anchor sweeps, all exit 0: check:adr-symbol-anchors (2167 anchors / 140 records resolve), check:scripts-symbol-anchors (3763 / 282 scripts), check:spec-docblock-symbol-anchors (5168 / 1886 spec sources), check:adr-anchors ('OK ... 42386 citation(s) across 5271 file(s) resolve'). check:nul-bytes exit 0. CI on head 96059eb at report time: 14 checks completed (success/skipped, none red), 17 in_progress. CI convergence is the PM's to read.",
"line_budget": "n/a — no skills/** or governed surface touched",
"deviations": [
"The list's call site in readFlattenedMetaItems now calls the shared envelopePackageId helper. It is a byte-equivalent refactor of 'packageId ?? it._packageId', in the same file, beyond 'getMetaItem's envelope merge'. Done so both doors keep one rule; the enumeration pin asserts both calls.",
"Pin (b) is asserted on the 12 copy cells only. On the stored-row cell, the read naming pkg_a on an unscoped kernel is a pre-existing defect (out_of_scope_findings[0]) and cannot be green without an out-of-scope fix.",
"Pins (a)/(c) were first written as one case; reverse verification run 1 showed (c) unreached behind (a), so they were split into separate cases (commit 4c75f23) and run 2 was taken on that committed HEAD.",
"dispatch-gates at the final head derived 6 families beyond the dispatch order's list (named in gates); all run.",
"A full 'turbo run build' of every package ran under the lock, to give two dist-reading gates and check:type-check-debt their prerequisite.",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Followed AGENTS.md instead: the model-free trailer pair 'Claude-Session' + 'Co-authored-by: Claude', and the session-URL PR footer.",
"PR body correction for the seat to apply (dev writes the body once, never PATCHes): in section H3, the line 'Totals: one yes (fixed), 17 no (one of them with an open question), the definition.' should read 'Totals: one yes (fixed), 16 no (one of them with an open question), the definition.' There are 17 calls; the table rows themselves are right.",
"No conflict between the dispatch order and os-dev.md was hit."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts",
"packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts",
".changeset/22024-no-package-read-wears-served-package.md"
],
"clause_2": "no — measured. The built entry declarations (dist/index.d.ts, dist/index.d.cts) are byte-identical base vs head (sha256 f91b87a1031d3aee39c721835976fcc846025fa30f185b216262a4d6be751916 for all four files). Positive control: the helper is present 3x in head dist/index.js and 0x in base. Accept set: no validation, refusal or write path changed; only the envelope (_packageId/_packageVersion/_provenance and the top-level packageId/packageVersion/provenance read off it) of a no-package by-name read moves. No narrowing, no widening.",
"h1": "Confirmed. getMetaItem's body comes from the answering step, and the envelope came from lookupArtifactItem(type, name, request.packageId) at :10431 (base): with no package, the first composite. Per step, naming no package, at base -> now: draft preview / state draft: returns before the merge, unchanged. Step 1, pkg_b-bound row: served item's _packageId pkg_b (row package_id); envelope first-registered -> pkg_b. Step 1, package-less row: none known; first-registered -> unchanged. Step 1b, pkg_b's copy: pkg_b (container's package); first-registered -> pkg_b. Step 1b, package-less copy: the overlaid container's package (runtimeViewContainerPackage, first-registered); unchanged. Step 2, MetadataService item stamped pkg_b: first-registered -> pkg_b. Step 3, registry composite: its own package; same -> unchanged. Step 3, shipped-flow arm: the artifact itself; unchanged.",
"h2": "Confirmed. With no package named, the envelope lookup is scoped to the served item's package (request.packageId ?? item._packageId). A named read passes its package unchanged. Package-less served bodies get the package-less lookup they had: one mechanical answer, nothing invented. For a single-shipper name that is that package's envelope; for a shared name it is one of the list's own pairings (control-pinned). resolveItemLock / artifactLockLayerAt are not touched. Fields that move with the fix: _packageId, _packageVersion, _provenance (all copied by mergeArtifactProtection from one artifact), and with them the response's top-level packageId, packageVersion and provenance (servedLockState -> extractProtection(decorated)). The _lock* family does not move, because getMetaItem passes itemLock so mergeArtifactProtection's lock branch is skipped.",
"h3_table": [
"8399 packagedArtifactBase(type, name) · no package · NO — the packaged i18n comparison base; nothing grafted onto the served body",
"8696 readCodeLayerForCarryForward(type, name, pkg) · can be undefined · NO — a save's credential carry-forward (a write; derived provenance keys stripped before persisting)",
"9554 readFlattenedMetaItems(request.type, itemName, itemPackageId) · can be undefined · NO — scoped by construction to each item's own package (the list's rule, now envelopePackageId)",
"10391 getMetaItem shipped-flow arm (request.type, request.name, request.packageId) · can be undefined · NO — the served body IS this artifact; the envelope lookup now asks its own package",
"10431 getMetaItem envelope merge · can be undefined · YES — the card's defect; fixed with envelopePackageId(request.packageId, item)",
"10764 getMetaItemLayered code layer (request.type, request.name, request.packageId) · can be undefined · NO by the graft test — the result IS the code layer (body and envelope one artifact), whose provenance the layered response reports by its own rule; open question on the code layer's selection",
"16356 isArtifactBacked(type, name) · no package · NO — boolean predicate",
"16442 isNestedArtifactField('object', name.slice(0, sep)) · no package · NO — boolean containment over an object's one owner",
"16861 packagedArtifactOwner(folded.type, folded.name) · no package · NO — returns a shipping package id for flow classification; no envelope on a served body",
"17261 definition",
"17331 shippedArtifactsOf(type, name) · no package · NO — the lock's artifact layer, deliberately untouched",
"17335 shippedArtifactsOf(type, name, packageId) · named · NO — scoped, own-package filter",
"18451 hydrateOverlayIntoRegistry(type, (data as any).name, options.packageId ?? undefined) · undefined for a package-less row · NO — scoped to the row's own package; a package-less row names none",
"18617 expandRuntimeViewContainer(type, String(item.name), ownPackageId) · not called when undefined · NO — own-package filter",
"18656 shippedViewContainerOf(type, name, packageId) · returns before the lookup when undefined · NO — own-package filter",
"18708 runtimeViewContainerPackage(type, container.name) · no package · NO — the overlaid package becomes the expansion's own _packageId and lends its own envelope; the by-name read now looks up at that same package",
"18794 isShippedByAnotherPackage(type, name) · no package · NO — boolean predicate",
"19658 viewContainerNameCollisionRefusal(type, name) · no package · NO — the save door's collision check; the package id names a shipper in a refusal sentence"
],
"h3_count": "git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded: 18 lines at merge base a7a48b7 (protocol.ts byte-identical to 8caa131) and 18 at HEAD; 17 calls plus the definition: 1 yes (10431, fixed) and 16 no, 10764 among the 16 with an open question",
"h4": "Confirmed. The env-wide list already paired each item with its own package's envelope ('packageId ?? it._packageId', base :9548). Measured: it serves (shipped by pkg_a, pkg_a) and (pkg_b copy, pkg_b). #21980's block (d) pinned the named reads against the slots and (h) pinned only the no-package read's body, so the no-package envelope parity was unpinned. It is pinned now ((i)(c) on 14 cells + 2 controls), and both doors call one function (envelopePackageId), which pin (d) asserts.",
"docs": "No hand-written docs line about a by-name read's _packageId becomes false. content/docs/ui/doc-pages.mdx's 'best-effort (first match)' describes which body a bare link resolves, which is unchanged.",
"cleanup": "Worktree ../objectstack-issue-22024: node_modules removed and the worktree removed after this report is posted. No dev server or background process was started."
}
Landed: PR #22055 → 2015c54028 on main. It merged through the merge queue at 2026-10-07T05:46Z, after entering the queue at 2026-10-07T05:09Z. Verified at 2026-10-07T05:46Z. domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi.
The squash. It is on origin/main as a single-parent commit (parent d5a14dd5c0). Its diffstat is the reviewed one: 4 files, +318/-3.
What is on main.
envelopePackageId(requested, served) is one module-level rule: the package the read names, else the package the served item is bound to.
getMetaItem's envelope merge and the list (readFlattenedMetaItems) both call it. So a by-name read naming no package wears the envelope (_packageId, _packageVersion, _provenance) of the package whose row, expansion or item it serves, as the list already did.
The body served, a read naming a package and the lock are unchanged.
Every lookupArtifactItem( in protocol.ts carries a recorded disposition, pinned by protocol.lookup-artifact-item-call-sites.test.ts.
The card.Fixes #22024 closed this card as completed. pm:dispatched is removed in this act. No other card was closed by the body.
From this release (@objectstack/metadata-protocolpatch, Clause-②: no): GET /api/v1/meta/TYPE/NAME with no package names the package whose body it serves, in the item and in the top-level packageId / provenance / packageVersion. Where two packages ship one name and one of them stores a copy or a row of it, the read no longer serves that package's body under the first-registered package's name.
Activity
objectstack-fleet commentedon Oct 6, 2026
Path: fleet decision — a by-name read's envelope names the package whose body it serves | 缺项 | none
Triage: first grade,
bug·priority:p3·domain:engine·area:api·pm:blockedbehind #21980 (findingremoved). This is the closing card for no-package envelope grafts, with an enumeration pinBlocked-by: #21980
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T17:59Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata-protocol/src/protocol.ts(getMetaItem's envelope merge, andlookupArtifactItemcalled with no package) ⇒domain:engine; rationale: the lane table putspackages/metadata*there, and it is #21980's file.getMetaItem, the method behind the meta read. Noreach:was measured at a public door, and it is not a data-disclosure class. The body served is right; the provenance it wears is wrong. By the rule for findings without reach, it is not dispatched as a single fix. ⇒ It becomes the closing card for this mechanism.git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/srcgives 19 hits onmainat this write (tests excluded), the definition included.undefined: does its_packageIdreach an answer whose body came from a different source?objectstack-fleet commentedon Oct 6, 2026
This amends my grade
6022310461: the enumeration pin's count is 18 hits, not 19. That isgit grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, onmain, and it includes the definition atprotocol.ts:17261. I re-counted at this write. The scope and the rule are unchanged.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U.objectstack-fleet commentedon Oct 7, 2026
Triage: unlocked,
pm:blocked→pm:queue. #21980 landed, and decision card #22027 ruled A, so the body this envelope must pair with is settledTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T02:58Z. ⛔ Not a claim, ⛔ not a dispatch.The blocker is released (read at this write):
8caa131e52, an ancestor oforigin/main.6028832449): a no-package by-name read of a shared name answers the stored copy, under ADR-0005's layered order. That ruling also names this card as the carrier of the package identity in that answer.The premise holds on
main:getMetaItemstill mergeslookupArtifactItem(request.type, request.name, request.packageId)over the served body (protocol.ts:10431). With no package named, that is the first-registered package's envelope.The enumeration pin, re-counted by the command at this write:
git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, prints 18 lines, the definition included. The scope is the grade's (6022310461, corrected by6022376040): each no-package call whose_packageIdreaches an answer whose body came from elsewhere takes the envelope of the package whose expansion or row was served. ⛔ No new key.Grade unchanged:
bug·priority:p3·domain:engine·area:api. TheBlocked-by: #21980line is spent.objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 53 · 2026-10-07T03:31Z
Session:
session_017ErfyP2Rx7XWHJA27QjyUiAccount:
os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22024-no-package-envelope-pairs-bodyWorktree:
objectstack-issue-22024Domain:
domain:engineSeat:
domain:engine#1Provenance:
pm:queue(6029982038) once 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 landed through PR fix(metadata-protocol)!: a package's stored copy of a container it ships overlays its shipped views, so a withdrawal saved in the copy holds at the anonymous form doors #22023 (8caa131e52) on ruling decision: #21980 merge condition — a no-package by-name read of a shared view name now answers the stored copy, not the owner's shipped item (before the change it depended on registry order): land PR #22023, or hold it? #22027 → A (6028832449). That ruling names this card as the carrier of the package identity in the no-package answer.pm:queuecard in this lane. The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」), and no dev slot is in use.File surface (at
origin/main8caa131e52), per triage's grade 6022310461:packages/metadata-protocol/src/protocol.ts:getMetaItem's envelope merge:lookupArtifactItem(request.type, request.name, request.packageId)at:10431, fed tomergeArtifactProtection(:1770).git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded, prints 18 lines at this write, the definition (:17261) included.undefined, and whose_packageIdreaches an answer whose body came from a different source, takes the envelope of the package whose expansion or row was served. ⛔ No new key.resolveItemLockoverartifactLockLayerAtstays as it is.task,pkg_bstores a copy, andpkg_aregisters first. The no-package by-name read wearspkg_b's_packageId, the package whose body it serves, on both kernels and in both registry orders, with the object owned bypkg_aand by none..changeset/22024-*.md(@objectstack/metadata-protocolpatch).Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
Thread-read: 6029982038
Serial constraints cleared: at 2026-10-07T03:31Z:
filename: none touchesprotocol.ts.sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (blocked on spec(identity):EvalUser.isPlatformAdminis the live PLATFORM_ADMIN standing predicate but is still marked Deprecated and positions-derived — lift the mark, describe it as ADR-0095 D3 standing, date-note ADR-0068 D2/D4 (#21886 spec half) #22012) shares no file with this card.objectstack-fleet commentedon Oct 7, 2026
os-dev-report
{
"issue": 22024,
"status": "done",
"branch": "claude/issue-22024-no-package-envelope-pairs-body",
"pr": "#22055",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent's (PM's) session id; identity is the branch",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 8caa131. getMetaItem naming no package merged lookupArtifactItem(type, name, undefined), the first-registered package's artifact envelope, over the served body. With pkg_a registered first, the measured case answered 'Intake (pkg_b copy)' with _packageId pkg_a and _packageVersion pkg_a@1, and the response's top-level packageId pkg_a. That held on both kernels and with the object owned by pkg_a, pkg_b or no package. The same mislabel occurred for a pkg_b-bound stored row of the name and for a MetadataService item stamped pkg_b. The fix looks the envelope up at envelopePackageId(request.packageId, item), i.e. request.packageId ?? the served item's own _packageId. That is the expression the list (readFlattenedMetaItems) already used; it is now one module-level function both doors call, and the list's call is a byte-equivalent refactor. A read naming a package, the lock (resolveItemLock / artifactLockLayerAt untouched) and a package-less served body are unchanged. Pins (a)-(d) were added and reverse-verified. Draft PR #22055 is open with Fixes #22024 and Clause-②: no.",
"tests": "At HEAD 96059eb (after merging origin/main a7a48b7). (1) pnpm --filter @objectstack/metadata-protocol exec vitest run: 'Test Files 220 passed | 3 skipped (223); Tests 28181 passed | 19 skipped (28200)'. (2) typecheck (tsc --noEmit): exit 0; --listFiles includes both touched test files. (3) New pins: block (i) in protocol.org-scoped-write-refused.test.ts has 42 cases: (a) 14, (b) 12, (c) 14, control 2. protocol.lookup-artifact-item-call-sites.test.ts has 4 cases. Both files green, 217/217 together. (4) Reverse verification on committed HEAD 4c75f23, through node scripts/ablation-replace.mjs. The anchor 'request.type, request.name, envelopePackageId(request.packageId, item),' was replaced by 'request.type, request.name, request.packageId,' (anchor x1->x0, replacement x0->x1, blob 05c2a69909ca -> 9efd4116e951). Predicted 15 red; measured 15 distinct red: (a) x7 and (c) x7, the pkg_a-first cells, plus the ledger x1. Each (a)/(c) case received ['Intake (pkg_b copy)','pkg_a','package'] where pkg_b was expected (the base's mislabel). All (b) cases and both controls stayed green. Restore: 'blob after restore 05c2a69909ca == HEAD, git diff HEAD empty'. The subject is imported from ./protocol.js (source), so no dist/ is involved and no rebuild applies. An earlier run on ef02620 measured 8 red / 183 green, as predicted; (c) was unreached behind (a) in the same case, so the pins were split and re-run. (5) Clause-② build measurement: base and head dist/index.d.ts and index.d.cts are byte-identical (sha256 f91b87a1031d3aee39c721835976fcc846025fa30f185b216262a4d6be751916). Positive control: envelopePackageId occurs 3 times in head dist/index.js and 0 times in base.",
"mcp_calls": "0 — no MCP GitHub tools used",
"api_writes": "3 — REST proxy writes, each a POST /repos/objectstack-ai/objectstack/dispatches to the fleet-write relay: (1) pr_create, which created #22055 (draft, read-back 17134 bytes sent = 17134 stored); (2) label-write --issue 22055 --assign os-project-manager (relay assign = POST /issues/22055/assignees; read-back MATCHES); (3) this os-dev-report comment on #22024 via post-stamped.mjs (POST /issues/22024/comments). Outside the REST count: git push x4 to the branch (the empty-branch probe, the fix, the pin split, the origin/main merge). Reads only otherwise (gh api single-card REST reads of #22024 and PR #22055).",
"open_questions": [
{
"question": "Line 10764, getMetaItemLayered's code layer: should the layered read naming no package follow the served overlay's package for a name two packages ship? Measured with pkg_b's copy served and pkg_a registered first: code = pkg_a's shipped item; effective._packageId = pkg_b; top-level packageId/provenance (read off code ?? overlay) = pkg_a. This PR leaves it unchanged, so getMetaItem's top-level packageId (now pkg_b) and the layered read's top-level packageId (pkg_a) differ for that address. On base both said pkg_a. getMetaItem's item now matches layered effective's _packageId, which it did not on base.",
"options": [
"A — scope the layered code layer to the served row's or expansion's package when none is named, so the no-package layered read equals the layered read naming that package. Cost: this moves the code layer's BODY (the diff baseline and the reset reference) for shared names, needs the overlay read before the code read, and goes beyond the claim's 'only the envelope moves'.",
"B — leave it. The code layer is the no-package code layer, which is what the no-package read serves once the overlay is removed, and the layered envelope is that layer's own by its declared rule. Cost: the two reads' top-level packageId can differ for a shared name with no package named.",
"C — B in this PR, with a separate card for the layered read's code-layer selection under a shared name."
],
"recommendation": "C. This card's ruling moves only the envelope. In the layered read the envelope IS the code layer's own body, so moving it is a body change with its own consequences (Studio's diff baseline and reset reference). That needs its own ruling, not a rider on this fix. It is a p3 in-process divergence, not a data-disclosure class. Fixes #22024 stays correct under B/C. If the seat picks A inside this card, the PR's first line should become 'Part of #22024'."
}
],
"out_of_scope_findings": [
"class: a · reach: in-process through saveMetaItem (packageId pkg_b) then getMetaItem (packageId pkg_a), the methods behind PUT and GET /api/v1/meta/view/NAME?package=..., on an unscoped kernel. Measured in both registry orders; not measured over HTTP. · evidence: a pkg_b-bound stored view-item row of a name both packages ship is hydrated by write-through into the registry's bare slot. The read naming pkg_a misses steps 1 and 1b and reaches step 3, where registry.getItem(view, NAME, 'pkg_a') answers the bare slot first. It serves pkg_b's row body ('Intake (pkg_b copy)') under _packageId pkg_a, while the list scoped to pkg_a serves pkg_a's shipped item. The environment-scoped kernel answers correctly. It is pre-existing: named reads take the identical path with this PR's change reverted. #21980 closed the same bare-slot shadow for container expansions, not for a row stored under the name itself. · dedupe words: 'unscoped kernel hydrated bare slot by-name read naming package serves other package row' · 'getMetaItem packageId registry getItem bare key shadows composite package-bound row' · 'write-through hydrateOverlayIntoRegistry package-bound row of a shared view name'",
"carrier: 承接者:无 · noted, not filed — a legacy stored row whose body still carries its own _packageId (from before the write door's derived-provenance strip) keeps that stamp, and the envelope is now looked up at it, exactly as the list already did for the same row; not measured (PR Acceptance note 3)",
"carrier: 承接者:无 · noted, not filed — line 8399 getPackagedViewBase/getPackagedDashboardBase answers the first-registered package's shipped item as the i18n comparison base for a shared name, whatever package's body is served; envelope-free, observation only, not measured"
],
"gates": "At HEAD 96059eb. dispatch-gates --commands (no paths, --repo objectstack-ai/objectstack; tree check 'it holds'; change set of 4 paths vs merge base a7a48b7, 321 changed lines) derived 64 commands, and all 64 exit 0. check:dual-build-cjs-loads and check:lean-entry-closure first answered PREREQUISITE NOT MET (exit 3) on a partly built tree; after turbo build of all packages (71/71 successful) both exit 0. --ran (exit-annotated record): '✓ 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)'. Six families beyond the order's list came from the real change set: check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt (run under the lock: '1 ledger entr(ies) re-measured ... none above its recorded number') and check:where-matcher. Artifact-roster block (53 families, outside the total): 53/53 exit 0. Three of them (check-closing-target-claim, check-single-claim-paths, check-partof-closing-keyword) answered NOT WIRED (exit 2) until PR #22055 existed, then exit 0 with PR context: '✓ check:closing-target-claim: PR #22055 closes #22024, and each carries a Claim: whose Branch: line names claude/issue-22024-no-package-envelope-pairs-body'. Symbol-anchor sweeps, all exit 0: check:adr-symbol-anchors (2167 anchors / 140 records resolve), check:scripts-symbol-anchors (3763 / 282 scripts), check:spec-docblock-symbol-anchors (5168 / 1886 spec sources), check:adr-anchors ('OK ... 42386 citation(s) across 5271 file(s) resolve'). check:nul-bytes exit 0. CI on head 96059eb at report time: 14 checks completed (success/skipped, none red), 17 in_progress. CI convergence is the PM's to read.",
"line_budget": "n/a — no skills/** or governed surface touched",
"deviations": [
"The list's call site in readFlattenedMetaItems now calls the shared envelopePackageId helper. It is a byte-equivalent refactor of 'packageId ?? it._packageId', in the same file, beyond 'getMetaItem's envelope merge'. Done so both doors keep one rule; the enumeration pin asserts both calls.",
"Pin (b) is asserted on the 12 copy cells only. On the stored-row cell, the read naming pkg_a on an unscoped kernel is a pre-existing defect (out_of_scope_findings[0]) and cannot be green without an out-of-scope fix.",
"Pins (a)/(c) were first written as one case; reverse verification run 1 showed (c) unreached behind (a), so they were split into separate cases (commit 4c75f23) and run 2 was taken on that committed HEAD.",
"dispatch-gates at the final head derived 6 families beyond the dispatch order's list (named in gates); all run.",
"A full 'turbo run build' of every package ran under the lock, to give two dist-reading gates and check:type-check-debt their prerequisite.",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Followed AGENTS.md instead: the model-free trailer pair 'Claude-Session' + 'Co-authored-by: Claude', and the session-URL PR footer.",
"PR body correction for the seat to apply (dev writes the body once, never PATCHes): in section H3, the line 'Totals: one yes (fixed), 17 no (one of them with an open question), the definition.' should read 'Totals: one yes (fixed), 16 no (one of them with an open question), the definition.' There are 17 calls; the table rows themselves are right.",
"No conflict between the dispatch order and os-dev.md was hit."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts",
"packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts",
".changeset/22024-no-package-read-wears-served-package.md"
],
"clause_2": "no — measured. The built entry declarations (dist/index.d.ts, dist/index.d.cts) are byte-identical base vs head (sha256 f91b87a1031d3aee39c721835976fcc846025fa30f185b216262a4d6be751916 for all four files). Positive control: the helper is present 3x in head dist/index.js and 0x in base. Accept set: no validation, refusal or write path changed; only the envelope (_packageId/_packageVersion/_provenance and the top-level packageId/packageVersion/provenance read off it) of a no-package by-name read moves. No narrowing, no widening.",
"h1": "Confirmed. getMetaItem's body comes from the answering step, and the envelope came from lookupArtifactItem(type, name, request.packageId) at :10431 (base): with no package, the first composite. Per step, naming no package, at base -> now: draft preview / state draft: returns before the merge, unchanged. Step 1, pkg_b-bound row: served item's _packageId pkg_b (row package_id); envelope first-registered -> pkg_b. Step 1, package-less row: none known; first-registered -> unchanged. Step 1b, pkg_b's copy: pkg_b (container's package); first-registered -> pkg_b. Step 1b, package-less copy: the overlaid container's package (runtimeViewContainerPackage, first-registered); unchanged. Step 2, MetadataService item stamped pkg_b: first-registered -> pkg_b. Step 3, registry composite: its own package; same -> unchanged. Step 3, shipped-flow arm: the artifact itself; unchanged.",
"h2": "Confirmed. With no package named, the envelope lookup is scoped to the served item's package (request.packageId ?? item._packageId). A named read passes its package unchanged. Package-less served bodies get the package-less lookup they had: one mechanical answer, nothing invented. For a single-shipper name that is that package's envelope; for a shared name it is one of the list's own pairings (control-pinned). resolveItemLock / artifactLockLayerAt are not touched. Fields that move with the fix: _packageId, _packageVersion, _provenance (all copied by mergeArtifactProtection from one artifact), and with them the response's top-level packageId, packageVersion and provenance (servedLockState -> extractProtection(decorated)). The _lock* family does not move, because getMetaItem passes itemLock so mergeArtifactProtection's lock branch is skipped.",
"h3_table": [
"8399 packagedArtifactBase(type, name) · no package · NO — the packaged i18n comparison base; nothing grafted onto the served body",
"8696 readCodeLayerForCarryForward(type, name, pkg) · can be undefined · NO — a save's credential carry-forward (a write; derived provenance keys stripped before persisting)",
"9554 readFlattenedMetaItems(request.type, itemName, itemPackageId) · can be undefined · NO — scoped by construction to each item's own package (the list's rule, now envelopePackageId)",
"10391 getMetaItem shipped-flow arm (request.type, request.name, request.packageId) · can be undefined · NO — the served body IS this artifact; the envelope lookup now asks its own package",
"10431 getMetaItem envelope merge · can be undefined · YES — the card's defect; fixed with envelopePackageId(request.packageId, item)",
"10764 getMetaItemLayered code layer (request.type, request.name, request.packageId) · can be undefined · NO by the graft test — the result IS the code layer (body and envelope one artifact), whose provenance the layered response reports by its own rule; open question on the code layer's selection",
"16356 isArtifactBacked(type, name) · no package · NO — boolean predicate",
"16442 isNestedArtifactField('object', name.slice(0, sep)) · no package · NO — boolean containment over an object's one owner",
"16861 packagedArtifactOwner(folded.type, folded.name) · no package · NO — returns a shipping package id for flow classification; no envelope on a served body",
"17261 definition",
"17331 shippedArtifactsOf(type, name) · no package · NO — the lock's artifact layer, deliberately untouched",
"17335 shippedArtifactsOf(type, name, packageId) · named · NO — scoped, own-package filter",
"18451 hydrateOverlayIntoRegistry(type, (data as any).name, options.packageId ?? undefined) · undefined for a package-less row · NO — scoped to the row's own package; a package-less row names none",
"18617 expandRuntimeViewContainer(type, String(item.name), ownPackageId) · not called when undefined · NO — own-package filter",
"18656 shippedViewContainerOf(type, name, packageId) · returns before the lookup when undefined · NO — own-package filter",
"18708 runtimeViewContainerPackage(type, container.name) · no package · NO — the overlaid package becomes the expansion's own _packageId and lends its own envelope; the by-name read now looks up at that same package",
"18794 isShippedByAnotherPackage(type, name) · no package · NO — boolean predicate",
"19658 viewContainerNameCollisionRefusal(type, name) · no package · NO — the save door's collision check; the package id names a shipper in a refusal sentence"
],
"h3_count": "git grep -n 'lookupArtifactItem(' -- packages/metadata-protocol/src, tests excluded: 18 lines at merge base a7a48b7 (protocol.ts byte-identical to 8caa131) and 18 at HEAD; 17 calls plus the definition: 1 yes (10431, fixed) and 16 no, 10764 among the 16 with an open question",
"h4": "Confirmed. The env-wide list already paired each item with its own package's envelope ('packageId ?? it._packageId', base :9548). Measured: it serves (shipped by pkg_a, pkg_a) and (pkg_b copy, pkg_b). #21980's block (d) pinned the named reads against the slots and (h) pinned only the no-package read's body, so the no-package envelope parity was unpinned. It is pinned now ((i)(c) on 14 cells + 2 controls), and both doors call one function (envelopePackageId), which pin (d) asserts.",
"docs": "No hand-written docs line about a by-name read's _packageId becomes false. content/docs/ui/doc-pages.mdx's 'best-effort (first match)' describes which body a bare link resolves, which is unchanged.",
"cleanup": "Worktree ../objectstack-issue-22024: node_modules removed and the worktree removed after this report is posted. No dev server or background process was started."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Landed: PR #22055 →
2015c54028onmain. It merged through the merge queue at 2026-10-07T05:46Z, after entering the queue at 2026-10-07T05:09Z. Verified at 2026-10-07T05:46Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.origin/mainas a single-parent commit (parentd5a14dd5c0). Its diffstat is the reviewed one: 4 files, +318/-3.main.envelopePackageId(requested, served)is one module-level rule: the package the read names, else the package the served item is bound to.getMetaItem's envelope merge and the list (readFlattenedMetaItems) both call it. So a by-name read naming no package wears the envelope (_packageId,_packageVersion,_provenance) of the package whose row, expansion or item it serves, as the list already did.lookupArtifactItem(inprotocol.tscarries a recorded disposition, pinned byprotocol.lookup-artifact-item-call-sites.test.ts.Fixes #22024closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.@objectstack/metadata-protocolpatch,Clause-②: no):GET /api/v1/meta/TYPE/NAMEwith no package names the package whose body it serves, in the item and in the top-levelpackageId/provenance/packageVersion. Where two packages ship one name and one of them stores a copy or a row of it, the read no longer serves that package's body under the first-registered package's name.pm:blockedp3 by triage):Generated by Claude Code
fix(metadata-protocol): a by-name read naming no package wears the en…
fix(metadata-protocol): a row bound to one package is not registered …