Repository navigation
fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default - #21965
Conversation
… displaces a code datasource; the host's code-datasource set decides The runtime keeps one in-memory set of the datasource names it registers from code (the artifact's declared datasources and the host's `default`), filled from AppPlugin's and DefaultDatasourcePlugin's init() so it is complete before any start(). The datasource-admin plugin's boot restore skips a stored row under one of those names, keeps it, and warns naming it; the metadata door's code-datasource resolver reads the same set, so it refuses edits to `default` as the admin door does. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…wins at the restore, the set's phase, and the /meta answer for default Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…st-code-datasource-set
…he metadata door refuses edits to default Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 9fe98ea34f3e235cbf284678fcec055b824cd7e2 && git checkout 9fe98ea34f3e235cbf284678fcec055b824cd7e2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 80f9f7e6ba5d2097a4cb32ca696908dcf9678102 d77e15070132990d8f99405b33e22afd4256abb5 && git checkout -B drift-repro 80f9f7e6ba5d2097a4cb32ca696908dcf9678102 && git merge --no-ff d77e15070132990d8f99405b33e22afd4256abb5
node scripts/docs-audit/affected-docs.mjs --json 80f9f7e6ba5d2097a4cb32ca696908dcf9678102
|
…st-code-datasource-set
|
CI status ·
Generated by Claude Code |
…n the set resolves at first read, so init() never walks packages[] AppPlugin.init()'s manifest registration is the one thing in init() allowed to touch the artifact's packages[]; resolving the owner list there made a malformed packages[] refuse from the collections getter too. The host's code-datasource set now holds deferred contributions, resolved at its first read (still after every init(), before any reader in start()), and a contribution that throws stays pending and reaches every reader. Also restores the space in `const listOf = (`. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…st's database configuration, not a *.datasource.ts that does not exist The origin-gated datasource row gains `hostOwned`: names the host defines from its own configuration. `default` is the one, reserved by contract (AppPlugin refuses an artifact declaring it, the admin service refuses to create it), so its PUT / no-row DELETE refusal now says what defines it and to restart the server; every package-declared datasource's sentence is byte-identical. Code, status and the refused set are unchanged. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
… decision in words instead of a tracker number (stage 25) (objectstack-ai#21975) Part of objectstack-ai#20749 Clause-②: no Stage 25 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the second and last name-ordered `api/` group: the 13 id-bearing test files directly under `packages/spec/src/api/` from `plugin-rest-api.handler-status-retirement.test.ts` to `zod-issues-to-fields.test.ts`. Those files carried 89 messages and 95 tracker ids, citing 43 records. All 95 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed. With this stage, `api/` carries no tracker id in a test string. ## Census at the base (`5a22eb5619`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages 10 to 24 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The worktree was cut from `origin/main` at `5a22eb5619`, the claim's base and stage 24's landing. Both instruments read **371 messages / 392 ids in 84 files**, the seat's reading and stage 24's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `api/` (this PR: all 13 files) | 13 | 89 / 95 | 86 / 92 | 3 / 3 | | `ui/` | 5 | 7 / 7 | 0 | 7 / 7 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **84** | **371 / 392** | **331 / 349** | **40 / 43** | The group reads **89 messages / 95 ids in 13 files**, the seat's figures file for file: | file (under `api/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `plugin-rest-api.handler-status-retirement.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 | | `plugin-rest-api.schema-refs.test.ts` | 2 / 2 | 2 / 2 | 0 | | `plugin-rest-api.test.ts` | 2 / 2 | 2 / 2 | 0 | | `protocol.test.ts` | 46 / 50 | 46 / 50 | 0 | | `registry-retirement.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `rest-api-config-dead-keys-retirement.test.ts` | 2 / 2 | 2 / 2 | 0 | | `rest-server.test.ts` | 19 / 19 | 18 / 18 | 1 / 1 | | `router.test.ts` | 1 / 1 | 1 / 1 | 0 | | `sortability.test.ts` | 3 / 4 | 3 / 4 | 0 | | `storage.test.ts` | 2 / 2 | 2 / 2 | 0 | | `validate-data.test.ts` | 3 / 3 | 3 / 3 | 0 | | `websocket.test.ts` | 1 / 1 | 1 / 1 | 0 | | `zod-issues-to-fields.test.ts` | 2 / 3 | 2 / 3 | 0 | | **13 files** | **89 / 95** | **86 / 92** | **3 / 3** | Five more test files sit in the same name range and carry no id (`query-adapter.test.ts`, `realtime-shared.test.ts`, `realtime.test.ts`, `retired-error-codes.test.ts`, `versioning.test.ts`). The three "other" strings are expect failure messages, rewritten and declared to the text-only tool: `plugin-rest-api.handler-status-retirement.test.ts:179` and `rest-server.test.ts:768` (template literals) and `registry-retirement.test.ts:89` (one leaf of a `+` chain). - **Controls.** Lit: `ui/notification.test.ts` (1 id) and `system/book.test.ts` (2 ids), outside the group, read the same at the base and at the head. Dark: `protocol.test.ts` reads 0 at the head while 65 of its lines still carry a number, every one of them a comment. Planted in a scratch tree: an id put into a `storage.test.ts` title reads 1 / 1 (`title:it`), and an id put into a `sortability.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in all 13 files at the base, and 0 in all 13 at the head. - **At the head:** 282 messages / 297 ids in 71 files. The 13 files read 0 / 0, `api/` leaves the table, and no other file moved. ## How the area was chosen `api/` has no subdirectory, so it is taken in name-ordered file groups near the ~100-id bound, the rule stages 20 to 24 used. Stage 24's cut named this group at 95 ids, and this census reads 95, so no re-cut was needed. `protocol.test.ts` (50 ids) fits one PR and one text-only proof, so it is not split. **Named for the next stages** (cut from the head census, 282 / 297): - **`system/`**, 167 ids in 34 files (one of them in `system/constants/`), two stages: - **first group:** `auth-config.test.ts` through `metadata-form-declared-rows.pin.test.ts`, 18 files, 91 messages / 97 ids (`i18n-resolver.test.ts` alone 53 / 56); - **second group:** `metadata-form-zod-reconciliation.test.ts` through `worker.test.ts`, 16 files, 63 / 70. Its first file carries 17 "other" strings, its ledger `why` entries. - The files directly in `src/`, 120, one stage. - The needles: the three docblock needles, the kept `ui/component-props-unknown-members.pin.test.ts:322` and stage 22's two. One stage, with an at-tier review. The four colour literals stay, as stage 21 decided. ## What each id became - **10 literals (11 ids)** now state a decision in words. - **12 literals (16 ids)** get their subject back in words, where the number stood for a thing. - **67 literals (68 ids)** drop a number the title already explains. Every cited record was fetched with all its comments through REST, and its decision was read from its ruling, ACCEPT and landing comments: a keyword digest of every record, and full reads wherever the new words carry a decision. 43 records are cited: 36 answer 200 and 7 answer 404. Two more were read for context: objectstack-ai#14478, whose ruling B objectstack-ai#15677 executes, and PR objectstack-ai#11426, objectstack-ai#11006's landing. The seven that answer 404 were read from what landed, through the commits endpoint (this checkout is shallow), each commit found through the CHANGELOG entry or the commit list of `protocol.test.ts`: - **objectstack-ai#6037**, from `18189983dd` (objectstack-ai#6474): `DataProtocol.validateData` asks the write path for its verdict and persists nothing, objectstack-ai#4633 ruling D; - **objectstack-ai#6239**, from `f549a0d4ad` (objectstack-ai#6526): `ViewProtocol`'s five viewId-addressed methods and ten schemas are retired; - **objectstack-ai#6361**, from `90bbf25107` (objectstack-ai#6866): the notification-list `cursor` is tombstoned on both halves (maintainer ruling 2026-08-07, option A); - **objectstack-ai#9740**, from `11b779e0f9` (objectstack-ai#9773): `MetadataProtocol.getMetaItemLayered` is declared, and the dead `'overlay'` `lockSource` arm is dropped; - **objectstack-ai#9741**, from `2a29caa532` (objectstack-ai#9804): `previewDrafts` / `state` are declared where the implementation enforces them, and `environmentId` is recorded as transport-level. Its changeset (`packages/spec/CHANGELOG.md:31798`) names it "maintainer ruling 2026-08-18", and `cccbe51bf7` cites "the objectstack-ai#9741 ruling"; - **objectstack-ai#11006**, from `cccbe51bf7` (objectstack-ai#11426): `publishMetaItem` is declared as an optional member with `PublishMetaItemRequest` (maintainer ruling 2026-08-22, option B); - **objectstack-ai#14691**, from `b3a63d32c9` (objectstack-ai#14868): the ten inert `RestServerConfig` keys the liveness ledger recorded as `dead` are retired. **The same-id titles stage 24 listed in this group:** - **`[objectstack-ai#5672]` x2** (`protocol.test.ts:508`, `:526`): objectstack-ai#5672's maintainer ruling A (`5199159328`): one closed capability vocabulary, emitted in full by both discovery producers, with an absent capability `enabled: false` rather than a missing key. `:508` now reads "strips a capability key outside the closed vocabulary". The old verb was "rejects", but the body pins the opposite: the parse stays green and the key does not survive it. `:526` now reads "… (ruled: an absent capability is `enabled: false`, not a missing key)". - **`(objectstack-ai#12038)` x5** (`:2575` to `:2686`): these five "declares the … body" describes are the describe-only transcriptions that the five-part ruling's implementation plan names (`5434804846`). None of them pins a lettered sub-ruling, so no letter is named; the title already says the decision, and only the number goes. - **`(objectstack-ai#12038 1C)`** (`:2710`): now "GetPublishedMetaItemResponseSchema stays opaque (ruled: no shape frozen against the current type registry)", ruling 1C's own reason. Its children pin the `unknown` body. - **`(objectstack-ai#19543, door ③)`** (`:2726`): door ③ is the AI-conversation list, which the schema name already names, and "declares the next-page signal" is that door's spec half (letter A, re-derivation `5825819437`). Only the number and the door label go. - **`(objectstack-ai#15677)`** in `plugin-rest-api.test.ts:694` and `websocket.test.ts:712`: now "… durations carry their unit in the key name", objectstack-ai#14478's ruling B (`5518649320`, population ruling `5548763981`), which objectstack-ai#15677 executes for `api/`. In `router.test.ts:565` the title already shows the rename (`RouteDefinition.timeout → timeoutMs`), so only the number goes. **Stated in words:** | record | literal (under `api/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#14478 via objectstack-ai#15677 | `plugin-rest-api.test.ts:694`, `websocket.test.ts:712` | "… durations carry their unit in the key name" | Ruling B: a `z.number()` duration key carries its unit in its name; the old spellings are `retiredKey()` tombstones. | | objectstack-ai#5672 | `protocol.test.ts:508` | "strips a capability key outside the closed vocabulary" | Ruling A (2026-08-06): one closed vocabulary. | | objectstack-ai#5672 | `protocol.test.ts:526` | "rejects a capability map that is missing part of the vocabulary (ruled: an absent capability is `enabled: false`, not a missing key)" | Ruling A: both producers emit the whole vocabulary. | | objectstack-ai#9406 | `protocol.test.ts:1313` | "probes is opaque BY DECLARATION (ruled: modeled only once a consumer needs a field): …" | Maintainer ruling 2026-08-18 (`5322875103`): `probes` gets a deliberately opaque passthrough, upgraded to a modeled schema only when a consumer needs a field of it. | | objectstack-ai#9343 | `protocol.test.ts:1383` | "PublishPackageDraftsResponseSchema published[].advisories (ruled: advisory findings ride each published element)" | Maintainer ruling 2026-08-17 (`5321046016`): `advisories` rides each `published[]` element, with no parallel top-level map. | | objectstack-ai#9741 | `protocol.test.ts:1739` | "environmentId stays OUT of the meta-read request shape — transport-level by decision, not omission" | The 2026-08-18 ruling, as landed in `2a29caa532`: `environmentId` is the transport-level multi-kernel routing key. | | objectstack-ai#12038 | `protocol.test.ts:2710` | "GetPublishedMetaItemResponseSchema stays opaque (ruled: no shape frozen against the current type registry)" | Ruling 1C (`5434804846`): a thin envelope with the body opaque, no union frozen against today's type registry. | | objectstack-ai#6037 | `validate-data.test.ts:25` | "ValidateDataRequest — asks the write path for its verdict instead of predicting it" | What landed in `18189983dd`: the dry run stops predicting the write's verdict and asks for it. | | objectstack-ai#6037 | `validate-data.test.ts:57` | "ValidateDataResponse — the verdict the write path would reach, persisting nothing" | The same commit: `validateData` reports the write path's verdict on candidate rows and persists nothing. | **Subject back in words** (12 literals): - "zero holders after objectstack-ai#13823" becomes "zero holders after its retirement", and "[objectstack-ai#13823] ADR-0087 registration" becomes "handlerStatus retirement — ADR-0087 registration", the form of the repo's other retirement registration describes (objectstack-ai#13823 ruled remove, `5494755488`); - "the routes wired in objectstack-ai#3899" becomes "the routes wired to the request-schema gate", the gate the file's header names; - "(objectstack-ai#13155 — carries objectstack-ai#5745 to the third verb)" becomes "(carries the declared = returned discipline to the third verb)", the discipline objectstack-ai#7294 and objectstack-ai#13155 name objectstack-ai#5745 for; - "(objectstack-ai#4717 — objectstack-ai#4463 D3 on the response)" and "(objectstack-ai#9176 — objectstack-ai#4463 D3 on the publish door)" become "(advisory findings ride the 2xx response)" and "(advisory findings ride the 2xx on the publish door too)": objectstack-ai#4463's D3 sends gating findings to 422 and lets advisory findings ride the 2xx; - "the objectstack-ai#9612-gate class" becomes "the package-closure publish-gate class": objectstack-ai#9612's gate judges a publish against the written package's closure; - "objectstack-ai#10235 the objectstack-ai#7865 anchor category" becomes "the unprovisioned injected-anchor category", the platform anchors injected into an external object whose storage the platform does not provision (objectstack-ai#7865, ruling B); - the two "pre-objectstack-ai#3689" storage shapes become shapes "from before the shared success envelope"; - "the objectstack-ai#4052 non-repeat" becomes "the non-repeat of the retired `validateOnly` dry-run flag"; - "every objectstack-ai#8055-shaped fixture" becomes "every malformed-flow-body fixture". **Dropped where already stated** (67 literals, 68 ids). A number goes only where the title already says its decision. Examples: the two `[objectstack-ai#13823]` describes and the twelve `[objectstack-ai#14691]` / `(objectstack-ai#14691)` retirement titles ("REJECTS `patterns` with the retirement prescription — …", "the tombstones reject one key each, not the config — …"); `[objectstack-ai#11983]` x3, `[objectstack-ai#4579]` x2, `[objectstack-ai#4939]`, `[objectstack-ai#6361]`, `[objectstack-ai#20294]` and `objectstack-ai#3899 —`; the `objectstack-ai#10235` prefixes on "resolveObjectSortability — the closed category set" and "wire validity — …"; the five "transport-level by the objectstack-ai#9741 ruling" titles, which now read "transport-level by ruling"; the parenthesized `(objectstack-ai#5745 — …)`, `(objectstack-ai#7294 — …)`, `(objectstack-ai#9406 — …)`, `(objectstack-ai#10524 — …)` x2, `(objectstack-ai#9726 — …)`, `(objectstack-ai#9741 — …)` and `(objectstack-ai#4717 — …)` pairs, which keep their words; and the tails `(objectstack-ai#6239)`, `(objectstack-ai#4286)`, `(objectstack-ai#9740)`, `(objectstack-ai#11006)` x2, `(objectstack-ai#11678)` x3, `(objectstack-ai#9426)`, `(objectstack-ai#12005)` x3, `(objectstack-ai#11679)` x2, `(objectstack-ai#12004)` x2, `(objectstack-ai#3718)`, `(objectstack-ai#4572)`, `(objectstack-ai#4579)`, `(objectstack-ai#20294)`, `(objectstack-ai#8124/objectstack-ai#8055)`, the five `(objectstack-ai#12038)` and the `(objectstack-ai#4738, …)` aside in one expect message. The 404 numbers among them (objectstack-ai#6239, objectstack-ai#6361, objectstack-ai#9740, objectstack-ai#9741, objectstack-ai#11006, objectstack-ai#14691) go only where the title already states what landed. **No file is renamed.** ## Readers - **Needles:** none. The three declared strings are assertion failure messages (the second argument of `expect`), none is an expected value, and no title or message in the group is matched against a source docblock or another file's text. The one self-read in the group, `rest-api-config-dead-keys-retirement.test.ts:519`, reads its own file for the id-free describe title "tree-scoped absence", which this PR does not touch. - **Test-name filters:** none. No tracked script, workflow or package config passes `-t` / `--testNamePattern` to vitest; the one vitest `-t` hit is a README example under `packages/qa/dogfood` filtering its own fixture. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`, and none of the 13 files calls a snapshot matcher. - **Projects:** `rest-api-config-dead-keys-retirement.test.ts` is in the `repo` project (`packages/spec/vitest.repo-tests.json:31`); the other 12 run in `local`. The base-versus-head run below takes both projects. - **By substring:** every old literal, its id-bearing fragment and a window around each id (270 needles) was searched with `git grep` at the base, across the tracked tree outside its own file. No gate, doc, filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test reads one. The 6 hits are sibling test titles: the two `(objectstack-ai#15677)` describes in this group hit each other (both rewritten here), `client/src/client.test.ts:1134` shares "query.distinct (objectstack-ai#4286)", and `metadata-protocol/src/protocol.validate-data.test.ts:102` shares "the objectstack-ai#4052 non-repeat". ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares the three expect-message lines named above. - **Result:** 13 of 13 files SAME on all three legs, with the per-file counts predicted in writing before any edit. - **Totals:** 89 changed string leaves in 89 literals: 86 titles and 3 declared. The diff's `+` and `-` lines are exactly the 89 planned lines as multisets, and every file keeps its line count. - **Controls (14 of 14 as predicted on the first run, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row given an id VIOLATION; an undeclared expect message changed VIOLATION; a title re-split into a `+` chain DIFF; a declared expect message reverted to base SAME; a declared template expect message given a new id VIOLATION; a declared `+`-chain leaf given a new id VIOLATION; a template-literal title given a new id VIOLATION. - **Templates and tables:** no `.each` title and no `$name` placeholder changes. The two template literals change only their text after the `${…}` span. **Test counts:** the 13 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 509 tests in 13 files, all passed, with the same count and status sequence per file in 13 of 13. 250 full test names change, and each changed name equals the base name with the planned replacements applied: 0 mismatches. No full name repeats on either side, and no head name carries `#` plus digits (250 base names did). `router.test.ts:565` writes its arrow as a `→` escape; the plan's anchor there starts after the escape, so the comparison tool, which reads escapes literally, met none, and vitest prints "RouteDefinition.timeout → timeoutMs …" on both sides. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 13 touched files are in it, and no `*.test.ts` at all. The controls `src/api/protocol.zod.ts`, `src/api/rest-server.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, two new phrases and an old one each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `c63eba0adf`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (`VERDICT command-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 619 files, 18480 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 13 group files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date, against the `dist/` the build above wrote. - **Gates:** `dispatch-gates --commands` derived 79 families: stage 24's 80 without `check:error-code-casing`, whose named sources this diff does not touch. All 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The same 79 derive from `origin/main` `230e4944b0` with this diff applied. The five roster families marked as sharing a directory with this diff (`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) each exit 0. - **ESLint, a proven narrowing:** `--no-inline-config` over the 13 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 13 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 178 changed lines (+89 / -89). - A control-byte scan over the 13 changed files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was six commits past the base (`c9761cd2fb`: objectstack-ai#21966, objectstack-ai#21951, objectstack-ai#21963, objectstack-ai#21969, objectstack-ai#21965, objectstack-ai#21962). They touch 28 files, none of the 13 and none under `packages/spec/src/api/`, so `main` was not merged. The two `packages/spec/src` files they change (`data/datasource-credential-redaction.ts` and its test) read 0 / 0 in the census at `c9761cd2fb`: the one id they add is a code comment. `git merge-tree` onto `c9761cd2fb` is clean, and none of the 4 open PRs touches any of the 13 files. ## Acceptance notes - **Same-id test titles in this card's later stages** go with those stages: `system/book.test.ts:413` (`(objectstack-ai#12038)`). - **Same-id test titles in other packages** stay: 97 lines in 15 packages (`runtime` 25, `objectql` 13, `lint` 12, `metadata-protocol` 12, `rest` 12, `client` 8, `metadata-core` 4, `qa/dogfood` 2, `service-automation` 2, `service-storage` 2, and one each in `examples/app-crm`, `examples/app-showcase`, `driver-sql`, `plugin-sharing` and `types`), each package's share under the objectstack-ai#20513 lane children. - **Code comments with live ids** remain in these files and their sources, among them the `* objectstack-ai#3899 —` header in `plugin-rest-api.schema-refs.test.ts`, the `* objectstack-ai#8124 —` header in `zod-issues-to-fields.test.ts`, the `// [objectstack-ai#5672] This fixture used to lead with …` comment above `protocol.test.ts:508`, and the `/** [objectstack-ai#20294] … */` docblock in `rest-api-config-dead-keys-retirement.test.ts`. Code comments are not this card's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
…rom provenance, and a metadata-door write reaches it in the same boot (objectstack-ai#21977) Fixes objectstack-ai#21923 Clause-②: no ## What changes A runtime datasource is one record that two doors serve: the datasource admin door (`/api/v1/datasources`) and the metadata door (`/api/v1/meta/datasource`). They disagreed about it in three ways. All three land in `packages/services/service-datasource/src/datasource-admin-plugin.ts`. 1. **Origin comes from provenance, not from the record.** `listDatasourceRecords` and `getDatasourceRecord` served `origin: r.origin ?? 'code'`. A metadata-door body has no `origin`, or it asserts `origin: 'code'`. So after a restart, the boot restore registered the row and the admin door served it as code-defined and refused its `PATCH`. A new `servedOrigin(ctx, name)` returns `code` only for a name in the host's code-datasource set, and `runtime` for every other name, whatever the record says. That set is the `code-datasource-names` kernel service PR objectstack-ai#21965 landed, which the boot restore and the metadata door's refusal already read. Boot pool rehydration filters on the served origin, so such a datasource also gets its live pool after a restart. 2. **Same-boot reach.** The metadata door persists to `sys_metadata` and the SchemaRegistry. It never registers the record in the MetadataService slot the admin door lists. At `start()`, the plugin now registers the protocol's awaited `datasource` mutation projector (ADR-0094, `registerMutationProjector`; no `metadata-protocol` edit). After each metadata-door save, publish, revert, rollback or delete, and before that door answers, the projector does three things: - it re-reads the stored row (`sys_metadata`, the same read the boot restore makes); - it registers or unregisters the MetadataService slot to match that row; - it converges the live pool through the existing `convergePool`. A name in the code set is skipped, because code wins. So the metadata door's repair `DELETE` of a stored shadow row under a code name keeps the code definition served. 3. **The reverse direction.** `persistDatasourceRow` wrote the `sys_metadata` row with no `checksum`. The metadata door's reads serve `row.checksum ?? hashSpec(body)` as the version, but its writes compare the parent against the raw column. So every metadata-door `PUT` and `DELETE` of an admin-created datasource answered `409 METADATA_CONFLICT`. The row now carries `hashSpec(record, 'datasource')`, imported from `@objectstack/metadata-core`. That is the same computation `SysMetadataRepository.put` stamps (`hashSpec(body, ref.type)`). `@objectstack/metadata-core` moves from devDependencies to dependencies; in the lockfile only the importer entry moves. `convergePool` is the receive half of the datasource cluster bridge, and the projector's pool step. It now decides "code" from the same set instead of `row.origin !== 'runtime'`. It pools every other stored row as runtime, and stamps the record `origin: 'runtime'` before the record reaches the connect context. Editing a code-defined datasource is still refused at both doors. ### Why `Clause-②: no` The `origin` docblock in `packages/spec/src/data/datasource.zod.ts` reads: "`runtime` — created via the Studio wizard, persisted in the runtime metadata store, environment-scoped, editable", and "Never accepted from client input". The published contract already says such a datasource is editable, so the admin door's refusal of a metadata-door datasource as code-defined was a false refusal. No key, export or stored shape moves. `projectionApplied` now appears on the answer to a datasource write through the metadata door. That key is already declared optional on the protocol's write answer, so this adds no new key to a published payload. ## Measured on the base (`c9761cd2fb`): the new door pin is red `datasource-meta-door-reaches-admin-door.dogfood.test.ts`, run on the unmodified base, gave 4 failed and 2 passed: - `:174` same boot, after `PUT /meta/datasource/dogfood_meta_none_21923` answered 200: `expected undefined to match object { origin: 'runtime', …(1) }`. The admin door did not list it. - `:193` an admin-created datasource, then `PUT /meta/datasource/dogfood_admin_rt_21923`: `409 METADATA_CONFLICT`, "Expected parent hmac-sha256:… but current is null." - `:206` after a restart: received `"origin": "code"`, expected `"runtime"`. ## Mechanism hypotheses, measured 1. **Does any code registration reach the MetadataService without its name in the set?** This was measured with `bootStack` on HEAD `493c13dbb3`, comparing `metadata.list('datasource')` against `code-datasource-names`: | composition | listed at boot | code set | listed but outside the set | |---|---|---|---| | showcase | `default`, `showcase_external` | `default`, `showcase_external` | none | | crm | `crm_analytics`, `crm_primary`, `default` | `crm_analytics`, `crm_primary`, `default` | none | | multi-package | `default` | `default` | none | A package installed after boot does not register datasources in the MetadataService at all: `registerApp` and the `sys_packages` rehydrate write only the engine registry. So the admin door never lists one. The three residual paths, read and not measured on a boot, are under Acceptance notes. The fix never falls back to `?? 'code'`. 2. **Same-boot reach.** Verified on main (the `:174` failure above). The seam is the awaited projector, not `onMetadataMutation`: the metadata door answers only after the admin door lists the record, and a projection failure is reported on that answer as well as logged. - **A live pool is needed.** The admin door's create calls `registerPool`, which connects. The dogfood pin asserts `connected` for a metadata-door save in the same boot and again after a restart. Before the fix there was no pool, and after a restart the served `code` kept the datasource out of pool rehydration. 3. **Checksum.** The fix lands inside `service-datasource` by importing the repository's own computation. The residual, rows already stored without a checksum, is under Acceptance notes. 4. **Carrier notes.** - `convergePool` is covered by the provenance rule (above). - The `restoreRuntimeDatasources` and `rehydratePools` warnings that go only to `options.logger` are not changed. The bounded in-place-fix condition "same defect class" does not hold for them; see Acceptance notes. ## Tests (head `493c13dbb3`) - `pnpm --filter @objectstack/service-datasource typecheck`: exit 0. The package's `tsc --listFiles` includes the edited test file. - `pnpm --filter @objectstack/service-datasource test`: 41 files and 760 tests pass. - 7 new cases in `datasource-admin-plugin.test.ts`: the served origin, the projector registration, a metadata-door save, an edit and a delete reaching the admin door with their pool, a write under a code name changing nothing, a peer signal pooling by provenance, and the checksum. - One fixture re-judged: the "artefact (code) datasource" case now carries the code set, as the runtime fills it, instead of relying on a missing `origin`. - `datasource-system-context.pin.test.ts` follows `convergePool`'s new `ctx` parameter. - `pnpm --filter @objectstack/dogfood typecheck`: exit 0. - Four dogfood files pass, 24 tests (after `pnpm --filter @objectstack/service-datasource build`, dist marker preflight present): the new pin, `datasource-restore-code-wins`, `meta-door-code-datasource`, and `external-import-code-datasource-namespace`. - The rejection pin asserts `code` plus `status`: the admin `PATCH` of `showcase_external` still answers `400 DATASOURCE_ADMIN_ERROR`, with the message's first sentence. ### Ablations (each mutation through `scripts/ablation-replace.mjs`, rebuilt, `ablation-dist-preflight` on both legs, restored to the HEAD blob `a7f28b52b967`, `git diff HEAD` empty, and the restore leg rebuilt) | put back | unit (26 in file) | dogfood | |---|---|---| | `origin ?? 'code'` in list and get | 3 red | new pin 3 red (`:174`, `:206`, `:222`) | | the record's own `origin` first (`r.origin ?? servedOrigin(…)`) | 2 red | new pin 2 red (`:175`, the body asserting `code` served as code) | | no projector (`void this.projectMetadataDoorWrites;`) | 4 red | new pin 4 red (`:174`, `:194`, `:206`, `:222`) | | `convergePool` reads `row.origin !== 'runtime'` | 3 red | new pin 2 red (`:178`, no pool) | | no checksum (`const checksum = null`) | 1 red | new pin 3 red (`:193` and `:221`, `409 METADATA_CONFLICT`) | | projector without its code-name guard | 1 red | `datasource-restore-code-wins` 1 red (`:271`, the repair `DELETE` unregisters the code definition) | Three first attempts did not run: the no-projector, `convergePool` and no-checksum mutations each failed the DTS build with an unused symbol (TS6133), so nothing was measured. Each was redone with a mutation that compiles. In every void attempt the restore leg was proven the same way. ## Gates (head `493c13dbb3`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` re-derived 78 families on this change. That is a superset of the 49 the dispatch listed; the additions come from the changeset, the lockfile and `package.json`. All 78 exit 0. `--ran` reconciles: 78 derived, 78 run, 0 NOT-MEASURED, each with a recorded exit code. One gate needed a rerun. `pnpm check:dual-build-cjs-loads` first exited 3 with PREREQUISITE NOT MET (8 unrelated packages had no `dist/`). After those packages were built, it exited 0: 106 require entry points across 66 packages load. Narrowed lint (`eslint --no-inline-config --format json`) over the 4 touched `.ts` files: - every one resolves a config (`--print-config`); - the JSON output counts 4 files, 0 errors and 0 warnings; - `eslint.config.mjs` enables no type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Rows stored before this release.** An admin-created datasource row written before this release has no `checksum`, so the metadata door still answers it 409 until the admin door next writes it; one admin edit is the remedy. The asymmetry itself is in `metadata-protocol`'s `sys-metadata-repository.ts`: `rowToItem` serves `row.checksum ?? hashSpec(body)` while `put` and `delete` compare the raw column. It holds for any type's null-checksum row, and is reported to the seat rather than edited here. - **Code datasources the code set does not cover** (read, not measured on a boot): - Dev artifact HMR (`artifactWatch`) re-registers the artifact's datasources through the MetadataPlugin door. A datasource added to the artifact after boot is not in the set, because `AppPlugin` memoizes its owners, so the admin door serves it as runtime until a restart. - The legacy `FilesystemLoader` (a `datasource/` directory under the metadata root) lists datasources nothing adds to the set. - A host that composes the artifact door without `AppPlugin` or `DefaultDatasourcePlugin` registers no set, so nothing is code there; the boot restore already treats such a host that way. The remedy for each is on the producer side: contribute to the set. It is not a consumer default. - **Cluster.** The projector runs on the writing replica only; the protocol's cluster channel replays mutation listeners, not projectors. So a metadata-door datasource write does not converge peer replicas' MetadataService or pools until they restart. Before this change no replica converged. Cross-replica MetadataService coherence is a separate, open measurement. - **Lost warnings under `os serve`.** The existing `restoreRuntimeDatasources` and `rehydratePools` warnings still go only to `options.logger`, which `os serve` does not pass. This change widens the population that reaches `rehydratePools`, because metadata-door datasources now rehydrate. The new projector adds no log line of its own: a projection failure is reported on the metadata door's answer (`projectionApplied`) and logged by the protocol. - **Not this card.** The metadata door's own `GET` serves a stored row under a code-defined name (the overlay read); objectstack-ai#21922 remains open for that half. ## Files - `packages/services/service-datasource/src/datasource-admin-plugin.ts` - `packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts` - `packages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.ts` - `packages/services/service-datasource/package.json`, `pnpm-lock.yaml` (`@objectstack/metadata-core` becomes a dependency) - `packages/qa/dogfood/test/datasource-meta-door-reaches-admin-door.dogfood.test.ts` (the door pin, declared on objectstack-ai#6024) - `.changeset/21923-datasource-origin-from-provenance.md` (`@objectstack/service-datasource` patch) --- _Generated by [Claude Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ource's code definition while a stored row exists (objectstack-ai#21985) Fixes objectstack-ai#21922 Clause-②: no ## What changes The metadata door no longer serves a stored `sys_metadata` row under a datasource name the host registers from code. While such a row exists, `GET /api/v1/meta/datasource/:name`, the `GET /api/v1/meta/datasource` list and the `effective` layer of `/layers` all serve the in-memory code definition. That is what the admin door and the boot restore already serve. This is the remaining half of this card, after the boot restore and admin door half landed in PR objectstack-ai#21965. The change is the shape triage's answer A names (comment 6013780883), the one the shipped-flow read already uses. In `packages/metadata-protocol/src/protocol.ts`: - **One predicate.** `declinesStoredRow(type, name)` asks "is this a name whose stored row the active reads never adopt?". It holds for exactly two kinds of name: a shipped flow name (`isShippedFlowName`, unchanged) and a code-defined datasource name (`isDeclaredCodeDatasource`, unchanged, reading the host's `code-datasource-names` set and then the installed packages). Its registry twin, `isStoredEntryOfDeclinedName`, catches the hydrated bare-key copy of such a row (`!isCodeArtifactBody`). ⛔ No third precedence path, and ⛔ no decision read from any `origin`. - **`getMetaItem`**: the stored-row step skips the row on the ACTIVE read (`storedRowDeclined`). Step 2 then serves the MetadataService's in-memory registration. The row is still FOUND (`storedRowServed`), so `servedLockState` keeps `deletable: true` while it exists. A draft read (`state: 'draft'`, or `previewDrafts`) is answered as a draft. - **The list** (`readFlattenedMetaItems`, both faces): the registry half drops the hydrated copy, and the stored-row half keeps the row out of the merge and out of the package stand-ins. The MetadataService base, merged in below, supplies the code definition. - **`getMetaItemLayered`**: `effective` asks the same predicate. The spec defines `effective` as "the value an ordinary `GET /meta/:type/:name` would return under `item`" (`GetMetaItemLayeredResponseSchema`), so it has to move with the by-name read. The row stays reported in `overlay`. - Every other type keeps ADR-0005's read order. The `/meta` DELETE path is untouched: its own row probe still finds and removes the row. ## Hypotheses (Partition 2), measured at `f76c6221ac` - **H1, confirmed.** `getMetaItem` adopted the row at `protocol.ts:10038` (`findServedOverlayRow`) and `:10047` (`if (record && !shippedFlowActiveRead)`), whatever the MetadataService held. - **H2, confirmed and extended.** The objectstack-ai#20946 shape is the by-name stored-row half (`:9952`–`:9978`), the list's registry half (`:8923`–`:8926`) and the list's stored-row half (`:8979`–`:8998`). `isDeclaredCodeDatasource` (`:16326`) reads the host set at `:16328`. Measured addition: the list needs the registry half too. The boot pull (`loadMetaFromDb`) and an unscoped list hydrate the row under the bare key, and the registry layer is merged OVER the MetadataService base (`:9287`). Ablation B below shows that half is load-bearing on its own. `getMetaItem` needs no registry half for datasource, because step 2 answers before step 3. - **H3, confirmed with no new rule.** `servedLockState` (`:16488`) reads `storedRowServed`, which is set from the row FOUND (`:10045`) before the adoption check, so a declined row keeps `deletable: true`. That is exactly what `originGatedRemovalRefusal`'s docblock states ("`deletable` true exactly while the read found a stored row"). `deleteMetaItem` (`:25771`) decides from its own probe and never reads the served body. The `_lock` gate's overlay layer is read whether or not the row is adopted, as for flows. Triage's stop condition is not met. - **H4, confirmed disjoint.** objectstack-ai#21967's branch touches `protocol.ts` hunks at `:2003`, `:9051`–`:9107`, `:9610`–`:9715` and `:19206` (read from its pushed branch). None of this PR's sites overlaps them. `origin/main` was merged (`65d7b740b2`); objectstack-ai#21967 had not landed. ## Pins - **Unit, `metadata-protocol`** (`protocol.code-defined-datasource-door.test.ts`, a new `[objectstack-ai#21922]` block on both kernel shapes, 14 cases): - (a) by name (both spellings), in the list before and after the row is hydrated, `default` through the host set, and the layered `effective`; - (b) the DELETE still removes the row, and the reads serve code after it; - (c) a runtime datasource's row is still served by name, in the list and as `effective`; - (d) the strict draft read and the preview arm serve the draft row. - The block reuses the file's pinned engine double, so the pinned ledger is untouched. That double's `find` now honours `where` and `limit` (it returned every row), and its registry gains an opt-in hydrating slot plus `isPackageDisabled`, which the list calls. - **Dogfood** (`datasource-restore-code-wins.dogfood.test.ts`, one case): after a restart over stored rows under `showcase_external` and `default`, `GET /api/v1/meta/datasource/:name` and the `/meta` list serve the code definition (label, `origin: 'code'`, `_packageId`, and never the row's file). A runtime datasource's row is still what both doors serve. The repair case that follows still removes each row. - **Fixture triage:** `meta-door-code-datasource.dogfood.test.ts` read `SHADOW_LABEL` after its restart. That read pinned exactly the branch this change removes, so it now expects the code label. The restore file's header paragraph saying the meta door serves the row is replaced. ## Reverse verification (on committed HEAD, restore proven by blob) - **Ablation A (unit):** `return typeof name === 'string' && name !== '' && this.isDeclaredCodeDatasource(type, name);` was replaced by `return false; // ABLATION-21922` via `scripts/ablation-replace.mjs` (anchor 1 to 0, blob `7890c4b99f6e` to `6d70364fc822`). **10 failed / 41 passed**: (a)x4 and (d) on both kernels. (b) and (c) stay green, as predicted, because they do not depend on the decline. Restored: blob == HEAD (`7890c4b99f6e`), `git diff HEAD` empty. - **Ablation B (unit, registry half only):** the list's filter reverted to `isStoredFlowEntryOfShippedName`. **4 failed / 47 passed**: exactly the post-hydration list cases (`showcase_external` and `default`) on both kernels. Restored by blob. - **Ablation C (public door, through `dist/`):** the same arm replaced by `return name === 'ABLATION-21922';`, then `pnpm --filter @objectstack/metadata-protocol build`, then `ablation-dist-preflight.mjs @objectstack/metadata-protocol ABLATION-21922` (exit 0, marker in dist), then both dogfood files: **3 failed / 9 passed**. The two direct reds are the new case and the flipped read. The third ("after the repair and a restart") is a cascade: the flipped case failed before its DELETE, so the row survived the restart and the ablated read served it. Restore leg: blob == HEAD, rebuild, then `ablation-dist-preflight --absent` (exit 0). ## Readings at head `65d7b740b2` (after the `origin/main` merge) - `pnpm --filter @objectstack/metadata-protocol test`: **218 files passed, 3 skipped; 28010 tests passed, 19 skipped**. `typecheck` (`tsc --noEmit`): exit 0. The package `tsconfig` includes `src/**/*`, so the edited test file is compiled. - Dogfood, both files: **12/12 passed**. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts up to date (spec moved on the `main` side of the merge). - `node scripts/pm/dispatch-gates.mjs --commands` (no paths; 5 paths vs merge base `04e776b39`): **68 derived; `--ran`: 68 run, 0 NOT-MEASURED, 0 UNRUN**. `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET` (exit 3, eight packages outside the dogfood closure had no `dist/`). It answered exit 0 after those were built, and the record carries that reading. - Artifact-roster block (53 rows, outside the total): 50 exit 0. `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths` answered NOT WIRED (exit 2: no PR context existed yet). They are NOT MEASURED here, and the report carries their run against this PR. - Symbol-anchor sweeps: `check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and `check:adr-anchors` all exit 0. - **Lint, a proven narrowing** (repo-wide `pnpm lint` is CI's): `eslint --no-inline-config --format json` over the four touched TypeScript files at `65d7b740b2` reports 4 files linted, none ignored, 0 errors and 0 warnings. The config never enables type-aware linting (no `parserOptions.project`, as `eslint.config.mjs` states), so this diff cannot move any untouched file's verdict. ## Changeset, measured against the built entry declarations `@objectstack/metadata-protocol` patch, with `Clause-②: no`. The BASE (`f76c6221ac`) and HEAD builds of `dist/index.d.ts` and `index.d.cts` differ in two non-comment lines only: `private declinesStoredRow;` and `private isStoredEntryOfDeclinedName;` on `ObjectStackProtocolImplementation`. No public member, parameter or return type moves. ## Named gap, not moved here: the `/published` door Measured with a throwaway probe at `8c4bd140de` (never committed): after a restart over a stored row, `GET /api/v1/meta/datasource/showcase_external` serves the code definition, and `/layers` answers `effective` = code with `overlay` = the row. `GET /api/v1/meta/datasource/showcase_external/published` still serves the row (`origin: runtime`). The REST door and its runtime dispatcher twin decide by asking the public `isShippedFlowName` alone. The spec describes that route as serving the active overlay row, so no published text is made false. But following the layered read there needs a public predicate on the exported class, which is a widening this claim's `Clause-②: no` does not cover. It is reported to the seat with its options. ## Acceptance notes - `getMetaItem`'s step-3 registry half stays flow-only. A code datasource is never a SchemaRegistry item, and step 2 has already served it. - A declined row is still hydrated into the registry as the tenant row it is, following the flow precedent. `git grep` finds no reader of registry `datasource` entries outside `protocol.ts`. - Read-only inference, not measured: a name declared by a package installed after boot is code to `isDeclaredCodeDatasource` (manifest) with no in-memory registration. For such a name the reads now fall through to whatever the MetadataService loaders hold, not the row. - Deviation, self-inflicted and recovered: a declaration measurement's restore (`git checkout HEAD -- protocol.ts`) discarded one uncommitted docblock edit. It was detected by a marker count, re-applied and committed as `0b2a164598`. Every reading above was taken after it. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #21922
Fixes #21944
Clause-②: no (narrowing)
What changes
A code-defined datasource (a
*.datasource.tsthe installed artifact declares, or the host's owndefault) is read-only by published contract:DatasourceSchema.originsays "code — authored as*.datasource.ts, GitOps-owned, read-only in the UI", the datasource registry entry inmetadata-plugin.zod.tssays code-defined datasources "win on name collision", anddatasource-admin-service.tssays "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 seedefaultat 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"):
packages/runtime/src/code-datasource-names.ts, new). One in-memorySetof the datasource names the host registers from code, on the kernel servicecode-datasource-names.contributeCodeDatasourceNamesregisters it on first use and adds to it after that, the shapeseed-summaryuses.init().AppPlugin.init()adds every datasource the artifact declares: the same list itsstart()registers in the MetadataService, now memoized so the two phases read one answer.DefaultDatasourcePlugin.init()addsdefault. Phase 1 completes before anystart(), so the set is whole before the restore runs, whatever order the plugins were composed in.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'soptions.logger, or to the kernel logger when the host passes none (os servepasses none).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 answersdefaultthe way it answers every code-defined datasource since PR fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one #21942.⛔ "Code" is never read from a stored row's
origin, the MetadataService slot'sorigin, the connection service'sConnectResult, or a request body'sorigin.Measured on a booted showcase
The harness is the
@objectstack/verifybootStackwith the datasource-admin routes mounted the wayserve.tsmounts them, in a temp cwd. The stored rows assertorigin: 'runtime'and their ownconfig.filename(the cards' case (b)). They were written through the metadata door's repository on the runtime-only intent, then the stack restarted. BEFORE is76fec88b16; AFTER is this branch at1d840709ae. The readings come from a throwaway probe that was never committed; the committed pins below assert the AFTER column.showcase_externalorigin: runtime, label "Shadow 21922"origin: code, "External Analytics (SQLite)"PATCH /api/v1/datasources/showcase_externalDATASOURCE_ADMIN_ERROR"… is code-defined and cannot be edited at runtime."defaultalready-registeredbecameconnectedalready-registeredshowcase_ext_customerreadPUT /api/v1/meta/datasource/defaultNOT_OVERRIDABLEDELETE /api/v1/meta/datasource/default, no stored rowNOT_OVERRIDABLEDELETE /api/v1/meta/datasource/showcase_external(repair), then metaGETin the same bootGETkept serving the stored edit until the next restartGETserves the code definitionThe dispatch's mechanism hypotheses
service-datasource(serve.ts,standalone-stack.ts, the verify harness),DefaultDatasourcePluginandAppPluginareuse()d beforeDatasourceAdminServicePlugin. None of the three declares an ordering edge to another, so theirstart()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.getServicesRegistry()), with nopackages/specchange. The ObjectQL registry was rejected: the engine's datasource definitions mix both origins, and a host package record would be a fabricated provenance.origin: 'runtime', and the slot the refusal reads holds AppPlugin's explicitorigin: 'code'. Under ablation A the admin door served the stored row asorigin: runtime, so the refusal cannot come from the admin read'sorigin ?? 'code'default.default: yes. The restored row reachedrehydratePools, which opened a second pool nameddefaulton the row's file. Routing did not move, because the engine never routes to a driver nameddefault; the default driver keeps its natural name. It is the same defect and the same decision fixes it, pinned bygetDriverByName('default')and the connect verdict. Forshowcase_external, nothing was re-pointed at boot in this composition: AppPlugin's connect ran first, so the rehydrate answeredalready-registered. The adminPATCH200 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)
code-datasource-names.tsis not re-exported frompackages/runtime/src/index.ts.service-datasourceandmetadata-protocolspell the service name privately and read the value structurally ashas(name), the way'datasource-connection'is read today.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 packagesminor, which holds under either reading.Named gap: the metadata door's read while a stored row exists
GET /api/v1/meta/datasource/:namestill 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 metaGETserved the stored row, forshowcase_externaland fordefault. 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 repairDELETEhas run, in the same boot. That read lives ingetMetaItem's overlay step, outsideisDeclaredCodeDatasource, andprotocol.tsis held by #21934 in other regions, so it is left to the seat.meta-door-code-datasource.dogfood.test.tsalready pins that read as it is.#21922 stays open for that read: this PR is
Part ofit, and the seat routes the remaining half through triage when it merges.Landing beyond the claim's named files
packages/runtime/src/app-plugin.tsis the producer of the packages' half of the set, in the declaredruntimepackage. The memo also makes its residual-owner warning print once instead of once per phase.packages/runtime/src/code-datasource-names.tsis new in the same package.Patch round 1 (head
d77e150701)Review
6011282321on #21922. The Tests, Ablations and Gates sections below are round 0's, at80fbcfdea6; this section carries the readings on the current head.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 anystart(). The resolution is deferred because the names come from the artifact'scollections, which walkpackages[].AppPlugin.init()'s manifest registration is the one thing ininit()allowed to touchpackages[], pinned byplugin-dev's malformed-stack falsifier, which this PR's first head turned red. The kernel service now holds aCodeDatasourceNames, a set with pending contributions; readers still usehas(name)only. A contribution that throws stays pending and rethrows to every reader.defaultrefusal names what defines it: the host's database configuration (the database URL the server starts with). It names no*.datasource.ts, because none declaresdefault. Every package-declared datasource's sentence is byte-identical, andcode,statusand the refused set are unchanged (packaged-base-regime.ts, the datasource row'shostOwned).const listOf = (spacing restored inapp-plugin.ts. Mergedorigin/main80f9f7e6baas49421a8fe6.defaultturned 6 unit cases and 1 dogfood case red, and was restored by blob.d77e150701(eachVERDICT command-exit 0):service-datasource: 748 / 748;metadata-protocol: 27978 passed, 19 skipped;runtimelocal: 4668 passed, 19 skipped;plugin-dev: 86 / 86;runtime(cli, client, verify, http-conformance, cloud-connection): all passed;d77e150701:dispatch-gates --commandsderived 72, reconciled with--ranas 72 run and 0 not measured, pluscheck:init-service-contractandcheck:startup-registry-verdict. All exit 0.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.--project isolated:datasource-restore-code-wins.dogfood.test.ts(new) andmeta-door-code-datasource.dogfood.test.ts, 2 files, 11 passed.typecheckgreen forservice-datasource,metadata-protocol,runtime(including itscheck:test-typecheckledger, held) anddogfood.tsc --listFilescounts each touched test file once in its program.datasource-admin-plugin.test.ts(the restore), 4 incode-datasource-names.test.ts(the set and its phase), and 2 resolver plus 8 door cases inprotocol.code-defined-datasource-door.test.ts(default, on both kernel shapes).Ablations
Each one was committed first and mutated with
scripts/ablation-replace.mjsin wrap mode, under a shell trap. For subjects resolved throughdist/, the package was rebuilt andablation-dist-preflight.mjsproved the marker was present. The restore leg was rebuilt and proven--absent, the blob equalled HEAD,git diff HEADwas empty, and the tree was clean.datasource-admin-plugin.ts; marker in 2 files ofservice-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 servedshowcase_externalas the stored row, and the repair case read the same.protocol.ts; marker in 2 files ofmetadata-protocol/dist). Unit: 5 failed, 30 passed (the resolver case, andPUTplus no-rowDELETEofdefaulton both kernels). Dogfood:PUT /meta/datasource/defaultanswered 200. The next case then failed as a cascade, because the row thatPUTstored made the seed conflict. The repairDELETEand runtime controls stayed green, as expected.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 frominit(); thestart()registration never fills it.Gates (head
80fbcfdea6)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 72 commands on the actual change, a superset of the 52 at dispatch. All 72 ran with exit codes captured before any pipe.--ranprinted "72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED".check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3) because eight packages outside this diff had nodist/. After building them it measured green.check:init-service-contract("34 declared / 1 self-provided / 3 without a workspace provider") andcheck: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.eslint --no-inline-config --format jsonon the 9 touched source and test files reported 9 files, 0 errors and 0 warnings.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules, as its own comment states), so this diff cannot move any untouched file's verdict.Acceptance notes
defaultrefusal's remedy names the host's database configuration (patch round 1).convergePoolreads a stored row directly and is unchanged. Its signals come from peer admin writes, and the admin door now refuses those for code names.options.logger, whichos servedoes not pass. They are unchanged here.listDatasourceRecords,getDatasourceRecordorpersistDatasourceRow. One interaction: a metadata-door-created datasource with nooriginstill restores, and is still read ascodeby the admin door's default.origin/maingained fix(spec): serve a field's translated help ondescription, never on an undeclaredhelp#21956, docs(qa): checklist item for public-form withdrawal layering (17.7 security follow-up) #21964 and test(spec): the first api/ file group's test titles state each cited decision in words instead of a tracker number (stage 24) #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