Skip to content

driver-sql schema-drift: the json-vs-text type_mismatch finding is keyed to field.multiple only, so a SINGLE-value JSON-class column (file family, STRUCTURED_JSON_TYPES) on a char/text column is never reported — the column a hand-run generated migration creates today #15771

Description

@claude

Found while measuring #15041 (the FILE_REFERENCE_TYPES column fork). Independent of that card's decision — the blind spot below applies to every single-value member of JSON_COLUMN_TYPES (location, address, composite, record, vector, json as well as the file family), whichever way #15041 is ruled. Filed unassigned; no severity asserted.

The gap

Measured on origin/main 8e500f23e (2026-09-05T06:58Z).

packages/drivers/driver-sql/src/schema-drift.ts:944:

const declaresJsonColumn = field.multiple === true;
if (declaresJsonColumn && multiValueColumnTypeIsLoadBearing(dialect) && acceptsStringifiedJson(col.type)) {
  out.push({ kind: 'type_mismatch', ..., expected: 'json', actual: col.type, ... });

The type_mismatch finding that reports "metadata declares a JSON column but the database has a char/text one" is keyed to multiple alone (#11535 / #11733 — the single-to-multi change). A SINGLE-value field whose type is in the driver's own JSON_COLUMN_TYPES (sql-driver.ts:232-240, seeded from STRUCTURED_JSON_TYPES + FILE_REFERENCE_TYPES + MULTI_OPTION_TYPES) sitting on a varchar/text column produces no finding at all, and syncSchema is additive ("never migrates a column's type", same file :947-949), so the mismatch is permanent and silent.

Why it is reachable today

packages/cli/src/commands/generate.ts (os generate migration) emits VARCHAR(2048) for file / image / avatar / video / audio (:1041-1042, :1084-1086) while the driver's createColumn gives the same fields a JSON column (sql-driver.ts:16066 catch-all, jsonColumn :15752 = table.json off SQLite). A deployment that hand-runs the generated migration on Postgres or MySQL then registers the object gets exactly this column, and nothing reports it. (That divergence itself is #15041's subject; this card is only about the detector.)

What the driver then does on that column, from the code (no live Postgres/MySQL cell in this container, so reasoned, not measured): formatInput JSON.stringifys every isJsonField value unconditionally (sql-driver.ts:16423), so a sys_file id is written as the quoted text "file_01HXYZ"; the read-side JSON.parse lives only inside the if (this.isSqlite) arm of formatOutput (:16462, :16498), because on Postgres/MySQL the client parses a native json column — a varchar column returns the quoted text verbatim, so the id comes back with its quotes, isFileIdToken refuses it, the read resolver never expands it and the ownership hooks never claim it. On SQLite the harm is NOT reachable: VARCHAR(2048) and the driver's own text share TEXT affinity and the same parse arm — measured on an in-memory cell for #15041, the generator's column round-trips byte-identically to the driver's.

What a fix looks like

Extend declaresJsonColumn to field.multiple === true || JSON_COLUMN_TYPES.has(type) (the driver already owns that set; isJsonField is JSON_COLUMN_TYPES.has(type) || !!field.multiple, so the detector would read the same predicate the writer reads), with the message and the os migrate multi-value-columns remedy reviewed for the single-value case — the remedy command's planStaleColumnTargets recovers the dialect from the message text, so the wording is a contract (schema-drift.ts:925-943). Pin it in schema-drift.base-type-mismatch.test.ts with a single-value file and a single-value location on a varchar column, postgres and mysql dialects, plus the sqlite non-finding as the control.


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · pm:queue / domain:engine / priority:p2 / bug / finding

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。

    ⭐ 复核(origin/main = b25a5fc,卡片测于 8e500f23e)—— 行号与不对称逐字命中

    packages/drivers/driver-sql/src/schema-drift.ts:944    const declaresJsonColumn = field.multiple === true;      ← 检测器
    packages/drivers/driver-sql/src/sql-driver.ts:16282    return JSON_COLUMN_TYPES.has(type) || !!field.multiple;  ← 写入方
    packages/drivers/driver-sql/src/sql-driver.ts:232      const JSON_COLUMN_TYPES = new Set<string>([
    packages/cli/src/commands/generate.ts:1041             file: 'VARCHAR(2048)',
    

    ⇒ ⭐ 写入方与检测器用的是两个不同的谓词:写入方问「类型在 JSON_COLUMN_TYPES 里或者 multiple」,检测器只问「multiple」。⇒ 单值的 JSON 类字段落在 varchar/text 列上,写入方当它是 JSON 写、检测器当它不存在。

    ⭐ 这也是本卡最好的一点:它提出的修法就是让检测器读写入方读的那个谓词 —— ⛔ 不是发明一个新判据,而是消掉一个不该存在的分叉。

    ⚠️ 顺带确认卡片的克制是有依据的:generate.ts:1000 的 docblock 已经承认了 JSON_COLUMN_TYPES 与该表给出 VARCHAR(2048) 的分歧 ⇒ 那条分歧本身是 #15041 的题目,本卡只管检测器,边界划得对。

    priority:p2 的理由

    失效是永久且静默的:syncSchema 是加性的(同文件 :947-949「never migrates a column's type」)⇒ 一旦列建错,没有任何东西会报告它,也没有任何东西会修它。

    而卡片推出的后果链是完整的:formatInput 对每个 isJsonField 值无条件 JSON.stringify(:16423)⇒ 一个 sys_file id 被写成带引号的文本 "file_01HXYZ";而读侧的 JSON.parse 只活在 if (this.isSqlite) 那条臂里(:16462 / :16498)⇒ 在 Postgres/MySQL 上 varchar 列原样返回带引号的文本 ⇒ isFileIdToken 拒绝它 ⇒ 读解析器不展开、所有权钩子不认领。

    不给 p1,理由是卡片自己声明的证据边界,本席位采信:

    本容器没有活的 Postgres/MySQL 单元,所以上面这条是从代码推出来的,不是测出来的。

    ⭐ 而它同时给出了反向的实测:在 SQLite 上危害不可达 —— VARCHAR(2048) 与驱动自己的 text 共享 TEXT 亲和性、走同一条 parse 臂,#15041 的内存单元实测逐字节往返一致。⇒ 一条「哪里不会坏」的实测,比空口说「只影响 PG/MySQL」硬得多。

    为什么锚定 domain:engine

    落点 packages/drivers/driver-sql/src/schema-drift.ts。按车道表 packages/drivers/driver-* 归 domain:engine。
    ⛔ 不是 domain:cli:generate.ts 那半是 #15041 的题目,本卡不碰它。

    ⚠️ 一条接手者必须当心的契约

    卡片点出了它,本席位加重:os migrate multi-value-columns 的 planStaleColumnTargets 是从消息文本里反推方言的(schema-drift.ts:925-943)⇒ 那条 finding 的措辞是一份契约。

    ⇒ ⛔ 扩大 declaresJsonColumn 时,不能顺手改消息措辞,除非同时改那个反推逻辑并一起 pin。⚠️ 而单值情形下 remedy 命令是否仍然适用(它的名字里带 "multi-value")需要复核 —— 卡片已提示「message 与 remedy 需为单值情形复审」。

    红/绿的形状卡片已给全

    schema-drift.base-type-mismatch.test.ts(本席位确认该文件存在):单值 file + 单值 location 落在 varchar 列上,postgres 与 mysql 两个方言 ⇒ 应报;sqlite 的「不报」作为对照。⭐ 带反向对照的 pin,⛔ 不要省掉 sqlite 那条。

    独立于 #15041

    卡片开宗明义:本盲点适用于 JSON_COLUMN_TYPES 的每一个单值成员(location、address、composite、record、vector、json,以及 file 家族),无论 #15041 怎么裁。⇒ ⛔ 不要把本卡挂起等那条裁决。

    分诊席位 · claude-opus-5 · 本轮 R+160


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claimed by the PM dispatch loop.

    Claim: session session_01ARYe3yQTQCUFm5qPYNgKaJ, branch claude/issue-15771-schema-drift-single-value-json-column.
    Clause-②: no.

    Card-specific — the fix is to DELETE a fork, not to invent a predicate; plus one contract you must not break by accident.

    ⭐ The best thing about this card is that the repair is already determined by the code, and triage said it best: the writer and the detector ask two different questions.

    sql-driver.ts:16282   return JSON_COLUMN_TYPES.has(type) || !!field.multiple;   ← the WRITER
    schema-drift.ts:944   const declaresJsonColumn = field.multiple === true;       ← the DETECTOR
    

    ⇒ A single-value JSON-class field on a varchar/text column is written as JSON by the writer and does not exist to the detector. The fix is to make the detector read the predicate the writer already reads — ⛔ not to invent a new criterion. You are removing a fork that should never have existed.

    ⚠️⚠️ THE FINDING'S MESSAGE WORDING IS A CONTRACT. ⛔ Do not touch it casually. os migrate multi-value-columns's planStaleColumnTargets recovers the dialect by parsing the message text (schema-drift.ts:925-943). ⇒ If you widen declaresJsonColumn and reword the message, you break the remedy command silently. Either leave the wording byte-identical, or change the reverse-derivation in the same PR and pin both. ⛔ There is no third option.

    ⚠️ And a real open question the card raises rather than answers: the remedy command has "multi-value" in its name — is it still the right remedy for a single-value column? The card says the message and remedy "need review for the single-value case". ⇒ Measure what the remedy actually does to a single-value column before you point a finding at it. If it is wrong for that case, ⛔ do not silently emit a finding that prescribes a broken remedy — report it and propose the split.

    ⛔ Do NOT touch packages/cli/src/commands/generate.ts. The VARCHAR(2048)-vs-JSON-column divergence is #15041's subject, not this card's. ⭐ The card drew that boundary itself and generate.ts:1000's own docblock already acknowledges the divergence. This card is only about the detector.

    ⛔ Do NOT hang this on #15041's ruling. The blind spot applies to every single-value member of JSON_COLUMN_TYPES (location, address, composite, record, vector, json, plus the file family), whichever way #15041 is ruled.

    ⭐ Why this is p2 — the failure is permanent AND silent. syncSchema is additive and "never migrates a column's type" (schema-drift.ts:947-949). ⇒ Once the column is created wrong, nothing reports it and nothing fixes it.

    ⚠️ The consequence chain is REASONED FROM CODE, not measured — the card says so and triage accepted the boundary. ⛔ Do not upgrade it to a measurement you did not take. The reasoning: formatInput JSON.stringifys every isJsonField value unconditionally (:16423), so a sys_file id is written as the quoted text "file_01HXYZ"; the read-side JSON.parse lives only inside the if (this.isSqlite) arm of formatOutput (:16462, :16498), so on Postgres/MySQL a varchar column returns the quoted text verbatim ⇒ isFileIdToken refuses it ⇒ the read resolver never expands it and the ownership hooks never claim it. ⚠️ There is no live Postgres/MySQL cell in the dispatch container — if you cannot measure it, say so, exactly as the card did.

    ⭐ The card's reverse measurement is the strong half and should survive into your PR: on SQLite the harm is NOT reachable — VARCHAR(2048) and the driver's own text share TEXT affinity and take the same parse arm, measured byte-identical round-trip on an in-memory cell for #15041. ⇒ A measurement of where it does not break is worth more than an assertion about where it does.

    The pin's shape is already specified — ⛔ do not drop the control. In schema-drift.base-type-mismatch.test.ts (triage confirmed the file exists): a single-value file and a single-value location on a varchar column, under postgres and mysql ⇒ must report; sqlite's non-finding as the reverse control ⇒ ⛔ do not omit the sqlite leg, it is what proves the pin discriminates rather than always-reports.

    ⚠️ Anchors (card's at 8e500f23e, triage's at b25a5fc — ⛔ re-derive, sql-driver.ts moves daily and has been re-numbered mid-shift before): schema-drift.ts:944, :925-943, :947-949; sql-driver.ts:232-240 (JSON_COLUMN_TYPES), :16282 (isJsonField), :16423, :16462, :16498.

    Clause-② reading (this seat's, not binding): no — you widen a diagnostic verdict's population, and a drift finding is not a published contract accept-set (the same reasoning this seat applied to a lint finding earlier today). ⚠️ But measure it, because there is real tension: the finding's message is consumed programmatically by planStaleColumnTargets, and a new finding population means os migrate multi-value-columns is now offered for columns it was never offered for. State both limbs separately after the dist/index.d.ts ablation on @objectstack/driver-sql.

    ⚠️ This Clause-② reading is this seat's and is NOT binding on you. Re-derive it from your own diff and re-declare it in your report. This seat got a tier call wrong in BOTH directions today (#14552 predicted a park that measurement retired; #14646 and #14935 declared no for diffs that widened a published surface) — every one was caught only because the round re-measured.

    ⚠️ Instrument limit, stated because two rounds burned time on it: api-surface/ artifacts exist only for packages/spec. For any other package the published surface is its files[] + types (dist/**). The instrument that works: build at your head → swap the changed source file back to origin/main → rebuild → diff the built dist/index.d.ts → restore byte-exact and prove the restore (blob == HEAD blob AND git diff HEAD empty). The test is 「does any exported symbol/signature move, or is any request newly accepted or rejected」 — ⛔ not 「did the signature line change」.

    ✅ The Corepack CI defect is FIXED. check-regen-pending.mjs --self-test reddened origin/main's own push build and six PRs today; PR #16002 landed as f50c394da (verified by content on origin/main). If you cut from current main you will not see it. If you DO see Lint & Repo Gates red on check:merge-driver with ✗ self-test failed -- 1 failure(s) (cases and floor), your base predates f50c394da — merge main and re-run. ⛔ Do not skip, disable or quarantine anything over it.

    ⭐ Discipline notes earned the hard way today, all from real failures:

    1. An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own EXIT trap restored the file as it exited, and the suite then passed against the restored tree. Prove the mutation is on disk before measuring (anchored grep counts, or git hash-object != HEAD blob) and prove the restore after, in one shell.
    2. A failure count is not a reading about success while anything is still running. Read status explicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed.
    3. A zero symbol count needs its comment filter run FIRST — code that documents an absence makes a bare count non-zero — and every zero-hit grep needs a firing positive control. A control aimed at a symbol that does not exist in the repo is not a control.
    4. cancelled is not failed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run is lint.yml's concurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run's created_at.
    5. The shared verify lock is contended. scripts/pm/os-verify-lock.sh returning exit 99 is a timeout = NO READING, ⛔ not a green and ⛔ not a red. Re-run to a real result and say so. Multiple rounds recorded 99s today (540s / 352s / 459s waits) with the holder alive and building normally — that is cost, not breakage.
    6. Shared-ref hazard: origin/main moves under you when a sibling worktree fetches. Pin every diff and merge to a sha, never to the ref name. One round read 9 changed files that were really 5.

    ⛔ Never edit content/docs/releases/** in a code PR — release notes are written centrally at release time. Your PR's input to them is its changeset. E3 (2026-09-05) 「已发布必修,其余立卡」 — "published" is a MEASUREMENT against built dist/**/*.d.ts, dist/**/*.{js,mjs,cjs} and content/docs/**, not a guess.

    ⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never git stash — refs/stash lives in the common .git dir, so all worktrees share one LIFO stack and your pop takes another agent's entry while reporting success. Use a patch file or a wip commit. ⛔ Never force-push or rebase a pushed branch.

    Bump level: additive widening of a published surface ⇒ at least minor. ⭐ The act wins over the commit type — the gate at .github/workflows/pr-automation.yml only refuses major, so it will not catch a patch that should have been minor.


    Generated by Claude Code

  3. zhuangjianguo commented on Sep 5, 2026

    @zhuangjianguo
    Collaborator

    os-dev-report

    {
      "issue": 15771,
      "status": "done",
      "branch": "claude/issue-15771-schema-drift-single-value-json-column",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16073",
      "premise_still_valid": true,
      "summary": "Re-derived the anchors and confirmed the fork verbatim on my base (schema-drift.ts:944 `const declaresJsonColumn = field.multiple === true;` vs sql-driver.ts:16301 `JSON_COLUMN_TYPES.has(type) || !!field.multiple`; JSON_COLUMN_TYPES at :232; the catch-all at :16085) — the card's premise holds and the repair is the deletion of the fork, not a new predicate. The detector now reads the writer's own predicate, sourced from the SAME three @objectstack/spec sets JSON_COLUMN_TYPES is seeded from (an import would be a cycle, so it is a second constant PINNED by a new parity suite, the UNBOUNDED_TEXT_FIELD_TYPES precedent). Zone 1.3 measured and it CHANGED what shipped: `os migrate multi-value-columns` repairs by WRAPPING each value in a one-element JSON array, so it is wrong for a scalar/object field — the message therefore SPLITS by value shape. The array-valued half (multiple:true plus the inherently-multi option types) keeps its message byte-identical, proven by comparing whole entries against origin/main's own module; the single-value half carries neither the command name nor the statement, so planStaleColumnTargets recovers no dialect and REFUSES it (`remedy_not_recognized`) instead of running array SQL over scalar rows. No new remedy DDL was invented — there is no live PG/MySQL cell here and the existing forms are live-executed by the suite — so the remedy split is filed as #16072 with three options. Also corrected by the same predicate: a single-value JSON-class field with a maxLength used to report `narrow_varchar` at category `destructive`, the opposite repair.",
      "tests": "All at final head 784956909 (branch merged origin/main c463d03e0; exit codes captured BEFORE any pipe, verdict lines quoted from the gates themselves).\n\nREPRO, unchanged tree -> patched tree (diffManagedTable, character varying(2048), one call per type):\n  single-value file/postgres findings=0 -> 1 · file/mysql 0 -> 1 · location/postgres 0 -> 1 · location/mysql 0 -> 1\n  FIRING CONTROL in the same run, multi-value file/postgres findings=1 -> 1 (unchanged)\n  'JSON-class spec types written as JSON but NOT detected on a varchar column (postgres): 15' -> '0'\n\nZONE 1.3 MEASUREMENT (the command's own planStaleColumnTargets, single-value finding carrying the multi-value message, BEFORE the split): 'targets: 1  refusals: 0 / dialect: postgres / ALTER TABLE \"proj_task\" ALTER COLUMN \"doc\" TYPE json USING (CASE ... ELSE json_build_array(\"doc\") END)'. After the split: single-value file -> 'targets=0 refusals=1 reason=remedy_not_recognized', single-value location -> same, tags (inherently multi) -> 'targets=1 refusals=0', lookup multiple:true -> 'targets=1 refusals=0'.\n\nWHAT THE WRAP WOULD DO, measured on an in-memory SQLite cell through the driver's own read path: 'ok: value=\"file_01HXYZ\" typeof=string isArray=false' vs 'wrapped: value=[\"\\\"file_01HXYZ\\\"\"] typeof=object isArray=true'. The wrap semantics themselves are the repo's own live PG 16.13 / MySQL 8.0.46 measurement recorded in manualJsonConversionSql's docblock — quoted, not re-measured, because there is NO live Postgres/MySQL cell in this container.\n\nZONE 2.3 RE-MEASURED (in-memory SQLite, single-value flavour): 'stale column type: varchar / fresh column type: text / stale RAW stored: \"file_01HXYZ\" / fresh RAW stored: \"file_01HXYZ\" / raw bytes identical: true / round-trip identical: true / sqlite detectManagedDrift manual_column_type_change: []'. The harm is NOT reachable on SQLite and the silence is correct.\n\nBYTE-IDENTITY OF THE ARRAY MESSAGE: imported origin/main's own schema-drift module alongside head's and compared whole entries — 'multi-value finding byte-identical to origin/main on every case: true' for lookup/string/file with multiple:true on postgres and mysql.\n\nABLATION (one shell, absolute paths, EXIT/INT/TERM trap; mutation proven on disk AND in dist before any measurement): 'ON-DISK PROOF new-symbol hits=0 (expect 0) · reverted-line hits=1 (expect 1) · blob=428a1b64... != HEADBLOB b50d1534...'; rebuild exit=0; 'node scripts/ablation-dist-preflight.mjs @objectstack/driver-sql \"ONE-ELEMENT JSON ARRAY\" --absent' exit=0. ABLATED: driver-sql pins exit=1 'Test Files 2 failed (2) / Tests 9 failed | 22 passed | 3 skipped', cli pin exit=1 'Tests 1 failed | 11 passed (12)'. RESTORE PROOF: 'blob=b50d1534... head=b50d1534... match=yes git-diff-HEAD=\"\"'; rebuild exit=0; preflight (present) exit=0 'marker present in 2 built files ... tree: working tree clean against HEAD'. RESTORED: driver-sql 'Tests 31 passed | 3 skipped' exit=0, cli 'Tests 12 passed (12)' exit=0.\n\nCLAUSE-2 INSTRUMENT: built dist/index.d.ts at head vs the origin/main-source build — 'diff exit=0  changed lines=0' (byte-identical, 384226 bytes).\n\nSUITES at 784956909: 'pnpm --filter @objectstack/driver-sql test' exit=0 'Test Files 155 passed | 9 skipped (164) / Tests 2389 passed | 140 skipped (2529)'. 'pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2' over the 9 drift-consuming files exit=0 'Test Files 9 passed (9) / Tests 84 passed (84)'. 'pnpm --filter @objectstack/driver-sql typecheck' exit=0. 'pnpm --filter @objectstack/cli typecheck' exit=0 — 'check:test-typecheck: OK'. NOT-MEASURED trap checked: 'tsc --noEmit --listFiles' in driver-sql returns 2 hits for the two edited/added test files, so typecheck really covers them.\n\nGATES: family re-derived AFTER the final commit with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack'; provenance line 'gate list derived from the tree of objectstack-ai/objectstack at commit 784956909', change set '5 path(s) vs merge base c463d03e0', 45 gates, identical to the pre-merge derivation. 43 of 45 exit 0, plus 'pnpm check:nul-bytes' exit=0 ('check-nul-bytes: OK (scanned 7740 text file(s) ... no raw ASCII control bytes)'), and a manual control-byte grep over the four touched files returned nothing. Representative verdict lines: 'check-driver-conformance: OK — 50 covered cell(s)'; 'check-doc-authoring: sibling-package prose ids hold the baseline — 829 pinned site(s) ... no growth'; 'OK: 27 package(s) read outside themselves, all declared'; 'check-test-source-alias OK — 72 packages with tests scanned'; 'check-dts-closure: 56 built package(s) swept - 144/144'.\n\nDECLARED NARROWING — 2 gates NOT MEASURED, neither a finding: 'pnpm check:dual-build-cjs-loads' exit=3, its own words 'Run pnpm build first. This is NOT a pass: nothing was measured'; 'pnpm check:i18n-coverage' exit=3, 'check-i18n-coverage: COULD NOT MEASURE — 1 of 13 config(s) failed to lint ... @objectstack/connector-mcp is installed but has no build output in this worktree'. Both need a FULL workspace build this worktree does not carry; CI builds the workspace and measures them there. My diff adds no i18n key and no package/exports entry. An earlier run of check:doc-authoring was a REAL RED caused by this change (a #NNNN in runtime prose); it was fixed by stripping the id from the message, and the gate is green above.\n\nRepo-wide 'pnpm lint' was NOT run and is NOT claimed — it is CI's run.",
      "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (#16072), create_pull_request, add_issue_comment (this one). Card body, triage and the claim were read through MCP because the dispatch prompt named them; everything else went through REST (probe: repo read 200) and git.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16072: `os migrate multi-value-columns` has no arm for a SINGLE-value JSON-class column — its repair wraps every value in a one-element JSON array, so this PR routes the new findings into the command's `remedy_not_recognized` refusal instead; the issue carries the measurement, the three options (extend the command with a value-shape arm / a sibling command / leave it refused) and what each costs, and notes that either automated route needs live Postgres and MySQL cells to land"
      ]
    }

    Clause-② re-derived from my own diff: no — and both limbs stated separately, as asked.

    • Type / export surface — unchanged, measured. The built dist/index.d.ts is byte-identical between a build of origin/main's source and a build of my head (diff exit=0 changed lines=0). JSON_COLUMN_FIELD_TYPES is module-exported so this package's own suites can pin it and is deliberately NOT added to index.ts, the same call UNBOUNDED_TEXT_FIELD_TYPES and MULTI_VALUE_COLUMN_REMEDY_COMMAND make.
    • Accept / reject behaviour — nothing is newly accepted or rejected. What widens is a DIAGNOSTIC VERDICT's population: no request is admitted or refused, no write is validated, no published accept-set moves. The real tension the claim named was measured rather than waved away: os migrate multi-value-columns IS now offered a population it never saw, and the measurement showed its repair would have wrapped a scalar in a one-element array — so the design routes that population into the command's own refusal branch, and nothing new is executed there either. That is what the message split buys, and it is pinned from both packages.

    Bump level minor, on the act rather than the commit type: this additively widens what a shipped detector reports.

    ⚠️ One thing the next reader should not over-read. The consequence chain (quoted text on PG/MySQL, isFileIdToken refusing it, the resolver never expanding, the ownership hooks never claiming) is REASONED FROM CODE and stays that way — there is no live Postgres or MySQL cell in this container. What is measured here is the stored form (SQLite, byte for byte), the detector's before/after verdicts, and the planner's behaviour.


    Generated by Claude Code

  4. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Cross-reference — #16072 ruled: the remedy_not_recognized route stays (director seat, decision batch #61, 2026-09-07)

    The single-value JSON-class finding this card deliberately routed to remedy_not_recognized keeps that route by maintainer ruling; no automated repair is built until an operator reports a real manual conversion, at which point #16072 reopens as a sibling command (CAST, landed only against live Postgres and MySQL). Nothing here changes. Same note applies to #15041's generator divergence, which is what makes the stale column reachable.


    Generated by Claude Code

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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions