fix(review): a code-review finding against a brief Non-Goal may drop to MINOR, never a BLOCKER
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3009c5eddb
commit
ce7700b71b
5 changed files with 533 additions and 4 deletions
|
|
@ -80,6 +80,79 @@ export const UNVERIFIED_REASONS = Object.freeze([
|
|||
'unknown-rule_key',
|
||||
'file-existence:indeterminate',
|
||||
]);
|
||||
// The brief-conflict mark (`contradicts_brief`) is in neither set on purpose:
|
||||
// it is a field on a KEPT finding, never a `suppressed_reason`. As a reason it
|
||||
// would fail closed through `classifySuppression`, land the finding in
|
||||
// `unverified` and forbid ALLOW — the opposite of what the mark is for.
|
||||
|
||||
// ---- The brief-conflict rule ---------------------------------------------------
|
||||
//
|
||||
// The one place that decides when a finding marked as contradicting the brief
|
||||
// (`contradicts_brief: "<the Non-Goal or stated choice, verbatim>"`) may drop
|
||||
// to MINOR. Only an engine finding (`engine` `code-review` or `security-review`)
|
||||
// is ever marked; only a `/code-review` finding at the MAJOR tier ever drops,
|
||||
// to MINOR, writing `brief_lowered_from: 'MAJOR'`. A BLOCKER and any
|
||||
// `/security-review` finding keep their severity and only carry the mark.
|
||||
// The verdict gate (verdict-gate.mjs) imports `illegalBriefLowering`; the
|
||||
// opt-in workflow review mirrors it. `original_severity` is never read here.
|
||||
|
||||
const ENGINE_NAMES = Object.freeze(['code-review', 'security-review']);
|
||||
|
||||
function carries(f, field) {
|
||||
return f?.[field] !== undefined && f?.[field] !== null;
|
||||
}
|
||||
|
||||
function isMark(v) {
|
||||
return typeof v === 'string' && v.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a finding, before the lowering, may drop to MINOR: a non-empty
|
||||
* string mark, `engine: 'code-review'`, a known `rule_key` whose catalogue tier
|
||||
* is MAJOR, and severity MAJOR. Anything else (missing engine, unknown key,
|
||||
* non-string mark, a BLOCKER-tier or MINOR-tier key) → false.
|
||||
* @param {object} f
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function lowerableBriefConflict(f) {
|
||||
return isMark(f?.contradicts_brief) &&
|
||||
f.engine === 'code-review' &&
|
||||
getRule(f.rule_key)?.severity === 'MAJOR' &&
|
||||
f.severity === 'MAJOR';
|
||||
}
|
||||
|
||||
function isLegalBriefLowering(f) {
|
||||
return isMark(f?.contradicts_brief) &&
|
||||
f.engine === 'code-review' &&
|
||||
getRule(f.rule_key)?.severity === 'MAJOR' &&
|
||||
f.severity === 'MINOR' &&
|
||||
f.brief_lowered_from === 'MAJOR';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when what the coordinator wrote is not the one legal shape and (a)
|
||||
* carries `brief_lowered_from`, or (b) carries a mark without an engine, or
|
||||
* (c) carries a mark below its catalogue tier, or (d) carries a mark with a
|
||||
* missing or unknown `rule_key` (fail-closed), or (e) is an engine finding
|
||||
* below its catalogue tier, marked or not. An unmarked reviewer finding (no
|
||||
* `engine`) without `brief_lowered_from` is never judged by this rule.
|
||||
* @param {object} f
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function illegalBriefLowering(f) {
|
||||
if (f === null || typeof f !== 'object') return false;
|
||||
if (isLegalBriefLowering(f)) return false;
|
||||
const marked = carries(f, 'contradicts_brief');
|
||||
const engine = ENGINE_NAMES.includes(f.engine);
|
||||
const rule = getRule(f.rule_key);
|
||||
const belowTier = rule !== null && severityRank(f.severity) > severityRank(rule.severity);
|
||||
if (carries(f, 'brief_lowered_from')) return true; // (a)
|
||||
if (marked && !engine) return true; // (b)
|
||||
if (marked && belowTier) return true; // (c)
|
||||
if (marked && rule === null) return true; // (d)
|
||||
if (engine && belowTier) return true; // (e)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a suppression reason. Anything not declared refuting is
|
||||
|
|
@ -165,6 +238,10 @@ export function dedupByTriplet(findings) {
|
|||
}
|
||||
const out = [];
|
||||
for (const group of groups.values()) {
|
||||
// A brief-conflict mark is not carried over from a group member to the
|
||||
// survivor: when the higher-severity survivor is unmarked, the group stays
|
||||
// unmarked and is judged at its full severity. Conservative on purpose —
|
||||
// a mark can only lower, so dropping it never makes a verdict greener.
|
||||
let survivor = group[0];
|
||||
for (const f of group.slice(1)) {
|
||||
const higher = severityRank(f.severity) < severityRank(survivor.severity);
|
||||
|
|
@ -202,11 +279,13 @@ export const TRIM_MARKER = ' […trimmed]';
|
|||
* The tier a finding will be judged at: the catalogue tier when the rule_key is
|
||||
* known (Pass 3 resets to it anyway), else the reviewer-asserted severity. The
|
||||
* trim decision reads this, so a reviewer cannot get a MAJOR trimmed by
|
||||
* mislabelling it MINOR.
|
||||
* mislabelling it MINOR. A finding the brief rule will lower (Pass 3) is
|
||||
* judged at MINOR, the tier it will be counted at.
|
||||
* @param {object} f
|
||||
* @returns {string}
|
||||
*/
|
||||
function effectiveSeverity(f) {
|
||||
if (lowerableBriefConflict(f)) return 'MINOR';
|
||||
return getRule(f.rule_key)?.severity ?? f.severity;
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +366,10 @@ export function judgeFilter(findings) {
|
|||
* checkable claim) and is dropped. `unknown-rule_key` does not: an ad-hoc key
|
||||
* is a real defect wearing the wrong label — v5.1.1 high-effort mode already
|
||||
* KEEPS these, normalised to PLAN_EXECUTE_DRIFT — so it routes to `unverified`.
|
||||
*
|
||||
* The brief rule runs here too: a finding `lowerableBriefConflict` accepts is
|
||||
* set to MINOR with `brief_lowered_from: 'MAJOR'`; every other finding gets
|
||||
* the catalogue reset above.
|
||||
* @param {object[]} findings
|
||||
* @returns {{ kept: object[], dropped: object[], unverified: object[] }}
|
||||
*/
|
||||
|
|
@ -294,7 +377,17 @@ export function reasonablenessFilter(findings) {
|
|||
const kept = [];
|
||||
const dropped = [];
|
||||
const unverified = [];
|
||||
for (const f of findings) {
|
||||
for (const incoming of findings) {
|
||||
// The brief rule, first: only this pass writes `brief_lowered_from`, so an
|
||||
// incoming one is removed; only an engine finding is ever marked, so a mark
|
||||
// on any other finding is removed. Every finding written here then makes
|
||||
// `illegalBriefLowering` false.
|
||||
const { brief_lowered_from: _lowered, ...rest } = incoming;
|
||||
let f = rest;
|
||||
if (!ENGINE_NAMES.includes(f.engine) && 'contradicts_brief' in f) {
|
||||
const { contradicts_brief: _mark, ...unmarked } = f;
|
||||
f = unmarked;
|
||||
}
|
||||
if (typeof f.file !== 'string' || f.file.length === 0 ||
|
||||
(typeof f.line === 'number' && f.line < 0)) {
|
||||
suppress(f, 'no-citation', dropped, unverified);
|
||||
|
|
@ -305,7 +398,13 @@ export function reasonablenessFilter(findings) {
|
|||
continue;
|
||||
}
|
||||
const rule = getRule(f.rule_key);
|
||||
if (rule && f.severity !== rule.severity) {
|
||||
if (lowerableBriefConflict(f)) {
|
||||
// The one legal lowering: a marked /code-review MAJOR on a MAJOR-tier key.
|
||||
kept.push({ ...f, severity: 'MINOR', brief_lowered_from: 'MAJOR' });
|
||||
} else if (rule && f.severity !== rule.severity) {
|
||||
// Today's catalogue reset. A marked engine finding arrives at its tier
|
||||
// (engine-findings.mjs), so it lands here only when its severity was
|
||||
// changed on the way, and the reset puts it back with its mark kept.
|
||||
kept.push({ ...f, severity: rule.severity, original_severity: f.severity });
|
||||
} else {
|
||||
kept.push(f);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
// 0 = ALLOW or WARN
|
||||
// 1 = BLOCK
|
||||
// 2 = could not decide: missing/unreadable file, not a review, no verdict,
|
||||
// an unknown verdict, a file that contradicts its own verdict, or a
|
||||
// an unknown verdict, a file that contradicts its own verdict, a
|
||||
// finding lowered or marked against the brief outside the one legal
|
||||
// shape (coordinator-contract `illegalBriefLowering`), or a
|
||||
// WARN because a reviewer or engine did not run (`missing-reviewer:` /
|
||||
// `unattributable-payload` in allow_blocked_by — operator decision
|
||||
// 2026-10-06: a review not performed in full stops a merge).
|
||||
|
|
@ -29,6 +31,7 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { validateReviewContent, VERDICT_VALUES } from '../validators/review-validator.mjs';
|
||||
import { fencedBlocks, isJsonBlock } from './findings-schema.mjs';
|
||||
import { illegalBriefLowering } from './coordinator-contract.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, UNDECIDED: 2 });
|
||||
|
|
@ -100,6 +103,13 @@ export function decideVerdict(text) {
|
|||
if (blockerFindings > 0 || blockerCount > 0 || blockerSection > 0) {
|
||||
return undecided(`verdict ${verdict} over BLOCKER finding(s) (json: ${blockerFindings}, counts: ${blockerCount}, ## Findings (BLOCKER): ${blockerSection})`, verdict);
|
||||
}
|
||||
// The brief-conflict rule (coordinator-contract `illegalBriefLowering`, the
|
||||
// one place it lives): a finding lowered or marked outside the one legal
|
||||
// shape means the verdict may be greener than the findings — never green.
|
||||
const illegal = payload.findings.filter((f) => illegalBriefLowering(f)).map((f) => f.id);
|
||||
if (illegal.length > 0) {
|
||||
return undecided(`verdict ${verdict} over a brief-conflict lowering outside the one legal shape (finding ${illegal.join(', ')})`, verdict);
|
||||
}
|
||||
const fmIds = new Set(Array.isArray(fm.findings) ? fm.findings : []);
|
||||
const jsonIds = new Set(payload.findings.map((f) => f.id));
|
||||
if (fmIds.size !== jsonIds.size || [...fmIds].some((id) => !jsonIds.has(id))) {
|
||||
|
|
|
|||
55
tests/fixtures/review-brief-conflict-illegal.md
vendored
Normal file
55
tests/fixtures/review-brief-conflict-illegal.md
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
type: trekreview
|
||||
review_version: "1.1"
|
||||
task: "t"
|
||||
slug: s
|
||||
project_dir: .claude/projects/2026-10-06-s/
|
||||
brief_path: .claude/projects/2026-10-06-s/brief.md
|
||||
scope_sha_end: abc
|
||||
reviewed_files_count: 1
|
||||
verdict: ALLOW
|
||||
findings:
|
||||
- 8530de651325215c87cc8ba418c42ba3e8a66ed2
|
||||
---
|
||||
|
||||
# Review: t
|
||||
|
||||
## Executive Summary
|
||||
|
||||
One reviewer-agent finding (no engine) carries a brief-conflict mark at MINOR: only an engine finding is ever marked, so the gate refuses it.
|
||||
|
||||
## Coverage
|
||||
|
||||
| File | Treatment | Reason |
|
||||
|------|-----------|--------|
|
||||
| lib/b.mjs | summary-only | default |
|
||||
|
||||
## Remediation Summary
|
||||
|
||||
x
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": "ALLOW",
|
||||
"counts": {
|
||||
"BLOCKER": 0,
|
||||
"MAJOR": 0,
|
||||
"MINOR": 1,
|
||||
"SUGGESTION": 0
|
||||
},
|
||||
"allow_blocked_by": [],
|
||||
"findings": [
|
||||
{
|
||||
"id": "8530de651325215c87cc8ba418c42ba3e8a66ed2",
|
||||
"severity": "MINOR",
|
||||
"rule_key": "MISSING_TEST",
|
||||
"file": "lib/b.mjs",
|
||||
"line": 7,
|
||||
"title": "No test for the retry path",
|
||||
"detail": "The retry branch has no test.",
|
||||
"recommended_action": "Add a test for the retry branch.",
|
||||
"contradicts_brief": "No retry layer around the HTTP client (Non-Goal 2)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
57
tests/fixtures/review-brief-conflict-legal.md
vendored
Normal file
57
tests/fixtures/review-brief-conflict-legal.md
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
type: trekreview
|
||||
review_version: "1.1"
|
||||
task: "t"
|
||||
slug: s
|
||||
project_dir: .claude/projects/2026-10-06-s/
|
||||
brief_path: .claude/projects/2026-10-06-s/brief.md
|
||||
scope_sha_end: abc
|
||||
reviewed_files_count: 1
|
||||
verdict: ALLOW
|
||||
findings:
|
||||
- d3c22f4b1309d6001a0f3948e377a8e80a4b9c55
|
||||
---
|
||||
|
||||
# Review: t
|
||||
|
||||
## Executive Summary
|
||||
|
||||
One /code-review finding contradicts a brief Non-Goal; it is kept, marked and lowered from MAJOR to MINOR (the one legal shape).
|
||||
|
||||
## Coverage
|
||||
|
||||
| File | Treatment | Reason |
|
||||
|------|-----------|--------|
|
||||
| lib/a.mjs | summary-only | default |
|
||||
|
||||
## Remediation Summary
|
||||
|
||||
x
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": "ALLOW",
|
||||
"counts": {
|
||||
"BLOCKER": 0,
|
||||
"MAJOR": 0,
|
||||
"MINOR": 1,
|
||||
"SUGGESTION": 0
|
||||
},
|
||||
"allow_blocked_by": [],
|
||||
"findings": [
|
||||
{
|
||||
"id": "d3c22f4b1309d6001a0f3948e377a8e80a4b9c55",
|
||||
"severity": "MINOR",
|
||||
"rule_key": "CODE_REVIEW_DEFECT",
|
||||
"file": "lib/a.mjs",
|
||||
"line": 12,
|
||||
"title": "No retry on transient failure",
|
||||
"detail": "A 503 fails the call.",
|
||||
"recommended_action": "Wrap the call in a retry with backoff.",
|
||||
"engine": "code-review",
|
||||
"contradicts_brief": "No retry layer around the HTTP client (Non-Goal 2)",
|
||||
"brief_lowered_from": "MAJOR"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
308
tests/review/brief-conflict-findings.test.mjs
Normal file
308
tests/review/brief-conflict-findings.test.mjs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
// tests/review/brief-conflict-findings.test.mjs
|
||||
// The brief-conflict rule (plan 2026-10-11-files-backticks-trial4, Step 8).
|
||||
//
|
||||
// A `/code-review` finding at the MAJOR tier whose recommended action
|
||||
// contradicts a named brief Non-Goal (or stated choice) is kept, marked with
|
||||
// `contradicts_brief: "<the Non-Goal, verbatim>"`, and drops to MINOR with
|
||||
// `brief_lowered_from: 'MAJOR'`. Nothing else is ever lowered by the mark: a
|
||||
// BLOCKER stays BLOCKER, a `/security-review` finding keeps its severity, a
|
||||
// reviewer-agent finding (no `engine`) is never marked. The verdict gate
|
||||
// refuses (exit 2) every shape outside the one legal one.
|
||||
//
|
||||
// The new exports are probed via `import * as m` so a missing export is an
|
||||
// assertion failure, never a load-time crash. Fixtures are read inside tests.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as m from '../../lib/review/coordinator-contract.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const GATE = join(ROOT, 'lib', 'review', 'verdict-gate.mjs');
|
||||
const LEGAL_FIXTURE = join(ROOT, 'tests', 'fixtures', 'review-brief-conflict-legal.md');
|
||||
const ILLEGAL_FIXTURE = join(ROOT, 'tests', 'fixtures', 'review-brief-conflict-illegal.md');
|
||||
|
||||
const NON_GOAL = 'No retry layer around the HTTP client (Non-Goal 2)';
|
||||
|
||||
function illegal(f) {
|
||||
assert.equal(typeof m.illegalBriefLowering, 'function',
|
||||
'coordinator-contract.mjs must export illegalBriefLowering(f)');
|
||||
return m.illegalBriefLowering(f);
|
||||
}
|
||||
|
||||
function lowerable(f) {
|
||||
assert.equal(typeof m.lowerableBriefConflict, 'function',
|
||||
'coordinator-contract.mjs must export lowerableBriefConflict(f)');
|
||||
return m.lowerableBriefConflict(f);
|
||||
}
|
||||
|
||||
function codeReviewDefect(extra = {}) {
|
||||
return {
|
||||
file: 'lib/http.mjs', line: 42, rule_key: 'CODE_REVIEW_DEFECT', severity: 'MAJOR',
|
||||
title: 'No retry on transient failure', detail: 'A 503 fails the call.',
|
||||
recommended_action: 'Wrap the call in a retry with backoff.', engine: 'code-review',
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function payload(reviewer, findings) {
|
||||
return [{ reviewer, findings }];
|
||||
}
|
||||
|
||||
function legalShape(extra = {}) {
|
||||
return codeReviewDefect({
|
||||
id: 'd3c22f4b1309d6001a0f3948e377a8e80a4b9c55',
|
||||
severity: 'MINOR', contradicts_brief: NON_GOAL, brief_lowered_from: 'MAJOR', ...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function assertAllOutputLegal(result) {
|
||||
for (const f of result.findings) {
|
||||
assert.equal(illegal(f), false,
|
||||
`runContract wrote a finding illegalBriefLowering refuses: ${JSON.stringify(f)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- runContract (Pass 3) ----------------------------------------------------
|
||||
|
||||
test('runContract — a marked /code-review MAJOR drops to MINOR, keeps the Non-Goal, verdict ALLOW, not unverified', () => {
|
||||
const result = m.runContract(payload('claude-code-review', [codeReviewDefect({ contradicts_brief: NON_GOAL })]));
|
||||
assert.equal(result.findings.length, 1, 'the brief-conflicting finding is kept, never removed');
|
||||
const f = result.findings[0];
|
||||
assert.equal(f.contradicts_brief, NON_GOAL, 'the Non-Goal is named verbatim');
|
||||
assert.equal(f.severity, 'MINOR');
|
||||
assert.equal(f.brief_lowered_from, 'MAJOR');
|
||||
assert.equal(result.verdict, 'ALLOW');
|
||||
assert.equal(result.unverified.length, 0, 'the mark is not a suppression reason');
|
||||
assert.deepEqual(result.allow_blocked_by, []);
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — the same /code-review MAJOR without the mark stays MAJOR and WARN', () => {
|
||||
const result = m.runContract(payload('claude-code-review', [codeReviewDefect()]));
|
||||
assert.equal(result.findings[0].severity, 'MAJOR');
|
||||
assert.equal('brief_lowered_from' in result.findings[0], false);
|
||||
assert.equal(result.verdict, 'WARN');
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — a marked /code-review BLOCKER-tier finding stays BLOCKER and BLOCK', () => {
|
||||
const f = codeReviewDefect({ rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER', contradicts_brief: NON_GOAL });
|
||||
const result = m.runContract(payload('claude-code-review', [f]));
|
||||
assert.equal(result.findings[0].severity, 'BLOCKER');
|
||||
assert.equal('brief_lowered_from' in result.findings[0], false);
|
||||
assert.equal(result.findings[0].contradicts_brief, NON_GOAL, 'the mark is kept on an engine finding');
|
||||
assert.equal(result.verdict, 'BLOCK');
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — a marked /security-review finding keeps BLOCKER', () => {
|
||||
const f = {
|
||||
file: 'lib/auth.mjs', line: 9, rule_key: 'SECURITY_REVIEW_VULNERABILITY', severity: 'BLOCKER',
|
||||
title: 'Token in log', detail: 'd', recommended_action: 'Remove the log line.',
|
||||
engine: 'security-review', contradicts_brief: NON_GOAL,
|
||||
};
|
||||
const result = m.runContract(payload('claude-security-review', [f]));
|
||||
assert.equal(result.findings[0].severity, 'BLOCKER');
|
||||
assert.equal('brief_lowered_from' in result.findings[0], false);
|
||||
assert.equal(result.verdict, 'BLOCK');
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — a marked MINOR-tier CODE_REVIEW_IMPROVEMENT stays MINOR with no brief_lowered_from', () => {
|
||||
const f = codeReviewDefect({ rule_key: 'CODE_REVIEW_IMPROVEMENT', severity: 'MINOR', contradicts_brief: NON_GOAL });
|
||||
const result = m.runContract(payload('claude-code-review', [f]));
|
||||
assert.equal(result.findings[0].severity, 'MINOR');
|
||||
assert.equal('brief_lowered_from' in result.findings[0], false);
|
||||
assert.equal(result.findings[0].contradicts_brief, NON_GOAL);
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — a reviewer finding (no engine) that arrives marked loses the mark and keeps today\'s reset', () => {
|
||||
const f = {
|
||||
file: 'lib/b.mjs', line: 7, rule_key: 'MISSING_TEST', severity: 'BLOCKER',
|
||||
title: 'No test', detail: 'd', recommended_action: 'Add a test.', contradicts_brief: NON_GOAL,
|
||||
};
|
||||
const result = m.runContract(payload('code-correctness-reviewer', [f]));
|
||||
const out = result.findings[0];
|
||||
assert.equal('contradicts_brief' in out, false, 'only an engine finding is ever marked');
|
||||
assert.equal(out.severity, 'MAJOR');
|
||||
assert.equal(out.original_severity, 'BLOCKER');
|
||||
assert.equal('brief_lowered_from' in out, false);
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('runContract — an incoming brief_lowered_from is removed from every finding', () => {
|
||||
const reviewer = {
|
||||
file: 'lib/b.mjs', line: 7, rule_key: 'MISSING_TEST', severity: 'MAJOR',
|
||||
title: 'No test', detail: 'd', recommended_action: 'Add a test.', brief_lowered_from: 'MAJOR',
|
||||
};
|
||||
const engineBlocker = codeReviewDefect({ line: 50, rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER', brief_lowered_from: 'BLOCKER' });
|
||||
const engineUnmarked = codeReviewDefect({ line: 60, brief_lowered_from: 'MAJOR' });
|
||||
const lowerableOne = codeReviewDefect({ line: 70, contradicts_brief: NON_GOAL, brief_lowered_from: 'BLOCKER' });
|
||||
const result = m.runContract([
|
||||
{ reviewer: 'code-correctness-reviewer', findings: [reviewer] },
|
||||
{ reviewer: 'claude-code-review', findings: [engineBlocker, engineUnmarked, lowerableOne] },
|
||||
]);
|
||||
const byLine = new Map(result.findings.map((f) => [f.line, f]));
|
||||
assert.equal('brief_lowered_from' in byLine.get(7), false);
|
||||
assert.equal('brief_lowered_from' in byLine.get(50), false);
|
||||
assert.equal(byLine.get(50).severity, 'BLOCKER');
|
||||
assert.equal('brief_lowered_from' in byLine.get(60), false);
|
||||
assert.equal(byLine.get(60).severity, 'MAJOR');
|
||||
assert.equal(byLine.get(70).severity, 'MINOR');
|
||||
assert.equal(byLine.get(70).brief_lowered_from, 'MAJOR', 'only the lowering itself writes the field');
|
||||
assertAllOutputLegal(result);
|
||||
});
|
||||
|
||||
test('lowerableBriefConflict — only a marked /code-review finding at MAJOR on a MAJOR-tier key', () => {
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL })), true);
|
||||
assert.equal(lowerable(codeReviewDefect()), false, 'no mark');
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: '' })), false, 'empty mark');
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: true })), false, 'non-string mark');
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL, engine: 'security-review' })), false);
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL, engine: undefined })), false, 'missing engine');
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL, rule_key: 'NOPE' })), false, 'unknown key');
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL, rule_key: 'SECURITY_INJECTION', severity: 'BLOCKER' })), false);
|
||||
assert.equal(lowerable(codeReviewDefect({ contradicts_brief: NON_GOAL, severity: 'BLOCKER' })), false, 'severity must be MAJOR');
|
||||
});
|
||||
|
||||
// ---- effectiveSeverity (Pass 2 trimming) -------------------------------------
|
||||
|
||||
test('judgeFilter — a marked /code-review MAJOR with a long detail is trimmed, its mark intact; unmarked it is kept whole', () => {
|
||||
const long = 'x'.repeat(900);
|
||||
const marked = codeReviewDefect({ detail: long, contradicts_brief: NON_GOAL });
|
||||
const unmarked = codeReviewDefect({ detail: long });
|
||||
const a = m.judgeFilter([marked]);
|
||||
assert.equal(a.trimmed.length, 1, 'the marked finding is judged MINOR, so its text may be trimmed');
|
||||
assert.equal(a.trimmed[0].contradicts_brief, NON_GOAL, 'trimming never touches the mark');
|
||||
assert.ok(a.trimmed[0].detail.length <= m.JUDGE_DETAIL_MAX);
|
||||
const b = m.judgeFilter([unmarked]);
|
||||
assert.equal(b.trimmed.length, 0, 'an unmarked MAJOR keeps every character');
|
||||
assert.equal(b.kept[0].detail, long);
|
||||
|
||||
const result = m.runContract(payload('claude-code-review', [marked]));
|
||||
assert.equal(result.trimmed_count, 1);
|
||||
assert.equal(result.findings[0].contradicts_brief, NON_GOAL);
|
||||
assert.equal(result.findings[0].severity, 'MINOR');
|
||||
});
|
||||
|
||||
// ---- illegalBriefLowering ----------------------------------------------------
|
||||
|
||||
test('illegalBriefLowering — the legal shape is false', () => {
|
||||
assert.equal(illegal(legalShape()), false);
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — a marked BLOCKER-tier engine finding below BLOCKER is true, with and without brief_lowered_from', () => {
|
||||
const base = { rule_key: 'SECURITY_INJECTION', contradicts_brief: NON_GOAL };
|
||||
assert.equal(illegal(codeReviewDefect({ ...base, severity: 'MINOR', brief_lowered_from: 'MAJOR' })), true);
|
||||
assert.equal(illegal(codeReviewDefect({ ...base, severity: 'MAJOR', brief_lowered_from: 'BLOCKER' })), true);
|
||||
assert.equal(illegal(codeReviewDefect({ ...base, severity: 'MAJOR' })), true);
|
||||
assert.equal(illegal(codeReviewDefect({ ...base, severity: 'MINOR' })), true);
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — a marked /security-review finding below its tier is true', () => {
|
||||
const f = {
|
||||
id: 'a'.repeat(40), file: 'lib/auth.mjs', line: 9, rule_key: 'SECURITY_REVIEW_VULNERABILITY',
|
||||
engine: 'security-review', contradicts_brief: NON_GOAL,
|
||||
};
|
||||
assert.equal(illegal({ ...f, severity: 'MAJOR' }), true);
|
||||
assert.equal(illegal({ ...f, severity: 'MINOR', brief_lowered_from: 'MAJOR' }), true);
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — a marked reviewer finding (no engine) is true at any severity', () => {
|
||||
const f = { id: 'b'.repeat(40), file: 'lib/b.mjs', line: 7, rule_key: 'MISSING_TEST', contradicts_brief: NON_GOAL };
|
||||
assert.equal(illegal({ ...f, severity: 'MINOR' }), true, 'below its tier');
|
||||
assert.equal(illegal({ ...f, severity: 'MAJOR' }), true, 'at its tier');
|
||||
assert.equal(illegal({ ...f, severity: 'BLOCKER' }), true, 'above its tier');
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — brief_lowered_from without a mark is true', () => {
|
||||
assert.equal(illegal(legalShape({ contradicts_brief: undefined })), true,
|
||||
'brief_lowered_from MAJOR on an otherwise legal finding without a mark');
|
||||
const reviewer = { id: 'b'.repeat(40), file: 'lib/b.mjs', line: 7, rule_key: 'MISSING_TEST', severity: 'MAJOR', brief_lowered_from: 'MAJOR' };
|
||||
assert.equal(illegal(reviewer), true, 'brief_lowered_from on an unmarked reviewer finding');
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — a marked engine finding with an unknown or missing rule_key is true', () => {
|
||||
assert.equal(illegal(codeReviewDefect({ rule_key: 'NOT_A_KEY', contradicts_brief: NON_GOAL })), true);
|
||||
assert.equal(illegal(codeReviewDefect({ rule_key: undefined, contradicts_brief: NON_GOAL })), true);
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — contradicts_brief: true (not a string) on the otherwise legal shape is true', () => {
|
||||
assert.equal(illegal(legalShape({ contradicts_brief: true })), true);
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — an unmarked engine finding below its tier is true', () => {
|
||||
const sec = {
|
||||
id: 'c'.repeat(40), file: 'lib/auth.mjs', line: 9, rule_key: 'SECURITY_REVIEW_VULNERABILITY',
|
||||
severity: 'MINOR', engine: 'security-review',
|
||||
};
|
||||
assert.equal(illegal(sec), true, 'unmarked /security-review SECURITY_REVIEW_VULNERABILITY at MINOR');
|
||||
assert.equal(illegal(codeReviewDefect({ severity: 'MINOR' })), true, 'unmarked /code-review CODE_REVIEW_DEFECT at MINOR');
|
||||
});
|
||||
|
||||
test('illegalBriefLowering — false for engine findings at their tier and for unmarked reviewer findings', () => {
|
||||
assert.equal(illegal(codeReviewDefect({ contradicts_brief: NON_GOAL })), false, 'a marked engine finding at its tier');
|
||||
assert.equal(illegal(codeReviewDefect()), false, 'an unmarked engine finding at its tier');
|
||||
const reviewer = { id: 'b'.repeat(40), file: 'lib/b.mjs', line: 7, rule_key: 'MISSING_TEST' };
|
||||
assert.equal(illegal({ ...reviewer, severity: 'MINOR' }), false,
|
||||
'an unmarked reviewer MISSING_TEST at MINOR without brief_lowered_from (the quick-mode shape)');
|
||||
assert.equal(illegal({ ...reviewer, severity: 'BLOCKER' }), false,
|
||||
'an unmarked reviewer MISSING_TEST asserted BLOCKER (above its tier)');
|
||||
});
|
||||
|
||||
// ---- review → plan handover --------------------------------------------------
|
||||
|
||||
test('review → plan handover — the lowered finding is left out by /trekplan\'s severity filter', () => {
|
||||
const prose = readFileSync(join(ROOT, 'commands', 'trekplan.md'), 'utf-8');
|
||||
const line = prose.split('\n').find((l) => l.includes('Filter findings to severity'));
|
||||
assert.ok(line, 'commands/trekplan.md carries the "Filter findings to severity" line');
|
||||
const set = line.match(/\{([^}]*)\}/);
|
||||
assert.ok(set, 'the filter line names a {…} severity set');
|
||||
const severities = new Set(set[1].split(',').map((s) => s.trim()));
|
||||
assert.deepEqual([...severities].sort(), ['BLOCKER', 'MAJOR']);
|
||||
|
||||
const lowered = m.runContract(payload('claude-code-review', [codeReviewDefect({ contradicts_brief: NON_GOAL })]));
|
||||
assert.equal(lowered.findings.filter((f) => severities.has(f.severity)).length, 0,
|
||||
'a brief-lowered finding does not become a plan goal');
|
||||
const unmarked = m.runContract(payload('claude-code-review', [codeReviewDefect()]));
|
||||
assert.equal(unmarked.findings.filter((f) => severities.has(f.severity)).length, 1,
|
||||
'known-positive control: the same finding unmarked does');
|
||||
});
|
||||
|
||||
// ---- the verdict gate --------------------------------------------------------
|
||||
|
||||
function runGate(path) {
|
||||
return spawnSync(process.execPath, [GATE, path], { encoding: 'utf-8' });
|
||||
}
|
||||
|
||||
test('verdict gate — exit 0 on the legal brief lowering', () => {
|
||||
assert.ok(existsSync(LEGAL_FIXTURE), 'tests/fixtures/review-brief-conflict-legal.md exists');
|
||||
const r = runGate(LEGAL_FIXTURE);
|
||||
assert.equal(r.status, 0, `legal lowering must pass: ${r.stdout}${r.stderr}`);
|
||||
});
|
||||
|
||||
test('verdict gate — exit 2 on the illegal brief lowering, the reason naming the finding id', () => {
|
||||
assert.ok(existsSync(ILLEGAL_FIXTURE), 'tests/fixtures/review-brief-conflict-illegal.md exists');
|
||||
const text = readFileSync(ILLEGAL_FIXTURE, 'utf-8');
|
||||
const json = JSON.parse(text.slice(text.lastIndexOf('```json') + 7, text.lastIndexOf('```')));
|
||||
assert.equal(json.verdict, 'ALLOW', 'the fixture claims ALLOW');
|
||||
const id = json.findings[0].id;
|
||||
assert.match(id, /^[0-9a-f]{40}$/);
|
||||
const r = runGate(ILLEGAL_FIXTURE);
|
||||
assert.equal(r.status, 2, `a marked reviewer finding must be refused: ${r.stdout}${r.stderr}`);
|
||||
assert.ok(r.stdout.includes(id), `the reason names the finding id ${id}: ${r.stdout}`);
|
||||
});
|
||||
|
||||
test('verdict gate — imports illegalBriefLowering from coordinator-contract.mjs (the rule lives once)', () => {
|
||||
const src = readFileSync(GATE, 'utf-8');
|
||||
assert.match(src,
|
||||
/^import\s*\{[^}]*\billegalBriefLowering\b[^}]*\}\s*from\s*['"]\.\/coordinator-contract\.mjs['"]/m,
|
||||
'verdict-gate.mjs must import illegalBriefLowering from ./coordinator-contract.mjs');
|
||||
assert.doesNotMatch(src, /function\s+illegalBriefLowering\b/,
|
||||
'verdict-gate.mjs must not carry its own copy of the rule');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue