docs(changelog): the second review remediation, decisions 27 to 31, with door lines

Review findings of S147; operator decisions 26 to 31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-14 12:55:03 +02:00
commit 2cca61170a
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 153 additions and 28 deletions

View file

@ -11,9 +11,9 @@ A new plan version, 1.9, in which every path in a step's `Files:` and on its
guesses paths from prose (decision 16). The same delivery closes the six
findings from the test repo's trial 4 and stops `npm test` from pointing a live
session's stop guard at a temp-repo lock. Every fix began with a test that
failed on the code before it. Suite 2459 (2457 pass, 0 fail, 2 skipped),
measured by `npm test` on the remediation's Step 13 commit (`5b94952`), the
commit before this section.
failed on the code before it. Suite 2508 (2506 pass, 0 fail, 2 skipped),
measured by `npm test` on the second remediation's Step 8 commit
(`f746f27`), the commit before this section.
Nothing here asks anything of users. Older plans warn, they do not fail: a plan
below 1.9 is read as in 6.3.1, apart from the fixes under Fixed, and gets one
@ -30,6 +30,11 @@ warning per step whose `Files:` names a path that is not backticked.
- two-way: the workflow review puts back every engine finding the coordinator did not return (operator decision 23) — opt-in path only; a revert restores the old floor.
- two-way: approval commits require a Co-Authored-By trailer — only Voyage's own commands call the CLI, and they pass it.
- two-way: explorer prompts, stamp order, test env — prose and test-side only.
- two-way: a 1.9 wrapped line keeps its paths only after a comma (operator decision 29), and a File: line with nothing after it takes its first wrapped line — a revert joins every wrapped line again.
- two-way: below 1.9 a test file after a note in brackets, and, plus or + is read when it is clearly a test (operator decision 28 and operator decision 31) — a revert reads only the run right after File: again.
- two-way: red proof reads the step's test files from the plan, prints them, and refuses a plan it cannot read (operator decision 30) — the older prose that passes --tests still runs against it.
- two-way: original_severity names what the kept finding asserted — only tests read the field.
- two-way: the workflow review fails closed when its coordinator returns nothing, and names every reviewer that did not run in allow_blocked_by — opt-in path only.
### Added
@ -68,11 +73,14 @@ warning per step whose `Files:` names a path that is not backticked.
such as `LICENSE` alone on a following line is read as a file.
- **The test files on the `Test first:` `File:` line** are the backticked
paths that stand right after `File:`, separated only by commas or spaces,
and every one of them is read, locked and copied for red proof, not only the
first. A path in the description after them is no test file, at every
plan_version (operator decision 20, 12.10; review `6c31212c`), so a step
whose `File:` line names its source in the description is no longer read as
tests-only.
and below plan_version 1.9 also each further path after a note in brackets
(`(new)`, `*(new)*`, `(existing …)`) or after `and`, `plus` or `+`, as long
as it is clearly a test (operator decision 28, 13.10, and
operator decision 31, 14.10; review `bd2520b0`). Every one of them is read,
locked and copied for red proof, not only the first. A path in the
description after them is no test file, at every plan_version
(operator decision 20, 12.10; review `6c31212c`), so a step whose `File:`
line names its source in the description is no longer read as tests-only.
- **A 1.9 `Test first:` names its test only on a line that opens with
`File:`**, the way the executor, K6 and red proof read it. Below 1.9 the
check reads as before.
@ -104,11 +112,15 @@ warning per step whose `Files:` names a path that is not backticked.
description, so a plain path after them is read as a file again, as 6.3.1
read it, and a step of two tests joined by `+` and a plain source is not
tests-only (review `182ceb93`).
- **A 1.9 entry keeps the paths on its wrapped lines, and a numbered `File:`
line names its test.** A line that is no list item continues the entry above
it, so a `Files:` list wrapped after a comma names every file: the step is
not tests-only, and K6 plans the second file too. The `Test first:` `File:`
line may open with `-`, `*`, `+`, `1.` or `1)`; a wrapped line joins it, and
- **A 1.9 entry keeps the paths of a line wrapped after a comma, and a
numbered `File:` line names its test.** A line that is no list item keeps
its paths only after a comma, when the entry so far ends with one
(operator decision 29, 14.10; reviews `d21ff81e`, `eaeb398e`), so a
`Files:` list wrapped after a comma names every file: the step is not
tests-only, and K6 plans the second file too. Otherwise that line, and
every later wrapped line of the entry, is description and never read. A
`File:` line with nothing after it takes its first wrapped line. The
`Test first:` `File:` line may open with `-`, `*`, `+`, `1.` or `1)`, and
any list item ends it (reviews `b4fa1927`, `4b6460e5`).
- **A 1.9 plan refuses placeholders and mixed-case bare names as files**
(operator decision 22, 13.10; review `33f37f53`). A leading backticked span
@ -151,17 +163,19 @@ warning per step whose `Files:` names a path that is not backticked.
- **The plan validator parses each step once.** `validatePlanContent` reads a
step's fields once and every check, `--steps-from` included, reads them from
there; its errors and warnings are unchanged (review `93382464`).
- **Red proof checks the test list against the plan, and the executor defines
the step's test files once** (review `cf241526`). With `--plan` and
`--step`, `red-proof.mjs` reads the step's test files from the plan as
decision 20 sets them: a `--tests` that names another set is exit 2 with a
reason that starts `tests-mismatch` and the plan's list in `plan_tests`;
without `--tests` the plan's list is used; a step whose plan names no test
file reads as before. `commands/trekexecute.md` defines the step's test
files in one sentence the other places point to: after a `tests-mismatch`
the executor reruns red proof once with `plan_tests`, and a second exit 2 is
read as the exit-2 row says. `step-implementer` reruns the same way, and its
lock's `--files` is the list red proof accepted.
- **Red proof reads the step's test files from the plan and prints them, and
the executor defines them once** (review `cf241526`;
operator decision 30, 14.10; reviews `793fc1aa`, `09ae46d8`, `d01df508`,
`11ee78c5`). With `--plan` and `--step`, `red-proof.mjs` reads the step's
test files from the plan as decision 20 and, below 1.9, decision 31 set
them, and prints the list it ran as `tests` (a `tests:` line in text mode).
The helper and the executor pass no `--tests`, and the lock's `--files` is
that printed list. A `--tests` that names another set is exit 2 with a
reason that starts `tests-mismatch` and the plan's list in `plan_tests`; a
plan or step it cannot read is exit 2 with a reason that starts
`plan-unreadable`, and the executor corrects its own call; a step whose
plan names no test file reads as before. `commands/trekexecute.md` defines
the step's test files in one sentence the other places point to.
- **The `/trekbrief` pass branch names no `## Brief Quality`, and the stats
rows span their blocks** (operator decision 21, 12.10). That section belongs
to the exhausted and force-stopped branches only. In
@ -169,6 +183,21 @@ warning per step whose `Files:` names a path that is not backticked.
and trekreview stats rows runs from the phase heading to the closing fence of
its stats-append block, and a test pins all four command rows (trekexecute's
too) by exact equality.
- **`original_severity` names what the kept finding asserted** (review
`08deae68`). When Pass 1 raises a finding to its duplicate group's
severity, Pass 3 writes `original_severity` from the finding's own
severity, and only when that differs from its catalogue tier; the carrier
field `pass1_raised_from` never leaves the contract.
- **The workflow review fails closed when its coordinator returns nothing**
(review `edf2ec56`). A coordinator that returns no verdict and findings
counts as a missing reviewer: the merged findings are read at their
catalogue tier or higher (never lowered), the verdict starts at WARN (a
BLOCK stays BLOCK), and every reviewer, engine and coordinator that did not
deliver is named in `allow_blocked_by` as `missing-reviewer:<name>`, so the
verdict gate exits 2 on such a WARN.
- **K6 keeps its own normaliser and `LIST_ITEM` is one literal again**
(operator decision 27, 13.10; review `7961a700`). The two `/simplify` edits
that crossed the first remediation's fence around K6 are reverted.
### Known issues

View file

@ -344,7 +344,7 @@ test('CHANGELOG: the Unreleased section\'s Known issues name the session split',
// right after File:), the workflow put-back names operator decision 23, the
// engine-provenance entry states the tier rank instead of "on a severity tie",
// Known issues name the default path's verdict gate (operator decision 25), and
// the intro's measurement names the remediation's Step 13 commit. Word
// the intro's measurement names the second remediation's Step 8 commit. Word
// boundaries keep "operator decision 2026-10-12" from counting as decision 20.
test('CHANGELOG: the door lines name operator decisions 20 and 23, and none says "every test file on the Test first File line"', () => {
const { heading, body } = firstChangelogSection();
@ -373,12 +373,108 @@ test('CHANGELOG: Known issues name the verdict gate, the engine payloads and ope
assert.match(known, /\boperator\s+decision\s+25\b/, `Known issues name operator decision 25: ${JSON.stringify(known)}`);
});
test('CHANGELOG: the intro no longer names acd0e3d and names the remediation\'s Step 13 commit', () => {
test('CHANGELOG: the intro names the second remediation\'s Step 8 commit, and no longer acd0e3d, 5b94952 or the Step 13 commit', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const firstSub = body.search(/^### /m);
const intro = firstSub < 0 ? body : body.slice(0, firstSub);
assert.ok(!intro.includes('acd0e3d'), `the intro no longer names acd0e3d: ${JSON.stringify(intro)}`);
assert.match(intro, /the\s+remediation's\s+Step\s+13\s+commit/,
`the intro names "the remediation's Step 13 commit": ${JSON.stringify(intro)}`);
assert.ok(!intro.includes('5b94952'), `the intro no longer names 5b94952: ${JSON.stringify(intro)}`);
assert.doesNotMatch(intro, /the\s+remediation's\s+Step\s+13\s+commit/,
`the intro no longer names "the remediation's Step 13 commit": ${JSON.stringify(intro)}`);
assert.match(intro, /the\s+second\s+remediation's\s+Step\s+8\s+commit/,
`the intro names "the second remediation's Step 8 commit": ${JSON.stringify(intro)}`);
});
// Second remediation, Step 9: the CHANGELOG records round 2. Door lines name
// operator decisions 29, 30 and 31; the Fixed bullets that name `b4fa1927`,
// `6c31212c` and `cf241526` follow decisions 29, 31 and 30; three new Fixed
// bullets name `08deae68`, `edf2ec56` and `7961a700` (operator decision 27).
// Each bullet is read on its own with bullet(): the CHANGELOG's bullets have no
// blank line between them. Phrases are looked for with runs of whitespace
// read as one space, so a phrase wrapped over two lines still counts.
const oneSpace = (text) => text.replace(/\s+/g, ' ');
test('CHANGELOG: door lines name operator decisions 29, 30 and 31, none bad, and the decision-29 line says "takes its first wrapped line"', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const d = releaseCheck.parseDoors(body);
assert.deepEqual(d.bad, [], 'every top-level door line reads "- one-way: ..." or "- two-way: ..."');
const texts = d.entries.map((e) => e.text);
for (const re of [/\boperator decision 29\b/, /\boperator decision 30\b/, /\boperator decision 31\b/]) {
assert.ok(texts.some((t) => re.test(t)), `a door line matches ${re}: ${JSON.stringify(texts)}`);
}
const d29 = texts.filter((t) => /\boperator decision 29\b/.test(t));
assert.ok(d29.some((t) => /takes\s+its\s+first\s+wrapped\s+line/.test(t)),
`the door line naming operator decision 29 says "takes its first wrapped line": ${JSON.stringify(d29)}`);
});
test('CHANGELOG: the section names 08deae68, edf2ec56 and operator decision 27', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
assert.ok(body.includes('08deae68'), 'the Unreleased section names 08deae68');
assert.ok(body.includes('edf2ec56'), 'the Unreleased section names edf2ec56');
assert.match(body, /\boperator decision 27\b/, 'the Unreleased section names operator decision 27');
});
test('CHANGELOG: the b4fa1927 bullet follows operator decision 29', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const b = bullet(body, /b4fa1927/, 'Fixed bullet b4fa1927');
assert.match(b, /\boperator decision 29\b/, `the b4fa1927 bullet names operator decision 29: ${JSON.stringify(b)}`);
assert.ok(b.includes('4b6460e5'), `the b4fa1927 bullet names 4b6460e5: ${JSON.stringify(b)}`);
assert.match(b, /only\s+after\s+a\s+comma/, `the b4fa1927 bullet says "only after a comma": ${JSON.stringify(b)}`);
assert.match(b, /takes\s+its\s+first\s+wrapped\s+line/, `the b4fa1927 bullet says "takes its first wrapped line": ${JSON.stringify(b)}`);
const flat = oneSpace(b);
assert.ok(!flat.includes('a wrapped line joins it'), `the b4fa1927 bullet no longer says "a wrapped line joins it": ${JSON.stringify(b)}`);
assert.ok(!flat.includes('keeps the paths on its wrapped lines'),
`the b4fa1927 bullet no longer says "keeps the paths on its wrapped lines": ${JSON.stringify(b)}`);
});
test('CHANGELOG: the 6c31212c bullet names operator decision 31 below 1.9 and keeps decision 20 at every plan_version', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const b = bullet(body, /6c31212c/, 'Fixed bullet 6c31212c');
assert.match(b, /\boperator decision 31\b/, `the 6c31212c bullet names operator decision 31: ${JSON.stringify(b)}`);
assert.match(b, /\bplus\b/, `the 6c31212c bullet names plus: ${JSON.stringify(b)}`);
assert.match(b, /separated\s+only\s+by\s+commas\s+or\s+spaces,\s+and\s+below\s+plan_version\s+1\.9/,
`the 6c31212c bullet qualifies "separated only by commas or spaces" below 1.9: ${JSON.stringify(b)}`);
assert.match(b, /at\s+every\s+plan_version\s+\(operator\s+decision\s+20\b/,
`the 6c31212c bullet keeps "at every plan_version (operator decision 20": ${JSON.stringify(b)}`);
});
test('CHANGELOG: the edf2ec56, 08deae68 and 7961a700 bullets say what was fixed', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const e = bullet(body, /edf2ec56/, 'Fixed bullet edf2ec56');
assert.match(e, /allow_blocked_by/, `the edf2ec56 bullet names allow_blocked_by: ${JSON.stringify(e)}`);
assert.match(e, /catalogue\s+tier\s+or\s+higher/, `the edf2ec56 bullet says "catalogue tier or higher": ${JSON.stringify(e)}`);
const o = bullet(body, /08deae68/, 'Fixed bullet 08deae68');
assert.match(o, /own\s+severity/, `the 08deae68 bullet says "own severity": ${JSON.stringify(o)}`);
assert.match(o, /pass1_raised_from/, `the 08deae68 bullet names pass1_raised_from: ${JSON.stringify(o)}`);
const k = bullet(body, /7961a700/, 'Fixed bullet 7961a700');
assert.match(k, /\boperator decision 27\b/, `the 7961a700 bullet names operator decision 27: ${JSON.stringify(k)}`);
});
test('CHANGELOG: the cf241526 bullet follows operator decision 30', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const b = bullet(body, /cf241526/, 'Fixed bullet cf241526');
assert.match(b, /\boperator decision 30\b/, `the cf241526 bullet names operator decision 30: ${JSON.stringify(b)}`);
assert.match(b, /pass\s+no\s+`--tests`/, `the cf241526 bullet says the callers pass no \`--tests\`: ${JSON.stringify(b)}`);
assert.match(b, /decision\s+20\s+and,\s+below\s+1\.9,\s+decision\s+31/,
`the cf241526 bullet says "decision 20 and, below 1.9, decision 31": ${JSON.stringify(b)}`);
assert.match(b, /printed\s+list/, `the cf241526 bullet names the printed list: ${JSON.stringify(b)}`);
assert.ok(b.includes('plan-unreadable'), `the cf241526 bullet names plan-unreadable: ${JSON.stringify(b)}`);
assert.ok(b.includes('tests-mismatch'), `the cf241526 bullet names tests-mismatch: ${JSON.stringify(b)}`);
assert.doesNotMatch(b, /as\s+decision\s+20\s+sets\s+them/, `the cf241526 bullet no longer says "as decision 20 sets them": ${JSON.stringify(b)}`);
});
test('CHANGELOG: the old rerun wording is gone from the Unreleased section', () => {
const { heading, body } = firstChangelogSection();
assert.equal(heading, UNRELEASED);
const flat = oneSpace(body);
for (const phrase of ['reruns red proof once with `plan_tests`', 'reruns the same way', 'the list red proof accepted']) {
assert.ok(!flat.includes(phrase), `the Unreleased section no longer says "${phrase}"`);
}
});