Skip to content

fix(cli): os migrate meta prints the manual change that judges an applied edit beside that edit, marked review - #21025

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20620-migrate-meta-pairing
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20620-migrate-meta-pairing

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20620
Clause-②: no

Direction 2 of triage's first grade on #20620 (comment 5888087153): 「Where an applied conversion has a semantic sibling that judges it, print the sibling beside the applied edit, marked review.」 It pairs through the declared SemanticMigration.conversionIds only, never through text mentions (release 5916560364). Direction 1 (order) landed in PR #20691. Direction 3 (write-back) is not taken by triage, so this PR closes the card.

What changes

printMigrationReport (packages/cli/src/commands/migrate/meta.ts), the human face of os migrate meta --from N, now prints group 2 through printAppliedEdits. Under each run of applied edits by one conversion, it prints one review line for every semantic entry that names that conversion in conversionIds:

  Applied 3 mechanical change(s):
    • dashboards[0].refreshIntervalSeconds: refreshInterval → refreshIntervalSeconds (dashboard-refresh-interval-to-refresh-interval-seconds)
    • flows[0].nodes[1].config.mode: mode unset (2 conditioned out-edges; every one whose condition held was taken) → inclusive (flow-decision-mode-inclusive-explicit)
    • flows[1].nodes[1].config.mode: mode unset (2 conditioned out-edges; every one whose condition held was taken) → inclusive (flow-decision-mode-inclusive-explicit)
      ↳ review the 2 edits above against the manual change [protocol 18] flow.nodes[].config.mode (decision) — an OMITTED mode on a decision that branches on its out-edges and carries two or more conditioned ones → nothing, where the out-edge conditions partition (exactly one can hold for any record): […]

(The review line is cut at […] here. It is printed in full.)

  • It is a copy, not a move. The review line repeats the entry's [protocol N] surface → replacement headline, which is the line group 3 prints for that entry. Group 3 still prints every entry, byte for byte, under the same N manual change(s) require your judgment: count, so ADR-0087 D3's "never silence" holds. The why and verify lines are found in group 3 under the same headline.
  • Declared links only. judgesByConversion reads todo.conversionIds, which the chain copies from the entry. Nothing matches prose.
  • Nothing else moves. Every edit line is byte-identical and in chain order, and the group 2 header count is unchanged. An edit that no entry judges prints with nothing under it. --json, --out, the exit code and the loader's stderr are untouched. --stored does not use this printer.

The dispatch's mechanism assumptions, measured

  • H1 holds. applyMetaMigrations builds each todo as { ...s, toMajor } (packages/spec/src/migrations/chain.ts), so every todo carries conversionIds. On main the registry declares exactly two links, and both point into the same hop: flow-decision-edge-branching-first-match (18) → flow-decision-mode-inclusive-explicit (18), and time-default-zone-refused (18) → time-default-utc-suffix-dropped (18). No authored link crosses a hop yet. The join runs over the flat applied / todos arrays, so a cross-hop link pairs by construction. A pin proves it by adding a link from a protocol-18 entry to the protocol-17 conversion action-execute-to-target on a real chain result. Across steps, the 108 step conversion ids are unique (0 duplicates).
  • H2 holds, as a copy. Group 3 keeps every entry, byte-identical, with its header count unchanged. The review line sits in group 2 beside the edits it judges. I did not change it to a move. A move would take the entry out of group 3, and its count would then stop matching the chain's todos.
  • H3: one review line per run, not one per edit. A conversion's edits are contiguous by construction, because the chain replays one conversion at a time and its apply emits every edit before the next conversion starts. So a run is "this conversion's edits in this hop". HotCRM had 13 edits by flow-decision-mode-inclusive-explicit. One review line per edit would repeat a paragraph-long replacement 13 times, which is the noise this card is about. No information is lost: every edit line still names its conversionId, and the review line counts the edits above it ("the 2 edits above", or "the edit above" for a run of one). If a conversion's edits were ever split into two runs, each run would get its own review line, so a review line never sits far from its edits.
  • H4: --stored does not use printMigrationReport. runStored prints formatStoredMigrationReport from @objectstack/metadata-protocol, so this PR does not touch it.

Evidence: real stacks through bin/run.js

Two small authored stacks (export default { … } configs) were run with os migrate meta --from 17, once on origin/main at 315888d660 and once at this PR's head, c6622657f2:

  • a decision stack: two flows, each with an edge-branched decision with two conditioned out-edges and no mode, plus a dashboard refreshInterval;
  • a time stack: two time field defaults, 09:00Z and 17:30:00+00:00, plus a dashboard refreshInterval.
decision stack time stack
stdout lines, before → after 813 → 814 813 → 814
stdout diff (timing and --out path normalised) one added line, the review line under the 2 flow-decision-mode-inclusive-explicit edits one added line, the review line under the 2 time-default-utc-suffix-dropped edits
group 3 entries / header count 253 = 253 253 = 253
--json apart from duration identical identical
--out snapshot byte-identical byte-identical
stderr 0 bytes both runs 0 bytes both runs
exit code (human / --json) 0 / 0 both runs 0 / 0 both runs

Before this change, the decision edits printed on lines 13 and 14, and the entry that judges them printed at line 351.

Tests

The pins extend packages/cli/src/commands/migrate/meta.report-order.test.ts rather than a new file. That file already holds the printer's helpers (run, appliedLines, noticeLines, refusalLines) and the SET pins the pairing must not break, so the pairing pins reuse them, and every pin on this printer stays in one file. The new PAIR block adds:

  1. the judge prints once, directly under the run of edits it judges, counting them, inside group 2;
  2. a run of one says "the edit above";
  3. an edit no entry judges prints exactly as before, with no review line under it, and group 2's edit lines equal the chain's, in order;
  4. a stack whose applied conversions have no link prints no review line at all;
  5. group 3 keeps every entry, the judge included, with the chain's count and bytes;
  6. every non-blank line is a header, a refusal, an edit, a notice, or a review line that the declared links call for. The expected review lines are computed without the printer's run logic;
  7. every link the registry declares is paired, replayed over the linked conversion's own fixture.before, so a link the spec lane adds later is covered when it lands. Anti-vacuity: the two links named in the dispatch are asserted present;
  8. a link to a conversion that an earlier step replays pairs across hops.

Measured at c6622657f2:

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 src/commands/migrate/meta.report-order.test.ts: 16 passed (8 existing, 8 new).
  • The unit-tier neighbours (doctor-deprecation-hint-commands, meta.stored-flags, migrate-meta-strict-factories, and this file): 4 files, 41 passed.
  • test/migrate-meta-default-range.test.ts, test/migrate-meta-engine-guidance.test.ts and test/migrate-meta.e2e.test.ts are integration tier, so they are declared to CI. Their only group 2 assertions are toContain checks, which an added line cannot break.
  • pnpm --filter @objectstack/cli typecheck: exit 0. The test file is in the tsc --noEmit program (--listFiles, 1 hit).

Ablation. The fix was committed first (19563f6c4e). node scripts/ablation-replace.mjs replaced const judges = judgesByConversion(result.todos); with an empty map (anchor x1 → x0, blob 868eef7e8983 → 542c67cdfca2). The subject is imported relatively from source, so no build was involved. Result: 5 red (pins 1, 2, 6, 7 and 8) and 11 green. The 11 are the 8 order, count and set pins and the 3 control pins: unjudged edit, no-link stack, and group 3 intact. The restore was proven by blob 868eef7e8983 equal to HEAD and an empty git diff HEAD.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, with no paths, at c6622657f2 (merge base 9b0de7de73), derived 62 commands. Each was run and its exit code recorded before any pipe. All 62 exited 0. --ran: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. On the first round, check:dual-build-cjs-loads and check:i18n-coverage answered PREREQUISITE NOT MET (exit 3, no dist). They were re-run after pnpm turbo run build --filter=!@objectstack/docs, and both are green. At reconciliation time, origin/main was already 3 commits ahead, and one family file (scripts/check-platform-checklist.mjs) had changed there. That family reads docs/qa/platform-checklist/, which this diff does not touch.

Lint, as a proven narrowing. pnpm lint is a repo-wide scan owned by CI. Run at c6622657f2:

  • Population. ESLint's own config was asked through its API (isPathIgnored and calculateConfigForFile against eslint.config.mjs, inline config off). meta.ts is LINTED with 6 rules and meta.report-order.test.ts is LINTED with 5. The changeset is in no config's population.
  • Count. --format json over those 2 files: 2 results, 0 errors, 0 warnings.
  • Invariance. eslint.config.mjs enables no type-aware linting: none of its 7 parserOptions blocks sets project. Its only disk reads at load are two baseline JSON files this diff does not touch. So no untouched file's verdict can move.

Acceptance notes

  • skills/objectstack-upgrade/SKILL.md:139-147 describes Applied N mechanical change(s) as "one line per rewritten site". With this PR, a judged run also carries a review line. The surface is Tier H governed, and a one-line mention can ride with the group reorder that the previous ACCEPT on this card already left for that surface's next queue entry.
  • The printMigrationReport docblock cites "242 notices for protocol 18" as the measurement behind direction 1. Today's chain prints 253. It is a dated measurement, not a claim this PR depends on, so it is left as written.
  • docs/qa/platform-checklist/areas/cli.json item cli.migrate-meta-codemod locates planted spellings in the Applied block by their edit line. The added review lines do not change that.

Generated by Claude Code

claude added 4 commits October 1, 2026 01:26
…pplied edit beside it, marked review

Group 2 of the human report now prints, under each run of applied edits by
one conversion, a review line copying the headline of every semantic entry
that names that conversion in its declared conversionIds. Pairing reads the
declared links only, never the entries' prose. Group 3 keeps every entry,
byte-identical, and its header count is unchanged; --json, the exit codes,
--out and the loader's stderr are untouched.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
Adds the PAIR pins beside the report-order pins: the judging entry prints
once under the run of edits it judges, counting them; an unjudged edit prints
as before; group 3 keeps every entry with the chain's count and bytes; every
link the registry declares is paired over its conversion's own fixture; and a
link to an earlier step's conversion pairs across hops.

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

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

13 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/upgrading.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))

⛔ 6 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/ 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 — 25 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 2f2fa11d756f665a4c06160480c1dce15b9d67a4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2f2fa11d756f665a4c06160480c1dce15b9d67a4

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c6622657f2f0d825b919efae6807645fc534fd28
Local-runs: none

Inputs read: card #20620 (body and all 9 comments — triage's first grade 5888087153, the direction-1 claim 5894939836, os-dev-report 5895725152, seat answer 5895765767, ACCEPT 5895943032, landing 5896210546, release 5916560364, the seat's claim 5922717794, the newest os-dev-report 5923481201); PR #21025 (body, the 3-file list, the one bot comment, and the net diff git diff 9b0de7de73..refs/review/pr-21025: 3 files, +344 / −15, four commits of which one is the merge of origin/main); PR #20691 with its record 5895941542 and #20697 with PR #20716's landing record 5900147099; the check-runs on the head; origin/main at 2f2fa11d75 by git show / git grep only: packages/spec/src/migrations/{chain,types,registry,migrations.test}.ts, the two linked semantic entries, packages/spec/src/conversions/{types,registry,conversions.test}.ts, the three packages/cli/test/migrate-meta*.test.ts, docs/qa/platform-checklist/areas/cli.json item cli.migrate-meta-codemod, skills/objectstack-upgrade/SKILL.md:130-155; and the head's whole meta.ts and meta.report-order.test.ts for the lines the diff does not show. One git read beyond those: a merge-tree probe (below). Nothing was built, run or re-run.

Check-runs on the head, converged at 2026-10-01T02:37:17Z, newest run per name: 34 names, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 0 failed, 0 pending. All seven required contexts are success: Lint & Repo Gates (02:37:00Z), TypeScript Type Check, Test Core (rollup and all six shards), Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Also success: Check Changeset, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch.

Merge state: git merge-tree --write-tree --name-only 2f2fa11d75 c6622657f2, run from a throwaway bare clone sharing the object store with no merge.os-regen.driver registered, answers a tree (bfe43ba5bc) with no conflicted path, exit 0: clean. None of the three files is a merge=os-regen path in any case. origin/main is 3 commits past the merge base 9b0de7de73 (1979df04ad, b84b240b2a, 2f2fa11d75); none touches packages/cli/src/commands/migrate/, packages/spec/src/migrations/ or packages/spec/src/conversions/, so the registry the head's pins enumerate is the one on main.

Binding direction: triage's first grade, direction 2 — print the semantic sibling that judges an applied conversion beside the applied edit, marked review — under direction 1's ban (no notice dropped, never by prose-matching surface), the release 5916560364 (pairs through the declared conversionIds, never through text mentions) and the claim 5922717794 (the human printer only; --json, exit codes, --out, loader stderr unchanged; group ③ keeps every entry with its header count unchanged). Direction 3 is not taken, so the PR closes the card with Fixes #20620, as the release prescribes. The newest Claim: on the card names this branch; the closing-target check-run agrees.

① Derived judgments

Accept-set: no change. File list is packages/cli/src/commands/migrate/meta.ts, the existing meta.report-order.test.ts beside it, and one changeset. packages/spec/** untouched: the claim's read-only clause held, and no link was authored here. No governed surface (Governed Surface Queue Guard success); +344 / −15, under every size line. RIGHT.

(a) The pairing, judgesByConversion and printAppliedEdits.

  • Join key: judgesByConversion reads todo.conversionIds ?? [] for every todo and nothing else — no read of surface, reason, replacement or acceptanceCriteria, no regex, no includes over prose. On main, applyMetaMigrations builds each todo as { ...s, toMajor: step.toMajor } (chain.ts), so the entry's declared field reaches the printer unchanged, and MigrationTodo extends SemanticMigration types it. Declared links only. RIGHT.
  • Every declared link on main pairs: git grep conversionIds: over entries/semantic/ on 2f2fa11d75 finds exactly two — flow-decision-edge-branching-first-match → flow-decision-mode-inclusive-explicit and time-default-zone-refused → time-default-utc-suffix-dropped, both protocol 18, the same two the claim read on dfe5a0863f. The seventh pin enumerates MIGRATIONS_BY_MAJOR[m].semantic[].conversionIds at test time and replays each linked conversion's own fixture.before from the floor (16) to the terminus, asserting the review line under that conversion's run; the spec side's own end-to-end test (migrations.test.ts, the decision-mode pair) and its dangling-id test (every id registered AND replayed at or below the entry's step) hold the data side. Both links pair, and a link authored later is covered the day it lands. RIGHT.
  • An unjudged conversion prints byte-identically: the edit-line template in printAppliedEdits is character-identical to the removed loop ( • path: red(from) → green(to) dim((conversionId))), the bold Applied N mechanical change(s): header and the trailing blank line are kept, and judges.get(id) ?? [] prints nothing under a conversion no entry names. The guard if (result.applied.length > 0) is unchanged. Pin 3 asserts the no-review-line case over the dashboard conversion and compares every • line against appliedLines(result), in order. RIGHT.
  • One review line per contiguous run: chain.ts replays, per step, for (const conversionId of step.conversionIds) one conversion.apply(current, cb) at a time, and the callback pushes every edit of that call before the next conversion starts, so a conversion's edits are contiguous within its hop. Across hops: conversions.test.ts:535 pins conversion ids unique, migrations.test.ts pins that a step's conversion ids all have toMajor === major (a graduated conversion belongs to its own major's step only), and step 18's list is derived from CONVERSIONS_BY_MAJOR[18], so no id is replayed in two hops. "Contiguous by construction" is true on main. And the printer does not depend on it: a split run would get its own review line, so the degenerate case loses nothing. The run counter resets on a conversion boundary, and the subject reads "the edit above" for one, "the N edits above" otherwise. RIGHT.
  • Cross-hop: the join runs over the flat result.todos × result.applied, which hold only the hops the run crossed (composeMigrationChain filters m > fromMajor && m <= toMajor). So a review line can never name an entry of an uncrossed hop (it is not in todos), and cannot miss an entry it did cross whose conversion produced an edit in this run (both halves are in the flat arrays whatever hop made them). Pin 8 proves the earlier-hop direction on a real --from 16 result by injecting an 18-entry → 17-conversion link (action-execute-to-target, toMajor: 17) and asserts edit.toMajor below judge.toMajor. Semantic observation, not a defect of this PR: a cross-hop link pairs only when both hops are crossed in one run — on a --from 17 run the 17 conversion is not replayed, so the entry prints in ③ with no review line, correctly; on a --from 16 --to 17 run the entry is not in the report at all. That is the data model (the entry belongs to its hop), and no cross-hop link is authored today. RIGHT.

(b) "⛔ No notice is dropped." The ③ loop (for (const t of result.todos) under the yellow N manual change(s) require your judgment: header) is outside every diff hunk and still iterates every todo with the same three lines. Pin 5 checks the header equals result.todos.length, the slice under it equals noticeLines(result) byte for byte, the entry count equals the todo count, and the judging entry is among them; pin 6 sorts every non-blank printed line against headers + refusals + edits + review lines + notices, so no line is added, dropped, merged or reworded beyond the declared review lines; the four pre-existing SET pins stay in force. The dev's real-stack reading (253 = 253, both stacks, before and after) is consistent with the 253 step-18 entries the generated registry holds at both 2f2fa11d75 and the head. Copy, not move: the claim 5922717794 fixes the reading — ③ keeps every entry and its header count — so a move would breach the claim and detach the count from todos; direction 2 says "print the sibling beside the applied edit", which a copy does. The copy is the headline [protocol N] surface → replacement — the replacement is the judgment itself ("nothing, where the out-edge conditions partition …") — and the why / verify lines sit in ③ under the identical headline text. Repeating a paragraph-long reason 13 times under 13 edits would be the noise the card is about. RIGHT.

(c) Unchanged surfaces, in source at the head. --json: the if (flags.json) { await emitJson({...}); if (flags.out) writeFileSync(...); return; } block (head lines 644–678) is outside every hunk and returns before the human branch. --out: written at the same two places (line 676 for --json, lines 447–450 after ③ in the printer) with the same resolve(flags.out). Exit codes: no this.exit site is in a hunk; the human branch still exits 0 on a schema-invalid run. --stored: runStored (lines 752–918) prints formatStoredMigrationReport(report).join('\n') from @objectstack/metadata-protocol and never calls printMigrationReport. Loader stderr: packages/cli/src/utils/config.ts is not in the file list. Tests on main that quote group ②: test/migrate-meta-default-range.test.ts:175 toContain('Applied 5 mechanical change(s)') and :176 toContain(RENAME_CONVERSION); test/migrate-meta-engine-guidance.test.ts:413 stdout.includes(block) where block is the ③ triple (⚠ [protocol N] surface → replacement, why:, verify:, lines 345–347); test/migrate-meta.e2e.test.ts reads --json, the Chain: line and the data-migration advice by toMatch. The checklist item cli.migrate-meta-codemod locates each planted spelling by its edit line (path: from → to (conversionId)) inside the Applied block. None asserts ② line-by-line or by line count; an added line under a run breaks none of them. RIGHT.

(d) The pins. Eight PAIR pins beside the eight existing ones, in the printer's one test file (the dev's deviation 3 — right, every pin on this printer stays in one file and reuses run, appliedLines, noticeLines, refusalLines). Pairing: 1 (judge once under the run, counting 2, inside ② between the two headers), 2 (a run of one says "the edit above"), 7 (every declared link over its fixture), 8 (cross-hop). Controls: 3 (unjudged edit unchanged, every • line equals the chain's, header count equals applied.length), 4 (a stack whose applied conversions carry no link prints zero review lines — FINDINGS_STACK, with the anti-vacuity that the chain does carry links). ③ invariance: 5 and 6. The expected review line is written from the todo's data (reviewLine), not from the printer. Ablation reading: with judges an empty map the printer prints no review line, so pins 1, 2, 6, 7, 8 go red and pins 3, 4, 5 plus the eight order/count/set pins stay green — exactly the dev's 5 red / 11 green, and the blob 868eef7e8983 the ablation names is the head's meta.ts blob (the diff's index line). The pins would red without the join. RIGHT. Two nits, non-blocking: reviewLine and reviewsUnder assume a one-line replacement (true of both links today), while noticeLines splits multi-line prose — a future link whose replacement carries a newline would red pins 1, 2, 6 and 7 for a test-side reason; and the pre-existing SET pin over FINDINGS_STACK would red the day the spec lane links dashboard-refresh-interval-to-refresh-interval-seconds or action-aria-removed, explained by pin 4 but a fixture coupling.

(e) The changeset .changeset/20620-migrate-meta-review-pairing.md, '@objectstack/cli': patch, Clause-②: no on its own line, the same form as 20620-migrate-meta-verdict-first.md. Every claim checks against the diff: the review line's shape and placement, "copies the headline the manual change prints in its own group", "two pairs are declared today" (the two on main), "only declared links pair", ③ byte for byte with the same count, an unjudged edit unchanged, --json / --out / exit code / loader stderr unchanged, --stored not affected. It claims nothing undelivered; the quoted line shows the plural subject only, and the singular "the edit above" is a detail the body need not carry. Check Changeset: success. RIGHT.

Wrong: none found.

② Semver level

patch is right: the diff adds lines to the human text face of one command and changes no machine face, no exit code, no authorable key, no export reachable through @objectstack/cli's exports map (judgesByConversion and printAppliedEdits are module-private; printMigrationReport was already exported for the in-process pins and is not re-exported from src/index.ts, per the direction-1 record). A functional improvement in a released package takes a changeset, never skip-changeset. Clause-②: no is right: nothing an author can write is added, removed or renamed; no arm, since no (widening) is malformed. The spec-side widening was #20716's minor with Clause-②: yes (widening), already landed.

③ Boundary flags

Deviations (5), all declared, all ANSWERED:

  1. Merge of origin/main into the branch (c6622657f2) after a STALE TREE flag; every reading was repeated on that head, the head's check-runs are green, and the merge-tree against today's main is clean. Process, no contract effect. The merge commit itself carries no trailer pair (the three non-merge commits carry the model-free Claude-Session: / Co-authored-by: Claude pair); Lint & Repo Gates, which holds check:commit-card-trailers, is green, and a squash landing discards the merge commit.
  2. pnpm lint not run, a proven narrowing reported instead (population from eslint.config.mjs through the ESLint API, 2 files linted, 0 errors, 0 warnings, no type-aware linting so no untouched file's verdict can move). The full scan is CI's, and Lint & Repo Gates on the head is success.
  3. Pins extend the existing file: right, judged in ①(d).
  4. Scratch-file and PID-file hygiene; 5. the first build wrapped as a ;-joined command: process only.

open_questions: none.

out_of_scope_findings:

  • skills/objectstack-upgrade/SKILL.md:139-140 reads "Applied N mechanical change(s) — one line per rewritten site": confirmed on 2f2fa11d75; a judged run now also carries a review line. Carrier given: the next Tier H queue entry for that surface, where the prior ACCEPT 5895943032 already parked the group-order reorder. RIGHT — skills/** is Tier H, it is untouched here, and one line rides with the reorder.
  • The printMigrationReport docblock (meta.ts:378) cites "242 notices for protocol 18": a dated reading from fix(cli): os migrate meta leads with the verdict and the refusals, then the applied edits, then the semantic notices #20691's base; the generated registry at 2f2fa11d75 and at the head holds 253 step-18 semantic entries (77 for 17), matching the dev's 253. Carrier given: 承接者:无. Arguable: the number sits in the very docblock this PR edits (the hunk at lines 387–392 is nine lines below it), so the natural carrier is this file's next touch — or this PR, if the seat wants the one-token correction now; nothing depends on it. Not blocking.

Semantic notice, mine, for the lane: a cross-hop link (allowed by the spec test) pairs only on a run that crosses both hops; on a one-hop run the entry prints in ③ with no review line. By the data model, not this PR; no such link exists today.

Implemented-by: claude/issue-20620-migrate-meta-pairing
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 02:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 02:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 6073bb9 Oct 1, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20620-migrate-meta-pairing branch October 1, 2026 04:12
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