Skip to content

test(create-objectstack): satisfiesCaret reads a prerelease floor, so the runtime-image agreement holds on a cut-rc version pass - #20581

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20566-caret-prerelease
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20566-caret-prerelease

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20566
Clause-②: no

What changed

One file, tests only: packages/create-objectstack/src/runtime-image.test.ts.

The scaffolder writes ^OWN_VERSION for @objectstack/cli. A cut-rc version pass in pre mode makes that version 18.0.0-rc.N, so the scaffolded range is ^18.0.0-rc.N. The test-local helper satisfiesCaret accepted only a plain ^X.Y.Z. On such a range it answered false for every version. So the fixture precondition and the agreement test would both go red on the RC version PR, and the failure would blame the fixture and the scaffolder.

  • satisfiesCaret now follows semver caret semantics for a prerelease floor. The version must have the same major and be at or above the floor. A release outranks its own prereleases, so 18.1.2 and 18.0.0 satisfy ^18.0.0-rc.1. A prerelease version counts only on a prerelease floor's own X.Y.Z, so 18.1.0-rc.1 does not satisfy it. Prerelease precedence is in a small comparePrerelease beside it.
  • A range the helper cannot read now throws instead of answering false. The thrown message names the range. A false here reads as the scaffolder's or the fixture's fault, and that is how this defect would have presented. Majors of 1 and up only: syncObjectStackDeps writes no range for a 0.x version.
  • New pins in describe('satisfiesCaret'):
    • Triage's grade: ^18.0.0-rc.1 is satisfied by 18.0.0-rc.2 and 18.1.2, and not by 17.9.0 or 19.0.0.
    • Three more prerelease-floor cases: 18.0.0 yes, 18.0.0-rc.0 no, 18.1.0-rc.1 no.
    • The plain-floor controls on ^17.4.0 (17.4.0, 17.5.2 yes; 17.3.9, 18.0.0 no).
    • One case for a prerelease version against a plain floor.
    • The refusal: ~18.0.0 and ^0.2.3 throw, naming the range.

semver was not used. It is not a create-objectstack devDependency at this ref (see H0 below), and triage's grade takes the "otherwise" branch in that case.

Measurements

All of these ran at 166635e4c0 (branch head), which the test/typecheck/lint/gate readings below cite.

  • H0: semver is not usable from the package without adding a dependency. packages/create-objectstack/package.json devDependencies are @objectstack/spec, @types/node, tsup, typescript, vitest and yaml. semver is a ROOT devDependency (package.json, used by two root scripts). From the package directory, import.meta.resolve('semver') does resolve at runtime, but only as a phantom through the root node_modules/semver link. It has no types: there is no @types/semver in the store and no .d.ts in semver@7.8.5, and the package's strict NodeNext tsconfig.json compiles this test file. So the import would resolve at runtime and fail the typecheck. That falls under triage's "otherwise" branch. (The typecheck failure is inferred from the absent declarations, not run.)
  • H1: the helper as it stood answers the prerelease range wrongly. The helper was extracted from f11b5f20a and compared with semver.satisfies (the root devDependency, used only as an oracle in a scratch script):
    • On ^18.0.0-rc.1 it returned false for 18.0.0-rc.2, 18.1.2 and 18.0.0, where semver says true. It also returned true for ^17.4.0 vs 17.5.0-rc.1, where semver says false. That is 4 of 12 cases.
    • Over a 104,979-pair matrix (majors 17–19, twelve prerelease tails, plus build metadata), the old helper disagrees with semver on 4,754 pairs. The new helper at 166635e4c0 disagrees on 0.
  • The card's symptom, measured end to end. It had not been run against an RC tree before. packages/create-objectstack/package.json was set to "version": "18.0.0-rc.1" through scripts/ablation-replace.mjs, and the suite ran twice:
    • The BASE copy of this test file: 2 failed | 15 passed, exactly the two the card predicted. The precondition failed with fixture defect: the planted @objectstack/cli 18.1.2 does not satisfy ^18.0.0-rc.1, and the agreement test with scaffolded Dockerfile pins :18.1.2 but the scaffolded package.json asks for "^18.0.0-rc.1".
    • This branch's file: 30 passed (30).
    • Restore proven: package.json blob equals HEAD (49e071ff2a9d), git diff HEAD is empty, the probe file is removed, and git status --porcelain shows 0 lines.
  • H2: the suite is not platform-gated. runtime-image.test.ts has no skipIf/runIf and ran on darwin here: 30/30. The one skipped file in the package run is scaffold-e2e-boot-probe.test.ts, which is Linux-only and untouched here. In CI this suite runs in ci.yml Test Core (create-objectstack#test).
  • H3: ablation. The prerelease branch was removed from the range regex (anchor hit x1 → x0, blob de6880de6d49 → 2a83466c65c3). The expected direction was declared before the run: the seven prerelease-floor pins go red because the range becomes unreadable, and the other 23 stay green. Observed: 7 failed | 23 passed (30). They are exactly the seven reads a prerelease floor cases, each failing on the helper's named refusal (satisfiesCaret reads ^MAJOR.MINOR.PATCH[-PRERELEASE] with MAJOR >= 1, got "^18.0.0-rc.1"). The plain-floor controls stayed green. Restore proven: blob de6880de6d49 equals HEAD and git diff HEAD is empty. There is no dist/ leg, because vitest reads this test file from source and the helper lives in it.

Verification

  • pnpm --filter create-objectstack exec vitest run --maxWorkers=2 src/runtime-image.test.ts: 30 passed (30) (17 before this change).
  • pnpm --filter create-objectstack exec vitest run --maxWorkers=2 (whole package): 15 passed | 1 skipped (16) files, 233 passed | 14 skipped (247) tests.
  • pnpm --filter create-objectstack typecheck: exit 0. tsc --listFiles includes src/runtime-image.test.ts (16 test files compiled).
  • Dependency closure first: pnpm --filter 'create-objectstack^...' build (builds @objectstack/spec), exit 0.
  • pnpm lint: exit 0 in 32 s. eslint --format json on the edited file: 1 result, 0 errors, 0 warnings (the file is linted, not ignored).
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 52 commands. All 52 were run and reconciled with --ran: 52 derived, 50 run, 2 NOT MEASURED, 0 UNRUN.
    • NOT MEASURED: check:dual-build-cjs-loads (86 packages without dist/) and check:type-check-debt (30 dependencies without built types). Both exited 3, PREREQUISITE NOT MET. A whole-workspace build would have 20 turbo cache misses (console and examples among them) on a shared host.
    • Why this diff cannot move either one: the diff changes no built output (dist grep below). create-objectstack has no require condition, and it is in neither the DEBT nor the TEST_DEBT ledger. CI's Lint & Repo Gates runs both after building the closure.
    • check:lean-entry-closure first exited 3. After a turbo run build --filter=@objectstack/objectql (a full cache hit) it re-ran green.
  • Roster rows that could apply: check:authz-resolver, check:error-code-casing, check:filter-alias-parity (the three rosters flagged under packages/), check:scaffold-emission-policy, check:docs-image-tag, spec check:error-code-provenance and check-changeset-fixed. All exit 0. check-single-claim-paths, check-closing-target-claim and check-partof-closing-keyword need PR context and were run once this PR existed; readings are in the report on the card.
  • node scripts/check-issue-citations.mjs --base origin/main: exit 0. origin/main is still f11b5f20a, an ancestor of HEAD (exit 0), so no merge was needed.
  • Nothing ships (skip-changeset evidence). files[] is dist, README.md and CHANGELOG.md. After pnpm --filter create-objectstack build, a grep of those paths finds satisfiesCaret, comparePrerelease and reads a prerelease floor in 0 files. The positive controls pinRuntimeImage and readResolvedCliVersion are each found in 1 file. The seat applies the label.

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.

NODE_OPTIONS=--max-old-space-size=4096 pnpm --workspace-concurrency=2 --filter 'create-objectstack^...' build
NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter create-objectstack exec vitest run --maxWorkers=2 src/runtime-image.test.ts
NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter create-objectstack exec vitest run --maxWorkers=2 --reporter=verbose src/runtime-image.test.ts
NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter create-objectstack typecheck
NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter create-objectstack exec vitest run --maxWorkers=2
bash ablation-h3.sh   (the H3 ablation above, wrapped whole)
bash rc-e2e.sh        (the RC end-to-end proof above, wrapped whole)
NODE_OPTIONS=--max-old-space-size=4096 pnpm exec turbo run build --filter=@objectstack/objectql --concurrency=2
NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter create-objectstack build
NODE_OPTIONS=--max-old-space-size=4096 pnpm lint

(The helper printed the wording above once per run, each time naming that run's command. It is pasted once here, with every unlocked command in the block.)

Acceptance notes

  • One answer changes on a plain floor, and it is unreachable from the fixtures. satisfiesCaret('^17.4.0', '17.5.0-rc.1') was true and is now false. That is semver's answer: a prerelease version is admitted only on a prerelease floor's own X.Y.Z. The suite's version argument is always a release (OWN_MAJOR.(OWN_MINOR+1).2, or the tag pinned from it), so no existing assertion read the old answer. It is pinned as admits a prerelease version only on a prerelease floor.
  • The refusal is new behaviour. An unreadable range (a ~ range, a 0.x caret, or any shape outside ^MAJOR.MINOR.PATCH[-PRERELEASE]) used to return false and now throws, naming the range. An unparseable VERSION still answers false, because a latest tag really does disagree with a range.
  • The fixture derivation needed no change for RC. String('18.0.0-rc.1').split('.').map(Number) gives ownMinor 0, so the planted version is 18.1.2. That lies inside ^18.0.0-rc.1 and above its floor, as the RC end-to-end run shows.
  • The oracle matrix, the H1 probe and the two mutation scripts were one-time proofs run from the session scratchpad. No permanent test depends on semver.

Generated by Claude Code

… the runtime-image agreement holds on a cut-rc version pass

In pre mode a cut-rc version pass makes the scaffolder's own version
18.0.0-rc.N, so it writes ^18.0.0-rc.N for @objectstack/cli. The test-local
helper accepted only a plain ^X.Y.Z, answered false for every version, and
the fixture precondition and the agreement test would both have gone red on
the release PR, blaming the fixture and the scaffolder.

The helper now follows semver caret semantics for a prerelease floor: same
major, at or above the floor, a release outranks its own prereleases, and a
prerelease version is admitted only on a prerelease floor's own X.Y.Z. A
range it cannot read throws instead of answering false. New unit pins cover
the prerelease floor, the plain-floor controls and the refusal.

Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 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 f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

skip-changeset: applied by the domain:cli seat on its own reading (tests only)

domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-29T05:03Z

  • The diff: one file, packages/create-objectstack/src/runtime-image.test.ts (+87 −6), at head 166635e4. No .changeset/*.md is added or edited.
  • What ships: create-objectstack's package.json files is dist, README.md and CHANGELOG.md, and a src/*.test.ts file is not in the tarball. The changed helper is test-local.
  • So: tests only, with no consumer-visible change. The repo's tests-only route is this label; ⛔ not an empty changeset. Check Changeset re-runs on the label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

1 participant