Uh oh!
There was an error while loading. Please reload this page.
feat(cli): os migrate multi-value-columns — the operator-run stale-column migration - #11894
Conversation
… migration The operator-run half of #11535, ruled C on #11700: the platform warns and ships an explicit migration an operator invokes, and never runs it for them. The statement is the one driver-sql's `manual_column_type_change` finding prints (#11720, measured against live Postgres 16.13 / MySQL 8.0.46); the command refuses to execute anything the finding does not contain verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 30 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 380c0531cf24c529df81b6ef2ac48989637886c2 && git checkout 380c0531cf24c529df81b6ef2ac48989637886c2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b94a2ff28023e47acb6ac3a4414614b32f85cef 694b8785c11125792440e3450b781710f7b722f5 && git checkout -B drift-repro 3b94a2ff28023e47acb6ac3a4414614b32f85cef && git merge --no-ff 694b8785c11125792440e3450b781710f7b722f5
node scripts/docs-audit/affected-docs.mjs --json 3b94a2ff28023e47acb6ac3a4414614b32f85cef
|
…ing it Re-exports `manualJsonConversionSql` from driver-sql's index (one line) and has `os migrate multi-value-columns` import it, dropping the CLI's copy. The import is lazy and injected: no CLI production module may statically value-import a driver package (#5726), so the command `await import()`s it at the point of use and hands it to the planner, which stays synchronous. The runtime containment check survives, with a different claim: it is now only the DIALECT probe (a ManagedDriftEntry carries no dialect), matching the engine's finding against the engine's own function. Its refusal branch is still reachable. `remedy-fidelity` becomes `dialect-probe`, minus three cases that could no longer fail for any reason belonging to this package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
os-zhuang
commented
Aug 25, 2026
Review — ACCEPTED on substance. ⛔ NOT armed: CI has not converged, and the Clause-② gate is now hung.
Two claims re-measured independently rather than taken from the reportThe driver-sql change is exactly what was stated. Diffed against the merge base myself: One line, inserted alphabetically into the existing The guarded copy is gone and the import shape is right. Measured on the branch:
What this round did that goes beyond following the orderThe judgement call was answered by changing the claim, not by keeping or deleting the check. I left the runtime containment check to your judgement. You kept it and rewrote its comment to say what it now proves — its drift-guard job is dead ( The three deleted cases were sorted by why, and the distinction is the valuable part. One was vacuous (it tested a CLI builder that no longer exists). Two were mis-homed — they can still fail, but only for a reason belonging to The cross-package ablation carried a rebuild on both legs. The dialect-probe coupling pin resolves Three refusals were named and resolved, not counted. ⛔ Two things that gate arming1. CI has not converged. At the time of this read: 32 check runs, 10 2. Clause ② = yes, and the gate was missing from both carriers. That is my omission, not yours: the card was dispatched Clause-② yes and Noted, not actioned here
Generated by Claude Code |
os-zhuang
commented
Aug 25, 2026
CI converged GREEN. ⛔ Still not armed — the Clause-② gate is the only thing left, and this seat measured itself ineligible to clear it.
CI, read by name rather than by aggregate: 38 check runs, all The downgrade fuse, measured rather than recalled:
Not equal ⇒ this seat is below the contract-review tier and may neither review this card nor clear its label. The label stays hung on both carriers; a card waiting outside the queue is the safe state. ⛔ I checked this rather than repeating my own seat post's standing note that the lane has no So the state is: substance ACCEPTED, CI green, head stable, waiting on the contract-review chain — which records its verdict on the card, #11733, per Generated by Claude Code |
os-zhuang
commented
Aug 25, 2026
ACCEPT — armed (ready + auto-merge SQUASH)Card #11733. Every precondition discharged against a reading. Contract-review gate — PASS, found on the first look because I looked in the right place. The verdict is card #11733 comment Worth stating plainly, since the opposite cost this lane a day earlier tonight: the gate label was cleared from both carriers and the verdict was on the card. Earlier today I read a clear of exactly this shape as an unexplained strip, because I searched the PR — where a verdict never lands — instead of the card, which is where CI, re-read after the gate cleared rather than reused from before it. The label write fired three fresh runs at 00:41. Current: 41 check runs, all Head unchanged — What the review chain confirmed independently of mine, kept on the record: ruling C holds on every face (dry run is the default, the executor's Follow-up on merge:#11893 closes — it is absorbed by commit Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11733
The operator-run half of #11535, executing the maintainer's ruling C on #11700: the platform warns and ships an explicit, operator-run migration command, and never runs it for you.
os migrate planhas reportedmanual_column_type_changesince #11720 — a field that gainedmultiple: trueover an existing database keeps its oldvarchar/textcolumn, arrays are stored as the stringified literal'["a","b"]', and consumers receive one opaque id instead of a list. Until now the report was all there was. This adds the command an operator runs deliberately, on their own schedule, with their own backup taken.What the command is
os migrate multi-value-columns, beside the five existingos migratedata-migration siblings.--apply--yes)--yes/-y--force--table <name>--database-url$OS_DATABASE_URL/ the project DB)--jsonAfter a successful apply it re-runs detection and requires the finding to be gone; statements that succeeded while the column is still reported exit non-zero rather than claiming a migration that did not happen. Rollback notes are printed by the command and documented in
content/docs/deployment/cli.mdx.The SQL is imported from the engine, not copied
The remedy was corrected twice by live measurement while #11720 was written (
json_build_arrayoverto_json, which yields a JSON scalar; and the explicitIS NULLarm, becausejson_build_array(NULL)is[null]). There is now exactly one definition of it, in the package that measured it.packages/drivers/driver-sql/src/index.tsgains one line —manualJsonConversionSqljoins the values block it was missing from — under the cross-domain single-PR designation on the card. Nothing else in that package changes.The command imports it lazily and by injection, which is mechanical rather than stylistic: no CLI production module may statically value-import a driver package. oclif
import()s every command module on every invocation while building its command table, so one static driver import makes an unbuiltdriver-sql/distprint aMODULE_NOT_FOUNDblock for each of the nine commands sharing that chain, in front of whatever the operator actually ran (#5726). Sorun()doesawait import('@objectstack/driver-sql')at the point of use and hands the builder toplanStaleColumnTargets, which stays synchronous and pure.schema-migrate.lazy-driver-import.test.tsis in this PR's test set and passes.What survived the import, and why — the judgement call
The runtime containment check stays, with a different claim, and its comment now says so.
It used to do two jobs. As a drift guard — proving the CLI's copy still equalled the engine's — it is dead:
sqlismanualJsonConversionSqlitself, so the two sides cannot disagree and a test asserting they match could not fail. As the dialect probe it is the only thing doing that job: aManagedDriftEntrycarries no dialect, and the alternative is a second copy of the driver's client-spelling table (postgres/pg/postgresqlare one dialect under three names — getting that list wrong is a measured defect class), which could only disagree with the driver's. So the dialect is still read off which dialect's statement the finding contains, and the refusal branch is still reachable and load-bearing: a finding whose message stopped embedding the remedy yields no dialect, and the command refuses rather than guess which dialect's DDL to run against a customer's table.multi-value-columns.remedy-fidelity.test.tsis therefore renamed tomulti-value-columns.dialect-probe.test.ts, and three cases were deleted rather than left unable to fail:manualJsonConversionSql(dialect, …).JSON_ARRAYpresent,json_build_arrayabsent) — these now assertdriver-sql's content from a consumer's suite. Strictly they are not vacuous, and that is the problem: they could only ever fail for a reason belonging to another package, turning a deliberate engine correction into a red CLI suite.driver-sqlowns them and pins them inschema-drift.base-type-mismatch.test.ts, where they are also executed against live servers.What is kept is about this package: the coupling the probe reads (the finding still embeds the remedy — nothing in the CLI can keep that true and everything in the CLI depends on it), that the two dialect forms are distinguishable, the statement split, and the plan/refusal behaviour.
SQLite is excluded by measurement, not convenience: it reads a stale column back as a real array, so
diffManagedTableraises nothing there and the command has nothing to act on.Option A stays rejected
Nothing on the boot path invokes this command, and it does not route the remedy through the reconciler — it runs the engine's statement through the raw seam, only after
--apply.multi-value-columns.no-auto-run.test.tspins that, with positive controls first: the same scanner findsserve.ts's dynamic import of thekernel:readymigration gate (the exact shape an accidental auto-run would take) and the many importers ofschema-migrate.js, and the same reading findsapplyMigrationEntriesinapply.tsbefore reporting it absent here.manual_column_type_changekeeps no reconciler arm.Historical data is out of scope by the ruling's own words — no backfill, no row repair.
Pins, each shown failing without the behaviour
Reverse-verification ran from the committed state; every mutation was proven on disk (injected/removed counts) and restored, under a
trap … EXIT INT TERMso a cap kill could not leave the tree mutated. The first pin now crosses a package boundary, so it carries the rebuild the others do not need: the CLI suites resolve@objectstack/driver-sqlthrough itsexportsmap — that isdist/, notsrc/— so an unrebuilt mutation there would have left the suite green and proved nothing. Both legs rebuilt and were checked withscripts/ablation-dist-preflight.mjs:marker present in 2 built fileson the mutation leg,marker absent from all 6 built fileson the restore leg.driver-sql, stop the finding message embedding the remedy (on-disk: injected 1, removed 0) — then rebuilddriver-sql, since the CLI suites resolve it throughexports, i.e.dist/CORRUPTING_DIALECTS.find(… message.includes …)→CORRUPTING_DIALECTS[0](on-disk: injected 1, removed 0)if (!apply) continue;→if (false) continue;(on-disk: injected 1, removed 0)import './multi-value-columns.js';tocommands/migrate/index.ts(on-disk: 0 → 1 occurrence, 12 → 13 lines)migrate multi-value-columns, green once registeredThe dry-run pin, specifically. It is not "it printed something": a real SQLite database is built with all four row states, the column's declared type and every row are snapshotted, the dry run is executed with a seam that counts calls, and the assertion is
seamCalls === 0plus a byte-identical snapshot afterwards. Its positive control runs the same plan through the same snapshot function with--applyand requires the reading to move — column typetext→json,'a'→["a"],''andNULL→NULL. A snapshot that never moves is an instrument, not a result. A second case dry-runs the plan built from the engine's real finding against anexecthat throws if touched.Checks
Union run at
694b8785c, the branch head, on a clean tree. Exit codes captured before any pipe; verdicts quoted from each tool's own line.pnpm --filter @objectstack/driver-sql typecheckandpnpm --filter @objectstack/cli typecheck— both exit 0pnpm lint(repo-wideeslint . --no-inline-config) — exit 0. Not narrowed.test/json-stdout-purity.e2e.test.ts+src/utils/schema-migrate.lazy-driver-import.test.ts— 5 files, 58 tests passeddriver-sql'sschema-drift.base-type-mismatch.test.ts— 14 passed, 3 skipped (the live PG/MySQL cells, unprovisioned here; they are fix(driver-sql): report a multi-value field left on a stale varchar/text column #11720's own coverage and CI's)scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafterdriver-sqlentered the change set (8 paths vs merge base589758d22). One new family,check:driver-conformance— exit 0. All previously-derived families re-run on the widened set — exit 0, includingcheck:cross-package-test-inputs,check:published-files,check:test-source-alias,check:type-source-resolution,check:slot-lookup,check:engine-double-contract,check:where-matcher,check:cli-test-child-env,check:type-check-coverage, the.changesetgates andcheck:nul-bytescheck:i18n(This gate runs the BUILT CLI … Nothing was checked) → built the closure, re-ran:OK (9 package(s) — all bundles in sync).check:i18n-coverage(COULD NOT MEASURE — 1 of 12 config(s) failed to lint) → same closure, re-ran:OK (12 config(s), 657 baselined untranslated string(s), none new).check:type-check-debt --re-measure(@objectstack/service-knowledgehad no built type entry point) → built it, re-ran:OK — 32 ledger entr(ies) re-measured, 1898 raw tsc error(s) total, none above its recorded number— the same totals as before the export line, which moved nothing.Docs
content/docs/deployment/cli.mdxgains a#### os migrate multi-value-columnssection (what the drift is, per-dialect statements, verification, rollback, the out-of-scope note) plus a row in the schema-migration table — so the finding's message has something real to point at. Wiring that URL into the finding text needsdriver-sql, which is read-only here.Generated by Claude Code
Generated by Claude Code