/** * R6 — the two "Required Frontmatter" contracts are enforced by something. * * `.claude/rules/agent-development.md` and `.claude/rules/command-development.md` * write down which frontmatter keys an agent/command MUST carry, and that agent * colors must be unique within the plugin. Nothing enforced any of it: the only * test that looked at agent frontmatter (`agent-prompt-shape`) asserted `name:` * on a HAND-WRITTEN list of 3 of the 7 agents. Everything was compliant and * unwatched — the state in which a rule becomes fiction one file at a time. * * Nothing here is hand-maintained, because a hand-kept list of what to sweep is * a premise, not a measurement (the #57 shape): * - the required KEYS are parsed out of each rule's own ```yaml block; * - the files swept are resolved from each rule's own `paths:` frontmatter; * - the plugin name is read from `.claude-plugin/plugin.json`. * Add a key to a rule and it is enforced on the next run, with no test edit. */ import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFile, readdir } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..', '..'); const RULES_DIR = resolve(ROOT, '.claude', 'rules'); function frontmatterOf(content) { const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); return m ? m[1] : ''; } /** Top-level keys of a frontmatter block, in source order. */ function topLevelKeys(frontmatter) { return frontmatter .split('\n') .map((line) => line.match(/^([A-Za-z][A-Za-z0-9_-]*):/)) .filter(Boolean) .map((m) => m[1]); } function valueOf(frontmatter, key) { const m = frontmatter.match(new RegExp(`^${key}:[ \\t]*(.*)$`, 'm')); return m ? m[1].trim() : null; } /** * A rule file is the contract. Read the required keys out of the ```yaml block * under "## Required Frontmatter", and the swept directory out of `paths:`. */ async function loadRule(fileName) { const content = await readFile(resolve(RULES_DIR, fileName), 'utf-8'); const paths = valueOf(frontmatterOf(content), 'paths'); const fence = content.match(/##\s+Required Frontmatter[\s\S]*?```ya?ml\r?\n([\s\S]*?)```/); const requiredKeys = fence ? topLevelKeys(fence[1]).filter((k) => k !== '---') : []; const dir = paths ? paths.split('/')[0] : null; return { fileName, paths, dir, requiredKeys }; } async function loadTargets(rule) { const dirAbs = resolve(ROOT, rule.dir); const names = (await readdir(dirAbs)).filter((n) => n.endsWith('.md')).sort(); return Promise.all( names.map(async (name) => { const frontmatter = frontmatterOf(await readFile(resolve(dirAbs, name), 'utf-8')); return { name, frontmatter, keys: topLevelKeys(frontmatter) }; }), ); } const AGENT_RULE = 'agent-development.md'; const COMMAND_RULE = 'command-development.md'; test('R6 derivation is not vacuous: both rules yield required keys and a non-empty file set', async () => { for (const fileName of [AGENT_RULE, COMMAND_RULE]) { const rule = await loadRule(fileName); assert.ok(rule.requiredKeys.length > 0, `${fileName}: no required keys parsed from its "Required Frontmatter" yaml block — every assertion below would be vacuously green`); assert.ok(rule.dir, `${fileName}: no paths: frontmatter to resolve a file set from`); const targets = await loadTargets(rule); assert.ok(targets.length > 0, `${fileName}: paths: ${rule.paths} resolved to 0 files — the sweep would prove nothing`); } }); test('R6: every agent and command carries the keys its rule requires, non-empty', async () => { for (const fileName of [AGENT_RULE, COMMAND_RULE]) { const rule = await loadRule(fileName); const targets = await loadTargets(rule); for (const target of targets) { for (const key of rule.requiredKeys) { assert.ok(target.keys.includes(key), `${rule.dir}/${target.name} is missing required frontmatter key "${key}" (required by .claude/rules/${fileName}; ${targets.length} files swept)`); const value = valueOf(target.frontmatter, key); assert.ok(value !== null && value !== '', `${rule.dir}/${target.name} has an empty "${key}" — a present-but-empty key satisfies no contract`); } } } }); test('R6: agent colors are unique within the plugin', async () => { const rule = await loadRule(AGENT_RULE); const targets = await loadTargets(rule); const seen = new Map(); for (const target of targets) { const color = valueOf(target.frontmatter, 'color'); if (seen.has(color)) { assert.fail(`duplicate agent color "${color}": ${seen.get(color)} and ${target.name} (.claude/rules/${AGENT_RULE}: "Color must be unique within the plugin")`); } seen.set(color, target.name); } assert.equal(seen.size, targets.length, `expected ${targets.length} distinct colors, got ${seen.size}`); }); test('R6: agent names are kebab-case with the -agent suffix', async () => { const rule = await loadRule(AGENT_RULE); for (const target of await loadTargets(rule)) { const name = valueOf(target.frontmatter, 'name'); assert.match(name, /^[a-z0-9]+(?:-[a-z0-9]+)*-agent$/, `agents/${target.name}: name "${name}" must be kebab-case with an -agent suffix`); } }); test('R6: command names are plugin:action, or the bare plugin name for the router', async () => { const plugin = JSON.parse(await readFile(resolve(ROOT, '.claude-plugin', 'plugin.json'), 'utf-8')).name; const rule = await loadRule(COMMAND_RULE); const targets = await loadTargets(rule); const routers = []; for (const target of targets) { const name = valueOf(target.frontmatter, 'name'); if (name === plugin) { routers.push(target.name); continue; } assert.match(name, new RegExp(`^${plugin}:[a-z0-9]+(?:-[a-z0-9]+)*$`), `commands/${target.name}: name "${name}" must be "${plugin}:action" (or the bare "${plugin}" router)`); } assert.equal(routers.length, 1, `expected exactly one bare-"${plugin}" router command, got ${routers.length}: ${routers.join(', ')}`); });