Repository navigation
fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) - #20817
Conversation
… the packaged-base lock PUT /automation/:name and DELETE /automation/:name now ask the metadata protocol's own locked-base verdict (packagedBaseRefusal, lifted out of saveMetaItem / deleteMetaItem unchanged) before the engine is called, and relay its refusal verbatim. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tocol verdict Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…rdict Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…owcase Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…ver on body stamps Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…em / deleteMetaItem verbatim The two helpers now carry the original inline lines unchanged (they throw, as that code did); packagedBaseRefusal is the one place a throw becomes a value, and it re-raises anything that is not the refusal. The changeset states the widening. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…-base lock too POST /automation onto a name the engine already holds is an overwrite; it now asks the same locked-base verdict as PUT /:name before anything is read or registered. Bounded in-place fix, adopted onto the claim's surface. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 6 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 — 31 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 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56 && git checkout 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79114850f0f559dcb4fb432f5bcfc12947a03de6 e4006d68594d433159b1a0e8addd9585e5929f59 && git checkout -B drift-repro 79114850f0f559dcb4fb432f5bcfc12947a03de6 && git merge --no-ff e4006d68594d433159b1a0e8addd9585e5929f59
node scripts/docs-audit/affected-docs.mjs --json 79114850f0f559dcb4fb432f5bcfc12947a03de6
|
The refusal's code and status match the metadata door everywhere; its sentence matches wherever the metadata protocol's own package door answers. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is the record of record for PR #20817 at Inputs:
Disclosure is kept at the public item's level: doors, roles, codes and statuses. Seat verification on adoption:
① Derived judgmentsDelta: the changeset sentence now reads "the same code and status the metadata door gives (
The first record's finding is closed. Carried forward, with the code unchanged:
Surface inventory:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…sion in words instead of a tracker number (stage 2) (objectstack-ai#20830) Part of objectstack-ai#20513 Clause-②: no **Stage 2 of 5 of this lane (`metadata-protocol`), under the maintainer's A / A ruling on the card.** The card stays open for stages 3-5, so this PR carries no closing keyword. Text only: no error `code`, field name, HTTP status, export or control flow moves. Every changed source line is a string-literal line (108 changed lines in 11 `.ts` files, checked line by line against the merge base). ## What this does The metadata protocol's refusals, hints and log lines sent the reader to a tracker number for the reason behind them. Each rewritten string now says that reason in words (form D, as the migration-entry rewrite and stage 1 applied it). Where the sentence already stated what was decided, only the citation goes. Where it did not, the decision is added in words: | Where | Cited | The sentence now says | |---|---|---| | `protocol.ts` `insertManyData` refusal (thrown) | 3172 | `insertMany` is the partial-success batch insert: an outcome per row, so a bad row neither fails the whole batch nor makes the good rows run their `beforeInsert` hooks twice. | | `protocol.ts` unknown metadata type refusal (400) | 8586 | A plugin cannot declare a type either: `additionalTypes` was retired because nothing ever read it. | | `protocol.ts` stored non-canonical type refusals on publish and on revert (`STORED_TYPE_NOT_CANONICAL`) | 7894, 8957 | The `/meta` URL door now folds a type to its canonical spelling before it writes, so such a row predates that; the stored migration's `skipped` report "with that same reason" loses only its citation. | | `runtime-authoring-gate.ts` schedule-flow `organization_id` hint | 6153 | The author's value wins, and the engine fills only an organization the run resolved, so for a schedule run the author is the one source. | | `plugin.ts` the three `kernel:ready` "migration skipped" warnings | 5839, 8629, 8686 | What the migration that did not run would have ensured: view-name uniqueness among ACTIVE rows only; the NULL-safe `sys_setting` row identity on tenant and global rows; the adoption of untenanted seed rows and their autonumber counter. | | `sys-metadata-repository.ts` history-counter abort (`error`) | 4867 | The old path answered 1 because it took a failed read for an empty table. | | `protocol.ts` publish-closure degrade (`warn`) | 10377 | Validation falls back to the LIVE declarations without the batch's own drafts in the closure. | | `protocol.ts` cold-boot org-scoped audit (`warn`) | 6190, 6992 | The write refusal it points at covers declared types only; the trailing "See" keeps ADR-0005. | | everything else: the overlay, `sys_view_definition` and `sys_setting` index migration messages (ADR-0120 D4 stays), the seed/API tenancy repair, its receipt and its skips, the batch-row withhold, the object-existence gate's no-registry warning, the nested-select, overlay and non-canonical-registry refusals, and the live-MySQL testkit error | 6418, 8725, 8629, 5839, 6417, 8686, 9451, 9261, 8502, 3770, 4196, 6190, 4432, 10382 | The sentence already said what was decided; only the citation goes. | Each claim was checked against today's code, not only against the cited card: `saveMetaItem` folds the request type before it persists; `additionalTypes` is a `retiredKey()` tombstone in `packages/spec`; the org-scoped write refusal is live in `orgScopedWriteRefusal`. One cited number (10382) answers 404 and was read through its landing commit `ee09d2119`; the testkit sentence already says what it guards. ## Order inside the stage All 46 id-bearing literals (50 occurrences) fit one PR, under the stop line, so the stage lands whole, in three commits in the ordered sequence: 1. `dc8a1a112` author-visible text: 10 literals (thrown refusals, the stored-type refusals, the hint); 2. `730cbcaf6` log lines: 35 literals, plus the two re-pinned tests; 3. `442473234` the `src/`-shipped testkit string, and the changeset. Each commit recomputes the ledger, so every commit on the branch is green on `check:doc-authoring`. ## Pins re-pinned: 3 assertion lines in 2 test files - `migrations/view-definition-active-index.test.ts` 342-343 asserted the two numbers in the MySQL degradation line. They now assert the two gaps in words: "an archived view keeps occupying its name slot" and "two same-name ACTIVE shared views (owner NULL)". - `protocol.batch-row-driver-text.test.ts` 396 asserted the number in the withhold warning. It now asserts "must not be quoted back on response data", the decision itself. A one-off mutation proves each new pin can fail, run on the committed head with `scripts/ablation-replace.mjs` (anchor hit once, disk-verified) under a shell trap. Changing "name slot" gives 1 failed / 27 passed. Changing "(owner NULL)" gives 1 failed / 27 passed. Changing "quoted back" gives 2 failed / 14 passed: the re-pin, and a knock-on in the next test, because the failed test never reached its `mockRestore`. After each leg, the blob equals HEAD and `git diff HEAD` is empty. The tree is clean after the run, and no test file was left behind. No string here is compared byte for byte with a twin in another package. The consumer pins outside the package read unchanged fragments: `runtime`'s `batch-row-driver-text-real-driver.integration.test.ts` reads the withhold prefix, `seed-tenancy-autonumber-split.integration.test.ts` reads "backfill skipped", and `meta-field-overlay-lock.test.ts`, `objectql`'s `protocol-meta.test.ts` and `rest`'s `meta-unknown-type-read-refusal.test.ts` read "is not a metadata type". I ran the three `runtime` files against the rebuilt `dist/`, and they passed. ## Ledger burn-down `scripts/doc-authoring-prose-id.baseline.json` was regenerated with `node scripts/check-doc-authoring.mjs --census-ledger` into a scratch file, so the growth refusal ran against the checked-in baseline, and then copied into place. Only `metadata-protocol` rows moved, and every one of them leaves: | File | Occurrences before | after | (file, id) pairs before | after | |---|---:|---:|---:|---:| | `metadata-protocol/src/protocol.ts` | 15 | 0 (row leaves) | 11 | 0 | | `metadata-protocol/src/migrations/seed-tenancy-backfill.ts` | 14 | 0 (row leaves) | 3 | 0 | | `metadata-protocol/src/migrations/view-definition-active-index.ts` | 7 | 0 (row leaves) | 3 | 0 | | `metadata-protocol/src/migrations/overlay-index.ts` | 4 | 0 (row leaves) | 2 | 0 | | `metadata-protocol/src/migrations/sys-setting-identity-index.ts` | 4 | 0 (row leaves) | 1 | 0 | | `metadata-protocol/src/plugin.ts` | 3 | 0 (row leaves) | 3 | 0 | | `metadata-protocol/src/migrations/live-mysql-database.testkit.ts` | 1 | 0 (row leaves) | 1 | 0 | | `metadata-protocol/src/runtime-authoring-gate.ts` | 1 | 0 (row leaves) | 1 | 0 | | `metadata-protocol/src/sys-metadata-repository.ts` | 1 | 0 (row leaves) | 1 | 0 | | **whole ledger** | 861 | 811 | 572 | 546 | The ledger's file count goes from 224 to 215, and other packages' rows moved: 0. The census's 40 messages reconcile with the ledger's 50 occurrences. The gate counts 46 string literals: 45 in the census population plus the testkit string, which the census filed as test-facing. The census folds a `+` chain into one message, so 5 two-literal chains make 45 literals into 40 messages. Four literals carry two ids each, which makes 46 literals into 50 occurrences. ## Verification (head `442473234`) - build: turbo over `@objectstack/runtime...` (30/30), then the whole workspace (72/72), then `@objectstack/metadata-protocol` directly. The new sentences are in `dist/index.js`, and the only citations left in `dist/` are docblocks. - `@objectstack/metadata-protocol` test: Test Files 190 passed, 3 skipped (193); Tests 2792 passed, 19 skipped. The skips are the live MySQL/PostgreSQL files: this container has no server. - `@objectstack/metadata-protocol` typecheck: exit 0; `tsc --listFiles` reads 193 of 193 test files. - `@objectstack/runtime`, the three consumer files above: 3 files, 33 tests passed. - `node scripts/pm/dispatch-gates.mjs --commands` (no paths; 13 paths against merge base `261c529f0`): 70 commands, all exit 0. `check:dual-build-cjs-loads` and `check:type-check-debt` first answered exit 3 PREREQUISITE NOT MET on the partial build. After the full build (and a direct rebuild of this package, whose `dist` a turbo cache hit had left older than the restored sources), both exit 0; `check:dts-closure` and `check:lean-entry-closure` were re-run there too. `--ran`: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. - `check:doc-authoring`: sibling-package prose ids hold the baseline, no growth, no burn-down unrecorded. - narrowed lint: `eslint --no-inline-config --format json` over the 11 touched `.ts` files reports 11 files, 0 errors and 0 warnings. The resolved `parserOptions` for these files are `ecmaVersion` and `sourceType` only, with no `project` or `projectService`. So no type-aware rule can move an untouched file. The repo-wide `pnpm lint` is CI's. - NOT MEASURED locally (CI's): the live PG/MySQL files, whose messages changed only ahead of the pinned MySQL statement lead-in; the Test Core shards; Dogfood; the workspace type-check lanes. ## Acceptance notes - PR objectstack-ai#20817 (another lane, draft) also edits `protocol.ts`. Whichever lands second merges `main` and recomputes the ledger with `--census-ledger`. - Docblocks and `//` comments in this package still cite numbers. They are out of scope here: comments are the sanctioned home for internal anchors, and a separate card owns stale ones. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…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>
…ion run proved stale (objectstack-ai#21339) Docs-only checklist revision from the 17.6.0 release-verification run objectstack-ai#21330 (subject `617f25f8`, Console pin `31971ff1e28f`). Every change follows the checklist README's lifecycle rule: the item's `revision` bumps and one `history` entry says what changed, why, and cites the run. No product code, no `content/docs/**`, no generated file. ## Stale clauses the run proved (each a FAIL in objectstack-ai#21330 with disposition stale-clause / assertion-defect) | item | rev | evidence | changed by | |---|---|---|---| | `access-security.audit-log-browser` | 2 → 3 | admin `GET /data/sys_audit_log?filter={"action":"delete"}` → 0 rows; the row is stored with correct attribution | `30c530e5` (objectstack-ai#21194): the ledger serves a non-system reader, admins included, only rows about records it can read | | `api-backend.filter-comparand-conformance` | 2 → 3 | POST `/query` → 400 `VALIDATION_FAILED` at `query.where.f_number.$eq`; GET `$filter` and engine → 400 `INVALID_FILTER`; no door returns rows | objectstack-ai#20116 (`cfc3bcf1` objectstack-ai#20247, `dd1b8031` objectstack-ai#20325) — the split query-contract-matrix rev 3 already records | | `api-backend.date-range-preset-matrix` | 1 → 2 | equality `{"signed_on":"today"}` → 400 `INVALID_FILTER` (temporal door); `$gte:"this_week"` → 400 with `bareDateRangePresetComparandMessage` | by design: `18.filter-preset-ordering-comparand-refused.ts` judges ordering positions only | | `records-forms.import-transform-matrix` | 1 → 2 | 400 `UNSUPPORTED_TRANSFORM` names the missing sandbox, 0 rows — but no `framework#2611` | `f115b1f` (objectstack-ai#21188): refusals state decisions in words, not tracker numbers | | `studio-authoring.view-authoring-live` | 1 → 2 | `GET /meta/view?object=repair_asset` serves `repair_asset.default` / `repair_asset.form` with the authored config; container name 0 hits | by design: `expandViewContainer` (objectstack-ai#7163, objectstack-ai#7736, objectstack-ai#13407) | ## Expected-fail notes 17.6.0 has made pass (clauses held in objectstack-ai#21330; only their framing was stale) | item | rev | measured | fixed by | |---|---|---|---| | `automation.packaged-flow-subflow-disable-refusal` | 1 → 2 | caller off → child's disable retry 200, ledger `active=false`; caller-first enable 409 `RESOURCE_CONFLICT` | `36d043b` objectstack-ai#20724, `0d9349f` objectstack-ai#20759; the enable guard is `679f95e` objectstack-ai#20711 (step 6 now enables the child first) | | `automation.packaged-flow-clone-contract` | 1 → 2 | clone survives a cold restart and fires; still unreachable from Studio | durability `cb4c31d` objectstack-ai#20907; reachability now filed as objectstack-ai#21332 (clause unchanged, still expected to fail) | | `access-security.packaged-flow-write-door-parity` | 1 → 2 | `PUT` / `DELETE /automation/showcase_urgent_task_alert` → 403 `NOT_OVERRIDABLE`, flow unchanged | `4b45afae` (objectstack-ai#20817); knownGap names the existing pin `packaged-flow-write-door-parity.dogfood.test.ts` | No clause was weakened: each still refuses the original failure mode (rows returned, a served delete row, a 200-with-zero-rows), and the clone clause keeps its expected fail. ## Validation - `node scripts/check-platform-checklist.mjs` → `OK — 15 areas, 269 items (265 active, 2 planned)`; symbol anchors and line-citation sweep green. - `api-backend.json` is re-serialized in its existing canonical 2-space form; the other four files are edited in place in their existing mixed formatting. Not in this PR (listed on objectstack-ai#21330's close-out instead): the other checklist-accuracy findings the run collected, and the two `planned` picklist items, which can only be promoted by a run in which they pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20679
Clause-②: yes (widening)
The
/automationdefinition doors now refuse a packaged flow, with the same locked-base verdict the metadata door gives. This is the public checklist itemaccess-security.packaged-flow-write-door-parity: clauses 2 and 3 flip to PASS, and clauses 1 and 4 still pass, measured on a booted showcase. The reproduction stays withheld as filed. This body stays at the door level the public item already describes.What changed
packages/runtime/src/domains/automation.tsPUT /:nameandDELETE /:namecallrefusePackagedFlowBaseChangebefore the engine is called, and relay its refusal.manage_metadataauthoring gate, then the body envelope check (PUT and POST only), then the lock, then the engine.POST /onto a name the engine already holds overwrites that flow. It now takes the same lock, right after the name check.3690c44b, before the fix, from a throwaway probe (real protocol over a realSchemaRegistry, deleted afterwards): a create onto a packaged flow's name answered200,registerFlowwas entered once, and the packaged flow's label was overwritten.403 NOT_OVERRIDABLE,registerFlownever entered, and a create under a new name still succeeds.protocolslot withresolveServiceOrLoud, unscoped, exactly as the/metadomain resolves it. So both doors ask the same protocol instance./metadoor to be at parity with.packages/metadata-protocol/src/protocol.ts: the widening, a cross-lane surface the seat adopted.ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation }). It returns the refusal the/metadoor gives for writing ('save') or removing ('delete') an existing item that a code package ships, ornullwhen that door would not refuse on this ground./metaverdict (isArtifactBacked+isOverlayAllowed, and its emitters) was private to this class, so a second door could not ask it any other way.refusePackagedBaseOverrideandrefusePackagedBaseRemoval, carrysaveMetaItem's anddeleteMetaItem's inline package-door code verbatim (proof below).packagedBaseRefusalis the one place a throw becomes a value. It re-raises anything that is not a403NOT_OVERRIDABLE/ITEM_LOCKED.NOT_OVERRIDABLEandITEM_LOCKEDare ledgered under@objectstack/metadata-protocol(ADR-0112).@objectstack/runtime's owner key lists neither.packages/spec.errorFromThrown, the code, status and sentence are the producer's.check:error-code-provenance: 330 stamp sites, 313 listed, 17 waived, OK.What the lock keys on. The flow's NAME, looked up in the registry's artifact-only lookup (
SchemaRegistry.getArtifactItem,packages/objectql/src/registry.ts:3919). That lookup scans the PACKAGE_ID:NAME entries the artifact loader registers.{ type, name, operation }and nothing else.Two refusals on DELETE. The lock (
403 NOT_OVERRIDABLE) answers before the engine's ADR-0126 §7.3 refusal (DELETE_RESTRICTED/409, a packaged subflow that packaged callers still reach).OS_METADATA_WRITABLE=flow.registerFlow/unregisterFlow: the boot pull registers packaged flows through them.What stays open.
POST /:name/clone, the ADR-0126 §7.1 customization path.POST /:name/toggle, the activation switch. Its packaged-only rule from automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726 is untouched.OS_METADATA_WRITABLE=flowoperator hatch, which the refusal sentence names. It opens this door exactly as it opens/meta: sameisOverlayAllowed.Lift proof: each lifted helper body, before and after, whitespace-insensitive
diff -wchanged linesrefusePackagedBaseOverriderefusePackagedBaseRemovalThe only added lines compute the locals the inline code read from its enclosing method:
overlayAllowedin the override helper;overlayAllowedandartifactBackedin the removal helper.Each is spelled exactly as in the calling method.
deleteMetaItemkeeps its own copies for itsNOT_CREATABLEcheck.Pins
packages/runtime/src/domains/automation-packaged-base-lock.test.ts(15 cases). RealObjectStackProtocolImplementationover a realSchemaRegistry, with the packaged flow registered the way the loader registers it; the automation service is a spy.saveMetaItem's /deleteMetaItem's thrown refusal: code, status and sentence.packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts(9 cases):nullfor a name no package ships and for a Regime O type;packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts: the derived fold population gainspackagedBaseRefusal("all fourteen").packages/qa/dogfood/test/packaged-flow-write-door-parity.dogfood.test.ts(5 cases), the real showcase composition over HTTP:PUT /meta/flow/:nameanswers403, codeNOT_OVERRIDABLE, in the REST door's envelope.PUT/DELETE /automation/:nameanswer403 NOT_OVERRIDABLE, and the definition reads back byte-identical.Tests (measured)
At
3690c44b(merge over #20726):@objectstack/runtimelocalproject: 292 files, 4215 passed, 1 skipped.@objectstack/runtimerepoproject: 727 passed.@objectstack/metadata-protocol: 191 files passed (3 skipped); 2801 passed, 19 skipped.@objectstack/metadata-protocoltypecheck: exit 0.@objectstack/objectql, the 20 files pinningNOT_OVERRIDABLE/NOT_CREATABLE/ITEM_LOCKEDonsaveMetaItem/deleteMetaItem: 362 passed.@objectstack/rest, 3 files: 41 passed.At
f5ea0060(head):automation-*.test.tsin@objectstack/runtime: 25 files, 478 passed.@objectstack/runtimetypecheck: exit 0,check:test-typecheckOK.Reverse verification
Each leg was committed first, then mutated through
scripts/ablation-replace.mjs(anchor hit 1, blob changed). Dist legs were proven byablation-dist-preflight(marker in 2 built files). Every restore was proven (blob == HEAD,git diff HEADempty, tree clean, marker absent from dist). Predicted and measured agree on every leg.3690c44b3690c44bpackagedBaseRefusalreturnsnull(rebuilt)3690c44b3690c44b3690c44bf5ea0060After every restore, all pins were green again.
Gates
dispatch-gates --commandsatf5ea0060: 67 derived.--ranreconciliation: 67 run, 0 NOT-MEASURED, 0 unrun, every exit 0.check:error-code-casingand@objectstack/speccheck:error-code-provenance, both exit 0.pnpm lint, narrowed and proven:eslint.config.mjs(the**/*.{ts,…}andpackages/**/*.{ts,…}blocks). All 6 changed.tsfiles are in it.--format jsoncounted 6 files linted, 0 errors, 0 warnings, atf5ea0060.eslint.config.mjsstates it: noparserOptions.project, no typed rules), and its four plugins are local AST rules. So this diff cannot move any untouched file's verdict.Acceptance notes (observed, not filed)
NOT_OVERRIDABLEsentence names "edit the source artifact and redeploy" and theOS_METADATA_WRITABLEhatch. It does not name clone (§7.1). This holds on both doors, because the sentence is one emitter. It is reported to the seat, not changed here./metaanswers through the REST server's refusal envelope,{ error, code }with a flat stringerror./automationanswers through the dispatcher's,{ success, error: { code, message } }. Pre-existing. The dogfood pin reads each where it lives.@objectstack/restinlines the protocol. It declares@objectstack/metadata-protocolas a devDependency, so its builtdist/carries its own copy of the protocol class. Pre-existing. It is rebuilt with the same source.Generated by Claude Code