Repository navigation
fix(metadata-core): an exclusive protocol comparator ('>18', '<18') names the major it admits in the refusal - #26595
Conversation
…it admits in the refusal '>18' on an 18 runtime targets 19 and takes the upgrade remedy; '<18' targets 17 and prescribes migrate meta --from 17. The admit verdict and the target major now read one span grammar. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5p97A4ysuQjSwrSaWT
…rget Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5p97A4ysuQjSwrSaWT
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5p97A4ysuQjSwrSaWT
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 6aac32bbc95c9d14852c85a08aa0eef949d8652b && git checkout 6aac32bbc95c9d14852c85a08aa0eef949d8652b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 898703f90338648e53808ccc8ad6b5120009e7a6 5046652093e15988f0a5d9f8778a2897a7d079a9 && git checkout -B drift-repro 898703f90338648e53808ccc8ad6b5120009e7a6 && git merge --no-ff 5046652093e15988f0a5d9f8778a2897a7d079a9
node scripts/docs-audit/affected-docs.mjs --json 898703f90338648e53808ccc8ad6b5120009e7a6
|
|
Seat note ·
The failing test's name sits beyond the 5000-line log window, and the full log's blob is refused by the egress proxy. The failed jobs are re-run once, through the relay. A second failure is treated as real. Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main (built on #26530's merge 7dd5a2ffa), Fixes #26546. 3 files: protocol-handshake.ts, its test and a patch changeset for @objectstack/metadata-core. Clause-②: no and Metadata-protocol: none appear in both the body and the changeset. |
| Fix | Triage 6436651479. One grammar, read once: alternativeMajorSpan turns each ` |
| Accept set | Unchanged. A 178,266-case differential shows the same admit verdict before and after; only the refused range's target and prescription move. |
| Downstream | Two CLI readers of targetMajor follow, as the changeset says. objectstack migrate meta now rewrites a '<18' range to '^18' when it migrates a stack to 18, where it used to leave it for the load to refuse. The protocol-version advisory now speaks for such a range. Both are the intended reading of a package behind the runtime. |
| Evidence | CI green on head 5046652093 (latest runs: 36 success, 11 skipped, 0 failed). Test Core (6/8) failed once in the plugin-security leg; the flake note 6439082411 judged it not this PR's, and the one re-run passed (job 122128956648). Governed check: NOT governed, 220 changed lines. Red first on main: 7 failed and 38 passed. With the fix 46 of 46 pass, and the ablation turns 8 red. Derived gates: 59, of which 58 exit 0 and 1 was NOT MEASURED (check:dual-build-cjs-loads, which CI runs). No ! and no narrowing arm, so no dogfood battery is owed. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26546
Clause-②: no
Metadata-protocol: none
维护者速读
engines.protocol用排他比较符时,OS_PROTOCOL_INCOMPATIBLE拒绝里的目标主版本按比较方向读。在 18 的运行时上,'>18'现在算“包比运行时新”,提示升级运行时到 19,不再给migrate meta;'<18'现在提示migrate meta --from 17,不再是--from 18。>、<当成^剥掉,两种写法都得到--from 18,也就是“从运行时自己的主版本迁移”,运维照做没有用。objectstack migrate meta迁到 18 时会把'<18'改写成'^18'(与'<=17'一致),版本差提示也会为它出声。回滚即 revert 本 PR。packages/metadata-core/src/protocol-handshake.ts(alternativeMajorSpan、comparatorMajorSpan、declaredTargetMajor);packages/metadata-core/src/protocol-handshake.test.ts末尾的[#26546]用例。pnpm --filter @objectstack/metadata-core exec vitest run src/protocol-handshake.test.ts,预期 46/46 通过,其中[#26546]九条。What changes
checkProtocolCompat(@objectstack/metadata-core) names a refused range's target major (diagnostic.targetMajor, alsoerror.details.targetMajoron both install doors) and, for a package behind the runtime,objectstack migrate meta --from N.declaredFloorMajorfound that major by peeling every leading^ ~ < > =and reading the first number, so a comparator's direction was lost.The single-range grammar is now read once, as a span of admitted majors:
comparatorMajorSpan(wascomparatorAdmitsMajor) returns the interval one comparator admits (examples with 18):'>=18'is 18 and up, bare'>18'is 19 and up,'>18.0.0'is 18 and up,'<=18'is 18 and below,'<18'/'<18.0.0'is 17 and below,'<18.5'is 18 and below. These are exactly the cases the oldswitchencoded.alternativeMajorSpan(was the body ofalternativeAdmitsMajor) returns one||alternative's span: compound ranges intersect their comparators, hyphen ranges are[lo, hi],^,~, wildcard and exact pin one major,*is open both ways.alternativeAdmitsMajorasks whether the span holds the runtime major.rangeAdmitsMajoris unchanged.declaredTargetMajor(wasdeclaredFloorMajor) reads the same span: its floor, or for a range open below, its ceiling. A||union still takes its lowest alternative. A span that holds no major ('>=19 <15','<0') names none (targetMajor: null, bareobjectstack migrate meta).checkProtocolCompat's ahead/behind split from #26530 is untouched; it now receives the right major. Answers on an 18 runtime:'>18'--from 18migrateCommand, "Upgrade the runtime to protocol 19"'<18','<18.0.0','<18.0'--from 18--from 17'<18 >=15'--from 18--from 15'>15 <18'--from 15--from 16'<17 || >19'--from 17--from 16'>18 || ^20'--from 18migrateCommand'>=19 <15'(admits nothing)migrateCommandobjectstack migrate meta'>=19','<=17','^17','~17.2','17.x','17','15 - 17','>=15 <18','^17 || ^16'On a 17 runtime
'>18'used to say "Upgrade the runtime to protocol 18", which'>18'does not admit; it now says 19. Partial bounds keep their major:'<18.5'on 19 targets 18 and'>18.1'on 17 targets 18, as before.The comments on
ProtocolIncompatibleDiagnostic.targetMajorand.migrateCommandnow describe the span reading. Changeset:@objectstack/metadata-corepatch.Why
Card #26546, ruled by triage
6436651479: on an 18 runtime,'>18'(the package is ahead) and'<18'(the package is behind) were both refused withmigrate meta --from 18, which cannot apply in either case. Reading the target from the same span the admit verdict uses means the verdict and the prescription cannot read one comparator two ways.Tests
Red first on
mainat7dd5a2ffa(#26530 merged), the new[#26546]block inprotocol-handshake.test.ts: 7 failed, 38 passed (45). The two inclusive controls ('>=19','<=17') passed onmain, as the ruling expects. The failures wereexpected 18 to be 19('>18'),expected 18 to be 17('<18'),expected 18 to be 15('<18 >=15'),expected 17 to be 16(union), and the'>18'HTTP answer carryingmigrateCommand. After the fix: 46/46 (one more case added after the red run: a range that admits no major names none).Ablation (fix committed,
scripts/ablation-replace.mjs):const major = empty ? null : (span.lo ?? span.hi);indeclaredTargetMajorreplaced by the old peel,leadingMajor(alternative.replace(/^[\^~<>=\s]+/, '')). Anchor 1 → 0, blobd918d6077d87→10fd32563699; 8 failed, 38 passed, every control green. Restored by the tool;git diff HEADempty afterwards.Admit verdicts do not move: a one-off differential (scratch script, not committed) loaded the
maincopy and this copy ofprotocol-handshake.tsside by side and comparedrangeAdmitsMajorandcheckProtocolCompat(...).statusover 8,103 ranges (every operator × 15 version tokens, comma and space compounds of every comparator pair, and||unions) on runtime majors 0 to 21: 178,266 cases, 0 differences.Whole package and dependents:
@objectstack/metadata-core:pnpm test836/836 (20 files);pnpm typecheck(tsc --noEmitand-p tsconfig.test.json) exit 0.@objectstack/cli(unit):src/utils/protocol-version-gap.test.tsandtest/migrate-meta-protocol-range.test.ts, 35/35.@objectstack/runtime:src/domains/packages-install-protocol-incompatible.test.tsandsrc/app-plugin.test.ts, 44/44.@objectstack/cloud-connection:src/marketplace-install-local-protocol-handshake.test.ts, 13/13.eslint --no-inline-configon the two touched.tsfiles: exit 0.Acceptance notes
targetMajorchange answer for'<18'-style ranges, measured (throwaway probe inpackages/cli, not committed; cli's vitest aliases metadata-core tosrc).planProtocolRange(objectstack migrate meta) used to leave'<18'alone when migrating to 18 (target 18 is not below 18, by code reading atmain); it now rewrites it to'^18'from--from 17, and with--from 18reportsbehind-fromnaming 17, the same answers it gives'<=17'and'^17'.checkProtocolVersionGapused to stay silent for'<18'on an 18 install and now says it "targets protocol v17".'>18'stays silent in both. The runtime's artifact boot refusal (artifact-reference.ts) namestargetMajorin its "run a runtime image for protocol N" line, which now reads 19 for'>18'. None of these readers' tests moved.Clause-②: no: no range moves between admitted and refused (the differential above), no exported type changes, andmigrateCommandwas already optional after fix(metadata-core)!: a package ahead of the runtime is told to upgrade, not to run migrate meta #26530.targetMajorchanges value only on refusals, and for the cases where it was wrong; the changeset names every changed reading.Metadata-protocol: none: no spec key, schema or stored shape is involved.OS_PROTOCOL_INCOMPATIBLErow comment inpackages/spec/src/api/error-code-ledger.zod.tslistsmigrateCommandamongerror.detailsmembers; it is absent in the ahead case since fix(metadata-core)!: a package ahead of the runtime is told to upgrade, not to run migrate meta #26530, and'>N'on an N runtime now reaches that case. ADR-0087 D1 describes the refusal carrying "the exact replay command". Both are under the spec seat and are left as they are.protocol-handshake.ts: checked the files of all 23 open PRs before opening this one.Verification
Derived gate families (dispatch-gates.mjs --commands, run at b17e3c3)
59 derived, 58 run with exit 0, 1 NOT MEASURED:
pnpm check:dual-build-cjs-loadsexit 3 (prerequisite not met: it needs a full workspace build; CI runs it).node scripts/check-plugin-teardown-shape.mjs --self-testfirst exited 3 because the shallow clone could not read its pinned fixture commit621a4876; aftergit fetch origin 621a4876…it exited 0 ("48 cases pass").dispatch-gates.mjs --ran: "59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED".The final head
504665209differs fromb17e3c33only in one prose line of the changeset. On the final head, with this body as the event:check-adr-0087-registration --base origin/mainexit 0 ("this PR adds no declared-breaking changeset");check-changeset-no-major --base origin/main --eventexit 0 (Metadata-protocol: none,patch);check-empty-changeset --base origin/mainexit 0;check:nul-bytesexit 0;check-closing-keyword-parity --bodybindsFixes #26546only.Human Minimum
Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.
packages/specholds no runtime logic. (Nopackages/specchange.)pnpm lintpasses. (Scoped to the touched files; the repo-wide run is CI's.)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5p97A4ysuQjSwrSaWT
Generated by Claude Code