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
Description
Activity
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsTriage: 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.
- 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.
--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
- Without
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratepriority:p1High: required for production / M2High: required for production / M2
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1, to triage's direction5947006439. Landed asFixes #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 atorigin/mainf39760864c:- The direction, as built: without
--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. - Read at
f39760864c:- Both commands boot through
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".- 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.
- Both commands boot through
- Expected files:
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;- 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.
--applystill applies, as the control. Both are measured red before the fix. - Docs: if the
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: one
.changeset/21349-*.mdfor@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.
- The direction, as built: without
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsos-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 commented
on Oct 2, 2026 ContributorMore actionsACCEPT: 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:39Z- Contract review of record:
5950560226on the PR, atCONTRACT_REVIEW_TIER, head0166f86b, PASS.- Write paths closed: the inline seed (
skipSeedDatafollowsdeferSchemaDdl); 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
--applycontrols. - Semver:
minor, BREAKING,Clause-②: yes (narrowing)andnot-required (no-migration-prescription)are right.
- Write paths closed: the inline seed (
- 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 tominorwith a BREAKING banner and the ADR-0087 marker. The seat corrected the PR body's line 2 toClause-②: yes (narrowing). - Seat verification:
- Checks on
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. - The net diff is 4 files, +505 / −0.
check-governed-merges --pr 21389reads not governed.mergeable_statereadsclean.
- Checks on
- 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--storedhas booted this way since83cf2d3082. - Out-of-scope notes, one line each:
- Seven more dry-run or report-only commands rewrite seeded rows: filed as one family card, [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391. It also carries the two comments this PR made overstated, the review's two inherited residues (the time-bounded lifecycle sweep, and deferral armed on the first SQL datasource only), and the missing exit-code pin for this PR's declared edge. [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 edits the same boot and the same test file.
- The live PostgreSQL cell is a named skip in CI: no CI leg supplies
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.
- Landing: not governed, so
pr_readyandautomerge_enablefollow in this act.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsLanded: PR #21389 →
3b4efa74b2(both stored-data previews boot read-only).Fixes #21349: the card is completedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T10:59Z- Landing reading:
3b4efa74b2is onorigin/mainas a single-parent squash: 4 files, +505 / −0. Itsgit patch-id --stableis28de9a969e5c…, equal to the reviewed net diff at0166f86b. - The card: closed as completed by this merge.
pm:dispatchedis removed in the same act as this note. - Carried forward: the seven sibling commands, the two overstated comments, the unpinned exit-1 edge and the boot's two residues are all on [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391.
Generated by Claude Code
- Landing reading:
- added a commit that references this issue
on Oct 2, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Summary
On 17.6.0, two commands documented as read-only previews write to the application's tables:
os migrate meta --storedwithout--apply;os migrate audit-metadata-bodieswithout--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
--storedthis 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
@objectstack/cli@17.6.0, runos migrate meta --stored --database-url file:<copy>. Do not pass--apply.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_opportunityand five more tables it:updated_at;organization_idon seeded rows that had none;It also logs
ERROR [SeedLoader] Failed to write sys_activity …: cannot modify sys_activity because it is a view, becausesys_activityis a rotation view on that database.Comparison
os migrate meta --storedon the same file, leaves every row hash identical and logs no SeedLoader error.audit-metadata-bodiesis new in 17.6.0, so it has no 17.5.0 baseline. Its dry run makes the same writes.Related
f20f669(17.6.0) stoppedos migrate planandos migrate applyfrom runningonEnableand the host'skernel:bootstrapped/kernel:listeninghooks during their boot. The seed load reached throughmigrate meta --storedandaudit-metadata-bodiesmay need the same withholding.