docs(plan): Handover 4, the planner and the template follow the 1.9 fixes

Operator decisions 20 and 22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-13 19:10:37 +02:00
commit 5fa956b974
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 133 additions and 11 deletions

View file

@ -41,6 +41,38 @@ function sentence(text, anchor, label) {
return text.slice(start, end);
}
// A line that opens a list item: its indent, then `-`, `*`, `+`, `1.` or `1)`.
const LIST_ITEM_LINE = /^([ \t>]*)(?:[-*+]|\d+[.)]) /;
/**
* The list item of `text` that holds the first match of `anchor`: from the
* start of the line that opens it (`- `) to the next line that opens a list
* item at the same indent (or a smaller one, which closes its list too), a
* blank line, or the end of the text. A wrapped line of the item belongs to it.
*/
function bullet(text, anchor, label) {
const m = anchor.exec(text);
assert.ok(m, `${label}: anchor ${anchor} not found`);
const lines = text.split('\n');
let at = 0;
let i = 0;
while (i < lines.length - 1 && at + lines[i].length < m.index) { at += lines[i].length + 1; i++; }
let first = i;
while (first >= 0 && !LIST_ITEM_LINE.test(lines[first])) {
assert.ok(lines[first].trim() !== '', `${label}: the match of ${anchor} stands in no list item`);
first--;
}
assert.ok(first >= 0, `${label}: the match of ${anchor} stands in no list item`);
const indent = LIST_ITEM_LINE.exec(lines[first])[1].length;
let last = first + 1;
for (; last < lines.length; last++) {
if (lines[last].trim() === '' || /^[ \t>]*$/.test(lines[last])) break;
const item = LIST_ITEM_LINE.exec(lines[last]);
if (item && item[1].length <= indent) break;
}
return lines.slice(first, last).join('\n');
}
function holds18OrLater(text, anchor, label) {
const s = sentence(text, anchor, label);
assert.match(s, /1\.8 or later/, `${label} must say "1.8 or later" in the sentence that carries the rule: ${JSON.stringify(s)}`);
@ -126,6 +158,81 @@ test('/trekplan: the 1.8 additions heading says "1.8 or later", and a separate 1
assert.ok(block.includes(ANNOTATION), `the 1.9 block says "${ANNOTATION}"`);
});
// Step 12: the docs follow the 1.9 fixes. Operator decision 20 (12.10): the
// `Test first:` files are the backticked paths right after `File:`, and the
// red proof checks `--tests` against them (`tests-mismatch`). The 1.9 `File:`
// line takes the numbered markers, a wrapped line joins it, a list item ends
// it. Operator decision 22 (13.10): a backticked placeholder or a mixed-case
// bare name is no path (`./Name` names such a file); the bare word none,
// unticked, stays PLAN_STEP_PATH_NOT_FIRST.
const BOTH_NONES = ['the bare word none', 'a backticked placeholder'];
function holdsBothNones(text, label) {
for (const phrase of BOTH_NONES) {
assert.ok(text.includes(phrase), `${label} must say "${phrase}": ${JSON.stringify(text)}`);
}
}
const invariant7 = () => {
const h4 = handover4();
const m7 = /^7\. \*\*Every plan_version/m.exec(h4);
assert.ok(m7, 'Handover 4 has invariant 7');
const m8 = /^8\. \*\*/m.exec(h4.slice(m7.index));
return h4.slice(m7.index, m8 ? m7.index + m8.index : undefined);
};
const trekplan19Block = () => {
const p = read('commands/trekplan.md');
const h19 = p.indexOf('**plan_version 1.9 additions**');
assert.ok(h19 >= 0, 'commands/trekplan.md has the "**plan_version 1.9 additions**" block');
const end = p.indexOf('\n>\n> **', h19);
return p.slice(h19, end < 0 ? undefined : end);
};
const template19Paragraph = () => {
const t = read('templates/plan-template.md');
const start = t.indexOf('From plan_version 1.9 the rule is the file first on every line');
assert.ok(start >= 0, 'the template has its 1.9 paragraph');
const end = t.indexOf('\n### Step 1:', start);
return t.slice(start, end < 0 ? undefined : end);
};
test('Handover 4 invariant 7 names operator decision 20 and the red proof\'s tests-mismatch', () => {
const inv7 = invariant7();
assert.match(inv7, /operator decision 20\b/, `invariant 7 names operator decision 20: ${JSON.stringify(inv7)}`);
assert.ok(inv7.includes('`tests-mismatch`'), `invariant 7 names \`tests-mismatch\`: ${JSON.stringify(inv7)}`);
});
test('Handover 4 invariant 8, the Test first bullet: numbered markers, the wrapped line, a list item ends the entry', () => {
const b = bullet(handover4(), /A line counts only when/, 'invariant 8 Test first bullet');
assert.ok(b.includes('`1.`') && b.includes('`1)`'), `the Test first bullet names the \`1.\` and \`1)\` markers: ${JSON.stringify(b)}`);
assert.match(b, /wrapped/, `the Test first bullet names the wrapped line: ${JSON.stringify(b)}`);
assert.match(b, /any list item\b[^;]*\bends\b/, `the Test first bullet says any list item ends the entry: ${JSON.stringify(b)}`);
});
test('Handover 4 invariant 8, the refusal bullet: operator decision 22 with PLAN_STEP_PATH_NOT_A_PATH, and both nones', () => {
const b = bullet(handover4(), /an entry that does not open with a backticked span/, 'invariant 8 refusal bullet');
const s = sentence(b, /operator decision 22\b/, 'invariant 8 refusal bullet, decision 22');
assert.ok(s.includes('`PLAN_STEP_PATH_NOT_A_PATH`'), `operator decision 22 stands with \`PLAN_STEP_PATH_NOT_A_PATH\`: ${JSON.stringify(s)}`);
holdsBothNones(b, 'the invariant 8 refusal bullet');
});
test('Handover 4 code table: the PLAN_STEP_PATH_NOT_A_PATH row names placeholders and mixed-case bare names', () => {
const m = /^\| `PLAN_STEP_PATH_NOT_A_PATH` \|.*$/m.exec(handover4());
assert.ok(m, 'Handover 4 has a PLAN_STEP_PATH_NOT_A_PATH row');
assert.match(m[0], /placeholder/, `the row names placeholders: ${JSON.stringify(m[0])}`);
assert.match(m[0], /mixed-case/, `the row names mixed-case bare names: ${JSON.stringify(m[0])}`);
});
for (const [label, slice] of [['the /trekplan 1.9 block', trekplan19Block], ['the template 1.9 paragraph', template19Paragraph]]) {
test(`${label} names the placeholders, \`./Name\` and both nones`, () => {
const text = slice();
assert.ok(text.includes('`TBD`') && text.includes('`N/A`'), `${label} names the placeholders (\`TBD\`, \`N/A\`): ${JSON.stringify(text)}`);
assert.ok(text.includes('`./Name`'), `${label} names \`./Name\`: ${JSON.stringify(text)}`);
holdsBothNones(text, label);
});
}
test('the template\'s own example steps validate as 1.9 with none of the new path codes', () => {
const r = validatePlanContent(read('templates/plan-template.md'));
assert.equal(r.parsed.planVersion, '1.9');