Commit 05cb2bc
Fixes #20586
Clause-②: yes (narrowing)
The `Clause-②` line above is the claim's (comment 5891827128), copied as
it stands. The changeset carries the same value.
Session `session_01DEvba2nBuD4tWzfq8r8NFY` (PM dispatch, `domain:engine`
seat 1, mode:subagent), branch
`claude/issue-20586-forced-local-refuses-sync-url`. The container
restarted mid-run. The branch was fast-forwarded to `origin/main`
`f4ce10c89` (BASE) before the first edit, and later got a true merge of
`origin/main` at `6bff748bb`. **Every final reading below was taken at
head `cfe05ec40`** unless it says otherwise.
## What changes
A turso config that forces `mode: 'local'` on a `file:` url (or
`:memory:`) beside a non-empty `syncUrl` is now refused at both doors,
with one message. Triage 5884612522 directed this: "A forced `mode:
'local'` beside `syncUrl` (or `sync`) is refused at both schema copies
and at the constructor, naming the conflict."
- **Authoring.** `tursoTransportIssues` in
`packages/spec/src/data/driver/turso.zod.ts` gains a forced-local arm,
placed after #20437's forced-replica arm. It returns one `custom` issue
on **`mode`**, the key the runtime would ignore, as #20437's does. It
reaches `DatasourceSchema` (as `config.mode`), `validateDriverConfig`,
`defineStack` / `os validate`, and a save or test connection through the
datasource admin service.
- **Construction.** `new TursoDriver()` refuses the same config with
`VALIDATION_ERROR` / 400, before `super()`, as the last of the sync-key
refusals. The message is a module constant,
`LOCAL_MODE_WITH_SYNC_URL_REFUSAL`, next to #20437's
`REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL`. It is thrown through the same
`refuseIgnoredSyncKey` helper, and the parity table pins it byte-equal
to the schema's issue.
- **The driver mirror**
(`packages/drivers/driver-turso/src/spec/turso.zod.ts`) carries the same
arm byte for byte. The mirror declares no `mode` and strips an authored
one, so the arm is unreachable through it. It stays for copy parity,
like the mirror's other forced-mode branches (see Deviations).
- **"(or `sync`)".** `sync` with no `syncUrl` was already refused in
every mode (#20200). `sync` beside a `syncUrl` under a forced local mode
is refused by this new arm. So every `sync` shape under a forced local
mode is covered, with no extra arm.
The message, the same text at both doors:
> `mode: 'local'` makes this datasource a plain local database, but
`syncUrl` names a remote to replicate from: the database would still be
synced with that remote as an embedded replica, so the declared local
mode would be ignored — the turso driver refuses this configuration when
it starts. For an embedded replica, drop `mode` and keep `syncUrl`
beside the local file: `url: 'file:./data/replica.db'`. For a plain
local database, drop `syncUrl` (and `sync`).
It names both ways out. It echoes no url and no `syncUrl`, and carries
no tracker id.
## H1: the premise, measured before the change (held)
At BASE `f4ce10c89`, a temporary probe ran on the driver source (deleted
after one run, never committed). The config was a `file:` url,
`syncUrl`, `sync: { intervalSeconds: 1 }`, and a client that counts
`sync()` calls:
| config | transportMode | syncs after connect | isSyncEnabled() |
interval | syncs after 1.3 s |
| --- | --- | --- | --- | --- | --- |
| forced `mode: 'local'` + `syncUrl` | `local` | 1 | true | started | 2
|
| `syncUrl`, no `mode` (the replica control) | `replica` | 1 | true |
started | 2 |
| forced `mode: 'local'`, no `syncUrl` | `local` | 0 | false | none | 0
|
So a forced local mode beside `syncUrl` ran exactly as a replica, and
only the label said `local`. The pins holding today's answer passed at
BASE:
- the parity row "file: + syncUrl under a forced mode: 'local'" read
constructor accept and spec accept (parity file 177 passed / 22
skipped);
- the spec test's accept fixture `{ url: 'file:./data/app.db', mode:
'local', syncUrl: … }`;
- the #20200 file's control "sync beside syncUrl under a forced mode:
'local' stays accepted".
`isSyncEnabled()` is `!!this.tursoConfig.syncUrl && this.libsqlClient
!== null`, as H1 states.
## H2: #20437 is the template (held), and what differs in this direction
It is mirrored arm for arm: one spec arm on `mode`, a byte-identical
mirror arm, a module constant, one constructor check, a new refusal test
file, a parity flip and a new D3 entry. What differs:
- The ignored key is `mode`, as in #20437, but the cause is the
opposite. A `syncUrl` is present, so the conflict is between two
declared keys, not a missing one. The message therefore offers "drop
`mode`" (replica) or "drop `syncUrl` (and `sync`)" (local).
- The arm is reachable on an in-memory url too. A local mode accepts
`:memory:` (`localEngineDefect` passes it), so `:memory:` and
`file::memory:` beside `syncUrl` under a forced local mode meet this
refusal. The replica way out points at a file url, so it is correct
there as well.
- No config meets two issues here. #20437 has one row where the schema
raises `sync` and `mode` together. This arm needs a non-empty `syncUrl`
and the `sync` refusal needs none, so they are exclusive, and every row
here raises exactly one issue.
## H3: ADR-0087 disposition — a new D3 entry, `registered`
Neither existing turso entry's `surface` names this shape.
`turso-config-transport-mismatch-refused` lists the url, in-memory,
WebSocket and remote-`syncUrl` combinations.
`turso-config-forced-replica-without-sync-url-refused` is the opposite
shape. The gate accepts `registered` only with an id that is new in this
diff, and `already-registered` only for an id that already covers the
refusal. So a new entry lands, following #20437's precedent:
`packages/spec/src/migrations/entries/semantic/18.turso-config-forced-local-with-sync-url-refused.ts`,
id `turso-config-forced-local-with-sync-url-refused`.
- `src/migrations/registry.ts` was regenerated by
`gen:migration-registry`, never by hand: +50 / -0, one entry. It reads
`320 semantic, 236 retired-key, 207 retired-def`.
- The changeset carries the disposition marker `registered
turso-config-forced-local-with-sync-url-refused`.
- `check-adr-0087-registration` reads it as
`[BREAKING+bang+clause-②-narrowing] registered
turso-config-forced-local-with-sync-url-refused (new here: …)`.
- `check:generated` reads "All 15 generated artifacts are up to date",
including `check:spec-changes` and `check:upgrade-guide`.
## H4: refusal order (held; the new arm is last)
- **Spec.** The arm sits after #20437's forced-replica arm, inside the
non-remote branch. Every url-shape refusal returns first, and they are
unchanged.
- **Constructor.** The check sits after the forced-replica check, which
is last among the existing refusals.
- **Exclusivity.** Every other refusal is exclusive of this one except
the url refusals, which both doors raise first. The remote refusals need
remote mode. The `sync` refusal needs no `syncUrl`. The replica refusal
needs a replica mode.
- **Pins.** ORDER rows cover a remote url, a bare path, `sync` with no
`syncUrl`, and #20437's forced replica with no `syncUrl`. Each keeps its
own message.
## H5: producer census (before any edit, at BASE `f4ce10c89`, repo
`objectstack-ai/objectstack`)
| query | hits |
| --- | --- |
| `git grep -E "mode:\s*['\"]local['\"]\|\"mode\"\s*:\s*\"local\""`
(whole tree) | 49 in 11 files: 11 in two CHANGELOGs, 38 in
`packages/drivers/driver-turso` and `packages/spec` (source, tests,
README). Of those, 3 author the shape beside a `syncUrl`, all tests: the
parity row, the spec accept fixture and the #20200 control, each flipped
here. The other `mode: 'local'` spellings carry no `syncUrl` or are
describe labels |
| `syncUrl` / `sync_url` / `SYNC_URL`, case-insensitive, per tree (turso
control count in brackets) | `examples/` 0 [2],
`packages/create-objectstack` 0 [0], `skills/` 0 [6], hand-written
`content/docs` 0 [109], `apps/` 0 [0] |
| env names read in `packages/**/src` (non-test) matching `TURSO_*` /
`OS_DATABASE_*` / `OS_TURSO_*` | `OS_DATABASE_URL`,
`OS_DATABASE_DRIVER`, `OS_DATABASE_AUTH_TOKEN`, `OS_DATABASE_POOL_MAX`,
`OS_DATABASE_SQLITE_JOURNAL_MODE`, `TURSO_DATABASE_URL`,
`TURSO_AUTH_TOKEN`, `TURSO_TOKEN` (plus code constants). None maps to
`mode` or `syncUrl` |
| who sets a turso `mode` | only `buildTursoDriverConfig`'s `mode`
reader
(`packages/services/service-datasource/src/turso-driver-config.ts:205`),
from an authored `datasource.config.mode`. Its two callers are
`packages/runtime/src/turso-driver-factory.ts:288` and
`packages/services/service-datasource/src/default-datasource-driver-factory.ts:1337`.
No other non-test `new TursoDriver` / `createTursoDriver` call sets
`mode` |
No shipped in-repo producer authors the shape, and no deployment default
sets it, so the `needs_decision` branch does not trigger.
**`objectstack-ai/cloud`: NOT MEASURED.** Attaching it read-only was
refused by the session's permission classifier. The seat or the
maintainer should census cloud before this lands.
## Tests (at `7ee1acb58`, the last code commit; the merge after it
touched no turso or spec path)
| suite | result |
| --- | --- |
| `@objectstack/driver-turso` vitest, whole package | 80 files · 2175
passed · 33 skipped · exit 0 |
| `@objectstack/driver-turso` typecheck (`tsc --noEmit`) | exit 0. `tsc
--listFilesOnly` lists all 3 touched test files |
| `@objectstack/spec` vitest `--project local`, 3 shards | 575 files ·
16946 passed · 1 todo (6106 + 5231 + 5609), exit 0 each |
| `@objectstack/spec` typecheck (tsc + scripts + `check:test-typecheck`)
| exit 0 |
| `@objectstack/spec` `check:generated` (after the spec build) | "All 15
generated artifacts are up to date" |
The 33 skips are the parity table's forced-mode rows for the mirror,
which strips `mode`. There were 22 before; this PR adds 11: 8 `mode`
rows, 2 ORDER `url` rows and 2 accept controls, less the 1 row that
moved.
- **`spec/turso-config-constructor-parity.test.ts`.** The row "file: +
syncUrl under a forced mode: 'local'" flips from `accept` to `refuse`,
`refusedOn: 'mode'`. Seven more `mode` rows are added: no `sync`, an
uppercase `FILE:` url, a url behind whitespace, `timeoutMs`, a `wss://`
`syncUrl`, `:memory:` and `file::memory:`. Two ORDER rows keep their
`url` refusal (a `libsql://` url and a bare path, each beside `syncUrl`
under a forced local mode). Two accept controls are added: a forced
local mode alone, and one beside an empty `syncUrl`. `SYNC_KEY_REFUSALS`
takes the new `mode` rows, so each of the 8 asserts the constructor's
message equals the spec issue's. New floors: `mode` at least 12, the
forced-local `mode` rows at least 8, sync-key refusals at least 20. The
unforced `file:` + `syncUrl` replica row ("a replica: file: + syncUrl")
is the unchanged control triage names.
- **`turso-driver-ignored-sync-key-refusal.test.ts`.** The control "sync
beside syncUrl under a forced mode: 'local' stays accepted" is removed,
because it pinned the defect. The header points to the new file. The
#20200 refusal assertions are untouched.
- **`turso-driver-forced-local-with-sync-url-refusal.test.ts` (new).**
The refusal is asserted as the envelope (`code` + `status`), plus its
first sentence and both ways out. It covers `file:` and `FILE:` urls,
`:memory:`, `file::memory:`, and a config beside `sync`, `timeout` or
`encryptionKey`. With a supplied client it is refused before any client
work. It also covers `createTursoDriver()` and a check that neither url
is echoed. ORDER: a remote url, a bare path, `sync` with no `syncUrl`,
and a forced replica with no `syncUrl` each keep their own refusal.
CONTROLS: the unforced replica still connects and syncs once; a forced
local mode with no `syncUrl` or an empty one syncs nothing; a forced
replica beside `syncUrl` and `:memory:` under a forced local mode both
construct; and `detectMode` still answers `local`.
- **`packages/spec/src/data/driver/turso.test.ts`.** The accept fixture
becomes `{ …, mode: 'local', syncUrl: '' }`, because an empty `syncUrl`
is unset. A new block asserts the refusal on `mode` over 6 configs, the
url echo, url-refusal ORDER, both authoring doors (`config.mode` and
`validateDriverConfig`), and the controls (the unforced replica with and
without `sync`, and a forced local mode alone, beside an empty
`syncUrl`, and on `:memory:`).
**Reverse verification** ran through `scripts/ablation-replace.mjs` from
the committed state at `7ee1acb58`. Each direction was predicted before
the run, and all three matched:
1. **The constructor refusal disabled.** ` if (mode === 'local' &&
config.syncUrl) {` became `if (false && …) {`, and the mutation landed
(anchor 1 → 0, blob `f9af3e93c573` → `35630f956f8f`). Predicted 26 RED.
Got **26 failed** / 201 passed: the new file's 10 refusal cases, plus
the parity table's 8 constructor verdicts and 8 byte-equality pins. No
ORDER or CONTROLS case failed. Restored: blob == HEAD, `git diff HEAD`
empty.
2. **One byte of the driver copy.** A doubled space was put after the
constant's first sentence (blob → `019694bf2026`). Predicted exactly the
8 byte-equality pins. Got **8 failed**, all "the constructor's message
is the spec contract's, byte for byte", while every verdict and
first-sentence case stayed green. Restored the same way.
3. **The spec arm disabled** in
`packages/spec/src/data/driver/turso.zod.ts` (blob `eed1efdaa33a` →
`7850f9fcb53e`), against the spec's own source-level test. Predicted 3
RED. Got **3 failed** / 36 passed: the refusal, the url-echo and the
both-doors cases. ORDER and controls stayed green. Restored the same
way.
The driver tests import the driver from `src`, so ablations 1 and 2
needed no build. Ablation 3 was read on the spec's own source tests
only. The parity table's spec half reads the built spec dist, and that
half was not re-ablated.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` ran at head `cfe05ec40`, after the final
commit and the merge. It lists 10 paths vs merge base `6bff748bb` 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` (workspace packages with no `dist/`) and
`check:type-check-debt` (it needs a whole-workspace build). Both are
CI's run.
- **Run twice:** `check:doc-formula-expressions` and
`check:lean-entry-closure` first answered exit 3. They are exit 0 after
building the `@objectstack/lint` and `@objectstack/objectql` closures.
- **Notable readings:**
- `check-adr-0087-registration`: `registered
turso-config-forced-local-with-sync-url-refused` (new here), BREAKING,
bang, clause-② narrowing;
- `check-changeset-no-major`: "This diff introduces no `major` bump";
- `check-empty-changeset`: exit 0;
- `check:migration-registry`, `check:spec-changes`,
`check:upgrade-guide`, `check:api-surface`, `check:authorable-surface`,
`check:docs`, `check:doc-authoring`, `check:nul-bytes`,
`check:test-source-alias`, `check:cross-package-test-inputs` and
`check:driver-conformance`: exit 0.
- **Narrowed lint:** `eslint --no-inline-config --format json` over the
9 changed TS files reports 9 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 the config enables no type-aware
linting (its one `parserOptions.project` mention is a comment saying
so), 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; and the downstream suites of
`@objectstack/service-datasource`, `@objectstack/runtime` and
`@objectstack/cli`. The narrowing is declared. The public surface's
bytes are unchanged (`check:api-surface` and `check:authorable-surface`
are green, and refinements are not in the JSON Schema). The census above
finds no consumer fixture that authors the refused shape.
**Driver-conformance ledger:** `check:driver-conformance` read `OK — 50
covered cell(s), 0 in the DEBT ledger, 0 exempt` both before
(`f4ce10c89`) and after (`cfe05ec40`). `driver-turso` is `ok` on all 10
case-sets both times, so there was no movement.
## Deviations (declared)
- **"Both schema copies" is met as text, not as a verdict, at the
mirror.** Triage's pin names the refusal "at both schema copies". The
driver mirror declares no `mode` key and strips an authored one, so it
cannot see a forced mode. It carries the arm byte for byte and still
accepts this config, judging it as the replica its url and `syncUrl`
select. That is #20437's documented reading, and the parity table skips
the mirror half of forced-mode rows. Giving the mirror a `mode` key is
outside this card: the parity test's header calls that shortness "not
this card's to change". The D3 entry's `surface` and the changeset say
this rather than claiming the mirror refuses.
- **Cloud producers were not measured** (H5). Attaching
`objectstack-ai/cloud` was refused by the permission classifier. Nothing
in-repo triggers `needs_decision`.
- **The H1 runtime probe used a supplied client.** A client that counts
syncs stood in for `@libsql/client`, so the probe made no network call.
The arm it exercises (`connect()`'s `syncUrl` branch) is the one a
driver-built client takes too.
## Acceptance notes
- `packages/drivers/driver-turso/README.md`: the constructor-refusal
list now reads four sync settings and gains an item for a forced `mode:
'local'` beside a non-empty `syncUrl`, with both ways out. Patch round 1
(`a0c4c033a`, README text only) made this change after contract review
5894260625 found the "three sync settings" sentence false at
`cfe05ec40`. Its gate re-run reconciles 91 derived, 89 run, 2
NOT-MEASURED, 0 UNRUN.
- The spec's `mode` key keeps its one-line TSDoc. The rule is carried by
the refusal text and by `TursoDriverConfig.mode`'s TSDoc in the driver,
which now names it (as does `syncUrl`'s).
- The existing D3 entries `turso-config-transport-mismatch-refused` and
`turso-config-forced-replica-without-sync-url-refused` are untouched.
Both stay true.
- Not touched: `turso-driver.ts`'s remote filter lowering
(`toRemoteUpperBound`, the `$between` / `$lte` arms), which the spec
lane's PR #20643 edits. This PR's hunks there are the file header, the
`syncUrl` / `mode` TSDoc, the new constant beside the sync-key refusals,
and one constructor check.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6981abf commit 05cb2bc
11 files changed
Lines changed: 575 additions & 30 deletions
File tree
- .changeset
- packages
- drivers/driver-turso
- src
- spec
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
| 226 | + | |
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
237 | 241 | | |
238 | 242 | | |
239 | 243 | | |
| |||
Lines changed: 34 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
34 | 37 | | |
35 | 38 | | |
36 | 39 | | |
| |||
103 | 106 | | |
104 | 107 | | |
105 | 108 | | |
106 | | - | |
| 109 | + | |
| 110 | + | |
107 | 111 | | |
108 | 112 | | |
109 | 113 | | |
| |||
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
| 126 | + | |
| 127 | + | |
122 | 128 | | |
123 | 129 | | |
124 | 130 | | |
| |||
131 | 137 | | |
132 | 138 | | |
133 | 139 | | |
| 140 | + | |
| 141 | + | |
134 | 142 | | |
135 | 143 | | |
136 | 144 | | |
| |||
162 | 170 | | |
163 | 171 | | |
164 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
165 | 184 | | |
166 | 185 | | |
167 | 186 | | |
168 | | - | |
169 | | - | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
170 | 190 | | |
171 | 191 | | |
172 | 192 | | |
| |||
225 | 245 | | |
226 | 246 | | |
227 | 247 | | |
228 | | - | |
229 | | - | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
230 | 253 | | |
231 | 254 | | |
232 | 255 | | |
| |||
258 | 281 | | |
259 | 282 | | |
260 | 283 | | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
265 | 288 | | |
266 | | - | |
| 289 | + | |
267 | 290 | | |
268 | 291 | | |
269 | 292 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
260 | 278 | | |
261 | 279 | | |
262 | 280 | | |
| |||
0 commit comments