fix(engine): close the linked-badge blind spot, de-tautologize 2 tests

Two remaining minor findings from STATE.md:

checkBadges treated being wrapped in a markdown link as proof of a
real run and skipped BADGE-STATIC-CLAIM entirely — but nothing ever
checked the link actually resolved. The link-target capture group was
missing from the regex outright. Now a linked run-claim badge with a
relative target is resolved against `present` (new BADGE-DEAD-LINK,
ERROR/broken): a dead link is worse than a static badge because it
looks verified. External targets (the ordinary case — a CI provider)
still need the network and stay out of scope, same precedent as
checkInternalLinks.

The "four pre-existing tautological tests" note undercounted on
re-measurement: only two exist ('no class requires a ROADMAP', 'CONTRIBUTING
and CODE_OF_CONDUCT are required by no class'). Both read
`required_files` off the test file's OWN local REGISTER fixture, so
they could only ever check the fixture against itself — a typo in the
real register/repos.json would drift past them silently. Switched
both to `loadRegister()` and verified the fix is real: temporarily
added ROADMAP.md to a class in the live register and confirmed the
test goes red, then restored it clean.

113 -> 116 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
This commit is contained in:
Kjell Tore Guttormsen 2026-08-04 22:20:36 +02:00
commit aefbadf9a8
2 changed files with 69 additions and 9 deletions

View file

@ -462,23 +462,46 @@ const BADGE_URL = /shields\.io|badgen\.net|\/badges?[/.]/i;
// cannot carry a hard limit.
const BADGE_INFLECTION = 5;
export function checkBadges({ readme }) {
export function checkBadges({ readme, present }) {
const have = new Set(present ?? []);
const findings = [];
let badgeCount = 0;
for (const line of String(readme ?? '').split('\n')) {
// Any image, any host. Restricting this to img.shields.io would have missed
// a self-hosted SVG asserting exactly the same unverified thing.
for (const m of line.matchAll(/(\[)?!\[([^\]]*)\]\(([^)\s]+)\)(\])?/g)) {
const linked = m[1] === '[' && m[4] === ']';
// The trailing `(?:\]\(target\))?` is the LINK the badge is wrapped in —
// previously unmatched, so being linked at all silently ended scrutiny
// whether or not the link actually went anywhere.
for (const m of line.matchAll(/(\[)?!\[([^\]]*)\]\(([^)\s]+)\)(?:\]\(([^)\s]+)\))?/g)) {
const linkTarget = m[1] === '[' ? m[4] : undefined;
const linked = linkTarget !== undefined;
const label = `${m[2]} ${m[3]}`;
if (BADGE_URL.test(m[3])) badgeCount++;
if (!linked && CLAIM_BADGE.test(label)) {
if (!CLAIM_BADGE.test(label)) continue;
if (!linked) {
findings.push({
level: 'WARN',
code: 'BADGE-STATIC-CLAIM',
bucket: 'weakening',
msg: `static badge asserts a run that nothing verifies: \`${m[2]}\`. A badge like this is a claim dressed as evidence — link it to a real run, or drop it.`,
});
continue;
}
// A linked badge is only as honest as its target. An external target
// (the ordinary case — a CI provider's own page) needs the network to
// verify and is deliberately out of scope, same as checkInternalLinks.
// A relative target this gate CAN check without the network — and a
// relative target that resolves nowhere is worse than a static badge:
// it LOOKS verified.
if (/^[a-z][a-z0-9+.-]*:/i.test(linkTarget)) continue;
const resolved = resolveRelative('README.md', linkTarget.split('#')[0]);
if (resolved !== null && !have.has(resolved)) {
findings.push({
level: 'ERROR',
code: 'BADGE-DEAD-LINK',
bucket: 'broken',
msg: `${m[2]} badge links to \`${linkTarget}\`, which does not resolve — a linked badge pointing nowhere is a claim dressed as evidence, worse than a static one because it looks verified.`,
});
}
}
}
@ -800,7 +823,7 @@ export function classifyRepo(
...checkLinks({ files }, register),
...checkInternalLinks({ files, present }),
...checkLicenseClaim({ readme, present }),
...checkBadges({ readme }),
...checkBadges({ readme, present }),
...checkReadmeLanguage({ readme, name }, register),
...checkBoilerplate({ files }),
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),