Files
gstack/test/helpers/qa-evidence-producer.ts
T
Garry Tan dcaea52800 v1.91.7.0 feat: add functional QA and pre-publication docs checks (#2983)
* feat: add surface-aware exploratory QA and ship documentation gates

* test: preserve delegated QA setup authority after main integration

* fix(qa): clarify exploration order and preserve report artifacts

* test(qa): follow the shared setup reference directly

* refactor(ship): make verification and recovery routes explicit

* test(ship): align evidence and review guards with explicit routes

* fix(workflows): clarify ship recovery and functional QA evidence

* fix(workflows): clarify approval recovery and full QA coverage

* refactor(workflows): order review transactions and clarify ship state

* fix(ship): clarify final verification and fail closed at publication

* fix(evals): attribute native atomic documentation writes

* fix(ship): clarify recovery and documentation lifecycle guidance

* fix(test): preserve observed native placeholder styling in CI

* fix(codex): report watchdog timeouts without a process-exit race

* Checkpoint functional QA implementation and workflow validation repairs

* Fix documentation and shared-review fixture contracts

* docs: clarify judge reuse and evaluation supervision

* test: align review evidence and selected case contracts

* test: verify append-only documentation checkpoints and recovery

* fix: qualify QA workflows and CI validation repairs

* fix: launch shared-libs fixture scripts on Windows

* fix: qualify QA deadlines, fixture isolation, and shard cleanup

* fix: preserve qualified QA and cancellation repairs

* fix: enforce functional fixture authority and share strict event decoding

* fix: retain free-test evidence and explain recovery

* fix: reject malformed native evidence after decoder consolidation

* test: use reliable capture for telemetry privacy filters

* test: refresh measured quick coverage and document validation costs

* Fix native fixture receipts and preserve VM validation evidence

* Align negative judge controls with upstream clarity policy

* Fix report-only QA preparation and public evidence handling

* Clarify QA-only preparation and current-report preservation

* Stream Ship quality judgments with an explicit 64k response contract

* Validate compact judge reasoning locally with supported wire schema

* Align functional QA fixture instructions with evidence acceptance

* Bind native browser diagnostics to execution evidence and align review verdicts

* Preserve native diagnostic line boundaries

* Serialize functional QA evidence from native captures

* Keep large QA evidence fixture payload out of Windows argv
2026-09-29 06:07:35 -07:00

105 lines
6.3 KiB
TypeScript

import * as fs from 'node:fs';
import * as path from 'node:path';
import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { readQaCapture } from '../../lib/qa-evidence';
export const QA_EVIDENCE_RUNTIME = [
'bin/gstack-qa-evidence', 'bin/gstack-qa-deadline', 'lib/qa-evidence.ts', 'lib/qa-deadline.ts',
'lib/claude-code-windows-job.ts', 'lib/fs-atomic.ts', 'lib/redact-engine.ts', 'lib/redact-patterns.ts',
];
export function qaEvidenceRuntimeFiles(): Record<string, string> {
return Object.fromEntries(QA_EVIDENCE_RUNTIME.map(file => [file, fs.readFileSync(path.resolve(import.meta.dir, '../..', file), 'utf8')]));
}
export interface QaEvidenceContext {
cwd: string;
reportRoot: string;
executable: string;
}
export interface QaEvidenceCommand {
action: 'capture' | 'checkpoint' | 'materialize';
id?: string;
source?: string;
nativeCommand?: string;
argv?: string[];
deadline?: string;
timeoutMs?: number;
publicOutput?: boolean;
intent?: { capture: string; observationCommand: string; hypothesis: string; nextCommand: string };
}
export function qaEvidenceCommand(command: string, context?: QaEvidenceContext): QaEvidenceCommand | undefined {
if (!context || typeof command !== 'string' || /[\r\n]/.test(command)) return;
const matches = [...command.matchAll(/'[^'\r\n]*'|"[^"\\$`\r\n]*"|[^\s'"\\;&|<>`$(){}*?\[\]~#]+/g)];
if (!matches.length || command.slice(0, matches[0].index).trim() || command.slice(matches.at(-1)!.index! + matches.at(-1)![0].length).trim()) return;
for (let index = 1; index < matches.length; index++) {
if (!/^ +$/.test(command.slice(matches[index - 1].index! + matches[index - 1][0].length, matches[index].index))) return;
}
const tokens = matches.map(match => /^['"]/.test(match[0]) ? match[0].slice(1, -1) : match[0]);
if ([tokens[1], tokens[3]].some(value => value?.split(/[\\/]/).includes('..'))) return;
if (tokens[0] !== 'bun' || path.resolve(context.cwd, tokens[1] ?? '') !== context.executable
|| path.resolve(context.cwd, tokens[3] ?? '') !== context.reportRoot) return;
const source = (value: string | undefined) => value && !value.split(/[\\/]/).includes('..')
&& path.resolve(context.reportRoot, value).startsWith(context.reportRoot + path.sep) ? path.resolve(context.reportRoot, value) : undefined;
if (tokens[2] === 'materialize' && tokens.length === 5 && source(tokens[4])) return { action: 'materialize', source: source(tokens[4]) };
if (!/^\d{3}$/.test(tokens[4] ?? '')) return;
if (tokens[2] === 'checkpoint' && tokens.length === 6 && source(tokens[5])) return { action: 'checkpoint', id: tokens[4], source: source(tokens[5]) };
if (tokens[2] === 'checkpoint' && tokens.length === 9 && /^\d{3}$/.test(tokens[5])) return { action: 'checkpoint', id: tokens[4],
intent: { capture: tokens[5], observationCommand: tokens[6], hypothesis: tokens[7], nextCommand: tokens[8] } };
const publicOutput = tokens[5] === '--public';
const option = publicOutput ? 6 : 5;
if (tokens[2] !== 'capture' || tokens[option + 2] !== '--' || tokens.length < option + 4) return;
const deadline = tokens[option] === '--deadline' ? source(path.resolve(context.cwd, tokens[option + 1])) : undefined;
if (tokens[option] === '--deadline' && !deadline) return;
if (tokens[option] === '--timeout-ms' && (!/^[1-9]\d*$/.test(tokens[option + 1]) || Number(tokens[option + 1]) > 2_147_483_647)) return;
if (!['--deadline', '--timeout-ms'].includes(tokens[option])) return;
return { action: 'capture', id: tokens[4], publicOutput, argv: tokens.slice(option + 3), nativeCommand: command.slice(matches[option + 3].index).trim(),
...(tokens[option] === '--deadline' ? { deadline } : { timeoutMs: Number(tokens[option + 1]) }) };
}
export type QaProducerCall = { name: string; input: Record<string, any>; output: string; failed: boolean; start: number; end: number };
export function qaProducerReceipt(call: QaProducerCall, context?: QaEvidenceContext, status: 'complete' | 'incomplete' = 'complete') {
if (call.name !== 'Bash' || call.end <= call.start) return;
const command = qaEvidenceCommand(call.input.command, context);
if (!command) return;
try {
const lines = call.output.split('\n').filter(line => line.startsWith('QA_EVIDENCE '));
if (lines.length !== 1) return;
const receipt = JSON.parse(lines[0].slice('QA_EVIDENCE '.length));
const exitCode = call.failed ? Number(/^Exit code (\d+)\n/.exec(call.output)?.[1]) : 0;
if (receipt.producer !== 'gstack-qa-evidence' || receipt.version !== 1 || receipt.action !== command.action
|| receipt.status !== status || !/^[a-f0-9]{64}$/.test(receipt.sha256)
|| receipt.exitCode !== exitCode
|| (command.id !== undefined && receipt.id !== command.id)
|| (call.failed && (command.action !== 'capture' || receipt.exitCode === 0))) return;
return { command, receipt };
} catch { return; }
}
export function qaNativeCapture(call: QaProducerCall, context?: QaEvidenceContext) {
const producer = qaProducerReceipt(call, context);
if (!context || producer?.command.action !== 'capture') return;
try {
const captured = readQaCapture(context.reportRoot, producer.command.id!, producer.receipt.sha256);
if (captured.receipt.cwd !== context.cwd || !isDeepStrictEqual(captured.receipt.argv, producer.command.argv)
|| captured.receipt.exitCode !== producer.receipt.exitCode || captured.receipt.signal !== producer.receipt.signal
|| captured.receipt.publicOutput !== producer.command.publicOutput || captured.receipt.publicOutput !== producer.receipt.publicOutput
|| (producer.command.deadline !== undefined && captured.receipt.deadline !== producer.command.deadline)) return;
if (producer.command.publicOutput && call.output.split('\n').filter(line => line === JSON.stringify(captured.observed)).length !== 1) return;
if (producer.command.timeoutMs !== undefined) {
const deadline = JSON.parse(fs.readFileSync(path.join(context.reportRoot, `.qa-evidence/${producer.command.id}/deadline.json`), 'utf8'));
if (captured.receipt.deadline !== path.join(context.reportRoot, `.qa-evidence/${producer.command.id}/deadline.json`)
|| deadline.budgetMs !== producer.command.timeoutMs) return;
}
return { ...producer, captured };
} catch { return; }
}
export function qaEvidenceHash(bytes: string): string {
return createHash('sha256').update(bytes).digest('hex');
}