Repository navigation
feat(driver-sql,driver-turso,driver-sqlite-wasm): SQLite-family JSON columns declare TEXT; server dialects keep native JSON - #13442
Conversation
…lite-json-columns-declare-text
Each dialect now declares the semantically correct JSON column type. Postgres and MySQL keep their native JSON types; the SQLite family (plain SqlDriver on sqlite3/better-sqlite3, TursoDriver in all three transport modes, and SqliteWasmDriver) declares text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 706d2148b09c13ca4c6234f4db2b03e36ea1e313 && git checkout 706d2148b09c13ca4c6234f4db2b03e36ea1e313
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bac201e9288bafbf23b95638aa2b48761effdea6 8058e2b606a94776d49be771f6f4a7ea491a498f && git checkout -B drift-repro bac201e9288bafbf23b95638aa2b48761effdea6 && git merge --no-ff 8058e2b606a94776d49be771f6f4a7ea491a498f
node scripts/docs-audit/affected-docs.mjs --json bac201e9288bafbf23b95638aa2b48761effdea6
|
… run (#13474) Fixes #13442 Clause-②: yes Metadata-protocol: none A 100-row list bulk update took about 100 s in the console for about 19 s of server work. Two defects compounded, and this PR carries both halves (the seat ruled one PR): 1. **Per-row results.** `ObjectStackAdapter.bulkUpdate` / `bulkDelete` read only the server's `succeeded`, so a partial failure reached the result dialog and the error CSV as one `batch_0: N records failed` entry. They now resolve to a `BulkWriteResult` (`{ succeeded, errors: [{ id, error }] }`) read off the `results[].errors[]` that `updateMany` / `deleteMany` already answer with `continueOnError: true`. `executeBulkBatch` reports each named failure under its own id and reason, so the dialog, the error CSV and Retry work per record whatever the `batchSize`, and Undo now skips the failed rows. Only failures the call does not name still fold into one `batch_N` entry. No objectstack change was needed. 2. **One refresh per run.** New optional `DataSource.batchMutationEvents(work)`: the adapter holds the `onMutation` events of the writes `work` makes and delivers them when it settles, one per (type, resource): the original event if only one was held, else one resource-level event. Scopes nest, and a rejection still delivers. `useBulkExecutor` runs every bulk run and every undo inside it. ## Clause-② surface (what the built declarations add or change) - `@object-ui/types`: `DataSource.bulkUpdate` / `bulkDelete` return `Promise` of `number | BulkWriteResult` (was `number`). New exported types `BulkWriteResult` and `BulkWriteRowError`. New optional member `DataSource.batchMutationEvents`. - `@object-ui/data-objectstack`: `ObjectStackAdapter.bulkUpdate` / `bulkDelete` resolve to `BulkWriteResult` (was a number). This is breaking for code that calls these two methods directly and reads a count: it must read `.succeeded`. In-tree there is no such caller (`git grep` finds only the executor). New method `batchMutationEvents`. - `@object-ui/core`: `BulkBatchOps.bulkCall` may resolve to `number | BulkWriteResult`. - `@object-ui/plugin-grid`: the executor's `dataSource` face gains the optional `batchMutationEvents`, derived from `DataSource` like the two bulk members. Backward compatibility is a union, not a replaced type. An adapter returning a plain number still satisfies `DataSource`. Reverse check against the REBUILT `packages/types/dist`: `const n: number = await ds.bulkUpdate(...)` is refused with TS2322 (`Type 'number | BulkWriteResult' is not assignable to type 'number'`), and in the same program a `bulkUpdate: async () => 3` implementor compiles. The changeset is `minor` on the four packages and states the breaking adapter return. ## Mechanism checks (dispatch hypotheses) - H1 holds: `bulkUpdate` returned `emitBulk((res as any).succeeded as number)` and its per-id fallback swallowed each error. `bulkDelete` had the same shape. - H2 holds, and it is wider than the two views. Six paths subscribe to `onMutation`: ListView, ObjectView (plus its `notifyDataChanged`), ObjectKanban, ObjectCalendar, `useDataRefresh`, and the console's `useMutationInvalidationBridge`, which puts every event on the data-invalidation bus for every bus reader (ObjectGrid's own query, related lists, and others). - H3 holds. Inside one batch, `executeBulkBatch`'s per-row pass is concurrent (`Promise.allSettled`). Batches run one after another. - H4, decided by measurement: hold at the source, not coalesce in the views. Holding at the source reaches all six subscriber paths. Coalescing in ListView / ObjectView reaches two of them, and a timer-based coalescer cannot bound a run whose rows are separated by network round-trips. Measured end to end (real adapter, real client, real `useBulkExecutor`, real `ObjectGrid` behind the real bridge, 25 rows, `batchSize: 1`): **1 list refetch per run with the scope, 22 with it removed.** `ListView.tsx` and `ObjectView.tsx` are therefore not changed. ## Tests - `packages/data-objectstack/src/bulkPerRowResults-13442.test.ts`: real adapter and `@objectstack/client`, only `fetch` stubbed. A 2-failure `updateMany` answer gives 2 per-record errors with ids and messages. A failed row is attributed by its request `index` when it has no `id` (the spec's own example shape). Several reasons on one row are joined. For `deleteMany`, a refused row keeps its reason, and a kept row with no `errors[]` gets a generic one. Six holding cases: N updates give 0 events while the scope is open and then 1; the unscoped control gives N; a single held event keeps its id and record; coalescing is per (type, resource); the result passes through and a rejection still delivers; nested scopes deliver once. - `packages/plugin-grid/src/__tests__/bulkRunPerRowResults-13442.test.tsx`: a 100-row run with 2 server rejections sends one `updateMany` (no PATCH) and ends with `errors` equal to the two records' ids and messages, with no `batch_` entry. The refetch count is asserted exactly as `1`.⚠️ That run is outside `act()`, on React's real scheduler, with each PATCH answering after a timer. Inside `act`, or with a microtask-only stub, the pin stayed green with the scope removed (measured), because React rendered once at the end. - `useBulkExecutor.test.ts` (4 new cases) and `bulkFastPath.test.ts` (5 new cases). The existing `bulkUpdate.test.ts` / `bulkDelete.test.ts` move to the new return shape. One-off ablations (WRAP mode of `ablation-replace.mjs`, each restore proven: blob equal to HEAD and `git diff HEAD` empty; no build is needed because vitest aliases these specifiers to `src`): - A. Executor scope off (`false &&` on the `batchMutationEvents` check): refetch pin red, `expected 22 to be 1`; scope unit pin red. - B. Adapter attribution off (`if (row || !row) continue;`): 5 red (4 adapter cases plus the end-to-end 2-failure pin). - C. Core attribution off (iterate `[]` instead of `res.errors`): 7 red (core, executor and end-to-end). - D. Adapter holding off (`mutationHoldDepth < 0`): 4 holding cases red, plus the refetch pin, `expected 22 to be 1`. ## Local verification at head `90d28e7` - `pnpm exec vitest run packages/types/ packages/core/ packages/data-objectstack/ packages/plugin-grid/` under the lock: VERDICT command-exit 0, `Test Files 946 passed (946)`, `Tests 18065 passed | 27 skipped (18092)`. - `type-check` of types, core and data-objectstack: Done (at `677b254`; none of their sources changed after, only the adapter README). plugin-grid: `type-check` green at `7609f9d`, then `tsc -p tsconfig.test.json` green at head for the reworked test. That last run was a declared narrowing, unlocked, because a sibling's verify run held the shared lock for about 20 minutes. - `lint` of the four packages: exit 0 (0 errors; the repository sets no warning ceiling). - Exit 0: `check-changeset-presence`, `check-changeset-no-major`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:test-path-roots`, `check:pending-changeset-literals`, `check:changeset-claims`, `check:doc-fences`, `check:doc-json`. `check-governed-queue-guard --test` over the 14 paths: NOT GOVERNED. - NOT MEASURED locally: `check:readme-exports` and `check:doc-snippets`. Both need the whole package set built (`check:readme-exports` stopped on 23 unbuilt packages, a precondition, not a verdict). In their place, the two new guide snippets were compiled `--strict` against the built `@object-ui/types` dist: exit 0. CI builds and runs both. - `origin/main` was still at the branch point `a3c0da9` when this opened, so the instructed merge was a no-op. objectui#13450 (same batch) touches `WidgetRegistry.ts` / `widget.ts`, which are disjoint files. ## Acceptance notes - Landing beyond the claimed file surface: `content/docs/guide/data-source.md` and `packages/data-objectstack/README.md`. These are docs only, and AGENTS.md commandment #2 requires them for a public API change. No other claim holds them. - `ValueDataSource` (`packages/core/src/adapters`) implements `onMutation` but not the new optional `batchMutationEvents`. A loop of writes against it still notifies once per write. This is outside this card's file surface. Successor: none. - The `bulkDelete` fallback loop, used only when the client has no `deleteMany`, counts a `{ success: false }` delete answer as deleted. The pinned client has `deleteMany`, so this does not run today. Successor: none. - In the console a bulk run now costs at most two list refetches: the held event's release, and the dialog's Done handler that ObjectGrid already had. Session: `https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ` --- _Generated by [Claude Code](https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ke test-only reset helpers off the barrels (objectui#13453, part 1) (#13506) <!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D4nNtkcxk6i4sCKHsvYsQgG)_ Part of #13453 Clause-②: no (nothing added a consumer or author can target) Metadata-protocol: none First of the two PRs the claim amendment (comment 6393918010) cuts on the governed line. This one is Tier S: no file under `.claude/**`, `skills/**`, `AGENTS.md` or `docs/adr/**`. #13453 remains open for PR 2 (the providers trio, the contributor-guide sentence and the two `skills/objectui/guides` files, Tier H) and for the 122 dead i18n keys. ## What this removes **1. Nine whole modules with no reader** (2,211 lines plus their own tests): - `@object-ui/core`: `builder/schema-builder.ts` (`form`/`button`/`input`/`card`/`grid`/`flex` and the six `*Builder` classes) and the whole `protocols/` directory (`DndProtocol`, `KeyboardProtocol`, `SharingProtocol`, and its now-empty `index.ts`). The barrel lines become short removal notes, the same disposition `NotificationProtocol` and `ResponsiveProtocol` got. - `@object-ui/react`: `useFocusTrap`, `useFocusManagement` (`useFocusReturn`, `useRovingFocus`), `usePageTransition`, `useSchemaPersistence` (+ `createLocalStorageAdapter`), with the two `useSchemaPersistence.*.test.tsx` files. The stale `check-doc-example-types.mjs` row for the deleted hook goes too. - `@object-ui/collaboration`: `LiveCursors` (+ `LiveCursorsProps`); the package README drops its section. `usePresence` still reports cursors. - Comments that named the deleted files (`types/src/index.ts`, `crud-retirement-5373.test.ts`, `useAnimation.ts`, `useETagCache.ts` and its test) now say "since-retired". **2. `@object-ui/runner` is private and out of the release group.** `private: true`; it leaves the `fixed` group for `ignore`, because `check-changeset-fixed.mjs` wants every workspace package classified. Two consequences handled here: - Two pending changesets, `13464-declare-side-effects.md` and `25685-one-react-line.md`, each listed `"@object-ui/runner": patch`. With runner ignored, changesets refuses them as mixed ("Found mixed changeset 13464-declare-side-effects … Mixed changesets that contain both ignored and not ignored packages are not allowed", read from `pnpm exec changeset status` on this tree before the edit). The runner line is deleted from both; nothing else in them changes. `check-changeset-overwrite` reports the modification (report-only); after it, `pnpm exec changeset status --since=origin/main` exits 0. - `check-published-tsconfig-tooling-exclude.mjs` reported `FAIL [stale-carve-out] @object-ui/runner`, so its runner carve-out entry is deleted and the shipped-list pin in its test shrinks to four. `check-changeset-presence.test.ts` pinned `packages/runner/index.html` as a guarded file; an unreleased package's entry is not guarded, so the pin moves it to the not-guarded list. - Docs: `content/docs/utilities/runner.mdx` and `packages/runner/README.md` no longer say it is on npm; the root README table marks it private. **3. Test-only reset helpers off the public barrels:** `resetColumnIdentityWarnings`, `resetRetiredSortSpellingReports`, `resetDashboardFilterWarnings`, `resetReferenceKeyWarnings` (core), `resetRuntimeConfigForTesting` (app-shell), `resetInflightGetsForTesting` (types), `__clearRecordEditableCache` (plugin-detail). The module keeps the export and the barrel stops re-exporting it; the 15 test files outside the owning package that imported one from a package root now import the module's `src` path relatively, the pattern `__resetVisibilityPredicateWarnings` and `useRecordEditable` already follow. Core's four `export *` lines for those modules become named lists, with a one-line note on each. ⛔ Not here, per the claim: `resetActionKeyWarnings` (its barrel path is `core/src/actions/index.ts`) and `clearSharedDiscoveryCache` (`data-objectstack`) wait for #13442, which holds those files. ## Census before deleting (lit controls) | Repository | Read at | Searched | Hits | Control | |---|---|---|---|---| | objectui | `76c560e` (branch base) | `git grep -w` for every exported name of the nine modules and the seven helpers; generic builder names (`form`, `card`, …) searched as named imports from `@object-ui/core` | only the module, its own tests, its barrel line and comments | `SchemaRenderer` lit | | objectstack | `d1e329e` | same name list | one prose mention of `LiveCursors` in `docs/qa/platform-checklist/FOLLOW-UPS.md`; no import | `@object-ui/` lit in 150 files, `SchemaRenderer` in 38 | | hotcrm | `700b567` | same name list | none | `@object-ui/` lit in 5 files, `SchemaRenderer` in 3 | | cloud, npm dependents | n/a | **NOT MEASURED**: not reachable from this session | n/a | n/a | ## Changesets One per package, all `minor` with the removal named (`13453-core-retire-dead-modules.md`, `13453-react-retire-dead-hooks.md`, `13453-collaboration-retire-live-cursors.md`, `13453-test-helpers-off-barrels.md`). Runner gets none: it is ignored now, and naming it would be a mixed changeset. ## Verification (local) The type-check and vitest readings below were taken on the branch while it still carried PR 2's providers commits too, so the tree they read is this PR plus the providers retirement. The gates line was re-run on this PR's own head. - **type-check** (`turbo run type-check --continue`, under the verify lock): core, react, collaboration, providers, types, app-shell, plugin-detail, plugin-grid, plugin-view, plugin-calendar, console, runner and the byo example. First run caught `http-inflight.test.ts` importing the helper from `../index.js`; fixed in `44c01fe`, after which `packages/types` type-check exits 0. - **vitest, whole packages** core, types, providers, collaboration, plugin-detail, react: Test Files 1028 passed, 1 skipped; Tests 19019 passed. - **vitest, files** (every touched test file outside those packages, plus the script tests that read runner or the release config): 21 files / 432 tests, one red, `check-changeset-presence.test.ts` (the runner pin above), fixed in `45580a0` and re-run green (53/53). Console project, the three touched files: 28/28. A wider sweep of 95 `scripts/__tests__` files that mention runner, providers or the release config: green apart from `check-doc-links.test.ts` and `check-skills-paths.test.ts`, which read paths PR 2 deletes; both scripts exit 0 on this PR's head. - **gates**, at `a5dc7bb`: `check-changeset-presence`, `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-claims`, `check-pending-changeset-literals`, `check-published-tsconfig-tooling-exclude`, `check-new-cross-file-line-citations`, `check-control-bytes`, `check-skills-paths`, `check-doc-links` all exit 0; earlier on this branch also `check:phantom-deps`, `check:unused-deps`, `check:lockfile-integrity`, `check:lockfile-dedupe`, `check:test-path-roots`, `check:unreferenced-sources`, `check:side-effects-array`, `check:sdui-registration-pins`, `check:self-import`, `check:vi-mock-specifiers`, `check:lint-rule-coverage`, `check:node-esm-load`. - **eslint** on the touched files with the repo config: 0 errors. With `--no-inline-config` there are 2 errors, both on untouched lines and both present at the base (`useETagCache.ts` preserve-caught-error, `types/src/index.ts` restricted `FormField` import). - **NOT MEASURED locally:** the whole `app-shell` package suite (its touched files ran), `check:readme-exports` and `check:doc-snippets` (need every package's `dist`); CI runs them. ## Follow-ups - **Stale published guides until PR 2 lands:** `skills/objectui/guides/project-setup.md` and `skills/objectui/guides/architecture.md` still describe `@object-ui/runner` as a standalone runtime; PR 2 fixes them (Tier H). - **Release-lane hazard to watch:** any open PR whose new changeset lists `"@object-ui/runner"` alongside other packages becomes a mixed changeset once this merges, and nothing on a PR runs `changeset status`. None of the pending changesets on `main` at `5399bbc` names it. - `origin/main` moved two commits (#13501, #13503) after the branch base; neither touches a file in this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_011yfYeuX2ptMSCcYZiq61tS)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… and ThemeProvider trio and fix the guides that taught them (objectui#13453, part 2) (#13507) <!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D4nNtkcxk6i4sCKHsvYsQgG)_ Part of #13453 Clause-②: no (nothing added a consumer or author can target) Metadata-protocol: none ## 维护者速读(草稿) - **改了什么:** `@object-ui/providers` 删掉三个和别的包重名的 Provider:`DataSourceProvider`、`MetadataProvider`、`ThemeProvider`(连同 `useDataSource` / `useMetadata` / `useTheme` 和四个类型)。包里只留上传用的 `UploadProvider` / `useUpload`。同时改好引用它们的地方:byo 示例、README 和架构文档、贡献者技能指南里的一句话、对外发布的 `skills/objectui` 两篇指南。另外把 #13506 已改成私有的 runner 从这几篇指南里拿掉。 - **为什么改:** 这三个名字在 `@object-ui/react`(数据源、主题)和 `@object-ui/app-shell`(元数据)里都有同名但实现不同的版本,console 用的都是那两个包的。providers 这份在三个仓里没有任何代码引用,只出现在文档和一个示例里,AI 和开发者照着文档写,很容易引错包。 - **风险与代价(含回滚):** 对 npm 上的外部使用者是破坏性删除。按版本政策只声明 `minor`,changeset 里写明删了哪些名字,并给出迁移路径。cloud 和外部 npm 依赖方没有测量到。回滚就是 revert 这一个 PR,没有数据或接口协议上的影响。 - **席位意见:** - **你要做的:** 审阅后在本 PR 点 Approve(os-zhuang 或 hotlong 任一人即可),之后由席位落地。 ## What this removes Second of the two PRs the claim amendment (comment 6393918010) cuts on the governed line; PR 1 (#13506, Tier S) merged at `0063f22`. This one is Tier H because it edits `.claude/skills/objectui-contributor/guides/console-development.md` and `skills/objectui/**`. **1. The providers trio.** `packages/providers/src/{DataSourceProvider,MetadataProvider,ThemeProvider}.tsx`, `types.ts`, and their two tests (`spec-symbol-batch7.test.ts`, `theme-mode-spec-parity.test.tsx`) are deleted. The barrel keeps only the upload surface (`UploadProvider`, `useUpload`, the upload adapters), with a note on what retired and where each replacement lives. Removed names: `DataSourceProvider`, `useDataSource`, `MetadataProvider`, `useMetadata`, `ThemeProvider`, `useTheme`, and the types `DataSourceProviderProps`, `MetadataProviderProps`, `ThemePreferenceProviderProps`, `ThemePreference`. Premise correction carried from the claim: `@object-ui/react` does not export `DataSourceProvider`, `useDataSource` or `MetadataProvider`. The replacements are: | Retired (`@object-ui/providers`) | Use instead | |---|---| | `DataSourceProvider` / `useDataSource` | `SchemaRendererProvider` (`dataSource` prop), `@object-ui/react` | | `ThemeProvider` / `useTheme` | `ThemeProvider` / `useTheme`, `@object-ui/react` (`defaultMode`; the old `system` is `auto`; `persist` keeps the choice in `localStorage`) | | `MetadataProvider` / `useMetadata` | `MetadataProvider` / `useMetadata`, `@object-ui/app-shell` | **2. The one code consumer, the byo example.** `examples/byo-backend-console/src/App.tsx` now wraps in `ThemeProvider defaultMode="light" persist storageKey=…` and `SchemaRendererProvider` from `@object-ui/react`; its `@object-ui/providers` dependency and the lockfile entry go. Its README follows. **3. Docs and exemptions that named the trio.** `README.md`, `docs/ARCHITECTURE.md` (its compiled snippets now import the replacements), `examples/README.md`, `packages/providers/README.md`, the `types/src/theme.ts` comments, and two test comments that pointed at the deleted files: `p2-spec-exports.test.ts`, and the `KNOWN_COLLISIONS` note in `one-authority-per-exported-name-6273.test.ts` (no entry changes; it now records that react's `ThemeProvider` is the only one left). `docs/audits/2901-spec-enum-renderer-coverage.md` unlinks the deleted `ThemeProvider.tsx` path (read by `check-doc-links`). **4. Governed text.** - `.claude/skills/objectui-contributor/guides/console-development.md`: the paragraph that called `MetadataProvider` "the one name with two hits" now says the app-shell one is the only one left (`check-skills-paths` reads the deleted path). - `skills/objectui/guides/project-setup.md`: the provider section and the decision matrix teach `SchemaRendererProvider` / `ThemeProvider` (react), `MetadataProvider` (app-shell) and `UploadProvider` (providers); the `@object-ui/runner` section becomes "use `objectui dev`", since #13506 made runner a private workspace app. - `skills/objectui/guides/architecture.md`: the integration-package bullets follow the same split and drop runner. - `skills/objectui/README.md`: runner leaves the Integration package list. ## Census before deleting (lit controls) | Repository | Read at | Searched | Hits outside `packages/providers/src` | Control | |---|---|---|---|---| | objectui | `0063f22` (main) | `git grep` for every removed name and for `from '@object-ui/providers'` | docs and READMEs, the guide files above, and `examples/byo-backend-console/src/App.tsx` (the only code import) | `UploadProvider` imports from `@object-ui/providers` lit in the console | | objectstack | `5874d25` | `@object-ui/providers` | none | `@object-ui/` lit in 150 files | | hotcrm | `a7faf81` | `@object-ui/providers` | none | `@object-ui/` lit in 5 files | | cloud, npm dependents | n/a | **NOT MEASURED**: not reachable from this session | n/a | n/a | ## Changeset `13453-providers-retire-trio.md`: `@object-ui/providers` `minor`, every removed name listed, with the migration above. Never `major`, per the version policy. ## Verification (local, on this head) - **type-check** (`turbo run type-check --continue` under the verify lock, filters providers, types, console, the byo example; 39 tasks with dependencies): 39 successful. - **vitest**: `packages/providers`, `p2-spec-exports.test.ts`, `one-authority-per-exported-name-6273.test.ts`, `check-skills-paths.test.ts`, `check-doc-links.test.ts`, `quick-reference-commands-4149.test.ts`, `check-skill-examples.test.ts`: 7 files, 318 tests passed. - **gates**: `check-doc-links`, `check-changeset-no-major`, `check-changeset-fixed`, and `pnpm exec changeset status --since=origin/main` all exit 0. - Earlier, on the unsplit branch that carried PR 1 plus this diff: whole-package vitest of core, types, providers, collaboration, plugin-detail and react passed (1028 files, 19019 tests). - **NOT MEASURED locally:** `check:readme-exports` and `check:doc-snippets` (need every package's `dist`); CI runs them. ## Still on #13453 after this - `resetActionKeyWarnings` and `clearSharedDiscoveryCache` wait for #13442. - The 122 dead i18n keys, after #13460's PR merges and a claim amendment. - `plugin-ai` and folding `UploadProvider` are out of scope (maintainer's call). --- _Generated by [Claude Code](https://claude.ai/code/session_011yfYeuX2ptMSCcYZiq61tS)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <jack@objectstack.ai>
Fixes #12738
Implements ruling C' (maintainer, 2026-08-28, recorded on the card): each dialect declares the semantically correct JSON column type. Neither of the card's own two directions is taken.
What changed
SqlDriver.createColumnhad two JSON routes, both hard-wired to knex'stable.json(name)on every dialect. They now go through one seam,SqlDriver.jsonColumn, which emitstable.text(name)on the SQLite family andtable.json(name)everywhere else.jsonjson— untouchedjsonjson— untouchedjsontextPre-work verification (ruling fence 1) — read, then measured
The C' scope claim rests on knex's actual DDL and on which dialects share the arm. Both were measured, not assumed.
knex's
t.json(), read in source then compiled: the base compiler is already correct —ColumnCompiler.prototype.bit = ColumnCompiler.prototype.json = 'text'. The sqlite3 dialect is the one that overrides it:ColumnCompiler_SQLite3.prototype.json = 'json'. Compiled DDL fort.json('v'):So
jsonon SQLite names a type the engine does not have. SQLite derives affinity from substrings of the declared type name, andjsoncontains none of the markers (INT,CHAR/CLOB/TEXT,BLOB,REAL/FLOA/DOUB), so it falls through to NUMERIC. Measured through raw SQL on such a column: a bare'0123'is stored as the integer123.Which dialects reach the arm. The two
createColumnsites are shared by every dialect, so the discriminator isthis.isSqlite. Its coverage was verified rather than presumed:SqlDriveronsqlite3/sqlite/better-sqlite3;TursoDriverin all three transport modes — local, embedded-replica and remote all configureclient: 'better-sqlite3';SqliteWasmDriver, which passes a knex Client constructor (so the string match answers'') and therefore overridesisSqlitetotruefor exactly this reason. An inlineconfig.clientstring test written at the call site would have silently missed the sibling SQLite dialect.One of the three named sites is deliberately NOT changed
The dispatch named three JSON-mapping sites. I changed two.
buildRebuiltColumn(sql-driver.ts:11096) maps an introspected existing column type back to a builder for a SQLite table rebuild — its input is a column's current declaration, not a field's metadata. Routing it through the new emitter would make an unrelated drift rebuild (a relaxed NOT NULL, a dropped column) re-declare a legacyjsoncolumn astextand, because the rebuild'sinsert ... selectcopies rows through the new affinity, rewrite the stored bytes of live data. That contradicts the ruling's own migration shape, so the arm keeps its behaviour and now carries the reason in code. Columns created after this change arrive there declaredtextand take thetextarm, so the rebuild is stable in both eras with no special-casing.Instrument: affinity-level, not
VALUE_ROUNDTRIPThe card's own negative result is that the shared round-trip case-set is green on both sides of this decision, so it cannot adjudicate direction. The pins added here assert, on both transports: the declared type from the catalog,
typeof(col)storage class per case, and a raw-SQL bare'0123'that must survive as text.Measured convergence, from the pin itself: the local/remote pair moved from
['json', 'TEXT']to['TEXT', 'TEXT'], and the list of cases whose storage class differs between the transports went from two entries to empty.A detail worth recording: knex writes
`v_json` text(lower case) into the storedCREATE TABLE, andpragma_table_inforeports it back asTEXT— so the two transports turn out to declare the byte-identical type string, not merely the same affinity class. The pin still asserts affinity as the primary property, because that is what matters and an exact-string pin would go red over a cosmetic re-spelling.Pins inverted in place, never re-baselined
Three suites pinned the old design. Each is inverted with the reason recorded, and each keeps a control that can still fail:
turso-json-column-type-asymmetry.test.ts— inverted per its own header, which instructed exactly this. It now pins the convergence. A new negative control creates a legacyjsoncolumn by hand and shows NUMERIC affinity is still real on the same SQLite build, so the "survives as text" probe cannot pass vacuously. The filename is kept: two source comments and a landed changeset refer to it by name.sql-driver-12380-json-roundtrip.test.ts— the catalog-type assertion is now dialect-aware. The affinity block is split: §A the column the driver emits today (text, no NUMERIC affinity), §B a legacyjsoncolumn where the original [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 measurements are asserted unchanged. That is the ruling's requirement that those pins stay green and stay in force for legacy columns. The legacy-corpus migration suite now builds its table from the driver's ownCREATE TABLEwithvalput back tojson, and asserts the rewrite landed — otherwise the corpus would have quietly contained no legacy cells at all and every assertion would have passed while measuring nothing.schema-drift.base-type-mismatch.test.ts— the fresh-table type expectation is dialect-aware. The inversion reinforces the suite: SQLite was already excluded from the multi-value base-type finding because the column type is not load-bearing there, and the emitter now agrees with the differ instead of merely being excused by it.Migration shape
New columns only. Existing columns keep their declared type, keep NUMERIC affinity, and keep the injective codec that defends them. Platform write-path behaviour is unchanged. No new schema-drift findings. Nothing on the read path consults the physical column type —
isJsonFieldanswers from metadata. The only visible difference is raw-SQL-only and in the safe direction: a value that would previously have been coerced to a number is now preserved as text; nothing preserved before is coerced now. Stated in the MINOR changeset.Unmeasured premise, passed through undiluted
The ruling flagged its own: "The replica DDL-sync detail is reasoned from the product model, not measured; the dev verifies before relying on it." It remains unmeasured — exercising embedded-replica schema sync needs a live Turso primary, which this container has none of, and the libsql test stub is a local better-sqlite3 double with no sync. Nothing here relies on it. C' is justified entirely by the measured affinity trap. The change also makes the question moot in the direction it was raised: all three turso modes now declare
TEXT, so a deployment's physical schema no longer depends on which side ran DDL.Verification at
8058e2b60629 derived gate families green, re-derived against the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafter mergingorigin/mainto clear a stale-tree warning. Includescheck:driver-conformance,check:test-source-alias,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:dispatcher-error-vocabulary,check:nul-bytes, and thecheck:type-check-debtratchet, which was run only after building the full package closure — it refuses outright on an unbuilt worktree, and that refusal is NOT MEASURED rather than green. It reports 29 ledger entries re-measured, none above its recorded number.The two
typecheckscripts were confirmed withtsc --listFilesto actually compile the three edited test files, so "typecheck clean" covers the edits rather than skipping them.Generated by Claude Code