Skip to content

fix(cli): os migrate meta --from N lists the conversions its tombstones prescribe, and an empty range stops reading as success - #17462

Draft
os-justin wants to merge 5 commits into
mainfrom
claude/issue-17134-migrate-meta-default-range
Draft

fix(cli): os migrate meta --from N lists the conversions its tombstones prescribe, and an empty range stops reading as success#17462
os-justin wants to merge 5 commits into
mainfrom
claude/issue-17134-migrate-meta-default-range

Conversation

@os-justin

@os-justin os-justin commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17134

os migrate meta --from 17 — the invocation 29 shipped tombstones prescribe — replayed an empty chain and answered ✓ Nothing to migrate, exit 0, for the very conversions that sent the author there.

What was actually wrong

--to defaulted to PROTOCOL_MAJOR, the major the runtime implements. Retirements land throughout a major's line and their ADR-0087 conversions register under the next one: @objectstack/spec@17.4.0 tombstones dashboard.refreshInterval while dashboard-refresh-interval-to-refresh-interval-seconds is toMajor: 18.

The retiredKey() house sentence names the major the source was authored against, so the tombstone is correct to say --from 17. What the template presumes is that the default terminus is at least the conversion's own toMajor — a presumption that holds only after the next major ships. For the whole 17.x line it composed 17 → 17, and composeMigrationChain keeps m > fromMajor, so no step was selected at all.

⚠️ That makes the failure worse than "lists nothing". With an empty chain, applied and todos are empty for every input, so the answer is unfalsifiable: os migrate meta --from at the installed major could not have reported anything else, for any stack, ever. The published acceptance check in skills/objectstack-upgrade was built on exactly that answer.

The repair

Both halves live in packages/cli/src/commands/migrate/meta.ts.

  1. --to defaults to the chain terminusMath.max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS), the highest major this build of @objectstack/spec carries a step for. The tombstone template's presumption now holds in every window instead of only after a major release, and it stays true one major on by construction. ⛔ This is not "migrating past what the runtime runs": every registered conversion maps a shape the installed schemas already refuse onto the one they accept, which is why the terminus is the only target for which this command's own schemaValid verdict is reachable. Math.max keeps the runtime's major as the floor for the reverse case.
  2. A range holding no step is answered as onealready canonical was a green verdict on a check that never ran. It now says so, names the range that would list the conversions, and no longer returns past the schema verdict that contradicted it. That early return was the second half of the defect: the same run reported schemaValid: false in --json while the human output claimed canonical and stopped.

packages/spec is untouched — it is the evidence here, and migrations/registry.ts is held by three open PRs.

Acceptance notes

All readings below were taken on f08b8ddf unless a different sha is named.

The tombstone count triage asked for

29 occurrences across 15 shipped source files prescribe os migrate meta --from 17:

git grep -c 'Run `os migrate meta --from 17`' -- 'packages/**/*.ts' ':!*CHANGELOG*' ':!*.test.ts'

The same command with --from 16 returns 78 across 25 files — those work today and keep working, which is what identifies --from 17 as the broken class rather than the sentence template being wrong.

Before / after, driven on the card's reproduction

Five dashboards authoring dashboard.refreshInterval, real CLI process, NO_COLOR=1. Before is the branch point 331a1a2b; after is f08b8ddf.

run before after
--from 17, no --to ✓ Nothing to migrate — the metadata is already canonical for this range. · exit 0 · 516 bytes Applied 5 mechanical change(s) then ✓ Migrated stack is schema-valid · exit 0
--from 17 --json applied: 0, todos: 0, schemaValid: false applied: 5, schemaValid: true
--from 17 --to 17 — the old default, now reachable only by typing it the same silent ⚠ No migration step exists for protocol 17 → 17, so this run replayed nothing … then ⚠ Protocol 17 → 18 has 5 mechanical and 112 manual change(s) for this stack — re-run with --to 18 … then ⚠ Stack does not pass schema validation, and this run replayed no conversion … · exit 0
--from 13 --to 14 on a canonical stack (control) ✓ Nothing to migrate ✓ Nothing to migrate — unchanged, plus the schema verdict the early return used to skip

Exit codes — read, deliberately not changed

Triage flagged the exit code as half of why this read as success. It stays 0 on every path above, and that is a decision rather than an oversight: this command reports findings instead of exiting on them — its existing schemaValid: false arm has always been a warning at exit 0 — so moving that convention is a wider contract change than this card, and the text now carries the signal. ⚠️ Flagged for the maintainer rather than decided silently; the pin asserts the 0 explicitly so a later ruling changes it deliberately.

Cost, measured rather than asserted

--from 17 now replays one hop further, so it prints that hop's semantic TODOs: the run above is 341,968 bytes (5 mechanical, 112 manual). ⚠️ Not a wall this change invents — --from 16 composes a real step today and prints 239,560 bytes (77 manual) for the same reason. The old --from 17 was 516 bytes only because it replayed nothing. The mechanical rewrite list is printed first, above the TODOs.

Reverse verification — both halves, from the committed state

Each leg mutated the committed source, proved the mutation reached disk by anchor counts plus a git hash-object differing from the HEAD blob, ran the pins, then restored with git checkout HEAD -- and proved the restored blob hash equals the HEAD blob hash with an empty git diff HEAD.

ablation result
--to default reverted to PROTOCOL_MAJOR 3 failed / 3 passed — exactly the three default pins
empty-range branch selection disabled (both result.hops.length === 0 arms) 2 failed / 4 passed — exactly the two empty-range pins

The sixth test — Nothing to migrate still answers a range that HAS steps and rewrote nothing — stays green under both, which is its job: it guards against over-fixing.

Tier placement

The pin spawns the real CLI (a flag default and a printed sentence have no in-process seam) but deliberately does not carry the .e2e name, because that name selects the nightly population and a p1 pinned only there sits outside the merge queue's required set. Queue by name, integration by behaviour — the combination vitest-tiers.ts sanctions. One process per distinct invocation; the file runs in ~34s.

skills/** line budget

reading before after ceiling
skills/objectstack-upgrade/SKILL.md lines 595 600
skills/objectstack-upgrade/SKILL.md tokens 7,199 7,269 8,333
all SKILL.md lines in the package 6,127 6,132
authored bundle tokens (whole ratcheted set) 128,870 128,940 145,552

pnpm check:skills-token-ratchet exits 0. The three edits are corrections, not expansion: that skill prescribed a replay check that was vacuous before this PR (empty range, same answer for every input) and unsatisfiable after it, so it now reads applied from --json with the one-sentence reason. This is breakage the diff causes, not scope growth.

Noted, not filed

  • Two step-18 semantic entries open their replacement with "Nothing to migrate to, because …", so the command's own verdict phrase appears inside its own TODO prose. A grep for the phrase can report the verdict present on a run that never printed it — which is a second, independent reason the corrected acceptance check reads applied rather than the headline. Successor: the skill correction in this PR. Carrier: none beyond it.
  • The tombstone/registry disagreement itself — a key tombstoned in the 17.x line whose conversion registers at toMajor: 18 — is a packages/spec fact, out of this lane and on a file three open PRs hold. This PR makes the CLI correct for it rather than changing it. Successor: none needed if the terminus default is accepted.

Verification

  • dispatch-gates --ran: 75 derived families, 75 run, 0 NOT-MEASURED, 0 UNRUN — every one recorded with its exit code, all 0.
  • pnpm lint (eslint . --no-inline-config, whole repo, no narrowing): exit 0 over 6,562 files, 0 errors, 0 warnings.
  • pnpm --filter @objectstack/cli typecheck: exit 0. pnpm --filter @objectstack/spec check:generated: exit 0, 15/15 artifacts current.
  • packages/cli pins: 6 passed / 6.

Clause-②: no — no new flag, no new exported symbol; the change pulls a prescribed invocation back to the contract its own tombstones declare.

Authored by Claude Code, session session_01DapQyvYrFb1MxSYe7BL2nt.

维护者速读(草稿)

改了什么os migrate meta--to 默认值,从「运行时自己的 protocol major」改为「本次构建的迁移注册表里最高的那个 major」。外加:一个不含任何迁移步骤的区间,不再借用成功文案,并且不再跳过与它相矛盾的 schema 判定行。

为什么改 — 墓碑里写的 os migrate meta --from 17 是 29 处已发布提示语共同指定的命令。它组合出的区间 17 → 17 一个步骤都选不中,于是对任何输入都回答「没有要迁移的」并以 0 退出。作者照着平台自己的处方走,得到的是一个永远不会失败的检查——已发布技能里的验收步骤正是建立在这个答案上的。

风险与代价(含回滚) — 跨 major 的运行会多跑一跳,输出体积 516 字节涨到 341,968 字节;但这正是 --from 16 今天已有的形态(239,560 字节),不是本次新造的噪音,机械改写清单仍在最上方。退出码一律不变。回滚 = 还原 meta.ts 的一处默认值与一个分支,无数据迁移、无 schema 改动、无新增公开面。

席位意见 — (留空,待定稿)

你要做的 — 本 PR 触及受管面 skills/**,按 Prime Directive #14 只能由维护者手动合入。请确认两点:① 终点默认值改为「注册表最高 major」这一裁决;② 退出码保持 0 是否接受(triage 曾点名它,本 PR 明确不改并把它写进了 pin)。


Generated by Claude Code

…nswer an empty range as an empty range

`--to` defaulted to `PROTOCOL_MAJOR`, but retirements land throughout a
major's line: `@objectstack/spec@17.4.0` tombstones keys whose ADR-0087
conversion is registered `toMajor: 18`. The tombstone sentence names the
major the source was AUTHORED against (`--from 17`), so the prescribed
invocation composed `17 → 17` — a range `composeMigrationChain` selects no
step for — and reported `Nothing to migrate` for the very conversions that
sent the author there. 29 shipped tombstones across 15 files prescribe it.

The terminus is now read off `MIGRATION_MAJORS`, floored at `PROTOCOL_MAJOR`,
so the tombstone template's presumption holds in every window rather than
only after a major release.

Second half: an empty range no longer borrows the success answer. A range
holding no step replayed nothing, so `already canonical` was a green verdict
on a check that never ran; it now says so, names the range that would list
the conversions, and no longer returns past the schema verdict that
contradicted it.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
…; correct the upgrade skill's replay check

The new spawned suite drives the card's reproduction — five sites authoring a
tombstoned key — through the real CLI, and derives every expectation from
`MIGRATION_MAJORS` / `PROTOCOL_MAJOR` so no major is written down.

`skills/objectstack-upgrade` prescribed `os migrate meta --from <target>` and
"must say Nothing to migrate" as its acceptance check. With the terminus
default that check is unsatisfiable — and it was vacuous before it, since the
range it composed selected no step and answered the same way for every input.
It now reads `applied` from `--json`, with the reason stated once.

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

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

11 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/apps.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))

5 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/ 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 23 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 e758131b3900eb13260f03643e295ca6d625c42bpackageMentionDocs.

Which tree this was computed on

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

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

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

…the whole verdict sentence

`*.e2e.test.ts` selects the nightly population, so a p1 pinned only there is
outside the merge queue's required set. The file keeps its spawn (a flag
default and a printed sentence have no in-process seam) and drops the name, so
it is queue by name and `integration` by behaviour — the combination
`vitest-tiers.ts` sanctions — and shares one process per distinct invocation.

The headline assertion now names the whole sentence: two step-18 semantic
entries open with "Nothing to migrate to, because …", so the bare phrase
appears in prose that is not this command's verdict.

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

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

domain:cli 执行 PM 席(#6024)对照自己读过的 diff 校正了 dev 的草稿并填入席位意见。⚠️ 本 PR 触及受管面 skills/objectstack-upgrade/SKILL.md,按规矩只能由你手动合入 —— ⛔ 本席未入队、未挂 auto-merge。

改了什么os migrate meta--to 默认值,从「运行时自己的 protocol major」改为「本次构建的迁移注册表里最高的那个 major」。附带两处:一个不含任何迁移步骤的区间不再借用成功文案;那条分支里的提前 return 不再跳过与它自相矛盾的 schema 判定。受管面那 8 行改的是同一个技能里被本次修复弄假的一条验收命令。

为什么改 — 墓碑里写的 os migrate meta --from 1729 处已发布提示语共同指定的命令。它组合出的区间 17 → 17 一个步骤都选不中,于是对任何输入都回答「没有要迁移的」并以 0 退出。作者照着平台自己的处方走,得到的是一个永远不会失败的检查 —— 而已发布技能里的验收步骤正是建立在这个答案上的。

本席复核时确认了一处比卡面更重的事实:那个空区间不只是「没帮上忙」,而是不可证伪 —— composeMigrationChain 保留 m > fromMajor,所以 --from N --to N 选不中任何步骤,applied / todos一切输入都为空。另有一处此前无人点名:零改动分支的提前 return 会跳过 schema 判定,于是同一次运行在 --json 里报 schemaValid: false,而人类看到的输出却宣称「已是规范形态」并停下。

风险与代价(含回滚) — 跨 major 的运行会多跑一跳,输出体积从 516 字节涨到约 342KB;⚠️ 但这正是 --from 16 今天已有的形态(约 240KB),不是本次新造的噪音,机械改写清单仍在最上方。退出码一律不变。回滚 = 还原 meta.ts 的一处默认值与一个分支(受管面那 8 行随之还原),无数据迁移、无 schema 改动、无新增公开面。

席位意见建议采纳。

  • ⭐ 受管面的改动是本次修复的后果,不是搭车扩写:技能里那条 --from 17 # must say "Nothing to migrate" 被本 PR 弄假了,不修就是留一条错的已发布指令。而且修法是加强而非打补丁 —— 从「grep 标题行」改成「读 --jsonapplied」。dev 还测到第二个理由:有两条 step-18 条目的 replacement 正文里就写着「Nothing to migrate to, because …」,所以按标题短语 grep 会在根本没打印过该判决的运行上误报。⇒ 旧检查在两个独立方向上都脆,现在都不脆了。
  • 预算全部在线内并已申报:文件 595 → 600 行、7199 → 7269 token(上限 8333);整包 128870 → 128940(上限 145552);check:skills-token-ratchet 退出 0。
  • 证据是驱动出来的,不是断言:卡面复现跑了前后两次,并且保留了成功文案(--from 13 --to 14 在规范栈上仍答「Nothing to migrate」)且有专门测试防止改过头;消融两半各自只红掉对应的 pin。
  • 一处必须讲明的限制:受管面 PR 的复核席按规矩应跑在契约复审档位,本席不在那个档位(2026-09-10T03:12Z 裁决把它保留给 skills 席、spec 席的条款②复核与总监席)。⇒ 上面是默认档复核,不是规矩要求的那次达档复核。本席已另立卡记录这条规则缺口,⛔ 未含糊带过。

你要做的 — 确认一件事即可:**终点默认值改为「注册表最高 major」**这个方向对不对。

⚠️ 附带一个可选的第二问(⛔ 不阻塞本 PR):分诊曾点名「退出码 0」是缺陷的一半。本 PR 不改它并说明了理由 —— 该命令的 schema-invalid 分支一向以 0 警告,改它会挪动整个 migrate 家族的约定。这个 0 已被写进 pin,所以你若裁反方向,是一行代码加一条断言的事。


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/m tests tooling

Projects

None yet

2 participants