Repository navigation
driver-sqlite-wasm: a text value does not round-trip the way driver-sql's does — an embedded NUL truncates the stored value, and a leading BOM is stripped on read #19978
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions分诊首次定级:
priority:p2·bug·domain:engine·pm:queue—— sqlite-wasm 驱动写入带 NUL 的文本会被悄悄截断,读回带 BOM 的文本会被去掉 BOMPath:
packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts(sql.js 的绑定与读取路径)Triage: lands in the sql.js face of the SQLite driver family ⇒
domain:engine,bug,priority:p2,pm:queue; rationale: a published driver silently changes a stored text value (truncated at an embedded NUL on write, a leading U+FEFF stripped on read) where its siblingSqlDriveron better-sqlite3 round-trips it byte-for-byte — a silent write degradation with no error, on inputs that are rare, hence p2 rather than p1.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T14:26Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并核对了在途 PR 的文件范围。本席核对
- 卡面的数据来自 driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912 开发者的临时探针,填卡席位和本席都没有重跑。
- 在途 PR fix(driver-sql): the local json backfill lets the codec decide, so JSON nested past SQLite's depth limit is no longer quoted into a string #19972(driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912)改的是
driver-sql的 json 回填和driver-turso的远端回填,没有改driver-sqlite-wasm的任何文件。⇒ 没有同文件冲突,不阻塞。 - 顺带一点:PR fix(driver-sql): the local json backfill lets the codec decide, so JSON nested past SQLite's depth limit is no longer quoted into a string #19972 自己的注释已经写明"sql.js 会丢掉内嵌 NUL 和开头的 U+FEFF",并在回填里按安全方向处理(不改写这类单元格)。这与本卡描述的现象一致,但它只处理回填,没有修读写本身。
定级说明
- p2:值被悄悄改掉、没有任何报错,属于持久性退化(AGENTS.md)。但包含 NUL 或开头 BOM 的文本很少见,所以不定 p1。
- 不挂
security:不涉及权限或数据泄露。
执行要点
- 先测清楚 sql.js 在哪一步丢字节:是绑定 JS 字符串时,还是解码结果时。记录命令和输出。
- 然后二选一:原样往返,或者明确拒绝这类值并给出错误。⛔ 不要继续静默改值。
- 把结论钉成共享的一致性用例(
FILTER_TEXT/ 值往返一类),让下一个驱动也不能悄悄不同。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim: PM loop round 22
Session:session_01Bvd69VPa6puiNzzPUroDBx
Branch:claude/issue-19978-sqlite-wasm-text-roundtrip
Worktree:objectstack-issue-19978
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/drivers/driver-sqlite-wasm/src/**(sqlite-wasm-driver.ts,wasm-connection.ts,knex-wasm-dialect.ts) and its tests,.changeset/19978-*.md; plus, only if the answer is pinned as a shared conformance cell, the shared value-roundtrip / text case file underpackages/drivers/driver-sql/src/that the SQLite family's conformance suites read (stop on breach; explain in the report). ⛔ Notsql-driver.ts.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5816062518
Serial constraints cleared: at 2026-09-24T15:29Z, the census of all 32 open PRs finds none touchingpackages/drivers/driver-sqlite-wasm/, and nopm:dispatchedclaim declares it. PR #19972 (#19912, landed today as9bfbacbf8b) documents the same sql.js byte loss inside the json backfill and handles it in the safe direction. This card fixes the read/write path itself, and that backfill's CAS behaviour is not to be changed here.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19978,
"status": "done",
"branch": "claude/issue-19978-sqlite-wasm-text-roundtrip",
"pr": "#19998",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, the parent's harness-stamped id (every commit's Claude-Session trailer)",
"premise_still_valid": true,
"summary": "Premise re-measured and confirmed on sql.js 1.14.1 against better-sqlite3. The loss is at two seams. The WRITE is sql.js Statement.bindString, which calls sqlite3_bind_text with length -1, so SQLite stores up to the first U+0000 (610062 stored as 61). The READ is Statement.getString, whose UTF8ToString stops at NUL and decodes through a default TextDecoder that drops a leading U+FEFF. The fix is in this package's transport only; sql.js is not patched and driver-sql is not touched. knex-wasm-dialect.ts now reads each TEXT cell from Statement.getBlob and decodes it with ignoreBOM: true. It binds a string holding U+0000 as its UTF-8 bytes through +CAST(PARAM AS TEXT); the unary plus keeps the no-affinity semantics of a bound text, and the change is measured and pinned. Parameters are numbered by SQLite's own rule (?, ?NNN, :@#$name), so no statement form is refused and Clause-② stays no. SqliteWasmDriver now matches better-sqlite3 line for line: create/findOne, update, equality, $in, $contains and $startsWith on U+FEFF. The shared VALUE_ROUNDTRIP_CASES table was not edited: it lives in packages/spec, outside the claim surface, and Postgres cannot store U+0000. The answer is pinned in a new package test instead, and the shared-row question goes to the seat. Side effect for the seat: the pending .changeset/19912-json-backfill-depth-limit.md sentence saying SqliteWasmDriver withholds U+FEFF/U+0000 legacy json cells is false at this head (measured). Neither that changeset nor sql-driver.ts was edited.",
"tests": "At 58ee6c0 (final head), in the worktree:pnpm --filter @objectstack/driver-sqlite-wasm testgives 30 files, 556 passed.pnpm --filter @objectstack/driver-sqlite-wasm typecheckexits 0, andtsc --listFilesincludes both new files. The new file src/sqlite-wasm-text-bytes-roundtrip.test.ts has 35 passing cases: WRITE, hex(v) equals the UTF-8 of the written value (U+0000 mid/trailing, U+FEFF lead short/long and mid, plain control); READ, cells planted by cast(x'..' as text) read back exactly (610062, EFBBBF78, long BOM, plain); create/findOne; update, including the returned row; filters: equality per case, equality 'a' no longer matching the 'a'+U+0000+'b' row, $in, $contains and $startsWith U+FEFF; the no-affinity pin (select 5 lt ? with ' '+U+0000 answers 1); placement past quotes, [..], comments and c$d; ?NNN and :name through the driver; SQLite numbering on a mixed statement; a binding no parameter receives is left as it was; the read throws when getBlob is absent. Ablations, from the committed state via scripts/ablation-replace.mjs, each proven on disk (anchor 1 to 0, blob changed) and restored (blob == HEAD, git diff HEAD empty); the subject resolves from src by relative import, so no build was involved: A, read seam back to stmt.getAsObject(): 11/35 red, exactly the read, update, $contains-values and ?NNN/:name pins; every write-hex and equality pin green. B, write rewrite disabled: 10/35 red, the U+0000 hex/read/update/prefix-equality/placement pins; every read-seam pin green. C, unary plus removed: 3 red, the affinity pin (r: 0) and two literal-placement pins. D, ?NNN numbered as bare ?: 2 red. B's first attempt was refused by the tool (the replacement was a substring of its anchor, count 1 to 1), so nothing ran; it was rerun with a distinct replacement. Backfill control: dist rebuilt with seams A+B ablated; ablation-dist-preflight showed the marker present, then absent after restore, with a clean tree. The probe then gave legacy EFBBBF78 / 610062 left as stored, versus 22EFBBBF7822 / 22615C75303030306222 at head, identical to better-sqlite3. Driver probe before (a7581b3) and after, better-sqlite3 as control: sql.js went from stored 61, read 'a', and read 'hello' to identical to better-sqlite3 on every line.",
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through REST GETs, writes through scripts/pm relay tools)",
"api_writes": "2 — (1) POST /repos/objectstack-ai/objectstack/pulls (draft, pr_create) via scripts/pm/fleet-write/dispatch.mjs, relay run 36029638390, as objectstack-fleet[bot], giving PR #19998; (2) POST /repos//issues/19978/comments (this os-dev-report) via scripts/pm/post-stamped.mjs through the same relay. Transport for each: one POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204). git push is not a REST write. Labels: 0 writes; the dispatch named none, and skip-changeset does not apply because the diff publishes.",
"open_questions": [
{
"question": "H4 — should a leading-U+FEFF and/or an embedded-U+0000 text row join the shared VALUE_ROUNDTRIP_CASES table (packages/spec/src/data/value-roundtrip-conformance.ts), so every driver is held to it? Not done here: the file is outside the claim surface (the claim named a driver-sql location that holds no such file), and driver-sql's suite runs that table against live Postgres, whose text type cannot store U+0000 (Postgres docs; NOT MEASURED here, no live PG).",
"options": [
"A — add a leading-U+FEFF row only (plus a mid-string U+FEFF control); keep U+0000 out of the shared table and pinned per SQLite face; measure the PG/MySQL decode in CI on the PR that adds it",
"B — add both, with U+0000 carried as a measured Postgres exemption in the census",
"C — leave both pinned per driver (this PR's state)"
],
"recommendation": "A. Real need: a leading U+FEFF is what an imported CSV or text file's first cell carries, a real value class, while a U+0000 in text is rare and not portable (Postgres refuses it at the engine). Long-term: one standard for the portable class is what stopped this driver from diverging silently. AI-mistakes: a shared row makes the next driver's silent BOM strip fail its own conformance cell. Scope: one row in an existing table, no new gate. B adds a permanent exemption for a value no driver can be made to store."
},
{
"question": "The pending .changeset/19912-json-backfill-depth-limit.md says the backfill leaves as stored, 'on SqliteWasmDriver, a legacy text with a leading U+FEFF or an embedded NUL (sql.js drops both when it reads the text)'. SqlDriver.backfillCanonicalJsonEncoding's doc block in sql-driver.ts carries the same parenthetical, and that block reaches driver-sql's d.ts because the method is protected. Both are false at this head (measured: SqliteWasmDriver now converges those cells exactly as better-sqlite3). Neither has shipped yet. Per the dispatch, neither was edited.",
"options": [
"A — the seat amends the pending changeset before a release consumes it (drop the SqliteWasmDriver clause; keep 'on any engine, text holding invalid UTF-8'), and corrects the sql-driver.ts parenthetical in a small follow-up",
"B — leave both; if this PR ships in the same release, the CHANGELOG line ships false"
],
"recommendation": "A, because a changeset is the text an upgrading reader greps, and correcting it before release is one line; after release it needs a dedicated docs-only PR."
}
],
"out_of_scope_findings": [
"class: a · A $contains or $startsWith whose comparand holds U+0000 silently widens on BOTH SQLite faces (better-sqlite3 and sql.js): the SQLite text predicate is GLOB, and glob() reads its pattern as a C string, so the comparand matches only up to the U+0000, and one that starts with U+0000 matches every row. Evidence: the driver probe on base a7581b3 (and unchanged at this head), on a 5-row text table: $contains U+0000 returns all 5 rows, and $contains U+0000+'b' returns all 5 rows, on better-sqlite3 and sql.js alike. Landing site: driver-sql's SQLite text-match predicate (textMatchPredicate). driver-memory/mongodb match by JS string semantics. Filed by the seat, not fixed here · dedupe words:glob U+0000 comparand matches every row·$contains NUL sqlite widen·textMatchPredicate embedded NUL pattern·sqlite glob C string pattern",
"carrier: 承接者:无 · wasm-connection.ts defaultLocateFile() calls require.resolve('sql.js/package.json'), which throws ERR_PACKAGE_PATH_NOT_EXPORTED on sql.js 1.14.1 (its exports map has no ./package.json), so it always returns undefined and sql.js locates its own .wasm; behaviour unaffected, only the docblock claim is dead · noted in PR Acceptance notes, not filed"
],
"gates": "Derived at 58ee6c0 withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; the change set was derived from merge base 67ebc84): 59 commands, all 59 exit 0 at 58ee6c0.--ranwith exit codes: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, not counted as measurements) and answered 0 after turbo built ./packages/* (72 tasks, under the verify lock). check:query-options-erasure first reddened (test surface 236 to 242, this test'sas anyqueries) and was fixed by typing the queries as DriverQuery. check:driver-conformance:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exemptboth on base a7581b3 (before) and at 58ee6c0 (after).node scripts/check-issue-citations.mjs --base origin/main: exit 0. check:nul-bytes: OK, plus a manual control-byte grep of every edited file (0 hits). Lint, narrowed and proven: eslint --no-inline-config --format json over the 3 changed .ts files gives 3 files, 0 errors, 0 warnings, none ignored; eslint.config.mjs has 0project:/projectService (not type-aware), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. CI on PR #19998 at report time: 31 check-runs, 21 in_progress, 7 success, 3 skipped, 0 failed (in_progress; not waited on).",
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"The first commit carried the harness-supplied model-named Co-Authored-By trailer. The pre-push hook (check:commit-card-trailers) refused it before anything was published, and the unpublished commit was reworded to the model-free pair. No landed history was touched.",
"origin/main (67ebc84) was merged into the branch before opening the PR (AGENTS.md: pull main before the PR; dispatch-gates flagged STALE TREE). There is zero path overlap and the lockfile did not move. The closure was rebuilt and package tests and gates were re-run on the merged head.",
"H4 stop-on-breach: the claim's file surface allowed a shared case file under packages/drivers/driver-sql/src/, which does not exist; the real table is in packages/spec, so it was not edited (see open_questions).",
"Intermediate commits 6ed215c..88c69c0 carried a NOT_IMPLEMENTED/501 refusal for statements whose parameters could not be matched one-for-one. d64527e replaced it with SQLite's parameter numbering, so the head refuses nothing and Clause-② no holds at head.",
"A full ./packages/* turbo build (6m58s under the verify lock) was run beyond the minimal ① closure, only to meet the prerequisites of three derived gates."
],
"files_changed": [
"packages/drivers/driver-sqlite-wasm/src/sqljs-exact-text.ts (new)",
"packages/drivers/driver-sqlite-wasm/src/knex-wasm-dialect.ts",
"packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-text-bytes-roundtrip.test.ts (new)",
".changeset/19978-sqlite-wasm-text-roundtrip.md (new; @objectstack/driver-sqlite-wasm patch; Clause-②: no)"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim amendment: the file surface widens by two text-only edits for patch round 1 on PR #19998
domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T16:50Z. It amends claim 5817120201. The branch and the dev are unchanged.The dev measured at head
58ee6c04that this PR makes two pending sentences false. After the fix,SqliteWasmDriverreads a stored leading U+FEFF and an embedded U+0000 back verbatim, so PR #19972's backfill now converges those legacy cells exactly as better-sqlite3 does. Both sentences are unreleased. A change that makes a sentence false corrects it in the same PR:.changeset/19912-json-backfill-depth-limit.md(pending, landed with PR fix(driver-sql): the local json backfill lets the codec decide, so JSON nested past SQLite's depth limit is no longer quoted into a string #19972): the clause "onSqliteWasmDriver, a legacy text with a leading U+FEFF or an embedded NUL (sql.js drops both when it reads the text)". This is a DELIBERATE CORRECTION undercheck-empty-changeset.mjs.Check Changesetgoes red by design; the confirmation is the same-head at-tier PASS record (ruling 1A, Why three engine-lane landings needed the maintainer this round (a pending release-note correction, a first-time queue-flake signature, a subagent's denied label write): can each become seat-decidable? #19940).packages/drivers/driver-sql/src/sql-driver.ts: thebackfillCanonicalJsonEncodingdoc block's matching parenthetical ("sql.js drops an …"). This is a comment-only edit; ⛔ no code line insql-driver.tsmoves.
Serial constraints re-checked: no open PR touches
packages/drivers/driver-sql/src/sql-driver.tsor that changeset (census of all open PRs, this fire).
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19978,
"round": "patch round 1 (addendum to os-dev-report 5818364146)",
"status": "done",
"branch": "claude/issue-19978-sqlite-wasm-text-roundtrip",
"pr": "#19998",
"head": "6a195b37c6af9fcba265588530716f93157033bd",
"previous_head": "58ee6c042d0d39ad651eab494ea04735710d0a48",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, the parent's harness-stamped id",
"premise_still_valid": true,
"summary": "Q2 → A is carried out under claim amendment 5818397702, as one commit (6a195b3) on the existing branch, with no force-push. The pending .changeset/19912-json-backfill-depth-limit.md drops the SqliteWasmDriver clause, keeps 'on any engine, text holding invalid UTF-8', and now says which engines leave a cell unconverted and why, from a fresh three-face measurement at head. The matching parenthetical in SqlDriver.backfillCanonicalJsonEncoding's doc block is corrected, and only its two comment lines change: the emitted dist/index.js and dist/index.mjs differ from a pre-edit build in exactly those two comment lines, and the driver-sql suite counts are identical before and after. My own changeset gains one bullet saying the json backfill now converges those legacy cells on this driver too. Q1 → C: packages/spec is untouched and the per-driver pins stay. The PR body was not edited.",
"rewritten_sentences": [
{
"file": ".changeset/19912-json-backfill-depth-limit.md",
"old": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: onSqliteWasmDriver, a legacy text with a leading U+FEFF or an embedded NUL (sql.js drops both when it reads the text); on any engine, text holding invalid UTF-8.",
"new": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: on any engine, text holding invalid UTF-8.SqlDriveron better-sqlite3,SqliteWasmDriver(sql.js) andTursoDriverin local mode (libsql) were each measured to read such a cell back with U+FFFD in place of the invalid bytes, so the text the rewrite would be decided from is not the stored text, and the cell is left alone. A legacy text with a leading U+FEFF or an embedded NUL is read back verbatim on all three, and is rewritten like any other plain string."
},
{
"file": "packages/drivers/driver-sql/src/sql-driver.ts (backfillCanonicalJsonEncoding doc block; two comment lines, shown joined with ' / ')",
"old": "stored bytes the engine does not read back verbatim (sql.js drops an / embedded NUL and a leading U+FEFF; any engine replaces invalid UTF-8): its",
"new": "stored bytes the engine does not read back verbatim (any engine replaces / invalid UTF-8 with U+FFFD; measured on better-sqlite3, sql.js, libsql): its"
},
{
"file": ".changeset/19978-sqlite-wasm-text-roundtrip.md (one bullet added under 'What changes')",
"old": "(absent)",
"new": "The localField.jsonstorage backfill (SqlDriver.backfillCanonicalJsonEncoding) now converges a legacy json text cell holding a leading U+FEFF or an embedded U+0000 on this driver too: measured on the nextinitObjects, a storedEFBBBF78becomes22EFBBBF7822and a stored610062becomes22615C75303030306222, the same bytes better-sqlite3 writes, where before this change both were left as stored because sql.js did not read them back verbatim."
}
],
"measurement_behind_the_rewrite": "Fresh at head 58ee6c0 (the fix, before these text edits): a scratch probe on each local face planted legacy json TEXT cells by SQL literal, then ran the backfill with a second initObjects. SqlDriver on better-sqlite3, SqliteWasmDriver (sql.js) and TursoDriver local (libsql, url ':memory:') answered identically. Converted: EFBBBF78 to 22EFBBBF7822, 610062 to 22615C75303030306222, and the control 68656C6C6F to 2268656C6C6F22. Left as stored: invalid UTF-8 FF78 and 61C3, which each face read back before the backfill as U+FFFD+'x' and 'a'+U+FFFD. The text the rewrite is decided from is therefore not the stored text, and the compare-and-set matches nothing. So on the three faces measured, invalid UTF-8 is the only class the backfill leaves unconverted. SqlDriver's sqlite3/sqlite clients are NOT MEASURED: that client is not installed here.",
"tests": "At 6a195b3:pnpm --filter @objectstack/driver-sqlite-wasm testgives 30 files, 556 passed, and its typecheck exits 0.pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2gives 181 files passed and 11 skipped (192), 2680 tests passed and 170 skipped (2850). Control run of that suite with sql-driver.ts at its pre-edit bytes (HEAD~1's copy, trapped restore proven by blob b60f5b117 == HEAD and an empty git diff HEAD) gives the identical 181|11 files and 2680|170 tests.pnpm --filter @objectstack/driver-sql typecheckexits 0. Emitted-code proof: driver-sql built from the pre-edit and the head source;diffof dist/index.js and dist/index.mjs shows only the two rewritten comment lines (6521-6522 and 6454-6455).",
"gates": "Requested set at 6a195b3:node scripts/check-changeset-no-major.mjs --base origin/mainexit 0 ('✓ This diff introduces nomajorbump.').node scripts/check-adr-0087-registration.mjs --base origin/mainexit 0 ('✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen).').node scripts/check-empty-changeset.mjs --base origin/mainexit 1, by design (DELIBERATE CORRECTION class). Its lines: 'This PR changes a changeset it did not add: .changeset/19912-json-backfill-depth-limit.md present on the merge base and CHANGED by this PR -- this is somebody else's release note' and 'DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back.' It also printed '✓ No empty-frontmatter changeset introduced by this diff'. No skip-changeset label was applied and nothing was restored.node scripts/check-issue-citations.mjs --base origin/mainexit 2, NOT attributable to this diff. The tool takes a declared base verbatim, and origin/main had moved to e8f163f, whose #19988 removed three '#16608' citations from plugin-security/src/security-plugin.ts (5 at merge base 67ebc84, 2 at e8f163f). This tree still carries the merge base's copy, so the tool reads them as added by this change; this diff does not touch that file. The same gate with--base 67ebc84a7(the merge base) exits 0: '✅ check-issue-citations: no issue citations added against 67ebc84 (3 file(s) read)'.pnpm check:doc-authoringexit 0.pnpm check:driver-conformanceexit 0, 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt' (unchanged). Re-derived union: dispatch-gates at 6a195b3 gives 62 commands (the previous 59 plus check:dispatcher-error-vocabulary, check:object-def-param-keys and check:tenant-chokepoint, placed by sql-driver.ts). After a turbo build of ./packages/* (72 tasks) all 62 ran: 61 exit 0 and check-empty-changeset exit 1 as above.--ranwith exit codes: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. CI at 6a195b3 when I read it: 34 check-runs, 30 success, 3 skipped, 1 failure. The failure is Check Changeset, and its annotation is exactly the 19912 deliberate-correction refusal.",
"mcp_calls": "0 — no MCP GitHub tool called this round",
"api_writes": "1 this round (3 cumulative): POST /repos//issues/19978/comments (this addendum) via scripts/pm/post-stamped.mjs through the fleet-write relay (one POST .../dispatches, HTTP 204). git push is not a REST write. The PR body was not edited, and no label was written.",
"open_questions": [],
"out_of_scope_findings": [],
"deviations": [
"check-issue-citations --base origin/main exit 2 is a moving-ref artifact: origin/main advanced after this branch's merge. The merge-base reading is exit 0. The branch was not re-merged with origin/main this round (dispatch-gates also printed STALE TREE, 10 commits behind), because a merge would change the head the requested re-runs describe; the seat decides whether to update the branch.",
"Beyond the requested set, the full re-derived union (62) was re-run after a ./packages/* turbo build, because sql-driver.ts brought in three new families."
],
"line_budget": "n/a — no skills/** in the diff",
"files_changed": [
".changeset/19912-json-backfill-depth-limit.md (one sentence rewritten; DELIBERATE CORRECTION)",
"packages/drivers/driver-sql/src/sql-driver.ts (two comment lines; no code line)",
".changeset/19978-sqlite-wasm-text-roundtrip.md (one bullet added)"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19978,
"round": "patch round 2 (addendum to os-dev-report 5818364146 and 5818798597), answering contract review 5819169241 FAIL",
"status": "done",
"branch": "claude/issue-19978-sqlite-wasm-text-roundtrip",
"pr": "#19998",
"head": "98cb908738599a72538e5f32df4531dc357e61d2",
"previous_head": "6a195b37c6af9fcba265588530716f93157033bd",
"merge_base": "67ebc84a7fa55cf6cd42c10fe87f571d446620af",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, the parent's harness-stamped id",
"premise_still_valid": true,
"summary": "The review's finding is right, and my round-1 text was wrong. I have confirmed it two ways. In source, TursoDriver.toKnexConfig gives each non-remote arm client: 'better-sqlite3' (turso-driver.ts lines 1213, 1221, 1229 and 1244), and the docblock at line 766 says so. At runtime, new TursoDriver({ url: ':memory:' }) at this head reports knex.client.driverName = better-sqlite3. So round 1's third measured face ran on better-sqlite3, and its 'libsql' label was false; I never measured libsql. One commit (98cb908) on top of 6a195b3, with no force-push and no merge of origin/main, changes text only, in the same two files as round 1: S1 of the 19912 note is softened to the engines measured; S2 names better-sqlite3 behind both SqlDriver and TursoDriver local, and sql.js behind SqliteWasmDriver; S3 reads 'on all three faces'; and the sql-driver.ts parenthetical names only better-sqlite3 and sql.js. The 19978 changeset is unchanged: no sentence in it names libsql or 'any engine', and edits 1–3 make none of it false. The PR body was not edited.",
"rewritten_sentences": [
{
"file": ".changeset/19912-json-backfill-depth-limit.md, line 10: the merge base's one sentence, which round 1 split into S1–S3",
"merge_base": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: onSqliteWasmDriver, a legacy text with a leading U+FEFF or an embedded NUL (sql.js drops both when it reads the text); on any engine, text holding invalid UTF-8.",
"round_1_old": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: on any engine, text holding invalid UTF-8.SqlDriveron better-sqlite3,SqliteWasmDriver(sql.js) andTursoDriverin local mode (libsql) were each measured to read such a cell back with U+FFFD in place of the invalid bytes, so the text the rewrite would be decided from is not the stored text, and the cell is left alone. A legacy text with a leading U+FEFF or an embedded NUL is read back verbatim on all three, and is rewritten like any other plain string.",
"new": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: text holding invalid UTF-8, measured on better-sqlite3 and sql.js.SqlDriveron better-sqlite3,TursoDriverin local mode (which runs on better-sqlite3 too) andSqliteWasmDriver(sql.js) were each measured to read such a cell back with U+FFFD in place of the invalid bytes, so the text the rewrite would be decided from is not the stored text, and the cell is left alone. A legacy text with a leading U+FEFF or an embedded NUL is read back verbatim on all three faces, and is rewritten like any other plain string."
},
{
"file": ".changeset/19912-json-backfill-depth-limit.md, S1",
"round_1_old": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: on any engine, text holding invalid UTF-8.",
"new": "A cell the engine does not read back verbatim is left as stored and keeps reading as it did, where the old statement rewrote it: text holding invalid UTF-8, measured on better-sqlite3 and sql.js."
},
{
"file": ".changeset/19912-json-backfill-depth-limit.md, S2",
"round_1_old": "SqlDriveron better-sqlite3,SqliteWasmDriver(sql.js) andTursoDriverin local mode (libsql) were each measured to read such a cell back with U+FFFD in place of the invalid bytes, so the text the rewrite would be decided from is not the stored text, and the cell is left alone.",
"new": "SqlDriveron better-sqlite3,TursoDriverin local mode (which runs on better-sqlite3 too) andSqliteWasmDriver(sql.js) were each measured to read such a cell back with U+FFFD in place of the invalid bytes, so the text the rewrite would be decided from is not the stored text, and the cell is left alone."
},
{
"file": ".changeset/19912-json-backfill-depth-limit.md, S3",
"round_1_old": "A legacy text with a leading U+FEFF or an embedded NUL is read back verbatim on all three, and is rewritten like any other plain string.",
"new": "A legacy text with a leading U+FEFF or an embedded NUL is read back verbatim on all three faces, and is rewritten like any other plain string."
},
{
"file": "packages/drivers/driver-sql/src/sql-driver.ts, backfillCanonicalJsonEncoding doc block, two comment lines (11311–11312) joined with ' / '",
"merge_base": "* stored bytes the engine does not read back verbatim (sql.js drops an / * embedded NUL and a leading U+FEFF; any engine replaces invalid UTF-8): its",
"round_1_old": "* stored bytes the engine does not read back verbatim (any engine replaces / * invalid UTF-8 with U+FFFD; measured on better-sqlite3, sql.js, libsql): its",
"new": "* stored bytes the engine does not read back verbatim (better-sqlite3 and / * sql.js, the engines measured, read invalid UTF-8 back as U+FFFD): its"
}
],
"measurement_behind_the_text": "These are the round-1 probe readings with the engines named correctly; nothing was re-measured for the text itself. SqlDriver on better-sqlite3, TursoDriver local (better-sqlite3, confirmed above) and SqliteWasmDriver (sql.js) each read the invalid-UTF-8 cells FF78 and 61C3 back as U+FFFD+'x' and 'a'+U+FFFD, and the backfill left them as stored. All three read EFBBBF78 and 610062 back verbatim, and the backfill rewrote them to 22EFBBBF7822 and 22615C75303030306222. Not measured by me: libsql (the reviewer measured @libsql/client 0.17.4 directly: FF78 panics the native binding, and 610062 reads back as 'a'), and SqlDriver's sqlite3/sqlite client, which is not installed here. The softened S1 therefore claims only better-sqlite3 and sql.js.",
"tests": "No code token moved, so the package suites were not re-run, as the patch order allows. Comment-only proof: driver-sql was built from sql-driver.ts at 6a195b3 and at the head (trapped restore, blob 0b2caa5d3 == HEAD, git diff HEAD empty).diffof dist/index.js and dist/index.mjs shows only the two rewritten comment lines (6521–6522 and 6454–6455). The same proof against the merge base 67ebc84 also shows only those two comment lines, so across the whole PR sql-driver.ts emits no code difference.git diff -U0 67ebc84a7 -- sql-driver.tshas 0 changed lines outside a ' * ' comment line. The pending changesets name libsql 0 times and 'any engine' 0 times (git grep).",
"gates": "At 98cb908:node scripts/check-changeset-no-major.mjs --base origin/mainexit 0: '✓ This diff introduces nomajorbump.'node scripts/check-adr-0087-registration.mjs --base origin/mainexit 0: '✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen).'node scripts/check-empty-changeset.mjs --base origin/mainexit 1, as expected (DELIBERATE CORRECTION class). Its lines: 'Diffing HEAD from 67ebc84 (merge base with origin/main).' / '✓ No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added).' / 'This PR changes a changeset it did not add: .changeset/19912-json-backfill-depth-limit.md present on the merge base and CHANGED by this PR -- this is somebody else's release note' / 'DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back.' It emits one ::error annotation, on .changeset/19912-json-backfill-depth-limit.md. No skip-changeset label was applied and nothing was restored.node scripts/check-issue-citations.mjs --base 67ebc84a7exit 0: '✅ check-issue-citations: no issue citations added against 67ebc84 (3 file(s) read).'pnpm check:doc-authoringexit 0.pnpm --filter @objectstack/driver-sql typecheckexit 0.pnpm check:nul-bytesexit 0. Re-derived with dispatch-gates at 98cb908: 62 families, identical to round 1's 62, so no new family to run. It printed STALE TREE (10 commits behind origin/main); per the order, origin/main was not merged.",
"corrections_to_my_earlier_reports": [
"Round 1 (5818798597), measurement_behind_the_rewrite: 'TursoDriver local (libsql, ...)' is false. That face runs better-sqlite3, so the three readings covered two engines, not three.",
"Round 0 (5818364146), out_of_scope_findings[0]: 'a $contains or $startsWith ... one that starts with U+0000 matches every row' overstated $startsWith. Re-measured at this head on better-sqlite3 and sql.js with rows 'a'+U+0000+'b', 'ab'+U+0000, U+0000+'z', 'plain' and '': $contains U+0000 and $endsWith U+0000 return all 5 rows; $contains U+0000+'b' returns all 5; $contains 'a'+U+0000 returns only 'a'+U+0000+'b'; $startsWith U+0000 returns only '' and U+0000+'z'; $startsWith 'a'+U+0000 returns only 'a'+U+0000+'b'. Both engines answer identically. The mechanism holds, and it is sharper than I first wrote: glob() cuts both the pattern and the value at their first U+0000, so a comparand beginning with U+0000 widens $contains and $endsWith to every row, and narrows $startsWith to the rows that are empty before their first U+0000. The finding stays class (a), for the seat to file, with the same dedupe words."
],
"mcp_calls": "0 — no MCP GitHub tool called this round",
"api_writes": "1 this round (4 cumulative): POST /repos//issues/19978/comments (this addendum) via scripts/pm/post-stamped.mjs through the fleet-write relay (one POST .../dispatches, HTTP 204). git push adds one commit (98cb908). The PR body was not edited, and no label was written.",
"open_questions": [],
"out_of_scope_findings": [],
"deviations": [
"I ran check:nul-bytes and two local probes beyond the requested list. The probes were a runtime check of TursoDriver local's Knex driverName, and a $startsWith/$contains/$endsWith re-measurement to correct my earlier finding. Neither wrote to GitHub or to the tree."
],
"line_budget": "n/a — no skills/** in the diff",
"files_changed": [
".changeset/19912-json-backfill-depth-limit.md (S1–S3 rewritten; DELIBERATE CORRECTION)",
"packages/drivers/driver-sql/src/sql-driver.ts (the same two comment lines; no code token)"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsACCEPT: PR #19998 at
98cb9087domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T18:12Z. Reviewed againstreferences/review-checklist.mdon GitHub, not from the dev's reports:os-dev-report5818364146, plus patch-round addenda 5818798597 and round 2, on this card.- Shape: the first line is
Fixes #19978, and the body declaresClause-②: no, matching the 19978 changeset (driver-sqlite-wasm: patch).Fixes #19978is the only closing keyword. driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912, fix(driver-sql): the local json backfill lets the codec decide, so JSON nested past SQLite's depth limit is no longer quoted into a string #19972 and driver-sql (SQLite faces): a$contains/$startsWith/$endsWithcomparand holding U+0000 is cut at the NUL byglob(), so the filter answers wrongly; one that starts with U+0000 makes$contains/$endsWithmatch every row #19999 are cited without one. - Scope: 6 files, 576 changed lines, not governed (
check-governed-merges --pr 19998). None of them is anos-regenpath.packages/drivers/driver-sqlite-wasm/src: the fix inknex-wasm-dialect.ts, the newsqljs-exact-text.ts, and the new test file;- the 19978 changeset;
- the two text-only edits added under claim amendment 5818397702: the pending 19912 note, and two comment lines in
sql-driver.ts.
- Contract review of record:
- FAIL at
6a195b37(5819169241): the round-1 text labelledTursoDriver's local mode "libsql", but every non-remote arm hands Knexbetter-sqlite3. - After one text-only patch round, PASS at
98cb9087(5819534172). Code is byte-identical across the two heads.
- FAIL at
- DELIBERATE CORRECTION under ruling 1A (Why three engine-lane landings needed the maintainer this round (a pending release-note correction, a first-time queue-flake signature, a subagent's denied label write): can each become seat-decidable? #19940, 5814546887):
.changeset/19912-json-backfill-depth-limit.mdline 10 is rewritten. The PASS record names the note and judges S1–S5 TRUE against this head. The three red-by-design conditions are on the PR (5819552741). - CI at this head: every required context is
success.- The only failures are three
Check Changesetruns, each annotated on the 19912 note in the DELIBERATE CORRECTION class. - The skips are
Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in), plus duplicate event-runAuto Label/Check PR Size. All are on the roster.
- The only failures are three
- Docs drift: none. No page in
content/docs/**orskills/**makes a NUL or BOM claim: zero hits forU+0000|U+FEFF|byte-order mark|embedded NUL|NUL byte, with the controlSqliteWasmDrivergiving 6 hits on the flagged pages. - Q1, the shared
VALUE_ROUNDTRIP_CASESrow, answered C:packages/specis untouched, and the answers are pinned per driver in this package.- Whether U+0000 belongs in the shared cross-dialect table is a platform question, not a driver fix: Postgres
textcannot store it, and that is unmeasured here. - It is noted for triage, not filed, because no acting reader can be named yet.
- Whether U+0000 belongs in the shared cross-dialect table is a platform question, not a driver fix: Postgres
- Out-of-scope findings:
- filed driver-sql (SQLite faces): a
$contains/$startsWith/$endsWithcomparand holding U+0000 is cut at the NUL byglob(), so the filter answers wrongly; one that starts with U+0000 makes$contains/$endsWithmatch every row #19999: the SQLiteglob()NUL cut in$contains/$startsWith/$endsWith. Its title and body were refined from this PR's measurements (5819292676, and a body line corrected after the PASS review); - noted, not filed (no acting reader):
WasmSqliteConnection.defaultLocateFile()'srequire.resolve('sql.js/package.json')always throws on sql.js 1.14.1. Behaviour is unaffected, and only the docblock is dead; - noted, not filed (no shipped local path):
@libsql/clientread directly panics on a stored invalid-UTF-8 cell. No local or replica face ofTursoDriverreads through it; - optional, not taken: the missing-
getBlobrefusal fires at the first TEXT read rather than at connect.
- filed driver-sql (SQLite faces): a
Landing:
ready+ auto-merge through the queue. The branch is behindmain, but no path in this diff moved onmainsince the merge base, and the queue builds the merged result.- Shape: the first line is
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsLanded: PR #19998, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T18:36Z.- Verified on main:
- PR fix(driver-sqlite-wasm): a text value round-trips byte-for-byte — U+0000 no longer truncates it, a leading U+FEFF is no longer dropped on read #19998 merged through the merge queue as
fc6ddb87a4, which has one parent and is an ancestor oforigin/mainafter a fresh fetch. readExactRowhas 2 hits inknex-wasm-dialect.tsat that commit and 0 at its parent.- The corrected sentence of
.changeset/19912-json-backfill-depth-limit.md(the DELIBERATE CORRECTION under ruling 1A) is present at that commit, and.changeset/19978-sqlite-wasm-text-roundtrip.mdis onmain.
- PR fix(driver-sqlite-wasm): a text value round-trips byte-for-byte — U+0000 no longer truncates it, a leading U+FEFF is no longer dropped on read #19998 merged through the merge queue as
- This card: closed
completedviaFixes #19978.pm:dispatchedwas removed in the same act. - No lane card closed by mistake: driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912 was already closed by PR fix(driver-sql): the local json backfill lets the codec decide, so JSON nested past SQLite's depth limit is no longer quoted into a string #19972. driver-sql (SQLite faces): a
$contains/$startsWith/$endsWithcomparand holding U+0000 is cut at the NUL byglob(), so the filter answers wrongly; one that starts with U+0000 makes$contains/$endsWithmatch every row #19999 is cited without a closing keyword and is still open; triage has since graded itpriority:p2·pm:queuein this lane. - Serial queue:
packages/drivers/driver-sqlite-wasm/src/**is released. Thedriver-sqlbackfillCanonicalJsonEncodingdoc block is released too, and so is the 19912 note. - For driver-sql (SQLite faces): a
$contains/$startsWith/$endsWithcomparand holding U+0000 is cut at the NUL byglob(), so the filter answers wrongly; one that starts with U+0000 makes$contains/$endsWithmatch every row #19999's taker: the text-match compile it names lives indriver-sql'stextMatchPredicate. This PR's write path binds a NUL-bearing string through+CAST(… AS TEXT)on the sql.js face only, which answers the same as better-sqlite3 (measured by the second contract review, 5819534172).
Generated by Claude Code
- Verified on main:
- added a commit that references this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site:
SqliteWasmDriver(packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts) and its sql.js bind/read path, whichSqlDriveron better-sqlite3 does not share. Finding class (a).Filed by the
domain:engineexecution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) from the out-of-scope findings of its #19912 dev (report comment 5815478298 on #19912). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.What happens
The dev's scratch probe ran
createthenfindOneon a plaintextfield, with driver dists built at PR #19972's head74ff944. The seat did not re-run it.'a'+ U+0000 +'b'610062→'a\u0000b'61(truncated) →'a''hello'EFBBBF68656C6C6F→ verbatimEFBBBF68656C6C6F(intact) →'hello'(BOM stripped)⇒ The sql.js face silently truncates a text value at an embedded NUL on write, and strips a leading BOM on read. Neither raises an error.
Why it matters
SqlDriverfamily reads back differently.Suggested shape (⛔ not a ruling)
Measure first where sql.js loses each byte (the bind of a JS string, or the result decode). Then either round-trip them exactly or refuse the value loudly, and pin the answer as a shared conformance cell (
FILTER_TEXT/VALUE_ROUNDTRIPfamily), so the next face cannot differ silently.Filing-gate answers
packages/drivers/driver-sqlite-wasmafter triage routes it.closedincluded:sqlite-wasm sql.js embedded NUL truncated BOM stripped text round-trip→ 1 hit: driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912 (the parent, unrelated defect).SqliteWasmDriver NUL byte text field stored truncated→ 6 hits: driver-sql: the local #12380 json backfill json_quotes JSON nested past SQLite depth limit, so a correctly stored deep array becomes a string on the next syncSchema #19912, [finding] Both migration generators captextfields at VARCHAR(255) while driver-sql creates an unbounded text column — a 300-char value the platform accepts is refused by every generated migration #16091, driver-sql: the varchar differ's expected width disagrees with whatcreateColumnwould emit — a boot-refusingnarrow_varcharon a bounded text field, and avarchar(n)past the dialect ceiling #12732, driver-sql: the string family ignoresmaxLengthtoo — a declared 1024 becomes varchar(255) and refuses legitimate writes #11431, driver-sql:richtextis emitted as varchar(255) while itsmarkdown/htmlsiblings get TEXT — a rich-text body is capped at 255 characters #11794, drivers(sql family): 文本算子的大小写折叠是「方言的」而非「契约的」——$contains在 SQLite 过折叠、$icontains在 PG/MySQL 过折叠 #6518 (all about varchar width or case folding). None is this defect.Dedupe words:
sqlite-wasm embedded NUL truncated·sql.js BOM stripped on read·SqliteWasmDriver text round-trip U+FEFF·wasm driver NUL byte text fieldGenerated by Claude Code