feat(review): the code-correctness reviewer checks the repo's tracked instruction files
This commit is contained in:
parent
349a4fc789
commit
fcba232ccd
10 changed files with 807 additions and 17 deletions
|
|
@ -2,8 +2,7 @@
|
|||
// One deterministic pass over review.md after the coordinator and after
|
||||
// lib/review/fill-finding-ids.mjs, on both paths (review_version 1.2, fix
|
||||
// round 4.5b2 part B). Its passes run in the Architecture order (engine,
|
||||
// instruction, carry, pre-existing, policy); later steps insert theirs at
|
||||
// their places in PASSES. Today it runs engine, carry, pre-existing and policy:
|
||||
// instruction, carry, pre-existing, policy), each at its place in PASSES:
|
||||
//
|
||||
// - engine, always the first pass: `putBack` (lib/review/engine-compare.mjs)
|
||||
// compares the engine payloads Phase 4.7 saved in the project folder
|
||||
|
|
@ -18,6 +17,22 @@
|
|||
// the frontmatter list, counts, a verdict that is only raised, and, as a
|
||||
// BLOCKER, its `### Step R<n>`. An engine finding in `out_of_scope` is
|
||||
// not compared (it stays declined, `engine:out-of-scope`).
|
||||
// - instruction, right after engine: a finding under `CLAUDE_MD_DEVIATION`
|
||||
// or `CLAUDE_MD_STALE` whose `instruction_ref` is missing or names a file
|
||||
// outside the repo's tracked instruction set (lib/review/instruction-scope.mjs
|
||||
// `instructionSet` at `scope_sha_end`, for the paths changed over
|
||||
// `scope_sha_start..scope_sha_end`) leaves the JSON findings, the
|
||||
// frontmatter list, `counts` and its `### ` entry under
|
||||
// `## Findings (MINOR|SUGGESTION)`, and goes to `declined_to_judge` with
|
||||
// `instruction-out-of-set` and its `instruction_ref` (the cited file, or
|
||||
// null); `## Declined to Judge` gets one line for it, by title, naming
|
||||
// the cited file. A git failure or no range is an empty set, so every
|
||||
// such finding is declined. Only a MINOR or SUGGESTION that is not a
|
||||
// security finding moves; one whose entry is not found exactly once
|
||||
// there, that an R-step names or that finalize added stays reported
|
||||
// (stricter). The same breach reported by an engine and a reviewer
|
||||
// (same triplet) was already merged by the coordinator's dedup, which
|
||||
// keeps the higher severity.
|
||||
// - carry (round 2 and later, `--previous <archive>`): `carry`
|
||||
// (lib/review/review-rounds.mjs) keeps every previous finding that this
|
||||
// round gives no positive evidence of being resolved, with `carried_from`;
|
||||
|
|
@ -90,6 +105,8 @@ import { computeFindingIds } from '../parsers/finding-id.mjs';
|
|||
import { fencedBlocks, isJsonBlock, validateReviewerOutput } from './findings-schema.mjs';
|
||||
import { FULL_SHA_RE } from './reviewed-commit.mjs';
|
||||
import { markFindings, scopeShas } from './pre-existing.mjs';
|
||||
import { changedPaths, instructionSet } from './instruction-scope.mjs';
|
||||
import { isSecurityFinding } from './review-answers.mjs';
|
||||
import { ENGINES } from './engine-findings.mjs';
|
||||
import { carry, frontmatterField, MAX_REVIEW_ROUNDS, runId } from './review-rounds.mjs';
|
||||
import { applyPolicy, readPolicy } from './review-policy.mjs';
|
||||
|
|
@ -220,6 +237,94 @@ function passEngine(payload, ctx) {
|
|||
return { headEdit: (head) => withVerdict(head, r.raisedVerdict) };
|
||||
}
|
||||
|
||||
// ---- pass: instruction -------------------------------------------------------------
|
||||
|
||||
const INSTRUCTION_KEYS = new Set(['CLAUDE_MD_DEVIATION', 'CLAUDE_MD_STALE']);
|
||||
// One of coordinator-contract `NOTED_REASONS`: the gate accepts it on a MINOR or SUGGESTION only.
|
||||
const OUT_OF_SET = 'instruction-out-of-set';
|
||||
const INSTRUCTION_MOVABLE = new Set(['MINOR', 'SUGGESTION']);
|
||||
|
||||
/**
|
||||
* The repo's tracked instruction files at `scope_sha_end` for the paths
|
||||
* changed over the review's range (`changedPaths`, the list of Phase 5's
|
||||
* `git diff --name-only`, deleted paths included); an empty set when either
|
||||
* cannot be read.
|
||||
*/
|
||||
function instructionFilesOf(repo, text) {
|
||||
const { start, end } = scopeShas(text);
|
||||
const changed = changedPaths(repo, start, end);
|
||||
if (!changed.ok) return { measured: false, files: new Set() };
|
||||
const r = instructionSet(repo, end, changed.paths);
|
||||
return r.ok ? { measured: true, files: new Set(r.files) } : { measured: false, files: new Set() };
|
||||
}
|
||||
|
||||
/** One `## Declined to Judge` line for a finding the instruction filter moved. */
|
||||
function instructionLine(d) {
|
||||
const cited = typeof d.instruction_ref === 'string' && oneLine(d.instruction_ref) !== ''
|
||||
? oneLine(d.instruction_ref) : 'no instruction file';
|
||||
return `- ${oneLine(d.file) || '(no file)'}:${d.line ?? '?'} ${oneLine(d.rule_key) || '(no rule_key)'} — ${OUT_OF_SET}, cites ${cited} (${oneLine(d.title) || '(no title)'})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass `instruction`, right after engine: a CLAUDE.md-key finding that cites
|
||||
* no file of the instruction set goes to Declined to Judge.
|
||||
*/
|
||||
function passInstruction(payload, ctx) {
|
||||
const keyed = payload.findings.filter((f) => INSTRUCTION_KEYS.has(f.rule_key));
|
||||
if (keyed.length === 0) return {};
|
||||
if (payload.declined_to_judge !== undefined && !Array.isArray(payload.declined_to_judge)) {
|
||||
ctx.instruction = { measured: false, moved: 0 };
|
||||
return {};
|
||||
}
|
||||
const set = instructionFilesOf(ctx.repo, ctx.text);
|
||||
const lines = ctx.head.split('\n');
|
||||
const fmEnd = frontmatterEnd(lines);
|
||||
const named = new Set(remediationSteps(lines).steps.map((st) => st.source).filter(Boolean));
|
||||
const take = keyed.filter((f) => (typeof f.instruction_ref !== 'string' || !set.files.has(f.instruction_ref))
|
||||
&& INSTRUCTION_MOVABLE.has(f.severity) && !isSecurityFinding(f)
|
||||
&& typeof f.id === 'string' && HEX40.test(f.id) && !named.has(f.id) && !ctx.added.has(f)
|
||||
&& movableEntry(lines, fmEnd, f.id) !== null);
|
||||
ctx.instruction = { measured: set.measured, moved: take.length };
|
||||
if (take.length === 0) return {};
|
||||
const declined = take.map((f) => ({
|
||||
file: f.file ?? null,
|
||||
line: f.line ?? null,
|
||||
rule_key: f.rule_key ?? null,
|
||||
severity: f.severity ?? null,
|
||||
title: f.title ?? null,
|
||||
...(f.engine !== undefined ? { engine: f.engine } : {}),
|
||||
instruction_ref: typeof f.instruction_ref === 'string' ? f.instruction_ref : null,
|
||||
reason: OUT_OF_SET,
|
||||
}));
|
||||
const takeSet = new Set(take);
|
||||
payload.findings = payload.findings.filter((f) => !takeSet.has(f));
|
||||
payload.declined_to_judge = [...(payload.declined_to_judge ?? []), ...declined];
|
||||
ctx.recount = true;
|
||||
const ids = take.map((f) => f.id);
|
||||
return {
|
||||
headEdit: (head) => {
|
||||
let out = head.split('\n');
|
||||
const fe = frontmatterEnd(out);
|
||||
for (const id of ids) {
|
||||
const e = movableEntry(out, fe, id);
|
||||
if (e) out.splice(e.at, e.end - e.at);
|
||||
}
|
||||
out = emptySectionsSayNone(out);
|
||||
const at = out.findIndex((l) => DECLINED_RE.test(l));
|
||||
if (at !== -1) {
|
||||
for (let i = sectionEnd(out, at) - 1; i > at; i--) {
|
||||
if (out[i].trim() !== 'None.') continue;
|
||||
out.splice(i, 1);
|
||||
if (out[i] === '' && out[i - 1] === '') out.splice(i, 1);
|
||||
}
|
||||
}
|
||||
out = appendToSection(out, DECLINED_RE, '## Declined to Judge', declined.map(instructionLine),
|
||||
[/^##\s+Remediation Summary\b/, /^##\s+Remediation Steps\b/, /^##\s+Answers\s*$/]);
|
||||
return out.join('\n');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---- pass: carry -------------------------------------------------------------------
|
||||
|
||||
const LATER_SECTIONS = [/^##\s+Suppressed Findings\b/, /^##\s+Pre-existing\s*$/, /^##\s+Resolved since round\b/,
|
||||
|
|
@ -425,6 +530,7 @@ function passPolicy(payload, ctx) {
|
|||
/** The passes, in the Architecture order; the id recomputation runs after them, always last. */
|
||||
export const PASSES = Object.freeze([
|
||||
{ name: 'engine', run: passEngine },
|
||||
{ name: 'instruction', run: passInstruction },
|
||||
{ name: 'carry', run: passCarry },
|
||||
{ name: 'pre-existing', run: passPreExisting },
|
||||
{ name: 'policy', run: passPolicy },
|
||||
|
|
@ -669,7 +775,7 @@ export function finalizeReview(text, { repo, round, roundBase = null, previous =
|
|||
return {
|
||||
exit: 0, text: `${head}${parts.fence}${body}${parts.tail}`, ...out, ids_rewritten: map.size,
|
||||
carried: ctx.added.size, resolved: payload.resolved ?? [], ran: payload.ran, r_steps_added: steps.added,
|
||||
policy: ctx.policy ?? null, put_back: ctx.engine?.put_back ?? 0,
|
||||
policy: ctx.policy ?? null, put_back: ctx.engine?.put_back ?? 0, instruction: ctx.instruction ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue