Repository navigation
fix(spec)!: TursoConfigSchema refuses the turso configs the driver refuses or ignores (#19977) - #20199
Conversation
…turso driver refuses or ignores A remote url in a local or replica mode, a url that is none of file:, :memory: or remote in those modes, a replica on an in-memory url, and timeoutMs beside a WebSocket url in remote mode are refused at construction by the driver; syncUrl under a forced mode: 'remote' is constructed and ignored. The spec contract now refuses all five at authoring, each naming the supported spelling. The url describe names the file: spelling instead of "a file path". Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…at the driver refuses or ignores The mirror carries the spec contract's transport refusals byte for byte, plus its sync-without-syncUrl refusal. One case table holds the constructor, the spec contract and this mirror to the same verdicts. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…set, regenerated reference Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…eplica, not a remote url beside syncUrl Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…rso-config-authoring-refusals
…al changeset Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2dfe95bcc9202adc142603bf7c69c00ccc99c588 && git checkout 2dfe95bcc9202adc142603bf7c69c00ccc99c588
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d7c024133e77f69aa0f26af359391c6a4b0142e4 59c2391e2a91e6ec2b26626a2c85bb6c710cd5d4 && git checkout -B drift-repro d7c024133e77f69aa0f26af359391c6a4b0142e4 && git merge --no-ff 59c2391e2a91e6ec2b26626a2c85bb6c710cd5d4
node scripts/docs-audit/affected-docs.mjs --json d7c024133e77f69aa0f26af359391c6a4b0142e4
|
Contract reviewServed-tier: ① Derived judgmentsInputs: card #19977 (body + 9 comments), PR #20199 (body, diff, files, head check-runs), the repo at
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…mote mode, and sync with no syncUrl (objectstack-ai#20200) (objectstack-ai#20447) Fixes objectstack-ai#20200 Clause-②: no (narrowing) The `Clause-②` line above is the amended claim's (comment 5869128452, unchanged in 5871001040), copied as it stands. The changeset carries the same value. Session `session_01N8TPEsoJxPsdSdNKGnNGEN` (PM dispatch, `domain:engine` seat 1, mode:subagent), branch `claude/issue-20200-turso-remote-sync-refused`. The branch starts at `e01d34730`, which already contains PR objectstack-ai#20422 and PR objectstack-ai#20425. It was then merged with `origin/main` at `87c37aec1` in a true merge commit. **Every reading below was taken at head `2242ad513`** unless it says otherwise. ## What changes `new TursoDriver(...)` refuses two configurations it used to build while ignoring one of their keys. Both are refused with `VALIDATION_ERROR` / 400, before `super()`, after the constructor's three existing refusals, so no configuration they already refuse gets a different message: - **`syncUrl` under a forced `mode: 'remote'`.** A remote url beside `syncUrl` with no `mode` was already refused, as a replica on a remote url (`localEngineDefect`), so only a forced remote mode reaches this refusal. - **`sync` with no `syncUrl`**, in every mode: local, replica and remote. An empty `syncUrl` counts as unset, which matches `detectMode`, `connect()`, `sync()` and the spec's refinement. Each refusal's message is `@objectstack/spec`'s `TursoConfigSchema` issue message for that key, byte for byte. Per the seat's ruling (option A of the report `5869079328`), each is a module-level constant in `turso-driver.ts`: `REMOTE_MODE_SYNC_URL_REFUSAL` and `SYNC_WITHOUT_SYNC_URL_REFUSAL`. The parity test holds each constant equal to the schema's issue, read from the built spec dist. No new `packages/spec` export. **The message must be true where it is thrown** (seat ruling 2, a declared cross-lane text edit). The spec's `syncUrl`-under-remote text said "the turso driver never hands `syncUrl` to the remote client and runs no sync, so the setting changes nothing". That clause now reads "the turso driver refuses this configuration when it starts", the form its sibling refusals in `tursoTransportIssues` use. The rest of the message is byte-identical. The three copies now read the same: the spec, the driver's `TursoConfigSchema` mirror and the constructor constant. The `sync` message is reused unchanged. The ADR-0087 entry `18.turso-config-transport-mismatch-refused` has its `reason` sentence on stored rows corrected. It now says the constructor also refuses `syncUrl` under a forced remote mode and `sync` with no `syncUrl` when the datasource boots, citing objectstack-ai#20200. Patch round 1 also put its header comment and its "One more it builds and then ignores" clause in the past tense, bounded by objectstack-ai#20200. Nothing else in the entry moved. `check:generated` then proved `src/migrations/registry.ts` stale, because the registry embeds the entry text, and `check:generated --fix` regenerated it: a 4-line text diff. No gate refused editing a registered entry. ## H1: before and after (dist probe, 9 configs) The same scratch script ran against `packages/drivers/driver-turso/dist/index.mjs`. Before is at `dbddf02c1` (origin/main when the first round measured). After is this branch's build. | config | before (`dbddf02c1`) | after | | --- | --- | --- | | `libsql://` + `mode: 'remote'` + `syncUrl` + `sync` | constructs, connects, `isSyncEnabled()` true, no interval, `sync()` rejects `SYNC_NOT_SUPPORTED` | refused, VALIDATION_ERROR / 400, `syncUrl` message | | `libsql://` + `mode: 'remote'` + `syncUrl` | the same | refused, `syncUrl` message | | `file:` + `mode: 'remote'` + `syncUrl` + `sync` | constructs, connects, `isSyncEnabled()` true, no interval, `sync()` rejects `SyncNotSupported("File")` | refused, `syncUrl` message | | `libsql://` + `mode: 'remote'` + `sync`, no `syncUrl` | constructs, `isSyncEnabled()` false, `sync()` a no-op | refused, `sync` message | | `libsql://` (no mode) + `sync`, no `syncUrl` | the same | refused, `sync` message | | CONTROL `libsql://` + `mode: 'remote'` | constructs, connects, `isSyncEnabled()` false | unchanged | | RIDER `file:` + `mode: 'replica'`, no `syncUrl` | constructs as `replica`, `isSyncEnabled()` false, `sync()` a no-op | **unchanged (see the rider section below)** | | RIDER + `sync`, no `syncUrl` | the same | refused, `sync` message (the `sync` key, not the rider) | | `file:` + `sync`, no `syncUrl`, no mode | local, `sync` ignored | refused, `sync` message | ## H4: what a stored row answers at boot now `buildTursoDriverConfig` (`packages/services/service-datasource/src/turso-driver-config.ts`, read and not edited) forwards a stored row's `syncUrl`, `sync` and `mode` unparsed. PR objectstack-ai#20199's ADR-0087 entry is semantic, so no D2 conversion rewrites such a row either. A row stored before PR objectstack-ai#20199 with a remote url, `mode: 'remote'` and `syncUrl`, or with `sync` and no `syncUrl`, therefore reaches the constructor as written: - `factory.create` throws the refusal (`default-datasource-driver-factory.ts` for open-core, `turso-driver-factory.ts` for the host default). - `DatasourceConnectionService` catches it and records the datasource as `failed-degraded`, with "datasource NAME: connect failed — MESSAGE". - Under ADR-0062 D5, boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. - A test connection answers `ok: false`, with "Failed to build driver: MESSAGE". Before this change the same row booted, reported sync as enabled, and never synced. No in-repo caller reads `isSyncEnabled()` or calls the driver's sync outside `@objectstack/driver-turso`'s own tests. The changeset's FROM → TO paragraph carries these readings and the way out: drop `syncUrl` / `sync` from a remote config, or use a `file:` url with the remote in `syncUrl`. ## The rider stays: `mode: 'replica'` on a `file:` url with no `syncUrl` This configuration still constructs and runs as a plain local database. The table's H1 rider row shows it: a declared replica that never syncs. It is deliberately not refused here. A constructor-only refusal would make construction refuse a configuration both `TursoConfigSchema` copies accept, which reopens objectstack-ai#19977's defect class in reverse (a datasource that authors clean and fails at boot). The parity table would go red on its row "file: under a forced mode: 'replica'", and `turso-driver-unrecognised-url-refusal.test.ts` pins `FILE: + mode 'replica', no syncUrl` as accepted. Closing it needs the spec half and the constructor half together, and the spec's accept set is outside this card, so the seat files it as its own card. The new test file pins the rider as still accepted, so whoever closes it moves that pin on purpose. ## Tests | suite at `2242ad513` | result | | --- | --- | | `@objectstack/driver-turso` vitest, whole package | 75 files · 2014 passed · 18 skipped · exit 0 | | `@objectstack/driver-turso` typecheck (`tsc --noEmit`) | exit 0; `--listFilesOnly` on the pre-merge commit shows both touched test files in the program | | `@objectstack/spec` vitest `--project local`, 3 shards | 564 files · 16640 passed · 1 todo (6026 + 5141 + 5473), exit 0 on each shard | | `@objectstack/spec` typecheck (tsc + scripts + `check:test-typecheck`) | exit 0 | The 18 skips are the parity table's forced-mode rows for the mirror, which strips `mode`: 16 before, plus the two new forced-mode `sync` rows. - **`spec/turso-config-constructor-parity.test.ts`:** the four `inert` rows flip to `ctor: 'refuse'` (three `syncUrl`, one `sync`). Four `sync` rows are added: a remote url, a forced remote, a forced replica on `file:`, and an empty `syncUrl`. The `inert` floor moves from at least 4 to exactly 0, with floors added for `syncUrl` (at least 3), `sync` (at least 5) and the sync-key refusals (at least 8). A new table, `SYNC_KEY_REFUSALS`, asserts for each of those 8 rows that the constructor's `error.message` equals the spec issue's message. - **`turso-driver-ignored-sync-key-refusal.test.ts` (new):** both refusals are asserted as the envelope (`code` + `status`) plus the message's first sentence, across `libsql://`, `https://`, `file:`, `:memory:`, forced remote, forced replica and an empty `syncUrl`. `createTursoDriver()` is covered too. Controls: forced remote with no `syncUrl` connects and reports sync off; a `file:` replica beside `syncUrl`; `sync` beside `syncUrl` under forced `mode: 'local'`; the rider. - **`packages/spec/src/data/driver/turso.test.ts`:** one assertion and its test title pinned the old clause ("never hands `syncUrl` to the remote client and runs no sync"). They now pin the new clause. This file is not named in the claim's surface; the change is the mechanical consequence of ruling 2's text edit (see Deviations). **Reverse verification**, via `scripts/ablation-replace.mjs` from the committed state (turso-driver.ts blob `afe3ad31`). The driver tests import `../turso-driver` from source, so no build is involved. Directions were predicted before each run, and all three matched: 1. `if (mode === 'remote' && config.syncUrl) {` became `if (false && …) {`, mutation landed (anchor 1 → 0, blob `afe3ad31` → `5f9aca88`). **11 failed** / 169 passed: exactly the 5 `syncUrl` cases in the new file, plus the 3 `syncUrl` rows' constructor verdicts and their 3 message pins. Restored: blob == HEAD, `git diff HEAD` empty. 2. `if (config.sync && !config.syncUrl) {` got the same mutation (blob → `32b6a28c`). **16 failed** / 164 passed: the 6 `sync` cases, plus 5 constructor verdicts and 5 message pins. Restored the same way. 3. One byte of the copy: a doubled space inside the `syncUrl` constant, after its first sentence (blob → `084c8d96`). **3 failed**: exactly the 3 `syncUrl` message pins, while every verdict and first-sentence case stayed green. This proves the byte-equality pin is what holds the copy. Restored the same way. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run after the last commit at `2242ad513`, lists 9 paths vs merge base `87c37aec1` and **91 commands**. Every command ran, with its exit code written to disk before any pipe. `--ran` reconciliation reads `91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, with 0 UNRUN. - **NOT MEASURED (exit 3, PREREQUISITE NOT MET):** `check:dual-build-cjs-loads` (dozens of workspace packages have no `dist/`) and `check:type-check-debt` (it needs a whole-workspace build). Both need a full-workspace build, which is CI's run. - **Run twice:** `check:doc-formula-expressions` and `check:lean-entry-closure` first answered exit 3. They are exit 0 after building `@objectstack/lint` and `@objectstack/objectql` with their closures. - **Notable readings:** - `check-adr-0087-registration` → `not-required (already-registered)` for `turso-config-transport-mismatch-refused`, clause-② narrowing, BREAKING, bang; - `check-changeset-no-major` exit 0; - `check:migration-registry` → `registry.ts is current`; - `check:driver-conformance`, `check:nul-bytes`, `check:doc-authoring`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:spec-changes` and `check:upgrade-guide` → exit 0. - **Roster families under a touched directory, also run:** five artifact-roster families whose roster sits under a directory this diff touches, all exit 0: `check-changeset-fixed`, spec `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **Narrowed lint:** `eslint --no-inline-config --format json` over the 8 changed TS files reports 8 files, 0 errors and 0 warnings. The population is `eslint.config.mjs`'s lint object, `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the changeset `.md` is outside it. Invariance holds because that config never enables type-aware linting (no `parserOptions.project`, per its own header), so this diff cannot move any untouched file's verdict. The full `pnpm lint` is CI's. - **Not run locally, left to CI:** - the whole-workspace type-check lanes; - the Test Core, Dogfood and Build Core jobs; - downstream suites of `@objectstack/service-datasource`, `@objectstack/runtime` and `@objectstack/cli`. Their turso fixtures go through `buildTursoDriverConfig` or a capturing constructor and never build the real driver. The downstream real-driver constructions are both in dogfood's `date-bucket-parity-turso.test.ts`: `new TursoDriver({ url: ':memory:' })`, and at `:144` a remote `{ url: 'libsql://test-db.turso.io', authToken }`. Neither carries a sync key, so neither refusal touches them. **Driver-conformance ledger (`lanes/engine.md`):** `check:driver-conformance` read `OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt` both before (`dbddf02c1`) and after (`2242ad513`). `driver-turso` is `ok` on all 10 case-sets both times. No movement. ## Deviations (declared) - `packages/spec/src/data/driver/turso.test.ts`: one assertion and one test title, rewritten because ruling 2's text edit makes the old pin false. It is outside the claim's listed surface ("nothing else in `packages/spec`"). Without it the spec suite goes red. - `turso-driver.ts` outside the constructor body. It carries the two module constants and a small throw helper (`refuseIgnoredSyncKey`) beside `refuseSuppliedClientTimeout`, as ruling 1 prescribes. It also extends the TSDoc on `TursoDriverConfig.syncUrl` and `.sync` so the declared config names the two new refusals. No other region of the file changed. ## Acceptance notes - **The mirror's dormant copy is now kept equal by this diff.** The driver mirror (`src/spec/turso.zod.ts`) strips `mode`, so its copy of the `syncUrl`-under-remote text is still unreachable through a parse, and no test can hold it equal. This diff applies the same one-clause edit there, so the three copies read identically today. - **The spec wording item from the report is done here** (ruling 2). - **Comments that described the ignored sync keys: corrected in patch round 1** (the amended claim 5871001040). Every comment in this diff's files that described `syncUrl` under a forced remote mode, or `sync` with no `syncUrl`, as constructed and ignored now says what holds since objectstack-ai#20200 (list below). A `git grep -n -i ignore` over the touched files finds no such wording left. What remains is only `packages/drivers/driver-turso/README.md`'s list of constructor refusals. It is scoped to the local-engine refusals and does not name the two new ones: incomplete, not false (the review's reading), and a docs follow-up. It is not runtime text. - Loader fixtures in `@objectstack/service-datasource` (`turso-driver-config.test.ts`, the `mode: 'remote'` + `syncUrl` key-list case) spell a configuration the real constructor now refuses. They exercise `buildTursoDriverConfig` only and never construct the driver, and the package is fenced, so they are unchanged. ## Patch round 1 (text only, head `7052b9111`) The at-tier review 5870987840 PASSed `2242ad513` and escalated comments this diff made false. With the `inert` floor at 0, the driver no longer constructs and ignores anything the schemas refuse. Per the amended claim 5871001040, those comments are corrected here as text only. No logic and no test assertion changed. - **Spec `packages/spec/src/data/driver/turso.zod.ts`:** - section heading 1b "refuses or ignores" → "refuses"; - the intro "or constructs and then ignores" → "or, until objectstack-ai#20200, constructed and then ignored"; - the `syncUrl`-under-remote bullet ("which the driver accepts and then IGNORES … That arm has no constructor refusal behind it") is now in the past tense and says the constructor refuses it too since objectstack-ai#20200; - "Nothing the constructor accepts is refused here, the `syncUrl`-under-`mode: 'remote'` arm aside" now says that exception has been a constructor refusal too since objectstack-ai#20200; - the superRefine comment "What the driver refuses at construction, or constructs and ignores" → "What the driver refuses at construction". - **Mirror `packages/drivers/driver-turso/src/spec/turso.zod.ts`:** section heading 2a "refuses or ignores" → "refuses"; the intro "or constructs and ignores" → "or, until objectstack-ai#20200, constructed and ignored"; the superRefine comment corrected as in the spec. - **Spec `turso.test.ts`:** the block comment "(which it constructs and ignores)" → "(since objectstack-ai#20200 that includes `syncUrl` under a forced `mode: 'remote'`, which it used to construct and ignore)". - **Parity test:** two comments. The header's "(or constructs and ignores)" → "(or, until objectstack-ai#20200, constructed and ignored)", and "nothing it accepts but the declared-and-ignored keys" → "… but an `inert` row's declared-and-ignored key (none since objectstack-ai#20200)". - **D3 entry `18.turso-config-transport-mismatch-refused`:** - its header comment now reads "the one combination the driver used to build and then ignore (syncUrl under a forced remote mode), which the constructor refuses too since objectstack-ai#20200"; - its `reason` clause "One more it builds and then ignores … Nothing the constructor accepts is refused, that key aside" is in the past tense and bounded by objectstack-ai#20200 ("One more it built and then ignored until objectstack-ai#20200 …"; "Nothing the constructor accepts is refused (at objectstack-ai#19977 that key was the one exception; since objectstack-ai#20200 there is none)"); - nothing else in the entry moved, and `registry.ts` was regenerated by `gen:migration-registry`, its hunk equal to the entry's. - **Left as they are, each true:** "would ignore" (a conditional, naming what the refusals prevent), the `inert` mechanism's own definition (with none today), the "before" measurement table in the new test file, and `@libsql/client` ignoring `syncUrl` beside a remote url (a fact about the client). Readings at `7052b9111`. That head is the round's two commits plus a true merge of `origin/main` `8cdbe0c6e`, which regenerated `registry.ts` for its own new entry; the registry is current, with 308 semantic entries. - **CI on `2242ad513` before the push:** 35 check runs, 32 success, 3 skipped, 0 failed. Test Core 1/6, 3/6 and 5/6 and Type Check · workspace all concluded success. - **Tests:** `@objectstack/driver-turso` 75 files · 2014 passed · 18 skipped, typecheck exit 0. `@objectstack/spec` `--project local` in 3 shards: 6025 + 5141 (1 todo) + 5473 passed, exit 0 on each. A first shard-2 attempt was killed by the runner's own timeout and re-run. Spec typecheck exit 0. - **Gates:** `dispatch-gates --commands` finds 9 paths vs merge base `8cdbe0c6e` and 91 commands, all run with exit codes recorded before any pipe. `--ran` reads `91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, 0 UNRUN; the two NOT MEASURED are `check:dual-build-cjs-loads` and `check:type-check-debt` (whole-workspace build; both green in CI at `2242ad513`). Also run: the five roster families under touched directories, and spec `check:generated` ("All 15 generated artifacts are up to date"), all exit 0. - **Other readings:** `check:driver-conformance` is unchanged at 50 covered, 0 DEBT. The narrowed eslint run finds 8 files, 0 errors. The control-byte scan finds nothing. --- _Generated by [Claude Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19977
Clause-②: no
The
Clause-②: noline above is the claim's line (comment 5852270976), copied as it stands. The changeset carriesClause-②: no (narrowing), because AGENTS.md makes a narrowing BREAKING andcheck:adr-0087-registrationreads 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:specseat 1), branchclaude/issue-19977-turso-config-authoring-refusals. Base49144fcc, then merged withorigin/mainat369bcbed, which includes #20104 as84880f92. Head59c2391e. Every reading below was taken on that merged tree unless it says otherwise.1. The premise, measured
packages/spec/src/data/driver/turso.zod.ts): thesuperRefinechecked onlysyncwithoutsyncUrl. Driver-local mirror (packages/drivers/driver-turso/src/spec/turso.zod.ts): no refinement at all, and nomodekey. It is a plainz.object, so an authoredmodeis stripped.main. Read from the source, not from notes:localEngineDefect,refuseWebSocketTimeout,refuseSuppliedClientTimeoutanddetectModeinturso-driver.ts. Probed on the built dist at49144fcc, the constructor refuses every row below and both schemas accepted every one of them:new TursoDriverlibsql://,https://,wss://,LIBSQL://url +syncUrllibsql://url +mode: 'replica'or'local'sqlite:,:MEMORY:,libsql:with no//(no mode, orsyncUrl, ormode: 'local'):memory:,file::memory:,FILE::memory:,file::memory:?cache=shared+syncUrl;:memory:+mode: 'replica'wss://orWS://+timeoutMs(no mode, ormode: 'remote')49144fcc.new TursoDriver({ url, mode: 'remote', syncUrl, sync: { intervalSeconds: 60 } })constructs and connects. After that,isSyncEnabledanswerstrue, no interval is started, and the sync call rejects withSYNC_NOT_SUPPORTED(on alibsql://url) orSyncNotSupported("File")(on afile:url). The builtcreateRemoteClientforwardsurl,authToken,concurrencyandfetch, and nosyncUrl.84880f92). Itsturso-driver.tshunks sit at:26,:588and:2720only (imports, and the remote refusals of inherited members).constructor,detectMode,localEngineDefect, therefuse*helpers,isSyncEnabled, the sync method andcreateRemoteClientare untouched, and its onlysyncUrlhit is a message string. So the refused set this PR mirrors is unchanged by it, and it covers nothing here.2. The change
@objectstack/specTursoConfigSchema. A new module-localtursoTransportIssuesruns inside the existingsuperRefine, after thesync-without-syncUrlrefusal. 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 afile:url whose path is:memory:(or starts:memory:?) is in-memory. The url is read trimmed, because both loaders trim it before construction (resolveTursoUrl), andsyncUrlcounts when it is a non-empty string, asbuildTursoDriverConfigforwards it. Checks run in the constructor's order, so each config gets exactly one refusal onurl.The same refinement in the driver-local mirror, byte for byte, plus the spec's
sync-without-syncUrlrefusal in the same words. The mirror had no refinement at all. The driver ignoressyncwithoutsyncUrl, 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/specand 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.urldescribe (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.mdxwas 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), withregistry.tsregenerated. The changeset.changeset/19977-turso-config-transport-refusals.mdisminorfor@objectstack/specand@objectstack/driver-turso, with the FROM → TO table and theregisteredmarker.3. Every refusal, verbatim (rendered from the built spec dist)
Each is one
customissue on the key named in the heading.on
url— a remote url besidesyncUrl({ url: 'libsql://db.turso.io', syncUrl: 'libsql://db.turso.io' }):on
url— a remote url under a forcedmode: 'replica'({ url: 'libsql://db.turso.io', mode: 'replica' }):on
url— a remote url under a forcedmode: 'local'({ url: 'https://db.turso.io', mode: 'local' }):on
url— a url that is none offile:,:memory:or remote ({ url: './data/app.db' }):The same refusal beside
syncUrl({ url: './data/replica.db', syncUrl: … }) swaps in the replica wording:on
url— a replica on an in-memory url ({ url: ':memory:', syncUrl: … }):With
mode: 'replica'instead ofsyncUrl, the way out readsor 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 }):on
syncUrl—syncUrlunder a forcedmode: 'remote'(constructed and ignored by the driver;{ url: 'libsql://db.turso.io', mode: 'remote', syncUrl: … }):on
sync, in the mirror only ({ url: 'file:…', sync: {…} }), the spec's existing words, unchanged: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://orFILE:url;file:+mode: 'replica'with nosyncUrl;file:+syncUrlundermode: 'local'; any url under a forcedmode: 'remote', includingfile:and a bare path (the constructor does not judge it, and@libsql/clientrefuses a bare path at connect);timeoutMsbesidelibsql:///https://;timeoutMson a replica syncing fromwss://; an emptysyncUrl; 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 ascode: '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 ONEcustomissue 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 amodeskip (c), because the mirror stripsmode(16 skips). Row floors per verdict class make a shrunken table fail. The spec half resolves@objectstack/spec/datathrough its builtdist, per theKNOWN_UNALIASED_TEST_IMPORTSpairdriver-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 theDatasourceSchemadoor (re-pathed toconfig.url), thevalidateDriverConfigdoor, the unchangedsyncmessage verbatim, and the describe text. The preservation list gained 11 accepted configs.syncUrl, a forced mode or a bare url, parsed by either schema):packages/spec/src/data/driver/turso.test.ts: thesync.intervalSecondsfixture putsyncUrlbeside a remote url. It now usesurl: 'file:./data/replica.db'. The spelling changed and the assertion is unchanged.packages/drivers/driver-turso/src/spec/turso.test.ts"all fields": same,url: 'file:./local-replica.db'.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 offile:,: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.packages/services/service-datasource/src/__tests__/datasource-config-redaction.test.ts, the turso:?authToken=in an authoredconfig.url/config.syncUrlquery string is credential material the #8082 userinfo refusal does not cover #8337 legacy row. It put both token-bearing urls on a remoteurlbesidesyncUrl, whichupdateDatasourcenow 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 onfile:./data/replica.db?authToken=…, and the author's hand edit isfile:./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 besidesyncUrl+mode: 'replica'. They exercise onlybuildTursoDriverConfigor a capturing constructor, never this schema or the real driver, so they are left unchanged (see Acceptance notes).Readings on the merged tree:
@objectstack/driver-tursovitest (whole package)@objectstack/driver-tursotypecheck (tsc --noEmit, program includes both touched test files)@objectstack/service-datasourcevitest (consumer)@objectstack/service-datasourcetypecheck@objectstack/spectypecheck (tsc+ scripts +check:test-typecheck)@objectstack/specvitest--project local, 3 shardsBefore the merge, at
dd67e8ed, the spec suite read 540 files · 15812 passed · 2 todo in 3 shards, and the targetedsrc/data/driver,src/data/datasource,src/migrations,src/conversionsandsrc/sharedrun 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)withtursoTransportIssues(cfg).slice(0, 0), which empties the refusals and leaves everything else in place.src/data/driver/turso.test.ts(spec tests import fromsrc/, so no build is involved): mutation landed (anchor 1 → 0, blob5e1932a6→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 HEADempty.src/spec/in driver-turso (the mirror is imported from source): mutation landed (blob7b32d246→1bcc7514). 22 failed · 157 passed · 16 skipped: exactly the 21 mirror-equality rows forurl/timeoutMsrefusals without a forced mode, plus the flipped placeholder case. The mirror'ssyncrow stayed green, because its refusal is a separate block. Restored: blob == HEAD (7b32d246),git diff HEADempty.The direction observed was the expected one (red).
6. Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwas re-derived on the actual change set atb7c250ca(10 paths vs merge base369bcbed): 114 commands, 36 more than the dispatch-time lead, because ofcontent/docs/references/**, the changeset and the service-datasource test. All 114 were run with exit codes written to disk first, and re-run at head59c2391eafter the last commit. Result at59c2391e: 111 exit 0, and 3 exit 3 (PREREQUISITE NOT MET, NOT MEASURED):check:skill-examples(nopackages/client-react/dist),check:dual-build-cjs-loads(59 packages have nodist/) andcheck:type-check-debt(12 ledgered dependencies have no built types). All three need a whole-workspace build, which is CI's run.dispatch-gates --ranreconciliation: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→ nomajorbump;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-keysandcheck:tenant-chokepoint, all exit 0. Not run locally, and left to CI:pnpm lintand the whole-workspace type-check lanes.Arm 2's constructor half: answered, not written
turso-driver.tsis fenced for this card. Shouldnew TursoDriveralso refusesyncUrl/syncundermode: 'remote'? Yes, and the evidence is above. The constructor accepts the pair,isSyncEnabledanswerstruefor 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 besiderefuseWebSocketTimeout(remote mode, beforesuper()), in the sameVALIDATION_ERROR/ 400 envelope, with this PR'ssyncUrlmessage 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
mode. zod strips an authoredmode, so the mirror judges every config in the mode its url andsyncUrlselect. For example, it accepts{ url: 'libsql://…', mode: 'replica' }and returns it withoutmode. That shortness is documented indocs/design/driver-turso.md§10 and is not changed here. No in-repo producer parses this mirror (a census ofTursoConfigSchemaimporters in this repo, objectui and cloud found its own test only).mode: 'replica'on afile:url with nosyncUrlconstructs and runs as a plain local database: the replica sync arm needssyncUrl. 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.mode: 'remote'is accepted by the constructor and refused by@libsql/clientat connect (URL_INVALID). That is loud, not silent, and the constructor does not judge it, so authoring does not either.assertValidConfigruns on create, test connection, and an update that touchesconfig/driver), so a stored row keeps loading.Generated by Claude Code