Skip to content

fix(rest,runtime): the published-snapshot doors answer the package's flow for a shipped flow name with a stored row, as the layered read does (#21002) - #21116

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21002-published-door-effective
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21002-published-door-effective

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21002
Clause-②: yes (widening)

The published-snapshot read of a flow name a managed package ships now answers the package's flow when a stored row of that name is at rest. This holds on the REST route and on its runtime-dispatcher twin. This is the second half of #21002, as triage ruled in 5924438659: option A, scoped by the decision, not by type. The first half, the layered read, landed in PR #21043.

⚠️ This body follows the #20761 family's disclosure discipline. It talks about doors, roles, codes and statuses only. It has no request body, header or field spelling, and no seeding steps.

What changed

packages/rest/src/rest-server.ts, the GET /meta/:type/:name/published handler:

  • It still reads the layered answer first. When that answer has a stored layer, the door now asks the protocol's isShippedFlowName about the answer's own type and name.
  • When the predicate holds, the layered read has put the loader's body over the stored row. The door then serves the effective layer, which is the loader's body.
  • In every other case it serves the stored layer, exactly as before. A protocol that brings no such predicate also keeps today's answer.

packages/runtime/src/domains/meta.ts, the dispatcher twin of that route:

  • The same change, in the same place.
  • MetaDomainProtocol gains the predicate as an optional member. It is Picked from ObjectStackProtocolImplementation, not restated, so a rename at the producer is a compile error here. This is the same move domains/automation.ts makes for packagedBaseRefusal.

packages/metadata-protocol/src/protocol.ts, the declared cross-lane surface the claim allows:

.changeset/21002-published-door-shipped-flow.md: @objectstack/metadata-protocol minor, @objectstack/rest patch, @objectstack/runtime patch.

How the doors learn the decision

There is one decision point, the predicate PR #21043 already calls.

  • No per-type list, no new response key, no fourth precedence path, and no second copy of the rule.
  • Each door asks the protocol's own predicate about the type and name the layered answer reports. The layered answer's type is the canonical singular, so the predicate gets exactly the arguments getMetaItemLayered used.
  • object is never named. Its effective layer differs from its stored layer by folding and governance, not by this decision, so it is served byte-identically.

The predicate was private to the protocol class. A door in another package could not reach it except by copying the rule (the flow-only scoping plus packagedArtifactOwner), which the ruling forbids. So making it public is the minimal reachability change.

Clause ② reads yes (widening), not the claim's no

The claim's own reading said the dev re-reads the line against the real diff. The real diff adds one public member to a class @objectstack/metadata-protocol exports, so its published declaration grows.

If the seat rules this no, the revert is two lines: this body's second line, and the changeset's minor back to patch with its own Clause line.

Reproduction

Showcase composition on a database file, cold boot, signed-in admin. The stored rows were written on a first boot and read on the second. The base is 63d1a7c378.

case door before after
shipped flow name, stored row REST 200, the stored body 200, the loader's body
shipped flow name, stored row dispatcher twin 200, the stored body 200, the loader's body
flow name no package ships, stored row REST and twin 200, the stored body unchanged, same bytes
object, published stored layer REST and twin 200, the stored layer unchanged, same bytes

"Same bytes" means the SHA-256 of the served document is equal before and after, on both doors. The dispatcher figures come from a throwaway probe that drove HttpDispatcher in-process over the booted kernel. The probe was deleted.

Pins

  • REST unit, packages/rest/src/meta-published-overlay.test.ts: 5 new cases. They use the real protocol and the file's own engine double, with a registry that ships one flow from a package.
    • A shipped name with a stored row answers the loader's body, equal to the layered effective layer.
    • The plural type spelling reaches the same decision.
    • Controls: a flow name no package ships keeps its stored row. An object's published stored row is served as stored, and its effective layer is shown to differ. A protocol without the predicate keeps the stored row.
  • Runtime unit, packages/runtime/src/domains/meta-published-runtime-publish.test.ts: the same 5 cases, through the real HttpDispatcher.
  • Dogfood, the new file packages/qa/dogfood/test/flow-shipped-name-published-door.dogfood.test.ts: 5 cases, showcase, cold boot.
    • A store check, plus the layered read putting the loader's body over the stored row.
    • The REST door answers the loader's body.
    • The REST door's body equals the layered effective layer.
    • Controls: a flow name no package ships keeps its stored body. An object's published stored layer is served unchanged, and its effective layer is shown to differ.
  • No existing flow-shipped-name-*.dogfood.test.ts file is edited. Neither unit file gains an engine double, so scripts/engine-double-contract.pinned.json is untouched.

Why the dispatcher twin is not in the dogfood file. The verify harness mounts no dispatcher /meta catch-all. That route is reached only on hosts that mount @objectstack/hono's catch-all, so in this composition the twin is not served at all. Driving HttpDispatcher in-process from the dogfood package means importing runtime source. check:test-source-alias then refuses four new dist-resolved imports for the dogfood package (metadata-protocol, observability, rest, service-datasource). Its remedy is to alias them to source in the dogfood vitest config, which is outside this claim's file surface and would change every isolated dogfood test's resolution. So the twin is pinned at the unit level, with the real protocol and the real dispatcher.

Ablation

The fix was committed first. Both mutations went through scripts/ablation-replace.mjs, replacing the predicate clause with a constant false. Each leg is shown below.

  • REST door (rest-server.ts), at head 292cc60f45:
    • The anchor went from 1 to 0 hits, and the blob from a97cfde7c227 to 418bc95a799c.
    • Unit (source-resolved): 2 failed (shipped name, plural spelling), 12 passed.
    • Rebuild of @objectstack/rest. Then ablation-dist-preflight --absent found the predicate call absent from all 6 built files.
    • Dogfood (dist-resolved): 2 failed (the REST door, and its equality with the effective layer), 3 passed (the store check and both controls).
    • Restore: blob equals HEAD a97cfde7c227 and git diff HEAD is empty. After the rebuild, the preflight found the call present in 2 built files, and the tree was clean.
  • Dispatcher twin (meta.ts), at head 292cc60f45:
    • The anchor went from 1 to 0 hits, and the blob from 0c4ddceecbb8 to 1e10bb639fc9.
    • Unit (source-resolved): 2 failed (shipped name, plural spelling), 8 passed.
    • Restore: blob equals HEAD and git diff HEAD is empty.

Verification

All at head 292cc60f45 unless a line names another.

  • Unit pins: meta-published-overlay.test.ts 14 passed (9 existing, 5 new). meta-published-runtime-publish.test.ts 10 passed (5 existing, 5 new). Both at 92f242cee4, and neither file has changed since.
  • Whole packages:
    • @objectstack/rest: 259 files, 5035 passed, 143 skipped, at b973faeca2.
    • @objectstack/runtime (--project local): 297 files, 4254 passed, 5 skipped, at b973faeca2.
    • The only later change in either package is the reworded docblock and one dropped fixture key in its unit file. Both files were re-run green after it.
    • @objectstack/metadata-protocol: 196 files passed and 3 skipped; 2930 tests passed and 19 skipped, at 92f242cee4.
  • Dogfood: the new file, 5 passed.
  • Typecheck: metadata-protocol, rest, runtime (including check:test-typecheck) and dogfood all exit 0. tsc --listFiles counts each new or edited test file once in its own program.
  • Gates: dispatch-gates --repo objectstack-ai/objectstack --commands derived 68 families. All 68 were run, each exit code recorded before any pipe, and every one is 0. --ran reports 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.
    • The first pass, at 92f242cee4, had two non-zero exits.
    • check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET: 8 packages outside the diff had no dist. They were built (41 of 41 turbo cache hits).
    • check:test-source-alias exited 1 on the dogfood dispatcher leg, which was then removed. The reason is under Pins.
  • Lint, a proven narrowing:
    • Population, from eslint's own config: 6 of the 7 touched paths are linted. The changeset is ignored, with no matching configuration.
    • Count, from --format json with the pnpm lint flags: 6 results, 0 errors, 0 warnings.
    • Invariance: there is no type-aware linting. Every parserOptions block is ecmaVersion and sourceType only. The config reads only two baseline JSON files, and this diff touches neither. So no untouched file's verdict can move.
  • Not measured locally, declared to CI: the Test Core shards, the full Dogfood Regression Gate, Temporal Conformance, Build Core, the type-check lanes, and the runtime repo test project.

Acceptance notes

  • Unchanged background fact, per the review 5924388874: in the showcase composition, a shipped flow with no stored row answers 501 NOT_IMPLEMENTED on the published door. That kernel has no code/package store. This PR does not change that path.
  • Not merged with origin/main: 9 commits landed since the base. None of them touches the 7 files here. PR CI tests the merge ref.
  • Read, not edited: getMetaItemLayered and the predicate's body.

Generated by Claude Code

claude added 5 commits October 1, 2026 06:12
…he layered read decided it for a shipped flow name (#21002)

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…with the stored-row and object controls (#21002)

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
… a field spelling (#21002)

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…r this composition mounts (#21002)

The dispatcher twin's /meta domain is not mounted by the verify harness, and
driving HttpDispatcher in-process from this package imports runtime source
whose dist-resolved imports check:test-source-alias refuses. The twin is
pinned with the real protocol and the real dispatcher in the runtime unit
test instead.

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 3 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/runtime, touching 8 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 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))
  • 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))
  • content/docs/permissions/system-context.mdx (via handleMetadataRequest (symbol, a top-level function))

⛔ 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 /meta/:type/:name/published (route, a path literal in a comment in ObjectStackProtocolImplementation))

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
  • 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
  • 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 — 38 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 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1

⚠️ 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 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 → 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: 292cc60f459f59e439bc8af4cd9d5a52a3fdbb84
Local-runs: none

This is the record of record for PR #21116 at 292cc60f45, card #21002 (the layered read of a shipped flow name reporting a stored row as the effective layer, and the published-snapshot read serving that layer), the published-door half, under triage's ruling 5924438659 ("A, scoped by the decision, not by type": when isShippedFlowName decided the effective layer, the published doors serve that effective layer; every other case serves exactly what it serves today; one decision point, the predicate PR #21043 already calls; no per-type list, no new key, no fourth precedence path; object byte-identical without being named; three pins), the seat's claim 5925672780 (file surface: the two published doors, protocol.ts only if the predicate must be made reachable, pins in the rest and runtime unit tests plus one cold-boot dogfood pin in a NEW file, one changeset at patch, the dev re-reads Clause ② against the real diff, stop and report on a fork), the newest os-dev-report 5926967159 (done, two open_questions) and the seat's answers 5927002103 (A and A).

Inputs:

Check-runs on 292cc60f45, 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, check:cross-package-test-inputs, check:test-source-alias, check:changeset-no-major, check:adr-0087-registration, check:adr-anchors and the rest of the check:* family over this diff), TypeScript Type Check (and its four sub-jobs, source gates, consumer gates, debt ledger, workspace, so the Pick on the runtime side and the structural annotation on the REST side compile under the workspace programs), Test Core (and all six shards, which run the ten new unit cases), 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 (The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same issue, No other open PR may claim the same single-writer path), Check Documentation Links and the labelers are success. No run is red, so there is no red to compare against origin/main.

Mergeability: git merge-tree --write-tree origin/main refs/review/pr-21116 (origin/main at 12fbb2fac8, seventeen commits past the merge-base) is clean, tree de7cfa3458, exit 0; the same command from a throwaway bare clone sharing the object store with no merge driver registered (AGENTS.md's probe) answers the same tree with no conflicted path. The two coincide by construction: none of the seven paths carries a merge=os-regen attribute (git check-attr merge answers unspecified on all seven). Of the seventeen commits, exactly one (8368f1c005, PR #21084) touches one of the seven paths, rest-server.ts, at the import line the claim's serial-constraints paragraph named as disjoint from the door; the other six paths and the three source files this review read are untouched on main since the base.

Disclosure is kept at the card's level: doors, roles, codes and statuses. The loader entry's package stamp, the stored 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; no request-body, header or field spelling appears, and no seeding step is written.

① Derived judgments

(a) Both published doors serve the layered answer's effective layer exactly when a stored layer is present and isShippedFlowName holds for the answer's own type and name, serve today's bytes in every other case, name no type, add no key, copy no rule, and read the decision at the layered read's own normalisation — RIGHT, verified from source at the head and the base.

  • The REST door (rest-server.ts): the base assigned publishedOverlay = layered.overlay inside if (layered?.overlay !== undefined && layered?.overlay !== null) (:8496-8497 at the base). At the head the same if (:8496) now holds a ternary (:8512-8516): the protocol in hand is read through a local structural annotation carrying one optional method, and publishedOverlay is layered.effective when that method is a function AND answers true for layered.type / layered.name, else layered.overlay. Nothing else in the handler moves: the layered call and its arguments (:8485-8495), the #5532 catch classification (:8537), the serve-outside-the-try (:8540), the snapshot fall-through and its 501 NOT_IMPLEMENTED are byte-identical to the base.
  • The dispatcher twin (meta.ts): the base's publishedOverlay = layered.overlay (:1134-1135 at the base) is the same ternary at the head (:1155-1158) inside the same if (:1145), with the twin's own swallow-and-fall-through catch unchanged and servePublished reached as before (:1162).
  • ONLY when a stored layer is present and the predicate holds: the assignment lives inside the overlay-present branch, and effective is taken on the predicate's true arm alone. With no stored layer publishedOverlay stays undefined and the snapshot path runs as before; with a protocol that brings no such member the typeof guard falls to the stored layer; every type other than flow and every flow name no package ships take the stored layer because the predicate answers false for them (protocol.ts:14644, the canonical-type gate, before any lookup). The PR body's "every other case serves exactly today's bytes" is true of the source.
  • No per-type list, no new response key, no copy: neither door names a type (the object in each door's comment is an illustration, not a condition); the served document is the layered answer's own layer, so no response schema moves; the rule's two halves (the canonical-flow gate and the loader's-set lookup, :14644-14646) exist once, in the predicate the doors call. Ruling bullets 2 and 3 are met literally.
  • object byte-identical by construction: for an object the predicate returns false at :14644, so the ternary's false arm hands back layered.overlay, the very value the base assigned. object's effective layer differs from its stored layer by folding and governance (foldObjectExtendersFromRegistry :7339-7341, governServedItem :379-380, materializeFromRegistry :7534-7535, each returning its input for every other type), which is why the controls below discriminate.
  • Same normalisation as the layered read, and stronger than "the same fold": getMetaItemLayered folds its request first (request = canonicalizeMetaRequestType(request), :9122; that function replaces type with the canonical singular and leaves name alone, :315-329), calls this.isShippedFlowName(request.type, request.name) on the folded request (:9399), and returns type: request.type, name: request.name (:9447-9448; the spec describes type as "canonical singular", protocol.zod.ts:492). Both doors call the predicate with layered.type and layered.name, the identical strings the layered read's own call used, after passing the raw URL segment into the layered read exactly as before. The plural spelling therefore reaches the same decision (pinned, unit case 2 in both files), and the predicate's own PLURAL_TO_SINGULAR fold (:14644) receives the canonical singular.
  • What effective is when the predicate holds: effectiveBase = code (:9399-9401), and code is the metadata-service copy or lookupArtifactItem(type, name, undefined) (the doors pass no package scope) — the same no-scope lookup packagedArtifactOwner asked to answer true (:14686), so the entry the predicate found is the entry the code layer reads and effective cannot be null on that arm; governServedObject (:7572-7574) is the identity for flow, and the per-type credential redaction (:9444) is applied to all three layers and was before. So the door serves the loader's body, the body the layered read reports as effective, which is what the pins assert by equality. One standing approximation, pre-existing and not moved: effective is served raw of decorateMetadataItem's decoration (the _diagnostics key is a sibling of effective, not inside it), the same reading the 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 record made of the layered door; the door still runs the shared read gate and the object mask over whatever it serves (servePublished, unchanged).

(b) protocol.ts: isShippedFlowName is made public with its body unchanged, the docblock paragraph states what is true, and no other member is exposed — RIGHT.

  • The diff to protocol.ts is +10/−1: the one removed line is the method's signature with private, the one added code line is the same signature without it (:14643), and the nine other added lines are the docblock paragraph. The body (:14644-14646) is byte-identical to the base and to origin/main; its four callers are untouched (the list :8207, the by-name read :8699, the layered read :9399, the registry-half predicate :14659). isStoredFlowEntryOfShippedName stays private; packagedArtifactOwner was already public (PR fix(automation): which flows are packaged is the loader's fact, and every flow written through an authoring door is tenant-authored (#20761) #20853). No other visibility moves.
  • The paragraph, sentence by sentence: "PUBLIC so the published-snapshot doors can ASK it, never re-derive it" — true, (a). "getMetaItemLayered decides its effective layer with this predicate" — true, :9399. "GET /meta/:type/:name/published (the REST route and its dispatcher twin) reads that layered answer: when a stored row is present and this predicate holds for the answer's type and name, the effective layer — the loader's body — is what the door serves, and in every other case the door serves the stored row as before" — true, (a). "One decision point for the three reads; the doors hold no copy of the rule" — true as the paragraph's own count (the layered read and the two doors); the predicate also serves the list and the by-name read, which the docblock's earlier paragraphs name. Not false.
  • The class is exported from the package entry (metadata-protocol/src/index.ts:3), so the member joins the published declaration — the widening ② grades. The runtime side reads it as a type: MetaDomainProtocol gains a Partial of a Pick of ObjectStackProtocolImplementation on isShippedFlowName (meta.ts:159-162), through a type-only import (:47) on a declared dependency (runtime/package.json:38), the move domains/automation.ts:1503 makes for packagedBaseRefusal; a rename at the producer is a compile error at the door. MetaDomainProtocol is not exported from the runtime package entry (runtime/src/index.ts has no such export), so runtime's public surface does not move.

(c) The pins — RIGHT; they hold the ruling's three cases on both doors, they red without the fix as reported, and the twin's unit-level coverage is acceptable.

  • REST unit (meta-published-overlay.test.ts:480-604, 5 new cases): the real ObjectStackProtocolImplementation over the file's own engine double, whose registry is replaced by a partial registry that ships ONE flow, the loader entry carrying its package stamp — the shape lookupArtifactItem's partial-registry fallback reads (:15036, :15053-15054, then the stamp filter) — so the real predicate, code layer and layered read run unmocked; a real RestServer with the protocol in its slot (setup, :195-207), the registered route handler invoked directly (callPublished). Case 1: a shipped name with a stored row answers 200, the loader's label, no stored marker anywhere in the served bytes, and toEqual(layered.effective), after asserting the layered answer put the loader's body over the stored row and the predicate answers true. Case 2: the plural spelling, same answer. Case 3 (control): a flow name no package ships keeps its stored row, toEqual(layered.overlay), predicate false. Case 4 (control): a runtime-published object — the test first asserts effective is NOT overlay, so a door serving the effective layer would red the last line — is served toEqual(layered.overlay). Case 5 (control): the same protocol behind a Proxy that hides the predicate from the door only (its own methods stay bound to the real instance, so the layered read still decides) answers the stored row.
  • Runtime unit (meta-published-runtime-publish.test.ts:329-459, 5 new cases): the same five through the real HttpDispatcher over a kernel double exposing the protocol (make, :173-180), handleMetadata('/flow/NAME/published', …, 'GET') and the plural path, asserting on the dispatcher's envelope.
  • Dogfood cold boot (flow-shipped-name-published-door.dogfood.test.ts, a new file, 5 cases): the showcase composition through bootStack with automation and a database file, two boots. The first boot reads two loader bodies, puts an environment-wide active stored row under the shipped name (told apart by a label and one renamed node) and one under a name no package ships (seeded from a shipped screen flow's body), and publishes an object into a writable base through the protocol's own save, draft then publish; the second boot is cold. Case 1: the store holds the three rows, the engine's own reader names the showcase package for the subject, and the layered read reports the stored node in the stored layer and the loader's nodes in effective. Case 2: the REST door answers 200, the loader's label and nodes, no stored node. Case 3: the REST door's document toEqual the layered answer's effective. Case 4 (control): the unshipped name's stored layer is env-scoped and the door answers it, toEqual(layered.overlay). Case 5 (control): the object's stored layer is non-null, its effective is NOT its stored layer (discriminating), and the door answers the stored layer. It runs in the dogfood isolated project (vitest.config.ts:259-260, the glob project), the escaping path is spelled fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url)), the form the cross-package gate recognises and the sibling pin (flow-shipped-name-layered-read.dogfood.test.ts:58) uses; the two fenced files are untouched.
  • The three cases, held on both doors: a shipped name with a stored row gets the loader's body (REST unit 1-2, runtime unit 1-2, dogfood 2-3); an unshipped name with a stored row is unchanged (REST unit 3, runtime unit 3, dogfood 4); an object overlay is byte-identical (REST unit 4, runtime unit 4, dogfood 5 — each asserts equality with the layered answer's stored layer, which is exactly the document the base served; the dev's SHA-256 equality before and after is the measurement, the pins assert the same document). The protocol-without-predicate case (unit 5 on both) pins the "including when the protocol lacks the predicate" clause of the ruling's "every other case".
  • Red without the fix, read from source: the dev's mutation replaces the predicate clause with a constant false, so the ternary always takes layered.overlay; exactly the assertions that the door serves the loader's body red and nothing else does. REST unit: cases 1-2 red, 3-5 green, the 9 existing green — the reported A1, 2 / 12. Runtime unit: cases 1-2 red, 3-5 and the 5 existing green — the reported B1, 2 / 8. Dogfood: cases 2-3 red (the door, and its equality with effective), case 1 green (it asserts the layered read, which the mutation does not touch) and the two controls green — the reported A2, 2 / 3, from a rebuilt dist with the preflight proof the body describes. All three counts read as predicted; the blob hashes and the restore proofs are the dev's, reported, not re-run here.
  • The twin's unit-level-only coverage — acceptable, and the premise behind it is true at the head. createDispatcherPlugin mounts explicit routes only: its header lists them (dispatcher-plugin.ts:806-816), its own comments say the standalone server "mounts ONLY the explicit routes here" and that the /meta catch-all belongs to @objectstack/hono (:1284-1285, :1335-1336), and nothing in that file registers a /meta path. The dispatcher's handleMetadata is reached in production only through @objectstack/hono's catch-all (adapters/hono/src/index.ts:712-739). The verify harness composes plugin-hono-server, createRestApiPlugin and createDispatcherPlugin (harness.ts:23-26, :486, :682-683), never @objectstack/hono, and injects requests through that Hono app (:713-714), so /meta/…/published there is served by the REST route alone and the twin is not served in the composition the dogfood boots. The in-process alternative the dev tried: @objectstack/dogfood's registered unaliased set (check-test-source-alias.mjs:390-398) carries neither runtime nor the four packages the report names, and the dogfood config aliases six packages to source, none of them runtime (vitest.config.ts:180 onward), so the refusal the dev reports is what that gate's design produces, and its remedy is a resolution change for every isolated dogfood test, outside the claim's surface. The twin is pinned with the real HttpDispatcher over the real protocol and reds under ablation. The seat's A (5927002103 item 2) is right; B and C are harness cards if ever wanted, carrier none.
  • The engine-double ledger: no new double; each file's existing double has its registry replaced and nothing else; scripts/engine-double-contract.pinned.json is untouched, as the report says; Lint & Repo Gates, which carries check:engine-double-contract, is the arbiter (the check-run paragraph).

(d) The changeset .changeset/21002-published-door-shipped-flow.md — @objectstack/metadata-protocol minor with Clause-②: yes (widening), @objectstack/rest and @objectstack/runtime patch — RIGHT on level, accurate sentence by sentence, and the discipline holds.

Surface inventory: no route, query set, status code or response schema changes; two doors' served document moves for exactly the shipped-flow-name-with-stored-row case, to the body the layered read already reports as effective; one private method becomes public on an exported class (the widening); one runtime-internal type gains an optional member; one changeset (minor / patch / patch); no generated artifact; no governed path.

② Semver level

The PR body's line 2 reads Clause-②: yes (widening), against the claim's no, and the changeset grades @objectstack/metadata-protocol minor. RIGHT, and the seat's answer A (5927002103 item 1) is right.

③ Boundary flags

  1. Deviations, all seven answered: (1) Clause ② yes (widening) with metadata-protocol at minor, not the claim's no / patch — right, ②; the claim invited the re-read. (2) The dogfood file pins the REST door only — right, ① (c); the ruling's "both published doors" is held by the unit pins, the twin with the real dispatcher. (3) The unit pins live in the nearest existing files on each file's own pinned double with only its registry replaced, so the ledger is untouched — right; the dispatch's row was conditional and the condition did not arise. (4) Not merged with origin/main — seventeen commits past the base at review time, nine at report time; one touches rest-server.ts at an import line the claim foresaw as disjoint; the merge-tree is clean (above); PR CI ran on the merge ref. (5) The dispatcher half of the reproduction table came from a deleted in-process probe — so the twin's standing evidence is the committed unit pin over the real dispatcher, which this record reads from source; the REST figures stand on the committed dogfood pin. Acceptable: both doors are pinned, as the ruling asked. (6) The model-free trailer pair is AGENTS.md's form, not a deviation (verified on all five commits). (7) Worktree cleanup — housekeeping.
  2. Fixes #21002 — RIGHT; the card closes. The card named two doors: the layered read, landed in 94990a29f8 (PR fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043, an ancestor of this head), and the published read with its twin, this PR. The ruling 5924438659 is delivered at the head on every clause: the effective layer is served when the predicate decided it (① a), one decision point (① a-b), no per-type list, no new key, no fourth precedence path (① a), object byte-identical without being named (① a, pinned), and the three pins on both doors (① c). The landing note 5925253077 set Fixes #21002 as the follow-up's first line; the claim 5925672780 names this branch; The card this PR closes must claim this branch and Part-of PR must not also close its card are both green. What the card does not close and nothing here claims to: the rows' fate (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, pm:blocked), and metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059 (next flag).
  3. metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059 — untouched and unaffected. Untouched: the protocol.ts diff is confined to :14633-14643 (the predicate's docblock paragraph and its signature); metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059's region, the code-layer fallback lookupArtifactItem(…) ?? registry.getItem(…) at :9255-9256, is byte-identical to the base. Unaffected: metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059 is about an UNSHIPPED name's code layer; for such a name the predicate answers false, so both doors take the stored layer exactly as before — metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059's own sentence, "the published-snapshot door is unaffected: it reads the stored layer", stays true after this PR; and this PR's unshipped controls assert equality with the stored layer, never with code, so the eventual metadata: once a stored flow row under a name no package ships is hydrated, the layered read reports it as the code layer, against the spec's "null when no artifact ships this item" #21059 fix (code: null for a hydrated unshipped row) reds none of them.
  4. Named by this review, not by the dev:
  5. The check-run picture above: every required context converged green and no run is red; there is no red to compare against origin/main.

Implemented-by: claude/issue-21002-published-door-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

2 participants