fix(gate): the threat model applies to new findings only (decision 221)
Undone: item 2 of the Part 2 fix round (1be961a). recordVerdict no longer
drops a copy rule A carried as counted when a finding with the same id is
backed this round, and no longer recomputes the counts, `lowered` and the
stop-rule summary for it; the isLowered helper and the SEVERITIES import it
needed are gone. Why: the id is (file, line, rule_key), so a DIFFERENT
backed security finding at the same line and key knocked a counted security
finding inside the threat model out of the count (round 2 PASS, its text
gone), against decisions 218 and 220.
The rule now, as decided: the threat model applies only to this round's
new findings (the deduped findings, before countRound); a finding counted
in an earlier round stays counted, through rule A's carry, until a reviewer
marks it resolved. The gate never counts less. Items 1 and 3-8 of the fix
round are untouched.
Tests: the fix-round case for item 2 now asserts the decided rule (the
carried copy stays counted, BLOCK, the stop rule fires not-fewer, the gate
stops); a new case pins that a different backed finding at the same line
and key leaves the counted one, with its own text, in the count.
The plan-gate header and the known_outside comment, and Handover 4's
known_outside sentence, state the rule. CHANGELOG Unreleased, Known issues:
a finding counted earlier stays counted when an Outside: line approved
later puts it outside, or when it moves to another plan line; the way on is
the operator's choice at the stop rule.
Part 2 of the E plan, decision 221.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1be961aab1
commit
ff46f57a89
4 changed files with 45 additions and 29 deletions
|
|
@ -381,23 +381,46 @@ test('fix 1: a string entry in a reviewer\'s findings still counts as a blocker
|
|||
assert.equal(r.verdict.verdict, 'BLOCK');
|
||||
});
|
||||
|
||||
test('fix 2: a finding counted in round 1 and backed in round 2 (same id, marked resolved: false) is listed once and counts nowhere', () => {
|
||||
// Decision 221: the threat model applies to this round's new findings only. A
|
||||
// finding counted in an earlier round stays counted (rule A's carry) until a
|
||||
// reviewer marks it resolved; the gate never counts less.
|
||||
function countedThenBacked(text2) {
|
||||
const t1 = buildPlan();
|
||||
const r1 = rec(t1, dd({ pc: [f('blocker', at(t1, R1_CHANGES), S1, 'SECURITY_RISK')] }));
|
||||
const id = r1.verdict.findings[0].id;
|
||||
const t2 = editA(r1.text);
|
||||
const r2 = rec(t2, dd({
|
||||
pc: [f('blocker', at(t2, R1_CHANGES), S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })],
|
||||
pc: [f('blocker', at(t2, R1_CHANGES), text2, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })],
|
||||
pcPrev: [{ id, resolved: false }], sgPrev: [{ id, resolved: false }],
|
||||
}), { base: baseOf(r1.text), threatModel: TM, briefPath: 'brief.md' });
|
||||
return { id, r2 };
|
||||
}
|
||||
|
||||
test('fix 2 (decision 221): a finding counted in round 1 and backed in round 2 stays counted until marked resolved', () => {
|
||||
const { id, r2 } = countedThenBacked(S1);
|
||||
const v = r2.verdict;
|
||||
assert.deepEqual(v.findings.map(x => x.id), [], 'the carried copy left the counted set');
|
||||
assert.deepEqual(v.counts, { blocker: 0, major: 0, minor: 0 });
|
||||
assert.equal(v.verdict, 'PASS');
|
||||
assert.deepEqual((v.known_outside ?? []).map(e => e.id), [id]);
|
||||
assert.equal(v.rounds.at(-1).blocking, 0, 'it left the stop-rule summary');
|
||||
assert.equal(v.stop_rule.fired, false, JSON.stringify(v.stop_rule));
|
||||
assert.equal(gatePlan(r2.text, { threatModel: TM }).code, 0);
|
||||
assert.deepEqual(v.findings.map(x => [x.id, x.carried_from]), [[id, 1]], 'the carried copy stays counted');
|
||||
assert.deepEqual(v.counts, { blocker: 1, major: 0, minor: 0 });
|
||||
assert.equal(v.verdict, 'BLOCK');
|
||||
assert.deepEqual((v.known_outside ?? []).map(e => e.id), [id], 'the backed re-report may stay listed');
|
||||
assert.equal(v.rounds.at(-1).blocking, 1);
|
||||
assert.equal(v.stop_rule.fired, true, 'the stop rule fires: the way on is the operator\'s choice');
|
||||
assert.equal(v.stop_rule.reason, 'not-fewer');
|
||||
assert.equal(gatePlan(r2.text, { threatModel: TM }).code, 1);
|
||||
});
|
||||
|
||||
test('decision 221: a DIFFERENT backed finding at the same line and key never knocks out a counted one', () => {
|
||||
const B = 'Step 1 writes the downloaded token into a world-readable cache file';
|
||||
const { id, r2 } = countedThenBacked(B);
|
||||
const v = r2.verdict;
|
||||
assert.equal(v.verdict, 'BLOCK');
|
||||
assert.deepEqual(v.counts, { blocker: 1, major: 0, minor: 0 });
|
||||
const a = v.findings.find(x => x.id === id);
|
||||
assert.ok(a, `A is still counted: ${JSON.stringify(v.findings)}`);
|
||||
assert.equal(a.carried_from, 1);
|
||||
assert.equal(a.text, S1, 'A keeps its own text');
|
||||
const g = gatePlan(r2.text, { threatModel: TM });
|
||||
assert.equal(g.code, 1, JSON.stringify(g.reasons));
|
||||
});
|
||||
|
||||
test('fix 3: a merged finding whose security key is only in rule_keys is backed at record and in the bare gate alike', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue