docs(claude-md): 8.10 — fix hooks count + add doc-consistency test for hook-table sync

This commit is contained in:
Kjell Tore Guttormsen 2026-04-30 17:12:49 +02:00
commit 97c5c9d934
2 changed files with 77 additions and 1 deletions

View file

@ -84,3 +84,78 @@ describe('doc-consistency — v2 cutoffs are documented in unified prose', () =>
assert.match(content, /score\s*[≥>=]+\s*65/);
});
});
// ---------------------------------------------------------------------------
// D4 (Batch C, Wave D): Hooks count must stay synchronized across three
// surfaces — the CLAUDE.md `## Hooks (N)` header, the markdown table directly
// underneath that header, and the canonical hooks/hooks.json definition.
// Drift previously masked a missing `pre-compact-scan.mjs` row in CLAUDE.md.
// This block fails fast if any of the three surfaces drift.
// ---------------------------------------------------------------------------
describe('doc-consistency — Hooks count consistency (D4)', () => {
const CLAUDE_MD = join(PLUGIN_ROOT, 'CLAUDE.md');
const HOOKS_JSON = join(PLUGIN_ROOT, 'hooks', 'hooks.json');
function readHeaderNumber(text) {
const match = text.match(/^##\s+Hooks\s*\((\d+)\)\s*$/m);
if (!match) throw new Error('No `## Hooks (N)` header found in CLAUDE.md');
return parseInt(match[1], 10);
}
function readTableRowCount(text) {
// Section spans from `## Hooks (N)` to next `^## ` heading.
const startIdx = text.search(/^##\s+Hooks\s*\(\d+\)\s*$/m);
if (startIdx < 0) throw new Error('Hooks header not found');
const tail = text.slice(startIdx);
const nextHeader = tail.search(/\n##\s+\S/);
const section = nextHeader > 0 ? tail.slice(0, nextHeader) : tail;
// Count rows that look like `| \`<name>.mjs\` | ...`.
// Excludes the header row (which uses bare `Script` not a backtick).
const rows = section.match(/^\|\s*`[^`|]+\.mjs`\s*\|/gm) || [];
return rows.length;
}
function readJsonHookCount(jsonText) {
const parsed = JSON.parse(jsonText);
const seen = new Set();
for (const eventArr of Object.values(parsed.hooks || {})) {
for (const entry of eventArr) {
for (const h of entry.hooks || []) {
// Dedupe by command path — a hook registered to multiple events
// counts as one script.
if (h.command) seen.add(h.command);
}
}
}
return seen.size;
}
it('header count, table row count, and hooks.json count agree', () => {
const claudeText = readFileSync(CLAUDE_MD, 'utf-8');
const hooksJsonText = readFileSync(HOOKS_JSON, 'utf-8');
const headerNumber = readHeaderNumber(claudeText);
const tableRowCount = readTableRowCount(claudeText);
const jsonHookCount = readJsonHookCount(hooksJsonText);
assert.equal(
headerNumber,
jsonHookCount,
`CLAUDE.md '## Hooks (${headerNumber})' header disagrees with hooks/hooks.json (${jsonHookCount} hooks). ` +
`Update the header to match.`,
);
assert.equal(
tableRowCount,
jsonHookCount,
`CLAUDE.md hooks table has ${tableRowCount} rows but hooks/hooks.json defines ${jsonHookCount} hooks. ` +
`Add/remove rows in the table to match.`,
);
assert.equal(
headerNumber,
tableRowCount,
`CLAUDE.md header (${headerNumber}) and table row count (${tableRowCount}) disagree. ` +
`These two surfaces must stay in sync.`,
);
});
});