fix(check-versions): mirror stats against the pinned ref, not the working tree [skip-docs]

Found live minutes after shipping fc95cbd: llm-security went ERROR on scanners
(23 vs 22) and tests (2013 vs 2034). The catalog was RIGHT and the gate was
wrong. llm-security had committed past its v7.8.3 tag without bumping the
version, and the gate was reading the sibling working tree — but the catalog
documents what INSTALLS, and `ref: v7.8.3` still installs 23/2013.

Stat badges are now read with `git show <ref>:README.md`, falling back to the
working tree only when the ref cannot be read (a ref resolving to nothing is
already its own ERROR, so the fallback cannot hide a dangling ref). The
version-badge check is unchanged and still reads the working tree: that one is
about the plugin's internal consistency, not about what the catalog promises.

This also corrects a stat I got wrong in fc95cbd. I had moved config-audit from
1410 to 1441 tests off the working tree; at the pinned v5.13.0 the badge says
1398. 1441 is unreleased. The catalog now says 1398 — what installs.

[skip-docs]: CLAUDE.md carries the rule and the "check `git show <ref>:README.md`
before believing the working tree" instruction; README.md changes by one number
because the gate was wrong about it.

Tests 117 -> 120 (+3, all regression). Gate green at 11 OK / 0 WARN / 0 ERROR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDSiMcgLMpEETwtkc86Nym
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 21:26:27 +02:00
commit 780f86ec9f
4 changed files with 63 additions and 6 deletions

View file

@ -47,3 +47,9 @@ their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
(repo-mailbox's read `6 CLI scripts` / `251 selftest checks` against a true 8 / 374). `N+` in the
catalog is read as a lower bound, not an equality. **Never hand-edit a stat line to silence the
gate** — the plugin's badge is the source for every stat number; fix the catalog to match it.
- **The stat mirror reads the plugin README AT THE PINNED `ref`, never the sibling working tree.**
The catalog documents what *installs*, and that is the tag. A plugin that commits past its tag
without bumping its version — measured 2026-08-02 on both llm-security (scanners 23→22, tests
2013→2034) and config-audit (tests 1398→1441) — would otherwise make the gate demand that the
catalog restate unreleased numbers, which is exactly backwards. **When the gate flags a stat,
check `git show <ref>:README.md` in the plugin repo before believing the working tree.**

View file

@ -47,7 +47,7 @@ Configuration intelligence for Claude Code. Claude reads instructions from 7+ fi
Key commands: `/config-audit posture`, `/config-audit feature-gap`, `/config-audit fix`, `/config-audit whats-active`, `/config-audit tokens`
7 agents · 16 scanners · 21 commands · 1441 tests · [Full documentation →](https://git.fromaitochitta.com/open/config-audit)
7 agents · 16 scanners · 21 commands · 1398 tests · [Full documentation →](https://git.fromaitochitta.com/open/config-audit)
---

View file

@ -96,6 +96,19 @@ function readStatSegment(segment, out) {
}
}
// Which copy of the plugin README the stat mirror must read.
//
// The catalog documents what INSTALLS, and what installs is the pinned `ref` — not
// whatever the sibling working tree happens to hold. Measured live 2026-08-02:
// llm-security had committed past its v7.8.3 tag (scanners 23->22, tests 2013->2034)
// without bumping the version, so reading the working tree reported the catalog stale
// while the catalog was correct about v7.8.3. Fall back to the working tree only when
// the ref cannot be read at all — a ref that resolves to nothing is already its own
// ERROR (check 1), so this fallback never hides a dangling ref.
export function pickStatSource({ atRef, atWorktree }) {
return atRef !== null && atRef !== undefined ? atRef : (atWorktree ?? null);
}
// Catalog README -> Map(axis -> count | {atLeast}). Reads ONLY the stat line of the
// named plugin's block, so prose numbers above it can never be mistaken for counts.
export function extractCatalogStats(readmeText, name) {
@ -175,6 +188,17 @@ export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, t
return { name, status, findings };
}
// Read one file as it exists AT a git ref, without touching the working tree.
// null when the ref or the path does not resolve there.
function gitShow(repoDir, ref, path) {
if (!ref) return null;
try {
return execFileSync('git', ['-C', repoDir, 'show', `${ref}:${path}`], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
} catch {
return null;
}
}
function gitTags(repoDir) {
try {
const out = execFileSync('git', ['-C', repoDir, 'tag', '--list', 'v*'], { encoding: 'utf8' });
@ -200,14 +224,18 @@ export function inspectPlugin(catalogDir, plugin) {
pluginVersion = pj.version ?? null;
} catch { /* leave null → flagged */ }
let readmeBadge = null;
let statBadges = null;
let worktreeReadme = null;
try {
const pluginReadme = readFileSync(join(repoDir, 'README.md'), 'utf8');
readmeBadge = extractBadgeVersion(pluginReadme);
statBadges = extractStatBadges(pluginReadme);
worktreeReadme = readFileSync(join(repoDir, 'README.md'), 'utf8');
} catch { /* no README → badge + stat checks skipped */ }
// Version-badge check 2 is about the working tree's INTERNAL consistency, so it keeps
// reading the working tree. The stat mirror is about what the catalog promises users,
// so it reads the pinned ref instead — see pickStatSource.
const readmeBadge = worktreeReadme === null ? null : extractBadgeVersion(worktreeReadme);
const statSource = pickStatSource({ atRef: gitShow(repoDir, catalogRef, 'README.md'), atWorktree: worktreeReadme });
const statBadges = statSource === null ? null : extractStatBadges(statSource);
let catalogLabel = null;
let catalogStats = null;
try {

View file

@ -9,6 +9,7 @@ import {
extractCatalogLabel,
classifyPlugin,
extractStatBadges,
pickStatSource,
extractCatalogStats,
} from './check-versions.mjs';
@ -269,3 +270,25 @@ test('stat maps omitted (legacy callers) → mirroring skipped entirely', () =>
});
assert.equal(r.status, 'OK');
});
// Regression, found live 2026-08-02: llm-security had committed past its v7.8.3 tag
// (scanners 23->22, tests 2013->2034) WITHOUT bumping the version. The gate read the
// sibling's working tree and reported the catalog stale — but the catalog documents
// what INSTALLS, and `ref: v7.8.3` still installs 23/2013. The catalog was right and
// the gate was wrong. Stat badges must therefore be read at the pinned ref.
test('pickStatSource prefers the README at the pinned ref over the working tree', () => {
const atRef = 'https://img.shields.io/badge/scanners-23-cyan';
const atWorktree = 'https://img.shields.io/badge/scanners-22-cyan';
assert.equal(pickStatSource({ atRef, atWorktree }), atRef);
assert.equal(extractStatBadges(pickStatSource({ atRef, atWorktree })).get('scanner'), 23);
});
test('pickStatSource falls back to the working tree only when the ref cannot be read', () => {
const atWorktree = 'https://img.shields.io/badge/scanners-22-cyan';
assert.equal(pickStatSource({ atRef: null, atWorktree }), atWorktree);
assert.equal(pickStatSource({ atRef: null, atWorktree: null }), null);
});
test('an empty README at the ref is a real read, not a failed one', () => {
assert.equal(pickStatSource({ atRef: '', atWorktree: 'badge/scanners-22-cyan' }), '');
});