// tests/lib/fix-round-4-5b2-b-prose.test.mjs // Prose pins for fix round 4.5b2, part B (plan // .claude/projects/2026-10-31-review-gate-4-5b2): the parts of the round that // are text a session reads. The tag in brackets is the part-B id from the // round's plan; each tagged test fails on an assertion at the step's base. // V12 review_version 1.2 is current: Handover 6's table and field row name // it, no line of docs/HANDOVER-CONTRACTS.md still names 1.1 as current, // the Versioning paragraph says what 1.1 -> 1.2 changed, MIGRATION.md // holds the 1.2 section with its N-1 window, and every other place that // states the current review_version says 1.2. // The helpers below are copied, never imported from an earlier prose test. // Fix the SOURCE a pin guards, not the pin. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const readCache = new Map(); /** A repo file's text; the file must exist. Read from disk once per run. */ // Copied from tests/lib/fix-round-4-5b2-a-prose.test.mjs (read). function read(rel) { if (!readCache.has(rel)) { assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`); readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8')); } return readCache.get(rel); } /** Whitespace (line breaks too) collapsed to one space. */ // Copied from tests/lib/fix-round-4-5b2-a-prose.test.mjs (flat). const flat = (s) => s.replace(/\s+/g, ' ').trim(); /** The text from `heading` up to the next `## ` heading; the heading must exist. */ // Copied from tests/lib/review-prose-pins.test.mjs (section). function section(text, heading) { const start = text.indexOf(heading); assert.ok(start >= 0, `heading missing: ${heading}`); const next = text.indexOf('\n## ', start + heading.length); return text.slice(start, next === -1 ? undefined : next); } /** Every sentence of `text` (flattened). */ const sentences = (text) => flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/); const CONTRACTS = 'docs/HANDOVER-CONTRACTS.md'; // --- V12: review_version 1.2 is current ------------------------------------------ test('[V12] docs/HANDOVER-CONTRACTS.md names 1.2 as current in the table and the row, and no line names 1.1 as current', () => { const text = read(CONTRACTS); const tableRows = text.split('\n').filter((l) => l.startsWith('| `review.md` | `review_version`')); assert.equal(tableRows.length, 1, `the versioning table has one review.md row; found ${tableRows.length}`); assert.match(tableRows[0], /\|\s*`1\.2`\s*\|\s*$/, `the versioning table's review.md row says 1.2 is current: ${tableRows[0]}`); const h6 = section(text, '## Handover 6'); const fieldRows = h6.split('\n').filter((l) => l.startsWith('| `review_version` |')); assert.equal(fieldRows.length, 1, `Handover 6 has one review_version row; found ${fieldRows.length}`); assert.match(fieldRows[0], /"1\.2"`? \(current\)/, `the review_version row lists 1.2 as current: ${fieldRows[0]}`); const stale = text.split('\n').filter((l) => /1\.1"?`?\s*\(current\)/.test(l) || /current is `?1\.1\b/.test(l) || (l.startsWith('| `review.md` | `review_version`') && /`1\.1`/.test(l))); assert.deepEqual(stale, [], `no line of ${CONTRACTS} may name review_version 1.1 as current`); }); test('[V12] the Handover 6 Versioning paragraph has the 1.1 -> 1.2 sentence', () => { const h6 = section(read(CONTRACTS), '## Handover 6'); const paras = h6.split('\n').filter((l) => l.startsWith('**Versioning:**')); assert.equal(paras.length, 1, `Handover 6 has one Versioning paragraph; found ${paras.length}`); const para = flat(paras[0]); assert.match(para, /current is `1\.2`/, `the Versioning paragraph says current is 1.2: ${para}`); const hits = sentences(para).filter((s) => /1\.1/.test(s) && /1\.2/.test(s)); assert.ok(hits.length > 0, `no sentence of the Versioning paragraph names both 1.1 and 1.2: ${para}`); assert.ok(hits.some((s) => /still validates/.test(s)), `the 1.1 -> 1.2 sentence says a 1.1 review still validates: ${hits.join(' | ')}`); assert.ok(hits.some((s) => /never opens/.test(s) && /unanswered MAJOR/.test(s) && /lock/.test(s)), `the 1.1 -> 1.2 sentence says a 1.1 review never opens the lock with an unanswered MAJOR: ${hits.join(' | ')}`); }); test('[V12] MIGRATION.md has the review_version 1.2 section with its N-1 window', () => { const text = read('MIGRATION.md'); const heads = text.split('\n').filter((l) => /^## review_version 1\.2\b/.test(l)); assert.deepEqual(heads, ['## review_version 1.2 (from the next release)'], 'MIGRATION.md has exactly one "## review_version 1.2 (from the next release)" section'); const body = flat(section(text, heads[0])); assert.match(body, /\bN-1\b/, `the 1.2 section names the N-1 window: ${body}`); assert.match(body, /\b1\.1\b/, `the 1.2 section says what happens to a 1.1 review: ${body}`); }); test('[V12] every current review_version statement names 1.2', () => { const tpl = read('templates/trekreview-template.md'); assert.match(tpl.split('\n')[2], /^review_version: "1\.2"$/, 'the template\'s frontmatter says review_version "1.2"'); assert.ok(!tpl.includes('review_version: "1.1"'), 'the template no longer writes review_version "1.1"'); const coord = read('agents/review-coordinator.md'); assert.ok(coord.includes('review_version: "1.2"'), 'the coordinator writes review_version "1.2"'); assert.ok(!coord.includes('review_version: "1.1"'), 'the coordinator no longer writes review_version "1.1"'); const cmd = read('commands/trekreview.md'); assert.match(cmd, /\*\*The review is a gate \(review_version 1\.2,/, '/trekreview says the review is a gate at review_version 1.2'); assert.doesNotMatch(cmd, /\(review_version 1\.1,/, '/trekreview no longer says review_version 1.1 is current'); const claude = read('CLAUDE.md'); assert.ok(claude.includes('**Review is a gate (review_version 1.2).**'), 'CLAUDE.md says the review gate is review_version 1.2'); assert.ok(!claude.includes('(review_version 1.1)'), 'CLAUDE.md no longer names review_version 1.1'); const arch = read('docs/architecture.md'); assert.ok(arch.includes('review_version 1.2'), 'docs/architecture.md names review_version 1.2'); assert.ok(!arch.includes('review_version 1.1'), 'docs/architecture.md no longer names review_version 1.1'); });