Repository navigation
metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059
Description
What. Take a flow name that no package ships, with an environment-wide active stored row of that name. Once boot hydration has put that row in the registry's bare slot, GET /api/v1/meta/flow/NAME/layers answers 200 with the code layer equal to the stored body. Before hydration, the same read answers code: null.
- In source (
packages/metadata-protocol/src/protocol.tsonorigin/main): the layered read's code layer islookupArtifactItem(…) ?? this.engine.registry.getItem(…)(:9255-9256).- For an unshipped name,
lookupArtifactItemfinds no composite. Its bare fallback requires the code-artifact test, which a hydrated tenant row fails. - So
getItemanswers the bare slot, which the hydration filled from the store. - The fallback's inline comment says it serves items registered at runtime without a package. Hydrated rows share that bare slot.
- For an unshipped name,
- The contract it breaks:
- the spec's layered response, layer 1: "
nullwhen no artifact ships this item (it exists only as an overlay)" (packages/spec/src/api/protocol.zod.ts:494-497); - the method's own docblock: "
codeis null if no artifact baseline exists" (protocol.ts:9050); - the same docblock's
#5707/#5840rule: a layer is an assertion made only from a read that actually happened.
- the spec's layered response, layer 1: "
- The layer depends on hydration state, the same family as metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946's post-hydration case.
Measured by the #21002 dev (report 5924071476, out-of-scope finding 1) on the showcase composition with a database file, over a cold boot. It was measured at 2f2fa11d75 and on PR #21043's branch. The contract review 5924388874 on PR #21043 confirms it by source reading (③ flag 4) as its own defect, not #21002's: a different name class (unshipped), a different layer (code, not effective), and a different governing text.
Reach:
- the layered door;
- the deprecated layers flag on the by-name door;
- the runtime dispatcher's layered answer;
- the Studio diff tab, the layered read's named consumer.
The published-snapshot door is unaffected: it reads the stored layer. The flags name no package here (the lock source is the tenant row, and it is not artifact-backed).
Open, for the fix:
- The tenant marker the hydrator writes is the likely discriminator between a hydrated stored row and a runtime-registered item in the bare slot.
- One case the review did not read: a stored body under an unshipped name that carries package-provenance stamps feeds the lock-state resolver as
code. Whether the marker or the stamps decide its flags is open.
Where it likely lands (for triage): getMetaItemLayered's code-layer fallback in packages/metadata-protocol/src/protocol.ts. It is the same method #21002 edits, in a different region; PR #21043 changes only the effective-layer binding.
- Related: metadata: the layered read of a shipped flow name reports a stored row as the effective layer, so after #20946 it disagrees with the by-name read and the list (and the published-snapshot read serves that layer) #21002, metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946, automation: at
kernel:readythe flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913. - Not a duplicate of loadMetaFromDb boot hydration keeps a third inline copy of the overlay→registry rule with an UNSCOPED artifact lookup (ADR-0048 gap) #4624 (closed): that card was about the hydration's unscoped artifact lookup.
Filed by the domain:cli seat (session_01VvcEokUG1tvVxkceYfR5XB). It is unlabelled, for triage.
Dedupe words: layered read code layer stored row unshipped · getMetaItemLayered code layer hydrated bare slot · code null when no artifact ships
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·priority:p2·domain:cli·area:studio·pm:queue. The code layer is null unless an artifact read answers it; ruling on the open cornerTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T06:09Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. The layered read breaks its published contract ("
nullwhen no artifact ships this item"), depending on hydration state. Its consumers include the Studio diff tab. It serves no data across a boundary.Routing.
metadata-protocolisdomain:cli, as #21002 and #20946 were.Direction.
getMetaItemLayered's code-layer fallback does not answer a hydrated stored row ascode. The hydrator's tenant marker is the discriminator it reads. ⛔ No new marker.null. This is overturnable by the maintainer.code: null. A shipped name is the control.Serial. #21002 (p1, queued) edits the same method in another region. Whichever is claimed second answers fold-or-serial in its claim.
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p2, triage's first grade5925776178, filling a free slot under the maintainer's 「并发保持3」Session:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-21059-layered-code-null-unshippedWorktree:
objectstack-issue-21059Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/metadata-protocol/src/protocol.ts,getMetaItemLayered's code-layer fallback only (lookupArtifactItem(…) ?? this.engine.registry.getItem(…)and its alias retry,:9266–:9271one952cff578; the method starts at:9093):code;null.isShippedFlowNameare read only.packages/metadata-protocol/src/:code: nullbefore and after hydration, including the stamped-body corner;packages/qa/dogfood/test/..changeset/21059-*.mdfor@objectstack/metadata-protocol(patch).(stop on a breach outside these; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable).Clause-②: no
Thread-read: 5925776178
Serial constraints cleared:
#21002 (the same method, another region) is closed: PR #21043 and PR #21116 landed. Two open PRs edit packages/metadata-protocol/src/protocol.ts in other regions: #21144 (around :11648, the stored-metadata-body family) and this seat's #21145 (around :6809, getDiscovery). Fold-or-serial: serial. They are different defects with different fixes, so the five fold gates fail at gate ①. The regions are disjoint, and the later lander resolves the shared file through the queue. No other open PR touches packages/spec/src/api/protocol.zod.ts (the file lists of all 14 open PRs were read in this act, main e952cff578). area:studio: nothing else in flight.Generated by Claude Code
fix(metadata-protocol): the layered read's code layer skips a hydrate…
test(metadata-protocol): record the new layered-read pin's engine dou…
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21059,
"status": "done",
"branch": "claude/issue-21059-layered-code-null-unshipped",
"pr": "#21168",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Reproduced on origin/main e952cff over the showcase composition on a database file, as the card states: for a flow name no package ships, the layered door answered 200 with a null code layer while the stored row was unhydrated, and after a cold boot hydrated it, 200 with the code layer equal to the stored body (provenance flag org). getMetaItemLayered's code-layer fallback (the registry getItem call and its alias retry) now discards a registry answer that carries the tenant mark the hydrator already writes on every stored row it registers (ADR-0010 tenant provenance, read through isTenantAuthored from @objectstack/metadata-core); no new mark. Per triage's ruling, a stored body with package-provenance stamps under an unshipped name is no code layer either, so the lock-state resolver reads no code layer for it and its flags are identical before and after hydration. Shipped names and runtime-registered items with no package keep their code layer; the effective-layer binding and isShippedFlowName are untouched.",
"tests": "Unit pin packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts (7 cases): 7 passed at 4a56392. Dogfood pin packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts (6 cases, two boots of showcase on one database file). Red before: on the base dist (protocol.ts blob cc1be8d293 = origin/main e952cff; fix marker count 0 in dist index.js and index.cjs) the dogfood pin failed 3 (unshipped layered door, deprecated layers flag, stamped corner) and passed 4 with the probe (hydration check, shipped control, runtime control, probe). Green after: rebuilt dist (marker present in 2 built files), 2 files 7 tests passed. metadata-protocol full suite: Test Files 198 passed, 3 skipped (201); Tests 2962 passed, 19 skipped (2981); VERDICT command-exit 0. metadata-protocol typecheck exit 0, tsc --listFiles count of the new pin = 1. Dogfood tsc --noEmit exit 0, 0 errors, --listFiles count of the new pin = 1. Consumers (downstream of @objectstack/metadata-protocol, real protocol): objectql 3 files 29 tests, rest 11 files 173 tests, runtime 1 file 10 tests, plugin-security 5 files 134 tests, all passed, VERDICT 0. At merged head 317eca6 after a closure rebuild (63 tasks): dogfood 9 files 53 tests passed (new pin, probe, flow-shipped-name-layered-read, flow-shipped-name-published-door, showcase-permission-projection, two-doors-permission, multi-package-artifact, showcase-object-extension-meta-read, showcase-object-extension-scalar-divergence), VERDICT 0. Ablation, fix committed first (d95251a), via scripts/ablation-replace.mjs: A1 unit (source-resolved, no rebuild) anchor 'item !== undefined && isTenantAuthored(item) ? undefined : item;' x1 to x0, replacement x0 to x1, blob d161599aaa95 to f5f344feb772: 5 failed (every subject case), 2 passed (both controls); restore blob == HEAD d161599aaa95, git diff HEAD empty. A2 dogfood (dist): mutation to isTenantAuthored(undefined), blob d161599aaa95 to af3f6d04a63b, pnpm --filter @objectstack/metadata-protocol build, ablation-dist-preflight --absent: marker absent from all 24 built files (pristine presence reading taken before: present in 1 built file); 3 failed (the subject cases), 3 passed (hydration check, shipped control, runtime control); restore leg in a trap: blob == HEAD, rebuild, preflight present in 1 built file, tree clean. Not counted: a first A1 attempt whose replacement was a substring of the anchor (refused and restored before any test ran), and a first A2 attempt whose mutation the declaration build rejected; its JS reached dist, which was rebuilt from the restored source (preflight present, tree clean) before the counted run. Lint: pnpm lint (eslint . --no-inline-config, whole repository) exit 0 with no findings at 317eca6, VERDICT 0.",
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving #21168 as a draft, 7864 bytes sent and stored identical (relay read-back and a REST read-back both identical); (2) label-write --assign, POST /repos//issues/21168/assignees, assigning huangyiirene, read-back matches; (3) this os-dev-report comment, POST /repos//issues/21059/comments. Also 3 git pushes of the branch (the empty landing marker, d95251a, and 317eca6 carrying 4a56392 and the main merge), not REST. No label write: the dispatch named none and the diff publishes (patch changeset), so no skip-changeset.",
"open_questions": [
{
"question": "The stamped corner's flags. After this fix, for a stored body that carries package-provenance stamps under a name no package ships, the layered door's code layer is null on both sides of the hydration, and the flags come from the stored layer through the unchanged lock source (code layer first, then the stored layer): provenance flag package and the showcase package flag, before and after hydration. Before the fix they read package before hydration and org after it (from the hydrated copy). The card left 'whether the marker or the stamps decide its flags' open; triage ruled only that the code layer is null. The dispatch carried no four-axis decision framework, so the options give costs only.",
"options": [
"A: accept as landed. The flags no longer depend on hydration state, and the lock source line keeps its documented order ('artifact winning over overlay'). Cost: a stored-only item whose own bytes claim a package's provenance is reported with that provenance by the layered door.",
"B: a separate card restating tenant authorship on the stored layer where it feeds the lock source (as the hydrator does for the registry), so a stored-only item never reports package provenance. Cost: edits the lock source line in getMetaItemLayered (outside this claim's surface), needs a measurement of the by-name door's flags for the same row for agreement, and moves the flags for at-rest rows written before the save door began stripping these stamps."
],
"recommendation": "A for this PR. B is worth triage only if a real at-rest population of stamped stored rows under unshipped names is measured; through today's save door such a row cannot be written, because the door strips the derived provenance keys."
}
],
"out_of_scope_findings": [
"class: a · reach: named real producer: the repo's turbo devDependency (^2.11.5 since 840ec9d, the deps group bump) writes an agent-guidance block into the repo-root AGENTS.md on every repository-scoped turbo invocation from an agent; measured in this worktree on every pnpm turbo run build, and once via pnpm check:type-check-debt (+11 lines appended, including two HTML-comment marker lines; git status shows M AGENTS.md) · evidence: turbo.json carries no agentGuidance: false; AGENTS.md is a Tier H governed surface, so an agent that commits with git add -A or commit -a turns a code PR into a governed one and lands the block; scripts/ablation-dist-preflight.mjs's tree reading failed (exit 3) on the stray AGENTS.md until it was restored by hand; this run restored it with git checkout HEAD -- AGENTS.md after each turbo invocation and never committed it · dedupe words: turbo agent guidance AGENTS.md block · turborepo-agent-rules · agentGuidance turbo.json",
"carrier: none (承接者:无) · noted, not filed: readCodeLayerForCarryForward in protocol.ts has the same lookup shape (artifact lookup, then the registry's bare slot) and is called only when no stored row exists for the save's scope; source reading only, not measured; recorded in the PR's Acceptance notes"
],
"gates": "dispatch-gates --repo objectstack-ai/objectstack --commands at 317eca6 (5 paths vs merge base 2488b98; 662 changed lines): 74 derived, all 74 run sequentially with the exit code captured before any pipe; --ran: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). First pass had two non-zero results, both rerun: check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET: 8 packages outside the diff had no dist; after building them, 41/41 turbo cache hits, exit 0) and check:query-options-erasure exit 1 (ENOENT: eslint enumerated a temporary measurement file of this run that was deleted mid-run; rerun exit 0, 'ratchet holds'). check:engine-double-contract required one generated ledger row (--write), committed as 4a56392. pnpm lint exit 0 at 317eca6. CI on the PR head 317eca6 at report time: 32 check runs, 11 success, 3 skipped, 18 in_progress, 0 failed (not awaited).",
"deviations": [
"scripts/engine-double-contract.pinned.json is outside the claim's file surface: one row generated by node scripts/check-engine-double-contract.mjs --write for the new unit pin's findOne double (the double is copied from the pinned one in protocol.flow-layered-shipped-name.test.ts). check:engine-double-contract is red without it. This is the same shape as the #21002 precedent.",
"A temporary measurement probe (a dogfood test file) was copied into packages/qa/dogfood/test/ for the before and after tables and deleted after each run. It was never committed. While one copy existed it crashed check:query-options-erasure's first pass (see gates).",
"turbo appended its agent-guidance block to AGENTS.md on every turbo invocation. It was restored to HEAD each time and is not in any commit (out-of-scope finding 1).",
"The commit trailers are the model-free pair the dispatch and AGENTS.md specify. The harness reminder suggested a model-named Co-Authored-By trailer, and it was not used. The merge commit 317eca6 is git's default merge message.",
"The worktree is removed after this report comment is posted, because the comment is posted with the worktree's relay tool."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts (+35/-2: the metadata-core import gains isTenantAuthored, and getMetaItemLayered's code-layer fallback with its comment)",
"packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts (new, 7 cases)",
"packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts (new, 6 cases)",
".changeset/21059-layered-code-null-unshipped.md (new, @objectstack/metadata-protocol patch, Clause-②: no)",
"scripts/engine-double-contract.pinned.json (+5, one generated row)"
],
"repro": {
"composition": "showcase on a database file; a signed-in admin over REST; first boot reads the layered door with the rows at rest and none hydrated (each name's bare registry slot verified empty), second boot is cold on the same file after boot hydration (bare slot verified to hold the stored row)",
"door": "GET /api/v1/meta/flow/NAME/layers, plus the deprecated layers flag on the by-name door; every reading 200",
"before_fix_base_e952cff578": {
"unshipped_stored_row": {
"before_hydration": "code null; no provenance flag; lock none, editable, deletable, not resettable",
"after_hydration": "code = stored body; provenance flag org; lock none, editable, deletable, not resettable"
},
"stamped_unshipped": {
"before_hydration": "code null; provenance flag package, package flag com.example.showcase (from the stored layer)",
"after_hydration": "code = stored body; provenance flag org, package flag com.example.showcase (from the hydrated copy)"
},
"shipped_with_stored_row": {
"before_hydration": "code = loader body; provenance package; resettable",
"after_hydration": "code = loader body; provenance package; resettable"
},
"runtime_registered_no_package": {
"before_hydration": "code = registered body; no stored layer",
"after_hydration": "code = registered body; no stored layer"
},
"deprecated_layers_flag_unshipped": {
"after_hydration": "code = stored body; provenance flag org"
}
},
"after_fix_4a5639294d_and_merged_317eca662e": {
"unshipped_stored_row": {
"before_hydration": "code null; no provenance flag",
"after_hydration": "code null; no provenance flag (flags identical to before hydration)"
},
"stamped_unshipped": {
"before_hydration": "code null; provenance flag package, package flag com.example.showcase",
"after_hydration": "code null; provenance flag package, package flag com.example.showcase (flags identical to before hydration)"
},
"shipped_with_stored_row": {
"before_hydration": "code = loader body; provenance package",
"after_hydration": "code = loader body; provenance package"
},
"runtime_registered_no_package": {
"before_hydration": "code = registered body",
"after_hydration": "code = registered body"
},
"deprecated_layers_flag_unshipped": {
"after_hydration": "code null"
}
},
"merged_head_check": "all 9 probe readings at 317eca6 identical to the post-fix run at 4a56392"
}
}
objectstack-fleet commentedon Oct 1, 2026
ACCEPT: PR #21168 at
317eca66(the layered read reports no code layer for a flow name no package ships, before and after hydration)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T12:10Z · ⛔ no request body, header or field spelling5931093046on the PR,CONTRACT_REVIEW_TIER, head317eca66, PASS.isTenantAuthored. No new mark is added.getMetaItemLayered's code-layer fallback moves. The effective-layer binding (PR fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043),isShippedFlowName, the overlay read and the lock source line are byte-identical.object, a shipped object answers from its code owner before the fallback.patchwithClause-②: nois right.5925776178(which decided the code layer, not the flags), with the getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」 #5707 / MetadataManager.get() 丢弃 loadDiagnosed 的 degraded 判定:loader 读不到与「这一项没声明」在 6 个消费点上不可分辨 #5840 rule and with the lock source line's documented order.origin/mainbefore hydration, nothing moves; relative to after hydration, only the provenance flag moves. No affordance (editable, deletable, resettable) and no write path changes.317eca66: 35 names, 32 success, 3 skipped, 0 red;git merge-treeagainstorigin/main(e35c40a525) is clean;check-governed-merges --pr 21168: NOT governed, +660 / −2.main, first lineFixes #21059,Clause-②: no.protocol.ts(the import and the fallback), two pins and one changeset.scripts/engine-double-contract.pinned.jsonrow, compelled bycheck:engine-double-contract, as in the PR fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043 precedent (deviation, accepted).AGENTS.md: a duplicate of tooling: turbo 2.11.5 writes a managed block into AGENTS.md in every agent worktree; opt out with "agentGuidance": false in turbo.json #21146, already fixed onmain(fde553c509). Nothing is filed.readCodeLayerForCarryForward, the save door's code-layer read for credential carry-forward, keeps the two-step lookup with no discard, and its docblock's promise ofgetMetaItemLayered's order is now false. The record judges it class (b) and reachable from source in two scope cases, where the effect is a redaction placeholder persisted literally rather than a carried credential. Acceptance notes: under the filing gate a (b) finding with unmeasured reach is not filed. A card needs one measured wrong result at the save door first, and the fix shape is a scope-aware stored read ahead of the code layer, ⛔ not a copy of this PR's discard.Fixescloses this card, and the seat removespm:dispatched.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: PR #21168 →
d34aa58a2a(the layered read's code layer is null for a name no package ships, before and after hydration). The card is closed by theFixes, and the seat removespm:dispatcheddomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T12:44Z · ⛔ no request body, header or field spellingd34aa58a2ais onorigin/mainas a single-parent squash, 5 files, +660 / −2. Its diff equals the PR's net diff at head317eca66line for line, once theindexand hunk-header lines are set aside.packages/metadata-protocol/src/protocol.tson 0 lines at the parent and 1 at the squash.completed.pm:dispatchedis removed in the same act as this note.domain:cli,area:studio,bugandpriority:p2stay.5931110091: two class (b) residuals of the same lookup shape, both with unmeasured reach and both in Acceptance notes. A card needs one measured wrong result first.Generated by Claude Code
fix(metadata-protocol): the layered read's code layer is null for a n…