Skip to content

feat(driver-turso): the remote transport issues auto_number values from the shared persistent sequence (#21113) - #21160

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21113-turso-remote-autonumber
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21113-turso-remote-autonumber

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21113
Clause-②: yes (widening)

Patch round 1 (review 5930100091)

The one blocking item was @objectstack/driver-sql at patch beside a root type re-export (AutoNumberReservation). Branch taken: the re-export is dropped and the two re-seed overrides in turso-driver.ts are typed Parameters indexed over SqlDriver's autoNumberValueExists (its second parameter) and resyncSequenceToDataMax (its first) — the premise (the protected member's parameter type is reachable from the subclass without the export) measured true: driver-sql typecheck exit 0, driver-turso typecheck exit 0, no structural copy. driver-sql's export list is byte-identical to main, so patch stands; the changeset sentence that named the export now reads "No export is added". origin/main merged again (fde553c50, which also lands the agentGuidance: false opt-out of #21151); re-run on the merged tree: closure build exit 0, both typechecks exit 0, driver-turso 82 passed (82) files / 2218 passed | 33 skipped (83 files / 2219 with the throwaway probe present), driver-sql autonumber suites 66 passed | 2 skipped, and the changeset, ADR-0087, nul-bytes and published-files gates all exit 0. Finding (b) was measured on the remote face as the review asked and reproduces; it is in the round-1 report on #21113 for the seat to file.

What this does

Disposition A of the remote-autonumber question, executed: a create, bulkCreate or upsert on the Turso REMOTE transport that leaves an auto_number slot empty now gets a generated value, issued atomically in the database from the same persistent _objectstack_sequences counter the local and embedded-replica faces draw from. supports.autonumber stays true on the remote face and is now honoured. The NOT_IMPLEMENTED / 501 refusal is gone; its suite is converted into the generation suite.

Why now: the appetite door the refusal left shut (「for want of measured demand」) was met by objectstack-ai/cloud#2531 — POST /api/v1/data/crm_account answers 501 on a hosted tenant, whose database is on this transport, so no object declaring an auto_number field could get a new record on the hosted product. That card carries the cloud half, Blocked-by: this one.

How, and what is shared rather than copied

Where the slots are filled. On the driver, one layer above the statement builder, exactly where the refusal was raised: RemoteTransport.create(object, data) takes no schema and caches none (pinned), while TursoDriver holds autoNumberFields from remote schema sync. The remote create / upsert / bulkCreate call the inherited SqlDriver.fillAutoNumberFields on a copy of the caller's row, then hand the filled row to the transport, which builds the same INSERT it built for a caller-supplied number.

One semantics (H3). The rules that decide WHICH counter a value is drawn from and WHERE a cold counter starts are SqlDriver's and are called, never copied. Lifted into protected members in the sequence region of sql-driver.ts (:7532–:8190 on this head; nothing else in that file): resolveSequenceTenantId (replaces three inline copies), defineSequencesTable (the table's one definition — the remote face compiles it to text with the connection-less Knex it already holds and sends that), maxAutonumberCounter + escapeLikePrefix (the bootstrap reading, suffix included, split from the Knex statement that fetches the values), sequencesTableName, autoNumberCollisionRetries. The two re-seed overrides take their parameter types through Parameters indexed over SqlDriver's protected members, so driver-sql's export list is unchanged. The format rendering and precedence were already shared (resolveAutonumberFormat at registration, renderAutonumber / missingFieldValues in fillAutoNumberFields), and are reached by calling that method.

What is this face's own: how the counter moves (H2). TursoDriver.getNextSequenceValue routes by transport. Local and replica keep the inherited Knex transaction. Remote moves the counter in ONE statement:

warm:  UPDATE _objectstack_sequences SET last_value = last_value + 1 WHERE key_hash = ? RETURNING last_value
cold:  scan the data table's MAX in JS by the shared reading, then
       INSERT … VALUES (…, max + 1) ON CONFLICT (key_hash) DO UPDATE SET last_value = last_value + 1 RETURNING last_value

Each is a single SQLite statement serialised by the database's write lock across connections and processes; no in-process state takes part (remoteSequencesTableEnsured remembers only that the TABLE exists). Two cold writers both scan and both INSERT; one wins the row, the other's ON CONFLICT arm increments it.

H2 falsified on one point, reported rather than followed: the PM expected the seed folded into the statement as a SQL MAX(…). It is not, because the bootstrap reading strips a declared suffix and reads the digit run after the prefix (readAutonumberCounter, #6468), which a dialect expression would be a second copy of — the exact collision H3 forbids. A Hrana batch was not needed either: the JS reading has to sit between the scan and the insert, and both insert arms are atomic on their own.

The legs (H4). create, bulkCreate and upsert all generate. bulkCreate on the remote face now runs through the driver's own create per row, which is what RemoteTransport.bulkCreate always did underneath (it loops the transport's create: one INSERT and one read-back per row, never one statement — so the old comment's 「all-or-nothing on this transport too」 was not true, and the statements sent and the mid-batch failure state are unchanged). upsert reserves before the statement, as SqlDriver.upsert does, and names the autonumber columns to the transport as insert-only (RemoteTransport.upsert gains an optional fifth argument, schema-free: WHICH columns, not WHY), so a merge keeps the number already in the row (#7011) and the insert leg — the one #7099 recorded as writing NULL — gets its number. The reservation a merge does not use is a gap, never a renumbering, the local face's rule byte for byte (pinned: seeded 42, merge, next create is 44). #7099's post-write warning is removed with the leg it reported.

Legacy shape and collision re-seed (H5), decided from the code. The pre-key_hash table: SqlDriver.ensureSequencesKeyHashShape rebuilds it through a live Knex connection, which this face has none of, and a raw-SQL rewrite would be a second copy of a migration; keying by the legacy (object, tenant_id, field) rule would be a second keying rule beside the shared one. So it is refused, DATABASE_ERROR / 500 with the remedy in the message (open the database once through the local or replica face), not cached as a verdict, caller-supplied numbers still written. It cannot arise on a database this face created. The #5495 re-seed is needed on this face — the bypass paths that create a stale counter (isSystem seed replay, preserveAudit import) are exactly the seed/import paths, and bulkCreate is their common door — and it is carried: autoNumberValueExists and resyncSequenceToDataMax are overridden for the remote face (the forward-only move is one atomic UPDATE guarded by last_value being below the observed MAX), and collidingAutoNumberReservations is the inherited discriminator, so a duplicate on a value the caller typed stays the caller's 409 (pinned).

H6. Clause-②: yes (widening) holds as measured: a create refused 501 today is accepted; no spec key moves; no export is added (the six lifted members are protected, and the overrides are typed through indexed types rather than a root re-export); RemoteTransport.upsert gains an optional parameter. Changeset: @objectstack/driver-turso minor, @objectstack/driver-sql patch.

Tests

Head for every number below: 52ab7ad76 (the merge of origin/main 1bd14c984), unless stated.

  • Converted, not deleted: turso-remote-autonumber-refusal.test.ts → turso-remote-autonumber-generation.test.ts (git mv then rewritten; git's rename detection does not fire at this similarity, so the diff reads D+A — the three-face structure, the controls, the update() / plain-object / caller-supplied cases and the layer pins are the refusal suite's own). 27 cases: generation on every leg incl. the Turso remote:带 id/conflictKeys 但没匹配上的 upsert 仍会静默写入 NULL 自增号(#6944 拒绝闸门覆盖不到的那条腿) #7099 leg; caller-supplied numbers, merge, update, plain object untouched; the two-face block (local Knex face and remote native-libsql face on ONE file: numbers interleave 1,2,3,4 on one key_hash row equal to the local face's sequenceKeyHash; per-tenant buckets; the 自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468 suffix seed read as 5 not 52026; the {field} refusal is the same sentence on both faces and writes nothing); the Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 re-seed on create and bulkCreate; the caller's own 409; the legacy-shape refusal with code + status; local and replica still issue; RemoteTransport.prototype still has no autonumber member and create.length === 2; supports.autonumber === true.
  • The two boundary pins rewritten, not deleted (turso-autonumber-resync.test.ts, turso-autonumber-batch-resync.test.ts): the transport-surface probe verbatim, and the refusal half replaced by generation + re-seed on the remote face.
  • Cross-process pin turso-remote-autonumber-concurrency.test.ts: two child processes (tsx over the package source, as packages/spec's process-boundary pins do; tsx added as a devDependency of driver-turso), each with its own @libsql/client native file: connection to one database file and its own remote-mode TursoDriver, released together by a file barrier; 2 × 200 creates per round → 400 distinct, exactly 1..400, strictly increasing per writer, no failed write, the table and the counter row agreeing. Those are the hard pins, asserted on every round. The overlap EVIDENCE (the writer changes at least twice in the global order) ends the loop on the first round that shows it; after three rounds without one the test is skipped with a note that says NOT MEASURED — because whether two processes overlap is the scheduler's decision, measured here once as a strictly serial run (1 change) under another seat's full-package run on this shared box. Final-head runs: 3/3 green standalone, measuring 400 writes · 400 distinct with 31, 21 and 23 writer changes; inside the full package run, 9. Said plainly: no sqld runs in this container; what is measured is two processes on libSQL's engine under its write lock, not an HTTP server.
  • Driver-conformance ledger (lane commitment): identical before and after — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt; the matrix has no autonumber cell for any driver, so none moved.

Runs, all under scripts/pm/os-verify-lock.sh after the merge: pnpm --filter '@objectstack/driver-turso^...' build exit 0 · driver-sql typecheck exit 0 · driver-turso typecheck exit 0 · driver-turso vitest run: 82 passed (82) files, 2218 passed | 33 skipped (2251) · driver-sql vitest run in two halves of 108 files: 103 passed | 5 skipped, 1389 passed | 100 skipped; 102 passed | 6 skipped, 1914 passed | 88 skipped (every file; the autonumber-named nine also run on their own: 66 passed | 2 skipped).

Reverse verification (predicted before each run; mutation and restore proven on disk by scripts/ablation-replace.mjs, blob == HEAD and git diff HEAD empty after each leg)

  1. Merge-set exclusion dropped (RemoteTransport.upsert ignores insertOnlyColumns). Predicted: exactly the two [#7011] merge pins red. Measured: 2 failed | 25 passed, both merge pins, expected 'CASE-00043' to be 'CASE-00042' and expected 'CASE-00099' to be 'CASE-00042' — the renumbering the exclusion exists to prevent.
  2. Warm path made non-atomic (SELECT last_value, then UPDATE to +1). Predicted: the distinct count in the concurrency pin drops below N; the stub-backed generation suite stays green. Measured — direction reversed, kept rather than tidied: the generation suite stayed green (27 passed) as predicted, but the concurrency pin went red on its 「no write failed」 assertion, not on the distinct count: the fixture's tenant-scoped unique index refused the second row carrying a duplicated number, and the forward-only re-seed could not outrun a writer that kept reading the same stale value, so writes failed with SQLITE_CONSTRAINT: UNIQUE constraint failed: index 'uniq_crm_case_organization_id_case_number' — 2 of 3 runs with no gap between the two statements, 3 of 3 with a 1 ms gap (the order of one HTTP round trip). On a declared-unique column a counter that hands out a number twice surfaces as refused writes before duplicate rows; recorded in the pin's docblock.
  3. Shared bootstrap reading zeroed in driver-sql (maxAutonumberCounter, reached through dist/: rebuilt, ablation-dist-preflight present in 2 built files, exit 0). Predicted: the bootstrap and re-seed pins red on BOTH faces. Measured: 10 failed | 25 passed across the three suites — generation: explicit-then-continues-above, merge-then-44, 自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468 suffix, re-seed create, re-seed bulkCreate; resync: LOCAL and REMOTE re-seed; batch-resync: LOCAL bulkCreate, LOCAL upsert, REMOTE batch. Restore: rebuilt, preflight --absent exit 0. A first attempt of this ablation was a null operation (the tool refused because the replacement restated the anchor; the preflight correctly reported the marker absent and that green run is void) and was redone with a different anchor.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 52ab7ad76 (no paths; 73 commands). Run on the final tree (2c9d2985a, every file identical to the commit): the 73 derived commands — 72 exited 0, incl. check:driver-conformance, check:query-options-erasure (holds: 67 unswept non-test sites, none new — my first head had added one as any on a find() query in the new concurrency pin, retyped as DriverQuery), check:test-source-alias, check:undeclared-dep-imports, check:doc-authoring, check:nul-bytes, check:lean-entry-closure (measured after building @objectstack/objectql: 15 packages, 201/199 modules, admitted set held), check:type-check-debt (399.5 s re-measure, none above record), check:pm-skill-ratchet (0 with AGENTS.md at the merge-base blob — the turbo 2.11.5 agent-rules block, #21146, had rewritten it once in this worktree and is restored). check:dts-closure exited 1 on its first final-tree run naming plugin-email and plugin-security (neither in this diff nor in its closure) with dist/index.d.ts absent at that moment; a forced rebuild of the two and a rerun exits 0 (71 packages, 167/167 declarations) — concurrent turbo activity in the same tree, not this change. NOT MEASURED (1): check:dual-build-cjs-loads exits 3 PREREQUISITE NOT MET locally (78 packages without dist/ in this worktree) — CI's. dispatch-gates --ran: 73 derived, 72 run, 1 NOT-MEASURED declared, 0 UNRUN (exit 0). Seven further families (agent-test-spelling, docs-audit-scope, pm-governed-merges, pm-governed-prose, pm-skill-id-lint, pm-skill-ratchet, required-contexts) were derived only while the turbo block sat in AGENTS.md; they were run anyway and all exit 0. The derivation itself still lists what no local run covers: the 53 artifact-roster families, 11 wide-population families, 6 path-scheduled CI jobs and the type-check lanes.

pnpm lint narrowed, as a measurement: ① population = the lint script's own eslint . --no-inline-config, under one eslint.config.mjs; ② --format json on the changed source files: 9 files, 0 errors, 0 warnings; ③ invariance: the config enables no type-aware linting for any file (eslint.config.mjs states it, no parserOptions.project), so this diff moves no untouched file's verdict.

Acceptance notes

  • Both faces: SQLite's LIKE has no escape character unless ESCAPE is declared, and the shared escapeLikePrefix writes backslashes. The remote statement declares ESCAPE '\'; the local Knex builder in scanMaxNumericTail does not (knex emits like ? with the escaped binding and no ESCAPE, measured on the sqlite and pg dialects), so on the local SQLite faces a format whose prefix contains _ or % seeds from 0 (measured: SO\_% matches nothing on SQLite without ESCAPE). Pre-existing, outside this card's class; filed in the report as a finding, not fixed here.
  • RemoteTransport.upsert with caller conflictKeys leaves id in the merge set ("id" = excluded."id") — the drivers(sql): an upsert that merges on a non-PK conflict key silently REWRITES the existing row's primary key — measured on SQLite and MySQL alike #8622 shape the local face closed. Not touched; noted for the lane.
  • The README's remote refusal table loses its 「Record numbers」 row; content/docs/** carries no statement of the remote refusal (grepped).

维护者速读(草稿)

  • 改了什么: Turso 远程传输面上,create / bulkCreate / upsert 留空的 auto_number 字段现在会生成编号,编号来自与本地面相同的持久计数表,在数据库里原子递增;原先的 501 拒绝改为生成。
  • 为什么改: 托管产品的租户库全部走远程传输,任何声明了 auto_number 字段的对象都无法新建记录(cloud#2531 实测:HotCRM 建客户答 501)。这正是 TursoDriver remote 面根本不生成自增号:RemoteTransport.create 自建 INSERT,auto_number 只是个 TEXT 列 #6944 当初留下的「等实测需求」的门,现在门开了。
  • 风险与代价(含回滚): 多进程并发只靠数据库写锁串行,无进程内计数;两个面共用一张计数表、一套键规则,已在同一数据库文件上对拍。遗留形状的计数表在远程面上明确拒绝、不迁移。回滚即还原本 PR;已生成的编号留在数据里,计数表形状与本地面一致。
  • 席位意见: (留空)
  • 你要做的: 批准后 cloud#2531 推进 pin 并在托管环境复验建客户。

Generated by Claude Code

claude added 8 commits October 1, 2026 08:49
…e shared persistent sequence

Disposition A of the remote-autonumber question, executed: a create,
bulkCreate or upsert on the Turso REMOTE transport that leaves an
auto_number slot empty now gets a generated value, atomically in the
database, from the same `_objectstack_sequences` counter the local and
embedded-replica faces draw from.

- sql-driver: the rules a second face must share are lifted into protected
  members (`resolveSequenceTenantId`, `defineSequencesTable`,
  `maxAutonumberCounter`, `escapeLikePrefix`, the table name and retry
  budget). No behaviour change on the local faces.
- turso-driver: `getNextSequenceValue` routes by transport; the remote face
  moves the counter in one statement (`UPDATE … RETURNING`, cold path
  `INSERT … ON CONFLICT DO UPDATE … RETURNING`), bootstraps from the data
  table by the shared reading, and carries the #5495 re-seed. A legacy
  (pre-key_hash) table is refused loudly, not keyed by a second rule.
- remote-transport: `upsert` takes insert-only columns so a merge keeps the
  number already in the row.
- The refusal suite is converted to the generation suite; the two resync
  boundary pins are rewritten to pin generation; a cross-process pin runs
  two writers in two processes against one database file.

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…oncurrency pin reports its measurement

The README's remote-mode refusal table loses its "Record numbers" row and
gains a section on how remote mode issues them. The changeset declares the
widening for @objectstack/driver-turso (minor) and the lifted protected
members for @objectstack/driver-sql (patch). The cross-process pin asks for
two writer changes in the global order rather than a non-contiguous run per
writer, which the scheduler does not guarantee, and prints what it measured.

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…blation in the concurrency pin

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…tonumber suites

`bypassTenantAudit` is a declared DriverOptions key and the read-back order is a DriverQuery, so neither needs an `as any`; the query-options-erasure ratchet counted the one on the concurrency pin's find().

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…te it in the working tree and a blanket add carried it into the previous commit

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
… un-overlapped run NOT MEASURED

Whether two processes overlap is the scheduler's decision: under another seat's full-package run one writer wrote all of its numbers before the other was scheduled past the barrier. The hard pins (distinct, complete, monotonic, no failed write) hold on every round; the overlap evidence ends the loop on the first round that shows it, and a run with none is skipped with a note rather than failed or passed.

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/driver-turso, touching 38 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.

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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
  • 2 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 — 14 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 fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a

⚠️ 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 fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a → 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: 2c9d2985a067984cfb97997d73757e9f38835315
Local-runs: none

Inputs: card #21113 (body, the claim comment, the dev report 5929937746), PR #21160 (body, the 14-file list, the net diff against the merge-base e952cff57), and the 32 check-runs on the head. Nothing built, run or re-run.

① Derived judgments

Accept-set, driver-turso remote face. A create / bulkCreate / upsert that leaves an autonumber slot empty (undefined, null, '') was refused NOT_IMPLEMENTED/501 and is now accepted with a generated value: the widening the PR declares. Right. It is #6944's disposition A behind its appetite door, opened by the measured demand the card records (cloud#2531). supports.autonumber on this face was already true, so the engine never ran its in-memory fallback here and none is introduced (pinned: supports.autonumber === true, and RemoteTransport.prototype still carries no autonumber member).

Atomicity (Acceptance 2). Warm path: UPDATE "_objectstack_sequences" SET last_value = last_value + 1 ... WHERE key_hash = ? RETURNING last_value, one statement, no read-then-write window. Cold path: a SELECT of the partition's stored values (read in JS by the shared maxAutonumberCounter), then INSERT ... ON CONFLICT (key_hash) DO UPDATE SET last_value = last_value + 1 RETURNING last_value: the counter move is again one statement. Two cold writers both scan, both INSERT; one wins the row and the other's CONFLICT arm increments it, so the values are distinct and no seed is lost. The window between the scan and the INSERT can only leave the seed stale (a bypass row landing meanwhile); SqlDriver.getNextSequenceValue has the same window (its scan at :7940 precedes its insert inside one transaction, and forUpdate is a no-op on SQLite), and both faces close it the same way: the #5495 re-seed, which on this face is UPDATE ... SET last_value = ? WHERE key_hash = ? guarded by last_value being below the observed MAX, forward-only and one statement, run when collidingAutoNumberReservations proves a unique violation to be this counter's. remoteSequencesTableEnsured holds only that the table exists and is reset on any failure; no in-process counter exists anywhere in the diff. Right.

What the concurrency pin proves, and what it does not. Two tsx child processes, each with its own @libsql/client native file: connection and its own remote-mode TursoDriver, released together by a file barrier, 2 x 200 creates per round: 400 distinct, exactly 1..400, strictly increasing per writer, no failed write, table and counter row agreeing, asserted every round. It proves the two statements are atomic under libSQL's write lock across OS processes, on the engine a Turso endpoint runs, which is the property the hosted runtime (several containers, one tenant database) needs. It does not exercise Hrana/HTTP: a statement retried after a lost response advances the counter twice, a gap the acceptance tolerates (gap-tolerant-monotonic), never a duplicate. And the overlap evidence is a skip, not a pass, when the scheduler serialises three rounds, so a green run of this file in CI is not by itself proof that the writers overlapped on that run; the skipped count is where it shows. The dev says both plainly. Judged adequate for Acceptance 2, with those two limits on record.

One semantics, shared not copied. Format and precedence: the inherited fillAutoNumberFields is called on every remote write (writeRemoteRowWithAutoNumbers), so resolveAutonumberFormat, renderAutonumber, the {field} refusal, the empty-slot predicate and the reservation report are one code path (pinned: the same refusal sentence on both faces, nothing written). Key: sequenceKeyHash(table, resolveSequenceTenantId(tenantField, tenantId), field, scope), pinned equal to the local face's sequenceKeyHash on one database file, with 1, 2, 3, 4 interleaving on one row whose last_value reads 4. Tenant: the same resolveSequenceTenantId, and the cold scan's tenant filter (resolvedTenantId === GLOBAL_TENANT ? null : resolvedTenantId) is the spelling SqlDriver.getNextSequenceValue uses at :7946. Bootstrap: maxAutonumberCounter with the suffix, pinned for #6468 (005-YYYY read as 5 on both faces, both continuing at 6 and 7). Table: defineSequencesTable compiled to text by the connection-less Knex, so both faces create one shape. What this face spells itself is the three statements and quoteSequenceName (identifier quoting that mirrors the transport's private tableSql), not a semantics copy. The ESCAPE '\' declared on the remote LIKE is right, and it is what exposed finding (a) in ③. Right.

The converted test (Acceptance 3). The refusal suite's 22 cases map onto the generation suite's 27. Every refusal became its generation counterpart (create, bulkCreate, the mixed batch, the no-id upsert, the '' and null slots). Every control is carried (caller-supplied number, batch with its own numbers, id-bearing upsert merges and keeps its number, update untouched, no-autonumber object untouched). The three face pins (LOCAL, LOCAL bulkCreate+upsert, REPLICA) and the two layer pins (RemoteTransport.create.length === 2, no autonumber member on the transport) are carried verbatim. The #7099 leg (an id-bearing upsert that INSERTS) is converted from "writes NULL and warns" to "gets its number, and no warning is logged". The other three #7099 pins asserted the warning's silence on legs that never warned; the warning is removed together with the leg it reported, so those pins have no subject left, and the behaviours behind them (merge keeps the number, no-autonumber object, caller-supplied) are pinned by the carried cases. Added: caller object not mutated, an explicit payload number does not renumber a merge, the two-face block (4 cases), the #5495 re-seed on create and bulkCreate plus the caller's own 409 (3), the legacy shape (1). The two resync pins keep their surface probe verbatim and replace the refusal half with generation and re-seed. Converted, not deleted. Right.

upsert and bulkCreate. RemoteTransport.upsert gains an optional fifth parameter, insertOnlyColumns (schema-free, default []); the driver passes remoteAutoNumberColumns(object, table), the object-then-table lookup fillAutoNumberFields makes, so a merge keeps its number and the unused reservation is a gap (pinned: seeded 42, merge, next create is 44; an explicit payload number does not renumber a merged row). SqlDriver.insertOnlyUpsertColumns also names id and created_at; the remote face names only the autonumber columns, so with caller conflictKeys the transport's merge set still carries id, which is pre-existing (#8622's shape) and is finding (b) in ③. bulkCreate on the remote face now loops this.create; RemoteTransport.bulkCreate was already a per-row loop of its own create (one INSERT and one read-back per row), so the statements sent and the mid-batch failure state are unchanged, and the old "all-or-nothing on this transport too" comment was false and is corrected. Right.

Legacy pre-key_hash table on the remote face. Refused DATABASE_ERROR/500 with the remedy in the message, not cached as a verdict (pinned: refused twice, nothing written, the table left exactly as found, a caller-supplied number still written through). Against ADR-0112: DATABASE_ERROR is a StandardErrorCode member (errors.zod.ts:111), 500 is the server band, and the same pair is what the spec's migration ledger records for "the object's table is absent", a correctly spelled request blocked by the backend's own state, which is this case. One consequence to know, not a defect of this PR: declaresServerFault (status at or above 500 with a string code, packages/types/src/error-leak.ts) means the REST doors put the code on the wire and withhold the prose, so the remedy reaches the operator's log rather than the tenant's body, which is where an operator action (open the database once through the local or replica face) belongs. Right.

Public surface. @objectstack/driver-sql's root gains one type re-export, AutoNumberReservation (the interface was exported from the module before, but the package exposes only ., so it was not reachable). Six new protected members on the exported SqlDriver (sequencesTableName, autoNumberCollisionRetries, resolveSequenceTenantId, defineSequencesTable, escapeLikePrefix, maxAutonumberCounter): subclass API in dist/index.d.ts, no runtime behaviour change on any dialect, every hunk a sequence member (:7532 to :8190 on the head, the region the claim bounded by content; its line numbers were read at an older base). RemoteTransport.upsert's optional fifth parameter. GLOBAL_TENANT was already exported. All right to make; the level of the root export is ② below.

AGENTS.md. Absent from the net diff: 14 paths, none is AGENTS.md, and the blob is e9e211fc9 at both the merge-base and the head (committed in 20a25d847, restored in 78fea1334). Confirmed.

tsx. devDependencies only. The lockfile gains only the importer entry (tsx: ^4.23.12, resolved 4.23.12); tsx@4.23.12 already resolves in the packages section for 12 other importers. Consistent, and Validate Package Dependencies is success on the head.

Check-runs on the head (32). Success: Build Core, Build Docs, Type Check source gates, Type Check debt ledger, Temporal Conformance (live PG + MySQL), Dogfood Regression Gate 2/3 and 3/3, Dogfood Verify CLI, Check Changeset, Validate Package Dependencies, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the four claim and path guards, Auto Label, filter. Skipped: Console Pin Gate, Packed-tarball smoke (opt-in). Still in_progress, so not verdicts: Test Core 1/6, 2/6, 3/6, 4/6, 5/6, 6/6, Dogfood Regression Gate 1/3, Lint and Repo Gates, Type Check consumer gates, Type Check workspace. Eleven, each named; none is read as green here, and the driver-turso pins (generation, concurrency, resync) run under Test Core. Check Changeset is check-changeset-no-major.mjs: presence and no-major, not the level, so its success does not answer ②.

② Semver level

Clause-②: yes (widening) is on the PR body and in the changeset body. The arm is right: a refused input is accepted, nothing narrows, nothing authorable moves, so no ADR-0087 marker is owed. @objectstack/driver-turso: minor. Right; yes takes at least minor (AGENTS.md Post-Task §3).

@objectstack/driver-sql: patch. Wrong. The diff adds a root export (AutoNumberReservation in src/index.ts's export-type block), and the changeset itself names it ("the AutoNumberReservation type is exported"). This package's own CHANGELOG levels exactly this shape: 17.5.0, "operatorFacingErrorText is a new export, present in dist/index.d.ts and in the export list. A purely additive widening takes at least minor", and "@objectstack/driver-memory is minor for the two new public-entry exports"; and the patch-side rule in 17.3.0, "Why patch and not minor: no export is added (the collector is protected)". The six protected members alone would be patch by that rule; the root type export is not. The dispatch premise "no spec key or export moves" was falsified by the implementation and reported in the PR body, but the level did not move with it. Either of two fixes closes this: bump @objectstack/driver-sql to minor in .changeset/21113-turso-remote-autonumber.md; or drop the root re-export and type the two overrides through the Parameters indexed type over SqlDriver that the autoNumberValueExists override already uses for its first parameter, leaving the export list unchanged and patch right. Nothing else in the changeset body is wrong.

③ Boundary flags

Implemented-by: claude/issue-21113-turso-remote-autonumber
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: FAIL

One blocking item, ②: @objectstack/driver-sql is levelled patch while the diff adds a root export; one line in the changeset, or dropping the re-export, closes it. Every ① judgment and every ③ flag holds on this head, so the re-review after that fix is bounded to the changeset and to the eleven check-runs still in progress.


Generated by Claude Code

claude added 2 commits October 1, 2026 11:11
…; the overrides read it off the protected members

Review 5930100091: a root type re-export levels driver-sql at minor, and patch was declared. The re-export is dropped and the two re-seed overrides on the remote face take their parameter types through Parameters indexed over SqlDriver's protected members, so driver-sql's export list is unchanged and patch stands.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5d09309a65a19d50631d0a349e0e002b1129d6b8
Local-runs: none

Round-1 delta review of FAIL 5930100091 @ 2c9d2985a. Inputs: card #21113 (body; the claim 5927179360, the round-0 report 5929937746, the seat's decision 5930121675, the round-1 report 5930264607), PR #21160 (body as rewritten for round 1, the 13-file list, the net diff against the merge-base fde553c50, which is the main commit merged in 605a1d2b5), the 39 check-runs on the head read once at 2026-10-01T11:22:44Z, and the previous at-tier record. Nothing built, run or re-run; the shared checkout was only fetched into a review ref and read.

What moved since 2c9d2985a. Two first-parent commits: the merge 605a1d2b5 (of main fde553c50, 10 commits) and the fix 5d09309a6. The PR's own hunks are patch-id identical before and after the merge (849a28b3c for both e952cff57..2c9d2985a and fde553c50..605a1d2b5, identical file lists), and main's one change to a PR file in that range (sql-driver.ts via #21117, hunks at :144, :3669 and :16232) is outside the sequence region (:7347 to :7950 at this head), so the merge carried nothing of its own and the net diff is the PR's hunks plus the fix. The fix is 3 files, +6/-11: driver-sql/src/index.ts drops the six-line AutoNumberReservation re-export; turso-driver.ts drops the type AutoNumberReservation import and retypes two override parameters; the changeset's last sentence changes. Nothing else.

① Derived judgments

The four questions of this round.

  • packages/drivers/driver-sql/src/index.ts is byte-identical to main: blob cb0a71d25 at the head, at origin/main and at the merge-base; git diff origin/main..head on it is empty. No new export reaches a consumer: @objectstack/driver-sql's package.json is unchanged (exports has only ., to dist/index.d.ts / .mjs / .js), the only driver-sql source in the diff is sql-driver.ts, and every addition there is protected (six members). Right.
  • The two re-seed overrides take their parameter types through Parameters indexed over SqlDriver's protected members: autoNumberValueExists takes Parameters of SqlDriver['autoNumberValueExists'] at index 0 and index 1; resyncSequenceToDataMax takes Parameters of SqlDriver['resyncSequenceToDataMax'] at index 0; getNextSequenceValue's parentTrx the same way (unchanged from round 0). No structural copy: a grep for AutoNumberReservation across driver-turso at this head hits only four prose mentions of collidingAutoNumberReservations; no interface or type named *Reservation and no object type spelling its fields exists in the package. The premise the seat required measuring (the type is reachable from the subclass without the export) is sound by construction, since an indexed access type on a protected member is legal inside the subclass body, and the same spelling already compiled at 2c9d2985a for the first parameter and for parentTrx (Type Check source gates success there and here). Right.
  • driver-sql at patch: right now, judged in ②.
  • AGENTS.md: absent from the net diff (13 paths, none is AGENTS.md), blob e9e211fc9 at both main and the head, and fde553c50 carries chore(turbo): opt out of the agent-guidance block in the root turbo.json #21151's agentGuidance: false opt-out, so the cause of the round-0 incident is gone from this branch's tree as well. Confirmed.

Every ① judgment of 5930100091, re-confirmed on this head. The hunks they were made on are unchanged by patch-id, so each holds unless the fix touched it:

  • Accept-set, driver-turso remote face: a create / bulkCreate / upsert leaving an autonumber slot empty (undefined, null, '') was refused NOT_IMPLEMENTED/501 and is now accepted with a generated value; supports.autonumber stays true and no in-memory fallback is introduced (pinned). Holds.
  • Atomicity (Acceptance 2): warm UPDATE ... RETURNING last_value, cold scan-then-INSERT ... ON CONFLICT (key_hash) DO UPDATE ... RETURNING, one statement each; the scan-to-insert window leaves only a stale seed, closed by the Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 forward-only re-seed exactly as on the local face; remoteSequencesTableEnsured remembers only that the table exists and is reset on failure. Holds.
  • The concurrency pin and its two limits (no Hrana/HTTP; a skip, not a pass, when three rounds serialise): holds. The round-1 report adds one measurement on the merged tree, 400 writes / 400 distinct / 5 writer changes inside the full-package run.
  • One semantics, shared not copied: fillAutoNumberFields called on every remote write; sequenceKeyHash(table, resolveSequenceTenantId(...), field, scope) pinned equal to the local face's on one database file; maxAutonumberCounter with the suffix (自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468); defineSequencesTable compiled to text; the ESCAPE '\' on the remote LIKE. Holds, and the fix strengthens it: the reservation type is no longer even named in driver-turso.
  • The converted test (Acceptance 3): refusal suite D (537 lines) to generation suite A (602 lines, 27 cases), controls and the three face pins and two layer pins carried, the Turso remote:带 id/conflictKeys 但没匹配上的 upsert 仍会静默写入 NULL 自增号(#6944 拒绝闸门覆盖不到的那条腿) #7099 leg converted, the two resync pins rewritten with the surface probe verbatim. Holds.
  • upsert and bulkCreate: RemoteTransport.upsert gains an optional fifth parameter insertOnlyColumns (default []), the driver names the autonumber columns, a merge keeps its number and the unused reservation is a gap (pinned 42, merge, 44); remote bulkCreate loops this.create and the transport was already a per-row loop, so statements and mid-batch state are unchanged. Holds.
  • Legacy pre-key_hash table on the remote face: refused DATABASE_ERROR/500 with the remedy, not cached, caller-supplied numbers still written (pinned). Holds.
  • Public surface, REVISED by the fix: @objectstack/driver-sql's root gains nothing; six protected members on SqlDriver (sequencesTableName, autoNumberCollisionRetries, resolveSequenceTenantId, defineSequencesTable, escapeLikePrefix, maxAutonumberCounter) are subclass API in dist/index.d.ts only. GLOBAL_TENANT, newly imported by turso-driver, was already exported (index.ts:107 on main). On @objectstack/driver-turso, RemoteTransport IS a root export (src/index.ts:38, unchanged), so upsert's optional fifth parameter is public surface of that package, additive, covered by its minor. All right to make.
  • tsx as a devDependency of driver-turso with only the importer entry in the lockfile (tsx@4.23.12 already resolved): holds; Validate Package Dependencies is success on this head.
  • Cosmetic, not a defect: the PR body's ":7532 to :8190 on this head" for the sequence region predates the merge; at 5d09309a6 the members sit at :7362 to :7999.

Check-runs on the head (39 entries, read once at 11:22:44Z). Success (19): Type Check source gates, Build Docs, Check Changeset (both runs), Validate Package Dependencies, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, The card this PR closes must claim this branch (both), No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both), Auto Label, filter. Skipped (4): Console Pin Gate, Packed-tarball smoke (opt-in), and one Auto Label and one Check PR Size duplicate. Still in_progress, so not verdicts, each named: Build Core (success on 2c9d2985a, re-running here), Test Core 1/6, 2/6, 3/6, 4/6, 5/6, 6/6 (these carry the driver-turso generation, concurrency and resync pins), Lint and Repo Gates, Type Check consumer gates, Type Check debt ledger, Type Check workspace, Dogfood Regression Gate 1/3, 2/3, 3/3, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL). Sixteen; none is read as green here, and none was polled. No check is failure or cancelled. Check Changeset is presence and no-major, not the level, so ② is judged from the diff.

② Semver level

Clause-②: yes (widening) is on the PR body and in the changeset body. The arm is right: a refused input is accepted, nothing narrows, no spec key or authorable surface moves, so no ADR-0087 marker is owed. @objectstack/driver-turso: minor. Right; yes takes at least minor, and the package also gains an optional parameter on an exported class.

@objectstack/driver-sql: patch. Right now. The root export that made it wrong at 2c9d2985a is gone (index.ts byte-identical to main); the six new members are protected; and every sql-driver.ts hunk is an extraction with identical logic: defineSequencesTable is the former inline createTable body, escapeLikePrefix the former regex, maxAutonumberCounter the former loop over the same values, resolveSequenceTenantId the former ternary at its three sites, and the two getters return the module constants and are not called by SqlDriver itself. No statement SqlDriver sends changes on any dialect. This is the 17.3.0 shape in the package's own CHANGELOG ("no export is added, the collector is protected"). The changeset sentence matches the diff: "No export is added and no behaviour changes on any dialect", and that sentence is the only changeset delta since 2c9d2985a. The blocking item of 5930100091 is closed on branch 1 of the seat's decision 5930121675, with the premise measured as that decision required; branch 2 was not taken and is not needed.

③ Boundary flags

Implemented-by: claude/issue-21113-turso-remote-autonumber
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

The one blocking item of 5930100091 is closed by dropping the root export and typing the two overrides through the indexed Parameters of the protected members; driver-sql patch and the changeset text are right on this head, every ① judgment of the previous record holds on hunks that are patch-id identical, and ③ carries nothing open for this PR. Sixteen check-runs were still in progress at the read and are not verdicts of this record; the merge gates read them on their own.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 11:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 11:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit e35c40a Oct 1, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21113-turso-remote-autonumber branch October 1, 2026 12:09
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… better-auth family (objectstack-ai#21094) (objectstack-ai#21162)

Fixes objectstack-ai#21094

Clause-②: no

Takes the 2026-10 production-dependency group that Dependabot opened as
objectstack-ai#21029 (closed in favour of this card), without the better-auth family
and without `next`. Maintainer ruling, verbatim:

> 按你的推荐:关掉21029立卡,better-auth先放着,21024授权你落地

## What lands

- **43 manifests, 64 range moves.** The 63 range moves objectstack-ai#21029 made
(diffed from its merge base `9b0de7de73` to its head `c0b7dd923e`) were
re-applied one line at a time onto current `main`. The 64th is the `tsx`
devDependency objectstack-ai#21160 added to
`packages/drivers/driver-turso/package.json`, lifted from `^4.23.12` to
`^4.23.15` after objectstack-ai#21160 was merged in. Each move matched the exact old
spelling or the script refused. Excluded: `apps/docs/package.json`
(`next` belongs to objectstack-ai#21055 / objectstack-ai#21083) and the five better-auth family
lines in `packages/plugins/plugin-auth/package.json`, which stay at
`1.7.3`. `pnpm-workspace.yaml` is not touched. Check, taken before the
objectstack-ai#21160 merge: the sorted `-`/`+` lines of this branch's manifest diff
equal objectstack-ai#21029's lines minus those exclusions (`diff` exit 0, 126 lines).
- **`pnpm-lock.yaml`**, regenerated with the tooling in three steps
(pnpm 10.31.0, the `packageManager` pin), never by hand:
  1. `pnpm install --lockfile-only`, starting from main's lockfile.
2. `pnpm --filter @objectstack/plugin-hono-server --filter
@objectstack/plugin-auth --filter @objectstack/hono update
--lockfile-only --no-save hono`. Step 1 left `hono` at 4.13.7, below the
`^4.13.9` that `plugin-hono-server` now publishes. The workspace
override for `hono` (selector below 5.0.0) rewrites every declaration to
`^4.13.5`, which 4.13.7 still satisfies, so lockfile inertia kept the
old version. CI would have certified 4.13.7 while a downstream install
gets 4.13.12. That is the declared-versus-tested split the card names
for better-auth. Step 2 moves the single `hono` copy to 4.13.12 without
editing the override or any manifest. An unfiltered `pnpm update -r` was
tried and discarded: it also dropped `knex`'s `mysql2` / `pg` /
`tedious` peer links in two importers.
3. After objectstack-ai#21160 landed: merge `main` (`e35c40a52`), lift driver-turso's
new `tsx` line, and run `pnpm install --lockfile-only` again.
- The result is a fixed point: a second `pnpm install --lockfile-only`
leaves it byte-identical, and `pnpm install --frozen-lockfile` passes.
Lockfile blob `30ba2147`. The resolved set is unchanged from the set the
OSV scan below covered: 0 names differ.
- **`packages/drivers/driver-turso/src/**`**: the item-4 restamp, 21
lines in 7 files (below).
- **`packages/spec/src/data/driver/common.zod.ts` and
`driver-credential-refusal.test.ts`**: the `@libsql/core` version label
is restamped from 0.17.4 to 0.18.0. The TSDoc of
`CREDENTIAL_URL_QUERY_PARAMS` says it was measured "in the versions
pinned by this tree", so leaving 0.17.4 would have made it false. The
behaviour was re-measured identical on the installed 0.18.0:
`?authToken=` overrides the config token, `auth%54oken` is decoded,
`AuthToken` and `token` give `URL_PARAM_NOT_SUPPORTED`, and the control
without a query keeps the config token.
- **`.changeset/21094-prod-deps-group.md`**: `patch` for the 19
published packages whose `dependencies` range moves. The list was
re-derived from this diff and matches the PM correction on the card,
`plugin-auth` included (`@noble/hashes`, `jose`).

## Resolved versions against the merge base

Every changed `name@version` pair in the `packages:` section, compared
with `main` at `e35c40a52`: 25 names change. **DOWN: 0.**

| package | merge base | this branch |
|:--|:--|:--|
| `@libsql/client` / `@libsql/core` | 0.17.4 | 0.18.0 |
| `zod` | 4.6.1 | 4.6.5 (4.6.1 kept by `apps/docs` only) |
| `@modelcontextprotocol/sdk` | 1.30.0 | 1.31.0 |
| `hono` | 4.13.7 | 4.13.12 |
| `mongodb` | 7.5.0 | 7.7.0 (7.5.0 kept by `mongodb-memory-server-core`
only) |
| `jose` | 6.2.7, 6.2.8 | 6.2.12 |
| `@noble/hashes` | 2.3.0 | 2.4.0 |
| `@noble/ciphers` | 2.3.0 | 2.4.0 (2.3.0 kept by better-auth 1.7.3
only) |
| `react` / `react-dom` / `@types/react` / `@types/react-dom` | 19.2.x |
19.3.0 (19.2.x kept by `apps/docs` only) |
| `tsx` | 4.23.12 | 4.23.15 |
| `yaml` | 2.9.0 | 2.9.1 (2.9.0 kept by the `apps/docs` fumadocs tree
only) |
| `chalk` | 6.0.0 | 6.0.1 |
| `sql.js` | 1.14.1 | 1.14.2 |
| `pinyin-pro` | 3.29.1 | 3.29.4 |
| deduped onto a newer copy already present | `@hono/node-server`
2.0.12, `ws` 8.21.1, `@types/ws` 8.18.1, `eventsource-parser` 3.1.0 |
removed (2.1.1, 8.22.0, 8.18.2, 3.1.1 stay) |
| new transitive copies beside the old ones | none | `bson` 7.3.3 and
`@mongodb-js/saslprep` 1.5.5 (with `mongodb` 7.7.0), `scheduler` 0.28.0
(with `react-dom` 19.3.0) |

`nodemailer` stays at **10.0.13**, main's version and at least the
required 10.0.12. Dependabot's regeneration had moved it down to
10.0.11.

## Item 4: `@libsql/client` lifted to `^0.18.0`, because the premise
holds

The premise was measured before any driver edit, three ways.

1. **Package diff.** `npm pack` of both releases. `@libsql/core` 0.17.4
and 0.18.0 differ only in `package.json` (the version). `@libsql/client`
differs only in `lib-esm/sqlite3.js`, `lib-cjs/sqlite3.js`,
`lib-esm/sqlite3.d.ts` and `package.json`. The change is a connection
pool for the local `file:` client. `http.js`, `ws.js` and `node.js` are
byte-identical. Installed side by side, `@libsql/hrana-client` 0.10.0
and native `libsql` 0.5.29 (with `@libsql/linux-x64-gnu`) are
byte-identical too.
2. **Executed probe, same script against both installs.** Output is
identical except for the version strings and one count: `syncUrl`
occurrences in `sqlite3.js` go from 3 to 4 (the new pool-size line).
3. **The driver's own suite.** At base with 0.17.4: 81 files, 2210
passed, 33 skipped. With 0.18.0, before the restamp: 2209 passed, 1
failed, 33 skipped. Per-test outcomes are identical except the version
pin (`expected '0.18.0' to be '0.17.4'`). After the restamp: 2210
passed, 33 skipped. At `0f87e8488`, with objectstack-ai#21160's tests merged in: 2218
passed, 33 skipped, 0 failed.

| site (at base) | claim | 0.18.0 reading | verdict |
|:--|:--|:--|:--|
| `turso-authtoken-url-channel.test.ts:17-18` | client / core / native
versions, range | client 0.18.0, core 0.18.0, native `libsql` 0.5.29;
range now `^0.18.0`. Answers 1-4 (live wire, child-process replica
endpoint) pass, 7/7 non-pin cases, as 8/8 did on 0.17.4 | held,
restamped |
| `turso-authtoken-url-channel.test.ts:281` | version pin | `0.18.0` |
pin moved |
| `turso-driver-remote-url-replica-refusal.test.ts:28`,
`turso-driver.ts:1288`, `:1384` (message) | no embedded replica for a
remote url | `syncUrl` lines: `http.js` 0, `ws.js` 0, control
`authToken` lines 6 / 6. `https` and `ws` clients' `sync()` reject
`SYNC_NOT_SUPPORTED`. `:memory:` + `syncUrl` throws `URL_INVALID`
("Embedded replica must use file for local db"). File 27/27 on both
versions | held |
| `turso-driver-timeout.test.ts:9`, `turso-driver.ts:152` | HTTP arm
rides `Config.fetch`; replica `sync()` is native | `http.js` (one
`config.fetch` site) and hrana-client byte-identical. Timeout file 5/5,
supplied-client refusal file 13/13 on both | held |
| `turso-driver.ts:871` | `Config.timeout` is the busy timeout; "remote
clients ignore it" | core `api.d.ts` docblock byte-identical | held |
| `turso-driver-unrecognised-url-refusal.test.ts:26`,
`turso-driver.ts:1313`, `:1417` (message) | refusals of urls the client
rejects | `URL_INVALID` for `./data/app.db`, `data/app.db`,
`/abs/app.db`, `:MEMORY:`, empty, `libsql:host` (bare paths "not in a
valid format"). `URL_SCHEME_NOT_SUPPORTED` for `sqlite:`, `memory://`,
`C:\data\app.db`. File 42/42 on both | held |
| `turso-driver-uppercase-ws-scheme-timeout-refusal.test.ts:15`, `:26`,
`turso-driver.ts:932` | `expandConfig` lowercases the scheme before the
switch | `WSS://…` gives `wss`, `Ws://…` gives `ws`, control
`LIBSQL://…` gives `https`. `_createClient(expandConfig(config, true))`
present (1 hit). File 20/20 on both | held |
| `turso-driver-ws-timeout-refusal.test.ts:10`, `turso-driver.ts:963`,
`:1001` (message) | the WS client takes no `fetch` and no timeout |
`ws.js`: `fetch` 0, `timeout` 0. hrana `ws/*.js` + `index.js` `timeout`
0, control `fetch` over `http/*.js` 15. File 11/11 on both | held |
| `turso-driver.ts:1061` (message) | a built client's transport cannot
be re-seamed | `config.fetch` is read once, inside `_createClient` in
`http.js` (byte-identical) | held |
| `turso-driver.ts:1212` | the client folds scheme case and opens each
spelling | `FILE:./x.db` gives `file`, `Wss://` gives `wss`, `LIBSQL://`
gives `https`. `createClient` opens each (`http` / `ws` / `file`) | held
|
| `turso-driver.ts:1244` | `:memory:` expands to `file::memory:`;
`isInMemoryConfig` | `file::memory:`. `true` for `file::memory:` and
`file::memory:?cache=shared`, control `false` for `file:./x.db` | held |

After the edit, `git grep -n -E "0\.17\.[0-9]" --
packages/drivers/driver-turso/src` returns 0 lines (exit 1). The control
is the 23 `0.18.0` occurrences in the same 7 files.

## What objectstack-ai#21029 never measured

- **Test Core shard 4's eight unreached packages**, measured at
`cf1d700b`, before the `main` merge: `rest` 250 files, 4728 passed / 248
skipped. `driver-sql` 205 files, 3303 passed / 188 skipped.
`plugin-email` 31 files, 510 passed. `plugin-approvals` 52 files, 804
passed. `plugin-webhooks` 13 files, 160 passed. `trigger-schedule` 8
files, 170 passed. `connector-mcp` 3 files, 23 passed. `client-react` 3
files, 34 passed. Turbo ran 45 of 45 tasks, exit 0.
- **`Lint & Repo Gates` from `check:vendor-export-contract` on**: `pnpm
check:vendor-export-contract` reads `VERDICT: PASS — vendor export
contract (installed workspace), 1 edge(s) verified` (better-auth 1.7.3),
and the `-resolve` variant passes on the registry. The 72 later steps of
that job were not run here. They belong to CI's run on this PR. The
families this diff derives are below.
- **OSV.** `osv-scanner` v2.3.8, built from the Go module proxy because
this container's egress refuses `api.osv.dev`. It ran on the offline npm
database over lockfile blob `70547c67` (its resolved set is identical to
the current `30ba2147`), with the repo's `osv-scanner.toml` loaded: 1388
packages, `No issues found`, exit 0. Positive control: the same lockfile
with `hono` rewritten to 4.12.32 gives exit 1 and 8 advisories on
`hono`, among them GHSA-8j4g-w8fx-2239. CI's online step is the
authoritative reading.
- **`zod` 4.6.5 and `z.properties()`**: `git grep -n -E
"z\.properties\(" -- packages/` returns 0 lines; control `z.object(`,
1825 lines.
- **`mongodb` live suites**: NOT MEASURED. They need a `mongod` download
from `fastdl.mongodb.org`, which this container's egress refuses
(CONNECT 403). The default `driver-mongodb` suite passes: 30 files, 675
passed, 172 skipped (the five opt-in live files).

## Gates and tests

Gate families derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (no paths, change set from git)
at HEAD **`0f87e8488`** (54 paths against merge base `e35c40a52`): 117
commands. All 117 exit 0, and so does `pnpm
check:vendor-export-contract`. Exit codes were captured before any pipe.
`--ran` reconciliation: `117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN`,
a derived zero with an exit code on every line.

Package suites, all exit 0:

- At `cf1d700b`: `driver-mongodb`, `mcp` 32/344, `plugin-hono-server`
27/324, `plugin-auth` 115/2472, `service-settings` 33/584,
`plugin-pinyin-search` 2/21, `driver-sqlite-wasm` 36/675,
`driver-memory` 69/1613, `service-analytics` 155/3526 (10 skipped),
`create-objectstack` 16/247, `@objectstack/hono` 5/122. Also
`@objectstack/cli` `--project unit` 242/3432 and `@objectstack/spec`
`--project local` 591 files / 17368 passed.
- `typecheck`: the 19 moving packages plus `client-react`,
`@objectstack/hono` and `lint`. Turbo ran 80 of 80 tasks.
- At `0f87e8488`, after the objectstack-ai#21160 merge and a full rebuild: the
`driver-turso` suite (82 files) gives 2218 passed / 33 skipped / 0
failed. That includes objectstack-ai#21160's new tests, run on 0.18.0; its two-process
concurrency test overlapped, with 400 writes and 400 distinct numbers.
`typecheck` passes, and `pnpm install --frozen-lockfile` passes.

## Acceptance notes

- **Duplicate copies left by the exclusions**: `zod` 4.6.1, `react`
19.2.8 and `yaml` 2.9.0 are kept only by the excluded `apps/docs` tree.
`@noble/ciphers` 2.3.0 is kept only by the excluded better-auth 1.7.3.
`mongodb` 7.5.0 is kept only by the test harness
`mongodb-memory-server-core` 11.3.0.
- **libsql 0.18.0's one behaviour change, from reading the code (not
measured):** the local client now pools connections, with one connection
for `:memory:` and for embedded replicas. On a replica, a second
`sync()` therefore waits for the first to release the connection. On
0.17.4 the two ran at once on the same native handle. With `timeout`
set, a sync that outlives the window keeps running natively,
uncancelled, so the next periodic sync queues behind it. No driver
docblock claims either behaviour.
- **The remaining 0.17.4 stamps stay**: outside `CHANGELOG.md`, only
`service-package` `index.ts:192` and `mysql2-tuple.test.ts:172` name
0.17.4 ("Measured on `@libsql/client` 0.17.4"). That is a dated
attestation and stays true; the `result.rows` shape was re-measured
identical on 0.18.0.
- **Environment side effect, nothing committed**: turbo 2.11.5 appends a
managed block to `AGENTS.md` whenever an agent runs a repository-scoped
turbo command. It was restored after each turbo run with `git restore
--source=HEAD --staged --worktree AGENTS.md`. No commit on this branch
touches `AGENTS.md`.
- **Merge-queue ejection and its fix**: objectstack-ai#21160 added driver-turso's
`tsx` at 4.23.12 while this branch removes `tsx@4.23.12`. The two
lockfiles merged into one that `pnpm install --frozen-lockfile` refuses.
Fixed by merging `main` at `e35c40a52`, lifting that `tsx` line, and
regenerating the lockfile with pnpm.

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

---------

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

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants