fix(review): the coordinator gets the brief's choices and writes the brief-conflict mark
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8cc6ba0181
commit
11315d04eb
4 changed files with 239 additions and 6 deletions
|
|
@ -66,10 +66,15 @@ You will receive a prompt containing:
|
|||
the /trekreview triage gate.
|
||||
- **Brief metadata** — `task`, `slug`, `project_dir`, `brief_path` from
|
||||
the brief frontmatter.
|
||||
- **Brief choices** — the brief's `## Non-Goals`, `## Constraints` and
|
||||
`## Preferences` sections and the operator decisions in
|
||||
`## Open Questions / Assumptions`, verbatim. Read only by the
|
||||
brief-conflict rule (Pass 3).
|
||||
- **Scope SHA range** — `scope_sha_start`, `scope_sha_end`,
|
||||
`reviewed_files_count`.
|
||||
- **Mode** — `default` or `quick`. In `quick` mode, skip Pass 3
|
||||
(reasonableness filter); Passes 1, 2, 4 still run.
|
||||
(reasonableness filter) except the brief-conflict rule, which applies in every mode;
|
||||
Passes 1, 2, 4 still run.
|
||||
- **Rule catalogue** — `lib/review/rule-catalogue.mjs`. Findings whose
|
||||
`rule_key` is not in this set are dropped by Pass 3.
|
||||
|
||||
|
|
@ -124,6 +129,7 @@ over-long `detail` sent a finding to `unverified`, and S118's most serious
|
|||
finding fell out of the Handover 6 list that way. Length is packaging, never
|
||||
a claim: only small findings are trimmed, and the trims are counted
|
||||
(`trimmed` in the JSON block). The trim decision reads the **catalogue** tier,
|
||||
except a lowerable brief conflict, which is judged as MINOR (decide the mark before trimming),
|
||||
so a mislabelled MAJOR is never cut. Borrowed: "report at most five nits …
|
||||
summarize the rest as a count" — cap only the nits (ai-native-sdlc
|
||||
`assets/REVIEW.md:7-23`, MIT).
|
||||
|
|
@ -157,8 +163,32 @@ Remove findings that fail ANY of these tests:
|
|||
catalogue tier (e.g., `MISSING_TEST` emitted as MINOR). Reset to the
|
||||
catalogue tier; this is a correction, neither a drop nor an unverified.
|
||||
|
||||
**The brief-conflict rule (one fixed rule).** Read the **Brief choices** input.
|
||||
An engine finding (`engine: code-review` or `security-review`) whose recommended action
|
||||
contradicts a passed Non-Goal or stated choice gets `contradicts_brief: "<verbatim>"`
|
||||
(that Non-Goal or choice, word for word) and is kept, never removed;
|
||||
only a `/code-review` finding at the MAJOR tier and at severity MAJOR then drops to MINOR, with `brief_lowered_from: MAJOR`
|
||||
(in place of the catalogue reset above);
|
||||
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding only get the mark,
|
||||
and no engine finding is otherwise written below its catalogue tier.
|
||||
The rule reads engine findings alone, so only an engine finding is ever marked:
|
||||
`engine` is written only on a finding whose kept Pass 1 survivor itself came from an engine payload
|
||||
(`claude-code-review` → `code-review`, `claude-security-review` → `security-review`),
|
||||
and a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue are never marked.
|
||||
Remove any `brief_lowered_from` an incoming finding carries, and any mark on a finding
|
||||
without an `engine`: only this rule writes them.
|
||||
The rule is about the finding's recommended action, not the rule key `NON_GOAL_VIOLATED`
|
||||
(a reviewer's claim that the delivered code breaks a Non-Goal is judged like any other finding).
|
||||
The mark is the brief-conflict reason (`contradicts_brief`), a field, never a `suppressed_reason`:
|
||||
the finding stays kept and counted at the severity written, never `unverified`.
|
||||
`lib/review/coordinator-contract.mjs` (`lowerableBriefConflict`, `illegalBriefLowering`)
|
||||
holds the same rule, and on an ALLOW or WARN review
|
||||
the verdict gate refuses (exit 2) every other marked or lowered shape.
|
||||
|
||||
In `quick` mode, skip this pass entirely. Note the skip in the
|
||||
Executive Summary so the reader knows reasonableness was not applied.
|
||||
The brief-conflict rule above still applies in quick mode (the mark and the
|
||||
one lowering) and changes nothing else.
|
||||
|
||||
**High-effort normalization (v5.1.1):** When the review is invoked
|
||||
under high-effort mode (`phase_signals[review].effort: high`), Pass 3
|
||||
|
|
@ -170,7 +200,8 @@ category in the catalogue). The original rule_key is
|
|||
preserved in the finding's `original_rule_key` field for diagnostic
|
||||
purposes. This normalization happens BEFORE writing review.md,
|
||||
ensuring all `rule_key` values in the final review match the
|
||||
catalogue.
|
||||
catalogue. The brief-conflict rule (Pass 3) still applies under high-effort
|
||||
mode (the mark and the one lowering) and changes nothing else.
|
||||
|
||||
### Suppression is two-valued (fail-closed)
|
||||
|
||||
|
|
@ -310,6 +341,9 @@ followed by these fields:
|
|||
- `- title: {short imperative title}`
|
||||
- `- detail: {what is wrong, with citation}`
|
||||
- `- recommended_action: {one imperative step}`
|
||||
- `- engine: {code-review | security-review, when the finding came from an engine}`
|
||||
- `- contradicts_brief: {verbatim Non-Goal or choice, only when set}`
|
||||
- `- brief_lowered_from: {MAJOR, only when lowered for the brief}`
|
||||
|
||||
### Trailing JSON block
|
||||
|
||||
|
|
@ -333,6 +367,20 @@ The LAST fenced block in the file is a `json` block:
|
|||
"title": "...",
|
||||
"detail": "...",
|
||||
"recommended_action": "..."
|
||||
},
|
||||
{
|
||||
"id": "<40-char-hex>",
|
||||
"severity": "MINOR",
|
||||
"rule_key": "CODE_REVIEW_DEFECT",
|
||||
"file": "lib/http.mjs",
|
||||
"line": 42,
|
||||
"brief_ref": "Non-Goal 2",
|
||||
"title": "...",
|
||||
"detail": "...",
|
||||
"recommended_action": "...",
|
||||
"engine": "code-review",
|
||||
"contradicts_brief": "No retry layer around the HTTP client",
|
||||
"brief_lowered_from": "MAJOR"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -346,13 +394,17 @@ for the ID list.
|
|||
## Hard rules
|
||||
|
||||
- **Bounded operations only.** You do NOT read the diff. You do NOT
|
||||
re-evaluate findings against the brief. You do NOT generate new
|
||||
re-evaluate findings against the brief, with one named exception: the
|
||||
brief-conflict rule (Pass 3), which marks an engine finding against the
|
||||
**Brief choices** and lowers only the one legal shape. You do NOT generate new
|
||||
findings. The reviewers' outputs are your sole input. Synthesis-level
|
||||
inference (e.g., "these 3 findings together suggest a pattern") is
|
||||
forbidden in v1.0.
|
||||
- **Verdict is mechanical.** No "ALLOW with caveats" or other custom
|
||||
verdicts. Only BLOCK / WARN / ALLOW per the threshold table.
|
||||
- **Severity floor is the catalogue.** Pass 3 corrects mismatches by
|
||||
- **Severity floor is the catalogue.** It holds for every finding
|
||||
except a lowerable brief conflict, which is MINOR (Pass 3, `brief_lowered_from: MAJOR`).
|
||||
Pass 3 corrects mismatches by
|
||||
resetting to the catalogue tier — never by dropping. Pass 1's severity
|
||||
tiebreak uses the catalogue tier, not the reviewer's emitted value.
|
||||
- **Block-style YAML for findings list.** The frontmatter parser
|
||||
|
|
@ -380,6 +432,7 @@ for the ID list.
|
|||
reviewer outputs. Never escalate a finding's severity beyond what the
|
||||
catalogue specifies.
|
||||
- **Quick mode is documented.** When mode is `quick`, the Executive
|
||||
Summary says so, and Pass 3 is skipped — no other changes.
|
||||
Summary says so, and Pass 3 is skipped — no other changes,
|
||||
except the brief-conflict rule, which applies in every mode.
|
||||
- **Honesty in fallback paths.** If `scope_sha_start` is null (mtime
|
||||
fallback), the Executive Summary names this limitation explicitly.
|
||||
|
|
|
|||
|
|
@ -480,6 +480,15 @@ from Phase 5 AND the Phase 4.7 engine payloads, plus the triage map, brief
|
|||
metadata, SHA range, the expected reviewer set (engines included), and each
|
||||
not-run engine with its reason.
|
||||
|
||||
**Brief choices.** Next to the brief metadata, pass the brief's choices
|
||||
verbatim: extract the `## Non-Goals`, `## Constraints` and `## Preferences`
|
||||
sections and the operator decisions in `## Open Questions / Assumptions` from
|
||||
the brief, word for word (a section the brief does not carry is passed as
|
||||
absent, never written for it). The coordinator reads them for the
|
||||
brief-conflict rule only (`agents/review-coordinator.md` Pass 3): an engine
|
||||
finding whose recommended action contradicts one of them is kept and marked
|
||||
`contradicts_brief`, never removed.
|
||||
|
||||
The coordinator runs the 4-pass process documented in
|
||||
`agents/review-coordinator.md`:
|
||||
|
||||
|
|
@ -552,7 +561,11 @@ Contract (verified in S10 part B — follow exactly):
|
|||
|
||||
Then continue at **Phase 7** exactly as the prose path does — Phase 7 rendering, Phase 8
|
||||
validation, and the operator gate are **shared** and substrate-independent (both paths
|
||||
return the same `{verdict, findings}` shape).
|
||||
return the same `{verdict, findings}` shape). The workflow's returned findings keep
|
||||
`engine` (set by the script from the payload a finding came from, never from a label),
|
||||
`contradicts_brief` and `brief_lowered_from`, and Phase 7 writes them into each
|
||||
finding's field list and the trailing JSON block of review.md exactly as the prose path
|
||||
does.
|
||||
|
||||
**Known limitation (per bake-off §Posture, surfaced not hidden).** Classifier interference
|
||||
was measured **0** at 9-agent concurrency in the session's default permission mode; an
|
||||
|
|
|
|||
|
|
@ -461,6 +461,22 @@ Since 1.1 (ordre 5):
|
|||
- `## Declined to Judge` — required at `review_version` ≥ 1.1 (strict error, soft warning; 1.0 reviews exempt): every finding set aside without a ruling, with its reason (including engine findings outside the review scope, `engine:out-of-scope`), plus `Trimmed: N`. **No finding is struck for length**: only MINOR/SUGGESTION text is trimmed, and counted (`trimmed` in the JSON block); a long BLOCKER/MAJOR keeps every character.
|
||||
- Findings from Claude Code's own `/code-review` and `/security-review` enter under the catalogue keys `CODE_REVIEW_DEFECT` (MAJOR), `CODE_REVIEW_IMPROVEMENT` (MINOR: a `/code-review` finding whose category is simplification, efficiency, reuse, style or readability) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security-review`.
|
||||
|
||||
**The brief-conflict mark (fields `engine`, `contradicts_brief`, `brief_lowered_from`).**
|
||||
Each finding may carry three more fields, in its body field list and in the trailing JSON block:
|
||||
`engine` (`code-review` | `security-review`, on a finding that came from an engine),
|
||||
`contradicts_brief` (the brief's Non-Goal or stated choice the finding's recommended action contradicts, verbatim)
|
||||
and `brief_lowered_from` (`MAJOR`, written only by the one lowering).
|
||||
The mark is a field on a kept finding, never a `suppressed_reason`.
|
||||
Only an engine finding is ever marked; a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue never are.
|
||||
The one legal lowering: a `/code-review` MAJOR-tier finding at severity MAJOR drops to MINOR, with `brief_lowered_from: MAJOR`;
|
||||
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding keep their severity and only carry the mark.
|
||||
On an ALLOW or WARN review the verdict gate exits 2 on every finding `illegalBriefLowering` (`lib/review/coordinator-contract.mjs`) names:
|
||||
any other `brief_lowered_from`, a mark on a reviewer-agent finding, a marked finding below its tier, a marked finding with an unknown key,
|
||||
and an engine finding below its tier, marked or not (a BLOCK review exits 1 as before).
|
||||
The gate cannot check an `engine` label on the prose path: there the coordinator writes the label, so a finding wrongly labelled `engine` is judged as an engine finding;
|
||||
the workflow path (`--workflow`) takes `engine` from the payload source and writes it into the returned finding.
|
||||
A plan made from the review skips a lowered finding: it is MINOR, and `/trekplan` takes BLOCKER and MAJOR findings only.
|
||||
|
||||
**Validation strategy:**
|
||||
|
||||
| Layer | When | What |
|
||||
|
|
|
|||
|
|
@ -306,3 +306,154 @@ test('verdict gate — imports illegalBriefLowering from coordinator-contract.mj
|
|||
assert.doesNotMatch(src, /function\s+illegalBriefLowering\b/,
|
||||
'verdict-gate.mjs must not carry its own copy of the rule');
|
||||
});
|
||||
|
||||
// ---- the prose states the same rule (Step 10) --------------------------------
|
||||
//
|
||||
// The coordinator is a model reading agents/review-coordinator.md; the gate
|
||||
// above enforces the rule on what it writes. These pins keep the prose saying
|
||||
// the same rule the code enforces. Every pin reads a bounded cut, never the
|
||||
// whole file, so a phrase elsewhere in the file cannot satisfy it.
|
||||
|
||||
const readDoc = (...rel) => readFileSync(join(ROOT, ...rel), 'utf-8');
|
||||
|
||||
/** The text from `startMarker` up to (not including) the first `endMarker` after it. */
|
||||
function cut(text, startMarker, endMarker, label) {
|
||||
const start = text.indexOf(startMarker);
|
||||
assert.ok(start >= 0, `${label}: start marker missing: ${JSON.stringify(startMarker)}`);
|
||||
const end = text.indexOf(endMarker, start + startMarker.length);
|
||||
assert.ok(end > start, `${label}: end marker ${JSON.stringify(endMarker)} missing after the start`);
|
||||
return text.slice(start, end);
|
||||
}
|
||||
|
||||
/** The sentence from `startPhrase` to its full stop (the first '.' after it). */
|
||||
function sentenceFrom(text, startPhrase, label) {
|
||||
const start = text.indexOf(startPhrase);
|
||||
assert.ok(start >= 0, `${label}: sentence start missing: ${JSON.stringify(startPhrase)}`);
|
||||
const stop = text.indexOf('.', start + startPhrase.length);
|
||||
assert.ok(stop > start, `${label}: the sentence has no full stop`);
|
||||
return text.slice(start, stop + 1);
|
||||
}
|
||||
|
||||
/** One hard-rule bullet, from its bold lead to the next bullet. */
|
||||
function hardRule(coord, lead) {
|
||||
const at = coord.indexOf('\n## Hard rules');
|
||||
assert.ok(at >= 0, 'the ## Hard rules section exists');
|
||||
const rules = coord.slice(at);
|
||||
return cut(`${rules}\n- **`, `- **${lead}**`, '\n- **', `hard rule "${lead}"`);
|
||||
}
|
||||
|
||||
test('trekreview Phase 6 — passes the brief\'s Non-Goals, Constraints, Preferences and Open Questions decisions to the coordinator', () => {
|
||||
const cmd = readDoc('commands', 'trekreview.md');
|
||||
const p6 = cut(cmd, '## Phase 6 — Coordinator dedup + verdict', '\n## ', 'Phase 6');
|
||||
for (const section of ['`## Non-Goals`', '`## Constraints`', '`## Preferences`', '`## Open Questions / Assumptions`']) {
|
||||
assert.ok(p6.includes(section), `Phase 6 names the brief section ${section}`);
|
||||
}
|
||||
assert.match(p6, /operator decisions in `## Open Questions \/ Assumptions`/, 'only the operator decisions of Open Questions');
|
||||
assert.match(p6, /verbatim/, 'the choices are passed verbatim');
|
||||
assert.match(p6, /brief choices/i, 'Phase 6 names the input the coordinator reads');
|
||||
});
|
||||
|
||||
test('trekreview workflow path — engine, contradicts_brief and brief_lowered_from are carried into review.md', () => {
|
||||
const cmd = readDoc('commands', 'trekreview.md');
|
||||
const wf = cut(cmd, 'Then continue at **Phase 7**', '\n\n', 'workflow paragraph');
|
||||
for (const field of ['`engine`', '`contradicts_brief`', '`brief_lowered_from`']) {
|
||||
assert.ok(wf.includes(field), `the workflow paragraph names ${field}`);
|
||||
}
|
||||
assert.match(wf, /review\.md/, 'the fields are written into review.md');
|
||||
});
|
||||
|
||||
test('review-coordinator Input — names "Brief choices" (the four sections) and the mode line names the exception', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const input = cut(coord, '## Input', '\n## Your 4-pass process', 'Input');
|
||||
assert.match(input, /\*\*Brief choices\*\*/);
|
||||
const choices = cut(input, '**Brief choices**', '\n- **', 'Brief choices bullet');
|
||||
for (const section of ['`## Non-Goals`', '`## Constraints`', '`## Preferences`', '`## Open Questions / Assumptions`']) {
|
||||
assert.ok(choices.includes(section), `Brief choices names ${section}`);
|
||||
}
|
||||
assert.match(choices, /verbatim/);
|
||||
const mode = cut(input, '- **Mode**', '\n- **', 'mode line');
|
||||
assert.match(mode, /except the brief-conflict rule, which applies in every mode/);
|
||||
});
|
||||
|
||||
test('review-coordinator Pass 3 — only an engine finding is marked, and the one lowering needs severity MAJOR', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const p3 = cut(coord, '### Pass 3', 'In `quick` mode', 'Pass 3');
|
||||
assert.match(p3, /`contradicts_brief: "<verbatim>"`/, 'the mark carries the Non-Goal or choice verbatim');
|
||||
assert.match(p3, /engine finding \(`engine: code-review` or `security-review`\)/, 'only an engine finding is marked');
|
||||
assert.match(p3, /only an engine finding is ever marked/);
|
||||
assert.match(p3, /only a `\/code-review` finding at the MAJOR tier and at severity MAJOR then drops to MINOR, with `brief_lowered_from: MAJOR`/,
|
||||
'the one lowering, severity MAJOR required');
|
||||
assert.match(p3, /a BLOCKER-tier, a MINOR-tier and any `\/security-review` finding only get the mark/);
|
||||
assert.match(p3, /no engine finding is otherwise written below its catalogue tier/);
|
||||
assert.match(p3, /`engine` is written only on a finding whose kept Pass 1 survivor itself came from an engine payload/);
|
||||
assert.match(p3, /a reviewer-agent finding \(no `engine`\) and a finding whose `rule_key` is not in the catalogue are never marked/);
|
||||
assert.match(p3, /not the rule key `NON_GOAL_VIOLATED`/);
|
||||
assert.ok(p3.includes('the brief-conflict reason (`contradicts_brief`), a field, never a `suppressed_reason`'),
|
||||
'the mark is named as a field, never a suppression reason');
|
||||
assert.match(p3, /verdict gate refuses \(exit 2\) every other marked or lowered shape/);
|
||||
});
|
||||
|
||||
test('review-coordinator — quick mode and high-effort normalization each still apply the brief-conflict rule', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const pass3 = coord.indexOf('### Pass 3');
|
||||
assert.ok(pass3 >= 0, 'Pass 3 heading');
|
||||
const quick = cut(coord.slice(pass3), 'In `quick` mode, skip this pass entirely.', '\n\n', 'quick-mode paragraph');
|
||||
assert.match(quick, /brief-conflict rule[^.]*still applies/, 'the quick-mode paragraph says the rule still applies');
|
||||
const high = cut(coord, '**High-effort normalization (v5.1.1):**', '\n### ', 'high-effort paragraph');
|
||||
assert.match(high, /brief-conflict rule[^.]*still applies/, 'the high-effort paragraph says the rule still applies');
|
||||
});
|
||||
|
||||
test('review-coordinator Pass 2 — the trim rule judges a lowerable brief conflict as MINOR, inside its own sentence', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const s = sentenceFrom(coord, 'The trim decision reads the **catalogue** tier', 'trim rule');
|
||||
assert.match(s, /except a lowerable brief conflict, which is judged as MINOR \(decide the mark before trimming\)/);
|
||||
assert.match(s, /so a mislabelled MAJOR is never cut\.$/, 'the sentence still ends on its own guarantee');
|
||||
});
|
||||
|
||||
test('review-coordinator hard rules — the brief re-evaluation, the severity floor and quick mode each name the exception', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
assert.match(hardRule(coord, 'Bounded operations only.'), /brief-conflict rule/,
|
||||
'"You do NOT re-evaluate findings against the brief" names its one exception');
|
||||
assert.match(hardRule(coord, 'Severity floor is the catalogue.'), /except a lowerable brief conflict, which is MINOR/);
|
||||
assert.match(hardRule(coord, 'Quick mode is documented.'), /except the brief-conflict rule, which applies in every mode/);
|
||||
});
|
||||
|
||||
test('review-coordinator output — the field list and the trailing JSON example carry engine, contradicts_brief, brief_lowered_from', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const fields = cut(coord, 'Each Findings subsection uses the', '### Trailing JSON block', 'field list');
|
||||
assert.ok(fields.includes('- `- engine: {code-review | security-review, when the finding came from an engine}`'));
|
||||
assert.ok(fields.includes('- `- contradicts_brief: {verbatim Non-Goal or choice, only when set}`'));
|
||||
assert.ok(fields.includes('- `- brief_lowered_from: {MAJOR, only when lowered for the brief}`'));
|
||||
assert.ok(!fields.includes('original_severity'), 'the brief rule does not read original_severity');
|
||||
|
||||
const block = coord.match(/### Trailing JSON block\n\nThe LAST fenced block in the file is a `json` block:\n\n```json\n([\s\S]*?)\n```/);
|
||||
assert.ok(block, 'the trailing JSON example is present');
|
||||
const objects = block[1].match(/\{[^{}]*\}/g) ?? [];
|
||||
const lowered = objects.find((o) => /"brief_lowered_from":\s*"MAJOR"/.test(o));
|
||||
assert.ok(lowered, 'the JSON example shows a finding with brief_lowered_from');
|
||||
assert.match(lowered, /"engine":\s*"code-review"/);
|
||||
assert.match(lowered, /"contradicts_brief":\s*"[^"]+"/);
|
||||
assert.match(lowered, /"severity":\s*"MINOR"/);
|
||||
assert.match(lowered, /"rule_key":\s*"CODE_REVIEW_DEFECT"/);
|
||||
});
|
||||
|
||||
test('Handover 6 — names the fields, engine findings only, the one lowering and the five shapes the gate refuses', () => {
|
||||
const contracts = readDoc('docs', 'HANDOVER-CONTRACTS.md');
|
||||
const h6 = cut(contracts, '## Handover 6 — `review.md` → plan', '\n## Handover 7', 'Handover 6');
|
||||
for (const field of ['`engine`', '`contradicts_brief`', '`brief_lowered_from`']) {
|
||||
assert.ok(h6.includes(field), `Handover 6 names ${field}`);
|
||||
}
|
||||
assert.match(h6, /only an engine finding is (?:ever )?marked/i, 'engine findings only');
|
||||
assert.match(h6, /`\/code-review` MAJOR-tier finding[^.]*MINOR[^.]*`brief_lowered_from: MAJOR`/, 'the one legal lowering');
|
||||
assert.match(h6, /exits 2/);
|
||||
const shapes = [
|
||||
/any other `brief_lowered_from`/,
|
||||
/a mark on a reviewer-agent finding/,
|
||||
/a marked finding below its tier/,
|
||||
/a marked finding with an unknown key/,
|
||||
/an engine finding below its tier, marked or not/,
|
||||
];
|
||||
for (const shape of shapes) assert.match(h6, shape, `Handover 6 names the refused shape ${shape}`);
|
||||
assert.match(h6, /cannot check an `engine` label on the prose path/);
|
||||
assert.match(h6, /skips a lowered finding/, 'a plan made from the review skips a lowered (MINOR) finding');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue