feat(review): the brief's threat model on the review side (decision 164, 167, 218)

The code-correctness reviewer's dimension 6 shrinks to what the engines do
not read: command, skill and agent markdown carrying shell, with Claude
Code's threshold (a concrete attack path, high confidence), the keys
SECURITY_INJECTION or SECURITY_RISK, and threat_model_ref quoting the
brief's Outside: line; the code-level bullets go to the engines.

The coordinator puts a Voyage security finding with a threat_model_ref
into the trailing JSON block's known_outside, never into findings.
review-finalize gains the pass `outside`, right after engine: it only
raises. An entry the brief at the review's brief_path does not back now
(line gone, threat model not usable, an engine finding, not a security
finding) goes back into the findings at its catalogue tier with a Source
line, R-step and raised verdict, as the engine put-back does; the rest are
listed under the new `## Outside the Threat Model` section.

verdict-gate decideOpenFindings (and so scripts/release-check.mjs)
re-checks every entry against brief_path through the new
notBackedOutside and exits 2 for one not backed. review-validator
shape-checks the optional known_outside (REVIEW_KNOWN_OUTSIDE_MALFORMED).
threat-model.mjs gains reviewThreatModel and isKnownEntry for the three.

/trekreview Phase 8.8 lists the known issues outside the threat model
(not answered, not counted); the review template shows the section and
field; Handover 1 names the optional ## Threat Model and its code,
Handover 6 names known_outside and its code.

Part 2, step 2.5 of the E plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-08 09:14:47 +01:00
commit c47061191c
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
10 changed files with 585 additions and 30 deletions

View file

@ -22,6 +22,7 @@ import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
import { extractFindingsBlock } from '../review/findings-schema.mjs';
import { isKnownEntry } from '../review/threat-model.mjs';
export const REVIEW_REQUIRED_FRONTMATTER = [
'type',
@ -76,8 +77,10 @@ export function remediationStepSources(body) {
// `fixed` without its R-step), REVIEW_POLICY_MALFORMED (`review_policy`,
// `review_policy_sha256`, a finding's `instruction_ref`), REVIEW_ROUND_MALFORMED
// (`round`, `round_base`, `finalized`, `resolved`, `ran`, a finding's
// `carried_from`) and REVIEW_PRE_EXISTING_MALFORMED (a finding's `pre_existing`,
// `pre_existing_counts`).
// `carried_from`), REVIEW_PRE_EXISTING_MALFORMED (a finding's `pre_existing`,
// `pre_existing_counts`) and REVIEW_KNOWN_OUTSIDE_MALFORMED (the optional
// `known_outside` list, E 2.5: Voyage's own security findings the brief's
// threat model puts outside; lib/review/threat-model.mjs `isKnownEntry`).
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const SHA256_RE = /^[0-9a-f]{64}$/;
@ -163,6 +166,7 @@ function review12Issues(fm, body) {
const policy = (msg) => out.push(issue('REVIEW_POLICY_MALFORMED', msg));
const pre = (msg) => out.push(issue('REVIEW_PRE_EXISTING_MALFORMED', msg));
const answers = (msg) => out.push(issue('REVIEW_ANSWERS_MALFORMED', msg));
const outside = (msg) => out.push(issue('REVIEW_KNOWN_OUTSIDE_MALFORMED', msg));
// Frontmatter.
if ('round' in fm && !(Number.isInteger(fm.round) && fm.round >= 1 && fm.round <= 3)) {
@ -211,6 +215,17 @@ function review12Issues(fm, body) {
round(`resolved must be an array of 40-char hex finding ids, got ${JSON.stringify(r)}`);
}
}
if ('known_outside' in payload) {
const k = payload.known_outside;
if (!Array.isArray(k)) outside(`known_outside must be an array, got ${JSON.stringify(k)}`);
else {
k.forEach((e, i) => {
if (!isKnownEntry(e)) {
outside(`known_outside[${i}] needs a string rule_key and file, an integer line >= 0 and a non-empty threat_model_ref, got ${JSON.stringify(e)}`);
}
});
}
}
if ('ran' in payload) {
const r = payload.ran;
const okRan = isPlainObject(r)