Skip to content

fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one - #21942

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21899-meta-door-code-datasource
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21899-meta-door-code-datasource

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21899
Clause-②: no (narrowing)

What changes

The metadata door now answers a code-defined datasource the way the published contract (DatasourceSchema.origin: "code — authored as *.datasource.ts, GitOps-owned, read-only in the UI") and the datasource-admin door already did: read-only. Triage ruled Q1-A and Q2-B in 6006929054; this PR implements both, in packages/metadata-protocol only.

  • The resolver (Q1-A). isArtifactBacked (packages/metadata-protocol/src/protocol.ts) gains a second non-standalone-artifact resolver, isDeclaredCodeDatasource, in the isNestedArtifactField shape from org-override-registry-gate: the field overlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743. It reads the installed packages' declared datasources (registry.getAllPackages(), each record's manifest.datasources, in the canonical array form defineStack leaves). datasource is added to that docblock's census. It never reads a MetadataService slot's origin or a request body's origin; a unit case pins a body asserting origin: 'runtime' on a code datasource as still refused, and one asserting origin: 'code' on a runtime name as still saved.
  • The package door's answer. With the resolver in place, the existing door refuses the save on both kernel shapes: refusePackagedBaseOverride on an environment kernel, and the repository write intent (override-artifact into SysMetadataRepository.assertAllowed) on a host-config kernel, which is the showcase's shape. The answer is NOT_OVERRIDABLE / 403. The sentence comes from the packaged-base sentence table (packaged-base-regime.ts), which gains one origin-gated row for datasource. ADR-0126 §3 records datasource outside the three regimes, as "origin-gated: code-defined read-only, runtime-created free", so the row is not a Regime C row. It carries no routes, only the owning source.
  • DELETE as repair (Q2-B). A DELETE that would remove nothing is refused with the same verdict. A DELETE of an existing stored row answers 200. Where the carve-out lives:
  • The read envelope. servedLockState reports what the doors do: editable: false, and deletable true only while the read found a stored row to remove.

The two doors' codes differ, by ruling

Triage's answer 6006929054: "The two doors' codes differ, and that is accepted. Each door speaks its own vocabulary; the verdict and the remedy agree." Measured on a real showcase boot at this branch:

  • metadata door PUT: 403 NOT_OVERRIDABLE — Datasource 'showcase_external' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.
  • admin door PATCH: 400 DATASOURCE_ADMIN_ERROR — Datasource 'showcase_external' is code-defined and cannot be edited at runtime.
  • metadata door DELETE with no stored row: 403 NOT_OVERRIDABLE — Datasource 'showcase_external' is code-defined and cannot be removed at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.
  • admin door DELETE: 400 DATASOURCE_ADMIN_ERROR — Datasource 'showcase_external' is code-defined and cannot be removed at runtime.

The host's default datasource (H4): the admin door treats it as code-defined; covering it here is a named gap

  • Measured on a real showcase boot (bootStack, admin routes mounted as serve.ts mounts them):
    • PATCH /api/v1/datasources/default answers 400 DATASOURCE_ADMIN_ERROR "Datasource 'default' is code-defined and cannot be edited at runtime.", and DELETE answers "… cannot be removed at runtime.".
    • On the metadata door, PUT /api/v1/meta/datasource/default answers 200 "Saved datasource 'default' (env-wide, state=active)" and the read then serves the edit. DELETE answers 200.
    • This PR leaves default unchanged: PUT answers 200 on this branch too, which is measured.
  • Why it is not covered here. The host's code datasource set is not readable from metadata-protocol without a runtime or service-datasource change:
    • DefaultDatasourcePlugin registers default only through MetadataService.registerInMemory. That is the slot whose origin triage ruled unsound, because a stored row overwrites it.
    • The connection service's retained state carries no origin (ConnectResult: name, status, reason, ownership).
    • The engine's listDatasourceDefs() mixes code and runtime definitions.
    • No package declares default.
  • What follows. Under the claim's stop condition, no runtime or service-datasource file is edited. This is reported for a follow-up card, and the changeset names it.

Pins, before and after (real showcase boot, showcase_external)

"Before" is the reverse-verification leg below (the base isArtifactBacked on committed HEAD) and the first run's measurements at 54fb60ac3f (6006105473). "After" is this branch.

Pin Before After
PUT /meta/datasource/showcase_external 200 "Saved datasource 'showcase_external' (env-wide, state=active)", a row persisted, the read served the edit 403 NOT_OVERRIDABLE with the verdict and remedy above; no sys_metadata row; the read serves the code label
DELETE, no stored row 200 "No datasource 'showcase_external' found - nothing to delete." 403 NOT_OVERRIDABLE, "cannot be removed at runtime"
DELETE, a pre-existing stored row (seeded as a pre-fix save wrote it, across a restart) 200, reset true 200, reset true, row gone; a second DELETE answers 403
after the repair and one more restart — both doors serve "External Analytics (SQLite)", origin code, _packageId com.example.showcase; no row
runtime datasource admin door: POST 201, PATCH 200, DELETE 204; metadata door: PUT 200, PUT 200, DELETE 200 the same

Reverse verification

Run on committed HEAD 8413b4622d, through scripts/ablation-replace.mjs (WRAP mode, its own restore trap, plus a git checkout HEAD trap):

  • The mutation restores the base form of isArtifactBacked's last line, dropping || this.isDeclaredCodeDatasource(type, name). On disk the anchor went 1 to 0 and the replacement 0 to 1. The blob went 8e2d759618ba to 51f36f712468.
  • Unit suite (src): protocol.code-defined-datasource-door.test.ts showed 14 failed and 11 passed. The red cases are the resolver, PUT refused (both kernels), DELETE with no row refused, the repair's second DELETE, the read envelope, and the 500-character bound. The green cases are the runtime controls, the hatch guard and the repository gate cases, which do not route through isArtifactBacked.
  • Dogfood (dist):
    • pnpm --filter @objectstack/metadata-protocol build emitted ESM/CJS and failed only DTS on TS6133, because the mutation leaves the new method unused.
    • node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol '...' --absent confirmed the marker absent from all 22 built files.
    • meta-door-code-datasource.dogfood.test.ts showed 4 failed and 2 passed. The PUT pin read expected { status: 200, code: undefined } to deeply equal { status: 403, code: 'NOT_OVERRIDABLE' }.
  • Restore: the blob after restore equals HEAD (8e2d759618ba), git diff HEAD is empty, and git status --porcelain is empty. A rebuild put the marker back in both built entry files (preflight: present). The unit file then passed 25/25 and the dogfood file 6/6.

Tests (HEAD dd81fb50d5)

  • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 217 files passed (3 skipped), 27941 tests passed.
  • pnpm --filter @objectstack/metadata-protocol typecheck and pnpm --filter @objectstack/dogfood typecheck are green. tsc --listFiles includes every touched test file.
  • Dogfood: meta-door-code-datasource.dogfood.test.ts (6/6) and external-import-code-datasource-namespace.dogfood.test.ts.
  • Consumer files touching the datasource /meta door were run, all green:
    • runtime: datasource-visibility, meta-type-write-capability-parity, stored-metadata-reader-contexts.pin, standalone-stack-hydrate-metadata, meta-write-org-scope, dispatcher-plugin.declared-5xx-prose-withhold.
    • rest: meta-type-read-capability, meta-type-write-capability, rest-server-meta-write-org-scope, meta-unknown-type-read-refusal, rest-server-meta-org-scope-url-spelling, rest.
    • service-datasource: datasource-admin-record-judgement.
    • objectql: overlay-precedence.
  • Three existing sweeps had pinned the old datasource delete refusal and were triaged. protocol.delete-rewrap-envelope, protocol.legacy-overlay-delete and protocol.read-lock-flags-write-door exclude the origin-gated type from the derived refusal sweeps or measure it at the protocol's delete door. Each change points at the new pin file.

Gates (HEAD dd81fb50d5)

  • node scripts/pm/dispatch-gates.mjs --commands derived 76 commands, and all 76 were run with exit 0. --ran reconciliation reports "76 derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED".
  • check:engine-double-contract asked for its ledger to learn the new file's pinned doubles (--write, +3 rows, committed).
  • The artifact-roster block (53) ran: 50 exited 0. check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths exited 2 with no PR context (NOT MEASURED locally); they run on this PR in CI.
  • The four symbol-anchor sweeps (check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors) exited 0.
  • check:adr-0087-registration --base origin/main: one declared-breaking changeset, not-required (no-migration-prescription). check-changeset-no-major reports no major.

Acceptance notes

  • Same boot after the repair DELETE. The read keeps serving the stored copy until the next restart, because the datasource-admin plugin's boot restore registered it in the MetadataService. The receipt still reads "reset to artifact default". That is finding(service-datasource): a stored datasource row overrides a code-defined datasource at boot, so after a restart the admin door serves and edits it at runtime (restoreRuntimeDatasources has no code-collision check) #21922's in-memory half, so the repair pin holds across a restart. Measured: in the same boot, GET after the repair served "Shadow 21899"; after the restart it served the code label.
  • An admin-created runtime datasource cannot be edited or deleted through the metadata door. Both answer 409 METADATA_CONFLICT, whether or not the version token is sent, because the admin door's sys_metadata row carries a null checksum. This is pre-existing and untouched here (runtime names are not artifact-backed). It is reported for filing in the dev report. The PM's hypothesis that sending the version makes it pass was measured false.
  • H2's "packaged-base sentence table". The table gains an origin-gated row rather than a Regime C row, per ADR-0126 §3. Its module header now scopes the "no redeploy prescription" rule to Regime C sentences.
  • The operator hatch. OS_METADATA_WRITABLE=datasource still opens the lock exactly as before; a guard case pins it.
  • Not measured. An artifact whose top-level datasources no package body declares (AppPlugin warns about this composition at boot) registers code datasources the resolver does not see, because no package record carries them.

Changeset

.changeset/21899-meta-door-code-datasource-read-only.md: @objectstack/metadata-protocol minor, BREAKING, Clause-②: no (narrowing). It states the remedy: edit the *.datasource.ts source, and delete a stored row through the metadata door to repair. It carries one ADR-0087 marker.


Generated by Claude Code

claude added 7 commits October 6, 2026 01:51
…ned datasource, and removes only a stored row under one

isArtifactBacked now sees a datasource an installed code package declares,
through a second non-standalone-artifact resolver read from the package
records' declared datasources. The existing package door and the
repository's write intent then refuse a PUT with NOT_OVERRIDABLE / 403 and
the datasource row of the packaged-base regime table: the admin door's
verdict (code-defined, cannot be edited at runtime, read-only) and its
remedy (edit the *.datasource.ts source).

A DELETE that would remove nothing is refused with the same verdict; a
DELETE of a stored row under a code-defined name stays possible as repair,
in the protocol's delete door and the repository's delete gate.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
… datasource, both kernel shapes

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…pair, and the refusal sweeps leave that tier to its own pins

servedLockState's deletable reads whether the read found a stored row for
an origin-gated code-defined item, so the read agrees with the door in both
states: refused with no row, admitted (repair) with one. The two delete
refusal sweeps derived from the registry flags exclude the origin-gated type,
and the read-versus-door table measures that type's host-config removal at
the protocol's delete door, where it is answered.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…and a pre-fix stored row is removable across restarts

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…ed datasource refusal (narrowing)

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…r test's pinned doubles

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 26 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), SysMetadataRepository (symbol, a top-level class), getMetaItemLayered (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/external-datasources.mdx (via showcase_external (literal, a string literal in a comment in ObjectStackProtocolImplementation))
  • content/docs/kernel/contracts/metadata-service.mdx (via getPublished (sdk, the bare tail of client method meta.getPublished, bound to GET /api/v1/meta/:type/:name/published; the bare tail of client method meta.getPublished, bound to GET /meta/:type/:name/published))
  • content/docs/kernel/services-checklist.mdx (via deleteMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via /:type/:name/published (route, bridged from symbol getMetaItemLayered — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via /api/v1/meta/datasource/:name (route, a path literal in a comment in ObjectStackProtocolImplementation))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5ce9eece1832504aecb46ded9ebed94d3ada5621 — the merge of head dd81fb50d5ad7bf85b6d9c65db325866a7890def into base 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5ce9eece1832504aecb46ded9ebed94d3ada5621 && git checkout 5ce9eece1832504aecb46ded9ebed94d3ada5621
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 dd81fb50d5ad7bf85b6d9c65db325866a7890def && git checkout -B drift-repro 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 && git merge --no-ff dd81fb50d5ad7bf85b6d9c65db325866a7890def

node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review) — PR #21942 at head dd81fb50d5

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-06T03:11Z. The os-dev report is on #21899 (6008535605). Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main.
    • Title fix(metadata-protocol)!: …. The first lines are Fixes #21899 and Clause-②: no (narrowing).
    • Assignee: os-project-manager.
  • Scope: 10 files, +1080/-55, all in metadata-protocol except the dogfood pin and the engine-double ledger rows the gate demanded. No runtime or service-datasource file is touched, as the order required.
  • The diff, read: triage's answer (6006929054), Q1-A and Q2-B, as ruled.
  • Public surface: packaged-base-regime.ts is not re-exported from @objectstack/metadata-protocol's entry, so (narrowing) is the only arm. It is BREAKING, with minor, a ! subject, the remedy, and ADR-0087 not-required (no-migration-prescription), which check:adr-0087-registration reads.
  • Door readings (dogfood on a showcase boot):
    • PUT on showcase_external: 200 → 403 NOT_OVERRIDABLE, with no row written.
    • DELETE with no row: 200 → 403.
    • DELETE of a pre-existing shadow row: 200 reset: true. The row is gone, and after a restart both doors serve the code definition.
    • A runtime datasource still writes through each door.
    • The admin door is unchanged.
  • H4, the host's default datasource: the admin door treats it as code-defined (measured). Its code set is not readable from metadata-protocol without a runtime or service-datasource edit. As triage directed, it is a named gap in the PR body, and the seat files its follow-up card in this act.
  • H5, falsified and recorded: a meta-door PUT or DELETE of an ADMIN-created runtime datasource answers 409 METADATA_CONFLICT with or without If-Match, because the admin door stores a null checksum. That is pre-existing, and it is folded into finding(service-datasource): a datasource created through the metadata door is missing from the admin door until restart, then reads as code-defined because the admin read defaults a missing origin to code #21923's family, noted on that card in this act.
  • Fixture triage: 5 pre-existing sweeps pinned the old datasource delete refusal (delete-rewrap-envelope, legacy-overlay-delete ×3, read-lock-flags). Each now excludes the origin-gated type and points at the new pin file. Every other assertion is untouched.
  • Reverse verification, from committed 8413b4622d: the resolver term was dropped and rebuilt. 14 unit and 4 dogfood pins went red, and the PUT pin read { status: 200 } vs { status: 403, code: NOT_OVERRIDABLE }. Restore was proved by blob equality.
  • Evidence:
    • metadata-protocol passes 27941 tests in 217 files;
    • the /meta door's consumers in runtime, rest, service-datasource and objectql pass;
    • metadata-protocol and dogfood typecheck exit 0.
  • Gates:
    • dispatch-gates --ran: 76 of 76 exit 0.
    • The roster (53), the four symbol-anchor sweeps and the 3 PR-context guards against this PR pass.
    • check-changeset-no-major passes.
  • CI: read at landing.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 03:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 03:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 9cc2c79 Oct 6, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21899-meta-door-code-datasource branch October 6, 2026 04:11
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ce row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default (objectstack-ai#21965)

Part of objectstack-ai#21922
Fixes objectstack-ai#21944
Clause-②: no (narrowing)

## What changes

A code-defined datasource (a `*.datasource.ts` the installed artifact
declares, or the host's own `default`) is read-only by published
contract: `DatasourceSchema.origin` says "code — authored as
`*.datasource.ts`, GitOps-owned, read-only in the UI", the datasource
registry entry in `metadata-plugin.zod.ts` says code-defined datasources
"win on name collision", and `datasource-admin-service.ts` says "A
runtime datasource never shadows a code one (code wins on collision)".
The datasource-admin plugin's boot restore broke all three, and the
metadata door could not see `default` at all.

The fix is the one host-owned set of code datasources the two cards'
triage asked for ("One set serves both, so do not build two"):

- **The set** (`packages/runtime/src/code-datasource-names.ts`, new).
One in-memory `Set` of the datasource names the host registers from
code, on the kernel service `code-datasource-names`.
`contributeCodeDatasourceNames` registers it on first use and adds to it
after that, the shape `seed-summary` uses.
- **Its producers, both in `init()`.** `AppPlugin.init()` adds every
datasource the artifact declares: the same list its `start()` registers
in the MetadataService, now memoized so the two phases read one answer.
`DefaultDatasourcePlugin.init()` adds `default`. Phase 1 completes
before any `start()`, so the set is whole before the restore runs,
whatever order the plugins were composed in.
- **The restore** (`restoreRuntimeDatasources`,
`packages/services/service-datasource/src/datasource-admin-plugin.ts`).
A stored row under a name in the set is not registered over the code
definition. It is kept, and one boot warning names it with the repair.
The warning goes to the host's `options.logger`, or to the kernel logger
when the host passes none (`os serve` passes none).
- **The resolver** (`isDeclaredCodeDatasource`,
`packages/metadata-protocol/src/protocol.ts`, nothing else in that
file). It reads the same set beside the installed packages, so the
metadata door answers `default` the way it answers every code-defined
datasource since PR objectstack-ai#21942.

⛔ "Code" is never read from a stored row's `origin`, the MetadataService
slot's `origin`, the connection service's `ConnectResult`, or a request
body's `origin`.

## Measured on a booted showcase

The harness is the `@objectstack/verify` `bootStack` with the
datasource-admin routes mounted the way `serve.ts` mounts them, in a
temp cwd. The stored rows assert `origin: 'runtime'` and their own
`config.filename` (the cards' case (b)). They were written through the
metadata door's repository on the runtime-only intent, then the stack
restarted. BEFORE is `76fec88b16`; AFTER is this branch at `1d840709ae`.
The readings come from a throwaway probe that was never committed; the
committed pins below assert the AFTER column.

| Reading after the restart | BEFORE | AFTER |
|---|---|---|
| admin list, `showcase_external` | `origin: runtime`, label "Shadow
21922" | `origin: code`, "External Analytics (SQLite)" |
| `PATCH /api/v1/datasources/showcase_external` | 200 | 400
`DATASOURCE_ADMIN_ERROR` "… is code-defined and cannot be edited at
runtime." |
| live pool named `default` | a second pool opened on the stored row's
file; verdict `already-registered` became `connected` | none; verdict
stays `already-registered` |
| `showcase_ext_customer` read | 3 rows (code fixture) | 3 rows (code
fixture) |
| boot warning naming each stored row | none | one per row |
| `PUT /api/v1/meta/datasource/default` | 200 "Saved datasource
'default'" | 403 `NOT_OVERRIDABLE` |
| `DELETE /api/v1/meta/datasource/default`, no stored row | 200 | 403
`NOT_OVERRIDABLE` |
| `DELETE /api/v1/meta/datasource/showcase_external` (repair), then meta
`GET` in the same boot | 200, but the meta `GET` kept serving the stored
edit until the next restart | 200, and the meta `GET` serves the code
definition |

## The dispatch's mechanism hypotheses

- **H1, start order.** In all three compositions that load
`service-datasource` (`serve.ts`, `standalone-stack.ts`, the verify
harness), `DefaultDatasourcePlugin` and `AppPlugin` are `use()`d before
`DatasourceAdminServicePlugin`. None of the three declares an ordering
edge to another, so their `start()`s run in insertion order: the code
registrations did land before the restore, but by list position alone,
which ADR-0116 says proves nothing. The answer is the first branch: the
set is filled by a phase that precedes the restore (`init()`). It is
pinned by a boot whose reader plugin is composed first, ahead of every
producer. The restore pin also covers a code registration that lands
after it.
- **H2, the seam.** It is a kernel service read through the services
registry the protocol already resolves (`getServicesRegistry()`), with
no `packages/spec` change. The ObjectQL registry was rejected: the
engine's datasource definitions mix both origins, and a host package
record would be a fabricated provenance.
- **H3, the admin refusal.** Measured, not assumed. The pins' stored
rows carry `origin: 'runtime'`, and the slot the refusal reads holds
AppPlugin's explicit `origin: 'code'`. Under ablation A the admin door
served the stored row as `origin: runtime`, so the refusal cannot come
from the admin read's `origin ?? 'code'` default.
- **H4, the live pool.** For `default`: yes. The restored row reached
`rehydratePools`, which opened a second pool named `default` on the
row's file. Routing did not move, because the engine never routes to a
driver named `default`; the default driver keeps its natural name. It is
the same defect and the same decision fixes it, pinned by
`getDriverByName('default')` and the connect verdict. For
`showcase_external`, nothing was re-pointed at boot in this composition:
AppPlugin's connect ran first, so the rehydrate answered
`already-registered`. The admin `PATCH` 200 was the open door to a
re-point (an update that changes connectivity rebuilds the pool), and it
is now refused.

## Seam and the Clause-② limb (for the seat)

- **Published exports added: none.** `code-datasource-names.ts` is not
re-exported from `packages/runtime/src/index.ts`. `service-datasource`
and `metadata-protocol` spell the service name privately and read the
value structurally as `has(name)`, the way `'datasource-connection'` is
read today.
- **What the seam does add is one kernel service entry**,
`code-datasource-names`, which two packages read by name. Whether that
is the claim's "service contract" limb is the seat's call. The line
above stays as the claim wrote it, and the changeset grades all three
packages `minor`, which holds under either reading.

## Named gap: the metadata door's read while a stored row exists

`GET /api/v1/meta/datasource/:name` still serves a stored row under a
code-defined name for as long as the row exists. The door reads its
stored overlay first (ADR-0005's read order), whatever the
MetadataService holds. The AFTER boot measured it: the admin door served
the code definition while the meta `GET` served the stored row, for
`showcase_external` and for `default`. So triage's pins "both doors
serve the code definition" and "removes it with no change to what is
served" hold for the admin door. For the metadata door they hold once
the repair `DELETE` has run, in the same boot. That read lives in
`getMetaItem`'s overlay step, outside `isDeclaredCodeDatasource`, and
`protocol.ts` is held by objectstack-ai#21934 in other regions, so it is left to the
seat. `meta-door-code-datasource.dogfood.test.ts` already pins that read
as it is.

objectstack-ai#21922 stays open for that read: this PR is `Part of` it, and the seat
routes the remaining half through triage when it merges.

## Landing beyond the claim's named files

`packages/runtime/src/app-plugin.ts` is the producer of the packages'
half of the set, in the declared `runtime` package. The memo also makes
its residual-owner warning print once instead of once per phase.
`packages/runtime/src/code-datasource-names.ts` is new in the same
package.

## Patch round 1 (head `d77e150701`)

Review `6011282321` on objectstack-ai#21922. The Tests, Ablations and Gates sections
below are round 0's, at `80fbcfdea6`; this section carries the readings
on the current head.

- **The plugin-dev pin (CI red on round 0).** `AppPlugin.init()` now
contributes its datasource names as a function that the host's
code-datasource set resolves at its first read. The set is still
contributed to only in Phase 1, before any `start()`. The resolution is
deferred because the names come from the artifact's `collections`, which
walk `packages[]`. `AppPlugin.init()`'s manifest registration is the one
thing in `init()` allowed to touch `packages[]`, pinned by
`plugin-dev`'s malformed-stack falsifier, which this PR's first head
turned red. The kernel service now holds a `CodeDatasourceNames`, a set
with pending contributions; readers still use `has(name)` only. A
contribution that throws stays pending and rethrows to every reader.
- **The `default` refusal names what defines it**: the host's database
configuration (the database URL the server starts with). It names no
`*.datasource.ts`, because none declares `default`. Every
package-declared datasource's sentence is byte-identical, and `code`,
`status` and the refused set are unchanged (`packaged-base-regime.ts`,
the datasource row's `hostOwned`).
- `const listOf = (` spacing restored in `app-plugin.ts`. Merged
`origin/main` `80f9f7e6ba` as `49421a8fe6`.
- **Ablation D:** the old sentence put back for `default` turned 6 unit
cases and 1 dogfood case red, and was restored by blob.
- **Tests at `d77e150701`** (each `VERDICT command-exit 0`):
  - `service-datasource`: 748 / 748;
  - `metadata-protocol`: 27978 passed, 19 skipped;
  - `runtime` local: 4668 passed, 19 skipped;
  - `plugin-dev`: 86 / 86;
  - the two dogfood files: 11 / 11;
- downstream consumers of `runtime` (cli, client, verify,
http-conformance, cloud-connection): all passed;
  - typechecks for the five packages: green.
- **Gates at `d77e150701`:** `dispatch-gates --commands` derived 72,
reconciled with `--ran` as 72 run and 0 not measured, plus
`check:init-service-contract` and `check:startup-registry-verdict`. All
exit 0.
- **Docs:** the 18 hand-written pages the Docs Drift Check lists were
read page by page. None states anything this PR makes false, so no docs
are edited.

## Tests (head `80fbcfdea6`)

- `pnpm --filter @objectstack/service-datasource exec vitest run
--maxWorkers=2`: 41 files, 748 passed.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 218 files passed, 3 skipped; 27976 tests passed, 19
skipped.
- `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2
--project local`: 331 files, 4658 passed, 19 skipped.
- Dogfood, `--project isolated`:
`datasource-restore-code-wins.dogfood.test.ts` (new) and
`meta-door-code-datasource.dogfood.test.ts`, 2 files, 11 passed.
- `typecheck` green for `service-datasource`, `metadata-protocol`,
`runtime` (including its `check:test-typecheck` ledger, held) and
`dogfood`. `tsc --listFiles` counts each touched test file once in its
program.
- Each package's run includes its new pins: 5 in
`datasource-admin-plugin.test.ts` (the restore), 4 in
`code-datasource-names.test.ts` (the set and its phase), and 2 resolver
plus 8 door cases in `protocol.code-defined-datasource-door.test.ts`
(`default`, on both kernel shapes).

## Ablations

Each one was committed first and mutated with
`scripts/ablation-replace.mjs` in wrap mode, under a shell trap. For
subjects resolved through `dist/`, the package was rebuilt and
`ablation-dist-preflight.mjs` proved the marker was present. The restore
leg was rebuilt and proven `--absent`, the blob equalled HEAD, `git diff
HEAD` was empty, and the tree was clean.

- **A, the restore registers over a code name**
(`datasource-admin-plugin.ts`; marker in 2 files of
`service-datasource/dist`). Unit: 3 failed, 16 passed. The slot served
the stored row, the warning was not called, and the order-independent
case registered the row. Dogfood: 2 failed, 3 passed. The admin list
served `showcase_external` as the stored row, and the repair case read
the same.
- **B, the resolver does not know the set** (`protocol.ts`; marker in 2
files of `metadata-protocol/dist`). Unit: 5 failed, 30 passed (the
resolver case, and `PUT` plus no-row `DELETE` of `default` on both
kernels). Dogfood: `PUT /meta/datasource/default` answered 200. The next
case then failed as a cascade, because the row that `PUT` stored made
the seed conflict. The repair `DELETE` and runtime controls stayed
green, as expected.
- **C, AppPlugin's `init()` contribution deleted** (`app-plugin.ts`; the
subject resolves from source). The reader-first boot saw `['default']`,
not `['app_wh', 'default']`. So the set comes from `init()`; the
`start()` registration never fills it.

## Gates (head `80fbcfdea6`)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 72 commands on the actual change, a
superset of the 52 at dispatch. All 72 ran with exit codes captured
before any pipe. `--ran` printed "72 derived famil(ies) accounted for —
72 run, 0 NOT-MEASURED".

- `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`
(exit 3) because eight packages outside this diff had no `dist/`. After
building them it measured green.
- Two families the derivation does not name were also run, both green:
`check:init-service-contract` ("34 declared / 1 self-provided / 3
without a workspace provider") and `check:startup-registry-verdict`
("none recording a verdict the boot can contradict").
- `check-changeset-no-major`'s level axis needs a PR payload, so CI
reads it.
- Lint is a proven narrowing, not the repo-wide run. `eslint
--no-inline-config --format json` on the 9 touched source and test files
reported 9 files, 0 errors and 0 warnings. `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, no typed rules,
as its own comment states), so this diff cannot move any untouched
file's verdict.

## Acceptance notes

- **The `default` refusal's remedy** names the host's database
configuration (patch round 1).
- **Cluster convergence.** `convergePool` reads a stored row directly
and is unchanged. Its signals come from peer admin writes, and the admin
door now refuses those for code names.
- **The restore's other warnings** (a failed read, a failed register)
still go only to `options.logger`, which `os serve` does not pass. They
are unchanged here.
- **objectstack-ai#21923 remains open.** This diff does not touch
`listDatasourceRecords`, `getDatasourceRecord` or
`persistDatasourceRow`. One interaction: a metadata-door-created
datasource with no `origin` still restores, and is still read as `code`
by the admin door's default.
- **Main drift.** `origin/main` gained objectstack-ai#21956, objectstack-ai#21964 and objectstack-ai#21961 (spec
and docs-qa only) after the round-1 merge. None touches a file here, and
the queue's merged generation is the check.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants