Repository navigation
test(create-objectstack): satisfiesCaret reads a prerelease floor, so the runtime-image agreement holds on a cut-rc version pass - #20581
Merged
Conversation
… 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>
Contributor
📓 Docs Drift CheckNothing 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): |
Contributor
Author
|
This was referenced Sep 29, 2026
objectstack-fleet
Bot
deleted the
claude/issue-20566-caret-prerelease
branch
September 29, 2026 05:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20566
Clause-②: no
What changed
One file, tests only:
packages/create-objectstack/src/runtime-image.test.ts.The scaffolder writes
^OWN_VERSIONfor@objectstack/cli. Acut-rcversion pass in pre mode makes that version18.0.0-rc.N, so the scaffolded range is^18.0.0-rc.N. The test-local helpersatisfiesCaretaccepted only a plain^X.Y.Z. On such a range it answeredfalsefor 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.satisfiesCaretnow 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, so18.1.2and18.0.0satisfy^18.0.0-rc.1. A prerelease version counts only on a prerelease floor's ownX.Y.Z, so18.1.0-rc.1does not satisfy it. Prerelease precedence is in a smallcomparePrereleasebeside it.false. The thrown message names the range. Afalsehere 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:syncObjectStackDepswrites no range for a0.xversion.describe('satisfiesCaret'):^18.0.0-rc.1is satisfied by18.0.0-rc.2and18.1.2, and not by17.9.0or19.0.0.18.0.0yes,18.0.0-rc.0no,18.1.0-rc.1no.^17.4.0(17.4.0,17.5.2yes;17.3.9,18.0.0no).~18.0.0and^0.2.3throw, naming the range.semverwas not used. It is not acreate-objectstackdevDependency 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.semveris not usable from the package without adding a dependency.packages/create-objectstack/package.jsondevDependencies are@objectstack/spec,@types/node,tsup,typescript,vitestandyaml.semveris 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 rootnode_modules/semverlink. It has no types: there is no@types/semverin the store and no.d.tsinsemver@7.8.5, and the package'sstrictNodeNexttsconfig.jsoncompiles 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.)f11b5f20aand compared withsemver.satisfies(the root devDependency, used only as an oracle in a scratch script):^18.0.0-rc.1it returnedfalsefor18.0.0-rc.2,18.1.2and18.0.0, where semver saystrue. It also returnedtruefor^17.4.0vs17.5.0-rc.1, where semver saysfalse. That is 4 of 12 cases.166635e4c0disagrees on 0.packages/create-objectstack/package.jsonwas set to"version": "18.0.0-rc.1"throughscripts/ablation-replace.mjs, and the suite ran twice:fixture defect: the planted @objectstack/cli 18.1.2 does not satisfy ^18.0.0-rc.1, and the agreement test withscaffolded Dockerfile pins :18.1.2 but the scaffolded package.json asks for "^18.0.0-rc.1".package.jsonblob equals HEAD (49e071ff2a9d),git diff HEADis empty, the probe file is removed, andgit status --porcelainshows 0 lines.runtime-image.test.tshas noskipIf/runIfand ran on darwin here: 30/30. The one skipped file in the package run isscaffold-e2e-boot-probe.test.ts, which is Linux-only and untouched here. In CI this suite runs inci.ymlTest Core (create-objectstack#test).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 sevenreads a prerelease floorcases, 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: blobde6880de6d49equals HEAD andgit diff HEADis empty. There is nodist/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 --listFilesincludessrc/runtime-image.test.ts(16 test files compiled).pnpm --filter 'create-objectstack^...' build(builds@objectstack/spec), exit 0.pnpm lint: exit 0 in 32 s.eslint --format jsonon 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 --commandsderived 52 commands. All 52 were run and reconciled with--ran: 52 derived, 50 run, 2 NOT MEASURED, 0 UNRUN.check:dual-build-cjs-loads(86 packages withoutdist/) andcheck: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.create-objectstackhas norequirecondition, and it is in neither theDEBTnor theTEST_DEBTledger. CI's Lint & Repo Gates runs both after building the closure.check:lean-entry-closurefirst exited 3. After aturbo run build --filter=@objectstack/objectql(a full cache hit) it re-ran green.check:authz-resolver,check:error-code-casing,check:filter-alias-parity(the three rosters flagged underpackages/),check:scaffold-emission-policy,check:docs-image-tag, speccheck:error-code-provenanceandcheck-changeset-fixed. All exit 0.check-single-claim-paths,check-closing-target-claimandcheck-partof-closing-keywordneed 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/mainis stillf11b5f20a, an ancestor of HEAD (exit 0), so no merge was needed.files[]isdist,README.mdandCHANGELOG.md. Afterpnpm --filter create-objectstack build, a grep of those paths findssatisfiesCaret,comparePrereleaseandreads a prerelease floorin 0 files. The positive controlspinRuntimeImageandreadResolvedCliVersionare each found in 1 file. The seat applies the label.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot 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.
(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
satisfiesCaret('^17.4.0', '17.5.0-rc.1')wastrueand is nowfalse. That is semver's answer: a prerelease version is admitted only on a prerelease floor's ownX.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 asadmits a prerelease version only on a prerelease floor.~range, a0.xcaret, or any shape outside^MAJOR.MINOR.PATCH[-PRERELEASE]) used to returnfalseand now throws, naming the range. An unparseable VERSION still answersfalse, because alatesttag really does disagree with a range.String('18.0.0-rc.1').split('.').map(Number)givesownMinor0, so the planted version is18.1.2. That lies inside^18.0.0-rc.1and above its floor, as the RC end-to-end run shows.semver.Generated by Claude Code