fix(catalog): release-plugin.mjs stat-line pre-flight + no raw stacktrace on post-write fail
Q3e (order 20260913T051659Z-717911204-from-.claude), after Q3d. Live incident 13.09 (operator's own run): `release-plugin.mjs repo-mailbox --version 0.34.0 --create-tag --write --commit --push` tagged+pushed v0.34.0, wrote the catalog ref + README label, then crashed with a raw Node stacktrace because check-versions found the catalog's own stat line stale against the plugin's NEW badge (868 vs 927). Neither --commit nor --push of the catalog ran; the push token was correctly consumed (Q3c). Operator fixed the line and committed/pushed manually. D1 — the ordinary pre-flight (applyRelease -> check-versions) only ever inspects the OLD ref, so a stat-line drift the release itself is about to expose slipped straight through it into a pushed tag + a written, uncommitted catalog. New `preflightStatMismatches` compares the catalog's stat line against what the release is about to make current (the target ref's badge if that tag already exists, else the plugin's worktree README — exactly what --create-tag is about to tag), BEFORE any tag or write. Extracted the shared mismatch logic into check-versions.mjs as `statMismatchFindings` (pure refactor, classifyPlugin's own behavior unchanged) so both the post-hoc gate and this pre-release check use one rule. D2 — the post-write confirmation was a bare execFileSync, which throws on a non-zero exit. `reportPostWriteCheck` catches any failure (a real ERROR, or the subprocess dying) and reports exactly what is done (tag pushed y/n, files written) and what remains (commit/push), returning an exit code instead of an unhandled exception. No version bump, no tag, no push, no CLAUDE.md wording this session. Verification: - node --test scripts/release-plugin.test.mjs: 41/41 (Q3d) -> 50/50 (9 new: 3 preflightStatMismatches unit + 2 real-git D1 integration + 3 reportPostWriteCheck unit + 1 real-git D2 integration) - node --test scripts/*.test.mjs: 158/158 (Q3d) -> 167/167 - node scripts/check-versions.mjs: 0 ERROR (1 known WARN: claude-design, unrelated) - Mutation evidence (D1): commented out the new pre-flight call in runRelease -> exactly the new "D1 (Q3e, real git)" test went red (49 pass, 1 fail), all others stayed green; restored -> 50/50 again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
19908a88b7
commit
b5b0e2b88e
3 changed files with 359 additions and 19 deletions
|
|
@ -141,6 +141,30 @@ export function extractCatalogStats(readmeText, name) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// Compare a catalog's stated stat-line counts for one plugin against that plugin's own
|
||||
// stat badges — per-axis, badge-gated (see the comment block above extractStatBadges).
|
||||
// Pure, and shared by two callers with different timing: classifyPlugin (post-hoc, gates
|
||||
// a release that has already happened) and release-plugin.mjs's pre-release check
|
||||
// (Q3e/D1 — BEFORE anything is tagged or written, order 20260913T051659Z-717911204).
|
||||
export function statMismatchFindings(catalogStats, statBadges) {
|
||||
const findings = [];
|
||||
if (!catalogStats || !statBadges) return findings;
|
||||
for (const [axis, stated] of catalogStats) {
|
||||
if (!statBadges.has(axis)) continue;
|
||||
const badge = statBadges.get(axis);
|
||||
const approx = stated !== null && typeof stated === 'object';
|
||||
const bad = approx ? badge < stated.atLeast : badge !== stated;
|
||||
if (bad) {
|
||||
const shown = approx ? `${stated.atLeast}+` : String(stated);
|
||||
findings.push({
|
||||
level: 'ERROR',
|
||||
msg: `catalog says ${shown} ${axis} but the plugin's badge says ${badge} (catalog stat line is stale)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
// Pure classifier — all I/O is resolved into the input shape before this is called.
|
||||
// tags === null means "repo not inspected" (missing locally); [] means "no tags".
|
||||
export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, tags, catalogLabel = null, statBadges = null, catalogStats = null, homepage = null, homepageReachable = null }) {
|
||||
|
|
@ -184,19 +208,7 @@ export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, t
|
|||
// badge carries but the catalog does not restate is likewise nothing: the catalog
|
||||
// chooses what to show. Only a stated number contradicting a badged one is a defect.
|
||||
if (statBadges !== null && catalogStats !== null) {
|
||||
for (const [axis, stated] of catalogStats) {
|
||||
if (!statBadges.has(axis)) continue;
|
||||
const badge = statBadges.get(axis);
|
||||
const approx = stated !== null && typeof stated === 'object';
|
||||
const bad = approx ? badge < stated.atLeast : badge !== stated;
|
||||
if (bad) {
|
||||
const shown = approx ? `${stated.atLeast}+` : String(stated);
|
||||
findings.push({
|
||||
level: 'ERROR',
|
||||
msg: `catalog says ${shown} ${axis} but the plugin's badge says ${badge} (catalog stat line is stale)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
findings.push(...statMismatchFindings(catalogStats, statBadges));
|
||||
}
|
||||
|
||||
// 6. plugin.json homepage must resolve when present — the first link an agent follows
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue