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
409
scripts/repo-standard-check.mjs
Normal file
409
scripts/repo-standard-check.mjs
Normal 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));
|
||||
}
|
||||
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