Repository navigation
fix(service-datasource): the admin door reads a datasource's origin from provenance, and a metadata-door write reaches it in the same boot - #21977
Conversation
…reeing on a runtime datasource Red on the base: a metadata-door datasource is missing from the admin door in the same boot and reads as code after a restart, and an admin-created datasource answers 409 at the metadata door. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
… project metadata-door writes, stamp the row checksum - The admin door's list and read serve origin code only for a name in the host's code-datasource set, runtime otherwise, never the record's own origin or a default for its absence. - A datasource the metadata door saves, publishes, reverts or deletes reaches the admin door in the same boot through the protocol's awaited datasource mutation projector: the MetadataService slot follows the stored row and the live pool converges on it. - Cluster convergence decides code by the same set and pools every other stored row as runtime. - An admin-written sys_metadata row carries the repository's checksum, so the metadata door's optimistic lock matches it. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…or projector, convergence by provenance and the row checksum Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…and metadata-door reach fix Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 1 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 91bb125dbf8d2cb3ffe6140b40a0c12b706ee395 && git checkout 91bb125dbf8d2cb3ffe6140b40a0c12b706ee395
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f76c6221acd3997dd778fdd3e8e7d43e0bec4851 493c13dbb312ec5123605bfd2bcf20522bf55d30 && git checkout -B drift-repro f76c6221acd3997dd778fdd3e8e7d43e0bec4851 && git merge --no-ff 493c13dbb312ec5123605bfd2bcf20522bf55d30
node scripts/docs-audit/affected-docs.mjs --json f76c6221acd3997dd778fdd3e8e7d43e0bec4851
|
…ed decision in words instead of a tracker number (stage 26) (objectstack-ai#21987) Part of objectstack-ai#20749 Clause-②: no Stage 26 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 first name-ordered `system/` group: the 18 id-bearing test files under `packages/spec/src/system/` from `auth-config.test.ts` to `metadata-form-declared-rows.pin.test.ts`, with `constants/system-names.test.ts` in its path position. Those files carried 91 messages and 97 tracker ids, citing 54 records. All 97 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. ## Census at the base (`dcf3eb494a`) 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 25 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 `dcf3eb494a`, the claim's base and stage 25's landing. Both instruments read **282 messages / 297 ids in 71 files**, the seat's reading and stage 25's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `system/` (this PR: 18 of its 34 files) | 34 | 154 / 167 | 128 / 138 | 26 / 29 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ui/` | 5 | 7 / 7 | 0 | 7 / 7 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **71** | **282 / 297** | **245 / 257** | **37 / 40** | The group reads **91 messages / 97 ids in 18 files**, the seat's figures file for file: | file (under `system/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `auth-config.test.ts` | 2 / 2 | 2 / 2 | 0 | | `book.test.ts` | 2 / 2 | 2 / 2 | 0 | | `cache.test.ts` | 1 / 1 | 1 / 1 | 0 | | `collaboration.test.ts` | 1 / 1 | 1 / 1 | 0 | | `compliance-families-retirement.test.ts` | 7 / 7 | 4 / 4 | 3 / 3 | | `constants/system-names.test.ts` | 1 / 1 | 1 / 1 | 0 | | `core-service-provider.test.ts` | 1 / 1 | 1 / 1 | 0 | | `data-migration-flag-column-move.pin.test.ts` | 2 / 3 | 2 / 3 | 0 | | `disaster-recovery.test.ts` | 1 / 1 | 1 / 1 | 0 | | `email-config.test.ts` | 2 / 2 | 2 / 2 | 0 | | `email-template-floor-locale-parity.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `environment-artifact.test.ts` | 5 / 5 | 5 / 5 | 0 | | `http-server.test.ts` | 2 / 2 | 2 / 2 | 0 | | `i18n-resolver.test.ts` | 53 / 56 | 50 / 53 | 3 / 3 | | `job.test.ts` | 4 / 4 | 4 / 4 | 0 | | `logging.test.ts` | 1 / 3 | 1 / 3 | 0 | | `message-queue-retirement.test.ts` | 3 / 3 | 1 / 1 | 2 / 2 | | `metadata-form-declared-rows.pin.test.ts` | 2 / 2 | 2 / 2 | 0 | | **18 files** | **91 / 97** | **83 / 89** | **8 / 8** | Ten more test files sit in the same name range and carry no id (`constants/platform-object-names`, `constants/platform-tool-names`, `core-services`, `deploy-bundle`, `doc`, `encryption`, `i18n-resolver.nav-label-identity`, `i18n-resolver.object-list-views`, `job-pull-organization`, `license`). The eight "other" strings are expect failure messages, rewritten and declared to the text-only tool: `compliance-families-retirement.test.ts:159` (a template literal), `:210` and `:494`; `message-queue-retirement.test.ts:87` (a template literal) and `:127`; `i18n-resolver.test.ts:3288` (the second leaf of a `+` chain), `:3299` and `:3303`. - **Controls.** Lit: `system/metrics.test.ts` (4 / 5) and `system/translation.test.ts` (12 / 12), outside the group, read the same at the base and at the head. Dark: `i18n-resolver.test.ts` reads 0 at the head while 47 of its lines still carry `#` plus digits: 45 comment lines and the two `(batch objectstack-ai#58)` titles below. Planted in a scratch tree: an id put into the rewritten `i18n-resolver.test.ts` "per-component copy" title reads 1 / 1 (`title:describe`), and an id put into a `job.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in 17 of the 18 files at the base. In `i18n-resolver.test.ts` it reads two more: `:1744` and `:1757` end in "(batch objectstack-ai#58)", a two-digit decision-batch label that the gate's three-to-five-digit pattern does not count. Neither literal carries a counted id, so both stay, in the form earlier stages kept batch labels (stage 21's `批 17`, stage 22's `批 14` / `batch 13`). At the head the wider pattern reads 0 in 17 files and those 2 in `i18n-resolver.test.ts`. - **At the head:** 191 messages / 200 ids in 53 files. The 18 files read 0 / 0, `system/` reads 63 / 70 in 16 files, and no other file moved. ## How the area was chosen `system/` is taken in path-ordered file groups near the ~100-id bound, the rule stages 20 to 25 used, with `constants/system-names.test.ts` sorting at `constants/`. Stage 25's cut named this group at 97 ids, and this census reads 97, so no re-cut was needed. `i18n-resolver.test.ts` (56 ids; objectstack-ai#21948 landed into it at `bab76850cd`, and its titles were read at this base) fits one PR and one text-only proof, so it is not split. **Named for the next stages** (cut from the head census, 191 / 200): - **the second `system/` group:** `metadata-form-zod-reconciliation.test.ts` through `worker.test.ts`, 16 files, 63 messages / 70 ids (45 / 49 titles, 18 / 21 other). Its first file alone carries 21 / 24, 17 of them "other" strings (its ledger `why` entries). - The files directly in `src/`, 120, one stage. - The needles: one stage, with an at-tier review. The four colour literals stay, as stage 21 decided. ## What each id became - **23 literals (25 ids)** now state a decision in words. - **11 literals (12 ids)** get their subject back in words, where the number stood for a thing. - **57 literals (60 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. 54 records are cited: 51 answer 200 (objectstack-ai#15514 is a PR, merged as `bf1054a4c0`) and 3 answer 404. The three that answer 404 were read from what landed, through the commits endpoint (this checkout is shallow), each found through its CHANGELOG entry or the file's own commit anchor: - **objectstack-ai#10926**, from `d173125fb8` (objectstack-ai#11438): the component-translation `submitLabel` copy key is retired (maintainer ruling 2026-08-22, option A); - **objectstack-ai#12961**, from `901355c3bc` (objectstack-ai#13111): `translatePage` descends into declared `properties.children` ("Ruled 2026-08-29 (option A)"; region-level id wins a collision); - **objectstack-ai#13218**, from `c45d8e6b4e` (objectstack-ai#13641): `walkAddressedPageComponents` is exported as the one addressed-component walk, consumed by `translatePage` and the CLI extractor alike. **The same-id title stage 25 listed:** `book.test.ts:413` "ResolvedBookSchema is the book-tree response contract (objectstack-ai#12038)". Its body parses the real `resolveBookTree()` output, accepts the honest-empty tree and pins each Zod schema type-identical to its interface: a describe-only transcription, which the five-part ruling's implementation plan names (`5434804846`). None of 1C · 2C · 3A · 4A · 5A is pinned, so no letter is named; the title already says the decision, and only the number goes. **"ruled:" appears in two titles,** each on a record that carries a ruling: - `data-migration-flag-column-move.pin.test.ts:72`: "(ruled: one nullable datetime on the flag row)". objectstack-ai#15989's comment `5556979386`, headed "The four questions — ruled", answers Q1 with A: a new nullable datetime field on `DataMigrationFlagSchema`, whose failure mode is absence. objectstack-ai#16185's body restates it ("The mechanism was ruled on objectstack-ai#15989 as A"). - `i18n-resolver.test.ts:4944`: "(ruled: the authored label is the default-locale text)". objectstack-ai#15711's maintainer ruling A, recorded in `5549577889`. **Stated in words** (23 literals): | record | literal (under `system/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#11739 | `auth-config.test.ts:423` | "AudienceConfigSchema — one closed audience posture, invite_only when undeclared" | One declared posture, a closed vocabulary (`invite_only` / `email_domain` / `open`), `invite_only` when undeclared, as the body records it ruled. | | objectstack-ai#15679 (objectstack-ai#14478 ruling B) | `cache.test.ts:336`, `collaboration.test.ts:1005`, `disaster-recovery.test.ts:235` | "… carry their unit in the key name" / "… carries its unit in the key name" | The `system/` duration keys carry their unit in the key name, stage 25's objectstack-ai#15677 reading. | | objectstack-ai#15513 | `compliance-families-retirement.test.ts:210`, `:494` (expect messages) | "… being undone — the three families were retired whole, none roadmapped" | Maintainer ruling A (`5548577921`): the three compliance-shaped families are retired whole; none is roadmapped. | | objectstack-ai#15989 Q1, via objectstack-ai#16185 | `data-migration-flag-column-move.pin.test.ts:72` | "columns_moved_at — absence is the contract (ruled: one nullable datetime on the flag row)" | Above. | | objectstack-ai#16185 constraint 2 | `data-migration-flag-column-move.pin.test.ts:126` | "the ONE arbiter is unchanged by the new member — read beside it, never inside it" | `isDataMigrationFlagVerified` is not touched; the new field is read in addition to the arbiter, never inside it. | | objectstack-ai#19184 | `job.test.ts:881` | "JobSchema's own @example — copied verbatim, it parses" | The example stops opening with the retired `id`, so a verbatim copy is accepted. | | objectstack-ai#8075 | `message-queue-retirement.test.ts:127` (expect message) | "… being undone — the family had no consumer and carried an inline credential" | Fork (b): exported but unconsumed, the credential-bearing shape is retired whole. | | objectstack-ai#16772 | `i18n-resolver.test.ts:1207` | "translateDashboard — global filters, addressable from a bundle" | Finding B: `dashboards.NAME.globalFilters` becomes a bundle group. | | objectstack-ai#6080 | `:1573` | "per-component copy, keyed by component id" | Page component copy gets a bundle address by component id. | | objectstack-ai#13218 (404) | `:2215` | "walkAddressedPageComponents — the one addressed-component walk, shared by the resolver and the CLI extractor" | What landed in `c45d8e6b4e`. | | objectstack-ai#5377 | `:2652` | "resolveTabLabel — filter-preset tab labels, keyed under `_tabs`" | Tab labels get the `objects.OBJECT._tabs.TAB.label` key and a resolver. | | objectstack-ai#3370 | `:3049` | "translateObject inline actions — served with their `_actions` translations" | Declared action labels are translated, not served as English literals. | | objectstack-ai#3833, objectstack-ai#3847 | `:3131` | "resolveObjectFieldLabels — the declared rich entries, read from the nested translation shape" | objectstack-ai#3833: the nested shape, never the retired flat dialect; objectstack-ai#3847: the rich `label` / `help` / `options` entries the response declares. | | objectstack-ai#7679 | `:3308` | "normalizeSupportedLocales — the declared supportedLocales narrow the advertised set" | `/i18n/locales` reports the app's declared locales, not every loaded bundle. | | objectstack-ai#11287 | `:3732`, `:4134` | "translateFlow — screen-flow copy from the `flows` bundle group" / "resolveFlowScreenTitle — a screen title from the `flows` bundle group" | The resolver half that reads `TranslationData.flows`. | | objectstack-ai#3786 | `:4726` | "… the REST boundary follows — one derived list, no hand-kept copy" | Derive from the one source instead of hand-copying a "keep in sync" list. | | objectstack-ai#14882 | `:4853` | "a declared fallback chain, not a literal `en`, at the resolver" | The chain honours the declared `fallbackLocale`, not a hard-wired `['en']`. | | objectstack-ai#15711 | `:4944` | "… for a default-locale request (ruled: the authored label is the default-locale text)" | Above. | | objectstack-ai#16458 | `:5011` | "resolveMetadataFormSchemaTitles — bundle labels overlaid as JSON Schema titles, repeater rows included" | Item-level property names get a translated title through the array's `items`. | **Subject back in words** (11 literals): - the two "zero holders after objectstack-ai#15513" / "after objectstack-ai#8075" template messages become "after the families' retirement" / "after the family's retirement", stage 25's form; - "[objectstack-ai#15513] ADR-0087 registration" becomes "compliance families retirement — ADR-0087 registration"; - "keeps PR objectstack-ai#15514's fourteen deadline-key registrations" becomes "keeps the earlier deadline-key retirement's fourteen registrations": PR objectstack-ai#15514 retired the fourteen inert deadline keys before the families went whole; - "shipped by plugin-email since objectstack-ai#5087" becomes "shipped by the SMTP transport in plugin-email": objectstack-ai#5087 implemented that transport; - "objectstack-ai#17614 — the published "must stay equal" claim" becomes "EMAIL_TEMPLATE_FLOOR_LOCALE — the published "must stay equal" claim", the constant whose docblock carries it; - "(objectstack-ai#4666 pin)" becomes "(pinned here: a type change the key-level gates cannot see)": objectstack-ai#4666's maintainer ruling chose direction B, fingerprinting defaults only, so the checksum object-to-string type change stays invisible to the key-level gates, and these parses are its gate; - the three "pre-objectstack-ai#20680" / "pre-objectstack-ai#8284" / "pre-objectstack-ai#20731" control titles become "the behaviour before the override rule", the rule their describes name ("the catalog loses to an explicit override"); - "the contract call the objectstack-ai#12961 line left open, made by objectstack-ai#16772" becomes "the contract call the `children`-descent ruling left open, made for tab and accordion panels": the 2026-08-29 ruling (`901355c3bc`) named `properties.children` only, and objectstack-ai#16772 made the `items[].children` call. **Dropped where already stated** (57 literals, 60 ids). A number goes only where the title already says its decision. Examples: `[objectstack-ai#15513]` x2 and `[objectstack-ai#8075]` retirement describes and `[objectstack-ai#4740]`; the four other `[objectstack-ai#15711]` prefixes, once the ruling is stated in the first; the tails `(objectstack-ai#18124)`, `(objectstack-ai#4667)` x2, `(objectstack-ai#12038)`, `(objectstack-ai#4611)`, `(objectstack-ai#4451)`, `(objectstack-ai#5307)`, `(objectstack-ai#14865)`, `(objectstack-ai#4938)`, `(objectstack-ai#5295)`, `(objectstack-ai#16292)`, `(objectstack-ai#19085)`, `(objectstack-ai#20439)`, `(objectstack-ai#4854)`, `(objectstack-ai#21257)`, `(objectstack-ai#10926)`, `(objectstack-ai#12961)`, `(objectstack-ai#5775)`, `(objectstack-ai#20940)` x3, `(objectstack-ai#16772)` x4, `(objectstack-ai#5377)` x2, `(objectstack-ai#5728)` x2, `(objectstack-ai#3847)` x2, `(objectstack-ai#7634)` x4 (one title and three expect messages), `(objectstack-ai#8284)`, `(objectstack-ai#11745)`, `(objectstack-ai#14253)` x6; the `objectstack#14972` tail with its repository qualifier; the ADR-bearing tails `(objectstack-ai#4740, ADR-0049)`, `(objectstack-ai#20680, ADR-0029 D9.2a)`, `(objectstack-ai#20731, ADR-0029 D9.2a)`, `(objectstack-ai#14478, ADR-0087 …)`, which keep their ADR; `pre-objectstack-ai#4740` before "v0 artifact"; and `(objectstack-ai#17782, objectstack-ai#15939, objectstack-ai#14478)` on "logging duration keys → *Ms", whose title shows the rename. The two 404 numbers among them (objectstack-ai#10926, objectstack-ai#12961) go only where the title already states what landed. **No file is renamed.** ## Readers - **Needles:** none. The eight declared strings are assertion failure messages (the second argument of `expect`), none is an expected value. The three `.pin.test.ts` files read source text, and none reads an id: `data-migration-flag-column-move.pin.test.ts` reads `isDataMigrationFlagVerified`'s body for `verified_at`, `blocking` and the absence of `columns_moved_at`; `email-template-floor-locale-parity.pin.test.ts` reads `DEFAULT_TEMPLATE_LOCALE` and "must stay equal"; `metadata-form-declared-rows.pin.test.ts` reads registered form rows. `job.test.ts`'s `@example` pin reads the anchor "@example Metadata Sync Job (Cron)". No title or message in the group is matched against a source docblock or another file's text. - **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 18 files calls a snapshot matcher. - **Projects:** `compliance-families-retirement.test.ts` and `email-template-floor-locale-parity.pin.test.ts` are in the `repo` project (`packages/spec/vitest.repo-tests.json`); the other 16 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 (272 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 12 hits are sibling test titles: the three `(objectstack-ai#15679)` titles in this group hit each other (all rewritten here) and the five `(objectstack-ai#15679)` titles of the second `system/` group (`metrics`, `object-storage`, `registry-config`, `tracing`, `worker`), which go with that stage. ## 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 eight expect-message lines named above. - **Result:** 18 of 18 files SAME on all three legs, with the per-file counts predicted in writing before any edit. - **Totals:** 91 changed string leaves in 91 literals: 83 titles and 8 declared. The diff's `+` and `-` lines are exactly the 91 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 18 files were run at the base, before the edit, and at the head, in the same worktree, with `--project local --project repo`. Both sides read 783 tests in 18 files, all passed, with the same count and status sequence per file in 18 of 18. 354 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. No head name carries a gate-pattern id (354 base names did); two head names carry the kept "(batch objectstack-ai#58)". No source escape sits in a planned anchor, so the comparison tool met none. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 18 touched files are in it, and no `*.test.ts` at all (`files[]` ships `src/**/*.zod.ts`, not tests). The controls `src/system/job.zod.ts`, `src/system/translation.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 `ed2bc649f3`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (turbo exit 0, recorded to a file; `VERDICT batch-last-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 619 files, 18485 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 18 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, the same 79 as stage 25. 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` `4e4e881427` 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 18 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 18 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 182 changed lines (+91 / -91). - A control-byte scan over the 18 changed files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was three commits past the base (`4e4e881427`: objectstack-ai#21976, objectstack-ai#21977, objectstack-ai#21352). They touch 490 files, none of the 18 and none under `packages/spec/src/system/`, so `main` was not merged. The census of `4e4e881427` with this diff applied reads 191 / 200, file for file the same as the head. `git merge-tree` onto `4e4e881427` is clean, and none of the 5 open PRs touches any of the 18 files. ## Acceptance notes - **Same-id test titles in this card's later stages** go with those stages: 22 lines, among them the five `(objectstack-ai#15679)` titles of the second `system/` group, `system/translation.test.ts`'s six (`objectstack-ai#16772`, `objectstack-ai#6080`, `objectstack-ai#10926`, `objectstack-ai#21257`, `objectstack-ai#11287`, `objectstack-ai#4667`) and `stack-email-template-locale-floor.test.ts`'s four `objectstack-ai#17614` titles. - **Same-id test titles in other packages** stay: 78 lines in 14 packages (`driver-sql` 13, `cli` 12, `objectql` 10, `rest` 10, `lint` 8, `platform-objects` 6, `plugin-auth` 6, `core` 3, `runtime` 3, `service-i18n` 3, and one each in `client`, `plugin-approvals`, `qa/dogfood` and `sdui-parser`), each package's share under the objectstack-ai#20513 lane children. - **The two "(batch objectstack-ai#58)" titles** (`i18n-resolver.test.ts:1744`, `:1757`) stay: a two-digit decision-batch label outside the gate's pattern, with no counted id in the literal. - **Code comments with live ids** remain in these files, among them the `// objectstack-ai#18124 — step 3 of ruling A on objectstack-ai#18115` header in `auth-config.test.ts`, the `[objectstack-ai#16185]` docblock in `data-migration-flag-column-move.pin.test.ts`, the `[objectstack-ai#17614]` docblock in `email-template-floor-locale-parity.pin.test.ts` and the `// ─── [objectstack-ai#8075]` / `[objectstack-ai#15513]` banners. 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>
…less sys_metadata row is served as (objectstack-ai#21990) Fixes objectstack-ai#21978 Clause-②: no ## What this changes `SysMetadataRepository` (`packages/metadata-protocol/src/sys-metadata-repository.ts`) served a `sys_metadata` row that has no `checksum` as the hash of its stored body (`rowToItem`), but `put` and `delete` judged the caller's parent against the raw column (`existing.checksum ?? null`). So a row like that could never be written or removed through the metadata door. Every `saveMetaItem` / `deleteMetaItem` answered `409 METADATA_CONFLICT` ("Expected parent hmac-sha256:… but current is null"), whether the parent was the version the door served or no `If-Match` was sent at all, because the door takes the parent from the same read. Publish, rollback and commit revert over such a row hit the same lock, and the post-promotion drain of a checksum-less draft was refused and silenced as a benign race. Per triage's direction (6014717866), with nothing narrowed and no backfill: - **One helper**, `servedVersion(ref, row)`: the stored `checksum`, else `hashSpec(body, type)`. `rowToItem` now reads it, so every read hands out this one value. - **One lock**, `lockAccepts(ref, row, parent)`, used by `put` and `delete`. It accepts the row's stored stamp, which is the old compare unchanged: a row with a `checksum` is judged exactly as before, and a `null` parent still matches a checksum-less row. For a checksum-less row it also accepts the served version. - **The conflict's head** (`lockHead`) is the served version, so a 409 on such a row names the version a read hands out (before this, `null`). A checksum-less row whose bytes do not parse keeps `null` there, so a lock refusal never becomes a parse error. - The lineage fields (`previous_checksum`, the event's `parentHash`) and the no-op check keep reading the raw stamp. So the first write over a checksum-less row, even with an identical body, stamps the row as usual. Nothing is rewritten at rest, and the header's "no backfill" non-goal stands, now with one line on how such a row is served. **File surface:** as dispatched. The producer that wrote such rows (the datasource admin door) already stamps a checksum since PR objectstack-ai#21977, which is on `main`, so the remaining work is the stored rows, and that lands in this repository class. Two test files in the same package: the pins, plus one fixture comment in `protocol-publish-drafts-package-scope.test.ts` that this change made false. Changeset: `@objectstack/metadata-protocol` patch. ## Pins (`protocol.served-content-hash.test.ts`, the existing conflict-test double) Through the protocol's real `saveMetaItem` / `deleteMetaItem` / `publishMetaItem`, on a row seeded with no `checksum`: - (a) saved and deleted with the version its read serves, in the keyed form a door hands out: the repository's own `get` read, keyed; - (a) unpinned (last-write-wins) save and delete succeed: the dogfood shape; - (b) a stale keyed token and the raw served hash are still refused with `METADATA_CONFLICT` / `409` on both doors; `actualHead` is the served token, the row is untouched, and retrying with that `actualHead` succeeds; - (c) a `null` parent still succeeds: `storedParentVersion: row.checksum ?? null`, the stored-row migration's in-process spelling; - (d) after each write the row carries `hashSpec(newBody, 'view')`; an identical re-save stamps it too; - publish over a checksum-less active row; the drain removes a checksum-less draft row; - repository level: a row WITH a checksum whose stamp differs from its body's hash refuses the body's hash and `null` (both name the stamp as head) and accepts its stamp; a checksum-less row accepts `null` and its served version, and refuses anything else with the served version as head. ## Reverse verification (committed HEAD `5c4815a6ab`) The mutation went through `scripts/ablation-replace.mjs` with an EXIT/INT/TERM restore trap and absolute paths. It restored the raw compare in both `put` and `delete` (anchor hit x2 → x0, replacement x0 → x2, blob `dc58518587` → `494fa3f0ee`; on disk, raw-compare 0 → 2 and `lockAccepts` call 2 → 0). - Predicted beforehand: 7 of the 9 new pins red, and green for the `null`-parent pin and the stamped-row pin, which guard against widening and against narrowing rather than this mutation. - **Observed: `Tests 7 failed | 16 passed (23)`**, the 7 predicted. The save door reproduced the card's text verbatim: "view/case_grid has been modified since you loaded it. Expected parent hmac-sha256:e532d121… but current is null." The drain pin read the draft row still present, and the repository pin read `actualHead` `null`. - Restore was proven by observation: blob after restore `dc58518587` equals the HEAD blob, `git diff HEAD` is empty, and `git status --porcelain` is empty. - An earlier invocation was a no-op: the tool refused with exit 2 before writing, because it located the repository from the shared checkout's cwd. On-disk counts were unchanged, and it was rerun from the worktree root. The subject is imported by relative `src` path (`./protocol.js`, `./sys-metadata-repository.js`), so no `dist/` sits on the ablation's resolution path. ## Clause-② (measured against the built entry declarations) `packages/metadata-protocol/dist/index.d.ts` was built at HEAD, and again with BASE `8a399b2b15`'s repository source swapped in behind a trap. The swap was restored and proven by blob equality, and HEAD was rebuilt, giving a byte-identical `index.d.ts`. The diff's non-comment lines are `private servedVersion;`, `private lockHead;` and `private lockAccepts;`, with 0 removed; everything else is doc text. `index.d.cts` has the identical diff. No exported type or signature moves. Behaviourally, `put` / `delete` accept for a checksum-less row the version the same repository already serves for it, which is the declared version token, not a new class of input. ## Tests and gates: all on HEAD `81606021e2` (after merging `origin/main` twice, the second bringing PR objectstack-ai#21979's `protocol.ts` change) - `pnpm --filter @objectstack/metadata-protocol test`: `Test Files 218 passed | 3 skipped (221)`, `Tests 28028 passed | 19 skipped (28047)`. `typecheck`: `tsc --noEmit` clean, and the test file is in the program (`--listFiles` count 1). Lock VERDICT command-exit 0. - `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived the 63 commands, and all ran at exit 0. `check:type-check-debt` ran under the verify lock ("1 ledger entr(ies) re-measured … 26 raw tsc error(s) total, none above its recorded number"). `check:dual-build-cjs-loads` and `check:lean-entry-closure` ran after a full `turbo run build` (72 tasks, 71 cached). Reconciliation, `--ran` with per-command exit codes: "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN". - The artifact-roster block (55 families, outside the total): 52 at exit 0. `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths` answered NOT WIRED (exit 2, no PR context); they are rerun against this PR and reported in the `os-dev-report` comment. - The four symbol-anchor sweeps (`check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors`, `check:adr-anchors`): exit 0. - NOT MEASURED locally, owned by CI: the five path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core) and the workspace type-check lanes. `packages/qa/dogfood/test/datasource-meta-door-reaches-admin-door.dogfood.test.ts` was not run locally. ## Census: writers of `sys_metadata` that can store a row with no `checksum` | Writer | Where | `checksum` | Still producing such rows | |---|---|---|---| | `SysMetadataRepository.put` (insert / update) | `metadata-protocol/src/sys-metadata-repository.ts` | always `hashSpec(body, type)` | no | | `SysMetadataRepository.delete` | same file | removes the row. Its tombstone goes to `sys_metadata_history` with `checksum: null` by design | n/a (history table) | | datasource admin door `writeDatasourceRow` | `service-datasource/src/datasource-admin-plugin.ts` | `hashSpec(record, 'datasource')` since PR objectstack-ai#21977; none before | no. Its pre-objectstack-ai#21977 rows are the stored population this PR makes writable | | datasource admin door delete fallback | same file | `update { state: 'inactive' }`, which keeps the column | no | | `DatabaseLoader` save / create / `registerRollback` | `metadata/src/loaders/database-loader.ts` | `contentHash` stamp | no | | protocol orphan adoption (`package_id` rebind) | `metadata-protocol/src/protocol.ts` | partial update, which keeps the column | no | | protocol legacy delete, permission-set overlay discard | `protocol.ts`, `plugin-security/src/permission-set-overlay-discard.ts` | delete only | no | | `env_id` → `project_id` migration | `metadata/src/migrations/migrate-env-id-to-project-id.ts` | column rename DDL | no | | stored-row migration, flow credential move | `protocol.ts` `migrateStoredMetadata`, `service-automation/src/flow-credential-migration.ts` | through `saveMetaItem` → `put` (stamps) | no. Both were refused on such rows before this PR and succeed now | | generic data door, MCP data bridge, flow write nodes, hook bodies | — | refused: `sys_metadata` declares `apiMethods: ['get', 'list']`, plus the stored-metadata family refusals | no | A tombstone reads back as a `delete` event with `hash: null` (`history()` / `rowToEvent`). `getByHash` never matches it, and `restoreVersion` refuses it with `VERSION_NOT_RESTORABLE`. **No writer is still live after this change, so no follow-up card.** ## Acceptance notes - `packages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts` (about `:190`) explains its explicit `parentVersion: null` by saying a raw-seeded row's derived parent "would 409". After this change it would not; the `null` it passes stays valid. Comment drift in another package, left as is. Owner: none. - The first write over a checksum-less row records `previous_checksum: null` / `parentHash: null`, the raw stamp. That is deliberate: no history row carries the served hash, so naming it would be a parent link to nothing. - A conflict-audit note on such a row now reads "current is (withheld)" where it read "current is null", because the head is no longer null. - `DraftDrainFailure.draftHash` is documented as "the row's `checksum`". It is the served version, the same value for a stamped row. This is a doc imprecision predating this PR. - Rollback (`restoreVersion`) and commit revert over a checksum-less active row take the served parent and pass the same lock. This was read in code; only publish is pinned as the representative internal caller. - No door read serves a version token for a stored row that has no history; the tokens come from receipts, history events and a 409's `actualHead`. So for a legacy row, the 409 is the first place a client sees its token. The stale-version pin covers that retry. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…objectstack-ai#21994) Fixes objectstack-ai#21989 Clause-②: no ## What this is This PR adds the curated release page for 17.7.0, `content/docs/releases/v17/17-7.mdx` (1,402 lines). It was written after the publish: npm `latest` moved on 2026-10-06, and `@objectstack/spec@17.7.0` went out at 12:22:14Z. It also wires the page in: - `content/docs/releases/v17/meta.json`: `17-7` comes ahead of `17-6`. - `content/docs/releases/v17/index.mdx`: the status blockquote, the minors warning, the per-release list and the checklist links now name 17.7.0 as current. This is the same shape objectstack-ai#21362 used for 17.6.0. - `scripts/docs-audit/handwritten-docs.json`: the page joins the docs-accuracy audit scope. - `content/docs/releases/v17/17-6.mdx`: one dated correction (2026-10-06) on the anonymous-endpoints known issue. objectstack-ai#21158 was closed as not planned on 2026-10-04. The page follows the 17.6 page's structure: 1. Highlights. 2. What's new: the counts, and the runtime changes that happen silently. 3. Breaking changes and migration, triaged by door and subject. It includes a coverage table that names the section carrying the migration for every ADR-0087 entry added since 17.6.0 (8 conversions, 41 D3 entries). 4. New capabilities. 5. Notable fixes. Security fixes are described only as classes and doors. 6. New in Console: each objectui declared-breaking change, with this repo's answer. 7. The code that shipped but is not listed. 8. The upgrade checklist. Every line is marked *Not exercised.* ## Sources and counts - The version commit `4e4e881427` (objectstack-ai#21352) consumed 323 changesets. 322 are new. One, `748b240` (objectstack-ai#21270), shipped in 17.6.0 and is listed again; the page explains this in its own section. - 69 CHANGELOGs carry a 17.7.0 section, and 48 of them have entries. Their 444 entries (194 minor, 250 patch) de-duplicate to the 323 changesets. - Two commits landed on `main` after the Version Packages PR's last refresh and before it merged: - `8a399b2b15` (objectstack-ai#21977, for objectstack-ai#21923) - `04e776b39a` (objectstack-ai#21976, for objectstack-ai#21968) Both are ancestors of the version commit (exit 0 for each), so the 17.7.0 packages carry their code. But the version commit did not consume their changesets, so no 17.7.0 CHANGELOG line names them. The release-integrity audit named both in a warning. - objectui: the pin moved five times and ends at `0abd4f9f8769`: - `89cad75d5570` (objectstack-ai#21380) - `ab1879721595` (objectstack-ai#21625) - `2e818d0b51ec` (objectstack-ai#21710) - `9dfaca654311` (objectstack-ai#21800) - `0abd4f9f8769` (objectstack-ai#21827) Across 173 commits, 254 changesets were added and 229 of them release something. 65 are declared breaking, plus one commit marked `!`. The Console table answers each. - `PROTOCOL_VERSION` is still 17.0.0. ## Premise corrections - The dispatch named two pin moves ending at `9dfaca654311`. The tree has five, ending at `0abd4f9f8769` (`8832655`, objectstack-ai#21827). The page covers all five. - One new D3 id contains a word that `check:role-word` refuses on docs pages, and release pages are not in its baseline. The coverage table therefore names that entry by its subject and points at `os migrate meta --from 17`. ## Fact-check A second pass checked every cited SHA, PR number, key name and behavioural claim against the commits, changesets and registry entries. It corrected **31 claims** (commit `e4a6280b46`). Two more corrections came earlier, while drafting: - objectstack-ai#21361 is closed; it is not tracking the issue. - There are seventeen `ui-object-*` members, not eighteen. Mechanical checks on the final page: - All 276 cited SHAs resolve, in objectstack or in an objectui clone carrying the final pin's history. - Each of the 216 distinct sha–PR pairs matches its commit subject. - Every printable new D3 id (40) and all 8 conversions appear on the page. - The MDX for 17-7, 17-6 and index compiles with @mdx-js/mdx 3.1.1 and remark-gfm 4.0.1. After the fact-check, `main` gained `1abfc58` (objectstack-ai#21985), which lands the read half of objectstack-ai#21922. It is not an ancestor of the version commit: the test returned exit 1, and the control commit `753e7a1` returned exit 0. So in 17.7.0, the metadata door's reads still serve a stored row under a code-defined datasource name. Commit `8347e0d172` says so in the datasource migration bullet. ## Independent fact-check and fix round An independent, read-only fact-check of head `8347e0d172` returned **FAIL** with 13 findings (record: objectstack-ai#21989 comment 6018402030): 2 wrong facts (a flow's `get_record` node still reads the stored-metadata tables, in projected form), 1 security line that named the filter shapes and depth threshold evading 17.6.0's refusal, 1 breaking change missing from the Breaking section (`0fc8087`, objectstack-ai#21626), 1 link whose label did not match its target, 4 overstatements, 1 missing security fix (`49524f6`, objectstack-ai#21420), 1 missing rollback caveat on `os secret rewrap --apply`, and 2 minor wording issues. All 13 were re-verified against their sources and applied in `a53c972fa7`; none was refuted. A scan for any other line naming a bypass shape of a fixed issue reduced two more lines to their class (`0728cbf`, `fb69825`). ## Measured on `a53c972fa7` - Derived gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives 57 commands; all 57 exited 0 (`--ran`: "57 run, 0 NOT-MEASURED (a DERIVED zero)"). The verdicts include: - check-doc-anchors: 458 links resolve. - check-issue-citations: 305 resolve as a PR, 9 resolve, 15 are cross-repo; every citation this change adds resolves. - `check:role-word`, `check:release-notes` and `check:release-page-status`: OK. - check-release-section-coverage: OK, both plain and with `--strict` (10 minors). - The docs-audit scope check and `check:nul-bytes`: OK. - Docs production build: `TURBO_FORCE=true pnpm turbo run build --filter=@objectstack/docs`, run under the verify lock, reports `Tasks: 2 successful, 2 total` and `Cached: 0 cached`. The built `releases/v17/17-7.html` carries the corrected text and none of the removed text. - Mechanical checks: 231 distinct sha–PR pairs, 0 unresolved, 0 subject mismatches; the MDX of 17-7, 17-6 and index compiles. - Not measured locally: the repo-wide lint and the CI-only families. CI owns them. ## Not in this PR - No changeset. The PR touches only docs and a docs-audit list, nothing a package ships (`skip-changeset`). - Nobody has walked the 17.6.0 → 17.7.0 upgrade. The checklist says so on each line. --- _Generated by [Claude Code](https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #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 inpackages/services/service-datasource/src/datasource-admin-plugin.ts.Origin comes from provenance, not from the record.
listDatasourceRecordsandgetDatasourceRecordservedorigin: r.origin ?? 'code'. A metadata-door body has noorigin, or it assertsorigin: 'code'. So after a restart, the boot restore registered the row and the admin door served it as code-defined and refused itsPATCH. A newservedOrigin(ctx, name)returnscodeonly for a name in the host's code-datasource set, andruntimefor every other name, whatever the record says. That set is thecode-datasource-nameskernel service PR 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 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.Same-boot reach. The metadata door persists to
sys_metadataand the SchemaRegistry. It never registers the record in the MetadataService slot the admin door lists. Atstart(), the plugin now registers the protocol's awaiteddatasourcemutation projector (ADR-0094,registerMutationProjector; nometadata-protocoledit). After each metadata-door save, publish, revert, rollback or delete, and before that door answers, the projector does three things:sys_metadata, the same read the boot restore makes);convergePool.A name in the code set is skipped, because code wins. So the metadata door's repair
DELETEof a stored shadow row under a code name keeps the code definition served.The reverse direction.
persistDatasourceRowwrote thesys_metadatarow with nochecksum. The metadata door's reads serverow.checksum ?? hashSpec(body)as the version, but its writes compare the parent against the raw column. So every metadata-doorPUTandDELETEof an admin-created datasource answered409 METADATA_CONFLICT. The row now carrieshashSpec(record, 'datasource'), imported from@objectstack/metadata-core. That is the same computationSysMetadataRepository.putstamps (hashSpec(body, ref.type)).@objectstack/metadata-coremoves from devDependencies to dependencies; in the lockfile only the importer entry moves.convergePoolis the receive half of the datasource cluster bridge, and the projector's pool step. It now decides "code" from the same set instead ofrow.origin !== 'runtime'. It pools every other stored row as runtime, and stamps the recordorigin: 'runtime'before the record reaches the connect context.Editing a code-defined datasource is still refused at both doors.
Why
Clause-②: noThe
origindocblock inpackages/spec/src/data/datasource.zod.tsreads: "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.projectionAppliednow 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 reddatasource-meta-door-reaches-admin-door.dogfood.test.ts, run on the unmodified base, gave 4 failed and 2 passed::174same boot, afterPUT /meta/datasource/dogfood_meta_none_21923answered 200:expected undefined to match object { origin: 'runtime', …(1) }. The admin door did not list it.:193an admin-created datasource, thenPUT /meta/datasource/dogfood_admin_rt_21923:409 METADATA_CONFLICT, "Expected parent hmac-sha256:… but current is null.":206after a restart: received"origin": "code", expected"runtime".Mechanism hypotheses, measured
Does any code registration reach the MetadataService without its name in the set? This was measured with
bootStackon HEAD493c13dbb3, comparingmetadata.list('datasource')againstcode-datasource-names:default,showcase_externaldefault,showcase_externalcrm_analytics,crm_primary,defaultcrm_analytics,crm_primary,defaultdefaultdefaultA package installed after boot does not register datasources in the MetadataService at all:
registerAppand thesys_packagesrehydrate 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'.Same-boot reach. Verified on main (the
:174failure above). The seam is the awaited projector, notonMetadataMutation: 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.registerPool, which connects. The dogfood pin assertsconnectedfor 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 servedcodekept the datasource out of pool rehydration.Checksum. The fix lands inside
service-datasourceby importing the repository's own computation. The residual, rows already stored without a checksum, is under Acceptance notes.Carrier notes.
convergePoolis covered by the provenance rule (above).restoreRuntimeDatasourcesandrehydratePoolswarnings that go only tooptions.loggerare 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'stsc --listFilesincludes the edited test file.pnpm --filter @objectstack/service-datasource test: 41 files and 760 tests pass.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.origin.datasource-system-context.pin.test.tsfollowsconvergePool's newctxparameter.pnpm --filter @objectstack/dogfood typecheck: exit 0.pnpm --filter @objectstack/service-datasource build, dist marker preflight present): the new pin,datasource-restore-code-wins,meta-door-code-datasource, andexternal-import-code-datasource-namespace.codeplusstatus: the adminPATCHofshowcase_externalstill answers400 DATASOURCE_ADMIN_ERROR, with the message's first sentence.Ablations (each mutation through
scripts/ablation-replace.mjs, rebuilt,ablation-dist-preflighton both legs, restored to the HEAD bloba7f28b52b967,git diff HEADempty, and the restore leg rebuilt)origin ?? 'code'in list and get:174,:206,:222)originfirst (r.origin ?? servedOrigin(…)):175, the body assertingcodeserved as code)void this.projectMetadataDoorWrites;):174,:194,:206,:222)convergePoolreadsrow.origin !== 'runtime':178, no pool)const checksum = null):193and:221,409 METADATA_CONFLICT)datasource-restore-code-wins1 red (:271, the repairDELETEunregisters the code definition)Three first attempts did not run: the no-projector,
convergePooland 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/objectstackre-derived 78 families on this change. That is a superset of the 49 the dispatch listed; the additions come from the changeset, the lockfile andpackage.json. All 78 exit 0.--ranreconciles: 78 derived, 78 run, 0 NOT-MEASURED, each with a recorded exit code.One gate needed a rerun.
pnpm check:dual-build-cjs-loadsfirst exited 3 with PREREQUISITE NOT MET (8 unrelated packages had nodist/). 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.tsfiles:--print-config);eslint.config.mjsenables no type-aware linting, so this diff cannot move a verdict on an untouched file.The repo-wide
pnpm lintis 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 inmetadata-protocol'ssys-metadata-repository.ts:rowToItemservesrow.checksum ?? hashSpec(body)whileputanddeletecompare 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):
artifactWatch) re-registers the artifact's datasources through the MetadataPlugin door. A datasource added to the artifact after boot is not in the set, becauseAppPluginmemoizes its owners, so the admin door serves it as runtime until a restart.FilesystemLoader(adatasource/directory under the metadata root) lists datasources nothing adds to the set.AppPluginorDefaultDatasourcePluginregisters 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 existingrestoreRuntimeDatasourcesandrehydratePoolswarnings still go only tooptions.logger, whichos servedoes not pass. This change widens the population that reachesrehydratePools, 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
GETserves a stored row under a code-defined name (the overlay read); 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 remains open for that half.Files
packages/services/service-datasource/src/datasource-admin-plugin.tspackages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.tspackages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.tspackages/services/service-datasource/package.json,pnpm-lock.yaml(@objectstack/metadata-corebecomes a dependency)packages/qa/dogfood/test/datasource-meta-door-reaches-admin-door.dogfood.test.ts(the door pin, declared on [PM seat] domain:cli — 🟢 os-warren · session_01RWZbGvPFcRKvUqASZtunCU #6024).changeset/21923-datasource-origin-from-provenance.md(@objectstack/service-datasourcepatch)Generated by Claude Code