fix(cli): os migrate meta --from N lists the conversions its tombstones prescribe, and an empty range stops reading as success - #17462
Conversation
…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>
…grate-meta-default-range
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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>
…grate-meta-default-range
维护者速读(终稿)
改了什么 — 为什么改 — 墓碑里写的 ⭐ 本席复核时确认了一处比卡面更重的事实:那个空区间不只是「没帮上忙」,而是不可证伪 —— 风险与代价(含回滚) — 跨 major 的运行会多跑一跳,输出体积从 516 字节涨到约 342KB; 席位意见 — 建议采纳。
你要做的 — 确认一件事即可:**终点默认值改为「注册表最高 major」**这个方向对不对。
Generated by Claude Code |
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
--todefaulted toPROTOCOL_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.0tombstonesdashboard.refreshIntervalwhiledashboard-refresh-interval-to-refresh-interval-secondsistoMajor: 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 owntoMajor— a presumption that holds only after the next major ships. For the whole 17.x line it composed17 → 17, andcomposeMigrationChainkeepsm > fromMajor, so no step was selected at all.appliedandtodosare empty for every input, so the answer is unfalsifiable:os migrate meta --fromat the installed major could not have reported anything else, for any stack, ever. The published acceptance check inskills/objectstack-upgradewas built on exactly that answer.The repair
Both halves live in
packages/cli/src/commands/migrate/meta.ts.--todefaults to the chain terminus —Math.max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS), the highest major this build of@objectstack/speccarries 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 ownschemaValidverdict is reachable.Math.maxkeeps the runtime's major as the floor for the reverse case.already canonicalwas a green verdict on a check that never ran. It now says so, names the range that would list the conversions, and no longerreturns past the schema verdict that contradicted it. That early return was the second half of the defect: the same run reportedschemaValid: falsein--jsonwhile the human output claimed canonical and stopped.packages/specis untouched — it is the evidence here, andmigrations/registry.tsis held by three open PRs.Acceptance notes
All readings below were taken on
f08b8ddfunless a different sha is named.The tombstone count triage asked for
29 occurrences across 15 shipped source files prescribe
os migrate meta --from 17:The same command with
--from 16returns 78 across 25 files — those work today and keep working, which is what identifies--from 17as 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 point331a1a2b; after isf08b8ddf.--from 17, no--to✓ Nothing to migrate — the metadata is already canonical for this range.· exit 0 · 516 bytesApplied 5 mechanical change(s)then✓ Migrated stack is schema-valid· exit 0--from 17 --jsonapplied: 0, todos: 0, schemaValid: falseapplied: 5, schemaValid: true--from 17 --to 17— the old default, now reachable only by typing it✓⚠ 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 14on a canonical stack (control)✓ Nothing to migrate✓ Nothing to migrate— unchanged, plus the schema verdict the early return used to skipExit 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⚠️ Flagged for the maintainer rather than decided silently; the pin asserts the 0 explicitly so a later ruling changes it deliberately.
schemaValid: falsearm 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.Cost, measured rather than asserted
--from 17now replays one hop further, so it prints that hop's semantic TODOs: the run above is 341,968 bytes (5 mechanical, 112 manual).--from 16composes a real step today and prints 239,560 bytes (77 manual) for the same reason. The old--from 17was 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-objectdiffering from theHEADblob, ran the pins, then restored withgit checkout HEAD --and proved the restored blob hash equals theHEADblob hash with an emptygit diff HEAD.--todefault reverted toPROTOCOL_MAJORresult.hops.length === 0arms)The sixth test —
Nothing to migratestill 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
.e2ename, because that name selects the nightly population and a p1 pinned only there sits outside the merge queue's required set. Queue by name,integrationby behaviour — the combinationvitest-tiers.tssanctions. One process per distinct invocation; the file runs in ~34s.skills/**line budgetskills/objectstack-upgrade/SKILL.mdlinesskills/objectstack-upgrade/SKILL.mdtokensSKILL.mdlines in the packagepnpm check:skills-token-ratchetexits 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 readsappliedfrom--jsonwith the one-sentence reason. This is breakage the diff causes, not scope growth.Noted, not filed
replacementwith "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 readsappliedrather than the headline. Successor: the skill correction in this PR. Carrier: none beyond it.toMajor: 18— is apackages/specfact, 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, all0.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/clipins: 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