chore: address Copilot review round 16

`parseClaudeVersion` dropped SemVer suffixes, so an installed `2.1.250-beta.1`
parsed as `[2, 1, 250]` and compared EQUAL to the stable pin — a different
validator schema running locally while the log claimed an exact match, which is
the cross-machine disagreement the pin exists to prevent.

It now returns `{ parts, prerelease }`, because the two callers want different
things: `isPinnedVersion` rejects any prerelease (build metadata still matches,
per SemVer precedence), while the eval's availability probe only needs some
usable CLI. `formatClaudeVersion` keeps the suffix in the messages, so a
prerelease is reported as what it is rather than as the pin.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-29 04:40:48 -04:00
commit c26995bf82
2 changed files with 102 additions and 54 deletions

View file

@ -253,21 +253,48 @@ export function validateEvalCases(skillName, cases) {
export const PINNED_CLI_VERSION = "2.1.250";
/**
* Read a semver triple out of `claude --version` output ("2.1.250 (Claude Code)").
* Read the version out of `claude --version` output ("2.1.250 (Claude Code)").
*
* Returns the release triple **and** whether a prerelease/build suffix was
* attached, because the two callers want different things: the pin comparison
* must reject `2.1.250-beta.1` (a different validator schema that would compare
* equal to the stable pin while the log claimed an exact match — Copilot),
* while the eval's availability probe only needs *some* usable CLI.
*
* @param {string} text
* @returns {number[] | null}
* @returns {{ parts: number[], prerelease: string | null } | null}
*/
export function parseClaudeVersion(text) {
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];
const m =
/(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?/.exec(text);
if (m === null) return null;
return {
parts: [Number(m[1]), Number(m[2]), Number(m[3])],
prerelease: m[4] ?? null,
};
}
/** A parsed version, formatted the way the CLI reports it. */
export function formatClaudeVersion(version) {
if (version === null) return "(unreadable version)";
return (
version.parts.join(".") +
(version.prerelease ? `-${version.prerelease}` : "")
);
}
/**
* @param {number[]} a
* @param {number[]} b
* @returns {number} negative if a < b, 0 if equal, positive if a > b.
* Whether a parsed version IS the pin — same triple and no prerelease.
*
* @param {{ parts: number[], prerelease: string | null } | null} version
* @param {string} pin e.g. `PINNED_CLI_VERSION`.
*/
export function isPinnedVersion(version, pin) {
if (version === null || version.prerelease !== null) return false;
const pinned = parseClaudeVersion(pin);
return pinned !== null && compareVersions(version.parts, pinned.parts) === 0;
}
export function compareVersions(a, b) {
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const diff = (a[i] ?? 0) - (b[i] ?? 0);

View file

@ -14,6 +14,8 @@ import {
parseClaudeVersion,
compareVersions,
PINNED_CLI_VERSION,
isPinnedVersion,
formatClaudeVersion,
} from "./skill-manifest.mjs";
const fm = (body) => `---\n${body}\n---\n\n# Body\n`;
@ -203,23 +205,47 @@ test("validateEvalCases rejects malformed cases", () => {
});
test("parseClaudeVersion reads the CLI's version banner", () => {
assert.deepEqual(parseClaudeVersion("2.1.250 (Claude Code)"), [2, 1, 250]);
assert.deepEqual(parseClaudeVersion("claude 10.0.4\n"), [10, 0, 4]);
assert.deepEqual(parseClaudeVersion("2.1.250 (Claude Code)"), {
parts: [2, 1, 250],
prerelease: null,
});
assert.deepEqual(parseClaudeVersion("claude 10.0.4\n"), {
parts: [10, 0, 4],
prerelease: null,
});
assert.equal(parseClaudeVersion("no version here"), null);
});
test("compareVersions can decide exact equality with the pin", () => {
// Both validators require the EXACT pinned version, so what matters is that
// "equal" is distinguishable from newer as well as older — a floor would let a
// newer local CLI validate against a different schema than CI's.
const pin = parseClaudeVersion(PINNED_CLI_VERSION);
assert.equal(compareVersions(pin, pin), 0);
assert.ok(compareVersions([pin[0], pin[1], pin[2] - 1], pin) < 0);
assert.ok(compareVersions([pin[0], pin[1], pin[2] + 1], pin) > 0);
assert.ok(compareVersions([pin[0] + 1, 0, 0], pin) > 0);
// Shorter and longer triples still order sanely.
assert.ok(compareVersions([pin[0]], pin) < 0);
assert.equal(compareVersions([...pin, 0], pin), 0);
test("parseClaudeVersion keeps a prerelease suffix rather than dropping it", () => {
// Dropping it made `2.1.250-beta.1` compare EQUAL to the stable pin — a
// different validator schema running while the log claimed an exact match.
assert.deepEqual(parseClaudeVersion("2.1.250-beta.1 (Claude Code)"), {
parts: [2, 1, 250],
prerelease: "beta.1",
});
assert.equal(
formatClaudeVersion(parseClaudeVersion("2.1.250-beta.1")),
"2.1.250-beta.1",
);
assert.equal(formatClaudeVersion(null), "(unreadable version)");
});
test("isPinnedVersion accepts only the exact stable pin", () => {
const pin = PINNED_CLI_VERSION;
assert.equal(isPinnedVersion(parseClaudeVersion(pin), pin), true);
// Build metadata is ignored for precedence by SemVer, so it still matches.
assert.equal(
isPinnedVersion(parseClaudeVersion(`${pin}+build.7`), pin),
true,
);
// A prerelease of the same triple is NOT the pin.
assert.equal(
isPinnedVersion(parseClaudeVersion(`${pin}-beta.1`), pin),
false,
);
assert.equal(isPinnedVersion(parseClaudeVersion("2.1.249"), pin), false);
assert.equal(isPinnedVersion(parseClaudeVersion("99.0.0"), pin), false);
assert.equal(isPinnedVersion(null, pin), false);
});
test("the listing-entry cap covers description AND when_to_use together", () => {