Repository navigation
fix(metadata-protocol): the by-name read of a shipped flow name serves the loader's body, as the list does (#20946) - #20994
Conversation
…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>
…package's flow 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>
📓 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:
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 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
|
Contract reviewServed-tier: This is the record of record for PR #20994 at Inputs:
Check-runs on 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)
(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.
(c) Confined to
(d)
(e) The changeset
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 ② Semver levelThe PR body's line 2 reads
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…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>
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.getMetaIteminpackages/metadata-protocol/src/protocol.tsnow calls the two predicates PR #20942 introduced for the list, and adds no precedence rule of its own: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.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
getMetaItemmoves inprotocol.ts(+36 / -1 there).Why
5920432754on metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946 (the interim): "the by-name read serves the loader's artifact for a shipped flow name, using the same predicates 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 introduces for the list and the execution view … ⛔ No third precedence path. The by-name read calls the predicate the list calls."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.
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.
origin/mainf6ccca4a44GET /meta/flow/NAME, shipped name with a stored rowGET /meta/flow, the entry for NAMEPins
packages/metadata-protocol/src/protocol.flow-by-name-shipped-name.test.ts, 10 cases. It uses a registry double with the realSchemaRegistrykey shapes, itsgetItemprecedence (the bare slot first) and its artifact lookup.packages/qa/dogfood/test/flow-shipped-name-by-name-read.dogfood.test.ts, 8 cases.flow-provenance-server-held.dogfood.test.tsis not touched.Verification, at head
07843e6889pnpm --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 --listFilesincludes the new unit pin.vitest runover 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'sflow-shipped-name-stored-row-boot,flow-provenance-server-heldandautomation-authoring-doors-durable. 4 files, 35 tests passed. The metadata-protocoldistcarries the fix.pnpm --filter @objectstack/dogfood run typecheck: exit 0.--listFilesincludes the new pin.origin/mainbuild 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 throughscripts/ablation-replace.mjs, with its anchor hit once and a blob change confirmed on disk. The unit pin imports./protocol.jsfrom source, so no rebuild was involved.Both restores were proven: the blob equals HEAD (
5d475cd667) andgit diff HEADis empty.Derived gates.
node scripts/pm/dispatch-gates.mjs --commandsprinted 74 commands for this tree. All 74 were run, each exit code captured before any pipe.--ranreconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 unrun.check:dts-closureandcheck:dual-build-cjs-loadsexited 1. Both named@objectstack/organizationsmissingdist/index.d.ts, a package outside this diff that was partially built in the shared local tree. Afterpnpm --filter @objectstack/organizations build, both exited 0.check-changeset-fixed,check-published-list-mirrors,check:authz-resolver,check:console-injection,check:error-code-casing,check:i18n-stale-fillandcheck:published-readme-exports.origin/main7fa67dada3(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):.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 (everyparserOptionsisecmaVersionandsourceTypeonly, with noproject). The only other files it 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
scripts/engine-double-contract.pinned.json, one generated row. The new unit pin's engine double has afindOne, so it routes throughassertEngineFindOnePredicate.check:engine-double-contractthen 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.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.origin/main. Neither had conflicts. The net delta againstmainis 5 files, +601 / -1.Acceptance notes
flowfirst);flowdeclares no org override..changeset/20913-flow-stored-row-shipped-name.mdends with "The by-name read,GET /api/v1/meta/flow/:name, is not changed."protocol.tsalready 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
GET /api/v1/meta/flow/NAME/layers.getMetaItemwould return". ADR-0126 §2 says "never an overlay read path".getMetaItemonly.meta flow layers effective stored row shipped name·layered read effective overlay flow regime C.Generated by Claude Code