Skip to content

fix(objectql,driver-turso): the one-definition-of-multi-valued invariant reaches packages/objectql and driver-turso - #18668

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-18408-multi-valued-invariant-objectql-turso
Sep 17, 2026
Merged

huangyiirene merged 2 commits into
mainfrom
claude/issue-18408-multi-valued-invariant-objectql-turso

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #18408

Clause-②: no

The domain:engine half of #17469's one-definition ruling, executed: packages/objectql and packages/drivers/driver-turso now ask @objectstack/spec's isMultiValueField the question they used to answer from a raw field.multiple. This NARROWS to a declared contract (the predicate FieldSchema already enforces at the authoring entrance) and widens nothing — hence Clause-②: no.

The population, and triage's 11 reconciled

The card said the counts are triage's and told a taker to re-derive them. Both halves of that are now answered.

What I counted (my population): every read of the field-level multiple flag in NON-TEST source of the two declared packages, located by content — .multiple, bracket access, destructuring, and a full word sweep of multiple over both src trees. 5 sites in 3 files.

Triage's table (5681445124) reproduces exactly — as SITES, not files. Its column header says "files"; the numbers are matching LINES. At triage's own ref 827cacbf4b, with triage's own declared predicate set (isMultiValueField / field.multiple / .multiple ??), counting matching lines over the WHOLE package (tests and CHANGELOG.md included):

package lines at 827cacbf4b triage's table files at the same ref
packages/cli 18 18 6
packages/objectql 11 11 4
packages/runtime 1 1 1
packages/drivers/driver-turso 1 1 1

All four cells match on the line reading and none matches on the file reading, so the gap was a population difference exactly as the claim suspected: sites, tests included, labelled "files". ⛔ Nothing in triage's work is wrong; the label is.

Two consequences worth stating rather than burying:

  • Triage's 11 for packages/objectql is dominated by sites that ALREADY ask the spec predicate — record-validator.ts imports it as specIsMultiValueField and wraps it — plus two CHANGELOG.md lines. Only one of those 11 is a raw read (engine.ts:4741, today :4759).
  • Triage's predicate set MISSES two raw reads, because they are spelled fdef.multiple rather than field.multiple: engine.ts:13168 and engine.ts:13602. A taker who worked from the grep alone would have shipped half the card.

⭐ Every site below was located by CONTENT. The card's engine.ts:4741 is at :4759 today, as the claim already measured.

The per-site premise — the discriminating question, asked five times

If isMultiValueField and the raw flag ever disagreed, would this site's behaviour be WRONG, or merely worded differently?

# site reads the flag to decide divergent input class verdict
1 objectql/src/engine.ts resolveOptionDefault the SHAPE of an option-derived insert default (array vs scalar) multiselect/checkboxes/tags with no flag; any non-capable type WITH the flag ALIGNED
2 objectql/src/engine.ts referenceProbeFilter $contains vs bare equality against the FK column master_detail + flag ALIGNED
3 objectql/src/engine.ts cascade multiValued the set_null write shape, the required-FK escalation, the probe narrowing master_detail + flag ALIGNED
4 objectql/src/validation/record-validator.ts:824 single-value vs multi-value validation branch for select/radio none TODAY — see below ALIGNED, with a reading
5 driver-turso/src/remote-transport.ts mapFieldTypeToSQL the DDL column type number/integer/boolean + flag ALIGNED

Site 1 is a live defect in both directions: a multiselect with an option marked default: true was defaulted to a bare scalar, which validateRecord's own multi-value branch then refuses as invalid_type_array on the very insert the default was resolved for. Measured — that case throws VALIDATION_ERROR before this PR.

Site 5 is the same defect #17469 repaired on driver-sql, here inside ONE driver: TursoDriver is dual-transport, its local half extends the ALIGNED SqlDriver, and its remote half spelled its own types. So { type: 'number', multiple: true } was a float column on one transport and a TEXT column on the other, chosen by which URL the deployment holds.

⚠️ Site 4 is the one a reviewer may want to overturn, so here is the reading

if ((t === 'select' || t === 'radio') && def.multiple !== true) is dominated by its own type gate: select and radio are both in MULTI_CAPABLE_TYPES and in neither multi-option set, so on that domain isMultiValueField(def) reduces BY CONSTRUCTION to multiple === true. The two authorities cannot disagree there today, and the alignment therefore changes no behaviour on any input — it is not one of the divergences above.

It is aligned anyway, and the reason is the ruling's own: the line is the exact complement of the isMultiValueField(def) branch twelve lines below it, and the equality holds only while packages/spec keeps select/radio in the set they are in now. Spelling one dispatch's two halves from two authorities that happen to agree is the drift the #17469 ruling exists to end. ⛔ If the seat reads this as out of scope, it is a two-line revert and nothing else in the PR depends on it.

Exclusions, with a reason each

Within the population defined above the exclusion list is empty, and the card is right that an empty list needs its own evidence. It is empty because the population is defined by the READ, not by a grep for multiple — every candidate the wider grep surfaces was examined and is listed here as excluded:

  1. objectql/src/validation/rule-validator.ts:2037 — CHOICE_FIELD_TYPES (select, multiselect, radio, checkboxes). A hand-listed type set that a MULTI_OPTION_TYPES grep matches. It asks "does this field draw its value(s) from options?" — select and radio are on it, lookup/file/user are not — and it never reads the flag. Different question: the driver-sql isJsonField and spec isMultiValueField disagree for master_detail / tree / text + multiple: true, so a consumer using the spec predicate to shape a query gets a 400 #17469 exclusion class, verbatim.
  2. Three parameter-slice type annotations in engine.ts ({ multiple?: unknown }). Declarations of what a function reads, not reads. (Two gained type?: unknown so the seam can be asked.)
  3. The FieldSchema citations in the cascade/set_null prose (engine.ts ×2). They name FieldSchema's multiple and required doc blocks as the authority for the RESIDUAL REPRESENTATION — that an emptied set is written [] and never null ([Decision] Multi-value lookup after set_null member removal: does the emptied array read back as [] or null? #9447). That is still FieldSchema's question and is untouched. Only the comments that named the flag as the authority for MULTI-VALUEDNESS were rewritten.
  4. record-validator.ts's local isMultiValueField wrapper and normalizeMultiValueFields. Already delegating to the spec predicate — the seam the suggested route asks for already exists in that file.
  5. turso-json-column-type-asymmetry.test.ts:215's "the multiple: true route" note. Its fixture is { type: 'lookup', multiple: true }, which IS multi-valued by the predicate, so the sentence stays true and the Converging driver-turso's two transports onto one physical column type for Field.json — and the measured reason VALUE_ROUNDTRIP cannot adjudicate it #12738 pin is unaffected.

Precedent check against #18199's own exclusion: nothing here CONSTRUCTS a descriptor the predicate is asked of one frame later (action-execution.ts:1279's shape), and nothing here interpolates the flag into a message (#17469's three).

The comment half

Copied from the domain:cli half: the three in-tree comments that declared the losing authority were rewritten rather than left standing — resolveOptionDefault's "multiple: true assembles an ARRAY … (FieldSchema.multiple)", referenceProbeFilter's "A field declaring multiple: true stores an ARRAY … (FieldSchema.multiple)", and cascadeDeleteRelations' "On a multiple: true field the FK is a SET". objectql gets one local seam, declaredMultiValued, for its three sites; driver-turso has exactly one site and calls the predicate directly rather than minting a second name for it.

Evidence

Reverse verification (mutation = the three source files restored to their pre-fix content, tests left as written; restore = git checkout HEAD -- path, proven byte-identical by git hash-object against the HEAD blob):

ANCHOR COUNTS, fixed   engine.ts declaredMultiValued=10   remote-transport.ts isMultiValueField=3
ANCHOR COUNTS, mutated engine.ts declaredMultiValued=0    remote-transport.ts isMultiValueField=0   -> MUTATION LANDED
RUN (mutated)  objectql      Tests  3 failed | 33 passed (36)
RUN (mutated)  driver-turso  Tests  2 failed |  1 passed (3)
RESTORE PROVEN BYTE-IDENTICAL (3/3 blobs == HEAD)

The five red assertions are the discriminating ones. The three that stay GREEN under the mutation are declared as such in the test bodies: two cascade guard legs ($contains is a substring test, so it still matches a scalar column, and the engine's own narrowing pass repairs the over-match) and the turso "ask the predicate directly" leg. Stating which halves cannot fail is the point of listing them.

Tests, on the final head:

  • pnpm --filter @objectstack/objectql test — 298 files / 4995 passed
  • pnpm --filter @objectstack/driver-turso test — 53 files / 1252 passed
  • pnpm --filter ... typecheck (both) — Done, 0 errors
  • Live PostgreSQL: pnpm --filter @objectstack/driver-sql test against a local PG 16 (initdb + pg_ctl, port 54318, timezone=Asia/Shanghai, TZ=America/New_York) — 186 passed | 3 skipped (189 files), 3315 passed | 85 skipped. Positive control that the live cells really ran: sql-driver-datetime-postgres-timezone.test.ts is 5 passed with OS_TEST_POSTGRES_URL set and 5 skipped without it, same tree. The 3 skipped files are the MySQL-gated cells — ⛔ no mysql-server was installed, OS_EXPECT_LIVE_DIALECT_MATRIX was deliberately left unset so those skip instead of becoming a named red that says nothing about this diff, and the MySQL half is left to CI.

New pins: turso-remote-multi-value-column-type.test.ts (the declared column type per predicate verdict, plus a cross-transport storage-class leg), two cases in engine-select-option-default.test.ts, three in engine-cascade-delete-multivalue-probe.test.ts.

Gates

Derived from the real changed paths and reconciled on the FINAL head 66de450f64 — one count, one reconciliation:

dispatch-gates --ran: 65 derived family(ies) accounted for — 63 run, 2 NOT-MEASURED
  (both NOT-MEASURED are a recorded exit 3 = PREREQUISITE NOT MET, ⛔ not a finding)
    - pnpm check:dual-build-cjs-loads   reads every package's built dist/; needs a full `pnpm build`
    - pnpm check:type-check-debt        re-measures against the built workspace closure; same prerequisite

All 63 measured families exit 0. Exit codes were captured to disk before anything was read.

⚠️ Two notes on gates that were red earlier in this round and are green here:

pnpm lint (the repo-wide scan CI owns) was run in full rather than narrowed; result in the report.

Acceptance notes — noted, not filed

  • RemoteTransport.mapFieldTypeToSQL's case 'formula': return '' is unreachable from all three of its callers, each of which skips type === 'formula' before calling. Dead branch, not a defect, and untouched here. Carrier: whoever next edits that mapper.
  • The remote transport resolves field.type || 'string' in the mapper while all three callers resolve the identical expression before calling it — two spellings of one default, one frame apart. This PR uses the mapper's own copy (one resolution now feeds both the predicate and the switch) and does not unify the two. Carrier: the same mapper edit as above.

Generated by Claude Code

…ant reaches these two packages

Maintainer ruling 2026-09-13 (decision batch #128 item 5, option 1'): there is
ONE definition of "is this field multi-valued", `@objectstack/spec`'s
`isMultiValueField`, and storage follows it. driver-sql was aligned by #17469
and `os generate migration` by #18199; these four sites read `field.multiple`
raw, which answers `true` on types the predicate calls single-valued and
`false` on the inherently-multi option types that carry no flag.

- objectql: one local seam, `declaredMultiValued`, for the option-derived
  insert default, the referential-integrity dependents probe and the
  cascade-delete multi-value verdict; record-validator's select/radio gate is
  now the negation of its own multi-value branch's predicate.
- driver-turso: `RemoteTransport.mapFieldTypeToSQL`'s short-circuit asks the
  predicate, so this driver's two transports stop declaring one field's column
  in two different storage classes.

Comments that named the losing authority are rewritten rather than left
standing.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v17/17-1.mdx (via cascadeDeleteRelations (symbol, a method of class ObjectQL))

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 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 69 pages)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 22 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 14d25441138aee0a52621f9369d564afa28df9d4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 14d25441138aee0a52621f9369d564afa28df9d4

⚠️ 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 14d25441138aee0a52621f9369d564afa28df9d4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@huangyiirene
huangyiirene marked this pull request as ready for review September 17, 2026 14:35
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit ef67b47 Sep 17, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-18408-multi-valued-invariant-objectql-turso branch September 17, 2026 14:59
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…silently dropping them (objectstack-ai#18717)

Fixes objectstack-ai#18616

Clause-②: yes

The Turso **remote** face now refuses transactions with
`NOT_IMPLEMENTED` / `501` instead of accepting them and doing nothing
with them. Local and embedded-replica modes are byte-for-byte unchanged
in behaviour.

## What was wrong

`@objectstack/spec`'s `driver.zod.ts` states the delivery mechanism
verbatim:

> A transaction handle to be passed to subsequent operations via
`options.transaction`.

On the remote transport nothing could receive it. ⭐ Every reading below
was **re-derived by content on this branch's base** `f8eaf670454a`,
which contains `ef67b47afb` (PR objectstack-ai#18668) — that PR edited
`remote-transport.ts` after the card was written, so the card's line
numbers no longer locate anything:

| reading | instrument | result |
|:--|:--|:--|
| `RemoteTransport` members naming a transaction | signature scan of
`remote-transport.ts` | **3** — `beginTransaction()`, `commit(t)`,
`rollback(t)` |
| `RemoteTransport` **data** methods accepting `options` or a
transaction | same file, same instrument | **0** |
| ⭐ firing control — data methods present at all in that file | same
file | **13** (`find`, `findOne`, `aggregate`, `create`, `update`,
`upsert`, `delete`, `count`, `bulkCreate`, `bulkUpdate`, `bulkDelete`,
`updateMany`, `deleteMany`) ⇒ the zero is a reading, not a dead grep |
| `this.isRemote` guards in `turso-driver.ts` | `git grep -c` | **26** —
matches the card |

⚠️ The card's reading said **9** data methods; re-derived by content it
is **13**. The difference is in the instrument, not in the finding — the
zero above and its firing control both stand, and the wider control
makes the zero stronger, not weaker.

So a write issued between `beginTransaction()` and `rollback()` executed
on the plain connection, was **already durable**, and the rollback
resolved having undone nothing. Every step reported success. The
local-mode pin in `turso-driver.test.ts` ("should support transactions
with rollback") already asserted the correct shape; the remote face was
the unpinned one.

## The line triage drew, and this PR's radius

⛔ The deliverable is **stop the silence**, not implement remote
transactions. Triage's ruling is quoted rather than paraphrased, because
the boundary is the deliverable:

> **止损(本卡)**:remote 模式遇到 `options.transaction` 或 `beginTransaction()`
时**大声拒绝 / 声明不支持**,让调用者立刻知道自己没有事务语义。
> **实现远程事务**:那是 **objectstack-ai#18116 已完成的那次「measure, do not
implement」量出来的半径**,是**另一件事**、另一个量级。⛔ 不要把它折进本卡。

Measured radius of the half NOT taken, reported rather than spent:
implementing remote transactions means giving `RemoteTransport`'s 13
data methods an `options` parameter, threading a libsql `Transaction`
through each statement builder, and deciding what `connect()`'s
knex-free remote arm does about `SqlDriver`'s `openTransactions`
accounting. That is a different order of change and it is not in this
PR.

## The judgement the dispatch left to me: which door, and why not only
one

The dispatch named three candidates and asked which callers can reach a
remote data method with a handle in hand. Measured, there are **two
independent producers**, so the answer is **both doors**:

1. **`driver.beginTransaction()`** — every in-repo producer of a handle
is one of these: `engine.transaction()`,
`ScopedContext.beginTransaction()`, the sandbox trio. Closing this door
closes all of them, and it closes them *before* the callback writes
anything.
2. **`ExecutionContext.transaction`**, threaded by the caller.
`buildDriverOptions` in `packages/objectql/src/engine.ts` reads it
**first** — its own comment: *"Explicit wins; ambient is the safety
net"* — and `ExecutionContext.transaction` is a declared member of the
envelope in `packages/spec/src/kernel/execution-context.zod.ts`. Such a
handle never passes through (1). The same-origin gate does not stop it
either: `transactionCoversDriverFor` attributes a handle only when it
**is** the ambient store's handle, and for anything else it declines to
judge and returns `true` — its own recorded limit.

⇒ **Door (1) alone is the false floor the dispatch warned about**, and
this is measured, not argued — see the second ablation below, where
keeping only the `beginTransaction()` refusal leaves all 17
`options.transaction` cases green while silently dropping the handle.

`commit()` / `rollback()` refuse on the remote arm too. With
`beginTransaction()` refusing, this driver issues no remote handle at
all, so the only way to reach them is with a handle from somewhere else
— and `rollback()` accepting one is the precise silence the card is
named for: it resolves, reports a successful undo, and undoes nothing.
Both engine faces call `commit`/`rollback` only with a handle their own
`beginTransaction()` returned, which now throws first, so no in-repo
caller can be stranded with an unrolled-back transaction by this
refusal.

## Where the refusal lives, and why not one layer down

On `TursoDriver`, not inside `RemoteTransport` — the same layering
argument the sibling `auto_number` refusal already records in this file.
The transport's data methods have no `options` parameter at all, so the
handle is already gone by the time a statement is built. This class is
the last layer that still holds one.

## Why `NOT_IMPLEMENTED` / 501, and why no `packages/spec` edit

The request is spelled correctly and the spec declares these members, so
the gap is the backend's — the same two-class taxonomy this package
already applies to remote `auto_number`, aggregate functions and date
buckets. ⭐ **No new error code is needed and `packages/spec` is
untouched**: `NOT_IMPLEMENTED` is a `StandardErrorCode` member, and the
error-code ledger registers *extension* codes only, so nothing has to be
added to it. The precedent is eleven hundred lines up in the same file.

## What this deliberately does not do

- It does not implement transactions on the remote transport.
- It does not remove `RemoteTransport`'s own `beginTransaction` /
`commit` / `rollback`, which stay reachable through
`getRemoteTransport()` and are pinned by an existing declared-types
suite.
- It does not touch `driver-sql`, the engine, or the spec.

## Verification

All commands run from this branch; exit codes captured before any pipe.

- **`pnpm --filter @objectstack/driver-turso test`** — `Test Files 54
passed (54)`, `Tests 1278 passed (1278)`. The pre-existing local-mode
transaction pins are inside that green.
- **`pnpm --filter @objectstack/driver-turso typecheck`** — exit 0.
- **`pnpm --filter '@objectstack/driver-turso^...' build`** — `VERDICT
command-exit 0` (dependency closure).
- **Full package build** — `pnpm exec turbo run build
--filter=./packages/* --filter=./packages/*/*` — `Tasks: 72 successful,
72 total`.
- **Gate families** — derived with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (⛔ not from a hand-written
diff): **60 families, 60 run, 0 NOT MEASURED, 0 UNRUN**, reconciled back
through `--ran` with an exit code recorded per family. One was a real
red and is fixed in this PR: `check:doc-authoring` refused the tracker
id inside the refusal's runtime string (maintainer ruling 「处理 issue
时犯的错应该总结成经验,保留 issue id没有意义」) — the id moved to the docblock. Three more
were `PREREQUISITE NOT MET` (exit 3) until the full build above, then
green: `check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:type-check-debt`.
- **`pnpm lint`** — the repo-wide `eslint . --no-inline-config` union,
exit 0, run at final commit `97009bc42e`. ⭐ This is the union, not a
narrowed run, so no narrowing argument is owed.

### Reverse verification — two ablations, directions predicted before
either was run

Both ran from the **committed** fix, mutated on disk with the mutation
proved by grep counts and a changed blob hash, and restored under a
`trap` that verifies the restored blob equals the `HEAD` blob and that
`git diff HEAD` is empty. Subject blob at `HEAD`: `4b2b21a0da4e`.

**① Both doors removed** (the options guard emptied, the trio restored
to its pre-fix delegation). Predicted: the two REMOTE blocks go red, the
silence controls stay green. Measured **`21 failed | 5 passed (26)`** —
4 trio cases plus 17 `options.transaction` cases red, the 5 controls
green. The failures reprint the defect rather than merely reporting an
absence: `expected the remote face to refuse, but it resolved with ...`,
carrying the row that landed.

**② Only the `options.transaction` door removed**, the
`beginTransaction` / `commit` / `rollback` refusals kept — i.e. exactly
the shape of a fix that closed candidate (1) alone. Predicted: the 17
`options` cases red, the 4 trio cases and the 5 controls green. Measured
**`17 failed | 9 passed (26)`**. ⭐ That is the false floor, measured: a
PR that had stopped at (1) would have shown a green trio block and
shipped the silent drop.

Restore verified on both legs: `RESTORE OK — on-disk blob
4b2b21a... == HEAD blob 4b2b21a..., and git diff HEAD is
empty`.

### Changeset, measured rather than assumed

`skip-changeset` is wrong here and it was **measured**, not taken from
the dispatch. `@objectstack/driver-turso` publishes and its `files[]` is
`dist`, `README.md`, `CHANGELOG.md`. After building the package,
grepping those paths for a symbol unique to this change gives
`dist/index.js:1`, `dist/index.mjs:1`; the positive control (a
pre-existing shipped refusal sentence) hits `dist/index.js:2`,
`dist/index.mjs:2`, `CHANGELOG.md:2`; the negative control (a test-only
helper name) hits **0** anywhere in `files[]`. ⇒ it publishes ⇒ a
changeset is owed, and it is graded `minor` to satisfy the clause-②
level axis while staying inside the launch-window ban on `major`.

## Acceptance notes

Observations found while measuring, ⛔ not filed and ⛔ not fixed here.

- **`engineCanRollBack` reads method PRESENCE and there is no way for a
driver to say "present, but not on this connection mode".**
`packages/core/src/utils/migration-journal.ts` gates on `typeof
defaultDriver.beginTransaction === 'function'`, which is `true` for a
remote `TursoDriver` because the method is inherited from `SqlDriver` at
class-definition time while the transport mode is a runtime config. The
spec's own retirement note for the `transactions` capability bit says
transaction use "is gated on METHOD PRESENCE", so this is the declared
design, not a bug in that helper. After this PR the consequence is loud
rather than silent (`batchData`'s atomic gate now gets a refusal instead
of a half-applied batch), so nothing is left silently wrong — but the
underlying "presence cannot express a per-mode capability" shape is
exactly the semantics half that objectstack-ai#18063 and objectstack-ai#18116 are already about, and
it belongs to that decision, not to a new card. **承接者:** the objectstack-ai#18063
ruling.
- **`RemoteTransport.beginTransaction` / `commit` / `rollback` are now
unreachable through `TursoDriver`.** They stay reachable via
`getRemoteTransport()` and are pinned by
`turso-driver-doors-declared-types.test.ts`, so they are not dead code;
removing them is part of the implement-or-retire decision above, not of
this refusal. **承接者:** the same ruling.

Neither is a class (a) reproducible defect, a class (b)
declared-contract violation, or a class (c) metadata-authoring trap, so
neither is filed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af)_

---------

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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine: the one-definition-of-multi-valued invariant reaches packages/objectql and driver-turso — the domain:engine half of #18199's ruling

2 participants