feat(repo-standard): v0.1.0 - per-repo gate for the open/ standard

Five checks a single repository can answer on its own: README first
screen, install block, files required by its class, open/<name>
references, description length. Pure classifiers with I/O resolved into
their input, mirroring check-versions.mjs; ERROR/WARN/SKIP/OK, exit 1
on ERROR. 32 tests.

The reference check has THREE outcomes: "matches no repo" (ERROR) is
separate from "matches a known non-repo" (WARN). Sharing an outcome
would let real dead links hide inside correct text. Only names in URL
position count, and .git is normalised first - without that a raw scan
turns 3 dead names into ~20.

enabledPlugins is treated as a legitimate second install form; what the
gate requires in addition is a CLI command. The JSON form is never
reported as the defect.

STATE.md is gitignored from this first commit - public remote.
No hook yet: a blocking gate must first be precise enough not to fail a
correct repository.

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 09:10:46 +02:00
commit 816ba97c63
11 changed files with 1319 additions and 0 deletions

View file

@ -0,0 +1,409 @@
#!/usr/bin/env node
// repo-standard — the per-repo gate.
//
// Checks ONE repository against the standard for its class:
// - README first screen: H1 is the repo name, next line IS the forge description
// - Install block complete, in the form its class actually uses
// - Required files present for its class
// - Every `open/<name>` reference in URL position resolves
// - Description within the length bound, measured in codepoints
//
// What it deliberately does NOT do: anything that needs to see all repos at once.
// Divergence across the org (0/18 topics, three competing install forms, README
// release notes duplicating a CHANGELOG that 16 of 18 repos have) is invisible
// from inside one repo. Those checks live in org-ops, not here.
//
// Structure mirrors the marketplace's check-versions.mjs on purpose: pure
// classifiers with all I/O resolved into their input, findings tagged
// ERROR/WARN/SKIP/OK, exit 1 on ERROR. This is a gate, not a checklist —
// the catalog's eleven descriptions are good because a gate runs on them; the
// forge's nine were empty. Same care, different outcome.
//
// Usage:
// node scripts/repo-standard-check.mjs [--dir <path>] [--name <repo>] [--offline] [--json]
// node scripts/repo-standard-check.mjs --refresh # register vs. live org listing
import { readFileSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const REGISTER_PATH = join(HERE, '..', 'register', 'repos.json');
const LEVELS = ['OK', 'SKIP', 'WARN', 'ERROR'];
// ------------------------------------------------------------ pure helpers
// Codepoints. Not bytes (an em-dash costs 3) and not UTF-16 units (`👉` costs 2).
// The em-dash exposes only the outer layer, which is why "characters, not bytes"
// was not enough on its own.
export function countCodepoints(s) {
return [...String(s ?? '')].length;
}
// ~20 "dead" repo names collapsed to 3 real ones once this ran. A clone URL
// ending in .git is a legitimate reference, not a broken one.
export function normalizeRepoRef(raw) {
return String(raw ?? '')
.replace(/\/+$/, '')
.replace(/\.git$/, '');
}
// Only names in URL position are resolvable references. That single rule
// excludes all three of the measured "correct text that looks broken" cases at
// once: a path position (`~/.claude/coord/_broadcast/`), running prose (`coord`
// is still the transport protocol's name), and a bare directory name.
const URL_REF = /(?::\/\/[^\s)\]"'`]*\/|@[^\s:]+:)open\/([A-Za-z0-9._-]+)/g;
export function extractOpenRefs(text) {
const out = [];
const lines = String(text ?? '').split('\n');
lines.forEach((line, i) => {
for (const m of line.matchAll(URL_REF)) {
out.push({ name: normalizeRepoRef(m[1]), line: i + 1, raw: m[0] });
}
});
return out;
}
// Three outcomes, never two. "No match" and "match on something that is not a
// repo" must stay distinguishable — if they share an outcome, the loss goes
// silent, and silent loss is the defect class this standard exists to catch.
export function classifyRef(name, register) {
if (Object.prototype.hasOwnProperty.call(register.repos ?? {}, name)) return 'repo';
if (Object.prototype.hasOwnProperty.call(register.non_repos ?? {}, name)) return 'non-repo';
return 'unknown';
}
export function checkLinks({ files }, register) {
const findings = [];
for (const [path, text] of Object.entries(files ?? {})) {
for (const ref of extractOpenRefs(text)) {
const kind = classifyRef(ref.name, register);
if (kind === 'repo') continue;
if (kind === 'non-repo') {
findings.push({
level: 'WARN',
code: 'LINK-NON-REPO',
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',
msg: `${path}:${ref.line}\`open/${ref.name}\` matches no repo in the register (dead reference)`,
});
}
}
}
return findings;
}
export function checkDescription(description, register) {
if (description === null || description === undefined) {
return [{ level: 'SKIP', code: 'DESC-UNAVAILABLE', msg: 'forge description not available — check not run (offline, or the listing failed)' }];
}
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 > max) {
return [{ level: 'ERROR', code: 'DESC-TOO-LONG', msg: `forge description is ${n} codepoints, bound is ${max}` }];
}
return [{ level: 'OK', code: 'DESC', msg: `description ${n}/${max} codepoints` }];
}
// The opening line makes description == catalog == README: the same thread on a
// third surface, and the only one of the three a machine can check from inside
// the repo.
export function checkFirstScreen({ readme, name, description }) {
const findings = [];
const lines = String(readme ?? '').split('\n');
const firstIdx = lines.findIndex((l) => l.trim() !== '');
if (firstIdx === -1 || lines[firstIdx].trim() !== `# ${name}`) {
findings.push({
level: 'ERROR',
code: 'README-H1',
msg: `README line 1 must be \`# ${name}\` (found: ${firstIdx === -1 ? '<empty file>' : `\`${lines[firstIdx].trim()}\``})`,
});
return findings;
}
findings.push({ level: 'OK', code: 'README-H1', msg: `H1 is \`# ${name}\`` });
if (description === null || description === undefined) {
findings.push({ level: 'SKIP', code: 'README-DESC', msg: 'forge description not available — opening-line match not checked' });
return findings;
}
const restIdx = lines.findIndex((l, i) => i > firstIdx && l.trim() !== '');
const opening = restIdx === -1 ? '' : lines[restIdx].trim();
if (opening !== String(description).trim()) {
findings.push({
level: 'ERROR',
code: 'README-DESC',
msg: `README opening line does not match the forge description\n README: ${opening}\n forge: ${description}`,
});
} else {
findings.push({ level: 'OK', code: 'README-DESC', msg: 'opening line matches the forge description' });
}
return findings;
}
// `claude plugin install x@mkt` or `/plugin install x@mkt` — the two CLI forms.
function hasCliInstall(readme, name, mkt) {
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?:claude\\s+plugin|/plugin)\\s+install\\s+${esc(name)}@${esc(mkt)}\\b`).test(readme);
}
function hasAnyPluginInstall(readme) {
return /(?:claude\s+plugin|\/plugin)\s+install\s+\S+@\S+/.test(readme);
}
export function checkInstallBlock({ readme, name, klass }, register) {
const form = register.classes?.[klass]?.install ?? 'none';
const text = String(readme ?? '');
const mkt = register.marketplace ?? {};
const findings = [];
if (form === 'none') return findings;
const addLines = text.split('\n').filter((l) => /plugin\s+marketplace\s+add/.test(l));
const hasAdd = addLines.length > 0;
// The forge UI's clone button hands out the ssh URL, and `marketplace add`
// answers it with "Invalid git URL" — a message that never mentions the
// protocol. Measured end-to-end 2026-07-25.
if (addLines.some((l) => /ssh:\/\//.test(l))) {
findings.push({
level: 'ERROR',
code: 'INSTALL-SSH',
msg: '`marketplace add` is shown with an ssh:// URL — it rejects those ("Invalid git URL"). Use the https form.',
});
}
if (form === 'plugin' || form === 'catalog') {
if (!hasAdd) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-MARKETPLACE',
msg: `no \`plugin marketplace add\` line — the reader is never told to add \`${mkt.name}\` (${mkt.url})`,
});
} else {
findings.push({ level: 'OK', code: 'INSTALL-MARKETPLACE', msg: '`marketplace add` present' });
}
}
if (form === 'plugin') {
// The corrected defect A. `enabledPlugins` in settings.json is a LEGITIMATE
// second form and it stands in 10 of 11 plugin READMEs — what is missing in
// 7 of them is a CLI command. So the contract requires the command and
// permits the JSON alongside it; it never accepts the JSON as a substitute.
// A reader who scrolls to the JSON block has a complete path; an agent told
// "install this" reaches for the CLI and finds `marketplace add` and nothing else.
if (!hasCliInstall(text, name, mkt.name)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-CLI',
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 {
findings.push({ level: 'OK', code: 'INSTALL-CLI', msg: `CLI install command names ${name}@${mkt.name}` });
}
}
if (form === 'vendor' && hasAnyPluginInstall(text)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
msg: 'shared asset shows a plugin install line — it is vendored into consumers, not installed. Document how to vendor it.',
});
}
if (form === 'package') {
if (hasAnyPluginInstall(text)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
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',
msg: 'no pip/uv install form found — expected for a standalone project',
});
}
}
return findings;
}
// 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 ?? [];
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}` });
}
}
if (findings.length === 0 && required.length > 0) {
findings.push({ level: 'OK', code: 'FILES', msg: `all ${required.length} required files present` });
}
return findings;
}
export function levelOf(findings) {
let worst = 'OK';
for (const f of findings ?? []) {
if (LEVELS.indexOf(f.level) > LEVELS.indexOf(worst)) worst = f.level;
}
return worst;
}
export function classifyRepo({ name, files, present, description }, register) {
const klass = register.repos?.[name];
if (!klass) {
return {
name,
klass: null,
status: 'SKIP',
findings: [{
level: 'SKIP',
code: 'REPO-UNREGISTERED',
msg: `\`${name}\` is not in the register — class unknown, so no class-specific rule can be applied. Add it to register/repos.json (or run --refresh).`,
}],
};
}
const readme = (files ?? {})['README.md'] ?? '';
const findings = [
...checkFirstScreen({ readme, name, description }),
...checkInstallBlock({ readme, name, klass }, register),
...checkRequiredFiles({ present, klass }, register),
...checkLinks({ files }, register),
...checkDescription(description, register),
];
return { name, klass, status: levelOf(findings), findings };
}
// ---------------------------------------------------------------- I/O shell
export function loadRegister(path = REGISTER_PATH) {
return JSON.parse(readFileSync(path, 'utf8'));
}
// ONE call. The org listing already carries description and topics; fetching
// per repo trips the rate limiter (HTTP 429). Reads anonymously — verified —
// so this works for any reader, not only for someone holding a token.
async function fetchOrgListing(register) {
const url = `${register.forge}/api/v1/orgs/${register.org}/repos?limit=50`;
const res = await fetch(url, { headers: { accept: 'application/json' } });
if (!res.ok) throw new Error(`org listing returned HTTP ${res.status}`);
return res.json();
}
function gitFiles(dir) {
try {
return execFileSync('git', ['-C', dir, 'ls-files'], { encoding: 'utf8' })
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
} catch {
return null;
}
}
function repoNameFrom(dir) {
try {
return basename(execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim());
} catch {
return basename(dir);
}
}
export function inspectRepo(dir, name, register, description) {
const tracked = gitFiles(dir);
const present = (tracked ?? []).filter((f) => existsSync(join(dir, f)));
// Link scanning covers every tracked Markdown file — a dead reference in a
// doc is as broken as one in the README.
const files = {};
for (const f of (tracked ?? []).filter((p) => p.endsWith('.md'))) {
try { files[f] = readFileSync(join(dir, f), 'utf8'); } catch { /* unreadable — skip */ }
}
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);
}
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 okCount = result.findings.filter((f) => f.level === 'OK').length;
if (okCount) console.log(` ${mark.OK} ${okCount} check(s) passed`);
}
async function refresh(register) {
const live = await fetchOrgListing(register);
const liveNames = new Set(live.map((r) => r.name));
const known = new Set(Object.keys(register.repos ?? {}));
const added = [...liveNames].filter((n) => !known.has(n)).sort();
const gone = [...known].filter((n) => !liveNames.has(n)).sort();
console.log(`register: ${known.size} repos · forge: ${liveNames.size} repos`);
if (added.length) console.log(`\n on the forge, not in the register (add with a class):\n ${added.join('\n ')}`);
if (gone.length) console.log(`\n in the register, not on the forge:\n ${gone.join('\n ')}`);
if (!added.length && !gone.length) console.log('\n ✓ register matches the forge');
return added.length + gone.length === 0 ? 0 : 1;
}
async function main(argv) {
const arg = (flag, fallback = null) => {
const i = argv.indexOf(flag);
return i === -1 ? fallback : argv[i + 1];
};
const register = loadRegister();
if (argv.includes('--refresh')) {
process.exit(await refresh(register));
}
const dir = arg('--dir', process.cwd());
const name = arg('--name', repoNameFrom(dir));
let description = null;
if (!argv.includes('--offline')) {
try {
const listing = await fetchOrgListing(register);
const row = listing.find((r) => r.name === name);
description = row ? (row.description ?? '') : null;
} catch {
// Unreachable forge leaves description null, which reads as SKIP — never
// as a pass. A check that could not run says so.
}
}
const result = inspectRepo(dir, name, register, description);
if (argv.includes('--json')) {
console.log(JSON.stringify(result, null, 2));
} else {
render(result);
}
process.exit(result.status === 'ERROR' ? 1 : 0);
}
if (process.argv[1] && process.argv[1].endsWith('repo-standard-check.mjs')) {
main(process.argv.slice(2));
}

View file

@ -0,0 +1,370 @@
// Tests for the repo-standard gate.
//
// The pure classifiers are the unit under test — the I/O shell (inspectRepo/runGate)
// is exercised against a live checkout by the CLI, not here.
//
// The link-check fixtures are the six measured false positives from the census.
// They are the reason this gate has three outcomes instead of a boolean.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
countCodepoints,
normalizeRepoRef,
extractOpenRefs,
classifyRef,
checkLinks,
checkDescription,
checkFirstScreen,
checkInstallBlock,
checkRequiredFiles,
classifyRepo,
levelOf,
} from './repo-standard-check.mjs';
const REGISTER = {
org: 'open',
marketplace: {
name: 'ktg-plugin-marketplace',
url: 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git',
},
repos: {
'llm-security': 'plugin',
'repo-mailbox': 'plugin',
'repo-standard': 'plugin',
'ktg-plugin-marketplace': 'catalog',
'playground-design-system': 'shared-asset',
'.profile': 'org-profile',
'llm-ingestion-pipeline-security': 'standalone',
},
non_repos: {
coord: 'Retired repo name, deliberately still alive in prose: the CLI, the mailbox root and CLAUDE_COORD_DIR kept it — they are the transport protocol, not the product.',
_broadcast: 'reserved engine namespace',
'llm-ingestion-guard': 'package name, not a repo',
'claude-code-llm-security': 'pre-split name of llm-security',
},
classes: {
plugin: {
required_files: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
install: 'plugin',
},
catalog: {
required_files: ['README.md', 'LICENSE', 'GOVERNANCE.md', 'CONVENTIONS.md', '.claude-plugin/marketplace.json'],
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' },
},
description_max_codepoints: 180,
};
// ---------------------------------------------------------------- measurement
test('countCodepoints measures codepoints, not bytes and not UTF-16 units', () => {
// The em-dash exposes only the byte layer: 3 bytes, 1 codepoint, 1 UTF-16 unit.
assert.equal(countCodepoints('a—b'), 3);
assert.equal(Buffer.byteLength('a—b', 'utf8'), 5);
// 👉 is astral: 1 codepoint but 2 UTF-16 units. This is the layer the em-dash hides.
assert.equal(countCodepoints('👉'), 1);
assert.equal('👉'.length, 2);
});
// ------------------------------------------------------------- ref extraction
test('normalizeRepoRef strips a .git suffix and a trailing slash', () => {
// ~20 "dead" names collapsed to 3 real ones once .git was normalised.
assert.equal(normalizeRepoRef('llm-security.git'), 'llm-security');
assert.equal(normalizeRepoRef('llm-security/'), 'llm-security');
assert.equal(normalizeRepoRef('llm-security'), 'llm-security');
});
test('extractOpenRefs finds names in URL position only', () => {
const text = [
'clone https://git.fromaitochitta.com/open/llm-security.git today',
'see https://git.fromaitochitta.com/open/repo-mailbox/src/branch/main/README.md',
].join('\n');
const names = extractOpenRefs(text).map((r) => r.name);
assert.deepEqual(names, ['llm-security', 'repo-mailbox']);
});
test('extractOpenRefs ignores path position, prose and bare directory names', () => {
// False positives #4, #5, #6 — the text is correct and will STAY correct.
const text = [
'the mailbox root is ~/.claude/coord/_broadcast/inbox/',
'coord is the transport protocol, not the product',
'the catalog lives in ktg-plugin-marketplace/catalog',
'the package llm-ingestion-guard is published from that repo',
].join('\n');
assert.deepEqual(extractOpenRefs(text), []);
});
test('extractOpenRefs handles the ssh scp-style form', () => {
const refs = extractOpenRefs('git@git.fromaitochitta.com:open/llm-security.git');
assert.deepEqual(refs.map((r) => r.name), ['llm-security']);
});
test('extractOpenRefs reports the 1-indexed line of each hit', () => {
const text = 'line one\nline two\nhttps://git.fromaitochitta.com/open/llm-security';
assert.equal(extractOpenRefs(text)[0].line, 3);
});
// ---------------------------------------------- three outcomes, not a boolean
test('classifyRef separates repo, non-repo and unknown', () => {
assert.equal(classifyRef('llm-security', REGISTER), 'repo');
assert.equal(classifyRef('.profile', REGISTER), 'repo');
assert.equal(classifyRef('coord', REGISTER), 'non-repo');
assert.equal(classifyRef('nonesuch', REGISTER), 'unknown');
});
test('a URL-position ref to a known non-repo is a DISTINCT outcome from no match', () => {
// The specification requirement: "no match" and "match on something that is
// not a repo" must never share an outcome, or the loss goes silent.
const bad = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/nonesuch' } }, REGISTER);
const odd = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/coord' } }, REGISTER);
assert.equal(bad[0].level, 'ERROR');
assert.equal(bad[0].code, 'LINK-DEAD');
assert.equal(odd[0].level, 'WARN');
assert.equal(odd[0].code, 'LINK-NON-REPO');
assert.notEqual(bad[0].code, odd[0].code);
// The reason travels with the finding, so the reader is not sent measuring again.
assert.match(odd[0].msg, /transport protocol/);
});
test('a dead ref names its successor when the register knows one', () => {
const f = checkLinks(
{ files: { 'README.md': 'https://git.fromaitochitta.com/open/claude-code-llm-security' } },
REGISTER,
);
assert.equal(f[0].level, 'WARN');
assert.match(f[0].msg, /pre-split name/);
});
test('the .git suffix does not manufacture a dead reference', () => {
const f = checkLinks(
{ files: { 'README.md': 'https://git.fromaitochitta.com/open/llm-security.git' } },
REGISTER,
);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the hidden .profile resolves — the enumerator sees what a glob misses', () => {
const f = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/.profile' } }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ------------------------------------------------------------- description
test('description length is bounded, measured in codepoints', () => {
assert.equal(checkDescription('a fine description', REGISTER)[0].level, 'OK');
assert.equal(checkDescription('', REGISTER)[0].level, 'ERROR');
assert.equal(checkDescription('x'.repeat(181), REGISTER)[0].level, 'ERROR');
assert.equal(checkDescription('x'.repeat(180), REGISTER)[0].level, 'OK');
});
test('an unavailable description is SKIP, never a pass', () => {
// Offline is not compliance. A check that could not run says so.
const f = checkDescription(null, REGISTER);
assert.equal(f[0].level, 'SKIP');
});
// ------------------------------------------------------------- first screen
test('README line 1 must be the H1, and the description line must match the forge', () => {
const readme = '# repo-mailbox\nA local mailbox for coordination.\n';
assert.equal(
checkFirstScreen({ readme, name: 'repo-mailbox', description: 'A local mailbox for coordination.' })
.every((f) => f.level === 'OK'),
true,
);
});
test('a README whose opening line diverges from the description is an ERROR', () => {
const readme = '# repo-mailbox\nSomething else entirely.\n';
const f = checkFirstScreen({ readme, name: 'repo-mailbox', description: 'A local mailbox for coordination.' });
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'README-DESC'), true);
});
test('first-screen description match is SKIP when the forge text is unavailable', () => {
const f = checkFirstScreen({ readme: '# x\nbody\n', name: 'x', description: null });
assert.equal(f.some((x) => x.code === 'README-DESC' && x.level === 'SKIP'), true);
});
test('a wrong or missing H1 is an ERROR', () => {
const f = checkFirstScreen({ readme: 'no heading here\n', name: 'x', description: null });
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'README-H1'), true);
});
// ------------------------------------------------------------ install block
const MKT = REGISTER.marketplace;
test('plugin install needs BOTH lines: marketplace add and a CLI install command', () => {
const readme = [
'## Install',
'```',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install repo-mailbox@${MKT.name}`,
'```',
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-mailbox', klass: 'plugin' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the slash form counts as the CLI command', () => {
const readme = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`/plugin install claude-design@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'claude-design', klass: 'plugin' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('enabledPlugins JSON is an ALLOWED ADDITION, never a replacement for the CLI command', () => {
// This is the corrected defect A: 7 of 11 plugin READMEs stop after
// `marketplace add`. The JSON form works and stands in 10 of 11 — what is
// missing is a CLI command, so the contract requires the command and permits
// the JSON alongside it.
const jsonOnly = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
'Or enable directly in `~/.claude/settings.json`:',
`"enabledPlugins": { "llm-security@${MKT.name}": true }`,
].join('\n');
const f = checkInstallBlock({ readme: jsonOnly, name: 'llm-security', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-NO-CLI'), true);
const both = jsonOnly + `\nclaude plugin install llm-security@${MKT.name}\n`;
const g = checkInstallBlock({ readme: both, name: 'llm-security', klass: 'plugin' }, REGISTER);
assert.equal(g.filter((x) => x.level === 'ERROR').length, 0);
});
test('a missing marketplace add is its own finding — okr and claude-design are opposite halves', () => {
const noAdd = ['## Install', `/plugin install claude-design@${MKT.name}`].join('\n');
const f = checkInstallBlock({ readme: noAdd, name: 'claude-design', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-NO-MARKETPLACE'), true);
// okr: neither line. Both findings fire — a rule saying "both lines" must not
// hit this repo blind.
const neither = ['## Install', `"enabledPlugins": { "okr@${MKT.name}": true }`].join('\n');
const g = checkInstallBlock({ readme: neither, name: 'okr', klass: 'plugin' }, REGISTER);
assert.equal(g.some((x) => x.code === 'INSTALL-NO-MARKETPLACE'), true);
assert.equal(g.some((x) => x.code === 'INSTALL-NO-CLI'), true);
});
test('the install target must name THIS repo, not another plugin', () => {
const readme = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install some-other-plugin@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-standard', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.code === 'INSTALL-NO-CLI'), true);
});
test('ssh in the marketplace add line is an ERROR — marketplace add rejects it', () => {
// Measured end-to-end: `marketplace add ssh://...` → "Invalid git URL".
// The forge UI's clone button hands you exactly that URL.
const readme = [
'## Install',
'claude plugin marketplace add ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git',
`claude plugin install repo-standard@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-standard', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-SSH'), true);
});
test('the install block is parametric — a different marketplace passes on its own values', () => {
// wiki-advise lives on the private ktg/ namespace and is distributed via
// `ktg-privat`. A skill that hardcodes the public marketplace produces an
// install line that does not work there — and being public itself, this skill
// cannot carry private marketplace names.
const priv = {
...REGISTER,
marketplace: { name: 'ktg-privat', url: 'https://git.fromaitochitta.com/ktg/ktg-privat.git' },
};
const readme = [
'## Install',
'claude plugin marketplace add https://git.fromaitochitta.com/ktg/ktg-privat.git',
'claude plugin install wiki-advise@ktg-privat',
].join('\n');
const f = checkInstallBlock({ readme, name: 'wiki-advise', klass: 'plugin' }, priv);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the catalog needs only marketplace add — it IS the marketplace', () => {
const readme = `## Install\nclaude plugin marketplace add ${MKT.url}`;
const f = checkInstallBlock({ readme, name: 'ktg-plugin-marketplace', klass: 'catalog' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('a shared asset is vendored, not installed — a plugin install line is wrong there', () => {
const asPlugin = `## Install\nclaude plugin install playground-design-system@${MKT.name}`;
const f = checkInstallBlock(
{ readme: asPlugin, name: 'playground-design-system', klass: 'shared-asset' },
REGISTER,
);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-WRONG-FORM'), true);
});
test('.profile needs no install section at all', () => {
const f = checkInstallBlock({ readme: '# .profile\nOrg profile.\n', name: '.profile', klass: 'org-profile' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ----------------------------------------------------------- required files
test('required files are per class, not flat across the org', () => {
const ok = checkRequiredFiles({ present: ['README.md'], klass: 'org-profile' }, REGISTER);
assert.equal(ok.filter((f) => f.level === 'ERROR').length, 0);
const missing = checkRequiredFiles({ present: ['README.md'], klass: 'plugin' }, REGISTER);
assert.equal(missing.some((f) => f.code === 'FILE-MISSING' && f.msg.includes('LICENSE')), true);
});
test('no class requires a ROADMAP — it is 0/18 and belongs to a later step', () => {
for (const klass of Object.keys(REGISTER.classes)) {
assert.equal(REGISTER.classes[klass].required_files.includes('ROADMAP.md'), false);
}
});
// -------------------------------------------------------------- aggregation
test('levelOf ranks ERROR above WARN above SKIP above OK', () => {
assert.equal(levelOf([{ level: 'OK' }, { level: 'WARN' }, { level: 'ERROR' }]), 'ERROR');
assert.equal(levelOf([{ level: 'OK' }, { level: 'WARN' }]), 'WARN');
assert.equal(levelOf([{ level: 'OK' }, { level: 'SKIP' }]), 'SKIP');
assert.equal(levelOf([{ level: 'OK' }]), 'OK');
assert.equal(levelOf([]), 'OK');
});
test('an unregistered repo is SKIP, not a pass — the gate refuses to guess a class', () => {
const r = classifyRepo({ name: 'stranger', files: {}, present: [], description: null }, REGISTER);
assert.equal(r.status, 'SKIP');
});
test('a fully compliant plugin repo classifies OK', () => {
const readme = [
'# repo-mailbox',
'A local mailbox for coordination.',
'',
'Body text.',
'',
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install repo-mailbox@${MKT.name}`,
].join('\n');
const r = classifyRepo(
{
name: 'repo-mailbox',
files: { 'README.md': readme },
present: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
description: 'A local mailbox for coordination.',
},
REGISTER,
);
assert.equal(r.status, 'OK');
});