From 1421c641e8a8ab41d5e2dc1cecad537759017b26 Mon Sep 17 00:00:00 2001 From: garrytan Date: Wed, 30 Sep 2026 20:04:19 +0000 Subject: [PATCH] feat(qa-evidence): refuse evidence observed on an older input snapshot than the latest capture When native probe output declares a top-level input snapshot, materialize compares each evidence row with the latest capture's snapshot and refuses stale rows unless they are classified superseded, naming the captures to rerun. ship-exploratory-late-input kept reporting a pre-change adverse probe green after the input changed. --- lib/qa-evidence.ts | 8 ++++++++ test/qa-evidence.test.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/qa-evidence.ts b/lib/qa-evidence.ts index 80d8a2f9c..9e92142eb 100644 --- a/lib/qa-evidence.ts +++ b/lib/qa-evidence.ts @@ -264,6 +264,14 @@ function materialize(root: string, source: string) { const captured = readQaCapture(root, row.capture); return { command: row.command, contract: row.contract, expected: row.expected, classification: row.classification, observed: captured.observed }; }); + const snapshotOf = (observed: unknown) => object(observed) && typeof observed.snapshot === 'string' ? observed.snapshot : undefined; + const latestCapture = latestCompleteCapture(root); + const currentSnapshot = latestCapture ? snapshotOf(readQaCapture(root, latestCapture).observed) : undefined; + const superseded = currentSnapshot === undefined ? [] : annotations.evidence.filter((row: any, index: number) => { + const snapshot = snapshotOf(evidence[index].observed); + return snapshot !== undefined && snapshot !== currentSnapshot && row.classification !== 'superseded'; + }).map((row: any) => row.capture); + if (superseded.length) throw new QaEvidenceError(`Superseded evidence: capture ${superseded.join(', ')} observed an older input snapshot than the latest capture ${latestCapture}; rerun the affected probe on current inputs, or classify the row "superseded" and keep its contract open`); const missing = completeCaptures(root).filter(capture => !captures.has(capture) && !annotations.limits.some((limit: string) => new RegExp(`\\b${capture}\\b`).test(limit))); if (missing.length) throw new QaEvidenceError(`Invalid report annotations: add an evidence row for capture ${missing.join(', ')} (every complete capture needs one, or name it in limits with why it is withheld)`); diff --git a/test/qa-evidence.test.ts b/test/qa-evidence.test.ts index b496383a1..0c8f1f1a0 100644 --- a/test/qa-evidence.test.ts +++ b/test/qa-evidence.test.ts @@ -315,3 +315,21 @@ test('both QA helpers answer --help with usage and exit 0, and the declared inte expect(qaCommandAllowed('bun bin/gstack-qa-evidence --version')).toBe(false); expect(qaCallerCommandAllowed('bun /abs/host/runtime/bin/gstack-qa-evidence --help')).toBe(true); }); + +test('materialize refuses evidence whose declared input snapshot predates the latest capture', () => { + const f = fixture(); + const command = (id: string, input: string) => `bun gstack-qa-evidence capture ${f.root} ${id} --timeout-ms 4000 -- probe ${input}`; + expect(f.capture('001', `console.log(JSON.stringify({ snapshot: 'before', charter: 'adverse' }))`).status).toBe(0); + expect(f.run('checkpoint', f.root, '001', '001', command('001', 'adverse'), 'The input changed, so the happy path must be rechecked on current inputs next.', command('002', 'happy')).status).toBe(0); + expect(f.capture('002', `console.log(JSON.stringify({ snapshot: 'after', charter: 'happy' }))`).status).toBe(0); + const rows = [ + { capture: '001', command: command('001', 'adverse'), contract: 'README.md', expected: 'rejects', classification: 'pass' }, + { capture: '002', command: command('002', 'happy'), contract: 'README.md', expected: 'doubles', classification: 'pass' }, + ]; + f.json('annotations.json', { revision: 'fixture-revision', limits: ['Adverse coverage predates the input change.'], evidence: rows }); + const stale = f.run('materialize', f.root, 'annotations.json'); + expect(stale.status).toBe(2); + expect(receipt(stale.stderr).message).toContain('capture 001 observed an older input snapshot than the latest capture 002'); + f.json('annotations.json', { revision: 'fixture-revision', limits: ['Adverse coverage predates the input change.'], evidence: [{ ...rows[0], classification: 'superseded' }, rows[1]] }); + expect(f.run('materialize', f.root, 'annotations.json').status).toBe(0); +});