docs(plan): Handover 4, the planner and the template follow decisions 29 to 31

Operator decisions 28 to 31 (13.10 and 14.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-14 12:43:01 +02:00
commit f746f27788
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 58 additions and 9 deletions

View file

@ -692,12 +692,12 @@ Do NOT write this synthesis to disk. It is internal working context only.
> `` `src/a.mjs`, `src/b.mjs` (new) ``, never `` `src/a.mjs` (new), `src/b.mjs` ``
> (the second path would be description); two test files share one line,
> `` File: `a.test.mjs`, `b.test.mjs` (new) ``. A line that is no list item
> continues the entry above it, so a list wrapped after a comma keeps its
> paths (`` `src/a.mjs`, `` with `` `src/b.mjs` `` on the next line names both)
> and a wrapped description stays description. The `File:` line may open with
> `-`, `*`, `+`, `1.` or `1)`, and any list item (the `Verifies:` line too)
> ends it. A 1.9 step always names a file:
> an entry that does not open with a backticked path, the bare word none
> continues the entry above it, so a list wrapped after a comma keeps its paths (`` `src/a.mjs`, `` with
> `` `src/b.mjs` `` on the next line names both) and a wrapped description stays description: once the
> entry so far does not end with a comma, a wrapped line is description even when it opens with a backticked
> span (operator decision 29), and a `File:` line with nothing after it takes its first wrapped line. The
> `File:` line may open with `-`, `*`, `+`, `1.` or `1)`, and any list item (the `Verifies:` line too) ends it.
> A 1.9 step always names a file: an entry that does not open with a backticked path, the bare word none
> (written without backticks), or an empty
> `Files:` is `PLAN_STEP_PATH_NOT_FIRST`; a leading span that is no path (a
> call, an option, a URL) is `PLAN_STEP_PATH_NOT_A_PATH`.

View file

@ -270,9 +270,9 @@ prose form (relaxed in S26 so the template's prose emission parses).
- **K10 — `Regression:`** (optional step field): ONE backticked command the criteria runner's allowlist runs, with no pipe and no `&&` (first backticked span, as for `Verify:`). The executor runs it green before the step's checkpoint; a step without it runs nothing new. `Verify:` stays the step's own red-first test. One that can never run is `PLAN_STEP_REGRESSION_NOT_RUNNABLE` (an error; a warning under `--soft`).
- **K11 — `On failure:`** opens with one of `revert`, `retry`, `skip`, `escalate` (case-insensitive; a leading `**`, `_`, backtick or `-` is ignored, other leading characters are not; `onFailureAction` in `lib/parsers/plan-schema.mjs`), the words `/trekexecute` reads; any other opening is the warning `PLAN_STEP_ON_FAILURE_UNKNOWN`. A missing `On failure:` is unchanged (the executor defaults to `escalate`).
- **A step of only tests** (order 10, voyage-lekerepo trial 3, 2026-10-09): when every file in a step's `Files:` is a test (a file on its `Test first:` `File:` line, or a path `lib/util/test-path.mjs` reads as a test: under `tests/`, `test/`, `__tests__/`, `spec/`, `specs/`, or named `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`, `test_*.py`, but never a document such as `docs/specs/api.md`; `stepTestsOnly` in `lib/parsers/plan-schema.mjs`), the warning is `PLAN_STEP_TESTS_ONLY`: the behaviour its tests cover exists at its start point, so it can never be proven red. A step without `Files:` is not judged. At run time `red-proof.mjs --plan <plan.md> --step <N>` puts the same reading on a `not-red-at-base` as `tests_only`: the plan's `Files:` alone decides, and the step's diff is not read (operator 2026-10-11), and `/trekexecute` sends a `tests_only: true` step straight up to the plan.
- **The `Test first:` files** (operator decision 20, 2026-10-12): a step's test files are the backticked paths that stand right after `File:` on its `Test first:` `File:` line, separated only by commas or whitespace; a path in the description after them (`` covers `parse()` in `src/parser.mjs` ``) is no test file. This holds at every plan_version (`testFirstFiles` in `lib/parsers/plan-schema.mjs`; from 1.9 the line is read by the grammar of invariant 8). `red-proof.mjs --plan <plan.md> --step <N>` checks its `--tests` against them: a different set is `not-run` with reason `tests-mismatch` and the plan's list in `plan_tests`; without `--tests` the plan's list is used.
- **The `Test first:` files** (operator decision 20, 2026-10-12): a step's test files are the backticked paths that stand right after `File:` on its `Test first:` `File:` line, separated only by commas or whitespace; a path in the description after them (`` covers `parse()` in `src/parser.mjs` ``) is no test file. This holds at every plan_version (`testFirstFiles` in `lib/parsers/plan-schema.mjs`; from 1.9 the line is read by the grammar of invariant 8). Below plan_version 1.9 the run also goes on past a note in brackets (`(new)`, `*(new)*`, `(existing …)`) and the words `and`, `plus` and `+`, to a further path only when it is clearly a test (as `lib/util/test-path.mjs` and the tests-only check read it); a path that is not ends the run (operator decision 28, 2026-10-13, and operator decision 31, 2026-10-14). `red-proof.mjs --plan <plan.md> --step <N>` reads them from the plan, and the documented call passes no `--tests` (operator decision 30, 2026-10-14): it prints the list it ran (`tests`); a given `--tests` that differs is `not-run` with reason `tests-mismatch` and the plan's list in `plan_tests`; a plan or step it cannot read is `not-run` with reason `plan-unreadable`.
8. **plan_version 1.9 only** (1.8 and 1.7 plans are read as before, with one warning, `PLAN_STEP_PATH_UNTICKED`, for a path their reader found outside backticks; decision 16, operator 2026-10-11) — the file first on every line:
- the entries of a step's `Files:` field are its first non-empty line and each list-item line (`-`, `*`, `+`, `1.`, `1)`); a line that is no list item continues the entry above it, and its text joins that entry with one space before the entry is read, so its backticked paths join the entry: a list wrapped after a comma keeps its paths, and a wrapped description stays description;
- the entries of a step's `Files:` field are its first non-empty line and each list-item line (`-`, `*`, `+`, `1.`, `1)`); a line that is no list item continues the entry above it, when the entry so far ends with a comma, its text joins that entry with one space before the entry is read, so a list wrapped after a comma keeps its paths; otherwise it is description, never read, and so is every later wrapped line of that entry (operator decision 29, 2026-10-14); an entry with nothing on its own line (a `File:` line with nothing after it) takes its first wrapped line;
- each entry opens with its backticked path(s), separated only by commas or whitespace; everything from the first other text on is description and is never read (`` `a.test.mjs` - covers `src/parse.mjs` `` names one file), so every path stands before any annotation (`` `src/a.mjs`, `src/b.mjs` (new) ``, never `` `src/a.mjs` (new), `src/b.mjs` ``);
- each `Test first:` `File:` line is one entry under the same grammar (several test files share one line: `` File: `a.test.mjs`, `b.test.mjs` (new) ``). A line counts only when `File:` opens it (after a list marker `-`, `*`, `+`, `1.` or `1)`, or `**`); a line right after it that is no list item continues it (a wrapped line, read as above), and any list item (a `Verifies:` or `Pattern:` line too) or a blank line ends it; a step whose `Test first:` has no such line with a backticked path names no test and is `PLAN_STEP_NO_TEST_FIRST` (`File:` mid-line is description, as the executor reads it);
- an entry that does not open with a backticked span, the bare word none (written without backticks), and an empty or absent `Files:` are `PLAN_STEP_PATH_NOT_FIRST` (a 1.9 step always names a file); a leading span that is no path is `PLAN_STEP_PATH_NOT_A_PATH`: a call, an option, a URL, and (operator decision 22, 2026-10-13) a backticked placeholder (`` `none` ``, `` `TBD` ``, `` `TODO` ``, `` `TBA` ``, `` `N/A` ``, `` `NA` ``, `` `...` ``, in any case) or a bare name (no `.` or `/`) that is neither in capitals (`` `LICENSE` ``) nor a build file (`` `Makefile` ``), such as the mixed-case `` `Parser` ``; `` `./Parser` `` names such a file (`./Name`; `isPathShape` in `lib/parsers/plan-schema.mjs`). Both are errors under `--strict`, warnings under `--soft`;

View file

@ -94,7 +94,9 @@ entry and each `Test first:` `File:` line opens with its backticked path(s),
separated only by commas or spaces, and everything after the first other text
is description that is never read. A 1.9 step always names a file
(the bare word none or an empty `Files:` is refused). A line that is no list item
continues the entry above it (a list wrapped after a comma keeps its paths),
continues the entry above it (a list wrapped after a comma keeps its paths;
once the entry so far does not end with a comma, the wrapped line is description;
a `File:` line with nothing after it takes its first wrapped line),
the `File:` line may open with `1.` or `1)` and any list item ends it, and
a backticked placeholder (`TBD`, `TODO`, `TBA`, `N/A`, `NA`, `none`, `...`) or a
mixed-case bare name (`Parser`) is no path, so write `./Name` for such a file. In `Files:`,

View file

@ -222,6 +222,53 @@ test('Handover 4 code table: the PLAN_STEP_PATH_NOT_A_PATH row names placeholder
assert.match(m[0], /mixed-case/, `the row names mixed-case bare names: ${JSON.stringify(m[0])}`);
});
// Second remediation, Step 8: the docs follow operator decisions 29 to 31
// (14.10). Decision 29: in 1.9 a wrapped line keeps its paths only when the
// entry so far ends with a comma; otherwise it is description, and a `File:`
// line with nothing after it takes its first wrapped line. Decision 31: below
// 1.9 the run of test files goes on past a note in brackets and the words
// and, plus and +. Decision 30: red proof reads the list from the plan, the
// documented call passes no `--tests`, and an unreadable plan is
// `plan-unreadable`.
// A blockquote's text without its `> ` markers, so a phrase wrapped across two
// quoted lines reads as one.
const unquote = (text) => text.replace(/\n>[ \t]?/g, '\n');
test('Handover 4 invariant 8, the Files bullet: operator decision 29, "ends with a comma", "takes its first wrapped line"', () => {
const b = bullet(handover4(), /the entries of a step's `Files:` field/, 'invariant 8 Files bullet');
assert.match(b, /\boperator decision 29\b/, `the Files bullet names operator decision 29: ${JSON.stringify(b)}`);
assert.match(b, /ends\s+with\s+a\s+comma/, `the Files bullet says a wrapped line keeps its paths when the entry ends with a comma: ${JSON.stringify(b)}`);
assert.match(b, /takes\s+its\s+first\s+wrapped\s+line/, `the Files bullet says an empty entry takes its first wrapped line: ${JSON.stringify(b)}`);
});
test('Handover 4 invariant 7 names operator decisions 30 and 31, plan-unreadable and plus', () => {
const inv7 = invariant7();
assert.match(inv7, /\boperator decision 30\b/, `invariant 7 names operator decision 30: ${JSON.stringify(inv7)}`);
assert.match(inv7, /\boperator decision 31\b/, `invariant 7 names operator decision 31: ${JSON.stringify(inv7)}`);
assert.ok(inv7.includes('plan-unreadable'), `invariant 7 names \`plan-unreadable\`: ${JSON.stringify(inv7)}`);
assert.match(inv7, /`plus`/, `invariant 7 names the word \`plus\`: ${JSON.stringify(inv7)}`);
});
test('Handover 4 invariant 7: "This holds at every plan_version" comes before "Below plan_version 1.9"', () => {
const inv7 = invariant7();
const every = inv7.indexOf('This holds at every plan_version');
const below = inv7.indexOf('Below plan_version 1.9');
assert.ok(every >= 0, `invariant 7 says "This holds at every plan_version": ${JSON.stringify(inv7)}`);
assert.ok(below >= 0, `invariant 7 says "Below plan_version 1.9": ${JSON.stringify(inv7)}`);
assert.ok(every < below, 'the every-plan_version sentence stands before the below-1.9 sentence');
});
for (const [label, slice] of [['the /trekplan 1.9 block', trekplan19Block], ['the template 1.9 paragraph', template19Paragraph]]) {
test(`${label} follows operator decision 29: a wrapped line is description unless the entry ends with a comma`, () => {
const text = unquote(slice());
assert.match(text, /entry\s+so\s+far\s+does\s+not\s+end\s+with\s+a\s+comma[^.]*description/,
`${label} says a wrapped line is description once the entry so far does not end with a comma: ${JSON.stringify(text)}`);
assert.match(text, /takes\s+its\s+first\s+wrapped\s+line/,
`${label} says a \`File:\` line with nothing after it takes its first wrapped line: ${JSON.stringify(text)}`);
});
}
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();