// tests/validators/plan-gate-threat-model.test.mjs // Part 2, step 2.4 of the E plan (decisions 164, 218, 220): the brief's threat // model on the plan side (lib/validators/plan-gate.mjs, the reader in // lib/review/threat-model.mjs). // // - `--record --brief ` applies the threat model to the deduped findings // before countRound: a backed finding (Voyage's own security finding whose // `threat_model_ref` equals an `Outside:` line of a usable threat model) // leaves the counted set and the stop rule and goes to the verdict's // `known_outside` (id, rule_key, text, line), carried until marked // resolved; the verdict stores `brief_path`. When the threat model is not // usable every security finding counts, and the output says why. // - The bare gate re-checks each entry against the brief: backed -> WARN // (PLAN_GATE_KNOWN_OUTSIDE); not backed -> STOP // (PLAN_GATE_OUTSIDE_NOT_BACKED, it counts in full). // - extractVerdict shape-checks both new fields. // // The record cases follow tests/validators/plan-gate-stop-rule.test.mjs; the // CLI case runs the gate as a child in its own temp `git init` repo with a // stamped and logged brief. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { resolve, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { gatePlan, recordVerdict, planHash, operativeText, extractVerdict } from '../../lib/validators/plan-gate.mjs'; import { dedupFindings } from '../../lib/review/plan-review-dedup.mjs'; import { readThreatModel } from '../../lib/review/threat-model.mjs'; import { stampIntentApproval } from '../../lib/validators/intent-approval.mjs'; import { appendApproval, normalizeHex, normalizeSubject, resolveRepo } from '../../lib/util/approval-log.mjs'; import { buildPlan } from '../helpers/plan-v18-builder.mjs'; import { childEnv } from '../helpers/child-env.mjs'; const NOW = '2026-11-08T00:00:00.000Z'; const PLAN = resolve('plan.md'); const PATHS = [PLAN]; const OUTSIDE = "a local user who can already edit the repo's hooks"; const INSIDE = 'a crafted file name in a pull request reaches the shell'; const BRIEF = `--- type: trekbrief brief_version: "2.2" created: 2026-11-08 task: "Add a parser CLI" slug: parser-cli project_dir: .claude/projects/2026-11-08-parser-cli/ research_topics: 0 research_status: complete framing: new-direction --- # Task ## TL;DR A parser CLI. ## Intent The CLI reads files from strangers. ## Goal A parser CLI that checks its input. ## Threat Model - Inside: ${INSIDE} - Outside: ${OUTSIDE} ## Success Criteria - It parses. `; const TM = { usable: true, why: null, threatModel: readThreatModel(BRIEF) }; const NOT_USABLE = { usable: false, why: 'the intent approval is not current (BRIEF_INTENT_APPROVAL_STALE)', threatModel: readThreatModel(BRIEF) }; function at(text, s) { const n = text.split('\n').findIndex(l => l.includes(s)) + 1; assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`); return n; } const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) }); function rec(text, d, opts = {}) { const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now: NOW, ...opts }); assert.equal(r.code, 0, r.message); return r; } function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) { return dedupFindings([ { agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } }, { agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } }, ]); } const f = (severity, line, text, rule_key, extra = {}) => ({ file: 'plan.md', line, rule_key, severity, text, ...extra }); const R1_CHANGES = 'Add `run1()`'; const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)'; const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.'; const editA = t => { assert.ok(t.includes(R2_CHANGES)); return t.replace(R2_CHANGES, R2_EDITED); }; const S1 = 'A hook file edited by a local user runs before the parser checks its input'; const B1 = 'The dependency on the parser module is never declared in the manifest list'; /** Round 1: one backed SECURITY_RISK blocker (outside) and nothing else. */ function backedRound1(extra = {}) { const t1 = buildPlan(); return rec(t1, dd({ pc: [f('blocker', at(t1, R1_CHANGES), S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })] }), { threatModel: TM, briefPath: 'brief.md', ...extra }); } // ---- recordVerdict -------------------------------------------------------------------- test('record: a backed security finding leaves the counted set and goes to known_outside; brief_path is stored', () => { const r = backedRound1(); const v = r.verdict; assert.deepEqual(v.counts, { blocker: 0, major: 0, minor: 0 }); assert.equal(v.verdict, 'PASS'); assert.deepEqual(v.findings, []); assert.equal(v.known_outside?.length, 1, JSON.stringify(v)); const e = v.known_outside[0]; assert.deepEqual(Object.keys(e).sort(), ['id', 'line', 'rule_key', 'rule_keys', 'text']); assert.match(e.id, /^[0-9a-f]{40}$/); assert.equal(e.rule_key, 'SECURITY_RISK'); assert.deepEqual(e.rule_keys, ['SECURITY_RISK']); assert.equal(e.text, S1); assert.equal(e.line, OUTSIDE); assert.equal(v.brief_path, 'brief.md'); assert.equal(r.threat_model?.applied, true); // It leaves the stop rule too: the round's summary has no blocking finding. assert.deepEqual(v.rounds, [{ round: 1, blocking: 0, newByClass: {} }]); // The written block reads back with both fields. const ex = extractVerdict(r.text); assert.equal(ex.error, null); assert.deepEqual(ex.verdict.known_outside, v.known_outside); }); test('record: not backed (no ref, an Inside line, an engine finding, or a threat model that is not usable) -> it counts', () => { const t1 = buildPlan(); const l = at(t1, R1_CHANGES); const cases = [ [f('blocker', l, S1, 'SECURITY_RISK'), TM], [f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: INSIDE }), TM], [f('blocker', l, S1, 'SECURITY_REVIEW_VULNERABILITY', { threat_model_ref: OUTSIDE }), TM], [f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE }), NOT_USABLE], [f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE }), undefined], ]; for (const [finding, threatModel] of cases) { const r = rec(t1, dd({ pc: [finding] }), { threatModel }); assert.equal(r.verdict.counts.blocker, 1, JSON.stringify(finding)); assert.equal(r.verdict.known_outside, undefined); assert.equal(r.verdict.verdict, 'BLOCK'); } const r = rec(t1, dd({ pc: [cases[3][0]] }), { threatModel: NOT_USABLE, briefPath: 'brief.md' }); assert.equal(r.threat_model?.applied, false); assert.match(String(r.threat_model?.why), /BRIEF_INTENT_APPROVAL_STALE/, 'the output says why every security finding counts'); }); test('record: a backed finding beside a counted one: only the counted one is blocking in the stop rule', () => { const t1 = buildPlan(); const l = at(t1, R1_CHANGES); const r1 = rec(t1, dd({ pc: [f('blocker', l, B1, 'dep/manifest'), f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })] }), { threatModel: TM, briefPath: 'brief.md' }); assert.equal(r1.verdict.counts.blocker, 1); assert.deepEqual(r1.verdict.rounds, [{ round: 1, blocking: 1, newByClass: { dep: 1 } }]); assert.equal(r1.verdict.known_outside?.length, 1); }); test('record: known_outside is carried to the next round until marked resolved; brief_path is carried without --brief', () => { const r1 = backedRound1(); assert.equal(r1.verdict.known_outside?.length, 1, 'round 1 lists the backed finding'); const id = r1.verdict.known_outside[0].id; const t2 = editA(r1.text); // Round 2, no --brief, nothing re-reported, no mark: carried, and brief_path with it. const r2 = rec(t2, dd(), { base: baseOf(r1.text) }); assert.deepEqual(r2.verdict.known_outside, r1.verdict.known_outside); assert.equal(r2.verdict.brief_path, 'brief.md'); // A mark resolved: false keeps it; resolved: true drops it. const kept = rec(t2, dd({ pcPrev: [{ id, resolved: false }] }), { base: baseOf(r1.text) }); assert.equal(kept.verdict.known_outside?.length, 1); const gone = rec(t2, dd({ pcPrev: [{ id, resolved: true }] }), { base: baseOf(r1.text) }); assert.equal(gone.verdict.known_outside, undefined); }); test('record: re-reported without its backing it counts in full and leaves known_outside; re-reported backed it is listed once', () => { const r1 = backedRound1(); const t2 = editA(r1.text); const l = at(t2, R1_CHANGES); const counted = rec(t2, dd({ pc: [f('blocker', l, S1, 'SECURITY_RISK')] }), { base: baseOf(r1.text), threatModel: TM, briefPath: 'brief.md' }); assert.equal(counted.verdict.counts.blocker, 1); assert.equal(counted.verdict.known_outside, undefined, 'the same id counts now; it is not also a known issue'); const again = rec(t2, dd({ pc: [f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })] }), { base: baseOf(r1.text), threatModel: TM, briefPath: 'brief.md' }); assert.equal(again.verdict.known_outside?.length, 1); assert.equal(again.verdict.counts.blocker, 0); }); test('extractVerdict shape-checks known_outside and brief_path', () => { const r = backedRound1(); const v = extractVerdict(r.text).verdict; const put = obj => r.text.replace(/```json\n[\s\S]*?\n```/, () => '```json\n' + JSON.stringify(obj, null, 2) + '\n```'); for (const bad of [ { ...v, known_outside: 'x' }, { ...v, known_outside: [null] }, { ...v, known_outside: [{ id: 'a', rule_key: 'SECURITY_RISK', text: 'x' }] }, { ...v, known_outside: [{ id: 'a', rule_key: 'SECURITY_RISK', text: 'x', line: '' }] }, { ...v, known_outside: [{ id: 7, rule_key: 'SECURITY_RISK', text: 'x', line: 'y' }] }, { ...v, brief_path: '' }, { ...v, brief_path: 3 }, ]) { assert.ok(extractVerdict(put(bad)).error, `malformed: ${JSON.stringify({ known_outside: bad.known_outside, brief_path: bad.brief_path })}`); } assert.equal(extractVerdict(put(v)).error, null); }); // ---- the bare gate ------------------------------------------------------------------ test('gate: a backed entry is a WARN (PLAN_GATE_KNOWN_OUTSIDE) and the plan may go on', () => { const r = backedRound1(); const g = gatePlan(r.text, { threatModel: TM }); assert.equal(g.code, 0, JSON.stringify(g.reasons)); const w = g.warnings.find(x => x.code === 'PLAN_GATE_KNOWN_OUTSIDE'); assert.ok(w, JSON.stringify(g.warnings)); assert.ok(w.message.includes(S1) && w.message.includes(OUTSIDE), w.message); }); test('gate: an entry not backed now (line gone, or the threat model not usable) STOPs with PLAN_GATE_OUTSIDE_NOT_BACKED', () => { const r = backedRound1(); const gone = { usable: true, why: null, threatModel: readThreatModel(BRIEF.replace(OUTSIDE, 'anyone at all')) }; for (const [tm, why] of [[gone, /no `?Outside:`? line/], [NOT_USABLE, /BRIEF_INTENT_APPROVAL_STALE/]]) { const g = gatePlan(r.text, { threatModel: tm }); assert.equal(g.code, 1); const s = g.reasons.find(x => x.code === 'PLAN_GATE_OUTSIDE_NOT_BACKED'); assert.ok(s, JSON.stringify(g.reasons)); assert.ok(s.message.includes(S1), s.message); assert.match(s.message, /counts? in full/); assert.match(s.message, why); assert.ok(!g.warnings.some(x => x.code === 'PLAN_GATE_KNOWN_OUTSIDE')); } }); test('gate guard: a planted known_outside entry that is not a Voyage security finding STOPs', () => { const r = backedRound1(); assert.equal(r.verdict.known_outside?.length, 1, 'round 1 lists the backed finding'); const planted = r.text.replace(/```json\n[\s\S]*?\n```/, () => { const v = extractVerdict(r.text).verdict; return '```json\n' + JSON.stringify({ ...v, known_outside: [{ ...v.known_outside[0], rule_key: 'MISSING_TEST', rule_keys: ['MISSING_TEST'] }] }, null, 2) + '\n```'; }); assert.equal(extractVerdict(planted).error, null); const g = gatePlan(planted, { threatModel: TM }); assert.equal(g.code, 1); assert.ok(g.reasons.some(x => x.code === 'PLAN_GATE_OUTSIDE_NOT_BACKED'), JSON.stringify(g.reasons)); }); test('gate: without --brief the brief is read from the verdict\'s brief_path; none named -> not backed', () => { const r = backedRound1({ briefPath: undefined }); assert.equal(r.verdict.brief_path, undefined); const g = gatePlan(r.text); assert.equal(g.code, 1); const s = g.reasons.find(x => x.code === 'PLAN_GATE_OUTSIDE_NOT_BACKED'); assert.ok(s, JSON.stringify(g.reasons)); assert.match(s.message, /brief_path/); }); // ---- CLI ------------------------------------------------------------------------------ const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..'); const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs'); const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined }; const run = (cwd, args, input) => { const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) }); return { code: r.status, out: r.stdout, err: r.stderr }; }; const lastJson = out => JSON.parse(out.split('\n').filter(l => l !== '').at(-1)); test('CLI: --record --brief lists a backed finding as a known issue; the gate warns, and STOPs once the Outside line changes', () => { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-'))); try { assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0); const b = join(dir, 'brief.md'); const s = stampIntentApproval(BRIEF, new Date(NOW)); assert.equal(s.stamped, true); writeFileSync(b, s.text); const repo = resolveRepo(dir); assert.equal(appendApproval({ repo, kind: 'intent', subject: normalizeSubject(repo, b), sha: normalizeHex(s.hash) }).appended, true); const p = join(dir, 'plan.md'); writeFileSync(p, buildPlan()); const t1 = readFileSync(p, 'utf-8'); const payload = dd({ pc: [{ ...f('blocker', at(t1, R1_CHANGES), S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE }), file: p }] }); const c1 = run(dir, ['--record', p, '--brief', 'brief.md', '--reviewed', planHash(t1)], JSON.stringify(payload)); assert.equal(c1.code, 0, c1.out + c1.err); const j1 = lastJson(c1.out); assert.equal(j1.verdict, 'PASS'); assert.equal(j1.known_outside, 1); assert.deepEqual(j1.threat_model, { applied: true, why: null }); const v = extractVerdict(readFileSync(p, 'utf-8')).verdict; assert.equal(v.brief_path, 'brief.md'); const g = run(dir, [p]); assert.equal(g.code, 0, g.out); assert.match(g.out, /WARN {2}\[PLAN_GATE_KNOWN_OUTSIDE\]/); // The operator lifts the finding back in: the Outside line changes (the approval goes stale too). writeFileSync(b, readFileSync(b, 'utf-8').replace(OUTSIDE, 'anyone at all')); const g2 = run(dir, [p, '--json']); assert.equal(g2.code, 1, g2.out); const j2 = JSON.parse(g2.out); assert.ok(j2.reasons.some(x => x.code === 'PLAN_GATE_OUTSIDE_NOT_BACKED'), g2.out); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('fix 6: brief_path is stored relative to the repo top level, and the gate run from a subdirectory reads the same brief', () => { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-'))); try { assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0); const b = join(dir, 'brief.md'); const s = stampIntentApproval(BRIEF, new Date(NOW)); writeFileSync(b, s.text); const repo = resolveRepo(dir); assert.equal(appendApproval({ repo, kind: 'intent', subject: normalizeSubject(repo, b), sha: normalizeHex(s.hash) }).appended, true); const sub = join(dir, 'sub'); mkdirSync(sub); const p = join(dir, 'plan.md'); for (const [cwd, briefArg] of [[dir, 'brief.md'], [sub, '../brief.md'], [sub, b]]) { writeFileSync(p, buildPlan()); const t1 = readFileSync(p, 'utf-8'); const payload = dd({ pc: [{ ...f('blocker', at(t1, R1_CHANGES), S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE }), file: p }] }); const c1 = run(cwd, ['--record', p, '--brief', briefArg, '--reviewed', planHash(t1)], JSON.stringify(payload)); assert.equal(c1.code, 0, c1.out + c1.err); assert.equal(lastJson(c1.out).known_outside, 1, `${cwd} ${briefArg}: ${c1.out}`); assert.equal(extractVerdict(readFileSync(p, 'utf-8')).verdict.brief_path, 'brief.md', `stored relative to the top level (${briefArg})`); const g = run(sub, [p]); assert.equal(g.code, 0, `the gate from a subdirectory: ${g.out}`); assert.match(g.out, /WARN {2}\[PLAN_GATE_KNOWN_OUTSIDE\]/); } } finally { rmSync(dir, { recursive: true, force: true }); } }); test('CLI: --brief without a value is a usage error; a threat model that is not usable is said in the output', () => { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-'))); try { assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0); const p = join(dir, 'plan.md'); writeFileSync(p, buildPlan()); const t1 = readFileSync(p, 'utf-8'); const bare = run(dir, ['--record', p, '--reviewed', planHash(t1), '--brief'], JSON.stringify(dd())); assert.equal(bare.code, 2, bare.out + bare.err); // An unapproved brief: the finding counts, and the output says why. writeFileSync(join(dir, 'brief.md'), BRIEF); const payload = dd({ pc: [{ ...f('blocker', at(t1, R1_CHANGES), S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE }), file: p }] }); const c1 = run(dir, ['--record', p, '--brief', 'brief.md', '--reviewed', planHash(t1)], JSON.stringify(payload)); assert.equal(c1.code, 0, c1.out + c1.err); const j1 = lastJson(c1.out); assert.equal(j1.verdict, 'BLOCK'); assert.equal(j1.known_outside, 0); assert.equal(j1.threat_model.applied, false); assert.match(j1.threat_model.why, /BRIEF_INTENT_NOT_APPROVED/); } finally { rmSync(dir, { recursive: true, force: true }); } }); // ---- Part 2 fix round --------------------------------------------------------------- test('fix 1: a string entry in a reviewer\'s findings still counts as a blocker through dedup and --record', () => { const t1 = buildPlan(); const d = dedupFindings([ { agent: 'plan-critic', payload: { findings: ['BLOCKER: step 2 runs rm -rf on an unquoted user path'] } }, { agent: 'scope-guardian', payload: { findings: [] } }, ]); assert.equal(d.findings.length, 1, 'the string entry is kept as a finding with no valid severity'); const r = rec(t1, d); assert.equal(r.verdict.counts.blocker, 1); assert.equal(r.verdict.verdict, 'BLOCK'); }); // 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), 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, 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', () => { const t1 = buildPlan(); const l = at(t1, R1_CHANGES); const d = dd({ pc: [f('blocker', l, S1, 'injection/shell', { threat_model_ref: OUTSIDE })], sg: [f('blocker', l, S1, 'SECURITY_RISK', { threat_model_ref: OUTSIDE })], }); assert.equal(d.findings.length, 1, 'dedup merged the two'); const r = rec(t1, d, { threatModel: TM, briefPath: 'brief.md' }); assert.equal(r.verdict.known_outside?.length, 1); assert.deepEqual(r.verdict.known_outside[0].rule_keys, ['injection/shell', 'SECURITY_RISK']); const g = gatePlan(r.text, { threatModel: TM }); assert.equal(g.code, 0, JSON.stringify(g.reasons)); assert.ok(g.warnings.some(w => w.code === 'PLAN_GATE_KNOWN_OUTSIDE'), JSON.stringify(g.warnings)); // extractVerdict accepts rule_keys and refuses a malformed one. const v = extractVerdict(r.text).verdict; const bad = r.text.replace(/```json\n[\s\S]*?\n```/, () => '```json\n' + JSON.stringify({ ...v, known_outside: [{ ...v.known_outside[0], rule_keys: [7] }] }, null, 2) + '\n```'); assert.ok(extractVerdict(bad).error, 'a rule_keys that is not a list of strings is malformed'); }); test('fix 4: a reviewer\'s mark on a known_outside id passes dedup and the count, and resolved: true removes the entry (diff mode and full count)', () => { const r1 = backedRound1(); const id = r1.verdict.known_outside?.[0]?.id; assert.ok(id, 'round 1 lists the backed finding'); const t2 = editA(r1.text); const d = dd({ pcPrev: [{ id, resolved: true }], sgPrev: [{ id, resolved: true }] }); assert.deepEqual(d.marks, [{ agent: 'plan-critic', id, resolved: true }, { agent: 'scope-guardian', id, resolved: true }]); for (const opts of [{ base: baseOf(r1.text) }, { baseNote: 'no snapshot; full counting' }]) { const r2 = recordVerdict(t2, d, { reviewed: planHash(t2), planPaths: PATHS, now: NOW, ...opts }); assert.equal(r2.code, 0, r2.message); assert.equal(r2.verdict.known_outside, undefined, JSON.stringify(opts)); } }); // ---- Handover 4 --------------------------------------------------------------------- test('Handover 4 names --brief, known_outside, brief_path and the two new codes', () => { const h = readFileSync(join(ROOT, 'docs', 'HANDOVER-CONTRACTS.md'), 'utf-8'); for (const s of ['`--brief `', '`known_outside`', '`brief_path`', '`PLAN_GATE_KNOWN_OUTSIDE`', '`PLAN_GATE_OUTSIDE_NOT_BACKED`']) { assert.ok(h.includes(s), `docs/HANDOVER-CONTRACTS.md names ${s}`); } });