function parseSemver(value) { const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/); if (!match) return null; return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), raw: `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}` }; } function compareSemver(left, right) { const a = parseSemver(left); const b = parseSemver(right); if (!a && !b) return 0; if (!a) return -1; if (!b) return 1; for (const key of ["major", "minor", "patch"]) { if (a[key] > b[key]) return 1; if (a[key] < b[key]) return -1; } return 0; } function isMajorJump(current, target) { const from = parseSemver(current); const to = parseSemver(target); return Boolean(from && to && from.major !== to.major); } function versionInRange(version, from, to) { return compareSemver(version, from) > 0 && compareSemver(version, to) <= 0; } function normalizeVersionEntry(entry, fallback = {}) { if (typeof entry === "string") return { version: entry, ...fallback }; if (!entry || typeof entry !== "object") return null; const version = entry.version || entry.name || entry.tag; if (!parseSemver(version)) return null; return { ...fallback, ...entry, version: parseSemver(version).raw }; } function normalizeVersions(entries, fallback = {}) { return (Array.isArray(entries) ? entries : []) .map((entry) => normalizeVersionEntry(entry, fallback)) .filter(Boolean) .sort((a, b) => compareSemver(a.version, b.version)); } function compatibleFromAllows(entry, current) { if (!entry?.compatible_from) return false; return compareSemver(current, entry.compatible_from) >= 0; } function isBridgeTarget(entry, current) { const from = parseSemver(current); const to = parseSemver(entry?.version); if (!from || !to || from.major === to.major) return false; if (entry.compatibility_bridge === true || entry.migration_kind === "compatibility_bridge") { return compatibleFromAllows(entry, current) || !entry.compatible_from; } if (to.major === from.major + 1 && to.minor === 0 && to.patch === 0) { return compatibleFromAllows(entry, current) || entry.compatible_from === undefined; } return false; } function findSafeTarget(currentVersion, versionEntries) { const current = parseSemver(currentVersion); const versions = normalizeVersions(versionEntries); if (!current) { return { target: null, blocked: true, reason: "Current version is not valid semver." }; } const newer = versions.filter((entry) => compareSemver(entry.version, current.raw) > 0); if (!newer.length) { return { target: null, blocked: false, reason: "Already up to date." }; } const latest = newer[newer.length - 1]; const sameMajor = newer.filter((entry) => parseSemver(entry.version)?.major === current.major); if (sameMajor.length) { return { target: sameMajor[sameMajor.length - 1], latest, blocked: false }; } const bridge = newer.find((entry) => isBridgeTarget(entry, current.raw)); if (bridge) { return { target: bridge, latest, blocked: false, warning: `Latest ${latest.version} crosses a major boundary; targeting compatibility bridge ${bridge.version}.` }; } return { target: null, latest, blocked: true, reason: `Latest ${latest.version} crosses a major boundary and no compatible bridge target was found.` }; } function collectChangelogRange(currentVersion, targetVersion, changelogEntries) { if (!targetVersion) return []; return normalizeVersions(changelogEntries) .filter((entry) => versionInRange(entry.version, currentVersion, targetVersion)) .sort((a, b) => compareSemver(b.version, a.version)); } module.exports = { parseSemver, compareSemver, isMajorJump, normalizeVersions, findSafeTarget, collectChangelogRange };