refactor: simplify pass (trekexecute)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-24 22:22:18 +02:00
commit af90e82794
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
12 changed files with 303 additions and 186 deletions

View file

@ -291,11 +291,7 @@ export function dedupByTriplet(findings) {
if (!tripletGroups.has(key)) tripletGroups.set(key, []);
tripletGroups.get(key).push({ f, i });
});
const groups = [...tripletGroups.values()].flatMap((entries) => groupTriplet(
entries,
(a, b) => a.reviewer !== b.reviewer || sameTitle(a, b),
(f) => f.reviewer,
));
const groups = [...tripletGroups.values()].flatMap((entries) => groupTriplet(entries));
const out = [];
for (const group of groups) {
// Ranked as the JSDoc above says. A brief-conflict mark is not carried over
@ -353,9 +349,14 @@ export function dedupByTriplet(findings) {
return out.map((f, i) => ({ ...f, id: ids[i] }));
}
/** A finding's title as the grouping compares it: trimmed, lower case. */
function normTitle(f) {
return String(f.title).trim().toLowerCase();
}
function sameTitle(a, b) {
if (a.title === undefined || b.title === undefined) return true;
return String(a.title).trim().toLowerCase() === String(b.title).trim().toLowerCase();
return normTitle(a) === normTitle(b);
}
/** Code-unit order of two strings (never the locale's). */
@ -364,20 +365,15 @@ function cmpStr(a, b) {
}
/**
* The groups of one triplet's findings, in the two passes `dedupByTriplet`
* describes. `entries` are `{ f, i }` (the finding and its input index);
* `fits(m, f)` says member `m` may share a group with `f`; `whoOf(f)` names the
* reviewer for the canonical order. Returns arrays of findings, each in the
* order the passes place them (classes alphabetically, each class in canonical
* order, untitled findings last in canonical order), the groups in the order
* of their earliest input index.
* The groups of one triplet's findings, in the two passes and the order
* `dedupByTriplet` describes. `entries` are `{ f, i }` (the finding and its
* input index). Returns arrays of findings.
* @param {Array<{f: object, i: number}>} entries
* @param {(m: object, f: object) => boolean} fits
* @param {(f: object) => unknown} whoOf
* @returns {object[][]}
*/
function groupTriplet(entries, fits, whoOf) {
const canon = (x, y) => cmpStr(String(whoOf(x.f) ?? ''), String(whoOf(y.f) ?? ''))
function groupTriplet(entries) {
const fits = (m, f) => m.reviewer !== f.reviewer || sameTitle(m, f);
const canon = (x, y) => cmpStr(String(x.f.reviewer ?? ''), String(y.f.reviewer ?? ''))
|| cmpStr(String(x.f.detail ?? ''), String(y.f.detail ?? ''))
|| cmpStr(JSON.stringify(x.f), JSON.stringify(y.f));
const classes = new Map();
@ -387,7 +383,7 @@ function groupTriplet(entries, fits, whoOf) {
untitled.push(e);
continue;
}
const t = String(e.f.title).trim().toLowerCase();
const t = normTitle(e.f);
if (!classes.has(t)) classes.set(t, []);
classes.get(t).push(e);
}

View file

@ -25,10 +25,10 @@
// shell; a git call that fails is a stale review (fails closed), never ok.
import { spawnSync } from 'node:child_process';
import { MARK_START, MARK_END, withPointer } from './recurrence.mjs';
import { LESSONS_FILE_NAME, MARK_START, MARK_END, withPointer } from './recurrence.mjs';
const SHA_RE = /^[0-9a-f]{7,40}$/i;
const LESSONS_REL = '.claude/rules/voyage-review-lessons.md';
const LESSONS_REL = `.claude/rules/${LESSONS_FILE_NAME}`; // posix, as `git diff` prints it
const CLAUDE_REL = 'CLAUDE.md';
const SYMLINK_MODE = '120000';
@ -42,7 +42,7 @@ export function reviewedSha(reviewText) {
const m = fm && /^scope_sha_end:[ \t]*(["']?)([^"'\s#]*)\1[ \t]*(?:#.*)?$/m.exec(fm[1]);
// A commit sha only: a ref name (HEAD, main, a tag) resolves to whatever it
// points at now, and coverage would pass whatever was reviewed (round 4).
return m && /^[0-9a-f]{7,40}$/i.test(m[2]) ? m[2] : '';
return m && SHA_RE.test(m[2]) ? m[2] : '';
}
function git(repo, args) {
@ -53,10 +53,14 @@ function stale(reason) {
return { ok: false, reason: `review-stale: ${reason}` };
}
/** What a git call said: its spawn error, else the first line of its stderr. */
function gitNote(r) {
return r.error?.message ?? String(r.stderr ?? '').trim().split('\n')[0];
}
/** A failed git call as a stale reason: fails closed. */
function gitFailed(what, r) {
const note = (r.error?.message ?? String(r.stderr ?? '').trim().split('\n')[0]) || `exit ${r.status}`;
return stale(`git ${what} failed (${note})`);
return stale(`git ${what} failed (${gitNote(r) || `exit ${r.status}`})`);
}
const short = (c) => c.slice(0, 12);
@ -74,18 +78,18 @@ function outsideBlock(text) {
}
/**
* One file in one tree: `{ ok, absent, mode, text }`; absent reads as ''.
* One file in one tree: `{ ok, mode, text }`; absent reads as mode null, text ''.
* `--full-tree`: the path is from the repo root, as `git diff` prints it.
*/
function blobAt(repo, rev, path) {
const ls = git(repo, ['ls-tree', '--full-tree', '-z', rev, '--', path]);
if (ls.status !== 0) return { ok: false, fail: gitFailed(`ls-tree ${rev}`, ls) };
const entry = ls.stdout.split('\0').find(Boolean);
if (!entry) return { ok: true, absent: true, mode: null, text: '' };
if (!entry) return { ok: true, mode: null, text: '' };
const mode = entry.split(' ')[0];
const blob = git(repo, ['cat-file', 'blob', `${rev}:${path}`]);
if (blob.status !== 0) return { ok: false, fail: gitFailed(`cat-file ${rev}:${path}`, blob) };
return { ok: true, absent: false, mode, text: blob.stdout };
return { ok: true, mode, text: blob.stdout };
}
/**
@ -101,7 +105,7 @@ export function checkReviewedCommit({ repo, sha } = {}) {
if (!SHA_RE.test(given)) return stale(notCommit);
const resolved = git(repo, ['rev-parse', '--verify', '--quiet', `${given}^{commit}`]);
if (resolved.status !== 0) {
const note = (resolved.error?.message ?? String(resolved.stderr ?? '').trim().split('\n')[0]);
const note = gitNote(resolved);
return stale(note ? `${notCommit} (${note})` : notCommit);
}
const full = resolved.stdout.trim();
@ -123,14 +127,14 @@ export function checkReviewedCommit({ repo, sha } = {}) {
const lessons = `${prefix}${LESSONS_REL}`;
const claude = `${prefix}${CLAUDE_REL}`;
const roots = git(repo, ['rev-list', '--max-parents=0', `${full}..HEAD`]);
if (roots.status !== 0) return gitFailed('rev-list --max-parents=0', roots);
const root = roots.stdout.split('\n').find(Boolean);
if (root) return stale(`commit ${short(root)} has no parent`);
// One line per commit, `<commit> <parent>...`; a line with one field is a root.
const list = git(repo, ['rev-list', '--parents', `${full}..HEAD`]);
if (list.status !== 0) return gitFailed('rev-list --parents', list);
const lines = list.stdout.split('\n').filter(Boolean).map((l) => l.split(' '));
const root = lines.find((f) => f.length === 1);
if (root) return stale(`commit ${short(root[0])} has no parent`);
const list = git(repo, ['rev-list', `${full}..HEAD`]);
if (list.status !== 0) return gitFailed('rev-list', list);
for (const c of list.stdout.split('\n').filter(Boolean)) {
for (const [c] of lines) {
// `--no-relative` and `--ignore-submodules=none`: neither `diff.relative`
// nor an `ignore =` setting (config or .gitmodules) may hide a changed path.
const names = git(repo, ['diff', '--no-relative', '--ignore-submodules=none', '--name-only', '-z', '--no-renames', `${c}^1`, c]);

View file

@ -116,15 +116,18 @@ const GATES_USAGE = 'usage: --gates takes true, false, closed or open';
function parseArgs(argv) {
const out = {};
const setGates = (v) => {
const g = gatesValue(v);
if (g === undefined) out.error = GATES_USAGE;
else out.gates = g;
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--state') out.state = argv[++i];
else if (a === '--event') out.event = argv[++i];
else if (a.startsWith('--gates=')) {
// The one-token form, read as `--gates <value>`; an empty value is no value.
const g = gatesValue(a.slice('--gates='.length));
if (g === undefined) out.error = GATES_USAGE;
else out.gates = g;
setGates(a.slice('--gates='.length));
} else if (a === '--gates') {
// Bare `--gates` (last, or followed by another flag) means on and
// consumes nothing. A value is case-sensitive: true|closed = on,
@ -133,9 +136,7 @@ function parseArgs(argv) {
if (v === undefined || v.startsWith('--')) {
out.gates = true;
} else {
const g = gatesValue(v);
if (g === undefined) out.error = GATES_USAGE;
else out.gates = g;
setGates(v);
i++;
}
}

View file

@ -47,11 +47,11 @@ export function newFilesInDiff(diffText) {
const paths = [];
const unread = [];
let header = null;
let headers = 0;
let sawHeader = false;
const lines = String(diffText).split('\n').map((l) => l.replace(ANSI_RE, ''));
for (const line of lines) {
if (line.startsWith('diff --git ')) {
headers++;
sawHeader = true;
header = line;
continue;
}
@ -64,7 +64,7 @@ export function newFilesInDiff(diffText) {
}
}
const first = lines.find((l) => l.trim() !== '');
if (headers === 0 && first !== undefined) unread.push(`no diff --git header in a non-empty diff (first line: ${first.slice(0, 200)})`);
if (!sawHeader && first !== undefined) unread.push(`no diff --git header in a non-empty diff (first line: ${first.slice(0, 200)})`);
return { paths, unread };
}
@ -118,14 +118,9 @@ export function parkAttemptFiles({ diffText, step, attempt, cwd = process.cwd()
continue;
}
const posix = rel.split('\\').join('/');
let intentToAdd = false;
if (trackedPaths.has(posix)) {
if (inHead(root, posix)) {
kept.push({ path, reason: 'tracked since the attempt: not the executor\'s to move', blocking: false });
continue;
}
// In the index but not in HEAD: registered --intent-to-add by the attempt.
intentToAdd = true;
if (trackedPaths.has(posix) && inHead(root, posix)) {
kept.push({ path, reason: 'tracked since the attempt: not the executor\'s to move', blocking: false });
continue;
}
const target = join(to, rel);
if (lexists(target)) {
@ -133,7 +128,8 @@ export function parkAttemptFiles({ diffText, step, attempt, cwd = process.cwd()
continue;
}
moves.push({ path, abs, rel, target });
if (intentToAdd) unindex.push(posix);
// In the index but not in HEAD: registered --intent-to-add by the attempt.
if (trackedPaths.has(posix)) unindex.push(posix);
}
// Out of the index first, all in ONE call before anything moves: git
// refuses the whole call when one path cannot be removed, so a failure

View file

@ -54,12 +54,15 @@ const RULE_KEYS = Object.keys(RULE_TIERS);
const sevRank = (s) => (SEVERITIES.includes(s) ? SEVERITIES.indexOf(s) : SEVERITIES.length);
// The catalogue tier of a rule_key, or null when the key is missing or unknown.
const tierOf = (key) => (typeof key === 'string' && Object.prototype.hasOwnProperty.call(RULE_TIERS, key) ? RULE_TIERS[key] : null);
// The rank of a finding's catalogue tier, else of its own severity.
const tierRank = (f) => sevRank(tierOf(f.rule_key) ?? f.severity);
// The (file,line,rule_key) triplet a finding is deduped and matched on.
const tripletOf = (f) => `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
// A finding's title as the grouping compares it: trimmed, lower case.
const normTitle = (f) => String(f.title).trim().toLowerCase();
// The row a finding is matched to after the coordinator: its triplet plus its
// title, normalized as `sameTitle` below reads it (trimmed, lower case). An
// untitled finding gets a key no titled row has.
const rowOf = (f) => `${tripletOf(f)}\u0000${f.title === undefined ? '\u0001' : String(f.title).trim().toLowerCase()}`;
// `normTitle`. An untitled finding gets a key no titled row has.
const rowOf = (f) => `${tripletOf(f)}\u0000${f.title === undefined ? '\u0001' : normTitle(f)}`;
// Findings schema — mirrors lib/review/findings-schema.mjs (NW1). The enum on
// rule_key enforces the catalogue at the StructuredOutput layer (stronger than
@ -134,8 +137,7 @@ const VERDICT_SCHEMA = {
// As in coordinator-contract Pass 1, one source never duplicates itself: its
// second finding on a triplet under another title opens a group of its own
// (voyage-lekerepo trial 5, finding 7). `sourceOf` names the reviewer or engine.
const sameTitle = (a, b) => a.title === undefined || b.title === undefined ||
String(a.title).trim().toLowerCase() === String(b.title).trim().toLowerCase();
const sameTitle = (a, b) => a.title === undefined || b.title === undefined || normTitle(a) === normTitle(b);
// As there too, a triplet's findings are grouped in two passes, so the order
// they arrive in never changes which row a finding joins: (1) every titled
// finding joins the class of its normalized title, over all of the triplet's
@ -165,7 +167,7 @@ function groupTriplet(entries) {
untitled.push(e);
continue;
}
const t = String(e.f.title).trim().toLowerCase();
const t = normTitle(e.f);
if (!classes.has(t)) classes.set(t, []);
classes.get(t).push(e);
}
@ -195,13 +197,12 @@ function dedupByTriplet(findings, { perSource = true, rows = null } = {}) {
const groups = [...byTriplet.values()].flatMap((entries) => (perSource
? groupTriplet(entries)
: [entries.map((e) => e.f)]));
const tier = (f) => sevRank(tierOf(f.rule_key) ?? f.severity);
const out = [];
for (const group of groups) {
let survivor = group[0];
for (const f of group.slice(1)) {
const tf = tier(f);
const ts = tier(survivor);
const tf = tierRank(f);
const ts = tierRank(survivor);
if (tf !== ts) {
if (tf < ts) survivor = f;
continue;
@ -381,9 +382,8 @@ for (const m of merged) {
rowsOn(counterparts, rowOf(m), m);
rowsOn(rowsOnTriplet, tripletOf(m), m);
}
const rowTier = (m) => sevRank(tierOf(m.rule_key) ?? m.severity);
const strictest = (rows) => rows.reduce((best, m) => (rowTier(m) < rowTier(best) ||
(rowTier(m) === rowTier(best) && sevRank(m.severity) < sevRank(best.severity)) ? m : best));
const strictest = (rows) => rows.reduce((best, m) => (tierRank(m) < tierRank(best) ||
(tierRank(m) === tierRank(best) && sevRank(m.severity) < sevRank(best.severity)) ? m : best));
const counterpartOf = (f) => {
const own = counterparts.get(rowOf(f));
if (own) return strictest(own);
@ -432,7 +432,7 @@ const returnedRows = new Set(returned.filter((f) => f && typeof f === 'object').
// source here.
const putBack = merged
.filter((m) => m && typeof m === 'object' && !returnedRows.has(rowOf(m)))
.flatMap((m) => dedupByTriplet((rowMembers.get(m) ?? [m]).filter((f) =>
.flatMap((m) => dedupByTriplet(rowMembers.get(m).filter((f) =>
f && typeof f === 'object' && f.engine), { perSource: false }));
for (const f of putBack) {
const { brief_lowered_from: _lowered, ...copy } = f;

View file

@ -7,7 +7,8 @@
# "Back to planning" line saves:
# a. a new file registered --intent-to-add is parked (exit 0, gone from
# the tree and the index, kept under .git/voyage/parked/);
# b. a coloured diff (color.diff=always) still parks its new file;
# b. a coloured diff (--color=always, as color.diff=always gives it)
# still parks its new file;
# c. --cwd a subdirectory of the repo parks the root-relative path.
# 2. The verdict gate (lib/review/verdict-gate.mjs --json <review> --repo .):
# a review of HEAD (0), a later lessons-only commit (0), a later source
@ -44,10 +45,8 @@ mkdir -p "$TMP/hooks-none" || fail "mkdir hooks-none"
newrepo() {
mkdir -p "$1" || fail "mkdir $1"
git -C "$1" init -q || fail "git init $1"
git -C "$1" config user.name "Test" || fail "git config user.name"
git -C "$1" config user.email "test@example.invalid" || fail "git config user.email"
git -C "$1" config commit.gpgsign false || fail "git config commit.gpgsign"
git -C "$1" config core.hooksPath "$TMP/hooks-none" || fail "git config core.hooksPath"
printf '[user]\n\tname = Test\n\temail = test@example.invalid\n[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "%s"\n' \
"$TMP/hooks-none" >> "$1/.git/config" || fail "git config $1"
}
# jsonok <check> <js expression on j>: stdin is JSON; exit 0 when the expression is true.
@ -66,9 +65,12 @@ git -C "$P" commit -q -m "base" || fail "git commit base"
START="$(git -C "$P" rev-parse HEAD)" || fail "rev-parse start"
PARKED="$P/.git/voyage/parked"
# savediff <file>: the "Back to planning" save line of commands/trekexecute.md.
# savediff <file> [color]: the "Back to planning" save line of commands/trekexecute.md;
# with "color", the same diff coloured (--color=always in place of --no-color).
savediff() {
git -C "$P" -c core.quotepath=off diff --no-ext-diff --no-color --no-renames --src-prefix=a/ --dst-prefix=b/ "$START" > "$1" \
color=--no-color
[ "${2:-}" = color ] && color=--color=always
git -C "$P" -c core.quotepath=off diff --no-ext-diff "$color" --no-renames --src-prefix=a/ --dst-prefix=b/ "$START" > "$1" \
|| fail "git diff for $1"
}
@ -92,8 +94,7 @@ parked_ok a.sh 1
# 1b. a coloured diff
printf 'stale\n' > "$P/tests/b.sh"
git -C "$P" add --intent-to-add -- tests/b.sh || fail "git add --intent-to-add tests/b.sh"
git -C "$P" -c color.diff=always diff --no-ext-diff --no-renames --src-prefix=a/ --dst-prefix=b/ "$START" > "$TMP/park-2.diff" \
|| fail "git diff (coloured)"
savediff "$TMP/park-2.diff" color
ESC="$(printf '\033')"
grep -q "$ESC" "$TMP/park-2.diff" || fail "park 2: control - the diff holds no colour code"
out="$(node "$PARK" --diff "$TMP/park-2.diff" --step 1 --attempt 2 --cwd "$P")" || fail "park 2 (coloured diff) exited non-zero: $out"

View file

@ -464,9 +464,13 @@ function orders(xs) {
return xs.flatMap((x, i) => orders([...xs.slice(0, i), ...xs.slice(i + 1)]).map((rest) => [x, ...rest]));
}
/** A row's members, rebuilt from its survivor plus each "Also flagged by" line, as sorted `reviewer|title` strings. */
/**
* A row's members, rebuilt from its survivor plus each "Also flagged by" line,
* as sorted `reviewer|title` strings; an untitled member is named by its
* rule_key, as the line names it.
*/
function members(row) {
const out = [`${row.reviewer}|${row.title}`];
const out = [`${row.reviewer}|${row.title ?? row.rule_key}`];
for (const m of String(row.detail ?? '').matchAll(/^Also flagged by (.+?): (.+)\.$/gm)) out.push(`${m[1]}|${m[2]}`);
return out.sort();
}
@ -535,13 +539,6 @@ test('[ORD] a full tie on one row: two /code-review MAJORs titled T, one marked
assert.deepEqual(split(a), split(b), 'the kept/unverified split');
});
/** A row's member set, rebuilt from its survivor plus each "Also flagged by" line; an untitled member is named by its rule_key, as the line names it. */
function memberSet(row) {
const out = [`${row.reviewer}|${row.title ?? row.rule_key}`];
for (const m of String(row.detail ?? '').matchAll(/^Also flagged by (.+?): (.+)\.$/gm)) out.push(`${m[1]}|${m[2]}`);
return out.sort().join('+');
}
test('[ORD] the title classes are placed in alphabetical order: cA, fB, cB and an untitled correctness BLOCKER give {cA, cU} BLOCKER and {fB, cB} MAJOR and one verdict in all 24 orders', () => {
const K = 'NOT_IN_THE_CATALOGUE';
const at = { file: 'bin/slug.mjs', line: 46, rule_key: K, detail: 'd' };
@ -562,12 +559,12 @@ test('[ORD] the title classes are placed in alphabetical order: cA, fB, cB and a
for (const order of all) {
const name = order.map(([n]) => n).join(', ');
const rows = dedupByTriplet(order.map(([, f]) => structuredClone(f)));
const bySet = Object.fromEntries(rows.map((r) => [memberSet(r), r.severity]));
const bySet = Object.fromEntries(rows.map((r) => [members(r).join('+'), r.severity]));
assert.deepEqual(bySet, expected, `order ${name}`);
const j = judgeFilter(rows);
verdicts.add(JSON.stringify({
kept: j.kept.map(memberSet).sort(),
unverified: j.unverified.map(memberSet).sort(),
kept: j.kept.map((r) => members(r).join('+')).sort(),
unverified: j.unverified.map((r) => members(r).join('+')).sort(),
verdict: computeVerdict(j.kept, { unverified: j.unverified }).verdict,
}));
}

View file

@ -19,7 +19,7 @@
// `unverified`, not dropped (lib/review/coordinator-contract.mjs).
// Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
@ -33,10 +33,15 @@ import { childEnv } from '../helpers/child-env.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
/** A repo file's text; the file must exist. */
const readCache = new Map();
/** A repo file's text; the file must exist. Read from disk once per run. */
function read(rel) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
return readFileSync(join(ROOT, rel), 'utf8');
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. */
@ -84,6 +89,16 @@ function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
/** The runtime ```bash block of `text` that holds `cmd`, or undefined. */
function bashBlockHolding(text, cmd) {
return text.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
}
/** A Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
// --- #23 ---------------------------------------------------------------------------
test('[#23] no agent file refers to a MANDATORY OVERRIDE rule', () => {
@ -656,11 +671,16 @@ function gatesShim(cmd, value) {
return l.replace(' [value]', value ? ` ${value}` : '');
}
/** Run a shim line from the prose; its `next_state`. */
const shimCache = new Map();
/** Run a shim line from the prose; its `next_state`. Each distinct line runs once. */
function shimNext(cmdLine) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
return JSON.parse(r.stdout).next_state;
if (!shimCache.has(cmdLine)) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
shimCache.set(cmdLine, JSON.parse(r.stdout).next_state);
}
return shimCache.get(cmdLine);
}
for (const cmd of GATES_COMMANDS) {
@ -1087,6 +1107,15 @@ function staleRepo() {
return { repo, reviewed, head, review, cleanup: () => rmSync(base, { recursive: true, force: true }) };
}
let sharedStale = null;
after(() => sharedStale?.cleanup());
/** The file's one staleRepo, built on first use: its tests only rewrite review.md, outside the repo. */
function staleFixture() {
if (!sharedStale) sharedStale = staleRepo();
return sharedStale;
}
/** The lock line with its review path filled in, run in `repo` through bash(). */
function runLock(cmd, dir, repo) {
const filled = cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
@ -1104,17 +1133,15 @@ test('[R6] the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdic
test('[R6] the extracted lock lines, run on an ALLOW review of an older commit with a later source commit, exit 2 with review-stale', () => {
const text = read(EXEC);
const lines = [line(section(text, MERGE_GATE), 'review/verdict-gate.mjs'), line(section(text, PHASE_72), 'review/verdict-gate.mjs')];
const fx = staleRepo();
try {
for (const cmd of lines) {
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
} finally { fx.cleanup(); }
const fx = staleFixture();
for (const cmd of lines) {
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
});
test('[R6] both decline lists name review-stale, the exit table says to re-run /trekreview on this commit, and --resume runs the same gate', () => {
@ -1133,12 +1160,10 @@ test('[R6] both decline lists name review-stale, the exit table says to re-run /
test('keep: Phase 7.2 step 4 run on a BLOCK review of an older commit still exits 1, so the fix loop opens', () => {
const step4 = line(section(read(EXEC), PHASE_72), 'review/verdict-gate.mjs');
const fx = staleRepo();
try {
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
} finally { fx.cleanup(); }
const fx = staleFixture();
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
});
test('keep: the other verdict-gate callers carry no --repo', () => {
@ -1202,11 +1227,6 @@ test('[D67] the End to end section says to record run_seen, also when no app was
const ENDSESSION = 'commands/trekendsession.md';
const ENDSESSION_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runEndsessionParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and its parser line run on fix-header gives no help', () => {
const phase0 = section(read(ENDSESSION), '## Phase 0 — `--help` handling');
assert.doesNotMatch(flat(phase0), /contains\s+`?--help`?\s+or\s+`?-h`?/i,
@ -1215,19 +1235,19 @@ test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and it
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, ENDSESSION_PARSER_LINE, 'the Phase 0 parser line is not the trekendsession parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
const block = bashBlockHolding(phase0, cmd);
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.equal(block.split('\n')[0], 'setopt nonomatch 2>/dev/null || true', 'the block does not open with setopt nonomatch');
const plain = runEndsessionParser(cmd, 'fix-header');
const plain = runParser(cmd, 'fix-header');
assert.equal(plain.status, 0, `the parser line refused fix-header: ${plain.stdout}${plain.stderr}`);
const r = JSON.parse(plain.stdout);
assert.equal(r.flags['--help'], undefined, 'fix-header set the --help flag');
assert.deepEqual(r.positional, ['fix-header']);
assert.notEqual(r.positional[0], '-h');
const help = runEndsessionParser(cmd, '--help');
const help = runParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'control: --help must set the help flag');
const two = runEndsessionParser(cmd, 'fix-header "Session 2"');
const two = runParser(cmd, 'fix-header "Session 2"');
assert.equal(two.status, 0, two.stdout + two.stderr);
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
});
@ -1276,31 +1296,26 @@ const CONTINUE = 'commands/trekcontinue.md';
const CONTINUE_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue -- $ARGUMENTS';
const CONSISTENCY_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/next-session-prompt-validator.mjs --json --consistency <RESOLVED-PATH-A> <RESOLVED-PATH-B> --state-file <RESOLVED-PATH-STATE>';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runContinueParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its parser line run with --help sets the help flag', () => {
const phase0 = section(read(CONTINUE), '## Phase 0 — `--help` handling');
assert.doesNotMatch(phase0, /parseArgs\(\$ARGUMENTS/, 'Phase 0 still calls parseArgs($ARGUMENTS, ...) as a function');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, CONTINUE_PARSER_LINE, 'the Phase 0 parser line is not the trekcontinue parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
const block = bashBlockHolding(phase0, cmd);
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.match(flat(phase0), /Run first, exactly like this:/);
assert.match(flat(phase0), /Read `flags` and `positional` from the JSON it prints/);
assert.match(flat(phase0), /`flags\['--help'\] === true`/, 'Phase 0 does not dispatch on the parsed --help flag');
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const help = runContinueParser(cmd, '--help');
const help = runParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'the parser line run with --help does not set the help flag');
const dir = runContinueParser(cmd, '.claude/projects/demo');
const dir = runParser(cmd, '.claude/projects/demo');
assert.equal(dir.status, 0, dir.stdout + dir.stderr);
const r = JSON.parse(dir.stdout);
assert.equal(r.flags['--help'], undefined, 'a project dir set the --help flag');
assert.deepEqual(r.positional, ['.claude/projects/demo']);
const h = runContinueParser(cmd, '-h');
const h = runParser(cmd, '-h');
assert.equal(h.status, 0, h.stdout + h.stderr);
assert.equal(JSON.parse(h.stdout).positional[0], '-h');
const p05 = flat(section(read(CONTINUE), '## Phase 0.5 — Cleanup mode dispatch'));

View file

@ -3,9 +3,9 @@
// build of the re-planned step they are moved into the report dir, never
// deleted (voyage-lekerepo trial 5, finding 4).
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
@ -20,15 +20,25 @@ function git(cwd, ...args) {
return r.stdout;
}
// The base repo (bin/slug.mjs committed), built once; repo() gives each test its own copy.
let seed = null;
after(() => {
if (seed) rmSync(seed, { recursive: true, force: true });
});
function repo() {
if (!seed) {
seed = mkdtempSync(join(tmpdir(), 'voyage-park-seed-'));
git(seed, 'init', '-q');
git(seed, 'config', 'user.email', 't@example.invalid');
git(seed, 'config', 'user.name', 't');
mkdirSync(join(seed, 'bin'));
writeFileSync(join(seed, 'bin/slug.mjs'), 'old\n');
git(seed, 'add', 'bin/slug.mjs');
git(seed, 'commit', '-q', '-m', 'base');
}
const dir = mkdtempSync(join(tmpdir(), 'voyage-park-'));
git(dir, 'init', '-q');
git(dir, 'config', 'user.email', 't@example.invalid');
git(dir, 'config', 'user.name', 't');
mkdirSync(join(dir, 'bin'));
writeFileSync(join(dir, 'bin/slug.mjs'), 'old\n');
git(dir, 'add', 'bin/slug.mjs');
git(dir, 'commit', '-q', '-m', 'base');
cpSync(seed, dir, { recursive: true });
return dir;
}
@ -57,9 +67,10 @@ const DIFF = [
'',
].join('\n');
function run(dir, diff = DIFF) {
/** The CLI run with `--cwd <cwd>` (the repo by default) on `diff` written into the repo's git dir. */
function run(dir, diff = DIFF, cwd = dir) {
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', dir], { encoding: 'utf8' });
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', cwd], { encoding: 'utf8' });
}
// Parked outside the work tree, in the git dir, so a later `git ls-files --others` never lists them.
@ -207,12 +218,6 @@ test('trekexecute: a re-planned step parks the earlier attempt\'s new files befo
// [F4c] The root was the --cwd as given, so a subdirectory read every
// root-relative path as absent; a git call that failed read as "nothing tracked".
/** The CLI run with `--cwd <cwd>` on `diff` written into the repo's git dir. */
function runFrom(dir, cwd, diff = DIFF) {
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', cwd], { encoding: 'utf8' });
}
/** The attempt's two new files, written on disk. */
function attemptFiles(dir) {
mkdirSync(join(dir, 'tests'));
@ -275,7 +280,7 @@ test('[F4c] --cwd <repo>/bin parks the root-relative path', () => {
const dir = repo();
try {
attemptFiles(dir);
const r = runFrom(dir, join(dir, 'bin'));
const r = run(dir, DIFF, join(dir, 'bin'));
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.ok(JSON.parse(r.stdout).parked.includes('tests/batch.sh'), `tests/batch.sh was not parked from a subdirectory: ${r.stdout}`);
assert.equal(existsSync(join(dir, 'tests/batch.sh')), false, 'the file is still in the tree');

View file

@ -53,16 +53,20 @@ function ev(name, over = {}) {
}
const passing = (name) => ev(name, { ok: true, code: null, cname: null, failureType: null });
/** The judge on a results file, the id list written into `dir`, with extra flags. */
function judgeFile(dir, ids, results, extra = []) {
writeFileSync(join(dir, 'ids'), ids.join('\n') + '\n');
const r = spawnSync(process.execPath, [JUDGE, '--ids', join(dir, 'ids'), '--results', results, ...extra],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
}
/** Run the judge on an id list and planted events, with extra flags. */
function judge(ids, events, extra = []) {
return inTmp((dir) => {
const idsFile = join(dir, 'ids');
const results = join(dir, 'results.jsonl');
writeFileSync(idsFile, ids.join('\n') + '\n');
writeFileSync(results, events.map((e) => JSON.stringify(e)).join('\n') + (events.length ? '\n' : ''));
const r = spawnSync(process.execPath, [JUDGE, '--ids', idsFile, '--results', results, ...extra],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
return judgeFile(dir, ids, results, extra);
});
}
@ -170,14 +174,6 @@ function report(dir, files) {
return out;
}
/** The judge on a results file the reporter wrote. */
function judgeFile(dir, ids, results) {
writeFileSync(join(dir, 'ids'), ids.join('\n') + '\n');
const r = spawnSync(process.execPath, [JUDGE, '--ids', join(dir, 'ids'), '--results', results],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
}
const HEAD = "import { test } from 'node:test';\nimport { strict as assert } from 'node:assert';\n";
test('red reporter + judge: a passing [X1] test and an [X2] test that throws a TypeError, run by node --test -> exit 1', () => {

View file

@ -9,10 +9,10 @@
// pointer block. Anything else is exit 2 `review-stale: ...`. Those tests run
// the CLI only, in throwaway git repos with no hooks.
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, symlinkSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, symlinkSync, appendFileSync, cpSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
@ -260,42 +260,63 @@ function g(cwd, ...args) {
return r.stdout.trim();
}
/**
* A throwaway repo with one reviewed commit (a.mjs, CLAUDE.md, the lessons
* file), no git hooks. `sub` puts the project in a subdirectory of the repo;
* `claudeLink` makes CLAUDE.md a symlink to AGENTS.md.
*/
function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {}) {
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-'));
/** Write `text` to `rel` under `dir`, making its directories. */
function putIn(dir, rel, text) {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), text);
}
/** A fixture variant's seed: `{ base, reviewed }`, built once (see gateRepo) and removed after the file's tests. */
const seeds = new Map();
after(() => {
for (const seed of seeds.values()) rmSync(seed.base, { recursive: true, force: true });
});
function seedRepo({ sub, claude, claudeLink }) {
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-seed-'));
const repo = join(base, 'repo');
const dir = sub ? join(repo, sub) : repo;
mkdirSync(join(dir, '.claude', 'rules'), { recursive: true });
mkdirSync(join(base, 'hooks-none'));
g(repo, 'init', '-q', '-b', 'main');
g(repo, 'config', 'user.name', 'Test');
g(repo, 'config', 'user.email', 'test@example.invalid');
g(repo, 'config', 'commit.gpgsign', 'false');
g(repo, 'config', 'core.hooksPath', join(base, 'hooks-none'));
const put = (rel, text) => {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), text);
};
put('a.mjs', 'export const a = 1;\n');
put(LESSONS, LESSONS_BEFORE);
appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n'
+ `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`);
putIn(dir, 'a.mjs', 'export const a = 1;\n');
putIn(dir, LESSONS, LESSONS_BEFORE);
if (claudeLink) {
put('AGENTS.md', claude);
putIn(dir, 'AGENTS.md', claude);
symlinkSync('AGENTS.md', join(dir, 'CLAUDE.md'));
} else {
put('CLAUDE.md', claude);
putIn(dir, 'CLAUDE.md', claude);
}
g(repo, 'add', '-A');
g(repo, 'commit', '-q', '-m', 'feat: the work');
const reviewed = g(repo, 'rev-parse', 'HEAD');
return { base, reviewed: g(repo, 'rev-parse', 'HEAD') };
}
/**
* A throwaway repo with one reviewed commit (a.mjs, CLAUDE.md, the lessons
* file), no git hooks. `sub` puts the project in a subdirectory of the repo;
* `claudeLink` makes CLAUDE.md a symlink to AGENTS.md. Each variant is built
* once; every call gets its own copy of it.
*/
function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {}) {
const key = JSON.stringify([sub, claude, claudeLink]);
if (!seeds.has(key)) seeds.set(key, seedRepo({ sub, claude, claudeLink }));
const seed = seeds.get(key);
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-'));
cpSync(seed.base, base, { recursive: true, verbatimSymlinks: true });
const repo = join(base, 'repo');
const dir = sub ? join(repo, sub) : repo;
// The copy's hooks path is its own empty hooks-none, not the seed's.
const config = join(repo, '.git', 'config');
writeFileSync(config, readFileSync(config, 'utf8').split(seed.base).join(base));
const { reviewed } = seed;
const put = (rel, text) => putIn(dir, rel, text);
const commit = (msg, files) => {
for (const [rel, text] of Object.entries(files)) put(rel, text);
g(repo, 'add', '-A');
g(repo, 'commit', '-q', '-m', msg);
return g(repo, 'rev-parse', 'HEAD');
};
const gate = ({ verdict = 'ALLOW', sha = reviewed } = {}) => {
const p = join(base, 'review.md');
@ -311,7 +332,7 @@ function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {})
/** The lessons commit /trekexecute Phase 7.2 makes after a review. */
function lessonsCommit(fx, claudeAfter = `# Project\n\n${POINTER}`) {
return fx.commit('docs(voyage): review lessons (round 1)', { [LESSONS]: LESSONS_AFTER, 'CLAUDE.md': claudeAfter });
fx.commit('docs(voyage): review lessons (round 1)', { [LESSONS]: LESSONS_AFTER, 'CLAUDE.md': claudeAfter });
}
function assertStale(r, re = /review-stale/) {
@ -380,7 +401,8 @@ test('[R6] --repo: the reviewed commit on an unmerged side branch → exit 2 "no
const fx = gateRepo();
try {
fx.git('checkout', '-q', '-b', 'side');
const side = fx.commit('fix: side work', { 'a.mjs': 'export const a = 3;\n' });
fx.commit('fix: side work', { 'a.mjs': 'export const a = 3;\n' });
const side = fx.git('rev-parse', 'HEAD');
fx.git('checkout', '-q', 'main');
lessonsCommit(fx);
assertStale(fx.gate({ sha: side }), /not an ancestor of HEAD/);

View file

@ -9,7 +9,7 @@ import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { illegalBriefLowering, severityRank as rank } from '../../lib/review/coordinator-contract.mjs';
import { dedupByTriplet, illegalBriefLowering, severityRank as rank } from '../../lib/review/coordinator-contract.mjs';
import { ENGINES } from '../../lib/review/engine-findings.mjs';
import { RULE_CATALOGUE } from '../../lib/review/rule-catalogue.mjs';
@ -719,6 +719,90 @@ test('[ORD] sibling: workflow — the title classes are placed in alphabetical o
}
});
// ---- Parity: the workflow's inlined dedup groups as coordinator-contract Pass 1 does ----
//
// The script cannot import, so its groupTriplet is a hand-kept copy of
// lib/review/coordinator-contract.mjs's. Both run on the ORD fixtures of this
// file and of tests/lib/coordinator-contract.test.mjs, plus the WF floor
// fixture below (the one where two titles from two sources share a row), in
// every arrival order within a payload, and must give the same rows in the
// same order. Each finding
// carries its workflow source as `reviewer` (what the contract groups and
// orders by) and, from /code-review, `engine` last (where the workflow puts
// it), so both read the same objects. A row is compared by its survivor and its
// group's severity.
/** Every order of `xs`. */
function permutations(xs) {
if (xs.length <= 1) return [xs];
return xs.flatMap((x, i) => permutations([...xs.slice(0, i), ...xs.slice(i + 1)]).map((rest) => [x, ...rest]));
}
const SRC_F = 'reviewer:brief-conformance';
const SRC_C = 'reviewer:code-correctness';
const SRC_E = 'engine:claude-code-review';
const from = (src, f) => ({ ...f, reviewer: src, ...(src === SRC_E ? { engine: 'code-review' } : {}) });
const rowSig = (r) => JSON.stringify([r.title ?? null, r.severity, r.engine ?? null, r.contradicts_brief ?? null, r.recommended_action ?? null]);
function parityFixtures() {
const out = [];
const add = (name, { f = [], c = [], e = [] }) => out.push({
name, f: f.map((x) => from(SRC_F, x)), c: c.map((x) => from(SRC_C, x)), e: e.map((x) => from(SRC_E, x)),
});
// tests/lib/coordinator-contract.test.mjs
const slug = { file: 'bin/slug.mjs', line: 46, detail: 'd' };
for (const [rule_key, a, b, cb] of [['MISSING_TEST', 'MAJOR', 'MAJOR', 'MAJOR'], ['NOT_IN_THE_CATALOGUE', 'MINOR', 'MAJOR', 'MINOR']]) {
add(`cA ${a}, fB ${b}, cB ${cb} (${rule_key})`, {
f: [{ ...slug, rule_key, severity: b, title: 'B' }],
c: [{ ...slug, rule_key, severity: a, title: 'A' }, { ...slug, rule_key, severity: cb, title: 'B' }],
});
}
const off = { ...slug, rule_key: 'NOT_IN_THE_CATALOGUE' };
add('cA, fB, cB and an untitled correctness BLOCKER', {
f: [{ ...off, severity: 'MAJOR', title: 'B' }],
c: [{ ...off, severity: 'BLOCKER', title: 'A', recommended_action: '' }, { ...off, severity: 'MINOR', title: 'B' }, { ...off, severity: 'BLOCKER' }],
});
const inj = { file: 'x.mjs', line: 3, rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER' };
add('one reviewer\'s two same-title BLOCKERs', {
c: [{ ...inj, title: 'Injection', recommended_action: '' }, { ...inj, title: 'injection ', recommended_action: 'Quote the argument.' }],
});
const t = { file: 'x.mjs', line: 3, rule_key: 'CODE_REVIEW_DEFECT', severity: 'MAJOR', title: 'T' };
add('two /code-review MAJORs titled T, one marked', { e: [{ ...t, contradicts_brief: MARK }, { ...t }] });
// this file
const K = 'CODE_REVIEW_DEFECT';
add('conformance B with /code-review A and B', {
f: [F(K, 'MINOR', { title: 'B' })], e: [F(K, 'MINOR', { title: 'A' }), F(K, 'MAJOR', { title: 'B' })],
});
add('a conformance R next to /code-review E1 and E2 (WF)', {
f: [F(K, 'MAJOR', { title: 'R' })], e: [F(K, 'MINOR', { title: 'E1' }), F(K, 'MINOR', { title: 'E2' })],
});
const { title: _t, ...untitled } = F(K, 'BLOCKER');
add('conformance B with /code-review A, B and an untitled BLOCKER', {
f: [F(K, 'MAJOR', { title: 'B' })], e: [F(K, 'BLOCKER', { title: 'A' }), F(K, 'MINOR', { title: 'B' }), untitled],
});
return out;
}
test('parity: the workflow dedup and coordinator-contract dedupByTriplet give the same rows on the ORD fixtures, in every arrival order within a payload', async () => {
let runs = 0;
for (const fx of parityFixtures()) {
for (const f of permutations(fx.f)) for (const c of permutations(fx.c)) for (const e of permutations(fx.e)) {
const name = `${fx.name}, order ${[...f, ...c, ...e].map((x) => `${x.reviewer}/${x.title ?? 'U'}`).join(', ')}`;
const s = stubs({
reviewers: [{ reviewer: 'brief-conformance', findings: structuredClone(f) }, { reviewer: 'code-correctness', findings: structuredClone(c) }],
coordinator: { verdict: 'WARN', findings: [] },
});
await exec({ ...BASE, enginePayloads: BOTH_ENGINES(structuredClone(e)) }, s);
const prompt = s.calls.coordinatorPrompt;
assert.ok(typeof prompt === 'string', `${name}: the coordinator ran`);
const merged = JSON.parse(prompt.slice(prompt.indexOf(PROMPT_JSON_MARK) + PROMPT_JSON_MARK.length));
assert.deepEqual(merged.map(rowSig), dedupByTriplet(structuredClone([...f, ...c, ...e])).map(rowSig), name);
runs++;
}
}
assert.equal(runs, 24, 'control: every fixture ran in each of its arrival orders');
});
// ---- WF: the floor and the put-back match each returned finding to its own row ----
//
// A row is a merged finding; its key is its triplet plus its normalized title.