Repository navigation
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
Conversation
…ts the loader's body as the effective layer For a flow name the loader ships, getMetaItemLayered now decides its effective layer with the stored-row predicate the list and the by-name read already call (isShippedFlowName): the code layer is effective, and a stored row of that name stays in the overlay layer as a shadowed layer of its own scope. Every other type, and a flow name no package ships, keeps overlay-wins. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…name agree across a cold boot Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…e engine-double ledger row its unit pin needs Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0637601f03daa6b01ba012f9cdd69dec42a3ec0d && git checkout 0637601f03daa6b01ba012f9cdd69dec42a3ec0d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b253fadfb7d1cb59448b1d7af8172e82aa245591 39ed9ac48a71c828ed30ef77711c1707dc6dd668 && git checkout -B drift-repro b253fadfb7d1cb59448b1d7af8172e82aa245591 && git merge --no-ff 39ed9ac48a71c828ed30ef77711c1707dc6dd668
node scripts/docs-audit/affected-docs.mjs --json b253fadfb7d1cb59448b1d7af8172e82aa245591
|
Contract reviewServed-tier: This is the record of record for PR #21043 at Inputs:
Check-runs on Mergeability: Disclosure is kept at the card's level: doors, roles, codes and statuses. The body's package-provenance stamps, the row's package binding, the tenant marker and the artifact's protection envelope are named abstractly here; the three layers are named by the method's own layer names, as the card and the precedent record name them; no request-body, header or field spelling appears, and no seeding step is written. ① Derived judgments(a)
(b) The registry-half predicate is not called — RIGHT; the branch would be unreachable, verified from source.
(c) The pins — RIGHT; they hold the direction's three pins and the controls, and they red without the fix as reported.
(d)
(e) The changeset
Surface inventory: no route, schema, query set, status code or exported signature changes; one method's effective layer and the flags' referent move for exactly the shipped-flow-name-with-stored-row case, through the three doors that read it; the published-snapshot door and its dispatcher twin are untouched; one ② Semver levelThe PR body's line 2 reads
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…flow for a shipped flow name with a stored row, as the layered read does (objectstack-ai#21002) (objectstack-ai#21116) Fixes objectstack-ai#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 objectstack-ai#21002, as triage ruled in `5924438659`: option A, scoped by the decision, not by type. The first half, the layered read, landed in PR objectstack-ai#21043.⚠️ This body follows the objectstack-ai#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 `Pick`ed 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: - `isShippedFlowName` changes from `private` to public. Its body is unchanged. - A docblock paragraph names the doors that ask it. - `getMetaItemLayered`'s effective-layer decision (PR objectstack-ai#21043) is not touched. **`.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 objectstack-ai#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. - The rule `check-changeset-no-major.mjs` quotes says a purely additive widening of a published package's public surface takes at least `minor`. - The two in-family precedents read it the same way. PR objectstack-ai#20817 made `packagedBaseRefusal` public, and PR objectstack-ai#20853 made `tenantAuthoredWriteRefusal` public. Both were declared `Clause-②: yes (widening)` with `@objectstack/metadata-protocol` at `minor`. - No wire shape moves. No request key, response key or accept set changes. `rest` and `runtime` stay `patch`: `MetaDomainProtocol` is not exported from the runtime package entry. 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](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #21002
Clause-②: no
What
For a flow name the loader ships from a managed package, the layered read (
GET /api/v1/meta/flow/NAME/layers, and the deprecated layers flag on the by-name door, which uses the same helper) now reports the loader's body as the effective layer. That is the body the by-name read (GET /api/v1/meta/flow/NAME) and the flow list (GET /api/v1/meta/flow) have answered for that name since #20946 and #20913. A stored row of that name is still reported, as a separate shadowed layer of its own scope, and is never the effective layer under the package's lock and provenance flags.getMetaItemLayeredinpackages/metadata-protocol/src/protocol.tsnow decides its effective layer with the stored-row predicate PR #20942 introduced and PR #20994 reuses,isShippedFlowName, judged by name. It adds no precedence rule of its own.getMetaItemLayeredmoves inprotocol.ts: the effective-layer binding and its docblock (+29 / -2 there).lookupArtifactItem, blind to tenant-authored rows) before the registry's bare slot, and a shipped name is one that set holds by definition. SoisStoredFlowEntryOfShippedNamewould add an unreachable branch.Why
5923270373: "IngetMetaItemLayered, the effective layer for a shipped flow name is the loader's body, decided by the predicate 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 / fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #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."flowis Regime C): "⛔ Never silent override, never an overlay read path". ADR-0131 D6: managed definitions are sealed.5904938166, rule 1: a body's package-provenance stamps are display only.What becomes of the stored rows themselves (keep, refuse, migrate) belongs to #15206. This PR does not decide it.
Why this PR is
Part of: the published-snapshot door does not followThe 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.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.200with the stored body, both before and after this change.The dispatch said to stop there and report, and not to edit that door in this card. The measurement and the options are in the report on #21002. #21002 remains open for that half.
Repro, before and after
Showcase composition on a database file, cold boot. A stored row is at rest under a shipped flow name, with a body that can be told apart from the loader's. There is also an organization-scoped row under a second shipped name, and an environment-wide row under a name no package ships.
origin/main2f2fa11d75/meta/flow/NAME/layers, shipped name with a stored row: the effective layerGET /meta/flow/NAMEGET /meta/flow, the entry for NAMEGET /meta/flow/NAME/published501 NOT_IMPLEMENTED(this kernel has no code/package store)Pins
packages/metadata-protocol/src/protocol.flow-layered-shipped-name.test.ts, 9 cases. It reuses the registry double of PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994's unit pin: the realSchemaRegistrykey shapes, itsgetItemprecedence (the bare slot first) and its artifact lookup.packages/qa/dogfood/test/flow-shipped-name-layered-read.dogfood.test.ts, 8 cases, a new file.flow-shipped-name-by-name-read.dogfood.test.tsandflow-provenance-server-held.dogfood.test.tsare not touched.Verification, at head
39ed9ac48aprotocol.tsand both pins are byte-identical between5cdb27e44dand39ed9ac48a. The last commit adds only the changeset and the ledger row.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2(the whole package), at39ed9ac48a: 196 files passed, 3 skipped; 2905 tests passed, 19 skipped.pnpm --filter @objectstack/metadata-protocol run typecheck: exit 0.tsc --listFilesincludes the new unit pin.vitest runover four files: the new pin, PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994'sflow-shipped-name-by-name-read, 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'sflow-shipped-name-stored-row-bootandflow-provenance-server-held. 4 files, 36 tests passed. The metadata-protocoldistcarries the fix: the guard's text is in 2 built files.pnpm --filter @objectstack/dogfood run typecheck: exit 0.--listFilesincludes the new pin.origin/mainbuild 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 throughscripts/ablation-replace.mjsand deleted the predicate clause from the effective-layer binding. The anchor hit once, and the blob changed from6056394ec7a6toed01c7ddc863../protocol.jsfrom source, no rebuilddist, rebuilt after the mutationscripts/ablation-dist-preflight.mjsfound the guard absent from all 24 built files after the mutated build.git diff HEADis empty. After the restored build, the guard is present in 2 built files and the working tree is clean against HEAD.Derived gates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsprinted 74 commands for this tree at39ed9ac48a. All 74 were run, each exit code captured before any pipe.--ranreconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 unrun.check:dual-build-cjs-loadsexited 3,PREREQUISITE NOT MET. Eight packages outside this diff had nodist/in this fresh worktree. After building those eight (all turbo cache hits), it exited 0.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-exportsandcheck-dts-references --self-test.origin/mainhas not moved since the branch point2f2fa11d75.Lint, a proven narrowing of
pnpm lint(the repo-wide run is CI's), at39ed9ac48a:.tsfiles. The.mdand.jsonfiles answer "File ignored because no matching configuration was supplied."--format json: 5 results. The 3 linted files have 0 errors and 0 warnings.eslint.config.mjsnever enables type-aware linting. All sevenparserOptionsblocks areecmaVersionandsourceTypeonly, with noproject. The only other files the config reads arescripts/slot-lookup-baseline.jsonandscripts/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:
Deviations
Part of #21002, not a closing line. The dispatch named a closing line. The published-snapshot door half of the card is measured unresolved and is now a decision for the seat, so this PR does not close the card. The seat can rewrite the first line if it rules that half out of the card.scripts/engine-double-contract.pinned.json, one generated row. The new unit pin's engine double has afindOne, socheck:engine-double-contractrequires the coverage ledger to learn the file, through--write. The diff is exactly that one row. PR fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) #20994 has the same precedent.Acceptance notes
flowfirst);flowdeclares no org override;501 NOT_IMPLEMENTEDfor 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