refactor(review): simplify pass over E (reuse, simplification, efficiency)

Quality only, no behaviour change (four cleanup reviewers, one implementer;
npm test 2480/0 on the working tree):

- threat-model.mjs splitBacked(): the one known_outside backing check of
  plan-gate (record and bare gate), verdict-gate and review-finalize, with
  one why-text; the engines' security keys derived from ENGINES; isSecurity.
- coordinator-contract: "not a security engine" read from ENGINES (a string
  engine only), not the code-review literal.
- review-rounds owns trailingBlock, findingsBlockSha, isMajor, isBlocker;
  review-answers imports them (review-rounds still imports nothing from it).
  prepare keeps the archive bytes it read instead of reading the previous
  archive again.
- engine-compare exports rowIn and payloadEngine; finalize uses them.
- plan-gate: one accepted_open builder; the stop-rule check hoisted; one
  top-level lookup for --record --brief.
- security-engine: notFullShas(), runCli's usage flag, readScopeList shared
  with engine-findings; findReport and reviewRoundSummary module-private;
  two dead defaults gone.
- tests: shared plan-gate harness and threat-model brief helpers; the [CNV]
  table cases call convertReport in-process, one CLI case per exit path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-08 21:27:59 +01:00
commit f820081bdc
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
18 changed files with 332 additions and 373 deletions

View file

@ -13,51 +13,28 @@
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { resolve } from 'node:path';
import { gatePlan, recordVerdict, planHash, operativeText, acceptOpen } from '../../lib/validators/plan-gate.mjs';
import { gatePlan, planHash, operativeText, acceptOpen } from '../../lib/validators/plan-gate.mjs';
import * as diffMod from '../../lib/review/critic-diff.mjs';
import { dedupFindings, contentOverlap } from '../../lib/review/plan-review-dedup.mjs';
import { contentOverlap } from '../../lib/review/plan-review-dedup.mjs';
import { computeFindingId } from '../../lib/parsers/finding-id.mjs';
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
import { spawnSync } from 'node:child_process';
import {
mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync, appendFileSync, realpathSync,
} from 'node:fs';
import { join, dirname } from 'node:path';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import { childEnv } from '../helpers/child-env.mjs';
import {
ROOT, at, baseOf, recorder, dd, R2_CHANGES, R2_EDITED, editA, runGate as gate, GIT_CLEAN, tempRepo,
} from '../helpers/plan-gate-harness.mjs';
const NOW = '2026-10-16T00:00:00.000Z';
const PLAN = resolve('plan.md');
const PATHS = [PLAN];
const BOTH = [{ agent: 'plan-critic', present: true }, { agent: 'scope-guardian', present: true }];
/** The 1-based full-file line holding `s` (findIndex over the whole text). */
function at(text, s) {
const n = text.split('\n').findIndex(l => l.includes(s)) + 1;
assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`);
return n;
}
/** The base a round diffs against: the operative text the previous round reviewed. */
const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) });
/** One round as Phase 9 runs it, with the plan path known to the gate. */
function rec(text, d, opts = {}) {
const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now: NOW, ...opts });
assert.equal(r.code, 0, r.message);
return r;
}
/** Reviewer payloads through the real dedup. */
function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) {
return dedupFindings([
{ agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } },
{ agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } },
]);
}
const rec = recorder(NOW);
const f = (severity, line, text, extra = {}) => ({ file: 'plan.md', line, rule_key: 'step-quality', severity, text, ...extra });
const byText = (v, s) => {
@ -69,15 +46,8 @@ const byId = (v, id) => v.findings.filter(x => x.id === id);
// ---- the edits ---------------------------------------------------------------
const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)';
const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.';
const WHY = 'Why: the brief asks for a parser CLI.';
/** Edit A: Step 2's Changes line becomes two lines (a two-line hunk, nothing above it moves). */
function editA(t) {
assert.ok(t.includes(R2_CHANGES));
return t.replace(R2_CHANGES, R2_EDITED);
}
/** Edit B: the Context line is reworded (a one-line hunk high in the plan, nothing moves). */
function editB(t) {
assert.ok(t.includes(WHY));
@ -702,26 +672,10 @@ test('acceptOpen after round 3 lists only the real majors, never a lowered one',
// .git. Expected hashes come from the CLI's own --hash (taken before the
// round), expected line numbers from findIndex over the plan text.
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
function gate(cwd, args, input) {
const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
return { code: r.status, out: r.stdout, err: r.stderr };
}
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
/** A fresh temp dir (real path), `git init`-ed when `repo` is true. */
function tempDir(prefix, repo) {
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
if (repo) {
const g = git(dir, ['init', '-q']);
assert.equal(g.status, 0, g.stderr);
}
return dir;
}
const tempDir = (prefix, repo) => (repo ? tempRepo(prefix) : realpathSync(mkdtempSync(join(tmpdir(), prefix))));
/** The plan in its own temp git repo; the gate runs with that repo as cwd. */
function withRepo(text, fn) {