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:
commit
816ba97c63
11 changed files with 1319 additions and 0 deletions
370
scripts/repo-standard-check.test.mjs
Normal file
370
scripts/repo-standard-check.test.mjs
Normal 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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue