Skip to content

fix(runtime,service-datasource): converge the two libSQL loaders on one config read and one error class (#7314) - #7999

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-7314-turso-host-loader-convergence
Aug 12, 2026
Merged

fix(runtime,service-datasource): converge the two libSQL loaders on one config read and one error class (#7314)#7999
hotlong merged 1 commit into
mainfrom
claude/issue-7314-turso-host-loader-convergence

Conversation

@hotlong

@hotlong hotlong commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Part of #7314 — points 2 and 3. Point 1 landed via #7384 (b0c16a561) from the domain:services seat and is not touched here.

The defect

Two loaders build the libSQL/Turso driver, and which one runs is decided by something an author cannot see — whether the datasource happens to be the host's default:

#6268 converged the two host loaders. It could not reach the third: runtime depends on service-datasource, never the reverse. The two had drifted in two ways.

Point 3 — half the config was silently dropped for default

turso-driver-factory.ts:248 built new TursoDriverCtor({ url, ...authToken })two keys. The open-core arm read nine. TursoConfigSchema accepts all nine, so an encrypted or embedded-replica default lost encryptionKey / syncUrl / sync / concurrency / timeout / mode / schemaMode with no diagnostic anywhere — and got them back the moment the datasource was renamed away from default.

Fixed by derivation, not a corrected copy. Both loaders now build through one exported buildTursoDriverConfig; its key set comes from a reader table that TypeScript refuses to compile with a key missing, so the list is never written twice. A second hand-transcribed list is how the first two came to disagree — they agreed on the day they were written too.

The host loader also now trims the url before testing it, as the open-core arm always has: a whitespace-only url is refused by name instead of reaching @libsql/client.

Point 2 — one MissingDriverPackageError

The class was declared at turso-driver-factory.ts:114 in @objectstack/runtime, a package the open-core arm cannot import, so that arm raised a plain Error — matched by no instanceof, pinnable only by message text. The only legal convergence is moving the class down, and that is what this does: it now lives in @objectstack/service-datasource/src/missing-driver-package-error.ts, and both loaders throw the same class object.

No importer changes. MissingDriverPackageError, TURSO_DRIVER_PACKAGE and TURSO_DRIVER_INSTALL_COMMAND are still exported from @objectstack/runtime (and from @objectstack/cli's utils/storage-driver.ts through it) — re-exports now rather than declarations. serve.ts's e instanceof MissingDriverPackageError fatal-boot branch depends on that identity, so dropping the old export would have been an API break performed for a refactor's convenience.

Pins

Both required pins assert the property that hides from the obvious test:

  • the constructor argument, not a successful boot (turso-driver-factory.convergence.test.ts). A dropped key produces a driver that constructs perfectly and connects to the wrong thing; every boot-level assertion stayed green through the years this loader read two keys. A default spec carrying all nine keys is asserted to reach the ctor with all nine, and to equal what buildTursoDriverConfig produces — so a second hand-written list that happens to agree today still fails.
  • class identity, not name or message. MissingDriverPackageError === the service-datasource binding; the host loader's error instanceof the open-core binding; and — the direction that could not be written before this change — the open-core arm's error instanceof the runtime binding.
  • the builder against the real driver config, compile-time, in packages/cli (the only one of the three packages carrying @objectstack/driver-turso, and whose typecheck compiles tests). A key the driver adds and the builder never reads is a defect with no runtime symptom at all.

Reverse verification

Each half reverted independently, failure observed, restored:

reverted observed
host loader back to { url, authToken } 2 failures, both AssertionError: expected { Object (url, authToken) } to deeply equal { Object (url, authToken, ...) }. The 5 identity cases stayed green — the halves are independent.
open-core arm back to throw new Error(...) 1 failure: AssertionError: expected Error: datasource 'warehouse': a libSQL/T… to be an instance of MissingDriverPackageError.
encryptionKey removed from the shared builder packages/cli typecheck red: storage-driver.test.ts(460,40): error TS2344: Type '"encryptionKey"' does not satisfy the constraint 'never'.

A finding, not a cleanup — MissingDriverPackageErrorTwin

packages/cli/src/utils/storage-driver.test.ts:447 declares a hand-rolled twin of the class. The dispatch brief read it as a stand-in written because the real class could not be reached, to be deleted once it was importable. It is not that, and it was kept. The real class was already reachable in that file — two cases above the twin, MissingDriverPackageError is asserted === the runtime one. The twin is a deliberate negative control: something that matches on message and name and still fails instanceof, which is the only thing in the file demonstrating that the passing assertion tests identity rather than wording. Deleting it would have removed the evidence that the pin has teeth. It is extended here to fail against the new open-core binding too, and the reasoning is recorded in place.

Checks

  • pnpm -w typecheck — clean (127 tasks).
  • @objectstack/service-datasource 339 ✓ · @objectstack/runtime 2165 ✓ · @objectstack/cli 1249 ✓.
  • pnpm check:type-check-debt — clean; the ledger does not rise (the new runtime test initially added +1 to TEST_DEBT; the type was fixed rather than the entry raised).
  • ESLint clean on every touched file.
  • Changeset: .changeset/turso-host-loader-convergence.md.

Out of scope

#7385 (the mongodb / sqlite-wasm arms of the same no-remedy defect class) stays with domain:services and is untouched. Noted while here: those two arms still raise an untyped Error. The class moved down is deliberately driver-agnostic (driverType is a field), so adopting it there is a constructor call away whenever that lane takes it up.


Generated by Claude Code

…ne config read and one error class (#7314)

Two loaders build the libSQL/Turso driver and which one runs is decided by
whether the datasource happens to be the host's `default`. #6268 converged the
two HOST-injected loaders; it could not reach the third — the open-core
`turso` arm in `@objectstack/service-datasource` — and the two had drifted.

Point 3: the host loader read `url` and `authToken`; the open-core arm read
nine keys. So a `default` libSQL datasource silently lost `encryptionKey` /
`syncUrl` / `sync` / `concurrency` / `timeout` / `mode` / `schemaMode`, all
accepted by `TursoConfigSchema` and all honoured the moment the datasource was
renamed. Both loaders now build through one `buildTursoDriverConfig`, whose key
set is DERIVED from a reader table rather than hand-listed; a `packages/cli`
compile-time pin fails when that builder and the driver's own
`TursoDriverConfig` stop covering the same keys. The host loader also trims the
url, as the open-core arm always has.

Point 2: `MissingDriverPackageError` was declared in `@objectstack/runtime`,
which `service-datasource` cannot import, so the open-core arm raised a plain
`Error`. The class moves DOWN to the lowest package that raises it and is
re-exported from its old home, so every existing importer keeps compiling —
against the same class object, which is what `serve.ts`'s
`e instanceof MissingDriverPackageError` fatal branch depends on.

Pinned on the constructor argument (not a successful boot) and by object
identity (not by name or message), because a dropped key and a twin class both
produce a driver that constructs cleanly and a message that reads correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjUepKTxiGcJX6WQtwFmcf
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 10:51am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/service-datasource.

20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/permissions/system-context.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@hotlong
hotlong marked this pull request as ready for review August 12, 2026 11:07

hotlong commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

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

The twin: the dispatch was wrong and the report corrects it

My claim comment read MissingDriverPackageErrorTwin as a stand-in written because the real class was unreachable, and said to delete it in favour of the real import. That was wrong, and the PR says so with evidence: the real class was already reachable in that file — two cases above the twin, MissingDriverPackageError is asserted === the runtime one.

The twin is a deliberate negative control: something that matches on message and name and still fails instanceof. It is the only thing in that file demonstrating that the passing assertion tests identity rather than wording. Deleting it would have removed the evidence that the pin has teeth — i.e. my instruction would have quietly weakened the exact property this card exists to establish.

Extending it to fail against the new open-core binding too, and recording the reasoning in place, is the right disposition. Noting this at length because the escape hatch is what saved it: the dispatch said "if it turns out to encode something the real class does NOT, say so — that is a finding, not a cleanup." A brief that had only said "delete it" would have been followed.

Point 3 is fixed by derivation, and the pin is built so a copy cannot reappear

One exported buildTursoDriverConfig, its key set from a reader table TypeScript refuses to compile with a key missing — so the list is never written twice. And the assertion is not "all nine keys arrive", it is "the ctor argument equals what the builder produces", which means a second hand-written list that happens to agree today still fails. The two loaders agreed on the day they were written too; that is precisely how this card came to exist.

The reverse verification proves the derivation is real rather than decorative: removing encryptionKey from the shared builder turns packages/cli's typecheck red (TS2344: Type '"encryptionKey"' does not satisfy the constraint 'never').

Two things found along the way that the card did not name: a ninth key (schemaMode), and the host loader not trimming the url — now refused by name instead of reaching @libsql/client.

The three pins each cover a different way this hides

  • The constructor argument, not a successful boot. Stated with the reason: a dropped key produces a driver that constructs perfectly and connects to the wrong thing, which is why every boot-level assertion stayed green for years.
  • Class identity in three directions, including the one that could not be written before this change — the open-core arm's error instanceof the runtime binding.
  • The builder against the real driver config, at compile time, in packages/cli — the only one of the three packages carrying @objectstack/driver-turso whose typecheck compiles tests. "A key the driver adds and the builder never reads is a defect with no runtime symptom at all." That third pin covers a direction neither of the other two can see.

The API-break trap was identified concretely

MissingDriverPackageError / TURSO_DRIVER_PACKAGE / TURSO_DRIVER_INSTALL_COMMAND stay exported from @objectstack/runtime as re-exports, and the PR names the consumer that made it load-bearing: serve.ts's e instanceof MissingDriverPackageError fatal-boot branch. That is the difference between "kept the export because the brief said so" and knowing what would have broken.

Ledger discipline held: the new runtime test initially added +1 to TEST_DEBT; the type was fixed, not the entry raised.

#7385 correctly untouched — with a useful note that the moved class is deliberately driver-agnostic (driverType is a field), so the mongodb / sqlite-wasm arms are a constructor call away whenever domain:services takes them up.


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit ecf0bef Aug 12, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-7314-turso-host-loader-convergence branch August 12, 2026 11:19
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants