From 6e7fc84e754059e13e65fe734d90a64d887d5bf4 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 3 Nov 2026 04:48:20 +0000 Subject: [PATCH] review: Step 11 fixes: results scripts keep secret, spend-cap and paid-call checks; platform depth needs a docs or tests class and the newest bot review; verify re-hashes the outside voice's diff file Repins lib/review-depth.ts and lib/review-risk.ts under GSTA-258-C: the planner over the same 200 main commits to 6f1dfd5b7c still plans 196 identically and the other 4 only raise (handoffs/GSTA-258/C-planner-noloss/plan-c200-final.tsv). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-review-dispatch | 2 +- lib/review-depth.ts | 6 +++--- lib/review-ledger.ts | 6 +++++- lib/review-pass.ts | 2 +- lib/review-platform.ts | 4 ++-- lib/review-risk.ts | 19 +++++++++++------ lib/ship-receipt-depth.ts | 4 ++-- test/review-depth-lean.test.ts | 33 ++++++++++++++++++++++++++++- test/review-packets-removed.test.ts | 4 ++-- test/ship-receipt-platform.test.ts | 1 + 10 files changed, 62 insertions(+), 19 deletions(-) diff --git a/bin/gstack-review-dispatch b/bin/gstack-review-dispatch index 28dd304b0..88228742a 100755 --- a/bin/gstack-review-dispatch +++ b/bin/gstack-review-dispatch @@ -155,7 +155,7 @@ function render(): number { const ledger = flag('--ledger') ?? path.join(out, 'review-ledger.jsonl'); const sha = promptHash(text); const targeted = brief === undefined ? {} : { classes: parseClassList(flag('--classes')!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(merged ? path.join(ROOT, 'review/specialists/targeted.md') : checklist), diff_base: diffBase ?? null, ...(diffBase ? targetedRead(process.cwd(), diffBase) : { patch_id: 'none', head: null }) }; - appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), workdir, tree, ...targeted, ...(disclosureDigest(flag('--disclosure')) ? { exclude: disclosureDigest(flag('--disclosure')) } : {}) }); + appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), workdir, tree, ...targeted, ...(diffFile.diff_file ? { payload: { file: diffFile.diff_file, sha256: diffFile.diff_sha256 } } : {}), ...(disclosureDigest(flag('--disclosure')) ? { exclude: disclosureDigest(flag('--disclosure')) } : {}) }); process.stdout.write(`BRIEF: reader=${readerKey(role, name)} sha256=${sha} bytes=${Buffer.byteLength(text)} file=${file}\n`); return EXIT.ok; } diff --git a/lib/review-depth.ts b/lib/review-depth.ts index b94393889..af06519cb 100644 --- a/lib/review-depth.ts +++ b/lib/review-depth.ts @@ -549,7 +549,7 @@ export function planReview(i: PlanInput): ReviewPlan { const depth = decideDepth(c, c.runtimeLines ?? (CLASS_RANK[c.class] >= CLASS_RANK.code ? i.diffLines : 0), i.floor); const { class: cls, runtimeLines } = depth; const sh = i.shape; - const platformNow = !!sh && sh.platform === 'on' && sh.platformReview?.head === sh.head && i.history.fullPasses === 0 && !riskClass(c, depth) && platformFits(i.paths, depth.runtimeLines, i.numbers); + const platformNow = !!sh && sh.platform === 'on' && sh.platformReview?.head === sh.head && i.history.fullPasses === 0 && !riskClass(c, depth) && platformFits(c.class, i.paths, depth.runtimeLines, i.numbers); const panel = !platformNow && (!sh || sh.panel === 'all' || (sh.panel === 'risk' && riskClass(c, depth))); const next = nextStep({ ...i.history, gate: cls === 'gate', adversarialRan: !!i.adversarialRan, lean: !panel }, depth.depth); if (!panel) return leanPlan(i, c, depth, next); @@ -635,7 +635,7 @@ export function planReview(i: PlanInput): ReviewPlan { /** Risk class read from the paths and lines (gate class, an R51 or review.risk_paths class, a classifier error), or a floor above light (FOUN-64 item 7: never the recorded review_depth). */ const riskClass = (c: ClassResult, depth: DepthResult) => c.class === 'gate' || c.risk.length > 0 || c.errors.length > 0 || depth.depth !== 'light'; /** Whether a light diff may take the platform depth (FOUN-64 item 2): docs or tests only, no numeric claims, no results, prereg, eval or checker path. The receipt and the queue ask the same. */ -export const platformFits = (paths: readonly string[], runtimeLines: number, numbers: NumbersResult) => runtimeLines === 0 && !numbers.publishes && !paths.some(p => CLAIM_PATH.test(p)); +export const platformFits = (cls: DiffClass, paths: readonly string[], runtimeLines: number, numbers: NumbersResult) => (cls === 'docs' || cls === 'tests') && runtimeLines === 0 && !numbers.publishes && !paths.some(p => CLAIM_PATH.test(p)); /** Paths that carry results or preregistration claims: never the platform depth (FOUN-64 item 2). */ const CLAIM_PATH = /(^|\/|[-_.])(results?|prereg|preregistration|benchmarks?|evals?|scorers?|checkers?)([-_./]|$)/i; @@ -649,7 +649,7 @@ function leanPlan(i: PlanInput, c: ClassResult, depth: DepthResult, next: NextSt dispatches.push({ role, name, effort, exclude: x ? [...exclude, ...x.specs] : exclude, ...extra, ...(x?.keep.length ? { keep: [...x.keep] } : {}) }); const plan = (n: NextStep, mode: ReviewMode): ReviewPlan => ({ ...depth, stepDepth: depth.depth, depthReasons: depth.reasons, reasons: c.reasons, next: n, numbers: i.numbers, dispatches, skipped, mode }); const pr = sh.platformReview; - const platformFit = !risky && platformFits(i.paths, depth.runtimeLines, i.numbers); + const platformFit = !risky && platformFits(c.class, i.paths, depth.runtimeLines, i.numbers); if (next.step === 'full' && platformFit && sh.platform === 'on' && pr && pr.head === sh.head) { const why = `planned skip: platform review ${pr.id} by ${pr.reviewer} at ${pr.head.slice(0, 12)}`; for (const r of ['core checklist', 'specialists', 'red-team', 'adversarial-native', 'outside-voice', 'targeted']) skipped.push(`${r} (${why})`); diff --git a/lib/review-ledger.ts b/lib/review-ledger.ts index dce6fde5d..4b81b416c 100644 --- a/lib/review-ledger.ts +++ b/lib/review-ledger.ts @@ -14,6 +14,7 @@ * The parent's judged set may add or promote a finding and may fold a duplicate into one at least * as severe; it never drops or downgrades one. verify replays every check from the ledger alone. */ +import { createHash } from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { checkReviewedTree, isReviewedTreeLine, promptHash, type TreeCheck } from './review-brief'; @@ -406,7 +407,10 @@ export function verify(ledger: string): { rows: VerifyRow[]; clean: boolean; bri const collected = outputs.some(({ e }) => reviewed(e, r)); const unavailable = !collected && outputs.some(({ e }) => unavailableOutput(e)); const status = !sent.length ? 'NOT_SENT' : mismatched ? 'MISMATCH' : collected ? 'MATCH' : unavailable ? 'UNAVAILABLE' : 'NOT_COLLECTED'; - const tampered = fileNow && fileNow !== r.sha256 ? ' file_changed_since_render' : ''; + const payload = r.payload as { file?: unknown; sha256?: unknown } | undefined; + let payloadNow: string | null = null; + if (!overwritten && payload && typeof payload.file === 'string') { try { payloadNow = createHash('sha256').update(fs.readFileSync(payload.file)).digest('hex'); } catch { payloadNow = ''; } } + const tampered = (fileNow && fileNow !== r.sha256 ? ' file_changed_since_render' : '') + (payloadNow !== null && payloadNow !== payload!.sha256 ? ' payload_changed_since_render' : ''); rows.push({ kind: 'BRIEF', reader: p.label, status: tampered ? 'MISMATCH' : status, detail: `rendered=${short(r.sha256)} sent=${sent.map(({ e }) => short(e.sha256)).join(',') || '-'}${epVoided ? ` voided=${epVoided}` : ''}${outputs.length && !collected && !unavailable ? ' not_reviewed' : ''}${tampered}` }); } for (const [i, o] of events.entries()) { diff --git a/lib/review-pass.ts b/lib/review-pass.ts index 14946097d..392b880b8 100644 --- a/lib/review-pass.ts +++ b/lib/review-pass.ts @@ -187,7 +187,7 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor: const diffFile = path.join(briefs, 'outside-voice.diff'); fs.writeFileSync(diffFile, s.text, { mode: 0o600 }); Object.assign(slots, { diff_file: diffFile, diff_bytes: String(s.bytes), diff_sha256: createHash('sha256').update(s.text).digest('hex'), ...(s.sliced ? { diff_slice: sliceNote(s, REVIEW_DIFF_MAX_BYTES) } : {}) }); - if (s.sliced) slice = { diff_slice: { kept: s.kept, files: s.files, bytes: s.bytes, total: s.total } }; + slice = { payload: { file: diffFile, sha256: slots.diff_sha256 }, ...(s.sliced ? { diff_slice: { kept: s.kept, files: s.files, bytes: s.bytes, total: s.total } } : {}) }; } if (role === 'fix-check') { const read = (f: string | undefined, what: string) => { if (!f) return undefined; try { return fs.readFileSync(f, 'utf8'); } catch { throw new BriefError(`cannot read the fix-check ${what} ${f}`); } }; diff --git a/lib/review-platform.ts b/lib/review-platform.ts index b22c8e414..b22c68ac0 100644 --- a/lib/review-platform.ts +++ b/lib/review-platform.ts @@ -25,8 +25,8 @@ export function platformReviewAt(root: string, head: string, o: { pr?: string; i let reviews: Array<{ id?: number; user?: { login?: string }; commit_id?: string; state?: string }>; try { reviews = (JSON.parse(list.out) as unknown[]).flat() as typeof reviews; } catch { return { ok: false, reason: `PR #${pr}'s reviews are not JSON` }; } const at = reviews.filter(r => r.user?.login === PLATFORM_REVIEWER && r.state !== 'PENDING' && r.commit_id?.toLowerCase() === head.toLowerCase() && r.id !== undefined); - const found = o.id ? at.find(r => String(r.id) === o.id) : at.at(-1); - const hit = found && (found.state === 'APPROVED' || found.state === 'COMMENTED') ? found : undefined; + const newest = at.at(-1); + const hit = newest && (!o.id || String(newest.id) === o.id) && (newest.state === 'APPROVED' || newest.state === 'COMMENTED') ? newest : undefined; if (!hit) return { ok: false, reason: o.id ? `review ${o.id} on PR #${pr} is not a ${PLATFORM_REVIEWER} review at ${head.slice(0, 12)}` : `no ${PLATFORM_REVIEWER} review of PR #${pr} at ${head.slice(0, 12)}` }; return { ok: true, review: { id: String(hit.id), head: head.toLowerCase(), reviewer: PLATFORM_REVIEWER } }; } diff --git a/lib/review-risk.ts b/lib/review-risk.ts index 7a0a4511c..7b313f42f 100644 --- a/lib/review-risk.ts +++ b/lib/review-risk.ts @@ -37,6 +37,8 @@ export const MONEY_LINE = [ /\bapi\.(?:anthropic|openai|voyageai)\.com\b/, /\bgenerativelanguage\.googleapis\.com\b/, /\bopenrouter\.ai\b/, /\bnew\s+(?:Anthropic|OpenAI|AnthropicBedrock|AnthropicVertex)\s*\(/, /\.(?:messages|chat\.completions|responses|embeddings)\.create\s*\(/, ]; +/** The MONEY_LINE entries a config file or results script still answers to: a spend cap and a paid-provider call site, never a bare _usd value. */ +export const PAID_MONEY_LINE = MONEY_LINE.slice(2); export const AUTH_PATH_TOKENS = ['auth', 'authn', 'authz', 'oauth', 'oauth2', 'jwt', 'session', 'sessions', 'permission', 'permissions', 'role', 'roles']; export const AUTH_PATH_PREFIXES = ['authenticat', 'authoriz']; export const SECRET_PATH = /(^|\/)(secrets?|credentials?|keychain|token[s]?)([._/-]|$)/i; @@ -90,8 +92,11 @@ export const DATA_LINE = [ * A key in a config file still names a secret. */ export const CONFIG_DATA_PATH = /\.(json|jsonl|ndjson|csv|tsv|ya?ml|toml|ini|cfg|conf|env|properties|xml|plist)$/i; -/** Analysis scripts that live with their results (docs/benchmarks/.../results/cost-intervals.py): judged by their path, which names no risk class. */ -export const STATS_SCRIPT = /(^|\/)(docs|results|benchmarks?|analysis|notebooks?)\/(?:[^/]+\/)*[^/]+\.(py|r|jl|ipynb)$/i; +/** + * Analysis scripts that live with their results (docs/benchmarks/.../results/cost-intervals.py): their path tokens and generic + * lines name no risk class, but a secret, a spend cap or a paid-provider call in one still does (PAID_MONEY_LINE). + */ +export const STATS_SCRIPT = /(^|\/)results\/(?:[^/]+\/)*[^/]+\.(py|r|jl|ipynb)$/i; /** * Docs that can print a credential (gbrain#8417: `mcp get gbrain` shows a stored bearer token): an Authorization header, a @@ -112,7 +117,7 @@ const namesMoney = (t: readonly string[], words: readonly string[]) => t.some(x export const RISK_RULES = createHash('sha256').update(JSON.stringify({ RISK_CLASSES, MONEY_PATH_TOKENS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME, SECURITY_PATH_TOKENS, DATA_PATH_TOKENS, EVAL_PATH_TOKENS, MONEY_LINE_TOKENS, MONEY_PAIRS, SPEND_PAIRS, - patterns: Object.entries({ PROSE_PATH: [PROSE_PATH], DOC_SECRET_LINE, CONFIG_DATA_PATH: [CONFIG_DATA_PATH], STATS_SCRIPT: [STATS_SCRIPT], MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH], SECURITY_LINE, DATA_LINE, SVG_PATH: [SVG_PATH], SVG_ACTIVE }).map(([k, v]) => [k, v.map(r => '/' + r.source + '/' + r.flags)]), + patterns: Object.entries({ PROSE_PATH: [PROSE_PATH], DOC_SECRET_LINE, CONFIG_DATA_PATH: [CONFIG_DATA_PATH], STATS_SCRIPT: [STATS_SCRIPT], MONEY_LINE, PAID_MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH], SECURITY_LINE, DATA_LINE, SVG_PATH: [SVG_PATH], SVG_ACTIVE }).map(([k, v]) => [k, v.map(r => '/' + r.source + '/' + r.flags)]), })).digest('hex').slice(0, 16); export const tokens = (p: string) => p.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean); @@ -134,9 +139,10 @@ export function matchRisk(i: RiskInput): Map { const hit = (c: RiskClass, reason: string) => { if (!why.has(c)) why.set(c, reason); }; for (const p of i.codePaths) { if (i.isRiskPath?.(p)) hit('policy', 'risk_paths: ' + p); - if (STATS_SCRIPT.test(p)) continue; - const t = tokens(p); + const stats = STATS_SCRIPT.test(p); + const t = stats ? [] : tokens(p); const config = CONFIG_DATA_PATH.test(p); + const pathOnly = config || stats; const prose = PROSE_PATH.test(p); if (i.scopeAuth && t.some(x => AUTH_PATH_TOKENS.includes(x) || AUTH_PATH_PREFIXES.some(a => x.startsWith(a)))) hit('auth', 'auth path: ' + p); if (SECRET_PATH.test(p)) hit('secret', 'secret path: ' + p); @@ -147,7 +153,8 @@ export function matchRisk(i: RiskInput): Map { if (SVG_PATH.test(p) && [i.added?.[p], i.removed?.[p]].some(l => l && svgActive(l.join('\n')))) hit('security', 'active SVG lines: ' + p); for (const line of [...(i.added?.[p] ?? []), ...(i.removed?.[p] ?? [])]) { if (!why.has('secret') && SECRET_LINE.some(r => r.test(line))) hit('secret', 'secret line: ' + p); - if (config) continue; + if (!why.has('money') && pathOnly && PAID_MONEY_LINE.some(r => r.test(line))) hit('money', 'spend cap or paid call: ' + p); + if (pathOnly) continue; if (!why.has('money') && (MONEY_LINE.some(r => r.test(line)) || namesMoney(tokens(line), MONEY_LINE_TOKENS) || (!prose && pairs(tokens(line), SPEND_PAIRS)))) hit('money', 'money line: ' + p); if (!why.has('security') && SECURITY_LINE.some(r => r.test(line))) hit('security', 'security line: ' + p); if (!why.has('data') && DATA_LINE.some(r => r.test(line))) hit('data', 'data line: ' + p); diff --git a/lib/ship-receipt-depth.ts b/lib/ship-receipt-depth.ts index 0a6df24a5..e8f2ad4db 100644 --- a/lib/ship-receipt-depth.ts +++ b/lib/ship-receipt-depth.ts @@ -132,7 +132,7 @@ export function platformReceiptDepth(repoRoot: string, base: string, depth: Rece const { review } = basePolicy(repoRoot, baseSha.ok ? `origin/${branch}` : branch, baseSha.ok ? baseSha.out : null); if (review.platform !== 'on') return { ok: false, message: `--platform-review needs review.platform on in ${branch}'s .gstack/ship-policy.json; with platform review off at least one /ship reader runs` }; const c = collectCommit(repoRoot, depth.base, head); - if (depth.depth !== 'light' || !platformFits(c.paths, depth.runtime_lines, scanNumbers(c.added))) return { ok: false, message: `head ${head.slice(0, 12)} needs ${depth.depth} review (${depth.class} class, ${depth.runtime_lines} runtime lines): the platform depth covers only docs or tests diffs with no numeric, results or prereg claims` }; + if (depth.depth !== 'light' || !platformFits(depth.class, c.paths, depth.runtime_lines, scanNumbers(c.added))) return { ok: false, message: `head ${head.slice(0, 12)} needs ${depth.depth} review (${depth.class} class, ${depth.runtime_lines} runtime lines): the platform depth covers only docs or tests diffs with no numeric, results or prereg claims` }; const found = platformReviewAt(repoRoot, head, { pr, id }); if (!found.ok) return { ok: false, message: `--platform-review ${id}: ${found.reason}` }; return { ok: true, depth: { ...depth, depth: 'platform', mode: 'platform', platform_review: found.review } }; @@ -290,7 +290,7 @@ export function receiptDepthVerdict(root: string, baseRef: string, r: ShipReceip const line = `REVIEW_DEPTH: receipt=${have ?? 'none'} required=${need.depth} runtime_lines=${need.runtimeLines} class=${need.class} reasons=${need.reasons.join(',')} inputs=${inputs} rounds=${r.review_loop?.rounds ?? (r.review_loop ? r.review_loop.passes + (r.review_loop.fix_checks ?? 0) : 'none')}`; if (have === 'platform') { const pr = rd!.platform_review; - const fits = need.depth === 'light' && platformFits(c.paths, need.runtimeLines, scanNumbers(c.added)); + const fits = need.depth === 'light' && platformFits(need.class, c.paths, need.runtimeLines, scanNumbers(c.added)); const ok = review.platform === 'on' && fits && !!pr && pr.head === head.toLowerCase(); if (ok) return { ok: true, line: `${line} platform_review=${pr!.id}@${pr!.head.slice(0, 12)}` }; return { ok: false, line, message: `the receipt records the platform depth, but ${review.platform !== 'on' ? `the base policy has review.platform ${review.platform}` : !fits ? `head ${head.slice(0, 12)} needs ${need.depth} review of a diff the platform depth does not cover` : `its platform review is not at head ${head.slice(0, 12)}`}; the receipt is unchanged; run /ship on ${head.slice(0, 12)}` }; diff --git a/test/review-depth-lean.test.ts b/test/review-depth-lean.test.ts index 367d965bc..6fd77c49d 100644 --- a/test/review-depth-lean.test.ts +++ b/test/review-depth-lean.test.ts @@ -1,7 +1,10 @@ import { describe, test, expect } from 'bun:test'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { classifyDiff, nextStep, planReview, scanNumbers, type PlanInput, type ReviewShape } from '../lib/review-depth'; +import { classifyDiff, nextStep, planReview, platformFits, scanNumbers, type PlanInput, type ReviewShape } from '../lib/review-depth'; +import { appendEvent, verify } from '../lib/review-ledger'; +import * as os from 'node:os'; +import { createHash } from 'node:crypto'; import { matchRisk } from '../lib/review-risk'; import { renderBrief, sectionTexts, sliceDiff, sliceNote } from '../lib/review-brief'; import { validatePolicy, DEFAULT_POLICY, REVIEW_POLICY_KEY_VERSION } from '../lib/ship-policy'; @@ -40,6 +43,16 @@ describe('HARN-1 false triggers (harness-evals#231 on 1.93.7.0): data, config an expect(matchRisk({ codePaths: [p], added: { [p]: ["rows = [json.loads(l) for l in open(os.path.join(sys.argv[1], 'trials.jsonl'))]"] }, scopeAuth: false }).size).toBe(0); expect(matchRisk({ codePaths: ['tools/fetch.py'], added: { 'tools/fetch.py': ['p = os.path.join(sys.argv[1], name)'] }, scopeAuth: false }).has('security')).toBe(true); }); + test('a paid-provider call, a spend cap or a key in a results script or config file still counts', () => { + const run = 'benchmarks/run_models.py', sweep = 'analysis/sweep.py', cfg = 'evals/config.yaml'; + expect(matchRisk({ codePaths: [run], added: { [run]: ['resp = client.messages.create(model=m)'] }, scopeAuth: false }).has('money')).toBe(true); + expect(matchRisk({ codePaths: [sweep], added: { [sweep]: ['MAX = spend_cap(5)'] }, scopeAuth: false }).has('money')).toBe(true); + expect(matchRisk({ codePaths: [run], added: { [run]: ['key = os.environ["OPENAI_API_KEY"]'] }, scopeAuth: false }).has('secret')).toBe(true); + expect(matchRisk({ codePaths: [cfg], added: { [cfg]: ['spend_cap: 9999'] }, scopeAuth: false }).has('money')).toBe(true); + expect(matchRisk({ codePaths: [cfg], added: { [cfg]: ['lease_usd: 3'] }, scopeAuth: false }).has('money')).toBe(false); + expect(matchRisk({ codePaths: ['lib/analysis/billing.py'], added: { 'lib/analysis/billing.py': ['charge(customer, amount_usd)'] }, scopeAuth: false }).has('money')).toBe(true); + expect(matchRisk({ codePaths: ['docs/conf.py'], added: { 'docs/conf.py': ['p = os.path.join(sys.argv[1], name)'] }, scopeAuth: false }).has('security')).toBe(true); + }); test('a secret in a config file still counts', () => { expect(matchRisk({ codePaths: ['deploy/app.yaml'], added: { 'deploy/app.yaml': [' key: ${OPENAI_API_KEY}'] }, scopeAuth: false }).has('secret')).toBe(true); }); @@ -117,6 +130,24 @@ describe('R65 lean dispatch (GSTA-258 build C): one reviewer, once', () => { }); describe('C6 platform depth (FOUN-32, founderstar#496): a planned skip needs the platform review at the exact head', () => { + test('runtime code never fits the platform depth, even with no runtime lines (a registered generated output, a mode-only change)', () => { + const none = scanNumbers({}); + expect(platformFits('code', ['dist/app.js'], 0, none)).toBe(false); + expect(platformFits('gate', ['scripts/start.sh'], 0, none)).toBe(false); + expect(platformFits('docs', ['docs/a.md'], 0, none)).toBe(true); + expect(platformFits('tests', ['test/a.test.ts'], 0, none)).toBe(true); + }); + test('verify marks an outside-voice diff file changed after render as a mismatch', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-')); + const ledger = path.join(dir, 'ledger.jsonl'), brief = path.join(dir, 'brief.md'), diff = path.join(dir, 'outside-voice.diff'); + fs.writeFileSync(brief, 'brief'); fs.writeFileSync(diff, '+code\n'); + const sha = (s: string) => createHash('sha256').update(s).digest('hex'); + appendEvent(ledger, { event: 'rendered', reader: 'outside-voice', role: 'outside-voice', file: brief, sha256: sha('brief'), payload: { file: diff, sha256: sha('+code\n') } } as any); + expect(verify(ledger).rows.find(r => r.kind === 'BRIEF')!.detail).not.toContain('payload_changed_since_render'); + fs.writeFileSync(diff, '+docs only\n'); + const row = verify(ledger).rows.find(r => r.kind === 'BRIEF')!; + expect([row.status, row.detail.includes('payload_changed_since_render')]).toEqual(['MISMATCH', true]); + }); const on = (review: ReviewShape['platformReview']) => lean({ platform: 'on', platformReview: review }); const docs = { paths: ['docs/howto-install.md', 'test/a.test.ts'] }; test('platform on with the review at HEAD on a docs or tests diff: no reader, a planned SKIP for every reader, the core read included', () => { diff --git a/test/review-packets-removed.test.ts b/test/review-packets-removed.test.ts index ed87bdfdc..e7da69017 100644 --- a/test/review-packets-removed.test.ts +++ b/test/review-packets-removed.test.ts @@ -95,8 +95,8 @@ const REVIEWER_BRIEFS: Record = { 'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' }, 'review/specialists/targeted.md': { sha: '7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5', replay: 'GST-257' }, 'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' }, - 'lib/review-depth.ts': { sha: '7a5ca1f98318902c5ebd3e537f37b7cef05689232ff307a429923397d60abdac', replay: 'GSTA-258-C' }, - 'lib/review-risk.ts': { sha: '2c5b96b921b8be5b977b734b3787361ed80e7d3f779ff6c4a20948f85e4d4474', replay: 'GSTA-258-C' }, + 'lib/review-depth.ts': { sha: 'aec55c5d2f834ad7d25fb0d9f7dba239f45d01361ad3fddc8bf54597d09bdfaa', replay: 'GSTA-258-C' }, + 'lib/review-risk.ts': { sha: '0c840724b6b8f09a3d7daae3eeef15424f2033ed90f4f7890e4a88d248eddb57', replay: 'GSTA-258-C' }, }; /** diff --git a/test/ship-receipt-platform.test.ts b/test/ship-receipt-platform.test.ts index cea35ef70..cb13c29a3 100644 --- a/test/ship-receipt-platform.test.ts +++ b/test/ship-receipt-platform.test.ts @@ -74,6 +74,7 @@ describe('C6 receipt: review_depth platform, honest and bound to the exact head expect(withGh(head, () => platformReceiptDepth(dir, 'main', d, head, '99'), 'APPROVED').ok).toBe(true); expect(withGh(head, () => platformReviewAt(dir, head), 'CHANGES_REQUESTED').ok).toBe(false); expect(withGh(head, () => platformReviewAt(dir, head))).toMatchObject({ ok: true, review: { id: '99' } }); + expect(withGh(head, () => platformReviewAt(dir, head, { id: '98' })).ok).toBe(false); }); test('the receipt records the risk_paths class and the panel mode the base policy gives', () => { const { dir, head } = repo({ review: { risk_paths: ['src/balance/**'] } }, { 'src/balance/x.ts': 'export const x = 1;\n' });