Skip to content

fix(metadata-core): an exclusive protocol comparator ('>18', '<18') names the major it admits in the refusal - #26595

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-860dh7
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-860dh7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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,也就是“从运行时自己的主版本迁移”,运维照做没有用。
  • 风险与回滚:低。哪些范围被接受、哪些被拒绝完全不变(新旧读法在 17.8 万组范围与主版本组合上对照,准入结论零差异);只改被拒绝时的目标主版本和提示。附带两处 CLI 读者跟着变对: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, also error.details.targetMajor on both install doors) and, for a package behind the runtime, objectstack migrate meta --from N. declaredFloorMajor found 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 (was comparatorAdmitsMajor) 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 old switch encoded.
  • alternativeMajorSpan (was the body of alternativeAdmitsMajor) 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.
  • alternativeAdmitsMajor asks whether the span holds the runtime major. rangeAdmitsMajor is unchanged.
  • declaredTargetMajor (was declaredFloorMajor) 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, bare objectstack migrate meta).

checkProtocolCompat's ahead/behind split from #26530 is untouched; it now receives the right major. Answers on an 18 runtime:

Range Before After
'>18' 18 · --from 18 19 · no migrateCommand, "Upgrade the runtime to protocol 19"
'<18', '<18.0.0', '<18.0' 18 · --from 18 17 · --from 17
'<18 >=15' 18 · --from 18 15 · --from 15
'>15 <18' 15 · --from 15 16 · --from 16
'<17 || >19' 17 · --from 17 16 · --from 16
'>18 || ^20' 18 · --from 18 19 · no migrateCommand
'>=19 <15' (admits nothing) 19 · no migrateCommand null · objectstack migrate meta
'>=19', '<=17', '^17', '~17.2', '17.x', '17', '15 - 17', '>=15 <18', '^17 || ^16' unchanged unchanged

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.targetMajor and .migrateCommand now describe the span reading. Changeset: @objectstack/metadata-core patch.

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 with migrate 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 main at 7dd5a2ffa (#26530 merged), the new [#26546] block in protocol-handshake.test.ts: 7 failed, 38 passed (45). The two inclusive controls ('>=19', '<=17') passed on main, as the ruling expects. The failures were expected 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 carrying migrateCommand. 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); in declaredTargetMajor replaced by the old peel, leadingMajor(alternative.replace(/^[\^~<>=\s]+/, '')). Anchor 1 → 0, blob d918d6077d87 → 10fd32563699; 8 failed, 38 passed, every control green. Restored by the tool; git diff HEAD empty afterwards.

Admit verdicts do not move: a one-off differential (scratch script, not committed) loaded the main copy and this copy of protocol-handshake.ts side by side and compared rangeAdmitsMajor and checkProtocolCompat(...).status over 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 test 836/836 (20 files); pnpm typecheck (tsc --noEmit and -p tsconfig.test.json) exit 0.
  • @objectstack/cli (unit): src/utils/protocol-version-gap.test.ts and test/migrate-meta-protocol-range.test.ts, 35/35.
  • @objectstack/runtime: src/domains/packages-install-protocol-incompatible.test.ts and src/app-plugin.test.ts, 44/44.
  • @objectstack/cloud-connection: src/marketplace-install-local-protocol-handshake.test.ts, 13/13.
  • eslint --no-inline-config on the two touched .ts files: exit 0.

Acceptance notes

  • Two CLI readers of targetMajor change answer for '<18'-style ranges, measured (throwaway probe in packages/cli, not committed; cli's vitest aliases metadata-core to src). planProtocolRange (objectstack migrate meta) used to leave '<18' alone when migrating to 18 (target 18 is not below 18, by code reading at main); it now rewrites it to '^18' from --from 17, and with --from 18 reports behind-from naming 17, the same answers it gives '<=17' and '^17'. checkProtocolVersionGap used 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) names targetMajor in its "run a runtime image for protocol N" line, which now reads 19 for '>18'. None of these readers' tests moved.
  • Declarations. Clause-②: no: no range moves between admitted and refused (the differential above), no exported type changes, and migrateCommand was already optional after fix(metadata-core)!: a package ahead of the runtime is told to upgrade, not to run migrate meta #26530. targetMajor changes 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.
  • Spec comment, not this lane. The OS_PROTOCOL_INCOMPATIBLE row comment in packages/spec/src/api/error-code-ledger.zod.ts lists migrateCommand among error.details members; 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.
  • No open PR touches 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-loads exit 3 (prerequisite not met: it needs a full workspace build; CI runs it). node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3 because the shallow clone could not read its pinned fixture commit 621a4876; after git 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 504665209 differs from b17e3c33 only in one prose line of the changeset. On the final head, with this body as the event: check-adr-0087-registration --base origin/main exit 0 ("this PR adds no declared-breaking changeset"); check-changeset-no-major --base origin/main --event exit 0 (Metadata-protocol: none, patch); check-empty-changeset --base origin/main exit 0; check:nul-bytes exit 0; check-closing-keyword-parity --body binds Fixes #26546 only.

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (Scoped to the touched files; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5p97A4ysuQjSwrSaWT


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-core, touching 9 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via checkProtocolCompat (symbol, a top-level function))

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 55 of 207 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 — 5 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 898703f90338648e53808ccc8ad6b5120009e7a6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6aac32bbc95c9d14852c85a08aa0eef949d8652b — the merge of head 5046652093e15988f0a5d9f8778a2897a7d079a9 into base 898703f90338648e53808ccc8ad6b5120009e7a6, 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 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

⚠️ 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 898703f90338648e53808ccc8ad6b5120009e7a6 → 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

Seat note · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-27T10:09Z · one re-run

Test Core (6/8) failed on head 5046652093 (run 40481442754, job 122113373284). The check-run annotation names only the packages/plugins/plugin-security leg (pnpm run test exited 1). The seat judges it not this PR's, on the card thread's measurement:

  • At this exact head, the plugin-security suite passes locally under the lock: 229 of 229 files, and 4962 tests passed with 45 skipped.
  • Nothing in plugin-security's source or tests reads checkProtocolCompat, targetMajor, migrateCommand, OS_PROTOCOL_INCOMPATIBLE or an engines.protocol range (git grep: zero hits).
  • The CLI leg, which does read targetMajor, passed in the same job.

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-27T10:31Z

Head 5046652093, report 6438363369 (on #26546), claim 6436864566. No contract review is owed: there is no packages/spec/** diff, no governed path, no narrowing and no Clause-②: yes.

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 10:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 10:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 804a2b0 Oct 27, 2026
68 of 70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-860dh7 branch October 27, 2026 10:53
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

Development

Successfully merging this pull request may close these issues.

metadata-core: an exclusive protocol comparator ('>18', '<18') on an 18 runtime prescribes migrate meta --from 18, which cannot apply

2 participants