fix(cli): os migrate meta names the protocol, not a package version - #16058
fix(cli): os migrate meta names the protocol, not a package version#16058os-litant wants to merge 1 commit into
os migrate meta names the protocol, not a package version#16058Conversation
The chain line ended `(runtime 17.0.0)`. That value is `PROTOCOL_VERSION` -- the protocol major padded to a semver -- and it never tracks the installed package version. Printed as a bare semver under the word "runtime", beside the real package versions of the same upgrade session, it read as "your runtime is 17.0.0": an apparent downgrade on a 17.3.0 install. The value was never wrong; the label and the semver form were. The line now states the fact in the protocol's own units -- `(this runtime implements protocol 17)` -- relabelled rather than dropped, because with `--to` stopping below this build's major it is the only place the operator learns where the runtime actually stands. The `--json` `runtime` key is deliberately left as published: a machine-readable key on a shipped payload owes a reader census and a deprecation window before it moves. A new e2e pin drives the real CLI over both halves and asserts that key's current value, so the contract move cannot happen silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 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 bab908eaa4d010d83d169ba87defa5296237ede0 && git checkout bab908eaa4d010d83d169ba87defa5296237ede0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53cbad9f75572600ef43bb2a18071633fd6c0f68 e702dfad69a846be9774dcafa192cdbe6bef2ed4 && git checkout -B drift-repro 53cbad9f75572600ef43bb2a18071633fd6c0f68 && git merge --no-ff e702dfad69a846be9774dcafa192cdbe6bef2ed4
node scripts/docs-audit/affected-docs.mjs --json 53cbad9f75572600ef43bb2a18071633fd6c0f68
|
Seat ruling — clause ② is NO on both limbs, and one correction about how that grade was reachedRecording this so a later reader can tell a contract review that was not owed from one that was skipped, and so the reasoning is auditable. Mechanical floor: NOThe Conformance limb: NO, on the narrow reading, and here is why the narrow reading is the right oneThe changed segment interpolates build constants only, so its bytes are identical for every input the command can be given. The input-dependent prefix of the same line — The wider reading — that the exact output bytes are themselves the verdict — grades every diagnostic-wording change in the tree as a contract move. A limb that answers YES to everything does not discriminate, and a rule that cannot fail to fire tells a reviewer nothing. That is the same principle this seat applies to controls, and it settles the reading here. One support the dev's own argument did not name: this repository already governs console-output drift through a different mechanism — the CLI transcript and examples gates ("CLI transcripts in
The correction: do not optimise a diff to keep a limb NOFrom the dev's own declaration, on what would have made the limb YES:
⛔ That is the wrong direction of optimisation, and naming it is partly this seat's job because the dispatch is what made clause ② feel like a cost. Clause ② is a review trigger, not a prohibition. Grading YES buys one review round; it does not block, penalise, or reflect badly on a diff. Trading away the better output to avoid that round optimises the wrong quantity, and if that instinct generalises it will quietly bias every dispatch on this lane toward the shape that dodges review rather than the shape that is right. The unconditional form is kept — but on its merits, not on that reasoning. It is defensible on the measurement the dev itself took: driving The rest, verifiedBoth halves of the defect are repaired: the word
Generated by Claude Code |
Part of #15585 — this lands option A only. The card's
--jsonhalf (option C) is deliberately left open; the section "What is NOT in this PR" says exactly what remains and why, so the card must stay open after this merges.The defect, driven
PROTOCOL_VERSIONis the protocol major padded to a semver ('17.0.0',packages/spec/src/kernel/protocol-version.ts:18). It is not, and never tracks, the installed package version. It was printed under the wordruntime, as a bare semver, beside the real package versions of the same upgrade session.This repo is itself at
@objectstack/spec17.3.0 /@objectstack/cli17.3.0, which is the reporter's exact scenario. Driving the real CLI (bin/run-dev.js,NO_COLOR=1) over a minimal stack config:Before
After
Why relabelled and not dropped
The card offers dropping the parenthetical, since the line already says
protocol 17 → 17. Driving the other case shows the parenthetical carries a fact nothing else on screen does — when--tostops below this build's major it is the only place the operator is told where the runtime actually stands:So the value stays; the label and the semver form are what changed. Both halves mattered: a line that merely stopped saying "runtime" would still print a padded semver in a version position.
The sweep — every other site that prints
PROTOCOL_VERSIONA one-site fix that leaves siblings lying would be worse than reporting the set, so the whole set was enumerated before touching one (
grepover the tree, excludingnode_modulesanddist). Every other site already labels the value correctly:packages/cli/src/commands/migrate/meta.ts:353(runtime ${PROTOCOL_VERSION})packages/cli/src/commands/migrate/meta.ts:330runtime: PROTOCOL_VERSION(--json)packages/cli/src/commands/migrate/meta.ts:201Target protocol major (defaults to this runtime's, ${PROTOCOL_MAJOR})packages/metadata-core/src/protocol-handshake.ts:225,285…but this runtime is protocol ${runtimeVersion}protocol. The machine field is namedruntimeVersion, which is reported as a finding rather than moved: it is a different shipped surface (theOS_PROTOCOL_INCOMPATIBLEdiagnostic) and its own contract movepackages/metadata-protocol/src/protocol.ts:15610protocol: PROTOCOL_VERSIONpackages/spec/scripts/build-spec-changes.ts:108protocolVersion: PROTOCOL_VERSIONpackages/spec/scripts/build-upgrade-guide.ts:40Current protocol: **17.0.0**What is NOT in this PR
The
--jsonruntimekey is untouched, on purpose. It is a machine-readable key on a published payload; renaming it is a contract change owing a reader census and a deprecation window, and the card's triage ruled A and C independent with an explicit instruction not to bundle them. The payload is byte-identical across this change — same key set, same values (durationexcluded, being a timer):A reader census for that key found no consumer in this tree: nothing reads
.runtimeoff this payload, and the publishedskills/objectstack-upgrade/SKILL.mddocuments--jsonwithout naming the field. That is a measurement offered to whoever decides C, not a decision.To keep the deferral honest rather than silent, a comment now sits on the emit site, and one of the new e2e cases pins the key's current value — so a future rename fails a test instead of shipping quietly.
Option B (printing the real package version too) stays out: it depends on
@objectstack/cli/package.jsonbeing resolvable, which is another card's work. Out of scope: #15325.Tests
packages/cli/test/migrate-meta.e2e.test.ts— four new cases in a new describe, driving the real CLI process, not a string in a fixture:PROTOCOL_VERSIONabsent, and noruntime N.N.Nanywhere);--tostops below this build's major;--jsonruntimekey is exactly as published.Every case derives its expectations from
PROTOCOL_MAJOR/PROTOCOL_VERSIONrather than hard-coding17, so they follow the next major bump.Suite:
pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/migrate-meta.e2e.test.ts→Test Files 1 passed (1) · Tests 18 passed (18).Ablation. The CLI is driven from
src/(bin/run-dev.jsimports../src/…), so no rebuild sits in this loop — stated because an ablation over adist/-resolved subject that skipped the rebuild would stay green and prove nothing. With the fix committed, the defective line was put back on disk and the mutation confirmed there before measuring (injected-line count 1, removed-line count 0, blob hash moved058a5bb7…→43064a9a…), not by trusting the editor's exit code:The one that stayed green is case 4 — the
--jsonpin, which the mutation did not touch. That is the discrimination working in both directions rather than a blanket red. Restore was proven by observed state, not by an exit code:git diff HEADempty, worktree blob back to058a5bb7…, fix line present, defect line absent.Gates
Union derived mechanically at the delivered commit —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— and asserted against its own reconciliation line: 56 families claimed, 56 command lines harvested, 56 green. Exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?).Four came back NOT MEASURED on the first pass for want of built output —
check:dual-build-cjs-loadsandcheck:i18n/check:i18n-coverage(exit 3, PREREQUISITE NOT MET) andcheck:i18n-walk-parity(its own verdict line reads PREREQUISITE NOT MET). All four were then built for and re-run green; none was reported as a pass while unmeasured.The Artifact rosters block (37 families, outside that total by the tool's design) was run separately: 35 green, 2 not measured —
node scripts/check-partof-closing-keyword.mjsandnode scripts/check-single-claim-paths.mjsreturned exit 2, NOT WIRED (noPR_BODY/PR_NUMBERoutside CI). Their pnpm spellings are green above but grade only the checkers' own fixtures — measured, not assumed: bothpackage.jsonentries resolve to--self-testalone. The body-judging form of the first was then run locally against this exact body withPR_BODYset, and passes.pnpm --filter @objectstack/spec run check:react-declaration-parityexits 1 withMANIFEST is not set … This gate did NOT run. It needs an objectui-derivedsdui.manifest.jsonand a browser, is an on-demand gate rather than a CI job, and this diff does not move the.objectui-shapin.Clause ② — declared per limb, from the delivered diff
Public-surface limb: NO. The diff adds no key to any published payload (the
--jsonpayload is byte-identical, shown above), touches nothing underpackages/spec/src/**, adds and changes no export, and leaves the command's flags, args and exit codes exactly as they were; the only shipped bytes that move are one human-readable diagnostic line on stdout, which no consumer in the tree parses.Conformance limb: NO. No input class is re-selected between two already-published verdicts: the same configs load, the schema verdict is computed by untouched code, the support-floor refusal and every exit code are unchanged, and the whole change is the rendering of one informational line after the verdict has already been reached.
Out of scope, filed rather than acted on
os migrate meta"also printsruntime 17.0.0on a 17.3.0 install" — a sentence the CLI repair invalidates, in a file a code PR may not touch #16056 —content/docs/releases/v17.mdx:5363asserts this command "also printsruntime 17.0.0on a 17.3.0 install", a sentence this repair invalidates.content/docs/releases/may not be edited by a code PR (AGENTS.md, Documentation Guardrails); a code PR's input to release notes is its changeset, and this PR's changeset carries the corrected line. Whether that sentence is stale guidance or a historical measurement is a judgement for the release process, which is why it is a card and not an edit.runtimeVersionon theOS_PROTOCOL_INCOMPATIBLEdiagnostic (packages/metadata-core/src/protocol-handshake.ts) carries the same padded protocol semver under a version-ish machine name. Its prose saysprotocol, so the human channel is unambiguous; the machine field is a separate shipped surface and a separate contract move. Reported here, not touched.Changeset
.changeset/migrate-meta-chain-line-protocol-label.md—@objectstack/cli: patch.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code