Repository navigation
fix(cli): os migrate meta --json says when no migration step ran - #29045
Conversation
An empty or inverted range replays no step. The human run warned that
nothing was replayed, but the --json answer looked like a clean replay.
The payload now carries replayedNothing { reason, widestRange } whenever
no step ran, read by the one function the human run renders, so the
human run also names an inverted range. A replay's payload and every
exit code are unchanged.
Claude-Session: https://claude.ai/code/session_01FEkQ5pLAfce3rbrPKLECmj
Co-Authored-By: Claude <noreply@anthropic.com>
readReplayedNothing hands the authored stack to the widest-range probe, which replays the chain through applyMetaMigrationsToPackages and so reads each package body. The call-site census now names it. Claude-Session: https://claude.ai/code/session_01FEkQ5pLAfce3rbrPKLECmj Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 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 2e2c4d25225696753881a521d488a6eae158e6a8 && git checkout 2e2c4d25225696753881a521d488a6eae158e6a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e ec0c6421707a92032041570d98f2d9aee8c617ba && git checkout -B drift-repro d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e && git merge --no-ff ec0c6421707a92032041570d98f2d9aee8c617ba
node scripts/docs-audit/affected-docs.mjs --json d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Reviewed 2026-11-02T06:38Z against the inputs the brief names: card #28947 (body, the triage note 6569489218, the claim 6569748971, the os-dev-report 6570616458), PR #29045 (its body, its four-file list, the net diff against ① Derived judgments
② Semver levelClause-②: yes (widening)
③ Boundary flags
Implemented-by: VERDICT: PASS |
An inverted range with --to below the terminus (--from T --to T-1) reported a widest range from T to T, which is itself empty. Any range from a --from at or above the terminus is empty or inverted, so widestRange is null there too. Claude-Session: https://claude.ai/code/session_01FEkQ5pLAfce3rbrPKLECmj Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Reviewed 2026-11-02T07:19Z against the inputs the brief names: card #28947 (body, the triage note 6569489218, the claim 6569748971, the os-dev-report 6570616458, with nothing newer on the card), PR #29045 (its body, its four-file list, the net diff against The face is the declaration face. The four paths contain no ① Derived judgments
② Semver levelClause-②: yes (widening)
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #29045 · 2026-11-02T07:21ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5pLAfce3rbrPKLECmj
Requested by Leehom · project thread
Fixes #28947
Clause-②: yes (widening)
Metadata-protocol: none
Clause-② measured: the
--jsonanswer's members are a documented contract in a published surface.skills/objectstack-upgrade/SKILL.md(shipped by@objectstack/skills) tells an agent to read.applied("appliedmust be []"),.todos[].surfaceand.specChangesoffos migrate meta --json, andcontent/docs/upgrading.mdxdocumentstodos/absentTodosunder--all. This PR adds one member to that answer, so it is a widening. AClause-②: yes (widening)declaration on its own is reviewed on its declaration face atTIER_DEFAULT. Metadata-protocol isnone: nothing a metadata author writes is accepted, refused or read differently.维护者速读
os migrate meta --json在一步迁移都没跑的时候(空区间,如--from 18;或倒置区间,如--from 19),JSON 多一个成员replayedNothing: { reason, widestRange },说明「本次什么都没重放」以及更宽的区间里有没有活。人读输出与 JSON 读的是同一个函数;倒置区间的人读文案也改为直说「区间倒置」。applied: []、schemaValid: true、退出码 0),而人读输出会警告「这不代表元数据已是规范形」。脚本或 CI 读 JSON 时,--from写错就会被告知迁移已完成。--from 17输出 2,624,990 字节,掩掉duration后与 main 同 md5),所有退出码不变,倒置区间不新增拒绝。回滚即 revert。packages/cli/src/commands/migrate/meta.ts的readReplayedNothing(唯一判定)与printReplayedNothing(人读渲染);同文件emitJson调用里新增的那一行展开。pnpm --filter @objectstack/cli exec vitest run --project unit test/migrate-meta-json-replayed-nothing.test.ts,预期Tests 8 passed (8)。What changes
packages/cli/src/commands/migrate/meta.ts:printEmptyRangeAnsweris split into one reading and one renderer.readReplayedNothing(stack, result)returnsnullwhen the run replayed at least one step (result.hops.length > 0), and otherwise{ reason, widestRange }.reasonisinverted-rangewhen--fromis above--toandempty-rangeotherwise.widestRangeisnullwhen no wider range exists (--toalready reaches the chain terminus, or--fromis at or above it, so any range from that--fromis itself empty or inverted), and otherwise{ from, to, applied, manual }from the same widest-chain probe the human answer always ran (applyMetaMigrationsToPackagesover the same normalized stack, manual counted throughlistedTodos).printReplayedNothingrenders that answer. The empty-range outcomes print the same text as before, except that an inverted range from a--fromat or above the terminus (--from T --to T-1) now prints the no-wider-range line rather thanThe widest range this build carries (protocol T → T) has nothing for this stack either. An inverted range now prints--from N is above --to M, so the range is inverted and this run replayed nothing — that is not a finding that the metadata is canonical.in place ofNo migration step exists for protocol N → M, ….--jsonpayload spreadsreplayedNothingin afterhopsonly when the reading is non-null, so a run that replayed a step keeps its old payload byte for byte. Exit codes are untouched everywhere, and--fromabove the terminus is answered, not refused.packages/cli/test/normalized-call-sites.test.ts: apackagesrow formigrate/meta.ts :: readReplayedNothing(normalized. The census caught the new call site in the unit tier. The function's only stack read is the widest-range probe, which goes throughapplyMetaMigrationsToPackages(the row above it), so package bodies are counted..changeset/28947-migrate-meta-json-replayed-nothing.md:@objectstack/clipatch with both declaration lines.Tests
packages/cli/test/migrate-meta-json-replayed-nothing.test.ts(unit tier by behaviour: in-processMigrateMeta.run, nothing spawned, no kernel). Every range is derived fromMIGRATION_MAJORS/PROTOCOL_MAJOR, and no major is hard-coded. 8 cases:T, andT-1is at or above the support floor;--from T --json:replayedNothingis{ reason: 'empty-range', widestRange: null }, exit 0;--from T --to T --json: the same;--from T+1 --json:{ reason: 'inverted-range', widestRange: null }, exit 0 (not refused);--from T --to T-1and--from T+1 --to T-1:{ reason: 'inverted-range', widestRange: null }(seat review: a widest range from a--fromat or above the terminus is itself empty or inverted);--from T-1 --to T-1:widestRangenamesT-1 → T, and the human run prints the same counts (face parity);--from T+1names the inverted range;--from T-1 --json(a real replay): no marker, andObject.keysequals the ten members the payload carried before.main's code (135054eb): 5 failed / 2 passed. The guard and the real-replay control were green. The--from T --to T-1case added on review was red first at4066e3ac(1 failed / 7 passed:widestRangecame back{ from: T, to: T, … }).scripts/ablation-replace.mjs, anchor hit once each, restored to the HEAD blob155f6cd4withgit diff HEADempty). The test importssrc/directly, so no build sits on the path.reasonpinned to'empty-range': 2 failed / 5 passed (the inverted JSON case and the inverted human case).fromMajor >= CHAIN_TERMINUS_MAJORarm of the no-wider-range guard removed: 1 failed / 7 passed (the--from T --to T-1case); restored to the HEAD blob6da11573,git diff HEADempty.--from 17 --jsononmainand on this branch are 2,624,990 bytes each, and both hash to md56edb407e…withdurationmasked.Acceptance notes
mainfirst (canonical scratch stack, built CLI at135054eb):--from 18 --json,--from 19 --jsonand--from 18 --to 18 --jsonall exited 0 withapplied: [],todos: [],schemaValid: trueand no member saying no step ran. The human runs printed thereplayed nothingwarning. Control--from 17 --jsonexited 0 with 448 todos (21 absent). On this branch the three empty runs carryreplayedNothing, and--from 17 --to 17 --jsonanswers{ reason: 'empty-range', widestRange: { from: 17, to: 18, applied: 0, manual: 427 } }. The human run prints the same0 mechanical and 427 manual.Verification
All at head
ec0c6421(base135054eb,origin/mainwhen the branch was cut).mainhas moved since; no conflict is reported, so the branch is not re-merged (AGENTS.md multi-agent §10).Package suites, typecheck and lint
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2(verify lock, VERDICT command-exit 0): Test Files 369 passed (369), Tests 5204 passed (5204). The first full run at8eae1397had 1 failed / 5202 passed:normalized-call-sites.test.tsnamed the newreadReplayedNothing(normalizedcall site, which4066e3acclassifies.pnpm --filter @objectstack/cli typecheck(tsc pluscheck:test-typecheckovertsconfig.test.json, which covers the new test): exit 0.pnpm exec eslint --no-inline-config packages/cli: exit 0.Integration tier: declared to CI. The diff touches no integration-tier file and no spawn entry.
Derived gates (dispatch-gates --commands, 61 families)
dispatch-gates.mjs --ran: "61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET: packages outside this closure, e.g.client-react,studio, have nodistin this worktree). CI runs it. (At4066e3ac, in a worktree that had those builds, it exited 0.)check:i18n-coverageexit 0 after the example closure build it names: "14 config(s), 426 baselined untranslated string(s), none new".Generated by Claude Code