Repository navigation
fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one - #21942
Conversation
…ned datasource, and removes only a stored row under one isArtifactBacked now sees a datasource an installed code package declares, through a second non-standalone-artifact resolver read from the package records' declared datasources. The existing package door and the repository's write intent then refuse a PUT with NOT_OVERRIDABLE / 403 and the datasource row of the packaged-base regime table: the admin door's verdict (code-defined, cannot be edited at runtime, read-only) and its remedy (edit the *.datasource.ts source). A DELETE that would remove nothing is refused with the same verdict; a DELETE of a stored row under a code-defined name stays possible as repair, in the protocol's delete door and the repository's delete gate. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… datasource, both kernel shapes Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…pair, and the refusal sweeps leave that tier to its own pins servedLockState's deletable reads whether the read found a stored row for an origin-gated code-defined item, so the read agrees with the door in both states: refused with no row, admitted (repair) with one. The two delete refusal sweeps derived from the registry flags exclude the origin-gated type, and the read-versus-door table measures that type's host-config removal at the protocol's delete door, where it is answered. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…and a pre-fix stored row is removable across restarts Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ed datasource refusal (narrowing) Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ta-door-code-datasource
…r test's pinned doubles Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5ce9eece1832504aecb46ded9ebed94d3ada5621 && git checkout 5ce9eece1832504aecb46ded9ebed94d3ada5621
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 dd81fb50d5ad7bf85b6d9c65db325866a7890def && git checkout -B drift-repro 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 && git merge --no-ff dd81fb50d5ad7bf85b6d9c65db325866a7890def
node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8
|
ACCEPT (seat review) — PR #21942 at head
|
…ce row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default (objectstack-ai#21965) Part of objectstack-ai#21922 Fixes objectstack-ai#21944 Clause-②: no (narrowing) ## What changes A code-defined datasource (a `*.datasource.ts` the installed artifact declares, or the host's own `default`) is read-only by published contract: `DatasourceSchema.origin` says "code — authored as `*.datasource.ts`, GitOps-owned, read-only in the UI", the datasource registry entry in `metadata-plugin.zod.ts` says code-defined datasources "win on name collision", and `datasource-admin-service.ts` says "A runtime datasource never shadows a code one (code wins on collision)". The datasource-admin plugin's boot restore broke all three, and the metadata door could not see `default` at all. The fix is the one host-owned set of code datasources the two cards' triage asked for ("One set serves both, so do not build two"): - **The set** (`packages/runtime/src/code-datasource-names.ts`, new). One in-memory `Set` of the datasource names the host registers from code, on the kernel service `code-datasource-names`. `contributeCodeDatasourceNames` registers it on first use and adds to it after that, the shape `seed-summary` uses. - **Its producers, both in `init()`.** `AppPlugin.init()` adds every datasource the artifact declares: the same list its `start()` registers in the MetadataService, now memoized so the two phases read one answer. `DefaultDatasourcePlugin.init()` adds `default`. Phase 1 completes before any `start()`, so the set is whole before the restore runs, whatever order the plugins were composed in. - **The restore** (`restoreRuntimeDatasources`, `packages/services/service-datasource/src/datasource-admin-plugin.ts`). A stored row under a name in the set is not registered over the code definition. It is kept, and one boot warning names it with the repair. The warning goes to the host's `options.logger`, or to the kernel logger when the host passes none (`os serve` passes none). - **The resolver** (`isDeclaredCodeDatasource`, `packages/metadata-protocol/src/protocol.ts`, nothing else in that file). It reads the same set beside the installed packages, so the metadata door answers `default` the way it answers every code-defined datasource since PR objectstack-ai#21942. ⛔ "Code" is never read from a stored row's `origin`, the MetadataService slot's `origin`, the connection service's `ConnectResult`, or a request body's `origin`. ## Measured on a booted showcase The harness is the `@objectstack/verify` `bootStack` with the datasource-admin routes mounted the way `serve.ts` mounts them, in a temp cwd. The stored rows assert `origin: 'runtime'` and their own `config.filename` (the cards' case (b)). They were written through the metadata door's repository on the runtime-only intent, then the stack restarted. BEFORE is `76fec88b16`; AFTER is this branch at `1d840709ae`. The readings come from a throwaway probe that was never committed; the committed pins below assert the AFTER column. | Reading after the restart | BEFORE | AFTER | |---|---|---| | admin list, `showcase_external` | `origin: runtime`, label "Shadow 21922" | `origin: code`, "External Analytics (SQLite)" | | `PATCH /api/v1/datasources/showcase_external` | 200 | 400 `DATASOURCE_ADMIN_ERROR` "… is code-defined and cannot be edited at runtime." | | live pool named `default` | a second pool opened on the stored row's file; verdict `already-registered` became `connected` | none; verdict stays `already-registered` | | `showcase_ext_customer` read | 3 rows (code fixture) | 3 rows (code fixture) | | boot warning naming each stored row | none | one per row | | `PUT /api/v1/meta/datasource/default` | 200 "Saved datasource 'default'" | 403 `NOT_OVERRIDABLE` | | `DELETE /api/v1/meta/datasource/default`, no stored row | 200 | 403 `NOT_OVERRIDABLE` | | `DELETE /api/v1/meta/datasource/showcase_external` (repair), then meta `GET` in the same boot | 200, but the meta `GET` kept serving the stored edit until the next restart | 200, and the meta `GET` serves the code definition | ## The dispatch's mechanism hypotheses - **H1, start order.** In all three compositions that load `service-datasource` (`serve.ts`, `standalone-stack.ts`, the verify harness), `DefaultDatasourcePlugin` and `AppPlugin` are `use()`d before `DatasourceAdminServicePlugin`. None of the three declares an ordering edge to another, so their `start()`s run in insertion order: the code registrations did land before the restore, but by list position alone, which ADR-0116 says proves nothing. The answer is the first branch: the set is filled by a phase that precedes the restore (`init()`). It is pinned by a boot whose reader plugin is composed first, ahead of every producer. The restore pin also covers a code registration that lands after it. - **H2, the seam.** It is a kernel service read through the services registry the protocol already resolves (`getServicesRegistry()`), with no `packages/spec` change. The ObjectQL registry was rejected: the engine's datasource definitions mix both origins, and a host package record would be a fabricated provenance. - **H3, the admin refusal.** Measured, not assumed. The pins' stored rows carry `origin: 'runtime'`, and the slot the refusal reads holds AppPlugin's explicit `origin: 'code'`. Under ablation A the admin door served the stored row as `origin: runtime`, so the refusal cannot come from the admin read's `origin ?? 'code'` default. - **H4, the live pool.** For `default`: yes. The restored row reached `rehydratePools`, which opened a second pool named `default` on the row's file. Routing did not move, because the engine never routes to a driver named `default`; the default driver keeps its natural name. It is the same defect and the same decision fixes it, pinned by `getDriverByName('default')` and the connect verdict. For `showcase_external`, nothing was re-pointed at boot in this composition: AppPlugin's connect ran first, so the rehydrate answered `already-registered`. The admin `PATCH` 200 was the open door to a re-point (an update that changes connectivity rebuilds the pool), and it is now refused. ## Seam and the Clause-② limb (for the seat) - **Published exports added: none.** `code-datasource-names.ts` is not re-exported from `packages/runtime/src/index.ts`. `service-datasource` and `metadata-protocol` spell the service name privately and read the value structurally as `has(name)`, the way `'datasource-connection'` is read today. - **What the seam does add is one kernel service entry**, `code-datasource-names`, which two packages read by name. Whether that is the claim's "service contract" limb is the seat's call. The line above stays as the claim wrote it, and the changeset grades all three packages `minor`, which holds under either reading. ## Named gap: the metadata door's read while a stored row exists `GET /api/v1/meta/datasource/:name` still serves a stored row under a code-defined name for as long as the row exists. The door reads its stored overlay first (ADR-0005's read order), whatever the MetadataService holds. The AFTER boot measured it: the admin door served the code definition while the meta `GET` served the stored row, for `showcase_external` and for `default`. So triage's pins "both doors serve the code definition" and "removes it with no change to what is served" hold for the admin door. For the metadata door they hold once the repair `DELETE` has run, in the same boot. That read lives in `getMetaItem`'s overlay step, outside `isDeclaredCodeDatasource`, and `protocol.ts` is held by objectstack-ai#21934 in other regions, so it is left to the seat. `meta-door-code-datasource.dogfood.test.ts` already pins that read as it is. objectstack-ai#21922 stays open for that read: this PR is `Part of` it, and the seat routes the remaining half through triage when it merges. ## Landing beyond the claim's named files `packages/runtime/src/app-plugin.ts` is the producer of the packages' half of the set, in the declared `runtime` package. The memo also makes its residual-owner warning print once instead of once per phase. `packages/runtime/src/code-datasource-names.ts` is new in the same package. ## Patch round 1 (head `d77e150701`) Review `6011282321` on objectstack-ai#21922. The Tests, Ablations and Gates sections below are round 0's, at `80fbcfdea6`; this section carries the readings on the current head. - **The plugin-dev pin (CI red on round 0).** `AppPlugin.init()` now contributes its datasource names as a function that the host's code-datasource set resolves at its first read. The set is still contributed to only in Phase 1, before any `start()`. The resolution is deferred because the names come from the artifact's `collections`, which walk `packages[]`. `AppPlugin.init()`'s manifest registration is the one thing in `init()` allowed to touch `packages[]`, pinned by `plugin-dev`'s malformed-stack falsifier, which this PR's first head turned red. The kernel service now holds a `CodeDatasourceNames`, a set with pending contributions; readers still use `has(name)` only. A contribution that throws stays pending and rethrows to every reader. - **The `default` refusal names what defines it**: the host's database configuration (the database URL the server starts with). It names no `*.datasource.ts`, because none declares `default`. Every package-declared datasource's sentence is byte-identical, and `code`, `status` and the refused set are unchanged (`packaged-base-regime.ts`, the datasource row's `hostOwned`). - `const listOf = (` spacing restored in `app-plugin.ts`. Merged `origin/main` `80f9f7e6ba` as `49421a8fe6`. - **Ablation D:** the old sentence put back for `default` turned 6 unit cases and 1 dogfood case red, and was restored by blob. - **Tests at `d77e150701`** (each `VERDICT command-exit 0`): - `service-datasource`: 748 / 748; - `metadata-protocol`: 27978 passed, 19 skipped; - `runtime` local: 4668 passed, 19 skipped; - `plugin-dev`: 86 / 86; - the two dogfood files: 11 / 11; - downstream consumers of `runtime` (cli, client, verify, http-conformance, cloud-connection): all passed; - typechecks for the five packages: green. - **Gates at `d77e150701`:** `dispatch-gates --commands` derived 72, reconciled with `--ran` as 72 run and 0 not measured, plus `check:init-service-contract` and `check:startup-registry-verdict`. All exit 0. - **Docs:** the 18 hand-written pages the Docs Drift Check lists were read page by page. None states anything this PR makes false, so no docs are edited. ## Tests (head `80fbcfdea6`) - `pnpm --filter @objectstack/service-datasource exec vitest run --maxWorkers=2`: 41 files, 748 passed. - `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2`: 218 files passed, 3 skipped; 27976 tests passed, 19 skipped. - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --project local`: 331 files, 4658 passed, 19 skipped. - Dogfood, `--project isolated`: `datasource-restore-code-wins.dogfood.test.ts` (new) and `meta-door-code-datasource.dogfood.test.ts`, 2 files, 11 passed. - `typecheck` green for `service-datasource`, `metadata-protocol`, `runtime` (including its `check:test-typecheck` ledger, held) and `dogfood`. `tsc --listFiles` counts each touched test file once in its program. - Each package's run includes its new pins: 5 in `datasource-admin-plugin.test.ts` (the restore), 4 in `code-datasource-names.test.ts` (the set and its phase), and 2 resolver plus 8 door cases in `protocol.code-defined-datasource-door.test.ts` (`default`, on both kernel shapes). ## Ablations Each one was committed first and mutated with `scripts/ablation-replace.mjs` in wrap mode, under a shell trap. For subjects resolved through `dist/`, the package was rebuilt and `ablation-dist-preflight.mjs` proved the marker was present. The restore leg was rebuilt and proven `--absent`, the blob equalled HEAD, `git diff HEAD` was empty, and the tree was clean. - **A, the restore registers over a code name** (`datasource-admin-plugin.ts`; marker in 2 files of `service-datasource/dist`). Unit: 3 failed, 16 passed. The slot served the stored row, the warning was not called, and the order-independent case registered the row. Dogfood: 2 failed, 3 passed. The admin list served `showcase_external` as the stored row, and the repair case read the same. - **B, the resolver does not know the set** (`protocol.ts`; marker in 2 files of `metadata-protocol/dist`). Unit: 5 failed, 30 passed (the resolver case, and `PUT` plus no-row `DELETE` of `default` on both kernels). Dogfood: `PUT /meta/datasource/default` answered 200. The next case then failed as a cascade, because the row that `PUT` stored made the seed conflict. The repair `DELETE` and runtime controls stayed green, as expected. - **C, AppPlugin's `init()` contribution deleted** (`app-plugin.ts`; the subject resolves from source). The reader-first boot saw `['default']`, not `['app_wh', 'default']`. So the set comes from `init()`; the `start()` registration never fills it. ## Gates (head `80fbcfdea6`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 72 commands on the actual change, a superset of the 52 at dispatch. All 72 ran with exit codes captured before any pipe. `--ran` printed "72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED". - `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET` (exit 3) because eight packages outside this diff had no `dist/`. After building them it measured green. - Two families the derivation does not name were also run, both green: `check:init-service-contract` ("34 declared / 1 self-provided / 3 without a workspace provider") and `check:startup-registry-verdict` ("none recording a verdict the boot can contradict"). - `check-changeset-no-major`'s level axis needs a PR payload, so CI reads it. - Lint is a proven narrowing, not the repo-wide run. `eslint --no-inline-config --format json` on the 9 touched source and test files reported 9 files, 0 errors and 0 warnings. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules, as its own comment states), so this diff cannot move any untouched file's verdict. ## Acceptance notes - **The `default` refusal's remedy** names the host's database configuration (patch round 1). - **Cluster convergence.** `convergePool` reads a stored row directly and is unchanged. Its signals come from peer admin writes, and the admin door now refuses those for code names. - **The restore's other warnings** (a failed read, a failed register) still go only to `options.logger`, which `os serve` does not pass. They are unchanged here. - **objectstack-ai#21923 remains open.** This diff does not touch `listDatasourceRecords`, `getDatasourceRecord` or `persistDatasourceRow`. One interaction: a metadata-door-created datasource with no `origin` still restores, and is still read as `code` by the admin door's default. - **Main drift.** `origin/main` gained objectstack-ai#21956, objectstack-ai#21964 and objectstack-ai#21961 (spec and docs-qa only) after the round-1 merge. None touches a file here, and the queue's merged generation is the check. --- _Generated by [Claude Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21899
Clause-②: no (narrowing)
What changes
The metadata door now answers a code-defined datasource the way the published contract (
DatasourceSchema.origin: "code — authored as*.datasource.ts, GitOps-owned, read-only in the UI") and the datasource-admin door already did: read-only. Triage ruled Q1-A and Q2-B in 6006929054; this PR implements both, inpackages/metadata-protocolonly.isArtifactBacked(packages/metadata-protocol/src/protocol.ts) gains a second non-standalone-artifact resolver,isDeclaredCodeDatasource, in theisNestedArtifactFieldshape from org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743. It reads the installed packages' declareddatasources(registry.getAllPackages(), each record'smanifest.datasources, in the canonical array formdefineStackleaves).datasourceis added to that docblock's census. It never reads a MetadataService slot'soriginor a request body'sorigin; a unit case pins a body assertingorigin: 'runtime'on a code datasource as still refused, and one assertingorigin: 'code'on a runtime name as still saved.refusePackagedBaseOverrideon an environment kernel, and the repository write intent (override-artifactintoSysMetadataRepository.assertAllowed) on a host-config kernel, which is the showcase's shape. The answer isNOT_OVERRIDABLE/ 403. The sentence comes from the packaged-base sentence table (packaged-base-regime.ts), which gains oneorigin-gatedrow fordatasource. ADR-0126 §3 recordsdatasourceoutside the three regimes, as "origin-gated: code-defined read-only, runtime-created free", so the row is not a Regime C row. It carries no routes, only the owning source.DELETEthat would remove nothing is refused with the same verdict. ADELETEof an existing stored row answers 200. Where the carve-out lives:ObjectStackProtocolImplementation.originGatedRemovalRefusal(new, besiderefusePackagedBaseRemoval) holds the package door's removal verdict.deleteMetaItemanswers it at its row probe: it throws when no stored row exists and lifts it when one does.SysMetadataRepository.assertDeleteAllowedmirrors the lift for the same type throughisOriginGatedType. This is the topology-independent gate a host-config kernel asks.saveMetaItem's metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086 record ("removing a code-only row that predates this refusal is repair, and must stay possible") and the A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 ruling (removal restores the code-declared state, which is the narrowing direction). A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960's ownsupportsOverlayboundary is not widened:object, which sharesdatasource's registry flags, keeps refusing both verbs, and a guard pins that.servedLockStatereports what the doors do:editable: false, anddeletabletrue only while the read found a stored row to remove.The two doors' codes differ, by ruling
Triage's answer 6006929054: "The two doors' codes differ, and that is accepted. Each door speaks its own vocabulary; the verdict and the remedy agree." Measured on a real showcase boot at this branch:
PUT:403 NOT_OVERRIDABLE—Datasource 'showcase_external' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.PATCH:400 DATASOURCE_ADMIN_ERROR—Datasource 'showcase_external' is code-defined and cannot be edited at runtime.DELETEwith no stored row:403 NOT_OVERRIDABLE—Datasource 'showcase_external' is code-defined and cannot be removed at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.DELETE:400 DATASOURCE_ADMIN_ERROR—Datasource 'showcase_external' is code-defined and cannot be removed at runtime.The host's
defaultdatasource (H4): the admin door treats it as code-defined; covering it here is a named gapbootStack, admin routes mounted asserve.tsmounts them):PATCH /api/v1/datasources/defaultanswers400 DATASOURCE_ADMIN_ERROR"Datasource 'default' is code-defined and cannot be edited at runtime.", andDELETEanswers "… cannot be removed at runtime.".PUT /api/v1/meta/datasource/defaultanswers 200 "Saved datasource 'default' (env-wide, state=active)" and the read then serves the edit.DELETEanswers 200.defaultunchanged:PUTanswers 200 on this branch too, which is measured.metadata-protocolwithout aruntimeorservice-datasourcechange:DefaultDatasourcePluginregistersdefaultonly throughMetadataService.registerInMemory. That is the slot whoseorigintriage ruled unsound, because a stored row overwrites it.ConnectResult: name, status, reason, ownership).listDatasourceDefs()mixes code and runtime definitions.default.runtimeorservice-datasourcefile is edited. This is reported for a follow-up card, and the changeset names it.Pins, before and after (real showcase boot,
showcase_external)"Before" is the reverse-verification leg below (the base
isArtifactBackedon committed HEAD) and the first run's measurements at54fb60ac3f(6006105473). "After" is this branch.PUT /meta/datasource/showcase_externalNOT_OVERRIDABLEwith the verdict and remedy above; nosys_metadatarow; the read serves the code labelDELETE, no stored rowNOT_OVERRIDABLE, "cannot be removed at runtime"DELETE, a pre-existing stored row (seeded as a pre-fix save wrote it, across a restart)DELETEanswers 403code,_packageIdcom.example.showcase; no rowPOST201,PATCH200,DELETE204; metadata door:PUT200,PUT200,DELETE200Reverse verification
Run on committed HEAD
8413b4622d, throughscripts/ablation-replace.mjs(WRAP mode, its own restore trap, plus agit checkout HEADtrap):isArtifactBacked's last line, dropping|| this.isDeclaredCodeDatasource(type, name). On disk the anchor went 1 to 0 and the replacement 0 to 1. The blob went8e2d759618bato51f36f712468.protocol.code-defined-datasource-door.test.tsshowed 14 failed and 11 passed. The red cases are the resolver,PUTrefused (both kernels),DELETEwith no row refused, the repair's secondDELETE, the read envelope, and the 500-character bound. The green cases are the runtime controls, the hatch guard and the repository gate cases, which do not route throughisArtifactBacked.pnpm --filter @objectstack/metadata-protocol buildemitted ESM/CJS and failed only DTS on TS6133, because the mutation leaves the new method unused.node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol '...' --absentconfirmed the marker absent from all 22 built files.meta-door-code-datasource.dogfood.test.tsshowed 4 failed and 2 passed. ThePUTpin readexpected { status: 200, code: undefined } to deeply equal { status: 403, code: 'NOT_OVERRIDABLE' }.8e2d759618ba),git diff HEADis empty, andgit status --porcelainis empty. A rebuild put the marker back in both built entry files (preflight: present). The unit file then passed 25/25 and the dogfood file 6/6.Tests (HEAD
dd81fb50d5)pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 217 files passed (3 skipped), 27941 tests passed.pnpm --filter @objectstack/metadata-protocol typecheckandpnpm --filter @objectstack/dogfood typecheckare green.tsc --listFilesincludes every touched test file.meta-door-code-datasource.dogfood.test.ts(6/6) andexternal-import-code-datasource-namespace.dogfood.test.ts./metadoor were run, all green:runtime:datasource-visibility,meta-type-write-capability-parity,stored-metadata-reader-contexts.pin,standalone-stack-hydrate-metadata,meta-write-org-scope,dispatcher-plugin.declared-5xx-prose-withhold.rest:meta-type-read-capability,meta-type-write-capability,rest-server-meta-write-org-scope,meta-unknown-type-read-refusal,rest-server-meta-org-scope-url-spelling,rest.service-datasource:datasource-admin-record-judgement.objectql:overlay-precedence.datasourcedelete refusal and were triaged.protocol.delete-rewrap-envelope,protocol.legacy-overlay-deleteandprotocol.read-lock-flags-write-doorexclude the origin-gated type from the derived refusal sweeps or measure it at the protocol's delete door. Each change points at the new pin file.Gates (HEAD
dd81fb50d5)node scripts/pm/dispatch-gates.mjs --commandsderived 76 commands, and all 76 were run with exit 0.--ranreconciliation reports "76 derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED".check:engine-double-contractasked for its ledger to learn the new file's pinned doubles (--write, +3 rows, committed).check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-pathsexited 2 with no PR context (NOT MEASURED locally); they run on this PR in CI.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchors,check:adr-anchors) exited 0.check:adr-0087-registration --base origin/main: one declared-breaking changeset,not-required (no-migration-prescription).check-changeset-no-majorreports no major.Acceptance notes
DELETE. The read keeps serving the stored copy until the next restart, because the datasource-admin plugin's boot restore registered it in the MetadataService. The receipt still reads "reset to artifact default". That is finding(service-datasource): a stored datasource row overrides a code-defined datasource at boot, so after a restart the admin door serves and edits it at runtime (restoreRuntimeDatasources has no code-collision check) #21922's in-memory half, so the repair pin holds across a restart. Measured: in the same boot,GETafter the repair served "Shadow 21899"; after the restart it served the code label.409 METADATA_CONFLICT, whether or not the version token is sent, because the admin door'ssys_metadatarow carries a null checksum. This is pre-existing and untouched here (runtime names are not artifact-backed). It is reported for filing in the dev report. The PM's hypothesis that sending the version makes it pass was measured false.origin-gatedrow rather than a Regime C row, per ADR-0126 §3. Its module header now scopes the "no redeploy prescription" rule to Regime C sentences.OS_METADATA_WRITABLE=datasourcestill opens the lock exactly as before; a guard case pins it.datasourcesno package body declares (AppPluginwarns about this composition at boot) registers code datasources the resolver does not see, because no package record carries them.Changeset
.changeset/21899-meta-door-code-datasource-read-only.md:@objectstack/metadata-protocolminor, BREAKING,Clause-②: no (narrowing). It states the remedy: edit the*.datasource.tssource, and delete a stored row through the metadata door to repair. It carries one ADR-0087 marker.Generated by Claude Code