Repository navigation
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
Conversation
… at both boot steps Red on the base: after kernel:ready the armed body is the stored row's, and the receipt renders both contenders as the package. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…oth boot steps; a stored row of a shipped flow name is shadowed, never armed The kernel:ready sync and the metadata:reloaded re-sync resolve the protocol's flow view through the same precedence decision the boot pull uses, with the engine's loader's-set reader. Within a name the loader's set holds, the loader's entry is armed (ADR-0126 section 2: a managed package's flow is sealed). The hydration registers a stored flow row without the artifact's envelope, and the flattened view serves a shipped flow name from the loader's entries alone, so the receipt reports the stored row as its own contender. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…med the body Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
… entry no longer states the retired direction; anchor ADR-0126 at the two seams Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…pins Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 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 152251076f96ea2f945b7b4f57c355edb3bbad04 && git checkout 152251076f96ea2f945b7b4f57c355edb3bbad04
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bee75cebe63889f93fa1e6dc358cd8c5f7b9a928 22d8d3593eb561c3f7e5a39bca691f2855c616e3 && git checkout -B drift-repro bee75cebe63889f93fa1e6dc358cd8c5f7b9a928 && git merge --no-ff 22d8d3593eb561c3f7e5a39bca691f2855c616e3
node scripts/docs-audit/affected-docs.mjs --json bee75cebe63889f93fa1e6dc358cd8c5f7b9a928
|
Contract reviewServed-tier: This is the record of record for PR #20942 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 tenant marker and the artifact's protection envelope are named abstractly here, and no seeding step is written. ① Derived judgments(a) One precedence decision — RIGHT. No path registers a flow body at boot or on reload outside it.
(b) The rank flip, the dev's open question 1 — EXECUTION of the direction, not a new product decision. Option A is right; confined exactly as claimed.
(c) The protocol side — RIGHT, scoped to
(d) The receipt — the shape is published and unchanged; only its values move for the defective case.
(e) The changesets and the deliberate correction, the dev's open question 2 — RIGHT; this record confirms the correction, sentence by sentence.
(f) The two
Surface inventory: no route, schema, query set, status code or exported signature changes; two exported types unchanged in shape; one barrel-exported function's outcome moves for exactly the held-name-with-stored-row case; two ② Semver levelThe PR body's line 2 reads
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
…oyment has not installed (objectstack-ai#20863) (objectstack-ai#20959) Fixes objectstack-ai#20863 Clause-②: no (narrowing) ## What this does A flow saved through the metadata door (`PUT /api/v1/meta/flow/:name`) may name, as its base, the package it belongs to. When that id was a package this deployment has never installed, the door answered `200`, stored the flow live, and served the binding back: a flow bound to a package that does not exist. It now answers `422 WRITABLE_PACKAGE_REQUIRED`, and nothing is written, served or registered. - **One rule, extended in place.** The refusal lives in `tenantAuthoredWriteRefusal` (`packages/metadata-protocol/src/protocol.ts`), the one shared function every flow write door asks, which PR objectstack-ai#20853 established. There is no second check. The rule gains a named-base arm between the locked-base lock and the provenance check. `saveMetaItem` already handed the rule the base it names, so its code is unchanged; only its comment at the hand-off says so. - **"Installed" is the set the metadata write path already resolves a base against.** The arm asks `resolveWritePackageScope`, the registry's package read that the runtime authoring gate uses for its package closure. It is not the loader's managed set alone: a tenant's own writable base, created through the package door and rehydrated from the package store at boot, is installed and ships no flow. No new registry read and no second list of packages. - **Whatever the definition carries.** Both branches that used to admit the save now refuse it: a definition with no provenance of its own, and one whose provenance names that same missing package. - **Order.** A shipped flow is still refused as a locked base first (objectstack-ai#20679's check, reused). With the operator's writable-types hatch open, the lock admits a shipped flow, and a missing base is still refused: the hatch unlocks a type, never a binding. - **Scope.** `flow` only, as the objectstack-ai#20761 ruling's rule 5 requires. Every other type keeps its old handling. The `/automation` create, update and clone doors name no base, so they do not move. The two server-stated rewrites (stored-metadata migration and package duplication) are not judged by the rule, as before. - **The stored-row sentinel is not a package.** A save naming it is a package-less save, admitted as before. ## Hypotheses from the dispatch, measured - **H1 held.** Measured on `origin/main` `31c39964fc` against the unmodified rule, with an environment id and without. A definition with no provenance returns `null` at the "body not code-shipped" branch. A definition whose provenance names the same missing id returns `null` at the "named base equals the stamp" branch. The ADR-0070 D1 gate further down then admits the save, because its writability predicate reads an unregistered id as a writable authoring workspace. At the door, the ablation's mutated leg below re-measured it on the showcase: `200`, state `active`. - **H2 held, with one qualification.** The reader exists: `resolveWritePackageScope` (registry `getPackage`), reused inside the rule. The qualification is that its `undefined` covers both "the registry does not hold this id" and "the registry cannot be read" (a registry with no package read at all, or one that throws). So on such a registry a named base is refused: the fail-closed direction, the same one the automation engine takes with no loader's-set reader. Every real composition's `SchemaRegistry` has the read. This is written down in the rule's docblock. - **H3: `WRITABLE_PACKAGE_REQUIRED` / 422.** Ledger row: `packages/spec/src/api/error-code-ledger.zod.ts:634`, in the `@objectstack/metadata-protocol` block (line 590). No code is minted. ADR-0070 D1 decided this code for exactly this condition: a runtime create whose resolved base is missing or read-only. Its remedy is the one this caller needs: choose or create a writable base, or name none. `INVALID_METADATA` (line 610, same block) was rejected because the definition may be perfectly valid; what is wrong is the base the request names, not a key in the body. The sentence is new, because the D1 emitter's sentence says "read-only", which is false for a package that does not exist. The refusal carries the refused id and the ADR-0070 docs pointer, as that emitter's does. - **H4 measured, one topology NOT MEASURED.** The in-repo environment kernel is the standalone stack (`createStandaloneStack`, environment id `env_local`). One-shot boot, not kept as a file, through the kernel's protocol service: both definition shapes answered `WRITABLE_PACKAGE_REQUIRED/422`, 0 metadata rows, registry item absent. The two controls saved `active`: no base named, and a base installed through `installPackage`. The unit pins also run every case with an environment id. The cloud's per-environment kernel manager is not in this repository: NOT MEASURED. - **H5 held and pinned.** Unit level: the store's insert and the registry's `registerItem` are never reached, for published and drafted saves, on both topologies. Door level: 0 `sys_metadata` rows under the name, the metadata read answers `404`, and the automation read answers `404` (the engine never armed it). ## Pins - `packages/metadata-protocol/src/protocol.tenant-authored-write.test.ts`, a new describe block with 7 cases: the refusal across 5 definition shapes and both topologies, plus the plural type spelling; nothing written or registered on published and drafted saves; three controls (no base or the sentinel passes, an installed base passes, a shipped flow is a locked base first whatever base is named); the hatch case; and another type left untouched. The rule's registry double now serves the registry's package read. - **One door-level pin** on the showcase host-config boot, in the existing `packages/qa/dogfood/test/flow-provenance-server-held.dogfood.test.ts`: one `it`, so no second boot. Triage's door-level controls are the cases already in that file: a customer flow with no base saves, one in the tenant's installed base saves, and a shipped flow is refused as a locked base. - **Fixtures re-judged, because they saved flows into a package their registry never held.** - `protocol-publish-drafts-advisories`, `protocol-publish-drafts-closure` and `protocol.publish-item-rebind-announce` now declare their base installed, with no namespace and no dependencies. The prefix pre-flight and the closure are unchanged, and no assertion moves. - `protocol.package-closure-gate`'s "narrows nothing when the registry cannot produce the written package" pinned the very branch this change shuts for a flow save. It now reaches that state the way it still arises: a draft promoted after its package left the registry, beside the installed control, which reports. ## Evidence All runs are on HEAD `e201d770b2` unless noted. - metadata-protocol, full suite at `f051bba0e2`: 194 files passed and 3 skipped; 2886 tests passed and 19 skipped; exit 0. `typecheck`: exit 0. The one file changed after that, `protocol.tenant-authored-write.test.ts`, re-ran at `e201d770b2`: 19/19, and `typecheck` exit 0. - Consumers, because the wire answer changed: - objectql full suite: 349/349 files, 6812/6812 tests. - runtime full suite: 300/300 files, 5000 passed and 5 skipped. - rest full suite: 245/245 files, 4878 passed and 114 skipped. - These three ran at `eb25706394`, before the objectstack-ai#20942 merge. - At `f051bba0e2`: - dogfood, 4 flow files (this pin's file, objectstack-ai#20942's `flow-shipped-name-stored-row-boot`, the clone door and the durable doors): 32/32. - runtime, 6 automation and `/meta` files: 312/312. - objectql's publish-conformance file: 15/15. - `typecheck` exit 0 for objectql, including its test-layer check, and for dogfood. - **Red/green ablation of the new arm** at `e5914e3f2c`, through `scripts/ablation-replace.mjs`, whose restore is armed on exit: - The mutation made the arm's condition unsatisfiable. On disk: marker count 1, guard text count 0. Both dists that carry the arm were rebuilt, `@objectstack/metadata-protocol` and `@objectstack/rest` (rest bundles a copy). `ablation-dist-preflight` found the marker in both dists. - Mutated leg: the unit file read 3 failed, 16 passed of 19 (the three refusal cases). The dogfood file read 1 failed, 13 passed of 14: the new pin, answering `200` with state `active`. - Restore: the blob equals HEAD, `git diff HEAD` is empty, and `git status --porcelain` is empty. After rebuilding both, `--absent` passed for both dists. The files read 19/19 and 14/14. ## Gates - `node scripts/pm/dispatch-gates.mjs --commands` derived 68 commands at `e201d770b2` from a tree that was not stale. All 68 were run, and every one exited 0. - `--ran` reconciliation: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. - Two first readings were not measurements and were re-run: - `check:dual-build-cjs-loads` answered PREREQUISITE NOT MET (exit 3), because 8 packages outside this closure had no `dist/`. They were built, and it exited 0. - `check:engine-double-contract` refused an `update` double this PR had added to the rule's test. The double was dropped rather than growing the pinned ledger, and the gate exited 0. - `check:adr-0087-registration`: one declared-breaking changeset, disposition `not-required (no-migration-prescription)`. - `pnpm lint`: a proven narrowing, not a full run. - (1) Population: all 8 changed `.ts` files are under `packages/**`, which the `packages/**/*.{ts,tsx,mts,cts}` and `**/*.{ts,…}` blocks of `eslint.config.mjs` lint. - (2) `eslint --no-inline-config --format json` over those files: 8 files, 0 errors, 0 warnings, at `e201d770b2`. - (3) Invariance: the config never enables type-aware linting (`eslint.config.mjs` lines 326-328), so a verdict on an untouched file cannot move. - CI: not awaited. ## Changeset `.changeset/20863-orphan-package-binding-refused.md`: `@objectstack/metadata-protocol` `minor`, **BREAKING** under the launch-window convention, following PR objectstack-ai#20907's shape. It carries one ADR-0087 marker and a line telling the caller what to send instead: an installed base, or no base. The dogfood package is private. The objectql change is a test file, which ships nothing. ## Declared deviations - **Outside the claim's file surface:** `packages/objectql/src/publish-package-drafts-response-conformance.test.ts`, +4 lines, in a separate commit (`eb25706394`) that can be dropped on its own. - Its harness staged flow drafts into a package its real registry never held, so any implementation of the ruling turns 5 of its cases red. It now installs that base, with no namespace. No assertion moves, and the file reads 15/15. - The claim did not name it. It is declared here, not taken silently. - The door-level pin went into the existing objectstack-ai#20761 dogfood file rather than a new file, to avoid a second showcase boot in CI. - `main` was merged twice (no rebase). The second merge brought objectstack-ai#20942 (`75519e1c0a`), which edits `protocol.ts` near this rule. Git merged it cleanly, and both changes are present. ## Acceptance notes (noted, not filed) - For every type other than `flow`, the metadata door still stores a row bound to a package id no installed package holds. The ADR-0070 writability predicate reads an unregistered id as a writable authoring workspace. This is kept deliberately: the objectstack-ai#20761 ruling's rule 5 leaves other types unchanged. Carrier: none. - A host with no package store loses a runtime-created base from the registry at restart. That is an existing degradation, and `installPackage` states it loudly. After this change, a flow save naming such a lost base is refused rather than stored bound to it. Not measured. Carrier: none. - `@objectstack/rest`'s built `dist/` carries its own copy of this package's protocol code: the new sentence appears there, and rest lists `@objectstack/metadata-protocol` as a devDependency. Observed while scoping the ablation's rebuild. Not investigated further. Carrier: none. - objectql's publish-conformance harness calls a registry method that `SchemaRegistry` does not declare (0 hits in `registry.ts`), behind optional chaining, so those two calls do nothing. This is a reading, not measured. Carrier: none. - The `protocol.ts` ADR anchor does not mention the new named-base arm. It was not added, because the anchor file is outside the claim's surface. The rule's docblock cites ADR-0070 D1 and ADR-0126 §2. Carrier: none. ## NOT MEASURED - The cloud per-environment kernel manager: it is not in this repository. H4's in-repo environment kernel was measured, as above. - Studio's round trip: objectui is not in this container. The server-side refusal is what Studio receives. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…s the loader's body, as the list does (objectstack-ai#20946) (objectstack-ai#20994) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 - Triage direction `5920432754` on objectstack-ai#20946 (the interim): "the by-name read serves the loader's artifact for a shipped flow name, using the same predicates PR objectstack-ai#20942 introduces for the list and the execution view … ⛔ No third precedence path. The by-name read calls the predicate the list calls." - 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. What becomes of the stored rows themselves (keep, refuse, migrate) belongs to objectstack-ai#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 objectstack-ai#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." - That stays true as that PR's own delta. This is the reading the objectstack-ai#20942 record applied to the 20864 note's bullet 5. - This PR's note states the change in the same release. - It is not corrected here because it is outside the claim's file list. The seat may choose to correct it. - **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](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…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 #20913
Clause-②: no
What
For a flow name the loader ships from a managed package, a cold boot now arms the loader's body at both boot steps, and a stored row of that name is reported as its own shadowed contender, never armed. Triage direction
5916734614, ruling5904938166rule 1, ADR-0126 §2 / §3, ADR-0131 D6.One decision, three seams:
service-automation/src/plugin.ts). Thekernel:readysync and themetadata:reloadedre-sync now resolve the protocol's flow view throughresolveFlowPrecedencewith the engine's ownpackagedFlowOwner, the same call the boot pull makes. The three callers share one private helper,resolveFlowContenders. Before, both syncs registered every listed body one after another, so the last body listed for a name was the one armed.service-automation/src/flow-precedence.ts). Within a name the loader's set holds, the loader's entry ranks before a stored row.flowis Regime C: "⛔ Never silent override, never an overlay read path". Apackagecontender exists only inside a held name (since automation: boot-time flow precedence still classifies its contenders from body stamps, not the loader's set (the remainder of #20761's ruling rule 1) #20864), so the rank changes nothing outside one. The collision warning now says which rule armed the body and names the remedy (clone under a new name, or switch the packaged flow off).metadata-protocol/src/protocol.ts).⛔ Not decided here: what happens to hatch-written rows themselves (keep, refuse, migrate). That is #15206's. This PR only makes them "never armed over the base".
Readings
Showcase composition on a database file, measured on a cold boot after a stored row was placed at rest under
showcase_urgent_task_alertbetween two boots. Before = base4d0b9cd542; after =22d8d3593e.kernel:readygetShadowedFlows()for the nameGET /api/v1/automation/NAMEGET /api/v1/meta/flowentrymetadata:reloadedGET /api/v1/meta/flow/NAME(by-name)Deviations: please confirm
flow-precedence.tsto exposing the existing decision, without re-shaping it. Measured on the base:package, and arrival order decided..changeset/20864-precedence-loader-set.mdis corrected. One clause is removed: "a flow authored in the deployment wins over the packaged flow of that name (ADR-0005)". This PR makes that clause false, and the note has not shipped yet.check-empty-changesetstays red by design for a deliberate correction of somebody else's pending note. Confirmation requested here; do not restore it from the base.flow-precedence.ts(ADR-0126, ADR-0048);protocol.ts's anchor.Tests at
22d8d3593epnpm --filter @objectstack/service-automation exec vitest run: 160 files, 2002 tests passed. Full run at97bd52e852; since then only one test title changed, and the six precedence and sync files were re-run at22d8d3593e(43 passed).pnpm --filter @objectstack/metadata-protocol exec vitest run: 194 files passed and 3 skipped; 2879 tests passed and 19 skipped.protocol.tsis unchanged since that run; the new file and the tenant-authored suite were re-run at22d8d3593e(21 passed).pnpm --filter @objectstack/metadata-protocol --filter @objectstack/service-automation --filter @objectstack/dogfood run typecheck: clean. Service-automation's typecheck was re-run at22d8d3593e; itscheck:test-typecheckreports 0 errors.isolatedproject, at22d8d3593e:flow-shipped-name-stored-row-boot(new, 6 cases), plusflow-provenance-server-held,automation-authoring-doors-durableandpackaged-flow-write-door-parity. 31 passed.service-automation/src/flow-sync-one-precedence.test.ts, 7 cases: both syncs, held name, two packages, unheld duplicates, tear-down.metadata-protocol/src/protocol.flow-stored-row-shipped-name.test.ts, 9 cases: hydration, both faces, name-only judgement, controls for an unheld flow and an overlay-regime type.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 77 families at22d8d3593e, and--ranaccounts for all 77 with exit codes.check-empty-changeset --base origin/mainexits 1: deviation 2, red by design.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (8 unrelated packages had nodist/). After building them it exits 0.pnpm lint(the whole repo,eslint . --no-inline-config) exits 0 at22d8d3593e.Ablations
Each ablation ran after the fix was committed. Every leg went through
scripts/ablation-replace.mjs: the anchor hit exactly once, the blob changed, and the restore was proven by blob equal to HEAD and an emptygit diff HEAD.Unit legs (subjects imported from
src/):kernel:readysync's precedencemetadata:reloadedre-sync's precedenceDogfood legs (the dogfood suite resolves
dist/, so each leg ran mutate, rebuild,ablation-dist-preflightmarker present, run, restore, rebuild,--absentand tree clean):Acceptance notes
GET /api/v1/meta/flow/NAMEstill serves the stored body under the package's provenance for a shipped name that has a stored row (measured after: 200, the stored body). It now disagrees with the list, which serves the loader's body.sys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206's keep / refuse / migrate decision.getMetaItemsreader offlowinherits the list change, by source reading:GET /meta/diagnostics?type=flow,findReferencesToMetasources of type flow, and the package export sweep inruntime/src/domains/packages.ts. For a shipped name with a stored row they now see the loader's body.flow.packages/cli/src/utils/format.ts) cites "ADR-0005 overlay precedence" for every shadowed flow; the armed and shadowed facts it prints come from the receipt and are correct;[Registry] Collisioninpackages/objectql/src/registry.tssays every read serves the stored row; for a flow, that is now true of the by-name door only;FlowContenderandFlowShadowingRecorddocblocks inengine.ts, which the automation: boot-time flow precedence still classifies its contenders from body stamps, not the loader's set (the remainder of #20761's ruling rule 1) #20864 review already routed to the next PR touching that type.metadata_org_scoped_unhydratedwarning names such a row. For a shipped name its "will NOT bind its triggers" clause is true of the row only; the loader's flow stays bound.Generated by Claude Code