Follow-up to #1077, split out so that issue stays scoped to the comparator itself.
Problem
resolveLowestKnownNonVulnerableVersion has a second code path that does not filter pre-releases, so it can recommend a version that predates the fix.
src/remediation/npm-registry.ts:176-178 sorts every published version with no pre-release filter:
const publishedVersions = Object.keys(packument.versions ?? {})
.filter(looksLikeVersion)
.sort(compareVersions);
Line 192 then picks the nearest version at or above the advisory's fixed-version hint:
const nearestPublishedVersion = publishedVersions.find(version => compareVersions(version, fixedVersionHint) >= 0) ?? null;
Reproduced against the real comparator:
published: ["1.2.0","1.2.2","1.2.3-rc.1","1.2.4","1.3.0"]
advisory fixed hint: 1.2.3
-> recommends: 1.2.3-rc.1
1.2.3-rc.1 is below 1.2.3 under semver §11.3, so it does not contain the fix. The CLI emits a copy-and-run upgrade command that does not remediate, along with a note presenting it as the nearest published version. This directly contradicts the actionable-output principle: a wrong command is worse than no command.
Note the sibling path at line 261 already gets this right with .filter(v => !isPreReleaseVersion(v)). Only this earlier branch is missing it.
Scope: this affects online mode
#1077's detection false negative is offline-only, because online detection goes through OSV querybatch and the npm bulk advisory endpoint, both server-side and semver-correct. This issue is different: remediation runs against the npm registry for everyone, so a bad fix recommendation reaches all users regardless of --offline.
Relationship to #1077
Fixing the comparator in src/utils/version.ts corrects the ordering but does not fix this on its own. Once compareVersions is semver-correct, 1.2.3-rc.1 >= 1.2.3 becomes false, so .find() would skip it and land on 1.2.4, which happens to be right here. But that is incidental: the path still has no explicit intent to exclude pre-releases, so an advisory whose fixed value is itself a pre-release would still select one. The filter should be explicit rather than emergent.
Sequence: land #1077 first, then this, then re-run the check above.
Suggested fix
Apply the same .filter(v => !isPreReleaseVersion(v)) used at line 261 to the publishedVersions list, and decide deliberately what should happen when the advisory's own fixed value is a pre-release. Add a regression test pinning that a pre-release is never returned as a resolved fix version.
Notes
Follow-up to #1077, split out so that issue stays scoped to the comparator itself.
Problem
resolveLowestKnownNonVulnerableVersionhas a second code path that does not filter pre-releases, so it can recommend a version that predates the fix.src/remediation/npm-registry.ts:176-178sorts every published version with no pre-release filter:Line 192 then picks the nearest version at or above the advisory's fixed-version hint:
Reproduced against the real comparator:
1.2.3-rc.1is below1.2.3under semver §11.3, so it does not contain the fix. The CLI emits a copy-and-run upgrade command that does not remediate, along with anotepresenting it as the nearest published version. This directly contradicts the actionable-output principle: a wrong command is worse than no command.Note the sibling path at line 261 already gets this right with
.filter(v => !isPreReleaseVersion(v)). Only this earlier branch is missing it.Scope: this affects online mode
#1077's detection false negative is offline-only, because online detection goes through OSV querybatch and the npm bulk advisory endpoint, both server-side and semver-correct. This issue is different: remediation runs against the npm registry for everyone, so a bad fix recommendation reaches all users regardless of
--offline.Relationship to #1077
Fixing the comparator in
src/utils/version.tscorrects the ordering but does not fix this on its own. OncecompareVersionsis semver-correct,1.2.3-rc.1 >= 1.2.3becomes false, so.find()would skip it and land on1.2.4, which happens to be right here. But that is incidental: the path still has no explicit intent to exclude pre-releases, so an advisory whosefixedvalue is itself a pre-release would still select one. The filter should be explicit rather than emergent.Sequence: land #1077 first, then this, then re-run the check above.
Suggested fix
Apply the same
.filter(v => !isPreReleaseVersion(v))used at line 261 to thepublishedVersionslist, and decide deliberately what should happen when the advisory's ownfixedvalue is a pre-release. Add a regression test pinning that a pre-release is never returned as a resolved fix version.Notes
suggestedFixCommandsemits a version that does not exist) may share this root cause and is worth re-checking once this lands. feat: make --fix / self-fix respect the configured release cooldown (hold back cooldown-window versions) #873 (cooldown-aware fix resolver) edits the same candidate pipeline, so sequence to avoid conflicts.