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:
parent
0142034979
commit
5fa956b974
4 changed files with 133 additions and 11 deletions
|
|
@ -691,11 +691,21 @@ Do NOT write this synthesis to disk. It is internal working context only.
|
|||
> is description and is never read. So every path stands before any annotation:
|
||||
> `` `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 1.9 step always names a file:
|
||||
> an entry that does not open with a backticked path, `none`, or an empty
|
||||
> `` 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
|
||||
> (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`. The canonical step
|
||||
> above already opens each line with its path.
|
||||
> call, an option, a URL) is `PLAN_STEP_PATH_NOT_A_PATH`.
|
||||
> So is a backticked placeholder (`` `none` ``, `` `TBD` ``, `` `TODO` ``,
|
||||
> `` `TBA` ``, `` `N/A` ``, `` `NA` ``, `` `...` ``) and a mixed-case bare name such as
|
||||
> `` `Parser` `` (no `.` or `/`, neither in capitals like `LICENSE` nor a build
|
||||
> file like `Makefile`), by operator decision 22: write `./Name` for such a
|
||||
> file. The canonical step above already opens each line with its path.
|
||||
>
|
||||
> **Two step rules at every plan_version** (no version bump; a plan without
|
||||
> the field is unchanged): the optional `Regression:` field (K10) holds ONE
|
||||
|
|
|
|||
|
|
@ -270,11 +270,12 @@ 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.
|
||||
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)`); any other line continues the entry above it;
|
||||
- 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;
|
||||
- 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 or `**`); 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, `none`, 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 (a call, an option, a URL) is `PLAN_STEP_PATH_NOT_A_PATH`. Both are errors under `--strict`, warnings under `--soft`;
|
||||
- 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`;
|
||||
- K6 (`lib/verification/plan-scope-sweep.mjs`) follows the grammar from 1.9: it reads a 1.9 plan's `Files:` and `Test first:` `File:` line by it (plan-schema `entryPaths` / `testFirstFiles`), and so does the red proof's `tests_only` reading (`lib/verification/red-proof.mjs`); below 1.9 both keep their earlier readers.
|
||||
|
||||
**Validation strategy:**
|
||||
|
|
@ -308,8 +309,8 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
|
|||
| `PLAN_STEP_REGRESSION_NOT_RUNNABLE` | K10, any version: a step's `Regression:` is not one command the allowlist runs (a pipe, `&&`, no backticks, an unknown runner) | One backticked runner call (`npm test`); a wider check goes in `bash tests/<script>.sh` |
|
||||
| `PLAN_STEP_ON_FAILURE_UNKNOWN` | K11, any version, warning: `On failure:` does not open with revert / retry / skip / escalate | Open with the word, then the specifics |
|
||||
| `PLAN_STEP_TESTS_ONLY` | Any version, warning: every file in the step's `Files:` is a test, so the step can never be proven red | Move the test into the step that builds the behaviour |
|
||||
| `PLAN_STEP_PATH_NOT_FIRST` | 1.9: a `Files:` entry does not open with a backticked path (also `none`, and an empty or absent `Files:`: the step names no file); error, a warning under `--soft` | Open each entry with its backticked path(s), then the description: `` `src/a.mjs` - the parser `` |
|
||||
| `PLAN_STEP_PATH_NOT_A_PATH` | 1.9: a `Files:` entry opens with a backticked span that is no path (a call `parse()`, an option, a URL); error, a warning under `--soft` | Put the path first; the span goes in the description |
|
||||
| `PLAN_STEP_PATH_NOT_FIRST` | 1.9: a `Files:` entry does not open with a backticked path (also the bare word none, and an empty or absent `Files:`: the step names no file); error, a warning under `--soft` | Open each entry with its backticked path(s), then the description: `` `src/a.mjs` - the parser `` |
|
||||
| `PLAN_STEP_PATH_NOT_A_PATH` | 1.9: a `Files:` entry opens with a backticked span that is no path (a call `parse()`, an option, a URL, a backticked placeholder such as `` `TBD` `` or `` `none` ``, a mixed-case bare name such as `` `Parser` ``; operator decision 22); error, a warning under `--soft` | Put the path first; the span goes in the description; a file named `Parser` is written `./Parser` |
|
||||
| `PLAN_STEP_PATH_UNTICKED` | Below 1.9, warning: the `Files:` reader found a path outside backticks; the plan is read as before (a warning under `--strict` too) | Backtick the path; from 1.9 an unticked path is refused |
|
||||
|
||||
**The plan gate (K3).** `node lib/validators/plan-gate.mjs <plan.md> [--json]` decides whether a plan may go on: **0** = it validates strict, its `## Critic Verdict` matches the current text, no BLOCKER is open, and every open MAJOR was accepted by the operator; **1** = stop, with `reasons` (`PLAN_GATE_INVALID_PLAN`, `PLAN_GATE_VERSION` — below plan_version 1.8, `PLAN_GATE_NO_VERDICT`, `PLAN_GATE_VERDICT_MALFORMED`, `PLAN_GATE_VERDICT_STALE`, `PLAN_GATE_OPEN_BLOCKER`, `PLAN_GATE_ROUNDS_EXHAUSTED`, `PLAN_GATE_OPEN_MAJOR` — rounds left: revise, `PLAN_GATE_OPEN_MAJOR_FINAL` — after the last round: ask the operator) and `warnings` (`PLAN_GATE_OPEN_MAJOR` for majors the operator accepted); **2** = could not read the plan. `--hash <plan.md>` prints the operative hash; Phase 9 takes it before the reviewers read the plan. `--record <plan.md> --reviewed <hash>` reads plan-review-dedup output on stdin (which lists the reviewers that delivered, `sources`) and writes the next round; exit 2 when a reviewer is missing or the plan changed since `--hash`; it refuses round 4 (exit 1, stop and ask) unless `--extra-round` follows the operator's yes, and refuses that flag before the cap is spent. Unrated findings count as blockers. The review log is excluded from the hash only up to its first other heading, so a step placed under it is hashed. `--accept-open <plan.md>` records the operator's "build as is" for the current round's open MAJORs (`accepted_open` in the verdict block, bound to the round and the plan hash; only after the last round; never for a BLOCKER; exit 1 when rounds are left, the verdict is stale, a blocker is open or no major is). Operator decision 2026-10-08: open majors used to be warnings, and a weakness the critics named was built and came back from `/trekreview` as MAJOR. plan-critic and scope-guardian rate on the review catalogue's tiers. The verdict is a trace of the review, not a signature.
|
||||
|
|
|
|||
|
|
@ -92,8 +92,12 @@ then the specifics.
|
|||
From plan_version 1.9 the rule is the file first on every line: each `Files:`
|
||||
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 (`none` or
|
||||
an empty `Files:` is refused). In `Files:`,
|
||||
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),
|
||||
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:`,
|
||||
every path stands before any annotation: `` `src/a.mjs`, `src/b.mjs` (new) ``,
|
||||
never `` `src/a.mjs` (new), `src/b.mjs` `` (in 1.9 that second path is
|
||||
description).
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue