Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21002-layered-flow-effective
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21002-layered-flow-effective

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #21002
Clause-②: no

What

For a flow name the loader ships from a managed package, the layered read (GET /api/v1/meta/flow/NAME/layers, and the deprecated layers flag on the by-name door, which uses the same helper) now reports the loader's body as the effective layer. That is the body the by-name read (GET /api/v1/meta/flow/NAME) and the flow list (GET /api/v1/meta/flow) have answered for that name since #20946 and #20913. A stored row of that name is still reported, as a separate shadowed layer of its own scope, and is never the effective layer under the package's lock and provenance flags.

getMetaItemLayered in packages/metadata-protocol/src/protocol.ts now decides its effective layer with the stored-row predicate PR #20942 introduced and PR #20994 reuses, isShippedFlowName, judged by name. It adds no precedence rule of its own.

  • The predicate is called, not edited. Only getMetaItemLayered moves in protocol.ts: the effective-layer binding and its docblock (+29 / -2 there).
  • The response shape is unchanged. No key is added or removed. The stored row stays where the layered answer already reports a stored row, beside the effective layer, with its own scope.
  • The registry half needs no call here. The code layer reads the loader's set (lookupArtifactItem, blind to tenant-authored rows) before the registry's bare slot, and a shipped name is one that set holds by definition. So isStoredFlowEntryOfShippedName would add an unreachable branch.
  • The lock and provenance flags are unchanged. They already resolved from the code layer first.

Why

What becomes of the stored rows themselves (keep, refuse, migrate) belongs to #15206. This PR does not decide it.

Why this PR is Part of: the published-snapshot door does not follow

The triage expected the published-snapshot read to follow the effective layer automatically. Measured, it does not.

  • GET /api/v1/meta/flow/NAME/published (packages/rest/src/rest-server.ts, about :8413–:8425) reads the layered answer, but it picks a layer itself: when a stored layer is present it serves that layer, and it never reads the effective one.
  • Its dispatcher twin in packages/runtime/src/domains/meta.ts (about :1121–:1137) has the same shape, by source reading. It was not measured: the dogfood stack routes through the REST transport.
  • So, with the stored row kept as a shadowed layer as the triage requires, that door still answers 200 with the stored body, both before and after this change.

The dispatch said to stop there and report, and not to edit that door in this card. The measurement and the options are in the report on #21002. #21002 remains open for that half.

Repro, before and after

Showcase composition on a database file, cold boot. A stored row is at rest under a shipped flow name, with a body that can be told apart from the loader's. There is also an organization-scoped row under a second shipped name, and an environment-wide row under a name no package ships.

Door or reading origin/main 2f2fa11d75 this branch
/meta/flow/NAME/layers, shipped name with a stored row: the effective layer 200, the stored body, under the package's provenance and package id 200, the loader's body, same flags
the same answer: the stored row reported as a separate layer, environment scope unchanged: reported, shadowed
the deprecated layers flag on the by-name door 200, effective layer is the stored body 200, effective layer is the loader's body
GET /meta/flow/NAME 200, the loader's body unchanged
GET /meta/flow, the entry for NAME the loader's body unchanged
GET /meta/flow/NAME/published 200, the stored body unchanged: 200, the stored body (see above)
control: a shipped name with no stored row, layers effective layer is the loader's body, no stored layer unchanged
control: the same name, published 501 NOT_IMPLEMENTED (this kernel has no code/package store) unchanged
control: a shipped name with an organization-scoped row only, layers effective layer is the loader's body, no stored layer unchanged
control: an unshipped name with a stored row, layers effective layer is the stored body unchanged
control: the same name, published 200, the stored body unchanged

Pins

  • Unit: packages/metadata-protocol/src/protocol.flow-layered-shipped-name.test.ts, 9 cases. It reuses the registry double of PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994's unit pin: the real SchemaRegistry key shapes, its getItem precedence (the bare slot first) and its artifact lookup.
    • A shipped name with a stored row: the effective and code layers are the loader's body, the stored row is reported with its own scope, and the package's flags stand. This holds before and after the row is hydrated.
    • The layered read, the by-name read and the list answer one and the same body.
    • The package-scoped read and the plural type spelling answer the same.
    • A row bound to the shipping package, or one whose body claims the package's stamps, is judged by name alone.
    • Controls: an unshipped name keeps its stored row as the effective layer; a shipped name with no row is unchanged; an organization-scoped row is out of reach; an overlay-regime type keeps overlay-wins.
  • Dogfood cold boot: packages/qa/dogfood/test/flow-shipped-name-layered-read.dogfood.test.ts, 8 cases, a new file.
    • The layered door reports the loader's body as the effective layer, under the package's flags.
    • The stored row is still reported, as a shadowed layer of its own scope.
    • The layered door, the by-name read and the list answer one and the same body.
    • The deprecated layers flag answers the same.
    • Three controls: a shipped name with no stored row, an organization-scoped row, and an unshipped name.
    • flow-shipped-name-by-name-read.dogfood.test.ts and flow-provenance-server-held.dogfood.test.ts are not touched.
    • The published-snapshot door is not pinned. The file's header says why.

Verification, at head 39ed9ac48a

protocol.ts and both pins are byte-identical between 5cdb27e44d and 39ed9ac48a. The last commit adds only the changeset and the ledger row.

Ablation. The fix was committed first (d690943261). Each leg ran through scripts/ablation-replace.mjs and deleted the predicate clause from the effective-layer binding. The anchor hit once, and the blob changed from 6056394ec7a6 to ed01c7ddc863.

Leg Resolution Result
A1, the unit pin ./protocol.js from source, no rebuild 5 failed, 4 passed. The 5 are every shipped-name case; the 4 controls pass.
A2, the dogfood pin the metadata-protocol dist, rebuilt after the mutation 3 failed, 5 passed. Failed: the effective layer, the three-way agreement and the deprecated flag. Passed: the store check, the shadowed-row report and the three controls.
  • A2 dist proof: scripts/ablation-dist-preflight.mjs found the guard absent from all 24 built files after the mutated build.
  • Restores: each restore was proven: the blob equals HEAD, and git diff HEAD is empty. After the restored build, the guard is present in 2 built files and the working tree is clean against HEAD.
  • A first A1 attempt was a no-op. The replacement text was a substring of the anchor, so the tool refused it before any test ran and restored the file. It produced no measurement.

Derived gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands printed 74 commands for this tree at 39ed9ac48a. All 74 were run, each exit code captured before any pipe. --ran reconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 unrun.

  • First pass: check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET. Eight packages outside this diff had no dist/ in this fresh worktree. After building those eight (all turbo cache hits), it exited 0.
  • Roster gates: the ten roster gates whose roster sits beside a path of this diff were also run, all exit 0. They are check-changeset-fixed, check-published-list-mirrors (plain and --self-test), check:authz-resolver, check:console-injection, check:engine-double-contract, check:error-code-casing, check:i18n-stale-fill, check:published-readme-exports and check-dts-references --self-test.
  • Base: origin/main has not moved since the branch point 2f2fa11d75.

Lint, a proven narrowing of pnpm lint (the repo-wide run is CI's), at 39ed9ac48a:

  1. Population, from eslint's own config: of the 5 touched paths, the config matches the 3 .ts files. The .md and .json files answer "File ignored because no matching configuration was supplied."
  2. Count, from --format json: 5 results. The 3 linted files have 0 errors and 0 warnings.
  3. Invariance: eslint.config.mjs never enables type-aware linting. All seven parserOptions blocks are ecmaVersion and sourceType only, with no project. The only other files the config reads are scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, and this diff touches neither. So the diff cannot move the verdict on any untouched file.

NOT MEASURED locally, declared to CI:

  • Test Core shards, Temporal Conformance, the full Dogfood Regression Gate and Dogfood Verify CLI.
  • Build Core and the workspace type-check lanes.
  • The runtime dispatcher's published twin (source reading only).

Deviations

  1. Part of #21002, not a closing line. The dispatch named a closing line. The published-snapshot door half of the card is measured unresolved and is now a decision for the seat, so this PR does not close the card. The seat can rewrite the first line if it rules that half out of the card.
  2. scripts/engine-double-contract.pinned.json, one generated row. The new unit pin's engine double has a findOne, so check:engine-double-contract requires the coverage ledger to learn the file, through --write. The diff is exactly that one row. PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 has the same precedent.
  3. No published-snapshot pin. The dispatch said to stop and report if that door picks a layer by itself. It does, so the door is measured and reported here, not pinned.

Acceptance notes

  • Unchanged, and named:
    • every other metadata type (the predicate gates on flow first);
    • flow names no managed package ships;
    • organization-scoped flow rows, which this read never reaches because flow declares no org override;
    • the lock, provenance and affordance flags, which already resolved from the code layer.
  • For an unshipped name with a stored row, the layered door's code layer is the stored body (measured on both trees). The code-layer fallback reaches the registry's bare slot, which holds the hydrated row. The spec describes that layer as null when no artifact ships the item. This is reported separately and is not touched here.
  • In the showcase composition, the published-snapshot door answers 501 NOT_IMPLEMENTED for a shipped flow with no stored row. That kernel has no code/package store for it to fall back to. This bears on what that door could answer for a shipped name, so it is part of the report.

Generated by Claude Code

claude added 3 commits October 1, 2026 02:39
…ts the loader's body as the effective layer

For a flow name the loader ships, getMetaItemLayered now decides its
effective layer with the stored-row predicate the list and the by-name
read already call (isShippedFlowName): the code layer is effective, and a
stored row of that name stays in the overlay layer as a shadowed layer of
its own scope. Every other type, and a flow name no package ships, keeps
overlay-wins.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…name agree across a cold boot

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…e engine-double ledger row its unit pin needs

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 6 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), getMetaItemLayered (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/contracts/metadata-service.mdx (via getPublished (sdk, the bare tail of client method meta.getPublished, bound to GET /api/v1/meta/:type/:name/published; the bare tail of client method meta.getPublished, bound to GET /meta/:type/:name/published))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via /:type/:name/published (route, bridged from symbol getMetaItemLayered — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b253fadfb7d1cb59448b1d7af8172e82aa245591 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0637601f03daa6b01ba012f9cdd69dec42a3ec0d — the merge of head 39ed9ac48a71c828ed30ef77711c1707dc6dd668 into base b253fadfb7d1cb59448b1d7af8172e82aa245591, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0637601f03daa6b01ba012f9cdd69dec42a3ec0d && git checkout 0637601f03daa6b01ba012f9cdd69dec42a3ec0d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b253fadfb7d1cb59448b1d7af8172e82aa245591 39ed9ac48a71c828ed30ef77711c1707dc6dd668 && git checkout -B drift-repro b253fadfb7d1cb59448b1d7af8172e82aa245591 && git merge --no-ff 39ed9ac48a71c828ed30ef77711c1707dc6dd668

node scripts/docs-audit/affected-docs.mjs --json b253fadfb7d1cb59448b1d7af8172e82aa245591

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b253fadfb7d1cb59448b1d7af8172e82aa245591 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 39ed9ac48a71c828ed30ef77711c1707dc6dd668
Local-runs: none

This is the record of record for PR #21043 at 39ed9ac48a, card #21002 (the layered read of a shipped flow name reporting a stored row as the effective layer under the package's lock and provenance flags, so that after #20946 it disagrees with the by-name read and the list), the layered-read half, under triage's first grade 5923270373 (the effective layer for a shipped flow name is the loader's body, decided by the predicate the list and the by-name read use; no fourth precedence path; the stored row appears as a shadowed layer; the docblock's promise becomes true; the published-snapshot read follows and is pinned too), the seat's claim 5923500567 (file surface: getMetaItemLayered only; the published door pinned, not edited, unless measured otherwise; stop and report) and the newest os-dev-report 5924071476 (needs_decision, one open question about the published door).

Inputs:

Check-runs on 39ed9ac48a, read after convergence and collapsed latest-per-name (a background poll of the commit's check-runs, no local run; every run reports this head): 35 runs, 32 success, 3 skipped (Build Docs and Console Pin Gate path-filtered, Packed-tarball smoke (opt-in) opt-in), 0 failure. All seven required contexts are success: Lint & Repo Gates (which carries check:engine-double-contract over ① (d), check:adr-anchors, check:cross-package-test-inputs and the rest of the check:* family), TypeScript Type Check (and its four sub-jobs, source gates, consumer gates, debt ledger, workspace), Test Core (and all six shards), Dogfood Regression Gate (and all three shards, which run the new cold-boot pin), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Check Changeset, Check PR Size, Spec property liveness, Dogfood Verify CLI, the claim, single-writer and part-of guards (Part-of PR must not also close its card, The card this PR closes must claim this branch), Check Documentation Links and the labelers are success. No run is red.

Mergeability: git merge-tree --write-tree of origin/main (d67b94280a, six commits past the merge-base, none of them touching the five paths of this diff or the three source files this review read) and refs/review/pr-21043, run from a throwaway bare clone with no merge driver registered (AGENTS.md's probe, since a local merge-tree is not GitHub's mergeability): clean, tree 7d3abc48a6, no conflicted path.

Disclosure is kept at the card's level: doors, roles, codes and statuses. The body's package-provenance stamps, the row's package binding, the tenant marker and the artifact's protection envelope are named abstractly here; the three layers are named by the method's own layer names, as the card and the precedent record name them; no request-body, header or field spelling appears, and no seeding step is written.

① Derived judgments

(a) getMetaItemLayered decides the effective layer for a shipped flow name with isShippedFlowName, keeps the stored row as the shadowed layer under its own scope, adds no key, and makes its docblock true — RIGHT, path by path.

  • The diff to protocol.ts is two hunks, both inside getMetaItemLayered (:9085 at the head): the docblock (:9041-9048) and the effective-layer binding with its comment (:9382-9407). The binding now reads overlay !== null && !this.isShippedFlowName(request.type, request.name) ? fold(overlay) : code (:9405-9407). The predicates (:14629-14633, :14644-14647) are byte-identical to the merge-base and are called, not edited. No comparator, no rank, no rule of this method's own: the binding's two outcomes are unchanged (the folded stored row, or the code layer) and the predicate's class is routed to the second, in the same polarity as the by-name read's step-1 guard (:8809, record && !shippedFlowActiveRead). Direction bullet 1 ("⛔ No fourth precedence path") is met literally.
  • The shadowed layer: the stored-layer read (:9287-9358) is untouched, so the row is still reported under the existing stored-row layer with its own scope (env for the card's state; org is unreachable for flow, below). The method's return type (:9085-9113) is unchanged: no key added or removed, and the spec's GetMetaItemLayeredResponseSchema is not touched. "Shadowed" is derivable from the answer (the effective layer equals the code layer, not the stored one) and is what the named consumer, the Studio diff tab, renders; the answer carries no per-layer provenance and no marker, which is a fact for ③ flag 1, not a defect under the direction, which asked for no key.
  • The lock and provenance flags: lockSource = code ?? overlay ?? {} (:9419) into resolveLockState(lockSource, artifactBacked) (:9420). For a shipped name the code layer is the loader's entry (:9258), so the flags name the package; and effective is now governServedObject('flow', code) (:9408), which is the code layer itself (governServedItem and materializeFromRegistry return their input for every type but object, :378-380, :7540-7541). Before, the flags described the code layer while effective was the stored row; now they describe the body served as effective. The PR body's "unchanged, they already resolved from the code layer first" is true of the source.
  • The docblock (:9043-9048), compared with getMetaItem (:8550) at the head, path by path:
    1. Active, no scope (GET /api/v1/meta/flow/NAME/layers, the deprecated layers flag on the by-name door, and the dispatcher's layered answer, all through this one method): getMetaItem discards the stored row (:8705, :8809), consults the metadata service (:8897), then the registry with the registry half (:8930-8942) → the loader's entry. The layered read's code layer consults the same service with the same arguments (:9231), then lookupArtifactItem (:9258) → the loader's entry, and effective is that layer. Same source order, same body. Changed; right; pinned (unit cases 1-3, dogfood cases 2 and 4).
    2. Active, package-scoped: both call lookupArtifactItem(type, name, packageId) (:8940, :9258); SchemaRegistry.getArtifactItem answers the prefer-local composite, then any composite passing the artifact test (registry.ts:3960-3972), so the shipping package's entry is served whichever package the caller names; the stored-row lookup in both doors reads the package-bound row then the package-less one, so the layered read reports the row and the by-name read discards it in both spellings. Pinned (unit case 4, both bindings in unit case 5).
    3. Strict draft and 4. previewDrafts: the layered read declares neither member; its stored-layer read is strictly active (:9296), so the sentence compares to the ordinary active read, which is what the spec's layer-3 description names ("the value an ordinary GET /meta/:type/:name would return"). getMetaItem's draft arms (:8713-8765, :8839-8853) are unfiltered by design (the fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 record's path 4). Unchanged; not a divergence this PR makes.
    4. Organization-scoped: orgId is undefined for flow in both methods (:8675, :9223; flow declares no org override), so only the environment-wide row is read, and an org-scoped row is out of reach. Pinned (unit case 8, dogfood case 7).
    5. Every other type: the predicate answers false before any lookup (:14630), so overlay-wins is byte-identical in effect. Pinned by the view overlay control (unit case 9).
    6. Shipped name with no row, unshipped name with a row: unchanged in both methods (unit cases 6-7, dogfood cases 6 and 8), with the one pre-existing exception on the code layer judged in ③ flag 4.
    7. The metadata-service step is consulted first in both methods with the same arguments, so whatever it answers the two cannot disagree through it; the fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 record read it as inert for flows on the showcase composition, and nothing here changes that.
      One standing approximation, pre-existing and not moved: effective is served raw of decoration (_diagnostics is a sibling key) and without mergeArtifactProtection, while getMetaItem's item passes through both; for the loader's entry the envelope is already on the body, so no byte moves for this name.

(b) The registry-half predicate is not called — RIGHT; the branch would be unreachable, verified from source.

  • For flow, isShippedFlowName(name) is packagedArtifactOwner (:14669-14674), which is lookupArtifactItem('flow', name) carrying a package id. The code layer's registry read is lookupArtifactItem(type, name, packageId) ?? registry.getItem(type, name, packageId) (:9258-9259). lookupArtifactItem goes through getArtifactItem (registry.ts:3920-3990), which scans the composite entries first (prefer-local, then any that passes the artifact test, :3960-3972) and reaches its bare fallback only when no composite holds the name; a shipped name has a composite by definition, under every package scope, so the left operand is defined for every shipped name and getItem, the bare-first read (:3888-3889) that would hand back the hydrated row, is never reached. isStoredFlowEntryOfShippedName is "shipped name and not isCodeArtifactBody" (:14644-14647); on the code layer of a shipped name it is always false, because the loader's entry passes the test (metadata-core code-artifact-provenance.ts:57-61). The call would never fire. The one path the argument does not cover is lookupArtifactItem's fallback for a partial registry double without getArtifactItem (:15104-15110), which is test territory only. Disclosed as Deviation 4 with the reason. Right.

(c) The pins — RIGHT; they hold the direction's three pins and the controls, and they red without the fix as reported.

  • Unit (protocol.flow-layered-shipped-name.test.ts, 9 cases): the effective and code layers are the loader's body with the stored row reported under its own scope and the package's flags beside it (case 1), the same after the row is hydrated into the registry's bare slot (case 2), the three-way agreement with getMetaItem and the list (case 3), the package-scoped and plural spellings (case 4), the row's package binding and the body's stamps judged by name alone (case 5), and four controls (unshipped keeps its stored row as effective, no row, org-scoped out of reach, overlay-regime view keeps overlay-wins). The registry double reproduces the real SchemaRegistry's two key shapes, getItem's bare-first precedence (registry.ts:3888-3889) and getArtifactItem's composite-first scan with the artifact test (:3960-3988), the shape PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994's pin used; the engine double carries no write verb and routes findOne through assertEngineFindOnePredicate. The hydration case drives the real getMetaItemsForExecution (:7912 → :8289).
  • Dogfood cold boot (flow-shipped-name-layered-read.dogfood.test.ts, 8 cases, a new file): the store check, the layered door's effective layer under the package's flags (case 2), the stored row reported as a shadowed layer of its own scope (case 3), the three-way agreement across /layers, the by-name read and the list (case 4), the deprecated layers flag on the by-name door answering the same layers (case 5; the door's closed query set admits the flag, rest-server.ts:6233), and the three controls. It runs in the dogfood isolated project (vitest.config.ts:236-237, the glob project) over two boots on one database file, as the fix(service-automation, metadata-protocol): a shipped flow name arms the loader's body at both boot steps, and a stored row of that name is reported as shadowed (#20913) #20942 and fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 pins did; the escaping path is spelled in the form check:cross-package-test-inputs recognises. The two files the claim fenced off are not touched; the published door is not pinned, with the file's header saying why (③ flag 3).
  • Red without the fix, read from source: with the predicate clause deleted from the binding (the dev's ablation, blob 6056394ec7 → ed01c7ddc863, restored and proven), effectiveBase returns to the stored row for a shipped name, so every assertion that the effective layer is the loader's body reds and nothing else does. Unit: cases 1-5 red, the four controls green — the reported 5 / 4. Dogfood: cases 2, 4 and 5 red; the store check, case 3 (which asserts the stored layer only) and the three controls green — the reported 3 / 5. Both counts read as predicted; the unit leg ran from source and the dogfood leg from a rebuilt dist with its preflight proof, as the body states.

(d) scripts/engine-double-contract.pinned.json, one generated row — RIGHT, compelled and exact, as in the #20994 record's ① (d).

  • Compelled: the new unit pin's engine double has a findOne, routed through the producer-side predicate; the gate's RETAINED invariant enumerates every pinned (file, verb) and reds on a pinned double the ledger does not record, prescribing --write; the alternative, an unguarded double, reds the PINNED invariant and would need a hand-written DEBT entry. The gate script is unchanged since that record.
  • Exact: the row { file: protocol.flow-layered-shipped-name.test.ts, verb: findOne, pinned: 1 } sits between protocol.flow-by-name-shipped-name.test.ts / findOne and protocol.flow-org-override-closed.test.ts / delete, its localeCompare position; the whole ledger is sorted at both trees; the entry count moves 833 → 834; the trailing newline and the $comment are untouched (the diff is the five lines). One engine double, one findOne, pinned: 1. Lint & Repo Gates, which carries check:engine-double-contract, is the byte-exact arbiter (the check-run paragraph). Disclosed as Deviation 2.

(e) The changeset .changeset/21002-layered-flow-read-shipped-name.md, @objectstack/metadata-protocol patch, Clause-②: no — RIGHT; every sentence is delivered at the head, it says nothing about the published door, and the discipline holds.

  • Sentence by sentence: flow is Regime C with no overlay read path — ADR-0126 §2 D1 and §3. The list and the by-name read already serve the package's flow for a shipped name — true since 75519e1c0a and 25f2e64657. The layered read did not, reporting a stored flow as the effective layer while its lock and provenance flags named the package — true of the merge-base (effectiveBase = overlay !== null ? … : code against lockSource = code ?? …). The layered read now decides with the same check the other two doors use — true, (a). For a shipped name the effective layer is the package's flow, with or without a package scope, whatever the stored flow's binding or markings say — true and pinned (unit cases 4-5). The stored flow is still reported as a separate layer of its own scope that does not take effect — true (unit 1, dogfood 3). The deprecated layers flag answers the same — true, same helper (rest-server.ts:6331-6342), pinned (dogfood 5). Not deleted, rewritten or refused — true; nothing in the diff writes. Unshipped names, organization-scoped rows and every other type are read as before — true, (a) paths 5-7.
  • It names the layered read, the by-name read and the list, and makes no statement about the published-snapshot door, which this PR does not change. Right: a note states its own PR's delta.
  • Package and level: @objectstack/metadata-protocol is released (17.5.0, not private); a bug fix in a released package takes patch; no accept set moves and no member is added, so Clause-②: no is right and no ADR-0087 disposition is owed. Check Changeset is green on this head.
  • Disclosure, across the changeset, the PR body, the report, the two file headers and every test title: doors by path, roles, codes and statuses only; the binding and the stamps are named abstractly ("package binding or markings", "the package's flags"); no request-body, header or field spelling; no seeding recipe (the PR body says a row "is at rest under a shipped flow name"). The dogfood pin's code is the one place the placement is spelled, as a cold-boot pin must, the same reading as the fix(service-automation, metadata-protocol): a shipped flow name arms the loader's body at both boot steps, and a stored row of that name is reported as shadowed (#20913) #20942 record's Residual 3 and the fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 record's ① (e). No model identifier in the three commits (the model-free trailer pair on each), the PR body (session-URL footer), the changeset or either pin; no tracker number in runtime prose (the new code is a comment and a predicate call).

Surface inventory: no route, schema, query set, status code or exported signature changes; one method's effective layer and the flags' referent move for exactly the shipped-flow-name-with-stored-row case, through the three doors that read it; the published-snapshot door and its dispatcher twin are untouched; one patch changeset; one generated ledger row; no governed path.

② Semver level

The PR body's line 2 reads Clause-②: no, as the claim did, and the changeset is patch. RIGHT.

③ Boundary flags

  1. The open question (the published-snapshot door, options A-D). Not chosen here. What the governing texts and the grade already decide, and what each option rests on, verified from source at the head:
    • Facts. (i) The REST door GET /api/v1/meta/flow/NAME/published (rest-server.ts:8270) reads the layered answer and, when a stored layer is present, serves THAT layer, never the effective one (:8412-8425 the pick, :8450 the serve); with no stored layer it falls to the metadata slot's published snapshot, answering 501 NOT_IMPLEMENTED when that slot has none (the branch at :8455, the code at :8468). (ii) Its dispatcher twin in runtime/src/domains/meta.ts has the same shape (:1053 the route, :1121-1135 the pick, :1139 the serve). (iii) For every type but object, the effective layer IS the stored layer by reference when one is present: the fold (:7345-7347) and both governance halves (:378-380, :7540-7541) return their input for a non-object type; for object the effective layer is the folded and governed body, not the raw row. So the report's three premises hold. (iv) MetadataManager.getPublished exists (packages/metadata/src/metadata-manager.ts:2142); the 501 on the showcase composition is the dev's measurement of what that composition's metadata slot exposes, not verified here. (v) The REST and dispatcher layered chains pass the protocol's effective through without substitution (no assignment to it in meta-item-read-gate.ts), so a door that read effective would read what this PR decides.
    • Decided by the texts. ADR-0126 §2 D1 admits no overlay read path for a Regime C type, and §3 puts flow there; the published door serving the stored row of a shipped flow name is one, so D (leave it) keeps the breach the card is filed on, as the report's own cost line says. The grade's bullet 2 ("the stored row appears as a shadowed layer") decides against B, which would drop the stored layer from the answer; and B lands the showcase door on the 501, not the loader's body. The grade's bullet 4 states the intended OUTCOME (the published door answers the effective layer, the loader's body) on a mechanism premise ("follows automatically") that is false at the head (facts i-ii); the outcome stands, the mechanism was never decided.
    • Left to the seat: A or C, the two that deliver the outcome. A reads effective at both doors: no key, no precedence path, two other lanes' files, bytes move only for shipped flow names and for object (fact iii), and the object half needs its own measurement before landing. C marks the stored layer and skips a marked one: Clause-②: yes, a spec regen, two doors edited, and the door then falls through to the snapshot, which is the 501 on the showcase (fact iv). The claim's own sentence ("An edit there is reported, not made, unless the measurement shows the door picks a layer by itself") reads two ways once the measurement came out as it did; the dev took the stop-and-report reading, which is also the claim's general clause, and the files are not on its surface. Right to escalate, and needs_decision is the right status.
  2. Part of #21002, not Fixes — RIGHT for this half. The card's title and body name the published-snapshot door as serving the stored layer, and the grade pins it ("Pin it as well"); that half is undelivered and measured unresolved. Part-of PR must not also close its card and The card this PR closes must claim this branch are both green on the head. If the seat narrows the card to the layered read, the published door needs its own card under Prime Directive chore: version packages #10 (a measured contract violation with reach is filed, not dropped, and D is excluded above), and the first line can then read Fixes.
  3. Deviations, all five answered: (1) Part of — right, flag 2. (2) The ledger row — compelled and exact, ① (d). (3) No published-snapshot pin — right: a pin of the current answer would lock in the ADR-0126 §2 breach, and a pin of the intended answer is red until the decision; the dogfood header discloses the omission and its reason, and the measurement is in the report. (4) The registry half not called — right, ① (b). (5) The model-free trailer pair is AGENTS.md's form, not a deviation (a harness-written model-named trailer is reporting, and the pre-push hook refuses a model identifier in the pair).
  4. The class-b finding (an unshipped flow name with a stored row: the code layer is the stored body) — CONFIRMED, and its own defect, not 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's.
    • Anchor, confirmed: the code layer's registry fallback lookupArtifactItem(…) ?? this.engine.registry.getItem(…) (:9258-9259). For a name no package ships, lookupArtifactItem is undefined (getArtifactItem finds no composite and its bare fallback requires the artifact test, registry.ts:3974-3988, which a hydrated row fails: the hydrator marks it tenant-authored and, for flow, withholds the envelope, :15967-15968), so getItem answers the bare slot (:3888-3889), which boot hydration fills from the store, and the stored row is served as code. Before hydration the same read answers code: null, so the layer depends on hydration state, the family 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 belonged to.
    • Contract text, confirmed: the spec's layer-1 description, "null when no artifact ships this item (it exists only as an overlay)" (protocol.zod.ts:494-498), and the docblock's "code is null if no artifact baseline exists" (:9053). Both are false for that name once hydrated; the #5707 / #5840 rule in the same docblock (a layer is an assertion made only from a read that happened) is the one it offends.
    • Its own defect: a different name class (unshipped), a different layer (code, not effective), a different text (the spec's layer-1 contract, not ADR-0126 §2), and no package claim is made (lockSource is the tenant row, artifactBacked is false, so the flags name no package and resettable is false). Reach: the layered door, the deprecated flag and the dispatcher's layered answer; the published door is unaffected (it reads the stored layer). Pre-existing on the merge-base, unchanged by this PR, and this PR's unshipped-name controls assert the effective layer, the stored layer and its scope and deliberately not code, so the fix will not red them. The fallback's inline comment names the population it legitimately serves (items registered at runtime without a package), which shares the bare slot with hydrated rows; the tenant marker the hydrator writes is the discriminator the fix would read. One question for the filing seat, not settled here: a stored body under an unshipped name that carries package-provenance stamps feeds resolveLockState as code, and whether the marker or the stamps decide its flags was not read. File it, class b; not security-family on its face.

Implemented-by: claude/issue-21002-layered-flow-effective
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants