feat(gate): buckets, traits, and the checks the brief calls load-bearing

Measured this build against a documentation brief for public repos. The
five original checks covered roughly one of its ten sections, so this
adds what a single repo can answer on its own.

New: required README headings per class (Non-goals is the cheapest
trust-builder there is), in-repo version consistency across manifest /
badge / CHANGELOG / tag, badge honesty, boilerplate, licence-claim,
and relative links. Findings now carry a BUCKET beside the level -
broken / missing / weakening - and output is grouped by it, because
that is the order the work gets done in.

Traits are a second axis beside class: class is structural and readable
off the catalog, a trait says what the code does. `security` attaches
SECURITY.md and a Known limitations section. The two names carrying it
are proposed, not measured - that list is the operator's.

Solo-maintained settles a category: CONTRIBUTING, CODE_OF_CONDUCT and
MAINTAINERS are required by no class. Consumer-facing documents are
untouched by that; SECURITY.md exists for the stranger who finds a hole.

Three bugs found by running against llm-security, not by reading:
- ~30 link findings, all noise. Regexes inside code spans are
  `[...](...)` to a naive scanner. Strip code first.
- `file:` and other schemes were treated as repo-relative paths.
- Relative links were resolved against the repo root instead of the
  file they sit in, calling two files missing that sat next to the
  README linking them.
Same fix applied to the boilerplate check: a document ABOUT placeholder
detection was tripping the placeholder detector.

Also removed this repo's own static tests badge. There is no CI - the
forge has zero Actions runners registered - so it could never become
real, and it is the exact anti-pattern the gate now flags.

67 tests. Against llm-security every remaining finding is real and
matches the census's independent hand-measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
This commit is contained in:
Kjell Tore Guttormsen 2026-07-27 16:06:33 +02:00
commit 720850a9ad
7 changed files with 915 additions and 39 deletions

View file

@ -32,6 +32,18 @@ const REGISTER_PATH = join(HERE, '..', 'register', 'repos.json');
const LEVELS = ['OK', 'SKIP', 'WARN', 'ERROR'];
// Findings carry a level AND a bucket, and the two are independent axes.
// The level says how sure and how loud; the bucket says what KIND of problem it
// is, which is what a reader triages on:
//
// broken works wrongly right now — a stranger is blocked or misled
// missing an expected artefact is simply absent
// weakening present and functional, but it reads as amateur
//
// A weakening finding can still be an ERROR: a README opening line that
// contradicts the published description blocks nobody, and is still wrong.
export const BUCKETS = ['broken', 'missing', 'weakening'];
// ------------------------------------------------------------ pure helpers
// Codepoints. Not bytes (an em-dash costs 3) and not UTF-16 units (`👉` costs 2).
@ -85,12 +97,14 @@ export function checkLinks({ files }, register) {
findings.push({
level: 'WARN',
code: 'LINK-NON-REPO',
bucket: 'weakening',
msg: `${path}:${ref.line}\`open/${ref.name}\` resolves to a known non-repo: ${register.non_repos[ref.name]}`,
});
} else {
findings.push({
level: 'ERROR',
code: 'LINK-DEAD',
bucket: 'broken',
msg: `${path}:${ref.line}\`open/${ref.name}\` matches no repo in the register (dead reference)`,
});
}
@ -105,9 +119,9 @@ export function checkDescription(description, register) {
}
const max = register.description_max_codepoints ?? 180;
const n = countCodepoints(description);
if (n === 0) return [{ level: 'ERROR', code: 'DESC-EMPTY', msg: 'forge description is empty' }];
if (n === 0) return [{ level: 'ERROR', code: 'DESC-EMPTY', bucket: 'missing', msg: 'forge description is empty' }];
if (n > max) {
return [{ level: 'ERROR', code: 'DESC-TOO-LONG', msg: `forge description is ${n} codepoints, bound is ${max}` }];
return [{ level: 'ERROR', code: 'DESC-TOO-LONG', bucket: 'weakening', msg: `forge description is ${n} codepoints, bound is ${max}` }];
}
return [{ level: 'OK', code: 'DESC', msg: `description ${n}/${max} codepoints` }];
}
@ -132,6 +146,7 @@ export function checkFirstScreen({ readme, name, description }) {
findings.push({
level: 'ERROR',
code: 'README-H1',
bucket: 'missing',
msg: `README must open with an H1 (expected \`# ${name}\`, found: ${heading === null ? '<empty file>' : `\`${heading}\``})`,
});
return findings;
@ -140,6 +155,7 @@ export function checkFirstScreen({ readme, name, description }) {
findings.push({
level: 'WARN',
code: 'README-H1',
bucket: 'weakening',
msg: `H1 is \`${heading}\`, not \`# ${name}\` — deliberate title, or drift? Operator's call.`,
});
} else {
@ -157,6 +173,7 @@ export function checkFirstScreen({ readme, name, description }) {
findings.push({
level: 'ERROR',
code: 'README-DESC',
bucket: 'weakening',
msg: `README opening line does not match the forge description\n README: ${opening}\n forge: ${description}`,
});
} else {
@ -193,6 +210,7 @@ export function checkInstallBlock({ readme, name, klass }, register) {
findings.push({
level: 'ERROR',
code: 'INSTALL-SSH',
bucket: 'broken',
msg: '`marketplace add` is shown with an ssh:// URL — it rejects those ("Invalid git URL"). Use the https form.',
});
}
@ -202,6 +220,7 @@ export function checkInstallBlock({ readme, name, klass }, register) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-MARKETPLACE',
bucket: 'broken',
msg: `no \`plugin marketplace add\` line — the reader is never told to add \`${mkt.name}\` (${mkt.url})`,
});
} else {
@ -220,6 +239,7 @@ export function checkInstallBlock({ readme, name, klass }, register) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-CLI',
bucket: 'broken',
msg: `no CLI install command for this repo — expected \`claude plugin install ${name}@${mkt.name}\` (or the \`/plugin install\` form). An \`enabledPlugins\` block is a welcome addition, but it is not a CLI command.`,
});
} else {
@ -231,6 +251,7 @@ export function checkInstallBlock({ readme, name, klass }, register) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
bucket: 'broken',
msg: 'shared asset shows a plugin install line — it is vendored into consumers, not installed. Document how to vendor it.',
});
}
@ -240,12 +261,14 @@ export function checkInstallBlock({ readme, name, klass }, register) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
bucket: 'broken',
msg: 'standalone project shows a plugin install line — use the pip/uv form.',
});
} else if (!/\b(pip\s+install|uv\s+(?:pip\s+)?(?:add|sync|install|run)|uvx)\b/.test(text)) {
findings.push({
level: 'WARN',
code: 'INSTALL-NO-PACKAGE-FORM',
bucket: 'missing',
msg: 'no pip/uv install form found — expected for a standalone project',
});
}
@ -256,13 +279,37 @@ export function checkInstallBlock({ readme, name, klass }, register) {
// Per class, never flat. A flat standard demands a CONTRIBUTING from a CSS
// library that takes no contributions and a ROADMAP from a five-line profile.
export function checkRequiredFiles({ present, klass }, register) {
const required = register.classes?.[klass]?.required_files ?? [];
// Requirements come from two axes. The CLASS is structural — it can be read off
// the catalog and the remotes. A TRAIT is about what the code does, which no
// remote can tell you: `security` attaches the obligations a tool acquires by
// handling untrusted input.
//
// Note what is NOT here: CONTRIBUTING, CODE_OF_CONDUCT, MAINTAINERS. The
// maintainer works alone and the published stance says so. Contributor-facing
// documentation for a project that accepts no contributors is theatre, and a
// code of conduct with an unattended placeholder address is worse than none.
// Consumer-facing documents are untouched by that — SECURITY.md exists for the
// outsider who finds a hole, and being solo does not remove them.
function requirementsFor(klass, traits, register) {
const cls = register.classes?.[klass] ?? {};
const files = [...(cls.required_files ?? [])];
const headings = [...(cls.required_headings ?? [])];
for (const t of traits ?? []) {
const tr = register.trait_requirements?.[t];
if (!tr) continue;
for (const f of tr.required_files ?? []) if (!files.includes(f)) files.push(f);
for (const h of tr.required_headings ?? []) if (!headings.includes(h)) headings.push(h);
}
return { files, headings };
}
export function checkRequiredFiles({ present, klass, traits }, register) {
const { files: required } = requirementsFor(klass, traits, register);
const have = new Set(present ?? []);
const findings = [];
for (const f of required) {
if (!have.has(f)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', msg: `missing required file for class \`${klass}\`: ${f}` });
findings.push({ level: 'ERROR', code: 'FILE-MISSING', bucket: 'missing', msg: `missing required file for class \`${klass}\`: ${f}` });
}
}
if (findings.length === 0 && required.length > 0) {
@ -271,6 +318,238 @@ export function checkRequiredFiles({ present, klass }, register) {
return findings;
}
// Fixed headings, because experienced readers skip rather than read. `## Install`
// on a predictable heading is what agents pattern-match on, and `## Non-goals`
// is the cheapest trust-builder there is: it proves someone thought about the
// boundary, and it stops misuse before it starts.
export function checkHeadings({ readme, klass, traits }, register) {
const { headings: required } = requirementsFor(klass, traits, register);
const text = String(readme ?? '');
const present = new Set(
text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('#')),
);
const findings = [];
for (const h of required) {
if ([...present].some((p) => p.toLowerCase() === h.toLowerCase())) continue;
// Same title, wrong depth: say that, rather than "missing". The contract
// wants a predictable top-level heading because that is what an agent
// pattern-matches on — but the section does exist, and the fix is a
// different edit than writing one from scratch.
const title = h.replace(/^#+\s*/, '');
const atOtherLevel = [...present].find(
(p) => p.replace(/^#+\s*/, '').toLowerCase() === title.toLowerCase(),
);
if (atOtherLevel) {
findings.push({
level: 'ERROR',
code: 'HEADING-LEVEL',
bucket: 'weakening',
msg: `README has \`${atOtherLevel}\` but the contract wants \`${h}\` — a predictable top-level heading is what readers and agents scan for`,
});
} else {
findings.push({ level: 'ERROR', code: 'HEADING-MISSING', bucket: 'missing', msg: `README has no \`${h}\` section` });
}
}
if (findings.length === 0 && required.length > 0) {
findings.push({ level: 'OK', code: 'HEADINGS', msg: `all ${required.length} required headings present` });
}
return findings;
}
// One version, four places it can be written down. This is the check that
// removes a whole defect class — "README says v0.3.1, the tag does not exist" —
// and the one that would have caught this repo's own 32→34 test-count drift.
export function checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }) {
const findings = [];
const v = pluginVersion ? String(pluginVersion).replace(/^v/, '') : null;
if (!v) {
return [{ level: 'SKIP', code: 'VERSION-UNAVAILABLE', msg: 'no package version found — nothing to compare against' }];
}
if (readmeBadge !== null && readmeBadge !== undefined && readmeBadge !== v) {
findings.push({ level: 'ERROR', code: 'VERSION-BADGE', bucket: 'weakening', msg: `README version badge is ${readmeBadge}, manifest says ${v}` });
}
if (changelogTop !== null && changelogTop !== undefined && changelogTop !== v) {
findings.push({ level: 'ERROR', code: 'VERSION-CHANGELOG', bucket: 'weakening', msg: `newest CHANGELOG entry is ${changelogTop}, manifest says ${v}` });
}
// Nothing released yet is a state, not a defect — and it must say so rather
// than pass quietly, because "SKIP is never a pass" is the whole discipline.
if (!tags || tags.length === 0) {
findings.push({ level: 'SKIP', code: 'VERSION-TAG', msg: `repo has no tags — cannot verify that v${v} was ever released` });
} else if (!tags.includes(`v${v}`)) {
findings.push({ level: 'ERROR', code: 'VERSION-TAG', bucket: 'broken', msg: `no tag \`v${v}\` — the documented version was never released (tags: ${tags.slice(-3).join(', ')})` });
}
if (findings.every((f) => f.level === 'OK' || f.level === 'SKIP')) {
findings.push({ level: 'OK', code: 'VERSION', msg: `version ${v} agrees across manifest, README and CHANGELOG` });
}
return findings;
}
// A static image asserting "tests: 642 passing" is a claim dressed as evidence.
// Version, licence and platform badges assert no run, so they are fine static.
const CLAIM_BADGE = /(tests?|build|ci|coverage|passing|status)/i;
export function checkBadges({ readme }) {
const findings = [];
for (const line of String(readme ?? '').split('\n')) {
for (const m of line.matchAll(/(\[)?!\[([^\]]*)\]\((https:\/\/img\.shields\.io\/badge\/[^)]+)\)(\])?/g)) {
const linked = m[1] === '[' && m[4] === ']';
const label = `${m[2]} ${m[3]}`;
if (!linked && CLAIM_BADGE.test(label)) {
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.`,
});
}
}
}
if (findings.length === 0) findings.push({ level: 'OK', code: 'BADGES', msg: 'no static badge asserts an unverified run' });
return findings;
}
// Template text that was never filled in. A visible unfinished template costs
// more trust than the missing document would have.
const BOILERPLATE = [
/your-project-name/i,
/\byour-org\b/i,
/\[INSERT[^\]]*\]/i,
/<your[- ][a-z]+>/i,
/TODO:\s*(fill|replace|update)/i,
/example@example\.(com|org)/i,
/FIXME/,
];
export function checkBoilerplate({ files }) {
const findings = [];
for (const [path, text] of Object.entries(files ?? {})) {
// Same discipline as the link check: code spans and fenced blocks are where
// a document ABOUT placeholders keeps its examples.
stripCode(text).split('\n').forEach((line, i) => {
for (const re of BOILERPLATE) {
if (re.test(line)) {
findings.push({
level: 'WARN',
code: 'BOILERPLATE',
bucket: 'weakening',
msg: `${path}:${i + 1} — unfilled template text: \`${line.trim().slice(0, 70)}\``,
});
return;
}
}
});
}
if (findings.length === 0) findings.push({ level: 'OK', code: 'BOILERPLATE', msg: 'no unfilled template text found' });
return findings;
}
// "LICENSE mentioned in the README, no file in the repo" is its own anti-signal:
// the claim is load-bearing for anyone deciding whether they may use this.
export function checkLicenseClaim({ readme, present }) {
const text = String(readme ?? '');
const claims = /\bLICEN[SC]E\b/i.test(text) || /\b(MIT|Apache|BSD|GPL)\b.{0,20}licen[sc]e/i.test(text);
const have = (present ?? []).some((f) => /^LICEN[SC]E(\.\w+)?$/i.test(f));
if (claims && !have) {
return [{
level: 'ERROR',
code: 'LICENSE-CLAIMED-ABSENT',
bucket: 'broken',
msg: 'README cites a licence but the repo has no LICENSE file — the claim a reader relies on to use this is unbacked',
}];
}
return [{ level: 'OK', code: 'LICENSE-CLAIM', msg: have ? 'LICENSE present' : 'no licence claim to back' }];
}
// Blank out fenced blocks and inline code spans, keeping line numbers intact.
// Documentation about regexes is full of strings that ARE markdown links to a
// naive scanner: `["']([A-Za-z0-9\-._]{16,64})["']` is `[...](...)` exactly.
// Running the first version against a real repo produced ~30 findings and every
// one of them was noise.
export function stripCode(text) {
let fenced = false;
return String(text ?? '')
.split('\n')
.map((line) => {
if (/^\s*(```|~~~)/.test(line)) {
fenced = !fenced;
return '';
}
if (fenced) return '';
if (/^(\s{4,}|\t)\S/.test(line)) return ''; // indented code block
return line.replace(/`[^`]*`/g, '');
})
.join('\n');
}
// A relative link resolves against the file it sits in, not against the repo
// root. Getting this wrong called two files missing that were right there next
// to the README linking them — and it would have done so in every nested doc.
// Returns null when the path escapes the repo, which is unresolvable from
// inside one repo rather than broken.
export function resolveRelative(fromFile, target) {
if (target.startsWith('/')) return null;
const baseParts = String(fromFile).split('/').slice(0, -1);
const out = [...baseParts];
for (const part of target.split('/')) {
if (part === '' || part === '.') continue;
if (part === '..') {
if (out.length === 0) return null;
out.pop();
} else {
out.push(part);
}
}
return out.join('/');
}
// Relative file links only. Anchor resolution depends on per-renderer heading
// slug rules and is a rabbit hole; external URLs need the network. Both are
// deliberately out — a check that is sometimes wrong teaches people to ignore it.
export function checkInternalLinks({ files, present }) {
const have = new Set(present ?? []);
const findings = [];
for (const [path, text] of Object.entries(files ?? {})) {
stripCode(text).split('\n').forEach((line, i) => {
for (const m of line.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
const target = m[1];
// Any scheme at all, not just http — `file:`, `vscode:`, `ftp:` are all
// somebody else's to resolve.
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || /^[#<]/.test(target)) continue;
const clean = target.split('#')[0];
if (!clean) continue;
const resolved = resolveRelative(path, clean);
// A path that leaves the repo cannot be judged from inside it — a
// plugin README pointing up at its marketplace is the ordinary case.
if (resolved === null) {
findings.push({
level: 'SKIP',
code: 'LINK-OUTSIDE-REPO',
msg: `${path}:${i + 1}\`${clean}\` points outside this repo; the gate sees one repo and cannot resolve it`,
});
continue;
}
if (!have.has(resolved)) {
findings.push({
level: 'ERROR',
code: 'LINK-INTERNAL-MISSING',
bucket: 'broken',
msg: `${path}:${i + 1} — link points at \`${clean}\` (${resolved}), which is not a tracked file`,
});
}
}
});
}
if (!findings.some((f) => f.level === 'ERROR')) {
findings.push({ level: 'OK', code: 'LINKS-INTERNAL', msg: 'every resolvable relative link resolves' });
}
return findings;
}
export function levelOf(findings) {
let worst = 'OK';
for (const f of findings ?? []) {
@ -279,13 +558,26 @@ export function levelOf(findings) {
return worst;
}
export function classifyRepo({ name, files, present, description }, register) {
export function bucketsOf(findings) {
const out = { broken: 0, missing: 0, weakening: 0 };
for (const f of findings ?? []) {
if (f.bucket && out[f.bucket] !== undefined) out[f.bucket] += 1;
}
return out;
}
export function classifyRepo(
{ name, files, present, description, pluginVersion, readmeBadge, changelogTop, tags },
register,
) {
const klass = register.repos?.[name];
if (!klass) {
return {
name,
klass: null,
traits: [],
status: 'SKIP',
buckets: { broken: 0, missing: 0, weakening: 0 },
findings: [{
level: 'SKIP',
code: 'REPO-UNREGISTERED',
@ -294,16 +586,23 @@ export function classifyRepo({ name, files, present, description }, register) {
};
}
const traits = register.traits?.[name] ?? [];
const readme = (files ?? {})['README.md'] ?? '';
const findings = [
...checkFirstScreen({ readme, name, description }),
...checkInstallBlock({ readme, name, klass }, register),
...checkRequiredFiles({ present, klass }, register),
...checkHeadings({ readme, klass, traits }, register),
...checkRequiredFiles({ present, klass, traits }, register),
...checkLinks({ files }, register),
...checkInternalLinks({ files, present }),
...checkLicenseClaim({ readme, present }),
...checkBadges({ readme }),
...checkBoilerplate({ files }),
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),
...checkDescription(description, register),
];
return { name, klass, status: levelOf(findings), findings };
return { name, klass, traits, status: levelOf(findings), buckets: bucketsOf(findings), findings };
}
// ---------------------------------------------------------------- I/O shell
@ -341,6 +640,49 @@ function repoNameFrom(dir) {
}
}
// The version the package itself claims, from whichever manifest this class uses.
function readPackageVersion(dir) {
for (const p of ['.claude-plugin/plugin.json', 'package.json', 'pyproject.toml']) {
const full = join(dir, p);
if (!existsSync(full)) continue;
try {
const raw = readFileSync(full, 'utf8');
if (p.endsWith('.json')) {
const v = JSON.parse(raw).version;
if (v) return String(v);
} else {
const m = /^\s*version\s*=\s*["']([^"']+)["']/m.exec(raw);
if (m) return m[1];
}
} catch { /* unparseable — try the next one */ }
}
return null;
}
export function extractBadgeVersion(readmeText) {
const m = /badge\/version-(\d+\.\d+\.\d+)/.exec(readmeText || '');
return m ? m[1] : null;
}
// Newest released version in the CHANGELOG. `## [Unreleased]` is skipped by
// design — it is not a claim that anything shipped.
export function extractChangelogTop(changelogText) {
for (const line of String(changelogText || '').split('\n')) {
const m = /^##\s*\[?v?(\d+\.\d+\.\d+)\]?/.exec(line.trim());
if (m) return m[1];
}
return null;
}
function gitTags(dir) {
try {
return execFileSync('git', ['-C', dir, 'tag', '--list', 'v*'], { encoding: 'utf8' })
.split('\n').map((s) => s.trim()).filter(Boolean);
} catch {
return [];
}
}
export function inspectRepo(dir, name, register, description) {
const tracked = gitFiles(dir);
const present = (tracked ?? []).filter((f) => existsSync(join(dir, f)));
@ -354,19 +696,53 @@ export function inspectRepo(dir, name, register, description) {
if (!files['README.md'] && existsSync(join(dir, 'README.md'))) {
files['README.md'] = readFileSync(join(dir, 'README.md'), 'utf8');
}
return classifyRepo({ name, files, present, description }, register);
const readme = files['README.md'] ?? '';
let changelog = null;
try { changelog = readFileSync(join(dir, 'CHANGELOG.md'), 'utf8'); } catch { /* absent */ }
return classifyRepo({
name,
files,
present,
description,
pluginVersion: readPackageVersion(dir),
readmeBadge: extractBadgeVersion(readme),
changelogTop: changelog === null ? null : extractChangelogTop(changelog),
tags: gitTags(dir),
}, register);
}
// Grouped by bucket, because that is the order the findings actually get acted
// on: what blocks a stranger today, then what is absent, then what merely reads
// badly. Severity within a bucket is secondary to that.
const BUCKET_TITLE = {
broken: 'BROKEN NOW — a stranger is blocked or misled',
missing: 'MISSING — an expected artefact is absent',
weakening: 'WEAKENING — present, but it reads as amateur',
};
function render(result) {
const mark = { OK: '✓', WARN: '!', ERROR: '✗', SKIP: '·' };
const klass = result.klass ? ` [${result.klass}]` : '';
console.log(`\n${mark[result.status]} ${result.name}${klass}${result.status}`);
for (const f of result.findings) {
if (f.level === 'OK') continue;
console.log(` ${mark[f.level]} ${f.level} ${f.code}: ${f.msg}`);
const traits = result.traits?.length ? ` {${result.traits.join(', ')}}` : '';
console.log(`\n${mark[result.status]} ${result.name}${klass}${traits}${result.status}`);
for (const bucket of BUCKETS) {
const inBucket = result.findings.filter((f) => f.bucket === bucket);
if (!inBucket.length) continue;
console.log(`\n ${BUCKET_TITLE[bucket]}`);
for (const f of inBucket) console.log(` ${mark[f.level]} ${f.level} ${f.code}: ${f.msg}`);
}
const skipped = result.findings.filter((f) => f.level === 'SKIP');
if (skipped.length) {
console.log('\n NOT CHECKED — these are not passes');
for (const f of skipped) console.log(` ${mark.SKIP} ${f.code}: ${f.msg}`);
}
const okCount = result.findings.filter((f) => f.level === 'OK').length;
if (okCount) console.log(` ${mark.OK} ${okCount} check(s) passed`);
console.log(`\n ${mark.OK} ${okCount} check(s) passed`);
}
async function refresh(register) {

View file

@ -17,6 +17,13 @@ import {
checkFirstScreen,
checkInstallBlock,
checkRequiredFiles,
checkVersionConsistency,
checkHeadings,
checkBadges,
checkBoilerplate,
checkLicenseClaim,
checkInternalLinks,
resolveRelative,
classifyRepo,
levelOf,
} from './repo-standard-check.mjs';
@ -45,15 +52,27 @@ const REGISTER = {
classes: {
plugin: {
required_files: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
required_headings: ['## Install', '## Non-goals', '## Changelog'],
install: 'plugin',
},
catalog: {
required_files: ['README.md', 'LICENSE', 'GOVERNANCE.md', 'CONVENTIONS.md', '.claude-plugin/marketplace.json'],
required_headings: ['## Install', '## Non-goals'],
install: 'catalog',
},
'shared-asset': { required_files: ['README.md', 'LICENSE'], install: 'vendor' },
'org-profile': { required_files: ['README.md'], install: 'none' },
standalone: { required_files: ['README.md', 'LICENSE'], install: 'package' },
'shared-asset': { required_files: ['README.md', 'LICENSE'], required_headings: ['## Non-goals'], install: 'vendor' },
'org-profile': { required_files: ['README.md'], required_headings: [], install: 'none' },
standalone: { required_files: ['README.md', 'LICENSE'], required_headings: ['## Install', '## Non-goals'], install: 'package' },
},
traits: {
'llm-security': ['security'],
'llm-ingestion-pipeline-security': ['security'],
},
trait_requirements: {
security: {
required_files: ['SECURITY.md'],
required_headings: ['## Known limitations'],
},
},
description_max_codepoints: 180,
};
@ -367,9 +386,17 @@ test('a fully compliant plugin repo classifies OK', () => {
'',
'Body text.',
'',
'![Version](https://img.shields.io/badge/version-0.7.0-blue)',
'',
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install repo-mailbox@${MKT.name}`,
'',
'## Non-goals',
'It is not a state store.',
'',
'## Changelog',
'See [CHANGELOG.md](CHANGELOG.md).',
].join('\n');
const r = classifyRepo(
{
@ -377,8 +404,293 @@ test('a fully compliant plugin repo classifies OK', () => {
files: { 'README.md': readme },
present: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
description: 'A local mailbox for coordination.',
pluginVersion: '0.7.0',
readmeBadge: '0.7.0',
changelogTop: '0.7.0',
tags: ['v0.7.0'],
},
REGISTER,
);
assert.equal(r.status, 'OK');
assert.deepEqual(r.buckets, { broken: 0, missing: 0, weakening: 0 });
});
test('the security trait travels from the register into the verdict', () => {
const r = classifyRepo(
{
name: 'llm-security',
files: { 'README.md': '# llm-security\ndesc\n## Install\n## Non-goals\n## Changelog\n' },
present: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
description: 'desc',
},
REGISTER,
);
assert.deepEqual(r.traits, ['security']);
assert.equal(r.findings.some((f) => f.code === 'FILE-MISSING' && f.msg.includes('SECURITY.md')), true);
assert.equal(r.findings.some((f) => f.code === 'HEADING-MISSING' && f.msg.includes('Known limitations')), true);
});
// =========================================================================
// The brief's reporting contract: findings carry a BUCKET alongside a level.
// "broken now" (strangers are blocked or misled), "missing" (an expected
// artefact is absent), "weakening" (present, but reads as amateur). The two
// axes are independent: a weakening finding can still be an ERROR.
// =========================================================================
test('every ERROR and WARN finding carries a bucket', () => {
const readme = ['## Install', `"enabledPlugins": { "okr@${MKT.name}": true }`].join('\n');
const all = [
...checkInstallBlock({ readme, name: 'okr', klass: 'plugin' }, REGISTER),
...checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/nonesuch' } }, REGISTER),
...checkRequiredFiles({ present: ['README.md'], klass: 'plugin' }, REGISTER),
...checkDescription('', REGISTER),
];
const graded = all.filter((f) => f.level === 'ERROR' || f.level === 'WARN');
assert.ok(graded.length > 0);
for (const f of graded) {
assert.ok(['broken', 'missing', 'weakening'].includes(f.bucket), `${f.code} has bucket ${f.bucket}`);
}
});
test('an unusable install path is `broken`, an absent file is `missing`', () => {
const readme = ['## Install', 'nothing useful here'].join('\n');
const inst = checkInstallBlock({ readme, name: 'okr', klass: 'plugin' }, REGISTER);
assert.equal(inst.find((f) => f.code === 'INSTALL-NO-CLI').bucket, 'broken');
const files = checkRequiredFiles({ present: ['README.md'], klass: 'plugin' }, REGISTER);
assert.equal(files.find((f) => f.code === 'FILE-MISSING').bucket, 'missing');
});
// ------------------------------------------------------- version consistency
test('version consistency: manifest, README badge and CHANGELOG must agree', () => {
const ok = checkVersionConsistency({ pluginVersion: '0.1.0', readmeBadge: '0.1.0', changelogTop: '0.1.0', tags: ['v0.1.0'] });
assert.equal(ok.filter((f) => f.level === 'ERROR').length, 0);
const drift = checkVersionConsistency({ pluginVersion: '0.1.0', readmeBadge: '0.0.9', changelogTop: '0.1.0', tags: ['v0.1.0'] });
assert.equal(drift.some((f) => f.level === 'ERROR' && f.code === 'VERSION-BADGE'), true);
const stale = checkVersionConsistency({ pluginVersion: '0.2.0', readmeBadge: '0.2.0', changelogTop: '0.1.0', tags: ['v0.1.0', 'v0.2.0'] });
assert.equal(stale.some((f) => f.level === 'ERROR' && f.code === 'VERSION-CHANGELOG'), true);
});
test('an untagged repo SKIPs the tag comparison rather than failing it', () => {
// Nothing has been released yet. That is a state, not a defect — and "SKIP is
// never a pass" means it must say so rather than quietly succeed.
const f = checkVersionConsistency({ pluginVersion: '0.1.0', readmeBadge: '0.1.0', changelogTop: '0.1.0', tags: [] });
assert.equal(f.some((x) => x.code === 'VERSION-TAG' && x.level === 'SKIP'), true);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
});
test('a released version with no matching tag is an ERROR', () => {
const f = checkVersionConsistency({ pluginVersion: '0.3.1', readmeBadge: '0.3.1', changelogTop: '0.3.1', tags: ['v0.1.0'] });
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'VERSION-TAG'), true);
});
// -------------------------------------------------------- required headings
test('required headings are per class — Non-goals is required, not optional', () => {
const full = '# x\n## Install\n## Non-goals\n## Changelog\n';
assert.equal(checkHeadings({ readme: full, klass: 'plugin' }, REGISTER).filter((f) => f.level === 'ERROR').length, 0);
const noNonGoals = '# x\n## Install\n## Changelog\n';
const f = checkHeadings({ readme: noNonGoals, klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.code === 'HEADING-MISSING' && x.msg.includes('Non-goals')), true);
assert.equal(f.find((x) => x.code === 'HEADING-MISSING').bucket, 'missing');
});
test('org-profile requires no headings at all', () => {
const f = checkHeadings({ readme: '# .profile\nprofile\n', klass: 'org-profile' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ------------------------------------------------------------ badge honesty
test('a static badge asserting test or build status is a finding', () => {
// "tests: 642 passing" as a static image is a claim dressed as evidence.
const f = checkBadges({ readme: '![Tests](https://img.shields.io/badge/tests-34-green)' });
assert.equal(f.some((x) => x.code === 'BADGE-STATIC-CLAIM'), true);
assert.equal(f.find((x) => x.code === 'BADGE-STATIC-CLAIM').bucket, 'weakening');
});
test('static version, licence and platform badges are fine — they assert no run', () => {
const readme = [
'![Version](https://img.shields.io/badge/version-0.1.0-blue)',
'![License](https://img.shields.io/badge/license-MIT-lightgrey)',
'![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)',
].join('\n');
assert.equal(checkBadges({ readme }).filter((f) => f.level !== 'OK').length, 0);
});
test('a build badge that links to a real run is fine', () => {
const readme = '[![CI](https://forge.example/repo/badges/workflows/ci.yml/badge.svg)](https://forge.example/repo/actions)';
assert.equal(checkBadges({ readme }).filter((f) => f.level !== 'OK').length, 0);
});
// -------------------------------------------------------------- boilerplate
test('unfinished template text is a finding', () => {
const f = checkBoilerplate({ files: { 'README.md': 'Install your-project-name today' } });
assert.equal(f.some((x) => x.code === 'BOILERPLATE'), true);
const g = checkBoilerplate({ files: { 'CODE_OF_CONDUCT.md': 'Report to [INSERT EMAIL ADDRESS]' } });
assert.equal(g.some((x) => x.code === 'BOILERPLATE'), true);
});
test('ordinary prose is not boilerplate', () => {
const f = checkBoilerplate({ files: { 'README.md': 'This project solves a real problem.' } });
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
// --------------------------------------------------- licence claim vs. file
test('a README that cites a LICENSE the repo does not have is broken', () => {
const f = checkLicenseClaim({ readme: 'Released under the [MIT licence](LICENSE).', present: ['README.md'] });
assert.equal(f.some((x) => x.code === 'LICENSE-CLAIMED-ABSENT' && x.bucket === 'broken'), true);
});
test('a cited LICENSE that exists passes', () => {
const f = checkLicenseClaim({ readme: 'See [LICENSE](LICENSE).', present: ['README.md', 'LICENSE'] });
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ------------------------------------------------------------ internal links
test('a relative link to a file that does not exist is a finding', () => {
const f = checkInternalLinks(
{ files: { 'README.md': 'See [the design](docs/design.md).' }, present: ['README.md'] },
);
assert.equal(f.some((x) => x.code === 'LINK-INTERNAL-MISSING'), true);
});
test('relative links that resolve, anchors and external URLs are left alone', () => {
const f = checkInternalLinks({
files: { 'README.md': '[a](docs/design.md) [b](#section) [c](https://example.com) [d](mailto:x@y.z)' },
present: ['README.md', 'docs/design.md'],
});
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
// ----------------------------------------------------------------- traits
test('the security trait adds a SECURITY.md requirement on top of the class', () => {
const f = checkRequiredFiles({ present: ['README.md', 'LICENSE'], klass: 'standalone', traits: ['security'] }, REGISTER);
assert.equal(f.some((x) => x.code === 'FILE-MISSING' && x.msg.includes('SECURITY.md')), true);
});
test('a repo without the security trait owes no SECURITY.md', () => {
const f = checkRequiredFiles({ present: ['README.md', 'LICENSE'], klass: 'standalone' }, REGISTER);
assert.equal(f.some((x) => x.msg.includes('SECURITY.md')), false);
});
test('the security trait requires limitations to be stated', () => {
const f = checkHeadings({ readme: '# x\n## Install\n## Non-goals\n', klass: 'standalone', traits: ['security'] }, REGISTER);
assert.equal(f.some((x) => x.code === 'HEADING-MISSING' && x.msg.includes('Known limitations')), true);
});
test('CONTRIBUTING and CODE_OF_CONDUCT are required by no class — the maintainer works alone', () => {
for (const klass of Object.keys(REGISTER.classes)) {
const req = REGISTER.classes[klass].required_files;
assert.equal(req.includes('CONTRIBUTING.md'), false);
assert.equal(req.includes('CODE_OF_CONDUCT.md'), false);
assert.equal(req.includes('MAINTAINERS.md'), false);
}
});
// ---------------------------------------------- link check: the noise sources
// All three found by running against llm-security, which produced ~30 false
// positives on the first pass. A check that is wrong this often teaches people
// to ignore it, which is worse than not having it.
test('a regex inside an inline code span is not a markdown link', () => {
// `["']([A-Za-z0-9\-._]{16,64})["']` is `[...](...)` to a naive scanner.
const line = '- **Regex:** `(?i)\\bapi[_\\-]?key\\s*[:=]\\s*["\']([A-Za-z0-9\\-._]{16,64})["\']`';
const f = checkInternalLinks({ files: { 'k.md': line }, present: ['k.md'] });
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
test('fenced code blocks are not scanned for links', () => {
const text = ['```bash', 'curl [x](not-a-real-file.md)', '```'].join('\n');
const f = checkInternalLinks({ files: { 'r.md': text }, present: ['r.md'] });
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
test('any URI scheme is left alone, not just http', () => {
const text = '[a](file:///abs/path.html) [b](ftp://x/y) [c](vscode://z)';
const f = checkInternalLinks({ files: { 'r.md': text }, present: ['r.md'] });
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
test('a path that escapes the repo is unresolvable, not broken', () => {
// Legitimately common: a plugin README pointing up at its marketplace.
// The gate sees one repo, so it cannot judge — and must not pretend to.
const f = checkInternalLinks({ files: { 'README.md': '[d](../../README.md)' }, present: ['README.md'] });
assert.equal(f.some((x) => x.code === 'LINK-OUTSIDE-REPO' && x.level === 'SKIP'), true);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
});
test('a genuinely missing sibling file is still an ERROR', () => {
const f = checkInternalLinks({ files: { 'README.md': '[e](docs/gone.md)' }, present: ['README.md'] });
assert.equal(f.some((x) => x.code === 'LINK-INTERNAL-MISSING' && x.level === 'ERROR'), true);
});
test('a required heading present at the wrong level says so', () => {
// llm-security has `### Install` nested under `## Quick Start`. The contract
// wants it at level 2 — the finding should name that, not just "missing".
const f = checkHeadings({ readme: '# x\n## Quick Start\n### Install\n## Non-goals\n## Changelog\n', klass: 'plugin' }, REGISTER);
const hit = f.find((x) => x.code === 'HEADING-LEVEL');
assert.ok(hit, 'expected a HEADING-LEVEL finding');
assert.match(hit.msg, /### Install/);
});
test('a document ABOUT placeholders does not trip the placeholder detector', () => {
// llm-security documents the very strings it scans for. Inside code spans and
// fenced blocks they are subject matter, not unfinished template text.
const text = [
'Skip if the matched value contains: `your-project-name`, `FIXME`, `changeme`.',
'',
'```bash',
'cd <your-fork>',
'```',
].join('\n');
const f = checkBoilerplate({ files: { 'k.md': text } });
assert.equal(f.filter((x) => x.level !== 'OK').length, 0);
});
test('placeholder text in ordinary prose is still caught', () => {
const f = checkBoilerplate({ files: { 'README.md': 'Install your-project-name to begin.' } });
assert.equal(f.some((x) => x.code === 'BOILERPLATE'), true);
});
// --------------------------------- relative links resolve against their file
test('resolveRelative joins against the containing file, not the repo root', () => {
assert.equal(resolveRelative('examples/demo/README.md', 'expected.md'), 'examples/demo/expected.md');
assert.equal(resolveRelative('README.md', 'docs/design.md'), 'docs/design.md');
assert.equal(resolveRelative('docs/a/b.md', '../c.md'), 'docs/c.md');
assert.equal(resolveRelative('docs/a.md', './b.md'), 'docs/b.md');
});
test('resolveRelative returns null when the path escapes the repo', () => {
assert.equal(resolveRelative('README.md', '../../README.md'), null);
assert.equal(resolveRelative('docs/a.md', '../../../x.md'), null);
assert.equal(resolveRelative('README.md', '/etc/passwd'), null);
});
test('a link from a nested README to its sibling resolves', () => {
// Found against llm-security: both files existed, and the gate called them
// missing because it compared a file-relative target to repo-root paths.
const f = checkInternalLinks({
files: { 'examples/demo/README.md': 'See [findings](expected-findings.md).' },
present: ['examples/demo/README.md', 'examples/demo/expected-findings.md'],
});
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('a nested link to a genuinely absent sibling is still an ERROR', () => {
const f = checkInternalLinks({
files: { 'examples/demo/README.md': 'See [gone](gone.md).' },
present: ['examples/demo/README.md'],
});
assert.equal(f.some((x) => x.code === 'LINK-INTERNAL-MISSING'), true);
});