docs(changelog): fix round 4.5b part 3 and the review's other two findings as known issues
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bba5cb89b3
commit
31af4fa774
2 changed files with 129 additions and 1 deletions
117
tests/lib/fix-round-4-5b-part-3-prose.test.mjs
Normal file
117
tests/lib/fix-round-4-5b-part-3-prose.test.mjs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// 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 — <title>` 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`);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue