Skip to content

refactor(runtime,cli): give the optional Turso/libSQL loader one owner (#6268) - #7313

Merged
os-help merged 1 commit into
mainfrom
claude/issue-6268-turso-loader-single-owner
Aug 10, 2026
Merged

refactor(runtime,cli): give the optional Turso/libSQL loader one owner (#6268)#7313
os-help merged 1 commit into
mainfrom
claude/issue-6268-turso-loader-single-owner

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #6268

@objectstack/driver-turso is an optional install, so neither host can let the open-core datasource factory build the default datasource for a libsql:// selection — both inject a host driver factory instead. That loader was written out twice, and the two copies were kept equal by hand:

file path it serves landed by
runtime packages/runtime/src/turso-driver-factory.ts os migrate, createStandaloneStack, embedded hosts #5820
cli packages/cli/src/utils/storage-driver.ts os serve, os start #5602 / PR #5819

Premise re-verified — and the drift had already started

Both copies were still present on origin/main @ 5777b1a, and they were no longer equal. Two divergences, both measured before any edit:

  1. isTursoDriverId. cli/runtime: explicit driver(--database-driver / OS_DATABASE_DRIVER)在 CLI 与 standalone stack 之间仍有三处分叉(#6265 后续) #6345 moved the CLI half onto @objectstack/spec's shared driver vocabulary (resolveDatabaseDriverId(id) === 'turso') and left the runtime half on a private Set(['turso', 'libsql']). They agree only because the spec table's turso row happens to list exactly those two aliases today — a third spelling added to the table would have reached one host and not the other, silently.
  2. The url-less create() refusal. The CLI raises UnsupportedDriverError; the runtime raises a plain Error with a [StandaloneStack] prefix.

That is the #3741#3758 shape the card names: one decision, two implementations, one of them fixed.

What changed

@objectstack/runtime owns the loader; @objectstack/cli consumes it. The runtime now exports loadTursoDriverFactory, isTursoDriverId, MissingDriverPackageError, TURSO_DRIVER_PACKAGE and TURSO_DRIVER_INSTALL_COMMAND; packages/cli/src/utils/storage-driver.ts re-exports them, so every existing CLI import site is unchangedserve.ts was not touched. isTursoDriverId moved onto the shared spec vocabulary on the runtime side too, which is what retires divergence 1 structurally rather than by re-alignment.

UnsupportedDriverError stays in the CLI per the ruling.

One class identity, and it is proven rather than argued

packages/cli/src/commands/serve.ts:1136 decides whether a boot failure is fatal with

if (e instanceof MissingDriverPackageError) throw e;

A convergence leaving two same-named classes would make that predicate stop matching and degrade a fatal branch to a non-fatal one with zero noise. So the CLI re-exports the runtime's class rather than declaring its own, and a new test raises the error from the runtime loader and asserts the CLI-side binding still matches it.

Reverse verification. A same-named twin class was injected into the CLI file on purpose, the suite re-run, and the twin removed:

× #6268 … > the CLI export IS the runtime class object, not a same-named twin
  → expected [Function MissingDriverPackageError] to be [Function MissingDriverPackageError] // Object.is equality
× #6268 … > an error raised by the runtime loader satisfies serve.ts's CLI-side instanceof
  → expected false to be true // Object.is equality
× #5602 … > fails loudly with the exact install command when the package is missing
  → expected MissingDriverPackageError: A libSQL/Turso… to be an instance of MissingDriverPackageError
 Tests  5 failed | 29 passed (34)      ← restored: 34 passed (34)

The third line is the trap's exact signature. The suite also carries an in-test demonstration that a message assertion is blind to this defect: a locally-declared twin matches the message, the name and the installCommand and fails only the instanceof.

Note honestly which direction is load-bearing: the reverse-direction case ("the CLI loader's error matches the runtime binding") stayed green under the break, correctly — with the twin injected the CLI still delegates, so the error was still runtime-raised. The forward direction is the one serve.ts performs and the one that moves.

Two things deliberately NOT moved

These are host-supplied inputs, not surviving duplication:

  1. The dynamic import() specifier — its resolution ROOT. import() resolves from the node_modules tree of the module that evaluates it. @objectstack/driver-turso is an optional peer of @objectstack/cli and is not declared by @objectstack/runtime at all. Measured in this worktree:

    $ ls -d packages/cli/node_modules/@objectstack/driver-turso packages/runtime/node_modules/@objectstack/driver-turso
    packages/cli/node_modules/@objectstack/driver-turso
    ls: cannot access 'packages/runtime/node_modules/@objectstack/driver-turso': No such file or directory
    

    Had the CLI taken the runtime's default thunk, an operator who ran the exact install command the error prints would still have been told the package was missing. The CLI therefore keeps passing its own (typed) thunk. Both halves are now pinned from opposite sides: a new CLI case asserts the default thunk does find the package, and the pre-existing standalone-stack.libsql.test.ts case asserts a libsql:// boot with no injected thunk takes the missing-package arm precisely because the runtime does not declare it.

  2. The error TYPE for a url-less turso config. CLI-only semantics by the ruling. Only the type is host-chosen; the message is passed down from the runtime, so the wording is not duplicated.

The one behaviour difference, reported rather than silently chosen

The card asked for this to be surfaced instead of decided quietly. The two copies' missing-package messages differed, and each difference was pinned by a test on its own side:

assertion pinned by
/optional peer/i cli
/would start the server against an empty local database/i cli
/OPTIONAL package/ runtime
/os migrate/ runtime
/refuses rather than falling back to SQLite/i both

One owner cannot carry two wordings without re-introducing the hand-alignment the card is about, so there is now one message that names both consequences — a server booted against an empty local database, and an os migrate DDL run against that same one — rather than each host mentioning only its own. Every assertion in the table above still passes unedited, on both sides: the two hosts' existing pins now jointly constrain the single message, which is the strongest available evidence that nothing was quietly dropped. content/docs/data-modeling/drivers.mdx paraphrases this failure and remains accurate; no doc edit was needed.

Everything else is identical for every existing path: same error class and fields, same npm install @objectstack/driver-turso, same underlying import error preserved, same factory handle shape, and no silent SQLite fallback anywhere.

Verification

  • pnpm --filter '@objectstack/cli^...' build — dependency closure first (fresh worktree).
  • pnpm --filter @objectstack/runtime test118 files, 1839 tests passed.
  • pnpm --filter @objectstack/cli test105 files, 1144 tests passed.
  • pnpm --filter @objectstack/cli --filter @objectstack/runtime typecheck → both Done, 0 errors.
  • Reverse verification as quoted above (5 red on a deliberate break, 34/34 restored).
  • turbo run build --concurrency=2 (72/72) then pnpm check:type-check-debtOK — 33 ledger entr(ies) re-measured, none above its recorded number; @objectstack/runtime TEST_DEBT records 227, re-measured 225 — it went down, not up. Ledger left untouched (lowering it is another card's).
  • node scripts/check-nul-bytes.mjs OK; eslint on all four changed files clean.

A changeset is included: the missing-package message is operator-visible and @objectstack/runtime gains public exports, so this is not purely internal.


Generated by Claude Code

#6268)

The libSQL/Turso host driver factory existed twice — runtime's
turso-driver-factory.ts (#5820) and cli's utils/storage-driver.ts (#5602) —
kept equal by hand. The drift had already started: #6345 moved the CLI half
onto @objectstack/spec's shared driver vocabulary and left the runtime half
on a private Set(['turso', 'libsql']).

The runtime now owns the loader; the CLI delegates and re-exports. Critically
MissingDriverPackageError is ONE class across both packages, because serve.ts
decides fatality with `e instanceof MissingDriverPackageError` and two
same-named classes would make that predicate silently stop matching.

Two things stay host-owned because moving them would change behaviour: the
dynamic import() specifier (the optional package is a peer of @objectstack/cli
and is not linked under @objectstack/runtime at all) and the error TYPE for a
url-less turso config (the CLI's UnsupportedDriverError, which the ruling keeps
in the CLI). The message for the latter is shared.

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

vercel Bot commented Aug 10, 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 10, 2026 6:26am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/runtime.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, packages/runtime)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @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/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/permissions/system-context.mdx (via packages/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @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)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli, @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.

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

2 participants