// tests/lib/review-loop-docs.test.mjs // Prose pins for the converging critic loop (decision 43): /trekplan Phase 9, // Handover 4, CLAUDE.md and the agent files must state rule A and rule B in the // exact words lib/review/critic-diff.mjs exports, name the optional verdict // fields, and no longer say the loop ends on a round with no BLOCKER. // Texts are compared with every run of whitespace read as one space, so a // sentence wrapped over two lines still counts. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { RULE_A, RULE_B, DIFF_FIELDS } from '../../lib/review/critic-diff.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const read = rel => readFileSync(join(ROOT, rel), 'utf-8'); const flat = text => String(text).replace(/\s+/g, ' '); function phase(text, n) { const start = text.indexOf(`\n## Phase ${n} `); assert.ok(start >= 0, `## Phase ${n} missing`); const end = text.indexOf('\n## Phase ', start + 1); return text.slice(start, end < 0 ? undefined : end); } function handover4() { const contract = read('docs/HANDOVER-CONTRACTS.md'); const start = contract.indexOf('## Handover 4'); const end = contract.indexOf('## Handover 5'); assert.ok(start >= 0 && end > start, 'Handover 4 and Handover 5 headings found in order'); return contract.slice(start, end); } const P9 = flat(phase(read('commands/trekplan.md'), 9)); test('RULE_A stands verbatim in Phase 9, Handover 4, CLAUDE.md and the planning orchestrator', () => { const rule = flat(RULE_A); assert.ok(P9.includes(rule), 'Phase 9 of commands/trekplan.md states RULE_A'); assert.ok(flat(handover4()).includes(rule), 'Handover 4 of docs/HANDOVER-CONTRACTS.md states RULE_A'); assert.ok(flat(read('CLAUDE.md')).includes(rule), 'CLAUDE.md states RULE_A'); assert.ok(flat(read('agents/planning-orchestrator.md')).includes(rule), 'agents/planning-orchestrator.md states RULE_A'); }); test('RULE_B stands verbatim in Phase 9 and in both reviewer agent files', () => { const rule = flat(RULE_B); assert.ok(P9.includes(rule), 'Phase 9 of commands/trekplan.md states RULE_B'); for (const rel of ['agents/plan-critic.md', 'agents/scope-guardian.md']) { assert.ok(flat(read(rel)).includes(rule), `${rel} states RULE_B`); } }); test('Handover 4 names every DIFF_FIELDS entry and says absent means round-1 behaviour', () => { const h4 = handover4(); assert.ok(DIFF_FIELDS.length > 0, 'DIFF_FIELDS lists the optional fields'); const missing = DIFF_FIELDS.filter(f => !h4.includes(`\`${f}\``)); assert.deepEqual(missing, [], `Handover 4 does not name (backticked): ${missing.join(', ')}`); assert.ok(flat(h4).includes('absent means round-1 behaviour'), 'Handover 4 says "absent means round-1 behaviour"'); }); test('Phase 9 names plan-gate --diff and says round 1 is unchanged', () => { assert.ok(P9.includes('plan-gate.mjs --diff'), 'Phase 9 runs plan-gate.mjs --diff'); assert.match(P9, /round 1 is unchanged/i, 'Phase 9 says "round 1 is unchanged"'); }); test('Phase 9 no longer says the loop ends on a round with no BLOCKER', () => { assert.ok(!P9.includes('ends on a round with no BLOCKER'), 'the stale "ends on a round with no BLOCKER" is gone'); });