Skip to content

fix(service-datasource): ledger the mounted datasource-admin routes (#7744) - #7957

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7744-datasource-admin-ledger-and-driver
Aug 12, 2026
Merged

fix(service-datasource): ledger the mounted datasource-admin routes (#7744)#7957
hotlong merged 2 commits into
mainfrom
claude/issue-7744-datasource-admin-ledger-and-driver

Conversation

@hotlong

@hotlonghotlong commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes#7744

The card had two halves, dispatched on different terms. (b) landed. (a) is falsified and nothing was changed for it — the evidence is below.


(b) Route-ledger parity — landed

What was actually wrong

The ten admin CRUD routes under /api/v1/datasources had no route-ledger entry in any of the platform's ledgers, and no guard was comparing them to anything. They are mounted the "third way" that service-storage and service-i18n grew their own ledgers for (#3636): objectstack serve builds a small com.objectstack.cli.datasource-admin-routes plugin whose init() resolves http.server and calls registerDatasourceAdminRoutes straight on IHttpServer (packages/cli/src/commands/serve.ts). Neither RouteManager nor RestServer.getRoutes() has ever seen them.

Why the entry is NOT in packages/rest/src/rest-route-ledger.ts

The card and the dispatch both name packages/rest as the site. That file structurally cannot host these rows, and the guard next to it says so in both directions:

  • rest-route-ledger.conformance.test.ts enumerates one thing — a RestServer booted the way production boots it, its own registerRoutes() plus mountAndRecordDirectRoutes (rest 的 9 条 direct-mount 路由对 RestServer 不可枚举 —— 因此进不了 /openapi.json,也进不了任何运行时自省 #5822). registerDatasourceAdminRoutes is not one of the registrars that function composes (direct-mount-composition.ts calls registerPackageRoutes and registerExternalDatasourceRoutes, and nothing else).
  • So a direct-mount row added there would fail two assertions immediately: "every direct-mount ledger entry is really registered by its registrar", and the directMounted.length === ledgerKeys('direct-mount').size identity.

The binding requirement from the dispatch — a ledger entry at the live spelling, pinned by a mount-derived test — is met; only the file it lives in differs, and it differs to the shape the repo already uses for exactly this class of route. New: packages/services/service-datasource/src/datasource-route-ledger.ts + its conformance guard, mirroring storage-route-ledger.ts / i18n-route-ledger.ts.

The spelling, and what was NOT done

⛔ No live route is renamed. The five datasources rows the REST ledger carries are the federation family (/:name/external/…) — a different, separately mounted family in a different package, not the admin family misspelled. Both are live. Two pairs do overlap (remote-tablesexternal/tables over listRemoteTables; object-draftexternal/tables/:remote/draft over generateObjectDraft), and that overlap is known and was deliberately reconciled rather than removed — #4249 gave the two paths one failure contract ("One operation, one failure contract now, on both paths", external-datasource-routes.ts). Every row here carries the spelling the mount uses.

The pin is derived from the mount, not transcribed

datasource-route-ledger.conformance.test.ts runs the registrar against a capturing IHttpServer and treats its registration calls as the route set. No path literal in the test is compared against the mount, so a future route addition fails the test instead of quietly re-creating this card. It also asserts non-vacuity (live.size > 0 and live.size === ledger.size), because both set-difference checks would pass on an empty enumeration — "absence must be loud".

Reverse-verified. Deleting the new GET /api/v1/datasources/:name/remote-tables row (fix committed first, restored with git checkout BRANCH -- PATH — never git stash) fails 3 assertions for the right reasons:

AssertionError: Datasource-admin routes with no datasource-route-ledger entry:
GET /api/v1/datasources/:name/remote-tables.
AssertionError: expected 10 to be 9 ← the non-vacuity size identity
AssertionError: GET /api/v1/datasources/:name/remote-tables is mounted but
unledgered — the #7744 gap, reopened.

Restored → 7/7 green.

Dispositions

All ten rows are server-only, each with a note naming its real consumer. Audited: no ObjectStackClient method and no CLI command reaches any of them (the client's datasources namespace holds only external, which targets the federation family; the three CLI datasource commands all call /external/*). Writing ten gap rows instead would have been a product decision made inside a bookkeeping fix — filed as #7954 for review instead. The client half joins the existing tranche-3 guard in service-route-ledger-coverage.test.ts.


(a) Unknown-driver draft → 201 with status:'error'NOT implemented; premise falsified

All three conditions were required. All three fail. Per the dispatch this is the success outcome for (a): it converts the card's second half from a bug into a documented design decision.

Condition 1 — "no late binding" — FAILS. This one is dispositive.

Late binding is not merely possible here; it is the written contract of the seam, and the create path's behaviour under it is specified.

packages/services/service-datasource/src/contracts/datasource-driver-factory.ts:7-18:

The framework deliberately ships no universal "driver-by-id" registry — concrete drivers … are constructed by the host stack …
When no factory is registered, or none supports() a given driver id, the admin service degrades gracefully: testConnection returns { ok: false, error } and create/update skip hot pool registration (the driver is picked up on the next boot instead).

That last clause is the accept-then-report design, stated at the contract. Three more reads carry it:

  • datasource-connection-service.ts:137 — the factory field is a thunk, documented "Resolve the host driver factory (lazy — may be registered after init)."
  • datasource-admin-plugin.ts:230-239 — the factory is resolved per call, with the reason given inline: init() may run before other plugins register their services, and admin requests arrive long after boot.
  • datasource-admin-plugin.ts:381-391, 428-455start() runs restoreRuntimeDatasources() then rehydratePools(), so a persisted record's driver is re-resolved on every boot. A driver contributed by a plugin (or an optional peer package such as @objectstack/driver-turso) installed after the draft exists binds then. Refusing at create time would make that authoring state unreachable.

And the create path already states the boundary deliberately — datasource-admin-service.ts:360-361: "A driver the platform ships no contract for passes untouched, matching the spec gate's boundary rather than inventing a stricter one for the UI."

Condition 2 — "nothing pins the 201" — FAILS.

  • service-datasource/src/__tests__/admin-routes.test.ts:139-151it('POST /api/v1/datasources creates a runtime datasource (201)'), expect(res.status).toBe(201).
  • service-datasource/src/__tests__/envelope.conformance.test.ts:113-116POST /datasources (201) as a declared envelope-conformance case.

Stated precisely: both drive driver: 'postgres', a known id, so an unknown-driver-only refusal would not by itself turn them red. What they pin is the create route's accept-and-return-the-summary shape, which is the shape the refusal would fork.

Control for this grep: the same query shape returns hits where a status code really is pinned — packages/types/src/response-envelope.test.ts:60, packages/rest/src/public-form-routes.test.ts:124, rest-dropped-fields.test.ts:107 and others — so the hits above are a real reading, not a lucky pattern.

Condition 3 — "status:'error' is not a consumed design state" — FAILS, and it reads the unknown-driver case by name.

The unknown-driver path is a named state with its own vocabulary entry, mapped through a declared class into a declared contract field, carrying a machine-readable reason:

factory.supports(driver) === falseConnectStatus 'skipped-unsupported' (datasource-connection-service.ts:154, 532-537, whose reason string is no driver factory supports driver 'NAME') → availabilityOf'failed' (:202-205) → summaryStatusstatus: 'error' + statusReason (datasource-admin-service.ts:109-125, 401-419).

The vocabulary doc spells out that this is intended — datasource-connection-service.ts:174-176:

failed — a connect was attempted and did not produce a usable driver (unreachable, bad credential, unsupported driver).

Consumed and tested: contracts/datasource-admin-service.ts:92 declares status: 'ok' | 'error' | 'blocked' | 'unvalidated' on DatasourceSummary; __tests__/datasource-admin-service.test.ts:412-425 pins each availability class onto a distinguishable status (expect(byName.dead!.status).toBe('error')); packages/runtime/src/datasource-autoconnect.test.ts:463 pins the sibling 'unvalidated'. The whole field exists because of #3827 — before it, "a datasource that died at boot looked exactly like one nobody had tested."

Control for this grep: querying the same vocabulary for the sibling value unvalidated returns hits across packages/runtime, packages/objectql and packages/spec, so a zero-hit result for 'error' would have meant absence rather than a broken search — and it was not zero.

Consequence

No code changed for (a), and per the dispatch no consolation change was made — the error message was not "improved". A status:'error' carrying statusReason: no driver factory supports driver 'NAME' is health-reporting working as designed, on a create path that is documented to accept a draft whose driver is not buildable in this process.


Verification

  • pnpm --filter @objectstack/service-datasource typecheck — clean.
  • pnpm --filter @objectstack/service-datasource test — 13 files / 333 tests pass (7 of them new).
  • Client ledger guards — service-route-ledger-coverage, rest-route-ledger-coverage, route-ledger-response-schema — 11 pass.
  • check:type-check-debt does not rise. packages/client's check:test-typecheck reports 3 problems locally, all TS2307: Cannot find module '@objectstack/runtime' in files this PR never touches; reverting only this PR's client edit reproduces the identical 3, so it is the unbuilt-local-dependency artifact of AGENTS.md §9, not a ratchet movement. Neither touched package carries a DEBT/TEST_DEBT entry, and the new test file is inside tsc --noEmit's program (no *.test.ts exclusion) with zero errors. Confirmed on CI: the required TypeScript Type Check job is green.
  • Changeset: .changeset/eighty-hoops-repeat.md (patch). Not breaking, so no ADR-0087 marker is owed.

Out-of-scope findings filed


Generated by Claude Code

…7744)
The admin CRUD family mounted at `/api/v1/datasources` had no route-ledger
entry anywhere. The three ledgers that came before it each stop at their own
package boundary and this surface is outside all of them: `objectstack serve`
builds a small plugin that resolves `http.server` and calls
`registerDatasourceAdminRoutes` straight on `IHttpServer`, so neither
`RouteManager` nor `RestServer.getRoutes()` has ever seen these ten routes —
the same third surface `service-storage` and `service-i18n` grew their own
ledgers for.
The REST ledger's five `datasources` rows are the FEDERATION family, spelled
`/:name/external/…`. That is a different mounted route family in a different
package, not the same one misspelled: `GET /:name/remote-tables` and
`GET /:name/external/tables` are both live, and their overlap was deliberately
reconciled rather than removed (one failure contract on both paths). So the
ledger is written at the spelling the mount uses, and no live route is renamed.
Adds `datasource-route-ledger.ts` (ten reviewed `server-only` rows — no client
method and no CLI command reaches any of them) plus a conformance guard that
DERIVES its expectations from the registrar: the mount is captured against a
recording `IHttpServer` and compared to the ledger in both directions, with a
non-vacuity assertion so the guard cannot pass by enumerating nothing. An
eleventh route now fails the test instead of silently re-creating this gap.
The client half joins the existing tranche-3 guard in
`service-route-ledger-coverage.test.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzFcriQJsJaaiieWSsrMu7
…t surfaced (#7744)
#7954 — the family has no SDK expression; the ten rows are recorded
`server-only` rather than `gap` because promoting them is a product decision,
not a bookkeeping one.
#7955 — the two `listRemoteTables` twins diverge on `?schema=`: only the
federation spelling forwards it. Out of scope here (a live route's request
handling), so it is filed rather than changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzFcriQJsJaaiieWSsrMu7
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 8:49am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 1 changed package(s). ✅

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — domain:cli seat (#6024). Verdict: accept, green, enqueueing. No change requests.

The landing-site deviation is correct, and the dispatch was one of the things that was wrong

This PR does not put the entry in packages/rest/src/rest-route-ledger.ts. The card said to, the triage grade said to, and my own dispatch instruction said to — all three were wrong, and this PR is right. Verified independently before accepting, because a deviation from a binding instruction has to be checked rather than taken on the PR's word:

  • packages/rest/src/direct-mount-composition.ts composes exactly two registrars — registerPackageRoutes (:117) and registerExternalDatasourceRoutes (:137). registerDatasourceAdminRoutes is not among them, so a direct-mount row there would fail the REST guard's own registrar-identity assertions on arrival.
  • Control, and it is better than a zero-hit: the single occurrence of registerDatasourceAdminRoutes anywhere in packages/rest/ is a commentexternal-datasource-routes.ts:26, noting the datasource service mounts them "via its own registerDatasourceAdminRoutes". The REST package already documents that these are not its routes.
  • The live mount is packages/cli/src/commands/serve.ts:2658.

The binding requirement — a ledger entry at the live spelling, pinned by a mount-derived test — is met exactly; only the file differs, and it differs toward the shape the repo already grew for this class (service-storage / service-i18n, #3636). ⛔ No live route was renamed, which was the one thing the dispatch forbade.

Two anti-vacuity moves worth calling out, because neither was asked for

  • The conformance guard asserts non-emptiness and size identity (live.size > 0, live.size === ledger.size) on top of the two set differences — both of which pass vacuously against an empty enumeration. A refactor that moves the mount would otherwise leave this file green while guarding nothing.
  • In the client-side guard, the author noticed that brokenIn(DATASOURCE_ROUTE_LEDGER)holds vacuously while every row is server-only, and added a measurement that fails the day the SDK grows a datasource-lifecycle method. A guard that can only ever pass is the exact shape these ledgers exist to remove; catching that in one's own new test is the right instinct.

(a) is falsified, and that is the success outcome

All three conditions fail, each cited, with a control on both greps as required.

Condition 1 is dispositive and settles it on its own: late binding is not merely possible, it is the written contractcontracts/datasource-driver-factory.ts:7-18 states that when no factory supports() a driver id, "create/update skip hot pool registration (the driver is picked up on the next boot instead)". That is accept-then-report, specified at the seam. Corroborated by the per-call factory resolution (datasource-connection-service.ts:137, "may be registered after init") and by start()'s restoreRuntimeDatasources()rehydratePools(). Refusing at create would make a documented authoring state unreachable.

Condition 3 is nearly as strong: status:'error' is a named state with its own vocabulary entry — supports() === false'skipped-unsupported'availabilityOf'failed'summaryStatus — and the vocabulary doc lists "unsupported driver" under failed explicitly.

Condition 2 is honestly reported as the weaker one (both pinning tests drive a known driver, so an unknown-driver-only refusal would not by itself turn them red; what they pin is the accept-and-return-summary shape). Saying that plainly rather than rounding it up to a third confirmation is what makes the other two believable.

And ⛔ no consolation change was made — the error message was not "improved". That instruction was followed to the letter.

So the card's second half is not a bug.status:'error' + statusReason: "no driver factory supports driver '…'" is health-reporting working as designed on a create path documented to accept a draft whose driver is not buildable in this process.

Also endorsed

The check:test-typecheck diagnosis carries a revert-and-reproduce control (reverting only this PR's client edit reproduces the identical 3 TS2307s in untouched files), which is what distinguishes an AGENTS.md §9 unbuilt-dependency artefact from a real ratchet movement. And #7954 / #7955 were split out rather than folded in — particularly #7955 (only the federation spelling forwards ?schema=, so the admin spelling silently drops the filter), which is a real behaviour divergence and correctly not fixed inside a bookkeeping PR.

25/25 checks green, mergeable_state: clean. Enqueueing.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review August 12, 2026 09:15
@hotlong
hotlong added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit a5d3aa1Aug 12, 2026
27 checks passed
@hotlong
hotlong deleted the claude/issue-7744-datasource-admin-ledger-and-driver branch August 12, 2026 10:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: datasource-admin route-ledger gap (D6) + an unknown-driver draft is accepted 201 with status:'error'

2 participants

@hotlong@claude