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:
parent
029e83117c
commit
efe981f7bc
18 changed files with 161 additions and 358 deletions
|
|
@ -5,110 +5,58 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
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 { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseGatesArgs } from '../../lib/util/autonomy-gate.mjs';
|
||||
import { sectionText } from '../helpers/md-section.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const SHIM = join(ROOT, 'lib', 'util', 'autonomy-gate.mjs');
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
|
||||
// ---- the exported parser ----------------------------------------------------
|
||||
|
||||
test('gatesValue maps true|closed to on, false|open to off, anything else to undefined', async () => {
|
||||
const mod = await import(pathToFileURL(SHIM).href);
|
||||
assert.equal(typeof mod.gatesValue, 'function', 'autonomy-gate.mjs must export gatesValue');
|
||||
for (const v of ['true', 'closed']) assert.equal(mod.gatesValue(v), true, v);
|
||||
for (const v of ['false', 'open']) assert.equal(mod.gatesValue(v), false, v);
|
||||
for (const v of ['TRUE', 'yes', 'adaptive', '', undefined]) assert.equal(mod.gatesValue(v), undefined, String(v));
|
||||
test('parseGatesArgs: bare --gates and true|closed are on, false|open and no flag are off, in both forms', () => {
|
||||
const on = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: true }, JSON.stringify(argv));
|
||||
const off = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: false }, JSON.stringify(argv));
|
||||
on(['--gates']);
|
||||
off([]);
|
||||
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 () => {
|
||||
const mod = await import(pathToFileURL(SHIM).href);
|
||||
for (const gone of ['STATES', 'EVENTS', 'transition', 'isTerminal']) {
|
||||
assert.equal(mod[gone], undefined, `autonomy-gate.mjs still exports ${gone}`);
|
||||
test('parseGatesArgs: any other value is the exact --gates usage error', () => {
|
||||
for (const bad of [['--gates', 'TRUE'], ['--gates', 'yes'], ['--gates', 'adaptive'], ['--gates=maybe'], ['--gates=']]) {
|
||||
assert.deepEqual(parseGatesArgs(bad), { ok: false, error: GATES_USAGE }, bad.join(' '));
|
||||
}
|
||||
});
|
||||
|
||||
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 ----------------------------------------------------------------
|
||||
|
||||
test('CLI: bare --gates → {"ok":true,"gates":true}, exit 0', () => {
|
||||
assert.equal(shimGates(['--gates']), true);
|
||||
const runShim = (args) => spawnSync(process.execPath, [SHIM, ...args], { encoding: 'utf8' });
|
||||
|
||||
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', () => {
|
||||
assert.equal(shimGates([]), false);
|
||||
test('CLI: a bad value prints the usage error as JSON, exit 1', () => {
|
||||
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 ----------------------------
|
||||
// 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
|
||||
|
|
@ -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.',
|
||||
];
|
||||
|
||||
const commandText = (cmd) => readFileSync(join(ROOT, 'commands', `${cmd}.md`), 'utf8');
|
||||
|
||||
/** Whitespace (line breaks too) collapsed to one space. */
|
||||
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
/** The command's one CLI line, `[value]` filled in with `value` (left out when ''). */
|
||||
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));
|
||||
assert.equal(hits.length, 1, `${cmd}: expected exactly one autonomy-gate.mjs line; found ${hits.length}`);
|
||||
const l = hits[0].trim();
|
||||
|
|
@ -155,7 +101,7 @@ function lineGates(cmdLine) {
|
|||
|
||||
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`, () => {
|
||||
const text = commandText(cmd);
|
||||
const text = read(`commands/${cmd}.md`);
|
||||
const f = flat(text);
|
||||
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`);
|
||||
|
|
@ -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
|
||||
// policy (fix round 4-5a, #1).
|
||||
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
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 '
|
||||
+ '`--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', () => {
|
||||
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('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');
|
||||
});
|
||||
|
||||
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`.'),
|
||||
'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');
|
||||
|
|
@ -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', () => {
|
||||
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(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
|
||||
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// cannot write the temp files the old prose told them to. Pin the A′ fix so the
|
||||
// broken /tmp-write instruction cannot creep back:
|
||||
// 1. Phase-9 prose in BOTH docs must use --stdin, not the dead /tmp/*-out.json
|
||||
// file path the reviewers can't produce.
|
||||
// 1. The Phase-9 prose of commands/trekplan.md must use --stdin, not the dead
|
||||
// /tmp/*-out.json file path the reviewers can't produce.
|
||||
// 2. BOTH reviewer agents must declare a machine-readable json findings block
|
||||
// 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', () => {
|
||||
const cmd = read('commands/trekplan.md');
|
||||
for (const [name, doc] of [['commands/trekplan.md', cmd]]) {
|
||||
assert.ok(
|
||||
doc.includes('--stdin'),
|
||||
`${name} Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)`,
|
||||
);
|
||||
assert.ok(
|
||||
!doc.includes('/tmp/plan-critic-out.json') && !doc.includes('/tmp/scope-guardian-out.json'),
|
||||
`${name} must NOT instruct the read-only reviewers to Write /tmp/*-out.json — they have no Write tool (defect #1)`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
cmd.includes('--stdin'),
|
||||
'commands/trekplan.md Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)',
|
||||
);
|
||||
assert.ok(
|
||||
!cmd.includes('/tmp/plan-critic-out.json') && !cmd.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)',
|
||||
);
|
||||
for (const agentFile of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
|
||||
const a = read(agentFile);
|
||||
assert.ok(
|
||||
|
|
|
|||
|
|
@ -291,9 +291,7 @@ test('[ENG] a finding returned under a trimmed title is not put back twice; an e
|
|||
|
||||
// --- the lib row rule ------------------------------------------------
|
||||
|
||||
test('[ENG] lib rowOf and trimmedTitle are exported; 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');
|
||||
test('[ENG] lib rowOf: an untitled finding gets a key no titled row has', () => {
|
||||
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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||||
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');
|
||||
|
||||
// ---- lib/ never imports from scripts/ ----------------------------------------
|
||||
|
||||
/**
|
||||
* The import specifiers in `text` (a file at `file`) that resolve into the
|
||||
* repo's scripts/ directory: static and side-effect imports, re-exports and
|
||||
* dynamic imports with a string literal.
|
||||
* The import specifiers in `text`: static and side-effect imports, re-exports
|
||||
* and 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 scripts = join(ROOT, 'scripts') + sep;
|
||||
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;
|
||||
return [...text.matchAll(re)].map((m) => m[1] ?? m[2]);
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
const out = [];
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
for (const e of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
||||
const p = `${dir}/${e.name}`;
|
||||
if (e.isDirectory()) out.push(...mjsFiles(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)', () => {
|
||||
const planted = importsIntoScripts(join(ROOT, 'lib', 'review', 'x.mjs'), "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/');
|
||||
const files = mjsFiles(join(ROOT, 'lib'));
|
||||
const planted = "import x from '../../scripts/release-check.mjs';\n";
|
||||
assert.deepEqual(forbiddenImports(['lib/review/x.mjs'], ['scripts'], () => planted),
|
||||
['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/`);
|
||||
const bad = files.flatMap((f) => importsIntoScripts(f, readFileSync(f, 'utf8')).map((s) => `${f.slice(ROOT.length + 1)}: ${s}`));
|
||||
assert.deepEqual(bad, [], 'lib/ imports from scripts/');
|
||||
assert.deepEqual(forbiddenImports(files, ['scripts']), [], 'lib/ imports from scripts/');
|
||||
});
|
||||
|
||||
// ---- the review modules import no scripts/ or hooks/ code ---------------------
|
||||
|
||||
const REVIEW_LIB = [
|
||||
'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-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('…')`. */
|
||||
function importSpecifiers(src) {
|
||||
const out = [];
|
||||
for (const m of src.matchAll(/^[ \t]*import\b(?!\s*\()[^;]*?(['"])([^'"\n]+)\1/gm)) out.push(m[2]);
|
||||
for (const m of src.matchAll(/^[ \t]*export\s*(?:\*|\{)[^;]*?\bfrom\s*(['"])([^'"\n]+)\1/gm)) out.push(m[2]);
|
||||
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 });
|
||||
}
|
||||
test('arch: the review modules import no hooks/ code, and the scan reports a planted import (control)', () => {
|
||||
const planted = (rel) => (rel === REVIEW_LIB[0] ? "await import('../../hooks/scripts/pre-bash-executor.mjs');\n" : '') + read(rel);
|
||||
assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks'], planted),
|
||||
[`${REVIEW_LIB[0]}: ../../hooks/scripts/pre-bash-executor.mjs`], 'control: the scan does not report a planted import from hooks/');
|
||||
assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks']), [], 'a review module imports hooks/ code');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
// 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 === 'CHANGELOG.md') return false;
|
||||
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
|
||||
// repository root. `rel` is the text's own path, for relative links.
|
||||
export function extractPaths(text, rel) {
|
||||
function extractPaths(text, rel) {
|
||||
const out = [];
|
||||
const lines = text.split('\n');
|
||||
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.
|
||||
export function deadPaths(root, files) {
|
||||
function deadPaths(root, files) {
|
||||
const dead = [];
|
||||
for (const rel of files) {
|
||||
const text = readFileSync(join(root, rel), 'utf-8');
|
||||
|
|
@ -89,9 +89,15 @@ export function deadPaths(root, files) {
|
|||
return dead;
|
||||
}
|
||||
|
||||
function trackedFiles(root) {
|
||||
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
|
||||
return out.split('\0').filter(Boolean);
|
||||
let scan = null;
|
||||
|
||||
/** 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
|
||||
|
|
@ -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', () => {
|
||||
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`);
|
||||
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')}`);
|
||||
});
|
||||
|
||||
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()) {
|
||||
const [rel, path] = key.split(' ');
|
||||
const rel = key.split(' ')[0];
|
||||
assert.ok(tracked.has(rel), `EXEMPT names untracked ${rel}`);
|
||||
const dead = deadPaths(ROOT, [rel]).map(exemptKey);
|
||||
assert.ok(dead.includes(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
|
||||
assert.ok(!existsSync(join(ROOT, path)), `EXEMPT path ${path} exists`);
|
||||
assert.ok(deadKeys.has(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import { fileURLToPath } from 'node:url';
|
|||
import { spawnSync } from 'node:child_process';
|
||||
import { MAX_ROUNDS } from '../../lib/validators/plan-gate.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 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. */
|
||||
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
|
||||
* blank-line-separated paragraph after the marker's own paragraph, flattened.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { validateFindings } from '../../lib/review/findings-schema.mjs';
|
|||
import { lintSkill } from '../../lib/skills/skill-lint.mjs';
|
||||
import * as routing from '../../lib/skills/routing.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;
|
||||
|
||||
|
|
@ -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
|
||||
// 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
|
||||
// headroom fails here on demand, not only under load. BEFORE_703D87E1 is the
|
||||
// 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 });
|
||||
// headroom fails here on demand, not only under load.
|
||||
|
||||
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 repo = gitRepo('r630-r9-control-');
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { strict as assert } from 'node:assert';
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
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 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).
|
||||
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). */
|
||||
function phase8(text) {
|
||||
const start = text.indexOf('## Phase 8 — Final report');
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
||||
test('routing: the reader takes block-scalar agent descriptions and skips orchestrator reference docs', () => {
|
||||
test('routing: the reader takes block-scalar agent descriptions', () => {
|
||||
withPlugin({
|
||||
agents: { 'plan-critic': 'Use this agent when a plan needs adversarial review.' },
|
||||
commands: { trekplan: 'Deep implementation planning from a task brief.' },
|
||||
}, (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();
|
||||
assert.deepEqual(ids, ['agent:plan-critic', 'command:trekplan']);
|
||||
assert.equal(loadEntries(root).find((e) => e.id === 'agent:plan-critic').description.trim(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue