Skip to content

fix(cli): os migrate meta --json says when no migration step ran - #29045

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sj98zz
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sj98zz

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5pLAfce3rbrPKLECmj

Requested by Leehom · project thread

Fixes #28947

Clause-②: yes (widening)
Metadata-protocol: none

Clause-② measured: the --json answer'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 ("applied must be []"), .todos[].surface and .specChanges off os migrate meta --json, and content/docs/upgrading.mdx documents todos / absentTodos under --all. This PR adds one member to that answer, so it is a widening. A Clause-②: yes (widening) declaration on its own is reviewed on its declaration face at TIER_DEFAULT. Metadata-protocol is none: nothing a metadata author writes is accepted, refused or read differently.

维护者速读

  • 改了什么:os migrate meta --json 在一步迁移都没跑的时候(空区间,如 --from 18;或倒置区间,如 --from 19),JSON 多一个成员 replayedNothing: { reason, widestRange },说明「本次什么都没重放」以及更宽的区间里有没有活。人读输出与 JSON 读的是同一个函数;倒置区间的人读文案也改为直说「区间倒置」。
  • 为什么改:以前 JSON 对空区间的回答和一次干净的重放一模一样(applied: []、schemaValid: true、退出码 0),而人读输出会警告「这不代表元数据已是规范形」。脚本或 CI 读 JSON 时,--from 写错就会被告知迁移已完成。
  • 风险与回滚:纯加法。真正跑了迁移步骤的 JSON 逐字节不变(实测 --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: printEmptyRangeAnswer is split into one reading and one renderer.
    • readReplayedNothing(stack, result) returns null when the run replayed at least one step (result.hops.length > 0), and otherwise { reason, widestRange }. reason is inverted-range when --from is above --to and empty-range otherwise. widestRange is null when no wider range exists (--to already reaches the chain terminus, or --from is at or above it, so any range from that --from is itself empty or inverted), and otherwise { from, to, applied, manual } from the same widest-chain probe the human answer always ran (applyMetaMigrationsToPackages over the same normalized stack, manual counted through listedTodos).
    • printReplayedNothing renders that answer. The empty-range outcomes print the same text as before, except that an inverted range from a --from at or above the terminus (--from T --to T-1) now prints the no-wider-range line rather than The 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 of No migration step exists for protocol N → M, ….
    • The --json payload spreads replayedNothing in after hops only 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 --from above the terminus is answered, not refused.
  • packages/cli/test/normalized-call-sites.test.ts: a packages row for migrate/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 through applyMetaMigrationsToPackages (the row above it), so package bodies are counted.
  • .changeset/28947-migrate-meta-json-replayed-nothing.md: @objectstack/cli patch with both declaration lines.

Tests

  • New packages/cli/test/migrate-meta-json-replayed-nothing.test.ts (unit tier by behaviour: in-process MigrateMeta.run, nothing spawned, no kernel). Every range is derived from MIGRATION_MAJORS / PROTOCOL_MAJOR, and no major is hard-coded. 8 cases:
    • guard: the registry has a step at the terminus T, and T-1 is at or above the support floor;
    • --from T --json: replayedNothing is { 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-1 and --from T+1 --to T-1: { reason: 'inverted-range', widestRange: null } (seat review: a widest range from a --from at or above the terminus is itself empty or inverted);
    • --from T-1 --to T-1: widestRange names T-1 → T, and the human run prints the same counts (face parity);
    • human --from T+1 names the inverted range;
    • control --from T-1 --json (a real replay): no marker, and Object.keys equals the ten members the payload carried before.
  • Red first on main's code (135054eb): 5 failed / 2 passed. The guard and the real-replay control were green. The --from T --to T-1 case added on review was red first at 4066e3ac (1 failed / 7 passed: widestRange came back { from: T, to: T, … }).
  • Ablations (scripts/ablation-replace.mjs, anchor hit once each, restored to the HEAD blob 155f6cd4 with git diff HEAD empty). The test imports src/ directly, so no build sits on the path.
    • The JSON spread replaced with an empty spread: 4 failed / 3 passed (the four JSON marker cases).
    • reason pinned to 'empty-range': 2 failed / 5 passed (the inverted JSON case and the inverted human case).
    • The fromMajor >= CHAIN_TERMINUS_MAJOR arm of the no-wider-range guard removed: 1 failed / 7 passed (the --from T --to T-1 case); restored to the HEAD blob 6da11573, git diff HEAD empty.
  • Byte identity of a real replay, measured with the built CLI on a canonical stack: --from 17 --json on main and on this branch are 2,624,990 bytes each, and both hash to md5 6edb407e… with duration masked.

Acceptance notes

  • Measured on main first (canonical scratch stack, built CLI at 135054eb): --from 18 --json, --from 19 --json and --from 18 --to 18 --json all exited 0 with applied: [], todos: [], schemaValid: true and no member saying no step ran. The human runs printed the replayed nothing warning. Control --from 17 --json exited 0 with 448 todos (21 absent). On this branch the three empty runs carry replayedNothing, and --from 17 --to 17 --json answers { reason: 'empty-range', widestRange: { from: 17, to: 18, applied: 0, manual: 427 } }. The human run prints the same 0 mechanical and 427 manual.
  • The human face changed for the inverted range only. It follows from the one reading: the JSON reason says the range is inverted, so the human line says so too. No existing pin reads that line, and the empty-range lines are byte-identical.

Verification

All at head ec0c6421 (base 135054eb, origin/main when the branch was cut). main has 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 at 8eae1397 had 1 failed / 5202 passed: normalized-call-sites.test.ts named the new readReplayedNothing(normalized call site, which 4066e3ac classifies.

  • pnpm --filter @objectstack/cli typecheck (tsc plus check:test-typecheck over tsconfig.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)
  • 60 run with exit 0, 1 NOT MEASURED. dispatch-gates.mjs --ran: "61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET: packages outside this closure, e.g. client-react, studio, have no dist in this worktree). CI runs it. (At 4066e3ac, in a worktree that had those builds, it exited 0.)
  • check:i18n-coverage exit 0 after the example closure build it names: "14 config(s), 426 baselined untranslated string(s), none new".
  • Gate logs are captured to file before any pipe.

Generated by Claude Code

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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 35 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 d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e2c4d25225696753881a521d488a6eae158e6a8 — the merge of head ec0c6421707a92032041570d98f2d9aee8c617ba into base d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e, 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 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

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

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 4066e3ac095d8cba93a15fd65755cce30622a567
Local-runs: none

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 main from merge base 135054eb), and the check-runs on the head. The face is the declaration face. The four paths contain no packages/spec/src/** path (no schema hit) and no GOVERNED_SURFACES path (no rules hit). The Clause-②: line reads yes (widening) on both the PR body and the changeset, with no narrowing arm. So reviewFace answers declaration, which maps to TIER_DEFAULT, and this face carries no ④ section.

① Derived judgments

  • The public surface widens by one JSON member, and the change is named right. os migrate meta --json now carries replayedNothing: { reason, widestRange } only when result.hops is empty, which covers an empty range (--from T, --from N --to N) and an inverted one (--from T+1). It is spread after hops in emitJson. The member is a documented contract: skills/objectstack-upgrade/SKILL.md on main reads .applied and .specChanges off this payload (lines 74, 225 and 403), and content/docs/upgrading.mdx documents todos and absentTodos under --json. So adding the member widens a published surface, and Clause-②: yes (widening) is the right declaration. A real replay keeps its old payload. The control case pins Object.keys to the ten members the payload had before, and the dev measured byte identity on --from 17.
  • The accept set is unchanged, and that is right. Every exit code is the same as before. --from above the terminus is answered, not refused, which is one of the two outcomes the card accepts ("refused like the floor, or answered with the same marker"). The floor refusal (unsupported_from_major) is untouched. No flag, argument or config key is added or removed.
  • Both faces read the same function, and that is right. printEmptyRangeAnswer is split into readReplayedNothing (the reading) and printReplayedNothing (the human renderer). Human-path gating is equivalent to before. The old test was hops.length === 0 inside the applied/todos-empty branch. The new test is readReplayedNothing(...) !== null, which is hops.length === 0. applied is flattened from the hops, so an empty hops implies an empty applied. The empty-range human lines are byte-identical. Only the inverted-range line changed, and it now names the inversion. Nothing on main pins the old wording for an inverted range: the existing pins (migrate-meta-default-range.test.ts, migrate-meta-out-snapshot.test.ts, meta.report-order.test.ts, the 18-1 release note) all use the empty-range sentence, which is unchanged.
  • The census row is right. In normalized-call-sites.test.ts, readReplayedNothing(normalized is classified reads: 'packages'. The function's only stack read is applyMetaMigrationsToPackages, which replays each packages[i].manifest body.
  • One edge of the new member is wrong (should-fix 1). widestRange is null only when toMajor >= CHAIN_TERMINUS_MAJOR. Take an inverted range whose --to is below the terminus: --from T --to T-1 probes T → T, and --from T+1 --to T-1 probes T+1 → T. Both answer a non-null widestRange naming a range that is itself empty or inverted, with applied: 0, manual: 0. The changeset defines the member as "the widest range this build carries from the same --from … or null when … no wider range exists". The changeset's own definition says no wider range exists in either case, so the member contradicts it. A consumer that retries with --to widestRange.to lands in another empty range. The human path printed the same degenerate "protocol T → T has nothing for this stack either" before this PR, so this is not a regression. But the JSON member is new, and changing it from an object to null after it ships changes the meaning of a published value. The fix is to answer null when fromMajor >= CHAIN_TERMINUS_MAJOR as well, and to add a test case for --from T --to T-1. No test covers an inverted range whose --to is below the terminus.
  • The tests are adequate for what they claim. There are 7 in-process unit cases, every range is derived from MIGRATION_MAJORS/PROTOCOL_MAJOR, and there is an anti-vacuity guard. The report also shows red first and two ablations that each turn named cases red. The face-parity case ties the JSON counts to the human sentence.

② Semver level

Clause-②: yes (widening)

  • The changeset .changeset/28947-migrate-meta-json-replayed-nothing.md declares '@objectstack/cli': patch. @objectstack/cli is a released package, and the change is a bug fix that adds an output member. It is a non-protocol widening, and AGENTS.md (Post-Task Checklist step 3) says that "a non-protocol change, widening or breaking, ships patch". The level matches what the diff publishes. Nothing else is published: the other three paths are two cli tests and the changeset itself.
  • Metadata-protocol: none is right. Nothing a metadata author writes is accepted, refused or read differently, and no packages/spec path is touched.
  • Both declaration lines are line-initial and identical on the PR body and the changeset body, with a single arm from the closed pair. No BREAKING banner or ADR-0087 disposition is owed.
  • main has moved since the base 135054eb. Its new packages/spec commits touch shared/region-currency.ts and the automation files, not migrations/** or packages/cli, so the diff's reading is not stale.

③ Boundary flags

  • open_questions: [], so there is nothing to answer.
  • Deviation: Clause-② declared yes where the claim expected no. Answered: the deviation is upheld (see ①). The claim itself anticipated it: "if the dev measures the --json answer shape as published, the dev declares yes (widening)".
  • Deviation: the PR body says a CONTRACT_REVIEW_TIER review is owed. Answered: wrong as worded. The claim and the dev report repeat the same statement. Under the face mapping (reviewFace in scripts/pm/record-recognisers.mjs, ruling 6159870311 on decision: which model serves the clause-② contract review — fable on every lane (ruling 5771798588), or fable only for the spec / skills lanes and governed surfaces with opus elsewhere? #23352), a review owed by the Clause-②: yes line alone, with no schema path, no governed path and no narrowing arm, is the declaration face, served at TIER_DEFAULT by the lane seat. This record is that review. Should-fix 2 is to correct the sentence in the PR body so that no later reader holds the PR for a contract-tier record it does not owe.
  • Deviation: the file surface widened by one cli test (the census row). Answered: this is within the claim's "a cli test" and is required by the census gate.
  • Out-of-scope finding, carrier: none (skills/objectstack-upgrade/SKILL.md around line 403: os migrate meta --from TARGET --json # applied must be [] cannot fail when TARGET is the terminus). Escalated: I confirmed it on main, so it is a real vacuous acceptance check. It is out of this card's surface. skills/** is a governed Tier H surface, so it must not ride on this PR. Should-fix 3 is that the seat files it as a card for the skills surface. The fix would check that replayedNothing is absent alongside applied == []. As written, it carries nothing.
  • Gates. All 53 check-runs on the head are completed: 36 success, 17 skipped, and none failure, cancelled or timed_out. The successes include Test Core 1/8 to 8/8 and its rollup, all four Type Check jobs, every Lint job, Build Core, Check Changeset (both runs), PR-body guards and Governed Surface Queue Guard. The skipped runs are the UI-path legs the filter job routed away, plus the opt-in harnesses (18.x upgrade, packed-tarball smoke) and the schedule gate. Those conclusions are the gate verdicts, and nothing red stands. The dev's local dispatch-gates claim (61 of 61) was not relied on and was not re-run.

Implemented-by: claude/project-thread-sj98zz
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: TIER_DEFAULT
Head-sha: ec0c6421707a92032041570d98f2d9aee8c617ba
Local-runs: none

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 main from merge base 135054eb), and the check-runs on this head. This record supersedes the one rendered on 4066e3ac. The new commit ec0c6421 addresses that record's should-fix 1 and was read as a delta on top of it.

The face is the declaration face. The four paths contain no packages/spec/src/** path (no schema hit) and no GOVERNED_SURFACES path (no rules hit). The Clause-②: line reads yes (widening) on both the PR body and the changeset, with no narrowing arm. So reviewFace answers declaration, which maps to TIER_DEFAULT, and this face carries no ④ section.

① Derived judgments

  • The public surface widens by one JSON member, and the change is named right. os migrate meta --json now carries replayedNothing: { reason, widestRange } only when result.hops is empty, which covers an empty range (--from T, --from N --to N) and an inverted one (--from above --to). It is spread after hops in emitJson. The member is a documented contract: skills/objectstack-upgrade/SKILL.md on main reads .applied and .specChanges off this payload, and content/docs/upgrading.mdx documents todos and absentTodos under --json. So adding the member widens a published surface, and Clause-②: yes (widening) is the right declaration. A real replay keeps its old payload. The control case pins Object.keys to the ten members the payload had before, and the dev measured byte identity on --from 17.
  • The accept set is unchanged, and that is right. Every exit code is the same as before. --from above the terminus is answered, not refused, which is one of the two outcomes the card accepts. The floor refusal (unsupported_from_major) is untouched. No flag, argument or config key is added or removed.
  • Both faces read the same function, and that is right. readReplayedNothing is the reading and printReplayedNothing is the human renderer. Human-path gating is equivalent to before: readReplayedNothing(...) !== null is hops.length === 0, and applied is flattened from the hops. The empty-range human lines are byte-identical. The inverted-range line now names the inversion, and no existing pin on main reads the old wording for an inverted range.
  • widestRange is now right at every edge. This was should-fix 1 on 4066e3ac, and ec0c6421 resolves it. readReplayedNothing answers widestRange: null when toMajor >= CHAIN_TERMINUS_MAJOR or fromMajor >= CHAIN_TERMINUS_MAJOR. composeMigrationChain keeps the majors with m > fromMajor, so a --from at or above the terminus has no step to the terminus. A non-null answer is left only where fromMajor and toMajor are both below the terminus. There, fromMajor → CHAIN_TERMINUS_MAJOR is a well-formed range that holds the terminus's own step, so the member never names an empty or inverted range as "the widest". The new test case pins --from T --to T-1 and --from T+1 --to T-1 to { reason: 'inverted-range', widestRange: null }, which closes the earlier gap. The changeset text and the interface docblock now give the same two conditions as the code. The human face follows from the shared reading: --from T --to T-1 now prints the "no wider range is available" line, not a degenerate "protocol T → T has nothing" line. No test pins that human line, which is acceptable because it is rendered from the value the JSON case pins.
  • The census row is right. In normalized-call-sites.test.ts, readReplayedNothing(normalized is classified reads: 'packages'. Its only stack read is applyMetaMigrationsToPackages. The delta adds no new call site.
  • The tests are adequate for what they claim. There are now 8 in-process unit cases, every range is derived from MIGRATION_MAJORS/PROTOCOL_MAJOR, and there is an anti-vacuity guard. The report also shows red first and two ablations on the original commit. The CI unit and Test Core shards are green on this head.

② Semver level

Clause-②: yes (widening)

  • The changeset .changeset/28947-migrate-meta-json-replayed-nothing.md still declares '@objectstack/cli': patch. The delta reworded only the widestRange bullet. @objectstack/cli is a released package, and the change is a bug fix that adds an output member. It is a non-protocol widening, which ships as a patch (AGENTS.md, Post-Task Checklist step 3). The level matches what the diff publishes, and nothing else is published.
  • Metadata-protocol: none is right. Nothing a metadata author writes is accepted, refused or read differently, and no packages/spec path is touched.
  • Both declaration lines are line-initial and identical on the PR body and the changeset body, with a single arm from the closed pair. No BREAKING banner or ADR-0087 disposition is owed.
  • main has moved since the base 135054eb. In packages/cli it touched only utils/i18n-extract.ts and its test, and it touched nothing under packages/spec/src/migrations, so the diff's reading is not stale.

③ Boundary flags

  • open_questions: [], so there is nothing to answer.
  • Deviation: Clause-② declared yes where the claim expected no. Answered: the deviation is upheld (see ①). The claim anticipated it.
  • Deviation: the PR body said a CONTRACT_REVIEW_TIER review was owed. Closed. This was should-fix 2 on 4066e3ac. The body was edited with the head unchanged, and as it stands it reads: "A Clause-②: yes (widening) declaration on its own is reviewed on its declaration face at TIER_DEFAULT." That matches reviewFace in scripts/pm/record-recognisers.mjs (ruling 6159870311 on decision: which model serves the clause-② contract review — fable on every lane (ruling 5771798588), or fable only for the spec / skills lanes and governed surfaces with opus elsewhere? #23352), and this record is that review. The body's declaration lines are unchanged and still line-initial. The Clause-② measured: prose line names the key twice, so it reads as describing, not declaring; the first declaration line, yes (widening), decides. Both PR-body guard runs on this head, before and after the edit, are success. The claim and the dev report still repeat the old sentence, but they are records of their time and need no edit.
  • Deviation: the file surface widened by one cli test (the census row). Answered: this is within the claim's "a cli test" and is required by the census gate.
  • Out-of-scope finding, carrier: none (skills/objectstack-upgrade/SKILL.md: os migrate meta --from TARGET --json # applied must be [] cannot fail when TARGET is the terminus). Escalated: I confirmed it on main, so it is a real vacuous acceptance check. It is outside this card. skills/** is a governed Tier H surface, so it must not ride on this PR. The seat files it as a card for the skills surface; the fix would also check that replayedNothing is absent. As written, it still carries nothing.
  • Gates. All 52 check-runs on this head are completed: 37 success, 15 skipped, and none failure, cancelled or timed_out. The successes include Test Core 1/8 to 8/8 and its rollup, all four Type Check jobs and TypeScript Type Check, every Lint job and its rollup, Build Core, Check Changeset (both runs), PR-body guards (both runs) and Governed Surface Queue Guard. The skipped runs are the UI-path legs the filter job routed away, plus the opt-in harnesses (18.x upgrade, packed-tarball smoke) and the schedule gate. Those conclusions are the gate verdicts, and nothing red stands.

Implemented-by: claude/project-thread-sj98zz
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #29045 · 2026-11-02T07:21Z

Head-sha: ec0c6421707a92032041570d98f2d9aee8c617ba

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28947,与认领 6569748971、派单 pm-orders/server/order-28947.md 一致。文件 4 个,均在申报面内(含 normalized-call-sites.test.ts 普查登记一行)。
  2. 前提实测:dev 在 main 实测空、倒置区间 JSON 与真实回放无从区分;红先后修绿;真实回放 JSON 的成员集合钉为原 10 个。复核 should-fix(倒置区间的 widestRange 指向空区间)已在 ec0c6421 修正并钉住,红先 1/8。
  3. 门禁:本 head check-runs 52 条全部结束:37 success、15 skipped、0 失败。dev 本地 60/61 派生门禁 exit 0,check:dual-build-cjs-loads 在重建的工作树前提缺失(上一 head 为 0),由 CI 作答。
  4. 接受的偏差:无。PR 正文已更正为声明面 TIER_DEFAULT。
  5. 范围外去向:dev 发现 skills/objectstack-upgrade/SKILL.md 的验收检查在最高迁移大版本上永远不会失败,席位已立 skills(objectstack-upgrade): the §3.3 acceptance check os migrate meta --from <target-major> --json with applied must be [] can never fail at the top migration major #29063(skills 车道,Tier H)。Metadata-protocol: none。
  6. 落地条款:Clause-②: yes (widening)(新增一个已发布 JSON 成员)⇒ 声明面 TIER_DEFAULT 复核 PASS 6571444301(Head-sha 与现 head 一致;上一 head 的 PASS 为 6570814542),needs:contract-review 已摘。semver:@objectstack/cli patch,与 changeset 一致。check-governed-merges --pr 29045:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 07:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 07:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit c749198 Nov 2, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-sj98zz branch November 2, 2026 08:37
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant