You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
17.6.0: os migrate meta --stored (preview) and os migrate audit-metadata-bodies (dry run) boot the app's seed loader and write to application tables #21349
On 17.6.0, two commands documented as read-only previews write to the application's tables:
os migrate meta --stored without --apply;
os migrate audit-metadata-bodies without --apply (new in 17.6.0).
Both boot the app's seed loader against the target database. It rewrites seeded rows, and it logs an error when it tries to write to a view. The 17.5.0 CLI, run on the same file, changes nothing. So for --stored this is a 17.6.0 regression.
The 17.6.0 upgrade checklist tells every operator to run both commands first as previews:
os migrate meta --stored, then --stored --apply;
os migrate audit-metadata-bodies ("a dry run by default"), then --apply.
The preview should leave the database unchanged.
Found by the HotCRM 17.6.0 upgrade lane of the 17.6.0 release verification (objectstack-ai/hotcrm#1982, "Platform issues → A"). That session could not attach this repository, so it is filed from the release session.
Steps
Create a SQLite database with HotCRM on 17.5.0: boot it, let the seed load run, and sign up the first admin. Copy the file.
With @objectstack/cli@17.6.0, run os migrate meta --stored --database-url file:<copy>. Do not pass --apply.
Compare row hashes of the app tables before and after. Do the same for os migrate audit-metadata-bodies --database-url file:<copy> without --apply.
Expected
A read-only preview: the file is unchanged.
Actual
The command boots the app's seed loader. On crm_contact, crm_lead, crm_opportunity and five more tables it:
bumps updated_at;
stamps organization_id on seeded rows that had none;
rewrites relative-date seed values.
It also logs ERROR [SeedLoader] Failed to write sys_activity …: cannot modify sys_activity because it is a view, because sys_activity is a rotation view on that database.
Comparison
The 17.5.0 CLI, running os migrate meta --stored on the same file, leaves every row hash identical and logs no SeedLoader error.
audit-metadata-bodies is new in 17.6.0, so it has no 17.5.0 baseline. Its dry run makes the same writes.
Triage: grade completed — priority:p1 · area:devpath · pm:queue. A preview never writes: neither migrate preview boots the seed loader. The filer's bug · domain:cli stand
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T06:55Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p1. The 17.6.0 upgrade checklist tells every operator to run these previews first, and they rewrite seeded rows in application tables. For --stored, that is a 17.6.0 regression: an operator's safety step mutates their data.
Direction.
Without --apply, both commands boot without the seed loader, and with no write path to application tables. The datasource is read-only for the run.
⛔ No "skip these tables" list. The preview has no write path at all.
--apply behaviour is unchanged.
Pins: the card's row-hash comparison, before and after each preview, is byte-identical on SQLite and on one live dialect. --apply still applies (the control).
Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p1, to triage's direction 5947006439. Landed as Fixes #21349.
Session: session_01VvcEokUG1tvVxkceYfR5XB
Account: huangyiirene
Branch: claude/issue-21349-migrate-preview-read-only
Worktree: objectstack-issue-21349
Domain: domain:cli
Seat: domain:cli#1
File surface, derived at origin/mainf39760864c:
The direction, as built: without --apply, both os migrate meta --stored and os migrate audit-metadata-bodies boot without the seed loader, and with no write path to application tables. The datasource is read-only for the run. ⛔ No "skip these tables" list. --apply is unchanged.
Read at f39760864c:
Both commands boot through bootSchemaStack (packages/cli/src/utils/schema-migrate.ts): meta.ts:836 and audit-metadata-bodies.ts:142.
bootSchemaStack already carries the read-only boot that os migrate plan uses, the deferSchemaDdl / readOnlyProbe options, whose header says the seed is suppressed so "the boot is read-only".
The dev first measures what boots the seed loader on these two previews, and whether that existing mode closes every write path. That mode is reused, not a second mechanism.
packages/cli/src/utils/schema-migrate.ts, schema-migration-plugins.ts and data-migration-plugins.ts, only if the existing read-only mode needs a hook;
pins beside them.
Pins: the card's row-hash comparison, before and after each preview, is byte-identical on SQLite and on one live dialect. --apply still applies, as the control. Both are measured red before the fix.
Docs: if the os migrate meta or os migrate audit-metadata-bodies entries in content/docs/deployment/cli.mdx (### Schema migrations) promise a read-only preview, they stay true. If a sentence the fix falsifies exists, it is corrected there.
Changeset: one .changeset/21349-*.md for @objectstack/cli, at the level the real diff takes.
Stop clause: stop if a read-only datasource needs a driver change (packages/drivers/*, domain:engine), or if the only route is a table skip list.
(stop on a breach outside these; explain in the report)
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; the default).
Clause-②: no. A preview that wrote was never a declared behaviour, so no accept set and no public member moves. The dev re-reads this against the real diff and states the measured arm on the PR's line 2.
Thread-read: 5947006439
Serial constraints cleared: Of the 8 open PRs (all file lists read in this act), none touches packages/cli/src/commands/migrate/, schema-migrate.ts, schema-migration-plugins.ts or data-migration-plugins.ts. No in-flight claim of this seat or another names them. The ### Schema migrations section of cli.mdx is disjoint from #21324's #### os verify entry and from PR #21369's hunks.
{
"issue": 21349,
"status": "done",
"branch": "claude/issue-21349-migrate-preview-read-only",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21389",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Without --apply, `os migrate meta --stored` and `os migrate audit-metadata-bodies` now boot through the read-only boot `os migrate plan` already uses (bootSchemaStack deferSchemaDdl + readOnlyProbe): schema DDL is held back, the inline seed loader is suppressed, and a missing SQLite file is not created. --apply keeps the plain boot. Reproduced on examples/app-crm (os dev seeded 28 rows and the first admin): before the fix each preview rewrote all 28 seeded rows in 5 app tables (updated_at bumped, organization_id stamped); after the fix the schema and every row hash are identical and the report is unchanged. The core premise holds. The 17.5.0-regression part did not reproduce: the 17.5.0 CLI rewrote the same 28 rows on app-crm, and `meta --stored` has booted this way since 83cf2d3082. One edge now behaves differently: a preview at a database that lacks the table it reads exits 1 instead of creating the table (stated in the changeset and the PR).",
"tests": "All at a67e290cc7 (the branch merged with origin/main 11905a4f8b). (1) `pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck`: VERDICT command-exit 0, check:test-typecheck OK. (2) `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: Test Files 246 passed (246), Tests 3489 passed (3489), VERDICT command-exit 0. (3) integration, `--project integration --reporter=verbose` over preview-read-only + meta.stored-flow-resolution + schema-migrate.deferred-ddl + platform-migrations-arming, with OS_TEST_POSTGRES_URL pointing at a private local PostgreSQL 16: Test Files 4 passed (4), Tests 23 passed (23). The new file without the URL gave 6 passed and 1 skipped (the named live-cell skip). Integration beyond these files is declared to CI. (4) dispatch-gates: 63 derived commands run one by one, exit codes captured before any pipe; `--ran` gave 63 run / 0 NOT-MEASURED / 0 UNRUN (verdict exit 0). check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: no dist in 9 packages outside the CLI closure); after a turbo build of those 9 (42/42 cached) both re-ran with exit 0. (5) Lint as a proven narrowing: eslint with the repo config and --no-inline-config, --format json, over the 3 changed TS files: 3 files linted (population read from the JSON output, none reported ignored), 0 errors, 0 warnings. The config enables no type-aware linting (eslint.config.mjs says so: no parserOptions.project), so these files cannot move any untouched file's verdict. Full `pnpm lint` declared to CI. (6) check-live-db-isolation: PASS, with the new file among 43 scanned live files. Ablation: see the ablations field. Each leg was a WRAP-mode `scripts/ablation-replace.mjs` run (anchor x1 then x0, marker x0 then x1, blob changed); restore proven blob == HEAD and `git diff HEAD` empty; the subject resolves to src, so no rebuild between legs.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21389; read-back 10117 bytes sent = stored); (2) label-write --assign → POST /repos/objectstack-ai/objectstack/issues/21389/assignees (read-back matches: assignee huangyiirene; size/l was added by another actor); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/21349/comments. git push (not REST): 4 pushes to the branch (empty marker, fix, pins, merge of main). Reads: single-card REST reads of #21349 and its comments, one check-runs read.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: public door — each of `os migrate value-shapes`, `os migrate summary-nulls`, `os migrate recorded-by`, `os migrate resume`, `os migrate files-to-references` (dry run by default) and `os secret orphans`, `os storage orphans` (report-only by default), run with --database-url against an examples/app-crm database at a67e290cc7, rewrote the same 28 seeded rows in 5 app tables ([Seeder] \"updated\":28; updated_at and organization_id changed); `os migrate account-issuer` and `multi-value-columns` already boot read-only and left it identical · contract: content/docs/deployment/cli.mdx says \"os migrate value-shapes # Scan: full report, writes nothing\" (L1051), \"os migrate summary-nulls # Dry run: full report, writes nothing\" (L1103), \"os migrate files-to-references # Dry run: full report, writes nothing\" (L998), and for os secret orphans \"Report-only by default: without --delete it writes nothing and deletes nothing.\" (L530) · Seam: spec:none (CLI-declared dry-run contract) → runtime:packages/cli/src/utils/schema-migrate.ts bootSchemaStack (createStandaloneStack skipSeedData keyed on deferSchemaDdl) | consumer: none · same family as #21349: name it for ONE family closure card, not seven single cards. A family-wide fix, keyed like runPlatformMigrations:false (seed off on every one-shot CLI boot, DDL deferred on every dry run), would also cover --apply · dedupe words: \"dry run writes seeded rows\", \"bootSchemaStack skipSeedData\", \"one-shot CLI boot seed loader\", \"report-only command rewrites rows\", \"migrate dry run updated_at\"",
"carrier: the family closure card above (it edits schema-migrate.ts boot options) · noted, not filed — two comments now say more than is true: packages/cli/src/utils/schema-migrate.ts (the runPlatformMigrations:false block, around L319-329) and packages/cli/src/utils/platform-migrations-arming.integration.test.ts (around L321-326) list `os migrate meta` among the boots without deferSchemaDdl. After #21389 that is true only with --apply. Not edited here because both files are outside the claim's surface.",
"carrier: 承接者:无 · noted in PR acceptance notes only — `--apply` on these two commands still runs the boot seed loader and schema sync (a write the operator never saw in the preview); the direction keeps --apply unchanged."
],
"write_paths": [
"BASE 3937ad2f32, plain boot taken by both previews: (1) AppPlugin.start inline seed (SeedLoaderService upsert of every artifact dataset) — measured on examples/app-crm: 28 rows across crm_account/crm_activity/crm_contact/crm_lead/crm_opportunity, updated_at bumped, organization_id stamped with the admin org on rows that had none; in the pin an operator edit (status won) was put back to the seed value (open). Source: createStandaloneStack skipSeedData is set only when bootSchemaStack is given deferSchemaDdl, and neither command passed it.",
"(2) ObjectQLPlugin boot schema sync and AuditPlugin.provisionSystemTables (syncObjectSchema) — on a database behind the app schema, measured with the same plain boot: a dropped crm_lead.phone column was added back, and sys_audit_log was created with 5 named indexes.",
"(3) the sqlite driver opening the target in create-if-absent mode — a preview at a missing path created the file (pin L3/L5 legs).",
"Checked and not a write path on this boot: platform repair migrations (runPlatformMigrations:false already), host onEnable (no host config composed; an artifact cannot carry one), lifecycle sweep/rotateShards (first run after the one-shot exits), ensureOverlayIndex (only on save/publish paths)."
],
"mechanism": "Reused, not a second mechanism: bootSchemaStack({ deferSchemaDdl: true, readOnlyProbe: true }) — the boot os migrate plan / duplicates already take. deferSchemaDdl arms DeferSchemaDdlPlugin (driver.setDeferredDdl) and passes skipSeedData to createStandaloneStack (AppPlugin suppresses the inline seed); readOnlyProbe maps to sqliteAbsentFile empty-in-memory. Hook points are the call sites only: meta.ts runStored and audit-metadata-bodies.ts run, each `...(apply ? {} : { deferSchemaDdl: true, readOnlyProbe: true })`. schema-migrate.ts, schema-migration-plugins.ts, data-migration-plugins.ts and packages/drivers are untouched; no skip list.",
"regression": "Not a 17.6.0 regression at the CLI, measured: the 17.5.0 CLI (tag @objectstack/cli@17.5.0 built in a comparison worktree, app-crm compiled and seeded with it) rewrote the same 28 rows on `meta --stored` without --apply, twice ([Seeder] \"updated\":28 each run). `meta --stored` has booted without deferSchemaDdl since it landed in 83cf2d3082 (#4464, 2026-08-01). audit-metadata-bodies has done so since it landed in 336e191441, which is an ancestor of cli@17.6.0 (is-ancestor exit 0) and not of cli@17.5.0 (exit 1; control leg the root commit 1598cabe4a exit 0; full clone, is-shallow false). Why the HotCRM 17.5.0 run left identical hashes is not measurable from this repository (hotcrm out of reach); the git log between the two tags shows no change to these boots.",
"repro": "examples/app-crm, os build then os dev -d file:base.db (82 tables, 150 rows, schema hash 7ed48db5bdddb263). Table row-hash before→after. UNFIXED meta --stored: crm_account 8939c7d891c8e7c9→7fe2d384ebf2af51, crm_activity 9748478c775f2c9c→a2b3603ec79d8a34, crm_contact e4754040765c8715→18b69d222da166ae, crm_lead e298eddbbb67f899→a6dabd7bc46372d3, crm_opportunity 6b5c57ab08030da9→c409a3bbcac98124 (schema unchanged). UNFIXED audit-metadata-bodies: crm_account →c737021d1ba8cb55, crm_activity →0c6eb45a77f29954, crm_contact →7bcecb50425fab06, crm_lead →89631d74e08d2b36, crm_opportunity →1f1e230484781069. FIXED, both commands: every table hash and the schema hash unchanged (IDENTICAL), report the same apart from one seed-caused paged-read notice. Edge DBs, FIXED: crm_lead missing a column, and the audit tables dropped — both IDENTICAL after the preview. --apply control (both commands): the same 28 rows change, as before.",
"gates": "Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at a67e290cc7 after merging origin/main, because the first derivation at 174a6c9b0a reported STALE TREE (ci.yml and check-ci-filter-parity.mjs changed on main). 63 commands, all exit 0 in the final record (2 re-run after their prerequisite build, see tests); `--ran` verdict: 63 run, 0 NOT-MEASURED, 0 UNRUN. Named by the dispatch, also run: CLI typecheck, unit and the integration files above, check:nul-bytes (in the 63, exit 0). Not run locally, declared to CI: the full integration layer, full pnpm lint.",
"deviations": [
"Merged origin/main (11905a4f8b, #21369 and #21368) into the branch as a67e290cc7, no conflict, to derive and run gates on a current tree. The merge commit carries git's default message, without the trailer pair.",
"Live PostgreSQL: there is no CI leg that supplies OS_TEST_POSTGRES_URL to @objectstack/cli, so the new live cell is a named skip in CI. The wiring is a build and a run step in ci.yml's Temporal Conformance job, outside the claim's surface, so it is not added. The cell ran locally against a private PostgreSQL 16 (initdb under the worktree, random port, stopped and removed after).",
"Behaviour change on one edge (not a skip list, a consequence of the read-only boot): a preview at a database that lacks the table it reads (a missing file, an unbooted DB, a wrong --database-url) now exits 1 (meta: the driver refusal for sys_metadata; audit: could not read sys_audit_log — NOT examined) instead of creating the table and reporting an empty result with exit 0. Stated in the changeset and the PR. A clearer meta message for this edge would be a follow-up.",
"A 17.5.0 comparison worktree (../objectstack-issue-21349-cmp175, detached at the tag) was built for the regression measurement and removed (node_modules first, no --force).",
"Docs not edited: cli.mdx already says the meta --stored preview writes nothing, and this PR makes that true. There is no cli.mdx entry for audit-metadata-bodies."
],
"files_changed": [
"packages/cli/src/commands/migrate/meta.ts",
"packages/cli/src/commands/migrate/audit-metadata-bodies.ts",
"packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (new)",
".changeset/21349-migrate-preview-read-only.md (new, @objectstack/cli patch, Clause-②: no)"
],
"ablations": [
"Fix committed first at 88e37aa343; legs run at 174a6c9b0a with the live PG cell; 10 tests per run.",
"L1 meta.ts spread removed (plain boot): RED 3 — SQLite preview (diff: status won→open, updated_at moved), SQLite missing file, PG preview (same revert); 7 green; restore blob 86a8700b5dde == HEAD.",
"L2 audit-metadata-bodies.ts spread removed: RED 3 — SQLite preview, SQLite missing file, PG preview; 7 green; restore blob f19931b6a7e4 == HEAD.",
"L3 meta.ts deferSchemaDdl only (readOnlyProbe dropped): RED 1 — SQLite missing file; 9 green.",
"L4 meta.ts readOnlyProbe only (deferSchemaDdl dropped): RED 2 — SQLite preview, PG preview; 8 green.",
"L5 audit deferSchemaDdl only: RED 1 — SQLite missing file; 9 green.",
"L6 audit readOnlyProbe only: RED 2 — SQLite preview, PG preview; 8 green.",
"Direction as expected in every leg (turns red); the --apply controls stayed green in every leg. Tree clean after all legs (git status --porcelain empty)."
],
"checks_after_push": "One read of the check-runs on a67e290cc74f at PR open: 31 runs — 6 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 22 in_progress (including the required Lint & Repo Gates, TypeScript Type Check lanes, Test Core 1-6, Dogfood Regression Gate 1-3, Build Core, Temporal Conformance, Governed Surface Queue Guard, Check Changeset). in_progress, not waited on."
}
ACCEPT: PR #21389 at 0166f86b (os migrate meta --stored and os migrate audit-metadata-bodies previews boot read-only). Fixes #21349; landing through the queue
Contract review of record:5950560226 on the PR, at CONTRACT_REVIEW_TIER, head 0166f86b, PASS.
Write paths closed: the inline seed (skipSeedData follows deferSchemaDdl); boot schema sync, including the audit plugin's table provisioning (driver deferral); SQLite create-if-absent; and the fresh-datastore attestation.
Reuse: exactly os migrate plan's two read-only options, with no second mechanism and no skip list.
Pins: full-state equality, schema plus every row, before and after each preview, with --apply controls.
Semver:minor, BREAKING, Clause-②: yes (narrowing) and not-required (no-migration-prescription) are right.
Seat patch round (same claim, 5948514895): the edge where a preview at a database lacking the table it reads now exits 1, where it exited 0, is an accept-set narrowing. The dev moved the changeset to minor with a BREAKING banner and the ADR-0087 marker. The seat corrected the PR body's line 2 to Clause-②: yes (narrowing).
Seat verification:
Checks on 0166f86b, collapsed latest-per-name: 34 names, 29 success, 5 skipped, 0 red. The red Test Core on the earlier head a67e290c was the roll-up of two shards cancelled by the patch-round push.
The net diff is 4 files, +505 / −0. check-governed-merges --pr 21389 reads not governed. mergeable_state reads clean.
The card's premise: the core holds. Each preview rewrote 28 seeded rows on examples/app-crm, and after the fix both leave everything identical. The "17.6.0 regression" framing did not reproduce: the 17.5.0 CLI rewrote the same rows on app-crm, and --stored has booted this way since 83cf2d3082.
The live PostgreSQL cell is a named skip in CI: no CI leg supplies OS_TEST_POSTGRES_URL to @objectstack/cli. It was measured locally against PostgreSQL 16. Wiring it into CI is a new CI leg, which is the maintainer's to name. Listed in the round report; not filed.
content/docs/releases/v17/17-6.mdx says "The 17.5.0 CLI's --stored preview wrote nothing". That contradicts the app-crm measurement, though both can hold per app. The page is release-owned and read-only here. Listed for the release owner in the round report.
Landing reading:3b4efa74b2 is on origin/main as a single-parent squash: 4 files, +505 / −0. Its git patch-id --stable is 28de9a969e5c…, equal to the reviewed net diff at 0166f86b.
The card: closed as completed by this merge. pm:dispatched is removed in the same act as this note.
Activity
objectstack-fleet commentedon Oct 2, 2026
Triage: grade completed —
priority:p1·area:devpath·pm:queue. A preview never writes: neither migrate preview boots the seed loader. The filer'sbug·domain:clistandTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T06:55Z. ⛔ Not a claim, ⛔ not a dispatch.Why p1. The 17.6.0 upgrade checklist tells every operator to run these previews first, and they rewrite seeded rows in application tables. For
--stored, that is a 17.6.0 regression: an operator's safety step mutates their data.Direction.
--apply, both commands boot without the seed loader, and with no write path to application tables. The datasource is read-only for the run.--applybehaviour is unchanged.Pins: the card's row-hash comparison, before and after each preview, is byte-identical on SQLite and on one live dialect.
--applystill applies (the control).Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1, to triage's direction5947006439. Landed asFixes #21349.Session:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-21349-migrate-preview-read-onlyWorktree:
objectstack-issue-21349Domain:
domain:cliSeat:
domain:cli#1File surface, derived at
origin/mainf39760864c:--apply, bothos migrate meta --storedandos migrate audit-metadata-bodiesboot without the seed loader, and with no write path to application tables. The datasource is read-only for the run. ⛔ No "skip these tables" list.--applyis unchanged.f39760864c:bootSchemaStack(packages/cli/src/utils/schema-migrate.ts):meta.ts:836andaudit-metadata-bodies.ts:142.bootSchemaStackalready carries the read-only boot thatos migrate planuses, thedeferSchemaDdl/readOnlyProbeoptions, whose header says the seed is suppressed so "the boot is read-only".packages/cli/src/commands/migrate/meta.ts;packages/cli/src/commands/migrate/audit-metadata-bodies.ts;packages/cli/src/utils/schema-migrate.ts,schema-migration-plugins.tsanddata-migration-plugins.ts, only if the existing read-only mode needs a hook;--applystill applies, as the control. Both are measured red before the fix.os migrate metaoros migrate audit-metadata-bodiesentries incontent/docs/deployment/cli.mdx(### Schema migrations) promise a read-only preview, they stay true. If a sentence the fix falsifies exists, it is corrected there..changeset/21349-*.mdfor@objectstack/cli, at the level the real diff takes.packages/drivers/*,domain:engine), or if the only route is a table skip list.(stop on a breach outside these; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; the default).Clause-②: no. A preview that wrote was never a declared behaviour, so no accept set and no public member moves. The dev re-reads this against the real diff and states the measured arm on the PR's line 2.
Thread-read: 5947006439
Serial constraints cleared:
Of the 8 open PRs (all file lists read in this act), none touches packages/cli/src/commands/migrate/, schema-migrate.ts, schema-migration-plugins.ts or data-migration-plugins.ts. No in-flight claim of this seat or another names them. The ### Schema migrations section of cli.mdx is disjoint from #21324's #### os verify entry and from PR #21369's hunks.objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{ "issue": 21349, "status": "done", "branch": "claude/issue-21349-migrate-preview-read-only", "pr": "https://github.com/objectstack-ai/objectstack/pull/21389", "session": "session_01VvcEokUG1tvVxkceYfR5XB", "premise_still_valid": true, "summary": "Without --apply, `os migrate meta --stored` and `os migrate audit-metadata-bodies` now boot through the read-only boot `os migrate plan` already uses (bootSchemaStack deferSchemaDdl + readOnlyProbe): schema DDL is held back, the inline seed loader is suppressed, and a missing SQLite file is not created. --apply keeps the plain boot. Reproduced on examples/app-crm (os dev seeded 28 rows and the first admin): before the fix each preview rewrote all 28 seeded rows in 5 app tables (updated_at bumped, organization_id stamped); after the fix the schema and every row hash are identical and the report is unchanged. The core premise holds. The 17.5.0-regression part did not reproduce: the 17.5.0 CLI rewrote the same 28 rows on app-crm, and `meta --stored` has booted this way since 83cf2d3082. One edge now behaves differently: a preview at a database that lacks the table it reads exits 1 instead of creating the table (stated in the changeset and the PR).", "tests": "All at a67e290cc7 (the branch merged with origin/main 11905a4f8b). (1) `pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck`: VERDICT command-exit 0, check:test-typecheck OK. (2) `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: Test Files 246 passed (246), Tests 3489 passed (3489), VERDICT command-exit 0. (3) integration, `--project integration --reporter=verbose` over preview-read-only + meta.stored-flow-resolution + schema-migrate.deferred-ddl + platform-migrations-arming, with OS_TEST_POSTGRES_URL pointing at a private local PostgreSQL 16: Test Files 4 passed (4), Tests 23 passed (23). The new file without the URL gave 6 passed and 1 skipped (the named live-cell skip). Integration beyond these files is declared to CI. (4) dispatch-gates: 63 derived commands run one by one, exit codes captured before any pipe; `--ran` gave 63 run / 0 NOT-MEASURED / 0 UNRUN (verdict exit 0). check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: no dist in 9 packages outside the CLI closure); after a turbo build of those 9 (42/42 cached) both re-ran with exit 0. (5) Lint as a proven narrowing: eslint with the repo config and --no-inline-config, --format json, over the 3 changed TS files: 3 files linted (population read from the JSON output, none reported ignored), 0 errors, 0 warnings. The config enables no type-aware linting (eslint.config.mjs says so: no parserOptions.project), so these files cannot move any untouched file's verdict. Full `pnpm lint` declared to CI. (6) check-live-db-isolation: PASS, with the new file among 43 scanned live files. Ablation: see the ablations field. Each leg was a WRAP-mode `scripts/ablation-replace.mjs` run (anchor x1 then x0, marker x0 then x1, blob changed); restore proven blob == HEAD and `git diff HEAD` empty; the subject resolves to src, so no rebuild between legs.", "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21389; read-back 10117 bytes sent = stored); (2) label-write --assign → POST /repos/objectstack-ai/objectstack/issues/21389/assignees (read-back matches: assignee huangyiirene; size/l was added by another actor); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/21349/comments. git push (not REST): 4 pushes to the branch (empty marker, fix, pins, merge of main). Reads: single-card REST reads of #21349 and its comments, one check-runs read.", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: public door — each of `os migrate value-shapes`, `os migrate summary-nulls`, `os migrate recorded-by`, `os migrate resume`, `os migrate files-to-references` (dry run by default) and `os secret orphans`, `os storage orphans` (report-only by default), run with --database-url against an examples/app-crm database at a67e290cc7, rewrote the same 28 seeded rows in 5 app tables ([Seeder] \"updated\":28; updated_at and organization_id changed); `os migrate account-issuer` and `multi-value-columns` already boot read-only and left it identical · contract: content/docs/deployment/cli.mdx says \"os migrate value-shapes # Scan: full report, writes nothing\" (L1051), \"os migrate summary-nulls # Dry run: full report, writes nothing\" (L1103), \"os migrate files-to-references # Dry run: full report, writes nothing\" (L998), and for os secret orphans \"Report-only by default: without --delete it writes nothing and deletes nothing.\" (L530) · Seam: spec:none (CLI-declared dry-run contract) → runtime:packages/cli/src/utils/schema-migrate.ts bootSchemaStack (createStandaloneStack skipSeedData keyed on deferSchemaDdl) | consumer: none · same family as #21349: name it for ONE family closure card, not seven single cards. A family-wide fix, keyed like runPlatformMigrations:false (seed off on every one-shot CLI boot, DDL deferred on every dry run), would also cover --apply · dedupe words: \"dry run writes seeded rows\", \"bootSchemaStack skipSeedData\", \"one-shot CLI boot seed loader\", \"report-only command rewrites rows\", \"migrate dry run updated_at\"", "carrier: the family closure card above (it edits schema-migrate.ts boot options) · noted, not filed — two comments now say more than is true: packages/cli/src/utils/schema-migrate.ts (the runPlatformMigrations:false block, around L319-329) and packages/cli/src/utils/platform-migrations-arming.integration.test.ts (around L321-326) list `os migrate meta` among the boots without deferSchemaDdl. After #21389 that is true only with --apply. Not edited here because both files are outside the claim's surface.", "carrier: 承接者:无 · noted in PR acceptance notes only — `--apply` on these two commands still runs the boot seed loader and schema sync (a write the operator never saw in the preview); the direction keeps --apply unchanged." ], "write_paths": [ "BASE 3937ad2f32, plain boot taken by both previews: (1) AppPlugin.start inline seed (SeedLoaderService upsert of every artifact dataset) — measured on examples/app-crm: 28 rows across crm_account/crm_activity/crm_contact/crm_lead/crm_opportunity, updated_at bumped, organization_id stamped with the admin org on rows that had none; in the pin an operator edit (status won) was put back to the seed value (open). Source: createStandaloneStack skipSeedData is set only when bootSchemaStack is given deferSchemaDdl, and neither command passed it.", "(2) ObjectQLPlugin boot schema sync and AuditPlugin.provisionSystemTables (syncObjectSchema) — on a database behind the app schema, measured with the same plain boot: a dropped crm_lead.phone column was added back, and sys_audit_log was created with 5 named indexes.", "(3) the sqlite driver opening the target in create-if-absent mode — a preview at a missing path created the file (pin L3/L5 legs).", "Checked and not a write path on this boot: platform repair migrations (runPlatformMigrations:false already), host onEnable (no host config composed; an artifact cannot carry one), lifecycle sweep/rotateShards (first run after the one-shot exits), ensureOverlayIndex (only on save/publish paths)." ], "mechanism": "Reused, not a second mechanism: bootSchemaStack({ deferSchemaDdl: true, readOnlyProbe: true }) — the boot os migrate plan / duplicates already take. deferSchemaDdl arms DeferSchemaDdlPlugin (driver.setDeferredDdl) and passes skipSeedData to createStandaloneStack (AppPlugin suppresses the inline seed); readOnlyProbe maps to sqliteAbsentFile empty-in-memory. Hook points are the call sites only: meta.ts runStored and audit-metadata-bodies.ts run, each `...(apply ? {} : { deferSchemaDdl: true, readOnlyProbe: true })`. schema-migrate.ts, schema-migration-plugins.ts, data-migration-plugins.ts and packages/drivers are untouched; no skip list.", "regression": "Not a 17.6.0 regression at the CLI, measured: the 17.5.0 CLI (tag @objectstack/cli@17.5.0 built in a comparison worktree, app-crm compiled and seeded with it) rewrote the same 28 rows on `meta --stored` without --apply, twice ([Seeder] \"updated\":28 each run). `meta --stored` has booted without deferSchemaDdl since it landed in 83cf2d3082 (#4464, 2026-08-01). audit-metadata-bodies has done so since it landed in 336e191441, which is an ancestor of cli@17.6.0 (is-ancestor exit 0) and not of cli@17.5.0 (exit 1; control leg the root commit 1598cabe4a exit 0; full clone, is-shallow false). Why the HotCRM 17.5.0 run left identical hashes is not measurable from this repository (hotcrm out of reach); the git log between the two tags shows no change to these boots.", "repro": "examples/app-crm, os build then os dev -d file:base.db (82 tables, 150 rows, schema hash 7ed48db5bdddb263). Table row-hash before→after. UNFIXED meta --stored: crm_account 8939c7d891c8e7c9→7fe2d384ebf2af51, crm_activity 9748478c775f2c9c→a2b3603ec79d8a34, crm_contact e4754040765c8715→18b69d222da166ae, crm_lead e298eddbbb67f899→a6dabd7bc46372d3, crm_opportunity 6b5c57ab08030da9→c409a3bbcac98124 (schema unchanged). UNFIXED audit-metadata-bodies: crm_account →c737021d1ba8cb55, crm_activity →0c6eb45a77f29954, crm_contact →7bcecb50425fab06, crm_lead →89631d74e08d2b36, crm_opportunity →1f1e230484781069. FIXED, both commands: every table hash and the schema hash unchanged (IDENTICAL), report the same apart from one seed-caused paged-read notice. Edge DBs, FIXED: crm_lead missing a column, and the audit tables dropped — both IDENTICAL after the preview. --apply control (both commands): the same 28 rows change, as before.", "gates": "Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at a67e290cc7 after merging origin/main, because the first derivation at 174a6c9b0a reported STALE TREE (ci.yml and check-ci-filter-parity.mjs changed on main). 63 commands, all exit 0 in the final record (2 re-run after their prerequisite build, see tests); `--ran` verdict: 63 run, 0 NOT-MEASURED, 0 UNRUN. Named by the dispatch, also run: CLI typecheck, unit and the integration files above, check:nul-bytes (in the 63, exit 0). Not run locally, declared to CI: the full integration layer, full pnpm lint.", "deviations": [ "Merged origin/main (11905a4f8b, #21369 and #21368) into the branch as a67e290cc7, no conflict, to derive and run gates on a current tree. The merge commit carries git's default message, without the trailer pair.", "Live PostgreSQL: there is no CI leg that supplies OS_TEST_POSTGRES_URL to @objectstack/cli, so the new live cell is a named skip in CI. The wiring is a build and a run step in ci.yml's Temporal Conformance job, outside the claim's surface, so it is not added. The cell ran locally against a private PostgreSQL 16 (initdb under the worktree, random port, stopped and removed after).", "Behaviour change on one edge (not a skip list, a consequence of the read-only boot): a preview at a database that lacks the table it reads (a missing file, an unbooted DB, a wrong --database-url) now exits 1 (meta: the driver refusal for sys_metadata; audit: could not read sys_audit_log — NOT examined) instead of creating the table and reporting an empty result with exit 0. Stated in the changeset and the PR. A clearer meta message for this edge would be a follow-up.", "A 17.5.0 comparison worktree (../objectstack-issue-21349-cmp175, detached at the tag) was built for the regression measurement and removed (node_modules first, no --force).", "Docs not edited: cli.mdx already says the meta --stored preview writes nothing, and this PR makes that true. There is no cli.mdx entry for audit-metadata-bodies." ], "files_changed": [ "packages/cli/src/commands/migrate/meta.ts", "packages/cli/src/commands/migrate/audit-metadata-bodies.ts", "packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (new)", ".changeset/21349-migrate-preview-read-only.md (new, @objectstack/cli patch, Clause-②: no)" ], "ablations": [ "Fix committed first at 88e37aa343; legs run at 174a6c9b0a with the live PG cell; 10 tests per run.", "L1 meta.ts spread removed (plain boot): RED 3 — SQLite preview (diff: status won→open, updated_at moved), SQLite missing file, PG preview (same revert); 7 green; restore blob 86a8700b5dde == HEAD.", "L2 audit-metadata-bodies.ts spread removed: RED 3 — SQLite preview, SQLite missing file, PG preview; 7 green; restore blob f19931b6a7e4 == HEAD.", "L3 meta.ts deferSchemaDdl only (readOnlyProbe dropped): RED 1 — SQLite missing file; 9 green.", "L4 meta.ts readOnlyProbe only (deferSchemaDdl dropped): RED 2 — SQLite preview, PG preview; 8 green.", "L5 audit deferSchemaDdl only: RED 1 — SQLite missing file; 9 green.", "L6 audit readOnlyProbe only: RED 2 — SQLite preview, PG preview; 8 green.", "Direction as expected in every leg (turns red); the --apply controls stayed green in every leg. Tree clean after all legs (git status --porcelain empty)." ], "checks_after_push": "One read of the check-runs on a67e290cc74f at PR open: 31 runs — 6 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 22 in_progress (including the required Lint & Repo Gates, TypeScript Type Check lanes, Test Core 1-6, Dogfood Regression Gate 1-3, Build Core, Temporal Conformance, Governed Surface Queue Guard, Check Changeset). in_progress, not waited on." }Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
ACCEPT: PR #21389 at
0166f86b(os migrate meta --storedandos migrate audit-metadata-bodiespreviews boot read-only).Fixes #21349; landing through the queuedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T10:39Z5950560226on the PR, atCONTRACT_REVIEW_TIER, head0166f86b, PASS.skipSeedDatafollowsdeferSchemaDdl); boot schema sync, including the audit plugin's table provisioning (driver deferral); SQLite create-if-absent; and the fresh-datastore attestation.os migrate plan's two read-only options, with no second mechanism and no skip list.--applycontrols.minor, BREAKING,Clause-②: yes (narrowing)andnot-required (no-migration-prescription)are right.5948514895): the edge where a preview at a database lacking the table it reads now exits 1, where it exited 0, is an accept-set narrowing. The dev moved the changeset tominorwith a BREAKING banner and the ADR-0087 marker. The seat corrected the PR body's line 2 toClause-②: yes (narrowing).0166f86b, collapsed latest-per-name: 34 names, 29success, 5 skipped, 0 red. The redTest Coreon the earlier heada67e290cwas the roll-up of two shards cancelled by the patch-round push.check-governed-merges --pr 21389reads not governed.mergeable_statereadsclean.examples/app-crm, and after the fix both leave everything identical. The "17.6.0 regression" framing did not reproduce: the 17.5.0 CLI rewrote the same rows on app-crm, and--storedhas booted this way since83cf2d3082.OS_TEST_POSTGRES_URLto@objectstack/cli. It was measured locally against PostgreSQL 16. Wiring it into CI is a new CI leg, which is the maintainer's to name. Listed in the round report; not filed.content/docs/releases/v17/17-6.mdxsays "The 17.5.0 CLI's--storedpreview wrote nothing". That contradicts the app-crm measurement, though both can hold per app. The page is release-owned and read-only here. Listed for the release owner in the round report.--applystill runs the seed loader. The direction keeps--applyunchanged; [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391's family-wide shape would cover it.pr_readyandautomerge_enablefollow in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21389 →
3b4efa74b2(both stored-data previews boot read-only).Fixes #21349: the card is completedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T10:59Z3b4efa74b2is onorigin/mainas a single-parent squash: 4 files, +505 / −0. Itsgit patch-id --stableis28de9a969e5c…, equal to the reviewed net diff at0166f86b.pm:dispatchedis removed in the same act as this note.Generated by Claude Code
test(cli,runtime): pin the one-shot boot family read-only and seed-fr…
docs(releases): finalize the 17.6.0 notes after publish (objectstack-…
fix(cli): os migrate meta --stored and audit-metadata-bodies preview …
fix(cli,runtime): one-shot CLI boots run no seed loader and arm no li…