Skip to content

fix(spec)!: TursoConfigSchema refuses the turso configs the driver refuses or ignores (#19977) - #20199

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-19977-turso-config-authoring-refusals
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-19977-turso-config-authoring-refusals

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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.
  • fix(driver-turso)!: a remote TursoDriver answers or refuses every inherited SqlDriver member (#20055) #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 turso: ?authToken= in an authored config.url / config.syncUrl query string is credential material the #8082 userinfo refusal does not cover #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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-turso, @objectstack/spec, touching 18 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via automation.trigger (sdk, the route ledger binds it to POST /automation/trigger/:name, selected by route anchor /trigger/:name))
  • content/docs/api/plugin-endpoints.mdx (via /trigger/:name (route, bridged from symbol timeoutMs — its route source's handler names it))
  • content/docs/automation/flows.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/automation/jobs.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/automation/webhooks.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/protocol/kernel/lifecycle.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/releases/v17/17-0.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))
  • content/docs/releases/v17/17-3.mdx (via automation.trigger (sdk, the route ledger binds it to POST /automation/trigger/:name, selected by route anchor /trigger/:name), /trigger/:name (route, bridged from symbol timeoutMs — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via timeoutMs (symbol, a field of interface TursoTransportKeys), timeoutMs (literal, a string literal in TursoTransportIssue; a string literal in tursoTransportIssues))

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.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 98 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d7c024133e77f69aa0f26af359391c6a4b0142e4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2dfe95bcc9202adc142603bf7c69c00ccc99c588 — the merge of head 59c2391e2a91e6ec2b26626a2c85bb6c710cd5d4 into base d7c024133e77f69aa0f26af359391c6a4b0142e4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d7c024133e77f69aa0f26af359391c6a4b0142e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 59c2391e2a91e6ec2b26626a2c85bb6c710cd5d4

① Derived judgments

Inputs: card #19977 (body + 9 comments), PR #20199 (body, diff, files, head check-runs), the repo at origin/main (d7c024133e) and at the head. turso-driver.ts at the head is byte-identical to origin/main (git diff --stat origin/main 59c2391e -- packages/drivers/driver-turso/src/turso-driver.ts is empty): the fence held, and the refused set the schemas mirror is main's constructor at :1375-1402 (detectMode :1490-1528, localEngineDefect :1103-1108, refuseWebSocketTimeout guard :1389-1392, supplied-client guard :1400-1402).

  • The mirrored predicate is exact against the loader-shaped config — RIGHT. I extracted the head's tursoTransportIssues verbatim (packages/spec/src/data/driver/turso.zod.ts:120-273) and main's constructor helpers by name (startsWithScheme, REMOTE_URL_PREFIXES, hasRemotePrefix, isFileUrl, namesInMemoryDatabase, localEngineDefect, ridesWebSocketTransport, timeoutWindow, plus a transcription of detectMode and the constructor guard) and ran a differential grid of 32 urls × 5 syncUrl values × 4 modes × 3 timeoutMs values = 1,920 configs (review/probe.mts, output in review/probe-output.txt). Result: 0 under-refusals (constructor refuses, schema accepts), 0 key mismatches (both refuse, different key), and 276 over-refusals, every one of them arm 2 (a truthy syncUrl under a forced mode: 'remote', refused on syncUrl); "over-refusals that are NOT arm 2: 0". Letter case (LIBSQL://, WS://, FILE:), :memory: exact (:MEMORY: refused as unrecognised), FILE::memory: and file::memory:?cache=shared (in-memory replica), a remote scheme with no //, a blank url, and timeoutMs 0 versus positive all agree with the constructor.
  • Trimming — RIGHT, with the caveat named. The schema classifies cfg.url.trim() (:170); the constructor reads config.url raw. Both datasource loaders hand the constructor resolveTursoUrl(spec), which trims (packages/services/service-datasource/src/turso-driver-config.ts:235-238; packages/runtime/src/turso-driver-factory.ts:267 and the CLI delegate to it). The probe's raw-versus-loader difference is confined to three whitespace-padded urls (" file:./x.db", " libsql://h ", " :memory:"), which the raw constructor refuses as unrecognised-url and the loader-shaped one accepts. So "exactly what new TursoDriver refuses" is true of every config that reaches it through a datasource, not of a host calling the constructor with an untrimmed string; the PR body and both file headers say so.
  • timeoutMs versus the constructor's timeout — RIGHT. The constructor reads config.timeout through timeoutWindow (:830-832); the loader maps timeoutMs onto it (turso-driver-config.ts:205). The spec's timeout key is a retiredKey() tombstone and the mirror's a z.never() tombstone, so no alias reaches the refinement. The schema's condition (timeoutMs is a number and strictly positive, :248) equals timeoutWindow's strictly-positive reading; the mirror's timeoutMs allows 0 (:348) and the constructor treats 0 as unbounded — consistent.
  • syncUrl truthiness — RIGHT. Schema: non-empty string (:171); constructor: config.syncUrl truthy; loader forwards only a truthy string (turso-driver-config.ts:169-170). An empty syncUrl is unset everywhere (preservation row); a whitespace-only one is set everywhere.
  • Supplied client — RIGHT to leave out. Not authorable (the spec is a strictObject without it, the mirror strips it); the parity test's docblock says so.
  • Each refusal is one custom issue on the key it names — RIGHT. Every early return in the local/replica branch yields one url issue; the remote branch can yield timeoutMs and syncUrl together for a config carrying both defects (probe: 12 grid rows, e.g. wss:// + timeoutMs + syncUrl + mode: 'remote'), and the pre-existing sync-alone refusal can stand beside a url refusal (spec test :330-341). Two defects, two keys — not a violation of "one issue per refusal". Messages checked against the driver: "the turso driver refuses this configuration when it starts" (constructor, before super()), "@libsql/client builds no replica for a remote url and ignores syncUrl beside one" (driver docblock measurement :1064-1070), "that transport takes no timeout" (:880-896), "never hands syncUrl to the remote client and runs no sync" (createRemoteClient :1614-1622 forwards url, authToken, concurrency, fetch only). No message echoes a url, only a remote url's scheme.
  • The 54-row parity test is honest. It drives the real new TursoDriver (turso-config-constructor-parity.test.ts:162-170), asserts every refusal is the ADR-0112 envelope (code: 'VALIDATION_ERROR', status: 400) rather than any throw, and compares the mirror to the spec with expect(mirror).toEqual(spec) on { refusedOn, code, message, count } (:176-183, :216-219), which is byte-for-byte on the message. It shapes the authored row as the loader does before construction (driverConfigOf :151-159: trim, drop empty syncUrl, timeoutMs → timeout) and says so in its docblock. Row arithmetic checks: 21 accepted + 26 on url + 3 on timeoutMs + 4 inert = 54; 16 rows force a mode and skip the mirror leg; the floors (≥20 / ≥25 / ≥3 / ≥4 / ≥35) hold with margin.
  • Arm 2 (syncUrl under a forced mode: 'remote') refused at authoring — RIGHT, and the tension is named. The card's own arm 2 asks for exactly this ("Refine both schema copies to refuse, at authoring, what the runtime refuses or cannot honour"); ADR-0049 declared ⇒ enforced makes a declared-and-ignored syncUrl the shape not to ship; the dispatch's "do not refuse anything it accepts" is the one rule it departs from, and the PR body, the table (inert: true, :138-141) and the ADR-0087 entry all mark it as the sole accepted-by-constructor refusal. The driver evidence is on main: connect() remote arm builds the client without syncUrl (:1657-1661, :1614-1622), isSyncEnabled() answers !!syncUrl && client !== null (:3168-3170), sync() calls libsqlClient.sync() on an HTTP/WS client (:3155-3163). The constructor half is correctly filed as driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200.
  • The mirror's sync-without-syncUrl refusal — TRUTHFUL. connect() reads sync.onConnect and sync.intervalSeconds only inside if (this.tursoConfig.syncUrl) (:1676-1702), and the remote arm never reads sync. The words are the spec's, byte for byte (mirror :383-390 versus spec :459-466).
  • The two copies — byte-identical helper. diff of spec :120-273 against mirror :118-271 differs only in the type-alias name (TursoTransportMode versus TursoTransportModeName, two lines).
  • url describe — ACCURATE. "a local file written as a file: URL (never a bare path)": in a local or replica mode the constructor refuses a bare path (localEngineDefect 'unrecognised-url'), and under mode: 'remote' @libsql/client refuses it at connect (URL_INVALID, README :198-199), so no spelling of a bare path opens a database. The regenerated reference (content/docs/references/data/driver-turso.mdx:68) matches.
  • Rewritten pins — RIGHT. The driver test's placeholder case flipped from accept to a url refusal (turso.test.ts:217-237): the constructor refuses the literal ${TURSO_DATABASE_URL} in the local mode it selects, and nothing resolves a placeholder. The two syncUrl-beside-remote fixtures moved to file: urls with assertions unchanged.
  • Preservation rows verified against the driver, not only the constructor. file: + syncUrl under a forced mode: 'local' is NOT inert: connect() gates the replica arm on syncUrl, not on the mode (:1676), so it syncs. mode: 'replica' on file: with no syncUrl IS a declared replica that never syncs (same gate) — correctly left unrefused (no constructor refusal to mirror) and carried as driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200's rider.

② Semver level

  • minor with a BREAKING banner for @objectstack/spec and @objectstack/driver-turso — CORRECT. An accept-set narrowing on a published schema is breaking; scripts/check-changeset-no-major.mjs (header, "During the launch window we ship breaking changes as minor") makes minor the level and the **BREAKING** banner + ADR-0087 disposition the carriers. Both packages are in the fixed group (.changeset/config.json:37). Precedent: the driver's own constructor narrowings shipped as minor + BREAKING (packages/drivers/driver-turso/CHANGELOG.md:127, :169, :189).
  • Clause-②: no (narrowing) — CORRECT. AGENTS.md :1074-1075: the arm pair is (widening)/(narrowing), (narrowing) is BREAKING; scripts/pm/clause2-line.mjs:95 names no (narrowing) as "NOT a widening, but breaking. This is the whole point". No key is added, removed or renamed; no export moves. The PR body's bare Clause-②: no copies the claim line and carries the same value.
  • ADR-0087 registered turso-config-transport-mismatch-refused — CORRECT and follows the right precedent. The gate (scripts/check-adr-0087-registration.mjs) requires one marker whose ids resolve at HEAD with at least one new in the diff: the entry is new (packages/spec/src/migrations/entries/semantic/18.turso-config-transport-mismatch-refused.ts) and registry.ts is regenerated. The FROM → TO table header | You wrote | Write instead | is in the gate's accepted stock (:1242-1257). The driver's constructor narrowings used not-required (no-migration-prescription) because they touched no schema; a spec-schema narrowing registers a semantic entry with no D2 conversion, as 18.datasource-config-postgres-url-unparseable-refused.ts and 18.rls-predicate-array-comparand-refused.ts do. surface carries no backtick.
  • Entry text — ACCURATE, including the upgrading deployment: loadDatasourceRows / loadDatasourceRow (packages/services/service-datasource/src/datasource-admin-plugin.ts:126-180) do JSON.parse plus the ADR-0087 rehydration and never parse a schema; assertValidConfig runs at testConnection (datasource-admin-service.ts:537), createDatasource (:558) and updateDatasource only when the patch carries config or driver (:633). So a stored bad row keeps loading as before — rows 1-5 of the changeset's table already fail at the constructor (boot or connect) since driver-turso: a remote url plus syncUrl is classified replica and handed a :memory: Knex connection, so every write lands in process memory and never reaches the remote #19893/driver-turso: a url whose scheme the classifier does not recognise (an uppercase LIBSQL://, a bare path) and no mode falls through to local on a :memory: Knex engine, so every write is lost on restart #19976, row 6 keeps loading and silently not syncing until driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200 — and any create, test connection, or save that resubmits config (a Studio form save included), defineStack or os validate (DatasourceSchema → validateDriverConfig, datasource.zod.ts:484) is refused at config.url / config.syncUrl / config.timeoutMs with the ways out; flipping active: false still works. The changeset's "the constructor already refuses the first five rows at boot" and the entry's "first four shapes" (its own enumeration) are both right. CI: Check Changeset ×2 and Lint & Repo Gates success at the head.

③ Boundary flags

  • service-datasource redaction fixture (turso: ?authToken= in an authored config.url / config.syncUrl query string is credential material the #8082 userinfo refusal does not cover #8337) — still tests what turso: ?authToken= in an authored config.url / config.syncUrl query string is credential material the #8082 userinfo refusal does not cover #8337 pinned. Both url-bearing keys still carry ?authToken=; the read still serves both parameter-free with redactedConfigKeys: ['syncUrl', 'url'], an untouched save restores both tokens independently, and a hand-edited url wins (datasource-config-redaction.test.ts:286-334). The redactor is key-based and scheme-agnostic, and the spec's own pin keeps the remote-url spelling through redactDatasourceConfig without the schema (packages/spec/src/data/datasource-credential-redaction.test.ts:298-307). The rewrite was forced by updateDatasource now refusing the old row at the door (2 red before the rewrite, named). Nothing pinned is lost.
  • Loader fixtures spelling remote url + syncUrl + mode: 'replica' — none parses the schema, none goes red. packages/runtime/src/turso-driver-factory.convergence.test.ts:65-82 uses a capturing class TursoDriver; packages/cli/src/utils/storage-driver.test.ts:290-300, :461-486 uses FakeTursoDriver and a compile-time pin on buildTursoDriverConfig; packages/services/service-datasource/src/__tests__/turso-driver-config.test.ts:36-62 calls the builder only (getDriverConfigSchema at :231 reads tombstone descriptions, not the fixture); standalone-stack.libsql.test.ts uses a fake ctor. Confirmed by the head's check-runs: Test Core (1/6)…(6/6), Test Core, Type Check · workspace, Type Check · consumer gates, Type Check · source gates, Lint & Repo Gates, Build Core, Build Docs, Spec property liveness, Dogfood Regression Gate ×3 + Dogfood Verify CLI, Temporal Conformance all success — 39 check-runs at 59c2391e: 34 success, 5 skipped, 0 failed (polled 2026-09-27T07:49Z).
  • driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200's premise — RIGHT. Read on main: the remote arm ignores syncUrl (:1614-1622, :1657-1661), isSyncEnabled() answers true after connect (:3168-3170), sync() rejects on an HTTP/WS client (:3155-3163); the rider (mode: 'replica' on file: without syncUrl runs as plain local) follows from the syncUrl gate at :1676. The filing's suggested placement (before super(), beside refuseWebSocketTimeout, reusing this PR's syncUrl message, flipping the inert rows) is consistent with this PR.
  • Flag the author named and I confirm as a real gap, pre-existing and out of this card's fence: the driver-local mirror declares no mode and strips it (docs/design/driver-turso.md §10), so the parity test skips all 16 forced-mode rows for the mirror and the two copies DISAGREE there, unpinned. Probed: { libsql://, mode: 'replica' } → spec refuses on url, mirror accepts (constructor refuses); { libsql://, mode: 'remote', syncUrl } → spec refuses on syncUrl, mirror refuses on url with the replica wording; { file:, mode: 'remote', syncUrl } → spec refuses on syncUrl, mirror ACCEPTS as a replica. The mirror is the driver package's published configSchema, so a consumer parsing an authored config through it silently reads a forced mode: 'remote' as replica/local. The PR's importer census found no in-repo producer; objectui/cloud were not among my inputs and are not measured here. Adding mode to the mirror would be a Clause-② widening and a different card.
  • Minor, not owed for this card: docs/design/driver-turso.md §10 still describes the mirror without its new refinement; the PR body's "each config gets exactly one refusal" is true of url but a config with two independent defects gets two issues on two keys (probe rows above) — the tests never claim otherwise.
  • Not measured by me: any out-of-repo deployment or objectui/cloud fixture authoring a now-refused combination (the changeset says the same).

Implemented-by: claude/issue-19977-turso-config-authoring-refusals
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 07:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 172b4cf Sep 27, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19977-turso-config-authoring-refusals branch September 27, 2026 08:13
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
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 protocol:data size/xl tests tooling

Projects

None yet

2 participants