Skip to content

Converging driver-turso's two transports onto one physical column type for Field.json — and the measured reason VALUE_ROUNDTRIP cannot adjudicate it #12738

Description

@claude

The measurement

While pinning the declared Field.json column-type asymmetry for #12586 (local json, remote TEXT), the pin's ablation performed a convergence — RemoteTransport.mapFieldTypeToSQL's JSON arm changed from TEXT to json, so both transports declare the same physical column — and ran the named instrument against it.

Result, measured on driver-turso at commit db8db7c7:

turso-value-roundtrip-conformance.test.ts   88 passed (88)   <- UNCHANGED under convergence
turso-json-column-type-asymmetry.test.ts     4 failed  2 passed

⇒ The VALUE_ROUNDTRIP case-set cannot distinguish a converged driver from today's driver. Every value in the shared table round-trips faithfully either way. The only thing that moved was the new asymmetry pin, which is measuring the declaration and the storage class rather than the round trip.

Why that is a reason to card it, not a reason to do it

The charter on #12586 deferred convergence because the question "why did remote choose TEXT?" is un-measured. This result does not answer that question, and it should not be read as "convergence is safe":

  • Converging in the direction the ablation took (remote emits json) gives the remote transport NUMERIC affinity it does not have today. Measured: with the mutation in place, a bare '0123' written into the remote column by raw SQL comes back as the integer 123; today it comes back as the string '0123'. That is the remote half acquiring the exact exposure [finding] driver-sql's SQLite json codec is not round-trip faithful for a string whose content is valid JSON — '123' reads back as a number, and Postgres does not agree #12380 had to defeat on the local half — plausibly the wrong direction.
  • Converging the other way (local emits TEXT) is not exercised by anything here and would change what knex declares for every SqlDriver dialect, not just SQLite.
  • Either direction changes what new columns are physically declared as on existing deployments, which is a migration question no test in the tree asks.

So the useful content of this card is the negative result: whoever takes convergence on must bring an instrument other than VALUE_ROUNDTRIP, because that one is green on both sides of the decision.

Why a separate card

Disposition 1 was recorded in the triage comment on #12586, and that card is being closed by the PR that lands the declaration and the pin. Without a card of its own the convergence option has no home once #12586 closes. #12586 is not addressed by this card and this card does not supersede it; #11535 tracks the class and is already closed.

Where the pin is

packages/drivers/driver-turso/src/turso-json-column-type-asymmetry.test.ts. Its header carries the instruction this card is the other half of: when convergence is genuinely taken on, delete or invert that pin as part of the change — never edit its expectations to match new output.

Filed unassigned by the os-dev seat that implemented #12586, recording only — not claimed.


Generated by Claude Code


Generated by Claude Code

Activity

  1. huangyiirene commented on Aug 27, 2026

    @huangyiirene
    Collaborator

    Triage: healing a half-annotated card — domain:engine was already right (packages/drivers/driver-turso) and stays; adding the pm-state and type it was missing. Type Task: convergence adds no capability and widens no accept set, and it is not a Bug either — after #12586 landed, the asymmetry is declared and pinned as intended, so nothing is currently violating a contract.

    needs-user-decision, and this one is a literal match for the criterion rather than a judgement call: the card states that either convergence direction "changes what new columns are physically declared as on existing deployments, which is a migration question no test in the tree asks." 存量数据/迁移形状 is on the manual floor by name. ⛔ Not proxy-adjudicable at this seat, whatever the prisms below say — the recommendation is an input, never a release.

    ⚠️ Note for the reader: #12586 closed at 2026-08-27T16:36:00Z, during this triage round. That is the event this card was filed to survive — the card exists precisely so the convergence option keeps a home once #12586 closes — so the timing confirms the filing decision rather than undermining it. The pin it points at is now landed tree state, not pending PR state.

    <!-- os-decision-facets -->

    一句话问题:同一个声明为 JSON 的字段,在两种连接方式下被存成了两种不同的物理列类型。今天没有任何客户因此丢数据或看到差异,但「两边长得不一样」这件事本身,直到 #12586 之前没有任何东西解释过。

    选项 × 真实代价

    做什么 真实代价
    A 现在不收敛,保留 #12586 落地的声明与 pin(推荐) 两条通道对同一个声明字段永远物理不同;但已被 pin 记录成有意为之,下一个读到的人不会再当缺陷重报
    B 收敛到 remote 也用 json 实测:裸 '0123' 用原生 SQL 写进 remote 列,收敛后读回变成整数 123,今天读回仍是字符串 '0123' —— 客户可感知的后果是一个带前导零的编号或条码字段静默变成数字。这正是 #12380 在 local 半边费力挡掉的暴露面,B 等于把它请回 remote 半边
    C 收敛到 local 也用 TEXT 改变 knex 对每一个 SqlDriver dialect 的声明,不只是 SQLite;本卡的实测完全没有覆盖这个方向

    B 与 C 共有一项代价:改变已有部署上新建列的物理声明,而树里没有任何测试问过这个迁移形状。

    四棱

    • ① 项目长远合理性:A 保留一个已记录的特例 —— 今天的分裂有 pin 也有墓碑式说明,不是无主状态;B/C 消除分裂,但都没有测量支撑,且 C 的作用域远大于本卡自己的题目。
    • ② 实际业务拉动:零,而且是实测出来的零 —— 88 个值的共享用例集在收敛前后逐项相同(turso-value-roundtrip-conformance.test.ts 88 passed,收敛后不变)。没有客户撞上,没有报告。零拉动 ⇒ 默认 defer。
    • ③ 防 AI 犯错:A 最安全。pin 的头部已经写明「真要收敛时删掉或反转这个 pin,永远不要把它的期望改成新输出」—— 这句话防的正是 AI 用「改测试让它变绿」收场。反过来 B 制造的是静默类型转换(前导零变数字),属于最难被发现的一类错。
    • ④ 创业阶段不扩散:B/C 都是无拉动的改造,而且各自拖着一个没人问过的迁移问题。

    推荐:A(现在不收敛)。回退:若出现任何一例可复现、且可直接归因于该不对称的客户可见缺陷,按 B 或 C 重开 —— 但必须带 VALUE_ROUNDTRIP 以外的仪器。这正是本卡唯一的、也是真正有价值的产出:那套用例集在决定的两侧都全绿,⛔ 它无法裁决方向,拿它当证据是错的。

    置信缺口(本分析看不见什么):看不见 remote 当初为什么选 TEXT。#12586 的 charter 推迟收敛就是因为这个问题未经测量,本卡同样没有回答它 —— 若那是一个有意的、记录在别处的决定,B 可能直接出局;若只是疏忽,B 与 C 的取舍要重算。

    裁后我会怎么执行(你不用管):裁 A ⇒ 翻 pm:on-hold,Restart-touch: 锚在 packages/drivers/driver-turso/src/turso-json-column-type-asymmetry.test.ts,任何人删除或反转该 pin 即唤醒本卡;裁 B 或 C ⇒ 翻 pm:queue 派 engine 席,派发令强制两件事 —— 非 VALUE_ROUNDTRIP 的仪器,以及一份迁移形状说明。


    Generated by Claude Code

  2. zhuangjianguo commented on Aug 30, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop round R1 (second wave — a slot freed when #13178 delivered)
    Session: session_01F3jdziLbAPGeceVNmSox5L
    Branch: claude/issue-12738-sqlite-json-columns-declare-text
    Worktree: objectstack-issue-12738
    Domain: domain:engine
    File surface: packages/drivers/driver-sql/src/sql-driver.ts (the JSON type-mapping region ONLY — measured at :11096 t.json(c.name), :15104 table.json(name), :15391 the JSON_COLUMN_TYPES arm), packages/drivers/driver-turso/src/ (remote-transport.ts mapFieldTypeToSQL at :2062, and the turso-json-column-type-asymmetry.test.ts pin) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — matches the ruling comment's own dispatch guidance (「Dispatch guidance: M / opus」). dispatch-gates.mjs --tier run this round: "no path-derived mandate … the tier stays the PM's per-card judgment call".
    Clause-②: no — no packages/spec/** edit, and the accept set is unchanged: this alters what DDL a driver emits for a new column, not what metadata the platform accepts.
    Serial constraints cleared: sql-driver.ts is this lane's hot file with no CI gate behind its serial queue, and #13324 is in flight against it — so I measured instead of assuming. #13324's two occurrences of isMissingTableError in this file, :807 and :6398, are both COMMENTS; it has no call site here. Your regions (:11096 / :15104 / :15391) are disjoint from both by thousands of lines in a 16,696-line file. ⇒ Parallel is permitted under this lane's measured-disjoint-regions rule. ⛔ If your change needs to leave that mapping region, stop and report — the lock assumption is then void.

    ⚠️ Correcting my own earlier reading: when I dispatched #13324 I listed sql-driver.ts in its caller surface from a grep for mentions, and flagged to that dev that I had measured mentions rather than true call sites. Now measured: comments only.

    ⚖️ RULED — implement C′, ⛔ neither of the card's own two directions

    Maintainer, 2026-08-28, live director session (summon #2, batch #3 item 3), recorded at 5448850889. Direction 「从长远看统一是最好的吧」 plus 「其他接受你的最新建议。」 ⇒ C′: each dialect declares the semantically correct JSON column type.

    Long-term rationale on record: turso's embedded-replica model means the physical schema is decided by whichever side ran DDL, so differing mappings make a deployment's physical schema depend on migration provenance.

    Zone 2 — PM mechanism assumptions. MEASURE THESE; falsifying them is a good run.

    • ⚠️ The ruling flags its own unmeasured premise, and I am passing it through undiluted: 「The replica DDL-sync detail is reasoned from the product model, not measured; the dev verifies before relying on it.」 ⛔ Do not rely on it until you have.
    • Fence 1 of the ruling is pre-work verification, not a formality: read knex's actual DDL output for t.json() under the sqlite dialect, and the shared mapping's structure. The C′ scope claim (that turso local and sibling SQLite dialects share the arm) rests on it — read, ⛔ not assumed. I measured the three call sites above; I did not measure which dialects reach each one. That is yours.
    • I assume this card is still live. It was ruled 2026-08-28 and sat unclaimed since — ⚠️ this seat wrongly reported it as "awaiting maintainer" for a whole round because a stale status line was inherited instead of the card being read. If anything has landed since that already converges these transports, premise_still_valid: false is the right answer.

    ⛔ Fences from the ruling, carried verbatim in substance

    1. Instrument: ⛔ NOT VALUE_ROUNDTRIP. This card's own negative result is that the case-set is green on both sides of the decision — 88 passed, unchanged under convergence — so it cannot adjudicate direction and using it as evidence is an error. Build affinity-level pins instead: typeof(col) storage-class assertions, and a raw-SQL '0123' that must come back as text, run on BOTH transports.
    2. The [finding] driver-turso puts one declared Field.json in two different physical column types — json on the local transport, TEXT on the remote one — and nothing records that as intended #12586 asymmetry pin (turso-json-column-type-asymmetry.test.ts) is inverted or deleted per its own header — that header says so in as many words. ⛔ Never edit its expectations to match new output.
    3. Changeset (MINOR) must state the migration shape: new columns only; existing columns keep their declared type; platform write-path behaviour unchanged ([finding] driver-sql's SQLite json codec is not round-trip faithful for a string whose content is valid JSON — '123' reads back as a number, and Postgres does not agree #12380's pins stay green and stay in force for legacy columns); the visible difference is raw-SQL-only and in the safe direction.
    4. ⛔ Do not touch packages/spec/**, docs/adr/**, .claude/**, skills/**, content/docs/releases/**. No skipping/disabling/quarantining tests; no expectation re-baselining — invert in place with the reason. PR stays draft.

    ⚠️ Derived gate family is not the whole farm — Lint & Repo Gates runs gates the derivation never names (#13333). Driver changes also owe check:driver-conformance. Re-derive against your actual diff: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack <changed paths>.

    Required of you

    1. Leave your OWN claim comment here with your own session ID and branch. ⛔ Do not touch the assignee field.
    2. Final report as an issue comment here first, then as your return message, with os-dev-report as literal first-line text (⛔ the sanitizer eats HTML-comment markers).
    3. Machine-checkable fields only: gates with exit codes, files changed, deviations, premise_still_valid.

    Generated by Claude Code

  3. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    Claim (os-dev seat, dispatched for this card):

    Session: session_01F3jdziLbAPGeceVNmSox5L
    Branch: claude/issue-12738-sqlite-json-columns-declare-text
    Worktree: objectstack-issue-12738
    Base: d23dc08a0f83f7121f69a3622a01d2a39df4dfbb (origin/main at claim time)

    Implementing C' per the ruling at comment 5448850889 (maintainer, 2026-08-28): SQLite-family JSON columns declare TEXT; PG/MySQL native JSON untouched. Pre-work fence 1 (knex's actual sqlite DDL output for t.json() and the shared mapping's structure) is being measured before any edit, not assumed.

    Edits are confined to the JSON type-mapping region of packages/drivers/driver-sql/src/sql-driver.ts plus packages/drivers/driver-turso/src/; I stop and report if the change needs to leave that region.

    Generated by Claude Code


    Generated by Claude Code

  4. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    Status brief for the PM probe (alive; not blocked). Full report follows shortly.

    1. Where I am: past pre-work and implementation, at the end of gates, about to open the draft PR. Branch had not moved because the last ~25 min were build/gate runs, not commits.
    2. Fence 1 is done and it holds. Read in knex source and then MEASURED by compiling DDL: base ColumnCompiler.prototype.json = 'text', and the sqlite3 dialect is the one that overrides it to json. Compiled: better-sqlite3/sqlite3 -> json, pg -> json, mysql2 -> json; t.text() under sqlite -> text. Which dialects share the arm: sites :15104/:15391 are shared by ALL dialects, so the discriminator is this.isSqlite — which covers turso local/replica/remote (all configure better-sqlite3) and SqliteWasmDriver, which overrides isSqlite to true precisely because it passes a Client constructor. An inline config.client string test would have missed sqlite-wasm.
    3. Instrument: not VALUE_ROUNDTRIP. Affinity-level pins on both transports, plus a negative control.
    4. One deviation from the dispatch, measured: of your three sites I changed two. Site :11096 (buildRebuiltColumn) maps an introspected existing column type, so routing it through the new emitter would silently convert legacy json columns during unrelated drift rebuilds — contradicting the ruling own migration shape. Left as-is with the reason recorded in code. Details in the report.
    5. Nothing contradicts the ruling. The replica DDL-sync premise remains unmeasured — see report; the fix does not rely on it.

    Generated by Claude Code


    Generated by Claude Code

  5. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 12738,
      "status": "done",
      "branch": "claude/issue-12738-sqlite-json-columns-declare-text",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13442",
      "premise_still_valid": true,
      "summary": "Implemented C'. SqlDriver.createColumn's two JSON routes now go through one new seam, SqlDriver.jsonColumn, which emits table.text() on the SQLite family and table.json() elsewhere; PG/MySQL native JSON untouched, RemoteTransport.mapFieldTypeToSQL unchanged (it already said TEXT). DEVIATION, measured: of the three sites you named I changed TWO. sql-driver.ts:11096 (buildRebuiltColumn) maps an INTROSPECTED EXISTING column type for a SQLite table rebuild, not field metadata; routing it through the new emitter would re-declare a legacy `json` column as `text` during an UNRELATED drift rebuild and, because the rebuild's insert-select copies rows through the new affinity, rewrite stored bytes of live data - contradicting the ruling's own migration shape. Left unchanged with the reason recorded in code. Edits never left the JSON type-mapping region, so the disjointness assumption vs #13324 holds. ZONE 2: Fence 1 verified and it holds - knex base is ColumnCompiler.prototype.json = 'text' and the sqlite3 dialect is the one OVERRIDING it to 'json'; compiled DDL confirms better-sqlite3/sqlite3/pg/mysql2 all emit `json` for t.json(), and t.text() under sqlite emits `text`. The two createColumn sites are shared by ALL dialects, so the discriminator is this.isSqlite - which covers turso local/replica/remote (all configure better-sqlite3) AND SqliteWasmDriver, which overrides isSqlite to true because it passes a knex Client CONSTRUCTOR; an inline config.client string test would have silently missed that sibling SQLite dialect. The ruling's flagged premise (replica DDL-sync) is STILL UNMEASURED and nothing here relies on it: exercising embedded-replica sync needs a live Turso primary this container lacks, and the libsql stub is a local better-sqlite3 double with no sync. C' stands on the measured affinity trap alone, and the change makes the provenance question moot anyway - all three turso modes now declare TEXT. Card premise still live: nothing had landed since the 2026-08-28 ruling that converges these transports.",
      "tests": "UNION RUN AT COMMIT 8058e2b606 (final head; all gates below re-run on that same head after the last commit). [1] pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso --filter @objectstack/driver-sqlite-wasm test -> os-verify-lock VERDICT command-exit 0. driver-sql 'Tests 2248 passed | 132 skipped (2380)', 'Test Files 147 passed | 9 skipped (156)'; driver-turso 'Tests 1108 passed (1108)', 'Test Files 41 passed (41)'; driver-sqlite-wasm 'Tests 446 passed (446)', 'Test Files 26 passed (26)'. The 132+9 skips are the live PG/MySQL DIALECT_CELLS, unavailable in this container and skipped identically before the change. [2] typecheck for the same three packages -> VERDICT command-exit 0, three 'typecheck: Done' lines. Confirmed NON-VACUOUS with `tsc --noEmit --listFiles`: 2 hits for the two edited driver-sql test files, 1 for the edited driver-turso test file, so typecheck really compiles the edits. [3] pnpm lint (eslint . --no-inline-config, WHOLE REPO, not narrowed) -> exit 0. No narrowing claim is being made; the repo-wide scan was run. [4] 29 derived gate families, exit code captured BEFORE any pipe (cmd > file 2>&1; e=$?): 27 green in the batch runner plus check:type-check-debt and pnpm lint. Zero failures. Includes the dispatch-named check:driver-conformance (exit 0), plus check:nul-bytes, check:test-source-alias, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:query-options-erasure, check:doc-authoring, check:type-check-coverage, and the changeset family. [5] check:type-check-debt: FIRST ATTEMPT REFUSED - 'Error: --re-measure cannot run: 22 workspace dependencies of the ledgered packages have no built type entry point on disk'. Read as NOT MEASURED, not as green and not as red. Built the full closure (turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2, 70/70 tasks successful) and re-ran: its own verdict line reads 'check-type-check-coverage --re-measure: OK - 29 ledger entries re-measured in 206.8s, 1547 raw tsc errors total, none above its recorded number.' [6] Gate derivation: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` with NO paths passed (script derives its own change set from merge-base). First run warned STALE TREE (4 commits behind origin/main, scripts/bump-objectui.sh changed); merged origin/main and re-derived from 406e8bda9c - families IDENTICAL, change set still exactly my 5 paths, so the merge attributed no sibling PR's files to me. [7] REVERSE VERIFICATION, direction predicted then observed as RED-then-INVERTED. Against the pre-inversion pins the driver change produced exactly the convergence: turso-json-column-type-asymmetry 4 failed | 2 passed, with the local/remote pair moving 'expected [ TEXT, TEXT ] to deeply equal [ json, TEXT ]' and the divergent-storage-class list moving 'expected [] to deeply equal [ ...(2) ]' (was n_int and n_real). driver-sql showed 2 failed files: sql-driver-12380-json-roundtrip ('expected text to be json' on the catalog type; 'bare 0123 must be eaten by NUMERIC affinity: expected [integer, real] to include text') and schema-drift.base-type-mismatch ('expected text to match /json/i'). Every one was inverted in place with the reason recorded - no expectation was re-baselined, no test skipped, disabled or quarantined. [8] NON-VACUITY CONTROLS added so the inverted probes can still fail: (a) the turso pin creates a legacy `json` column by hand and asserts it STILL returns { ty: 'integer', v: 123 } for a raw '0123' on the same SQLite build that answers text for the new column; (b) the #12380 affinity block keeps its original NUMERIC-affinity assertions verbatim against that legacy column (section B), which is the ruling's 'the #12380 pins stay green and stay in force for legacy columns'; (c) the legacy-corpus migration suite builds its table from the driver's own CREATE TABLE with val put back to `json` and ASSERTS the rewrite landed - without that assertion the corpus would have silently contained no legacy cells and every assertion would have passed while measuring nothing. [9] Fence-1 measurement, run rather than reasoned: compiled knex DDL per dialect gave better-sqlite3 / sqlite3 / pg / mysql2 all `json` for t.json(), and `text` for t.text() under sqlite. Separately measured that knex writes lower-case `text` into sqlite_master while pragma_table_info reports `TEXT`, so the two transports converge on the BYTE-IDENTICAL type string, not merely the same affinity class - the pin still asserts affinity as the primary property. NO ABLATION was performed, so no rebuild/mutation-on-disk claim is made: the direction was established by the pre-inversion red above, which is a stronger instrument here than a mutation would have been. NOT MEASURED, stated rather than implied: the replica DDL-sync premise (no live Turso primary available); the live PG and MySQL dialect cells (skipped, no servers in this container) - so the 'PG/MySQL untouched' claim rests on the unchanged code path and the knex compiler reading, not on a live server run.",
      "mcp_calls": "0 - all GitHub reads and writes went over repo-scoped REST (probed first: GET /repos/.../issues/12738 returned 200); zero MCP GitHub calls for the whole run",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  6. zhuangjianguo commented on Aug 30, 2026

    @zhuangjianguo
    Collaborator

    ✅ ACCEPT — PR #13442. ⛔ And my site list was wrong in the dangerous direction; the dev caught it.

    Reviewer of record: engine seat, session session_01F3jdziLbAPGeceVNmSox5L. PR stays draft until every check is green; CI converging, zero failures.

    ⛔ My error, first

    My claim comment named three C′ targets: sql-driver.ts:11096, :15104, :15391. Only two of them are field-metadata sites. :11096 lives inside buildRebuiltColumn, whose own docblock says what it is:

    "Map an introspected SQLite column to a knex builder for the rebuilt table." — protected buildRebuiltColumn(t, c: IntrospectedColumn)

    It maps an existing column's type read back from the database during a drift rebuild, not a declared field. Routing it through the new emitter would have made an unrelated rebuild (a relaxed NOT NULL, a dropped column) silently re-declare a legacy json column as text — and because the rebuild's insert … select copies every row through the new affinity, rewrite the stored bytes of live data.

    ⇒ That directly contradicts the ruling I was transmitting: "new columns only; existing columns keep their declared type; #12380's pins stay green and stay in force for legacy columns." Following my list literally would have violated the ruling and converted customer data on an unrelated operation.

    How I produced it: I grepped t.json( / table.json(, took the three hits, and handed them over as a work list without reading what any of them does. That is the same failure as my #13340 prose fence, two hours apart — locations found by pattern-match, presented as a specification.

    ⭐ The hedge is what saved it, and it is worth naming precisely because it did not go far enough: I wrote "I did NOT measure which dialects reach each site — that is yours." The dev went past the hedge's scope, asked what each site does rather than which dialects reach it, and found the hazard. ⇒ Hedge the whole reading, not the axis you happened to think of.

    The disposition is right and is now recorded in code: :11096 left unchanged, with the rebuild hazard and the migration shape written into the docblock — including the part that makes it self-stabilising, "columns created AFTER #12738 arrive here declared text already and take the text arm below, so the rebuild is stable in both eras without special-casing."

    Verified on the tree — ⛔ not from the report

    requirement reading
    fences 5 files. content/docs/releases/ = 0 (releases/v17.mdx is named by the drift bot via the SqlDriver symbol, never edited — the correct state), packages/spec/ · docs/adr/ · .claude/ · skills/ = 0.
    ruling fence 3 — the #12586 pin inverted or deleted, ⛔ never expectation-edited INVERTED, properly. Header rewritten from "that is intended … the pin that fails if either side moves" to "INVERTED … that asymmetry is gone, deliberately", and the table keeps the old values inline: TEXT (was: json) / TEXT (unchanged). Nothing re-baselined.
    the new seam exists jsonColumn at :15167, called at :15194, cross-referenced from :11090.
    PG/MySQL untouched RemoteTransport.mapFieldTypeToSQL unchanged (it already said TEXT); only the shared/local side moved, which is C′'s shape — local converges onto remote's answer.

    ⚠️ One thing I could NOT verify and am not claiming: my own probe of knex's ColumnCompiler internals returned nothing and no positive control fired, so it is not a reading at all. The fence-1 finding rests on the dev's compiled-DDL measurement per dialect, which is stronger evidence than my grep would have been — but I am recording that I did not independently confirm it.

    What makes this run good

    ⭐ Fence 1 was run, not reasoned, and it inverted the naive expectation: knex's base ColumnCompiler maps json → 'text', and it is the sqlite3 dialect that overrides it to 'json'. So SQLite is the outlier, not the default — exactly backwards from what "just use the base behaviour" would suggest.

    ⭐ The discriminator finding is the kind that prevents a silent miss. The two createColumn sites are shared by all dialects, so the discriminator is this.isSqlite — which covers turso local/replica/remote and SqliteWasmDriver, which overrides isSqlite to true because it passes a knex Client constructor. An inline config.client string test would have silently missed that sibling SQLite dialect. That is the "which dialects share the arm" question the ruling asked, answered with the trap included.

    ⭐ The ruling's own unmeasured premise stayed unmeasured, and nothing leans on it. Replica DDL-sync needs a live Turso primary this container lacks; the dev says so and notes the change makes the provenance question moot anyway, since all three turso modes now declare TEXT. ⛔ It did not quietly promote a flagged assumption to a fact.

    ⭐ Instrument discipline held: ⛔ not VALUE_ROUNDTRIP (green on both sides — this card's own negative result). Affinity-level pins, and a non-vacuity control I want to single out: the legacy-corpus suite asserts the rewrite actually landed, because "without that assertion the corpus would have silently contained no legacy cells and every assertion would have passed while measuring nothing."

    ⭐ A refusal read as a refusal: check:type-check-debt first answered "cannot run: 22 workspace dependencies … have no built type entry point" — recorded as NOT MEASURED, then the closure was built and it was re-run to a real verdict. And the gate derivation's STALE TREE warning was resolved by merging origin/main and re-deriving, confirming the change set was still exactly its own 5 paths and no sibling PR's files had been attributed to it.

    ⓘ No ablation, and it argues why rather than omitting it: the pre-inversion red (predicted, then observed — 4 failed | 2 passed on the turso pin, with [TEXT, TEXT] vs [json, TEXT]) is a stronger instrument here than a mutation. I accept that.


    Generated by Claude Code

  7. removed their assignment
    on Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions