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:
parent
b94b060035
commit
f820081bdc
18 changed files with 332 additions and 373 deletions
|
|
@ -148,8 +148,9 @@ import { splitSections, fenceTracker, extractPlanVersion, versionAtLeast } from
|
|||
import { normalizeSeverity, isSecurity } from '../review/plan-review-dedup.mjs';
|
||||
import { countRound, lineDiff, oneLine } from '../review/critic-diff.mjs';
|
||||
import { roundSummary, decideStop, describeStop, STOP_REASONS } from '../review/stop-rule.mjs';
|
||||
import { usableInRepo, repoRelativeBrief, backedLine } from '../review/threat-model.mjs';
|
||||
import { usableInRepo, repoRelativeBrief, splitBacked } from '../review/threat-model.mjs';
|
||||
import { resolveRepo } from '../verification/repo-tools.mjs';
|
||||
import { resolveRepo as resolveTopLevel } from '../util/approval-log.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const MAX_ROUNDS = 3;
|
||||
|
|
@ -410,19 +411,15 @@ export function recordVerdict(text, dedup, opts = {}) {
|
|||
// known_outside; a copy rule A carries from an earlier round stays counted
|
||||
// until a reviewer marks it resolved (decision 221).
|
||||
const use = opts.threatModel;
|
||||
const tm = use && use.usable === true ? use.threatModel : null;
|
||||
const backed = [];
|
||||
const counted = [];
|
||||
for (const f of dedup.findings) {
|
||||
const line = tm ? backedLine(f, tm) : null;
|
||||
if (line === null) { counted.push(f); continue; }
|
||||
// rule_keys kept, so the bare gate re-checks the entry with the same keys (keysOf).
|
||||
backed.push({
|
||||
id: f.id ?? null, rule_key: f.rule_key ?? null,
|
||||
...(Array.isArray(f.rule_keys) ? { rule_keys: f.rule_keys.filter(k => typeof k === 'string') } : {}),
|
||||
text: typeof f.text === 'string' ? f.text : '', line,
|
||||
});
|
||||
}
|
||||
const split = splitBacked(dedup.findings, use);
|
||||
const counted = split.unbacked;
|
||||
// rule_keys kept, so the bare gate re-checks the entry with the same keys
|
||||
// (keysOf); line = the `Outside:` text, which a backed threat_model_ref equals.
|
||||
const backed = split.backed.map(f => ({
|
||||
id: f.id ?? null, rule_key: f.rule_key ?? null,
|
||||
...(Array.isArray(f.rule_keys) ? { rule_keys: f.rule_keys.filter(k => typeof k === 'string') } : {}),
|
||||
text: typeof f.text === 'string' ? f.text : '', line: f.threat_model_ref,
|
||||
}));
|
||||
const backedIds = new Set(backed.map(e => e.id).filter(id => id !== null));
|
||||
const c = countRound({
|
||||
round,
|
||||
|
|
@ -506,32 +503,34 @@ export function acceptOpen(rawText, opts = {}) {
|
|||
const ex = extractVerdict(text);
|
||||
if (!ex.found || ex.error) return { code: 2, message: `no readable verdict: ${ex.error ?? 'no ## Critic Verdict'}` };
|
||||
const v = ex.verdict;
|
||||
const fired = v.stop_rule?.fired === true;
|
||||
if (v.plan_sha256 !== planHash(text)) {
|
||||
return {
|
||||
code: 1,
|
||||
message: v.stop_rule?.fired === true
|
||||
message: fired
|
||||
? 'the verdict is stale: the plan changed after the last round, and the stop rule fired, so there is no next round; restore the reviewed text to accept, or stop and take the direction to the operator'
|
||||
: 'the verdict is stale: the plan changed after the last round; run the next round instead',
|
||||
};
|
||||
}
|
||||
if (v.stop_rule?.fired === true) {
|
||||
// The verdict with `accepted_open` naming `list`, bound to this round and plan text.
|
||||
const accept = (list, under) => ({
|
||||
...v,
|
||||
accepted_open: {
|
||||
round: v.round,
|
||||
plan_sha256: v.plan_sha256,
|
||||
accepted_at: opts.now ?? new Date().toISOString(),
|
||||
...(under ? { under } : {}),
|
||||
findings: list.map(m => ({ id: m.id ?? null, text: m.text ?? '' })),
|
||||
},
|
||||
});
|
||||
if (fired) {
|
||||
const open = v.findings.filter(f => sevOf(f) !== 'minor');
|
||||
if (open.length === 0) return { code: 1, message: 'no BLOCKER or MAJOR is open; nothing to accept' };
|
||||
const security = open.filter(isSecurity);
|
||||
if (security.length > 0) {
|
||||
return { code: 1, message: `PLAN_GATE_SECURITY_OPEN: ${security.length} open security finding(s) count and are never accepted: ${security.map(s => s.text).join(' | ')}` };
|
||||
}
|
||||
const accepted = {
|
||||
...v,
|
||||
accepted_open: {
|
||||
round: v.round,
|
||||
plan_sha256: v.plan_sha256,
|
||||
accepted_at: opts.now ?? new Date().toISOString(),
|
||||
under: 'stop-rule',
|
||||
findings: open.map(m => ({ id: m.id ?? null, text: m.text ?? '' })),
|
||||
},
|
||||
};
|
||||
return { code: 0, text: placeSection(text, renderSection(accepted)), message: `${open.length} open finding(s) of round ${v.round} accepted as known issues under the stop rule` };
|
||||
return { code: 0, text: placeSection(text, renderSection(accept(open, 'stop-rule'))), message: `${open.length} open finding(s) of round ${v.round} accepted as known issues under the stop rule` };
|
||||
}
|
||||
if (v.findings.some(f => sevOf(f) !== 'major' && sevOf(f) !== 'minor')) return { code: 1, message: 'a BLOCKER is open; a blocker is never accepted' };
|
||||
const majors = v.findings.filter(f => sevOf(f) === 'major');
|
||||
|
|
@ -540,16 +539,7 @@ export function acceptOpen(rawText, opts = {}) {
|
|||
// (found by the independent review: acceptance at round 1 passed the gate).
|
||||
const left = v.max_rounds + (v.extra_rounds ?? 0) - v.round;
|
||||
if (left > 0) return { code: 1, message: `${left} critic round(s) left: revise the plan and run the next round; acceptance is only for after the last round` };
|
||||
const accepted = {
|
||||
...v,
|
||||
accepted_open: {
|
||||
round: v.round,
|
||||
plan_sha256: v.plan_sha256,
|
||||
accepted_at: opts.now ?? new Date().toISOString(),
|
||||
findings: majors.map(m => ({ id: m.id ?? null, text: m.text ?? '' })),
|
||||
},
|
||||
};
|
||||
return { code: 0, text: placeSection(text, renderSection(accepted)), message: `${majors.length} open MAJOR(s) of round ${v.round} accepted` };
|
||||
return { code: 0, text: placeSection(text, renderSection(accept(majors))), message: `${majors.length} open MAJOR(s) of round ${v.round} accepted` };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -559,13 +549,8 @@ export function acceptOpen(rawText, opts = {}) {
|
|||
* @param {{usable: boolean, why: string|null, threatModel: object}} use - threat-model.mjs `usable`
|
||||
*/
|
||||
function checkKnownOutside(entries, use, reasons, warnings) {
|
||||
const tm = use?.usable === true ? use.threatModel : null;
|
||||
const backed = [];
|
||||
const unbacked = [];
|
||||
for (const e of entries) {
|
||||
const asFinding = { rule_key: e.rule_key, ...(Array.isArray(e.rule_keys) ? { rule_keys: e.rule_keys } : {}), threat_model_ref: e.line };
|
||||
(tm && backedLine(asFinding, tm) !== null ? backed : unbacked).push(e);
|
||||
}
|
||||
const { backed, unbacked, why } = splitBacked(entries, use,
|
||||
e => ({ rule_key: e.rule_key, ...(Array.isArray(e.rule_keys) ? { rule_keys: e.rule_keys } : {}), threat_model_ref: e.line }));
|
||||
if (backed.length > 0) {
|
||||
warnings.push({
|
||||
code: 'PLAN_GATE_KNOWN_OUTSIDE',
|
||||
|
|
@ -573,7 +558,6 @@ function checkKnownOutside(entries, use, reasons, warnings) {
|
|||
});
|
||||
}
|
||||
if (unbacked.length > 0) {
|
||||
const why = tm ? 'no `Outside:` line in the brief equals it, or it is not a Voyage security finding' : (use?.why ?? 'the threat model is not usable');
|
||||
reasons.push({
|
||||
code: 'PLAN_GATE_OUTSIDE_NOT_BACKED',
|
||||
message: `${unbacked.length} finding(s) recorded as outside the threat model are not backed by the brief now (${why}), so they count in full: ${unbacked.map(e => e.text).join(' | ')}`,
|
||||
|
|
@ -609,10 +593,11 @@ export function gatePlan(rawText, opts = {}) {
|
|||
} else if (ex.error) {
|
||||
reasons.push({ code: 'PLAN_GATE_VERDICT_MALFORMED', message: ex.error });
|
||||
} else {
|
||||
const stop = v.stop_rule?.fired === true ? v.stop_rule : null;
|
||||
if (v.plan_sha256 !== planHash(text)) {
|
||||
reasons.push({
|
||||
code: 'PLAN_GATE_VERDICT_STALE',
|
||||
message: v.stop_rule?.fired === true
|
||||
message: stop
|
||||
? `The plan changed after critic round ${v.round}, and the stop rule fired, so there is no next round; restore the reviewed text, or stop and take the direction to the operator.`
|
||||
: `The plan changed after critic round ${v.round}; run the next round.`,
|
||||
});
|
||||
|
|
@ -620,7 +605,6 @@ export function gatePlan(rawText, opts = {}) {
|
|||
const sev = f => normalizeSeverity(f.severity);
|
||||
const blockers = v.findings.filter(f => sev(f) !== 'major' && sev(f) !== 'minor');
|
||||
const majors = v.findings.filter(f => sev(f) === 'major');
|
||||
const stop = v.stop_rule?.fired === true ? v.stop_rule : null;
|
||||
if (stop) {
|
||||
// The stop rule replaces the open-blocker and open-major reasons: no new
|
||||
// round, the operator decides (decision 220). Covered = accepted under
|
||||
|
|
@ -844,9 +828,11 @@ if (isMain(import.meta.url)) {
|
|||
const dir = snapshotDir(planPath);
|
||||
const opts = { extraRound: args.includes('--extra-round'), reviewed, planPaths: planPathsOf(planPath) };
|
||||
if (briefArg !== undefined) {
|
||||
// Stored relative to the repo's top level, never absolute; read from there.
|
||||
opts.briefPath = repoRelativeBrief(briefArg);
|
||||
opts.threatModel = usableInRepo(opts.briefPath);
|
||||
// Stored relative to the repo's top level, never absolute; read from there
|
||||
// (one top-level lookup for both).
|
||||
const top = resolveTopLevel();
|
||||
opts.briefPath = repoRelativeBrief(briefArg, top);
|
||||
opts.threatModel = usableInRepo(opts.briefPath, top);
|
||||
}
|
||||
const lb = loadBase(text, dir);
|
||||
if (lb.verdict) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue