// tests/lib/fix-round-4-5b-part-3-prose.test.mjs // Prose pins for fix round 4-5b, part 3 (plan // .claude/projects/2026-10-28-fix-round-4-5b-part-3, decision 89): the fixes // that are text a session reads. The tag in brackets is the part-3 id from the // round's brief; the tagged test fails on an assertion at ce98f62, part 2's end // point. The keep-behaviour checks sit inside the tagged test as "Control:" // assertions. // KI3 the CHANGELOG lists part 3's fixes (CQW, PQO) under ### Fixed, its // door under ### Doors, the review's other two MAJOR (`3b98d18b`, // `69b086eb`) and the typed arguments the guard now refuses under // ### Known issues, and the tokenizer Known-issues line says exactly // what is left and when the operator accepted it. // No static import of a new export: the file must load at ce98f62. // 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. */ 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. */ const flat = (s) => s.replace(/\s+/g, ' ').trim(); // --- KI3: the round's CHANGELOG entry ---------------------------------------------- // Part 3 stands in the same Unreleased section as parts 1 and 2. The section is // found by its title, by the rule tests/lib/fix-round-4-5b-part-2-prose.test.mjs // uses (copied, not imported): `## Unreleased — ` before the release, // `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `. const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part"; const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); /** The lines of the round's CHANGELOG section, heading excluded. */ function changelogSection() { const lines = read('CHANGELOG.md').split('\n'); const at = []; lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); }); assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`); let end = lines.length; for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; } return lines.slice(at[0] + 1, end); } /** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */ function changelogBullets(heading) { const lines = changelogSection(); const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading); assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`); const out = []; for (const l of lines.slice(s + 1)) { if (/^### /.test(l)) break; if (/^- /.test(l)) out.push(l); else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`; else if (l.trim() !== '') out.push(''); } return out.filter((b) => b !== '').map(flat); } // The review's two MAJOR findings part 3 leaves open, and the two it fixes; the // ids are the review's, written out here. const OPEN_IDS = ['3b98d18b', '69b086eb']; const FIXED_IDS = ['87a693f0', '021996c6']; test('[KI3] the CHANGELOG lists part 3\'s fixes, the review\'s other two MAJOR and the corrected tokenizer line', () => { const known = changelogBullets('### Known issues'); // Red: each open finding has exactly one Known-issues bullet, and that bullet names decision 89. for (const id of OPEN_IDS) { const hits = known.filter((b) => b.includes(id)); assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`); assert.ok(hits[0].includes('decision 89'), `the Known-issues bullet naming ${id} does not say decision 89: ${hits[0]}`); } // Red: the typed arguments the guard now refuses have one Known-issues bullet. const refused = known.filter((b) => b.includes("<<$'\\t'")); assert.equal(refused.length, 1, `### Known issues must hold exactly one bullet naming the typed argument <<$'\\t'; found ${refused.length}`); // Red: the tokenizer bullet says what is left and when the operator accepted it. const tok = known.filter((b) => b.includes("'90s")); assert.equal(tok.length, 1, `### Known issues must hold exactly one bullet holding '90s; found ${tok.length}`); for (const part of ['say "hi"', 'x --since ', '2026-10-27', '2026-10-28']) { assert.ok(tok[0].includes(part), `the tokenizer Known-issues bullet must hold ${JSON.stringify(part)}: ${tok[0]}`); } for (const gone of ['"a "', 'accepted these']) { assert.ok(!tok[0].includes(gone), `the tokenizer Known-issues bullet must no longer hold ${JSON.stringify(gone)}: ${tok[0]}`); } // Red elsewhere: one ### Fixed bullet per part-3 id, each naming its review // finding, and a door for the round. const fixed = changelogBullets('### Fixed'); for (const [tag, id] of [['(CQW)', '87a693f0'], ['(PQO)', '021996c6']]) { const hits = fixed.filter((b) => b.includes(tag)); assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag}; found ${hits.length}`); assert.ok(hits[0].includes(id), `the ### Fixed bullet marked ${tag} must name review finding ${id}: ${hits[0]}`); } const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 89')); assert.equal(doors.length, 1, `### Doors must hold one bullet naming decision 89; found ${doors.length}`); // Control: no fixed finding is listed as open. for (const id of FIXED_IDS) { assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`); } });