Skip to content

fix(spec,driver-turso)!: refuse a forced mode local beside syncUrl at authoring and at construction (#20586) - #20669

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20586-forced-local-refuses-sync-url
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20586-forced-local-refuses-sync-url

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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."

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:

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:

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)

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 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 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 turso: a datasource config with mode: 'replica' on a file: url and no syncUrl is accepted at authoring and at construction, then runs as a plain local database that never syncs #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 fix(spec,drivers): a datetime $lte or $between maximum on 9999-12-31 includes the whole last supported day (#20600) #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

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-turso, @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-4.mdx (via TursoDriverConfig (symbol, a top-level interface))

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
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 140 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 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9b87d87db8b97f876f764704d262676ba439aae7 — the merge of head a0c4c033af1498b5dfd41105b3e3c406b3acab1e into base 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345, 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 9b87d87db8b97f876f764704d262676ba439aae7 && git checkout 9b87d87db8b97f876f764704d262676ba439aae7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345 a0c4c033af1498b5dfd41105b3e3c406b3acab1e && git checkout -B drift-repro 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345 && git merge --no-ff a0c4c033af1498b5dfd41105b3e3c406b3acab1e

node scripts/docs-audit/affected-docs.mjs --json 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345

⚠️ 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 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345 → 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: cfe05ec40cc7cd6bc92bfd98ff47467a36868556
Local-runs: none

Read-only, at tier, on PR #20669 for card #20586, branch claude/issue-20586-forced-local-refuses-sync-url. Inputs: the card body and its three comments (triage 5884612522, claim 5891827128, os-dev-report 5894078917); the PR body and file list; the net diff origin/main... head (10 files, +569 / -28: the wip commits fadd8c0, 8a38e4e, 7ee1acb and a true merge of origin/main 6bff748); the check-runs on the head, read once and not polled. Sources were read with git show, git diff, git grep and git log only; nothing was built, run or re-run. The twin is PR #20504 (#20437) with its records 5877910448, 5878149990 and 5883870097, and the pending .changeset/20437-turso-forced-replica-needs-sync-url.md. Not fed: the dispatch order or the seat's own conclusions.

① Derived judgments

  1. The accept set narrows by exactly one combination, by the same predicate at both doors — right. Spec tursoTransportIssues (packages/spec/src/data/driver/turso.zod.ts): hasSyncUrl is a non-empty string, mode is the forced one first (tursoTransportModeOf returns cfg.mode when set), and the new arm mode === 'local' && hasSyncUrl sits inside the non-remote branch after the remote-url, unrecognised-url, in-memory-replica and forced-replica arms, returning one issue on mode. Constructor (turso-driver.ts): mode is detectMode's answer (a forced mode returned first), localEngineDefect and the two timeout refusals run first, then the 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 pair, then turso: a datasource config with mode: 'replica' on a file: url and no syncUrl is accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's, then if (mode === 'local' && config.syncUrl) last, before super(). An empty syncUrl is unset at both doors (a length above zero in the spec, truthiness in the driver). The refused set is therefore: a forced local on a file: url (any scheme case, trimmed) or on :memory: / file::memory: beside a non-empty syncUrl; nothing else moves. :memory: is reachable because localEngineDefect refuses an in-memory url only for replica (head line 1328). No config meets two issues: the sync refusal needs no syncUrl, the remote-mode refusals need mode === 'remote', the replica refusals need replica. The same shape as turso: a datasource config with mode: 'replica' on a file: url and no syncUrl is accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's arm the other way round.

  2. The refusal message — every sentence TRUE, one text at three copies. "makes this datasource a plain local database": detectMode honours the forced mode and transportMode reads local. "the database would still be synced with that remote as an embedded replica, so the declared local mode would be ignored": connect() builds the sync client whenever tursoConfig.syncUrl is set in the non-remote branch (head 1927), toKnexConfig has no local-versus-replica arm, and isSyncEnabled() is !!syncUrl && client !== null (head 3643). "the turso driver refuses this configuration when it starts": the constructor, in the sibling arms' phrase. "drop mode and keep syncUrl beside the local file": with no mode, file: + syncUrl selects replica and is an accept control at both doors. "drop syncUrl (and sync)": sync with no syncUrl is refused since 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, so the parenthesis is load-bearing. No url echoed, no tracker id. The spec arm, the mirror arm and LOCAL_MODE_WITH_SYNC_URL_REFUSAL are byte-identical in the diff; the constant is module-local, not exported.

  3. The driver mirror (packages/drivers/driver-turso/src/spec/turso.zod.ts) — right as parity text, unreachable as a verdict. The mirror declares no mode (its header, lines 107-109 on main) and zod strips an authored one, so the arm cannot fire and a file: + syncUrl config is judged the replica its url selects. That is the identical shape fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504 landed for the forced-replica arm, and the parity table skips the mirror half of every forced-mode row. Triage's "both schema copies" is met as text; giving the mirror a mode key is the pre-existing shortness the parity header calls "not this card's to change".

  4. Public surface — nothing moves. No export added, removed or renamed in either package; no schema key, .describe() or .meta() changed (a refinement does not reach the JSON Schema), so check:api-surface, check:authorable-surface and check:docs have nothing to regenerate. The only new symbols are a module-local constant and a new test file. TursoDriverConfig.mode and .syncUrl TSDoc gain one sentence each — TRUE against the constructor.

  5. The pins — right. (a) Parity table: the row "file: + syncUrl under a forced mode: 'local'" flips from accept to refuse / refusedOn: 'mode' with seven mode siblings (no sync, FILE:, whitespace, timeoutMs, a wss:// syncUrl, :memory:, file::memory:); two ORDER rows keep url (a remote url, a bare path); two accept controls (a forced local alone, and beside syncUrl: '') take the flipped row's accept slot. Floors 12 / 8 / 20 are the exact counts (4 + 8 mode; 3 syncUrl + 5 sync + 12 mode). SYNC_KEY_REFUSALS already selected refusedOn === 'mode', so all 8 new rows carry the byte-equality pin. (b) turso-driver-ignored-sync-key-refusal.test.ts: the removed control pinned the old answer (constructor accept, transportMode local); every 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 refusal assertion is untouched; the header re-points. (c) The new turso-driver-forced-local-with-sync-url-refusal.test.ts: seven refusal cases plus a supplied client, createTursoDriver (which is new TursoDriver, index.ts:97) and a no-echo case; four ORDER; five CONTROLS including the unforced replica syncing once. Its two testkit imports exist on main. (d) turso.test.ts: the accept fixture becomes syncUrl: ''; the new block covers six refused configs, the echo, url ORDER, both authoring doors (DatasourceSchema at config.mode, validateDriverConfig) and five controls.

  6. ADR-0087 disposition — a new D3 entry, right. Neither turso-config-transport-mismatch-refused nor turso-config-forced-replica-without-sync-url-refused names this shape, so registered with a new id is the honest arm. 18.turso-config-forced-local-with-sync-url-refused.ts carries the five SemanticMigration fields; surface has no backtick. registry.ts: +50 / -0, the entry's comment run and literal re-indented four spaces, inserted in id order between translation-per-app-settings-platform-only and turso-config-forced-replica-without-sync-url-refused (build-migration-registry.ts sorts by major, then id) — generator output, no hand edit. Every sentence of the entry — TRUE: the surface (both doors on mode; the mirror carries the text and cannot see a forced mode — spelled correctly from the start, the clause fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504's round 0 had to fix); the replacement (both ways out); the reason (the triage ruling of 2026-09-29 is 5884612522; the measured behaviour matches the source reading in judgment 2; sibling order; an empty syncUrl unset; the stored-row outcome — failed-degraded at datasource-connection-service.ts:355 / :457, "Failed to build driver" at datasource-admin-plugin.ts:707, ADR-0062 D5 and OS_ALLOW_DRIVER_CONNECT_FAILURE; ADR-0049 / 0087 / 0112 exist); the acceptance criteria.

  7. The changeset — every sentence TRUE. Audited under ② below.

  8. The driver README — one shipped sentence goes FALSE on this head. Wrong; the FAIL item. packages/drivers/driver-turso/README.md:226 on main: "The constructor also refuses (VALIDATION_ERROR / 400) three sync settings that nothing would honour, each with the message @objectstack/spec's TursoConfigSchema gives at authoring", followed by exactly three items. At the head the constructor refuses four: the driver's own refuseIgnoredSyncKey doc was rewritten to "one of the four sync-key refusals"; the README was not. The README ships in the npm tarball (package.json files: dist, README.md, CHANGELOG.md). On main the sentence is true; this diff makes it false, and the dev names no carrier. fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504's own delta record judged this list in scope of the change it documents ("a README the diff's own refusals made incomplete"); here the list is not incomplete but wrong. Fix, text only: "three" becomes "four", plus one list item in the list's style — a forced mode: 'local' beside a non-empty syncUrl, which the driver would sync anyway; drop mode for an embedded replica, or drop syncUrl (and sync) for a local database. No code, test, schema text or message moves: the fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504 patch-round-1 shape.

  9. The merge cfe05ec40 — clean. Parents 7ee1acb and origin/main 6bff748; BASE f4ce10c is an ancestor of 6bff748. No PR path moved on main in f4ce10c..6bff748 (a git diff --stat over the ten paths is empty), and the three-way check holds with hunk headers stripped: 7ee1acb..head equals main's window diff and 6bff748..head equals the PR's own diff over the ten paths; the merge differs from its main parent by exactly those ten files. No os-regen deferral question arises: registry.ts is not a merge=os-regen path (only spec-changes.json and the liveness counts are).

  10. Producer census (in-repo), re-read on origin/main. syncUrl in any letter case: zero hits in examples/, packages/create-objectstack, skills/, apps/ and hand-written content/docs; docs/design/driver-turso.md shows it once, in a replica example with no mode. No SYNC_URL env name under packages/**/src outside the driver's own constants. mode: 'local' outside tests: only the README's prose and the driver's own comments. buildTursoDriverConfig forwards an authored config.mode (turso-driver-config.ts:205). TRUE.

  11. PR body — TRUE, one note. "Fixes turso: a datasource forced to mode: 'local' beside a syncUrl still replicates (syncs on connect and on the interval, isSyncEnabled() true), so the declared local mode is ignored (#20437's twin, read at source) #20586" and the Clause-② line copied from claim 5891827128; the file surface is the claim's; the H1 readings match the source; H2 (:memory: reachable, no two-issue row); H3; H4 (the arm is last at both doors); H5 (cloud NOT MEASURED, declared). The test inventory and the three ablations are dev-run and consistent with the diff's case counts (8 mode rows give 8 verdicts + 8 byte pins, plus the new file's 10 refusal cases = 26). Gates are dev-run; the head's check-runs are the verdict. The acceptance note "the README keeps its list of constructor refusals as it is" — TRUE, and that is the defect of judgment 8. "PR fix(spec,drivers): a datetime $lte or $between maximum on 9999-12-31 includes the whole last supported day (#20600) #20643's turso-driver.ts hunks are another region" — outside these inputs, not judged; this diff's turso-driver.ts hunks are the file header, two TSDoc sentences, one constant and one constructor check. TRUE.

  12. Check-runs on the head at the moment read (32 runs, no failure). success: Check Changeset, Governed Surface Queue Guard, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Check PR Size, Auto Label, Check Documentation Links, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. in_progress: Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Test Core 1-6, Dogfood Regression Gate 1-3, Dogfood Verify CLI, Build Core, Temporal Conformance (live PG + MySQL). skipped: Build Docs, Console Pin Gate, Packed-tarball smoke. Placement: check-adr-0087-registration, check-changeset-no-major and check-empty-changeset run in Check Changeset (success); check:spec-changes and check:upgrade-guide in Type Check · source gates (success); check:migration-registry and check:doc-authoring in Lint & Repo Gates (in progress); check:type-check-debt in Type Check · debt ledger (success); check:dual-build-cjs-loads in ci.yml's Build Core (in progress).

② Semver level

Clause-②: yes (narrowing)

.changeset/20586-turso-forced-local-refuses-sync-url.md: @objectstack/spec minor, @objectstack/driver-turso minor, the title with the bang, Clause-②: yes (narrowing) matching the claim and the PR body, the migration FROM to TO table, and the HTML-comment disposition marker adr-0087: registered turso-config-forced-local-with-sync-url-refused, an id new in this diff. That matches what the diff publishes: an accept-set narrowing on a published schema and a published constructor with no key, export or describe moving, shipped minor under the launch-window convention (scripts/check-changeset-no-major.mjs, green in Check Changeset). Both packages whose behaviour changes are named; skip-changeset does not apply. Sentence audit: "accepted by TursoConfigSchema, the published mirror and new TursoDriver()" (the parity row pinned accept on main); the measured readings (consistent with judgment 2's source reading; dev-run); "at both doors together, with one message"; the door lists (DatasourceSchema and validateDriverConfig are pinned; defineStack / os validate and the admin service reach validateDriverConfig, datasource-admin-service.ts, as #20504's audit found); "before any client or database is opened" (before super(); Knex opens lazily); the byte-equality pin; the twin sentence; sibling order; "An empty syncUrl is unset and is still accepted"; the mirror clause; the table's two rows match the message's two ways out; the stored-row paragraph (the same sites as #20504's, still at those lines); the blast-radius paragraph (judgment 10, with cloud "NOT measured and not claimed to be zero"). All TRUE. Same level, same shape and same twin as .changeset/20437-turso-forced-replica-needs-sync-url.md.

③ Boundary flags

  1. The mirror carries the arm but cannot reach it — answered, accepted: judgment 3. Declared in the D3 surface, the changeset and the PR; the disposition fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504 landed with; the two doors that judge the config both refuse.
  2. One existing accept control removed ("sync beside syncUrl under a forced mode: local") — answered, right: it pinned the old answer; the new file's "beside sync" refusal case and the flipped parity row replace it; no 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 refusal assertion moved (diff read).
  3. Cloud producer census NOT MEASURED — escalated, not a FAIL: declared in the report, the PR and the changeset ("not claimed to be zero"), unreachable from a dev session as it was from fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504's reviews; the in-repo census holds. The owning seat or the maintainer censuses objectstack-ai/cloud for a turso datasource declaring mode: 'local' beside syncUrl before this lands, as fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) #20504's was waived by the seat.
  4. README gains no line (carrier none) — answered: the FAIL item, judgment 8. The file is in the repository at the head and readable with git show; the dev's classifier refusal is its own statement and is not verified here. The fix rides a text-only patch round on this PR, then a delta record on the new head.
  5. Container restart mid-run — answered, no residue: no work commit predates it; the branch is three wip commits plus the merge, each wip commit carrying the model-free trailer pair; the merge is clean (judgment 9).
  6. check:dual-build-cjs-loads and check:type-check-debt NOT MEASURED locally — answered: both are CI's; Type Check · debt ledger is success on this head; Build Core was in progress when read.
  7. open_questions is empty — nothing to answer.

Implemented-by: claude/issue-20586-forced-local-refuses-sync-url
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: FAIL

One item: packages/drivers/driver-turso/README.md's shipped count sentence ("three sync settings") is false at this head. Correct the count and add the fourth item in the list's style, text only, and post a delta record on the new head.


Generated by Claude Code

…cUrl refusal as the fourth sync setting the constructor refuses

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a0c4c033af1498b5dfd41105b3e3c406b3acab1e
Local-runs: none

Delta record, read-only, at tier, on PR #20669 for card #20586, branch claude/issue-20586-forced-local-refuses-sync-url. It supersedes record 5894260625 (FAIL at cfe05ec40) on this head. Inputs: the card body and its five comments (triage 5884612522, claim 5891827128, os-dev-report 5894078917, the seat amendment 5894275846, the patch-round os-dev-report 5894571248); the PR body as edited after the patch round, and its file list; the net diff against main at this head (11 files, +575 / −30, merge base 6bff748bb, unchanged since the prior record); the prior record; and the check-runs on this head, read once at 2026-09-29T16:53Z and not polled. Sources were read with git show, git diff, git grep, git log and git rev-parse only; nothing was built, run or re-run. Not fed: the dispatch order or the dispatching seat's own conclusions.

① Derived judgments

  1. The delta is one commit, README text only — right. git log cfe05ec40..a0c4c033a lists exactly a0c4c033a ("docs(driver-turso): the README lists the forced mode local beside syncUrl refusal as the fourth sync setting the constructor refuses"), carrying the model-free trailer pair. git diff --stat cfe05ec40..a0c4c033a: packages/drivers/driver-turso/README.md, +6 / −2, and nothing else.

  2. The rest of the net diff is byte-identical to what 5894260625 judged, so its judgments 1 to 7 and 9 to 11 carry. The net diff against the merge base with the README excluded hashes the same at both heads (sha256 de71da62…d22410 for 6bff748bb..cfe05ec40 and for 6bff748bb..a0c4c033a); each of the other ten PR paths has the same blob id at both heads (turso-driver.ts f9af3e93c573, spec turso.zod.ts eed1efdaa33a, the mirror a05fe3f09733, the parity test 20132e155444, the changeset fe7c25cfce80, the D3 entry 76a689956192, registry.ts e013f3ddb066, and the three remaining tests); git diff --name-status cfe05ec40..a0c4c033a names the README alone; the merge base against origin/main is still 6bff748bb. The public surface (nothing moves), the accept set (one combination narrower, at both doors, on mode), the three byte-identical message copies, the pins, the D3 entry and the in-repo census stand exactly as recorded there.

  3. The FAIL item is discharged: every sentence the README now says about these refusals is TRUE at this head. Read against the delta diff, LOCAL_MODE_WITH_SYNC_URL_REFUSAL (turso-driver.ts lines 1182 to 1187) and the constructor (lines 1598 to 1655):

    • "four sync settings that nothing would honour" — the constructor holds exactly four refuseIgnoredSyncKey(…) sites, one per module constant (REMOTE_MODE_SYNC_URL_REFUSAL, SYNC_WITHOUT_SYNC_URL_REFUSAL, REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL, LOCAL_MODE_WITH_SYNC_URL_REFUSAL), each thrown as VALIDATION_ERROR / 400; the helper's own doc now says "one of the four sync-key refusals".
    • "each with the message @objectstack/spec's TursoConfigSchema gives at authoring" — TursoConfigSchema's superRefine (spec turso.zod.ts lines 500 to 516) iterates tursoTransportIssues, whose new arm (lines 271 to 285) carries the same five string segments as the constant, read side by side; the parity table's byte-equality pin on every refusedOn: 'mode' row holds it (the prior record's judgment 5a, unchanged bytes).
    • "a forced mode: 'local' beside a non-empty syncUrl" — the constructor's predicate is mode === 'local' && config.syncUrl (truthy, so an empty string is unset) and the spec's hasSyncUrl is a string of length above zero. "Forced" is exact: detectMode returns config.mode first, and every auto-detected branch beside a syncUrl answers replica (file: / :memory:, a remote prefix, and the bare-path fallback alike), so no unforced config reaches this arm.
    • "which would still be synced with that remote as an embedded replica, so the declared local mode would be ignored" — the constant's own words; the prior record's judgment 2 read connect()'s syncUrl branch and isSyncEnabled(), unchanged bytes.
    • "Drop mode for an embedded replica" — with no mode, detectMode answers config.syncUrl ? 'replica' : 'local' for a file: url, an accept control at both doors. "or drop syncUrl (and sync) for a plain local database" — sync with no syncUrl meets SYNC_WITHOUT_SYNC_URL_REFUSAL, so the parenthesis is load-bearing, as it is in the constant.
    • The forced-replica item's terminator moves from . to ; — the list ends every item but the last with ;; declared in report 5894571248's deviations. Right.
  4. Nothing else in the README the change touches or makes false remains. The whole file (427 lines) was read at this head. The auto-detection table (file: + syncUrl is replica), the url-refusal list ("under a forced mode: 'local' / 'replica'" — the url refusal still fires first, ORDER pins carried), the ways out under it, the "force a specific mode" example (a remote url with mode: 'remote', no syncUrl), the embedded-replica example (no mode), the TursoDriverConfig block (syncUrl "for embedded replica mode", sync "requires syncUrl", mode "auto-detected from the URL" if not set) and the feature table each stay true. A git grep of the head tree finds no other "three sync" text anywhere; "four sync" appears at the README line and in the driver helper's doc only. The README ships: package.json files is dist, README.md, CHANGELOG.md; @objectstack/driver-turso is public.

  5. The PR body's first Acceptance-notes bullet — TRUE. "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" — the delta diff. "Patch round 1 (a0c4c033a, README text only)" — judgment 1. "after contract review 5894260625 found the 'three sync settings' sentence false at cfe05ec40" — that record's judgment 8 and verdict. "Its gate re-run reconciles 91 derived, 89 run, 2 NOT-MEASURED, 0 UNRUN" — dev-run, consistent with report 5894571248's gates block; the head's check-runs are the verdict. The body's "every final reading … at head cfe05ec40 unless it says otherwise" still holds, and no other sentence of the body was made false by the patch (the round-0 "keeps its list of constructor refusals as it is" bullet is gone).

  6. The file list matches the amended claim. Claim 5891827128 named ten paths; amendment 5894275846 adds the README "text only … nothing else in the README moves"; the PR's eleven files are those and no other, and the README delta is the +6 / −2 the amendment describes. No governed surface is touched (no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md); 605 changed lines, under the 5,000 threshold; head repo is the base repo.

  7. origin/main has moved since the merge base — noted, not a defect of this head. Five commits; among the eleven PR paths only the generated packages/spec/src/migrations/registry.ts moved (+10 / −11: prose of the object-grid-default-filters and page-component-filter entries at file lines 5664, 9298 and 13438, regenerated on main from entry files main also changed). The PR's hunk is +50 / −0 at line 16982, its own new entry; no line overlaps, GitHub reports mergeable: true, and neither side edits a count line. The queue's Lint & Repo Gates (check:migration-registry) judges the merged generation; should it red, the seat merges main, regenerates with gen:migration-registry and pushes — a pure regeneration, which this record carries under the pure-regeneration ruling once the seat posts its provenance line.

  8. Check-runs on this head at the moment read (41 runs, no failure). success: Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate (and its three shards), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Test Core 2/6, 3/6 and 4/6, Check Changeset (first run), Type Check · source gates, Type Check · debt ledger, Type Check · workspace, Type Check · consumer gates, Spec property liveness, Dogfood Verify CLI, Check Documentation Links, Flag docs affected by code changes, Check PR Size, Auto Label, filter, and the four claim / part-of guards (each run twice, both success). in_progress: Test Core 1/6, 5/6 and 6/6, and a second Check Changeset run started after the first's success. skipped: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), and the second Auto Label and Check PR Size runs. Of the seven required contexts six are success and Test Core is three shards short. The two gates the dev could not measure locally are green here: check:type-check-debt in Type Check · debt ledger, check:dual-build-cjs-loads in Build Core.

② Semver level

Clause-②: yes (narrowing)

Unchanged from the prior record: .changeset/20586-turso-forced-local-refuses-sync-url.md is the same blob (fe7c25cfce80) — @objectstack/spec minor, @objectstack/driver-turso minor, the bang title, Clause-②: yes (narrowing) matching the claim, the amendment ("Clause-② is unchanged") and the PR body, the FROM → TO table, and the HTML-comment disposition marker adr-0087: registered turso-config-forced-local-with-sync-url-refused. The README correction is text in a file the @objectstack/driver-turso tarball already ships under that package's minor bump; it moves no accept set, key or export and owes no changeset sentence of its own. skip-changeset does not apply. Check Changeset: success on this head (its second run in progress when read). Level and shape match what the diff publishes.

③ Boundary flags

  1. README "three sync settings" (the prior record's FAIL item; round-0's out_of_scope finding "carrier: none") — discharged: judgments 3 and 4.
  2. Seat amendment 5894275846 (file surface gains the README, text only) — answered, honoured: judgments 1 and 6; Clause-② unchanged.
  3. Deviation: the forced-replica item's . becomes ; — answered: judgment 3, the list's own punctuation.
  4. Deviation: worktree recreated at cfe05ec40 then removed; the report posted from the shared checkout — answered: nothing of it is in the diff; the seat's statement about the shared checkout is its own and is not verified here.
  5. open_questions is empty in both reports; out_of_scope_findings is empty this round — nothing to answer.
  6. PR body edited by the seat after the patch round — answered: judgment 5, the bullet is true.
  7. Carried from 5894260625, unchanged bytes: the mirror carries the arm but cannot reach it (accepted); one accept control removed (right); container restart (no residue); the two locally NOT MEASURED gates (green on this head, judgment 8).
  8. Cloud producer census NOT MEASURED — still escalated, not a FAIL: nothing this round touched it. The owning seat or the maintainer censuses objectstack-ai/cloud for a turso datasource declaring mode: 'local' beside syncUrl before this lands.
  9. registry.ts moved on main — escalated as a landing note: judgment 7.

Implemented-by: claude/issue-20586-forced-local-refuses-sync-url
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 16:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 05cb2bc Sep 29, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20586-forced-local-refuses-sync-url branch September 29, 2026 17:17
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/l tests tooling

Projects

None yet

2 participants