Skip to content

fix(cli): os migrate meta --out writes its snapshot on a run with nothing to migrate - #22121

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22116-migrate-meta-out-snapshot
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22116-migrate-meta-out-snapshot

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22116
Clause-②: no

What this changes

os migrate meta --from 18 --to 18 --out FILE (a range that crosses no step) exited 0, printed no snapshot line and wrote no FILE. The same command with --json wrote FILE. printMigrationReport returns early when the chain applied no edit and listed no manual change, and that return came before the --out write.

  • packages/cli/src/commands/migrate/meta.ts: the --out write and its line move into one helper, writeStackSnapshot(out, stack), called from both exits of printMigrationReport. On the early return it runs after the range answer and before --write's outcome and the data migrations. That is the main path's order: the snapshot after group ③, then --write, then the data migrations last.
  • Why a helper rather than moving the write above the branch: above the branch, the snapshot line would print before group ② on the main path and change that path's order. The helper keeps one writer and leaves every main-path line where it was.
  • Untouched, per the ruling: the --json path (it writes FILE on its own branch), --write, --stored / --apply and the chain.
  • .changeset/22116-migrate-meta-out-snapshot.md: @objectstack/cli patch.

Measured before the change (base a959493c)

Source CLI (packages/cli/bin/run-dev.js) over a one-object fixture project:

run exit snapshot line FILE
--from 18 --to 18 --out h.json 0 none absent (ls: No such file)
--from 18 --to 18 --out j.json --json 0 n/a written, 663 bytes
--from 17 --to 18 --out c.json (control) 0 printed after 306 manual change(s) written, byte-identical to j.json

Which ranges reach the early return

The early return is keyed on "nothing applied and nothing listed", not on "no step". So a range WITH steps over canonical metadata could in principle take its other arm (Nothing to migrate). Measured on a959493c, no range does. Every major in the registry carries semantic entries (protocol 17: 77, protocol 18: 306), and the chain lists every entry of every hop it crosses, whatever the stack holds. Over a canonical stack, as hops / applied / todos:

  • 16 → 16: 0 / 0 / 0; 17 → 17: 0 / 0 / 0; 18 → 18: 0 / 0 / 0
  • 16 → 17: 1 / 0 / 77; 17 → 18: 1 / 0 / 306; 16 → 18: 2 / 0 / 383

So today only an empty range reaches the early return. The change sits in the branch both arms share, so it covers both, and the second arm is pinned in-process over a real chain result with its notices taken away. The draft PR #22115 keeps result.todos whole, filters only what it lists, and does not touch the early-return condition, so that arm stays unreachable after it lands too.

Pins

packages/cli/test/migrate-meta-out-snapshot.test.ts, unit tier (in-process MigrateMeta.run over a temp project that links the real @objectstack/spec; nothing is spawned and no kernel is booted):

  1. Every empty range the command accepts (--from N --to N for N = 16, 17, 18, derived from the registry): the human mode exits 0, writes FILE and names it on exactly one line, after the range answer. The --json mode writes FILE too. The two FILEs are byte-identical, and FILE is the stack.
  2. A stale FILE left by an earlier run is overwritten.
  3. With --write, the snapshot line prints before --write's outcome.
  4. Control: --from 17 --to 18 writes FILE and names it after the manual-change header, and its bytes equal the --json mode's.
  5. printMigrationReport on both arms of the early return (an empty range, and a range with steps whose notices are taken away): FILE is written, and the snapshot line, --write's outcome and the data migrations print in that order.

Red before, green after, and one ablation:

  • Red on 76439ebd (the pins, before the change): 7 failed, 1 passed (the control). The failures were FILE absent (ENOENT) or the stale bytes kept.
  • Green on 0bd4955e: the new pin plus meta.report-order.test.ts and migrate-meta-write.test.ts, 3 files, 47 tests passed.
  • Ablation on 0bd4955e, through scripts/ablation-replace.mjs in wrap mode (trap restore). It deleted only the early-return call if (report.out) writeStackSnapshot(report.out, result.stack); (anchor 1 → 0, blob 20cadb91c148 → 2542a9eca0f5). Result: 7 failed, 1 passed, with the control green. Restored: blob equal to HEAD 20cadb91c148, git diff HEAD empty.

Verification (all on 0bd4955e)

  • @objectstack/cli unit tier (vitest run --project unit --maxWorkers=2): 263 of 263 files, 3869 tests passed. The first full run reported 2 files failed with packages/cli is not built (./dist/index.js is absent). That is a prerequisite, not a measurement, and both files passed (29 tests) after pnpm build. The integration tier is declared to CI: the diff touches no integration-tier file and no spawn entry.
  • pnpm --filter @objectstack/cli typecheck (tsc --noEmit, then check:test-typecheck): exit 0. tsc --listFiles shows the new test in tsconfig.test.json's program.
  • Gates: each of the 65 commands node scripts/pm/dispatch-gates.mjs --commands derives for this diff exited 0. Four first exited 3 (PREREQUISITE NOT MET, an unbuilt workspace) and passed when re-run after pnpm build: check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity. The --ran reconciliation, over recorded exit codes, reads 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. The four artifact-roster gates whose roster sits under a changed path (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) also exited 0.
  • pnpm lint (full repository, eslint . --no-inline-config): exit 0, no findings.

Acceptance notes


Generated by Claude Code

claude added 2 commits October 7, 2026 19:28
Red on the unfixed report: the human path's early return skips the
--out write, while the --json path writes FILE.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-authored-by: Claude <noreply@anthropic.com>
…hing to migrate

The human report's early return (no applied edit, no manual change)
returned before the --out write, so --from 18 --to 18 --out FILE exited 0
with no snapshot line and no FILE, while --json wrote FILE. The write and
its line move into one helper, called from both exits of the report, in
the main path's order: snapshot, then --write's outcome, then the data
migrations.

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 3 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))

⛔ 8 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 meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-7.mdx (via 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 — 28 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 f2a45db2ad6302cb669fe646df34941146917abb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1fec15794505a37080e4593644c8ebb8e9b96fa2 — the merge of head 0bd4955e8509c49094d868a0a0641ea57ee800af into base f2a45db2ad6302cb669fe646df34941146917abb, 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 1fec15794505a37080e4593644c8ebb8e9b96fa2 && git checkout 1fec15794505a37080e4593644c8ebb8e9b96fa2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f2a45db2ad6302cb669fe646df34941146917abb 0bd4955e8509c49094d868a0a0641ea57ee800af && git checkout -B drift-repro f2a45db2ad6302cb669fe646df34941146917abb && git merge --no-ff 0bd4955e8509c49094d868a0a0641ea57ee800af

node scripts/docs-audit/affected-docs.mjs --json f2a45db2ad6302cb669fe646df34941146917abb

⚠️ 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 f2a45db2ad6302cb669fe646df34941146917abb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 20:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 20:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 54ace18 Oct 7, 2026
35 of 36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22116-migrate-meta-out-snapshot branch October 7, 2026 20:55
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(cli): os migrate meta --out FILE over a range with no step exits 0 and writes no snapshot, while the same run with --json writes FILE

2 participants