Repository navigation
Commit 172b4cf
Fixes #19977
Clause-②: no
The `Clause-②: no` line above is the claim's line (comment 5852270976),
copied as it stands. The changeset carries `Clause-②: no (narrowing)`,
because AGENTS.md makes a narrowing BREAKING and
`check:adr-0087-registration` reads the arm there. Both lines give the
same value. Nothing is widened: no key is added, removed or renamed, and
no exported symbol moves.
Session `session_01Rjy9MeetSfq34PKn81CRiN` (PM dispatch, `domain:spec`
seat 1), branch `claude/issue-19977-turso-config-authoring-refusals`.
Base `49144fcc`, then merged with `origin/main` at `369bcbed`, which
includes #20104 as `84880f92`. Head `59c2391e`. Every reading below was
taken on that merged tree unless it says otherwise.
## 1. The premise, measured
- **Spec copy** (`packages/spec/src/data/driver/turso.zod.ts`): the
`superRefine` checked only `sync` without `syncUrl`. **Driver-local
mirror** (`packages/drivers/driver-turso/src/spec/turso.zod.ts`): no
refinement at all, and no `mode` key. It is a plain `z.object`, so an
authored `mode` is stripped.
- **What the constructor refuses on `main`.** Read from the source, not
from notes: `localEngineDefect`, `refuseWebSocketTimeout`,
`refuseSuppliedClientTimeout` and `detectMode` in `turso-driver.ts`.
Probed on the built dist at `49144fcc`, the constructor refuses every
row below and **both schemas accepted every one of them**:
| authored config | `new TursoDriver` | spec / mirror before |
| --- | --- | --- |
| `libsql://`, `https://`, `wss://`, `LIBSQL://` url + `syncUrl` |
VALIDATION_ERROR / 400 | accept / accept |
| `libsql://` url + `mode: 'replica'` or `'local'` | VALIDATION_ERROR /
400 | accept / accept |
| bare path, `sqlite:`, `:MEMORY:`, `libsql:` with no `//` (no mode, or
`syncUrl`, or `mode: 'local'`) | VALIDATION_ERROR / 400 | accept /
accept |
| `:memory:`, `file::memory:`, `FILE::memory:`,
`file::memory:?cache=shared` + `syncUrl`; `:memory:` + `mode: 'replica'`
| VALIDATION_ERROR / 400 | accept / accept |
| `wss://` or `WS://` + `timeoutMs` (no mode, or `mode: 'remote'`) |
VALIDATION_ERROR / 400 | accept / accept |
- **Arm 2, re-probed on the built dist at `49144fcc`.** `new
TursoDriver({ url, mode: 'remote', syncUrl, sync: { intervalSeconds: 60
} })` constructs and connects. After that, `isSyncEnabled` answers
`true`, no interval is started, and the sync call rejects with
`SYNC_NOT_SUPPORTED` (on a `libsql://` url) or
`SyncNotSupported("File")` (on a `file:` url). The built
`createRemoteClient` forwards `url`, `authToken`, `concurrency` and
`fetch`, and no `syncUrl`.
- **#20104 (merged as `84880f92`).** Its `turso-driver.ts` hunks sit at
`:26`, `:588` and `:2720` only (imports, and the remote refusals of
inherited members). `constructor`, `detectMode`, `localEngineDefect`,
the `refuse*` helpers, `isSyncEnabled`, the sync method and
`createRemoteClient` are untouched, and its only `syncUrl` hit is a
message string. So the refused set this PR mirrors is unchanged by it,
and it covers nothing here.
## 2. The change
- **`@objectstack/spec` `TursoConfigSchema`.** A new module-local
`tursoTransportIssues` runs inside the existing `superRefine`, after the
`sync`-without-`syncUrl` refusal. That refusal is byte-identical and
pinned verbatim. The predicates mirror the constructor's: a scheme
matches in any letter case, `:memory:` matches exactly, and a `file:`
url whose path is `:memory:` (or starts `:memory:?`) is in-memory. The
url is read **trimmed**, because both loaders trim it before
construction (`resolveTursoUrl`), and `syncUrl` counts when it is a
non-empty string, as `buildTursoDriverConfig` forwards it. Checks run in
the constructor's order, so each config gets exactly one refusal on
`url`.
- **The same refinement in the driver-local mirror, byte for byte, plus
the spec's `sync`-without-`syncUrl` refusal in the same words.** The
mirror had no refinement at all. The driver ignores `sync` without
`syncUrl`, and the one-table parity pin below needs the mirror to answer
every row the spec answers. The helper is not shared, deliberately. It
is internal to `@objectstack/spec` and not on a published entry point,
and publishing a turso-specific function only to feed one mirror would
grow the spec's public surface. One case table holds the two copies
together instead.
- **`url` describe** (spec), before → after:
- `libSQL endpoint or local file: a remote libsql/https Turso URL, a
file path, or :memory:`
- `libSQL endpoint or local file: a remote libsql/https Turso URL, a
local file written as a file: URL (never a bare path), or :memory:`
`content/docs/references/data/driver-turso.mdx` was regenerated from it
(`check:generated --fix`, the only stale artifact).
- **ADR-0087:** a new semantic entry,
`turso-config-transport-mismatch-refused`
(`packages/spec/src/migrations/entries/semantic/18.turso-config-transport-mismatch-refused.ts`),
with `registry.ts` regenerated. The changeset
`.changeset/19977-turso-config-transport-refusals.md` is `minor` for
`@objectstack/spec` and `@objectstack/driver-turso`, with the FROM → TO
table and the `registered` marker.
## 3. Every refusal, verbatim (rendered from the built spec dist)
Each is one `custom` issue on the key named in the heading.
**on `url` — a remote url beside `syncUrl`** (`{ url:
'libsql://db.turso.io', syncUrl: 'libsql://db.turso.io' }`):
> `url` is a remote `libsql://` url, but `syncUrl` makes this datasource
an embedded replica, which runs every read and write through a local
SQLite engine that cannot open a remote url — the turso driver refuses
this configuration when it starts. (An embedded replica is a local file
kept in sync with the remote: @libsql/client builds no replica for a
remote url and ignores `syncUrl` beside one.) For a remote database,
drop `syncUrl` (and `sync`) and keep the remote url alone. For an
embedded replica, point `url` at a local file and keep the remote in
`syncUrl`: `url: 'file:./data/replica.db'`.
**on `url` — a remote url under a forced `mode: 'replica'`** (`{ url:
'libsql://db.turso.io', mode: 'replica' }`):
> `url` is a remote `libsql://` url, but `mode: 'replica'` makes this
datasource an embedded replica, which runs every read and write through
a local SQLite engine that cannot open a remote url — the turso driver
refuses this configuration when it starts. (An embedded replica is a
local file kept in sync with the remote: @libsql/client builds no
replica for a remote url and ignores `syncUrl` beside one.) For a remote
database, drop `mode` (a `libsql://` url is detected as remote) or set
`mode: 'remote'`. For an embedded replica, point `url` at a local file
and name the remote in `syncUrl`: `url: 'file:./data/replica.db'`.
**on `url` — a remote url under a forced `mode: 'local'`** (`{ url:
'https://db.turso.io', mode: 'local' }`):
> `url` is a remote `https://` url, but `mode: 'local'` makes this
datasource a local database, which runs every read and write through a
local SQLite engine that cannot open a remote url — the turso driver
refuses this configuration when it starts. For a remote database, drop
`mode` (a `https://` url is detected as remote) or set `mode: 'remote'`.
For a local database, point `url` at a file: `url:
'file:./data/app.db'`.
**on `url` — a url that is none of `file:`, `:memory:` or remote** (`{
url: './data/app.db' }`):
> `url` is not a url the turso driver can open: it is not `:memory:`,
not a `file:` url, and not a remote `libsql://`, `https://`, `http://`,
`wss://` or `ws://` url (a scheme matches in any letter case). With no
`mode` and no remote scheme, this datasource is a local database, which
runs every read and write through a local SQLite engine that opens only
a `file:` url or `:memory:` — the turso driver refuses this
configuration when it starts. For a local database file, spell the path
as a `file:` url: `url: 'file:./data/app.db'`. For a throwaway in-memory
database, `url: ':memory:'`. For a remote database, use one of the
remote schemes above.
The same refusal beside `syncUrl` (`{ url: './data/replica.db', syncUrl:
… }`) swaps in the replica wording:
> … `syncUrl` makes this datasource an embedded replica, which runs
every read and write through a local SQLite engine that opens only a
`file:` url or `:memory:` — the turso driver refuses this configuration
when it starts. For an embedded replica, spell the local path as a
`file:` url and keep the remote in `syncUrl`: `url:
'file:./data/replica.db'`. For a remote database, drop `syncUrl` (and
`sync`) and use one of the remote schemes above.
**on `url` — a replica on an in-memory url** (`{ url: ':memory:',
syncUrl: … }`):
> `url` names an in-memory database, so it cannot hold the embedded
replica `syncUrl` asks for: a replica is a local file kept in sync with
the remote named in `syncUrl` — the turso driver refuses this
configuration when it starts. Point `url` at a local file (`url:
'file:./data/replica.db'` beside `syncUrl`), or drop `syncUrl` (and
`sync`) for a plain in-memory local database.
With `mode: 'replica'` instead of `syncUrl`, the way out reads `or drop
\`mode: 'replica'\` for a plain in-memory local database.`
**on `timeoutMs` — a window beside a WebSocket url in remote mode** (`{
url: 'wss://db.turso.io', timeoutMs: 5000 }`):
> `timeoutMs` is set beside a `wss://` url, which rides @libsql/client's
WebSocket transport, and that transport takes no timeout: the window
would bound nothing, so the turso driver refuses this configuration when
it starts. Either drop `timeoutMs` and run this remote database
unbounded, or keep it and spell the url `libsql://` or `https://`, where
every request is bounded.
**on `syncUrl` — `syncUrl` under a forced `mode: 'remote'`**
(constructed and ignored by the driver; `{ url: 'libsql://db.turso.io',
mode: 'remote', syncUrl: … }`):
> `syncUrl` configures an embedded replica, but `mode: 'remote'` sends
every read and write straight to `url` and builds no replica: the turso
driver never hands `syncUrl` to the remote client and runs no sync, so
the setting changes nothing. For a remote database, drop `syncUrl` (and
`sync`). For an embedded replica, drop `mode` and point `url` at a local
file beside `syncUrl`: `url: 'file:./data/replica.db'`.
**on `sync`, in the mirror only** (`{ url: 'file:…', sync: {…} }`), the
spec's existing words, unchanged:
> `sync` configures embedded-replica syncing, which only runs when
`syncUrl` names the remote to replicate from. Set `syncUrl`, or remove
`sync` — on its own it configures nothing.
No message echoes a url, only a remote url's scheme, because a url may
carry a token. No message carries an issue number.
**Deliberately NOT refused**, because the constructor accepts them: an
uppercase `LIBSQL://` or `FILE:` url; `file:` + `mode: 'replica'` with
no `syncUrl`; `file:` + `syncUrl` under `mode: 'local'`; any url under a
forced `mode: 'remote'`, including `file:` and a bare path (the
constructor does not judge it, and `@libsql/client` refuses a bare path
at connect); `timeoutMs` beside `libsql://` / `https://`; `timeoutMs` on
a replica syncing from `wss://`; an empty `syncUrl`; and a
whitespace-padded url, which is read trimmed as the loaders read it.
Each is a preservation row below.
## 4. Tests
-
**`packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts`**
(new): one table of **54 rows**. For each row it asserts (a) the
constructor's verdict, with a refusal asserted as `code:
'VALIDATION_ERROR'`, `status: 400`; (b) the spec contract's verdict,
which must be refused exactly when the constructor refuses or the row is
marked inert, as ONE `custom` issue on the named key; and (c) the
mirror's verdict, whose `{ path, code, message, count }` must **equal**
the spec's (messages byte-identical). Rows that force a `mode` skip (c),
because the mirror strips `mode` (16 skips). Row floors per verdict
class make a shrunken table fail. The spec half resolves
`@objectstack/spec/data` through its built `dist`, per the
`KNOWN_UNALIASED_TEST_IMPORTS` pair `driver-turso` → `spec`.
- **`packages/spec/src/data/driver/turso.test.ts`:** 14 new refusal
cases. Each asserts the issue code, the path, the prescription
substrings and that there is exactly one issue. It also asserts the
`DatasourceSchema` door (re-pathed to `config.url`), the
`validateDriverConfig` door, the unchanged `sync` message verbatim, and
the describe text. The preservation list gained 11 accepted configs.
- **Fixtures rewritten by the pin sweep** (a repo-wide grep for turso
configs spelling a remote url beside `syncUrl`, a forced mode or a bare
url, parsed by either schema):
1. `packages/spec/src/data/driver/turso.test.ts`: the
`sync.intervalSeconds` fixture put `syncUrl` beside a remote url. It now
uses `url: 'file:./data/replica.db'`. The spelling changed and the
assertion is unchanged.
2. `packages/drivers/driver-turso/src/spec/turso.test.ts` "all fields":
same, `url: 'file:./local-replica.db'`.
3. `packages/drivers/driver-turso/src/spec/turso.test.ts` "environment
variable patterns" **pinned the opposite**: `url:
'${TURSO_DATABASE_URL}'` was asserted as accepted. The constructor
refuses that literal (it is none of `file:`, `:memory:` or remote, in
local mode), and nothing resolves a placeholder. The case is replaced by
a refusal assertion: `code: 'custom'`, `path: ['url']`, and the
prescription.
4.
`packages/services/service-datasource/src/__tests__/datasource-config-redaction.test.ts`,
the #8337 legacy row. It put both token-bearing urls on a remote `url`
beside `syncUrl`, which `updateDatasource` now refuses at the door: 2 of
693 were red on the first run. Both url-bearing keys still carry
`?authToken=` and are still redacted and restored independently, but the
row is now an embedded replica on `file:./data/replica.db?authToken=…`,
and the author's hand edit is `file:./data/elsewhere.db`. This file is
outside the claim's first file surface. The seat amended the surface for
it.
Loader fixtures in `@objectstack/runtime`
(`turso-driver-factory.convergence.test.ts`), `@objectstack/cli`
(`storage-driver.test.ts`) and `@objectstack/service-datasource`
(`turso-driver-config.test.ts`) also spell a remote url beside `syncUrl`
+ `mode: 'replica'`. They exercise only `buildTursoDriverConfig` or a
capturing constructor, never this schema or the real driver, so they are
left unchanged (see Acceptance notes).
Readings on the merged tree:
| suite | result |
| --- | --- |
| `@objectstack/driver-turso` vitest (whole package) | 72 files · 1909
passed · 16 skipped · exit 0 |
| `@objectstack/driver-turso` typecheck (`tsc --noEmit`, program
includes both touched test files) | exit 0 |
| `@objectstack/service-datasource` vitest (consumer) | 34 files · 693
passed · exit 0 |
| `@objectstack/service-datasource` typecheck | exit 0 |
| `@objectstack/spec` typecheck (`tsc` + scripts +
`check:test-typecheck`) | exit 0 |
| `@objectstack/spec` vitest `--project local`, 3 shards | 540 files ·
15835 passed · 2 todo (5637 + 4846 + 5352), exit 0 on each shard |
Before the merge, at `dd67e8ed`, the spec suite read 540 files · 15812
passed · 2 todo in 3 shards, and the targeted `src/data/driver`,
`src/data/datasource`, `src/migrations`, `src/conversions` and
`src/shared` run read 55 files · 1937 passed.
## 5. Reverse verification (ablations, via
`scripts/ablation-replace.mjs`, restore proven)
Both ablations ran from the committed state. Each replaced the
refinement's loop source `tursoTransportIssues(cfg)` with
`tursoTransportIssues(cfg).slice(0, 0)`, which empties the refusals and
leaves everything else in place.
- **Spec copy**, running `src/data/driver/turso.test.ts` (spec tests
import from `src/`, so no build is involved): mutation landed (anchor 1
→ 0, blob `5e1932a6` → `3725d3fa`). **14 failed · 15 passed**: every new
refusal case went red, and the preservation, describe and pre-existing
cases stayed green. Restored: blob == HEAD (`5e1932a6`), `git diff HEAD`
empty.
- **Mirror**, running `src/spec/` in driver-turso (the mirror is
imported from source): mutation landed (blob `7b32d246` → `1bcc7514`).
**22 failed · 157 passed · 16 skipped**: exactly the 21 mirror-equality
rows for `url` / `timeoutMs` refusals without a forced mode, plus the
flipped placeholder case. The mirror's `sync` row stayed green, because
its refusal is a separate block. Restored: blob == HEAD (`7b32d246`),
`git diff HEAD` empty.
The direction observed was the expected one (red).
## 6. Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` was re-derived on the actual change set at
`b7c250ca` (10 paths vs merge base `369bcbed`): **114 commands**, 36
more than the dispatch-time lead, because of
`content/docs/references/**`, the changeset and the service-datasource
test. All 114 were run with exit codes written to disk first, and re-run
at head `59c2391e` after the last commit. **Result at `59c2391e`: 111
exit 0, and 3 exit 3 (`PREREQUISITE NOT MET`, NOT MEASURED):
`check:skill-examples` (no `packages/client-react/dist`),
`check:dual-build-cjs-loads` (59 packages have no `dist/`) and
`check:type-check-debt` (12 ledgered dependencies have no built types).
All three need a whole-workspace build, which is CI's run.
`dispatch-gates --ran` reconciliation: `114 derived famil(ies) accounted
for — 111 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)`, 0
UNRUN.** Notable readings: `check-adr-0087-registration` → `registered
turso-config-transport-mismatch-refused (new here: …)`;
`check:generated` → `All 15 generated artifacts are up to date`;
`check:changeset-no-major` → no `major` bump;
`check:authorable-surface`, `check:api-surface`, `check:docs`,
`check:migration-registry`, `check:test-source-alias`,
`check:cross-package-test-inputs`, `check:nul-bytes`,
`check:doc-authoring` → exit 0. The six artifact-roster gates whose
roster sits under a touched directory were also run:
`check:meta-url-spelling`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`,
`check:object-def-param-keys` and `check:tenant-chokepoint`, all exit 0.
Not run locally, and left to CI: `pnpm lint` and the whole-workspace
type-check lanes.
## Arm 2's constructor half: answered, not written
`turso-driver.ts` is fenced for this card. Should `new TursoDriver` also
refuse `syncUrl` / `sync` under `mode: 'remote'`? **Yes, and the
evidence is above.** The constructor accepts the pair, `isSyncEnabled`
answers `true` for a sync that can never run, and the sync call rejects
as not supported. That is a declared setting the runtime does not
honour. The refusal would sit beside `refuseWebSocketTimeout` (remote
mode, before `super()`), in the same `VALIDATION_ERROR` / 400 envelope,
with this PR's `syncUrl` message as its wording. This PR refuses the
pair at every authoring door. A stored row or a host-built config that
bypasses the schema still constructs. Routed to the engine lane (carrier
#20104's lane, `domain:engine`) in the report.
## Acceptance notes
- **The mirror declares no `mode`.** zod strips an authored `mode`, so
the mirror judges every config in the mode its url and `syncUrl` select.
For example, it accepts `{ url: 'libsql://…', mode: 'replica' }` and
returns it without `mode`. That shortness is documented in
`docs/design/driver-turso.md` §10 and is not changed here. No in-repo
producer parses this mirror (a census of `TursoConfigSchema` importers
in this repo, objectui and cloud found its own test only).
- **`mode: 'replica'` on a `file:` url with no `syncUrl`** constructs
and runs as a plain local database: the replica sync arm needs
`syncUrl`. It is the same family as arm 2 (a declared mode the driver
does not honour), but no constructor refusal fixes its shape, so it is
not refused here.
- **A bare path under a forced `mode: 'remote'`** is accepted by the
constructor and refused by `@libsql/client` at connect (`URL_INVALID`).
That is loud, not silent, and the constructor does not judge it, so
authoring does not either.
- The loader fixtures listed in §4 spell a configuration both the
constructor and the schema now refuse, while testing key forwarding
only. They were left as they are.
- No example, template, published skill or hand-written doc in this repo
authors a refused combination. Stored datasource rows are not re-parsed
on load (`assertValidConfig` runs on create, test connection, and an
update that touches `config` / `driver`), so a stored row keeps loading.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4db1bf1 commit 172b4cf
10 files changed
Lines changed: 1018 additions & 14 deletions
File tree
- .changeset
- content/docs/references/data
- packages
- drivers/driver-turso/src/spec
- services/service-datasource/src/__tests__
- spec/src
- data/driver
- migrations
- entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| |||
0 commit comments