feat(plan): the plan gate counts critic rounds 2+ by rule A and records rule B [skip-docs]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
31fa6f8e43
commit
7b46cdd788
3 changed files with 1018 additions and 20 deletions
|
|
@ -53,12 +53,30 @@
|
|||
// then stop and ask; never silently on. Unrated findings count as blockers.
|
||||
// The verdict is a trace of a process, not a signature: anything that can
|
||||
// write the plan can write the block.
|
||||
//
|
||||
// Rule A (decision 43): from critic round 2 on, `recordVerdict` counts a round
|
||||
// against the diff between the operative text the previous round reviewed (the
|
||||
// base, `opts.base`) and the plan now, through `countRound` in
|
||||
// lib/review/critic-diff.mjs. A new MAJOR that cites a plan line outside that
|
||||
// diff counts as minor; a blocker, a security finding and a finding with no
|
||||
// plan line count at full severity; a previous finding stays counted (carried,
|
||||
// with its id) until every reviewer that raised it marks it resolved and nobody
|
||||
// re-reports it. Rule B (a major a named test, `Regression:` command or gate
|
||||
// will fail on is minor) is recorded from round 1 and never lowers a blocker or
|
||||
// a security finding. Without a usable base (none, not the previous round's
|
||||
// text, or the plan unchanged since it) the round is counted as before, and
|
||||
// `diff_note` says why in one line. Every new verdict field is optional
|
||||
// (DIFF_FIELDS); its absence means round-1 behaviour, so an older block gives
|
||||
// the same exits. Trust limit: anything that can write the plan can also
|
||||
// rewrite the previous verdict's plan_sha256 and plant a matching base; rule A
|
||||
// raises what such a forgery buys, and an unchanged base lowers nothing.
|
||||
|
||||
import { readFileSync, existsSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { validatePlanContent } from './plan-validator.mjs';
|
||||
import { splitSections, fenceTracker, extractPlanVersion, versionAtLeast } from '../parsers/plan-schema.mjs';
|
||||
import { normalizeSeverity } from '../review/plan-review-dedup.mjs';
|
||||
import { countRound } from '../review/critic-diff.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const MAX_ROUNDS = 3;
|
||||
|
|
@ -218,9 +236,15 @@ function missingReviewers(dedup) {
|
|||
* @param {string} text - the plan
|
||||
* @param {{findings: Array<object>, sources: Array<{agent: string, present: boolean}>}} dedup
|
||||
* plan-review-dedup.mjs output
|
||||
* @param {{reviewed?: string, extraRound?: boolean, maxRounds?: number, now?: string}} [opts]
|
||||
* reviewed = planHash of the text the reviewers read (from --hash)
|
||||
* @returns {{code: 0|1|2, text?: string, verdict?: object, message: string}}
|
||||
* @param {{reviewed?: string, extraRound?: boolean, maxRounds?: number, now?: string,
|
||||
* base?: {sha256: string, text: string}|null, baseNote?: string, planPaths?: string[]}} [opts]
|
||||
* reviewed = planHash of the text the reviewers read (from --hash);
|
||||
* base = the operative text the previous round reviewed, with its sha256;
|
||||
* baseNote = why there is no base (one line, written as diff_note);
|
||||
* planPaths = the absolute forms of the plan path (a finding whose
|
||||
* resolve(file) is one of them cites the plan). Without base, baseNote and
|
||||
* planPaths the round is recorded as before, plus the rule-B record.
|
||||
* @returns {{code: 0|1|2, text?: string, verdict?: object, lowered?: number, message: string}}
|
||||
*/
|
||||
export function recordVerdict(text, dedup, opts = {}) {
|
||||
if (!dedup || typeof dedup !== 'object' || !Array.isArray(dedup.findings)) {
|
||||
|
|
@ -257,20 +281,19 @@ export function recordVerdict(text, dedup, opts = {}) {
|
|||
};
|
||||
}
|
||||
const extra = prevExtra + (capped ? 1 : 0);
|
||||
const findings = dedup.findings.map((f) => {
|
||||
const sev = normalizeSeverity(f.severity);
|
||||
const out = {
|
||||
id: f.id ?? null,
|
||||
severity: sev ?? 'blocker',
|
||||
rule_key: f.rule_key ?? null,
|
||||
file: f.file ?? null,
|
||||
line: f.line ?? null,
|
||||
text: typeof f.text === 'string' ? f.text : '',
|
||||
raised_by: Array.isArray(f.raised_by) ? f.raised_by : [],
|
||||
};
|
||||
if (sev === null) out.severity_defaulted = true;
|
||||
return out;
|
||||
const diffMode = opts.base !== undefined || opts.baseNote !== undefined || opts.planPaths !== undefined;
|
||||
const lines = operativeLines(text);
|
||||
const c = countRound({
|
||||
round,
|
||||
previous: prev.verdict ?? null,
|
||||
base: opts.base ?? null,
|
||||
baseNote: opts.baseNote ?? null,
|
||||
current: { text: lines.text, sha256: current, fullToOp: lines.fullToOp, opToFull: lines.opToFull },
|
||||
planPaths: Array.isArray(opts.planPaths) ? opts.planPaths : [],
|
||||
findings: dedup.findings,
|
||||
marks: Array.isArray(dedup.marks) ? dedup.marks : [],
|
||||
});
|
||||
const findings = c.findings;
|
||||
const counts = { blocker: 0, major: 0, minor: 0 };
|
||||
for (const f of findings) counts[f.severity] += 1;
|
||||
const verdict = {
|
||||
|
|
@ -280,11 +303,17 @@ export function recordVerdict(text, dedup, opts = {}) {
|
|||
extra_rounds: extra,
|
||||
recorded_at: opts.now ?? new Date().toISOString(),
|
||||
plan_sha256: current,
|
||||
verdict: verdictFor(counts),
|
||||
counts,
|
||||
findings,
|
||||
};
|
||||
return { code: 0, text: placeSection(text, renderSection(verdict)), verdict, message: `round ${round} recorded: ${verdict.verdict}` };
|
||||
if (c.diffBase !== null) verdict.diff_base = c.diffBase;
|
||||
if (diffMode && c.diffNote !== null) verdict.diff_note = c.diffNote;
|
||||
verdict.verdict = verdictFor(counts);
|
||||
verdict.counts = counts;
|
||||
verdict.findings = findings;
|
||||
if (c.resolved !== null) verdict.resolved = c.resolved;
|
||||
return {
|
||||
code: 0, text: placeSection(text, renderSection(verdict)), verdict, lowered: c.lowered,
|
||||
message: `round ${round} recorded: ${verdict.verdict}`,
|
||||
};
|
||||
}
|
||||
|
||||
const sevOf = f => normalizeSeverity(f.severity);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue