Skip to content

fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) - #20994

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20946-by-name-flow-read
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20946-by-name-flow-read

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20946
Clause-②: no

What

For a flow name the loader ships from a managed package, the by-name read (GET /api/v1/meta/flow/NAME) now answers the loader's body. That is the same body the flow list (GET /api/v1/meta/flow) and the execution view have answered since #20913. A stored row of that name is no longer served by name as the package's definition.

getMetaItem in packages/metadata-protocol/src/protocol.ts now calls the two predicates PR #20942 introduced for the list, and adds no precedence rule of its own:

  • The stored-row half, isShippedFlowName, judged by name. The active read does not adopt the environment-wide stored row of a shipped flow name. The row's package binding and the body's package-provenance stamps decide nothing.
  • The registry half, isStoredFlowEntryOfShippedName. The registry answers its bare slot first, and for a shipped flow name that slot holds the hydrated stored row. That entry is not one of the loader's, so the loader's entry is served.

The predicates are called, not edited. Only getMetaItem moves in protocol.ts (+36 / -1 there).

Why

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

Repro, before and after

Showcase composition on a database file, cold boot. Between two boots, a stored row was placed at rest under a shipped flow name, with a body that can be told apart from the loader's (its own label, one node renamed). Two more rows were placed: 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 f6ccca4a44 this branch
GET /meta/flow/NAME, shipped name with a stored row 200, the stored body, under the package's stamps 200, the loader's body
the same door with a package scope 200, the stored body 200, the loader's body
GET /meta/flow, the entry for NAME the loader's body the loader's body (unchanged)
startup receipt for NAME armed: package, shadowed: runtime unchanged
control: a shipped name with no stored row the loader's body unchanged
control: a shipped name with an organization-scoped row only the loader's body unchanged
control: an unshipped name with a stored row the stored body unchanged

Pins

  • Unit: packages/metadata-protocol/src/protocol.flow-by-name-shipped-name.test.ts, 10 cases. It uses a registry double with the real SchemaRegistry key shapes, its getItem precedence (the bare slot first) and its artifact lookup.
    • A shipped name with a stored row answers the loader's body, both before and after the row is hydrated.
    • By name and in the list, the shipped name answers 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; a shipped name with no row is unchanged; an organization-scoped row is out of reach; an overlay-regime type keeps its overlay.
  • Dogfood cold boot: packages/qa/dogfood/test/flow-shipped-name-by-name-read.dogfood.test.ts, 8 cases.
    • By name, on both spellings of the door, the loader's body.
    • By name and in the list, one and the same body.
    • The stored row is still reported as a shadowed contender, and the loader's body is what is armed.
    • The three controls in the table above.
    • The pin is a new file. flow-provenance-server-held.dogfood.test.ts is not touched.

Verification, at head 07843e6889

  • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 (the whole package): 195 files passed, 3 skipped; 2896 tests passed, 19 skipped.
  • pnpm --filter @objectstack/metadata-protocol run typecheck: exit 0. tsc --listFiles includes the new unit pin.
  • Dogfood, vitest run over four files: the new pin, PR 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's flow-shipped-name-stored-row-boot, flow-provenance-server-held and automation-authoring-doors-durable. 4 files, 35 tests passed. The metadata-protocol dist carries the fix.
  • pnpm --filter @objectstack/dogfood run typecheck: exit 0. --listFiles includes the new pin.
  • Red before: the dogfood pin against the origin/main build of metadata-protocol gives 3 failed and 5 passed. The three failures are the by-name cases; the store check, the receipt and the controls pass.

Ablation. The fix was committed first (09f3a596bd). Each leg ran through scripts/ablation-replace.mjs, with its anchor hit once and a blob change confirmed on disk. The unit pin imports ./protocol.js from source, so no rebuild was involved.

Leg What was removed Result
A1 the stored-row half 6 failed, 4 passed
A2 the registry half 2 failed, 8 passed: the post-hydration case and the list-agreement case

Both restores were proven: the blob equals HEAD (5d475cd667) and git diff HEAD is empty.

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

  • On the first pass, check:dts-closure and check:dual-build-cjs-loads exited 1. Both named @objectstack/organizations missing dist/index.d.ts, a package outside this diff that was partially built in the shared local tree. After pnpm --filter @objectstack/organizations build, both exited 0.
  • The seven roster gates whose roster sits beside a path of this diff were also run, all exit 0: check-changeset-fixed, check-published-list-mirrors, check:authz-resolver, check:console-injection, check:error-code-casing, check:i18n-stale-fill and check:published-readme-exports.
  • The head is 3 commits behind origin/main 7fa67dada3 (formula, plugin-security, service-analytics and the PM fleet-write scripts). None of those commits touches a path of this diff.

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

  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 (every parserOptions is ecmaVersion and sourceType only, with no project). The only other files it 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.

Deviations

  1. scripts/engine-double-contract.pinned.json, one generated row. The new unit pin's engine double has a findOne, so it routes through assertEngineFindOnePredicate. check:engine-double-contract then requires the coverage ledger to learn the file, and it prescribes --write. The diff is exactly that one row. The file is outside the claim's file list, and the gate compels it.
  2. The package-scoped spelling is changed too. The dispatch's mechanism hypothesis listed reads with a package id as unchanged. Measured on origin/main, the package-scoped by-name read served the stored body as well, because the stored-row lookup falls back to the package-less row. The list applies the two predicates whatever the package scope. Leaving this spelling out would have left the defect reachable on the same door, so it follows the ruling's intent, and it is pinned in the unit and dogfood suites.
  3. Two merges of origin/main. Neither had conflicts. The net delta against main is 5 files, +601 / -1.

Acceptance notes

  • Unchanged, and named:
    • the strict draft read and the draft-preview arm (a draft is answered as a draft, never under the artifact's envelope, and the list's preview arm is equally unfiltered);
    • every other metadata type (both predicates gate 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 metadata-service step of the by-name read is untouched. Measured on the showcase composition, it answers nothing for a shipped flow name, an unshipped one or a stored one. The list's own metadata-service merge is not filtered by the predicates either.
  • The pending note .changeset/20913-flow-stored-row-shipped-name.md ends with "The by-name read, GET /api/v1/meta/flow/:name, is not changed."
  • The ADR anchor for protocol.ts already lists ADR-0126. Its invariant sentence names only the list, which is still true. It could gain a by-name clause on its next touch. Carrier: none.

Out-of-scope finding, for the seat to file

  • class b · the layered read door, GET /api/v1/meta/flow/NAME/layers.
    • What it serves: for a shipped flow name with a stored row, it answers its effective layer as the stored body, and the response's provenance names the package.
    • Measured: 200 both before and after this PR, on the cold boot above.
    • Contract: the method's own docblock says the effective layer is "what getMetaItem would return". ADR-0126 §2 says "never an overlay read path".
    • Why now: after this PR it is the one read door for that name that disagrees with the list and the by-name read.
    • Remedy shape: the same predicate, so the effective layer takes the code layer for a shipped flow name.
    • Not done here: this claim's region is getMetaItem only.
    • Dedupe words: meta flow layers effective stored row shipped name · layered read effective overlay flow regime C.

Generated by Claude Code

…ed name across a cold boot

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…s the loader's body, as the list does

The by-name read now calls the two predicates the flattened view applies
for a flow name the loader's set holds: the stored row of that name is not
adopted, and the registry's hydrated copy of it does not stand in for the
loader's entry. Active reads only; drafts and every other type unchanged.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…t the engine refuses; the pinned ledger learns it

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l 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 1 documentable anchor(s).

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

  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
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 7fa67dada3a59fa185af0f09f89585c912908aa0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7f99f0d9d112dd58e80c13d52340be649ad239c4 — the merge of head 07843e6889270dbca3c5d89442fb9f6b662bb297 into base 7fa67dada3a59fa185af0f09f89585c912908aa0, 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 7f99f0d9d112dd58e80c13d52340be649ad239c4 && git checkout 7f99f0d9d112dd58e80c13d52340be649ad239c4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7fa67dada3a59fa185af0f09f89585c912908aa0 07843e6889270dbca3c5d89442fb9f6b662bb297 && git checkout -B drift-repro 7fa67dada3a59fa185af0f09f89585c912908aa0 && git merge --no-ff 07843e6889270dbca3c5d89442fb9f6b662bb297

node scripts/docs-audit/affected-docs.mjs --json 7fa67dada3a59fa185af0f09f89585c912908aa0

⚠️ 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 7fa67dada3a59fa185af0f09f89585c912908aa0 → 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: 07843e6889270dbca3c5d89442fb9f6b662bb297
Local-runs: none

This is the record of record for PR #20994 at 07843e6889, card #20946 (the by-name flow read serving a stored row's body under the shipping package's provenance for a shipped flow name, so that it disagrees with the flow list), under triage's direction 5920432754 (take the interim; the by-name read calls the list's predicates; no third precedence path; the rows' fate is #15206's), the seat's claim 5921284256 (file surface: getMetaItem only; a region split against PR #20959) and the newest os-dev-report 5922452902 with its two open_questions.

Inputs:

Check-runs on 07843e6889, 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), 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, Check Documentation Links and the labelers are success. No run is red.

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, no request-body, header or field spelling appears, and no seeding step is written.

① Derived judgments

(a) getMetaItem calls the list's two predicates and adds no precedence rule of its own — RIGHT, and every read path through the method is accounted for.

  • The diff to protocol.ts is three hunks, all inside getMetaItem (:8550-9079 at the head): one binding, shippedFlowActiveRead = readState === 'active' && this.isShippedFlowName(request.type, request.name) (:8705); one guard on step 1, if (record && !shippedFlowActiveRead) (:8809); one re-selection after step 3, if (this.isStoredFlowEntryOfShippedName(request.type, item)) item = this.lookupArtifactItem(request.type, request.name, request.packageId) (:8939-8941). The predicates themselves (:14602-14620) are byte-identical to the merge-base; they gate on the canonical flow type first and ask packagedArtifactOwner by NAME, so a row's own bytes decide nothing. No comparator, no rank, no third rule: the method asks the same two questions the list asks at :8065 and :8213, in the same polarity (the stored-row half refuses the row by name; the registry half refuses the registry's bare copy and serves the loader's entry). Direction bullet 3 ("⛔ No third precedence path. The by-name read calls the predicate the list calls.") is met literally.
  • Why the registry half cannot answer undefined when it fires: isStoredFlowEntryOfShippedName is true only when packagedArtifactOwner found a loader entry for the name, and SchemaRegistry.getArtifactItem (registry.ts:3920-3990) answers the prefer-local composite, then ANY composite that passes the artifact test, before its bare-key fallback — so a package-scoped request naming a package that does not ship the name still receives the shipping package's entry, and the served body is always the loader's. isCodeArtifactBody (metadata-core) is the loader's-entry test the list uses too.
  • Why the merge at the end now serves the package's definition truthfully: mergeArtifactProtection(item, artifactItem) (:8980-8983) grafts the artifact's envelope over whatever item is; before, item was the stored body and the envelope made it read as the package's (the card's "graft"); now item is the loader's entry, so the envelope describes the body it covers.
  • The read paths, enumerated, with what changed:
    1. Active, no scope (GET /api/v1/meta/flow/NAME, and the cached wrapper, which delegates to this method): for a shipped flow name with an environment-wide stored row, step 1 reads the row and discards it, step 2 (metadata service) is now consulted where before it was skipped, step 3's bare slot (the hydrated row, when the list or the boot hydrated it) is replaced by the loader's entry. Served: the loader's body. Changed; right, and pinned (unit cases 1-3, dogfood cases 2 and 4). Step 1 still performs the store read, so a store outage still surfaces as the getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532 503 rather than being masked by a loader's answer: the right order.
    2. Active, package-scoped: judged in (b). Changed; right.
    3. Strict state: 'draft': readState === 'draft' makes shippedFlowActiveRead false; step 1 adopts the draft row and the draft return (:8839-8855) answers it as a draft, never under the artifact's envelope; NO_DRAFT 404 otherwise. Unchanged; right — a draft is a pending edit, not a served definition, and the direction names the active read.
    4. previewDrafts: the preview arm (:8713-8770) runs before step 1 and is untouched; a draft row of a shipped flow name is previewed as a draft, tagged, under no envelope. Unchanged; right by the direction's own standard — the list's preview arm (:8313-8350) applies neither predicate either, so the two doors agree on drafts exactly as they agree on the active read. The only producer of such a draft is the operator's hatch or a direct store write (every authoring door refuses a held name as a locked base since access-security.packaged-flow-write-door-parity clauses 2 and 3 fail on main — detail withheld pending maintainer #20679 / fix(automation): which flows are packaged is the loader's fact, and every flow written through an authoring door is tenant-authored (#20761) #20853), and its fate is feat(metadata-core,metadata-protocol,objectql,plugin-security): the sys_metadata family goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206's. Disclosed in the PR body's acceptance notes.
    5. packageId: see (b).
    6. organizationId: orgId is undefined for flow (organizationIdForMetaRead, flow declares no org override), so the org-scoped findOverlay is never called and only the environment-wide row is read, before and after. Unchanged; right. Pinned: an org-scoped row under a shipped name answers the loader's body, under an unshipped name answers nothing (unit case 9, dogfood case 7).
    7. Every other type: both predicates return false before any lookup, so steps 1-3 and the merge are byte-identical in effect. Unchanged; right. Pinned by the view overlay control (unit case 10): the overlay is still served over the artifact, with the artifact's package stamped.
    8. Shipped flow name with no stored row, and unshipped flow name with a stored row: steps 1 and 3 answer as before (the loader's entry; the tenant's row). Unchanged; right (unit cases 7-8, dogfood cases 6 and 8).
    9. The metadata-service step (step 2) now runs for a shipped flow name where step 1 used to short-circuit it. Its source is the loaders and HMR, never the store; the list merges the same service unfiltered (:8363-8440), so the two doors cannot disagree through it; the dev measured it inert on the showcase composition. Disclosed. Right.
  • The ablation legs (A1: the stored-row half removed, 6 red; A2: the registry half removed, 2 red — the post-hydration and list-agreement cases) read as predicted from the source: without A2 the loader's body is still served until the list or the boot hydrates the row under the bare key, and from then the bare slot wins getItem (registry.ts:3888-3889). The split confirms both halves are load-bearing.

(b) The package-scoped read changed too — RIGHT under the direction's intent, and the hypothesis it departs from was not an input to this review.

  • Source: step 1's lookup (:8772-8800) tries the row bound to the requested package, then falls back to the package-less row — so an environment-wide, package-less stored row (the card's state) is found by the fallback whatever package scope the caller names; a row bound to the shipping package is found by the scoped read. On the merge-base both paths adopted the row and the merge grafted the envelope: the package-scoped spelling of the same door served the stored body (the dev's measurement, and what the source says). The REST by-name door forwards its package query into the request (rest-server.ts:6617-6639), bypassing the cache when a scope is named, so that spelling is reachable by any client.
  • The direction's intent is the agreement of the two read doors for a shipped flow name. The list applies both predicates with no package-scope condition (:8065, :8213, after listItems(type, packageId)), so a package-scoped list already serves the loader's body; a package-scoped by-name read that kept the stored body would be the same disagreement one spelling over, on the same door. Including it is the direction, not an extension of it. The row's package binding and the body's stamps decide nothing in either spelling (unit case 5, both bindings, both spellings), which is rule 1 of automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 applied by name.
  • Pinned on both suites (unit case 4; dogfood case 3). Disclosed as Deviation 2 with the measurement. Right.

(c) Confined to getMetaItem, away from PR #20959's regions — RIGHT.

(d) scripts/engine-double-contract.pinned.json, one generated row — RIGHT, compelled by the gate, and the row is what --write produces.

  • Compelled: the new unit pin's engine double carries a findOne (the by-name read's step 1 calls engine.findOne), routed through assertEngineFindOnePredicate. The gate has a findOne slice (check-engine-double-contract.mjs:401-402), and its RETAINED invariant enumerates every pinned (file, verb) in the ledger: a newly pinned double that the ledger does not record reds the gate (:2794), and the prescription is --write (:2685). The alternative — leaving the double unguarded — reds the PINNED invariant instead and would need a hand-written DEBT entry in the shrink-only baseline, which is the worse of the two. The claim asked for the unit pin; the pin needs the row.
  • Exact: --write serialises { $comment, entries: census } with two-space indentation and a trailing newline (:3387-3390), the census sorted by file then verb with localeCompare (:2627). At the head the row { file: protocol.flow-by-name-shipped-name.test.ts, verb: findOne, pinned: 1 } sits between protocol.dropped-fields.test.ts / update and protocol.flow-org-override-closed.test.ts / delete, which is its sorted position; the entry count moves 832 → 833 (one row, no loss); the file keeps its trailing newline; the $comment is unchanged. One engine double, one findOne, pinned: 1 is the count. Lint & Repo Gates, which carries check:engine-double-contract, is the byte-exact arbiter (the check-run paragraph). Disclosed as Deviation 1. Right.

(e) The changeset .changeset/20946-by-name-flow-read-shipped-name.md, @objectstack/metadata-protocol patch, Clause-②: no — RIGHT; every sentence is delivered at the head, and the discipline holds.

  • Sentence by sentence: flow is Regime C with no overlay read path — ADR-0126 §2 D1, quoted above. The list and the execution view already serve the package's flow for a shipped name (automation: at kernel:ready the 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) — true since 75519e1c0a, both faces through readFlattenedMetaItems. The by-name read did not, and served a stored flow of that name marked as the package's — true of the merge-base (the graft at :8980-8983 over the adopted row). So the two doors answered two different flows for one name — true, the card's finding. It now applies the same rule through the same checks — true, (a). For a shipped name it serves the package's flow, with or without a package scope, whatever the stored flow's binding or markings say — true and pinned, (a) and (b). The stored flow is not deleted, rewritten or refused; it stays in the store and the engine still reports it as shadowed at startup — true; nothing in the diff writes, and the dogfood receipt case pins armed package / shadowed runtime. Pending drafts, unshipped names, organization-scoped rows and every other type are read as before — true, (a) paths 3-4, 6-8.
  • Package and level: @objectstack/metadata-protocol is released (17.5.0, not private); a bug fix in a released package takes patch (AGENTS.md Post-Task Checklist 3); no accept set moves and no public member is added, so Clause-②: no is the right declaration and no ADR-0087 disposition is owed. Check Changeset is green on this head, so the note is well-formed and no foreign note is corrected.
  • Disclosure, across the changeset, the PR body, the report and every added test title: doors, roles, codes and statuses only; the stamps, the binding and the envelope are named abstractly; no request-body, header or field spelling; no seeding recipe (the PR body says a row "was placed at rest under a shipped flow name" between two boots). The dogfood pin's code is the one place the placement is spelled, as a cold-boot pin must; the claim's discipline names the PR body, changeset and test title, not test bodies — 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. No model identifier in the six commits (the model-free trailer pair on each authored commit), the PR body (session-URL footer) or the changeset; no tracker number in runtime prose (the new code carries ids in comments only).

Surface inventory: no route, schema, query set, status code or exported signature changes; one method's served answer moves for exactly the shipped-flow-name-with-stored-row case, in both package-scope spellings; 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

  • Deviations, all four answered: (1) the pinned-ledger row is compelled and exact, ① (d). (2) the package-scoped spelling is inside the direction's intent, ① (b). (3) two clean merges of origin/main; fix(metadata-protocol)!: refuse a flow saved into a package this deployment has not installed (#20863) #20959 landed between them in disjoint regions, ① (c); the net diff is what the file list says. (4) the harness reminder's model-named trailer was not used and AGENTS.md's model-free pair was — that is the rule, not a deviation (AGENTS.md: a harness-written trailer is reporting, and the pre-push hook refuses a model identifier in the pair).
  • Open question 1 (the layered read door) → A, confirmed, with one widening of the reach for the seat's card.
    • Class b, confirmed from source at the head: getMetaItemLayered computes its effective layer as overlay-wins — effectiveBase = overlay !== null ? fold(overlay) : code (:9378-9381, the effectiveBase binding the report names) — while its docblock (:9042-9044) states the effective layer is "what getMetaItem would return", and two later comments restate that sentence as the contract _diagnostics is computed from. After this PR getMetaItem answers the loader's body for a shipped flow name with an environment-wide stored row and the layered read's effective layer answers the stored body: the docblock is false for exactly that name, and ADR-0126 §2's "never an overlay read path" is breached on that door, with the lock and provenance flags derived from code ?? overlay naming the package. Reach, confirmed: GET /api/v1/meta/flow/NAME/layers (rest-server.ts:6125) and the deprecated layers query flag (:6283-6331), both through the one layered chain, and the runtime dispatcher's answerMetaLayered (meta.ts:745-800), which serves both spellings on that transport. Measured by the dev as unchanged before and after this PR on the cold boot. Pre-existing on the merge-base; this PR changes nothing that door serves.
    • Wider than the report names, for the seat's card: the published-snapshot door GET /api/v1/meta/flow/NAME/published (rest-server.ts:8270-8425) and its runtime-dispatcher twin (meta.ts:1129-1135) read getMetaItemLayered and, when the overlay layer is present, serve that layer AS the response — for a shipped flow name with a stored row, the stored body. Same class, same remedy shape (the overlay layer is not an overlay for a Regime C name the loader ships), one more door. Source reading only, not measured.
    • Own card (A) is right, not this PR: the claim's file surface is getMetaItem only with "stop on a breach outside these"; the direction's interim names the by-name read and the agreement of the two read doors; the layered read has its own docblock contract, its own consumer (the Studio diff tab) and its own pins to write, and the published door adds a second consumer. Folding it (B) would breach the claim and rewrite the PR; parking it on feat(metadata-core,metadata-protocol,objectql,plugin-security): the sys_metadata family goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 (C) parks a measured security-family finding on a card that is pm:blocked. Prime Directive chore: version packages #10 files a contract violation with measured reach — the seat files it, naming both doors. Not blocking this PR: every direction pin is delivered, and the disagreement count does not rise (before: by-name and layered agreed with each other against the list; after: layered alone).
  • Open question 2 (the pending 20913 note's last sentence) → A, confirmed: no correction is owed. The sentence "The by-name read, GET /api/v1/meta/flow/:name, is not changed." is true as that PR's own delta — PR 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's diff left getMetaItem untouched — which is what a changeset states; this PR's note states the moved case in the same release, under its own card number, for the same package. That is the reading 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 applied to the 20864 note's bullet 5 ("still true as that PR's own delta … the 20913 note is what states the moved case in the same release, so no further correction is owed"), and the two cases have the same shape: a sentence about what one PR did not move, followed in the same release by the note that moves it. It differs from the clause 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 DID correct, which stated a RULE ("a flow authored in the deployment wins over the packaged flow") that the later PR made false as a rule. A deliberate correction here (B) would red Check Changeset by design and edit outside the claim's file list for a sentence whose correction sits beside it in the compiled entry. Check Changeset is green on this head, consistent with A. The seat may still choose to correct it later as a docs-only act; nothing in this record requires it.
  • The second out-of-scope finding (the protocol.ts ADR anchor): RIGHT to note, not compelled, carrier none. The anchor already lists ADR-0126 (added by PR 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), so check:adr-anchors has nothing new to reconcile; its invariant paragraph names the hydration and the flattened list ("both faces") and is still true at the head. It does not name the by-name read, so it is incomplete rather than false. A by-name clause on the next touch of that anchor is the right carrier; a one-paragraph edit the seat may also ask for on this PR without a re-review, since the anchor is not a governed surface and the gate reads ids only.
  • Named by this review, not by the dev:
    1. The published door and its dispatcher twin widen open question 1's reach (above); for the seat's card.
    2. Stale prose, no defect class, carrier none: step 1's own comment in getMetaItem ("Per ADR-0005 (revised), org-scoped row wins; env-wide row is the fallback") and SchemaRegistry.getItem's comment ("A bare-key entry … intentionally shadows the packaged composite item — ADR-0005 overlay precedence") now describe every type but a shipped flow name; the same family 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 finding 2 noted for the collision log line.
    3. The cold-boot pin boots the showcase twice in the isolated dogfood project (its file is not on the shared-showcase roster, so it takes the glob project), 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 pin did. Cost, not correctness; the gate's shards are green.
  • The check-run picture above: every required context converged green and no run is red.

Implemented-by: claude/issue-20946-by-name-flow-read
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 01:09
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 01:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 25f2e64 Oct 1, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20946-by-name-flow-read branch October 1, 2026 01:43
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts the loader's body as the effective layer, as the by-name read and the list do (objectstack-ai#21002) (objectstack-ai#21043)

Part of objectstack-ai#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 objectstack-ai#20946 and objectstack-ai#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 objectstack-ai#20942
introduced and PR objectstack-ai#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

- Triage direction `5923270373`: "In `getMetaItemLayered`, the effective
layer for a shipped flow name is the loader's body, decided by the
predicate PR objectstack-ai#20942 / objectstack-ai#20994 put on the list and the by-name read. ⛔ No
fourth precedence path." And: "The stored row appears **as a shadowed
layer**, with its own provenance, never as the effective layer under the
package's lock flags."
- ADR-0126 §2 (`flow` is Regime C): "⛔ Never silent override, never an
overlay read path". ADR-0131 D6: managed definitions are sealed.
- objectstack-ai#20761's ruling `5904938166`, rule 1: a body's package-provenance
stamps are display only.
- The method's own docblock, and the spec's description of the layered
response, say the effective layer is what the ordinary by-name read
returns. For a shipped flow name that is now true.

What becomes of the stored rows themselves (keep, refuse, migrate)
belongs to objectstack-ai#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 objectstack-ai#21002.
objectstack-ai#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 objectstack-ai#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.

- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2` (the whole package), at `39ed9ac48a`: 196 files passed,
3 skipped; 2905 tests passed, 19 skipped.
- `pnpm --filter @objectstack/metadata-protocol run typecheck`: exit 0.
`tsc --listFiles` includes the new unit pin.
- Dogfood, `vitest run` over four files: the new pin, PR objectstack-ai#20994's
`flow-shipped-name-by-name-read`, PR objectstack-ai#20942's
`flow-shipped-name-stored-row-boot` and `flow-provenance-server-held`. 4
files, 36 tests passed. The metadata-protocol `dist` carries the fix:
the guard's text is in 2 built files.
- `pnpm --filter @objectstack/dogfood run typecheck`: exit 0.
`--listFiles` includes the new pin.
- **Red before:** on the `origin/main` build of metadata-protocol, the
layered door's effective layer for the subject was the stored body. The
table above shows this.

**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 objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

2 participants