feat(cli)!: rename the --json payload key specVersionGap to protocolVersionGap - #17240
Conversation
…ersionGap The advisory's axis moved to `manifest.engines.protocol` in #13860; the published key name lagged one release behind it. A key spelled `specVersion*` invites the inference that a writable `manifest.specVersion` exists, and because `ManifestSchema` is not `.strict()` and drops unknown keys with nothing said (#14192), acting on that inference yields a manifest that looks normal and whose line never took effect. One stroke, no alias, no dual-key window. Value shape unchanged. The three in-repo e2e suites that pinned the old key move with it; zero external consumers were measured. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
…y rename `check-adr-0087-registration` requires a declared-breaking changeset to state its ledger disposition in writing. The renamed member is a CLI `--json` output key emitted from an inline object literal: no Zod schema, no `packages/spec` declaration, no stored representation, so `objectstack migrate meta` has nothing to reach. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 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 c07714e5486635f908918c9d334cd5a4abd17475 && git checkout c07714e5486635f908918c9d334cd5a4abd17475
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 559e531a94dab4938d1ab54fd6631acbc5303d61 141f5faf66d5de0fe1b73fbd62b373189b9a6fa8 && git checkout -B drift-repro 559e531a94dab4938d1ab54fd6631acbc5303d61 && git merge --no-ff 141f5faf66d5de0fe1b73fbd62b373189b9a6fa8
node scripts/docs-audit/affected-docs.mjs --json 559e531a94dab4938d1ab54fd6631acbc5303d61
|
Fixes #14261
Clause-②: no
Renames the published
--jsonpayload keyspecVersionGaptoprotocolVersionGapin onestroke — no alias, no dual-key window, value shape unchanged.
Why this is a correctness change, not a tidy-up
Because
ManifestSchemais not.strict()and silently drops unknown keys (#14192), areader who infers a writable
manifest.specVersionfrom the key's old name does not get anerror — they get 「一份看起来正常、而那一行从未生效的 manifest」. The output-side breadcrumb
is the same ghost-key mechanism that caused the parent card #13860; this removes it.
Which option was ruled, and by whom (history, not a question)
Maintainer, 2026-09-02, comment
5507416523, verbatim:Option B, now, graded
minor. Option C (dual keys for a transition window) stays refused,consistent with the 2026-08-27 ruling 「项目在创业阶段…短期不考虑渐进」.
The occurrence set, re-derived rather than inherited
specVersionGaponorigin/mainatbccf311100, measured with a control (protocolGap,which fires 5/4/5 in
compile.ts/doctor.ts/validate.ts):packages/cli/src/commands/compile.ts— theos build --jsonemitpackages/cli/src/commands/validate.ts— theos validate --jsonemit, plus its--strictscope commentpackages/cli/test/build-json-advisory-parity.e2e.test.tspackages/cli/test/build-json-undeclared-key-parity.e2e.test.tspackages/cli/test/validate-json-warning-parity.e2e.test.ts— header prose, payload type member, assertionscripts/pm/check-half-states.mjs:22779packages/cli/CHANGELOG.md(3 rows)os doctoris NOT a third emitter, measured. The parent card touched three commands, sothis was checked rather than assumed:
doctor.tscallscheckProtocolVersionGap()and printsa human-readable line; it has no
--jsonface at all. Two emitters, not three.There is no pending changeset naming the old key — the one the 2026-09-02 ruling asked to move
has since been released into
packages/cli/CHANGELOG.md, which is history and stays as written.All three suites carry
*.e2e.test.*, whichtest-nightly-tiers.ymlmoved to the nightlytier (#16455).
ci.yml's Test Core and the merge queue run underOS_TEST_TIERS=queue, whichexcludes them. So a reviewer reading a green merge queue has not seen these pins pass. They
were driven by hand here, under
OS_TEST_TIERS=nightly:Test Files 3 passed (3)·Tests 17 passed (17)· lock VERDICTcommand-exit 0.Ablation — prediction recorded BEFORE any leg ran
For a rename the interesting leg is the one proving the pins bind the new name. Predicted,
in writing, before the first mutation: restoring
specVersionGapat the two emit sites reddensall three suites by three distinct mechanisms — exact key-set equality in the two build-parity
suites, and
expect(payload.protocolVersionGap).toBeNull()readingundefinedin the third(
toBeNull()rejectsundefined, which is what makes that pin non-vacuous).Observed: exactly that, all three mechanisms.
Test Files 3 failed (3)·Tests 4 failed | 13 passed:On-disk proof of the mutation, and of the restore, under
trap ... EXIT INT TERMwith absolutepaths:
compile.tsvalidate.tsbbb743b63ef26f27b74c09976993caf838424dc1057339febb0da7c707ae442ec0acf64acd54524db4fca212193e5e0fa8cfe895d33bc3b0643f6ce2125c64e99ba4b9074e7cd92b9e511ec69a53fcfebbb743b6…— equal to HEAD057339fe…— equal to HEADAnchor counts moved 1→0 (new name) and 0→1 (old name) per file and back. Restore was
git checkout HEAD -- ABSPATH, never bare;git diff HEADempty,git status --porcelainempty.No
distleg: these suites spawnbin/run-dev.jsunder tsx, which readssrc/.Verification
Every exit code captured by redirect-then-
$?, never through a pipe.pnpm --filter '@objectstack/cli^...' build --concurrency=2command-exit 0pnpm --filter @objectstack/cli buildcommand-exit 0pnpm --filter @objectstack/cli typecheckcommand-exit 0— andcheck:test-typecheckcompiles the test layer, with all three edited e2e files proven in the program bytsc -p tsconfig.test.json --listFilesvitest run --project unitpnpm lint(eslint . --no-inline-config)141f5faf66dispatch-gates.mjs --commands --repo objectstack-ai/objectstack--ran: 60 run, 0 NOT-MEASURED, 0 UNRUNTwo of the 60 returned exit 3 = PREREQUISITE NOT MET, which is ⛔ not a pass and ⛔ not a
finding —
check:dual-build-cjs-loadsandcheck:i18n-coverageboth need a full-repopnpm build(they name absent
dist/in studio, client-react, the connectors and several plugins — none ofthem a surface this diff touches). Declared as NOT MEASURED locally and left to CI, which builds
the repo fresh.
check-adr-0087-registrationinitially reported a real problem against this diff — adeclared-breaking changeset with no ledger disposition — and is now green with
not-required (no-migration-prescription): the renamed member is an output key on an inlineobject literal, with no Zod schema, no
packages/specdeclaration and no stored representation,so
objectstack migrate metahas nothing to reach.Docs-drift round, on the final head
check-affected-docsandcheck-drift-commentboth exit 0. Because a published--jsonkey isthe kind of thing a page states by its output, this was also swept by hand rather than left to
the emitter-only tool: no page under
content/docs/ordocs/prints or enumerates eitherpayload's key set, and the prose-class sweep over pages naming
os validate --json/os build --jsonfound only flag lists.docs/is outside the tool's walk, so it was greppeddirectly. No release page is wrong, so there is nothing to hand back as a docs-only PR.
Single-writer
Measured from the open PR list (26 open PRs, 359 (PR, file) rows), each PR against its own
merge base via
pulls/{n}/files, never remote branches. None of the five faces is held by anyopen PR. Control fired:
packages/cli/test/i18n-walk-output-parity.test.tsmatched on PR #17223,so the exact-path matcher was live.
5607961637—packages/cli/src/commands/generate.tsheld by#17230 — did not fire on re-measure. Reason established rather than assumed: #17230 merged at
2026-09-09T20:45:29Z, during this run. Reported as observed; a replacement control was used.
验收备注
packages/cli/CHANGELOG.mdcarries three historical rows namingspecVersionGap. Left exactlyas written — they record what shipped under that name and rewriting release history would make
them false. Not a finding.
scripts/pm/check-half-states.mjs:22779uses this card's own title as row H54's anchor fixture.Deliberately untouched: it is a fixture about a title string, not a consumer of the key.
because renaming it is "a break nobody asked for". They are rewritten, not deleted — a stale
comment asserting the opposite of the code is worse than none.
Generated by Claude Code