fix(spec,driver-turso)!: refuse a forced mode local beside syncUrl at authoring and at construction (#20586) - #20669
Conversation
…both doors Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
… refusal tests Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…hangeset Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
Contract reviewServed-tier: Read-only, at tier, on PR #20669 for card #20586, branch ① Derived judgments
② Semver levelClause-②: yes (narrowing)
③ Boundary flags
Implemented-by: VERDICT: FAIL One item: 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>
Contract reviewServed-tier: Delta record, read-only, at tier, on PR #20669 for card #20586, branch ① Derived judgments
② Semver levelClause-②: yes (narrowing) Unchanged from the prior record: ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
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:engineseat 1, mode:subagent), branchclaude/issue-20586-forced-local-refuses-sync-url. The container restarted mid-run. The branch was fast-forwarded toorigin/mainf4ce10c89(BASE) before the first edit, and later got a true merge oforigin/mainat6bff748bb. Every final reading below was taken at headcfe05ec40unless it says otherwise.What changes
A turso config that forces
mode: 'local'on afile:url (or:memory:) beside a non-emptysyncUrlis now refused at both doors, with one message. Triage 5884612522 directed this: "A forcedmode: 'local'besidesyncUrl(orsync) is refused at both schema copies and at the constructor, naming the conflict."tursoTransportIssuesinpackages/spec/src/data/driver/turso.zod.tsgains a forced-local arm, placed after turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's forced-replica arm. It returns onecustomissue onmode, the key the runtime would ignore, as turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's does. It reachesDatasourceSchema(asconfig.mode),validateDriverConfig,defineStack/os validate, and a save or test connection through the datasource admin service.new TursoDriver()refuses the same config withVALIDATION_ERROR/ 400, beforesuper(), as the last of the sync-key refusals. The message is a module constant,LOCAL_MODE_WITH_SYNC_URL_REFUSAL, next to turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437'sREPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL. It is thrown through the samerefuseIgnoredSyncKeyhelper, and the parity table pins it byte-equal to the schema's issue.packages/drivers/driver-turso/src/spec/turso.zod.ts) carries the same arm byte for byte. The mirror declares nomodeand strips an authored one, so the arm is unreachable through it. It stays for copy parity, like the mirror's other forced-mode branches (see Deviations).sync)".syncwith nosyncUrlwas already refused in every mode (driver-turso:new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200).syncbeside asyncUrlunder a forced local mode is refused by this new arm. So everysyncshape under a forced local mode is covered, with no extra arm.The message, the same text at both doors:
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 afile:url,syncUrl,sync: { intervalSeconds: 1 }, and a client that countssync()calls:mode: 'local'+syncUrllocalsyncUrl, nomode(the replica control)replicamode: 'local', nosyncUrllocalSo a forced local mode beside
syncUrlran exactly as a replica, and only the label saidlocal. The pins holding today's answer passed at BASE:{ url: 'file:./data/app.db', mode: 'local', syncUrl: … };new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200 file's control "sync beside syncUrl under a forced mode: 'local' stays accepted".isSyncEnabled()is!!this.tursoConfig.syncUrl && this.libsqlClient !== null, as H1 states.H2: #20437 is the template (held), and what differs in this direction
It is mirrored arm for arm: one spec arm on
mode, a byte-identical mirror arm, a module constant, one constructor check, a new refusal test file, a parity flip and a new D3 entry. What differs:mode, as in turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437, but the cause is the opposite. AsyncUrlis present, so the conflict is between two declared keys, not a missing one. The message therefore offers "dropmode" (replica) or "dropsyncUrl(andsync)" (local).:memory:(localEngineDefectpasses it), so:memory:andfile::memory:besidesyncUrlunder a forced local mode meet this refusal. The replica way out points at a file url, so it is correct there as well.mode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437 has one row where the schema raisessyncandmodetogether. This arm needs a non-emptysyncUrland thesyncrefusal needs none, so they are exclusive, and every row here raises exactly one issue.H3: ADR-0087 disposition — a new D3 entry,
registeredNeither existing turso entry's
surfacenames this shape.turso-config-transport-mismatch-refusedlists the url, in-memory, WebSocket and remote-syncUrlcombinations.turso-config-forced-replica-without-sync-url-refusedis the opposite shape. The gate acceptsregisteredonly with an id that is new in this diff, andalready-registeredonly 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, idturso-config-forced-local-with-sync-url-refused.src/migrations/registry.tswas regenerated bygen:migration-registry, never by hand: +50 / -0, one entry. It reads320 semantic, 236 retired-key, 207 retired-def.registered turso-config-forced-local-with-sync-url-refused.check-adr-0087-registrationreads it as[BREAKING+bang+clause-②-narrowing] registered turso-config-forced-local-with-sync-url-refused (new here: …).check:generatedreads "All 15 generated artifacts are up to date", includingcheck:spec-changesandcheck:upgrade-guide.H4: refusal order (held; the new arm is last)
mode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's forced-replica arm, inside the non-remote branch. Every url-shape refusal returns first, and they are unchanged.syncrefusal needs nosyncUrl. The replica refusal needs a replica mode.syncwith nosyncUrl, and turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis accepted at authoring and at construction, then runs as a plain local database that never syncs #20437's forced replica with nosyncUrl. Each keeps its own message.H5: producer census (before any edit, at BASE
f4ce10c89, repoobjectstack-ai/objectstack)git grep -E "mode:\s*['\"]local['\"]|\"mode\"\s*:\s*\"local\""(whole tree)packages/drivers/driver-tursoandpackages/spec(source, tests, README). Of those, 3 author the shape beside asyncUrl, all tests: the parity row, the spec accept fixture and the #20200 control, each flipped here. The othermode: 'local'spellings carry nosyncUrlor are describe labelssyncUrl/sync_url/SYNC_URL, case-insensitive, per tree (turso control count in brackets)examples/0 [2],packages/create-objectstack0 [0],skills/0 [6], hand-writtencontent/docs0 [109],apps/0 [0]packages/**/src(non-test) matchingTURSO_*/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 tomodeorsyncUrlmodebuildTursoDriverConfig'smodereader (packages/services/service-datasource/src/turso-driver-config.ts:205), from an authoreddatasource.config.mode. Its two callers arepackages/runtime/src/turso-driver-factory.ts:288andpackages/services/service-datasource/src/default-datasource-driver-factory.ts:1337. No other non-testnew TursoDriver/createTursoDrivercall setsmodeNo shipped in-repo producer authors the shape, and no deployment default sets it, so the
needs_decisionbranch 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)@objectstack/driver-tursovitest, whole package@objectstack/driver-tursotypecheck (tsc --noEmit)tsc --listFilesOnlylists all 3 touched test files@objectstack/specvitest--project local, 3 shards@objectstack/spectypecheck (tsc + scripts +check:test-typecheck)@objectstack/speccheck:generated(after the spec build)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: 8moderows, 2 ORDERurlrows 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 fromaccepttorefuse,refusedOn: 'mode'. Seven moremoderows are added: nosync, an uppercaseFILE:url, a url behind whitespace,timeoutMs, awss://syncUrl,:memory:andfile::memory:. Two ORDER rows keep theirurlrefusal (alibsql://url and a bare path, each besidesyncUrlunder a forced local mode). Two accept controls are added: a forced local mode alone, and one beside an emptysyncUrl.SYNC_KEY_REFUSALStakes the newmoderows, so each of the 8 asserts the constructor's message equals the spec issue's. New floors:modeat least 12, the forced-localmoderows at least 8, sync-key refusals at least 20. The unforcedfile:+syncUrlreplica 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 TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_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 coversfile:andFILE:urls,:memory:,file::memory:, and a config besidesync,timeoutorencryptionKey. With a supplied client it is refused before any client work. It also coverscreateTursoDriver()and a check that neither url is echoed. ORDER: a remote url, a bare path,syncwith nosyncUrl, and a forced replica with nosyncUrleach keep their own refusal. CONTROLS: the unforced replica still connects and syncs once; a forced local mode with nosyncUrlor an empty one syncs nothing; a forced replica besidesyncUrland:memory:under a forced local mode both construct; anddetectModestill answerslocal.packages/spec/src/data/driver/turso.test.ts. The accept fixture becomes{ …, mode: 'local', syncUrl: '' }, because an emptysyncUrlis unset. A new block asserts the refusal onmodeover 6 configs, the url echo, url-refusal ORDER, both authoring doors (config.modeandvalidateDriverConfig), and the controls (the unforced replica with and withoutsync, and a forced local mode alone, beside an emptysyncUrl, and on:memory:).Reverse verification ran through
scripts/ablation-replace.mjsfrom the committed state at7ee1acb58. Each direction was predicted before the run, and all three matched:if (mode === 'local' && config.syncUrl) {becameif (false && …) {, and the mutation landed (anchor 1 → 0, blobf9af3e93c573→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 HEADempty.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.packages/spec/src/data/driver/turso.zod.ts(blobeed1efdaa33a→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/objectstackran at headcfe05ec40, after the final commit and the merge. It lists 10 paths vs merge base6bff748bband 91 commands. Every command ran, with its exit code written to disk before any pipe.--ranreconciliation reads91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), with 0 UNRUN.check:dual-build-cjs-loads(workspace packages with nodist/) andcheck:type-check-debt(it needs a whole-workspace build). Both are CI's run.check:doc-formula-expressionsandcheck:lean-entry-closurefirst answered exit 3. They are exit 0 after building the@objectstack/lintand@objectstack/objectqlclosures.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 nomajorbump";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-inputsandcheck:driver-conformance: exit 0.eslint --no-inline-config --format jsonover the 9 changed TS files reports 9 files, 0 errors and 0 warnings. The population iseslint.config.mjs's lint object,files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']; the changeset.mdis outside it. Invariance holds because the config enables no type-aware linting (its oneparserOptions.projectmention is a comment saying so), so this diff cannot move any untouched file's verdict. The fullpnpm lintis CI's.@objectstack/service-datasource,@objectstack/runtimeand@objectstack/cli. The narrowing is declared. The public surface's bytes are unchanged (check:api-surfaceandcheck:authorable-surfaceare 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-conformancereadOK — 50 covered cell(s), 0 in the DEBT ledger, 0 exemptboth before (f4ce10c89) and after (cfe05ec40).driver-tursoisokon all 10 case-sets both times, so there was no movement.Deviations (declared)
modekey 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 andsyncUrlselect. That is turso: a datasource config withmode: 'replica'on afile:url and nosyncUrlis 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 amodekey is outside this card: the parity test's header calls that shortness "not this card's to change". The D3 entry'ssurfaceand the changeset say this rather than claiming the mirror refuses.objectstack-ai/cloudwas refused by the permission classifier. Nothing in-repo triggersneeds_decision.@libsql/client, so the probe made no network call. The arm it exercises (connect()'ssyncUrlbranch) 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 forcedmode: 'local'beside a non-emptysyncUrl, 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 atcfe05ec40. Its gate re-run reconciles 91 derived, 89 run, 2 NOT-MEASURED, 0 UNRUN.modekey keeps its one-line TSDoc. The rule is carried by the refusal text and byTursoDriverConfig.mode's TSDoc in the driver, which now names it (as doessyncUrl's).turso-config-transport-mismatch-refusedandturso-config-forced-replica-without-sync-url-refusedare untouched. Both stay true.turso-driver.ts's remote filter lowering (toRemoteUpperBound, the$between/$ltearms), 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, thesyncUrl/modeTSDoc, the new constant beside the sync-key refusals, and one constructor check.Generated by Claude Code