Skip to content

fix(cli): os migrate meta --stored and audit-metadata-bodies preview on a read-only boot - #21389

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21349-migrate-preview-read-only
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21349-migrate-preview-read-only

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21349
Clause-②: yes (narrowing)

What changed

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: true, readOnlyProbe: true }). Schema DDL is held back, the artifact's inline seed loader is suppressed (deferSchemaDdl implies skipSeedData), and a SQLite file that does not exist is opened as an empty in-memory stand-in instead of being created. --apply keeps the plain boot, unchanged.

The code change is one spread per command (meta.ts, audit-metadata-bodies.ts). There is no new mechanism, no table skip list and no driver change.

Measured before the fix (the card's steps, on examples/app-crm)

Setup: os build, then os dev -d file:base.db on examples/app-crm. The seed load ran (28 rows) and the first admin was seeded. The server was stopped and the file copied. The copy has 82 tables and 150 rows. Each command ran on its own copy; the row hashes were read on a separate read-only connection.

run crm_account crm_activity crm_contact crm_lead crm_opportunity schema
meta --stored, BASE 3937ad2f32 3 of 3 rows changed 5 of 5 3 of 3 5 of 5 12 of 12 unchanged
audit-metadata-bodies, BASE 3 of 3 5 of 5 3 of 3 5 of 5 12 of 12 unchanged
meta --stored, this PR identical identical identical identical identical identical
audit-metadata-bodies, this PR identical identical identical identical identical identical

The changed columns were updated_at (bumped) and organization_id (stamped with the admin's organization on seeded rows that had none). The log line was [Seeder] Seed loading complete {"inserted":0,"updated":28,...}. After the fix it is [Seeder] skipSeedData — inline seed suppressed. The report each preview prints is the same as before, apart from one paged-read notice the seed loader itself caused.

The write paths found, and what closes each

  1. The app's inline seed (AppPlugin.start): an upsert of every seeded row. In the pin below it also puts an operator's edit to a seeded row back to the seed's value (status: 'won' back to 'open'). This is closed by skipSeedData.
  2. Boot schema sync: on a database behind the app's schema, the plain boot adds missing columns and creates missing tables. Measured with the plain boot --apply takes, which is the boot both previews took before this PR: a dropped crm_lead.phone column was added back, and sys_audit_log was created with five named indexes. This is closed by deferSchemaDdl. On the same two edge databases, the fixed previews left the schema and every row identical.
  3. Creating the target: a preview at a missing SQLite path created the file. This is closed by readOnlyProbe.

These are not write paths on this boot: the platform repair migrations (already off: runPlatformMigrations: false), a host onEnable (these commands compose no host config, and a compiled artifact cannot carry one), and the lifecycle sweep (its first run comes after the one-shot command has exited).

Pins

packages/cli/src/commands/migrate/preview-read-only.integration.test.ts runs the real commands through oclif: parse, occupancy probe, boot, walk and shutdown. Each case runs against a database that a plain boot seeded and an operator then edited, and that carries one legacy flow row and one cleartext audit copy of a datasource body. Per dialect cell:

  • meta --stored without --apply: the schema and every row are byte-identical, and the report still names the one pending row.
  • meta --stored --apply --yes: the legacy row is rewritten (the control).
  • audit-metadata-bodies without --apply: byte-identical, and the report still counts the one copy to rewrite.
  • audit-metadata-bodies --apply --yes: the copy is redacted (the control).
  • SQLite only: either preview at a missing file creates no file, -wal, -shm or -journal.

Cells: SQLite always runs. Live PostgreSQL runs with OS_TEST_POSTGRES_URL, in a database derived from the file's path (os_lv_ + slug + 12 hex characters of sha256; check-live-db-isolation PASS with this file in its 43 scanned files). Without the URL the cell is a named skip, and a failure under OS_EXPECT_LIVE_DIALECT_MATRIX=1.

Ablations

The fix was committed first (88e37aa343). Each leg ran through scripts/ablation-replace.mjs in WRAP mode: the anchor hit once, the mutation was confirmed on disk (anchor 1 to 0, marker 0 to 1, blob changed), and the restore was proven (blob == HEAD, git diff HEAD empty). The pin imports the commands by relative path, so it resolves to src, not dist. No rebuild was needed between legs. Runs used the live PostgreSQL cell.

leg mutation red green
L1 meta.ts: plain boot (spread removed) SQLite preview, SQLite missing file, PG preview (3) 7
L2 audit-metadata-bodies.ts: plain boot SQLite preview, SQLite missing file, PG preview (3) 7
L3 meta.ts: deferSchemaDdl only SQLite missing file (1) 9
L4 meta.ts: readOnlyProbe only SQLite preview, PG preview (2) 8
L5 audit-metadata-bodies.ts: deferSchemaDdl only SQLite missing file (1) 9
L6 audit-metadata-bodies.ts: readOnlyProbe only SQLite preview, PG preview (2) 8

The L1 diff shows the operator's edit reverted on both dialects: "status": "won" became "open" with a new updated_at.

The 17.5.0 comparison

This did not reproduce on examples/app-crm. The 17.5.0 CLI (tag @objectstack/cli@17.5.0, built in a comparison worktree) ran on a database it had seeded itself, and meta --stored rewrote the same 28 rows. That was measured twice, and both runs logged "updated":28. meta --stored has booted without the read-only options since it landed in 83cf2d3082 (#4464). audit-metadata-bodies has done the same since it landed in 336e191441, which is in 17.6.0 and not in 17.5.0 (git merge-base --is-ancestor exit 1; control leg, the root commit, exit 0; full clone). Why the HotCRM 17.5.0 run left its hashes identical cannot be measured from this repository.

One edge behaves differently

A preview pointed at a database that lacks the table it reads now fails and exits 1. Before, it created the table and reported nothing to examine. Measured: meta --stored on a missing file, or on a database without sys_metadata, exits 1 with the driver's refusal for sys_metadata, and no file is created. audit-metadata-bodies on a database without the audit tables exits 1 with could not read sys_audit_log — its rows were NOT examined. Before the fix, the same runs created the file or the tables and answered No stored metadata to examine or Nothing to rewrite with exit 0. The changeset states this.

Verification at a67e290cc7 (this branch merged with origin/main 11905a4f8b)

  • pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck: exit 0, and check:test-typecheck OK. The new test is under src, which tsconfig.json includes.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 246 files, 3489 tests passed.
  • Integration: the new file plus meta.stored-flow-resolution, schema-migrate.deferred-ddl and platform-migrations-arming gave 4 files and 23 tests passed, with the live PostgreSQL cell (local PostgreSQL 16 on a private port). Without the URL: 6 passed and 1 named skip.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 63 families, each run one by one with its exit code recorded before any pipe. --ran reported 63 run, 0 NOT-MEASURED, 0 UNRUN. check:dual-build-cjs-loads and check:i18n-coverage first answered exit 3 (PREREQUISITE NOT MET: nine packages outside the CLI closure had no dist). After a turbo build of those nine (42 of 42 cache hits), both exited 0.
  • Lint, as a proven narrowing: eslint with the repo config and --no-inline-config over the 3 changed TS files (--format json) reported 3 files linted, 0 errors and 0 warnings, and no file was reported as ignored. The config enables no type-aware linting (eslint.config.mjs says so: no parserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file. The full pnpm lint is left to CI.

Acceptance notes

  1. The same defect in seven sibling commands, not changed here because they are outside this claim's surface. Measured at a67e290cc7 on the same app-crm database: os migrate value-shapes, summary-nulls, recorded-by, resume, files-to-references, os secret orphans and os storage orphans each ran the seed loader and rewrote the same 28 rows. All seven are dry-run by default or report-only. os migrate account-issuer and multi-value-columns already boot read-only and left the database identical. This is reported to the seat as one family.
  2. --apply on these two commands still runs the boot's seed loader and schema sync, as before. The direction keeps --apply unchanged.
  3. Two comments now say more than is true: the runPlatformMigrations block in schema-migrate.ts and the non-deferred case in platform-migrations-arming.integration.test.ts list os migrate meta among the boots without deferSchemaDdl. After this PR that is true only with --apply. They are not edited here because both files are outside this claim's surface.
  4. The live PostgreSQL cell has no CI leg. No step supplies OS_TEST_POSTGRES_URL to @objectstack/cli, so in CI the cell is a named skip. Wiring it takes one build step and one run step in the Temporal Conformance job of .github/workflows/ci.yml, which is also outside this claim's surface.
  5. Docs: content/docs/deployment/cli.mdx already says the meta --stored preview "writes nothing", and this PR makes that true, so the docs are not edited. cli.mdx has no entry for audit-metadata-bodies.

Generated by Claude Code

claude added 3 commits October 2, 2026 09:08
…on a read-only boot

Without --apply, both commands now boot the stack the way `os migrate plan`
does: `deferSchemaDdl` holds schema DDL back and suppresses the artifact's
inline seed loader, and `readOnlyProbe` keeps a missing sqlite file from being
created. The plain boot they used before ran schema sync and the seed loader,
whose upserts rewrote every seeded row of the app's tables during a preview.
`--apply` keeps the plain boot.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…ve PostgreSQL

Runs the real `os migrate meta --stored` and `os migrate audit-metadata-bodies`
commands against a database a plain boot seeded, an operator edited, and that
carries one legacy flow row and one cleartext audit copy. Without --apply the
schema and every row stay byte-identical while the report still names the
work; --apply still does that work; and a preview at a missing SQLite file
creates no file. The live PostgreSQL cell runs in a database derived from
this file's path and is a named skip without OS_TEST_POSTGRES_URL.

Adds the patch changeset for @objectstack/cli.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/automation/hook-bodies.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/fields.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/objects.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/queries.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/deployment/cli.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/deployment/index.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/protocol/objectql/query-syntax.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/protocol/objectui/actions.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/ui/actions.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/ui/apps.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/ui/dashboards.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/ui/translations.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/upgrading.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-0.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-1.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-6.mdx (via os migrate audit-metadata-bodies (command, read off packages/cli/src/commands/migrate/audit-metadata-bodies.ts), os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 51550933dbc5c7cbd89349a6beb5145363fc881d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 82df0792bc82bd1063d5d566e7529bb375c0fd06 — the merge of head 0166f86ba29896dc08238613b88a19f048c69388 into base 51550933dbc5c7cbd89349a6beb5145363fc881d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 82df0792bc82bd1063d5d566e7529bb375c0fd06 && git checkout 82df0792bc82bd1063d5d566e7529bb375c0fd06
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 51550933dbc5c7cbd89349a6beb5145363fc881d 0166f86ba29896dc08238613b88a19f048c69388 && git checkout -B drift-repro 51550933dbc5c7cbd89349a6beb5145363fc881d && git merge --no-ff 0166f86ba29896dc08238613b88a19f048c69388

node scripts/docs-audit/affected-docs.mjs --json 51550933dbc5c7cbd89349a6beb5145363fc881d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 51550933dbc5c7cbd89349a6beb5145363fc881d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 2, 2026 10:14
…wing (minor, BREAKING banner, ADR-0087 not-required)

A preview of `os migrate meta --stored` or `os migrate audit-metadata-bodies`
at a database that lacks the table it reads now exits 1 where it exited 0, so
the changeset takes the narrowing arm: `minor`, `Clause-②: yes (narrowing)`,
the BREAKING banner, and one ADR-0087 disposition,
`not-required (no-migration-prescription)`.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0166f86ba29896dc08238613b88a19f048c69388
Local-runs: none

Reviewed against card #21349 (body, triage direction 5947006439, claim 5948514895, the os-dev-report), PR #21389 (body as corrected, 4-file list, net diff against merge base ee75aae1a8 with origin/main), the code the diff calls read at the head sha, content/docs/deployment/cli.mdx ### Schema migrations, content/docs/releases/v17/17-6.mdx, .github/workflows/ci.yml, and the head's check-runs collapsed latest-per-name. Nothing was built, run or re-run.

① Derived judgments

(a) Write paths closed by deferSchemaDdl: true, readOnlyProbe: true at the two call sites — right, with two inherited residues named. Traced at the head:

  • The artifact's inline seed: bootSchemaStack passes skipSeedData: true to createStandaloneStack whenever deferSchemaDdl is set (schema-migrate.ts L306); AppPlugin.start then suppresses the seed source and writes no rows (app-plugin.ts L1535-1542). This is the card's measured write (updated_at, organization_id, relative dates) and the source of the sys_activity view error; closed.
  • Boot schema sync and the audit plugin's provisioning: DeferSchemaDdlPlugin.init arms driver.setDeferredDdl(true) before any start(); SqlDriver.initObjects then records each object into deferredSchemaObjects and continues before ensureRotation, hasTable, create and alter (sql-driver.ts L12335-12338), and skips ensureDatabaseExists (L12320). AuditPlugin.provisionSystemTables runs at kernel:ready through engine.syncObjectSchema into that same initObjects, and nothing on a preview clears the flag (only flushDeferredSchemaDdl does), so sys_audit_log / sys_activity / sys_comment are recorded, not created; closed.
  • SQLite create-if-absent: readOnlyProbe maps to sqliteAbsentFile: 'empty-in-memory' and suppresses the state-dir mkdir (standalone-stack.ts L659-660, L682); closed. Postgres/MySQL CREATE DATABASE sits inside ensureDatabaseExists, skipped under deferral; closed.
  • The fresh-datastore attestation (sys_migration insert at kernel:ready, platform-objects/src/plugin.ts L241-260) requires engine.wasDatastoreCreatedFromEmpty(), which needs a created count above zero from getSchemaSyncStats; deferred initObjects never reaches tablesCreatedHere.add, so it stays false; closed. The dev did not list this one; verified here.
  • "Checked and not a write path" claims: platform repair migrations are off unconditionally (runPlatformMigrations: false, pre-existing); no host config is composed on either command (composeHostStack is not passed, so no onEnable); ensureOverlayIndex is reached only from saveMetaItem, promoteDraftForPublish, listDrafts, publishPackageDrafts, discardPackageDrafts, revertCommit, rollbackMetaItem — the preview's walk reads sys_metadata through engine.find directly and never enters those (the method's own docblock saying it "runs on EVERY boot" is stale against its callers; outside this surface). MetadataPlugin persists nothing on an artifact boot (metadata/src/plugin.ts L262).
  • Residues the read-only boot leaves open, both inherited from the boot os migrate plan takes and neither introduced here: (1) the lifecycle sweep (rotateShards, retention reaping) is armed with DEFAULT_LIFECYCLE_INITIAL_DELAY_MS = 60_000 on unref'd timers and stopped on destroy — the dev's "first run after the one-shot exits" is a time-bounded claim, true for every measured run, not a structural one; (2) DeferSchemaDdlPlugin arms deferral on the first driver.* SQL service only, so a second SQL datasource an artifact declares (including a lifecycle-class telemetry store that sys_audit_log / sys_activity route to) would still schema-sync. Not the card's shape (single-file SQLite, one live cell). Named for [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 fix to weigh; no fix demanded here.
  • ⛔ No skip list: confirmed, the diff adds no table list; --apply passes an empty spread and boots exactly as before.

(b) Reuse of os migrate plan's boot — right. The two call sites pass the same two read-only options plan.ts passes (deferSchemaDdl: true, readOnlyProbe: true); plan additionally passes composeHostStack: true for its own object-set concern (#12938), which these DATA commands never took and still do not — they keep extraPlugins from buildDataMigrationPlugins, unchanged. os migrate duplicates is the exact-options sibling. One mechanism; schema-migrate.ts, schema-migration-plugins.ts, data-migration-plugins.ts and packages/drivers are untouched (the net diff is exactly the 4 listed files, 505 additions, 0 deletions).

(c) The pins — right as delivered, with one unpinned declaration and one CI gap. preview-read-only.integration.test.ts runs the real oclif commands against a database a plain boot seeded and an operator edited, carrying one legacy flow row and one cleartext audit copy. readState reads the schema (sqlite_master type/name/sql; on Postgres information_schema.tables/columns plus pg_indexes) and every row of every base table on a probe connection of its own, and the preview cases assert toEqual(before) — full-state equality, stronger than a row hash — while asserting the report still names the pending work (pending: 1; sys_audit_log.rewritten: 1), so the identity is not vacuous. Both --apply --yes controls are present and assert the rewrite landed. The missing-file edge runs for both commands on SQLite and asserts no file, -wal, -shm or -journal. Gap: that edge case does not assert the exit code or the refusal, so the declared narrowing (exit 1, the changeset's BREAKING line) has no pin. The CLI's vitest run executes both tier projects, so Test Core runs the SQLite cell; the live PostgreSQL cell is keyed on OS_TEST_POSTGRES_URL, a named skip without it and a failure under OS_EXPECT_LIVE_DIALECT_MATRIX=1. ci.yml supplies that URL to the driver-sql, metadata-protocol and runtime legs of Temporal Conformance only — never to @objectstack/cli — so in CI the live cell is a report, not coverage (the test header says so itself). The triage pin ("byte-identical on SQLite and on one live dialect") asked for the measurement; the report records it against a private PostgreSQL 16 (23 tests, 4 files, with the URL), and the wiring is outside the claim's file surface. Acceptable to land; the wiring is escalated below as a follow-up with no carrier today.

(d) Changeset and PR prose — true at the head, two sentences qualified.

  • deferSchemaDdl implies skipSeedData: true (L306). "One spread per command, no new mechanism, no skip list, no driver change": true.
  • The BREAKING edge: under deferral a read of a table in the driver's own deferred set is refused (sql-driver.ts L10198-10210); migrateStoredMetadata reads sys_metadata outside any try, so meta.ts catches and sets exitCode = 1; migrateStoredMetadataBodyCopies counts an unreadable table as a failure (stored-metadata-body-migration.ts L232-239) and audit-metadata-bodies.ts exits 1 when failures is above zero. The sentence is right.
  • "The 17.6.0 upgrade checklist runs both previews before their --apply runs": at the head 17-6.mdx L1589-1590 instructs audit-metadata-bodies then --apply literally; the meta step's imperative (L1578) is --from 17 then --stored --apply, with the --stored preview named in its warning and the page's own known-issues entry (L1487) calling both "two previews in the upgrade checklist". True as the release page describes itself; on the stricter reading the meta preview is referenced, not instructed. Not a false sentence.
  • "re-evaluated relative-date seed values": the card's HotCRM observation, not reproduced on app-crm (the PR's table names updated_at and organization_id only); a true property of the seed upsert, sourced from the card.
  • "minor under the launch-window convention for accept-set narrowings": matches check-changeset-no-major.mjs (a declared narrowing ships minor until GA).
  • PR body: meta --stored landed in 83cf2d3082 (feat(migrate,metadata-protocol): os migrate meta --stored rewrites sys_metadata rows in place (#4327) #4464) and audit-metadata-bodies in 336e191441 (fix(security): close out the stored-metadata-body family — project or refuse every further read/copy/evaluate exit #21144): both commits exist and touch those files; both CLI tags exist. "Lifecycle sweep: first run after the one-shot exits" holds for every measured run (60 s initial delay, unref'd); stated as time-bounded above. Acceptance notes 1-5 are each true: [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 exists, open, labelled finding, and carries the two overstated comments (schema-migrate.ts runPlatformMigrations block and platform-migrations-arming.integration.test.ts L321-326 both still list os migrate meta among the non-deferred boots; after this PR that holds only with --apply); cli.mdx L1197 says the --stored preview "writes nothing" and has no audit-metadata-bodies entry.

② Semver level

@objectstack/cli minor, BREAKING banner, Clause-②: yes (narrowing), adr-0087: not-required (no-migration-prescription) — right. The accept set of a public CLI surface shrank: a database lacking the table the preview reads was answered with exit 0 and is now refused with exit 1, observable to any script that ran the preview first. Under AGENTS.md's declaration rule yes takes at least minor and (narrowing) is BREAKING, so the banner and a disposition marker are owed; the launch-window guard grades it minor, not major. The category is the honest one: no authorable key, export or stored shape moves, nothing a consumer rewrites, and the operator remedy ("point --database-url at the deployment's database, or boot it once") is not a code prescription — the body carries no FROM/TO label, arrow rewrite or rewrite table for the detector to refuse. The claim's earlier Clause-②: no addressed the writes, not the exit code; the seat's patch round corrected the arm. Note the PR title keeps fix(cli), and the commit type may raise a bump but never lower it, so minor stands.

③ Boundary flags

  • Regression premise (17.5.0 wrote nothing): answered by the dev — the 17.5.0 CLI rewrote the same 28 rows on app-crm, twice, and --stored has booted this way since 83cf2d3082. The HotCRM observation cannot be measured from this repository. ESCALATE to the release seat: 17-6.mdx L1491 (RELEASE-OWNED) states "The 17.5.0 CLI's --stored preview wrote nothing", which the app-crm measurement contradicts for that app; both can be true per app (an inline seed is also skipped on a multi-tenant boot — app-plugin.ts L1544-1549 — which is a hypothesis, not a measurement). A docs-only PR or a release-notes note, never a rider here.
  • Live PostgreSQL CI wiring: answered in acceptance note 4 and true (no leg supplies the URL to @objectstack/cli). ESCALATE as a follow-up card: today the cell has no carrier, and a named skip in CI will never go red for PG. One build step plus one run step in Temporal Conformance, with OS_EXPECT_LIVE_DIALECT_MATRIX=1 scoped to the step, is the shape ci.yml already uses for runtime ([finding] the new live-PostgreSQL cascade-delete cell is red-capable but CI never provisions it — the Temporal Conformance job runs driver-sql, not @objectstack/runtime #18734).
  • Edge behaviour change (exit 1 where it exited 0): declared in the changeset as BREAKING and in the PR body; the mechanism is verified above. FLAG: not pinned — the SQLite missing-file case asserts only that no file appears. A one-line exit-code assertion is owed by whoever next touches this file; not a blocker, the declaration is the carrier the launch window relies on.
  • Family finding: filed as [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 (open, finding), one card for seven commands, with the dev's family-wide suggestion (seed off on every one-shot boot, DDL deferred on every dry run). Answered. The two residues named in ①(a) — the time-bounded lifecycle sweep and the default-driver-only deferral — belong on that card's design, not on this PR.
  • The two overstated comments: carried by [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 ("Also on this card (carrier)"); both files are outside this claim's surface. Answered.
  • --apply still running the seed loader and schema sync: the direction keeps --apply unchanged; the dev noted it with no carrier; [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 suggestion would cover it. Answered, no action here.
  • Fixes #21349: right to close. The card names two commands and an expected outcome ("the file is unchanged"); both previews now leave schema and every row identical on the card's steps, the sys_activity view error goes with the seed, and the direction's pins exist (SQLite in CI, PG measured locally). The 17.5.0-regression framing did not reproduce but is not part of the fix; the family is [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391. The Part-of PR must not also close its card and The card this PR closes must claim this branch checks both passed on this head.
  • Governed surfaces: none of the 4 files is one (.changeset/, packages/cli/), so the landing needs no record; this record is rendered because the seat asked.
  • Check-runs on 0166f86ba29896dc08238613b88a19f048c69388, collapsed latest-per-name, converged at 2026-10-02T10:36:02Z after three 5-minute polls (they had not converged at dispatch): 34 names, 29 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke), 0 failed, 0 pending. All seven required contexts are success: Lint & Repo Gates (carrying check:adr-0087-registration, check:live-db-isolation, check:test-source-alias, check:cross-package-test-inputs), TypeScript Type Check, Test Core (all six shards; this is where the new file's SQLite cell runs), Dogfood Regression Gate (three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Also success: Check Changeset, Dogfood Verify CLI, the four Type Check lanes, the single-writer and claim checks. Cited from the converged snapshot only.

Implemented-by: claude/issue-21349-migrate-preview-read-only
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants