docs(changelog): fix round 4.5a

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-24 20:11:22 +02:00
commit e351e7c29d
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 159 additions and 0 deletions

View file

@ -22,6 +22,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { findSection } from '../../lib/parsers/plan-schema.mjs';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
import { childEnv } from '../helpers/child-env.mjs';
@ -1483,3 +1484,90 @@ test('red script: the id list is the round\'s 30 ids, in the brief\'s order', ()
assert.ok(m, `the IDS= line is not one single-quoted list: ${lines[0]}`);
assert.deepEqual(m[1].trim().split(/\s+/), RED_IDS);
});
// --- CHANGELOG: what the round fixed ----------------------------------------
// Untagged: the CHANGELOG text is the round's report, not one id's fix. The
// round's section is found by its title, as tests/helpers/changelog-section.mjs
// finds the 6.4.0 delivery's: `## Unreleased — <title>` before the release,
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `
// heading. The released 6.5.0 section must stay byte-equal: its sha256 (the
// heading line through the line before the next `## ` heading, joined with
// `\n`) was computed once from `git show ab6b32c:CHANGELOG.md` with this
// slice; the test reads no git history.
const CHANGELOG = 'CHANGELOG.md';
const ROUND_TITLE = "The annotating skill, the run coupling and P10's model-free part";
const ROUND_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${ROUND_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
const V650_SHA256 = '1df8fd0fe7730204740ca509c8fceb6f9d331859b98ce5c1f8e62b9551ce3274';
/** The CHANGELOG lines from the one heading `isHeading` accepts to the line before the next `## ` heading. */
function changelogPart(isHeading, what) {
const lines = read(CHANGELOG).split('\n');
const at = [];
lines.forEach((l, i) => { if (isHeading(l.replace(/\s+$/, ''))) at.push(i); });
assert.equal(at.length, 1, `${CHANGELOG} has the ${what} 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], end).join('\n');
}
const roundPart = () => changelogPart((l) => ROUND_HEADING.test(l), `"${ROUND_TITLE}"`);
/** The `### ` subsection headings of the round's section, in order. */
const roundSubsections = () => roundPart().split('\n').filter((l) => /^### /.test(l)).map((l) => l.replace(/\s+$/, ''));
/** The bullets of one `### ` subsection of the round's section, each flattened to one line. */
function roundBullets(heading) {
const lines = section(roundPart(), heading).split('\n').slice(1);
const out = [];
for (const l of lines) {
if (/^- /.test(l)) out.push(l);
else if (out.length && l.trim() !== '') out[out.length - 1] += ` ${l}`;
}
return out.map(flat);
}
const FIXED_KNOWN_ISSUES = [
['trial-5 finding 9 (core code never deep-reviewed)', /trial-5 finding 9/],
['the park step', /park step/],
['the workflow floor and put-back', /`--workflow`.*floor.*put-back|`--workflow`.*put-back.*floor/],
['the row order (which row a finding joins)', /which row a finding joins/i],
['the coordinator\'s line numbers (a line past the end refuted)', /line (?:number )?past the end of the file.*(?:refuted|refutes)/],
['the Declined to Judge copy-through', /Declined to Judge.*`review\.md`/],
];
test('CHANGELOG: the round\'s section has ### Fixed between ### Changed and ### Doors, and ### Known issues after ### Fixed', () => {
const subs = roundSubsections();
for (const h of ['### Changed', '### Fixed', '### Known issues', '### Doors']) {
assert.equal(subs.filter((s) => s === h).length, 1, `the round's section must have "${h}" once; it has ${JSON.stringify(subs)}`);
}
const at = (h) => subs.indexOf(h);
assert.ok(at('### Changed') < at('### Fixed') && at('### Fixed') < at('### Doors'),
`### Fixed must stand between ### Changed and ### Doors: ${JSON.stringify(subs)}`);
assert.ok(at('### Fixed') < at('### Known issues'), `### Known issues must come after ### Fixed: ${JSON.stringify(subs)}`);
});
test('CHANGELOG: ### Fixed names each of the six 6.5.0 Known issues the round fixed, each as a 6.5.0 Known issue', () => {
const bullets = roundBullets('### Fixed');
const known = bullets.filter((b) => /6\.5\.0 Known issue/.test(b));
const missing = FIXED_KNOWN_ISSUES.filter(([, re]) => !known.some((b) => re.test(b))).map(([name]) => name);
assert.deepEqual(missing, [], `### Fixed names no entry marked "6.5.0 Known issue" for: ${missing.join('; ')}`);
});
test('CHANGELOG: ### Known issues names that a plan\'s profile: field has no writer, and names /trekexecute and /trekcontinue', () => {
const hits = roundBullets('### Known issues').filter((b) => b.includes("**A plan's `profile:` field has no writer.**"));
assert.equal(hits.length, 1, `expected one Known issues line on the plan's profile: field; found ${hits.length}`);
assert.ok(hits[0].includes('`/trekexecute` and `/trekcontinue`'), `the line does not name \`/trekexecute\` and \`/trekcontinue\`: ${hits[0]}`);
});
test('CHANGELOG: ### Doors has the two-way line for fix round 4.5a', () => {
const hits = roundBullets('### Doors').filter((b) => b.startsWith('- two-way: fix round 4.5a'));
assert.deepEqual(hits, ['- two-way: fix round 4.5a (decisions 57, 67, 68) — a revert restores the old texts and checks'],
'the Doors part has no single two-way line for fix round 4.5a');
});
test('CHANGELOG: the released v6.5.0 section is byte-equal to its text at ab6b32c (sha256)', () => {
const part = changelogPart((l) => l.startsWith('## v6.5.0 — '), 'v6.5.0');
assert.equal(createHash('sha256').update(part, 'utf8').digest('hex'), V650_SHA256,
'the released v6.5.0 section changed; edit only the round\'s section');
});