refactor(cleanup): simplify pass on the R-a diff

engine-compare imports the coordinator's title limit and trim instead
of a copy whose reason went with Arm B; the autonomy gate exports one
parser and its test runs in-process with two CLI runs; one import
scanner in lib-imports; the fenced-section walker shared from
tests/helpers/md-section.mjs instead of six copies; plan-gate hash
in-process; living-paths scans once; routing drops the orchestrator
filter whose files are gone; small leftovers removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-07 13:31:30 +01:00
commit efe981f7bc
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
18 changed files with 161 additions and 358 deletions

View file

@ -444,7 +444,7 @@ function effectiveSeverity(f) {
return getRule(f.rule_key)?.severity ?? f.severity; return getRule(f.rule_key)?.severity ?? f.severity;
} }
function trimText(text, max) { export function trimText(text, max) {
return text.slice(0, max - TRIM_MARKER.length) + TRIM_MARKER; return text.slice(0, max - TRIM_MARKER.length) + TRIM_MARKER;
} }

View file

@ -18,6 +18,7 @@
// (lib/review/coordinator-contract.mjs runContract, `engine:out-of-scope`). // (lib/review/coordinator-contract.mjs runContract, `engine:out-of-scope`).
import { ENGINES } from './engine-findings.mjs'; import { ENGINES } from './engine-findings.mjs';
import { JUDGE_TITLE_MAX, trimText } from './coordinator-contract.mjs';
// The (file,line,rule_key) triplet a finding is deduped and matched on. // 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}`; const tripletOf = (f) => `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
@ -26,14 +27,10 @@ const normTitle = (f) => String(f.title).trim().toLowerCase();
// The row a finding is matched to after the coordinator: its triplet plus its // The row a finding is matched to after the coordinator: its triplet plus its
// `normTitle`. An untitled finding gets a key no titled row has. // `normTitle`. An untitled finding gets a key no titled row has.
const rowOf = (f) => `${tripletOf(f)}\u0000${f.title === undefined ? '\u0001' : normTitle(f)}`; const rowOf = (f) => `${tripletOf(f)}\u0000${f.title === undefined ? '\u0001' : normTitle(f)}`;
// The coordinator trims an over-long MINOR/SUGGESTION title as // A title as lib/review/coordinator-contract.mjs judgeFilter trims an
// lib/review/coordinator-contract.mjs judgeFilter does; its marker and limit // over-long MINOR/SUGGESTION title: over JUDGE_TITLE_MAX characters it is
// are inlined. // trimText's cut; any other title unchanged.
const TRIM_MARKER = ' […trimmed]'; const trimmedTitle = (t) => (t.length > JUDGE_TITLE_MAX ? trimText(t, JUDGE_TITLE_MAX) : t);
const JUDGE_TITLE_MAX = 100;
// A title as that trim writes it: over JUDGE_TITLE_MAX characters, cut to
// leave room for the marker, then the marker; any other title unchanged.
const trimmedTitle = (t) => (t.length > JUDGE_TITLE_MAX ? t.slice(0, JUDGE_TITLE_MAX - TRIM_MARKER.length) + TRIM_MARKER : t);
export { tripletOf, normTitle, rowOf, trimmedTitle }; export { tripletOf, normTitle, rowOf, trimmedTitle };

View file

@ -150,10 +150,7 @@ export function collisions(corpus, t = THRESHOLDS) {
// ---- loading ---- // ---- loading ----
/** Orchestrator reference docs are not spawnable capabilities (CLAUDE.md, Inventory). */ /** Skills, commands and agents of a plugin root. */
const isReferenceDoc = (name, description) => /-orchestrator$/.test(name) || /^Reference document, not a spawnable/.test(description.trim());
/** Skills, commands and spawnable agents of a plugin root. */
export function loadEntries(root) { export function loadEntries(root) {
const entries = []; const entries = [];
const skillsDir = join(root, 'skills'); const skillsDir = join(root, 'skills');
@ -174,9 +171,7 @@ export function loadEntries(root) {
const text = readFileSync(join(d, file), 'utf8'); const text = readFileSync(join(d, file), 'utf8');
const fm = readFrontmatterFields(text).fields; const fm = readFrontmatterFields(text).fields;
const name = fm.name ? String(fm.name.value) : file.replace(/\.md$/, ''); const name = fm.name ? String(fm.name.value) : file.replace(/\.md$/, '');
const description = descriptionOf(text); entries.push({ id: `${kind}:${name}`, kind, name, description: descriptionOf(text) });
if (kind === 'agent' && isReferenceDoc(name, description)) continue;
entries.push({ id: `${kind}:${name}`, kind, name, description });
} }
} }
return entries; return entries;

View file

@ -15,13 +15,8 @@
import { isMain } from './is-main.mjs'; import { isMain } from './is-main.mjs';
/** /** A --gates value: true|closed = on, false|open = off, else undefined. */
* A --gates value: true|closed = on, false|open = off, else undefined. function gatesValue(v) {
*
* @param {string|undefined} v
* @returns {boolean|undefined}
*/
export function gatesValue(v) {
if (v === 'true' || v === 'closed') return true; if (v === 'true' || v === 'closed') return true;
if (v === 'false' || v === 'open') return false; if (v === 'false' || v === 'open') return false;
return undefined; return undefined;

View file

@ -17,7 +17,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { runHook } from '../helpers/hook-helper.mjs'; import { runHook } from '../helpers/hook-helper.mjs';
import { findSection } from '../../lib/parsers/plan-schema.mjs'; import { sectionText } from '../helpers/md-section.mjs';
const HERE = dirname(fileURLToPath(import.meta.url)); const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..'); const ROOT = join(HERE, '..', '..');
@ -300,24 +300,6 @@ test('trekcontinue Bug 4 (f-2) confirm deletes and (f-3) idempotent re-run handl
/** Whitespace (line breaks too) collapsed to one space. */ /** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim(); const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */ /** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function libLine(text, mod, verb = '') { function libLine(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');

View file

@ -17,7 +17,7 @@ import { dirname, join } from 'node:path';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { childEnv } from '../helpers/child-env.mjs'; import { childEnv } from '../helpers/child-env.mjs';
import { findSection } from '../../lib/parsers/plan-schema.mjs'; import { sectionText } from '../helpers/md-section.mjs';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs'; import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url)); const HERE = dirname(fileURLToPath(import.meta.url));
@ -244,24 +244,6 @@ test("[DJ5] a label with an apostrophe is kept", () => {
/** Whitespace (line breaks too) collapsed to one space. */ /** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim(); const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */ /** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function libLine(text, mod, verb = '') { function libLine(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');

View file

@ -13,7 +13,7 @@ import { spawnSync } from 'node:child_process';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs'; import { parseDocument } from '../../lib/util/frontmatter.mjs';
import { findSection } from '../../lib/parsers/plan-schema.mjs'; import { sectionText } from '../helpers/md-section.mjs';
import { childEnv } from '../helpers/child-env.mjs'; import { childEnv } from '../helpers/child-env.mjs';
const HERE = dirname(fileURLToPath(import.meta.url)); const HERE = dirname(fileURLToPath(import.meta.url));
@ -165,24 +165,6 @@ test('trekexecute — iterations_remaining decrement + backfill + gate cross-che
/** Whitespace (line breaks too) collapsed to one space. */ /** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim(); const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */ /** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function libLine(text, mod, verb = '') { function libLine(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
@ -340,11 +322,11 @@ function staleFixture() {
return sharedStale; return sharedStale;
} }
/** The lock line with its review path filled in, run in `repo` through bash(). */ /** The lock line with its review path filled in for review.md in `dir`. */
function runLock(cmd, dir, repo) { const fillLock = (cmd, dir) => cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
const filled = cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
return bash(filled, repo); /** The lock line, filled for `dir`, run in `repo` through bash(). */
} const runLock = (cmd, dir, repo) => bash(fillLock(cmd, dir), repo);
test('trekexecute merge lock: the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdict-gate.mjs line, with --repo .', () => { test('trekexecute merge lock: the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdict-gate.mjs line, with --repo .', () => {
const text = read(); const text = read();
@ -358,10 +340,14 @@ test('trekexecute merge lock: the extracted lock lines, run on an ALLOW review o
const text = read(); const text = read();
const lines = [libLine(sectionText(text, MERGE_GATE), 'review/verdict-gate.mjs'), libLine(sectionText(text, PHASE_72), 'review/verdict-gate.mjs')]; const lines = [libLine(sectionText(text, MERGE_GATE), 'review/verdict-gate.mjs'), libLine(sectionText(text, PHASE_72), 'review/verdict-gate.mjs')];
const fx = staleFixture(); const fx = staleFixture();
for (const cmd of lines) { const filled = lines.map((cmd) => fillLock(cmd, fx.review('ALLOW', fx.head)));
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo); assert.equal(filled[0], filled[1], 'both lock lines fill to the same command');
for (const cmd of new Set(filled)) {
fx.review('ALLOW', fx.head);
const ok = bash(cmd, fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`); 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); fx.review('ALLOW', fx.reviewed);
const r = bash(cmd, fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`); 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; const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`); assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);

View file

@ -0,0 +1,26 @@
// tests/helpers/md-section.mjs
// The text of one markdown section, for the prose-pin tests.
import { strict as assert } from 'node:assert';
import { findSection } from '../../lib/parsers/plan-schema.mjs';
/**
* The text of a heading's section (level from its #-count), up to the next
* heading of the same or a higher level outside a fence. The heading must exist.
* @param {string} text
* @param {string} heading - e.g. `## Phase 8 — Final report`
* @returns {string}
*/
export function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}

View file

@ -5,110 +5,58 @@
import { test } from 'node:test'; import { test } from 'node:test';
import { strict as assert } from 'node:assert'; import { strict as assert } from 'node:assert';
import { execFileSync, spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url';
import { findSection } from '../../lib/parsers/plan-schema.mjs'; import { parseGatesArgs } from '../../lib/util/autonomy-gate.mjs';
import { sectionText } from '../helpers/md-section.mjs';
const HERE = dirname(fileURLToPath(import.meta.url)); const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..'); const ROOT = join(HERE, '..', '..');
const SHIM = join(ROOT, 'lib', 'util', 'autonomy-gate.mjs'); const SHIM = join(ROOT, 'lib', 'util', 'autonomy-gate.mjs');
const GATES_USAGE = 'usage: --gates takes true, false, closed or open'; const GATES_USAGE = 'usage: --gates takes true, false, closed or open';
const USAGE = 'usage: autonomy-gate.mjs [--gates [true|false|closed|open]]';
function runShim(args) { const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
try {
const out = execFileSync(process.execPath, [SHIM, ...args], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return { code: 0, out };
} catch (e) {
return { code: e.status ?? 1, out: e.stdout?.toString() ?? '' };
}
}
// The exit code is asserted before any JSON.parse, so a crash or a usage
// error never reads as a wrong value.
function shimGates(args) {
const r = runShim(args);
assert.equal(r.code, 0, `expected exit 0 for ${JSON.stringify(args)}, got ${r.code}: ${r.out}`);
const parsed = JSON.parse(r.out.trim());
assert.deepEqual(parsed, { ok: true, gates: parsed.gates });
assert.equal(typeof parsed.gates, 'boolean', `gates must be a boolean: ${r.out}`);
return parsed.gates;
}
function shimUsage(args) {
const r = runShim(args);
assert.equal(r.code, 1, `expected exit 1 for ${JSON.stringify(args)}, got ${r.code}: ${r.out}`);
const parsed = JSON.parse(r.out.trim());
assert.equal(parsed.ok, false);
assert.equal(Object.keys(parsed).sort().join(','), 'error,ok');
return parsed.error;
}
// ---- the exported parser ---------------------------------------------------- // ---- the exported parser ----------------------------------------------------
test('gatesValue maps true|closed to on, false|open to off, anything else to undefined', async () => { test('parseGatesArgs: bare --gates and true|closed are on, false|open and no flag are off, in both forms', () => {
const mod = await import(pathToFileURL(SHIM).href); const on = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: true }, JSON.stringify(argv));
assert.equal(typeof mod.gatesValue, 'function', 'autonomy-gate.mjs must export gatesValue'); const off = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: false }, JSON.stringify(argv));
for (const v of ['true', 'closed']) assert.equal(mod.gatesValue(v), true, v); on(['--gates']);
for (const v of ['false', 'open']) assert.equal(mod.gatesValue(v), false, v); off([]);
for (const v of ['TRUE', 'yes', 'adaptive', '', undefined]) assert.equal(mod.gatesValue(v), undefined, String(v)); for (const v of ['true', 'closed']) { on(['--gates', v]); on([`--gates=${v}`]); }
for (const v of ['false', 'open']) { off(['--gates', v]); off([`--gates=${v}`]); }
}); });
test('the module is the flag parser only: no state machine is exported', async () => { test('parseGatesArgs: any other value is the exact --gates usage error', () => {
const mod = await import(pathToFileURL(SHIM).href); for (const bad of [['--gates', 'TRUE'], ['--gates', 'yes'], ['--gates', 'adaptive'], ['--gates=maybe'], ['--gates=']]) {
for (const gone of ['STATES', 'EVENTS', 'transition', 'isTerminal']) { assert.deepEqual(parseGatesArgs(bad), { ok: false, error: GATES_USAGE }, bad.join(' '));
assert.equal(mod[gone], undefined, `autonomy-gate.mjs still exports ${gone}`);
} }
}); });
test('parseGatesArgs: any other argument (the old --state/--event too) is the exact usage error', () => {
assert.deepEqual(parseGatesArgs(['--gates', '--state', 'idle']), { ok: false, error: USAGE });
});
// ---- the CLI ---------------------------------------------------------------- // ---- the CLI ----------------------------------------------------------------
test('CLI: bare --gates → {"ok":true,"gates":true}, exit 0', () => { const runShim = (args) => spawnSync(process.execPath, [SHIM, ...args], { encoding: 'utf8' });
assert.equal(shimGates(['--gates']), true);
test('CLI: --gates closed prints {"ok":true,"gates":true}, exit 0', () => {
const r = runShim(['--gates', 'closed']);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.equal(r.stdout, '{"ok":true,"gates":true}\n');
}); });
test('CLI: no flag → {"ok":true,"gates":false}, exit 0', () => { test('CLI: a bad value prints the usage error as JSON, exit 1', () => {
assert.equal(shimGates([]), false); const r = runShim(['--gates', 'maybe']);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.equal(r.stdout, `${JSON.stringify({ ok: false, error: GATES_USAGE })}\n`);
}); });
test('CLI: --gates true and --gates closed → gates true', () => {
assert.equal(shimGates(['--gates', 'true']), true);
assert.equal(shimGates(['--gates', 'closed']), true);
});
test('CLI: --gates false and --gates open → gates false', () => {
assert.equal(shimGates(['--gates', 'false']), false);
assert.equal(shimGates(['--gates', 'open']), false);
});
test('CLI: the one-token --gates=<value> form is read as --gates <value>', () => {
assert.equal(shimGates(['--gates=true']), true);
assert.equal(shimGates(['--gates=closed']), true);
assert.equal(shimGates(['--gates=false']), false);
assert.equal(shimGates(['--gates=open']), false);
});
for (const bad of [['--gates', 'TRUE'], ['--gates', 'yes'], ['--gates', 'adaptive'], ['--gates=maybe'], ['--gates=']]) {
test(`CLI: ${bad.join(' ')} → the exact usage error, exit 1`, () => {
assert.equal(shimUsage(bad), GATES_USAGE);
});
}
for (const old of [
['--state', 'idle', '--event', 'start', '--gates', 'true'],
['--state', 'idle', '--event', 'start'],
['--gates', '--state', 'idle'],
['--event', 'start'],
]) {
test(`CLI: the old state-machine arguments are a usage error (${old.join(' ')}), exit 1`, () => {
assert.match(shimUsage(old), /^usage:/);
});
}
// ---- the four commands' --gates rule and CLI line ---------------------------- // ---- the four commands' --gates rule and CLI line ----------------------------
// Each of the four commands holds one rule: the next token is the flag's value // Each of the four commands holds one rule: the next token is the flag's value
// only when it is true, false, closed or open; the CLI maps it, run once; a // only when it is true, false, closed or open; the CLI maps it, run once; a
@ -124,14 +72,12 @@ const GATES_RULE = [
'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.', 'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.',
]; ];
const commandText = (cmd) => readFileSync(join(ROOT, 'commands', `${cmd}.md`), 'utf8');
/** Whitespace (line breaks too) collapsed to one space. */ /** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim(); const flat = (s) => s.replace(/\s+/g, ' ').trim();
/** The command's one CLI line, `[value]` filled in with `value` (left out when ''). */ /** The command's one CLI line, `[value]` filled in with `value` (left out when ''). */
function gatesLine(cmd, value) { function gatesLine(cmd, value) {
const hits = commandText(cmd).split('\n') const hits = read(`commands/${cmd}.md`).split('\n')
.filter((l) => /^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/util\/autonomy-gate\.mjs\b/.test(l)); .filter((l) => /^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/util\/autonomy-gate\.mjs\b/.test(l));
assert.equal(hits.length, 1, `${cmd}: expected exactly one autonomy-gate.mjs line; found ${hits.length}`); assert.equal(hits.length, 1, `${cmd}: expected exactly one autonomy-gate.mjs line; found ${hits.length}`);
const l = hits[0].trim(); const l = hits[0].trim();
@ -155,7 +101,7 @@ function lineGates(cmdLine) {
for (const cmd of GATES_COMMANDS) { for (const cmd of GATES_COMMANDS) {
test(`[#1] commands/${cmd}.md holds the shared --gates rule and a CLI line that maps closed to gates true`, () => { test(`[#1] commands/${cmd}.md holds the shared --gates rule and a CLI line that maps closed to gates true`, () => {
const text = commandText(cmd); const text = read(`commands/${cmd}.md`);
const f = flat(text); const f = flat(text);
for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`); for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`);
assert.doesNotMatch(text, /--state \w+ --event \w+/, `${cmd}: the state-machine line is still there`); assert.doesNotMatch(text, /--state \w+ --event \w+/, `${cmd}: the state-machine line is still there`);
@ -178,27 +124,8 @@ test('every command\'s CLI line maps a bare --gates to true and open to false',
// flag and pauses on nothing; the docs give the four values and no strictness // flag and pauses on nothing; the docs give the four values and no strictness
// policy (fix round 4-5a, #1). // policy (fix round 4-5a, #1).
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const EXEC = 'commands/trekexecute.md'; const EXEC = 'commands/trekexecute.md';
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function section(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
const GATES_ONE_PAUSE = '`gates_mode = true` adds one pause, before the auto chain starts (Phase 5); the chain does not pass ' const GATES_ONE_PAUSE = '`gates_mode = true` adds one pause, before the auto chain starts (Phase 5); the chain does not pass '
+ '`--gates` on to `/trekresearch` or `/trekplan`. Default `gates_mode = false` skips that pause.'; + '`--gates` on to `/trekresearch` or `/trekplan`. Default `gates_mode = false` skips that pause.';
@ -210,14 +137,14 @@ test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause
}); });
test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => { test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => {
const p1 = flat(section(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input')); const p1 = flat(sectionText(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input'));
assert.ok(p1.includes(GATES_ONE_PAUSE), 'Phase 1 does not say --gates adds one pause before the auto chain'); assert.ok(p1.includes(GATES_ONE_PAUSE), 'Phase 1 does not say --gates adds one pause before the auto chain');
assert.ok(!p1.includes('every phase boundary in the downstream pipeline'), 'Phase 1 still promises a pause at every downstream phase boundary'); assert.ok(!p1.includes('every phase boundary in the downstream pipeline'), 'Phase 1 still promises a pause at every downstream phase boundary');
assert.ok(!p1.includes('runs continuously until the main-merge gate'), 'Phase 1 still says the auto chain runs until the main-merge gate'); assert.ok(!p1.includes('runs continuously until the main-merge gate'), 'Phase 1 still says the auto chain runs until the main-merge gate');
}); });
test('[#1] sibling: /trekbrief Phase 5 says the auto chain ends at the plan, never at the main-merge gate', () => { test('[#1] sibling: /trekbrief Phase 5 says the auto chain ends at the plan, never at the main-merge gate', () => {
const p5 = flat(section(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)')); const p5 = flat(sectionText(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
assert.ok(p5.includes('The chain ends when `/trekplan` has written the plan (Step 6e); it never starts `/trekexecute`.'), assert.ok(p5.includes('The chain ends when `/trekplan` has written the plan (Step 6e); it never starts `/trekexecute`.'),
'Phase 5 does not say the chain ends when /trekplan has written the plan'); 'Phase 5 does not say the chain ends when /trekplan has written the plan');
assert.ok(!p5.includes('The chain stops only at the main-merge gate'), 'Phase 5 still says the chain stops only at the main-merge gate'); assert.ok(!p5.includes('The chain stops only at the main-merge gate'), 'Phase 5 still says the chain stops only at the main-merge gate');
@ -263,7 +190,7 @@ test('[#1] sibling: docs/command-modes.md gives every --gates row the four value
test('[#1] sibling: README.md gives the four values, and no strictness table', () => { test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
const text = read('README.md'); const text = read('README.md');
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)')); const sec = flat(sectionText(text, '### Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/); assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there'); assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
assert.doesNotMatch(sec, /`adaptive` \(default\)/); assert.doesNotMatch(sec, /`adaptive` \(default\)/);

View file

@ -288,22 +288,20 @@ test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', (
// The read-only reviewers (plan-critic, scope-guardian: Read/Glob/Grep, no Write) // The read-only reviewers (plan-critic, scope-guardian: Read/Glob/Grep, no Write)
// cannot write the temp files the old prose told them to. Pin the A′ fix so the // cannot write the temp files the old prose told them to. Pin the A′ fix so the
// broken /tmp-write instruction cannot creep back: // broken /tmp-write instruction cannot creep back:
// 1. Phase-9 prose in BOTH docs must use --stdin, not the dead /tmp/*-out.json // 1. The Phase-9 prose of commands/trekplan.md must use --stdin, not the dead
// file path the reviewers can't produce. // /tmp/*-out.json file path the reviewers can't produce.
// 2. BOTH reviewer agents must declare a machine-readable json findings block // 2. BOTH reviewer agents must declare a machine-readable json findings block
// with the keys the dedup helper consumes (agent + findings + rule_key). // with the keys the dedup helper consumes (agent + findings + rule_key).
test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json findings block', () => { test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json findings block', () => {
const cmd = read('commands/trekplan.md'); const cmd = read('commands/trekplan.md');
for (const [name, doc] of [['commands/trekplan.md', cmd]]) { assert.ok(
assert.ok( cmd.includes('--stdin'),
doc.includes('--stdin'), 'commands/trekplan.md Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)',
`${name} Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)`, );
); assert.ok(
assert.ok( !cmd.includes('/tmp/plan-critic-out.json') && !cmd.includes('/tmp/scope-guardian-out.json'),
!doc.includes('/tmp/plan-critic-out.json') && !doc.includes('/tmp/scope-guardian-out.json'), 'commands/trekplan.md must NOT instruct the read-only reviewers to Write /tmp/*-out.json — they have no Write tool (defect #1)',
`${name} must NOT instruct the read-only reviewers to Write /tmp/*-out.json — they have no Write tool (defect #1)`, );
);
}
for (const agentFile of ['agents/plan-critic.md', 'agents/scope-guardian.md']) { for (const agentFile of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
const a = read(agentFile); const a = read(agentFile);
assert.ok( assert.ok(

View file

@ -291,9 +291,7 @@ test('[ENG] a finding returned under a trimmed title is not put back twice; an e
// --- the lib row rule ------------------------------------------------ // --- the lib row rule ------------------------------------------------
test('[ENG] lib rowOf and trimmedTitle are exported; an untitled finding gets a key no titled row has', () => { test('[ENG] lib rowOf: an untitled finding gets a key no titled row has', () => {
assert.equal(typeof compareMod.rowOf, 'function', 'engine-compare.mjs exports rowOf');
assert.equal(typeof compareMod.trimmedTitle, 'function', 'engine-compare.mjs exports trimmedTitle');
const untitled = { file: 'src/a.mjs', line: 1, rule_key: 'CODE_REVIEW_DEFECT' }; const untitled = { file: 'src/a.mjs', line: 1, rule_key: 'CODE_REVIEW_DEFECT' };
assert.notEqual(compareMod.rowOf(untitled), compareMod.rowOf({ ...untitled, title: '' }), 'an untitled finding gets a key no titled row has'); assert.notEqual(compareMod.rowOf(untitled), compareMod.rowOf({ ...untitled, title: '' }), 'an untitled finding gets a key no titled row has');
}); });

View file

@ -5,8 +5,7 @@
import { test } from 'node:test'; import { test } from 'node:test';
import { strict as assert } from 'node:assert'; import { strict as assert } from 'node:assert';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { readFileSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve, sep } from 'node:path'; import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -14,30 +13,36 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8'); const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
// ---- lib/ never imports from scripts/ ----------------------------------------
/** /**
* The import specifiers in `text` (a file at `file`) that resolve into the * The import specifiers in `text`: static and side-effect imports, re-exports
* repo's scripts/ directory: static and side-effect imports, re-exports and * and dynamic imports with a string literal.
* dynamic imports with a string literal.
*/ */
function importsIntoScripts(file, text) { function specifiers(text) {
const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g; const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g;
const scripts = join(ROOT, 'scripts') + sep; return [...text.matchAll(re)].map((m) => m[1] ?? m[2]);
const hits = [];
for (const m of text.matchAll(re)) {
const spec = m[1] ?? m[2];
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
if (resolve(dirname(file), spec).startsWith(scripts)) hits.push(spec);
}
return hits;
} }
/** Every .mjs file under `dir`, recursively. */ /**
* `file: specifier` for each import of `files` (repo-relative) that resolves
* into one of `dirs` of the repo. `textOf` reads a file's text.
*/
function forbiddenImports(files, dirs, textOf = read) {
const out = [];
for (const rel of files) {
for (const spec of specifiers(textOf(rel))) {
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
const target = relative(ROOT, resolve(ROOT, dirname(rel), spec)).split(sep).join('/');
if (dirs.some((d) => target === d || target.startsWith(`${d}/`))) out.push(`${rel}: ${spec}`);
}
}
return out;
}
/** Every .mjs file under the repo-relative `dir`, recursively, repo-relative. */
function mjsFiles(dir) { function mjsFiles(dir) {
const out = []; const out = [];
for (const e of readdirSync(dir, { withFileTypes: true })) { for (const e of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
const p = join(dir, e.name); const p = `${dir}/${e.name}`;
if (e.isDirectory()) out.push(...mjsFiles(p)); if (e.isDirectory()) out.push(...mjsFiles(p));
else if (e.name.endsWith('.mjs')) out.push(p); else if (e.name.endsWith('.mjs')) out.push(p);
} }
@ -45,74 +50,23 @@ function mjsFiles(dir) {
} }
test('arch: lib/ never imports from scripts/, and the scan reports a planted import (control)', () => { test('arch: lib/ never imports from scripts/, and the scan reports a planted import (control)', () => {
const planted = importsIntoScripts(join(ROOT, 'lib', 'review', 'x.mjs'), "import x from '../../scripts/release-check.mjs';\n"); const planted = "import x from '../../scripts/release-check.mjs';\n";
assert.deepEqual(planted, ['../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/'); assert.deepEqual(forbiddenImports(['lib/review/x.mjs'], ['scripts'], () => planted),
const files = mjsFiles(join(ROOT, 'lib')); ['lib/review/x.mjs: ../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/');
const files = mjsFiles('lib');
assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`); assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`);
const bad = files.flatMap((f) => importsIntoScripts(f, readFileSync(f, 'utf8')).map((s) => `${f.slice(ROOT.length + 1)}: ${s}`)); assert.deepEqual(forbiddenImports(files, ['scripts']), [], 'lib/ imports from scripts/');
assert.deepEqual(bad, [], 'lib/ imports from scripts/');
}); });
// ---- the review modules import no scripts/ or hooks/ code ---------------------
const REVIEW_LIB = [ const REVIEW_LIB = [
'lib/review/review-answers.mjs', 'lib/review/pre-existing.mjs', 'lib/review/review-rounds.mjs', 'lib/review/review-answers.mjs', 'lib/review/pre-existing.mjs', 'lib/review/review-rounds.mjs',
'lib/review/review-policy.mjs', 'lib/review/engine-compare.mjs', 'lib/review/instruction-scope.mjs', 'lib/review/review-policy.mjs', 'lib/review/engine-compare.mjs', 'lib/review/instruction-scope.mjs',
'lib/review/review-finalize.mjs', 'lib/review/merge-evidence.mjs', 'lib/util/doors.mjs', 'lib/review/review-finalize.mjs', 'lib/review/merge-evidence.mjs', 'lib/util/doors.mjs',
]; ];
const FORBIDDEN_DIRS = ['scripts', 'hooks'];
const PLANTED = "import { checkReview } from '../../scripts/release-check.mjs';\n";
/** The specifiers of a module's import lines: `import … from`, bare `import '…'`, `export … from`, `import('…')`. */ test('arch: the review modules import no hooks/ code, and the scan reports a planted import (control)', () => {
function importSpecifiers(src) { const planted = (rel) => (rel === REVIEW_LIB[0] ? "await import('../../hooks/scripts/pre-bash-executor.mjs');\n" : '') + read(rel);
const out = []; assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks'], planted),
for (const m of src.matchAll(/^[ \t]*import\b(?!\s*\()[^;]*?(['"])([^'"\n]+)\1/gm)) out.push(m[2]); [`${REVIEW_LIB[0]}: ../../hooks/scripts/pre-bash-executor.mjs`], 'control: the scan does not report a planted import from hooks/');
for (const m of src.matchAll(/^[ \t]*export\s*(?:\*|\{)[^;]*?\bfrom\s*(['"])([^'"\n]+)\1/gm)) out.push(m[2]); assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks']), [], 'a review module imports hooks/ code');
for (const m of src.matchAll(/\bimport\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g)) out.push(m[2]);
return out;
}
/** `file: specifier` for each import of `files` (paths under `root`) that resolves into scripts/ or hooks/ of `root`. */
function forbiddenImports(root, files) {
const out = [];
for (const rel of files) {
const abs = join(root, rel);
for (const spec of importSpecifiers(readFileSync(abs, 'utf8'))) {
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
const target = relative(root, resolve(dirname(abs), spec)).split(sep).join('/');
if (FORBIDDEN_DIRS.some((d) => target === d || target.startsWith(`${d}/`))) out.push(`${rel}: ${spec}`);
}
}
return out;
}
test('arch: the review modules import no scripts/ or hooks/ code', () => {
assert.deepEqual([...REVIEW_LIB].sort(), [
'lib/review/engine-compare.mjs', 'lib/review/instruction-scope.mjs', 'lib/review/merge-evidence.mjs',
'lib/review/pre-existing.mjs', 'lib/review/review-answers.mjs', 'lib/review/review-finalize.mjs',
'lib/review/review-policy.mjs', 'lib/review/review-rounds.mjs', 'lib/util/doors.mjs',
], 'the scanner reads exactly these nine files');
for (const rel of REVIEW_LIB) assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
// The real files: nothing.
assert.deepEqual(forbiddenImports(ROOT, REVIEW_LIB), [], 'a review module imports scripts/ or hooks/ code');
const tmp = mkdtempSync(join(tmpdir(), 'lib-imports-'));
try {
for (const rel of REVIEW_LIB) {
mkdirSync(dirname(join(tmp, rel)), { recursive: true });
writeFileSync(join(tmp, rel), read(rel));
}
// Control: the copy reads like the real files.
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [], 'Control: the clean copy reports nothing');
// A planted import of a scripts/ module is reported.
writeFileSync(join(tmp, REVIEW_LIB[0]), PLANTED + read(REVIEW_LIB[0]));
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [`${REVIEW_LIB[0]}: ../../scripts/release-check.mjs`],
'the scanner reports the planted import of ../../scripts/release-check.mjs');
// The clean copy again: nothing.
writeFileSync(join(tmp, REVIEW_LIB[0]), read(REVIEW_LIB[0]));
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [], 'the clean copy again reports nothing');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}); });

View file

@ -41,7 +41,7 @@ const SCOPE_DIRS = ['agents/', 'commands/', 'skills/', 'templates/', 'docs/'];
const SCOPE_ROOT = new Set(['README.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'SECURITY.md']); const SCOPE_ROOT = new Set(['README.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'SECURITY.md']);
// Living tracked text in scope (see the header). // Living tracked text in scope (see the header).
export function inScope(rel) { function inScope(rel) {
if (!rel.endsWith('.md') || /\.local\./.test(rel)) return false; if (!rel.endsWith('.md') || /\.local\./.test(rel)) return false;
if (rel === 'CHANGELOG.md') return false; if (rel === 'CHANGELOG.md') return false;
return SCOPE_ROOT.has(rel) || SCOPE_DIRS.some(d => rel.startsWith(d)); return SCOPE_ROOT.has(rel) || SCOPE_DIRS.some(d => rel.startsWith(d));
@ -49,7 +49,7 @@ export function inScope(rel) {
// Every plugin path a text names, as { line, path } with path relative to the // Every plugin path a text names, as { line, path } with path relative to the
// repository root. `rel` is the text's own path, for relative links. // repository root. `rel` is the text's own path, for relative links.
export function extractPaths(text, rel) { function extractPaths(text, rel) {
const out = []; const out = [];
const lines = text.split('\n'); const lines = text.split('\n');
lines.forEach((line, i) => { lines.forEach((line, i) => {
@ -78,7 +78,7 @@ export function extractPaths(text, rel) {
} }
// "file:line path" for every named path that does not exist under root. // "file:line path" for every named path that does not exist under root.
export function deadPaths(root, files) { function deadPaths(root, files) {
const dead = []; const dead = [];
for (const rel of files) { for (const rel of files) {
const text = readFileSync(join(root, rel), 'utf-8'); const text = readFileSync(join(root, rel), 'utf-8');
@ -89,9 +89,15 @@ export function deadPaths(root, files) {
return dead; return dead;
} }
function trackedFiles(root) { let scan = null;
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
return out.split('\0').filter(Boolean); /** The tracked files and every dead hit in the in-scope ones (EXEMPT not applied), computed once. */
function repoScan() {
if (!scan) {
const files = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf-8' }).split('\0').filter(Boolean);
scan = { files, dead: deadPaths(ROOT, files.filter(inScope)) };
}
return scan;
} }
// Named exceptions: "file path" -> reason. Each must still be dead (a stale // Named exceptions: "file path" -> reason. Each must still be dead (a stale
@ -156,19 +162,19 @@ test('scope keeps living docs and drops history and target-repo text', () => {
}); });
test('living tracked text names only paths that exist', () => { test('living tracked text names only paths that exist', () => {
const files = trackedFiles(ROOT); const { files, dead } = repoScan();
assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`); assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`);
const hits = deadPaths(ROOT, files.filter(inScope)).filter(h => !EXEMPT.has(exemptKey(h))); const hits = dead.filter(h => !EXEMPT.has(exemptKey(h)));
assert.deepEqual(hits, [], `${hits.length} dead path reference(s):\n${hits.join('\n')}`); assert.deepEqual(hits, [], `${hits.length} dead path reference(s):\n${hits.join('\n')}`);
}); });
test('every EXEMPT entry is still a dead reference in a tracked file', () => { test('every EXEMPT entry is still a dead reference in a tracked file', () => {
const tracked = new Set(trackedFiles(ROOT)); const { files, dead } = repoScan();
const tracked = new Set(files);
const deadKeys = new Set(dead.map(exemptKey));
for (const key of EXEMPT.keys()) { for (const key of EXEMPT.keys()) {
const [rel, path] = key.split(' '); const rel = key.split(' ')[0];
assert.ok(tracked.has(rel), `EXEMPT names untracked ${rel}`); assert.ok(tracked.has(rel), `EXEMPT names untracked ${rel}`);
const dead = deadPaths(ROOT, [rel]).map(exemptKey); assert.ok(deadKeys.has(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
assert.ok(dead.includes(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
assert.ok(!existsSync(join(ROOT, path)), `EXEMPT path ${path} exists`);
} }
}); });

View file

@ -12,7 +12,8 @@ import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { MAX_ROUNDS } from '../../lib/validators/plan-gate.mjs'; import { MAX_ROUNDS } from '../../lib/validators/plan-gate.mjs';
import { DESIGN_SUBSECTIONS, OPTIONAL_STEP_FIELDS } from '../../lib/validators/plan-validator.mjs'; import { DESIGN_SUBSECTIONS, OPTIONAL_STEP_FIELDS } from '../../lib/validators/plan-validator.mjs';
import { findSection, findSteps, PLAN_VERSION_CURRENT } from '../../lib/parsers/plan-schema.mjs'; import { findSteps, PLAN_VERSION_CURRENT } from '../../lib/parsers/plan-schema.mjs';
import { sectionText } from '../helpers/md-section.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = rel => readFileSync(join(ROOT, rel), 'utf-8'); const read = rel => readFileSync(join(ROOT, rel), 'utf-8');
@ -134,24 +135,6 @@ test('the verification CLIs credit what they borrow and say they install nothing
/** Whitespace (line breaks too) collapsed to one space. */ /** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim(); const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section (level from its #-count), up to the next
* heading of the same or a higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** /**
* From the one line of `rel` holding `marker` through the end of the next * From the one line of `rel` holding `marker` through the end of the next
* blank-line-separated paragraph after the marker's own paragraph, flattened. * blank-line-separated paragraph after the marker's own paragraph, flattened.

View file

@ -21,7 +21,7 @@ import { validateFindings } from '../../lib/review/findings-schema.mjs';
import { lintSkill } from '../../lib/skills/skill-lint.mjs'; import { lintSkill } from '../../lib/skills/skill-lint.mjs';
import * as routing from '../../lib/skills/routing.mjs'; import * as routing from '../../lib/skills/routing.mjs';
import { childEnv } from '../helpers/child-env.mjs'; import { childEnv } from '../helpers/child-env.mjs';
import * as timing from '../helpers/timing.mjs'; import { CONTROL_RUN } from '../helpers/timing.mjs';
const awaitImportSync = () => routing; const awaitImportSync = () => routing;
@ -1148,12 +1148,10 @@ test('review 630 r9 / 7b3750e9 plan-schema.mjs:372: a plain path that ends with
// control run. Its numbers live in tests/helpers/timing.mjs (CONTROL_RUN): the // control run. Its numbers live in tests/helpers/timing.mjs (CONTROL_RUN): the
// timeout leaves headroom over a slow start, the hang outlasts it. Every run // timeout leaves headroom over a slow start, the hang outlasts it. Every run
// but the control starts slowly on purpose (slowStartMs), so a timeout without // but the control starts slowly on purpose (slowStartMs), so a timeout without
// headroom fails here on demand, not only under load. BEFORE_703D87E1 is the // headroom fails here on demand, not only under load.
// record of the numbers before the fix, taken only when the export is absent.
const BEFORE_703D87E1 = Object.freeze({ timeoutMs: 2000, hangMs: 6000, slowStartMs: 2500 });
test('review 630 r9 / brief note 3 + 703d87e1: red proof reports a control run it stopped as not-run, in the control run, when every other run starts slowly', async () => { test('review 630 r9 / brief note 3 + 703d87e1: red proof reports a control run it stopped as not-run, in the control run, when every other run starts slowly', async () => {
const c = timing.CONTROL_RUN ?? BEFORE_703D87E1; const c = CONTROL_RUN;
const { proveRed } = await import('../../lib/verification/red-proof.mjs'); const { proveRed } = await import('../../lib/verification/red-proof.mjs');
const repo = gitRepo('r630-r9-control-'); const repo = gitRepo('r630-r9-control-');
try { try {

View file

@ -11,7 +11,7 @@ import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { findSection } from '../../lib/parsers/plan-schema.mjs'; import { sectionText } from '../helpers/md-section.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8'); const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
@ -63,24 +63,6 @@ test('run coupling: the writing-criteria quiet case is owned by run, with no sta
// also one with no app to launch (the "no — {reason}" form). // also one with no app to launch (the "no — {reason}" form).
const EXEC = 'commands/trekexecute.md'; const EXEC = 'commands/trekexecute.md';
/**
* The text of a heading's section (level from its #-count), up to the next
* heading of the same or a higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** Phase 8 of /trekexecute, read up to Phase 9 (its report fence holds its own `## ` heading). */ /** Phase 8 of /trekexecute, read up to Phase 9 (its report fence holds its own `## ` heading). */
function phase8(text) { function phase8(text) {
const start = text.indexOf('## Phase 8 — Final report'); const start = text.indexOf('## Phase 8 — Final report');

View file

@ -107,13 +107,11 @@ test('routing: rank puts the matching description first, and a no-overlap prompt
assert.ok(rank('zebra quantum', corpus).every((r) => r.score === 0)); assert.ok(rank('zebra quantum', corpus).every((r) => r.score === 0));
}); });
test('routing: the reader takes block-scalar agent descriptions and skips orchestrator reference docs', () => { test('routing: the reader takes block-scalar agent descriptions', () => {
withPlugin({ withPlugin({
agents: { 'plan-critic': 'Use this agent when a plan needs adversarial review.' }, agents: { 'plan-critic': 'Use this agent when a plan needs adversarial review.' },
commands: { trekplan: 'Deep implementation planning from a task brief.' }, commands: { trekplan: 'Deep implementation planning from a task brief.' },
}, (root) => { }, (root) => {
writeFileSync(join(root, 'agents', 'planning-orchestrator.md'),
'---\nname: planning-orchestrator\ndescription: Reference document, not a spawnable capability.\n---\n');
const ids = loadEntries(root).map((e) => e.id).sort(); const ids = loadEntries(root).map((e) => e.id).sort();
assert.deepEqual(ids, ['agent:plan-critic', 'command:trekplan']); assert.deepEqual(ids, ['agent:plan-critic', 'command:trekplan']);
assert.equal(loadEntries(root).find((e) => e.id === 'agent:plan-critic').description.trim(), assert.equal(loadEntries(root).find((e) => e.id === 'agent:plan-critic').description.trim(),

View file

@ -721,16 +721,12 @@ const snapDir = dir => join(dir, '.git', 'voyage', 'plan-snapshots');
const snaps = dir => (existsSync(snapDir(dir)) ? readdirSync(snapDir(dir)).sort() : []); const snaps = dir => (existsSync(snapDir(dir)) ? readdirSync(snapDir(dir)).sort() : []);
const sha256 = s => createHash('sha256').update(s).digest('hex'); const sha256 = s => createHash('sha256').update(s).digest('hex');
function hashOf(dir, p) { // What the CLI's --hash prints for the plan at `p`, computed in-process.
const r = gate(dir, ['--hash', p]); const hashOf = p => planHash(readFileSync(p, 'utf-8'));
assert.equal(r.code, 0, r.out + r.err);
assert.match(r.out.trim(), /^[0-9a-f]{64}$/);
return r.out.trim();
}
/** One round as Phase 9 runs it: --hash, then --record with the payloads through the real dedup. */ /** One round as Phase 9 runs it: the --hash value, then --record with the payloads through the real dedup. */
function cliRound(dir, p, payload = {}, extra = []) { function cliRound(dir, p, payload = {}, extra = []) {
const reviewed = hashOf(dir, p); const reviewed = hashOf(p);
const r = gate(dir, ['--record', p, '--reviewed', reviewed, ...extra], JSON.stringify(dd(payload))); const r = gate(dir, ['--record', p, '--reviewed', reviewed, ...extra], JSON.stringify(dd(payload)));
const lines = r.out.split('\n').filter(l => l !== ''); const lines = r.out.split('\n').filter(l => l !== '');
return { ...r, reviewed, lines, j: lines.length ? JSON.parse(lines[lines.length - 1]) : null }; return { ...r, reviewed, lines, j: lines.length ? JSON.parse(lines[lines.length - 1]) : null };
@ -805,7 +801,7 @@ test('20 (CLI): a refused round writes nothing: exit 2 on a stale --reviewed, ex
assert.deepEqual(snaps(dir), [`${r1.reviewed}.md`]); assert.deepEqual(snaps(dir), [`${r1.reviewed}.md`]);
// exit 2: the plan changed after --hash // exit 2: the plan changed after --hash
const stale = hashOf(dir, p); const stale = hashOf(p);
const t2 = editA(readPlan(p)); const t2 = editA(readPlan(p));
writeFileSync(p, t2); writeFileSync(p, t2);
const s = gate(dir, ['--record', p, '--reviewed', stale], JSON.stringify(dd())); const s = gate(dir, ['--record', p, '--reviewed', stale], JSON.stringify(dd()));
@ -938,9 +934,9 @@ test('23 (CLI): --record on a plan outside any git repo keeps no snapshot anywhe
// CLI's --stdin, its stdout into the gate's --record (no in-process dedup). // CLI's --stdin, its stdout into the gate's --record (no in-process dedup).
const DEDUP = join(ROOT, 'lib', 'review', 'plan-review-dedup.mjs'); const DEDUP = join(ROOT, 'lib', 'review', 'plan-review-dedup.mjs');
/** One round through the two CLIs: --hash, dedup --stdin on `payload`, then --record on its output. */ /** One round through the two CLIs: the --hash value, dedup --stdin on `payload`, then --record on its output. */
function pipeRound(dir, p, payload) { function pipeRound(dir, p, payload) {
const reviewed = hashOf(dir, p); const reviewed = hashOf(p);
const d = spawnSync(process.execPath, [DEDUP, '--stdin'], { cwd: dir, input: JSON.stringify(payload), encoding: 'utf-8', env: childEnv(GIT_CLEAN) }); const d = spawnSync(process.execPath, [DEDUP, '--stdin'], { cwd: dir, input: JSON.stringify(payload), encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
assert.equal(d.status, 0, `plan-review-dedup --stdin: ${d.stdout}${d.stderr}`); assert.equal(d.status, 0, `plan-review-dedup --stdin: ${d.stdout}${d.stderr}`);
const r = gate(dir, ['--record', p, '--reviewed', reviewed], d.stdout); const r = gate(dir, ['--record', p, '--reviewed', reviewed], d.stdout);