// 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 // `[] 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. // 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}`); });