Repository navigation
fix(cli): os migrate meta leads with the verdict and the refusals, then the applied edits, then the semantic notices - #20691
Conversation
…en the applied edits, then the semantic notices The authored-source report printed the whole semantic catalogue first and the schema verdict last, and never listed the refusals that keep the migrated stack from parsing. The human report is now one function that prints three groups, each under one header line counting it: the verdict with every refusal left after the chain, the applied mechanical edits, then every semantic notice. No notice is dropped or reworded, and the --json payload is unchanged. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…ess, and add the changeset Order pins: the verdict and its refusals are the first lines, then the applied-edit header, then the manual-change header, and nothing but the refusals sits between the verdict and the applied edits. Completeness pins: every applied edit and every semantic notice of a real two-hop chain run prints exactly once, byte-identical and in chain order; every refusal of the migrated stack is listed; nothing else is printed. Neither kind reads the other's positions, so a reorder alone cannot redden the completeness pins. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 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 853e69de48a50704c31b4ac616e3a2234ecc5761 && git checkout 853e69de48a50704c31b4ac616e3a2234ecc5761
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 14f80e23957165f6fb23c2b3d59bdc7668652dfb 8304fb9e1307a9348065e2e761dcceaf9de55300 && git checkout -B drift-repro 14f80e23957165f6fb23c2b3d59bdc7668652dfb && git merge --no-ff 8304fb9e1307a9348065e2e761dcceaf9de55300
node scripts/docs-audit/affected-docs.mjs --json 14f80e23957165f6fb23c2b3d59bdc7668652dfb
|
Contract reviewServed-tier: Inputs read, via Check-runs on the head at read, 2026-09-29T18:06:45Z, newest run per name: 33 names. 28 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 2 in progress (Lint & Repo Gates; Test Core 3/6). 0 failed. Workflow runs on the head: 11 (8 success, 1 skipped, CI queued with shards still running, Lint & Type Check in progress). The two running checks are recorded as running; this record infers nothing for them, and the adopting seat reads them at adoption. Binding direction: triage's first grade, direction 1 only (lead with the verdict and the refusals, then the applied mechanical edits, then the semantic notices, one header line counting each group; no notice dropped, ADR-0087 D3). Direction 2 is split to #20697 and direction 3 is out, per ① Derived judgmentsAccept-set: no change. The file list is Public-surface changes the diff implies:
(1) Byte-identity and order within group. The diff lifts the applied loop, the (2) The refusal group. The refusals are Header count. The invalid verdict carries the count ( (3) (4) The two reworded sentences. Old A ended "resolve the manual changes above, then run (5) Ablation. The test file is 3 ORDER pins, 1 header-count pin, 4 SET pins, 8 in all. With the verdict moved back to last, ORDER 1 fails at (6) Wrong: none found. ② Semver level
Clause-②: no — right. Nothing an author can write is added or removed, so ③ Boundary flags
Deviations (5), all declared, all ANSWERED:
Declared narrowing, verification run unlocked (no PR-context gates NOT MEASURED locally (single-claim paths, part-of closing keyword, closing-target claim): ANSWERED by the head's check-runs, all success: "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".
Semantic notice, mine: with stderr and stdout on one terminal an upgrader now sees two refusal lists, the loader's as-authored list before the chain and the report's post-chain list, with different counts. The report's "left after the chain" and the loader's Implemented-by: VERDICT: PASS |
…lied edit beside that edit, marked review (objectstack-ai#21025) Fixes objectstack-ai#20620 Clause-②: no Direction 2 of triage's first grade on objectstack-ai#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 objectstack-ai#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`: ```text 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](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20620
Clause-②: no
Direction 1 of the card's triage (order). Direction 2 (pairing an applied conversion with the semantic entry that judges it) needs a structured link on
SemanticMigrationinpackages/spec, so it stays on the card. Direction 3 (write-back) is out, per triage.What changed
os migrate meta --from N(the authored-source report, human output only) now prints three groups, in the order an upgrader acts on them. Each group opens with one header line that counts it:✓ Migrated stack is schema-valid (Nms), or⚠ Migrated stack does not yet pass schema validation — N refusals left after the chain. Resolve them, then runos validate:followed by one✗ path: messageline per issue of the migrated stack's parse. The lines are rendered byformatZodIssue, the same renderer behind the loader'sdefineX() validation failedblock.Applied N mechanical change(s):, unchanged.N manual change(s) require your judgment:, unchanged.The human branch of
run()is now one exported function,printMigrationReport, so the order can be pinned in process.--stepcheckpoints stay between groups 2 and 3, and the--outline and the data-migration advice stay after group 3. A range that holds no step also leads with the verdict, which now lists the source's refusals, and the range note follows it.Unchanged:
--json: same keys in the same order, same values, same array order.--json'sappliedandtodosarrays are in chain order, and a consumer could rely on that order. It is left as it is.Measurements
Each "before" run used the
bin/run.jsof a worktree at6981abfd26(this branch's base). Each "after" run used the build of7c484d891f. Both runs used the same@objectstack/spec(6981abfd26). The merge ofmainsince then does not touchpackages/cli. Stacks:examples/app-crmat6981abfd26;defineStackconfig: two currency fields carryingscale(refused, no conversion), twodashboard.refreshInterval, twoaction.aria, and a two-branch flow decision with nomode;refreshInterval, run--from 16so the data-migration advice prints.stdout line where each group starts:
--from 17(5 applied, 242 notices, 2 refusals left)--from 17(0 applied, 242 notices, valid)--from 16(1 applied, 319 notices, valid)[authored-source] defineStack()warning on stderr, printed during load, before any chain output. That list describes the stack as authored: 6 issues on h1, 4 of which the chain goes on to convert. The 2 refusals that remain after the chain were printed nowhere.--stepand--out, with the snapshot path normalised. The only lines added are the refusal list: 2 on h1, 0 on the valid stacks. That is the content direction 1 asks for, and the printer had none of it before. The only lines reworded are the two schema-invalid verdict sentences: the old one pointed at "the manual changes above", which are now below.--jsonis identical oncedurationis removed, in the same key order (h1, h1 empty range, crm, dm). The--outsnapshot is byte-identical. stderr is byte-identical in every run.scripts/ablation-replace.mjs(anchor 1 → 0, blob1a002a9b→d80c2332) under a trap-guarded restore. Result: exactly the 3 ORDER pins red, e.g.the verdict is the first line the report prints: expected 1030 to be +0. The header-count pin and the 4 SET pins stayed green. Restore proven: blob1a002a9bequals theHEADblob,git diff HEADis empty, and the marker count is 0. The subject is./meta.js, resolved tosrcby vitest (a relative import), so nodistwas involved.Verification
At
8304fb9e13(final head:mainmerged in, thenpnpm install --frozen-lockfileand the CLI closure rebuilt):pnpm --filter @objectstack/cli typecheck: exit 0. The new test is in thetsc --noEmitprogram (--listFilescount 1).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 233 files passed, 2 failed (5 tests). The 2 aretest/published-subpath-console.pin.test.tsandtest/published-subpath-hook-body.pin.test.ts. They fail the same way on a clean6981abfd26worktree: a macOS/varvs/private/varrealpath difference. See the acceptance notes.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ranover the 62 derived commands, each with its exit code: 62 run, all exit 0, 0 NOT-MEASURED.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answeredPREREQUISITE NOT MET(exit 3). They were re-run after a fullturbo run buildand exited 0.check-changeset-fixed,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity(all marked ⛔);release-pending-publish --self-test,check:cli-examples-parityandcheck-platform-checklist-watchdog;check-published-list-mirrorsandcheck:commit-card-trailers.check-single-claim-paths,check-partof-closing-keywordandcheck-closing-target-claim.pnpm lint(the fulleslint .): exit 0. Both touched files are linted, with 0 errors and 0 warnings (--format json).node scripts/check-issue-citations.mjs --base origin/main: exit 0, 3 citations resolve.At
7c484d891f(before the merge, samepackages/cli):test/migrate-meta-default-range.test.tsandtest/migrate-meta-engine-guidance.test.ts(--project integration): 10 passed, 1 skipped (a registry-selected skip, by design).test/migrate-meta.e2e.test.ts(nightly tier): 19 passed.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Acceptance notes
docs/qa/platform-checklist/areas/cli.json, itemcli.migrate-meta-codemod(revision 3), quotes the old invalid-verdict sentence verbatim ("resolve the manual changes above"). Its acceptance clause (the verdict agrees withschemaValid) still holds. The quoted step is now stale. It is not edited here: it needs a revision bump and a history entry by the checklist's author.skills/objectstack-upgrade/SKILL.mdlists the printed groups as applied, manual, verdict. It claims no order, and every string it quotes is still printed. It is a governed surface, not edited here.pnpm check:platform-checklistis red onmain. The failure isareas/identity-auth.json: ABSENT SYMBOL … auth-plugin.ts#twoFactor, red on6981abfd26as well. The gate is maintainer-run and deliberately out of per-PR CI.published-subpath-console.pin.test.tsandpublished-subpath-hook-body.pin.test.tscompare a/var/folders/…path with its/private/var/folders/…realpath. They are red on a clean base worktree too, so the failure comes from the host, not from this diff.os migrate metacannot open an object built withObjectSchema.createthat carries a retired key. The load-time tolerance wraps only thedefine*exports, andObjectSchema.create(is used 35 times acrossexamples/, as well as by HotCRM.ObjectSchema.create({ …, tenancy: { enabled: true, organizationField: 'organization_id' } })insidedefineStack.os migrate meta --from 17exits 1 at load with the raw ZodError array. The array's own message prescribes "Runos migrate meta --from 17…".object-tenancy-organization-field-removedapplies, andschemaValidis true.test/migrate-meta-default-range.test.tsrecords.Generated by Claude Code