Skip to content

Commit 7ee1acb

Browse files
committed
wip(spec): register the forced-local refusal as a D3 entry, and its changeset
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8a38e4e commit 7ee1acb

3 files changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-turso': minor
4+
---
5+
6+
fix(spec,driver-turso)!: a turso config that forces `mode: 'local'` beside a `syncUrl` is refused where it is written and when the driver is built, instead of running as an embedded replica under a `local` label
7+
8+
Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves.
9+
10+
A `syncUrl` names the remote an embedded replica syncs with. A config that forced `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the driver source before this change, with a client that counts syncs: it constructed with `transportMode` `'local'`, then synced on connect, started the sync interval, and `isSyncEnabled()` answered `true` — exactly what the same config with no `mode` (a replica) did. A datasource declared local was kept in sync with a remote, and only a label said otherwise.
11+
12+
**BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out:
13+
14+
- **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service;
15+
- **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened.
16+
17+
The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. It is the twin of the forced `mode: 'replica'`-without-`syncUrl` refusal, the other way round: honouring `mode: 'local'` by skipping the sync would ignore a declared `syncUrl` instead, which is the same defect with the keys swapped. The sibling refusals keep their order: a forced local mode on a remote url or a bare path still meets its `url` refusal first. An empty `syncUrl` is unset and is still accepted. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it.
18+
19+
### Migration: FROM → TO
20+
21+
| You wrote | Write instead |
22+
| --- | --- |
23+
| `url: 'file:./data/replica.db', mode: 'local', syncUrl: 'libsql://my-db.turso.io'` | an embedded replica: drop `mode` (`url` and `syncUrl` select the replica) |
24+
| the same | a plain local database: drop `syncUrl` (and `sync`), keeping `url: 'file:./data/app.db'` with or without `mode: 'local'` |
25+
26+
A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the 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. Before this change the same row booted and synced with the remote under a `local` label. The way out is the table above.
27+
28+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` or `syncUrl` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
29+
30+
<!-- adr-0087: registered turso-config-forced-local-with-sync-url-refused -->
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// A forced local mode beside a remote to replicate from, refused at both doors
6+
// together: the datasource contract (on mode) and the turso driver's
7+
// constructor, in one message. The driver used to label it local and run it as
8+
// an embedded replica anyway. The twin of
9+
// turso-config-forced-replica-without-sync-url-refused, the other way round. A
10+
// structured TODO, not a D2 conversion: whether the author meant an embedded
11+
// replica of that remote or a plain local file is intent no artifact records.
12+
export const entry: SemanticMigration = {
13+
id: 'turso-config-forced-local-with-sync-url-refused',
14+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
15+
// span already, and a nested backtick would close it.
16+
surface:
17+
'data.TursoConfig (a turso / libsql datasource.config) and the TursoDriver constructor of '
18+
+ '@objectstack/driver-turso — mode local beside a non-empty syncUrl is now refused, on mode at '
19+
+ 'authoring and at construction. The published TursoConfigSchema mirror of '
20+
+ '@objectstack/driver-turso carries the same text for parity but declares no mode key and strips '
21+
+ 'an authored one, so it cannot see a forced mode and still accepts the config as a replica',
22+
replacement:
23+
'the configuration the author meant. An embedded replica drops mode: keep the file: url and '
24+
+ 'syncUrl, for example url file:./data/replica.db with syncUrl naming the remote, and the url '
25+
+ 'and syncUrl select the replica. A plain local database drops syncUrl and sync: a file: url '
26+
+ '(or :memory:) with no syncUrl is a local database, with or without mode local',
27+
reason:
28+
'A syncUrl names the remote an embedded replica syncs with, and the turso driver syncs whenever '
29+
+ 'it is set on a local engine, whatever mode says. The triage ruling of 2026-09-29 weighed '
30+
+ 'refusing this shape against honouring mode local by skipping the sync, and refused it: '
31+
+ 'honouring it would ignore a declared syncUrl, the same defect with the keys swapped, and a '
32+
+ 'loud contradiction is the author\'s to resolve. A forced mode local beside a syncUrl parsed '
33+
+ 'clean at authoring, and the driver built it with a local transport label and then ran it as '
34+
+ 'a replica: it synced on connect, started the sync interval and answered true to the '
35+
+ 'sync-enabled check, exactly as the same config with no mode did (measured on the driver '
36+
+ 'source). A declared mode the runtime ignores is the declared-but-not-enforced shape ADR-0049 '
37+
+ 'does not ship, so the datasource contract and the constructor now refuse it together, with '
38+
+ 'one message, which names both ways out. The sibling refusals keep their order: a forced '
39+
+ 'local mode on a remote url or a bare path meets its url refusal first. An empty syncUrl is '
40+
+ 'unset and is not refused. Stored datasource rows are not re-parsed when they load, so a '
41+
+ 'stored row in this shape now fails when its driver is built: the connection service records '
42+
+ 'it as failed-degraded, a test connection answers ok false, and under ADR-0062 D5 the boot '
43+
+ 'fails fast when objects bind to that datasource, unless OS_ALLOW_DRIVER_CONNECT_FAILURE is '
44+
+ 'set. Measured on this tree at the change: no example, template, published skill or '
45+
+ 'hand-written doc authors the shape, and no host default or environment variable sets mode '
46+
+ 'or syncUrl. ADR-0049 / ADR-0087 / ADR-0112.',
47+
acceptanceCriteria:
48+
'Validate every stack and re-save every turso datasource: os validate or defineStack, and a '
49+
+ 'save or test connection through the datasource admin service, report a forced local mode '
50+
+ 'beside a syncUrl at config.mode with both ways out. Decide per datasource whether it is an '
51+
+ 'embedded replica (drop mode) or a local file (drop syncUrl and sync). Done when every turso '
52+
+ 'datasource parses, the driver builds from it, and no datasource that declares mode local '
53+
+ 'carries a syncUrl.',
54+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16982,6 +16982,56 @@ const step18: MigrationStep = {
1698216982
+ '(`@objectstack/service-settings`’s `settingsBuiltinTranslations`) — ⛔ do not re-add '
1698316983
+ 'app-side copy at either door, which is refused.',
1698416984
},
16985+
// A forced local mode beside a remote to replicate from, refused at both doors
16986+
// together: the datasource contract (on mode) and the turso driver's
16987+
// constructor, in one message. The driver used to label it local and run it as
16988+
// an embedded replica anyway. The twin of
16989+
// turso-config-forced-replica-without-sync-url-refused, the other way round. A
16990+
// structured TODO, not a D2 conversion: whether the author meant an embedded
16991+
// replica of that remote or a plain local file is intent no artifact records.
16992+
{
16993+
id: 'turso-config-forced-local-with-sync-url-refused',
16994+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
16995+
// span already, and a nested backtick would close it.
16996+
surface:
16997+
'data.TursoConfig (a turso / libsql datasource.config) and the TursoDriver constructor of '
16998+
+ '@objectstack/driver-turso — mode local beside a non-empty syncUrl is now refused, on mode at '
16999+
+ 'authoring and at construction. The published TursoConfigSchema mirror of '
17000+
+ '@objectstack/driver-turso carries the same text for parity but declares no mode key and strips '
17001+
+ 'an authored one, so it cannot see a forced mode and still accepts the config as a replica',
17002+
replacement:
17003+
'the configuration the author meant. An embedded replica drops mode: keep the file: url and '
17004+
+ 'syncUrl, for example url file:./data/replica.db with syncUrl naming the remote, and the url '
17005+
+ 'and syncUrl select the replica. A plain local database drops syncUrl and sync: a file: url '
17006+
+ '(or :memory:) with no syncUrl is a local database, with or without mode local',
17007+
reason:
17008+
'A syncUrl names the remote an embedded replica syncs with, and the turso driver syncs whenever '
17009+
+ 'it is set on a local engine, whatever mode says. The triage ruling of 2026-09-29 weighed '
17010+
+ 'refusing this shape against honouring mode local by skipping the sync, and refused it: '
17011+
+ 'honouring it would ignore a declared syncUrl, the same defect with the keys swapped, and a '
17012+
+ 'loud contradiction is the author\'s to resolve. A forced mode local beside a syncUrl parsed '
17013+
+ 'clean at authoring, and the driver built it with a local transport label and then ran it as '
17014+
+ 'a replica: it synced on connect, started the sync interval and answered true to the '
17015+
+ 'sync-enabled check, exactly as the same config with no mode did (measured on the driver '
17016+
+ 'source). A declared mode the runtime ignores is the declared-but-not-enforced shape ADR-0049 '
17017+
+ 'does not ship, so the datasource contract and the constructor now refuse it together, with '
17018+
+ 'one message, which names both ways out. The sibling refusals keep their order: a forced '
17019+
+ 'local mode on a remote url or a bare path meets its url refusal first. An empty syncUrl is '
17020+
+ 'unset and is not refused. Stored datasource rows are not re-parsed when they load, so a '
17021+
+ 'stored row in this shape now fails when its driver is built: the connection service records '
17022+
+ 'it as failed-degraded, a test connection answers ok false, and under ADR-0062 D5 the boot '
17023+
+ 'fails fast when objects bind to that datasource, unless OS_ALLOW_DRIVER_CONNECT_FAILURE is '
17024+
+ 'set. Measured on this tree at the change: no example, template, published skill or '
17025+
+ 'hand-written doc authors the shape, and no host default or environment variable sets mode '
17026+
+ 'or syncUrl. ADR-0049 / ADR-0087 / ADR-0112.',
17027+
acceptanceCriteria:
17028+
'Validate every stack and re-save every turso datasource: os validate or defineStack, and a '
17029+
+ 'save or test connection through the datasource admin service, report a forced local mode '
17030+
+ 'beside a syncUrl at config.mode with both ways out. Decide per datasource whether it is an '
17031+
+ 'embedded replica (drop mode) or a local file (drop syncUrl and sync). Done when every turso '
17032+
+ 'datasource parses, the driver builds from it, and no datasource that declares mode local '
17033+
+ 'carries a syncUrl.',
17034+
},
1698517035
// A forced embedded replica with no remote to replicate from, refused at both
1698617036
// doors together: the datasource contract (on mode) and the turso driver's
1698717037
// constructor, in one message. The driver used to build it as a replica that

0 commit comments

Comments
 (0)