voyage/tests/lib/fix-round-4-5b-prose.test.mjs
Kjell Tore Guttormsen ad053e0e0e
docs(changelog): fix round 4.5b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:02:58 +01:00

602 lines
37 KiB
JavaScript

// tests/lib/fix-round-4-5b-prose.test.mjs
// Prose pins for fix round 4-5b (plan
// .claude/projects/2026-10-25-fix-round-4-5b-findings): the fixes that are
// text a session or a helper reads. The tags in brackets are 4.5b ids from the
// round's brief (for example `[M4]`, `[M11]`); each tagged test fails on an
// assertion at be0b631, the round's start point. A sibling's test is tagged
// `[<parent id>] sibling:`.
// M4 A git call the verdict gate cannot complete is reported as
// `review-not-measured:`, never as a stale review: Handover 6 names the
// prefix and lists review_version 1.1 as current, and /trekexecute's
// main-merge gate maps it to "not measured" with the merge locked.
// M11 Handover 6 states that only a full 40- or 64-hex reviewed commit is read.
// E22e The park tool parks every file new in the attempt's diff, untracked or
// staged but not in HEAD: its header and /trekexecute's resume paragraph
// no longer say "still untracked".
// M3 The "Back to planning" save line runs its git diff with --no-relative,
// so a session in a subdirectory with diff.relative on saves every file.
// M9 The review coordinator's Pass 2 text says what the lib does: a root
// escape or a negative line refutes a finding; any other line number
// alone never does, and a line past the end goes to `unverified`.
// M2 The four flag-table summaries of `--quick` (trekreview, orchestrator,
// command-modes, README) name the brief-conflict rule as the exception
// to the Pass 3 skip.
// K3 The orchestrator's 4-pass item 3 names the brief-conflict lowering as
// the exception to the catalogue reset.
// ARG /trekendsession and /trekcontinue Phase 0 and /trekreview Phase 1
// give $ARGUMENTS to the arg parser (`--stdin`) as the one body line of
// a quoted heredoc, never as shell words.
// M8 /trekcontinue and /trekendsession Phase 0 stop on the parser's exit 1
// with its `errors`, and stop when the parser prints no JSON.
// M10 /trekendsession Phase 2 reads the label from the parser's `tail`.
// M13 /trekbrief's research_topics = 0 exit runs Phase 7 with
// `auto_result: manual`.
// K2 /trekbrief's Step 6a "Cancel — do manual" row runs Phase 7 with
// `auto_result: cancelled`, like the sentence after the table.
// E14 /trekexecute keeps `run_seen` in the progress file (template, field
// table, Phase 7) and Phase 8 reads it from there, so the resume and
// multi-session paths report it; Handover 5 lists it as optional.
// DJ4 /trekexecute's Phase 7.2 step 3 commits only the review's paths git
// does not ignore, and says an ignored one is left uncommitted.
// E17a The two /trekresearch wave 2 sentences on outbound queries are pinned
// exactly (red under the E17a mutation).
// E17b The deep-research adapter says where wave 2's contrarian findings merge.
// Da The README deep-research parenthesis says "when the engine is active".
// E18a The F9 deep-review and summary-only rows are pinned exactly (red under
// the E18a mutation).
// Db No README line stars security-researcher (red under the Db mutation).
// M1 No test name in the 4.5a prose file starts with `found in execution:`;
// the tests f6bdc2d added carry their 4.5a parent tag (red under the
// M1 mutation, which puts the names back).
// CHANGELOG (the round's entry in the existing Unreleased section):
// K4 the siblings entry says the commands read only the two-token
// `--gates <value>` form and that `--gates=true` is the gates shim's own.
// M4 the merge-lock entry says a failed git call is reported as
// `review-not-measured:`, not `review-stale`.
// M2 ### Fixed records the brief-conflict exception in the four `--quick`
// summaries.
// ARG ### Fixed names the Bash-guard block on a parser heredoc that closes
// early (R6), and ### Changed says `~` and `$HOME` in the arguments are
// no longer expanded.
// No static import of a new export: the file must load at be0b631.
// Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const readCache = new Map();
/** A repo file's text; the file must exist. Read from disk once per run. */
function read(rel) {
if (!readCache.has(rel)) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
}
return readCache.get(rel);
}
/** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim();
/** The text from the line holding `start` up to (not including) the next line holding `stop`; both must exist. */
function between(rel, start, stop) {
const text = read(rel);
const s = text.indexOf(start);
assert.ok(s !== -1, `${rel} must hold "${start}"`);
const e = text.indexOf(stop, s + start.length);
assert.ok(e !== -1, `${rel} must hold "${stop}" after "${start}"`);
return text.slice(s, e);
}
/** Handover 6 of the handover contracts, up to Handover 7. */
const handover6 = () => between('docs/HANDOVER-CONTRACTS.md', '## Handover 6 ', '## Handover 7 ');
/** The one table row of Handover 6 whose first cell is `field`. */
function fieldRow(field) {
const rows = handover6().split('\n').filter((l) => l.startsWith(`| \`${field}\` |`));
assert.equal(rows.length, 1, `Handover 6 must hold exactly one \`${field}\` row; found ${rows.length}`);
return rows[0];
}
/** The verdict-gate paragraph of Handover 6 (the "Second consumer" one), flattened. */
function gateParagraph() {
const para = handover6().split('\n').filter((l) => l.startsWith('**Second consumer — the verdict gate'));
assert.equal(para.length, 1, `Handover 6 must hold one verdict-gate paragraph; found ${para.length}`);
return flat(para[0]);
}
/** Every sentence of `text` (flattened) that holds `needle`. */
function sentencesWith(text, needle) {
return flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => s.includes(needle));
}
// --- M4 ----------------------------------------------------------------------------
test('[M4] Handover 6 names the review-not-measured reason', () => {
const hits = sentencesWith(gateParagraph(), '`review-not-measured:`');
assert.ok(hits.length > 0, 'the verdict-gate paragraph of Handover 6 does not name the `review-not-measured:` reason prefix');
assert.ok(hits.some((s) => /\bgit\b/.test(s) && /never green|not measured/.test(s)),
`no sentence ties \`review-not-measured:\` to a failed git call read as not measured: ${hits.join(' | ')}`);
});
test('[M4] Handover 6 lists review_version 1.1 as current', () => {
const row = fieldRow('review_version');
assert.match(row, /"1\.1"`? \(current\)/, `the review_version row does not list 1.1 as current: ${row}`);
assert.doesNotMatch(row, /"1\.0"`? \(current\)/, `the review_version row still lists 1.0 as current: ${row}`);
});
test('[M4] the main merge gate maps review-not-measured to not measured', () => {
const gate = between('commands/trekexecute.md', '**1. The verdict.**', '**2. Open:');
const hits = sentencesWith(gate, '`review-not-measured:`');
assert.ok(hits.length > 0, 'the main-merge gate (Phase 8, 1. The verdict) does not name the `review-not-measured:` reason prefix');
const ok = hits.find((s) => {
const rest = s.split('`review-not-measured:`').join('');
return /"not measured"|\bnot measured\b/.test(rest) && /never green/.test(rest) && /merge stays (locked|closed)/.test(rest);
});
assert.ok(ok, `no sentence says a \`review-not-measured:\` reason is reported as "not measured", never green, with the merge locked: ${hits.join(' | ')}`);
});
// --- M11 ---------------------------------------------------------------------------
test('[M11] Handover 6 states the full 40- or 64-hex reviewed commit', () => {
const row = fieldRow('scope_sha_end');
assert.match(row, /full 40- or 64-hex/, `the scope_sha_end row does not state the full 40- or 64-hex commit id: ${row}`);
assert.match(gateParagraph(), /`scope_sha_end`[^.]*full 40- or 64-hex/,
'the verdict-gate paragraph of Handover 6 does not state that only a full 40- or 64-hex scope_sha_end is read');
});
// --- E22e --------------------------------------------------------------------------
/** The sentences of `text` (flattened) that say what the park tool moves or parks. */
function parkSentences(text) {
return flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => /\b(moves|parks)\b/.test(s) && /\bfile\b/.test(s));
}
test('[E22e] the park header says untracked or staged but not in HEAD', () => {
const src = read('lib/util/park-attempt-files.mjs');
const header = src.slice(0, src.indexOf('\nimport '));
assert.ok(header.length > 0, 'lib/util/park-attempt-files.mjs has no header before its imports');
const comment = header.split('\n').map((l) => l.replace(/^\/\/ ?/, '')).join('\n');
assert.doesNotMatch(flat(comment), /still untracked/, 'the park header still says the tool parks only files "still untracked"');
const hits = parkSentences(comment);
assert.ok(hits.some((s) => /new in the attempt's diff/.test(s) && /untracked, or staged but not in HEAD/.test(s)),
`no header sentence says every file new in the attempt's diff is parked, untracked, or staged but not in HEAD: ${hits.join(' | ')}`);
});
test('[E22e] the execute command says the same', () => {
const para = between('commands/trekexecute.md', 'A step at `fix_rounds` 4 on `--resume` was re-planned', 'Then launch a fresh');
assert.doesNotMatch(flat(para), /still untracked/, 'the /trekexecute resume paragraph still says the tool moves only files "still untracked"');
const hits = parkSentences(para);
assert.ok(hits.some((s) => /new in the attempt's diff/.test(s) && /untracked, or staged but not in HEAD/.test(s)),
`no /trekexecute sentence says every file new in the attempt's diff is parked, untracked, or staged but not in HEAD: ${hits.join(' | ')}`);
});
// --- M3 ----------------------------------------------------------------------------
test('[M3] the back-to-planning save line carries --no-relative', () => {
const lines = read('commands/trekexecute.md').split('\n').filter((l) => l.startsWith('**Back to planning** (`fix_rounds = 4`): save `'));
assert.equal(lines.length, 1, `expected one "Back to planning" save line; found ${lines.length}`);
const m = lines[0].match(/save `([^`]+)`/);
assert.ok(m, `no backticked command on the save line: ${lines[0]}`);
assert.match(m[1], /^git (?:-c \S+ )*diff (?:--\S+ )*--no-relative (?:--\S+ )*\{start_sha\}$/,
`the save line's git diff does not carry --no-relative: ${m[1]}`);
});
// --- M9 ----------------------------------------------------------------------------
test('[M9] the coordinator text does not say a line number alone never refutes', () => {
const coord = flat(read('agents/review-coordinator.md'));
assert.doesNotMatch(coord, /\bA line number alone never refutes/,
'agents/review-coordinator.md still says a line number alone never refutes, next to a negative line that does');
const p2 = between('agents/review-coordinator.md', '### Pass 2 — HubSpot Judge filters', '### Pass 3 — ');
assert.ok(sentencesWith(p2, 'a citation that escapes the repo root, or a negative line, refutes the finding').length > 0,
'Pass 2 no longer says a root escape or a negative line refutes the finding');
const hits = sentencesWith(p2, 'line number alone never refutes');
assert.ok(hits.some((s) => /\b[Aa]ny other line number alone never refutes\b/.test(s)
&& /`unverified` \(`accuracy:line-out-of-range`\)/.test(s)),
`no Pass 2 sentence says any other line number alone never refutes and goes to \`unverified\`: ${hits.join(' | ')}`);
});
// --- M2 ----------------------------------------------------------------------------
/** The one line of `rel` that starts with `prefix` and holds `needle`. */
function oneLine(rel, prefix, needle) {
const hits = read(rel).split('\n').filter((l) => l.startsWith(prefix) && l.includes(needle));
assert.equal(hits.length, 1, `${rel} must hold exactly one line starting "${prefix}" with "${needle}"; found ${hits.length}`);
return hits[0];
}
const BRIEF_CONFLICT_SKIP = /the coordinator's reasonableness filter,? except the brief-conflict rule\b/;
test('[M2] each of the four flag-table summaries names the brief-conflict exception', () => {
const summaries = [
['commands/trekreview.md --quick row', oneLine('commands/trekreview.md', '| `--quick` |', 'reasonableness filter')],
['agents/review-orchestrator.md quick mode', flat(between('agents/review-orchestrator.md', '- `quick` — ', '- `validate` — '))],
['docs/command-modes.md --quick row', oneLine('docs/command-modes.md', '| `--quick` |', 'reasonableness filter')],
['README.md Quick row', oneLine('README.md', '| **Quick** | `/trekreview ', 'reasonableness filter')],
];
for (const [where, text] of summaries) {
assert.match(text, BRIEF_CONFLICT_SKIP, `${where} skips the reasonableness filter without the brief-conflict exception: ${text}`);
}
});
// --- K3 ----------------------------------------------------------------------------
test('[K3] orchestrator item 3 names the brief-conflict lowering as the exception to the reset', () => {
const item = flat(between('agents/review-orchestrator.md', '3. **Cloudflare reasonableness**', '4. **Compute verdict**'));
const at = item.indexOf('reset a severity that does not match its catalogue tier.');
assert.ok(at !== -1, `item 3 no longer resets a severity to its catalogue tier: ${item}`);
const next = sentencesWith(item.slice(at + 'reset a severity that does not match its catalogue tier.'.length), ' ')[0] ?? '';
assert.match(next, /\bexception to (the|this) reset\b/, `the sentence after the reset does not name an exception to it: ${next}`);
assert.match(next, /brief-conflict lowering/, `the exception to the reset is not the brief-conflict lowering: ${next}`);
assert.match(next, /MAJOR[^.]*\bMINOR\b/, `the exception does not say a MAJOR drops to MINOR: ${next}`);
assert.match(next, /`brief_lowered_from: MAJOR`/, `the exception does not name brief_lowered_from: MAJOR: ${next}`);
});
// --- ARG / M8 / M10 ----------------------------------------------------------------
const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
const ARGS_OPENER = `<<'${ARGS_DELIM}'`;
/** The text of a `## ` section of `rel` from the line starting with `heading` to the next `## ` line outside a fence. */
function phase(rel, heading) {
const lines = read(rel).split('\n');
const s = lines.findIndex((l) => l.startsWith(heading));
assert.ok(s !== -1, `${rel} must hold a heading starting "${heading}"`);
let fenced = false;
for (let i = s + 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
if (!fenced && lines[i].startsWith('## ')) return lines.slice(s, i).join('\n');
}
return lines.slice(s).join('\n');
}
// [file, command, the section that runs the parser]
const PARSER_PHASES = [
['commands/trekendsession.md', 'trekendsession', '## Phase 0 — `--help` handling'],
['commands/trekcontinue.md', 'trekcontinue', '## Phase 0 — `--help` handling'],
['commands/trekreview.md', 'trekreview', '## Phase 1 — Parse mode and validate input'],
];
/** The one runtime ```bash block of the section that runs the arg parser for `cmd`, as lines, with the parser line's index. */
function parserBlock(rel, cmd, heading) {
const text = phase(rel, heading);
const blocks = text.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0])
.filter((b) => b.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
assert.equal(blocks.length, 1, `${rel} ${heading} must hold one \`\`\`bash block that runs the ${cmd} arg parser; found ${blocks.length}`);
const lines = blocks[0].split('\n');
const at = lines.findIndex((l) => l.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
return { lines, at };
}
test('[ARG] each of the three parser lines reads the arguments from a quoted heredoc', () => {
for (const [rel, cmd, heading] of PARSER_PHASES) {
const { lines, at } = parserBlock(rel, cmd, heading);
const opener = lines[at].trim();
assert.ok(opener.endsWith(` ${ARGS_OPENER}`),
`${rel}: the parser line does not end with the quoted heredoc opener ${ARGS_OPENER}: ${opener}`);
assert.equal(lines[at + 1], '$ARGUMENTS',
`${rel}: the line inside the heredoc is not $ARGUMENTS alone: ${JSON.stringify(lines[at + 1])}`);
assert.match(opener, new RegExp(`^node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/parsers/arg-parser\\.mjs --command ${cmd} --stdin <<`),
`${rel}: the parser line does not read stdin (--command ${cmd} --stdin right after arg-parser.mjs): ${opener}`);
const filled = lines.filter((l) => l.trim() !== '');
assert.ok(lines[at + 2] === ARGS_DELIM && filled[filled.length - 1] === ARGS_DELIM,
`${rel}: the closing line ${ARGS_DELIM} does not follow $ARGUMENTS as the block's last non-empty line: ${JSON.stringify(lines)}`);
}
});
test('[ARG] no line outside the heredoc carries $ARGUMENTS in those blocks', () => {
for (const [rel, cmd, heading] of PARSER_PHASES) {
const { lines } = parserBlock(rel, cmd, heading);
const open = lines.findIndex((l) => l.trimEnd().endsWith(ARGS_OPENER));
assert.ok(open !== -1, `${rel}: the parser block opens no ${ARGS_OPENER} heredoc: ${JSON.stringify(lines)}`);
const close = lines.findIndex((l, i) => i > open && l.trim() === ARGS_DELIM);
assert.ok(close !== -1, `${rel}: the parser block's heredoc has no closing line ${ARGS_DELIM}`);
const outside = lines.filter((l, i) => (i <= open || i >= close) && l.includes('$ARGUMENTS'));
assert.deepEqual(outside, [], `${rel}: a line outside the heredoc carries $ARGUMENTS`);
}
});
const PHASE0 = '## Phase 0 — `--help` handling';
const STOP_COMMANDS = ['commands/trekcontinue.md', 'commands/trekendsession.md'];
test('[M8] /trekcontinue and /trekendsession Phase 0 stop on exit 1 with the parser\'s errors', () => {
for (const rel of STOP_COMMANDS) {
const hits = sentencesWith(phase(rel, PHASE0), 'exit 1');
assert.ok(hits.some((s) => /^On exit 1, print (its|the parser's) `errors` and stop\.$/.test(s)),
`${rel} Phase 0 does not say: on exit 1, print the parser's \`errors\` and stop: ${hits.join(' | ')}`);
}
});
test('[M8] /trekcontinue and /trekendsession Phase 0 stop when the parser prints no JSON', () => {
for (const rel of STOP_COMMANDS) {
const hits = sentencesWith(phase(rel, PHASE0), 'no JSON');
assert.ok(hits.some((s) => /\bprints no JSON\b/.test(s) && /\bthe parser did not run\b/.test(s) && /\band stop\.$/.test(s)),
`${rel} Phase 0 does not say: when the parser prints no JSON, say that it did not run and stop: ${hits.join(' | ')}`);
}
});
test('[M10] /trekendsession Phase 2 reads the label from tail', () => {
const p2 = flat(phase('commands/trekendsession.md', '## Phase 2 — Required args check'));
assert.doesNotMatch(p2, /`positional\[1\]`/, 'Phase 2 still reads <next-label> from positional[1], which splits a label at spaces');
assert.match(p2, /`<next-label>` is the parser's `tail`/, `Phase 2 does not read <next-label> from the parser's tail: ${p2}`);
});
// --- M13 / K2 ----------------------------------------------------------------------
const TREKBRIEF = 'commands/trekbrief.md';
test('[M13] the research_topics = 0 exit names auto_result manual', () => {
const opening = between(TREKBRIEF, '## Phase 5 — Auto-orchestration opt-in', 'Ask the user via `AskUserQuestion`');
const hits = sentencesWith(opening, 'research_topics = 0');
assert.equal(hits.length, 1, `Phase 5 must hold one sentence on research_topics = 0; found ${hits.length}: ${hits.join(' | ')}`);
assert.match(hits[0], /, then run Phase 7 \(`auto_result: manual`\) and stop\.$/,
`the research_topics = 0 exit does not run Phase 7 with auto_result manual: ${hits[0]}`);
});
test('[K2] the 6a row names Phase 7 with auto_result cancelled', () => {
const step6a = between(TREKBRIEF, '### Step 6a — Confirm proceed', '### Step 6b ');
const rows = step6a.split('\n').filter((l) => l.startsWith('| **Cancel — do manual** |'));
assert.equal(rows.length, 1, `Step 6a must hold one "Cancel — do manual" row; found ${rows.length}`);
assert.equal(rows[0], '| **Cancel — do manual** | Revert to manual path (run Phase 7 with `auto_result: cancelled`, print commands, stop). |',
'the 6a cancel row does not name Phase 7 with auto_result cancelled');
assert.match(flat(step6a), /If cancelled → fall back to manual path output, run Phase 7 \(`auto_result: cancelled`\) and stop\./,
'the sentence after the 6a table no longer names Phase 7 with auto_result cancelled');
});
// --- DJ1 / DJ2 ---------------------------------------------------------------------
const TREKPLAN = 'commands/trekplan.md';
test('[DJ1] the execute and team exits run Phase 12', () => {
const execute = flat(between(TREKPLAN, '### "execute" / "go" / "start"', '### "execute with team" / "team"'));
assert.match(execute, /`\/trekexecute \{plan_path\}` without a project directory — then run Phase 12 \(`outcome: execute`\)\./,
`the "execute" exit does not run Phase 12 with outcome execute: ${execute}`);
assert.doesNotMatch(execute, /\band stop\b/, `the "execute" exit still ends in a bare "and stop": ${execute}`);
const team = flat(between(TREKPLAN, '### "execute with team" / "team"', '## Phase 12 — Session tracking'));
assert.match(team, /There is no team path\. Print the same `\/trekexecute` line, then run Phase 12 \(`outcome: execute_team`\);/,
`the "team" exit does not run Phase 12 with outcome execute_team: ${team}`);
assert.doesNotMatch(team, /\band stop\b/, `the "team" exit still ends in a bare "and stop": ${team}`);
const p12 = between(TREKPLAN, '## Phase 12 — Session tracking', '## Profile');
assert.ok(p12.includes('"outcome": "{execute|execute_team|save|refine}"'),
'Phase 12 no longer holds the outcome enum the two exits name');
});
test('[DJ2] the high-effort loop names the model for a turn that spawns', () => {
const loop = between('commands/trekresearch.md', '### Per-turn protocol', '### Exits (all three, always one of them)');
const hits = sentencesWith(loop, 'spawns');
assert.ok(hits.some((s) => /^A turn that spawns an agent passes `model: phase_signal_result\.model`, else `opus`/.test(s)),
`the per-turn protocol does not say a turn that spawns passes model: phase_signal_result.model, else opus: ${hits.join(' | ')}`);
assert.doesNotMatch(loop, /model: "opus"/, 'the per-turn protocol pins a spawn to model: "opus"');
});
// --- E14 ---------------------------------------------------------------------------
/** The progress-file template of /trekexecute's Phase 3, parsed as JSON. */
function progressTemplate() {
const block = between('commands/trekexecute.md', '### Progress file schema', '**The step-loop fields');
const m = block.match(/```json\n([\s\S]*?)\n```/);
assert.ok(m, 'the Progress file schema section holds no ```json block');
return JSON.parse(m[1]);
}
test('[E14] the progress template carries run_seen', () => {
const tpl = progressTemplate();
assert.ok(Object.prototype.hasOwnProperty.call(tpl, 'run_seen'),
'the progress template has no top-level run_seen field');
assert.equal(tpl.run_seen, null, `the progress template's run_seen is not null: ${JSON.stringify(tpl.run_seen)}`);
assert.ok(!Object.prototype.hasOwnProperty.call(tpl.steps['1'], 'run_seen'),
'run_seen sits in the per-step record, not at the top level');
});
test('[E14] the field table describes run_seen', () => {
const table = between('commands/trekexecute.md', '**The step-loop fields', '**The coupling fields');
const rows = table.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
assert.equal(rows.length, 1, `the step-loop field table must hold exactly one run_seen row; found ${rows.length}`);
const cells = rows[0].split(' | ');
assert.match(cells[1], /^Phase 7$/, `the run_seen row is not written in Phase 7: ${rows[0]}`);
assert.match(rows[0], /Claude Code's (built-in )?`run`/, `the run_seen row does not say it is what Claude Code's run showed: ${rows[0]}`);
assert.match(rows[0], /`null` = not run/, `the run_seen row does not say null = not run: ${rows[0]}`);
});
test('[E14] Phase 8 reads run_seen from progress.json', () => {
const p7 = between('commands/trekexecute.md', '### End to end — "run the thing"', '### Architecture rules (K5)');
const rec = sentencesWith(p7, 'Record `run_seen`');
assert.equal(rec.length, 1, `Phase 7 must hold one "Record \`run_seen\`" sentence; found ${rec.length}`);
assert.match(rec[0], /\bin the progress file\b|\bto the progress file\b|`progress\.json`/,
`Phase 7 records run_seen without writing it to the progress file: ${rec[0]}`);
const p8 = between('commands/trekexecute.md', '## Phase 8 — Final report', '## Phase 9 — Stats tracking');
const hits = sentencesWith(p8, '`run_seen`');
const ok = hits.find((s) => /\bread\b|\breads\b/.test(s)
&& /progress file|`progress\.json`/.test(s)
&& /\*\*Seen with `run`:\*\*/.test(s)
&& /summary/.test(s)
&& /resume/.test(s) && /multi-session/.test(s));
assert.ok(ok, `no Phase 8 sentence says the Seen with run line and the summary's run_seen are read from the progress file, so the resume and multi-session paths report it: ${hits.join(' | ')}`);
});
test('[E14] Handover 5 lists run_seen as optional', () => {
const h5 = between('docs/HANDOVER-CONTRACTS.md', '## Handover 5 ', '## Handover 6 ');
const rows = h5.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
assert.equal(rows.length, 1, `Handover 5 must hold exactly one run_seen row; found ${rows.length}`);
const cells = rows[0].split(' | ');
assert.equal(cells[2], 'optional', `the run_seen row's Required cell is not "optional": ${rows[0]}`);
assert.match(rows[0], /additive/, `the run_seen row does not say the field is additive: ${rows[0]}`);
assert.match(rows[0], /Phase 7/, `the run_seen row does not say Phase 7 records it: ${rows[0]}`);
});
// --- DJ4 ---------------------------------------------------------------------------
test('[DJ4] Phase 7.2 step 3 commits only paths git does not ignore', () => {
const step3 = between('commands/trekexecute.md', '3. **`/trekreview`**', '4. **`verdict-gate.mjs`**');
const hits = sentencesWith(step3, 'git does not ignore');
assert.ok(hits.some((s) => /^Commit\b/.test(s) && /`git check-ignore -q -- \{path\}`/.test(s)
&& /`docs\(voyage\): review lessons \(round \{R\}\)`/.test(s)),
`Phase 7.2 step 3 does not say it commits only the changed paths git does not ignore, checked with git check-ignore: ${hits.join(' | ')}`);
const left = sentencesWith(step3, 'ignored');
assert.ok(left.some((s) => /left uncommitted/.test(s) && /`"ignored-skipped"`/.test(s) && /`"symlink-skipped"`/.test(s)),
`Phase 7.2 step 3 does not say an ignored path is left uncommitted and name the two skipped pointer results: ${left.join(' | ')}`);
assert.doesNotMatch(flat(step3), /Nothing the review wrote is left uncommitted/,
'Phase 7.2 step 3 still says nothing the review wrote is left uncommitted');
});
// --- E17a / E17b / Da / E18a / Db --------------------------------------------------
// E17a, E18a and Db are already right at be0b631; their tests are proven red
// under the E17a, E18a and Db entries of tests/fix-round-4-5b-mutations.mjs.
const LAUNCH = () => between('commands/trekresearch.md', '### Launch rules', '- Inject the resolved model at every spawn');
test('[E17a] the outbound-query sentence on local names is pinned', () => {
const hits = sentencesWith(LAUNCH(), 'It searches with the words it is given');
assert.deepEqual(hits, ['It searches with the words it is given, so no local file name or project term may be in them.'],
'the wave 2 rule no longer says the contrarian searches with the words it is given, so no local name may be in them');
});
test('[E17a] the no-leading-answer sentence is pinned', () => {
const hits = sentencesWith(LAUNCH(), 'no leading answer');
assert.deepEqual(hits, ['When wave 1\'s external findings hold no leading answer, say so and give it the research question alone, stripped the same way.'],
'the wave 2 rule no longer says what to give the contrarian when wave 1 holds no leading answer');
});
test('[E17b] the adapter says where wave 2\'s contrarian findings merge', () => {
const adapter = between('commands/trekresearch.md', '### Deep-research engine adapter (engine = deep-research only)', '### Output self-check');
const hits = sentencesWith(adapter, '`contrarian-researcher`');
assert.ok(hits.some((s) => /^Wave 2's `contrarian-researcher` findings\b/.test(s)
&& /\bmerged in per dimension as \*\*External findings\*\*/.test(s)
&& /\bthe dimension's \*\*Contradictions\*\* gives both sides\b/.test(s)),
`the deep-research adapter does not say wave 2's contrarian findings merge per dimension as External findings, with disputes under Contradictions: ${hits.join(' | ')}`);
});
test('[Da] the README parenthesis says when the engine is active', () => {
const text = oneLine('README.md', '`* = conditional`:', 'contrarian-researcher');
assert.ok(text.includes('(with `--engine deep-research`, when the engine is active, after the engine returns, from the leading answer of its report)'),
`the README deep-research parenthesis does not say "when the engine is active": ${text}`);
});
test('[E18a] the F9 deep row and summary-only row equal their text exactly', () => {
const p4 = between('commands/trekreview.md', '## Phase 4 — Triage gate', '## Phase 4.5 — ');
const row = (cell) => {
const rows = p4.split('\n').filter((l) => l.startsWith(`| ${cell} |`));
assert.equal(rows.length, 1, `Phase 4 must hold exactly one ${cell} row; found ${rows.length}`);
return rows[0];
};
assert.equal(row('`deep-review`'), '| `deep-review` | Default for every changed file not in the other two rows — code, tests, `commands/`, `agents/`, `skills/` Markdown, and the instruction files `CLAUDE.md`, `AGENTS.md` and `.claude/rules/**`. |',
'the deep-review triage row is not the F9 text');
assert.equal(row('`summary-only`'), '| `summary-only` | `docs/**`, Markdown files at the repo root other than `CLAUDE.md` and `AGENTS.md` (README, CHANGELOG), `tests/fixtures/**`. |',
'the summary-only triage row is not the F9 text');
});
test('[Db] the README stars security-researcher nowhere', () => {
const readme = read('README.md');
assert.match(readme, /security-researcher/, 'README.md names security-researcher nowhere');
assert.doesNotMatch(readme, /security-researcher\*/, 'a README line stars security-researcher as conditional');
});
// M1: the tests the 4.5a discovered-error fix (f6bdc2d) added carry their id
// tags, so a red script judges them; none is left under the untagged
// `found in execution:` prefix.
const PROSE_4_5A = 'tests/lib/fix-round-4-5a-prose.test.mjs';
/** The name text after each `test(` call opener in `rel`: the start of every test name, templates included. */
function testNameStarts(rel) {
return [...read(rel).matchAll(/\btest\(\s*(['"`])([^\n]*)/g)].map((m) => m[2]);
}
test('[M1] no test name in the 4.5a prose file starts with found in execution:', () => {
const names = testNameStarts(PROSE_4_5A);
assert.ok(names.length > 0, `${PROSE_4_5A} holds no test name`);
const untagged = names.filter((n) => n.startsWith('found in execution:'));
assert.deepEqual(untagged, [], `${PROSE_4_5A} still names ${untagged.length} test(s) "found in execution:"`);
});
// --- K4 / M4 / M2 / ARG: the round's CHANGELOG entry --------------------------------
// The round's entry stands in the existing Unreleased section (no release lies
// between 4.5a and 4.5b). The section is found by its title, by the rule
// tests/helpers/changelog-section.mjs uses for the 6.4.0 delivery:
// `## Unreleased — <title>` before the release, `## vX.Y.Z — <date> — <title>`
// after it, exactly once, up to the next `## ` heading. The helper is bound to
// the 6.4.0 title, so its rule is repeated here for this section's title.
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
/** The lines of the round's CHANGELOG section, heading excluded. */
function changelogSection() {
const lines = read('CHANGELOG.md').split('\n');
const at = [];
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" 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] + 1, end);
}
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
function changelogBullets(heading) {
const lines = changelogSection();
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
const out = [];
for (const l of lines.slice(s + 1)) {
if (/^### /.test(l)) break;
if (/^- /.test(l)) out.push(l);
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
else if (l.trim() !== '') out.push('');
}
return out.filter((b) => b !== '').map(flat);
}
test('[K4] the CHANGELOG names --gates=true as the shim\'s own form', () => {
const hits = changelogBullets('### Fixed').filter((b) => b.startsWith('- **The texts next to the fixes say the same**'));
assert.equal(hits.length, 1, `### Fixed must hold one siblings entry; found ${hits.length}`);
assert.ok(hits[0].includes('the commands read only the two-token `--gates <value>` form'),
`the siblings entry does not say the commands read only the two-token \`--gates <value>\` form: ${hits[0]}`);
assert.ok(hits[0].includes('`--gates=true` is the gates shim\'s own form'),
`the siblings entry does not name \`--gates=true\` as the gates shim's own form: ${hits[0]}`);
assert.doesNotMatch(hits[0], /the gates shim reads `--gates=true` as on\./,
`the siblings entry still ends on the gates shim reading \`--gates=true\` as on, which reads as a command form: ${hits[0]}`);
});
test('[M4] the Unreleased entry says review-not-measured for a failed git call', () => {
const hits = changelogBullets('### Fixed').filter((b) => b.startsWith('- **The merge lock refuses a review of an older commit**'));
assert.equal(hits.length, 1, `### Fixed must hold one merge-lock entry; found ${hits.length}`);
assert.doesNotMatch(hits[0], /a failed git call included/,
`the merge-lock entry still counts a failed git call among the \`review-stale\` reasons: ${hits[0]}`);
const said = sentencesWith(hits[0], 'failed git call');
assert.ok(said.some((s) => /a failed git call exits 2 with a reason that starts `review-not-measured:`, not `review-stale`/.test(s)),
`the merge-lock entry does not say a failed git call exits 2 with \`review-not-measured:\`, not \`review-stale\`: ${said.join(' | ') || hits[0]}`);
});
test('[M2] the Unreleased entry records the brief-conflict exception in the flag summaries', () => {
const hits = changelogBullets('### Fixed').filter((b) => b.includes('`--quick`') && b.includes('brief-conflict rule'));
assert.equal(hits.length, 1, `### Fixed must hold one entry on the \`--quick\` summaries and the brief-conflict rule; found ${hits.length}`);
assert.match(hits[0], /name the brief-conflict rule as the exception to the skipped reasonableness filter/,
`the entry does not say the summaries name the brief-conflict rule as the exception: ${hits[0]}`);
for (const where of ['`/trekreview`', 'the review orchestrator', '`docs/command-modes.md`', 'README']) {
assert.ok(hits[0].includes(where), `the entry does not name ${where} among the four summaries: ${hits[0]}`);
}
});
test('[ARG] the CHANGELOG names the Bash-guard block on a parser heredoc that closes early', () => {
const hits = changelogBullets('### Fixed').filter((b) => b.includes('`args-heredoc-closed-early`'));
assert.equal(hits.length, 1, `### Fixed must hold one entry naming \`args-heredoc-closed-early\`; found ${hits.length}`);
assert.ok(hits[0].includes('`hooks/scripts/pre-bash-executor.mjs`'), `the entry does not name the Bash guard's file: ${hits[0]}`);
assert.ok(hits[0].includes('`VOYAGE_ARGS_EOF_7F3C9E`'), `the entry does not name the heredoc's closing line: ${hits[0]}`);
assert.match(hits[0], /not the last non-empty line/, `the entry does not say when the guard blocks: ${hits[0]}`);
assert.match(hits[0], /\(R6\)/, `the entry does not carry the R6 mark: ${hits[0]}`);
});
test('[ARG] the CHANGELOG says ~ and $HOME in the arguments are no longer expanded', () => {
const hits = changelogBullets('### Changed').filter((b) => b.includes('`~`') && b.includes('`$HOME`'));
assert.equal(hits.length, 1, `### Changed must hold one entry on \`~\` and \`$HOME\`; found ${hits.length}`);
for (const cmd of ['`/trekendsession`', '`/trekcontinue`', '`/trekreview`']) {
assert.ok(hits[0].includes(cmd), `the entry does not name ${cmd}: ${hits[0]}`);
}
assert.match(hits[0], /are no longer expanded/, `the entry does not say they are no longer expanded: ${hits[0]}`);
assert.match(hits[0], /write the path out/, `the entry does not tell the reader to write the path out: ${hits[0]}`);
});