feat(gate): stop rule in the plan gate (decision 217, 220)
recordVerdict appends each round's summary to a new optional verdict field `rounds` (carried from the previous verdict; newByClass null for a round counted in full) and stores `stop_rule`; extractVerdict shape-checks both. After a fired stop rule --record exits 1 with PLAN_GATE_STOP_RULE and writes nothing (--extra-round included). The bare gate stops with PLAN_GATE_STOP_RULE and the operator line in place of the open-blocker and open-major reasons; --json and --record output carry the decision. --accept-open after a fired stop rule is allowed with rounds left, covers open blockers and majors (`under: "stop-rule"`), refuses a counting security finding (PLAN_GATE_SECURITY_OPEN); the gate then warns. Known issue fixed: REVIEW_LOG_SECTIONS drops "Adversarial Pass 2 (v5.1.1 high-effort)"; the CHANGELOG known-issue line closes and Handover 4 names the heading list, the new fields and codes. Existing tests updated for the planned rule: fixtures that ran a later round after one with no fewer blocking findings now use a falling count (plan-gate.test round cap, plan-gate-open-major lastRound, diff-mode 5/18/27 resolve two earlier majors in round 2); the diff-mode CLI pipe case now expects PLAN_GATE_STOP_RULE (round 3: 2 blocking) after its clean round 2. Part 1, step 1.2 of the E plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7e09edb24f
commit
a576391e28
7 changed files with 448 additions and 32 deletions
|
|
@ -29,12 +29,19 @@ const dedup = (...f) => ({ findings: f, sources: BOTH });
|
|||
const rec = (text, d, opts = {}) => recordVerdict(text, d, { reviewed: planHash(text), ...opts });
|
||||
const codes = (list) => list.map(x => x.code);
|
||||
|
||||
// The earlier rounds hold more open majors than the last (a falling count), so
|
||||
// the stop rule (lib/review/stop-rule.mjs, "not fewer") does not fire and the
|
||||
// last round is judged by the open-major rules alone.
|
||||
function lastRound(...f) {
|
||||
let text = buildPlan();
|
||||
for (let i = 1; i < MAX_ROUNDS; i++) text = rec(text, dedup()).text;
|
||||
for (let i = 1; i < MAX_ROUNDS; i++) {
|
||||
const earlier = Array.from({ length: MAX_ROUNDS - i + 5 }, (_, k) => finding('major', `earlier round ${i} open point ${'x'.repeat(k)}`));
|
||||
text = rec(text, dedup(...earlier)).text;
|
||||
}
|
||||
const r = rec(text, dedup(...f));
|
||||
assert.equal(r.code, 0, r.message);
|
||||
assert.equal(r.verdict.round, MAX_ROUNDS);
|
||||
assert.equal(r.verdict.stop_rule?.fired, false, 'the stop rule does not fire on a falling count');
|
||||
return r.text;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +83,8 @@ test('--accept-open never accepts a BLOCKER, and refuses when no MAJOR is open',
|
|||
});
|
||||
|
||||
test('an acceptance does not carry over to a later round', () => {
|
||||
const accepted = acceptOpen(lastRound(finding('major', WEAK))).text;
|
||||
// Two majors accepted, one in the extra round: fewer, so the stop rule stays quiet.
|
||||
const accepted = acceptOpen(lastRound(finding('major', WEAK), finding('major', 'Step 3 verify line never names the failing assertion'))).text;
|
||||
const revised = accepted.replace('Add `run1()`', 'Add `run1()` and its error path');
|
||||
const next = rec(revised, dedup(finding('major', 'Step 1 error path untested')), { extraRound: true });
|
||||
assert.equal(next.code, 0, next.message);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue