Skip to content

Commit 9dc5642

Browse files
dmealingclaude
andcommitted
fix(gates): an RC tag would have hijacked the metamodel-version baseline
`check-metamodel-version.mjs` diffs `expected-registry.json` against the last release tag, chosen with `git tag --list 'v[0-6].*' --sort=-version:refname`. git's version sort has no notion of a semver pre-release unless `versionsort.suffix` is configured, and this repo configures none — measured in a scratch repo, the descending order of four tags is: v1.0.1, v1.0.0-rc.5, v1.0.0, v0.25.0 So `v1.0.0-rc.5` sorts ABOVE the release it is a candidate for. The repo has carried no pre-release tag until now, which is the only reason this has never bitten; cutting a tagged RC introduces one, and from the 1.0 cut until the next tag the gate would diff the vocabulary against a CANDIDATE rather than against what adopters resolve. The baseline is now the last STABLE tag. The exclusion is derived from the tag string — a semver pre-release is exactly "a `-` after the version core" — not a list of RC spellings, so any future pre-release scheme is covered by construction. `pickBaselineTag` is extracted and exported so the gate's own test drives it without git. Six cases, fed git's real output order; three go red with the filter removed. The first case is deliberately the post-cut list `[v1.0.0-rc.5, v1.0.0, v0.25.0]` and not one led by a higher stable tag, which would have passed either way and proved nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent ecda2fa commit 9dc5642

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

scripts/check-metamodel-version.mjs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,28 @@ function diffChildren(baseKids, curKids, where, breaking, additive) {
276276
const git = (...args) =>
277277
execFileSync("git", args, { cwd: REPO, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
278278

279-
/** The highest published release tag on the npm/PyPI/NuGet line (`v0.*` / `v1.*` …).
280-
* The four registries cut together, so any one line dates the metamodel baseline. */
281-
function lastReleaseTag() {
282-
const tags = git("tag", "--list", "v[0-6].*", "--sort=-version:refname")
283-
.split("\n")
279+
/** The highest published STABLE release tag on the npm/PyPI/NuGet line (`v0.*` / `v1.*` …).
280+
* The four registries cut together, so any one line dates the metamodel baseline.
281+
*
282+
* Pre-release tags (`v1.0.0-rc.5`) are excluded, and the exclusion is load-bearing:
283+
* `--sort=version:refname` has no notion of a semver pre-release without
284+
* `versionsort.suffix` configured, so it sorts `v1.0.0-rc.5` ABOVE `v1.0.0` — an RC
285+
* tag would keep winning the baseline after the release it was a candidate for
286+
* shipped, and this gate would then diff the vocabulary against a candidate rather
287+
* than against what adopters actually resolve. Derived from the tag string (a semver
288+
* pre-release is exactly "a `-` after the version core"), never a list of RC names. */
289+
export function pickBaselineTag(tagLines) {
290+
const tags = tagLines
284291
.map((t) => t.trim())
285-
.filter(Boolean);
292+
.filter((t) => t && !t.includes("-"));
286293
return tags[0] ?? null;
287294
}
288295

296+
function lastReleaseTag() {
297+
// git already sorted these descending; pickBaselineTag only drops pre-releases.
298+
return pickBaselineTag(git("tag", "--list", "v[0-6].*", "--sort=-version:refname").split("\n"));
299+
}
300+
289301
/**
290302
* Read the baseline manifest, distinguishing "genuinely not there yet" (skip) from
291303
* "something went wrong" (fail). The previous form swallowed EVERY error into `null`,

scripts/test-metamodel-version.mjs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
// node scripts/test-metamodel-version.mjs
1212

13-
import { classify, requiredBump, satisfies, parseVersion } from "./check-metamodel-version.mjs";
13+
import { classify, requiredBump, satisfies, parseVersion, pickBaselineTag } from "./check-metamodel-version.mjs";
1414

1515
let fails = 0;
1616
const ok = (m) => console.log(`ok: ${m}`);
@@ -335,5 +335,36 @@ else ok("2.0 → 1.9 is a REGRESSION across majors and must not satisfy");
335335
if (!sat("minor", "1.9", "2.0")) bad("1.9 → 2.0 moves forward across a major and satisfies a minor");
336336
else ok("1.9 → 2.0 moves forward across a major and satisfies a minor");
337337

338+
// ---------------------------------------------------------------------------
339+
// Baseline tag selection — the gate diffs against the last STABLE release.
340+
// ---------------------------------------------------------------------------
341+
// `git tag --sort=version:refname` has no notion of a semver pre-release unless
342+
// `versionsort.suffix` is configured, so it sorts `v1.0.0-rc.5` ABOVE `v1.0.0`.
343+
// Measured, not assumed: with four tags in a scratch repo the descending order is
344+
// v1.0.1, v1.0.0-rc.5, v1.0.0, v0.25.0. An RC tag left in the candidate set would
345+
// therefore keep winning the baseline after the release it was a candidate FOR had
346+
// shipped, and this gate would diff the vocabulary against a candidate instead of
347+
// against what adopters resolve. These feed pickBaselineTag git's own output order.
348+
const stable = (label, lines, expect) => {
349+
const got = pickBaselineTag(lines);
350+
if (got !== expect) bad(`${label}: expected ${expect}, got ${got}`);
351+
else ok(label);
352+
};
353+
stable(
354+
"an RC tag sorted above its own release does NOT become the baseline",
355+
// exactly git's descending output once v1.0.0 is cut alongside v1.0.0-rc.5
356+
["v1.0.0-rc.5", "v1.0.0", "v0.25.0"],
357+
"v1.0.0",
358+
);
359+
stable(
360+
"with only an RC above it, the last STABLE tag is the baseline",
361+
["v1.0.0-rc.5", "v0.25.0", "v0.24.5"],
362+
"v0.25.0",
363+
);
364+
stable("a stable-only list is unchanged", ["v0.25.0", "v0.24.5"], "v0.25.0");
365+
stable("blank lines from git's trailing newline are ignored", ["", "v0.25.0", ""], "v0.25.0");
366+
stable("an all-pre-release list yields no baseline", ["v1.0.0-rc.5", "v1.0.0-rc.4"], null);
367+
stable("an empty list yields no baseline", [], null);
368+
338369
console.log(fails === 0 ? "\nmetamodel-version classifier: all checks passed" : `\n${fails} failure(s)`);
339370
process.exit(fails === 0 ? 0 : 1);

0 commit comments

Comments
 (0)