feat(evals): red-head and READY-arrival replays for the gate-cost model (GSTA-49 T0)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-18 18:44:12 +00:00
1 parent e9dfcf64f0
commit b27bf7e10a
5 files changed
+911 -28

No files matched your search

+271 -12
View File
@@ -10,7 +10,12 @@
* bun run scripts/gate-cost-replay.ts --runs <id,id,...> [--since YYYY-MM-DD --until YYYY-MM-DD | --prs 3201,3202]
* [--ref origin/main] [--repo owner/name] [--cache .context/gate-cost-cache] [--window-days N]
* [--usd-per-second R]
* [--train-size 5] [--reps 400] [--seed 1] [--cases] [--json]
* [--train-size 5] [--reps 400] [--seed 1] [--cases] [--history] [--json]
*
* --history (needs --since/--until) adds the red-head replay and the
* READY-arrival train replay (lib/gate-cost-history.ts): it lists the
* window's E2E Evals and Free Tests runs and PRs, downloads report-verdict
* and trial-outcomes artifacts of red runs, and fetches PR heads by SHA.
*
* --runs names complete full-gate runs (108 rule cases + 24 judges). Bare
* dates are UTC days; --until is inclusive. Cases without a recorded cost are
@@ -23,14 +28,19 @@ import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
SCREEN_LABEL, TODAY_WEEKLY_USD, AFTER_GSTA48_WEEKLY_USD, caseCostTable, changedLinesByFile, fullGateUsd, isSkillMd,
parseEvalReport, parseTrialOutcomes, prSelectionCost, simulateTrains, summarizeGates, summarizeSelector,
parseEvalReport, parseTrialOutcomes, prSelectionCost, screenSelection, simulateTrains, summarizeGates, summarizeSelector, todaySelection,
type CaseCostEntry, type GateRecord, type GateSummary, type PrDiff, type PrSelectionCost, type ScreenPolicy,
type SelectionDeps, type SelectorSummary, type TrainModelResult,
} from '../lib/gate-cost';
import { GH_HISTORY, downloadRunArtifacts, repoSlug } from './lib/ci-history';
import {
STAMP_FILES, classifyReds, headGreenAt, latencySummary, parseReportVerdict, prForRun, readyTimes, screenHit, simulateArrivalTrains,
stampFreeSignature, summarizeRedHeads, SCREEN_CANARY_CASES,
type ArrivalTrainResult, type CheckRun, type GatedRun, type HeadDiff, type LatencySummary, type ReadyRow, type RedHeadRow, type RedHeadSummary, type RedRun,
} from '../lib/gate-cost-history';
import { GH_CLIENT, GH_HISTORY, downloadRunArtifacts, repoSlug } from './lib/ci-history';
import { computePaidCaseSelection } from './lib/paid-select';
import { packageChangeOnlyVersion } from './test-pr-profile';
import { E2E_TIERS, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES, matchGlob } from '../test/helpers/touchfiles';
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES, matchGlob } from '../test/helpers/touchfiles';
const ROOT = path.resolve(import.meta.dir, '..');
const DAY_MS = 86_400_000;
@@ -55,12 +65,13 @@ export interface ReplayArgs {
reps: number;
seed: number;
cases: boolean;
history: boolean;
json: boolean;
}
export function parseArgs(argv: string[]): ReplayArgs {
const args: ReplayArgs = { runs: [], prs: [], ref: 'origin/main', repo: '', cache: path.join(ROOT, '.context', 'gate-cost-cache'),
trainSize: 5, reps: 400, seed: 1, cases: false, json: false };
trainSize: 5, reps: 400, seed: 1, cases: false, history: false, json: false };
const list = (value: string) => value.split(/[\s,]+/).filter(Boolean);
const positive = (flag: string, value: string) => {
const n = Number(value);
@@ -71,6 +82,7 @@ export function parseArgs(argv: string[]): ReplayArgs {
const flag = argv[i]!;
if (flag === '--json') { args.json = true; continue; }
if (flag === '--cases') { args.cases = true; continue; }
if (flag === '--history') { args.history = true; continue; }
const value = argv[i + 1];
if (value === undefined || value.startsWith('--')) throw new Error(`${flag} needs a value`);
i += 1;
@@ -93,6 +105,7 @@ export function parseArgs(argv: string[]): ReplayArgs {
if (args.prs.some(pr => !Number.isInteger(pr) || pr <= 0)) throw new Error('--prs takes PR numbers');
if (args.prs.length && (args.since || args.until)) throw new Error('use either --prs or --since/--until, not both');
if (!!args.since !== !!args.until) throw new Error('--since and --until go together');
if (args.history && !args.since) throw new Error('--history needs a window: --since and --until');
return args;
}
@@ -184,20 +197,23 @@ function mergedPrs(args: ReplayArgs): { prs: MergedPr[]; windowDays: number | nu
return { prs: args.prs.map(pr => all.get(pr)!), windowDays: args.windowDays ?? null };
}
function prDiff(merge: MergedPr): PrDiff {
const files = git(['diff', '--name-only', '--no-renames', `${merge.sha}^`, merge.sha]).split('\n').filter(Boolean);
/** The PR diff the selectors see between two revisions. */
function revisionDiff(pr: number, from: string, to: string): PrDiff {
const files = git(['diff', '--name-only', '--no-renames', from, to]).split('\n').filter(Boolean);
let packageVersionOnly = false;
if (files.includes('package.json')) {
const show = (rev: string) => spawnSync('git', ['show', `${rev}:package.json`], { cwd: ROOT, encoding: 'utf8', timeout: 30_000 });
const before = show(`${merge.sha}^`), after = show(merge.sha);
const before = show(from), after = show(to);
packageVersionOnly = before.status === 0 && after.status === 0 && packageChangeOnlyVersion(before.stdout, after.stdout);
}
const skillMds = files.filter(isSkillMd);
const skillMdChangedLines = skillMds.length >= 10
? changedLinesByFile(git(['diff', '-U0', '--no-renames', `${merge.sha}^`, merge.sha, '--', ...skillMds], 120_000)) : {};
return { pr: merge.pr, sha: merge.sha.slice(0, 8), files, packageVersionOnly, skillMdChangedLines };
? changedLinesByFile(git(['diff', '-U0', '--no-renames', from, to, '--', ...skillMds], 120_000)) : {};
return { pr, sha: to.slice(0, 8), files, packageVersionOnly, skillMdChangedLines };
}
const prDiff = (merge: MergedPr): PrDiff => revisionDiff(merge.pr, `${merge.sha}^`, merge.sha);
/** The selectors in this checkout: today's PR profile (with R4's shadow) and the plain touchfile selection. */
export function checkoutSelectionDeps(rootDir = ROOT): SelectionDeps {
const gateCases = Object.keys(E2E_TIERS).filter(id => E2E_TIERS[id] === 'gate');
@@ -236,6 +252,7 @@ export interface ReplayReport {
prs: PrSelectionCost[];
selection: { today: SelectorSummary; shadow: SelectorSummary; screenProvisional: SelectorSummary } | null;
model: { screenScale: number; fullGateUsd: number; trainSize: number; reps: number; seed: number; rows: TrainModelResult[] } | null;
history?: HistoryReport;
}
export function buildReport(args: ReplayArgs, gates: GateRecord[], merges: ReturnType<typeof mergedPrs>, deps: SelectionDeps,
@@ -333,6 +350,244 @@ export function formatReport(report: ReplayReport, options: { cases?: boolean }
return out.join('\n');
}
// --- History (--history): red-head replay and READY-arrival trains ---
const REPORT_ARTIFACT = /^report-verdict-a\d+$/;
const TRIALS_ARTIFACT = /^trial-outcomes[A-Za-z0-9_-]*?-a\d+$/;
const TRAIN_POLICIES: ScreenPolicy[] = [{ kind: 'all' }, { kind: 'random', fraction: 0.5 }, { kind: 'none' }];
const GH_RUN_PAGE = 100;
const HISTORY_GIT_TIMEOUT_MS = 120_000;
const FETCH_TIMEOUT_MS = 300_000;
interface HistoryRun { id: number; sha: string; branch: string; createdAt: string; updatedAt: string; conclusion: string | null; prNumbers: number[] }
interface PullRow { number: number; ref: string; head: string; createdAt: string; mergedAt: string | null; closedAt: string | null }
type Merges = ReturnType<typeof mergedPrs>;
export interface HistoryReport {
window: { since: string; until: string };
runs: { evals: number; gated: number; red: number; free: number; unmapped: number };
redRuns: RedRun[];
redHeads: RedHeadRow[];
redSummary: RedHeadSummary;
perPr: Array<{ pr: number; paidRelevant: boolean; prCausedRed: boolean; screenedMiss: boolean }>;
ready: ReadyRow[];
baseline: LatencySummary & { gatesAfterReady: number; sources: Record<string, number> };
trainParams: { trainSize: number; fillLimitHours: number; gateMinutes: number; bisectMinutes: number; redScreened: number; redUnscreened: number; reps: number; seed: number };
trains: ArrivalTrainResult[];
}
function tryGit(args: string[], timeout = HISTORY_GIT_TIMEOUT_MS): string | null {
const result = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8', timeout, maxBuffer: 256 * 1024 * 1024 });
return result.status === 0 ? result.stdout : null;
}
function cachedJson<T>(file: string, load: () => T): T {
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8')) as T;
const value = load();
fs.writeFileSync(file, JSON.stringify(value));
return value;
}
/** Completed runs of one workflow, one UTC day per query: GitHub caps a filtered run listing at 1,000 results. */
function listRunsByDay(repo: string, workflow: string, since: Date, until: Date, event?: string): HistoryRun[] {
const out: HistoryRun[] = [];
for (let day = new Date(since.toISOString().slice(0, 10)); day < until; day = new Date(day.getTime() + DAY_MS)) {
const date = day.toISOString().slice(0, 10);
for (let page = 1; page <= 10; page += 1) {
const query = [`status=completed`, `per_page=${GH_RUN_PAGE}`, `page=${page}`, `created=${date}..${date}`, ...(event ? [`event=${event}`] : [])];
const body = GH_CLIENT.getJson(`repos/${repo}/actions/workflows/${workflow}/runs?${query.join('&')}`) as { workflow_runs?: any[] };
const runs = Array.isArray(body?.workflow_runs) ? body.workflow_runs : [];
out.push(...runs.map(run => ({ id: Number(run.id), sha: String(run.head_sha ?? ''), branch: String(run.head_branch ?? ''), createdAt: String(run.created_at ?? ''),
updatedAt: String(run.updated_at ?? ''), conclusion: run.conclusion ?? null,
prNumbers: Array.isArray(run.pull_requests) ? run.pull_requests.map((pr: any) => Number(pr.number)).filter(Number.isFinite) : [] })));
if (runs.length < GH_RUN_PAGE) break;
}
}
return out.filter(run => run.createdAt >= since.toISOString() && run.createdAt < until.toISOString());
}
function listPulls(repo: string, since: Date): PullRow[] {
const out: PullRow[] = [];
for (let page = 1; page <= 30; page += 1) {
const body = GH_CLIENT.getJson(`repos/${repo}/pulls?state=all&sort=updated&direction=desc&per_page=100&page=${page}`) as any[];
if (!Array.isArray(body)) break;
out.push(...body.map(p => ({ number: Number(p.number), ref: String(p.head?.ref ?? ''), head: String(p.head?.sha ?? ''), createdAt: String(p.created_at ?? ''),
mergedAt: p.merged_at ?? null, closedAt: p.closed_at ?? null })));
if (body.length < 100 || String(body[body.length - 1]?.updated_at ?? '') < since.toISOString()) break;
}
return out;
}
/** Artifacts of one run matching a name pattern, downloaded once (a .history marker records that the run was listed). */
function runArtifactDirs(runId: number, repo: string, cache: string, match: RegExp): string[] {
const dir = path.join(cache, String(runId));
const marker = path.join(dir, `.history-${match.source.replace(/[^a-z]/gi, '')}`);
if (!fs.existsSync(marker)) {
downloadRunArtifacts({ repo, run: { id: runId }, cacheDir: cache, match: name => match.test(name), maxBytes: 16 * 1024 * 1024, fetcher: GH_HISTORY });
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(marker, '');
}
return fs.readdirSync(dir).filter(name => match.test(name)).sort((a, b) => Number(a.split('-a').pop()) - Number(b.split('-a').pop())).map(name => path.join(dir, name));
}
/** The run's report verdict over its attempts: the last attempt's verdict, every attempt's failures; null when no report exists. */
function runVerdict(runId: number, repo: string, cache: string): { green: boolean; failures: GatedRun['failures'] } | null {
const reports = runArtifactDirs(runId, repo, cache, REPORT_ARTIFACT).map(dir => path.join(dir, 'report.txt')).filter(file => fs.existsSync(file))
.map(file => parseReportVerdict(fs.readFileSync(file, 'utf8')));
if (!reports.length) return null;
return { green: reports[reports.length - 1]!.verdict === 'GREEN', failures: reports.flatMap(r => r.failures) };
}
/** The tested merge commit of a run, from its trial outcomes. */
function testedMergeSha(runId: number, repo: string, cache: string): string | null {
for (const dir of runArtifactDirs(runId, repo, cache, TRIALS_ARTIFACT)) {
const file = path.join(dir, 'trial-outcomes.jsonl');
const first = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').find(Boolean) : undefined;
try { const sha = first ? JSON.parse(first).sha : null; if (typeof sha === 'string' && /^[0-9a-f]{40}$/.test(sha)) return sha; } catch { /* torn line */ }
}
return null;
}
/** Fetch commits this clone lacks by SHA (GitHub serves PR heads and merge refs that no branch holds anymore). */
function ensureCommits(shas: string[]): void {
const missing = [...new Set(shas)].filter(sha => tryGit(['cat-file', '-e', `${sha}^{commit}`], 30_000) === null);
for (let i = 0; i < missing.length; i += 20) spawnSync('git', ['fetch', '-q', 'origin', ...missing.slice(i, i + 20)], { cwd: ROOT, timeout: FETCH_TIMEOUT_MS });
}
function headDiff(sha: string, ref: string): HeadDiff | null {
const mergeBase = tryGit(['merge-base', ref, sha])?.trim();
if (!mergeBase) return null;
const files = tryGit(['diff', '--name-only', '--no-renames', mergeBase, sha]);
const diff = tryGit(['diff', '-U0', '--no-color', '--no-renames', '--no-ext-diff', mergeBase, sha, '--', '.', ...STAMP_FILES.map(file => `:(exclude)${file}`)]);
return files === null || diff === null ? null : { ownSig: stampFreeSignature(diff), files: files.split('\n').filter(Boolean) };
}
/** The base a red run tested: its merge commit's first parent, else main's first-parent tip when the run was created. */
function testedBase(run: RedRun, args: ReplayArgs, repo: string): { base: string; source: RedHeadRow['baseSource'] } | null {
const merge = testedMergeSha(run.runId, repo, args.cache);
if (merge) {
ensureCommits([merge]);
const parents = (tryGit(['rev-list', '--parents', '-n1', merge]) ?? '').trim().split(' ').slice(1);
if (parents.length === 2 && parents[1] === run.head) return { base: parents[0]!, source: 'tested-merge' };
}
const tip = tryGit(['rev-list', '-1', '--first-parent', `--before=${run.createdAt}`, args.ref])?.trim();
return tip ? { base: tip, source: 'main-at-run' } : null;
}
function collectHistory(args: ReplayArgs, repo: string, merges: Merges, body: Omit<ReplayReport, 'repo' | 'ref' | 'head'>, deps: SelectionDeps, gates: GateRecord[]): HistoryReport {
const since = merges.since!, until = merges.until!;
const key = `${since.toISOString()}_${until.toISOString()}`.replace(/[^0-9TZ_]/g, '');
const cached = <T>(name: string, load: () => T) => cachedJson(path.join(args.cache, `history-${key}-${name}.json`), load);
const evals = cached('evals', () => listRunsByDay(repo, 'evals.yml', since, until));
const free = cached('free', () => listRunsByDay(repo, 'free-tests.yml', since, until, 'pull_request'));
const pulls = cached('pulls', () => listPulls(repo, since));
const prOf = new Map([...evals, ...free].map(run => [run.id, prForRun(run, pulls)]));
const gated: GatedRun[] = evals.flatMap(run => {
const pr = prOf.get(run.id) ?? null;
if (run.conclusion === 'success') return [{ id: run.id, pr, head: run.sha, createdAt: run.createdAt, green: true, failures: [] }];
const verdict = run.conclusion === 'failure' ? runVerdict(run.id, repo, args.cache) : null;
return verdict ? [{ id: run.id, pr, head: run.sha, createdAt: run.createdAt, ...verdict }] : [];
});
const mergedSet = new Set(merges.prs.map(m => m.pr));
const pullOf = new Map(pulls.map(p => [p.number, p]));
const checkRuns = [...evals.map(run => ({ run, workflow: 'evals' as const })), ...free.map(run => ({ run, workflow: 'free' as const }))]
.filter(({ run }) => mergedSet.has(prOf.get(run.id) ?? -1));
const heads = [...new Set([...gated.filter(r => r.pr !== null).map(r => r.head), ...checkRuns.map(({ run }) => run.sha),
...merges.prs.map(m => pullOf.get(m.pr)?.head ?? '')].filter(Boolean))];
ensureCommits(heads);
const diffs = cached('heads', () => Object.fromEntries(heads.map(sha => [sha, headDiff(sha, args.ref)])));
const headMap = new Map(Object.entries(diffs).filter((e): e is [string, HeadDiff] => e[1] !== null));
const caseFiles = new Map(gates.flatMap(g => g.trials.map(t => [t.caseId, t.file] as const)));
const caseFile = (id: string) => caseFiles.get(id) ?? null;
const inputsOf = (id: string) => /^test\/.+\.test\.ts$/.test(id)
? [id, ...[...caseFiles].filter(([, file]) => file === id).flatMap(([c]) => E2E_TOUCHFILES[c] ?? [])]
: [...(E2E_TOUCHFILES[id] ?? []), ...(caseFile(id) ? [caseFile(id)!] : [])];
const redRuns = classifyReds(gated, {
heads: headMap,
delta: (from, to) => tryGit(['diff', '--name-only', '--no-renames', from, to])?.split('\n').filter(Boolean) ?? null,
touches: (id, files) => files.filter(file => [...inputsOf(id), ...GLOBAL_TOUCHFILES].some(pattern => matchGlob(file, pattern))),
});
const redHeads: RedHeadRow[] = redRuns.filter(run => run.cause === 'pr').flatMap(run => {
const tested = testedBase(run, args, repo);
const mergeBase = tested && tryGit(['merge-base', tested.base, run.head])?.trim();
if (!tested || !mergeBase) return [];
const diff = revisionDiff(run.pr, mergeBase, run.head);
const selection = screenSelection(diff, deps, todaySelection(diff, deps));
return run.cases.filter(c => c.cause === 'pr').map(c => ({ caseId: c.caseId, kind: c.kind, pr: run.pr, runId: run.runId, head: run.head.slice(0, 8),
base: tested.base.slice(0, 8), baseSource: tested.source, files: diff.files.length, hit: screenHit(c, selection, caseFile), canaryReasons: selection.canaryReasons }));
});
const selected = new Map(body.prs.map(row => [row.pr, row]));
const merged = merges.prs.map(m => ({ pr: m.pr, paidRelevant: (selected.get(m.pr)?.today.usd ?? 0) > 0 }));
const redSummary = summarizeRedHeads(redHeads, redRuns, merged);
const ready = readyTimes(merges.prs.flatMap(m => {
const pull = pullOf.get(m.pr);
if (!pull?.mergedAt) return [];
const prHeads = [...new Set([...checkRuns.filter(({ run }) => prOf.get(run.id) === m.pr).map(({ run }) => run.sha), pull.head])];
return [{ pr: m.pr, mergedAt: pull.mergedAt, finalHead: pull.head, heads: prHeads.map(sha => ({ sha, ownSig: headMap.get(sha)?.ownSig ?? null })),
gatedRuns: gated.filter(r => r.pr === m.pr).map(r => r.createdAt) }];
}), headGreenAt(checkRuns.map(({ run, workflow }): CheckRun => ({ head: run.sha, workflow, createdAt: run.createdAt, completedAt: run.updatedAt, conclusion: run.conclusion }))));
const withReady = ready.filter(row => row.ready !== null);
const sources: Record<string, number> = {};
for (const row of ready) sources[row.source] = (sources[row.source] ?? 0) + 1;
const scale = body.model?.screenScale ?? 1;
const arrivals = withReady.map(row => ({ pr: row.pr, readyMs: Date.parse(row.ready!), paidRelevant: (selected.get(row.pr)?.today.usd ?? 0) > 0,
screenUsd: (selected.get(row.pr)?.screenProvisional.usd ?? 0) * scale }));
const trainParams = { trainSize: args.trainSize, fillLimitHours: 3, gateMinutes: 45, bisectMinutes: 45, redScreened: 0.06, redUnscreened: 0.25, reps: args.reps, seed: args.seed };
return {
window: { since: since.toISOString(), until: until.toISOString() },
runs: { evals: evals.length, gated: gated.length, red: gated.filter(r => !r.green).length, free: free.length, unmapped: evals.filter(run => prOf.get(run.id) === null).length },
redRuns, redHeads, redSummary,
perPr: merged.map(m => ({ ...m, prCausedRed: redSummary.prsWithPrRed.includes(m.pr), screenedMiss: redSummary.missedPrs.includes(m.pr) })),
ready, baseline: { ...latencySummary(withReady.map(row => row.readyToMergeHours!)), gatesAfterReady: withReady.reduce((sum, row) => sum + row.gatesAfterReady, 0), sources },
trainParams,
trains: TRAIN_POLICIES.flatMap(policy => MODEL_OUTSIDE_RED.map(outsideRed => simulateArrivalTrains({ ...trainParams, arrivals, policy, outsideRed, windowDays: merges.windowDays! }))),
};
}
export function formatHistory(h: HistoryReport): string {
const s = h.redSummary;
const label = SCREEN_LABEL;
const share = (n: number, d: number) => `${n} of ${d} (${pct(n, d)})`;
const causes = (cause: string) => h.redRuns.filter(run => run.cause === cause).length;
const out = [`History replays, E2E Evals and Free Tests runs created ${h.window.since} to ${h.window.until}:`,
` E2E Evals runs ${h.runs.evals} (${h.runs.unmapped} not mapped to a PR), gated ${h.runs.gated}, red ${h.runs.red}: PR-caused ${causes('pr')}, outside ${causes('outside')}, unknown ${causes('unknown')}; Free Tests PR runs ${h.runs.free}`,
' Classification (GSTA-47): per failed case, first match wins: transcript diagnosis; red in another PR within 72 h -> outside;',
' no later gated run -> unknown; next gated run has the same own diff -> outside; the push to it changed the case\'s inputs',
' (touchfiles, test file, harness globals) among either head\'s own files -> PR-caused; else outside. A run with any PR-caused case is PR-caused.',
'', `Red-head replay (${label} on the failing head's diff from its tested base; canary set fixed before the replay: ${Object.values(SCREEN_CANARY_CASES).join(', ')}):`,
` ${pad(['PR', 'run', 'head', 'base', 'files', 'case', 'kind', `${label} hit`], [6, 12, 9, 22, 5, 46, 6])}`];
for (const row of h.redHeads) {
out.push(` ${pad([`#${row.pr}`, row.runId, row.head, `${row.base} ${row.baseSource}`, row.files, row.caseId, row.kind, row.hit ?? 'MISSED'], [6, 12, 9, 22, 5, 46, 6])}`);
}
out.push(` ${label} hits by kind: ${Object.entries(s.byHit).map(([k, n]) => `${k} ${n}`).join(', ') || 'none'}`,
` ${label} misses: ${share(s.missedPrs.length, s.prsWithPrRed.length)} merged PRs with a PR-caused red (${s.missedCases} of ${s.prCausedCases} case results)${s.missedPrs.length ? `: #${s.missedPrs.join(', #')}` : ''}`,
` ${label} misses with an empty canary set: ${s.missedPrsWithoutCanary.length}${s.missedPrsWithoutCanary.length ? ` (#${s.missedPrsWithoutCanary.join(', #')})` : ''}`,
` P-i (${label} misses over paid-relevant merged PRs): ${share(s.missedPrs.length, s.paidRelevantPrs)}`,
` P-k (merged PRs with any PR-caused red run): ${share(s.prsWithPrRed.length, s.mergedPrs)}${s.prsWithPrRed.length ? `: #${s.prsWithPrRed.join(', #')}` : ''}; ${s.prCausedRuns} PR-caused red runs`,
` PR-caused red runs on PRs that did not merge in the window (not counted): ${s.otherPrCausedRuns}`,
'', `Per merged PR (paid-relevant: today's selector picks a case on the squash; red: any PR-caused red run; miss: a ${label} miss):`);
for (let i = 0; i < h.perPr.length; i += 6) {
out.push(` ${h.perPr.slice(i, i + 6).map(p => `#${p.pr} ${p.paidRelevant ? 'paid' : '----'} ${p.prCausedRed ? 'red ' : '--- '}${p.screenedMiss ? 'miss' : '----'}`).join(' ')}`);
}
const b = h.baseline;
const hrs = (v: number) => v.toFixed(1);
const p = h.trainParams;
out.push('', 'READY-to-merge. READY = when the merged own diff (modulo main merges and stamps) first had green Free Tests and E2E Evals on a',
' head, before the merge (fallback: the final head\'s green time; READY reports are not machine-readable).',
` READY signal: ${Object.entries(b.sources).map(([k, n]) => `${k} ${n}`).join(', ')}`,
` today (one PR per gate): ${b.prs} PRs, READY-to-merge p50 ${hrs(b.p50Hours)} h, p90 ${hrs(b.p90Hours)} h, mean ${hrs(b.meanHours)} h; ${b.gatesAfterReady} gated E2E Evals runs after READY`,
` trains (${p.reps} reps, seed ${p.seed}; size ${p.trainSize}, fill limit ${p.fillLimitHours} h, screened first, unscreened alone when ${p.trainSize}+ wait; ${p.gateMinutes} min a gate,`,
` ${p.bisectMinutes} min a bisect, one train gating at a time; defects ${p.redScreened * 100}% screened / ${p.redUnscreened * 100}% unscreened; a dropped PR rejoins at once):`,
` ${pad(['policy', 'o', 'trains/w', 'occupancy', 'full', 'sizes 1..k per window', 'gates/w', 'drop+gate/w', 'rebld/w', 'stalls/w', 'p50 h', 'p90 h'], [10, 5, 8, 9, 5, 22, 7, 11, 7, 8, 6])} mean h`);
for (const r of h.trains) {
out.push(` ${pad([r.policy, r.outsideRed.toFixed(2), r.trainsWeekly.toFixed(2), r.meanOccupancy.toFixed(2), pct(r.fullTrainShare, 1), r.occupancy.map(n => n.toFixed(1)).join('/'),
r.gateAttemptsWeekly.toFixed(2), r.attributedRebuildsWeekly.toFixed(2), r.rebuildOnceWeekly.toFixed(2), r.stallsWeekly.toFixed(2), hrs(r.readyToMerge.p50Hours), hrs(r.readyToMerge.p90Hours)],
[10, 5, 8, 9, 5, 22, 7, 11, 7, 8, 6])} ${hrs(r.readyToMerge.meanHours)}`);
}
out.push(` "screened" follows the ${label} policy column; PRs with no paid case ride as screened.`);
return out.join('\n');
}
export function main(argv: string[]): number {
let args: ReplayArgs;
try { args = parseArgs(argv); } catch (error) {
@@ -346,8 +601,12 @@ export function main(argv: string[]): number {
const empty = gates.filter(g => g.trials.length === 0);
if (empty.length) throw new Error(`run(s) ${empty.map(g => g.runId).join(', ')} have no trial outcomes (expired artifacts, or not a paid gate run)`);
const merges = mergedPrs(args);
const report: ReplayReport = { repo, ref: args.ref, head: git(['rev-parse', args.ref]).trim(), ...buildReport(args, gates, merges, checkoutSelectionDeps()) };
console.log(args.json ? JSON.stringify(report, null, 2) : formatReport(report, { cases: args.cases }));
const deps = checkoutSelectionDeps();
const body = buildReport(args, gates, merges, deps);
const report: ReplayReport = { repo, ref: args.ref, head: git(['rev-parse', args.ref]).trim(), ...body,
...(args.history ? { history: collectHistory(args, repo, merges, body, deps, gates) } : {}) };
const text = formatReport(report, { cases: args.cases }) + (report.history ? `\n\n${formatHistory(report.history)}` : '');
console.log(args.json ? JSON.stringify(report, null, 2) : text);
return 0;
} catch (error) {
console.error(`gate-cost-replay: ${(error as Error).message}`);