Repository navigation
Commit 077b75f
fix(metadata-core): the protocol handshake reads an engines.protocol
Fixes #24029
Clause-②: no
## What was wrong
`rangeAdmitsMajor` (`packages/metadata-core/src/protocol-handshake.ts`)
had no branch for `||`. A union failed the compound-comparator test and
the hyphen test, reached the caret branch, and was read as a caret on
its first alternative. Measured on `origin/main` at `b74434c58` through
`checkProtocolCompat`: `engines.protocol: '^17.7.0 || ^18.0.0'` on
protocol `18.0.0` answered `status: 'incompatible'`
(`OS_PROTOCOL_INCOMPATIBLE`, `targetMajor: 17`). The function's own doc
and every caller's comment promise that a range is never refused unless
it positively excludes the runtime major, so this is the implementation
failing its declared contract (`Clause-②: no`: no accept set the
contract publishes moves).
## The fix
- `rangeAdmitsMajor` splits the trimmed range on `||` and reads each
alternative with the unchanged single-range body (now
`alternativeAdmitsMajor`). Any alternative admits, so the union admits.
Else any alternative is unrecognized (an empty one included), so the
union answers `null` (`unparsed-range`: admitted with the existing
`[protocol]` warning). Else `false`. A range with no `||` is its own
single alternative, so every existing single-range verdict is unchanged:
the existing pins pass unchanged, plus a CONTROL pin on a plain caret, a
hyphen range, `x` and `*`. The 128-character bound still runs before the
split.
- Two sibling readers of the same range read a union as its first
alternative too, both in the claimed surface, and now read it the same
way:
- `checkProtocolCompat`'s `targetMajor` (and so `migrateCommand`): a
refused union targets its lowest alternative's major. Before: `'^17 ||
^16'` refused on 18 gave `--from 17` and `'^16 || ^17'` gave `--from
16`. After: both give `--from 16`, the floor as the upgrade skill reads
it.
- `parseRangeFloor` (`artifact-forward-conversion.ts`): a union's floor
is its lowest alternative's, and `null` when any alternative is
unreadable. Measured on `origin/main` against the installed spec label
`18.0.0-next.2`: an artifact declaring `'^18.0.0 || ^17.1.0'` read floor
`18.0.0`, took verdict `converted-major-step`, and left a step-11
retired `http_request` flow node unconverted. The same artifact spelled
`'^17.1.0 || ^18.0.0'` read `17.1.0`, took `converted-forward`, and
converted the node. After: both spellings read `17.1.0` and take
`converted-forward`.
- One `patch` changeset for `@objectstack/metadata-core`.
## Readers census (acceptance 3)
`rangeAdmitsMajor` has exactly one caller, `checkProtocolCompat` (`git
grep` over the tree). Every other reader of `engines.protocol` was read:
| Reader | Reads `\|\|` itself? | Disposition |
|---|---|---|
| `checkProtocolCompat`: `targetMajor` / `migrateCommand` | yes, as its
first alternative | fixed here (lowest alternative), pinned |
| `parseRangeFloor` (`artifact-forward-conversion.ts`) | yes, as its
first alternative | fixed here (lowest alternative; unreadable
alternative gives `null`), pinned |
| `resolveDeclaredRange` | no: picks the key and trims, never parses |
none |
| `service-package` hydration, `cloud-connection` install-local and
rehydrate, `metadata-protocol` `installPackage`, `runtime`
(`artifact-reference.ts`, `domains/packages.ts`, `app-plugin.ts`) | no:
delegate to `checkProtocolCompat` / `assertProtocolCompat` | inherit the
fix; not edited |
| `objectql` `engine.ts` | no: `resolveDeclaredRange(...) === null`
presence test only | none |
| CLI `protocol-version-gap.ts` (doctor/validate/compile advisory),
`migrate/meta.ts` `planProtocolRange` | no: read the verdict and
`diagnostic.targetMajor` | inherit the fix; not edited |
| CLI `lint.ts` `protocol/missing-engines-range` | no: `typeof … ===
'string'` presence test only | none |
| `@objectstack/core` `SemanticVersionManager.satisfies`
(`dependency-resolver.ts`) | yes, its own grammar: caret on the first
alternative, `false` for an unrecognized shape | not a reader of
`engines.protocol` and not a caller of `rangeAdmitsMajor`; plugin
dependency constraints; zero in-tree callers outside `core`. Acceptance
note below |
Docs: ADR-0087 D1 states no range grammar. No page under `content/docs/`
or `skills/` says unions are unsupported. The upgrade skill reads
`--from` as "the floor ... never the upper bound", which the
`targetMajor` change follows. Nothing is falsified and no doc is edited.
## Edge readings (measured on the built `dist/`, before and after)
| Range | Major | `origin/main` | This PR |
|---|---|---|---|
| `'^17.7.0 \|\| ^18.0.0'` | 18 / 17 / 19 | `false` / `true` / `false` |
`true` / `true` / `false` |
| `'^16 \|\| ^17'` | 18 | `false` | `false` |
| `'^17 \|\| foo'` | 18 | `false` | `null` |
| `'foo \|\| ^18'` | 18 | `null` | `true` |
| `'^17 \|\|'` | 18 / 17 | `false` / `true` | `null` / `true` |
| `'\|\| ^18'` | 18 / 17 | `null` / not measured | `true` / `null` |
| `'\|\|'` | 18 | `null` | `null` |
| `'^17 \|\| \|\| ^18'` | 18 | `false` | `true` |
| `'17 - 18 \|\| ^20'` | 18 / 20 | `true` / `false` (the hyphen split
read `18 \|\| ^20` as its upper bound) | `true` / `true` |
| `'^17 \| ^18'` (single bar) | 18 / 17 | `false` / `true` | unchanged:
`false` / `true` |
No empty-alternative edge reads `false`. The single `|` is not split, as
dispatched, and reads exactly as on `origin/main`. See Acceptance notes.
## Evidence
The red leg and the ablations ran at `27f22bd99`, the fix commit. The
package test and typecheck ran again at `b62e33f71`, after `origin/main`
was merged. That merge touches no file in this diff.
- **Red leg** (BASE `b74434c58`'s two source files restored into the
tree, the new pins kept): `Tests 9 failed | 227 passed (236)`. The two
that stay green on BASE are the `'^16 || ^17'` refusal pin
(`origin/main` refuses it too) and the CONTROL. Restore proven by blob
hash equal to HEAD and an empty `git diff HEAD`.
- **Ablations** (`scripts/ablation-replace.mjs`, wrapped mode; the
subject is imported from `src/` by relative path, so no `dist/` leg is
involved). Each landed with anchor 1 → 0 and a changed blob, and each
restore was proven by blob equal to HEAD and an empty `git diff HEAD`.
1. Drop the union split in `rangeAdmitsMajor` (`rangeAlternatives(r)`
replaced by `[r]`): `5 failed | 231 passed`. Red: the bridging range,
unreadable-beside-refusing, the per-alternative grammar, the empty
alternative, and the boundary throw.
2. A lenient union that never refuses (`return verdict;` replaced by
`return r.includes('||') && verdict === false ? null : verdict;`): `5
failed | 231 passed`, including the `'^16 || ^17'` refusal pin and the
`19` refusal.
3. Drop the split in `declaredFloorMajor`: `1 failed` (the `targetMajor`
pin).
4. Drop the split in `parseRangeFloor`: `3 failed` (the three
floor/window pins).
- **Package:** `pnpm --filter @objectstack/metadata-core test`: `Test
Files 18 passed (18)`, `Tests 761 passed (761)`. `pnpm --filter
@objectstack/metadata-core typecheck` (`tsc --noEmit && tsc --noEmit -p
tsconfig.test.json`): exit 0. `--listFiles` of `tsconfig.test.json`
lists both touched test files.
- **Gates** at head `b62e33f71`: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` derives 63 commands (with
no paths: the change set comes from the merge base). All 63 ran with
their exit codes recorded, and all exited 0. `--ran` reconciliation: `63
derived, 63 run, 0 NOT-MEASURED, 0 UNRUN`. `check:dual-build-cjs-loads`
and `check:lean-entry-closure` first answered `PREREQUISITE NOT MET`
(exit 3, unbuilt workspace). `check:type-check-debt` then built the
workspace closure, and both were re-run to exit 0. `check:dts-closure`
and `check:sourcemap-no-sources-content` were re-run after that build
(74 and 71 built packages swept). The four artifact-roster families
whose roster sits in a directory this diff touches
(`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`) also ran: exit
0. The `test:repo` vitest project and the declared wide-population
families are CI's.
## Acceptance notes
- **Single `|`** (`'^17 | ^18'`): not a union in npm semver, and not
split here, as dispatched. It reads as `^17` because the caret and tilde
branches read only the leading integer (`leadingMajor`) and ignore
trailing text. The same leniency reads `'^17 garbage'` as `^17`. So a
mistyped union is refused on 18 rather than answered `unparsed-range`.
Not measured at a public door; carrier: none.
- **`@objectstack/core` `SemanticVersionManager.satisfies`** parses its
own grammar for plugin dependency constraints: caret on the first
alternative of a union, `false` for an unrecognized shape. It has zero
in-tree callers outside `core`, and it does not read `engines.protocol`.
Carrier: none.
- **`parseRangeFloor('<18')`** reads `18.0.0` although `<18` admits
everything below 18. This predates this PR, is not about unions, and is
inside the function's documented scope ("every range the tooling stamps
or the docs teach leads with its floor"). Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi)_
Co-authored-by: Claude <noreply@anthropic.com>|| union instead of refusing it on its first alternative (#24051)1 parent 62f959d commit 077b75f
5 files changed
Lines changed: 184 additions & 7 deletions
File tree
- .changeset
- packages/metadata-core/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1087 | 1087 | | |
1088 | 1088 | | |
1089 | 1089 | | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
1090 | 1109 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
326 | 327 | | |
327 | 328 | | |
328 | 329 | | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
329 | 335 | | |
330 | 336 | | |
331 | 337 | | |
332 | 338 | | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
333 | 350 | | |
334 | 351 | | |
335 | 352 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
69 | 73 | | |
70 | 74 | | |
71 | 75 | | |
| |||
216 | 220 | | |
217 | 221 | | |
218 | 222 | | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
219 | 231 | | |
220 | 232 | | |
221 | 233 | | |
222 | 234 | | |
223 | 235 | | |
224 | 236 | | |
225 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
226 | 245 | | |
227 | 246 | | |
228 | 247 | | |
229 | | - | |
230 | 248 | | |
231 | | - | |
232 | | - | |
233 | | - | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
234 | 253 | | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
235 | 266 | | |
236 | 267 | | |
237 | 268 | | |
| |||
284 | 315 | | |
285 | 316 | | |
286 | 317 | | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
287 | 335 | | |
288 | 336 | | |
289 | 337 | | |
| |||
352 | 400 | | |
353 | 401 | | |
354 | 402 | | |
355 | | - | |
| 403 | + | |
356 | 404 | | |
357 | 405 | | |
358 | 406 | | |
| |||
0 commit comments