mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* refactor(eval-history): share pass-rate history loading as loadPassRateHistory() eval:pass-rates' main block loaded local dirs or the weekly trial-outcomes artifacts inline. Move that into an exported function returning records, scope and fetch errors, so the off-ship sweep reads the same history the same way. Behavior unchanged. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ship-measure): the measurement bar and the weekly off-ship sweep CLI Decide each diagnostic measurement with one bar (scripts/lib/measure-bar.ts): MEETS at 9/10; MEETS-qualified at 8/10 when every red is a mechanical provider error, judge noise or a cited model miss; EXTEND at 7/10 (or an unqualified 8/10) for one more batch on identical inputs, decided once on the pooled 20; BELOW otherwise and for any timeout, hang, regression or fixable cause. Behavior uses 12 trials (11/10/9; 22/20 of 24); judge counts median-panel outputs. A batch with more than 30% provider-evidence failures is void and redispatched once. Below-strict reds nobody classified return needs-classify. ship-measure gains classify (append-only records; provider refused without evidence), decide and extend (refuses a changed identity, names the field, never a third batch), exit codes 5 and 6, and a pooling identity (frozen tree, runner, CLI, model, CI image, Bun, policy and harness versions). ship-measure sweep ranks main's gate cases by their cost to the all-green probability from eval:pass-rates history, measures the top K with the same runner and bar under a weekly cap shared with the last 7 days of sweep reports (ship_measure_sweep_cases, ship_measure_sweep_budget_usd), and writes a report with the predicted all-green probability before and after. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ship): the measure loop decides by the bar's exit codes Step 4 branches on MEETS/MEETS-qualified, needs-classify (with the qualifying and fix-round classes), EXTEND once and BELOW; the rest of the loop prose is tightened to offset most of the growth. ship's union size cap moves from 1.495 to 1.4975 (measured 281,058 / 187,706 = 1.4973). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci: weekly eval-sweep workflow on main Mondays 12:00 UTC and on dispatch (k, cap_usd, dry_run): runs ship-measure sweep in the published CI image by digest with the census's paid-eval setup, downloads the last 7 days of sweep reports first so the weekly cap holds across runs, uploads the report and captures, and writes the job summary. Read-only grants; it never pushes or opens a pull request. One sweep at a time repository-wide, never cancelled mid-spend. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: the measurement bar table and the weekly sweep Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: v1.91.33.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation" This reverts commitf4c58f2869. * ci: classify eval-sweep.yml, the sweep CLI and actionlint labels as free-only PR inputs None is read by a PR-gate case, so a change to them selects no paid case instead of restoring the full gate (test/free-fixtures.test.ts workflow inventory). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-measure): the selection preflight accepts name-mode cases A case registered beside others in its file lists its trial shard as <file>#<id>~t1, not <file> (trial 1), so the preflight refused every name-mode case (for example ship-exploratory-late-input) before any paid trial. Accept both forms. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "ci(temp): run eval-sweep on push to this branch (K=2, $25) for end-to-end validation" This reverts commit9619734307. * test: record the test-pr-profile.ts change as non-behavioral for HARNESS_VERSION Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): the name-mode preflight fix Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-measure): a partly funded sweep row shows the trials that ran Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(sweep): the fake single-case command is a Bun script, so it runs on Windows The Windows free lane runs test/ship-measure-sweep.test.ts; CreateProcess cannot exec a shebang shell stub, so every fake trial errored and the cap stopped the sweep after one trial. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(plan-mode-no-op): the CEO sub-test reviews a fixed pasted plan Without a plan, plan-ceo-review reviewed the checkout's branch diff, so its pre-question work (audit and landscape searches) grew with whatever PR the gate ran on. On this PR it timed out at 300s in 3 of 10 trials while still researching the diff (ship-measure baseline 7/10). With a fixed small plan, the same way the devex sub-test already runs, it measured 10/10 (CEO sub-test 83-255s). No budget or threshold changed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-measure): each diagnostic trial gets its own EVALS_RUN_ID Evidence-retaining cases (functional QA, docs faults) refuse to start without EVALS_RUN_ID, which only CI sets, so measuring them locally ran 0/10 sessions. qa-functional-cli-report, red once in this PR's gate, measured 10/10 once the trials started ($11.13). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
697 lines
37 KiB
TypeScript
697 lines
37 KiB
TypeScript
#!/usr/bin/env bun
|
||
/**
|
||
* eval-pass-rates (alias: eval-flake-rank) — per-case trial pass rates.
|
||
*
|
||
* Reads trial records (one JSONL line per trial: case, kind, trial, outcome,
|
||
* exit_reason, duration, cost, model, CLI version, series identity, run id,
|
||
* sha, policy_version) from the last N completed `evals-periodic.yml` runs of
|
||
* the weekly history: scheduled runs on `main` plus `main` dispatches
|
||
* (`--branch <name>` reads one branch's runs for inspection). Trials from
|
||
* branch census runs in the same window pool into a series main has also
|
||
* run, never into a new one; weeks for quarantine expiry count main only,
|
||
* downloading only each run's small `trial-outcomes` artifact through `gh`,
|
||
* plus any local eval dirs, and
|
||
* prints per-case per-trial pass rates with 95% Wilson intervals.
|
||
*
|
||
* A series is one case under one input identity (EVAL_POLICY v2: the bytes
|
||
* the case owns plus HARNESS_VERSION, stamped by scripts/eval-trial-series.ts
|
||
* caseSeriesIdentitiesV2), grouped by model and CLI version, per
|
||
* policy_version. A new identity starts a new series; earlier
|
||
* series stay visible. Only trials of the current series under the reader's
|
||
* own EVAL_POLICY.version feed the labels and alarms: an older policy's
|
||
* trials are display-only, and a newer policy's trials (written by a later
|
||
* checkout) are ignored with a printed count, so a reader rolled back past a
|
||
* policy bump never pools records it cannot interpret. Legacy eval-store records (`--backfill`, `--dir`) are
|
||
* imported as pre-policy trials (first attempt only; a missing attempt means
|
||
* 1) and are display-only.
|
||
*
|
||
* Labels: INCONCLUSIVE (below the entry rule's minimum trials), BROKEN (latest run 0/n
|
||
* after a prior interval at or above the entry rate), FLAKY (failures and an
|
||
* interval straddling the entry rate), FAILING (interval below the entry
|
||
* rate), PASSING (otherwise).
|
||
*
|
||
* The weekly gate (`--gate`) exits non-zero with ACTION REQUIRED when a
|
||
* non-quarantined case meets the quarantine entry rule, a rule case behaves
|
||
* like a behavior case, a blocking case's current-identity rate is
|
||
* significantly below its previous identity (one-sided Fisher exact,
|
||
* Holm-controlled across cases), or a CASE_QUARANTINE entry has met its exit
|
||
* rule, expired, or pushed its tier over the cap. History that cannot be
|
||
* fetched fails the gate closed.
|
||
*
|
||
* Usage:
|
||
* bun run eval:pass-rates # last 10 weekly runs (main: scheduled + dispatched)
|
||
* bun run eval:pass-rates --case <id> --runs 20
|
||
* bun run eval:pass-rates --dir <path> # local eval dirs / downloaded artifacts (repeatable)
|
||
* bun run eval:pass-rates --backfill # also import legacy slice artifacts, labeled pre-policy
|
||
* bun run eval:pass-rates --json | --gate
|
||
* bun run eval:pass-rates --headroom | --reds | --run <id> # report views (scripts/lib/eval-history.ts)
|
||
* bun run eval:pass-rates --help # every flag, no network
|
||
*/
|
||
|
||
import * as fs from 'node:fs';
|
||
import * as path from 'node:path';
|
||
import { isPartialEval, isFinalizedEvalResultFile, evalEntryOutcome, failureClassOf, parseTrialOutcomes, sanitizeTrialError,
|
||
TRIAL_OUTCOME_SCHEMA, type EvalCaseKind, type EvalResult, type TrialOutcomeRecord } from '../test/helpers/eval-store';
|
||
import { flakeLedgerPath, type FlakeLedgerEntry } from './test-free-shards';
|
||
import { E2E_KINDS, E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
|
||
import { CASE_QUARANTINE, EVAL_POLICY } from '../test/helpers/periodic-exclude-data';
|
||
import { CASE_TEST_NAMES } from './test-paid-shards';
|
||
import { resolveStateRoot } from '../lib/state-root';
|
||
import { GH_JOBS, PASS_RATES_USAGE, criticalPath, formatCriticalPath, formatHeadroom, formatRedLedger, headroom, headroomAlarms, parsePassRatesArgs,
|
||
redLedger, triageRun, CENSUS_RED_GUIDE, type CriticalPath } from './lib/eval-history';
|
||
import { downloadRunArtifacts, GH_HISTORY, isPooledTrialRun, isWeeklyHistoryRun, listWeeklyRuns, parseFlakeLedger, repoSlug, TRIAL_OUTCOMES_MAX_BYTES, type WeeklyRun } from './lib/ci-history';
|
||
|
||
interface TestSeries {
|
||
name: string;
|
||
runs: number;
|
||
passes: number;
|
||
fails: number;
|
||
manualAccepted: number;
|
||
retriedPasses: number;
|
||
totalAttempts: number;
|
||
totalCostUsd: number;
|
||
totalDurationMs: number;
|
||
lastSeen: string;
|
||
}
|
||
|
||
export function aggregate(evalFiles: string[]): Map<string, TestSeries> {
|
||
const series = new Map<string, TestSeries>();
|
||
for (const file of evalFiles) {
|
||
let run: EvalResult;
|
||
try {
|
||
run = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||
} catch { continue; }
|
||
if (isPartialEval(run, file)) continue; // in-progress accumulators are not runs
|
||
if (!Array.isArray(run.tests)) continue;
|
||
// Group this run's entries by name so N attempts = 1 run of that test.
|
||
const byName = new Map<string, typeof run.tests>();
|
||
for (const t of run.tests) {
|
||
const list = byName.get(t.name) ?? [];
|
||
list.push(t);
|
||
byName.set(t.name, list);
|
||
}
|
||
for (const [name, entries] of byName) {
|
||
const s = series.get(name) ?? {
|
||
name, runs: 0, passes: 0, fails: 0, manualAccepted: 0, retriedPasses: 0,
|
||
totalAttempts: 0, totalCostUsd: 0, totalDurationMs: 0, lastSeen: '',
|
||
};
|
||
const final = entries[entries.length - 1];
|
||
s.totalAttempts += entries.length;
|
||
const outcome = evalEntryOutcome(final);
|
||
if (outcome === 'manual-review') s.manualAccepted += 1;
|
||
else {
|
||
s.runs += 1;
|
||
if (outcome === 'passed') s.passes += 1; else s.fails += 1;
|
||
if (outcome === 'passed' && entries.length > 1) s.retriedPasses += 1;
|
||
}
|
||
for (const e of entries) {
|
||
s.totalCostUsd += e.cost_usd || 0;
|
||
s.totalDurationMs += e.duration_ms || 0;
|
||
}
|
||
if (run.timestamp > s.lastSeen) s.lastSeen = run.timestamp;
|
||
series.set(name, s);
|
||
}
|
||
}
|
||
return series;
|
||
}
|
||
|
||
export function collectEvalFiles(dir: string, sinceDays = 60): string[] {
|
||
if (!fs.existsSync(dir)) return [];
|
||
const cutoff = Date.now() - sinceDays * 86_400_000;
|
||
const out: string[] = [];
|
||
for (const name of fs.readdirSync(dir, { recursive: true }) as string[]) {
|
||
if (!isFinalizedEvalResultFile(name)) continue;
|
||
const full = path.join(dir, name);
|
||
try {
|
||
// Recency bound (review finding): E2E results embed full transcripts
|
||
// (MBs each) and the scan is otherwise unbounded over all-time history.
|
||
if (fs.statSync(full).mtimeMs < cutoff) continue;
|
||
} catch { continue; }
|
||
out.push(full);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** The local free-suite flake ledger (CI ledgers are read by test:health through scripts/lib/ci-history.ts). */
|
||
function readFreeLedger(): FlakeLedgerEntry[] {
|
||
try {
|
||
return parseFlakeLedger(fs.readFileSync(flakeLedgerPath(), 'utf-8'));
|
||
} catch { return []; }
|
||
}
|
||
|
||
// --- Trial records ---
|
||
|
||
/**
|
||
* A trial record as pass-rates reads it: eval-store's trial-outcomes schema
|
||
* plus what the report job stamps (scripts/eval-trial-series.ts): the series
|
||
* identity, the full consumed-input fingerprint and HARNESS_VERSION, both
|
||
* provenance only. policy_version 0 marks a pre-policy (backfilled) record.
|
||
*/
|
||
export type TrialRecord = TrialOutcomeRecord & { series_identity?: string; series_fingerprint?: string; harness_version?: number };
|
||
|
||
|
||
/** Every `trial-outcomes*.jsonl` file under a directory, size-capped, schema-validated by eval-store. */
|
||
export function readTrialOutcomeDir(dir: string): { records: TrialRecord[]; errors: string[] } {
|
||
const records: TrialRecord[] = [];
|
||
const errors: string[] = [];
|
||
if (!fs.existsSync(dir)) return { records, errors };
|
||
for (const name of fs.readdirSync(dir, { recursive: true }) as string[]) {
|
||
if (!/^trial-outcomes[^/\\]*\.jsonl$/.test(path.basename(name))) continue;
|
||
const full = path.join(dir, name);
|
||
const parsed = parseTrialOutcomes(fs.readFileSync(full, 'utf8'), { maxBytes: TRIAL_OUTCOMES_MAX_BYTES });
|
||
records.push(...parsed.records.map(record => ({
|
||
...record, series_identity: typeof (record as TrialRecord).series_identity === 'string'
|
||
? (record as TrialRecord).series_identity!.slice(0, 64) : undefined })));
|
||
errors.push(...parsed.errors.map(error => `${full}: ${error}`));
|
||
}
|
||
return { records, errors };
|
||
}
|
||
|
||
// --- Registry attribution and series identity ---
|
||
|
||
export interface Registry {
|
||
kinds: Record<string, EvalCaseKind>;
|
||
tiers: Record<string, string>;
|
||
touchfiles: Record<string, string[]>;
|
||
judgeTouchfiles: Record<string, string[]>;
|
||
globals: readonly string[];
|
||
testNames: Record<string, string>;
|
||
}
|
||
|
||
export const LIVE_REGISTRY: Registry = {
|
||
kinds: E2E_KINDS, tiers: E2E_TIERS, touchfiles: E2E_TOUCHFILES, judgeTouchfiles: LLM_JUDGE_TOUCHFILES,
|
||
globals: GLOBAL_TOUCHFILES, testNames: CASE_TEST_NAMES,
|
||
};
|
||
|
||
/** A case's tier: its E2E_TIERS value, or 'judge' for an LLM-judge entry. */
|
||
export function caseTier(id: string, registry: Registry = LIVE_REGISTRY): string {
|
||
return registry.tiers[id] ?? (id in registry.judgeTouchfiles ? 'judge' : 'unknown');
|
||
}
|
||
|
||
/**
|
||
* Attribute a legacy eval-store record to a registry id: the case-shard slug
|
||
* suffix (`<file>--<id>`), the recorded name or its exact slug (`/qa b6-static`
|
||
* is `qa-b6-static`), a CASE_TEST_NAMES label, or the only id its shard file
|
||
* registers. Anything else is unattributed (null).
|
||
*/
|
||
export function attributeLegacyRecord(name: string, shard: string | undefined, registry: Registry = LIVE_REGISTRY): string | null {
|
||
const known = (id: string) => id in registry.kinds;
|
||
const [slugFile, slugCase] = (shard ?? '').split('--');
|
||
if (slugCase && known(slugCase)) return slugCase;
|
||
if (known(name)) return name;
|
||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||
if (known(slug)) return slug;
|
||
const labeled = Object.entries(registry.testNames).find(([, label]) => label === name)?.[0];
|
||
if (labeled && known(labeled)) return labeled;
|
||
if (slugFile) {
|
||
const file = `test/${slugFile}.test.ts`;
|
||
const owners = Object.keys(registry.touchfiles).filter(id => registry.touchfiles[id]!.includes(file));
|
||
if (owners.length === 1 && known(owners[0]!)) return owners[0]!;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Import legacy eval-store result files as pre-policy trials (policy_version
|
||
* 0, source 'backfill'): first attempt only (a missing attempt means 1),
|
||
* attributed by registry id, never guessed. A manual-review acceptance carries
|
||
* no automated verdict: it is counted and shown, never scored. Without a CI
|
||
* run, each local result file is its own run.
|
||
*/
|
||
export function backfillEvalFiles(files: string[], run?: { run_id: string; sha?: string; timestamp?: string },
|
||
registry: Registry = LIVE_REGISTRY): { records: TrialRecord[]; unattributed: string[]; manualReviews: string[] } {
|
||
const records: TrialRecord[] = [];
|
||
const unattributed = new Set<string>();
|
||
const manualReviews: string[] = [];
|
||
for (const file of files) {
|
||
let result: EvalResult & { shard?: string; claude_cli_version?: string };
|
||
try { result = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { continue; }
|
||
if (isPartialEval(result, file) || !Array.isArray(result.tests)) continue;
|
||
const seen = new Set<string>();
|
||
for (const entry of result.tests) {
|
||
if ((entry.attempt ?? 1) !== 1 || seen.has(entry.name)) continue;
|
||
seen.add(entry.name);
|
||
const id = attributeLegacyRecord(entry.name, result.shard, registry);
|
||
if (!id) { unattributed.add(entry.name); continue; }
|
||
const outcome = evalEntryOutcome(entry);
|
||
if (outcome === 'manual-review') { manualReviews.push(id); continue; }
|
||
records.push({
|
||
schema: TRIAL_OUTCOME_SCHEMA, case: id,
|
||
file: result.shard ? `test/${result.shard.split('--')[0]}.test.ts` : 'unknown',
|
||
tier: caseTier(id, registry), kind: registry.kinds[id]!, trial: 1, panel: { n: 1, k: 1 }, attempt: 1,
|
||
outcome, ...(outcome === 'failed' ? { failure_class: failureClassOf(entry) } : {}),
|
||
exit_reason: entry.exit_reason, error: sanitizeTrialError(entry.error),
|
||
duration_ms: Math.max(0, entry.duration_ms || 0), cost_usd: Math.max(0, entry.cost_usd || 0), ...(entry.cost_known === false ? { cost_known: false } : {}),
|
||
model: entry.model, cli_version: result.claude_cli_version, policy_version: 0, quarantined: false,
|
||
execution: entry.execution === 'reused' ? 'reused' : 'executed', source: 'backfill',
|
||
run_id: run?.run_id ?? `local:${file}`, sha: run?.sha ?? result.git_sha, recorded_at: run?.timestamp ?? result.timestamp,
|
||
});
|
||
}
|
||
}
|
||
return { records, unattributed: [...unattributed].sort(), manualReviews };
|
||
}
|
||
|
||
// --- Statistics ---
|
||
|
||
/** 95% Wilson score interval for k successes in n trials. */
|
||
export function wilsonInterval(k: number, n: number, z = 1.96): { lo: number; hi: number } {
|
||
if (n <= 0) return { lo: 0, hi: 1 };
|
||
const p = k / n, z2 = z * z, denom = 1 + z2 / n;
|
||
const center = (p + z2 / (2 * n)) / denom;
|
||
const half = (z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n))) / denom;
|
||
return { lo: Math.max(0, center - half), hi: k === n ? 1 : Math.min(1, center + half) };
|
||
}
|
||
|
||
function logChoose(n: number, k: number): number {
|
||
let sum = 0;
|
||
for (let i = 1; i <= k; i++) sum += Math.log(n - k + i) - Math.log(i);
|
||
return sum;
|
||
}
|
||
|
||
/**
|
||
* One-sided Fisher exact p-value that the CURRENT pass rate is below the
|
||
* PREVIOUS one: P(X <= curPass) under the hypergeometric null with the
|
||
* observed margins.
|
||
*/
|
||
export function fisherOneSidedLower(curPass: number, curN: number, prevPass: number, prevN: number): number {
|
||
const passes = curPass + prevPass, total = curN + prevN;
|
||
const denom = logChoose(total, passes);
|
||
let p = 0;
|
||
for (let x = Math.max(0, passes - prevN); x <= curPass; x++) p += Math.exp(logChoose(curN, x) + logChoose(prevN, passes - x) - denom);
|
||
return Math.min(1, p);
|
||
}
|
||
|
||
/** Holm step-down: the indices whose p-values are rejected at family-wise alpha. */
|
||
export function holmRejections(pValues: number[], alpha: number): Set<number> {
|
||
const order = pValues.map((p, index) => ({ p, index })).sort((a, b) => a.p - b.p);
|
||
const rejected = new Set<number>();
|
||
for (let rank = 0; rank < order.length; rank++) {
|
||
if (order[rank]!.p > alpha / (order.length - rank)) break;
|
||
rejected.add(order[rank]!.index);
|
||
}
|
||
return rejected;
|
||
}
|
||
|
||
// --- Analysis ---
|
||
|
||
export type PassRateLabel = 'INCONCLUSIVE' | 'BROKEN' | 'FLAKY' | 'FAILING' | 'PASSING';
|
||
export type AlarmKind = 'drift' | 'rule-as-behavior' | 'regression' | 'quarantine-exit' | 'quarantine-expired'
|
||
| 'quarantine-cap' | 'quarantine-invalid' | 'headroom';
|
||
|
||
/** The EVAL_POLICY fields pass-rates reads (structural, so tests can vary them). */
|
||
export interface PassRatePolicy {
|
||
version: number;
|
||
quarantine: { entry: { rate: number; minTrials: number }; exit: { rate: number; minTrials: number }; capFraction: number; expiryWeeklyRuns: number };
|
||
drift: { fisherAlpha: number; fisherMinPerSide: number };
|
||
}
|
||
|
||
export type QuarantineEntry = (typeof CASE_QUARANTINE)[string];
|
||
|
||
/** Tiers whose cases block a lane; quarantine applies only to them. */
|
||
export const BLOCKING_TIERS: readonly string[] = ['gate', 'periodic'];
|
||
const QUARANTINE_FAILURE_CLASSES: readonly string[] = ['detector', 'harness', 'model-latency'];
|
||
|
||
export interface SeriesStats {
|
||
key: string;
|
||
identity: string;
|
||
model: string;
|
||
cli: string;
|
||
policyVersion: number;
|
||
passes: number;
|
||
/** Scored trials: passed + failed (skipped trials carry no verdict). */
|
||
trials: number;
|
||
infra: number;
|
||
interval: { lo: number; hi: number };
|
||
firstSeen: string;
|
||
lastSeen: string;
|
||
runs: string[];
|
||
}
|
||
|
||
export interface CasePassRate {
|
||
case: string;
|
||
kind: EvalCaseKind;
|
||
tier: string;
|
||
quarantined: boolean;
|
||
label: PassRateLabel;
|
||
/** Manual-review acceptances: visible, never scored. */
|
||
manualReviews: number;
|
||
current: SeriesStats | null;
|
||
previous: SeriesStats | null;
|
||
prePolicy: SeriesStats | null;
|
||
series: SeriesStats[];
|
||
latestRun: { runId: string; passes: number; trials: number } | null;
|
||
}
|
||
|
||
export interface Alarm { kind: AlarmKind; case: string; message: string }
|
||
|
||
export interface PassRateReport {
|
||
policyVersion: number;
|
||
cases: CasePassRate[];
|
||
alarms: Alarm[];
|
||
/** Trials under this reader's EVAL_POLICY.version: the only ones scored. */
|
||
postPolicyTrials: number;
|
||
/** Backfilled pre-policy trials (policy_version 0): display only. */
|
||
prePolicyTrials: number;
|
||
/** Trials under an earlier policy version: display only, never scored. */
|
||
olderPolicyTrials: number;
|
||
/** Trials under a later policy version than this reader: ignored entirely. */
|
||
newerPolicyTrials: number;
|
||
unattributed: string[];
|
||
errors: string[];
|
||
}
|
||
|
||
export interface AnalyzeOptions {
|
||
registry?: Registry;
|
||
quarantine?: Record<string, QuarantineEntry>;
|
||
policy?: PassRatePolicy;
|
||
/** Completed weekly-run timestamps in the window, for quarantine expiry. */
|
||
weeklyRuns?: string[];
|
||
/**
|
||
* Run ids of branch census runs (isPooledTrialRun). Their trials count only
|
||
* toward a series that a non-pooled record also has, so a branch can extend
|
||
* main's history but never start, or become, a case's current series.
|
||
*/
|
||
pooledRunIds?: ReadonlySet<string>;
|
||
now?: number;
|
||
unattributed?: string[];
|
||
errors?: string[];
|
||
manualReviews?: string[];
|
||
}
|
||
|
||
const at = (record: TrialRecord) => record.recorded_at ?? '';
|
||
const runOf = (record: TrialRecord) => `${record.run_id ?? record.sha ?? 'local'}#${record.attempt}`;
|
||
|
||
function seriesStats(key: string, records: TrialRecord[]): SeriesStats {
|
||
const scored = records.filter(record => record.outcome !== 'skipped');
|
||
const passes = scored.filter(record => record.outcome === 'passed').length;
|
||
const times = records.map(at).sort();
|
||
const first = records[0]!;
|
||
return {
|
||
key, identity: first.series_identity ?? 'unknown', model: first.model ?? 'unknown', cli: first.cli_version ?? 'unknown',
|
||
policyVersion: first.policy_version, passes, trials: scored.length,
|
||
infra: scored.filter(record => record.outcome === 'failed' && record.failure_class === 'infra').length,
|
||
interval: wilsonInterval(passes, scored.length), firstSeen: times[0] ?? '', lastSeen: times[times.length - 1] ?? '',
|
||
runs: [...new Set(records.map(runOf))],
|
||
};
|
||
}
|
||
|
||
/** Weekly runs completed after an entry's enteredAt; offline, whole weeks elapsed. */
|
||
export function quarantineRunsSince(enteredAt: string, weeklyRuns: string[] | undefined, now: number): number {
|
||
const entered = Date.parse(enteredAt);
|
||
if (!Number.isFinite(entered)) return Number.POSITIVE_INFINITY;
|
||
if (weeklyRuns && weeklyRuns.length) return weeklyRuns.filter(time => Date.parse(time) > entered).length;
|
||
return Math.floor((now - entered) / (7 * 86_400_000));
|
||
}
|
||
|
||
/**
|
||
* Static CASE_QUARANTINE problems, shared by the free policy test and the
|
||
* weekly gate: an id that is not a blocking-tier E2E case, a missing field,
|
||
* a failure class outside detector / harness / model-latency (a product
|
||
* defect is fixed or named, never quarantined), a malformed or future date,
|
||
* and a tier over its cap.
|
||
*/
|
||
export function quarantinePolicyProblems(quarantine: Record<string, QuarantineEntry>,
|
||
registry: Registry = LIVE_REGISTRY, policy: PassRatePolicy = EVAL_POLICY, now = Date.now()): Alarm[] {
|
||
const problems: Alarm[] = [];
|
||
const invalid = (id: string, message: string) => problems.push({ kind: 'quarantine-invalid', case: id, message: `${id}: ${message}` });
|
||
const perTier = new Map<string, number>();
|
||
for (const [id, entry] of Object.entries(quarantine)) {
|
||
const tier = registry.tiers[id];
|
||
if (!tier || !(id in registry.kinds)) { invalid(id, 'CASE_QUARANTINE names no registered E2E case'); continue; }
|
||
if (!BLOCKING_TIERS.includes(tier)) invalid(id, `tier ${tier} is not blocking; only ${BLOCKING_TIERS.join(' and ')} cases are quarantined`);
|
||
for (const field of ['reason', 'failureClass', 'tracking', 'owner', 'enteredAt', 'exit'] as const) {
|
||
if (typeof entry[field] !== 'string' || !entry[field].trim()) invalid(id, `missing ${field}`);
|
||
}
|
||
if (typeof entry.reason === 'string' && entry.reason.trim().length < 40) invalid(id, 'reason must be a written diagnosis (at least 40 characters)');
|
||
if (!QUARANTINE_FAILURE_CLASSES.includes(entry.failureClass)) {
|
||
invalid(id, `failureClass ${JSON.stringify(entry.failureClass)} is not ${QUARANTINE_FAILURE_CLASSES.join(', ')}; a product defect is fixed or named as a red, never quarantined`);
|
||
}
|
||
const entered = Date.parse(entry.enteredAt);
|
||
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.enteredAt ?? '') || !Number.isFinite(entered)) invalid(id, 'enteredAt must be YYYY-MM-DD');
|
||
else if (entered > now) invalid(id, 'enteredAt is in the future');
|
||
perTier.set(tier, (perTier.get(tier) ?? 0) + 1);
|
||
}
|
||
for (const [tier, count] of perTier) {
|
||
const size = Object.values(registry.tiers).filter(value => value === tier).length;
|
||
const cap = Math.floor(size * policy.quarantine.capFraction);
|
||
if (count > cap) problems.push({ kind: 'quarantine-cap', case: tier,
|
||
message: `${count} quarantined ${tier} cases exceed the ${pct(policy.quarantine.capFraction)} cap (${cap} of ${size})` });
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
export function analyzePassRates(records: TrialRecord[], options: AnalyzeOptions = {}): PassRateReport {
|
||
const registry = options.registry ?? LIVE_REGISTRY;
|
||
const quarantine = options.quarantine ?? CASE_QUARANTINE;
|
||
const policy = options.policy ?? EVAL_POLICY;
|
||
const now = options.now ?? Date.now();
|
||
const newerPolicyTrials = records.filter(record => record.policy_version > policy.version).length;
|
||
const byCase = new Map<string, TrialRecord[]>();
|
||
for (const record of records) {
|
||
if (record.policy_version > policy.version) continue;
|
||
const list = byCase.get(record.case) ?? [];
|
||
list.push(record);
|
||
byCase.set(record.case, list);
|
||
}
|
||
for (const id of options.manualReviews ?? []) if (!byCase.has(id)) byCase.set(id, []);
|
||
const cases: CasePassRate[] = [];
|
||
for (const [id, list] of [...byCase].sort(([a], [b]) => a.localeCompare(b))) {
|
||
list.sort((a, b) => at(a).localeCompare(at(b)) || runOf(a).localeCompare(runOf(b)) || a.trial - b.trial);
|
||
const groups = new Map<string, TrialRecord[]>();
|
||
const keyOf = (record: TrialRecord) => record.policy_version === 0 ? 'pre-policy'
|
||
: [record.series_identity ?? 'unknown', record.model ?? 'unknown', record.cli_version ?? 'unknown', `v${record.policy_version}`].join('|');
|
||
const pooled = (record: TrialRecord) => !!options.pooledRunIds?.has(record.run_id ?? '');
|
||
const mainKeys = new Set(list.filter(record => !pooled(record)).map(keyOf));
|
||
for (const record of list) {
|
||
const key = keyOf(record);
|
||
if (pooled(record) && !mainKeys.has(key)) continue;
|
||
const group = groups.get(key) ?? [];
|
||
group.push(record);
|
||
groups.set(key, group);
|
||
}
|
||
const series = [...groups].map(([key, group]) => seriesStats(key, group))
|
||
.sort((a, b) => a.lastSeen.localeCompare(b.lastSeen));
|
||
const post = series.filter(entry => entry.policyVersion === policy.version);
|
||
const current = post[post.length - 1] ?? null;
|
||
const previous = post[post.length - 2] ?? null;
|
||
const scored = current ? groups.get(current.key)!.filter(record => record.outcome !== 'skipped') : [];
|
||
const latestRun = scored.length ? runOf(scored[scored.length - 1]!) : null;
|
||
const latest = scored.filter(record => runOf(record) === latestRun);
|
||
const prior = scored.filter(record => runOf(record) !== latestRun);
|
||
const priorPasses = prior.filter(record => record.outcome === 'passed').length;
|
||
const entryRate = policy.quarantine.entry.rate;
|
||
let label: PassRateLabel;
|
||
if (latest.length > 0 && latest.every(record => record.outcome === 'failed')
|
||
&& prior.length > 0 && wilsonInterval(priorPasses, prior.length).lo >= entryRate) label = 'BROKEN';
|
||
else if (!current || current.trials < policy.quarantine.entry.minTrials) label = 'INCONCLUSIVE';
|
||
else if (current.interval.hi < entryRate) label = 'FAILING';
|
||
else if (current.passes < current.trials && current.interval.lo < entryRate) label = 'FLAKY';
|
||
else label = 'PASSING';
|
||
cases.push({
|
||
case: id, kind: registry.kinds[id] ?? list[0]!.kind, tier: caseTier(id, registry),
|
||
quarantined: id in quarantine, label, current, previous,
|
||
manualReviews: (options.manualReviews ?? []).filter(name => name === id).length,
|
||
prePolicy: series.find(entry => entry.policyVersion === 0) ?? null, series,
|
||
latestRun: latestRun ? { runId: latestRun, passes: latest.filter(record => record.outcome === 'passed').length, trials: latest.length } : null,
|
||
});
|
||
}
|
||
|
||
const alarms: Alarm[] = [];
|
||
const rate = (stats: SeriesStats) => stats.passes / stats.trials;
|
||
for (const entry of cases) {
|
||
const current = entry.current;
|
||
if (!current) continue;
|
||
const below = current.trials >= policy.quarantine.entry.minTrials && rate(current) < policy.quarantine.entry.rate;
|
||
if (below && !entry.quarantined && BLOCKING_TIERS.includes(entry.tier)) alarms.push({ kind: 'drift', case: entry.case,
|
||
message: `${entry.case} passes ${current.passes}/${current.trials} (below ${pct(policy.quarantine.entry.rate)} over >= ${policy.quarantine.entry.minTrials} trials): fix it, or propose a CASE_QUARANTINE entry with a written diagnosis (product defects are never quarantined)` });
|
||
if (below && entry.kind === 'rule') alarms.push({ kind: 'rule-as-behavior', case: entry.case,
|
||
message: `${entry.case}: rule case behaving like behavior (${current.passes}/${current.trials}): fix or reclassify` });
|
||
if (entry.quarantined && current.trials >= policy.quarantine.exit.minTrials && rate(current) >= policy.quarantine.exit.rate) {
|
||
alarms.push({ kind: 'quarantine-exit', case: entry.case,
|
||
message: `${entry.case} passes ${current.passes}/${current.trials} (>= ${pct(policy.quarantine.exit.rate)}): remove its CASE_QUARANTINE entry` });
|
||
}
|
||
}
|
||
const tested = cases.filter(entry => BLOCKING_TIERS.includes(entry.tier) && entry.current && entry.previous
|
||
&& entry.current.trials >= policy.drift.fisherMinPerSide && entry.previous.trials >= policy.drift.fisherMinPerSide);
|
||
const pValues = tested.map(entry => fisherOneSidedLower(entry.current!.passes, entry.current!.trials, entry.previous!.passes, entry.previous!.trials));
|
||
for (const index of holmRejections(pValues, policy.drift.fisherAlpha)) {
|
||
const entry = tested[index]!;
|
||
alarms.push({ kind: 'regression', case: entry.case,
|
||
message: `${entry.case}: current identity ${entry.current!.passes}/${entry.current!.trials} is significantly below the previous ${entry.previous!.passes}/${entry.previous!.trials} (one-sided Fisher p=${pValues[index]!.toFixed(4)}, Holm over ${tested.length} cases)` });
|
||
}
|
||
for (const [id, entry] of Object.entries(quarantine)) {
|
||
const runs = quarantineRunsSince(entry.enteredAt, options.weeklyRuns, now);
|
||
if (runs >= policy.quarantine.expiryWeeklyRuns) alarms.push({ kind: 'quarantine-expired', case: id,
|
||
message: `${id}: entered ${runs} weekly runs ago (limit ${policy.quarantine.expiryWeeklyRuns}): fix it, name it as a red, or re-diagnose with fresh evidence` });
|
||
}
|
||
alarms.push(...quarantinePolicyProblems(quarantine, registry, policy, now));
|
||
|
||
const count = (match: (version: number) => boolean) => records.filter(record => match(record.policy_version)).length;
|
||
return { policyVersion: policy.version, cases, alarms, postPolicyTrials: count(version => version === policy.version),
|
||
prePolicyTrials: count(version => version === 0), olderPolicyTrials: count(version => version > 0 && version < policy.version),
|
||
newerPolicyTrials, unattributed: options.unattributed ?? [], errors: options.errors ?? [] };
|
||
}
|
||
|
||
function pct(value: number): string { return `${Math.round(value * 1000) / 10}%`; }
|
||
|
||
function formatStats(stats: SeriesStats | null): string {
|
||
if (!stats) return '-';
|
||
return `${stats.passes}/${stats.trials} [${pct(stats.interval.lo)}–${pct(stats.interval.hi)}]${stats.infra ? ` (${stats.infra} infra)` : ''}`;
|
||
}
|
||
|
||
export function formatPassRates(report: PassRateReport, options: { caseFilter?: string } = {}): string {
|
||
const lines: string[] = [];
|
||
lines.push(`pass-rates: policy v${report.policyVersion}, ${report.postPolicyTrials} post-policy trial(s), ${report.prePolicyTrials} pre-policy (display only)`
|
||
+ (report.olderPolicyTrials ? `, ${report.olderPolicyTrials} under an older policy (display only)` : ''));
|
||
if (report.newerPolicyTrials) {
|
||
lines.push(` ignored ${report.newerPolicyTrials} trial(s) recorded under a policy newer than v${report.policyVersion}: this checkout predates them. `
|
||
+ 'Fix: run pass-rates from a checkout at or after the commit that bumped EVAL_POLICY.version (docs/TESTING_INTERNALS.md#pass-rate-policy-versions).');
|
||
}
|
||
if (report.postPolicyTrials === 0) lines.push(' no post-policy trials yet: every series starts INCONCLUSIVE');
|
||
const cases = report.cases.filter(entry => !options.caseFilter || entry.case === options.caseFilter);
|
||
lines.push(' label kind tier current series pre-policy manual case');
|
||
for (const entry of cases) {
|
||
const group = entry.current ? ` ${entry.current.model} / ${entry.current.cli}` : '';
|
||
const reset = entry.previous ? ' (baseline reset)' : '';
|
||
lines.push(` ${entry.label.padEnd(12)} ${entry.kind.padEnd(8)} ${entry.tier.padEnd(8)} ${formatStats(entry.current).padEnd(29)} `
|
||
+ `${formatStats(entry.prePolicy).padEnd(18)} ${String(entry.manualReviews).padStart(6)} ${entry.case}${entry.quarantined ? ' [quarantined]' : ''}${group}${reset}`);
|
||
}
|
||
if (report.unattributed.length) lines.push(` unattributed records (${report.unattributed.length}, never guessed): ${report.unattributed.slice(0, 20).join(', ')}`);
|
||
if (report.errors.length) lines.push(` rejected ${report.errors.length} invalid trial line(s): ${report.errors.slice(0, 5).join('; ')}`);
|
||
if (report.alarms.length) {
|
||
lines.push(`ACTION REQUIRED (${report.alarms.length}):`);
|
||
for (const alarm of report.alarms) lines.push(` [${alarm.kind}] ${alarm.message}`);
|
||
}
|
||
return lines.join('\n');
|
||
}
|
||
|
||
export interface PassRateHistory {
|
||
records: TrialRecord[];
|
||
unattributed: string[];
|
||
errors: string[];
|
||
manualReviews: string[];
|
||
weeklyRuns?: string[];
|
||
fetched: WeeklyRun[];
|
||
pooledRunIds?: Set<string>;
|
||
historyError: string | null;
|
||
scope: string;
|
||
cacheDir: string;
|
||
}
|
||
|
||
/**
|
||
* The trial history every eval:pass-rates view reads (and the ship-measure
|
||
* sweep ranks from): local dirs, or the trial-outcomes artifacts of the last
|
||
* N weekly runs on the branch plus the branch census runs that pool into
|
||
* main's series. A fetch failure is returned as historyError, never thrown.
|
||
*/
|
||
export function loadPassRateHistory(o: { repo: string; workflow: string; branch: string; runsLimit: number; dirs: string[]; sinceDays: number; backfill?: boolean; runId?: number }): PassRateHistory {
|
||
const records: TrialRecord[] = [];
|
||
const unattributed = new Set<string>();
|
||
const errors: string[] = [];
|
||
const manualReviews: string[] = [];
|
||
let historyError: string | null = null;
|
||
let weeklyRuns: string[] | undefined;
|
||
let fetched: WeeklyRun[] = [];
|
||
let pooledRunIds: Set<string> | undefined;
|
||
const cacheDir = path.join(path.resolve(resolveStateRoot()), 'eval-pass-rates-cache', o.repo.replace('/', '-'));
|
||
const importDir = (dir: string, run: { run_id: string; sha?: string; timestamp?: string } | undefined, legacyDays: number) => {
|
||
const trials = readTrialOutcomeDir(dir);
|
||
records.push(...trials.records);
|
||
errors.push(...trials.errors);
|
||
const legacy = backfillEvalFiles(collectEvalFiles(dir, legacyDays), run);
|
||
records.push(...legacy.records);
|
||
manualReviews.push(...legacy.manualReviews);
|
||
legacy.unattributed.forEach(name => unattributed.add(name));
|
||
};
|
||
if (o.dirs.length) {
|
||
for (const dir of o.dirs) importDir(dir, undefined, o.sinceDays);
|
||
} else {
|
||
try {
|
||
const weekly = listWeeklyRuns({ repo: o.repo, workflow: o.workflow, branches: [o.branch], limit: o.runsLimit }).filter(run => o.branch !== 'main' || isWeeklyHistoryRun(run));
|
||
weeklyRuns = weekly.map(run => run.createdAt);
|
||
// Branch census trials pool into main's matching series over the same window (isPooledTrialRun).
|
||
const oldest = weekly[weekly.length - 1]?.createdAt;
|
||
const pooledRuns = o.branch === 'main' && oldest
|
||
? listWeeklyRuns({ repo: o.repo, workflow: o.workflow, branches: [''], limit: 100 }).filter(run => isPooledTrialRun(run) && run.createdAt >= oldest)
|
||
: [];
|
||
pooledRunIds = new Set(pooledRuns.map(run => `${run.id}`));
|
||
const runs = [...weekly, ...pooledRuns];
|
||
if (o.runId !== undefined && !runs.some(run => run.id === o.runId)) runs.push(GH_JOBS.getRun(o.repo, o.runId));
|
||
fetched = runs;
|
||
const match = o.backfill
|
||
? (name: string) => name.startsWith('trial-outcomes') || /^(paid-slice-\d+|gate-census-\d+)(-a\d+)?$/.test(name)
|
||
: (name: string) => name.startsWith('trial-outcomes');
|
||
for (const run of runs) {
|
||
const dirsForRun = downloadRunArtifacts({ repo: o.repo, run, match, cacheDir, maxBytes: o.backfill ? 64 * 1024 * 1024 : undefined });
|
||
for (const dir of dirsForRun) importDir(dir, { run_id: `${run.id}`, sha: run.sha, timestamp: run.createdAt }, 3650);
|
||
}
|
||
} catch (error) {
|
||
historyError = error instanceof Error ? error.message : String(error);
|
||
}
|
||
}
|
||
const scope = o.dirs.length ? `local dirs ${o.dirs.join(', ')}` : `${o.repo} ${o.workflow} on ${o.branch}${pooledRunIds?.size ? ` + ${pooledRunIds.size} pooled branch census run(s)` : ''}, `
|
||
+ `last ${o.runsLimit} completed run(s): ${fetched.map(run => run.id).join(', ') || 'none'}`;
|
||
return { records, unattributed: [...unattributed].sort(), errors, manualReviews, weeklyRuns, fetched, pooledRunIds, historyError, scope, cacheDir };
|
||
}
|
||
|
||
// --- GitHub history (shared reader: scripts/lib/ci-history.ts) ---
|
||
|
||
export { downloadRunArtifacts, GH_HISTORY, listWeeklyRuns, TRIAL_OUTCOMES_MAX_BYTES,
|
||
type HistoryFetcher, type RunArtifact, type WeeklyRun } from './lib/ci-history';
|
||
|
||
if (import.meta.main) {
|
||
const parsed = parsePassRatesArgs(process.argv.slice(2), id => Object.hasOwn(E2E_TIERS, id) || Object.hasOwn(LLM_JUDGE_TOUCHFILES, id)
|
||
|| (/^test\/[\w./-]+\.test\.ts$/.test(id) && fs.existsSync(id)));
|
||
if ('help' in parsed) { console.log(PASS_RATES_USAGE); process.exit(0); }
|
||
if ('error' in parsed) { console.error(`eval:pass-rates: ${parsed.error}\n${PASS_RATES_USAGE}`); process.exit(2); }
|
||
const { dirs, json: asJson, gate, backfill, caseFilter, runs: runsLimit, sinceDays, view } = parsed;
|
||
const repo = parsed.repo ?? repoSlug();
|
||
const workflow = parsed.workflow ?? 'evals-periodic.yml';
|
||
const branch = parsed.branch ?? 'main';
|
||
|
||
const history = loadPassRateHistory({ repo, workflow, branch, runsLimit, dirs, sinceDays, backfill, runId: parsed.runId });
|
||
const { records, errors, manualReviews, weeklyRuns, fetched, pooledRunIds, historyError, scope, cacheDir } = history;
|
||
const unattributed = new Set(history.unattributed);
|
||
const scoped = caseFilter ? records.filter(record => record.case === caseFilter) : records;
|
||
const caseHeadroom = headroom(scoped);
|
||
if (view !== 'rates') {
|
||
const critical: CriticalPath[] = [];
|
||
if (view !== 'reds' && !dirs.length) {
|
||
for (const run of fetched.filter(run => view === 'headroom' || run.id === parsed.runId)) {
|
||
try { critical.push(criticalPath(String(run.id), GH_JOBS.listJobs(repo, run.id))); } catch { critical.push({ run: String(run.id), jobs: 0, slowest: null, wallMs: null }); }
|
||
}
|
||
}
|
||
const lines = view === 'headroom' ? formatHeadroom(caseHeadroom, critical) : view === 'reds' ? formatRedLedger(redLedger(scoped))
|
||
: historyError ? [] : triageRun({ repo, run: fetched.find(run => run.id === parsed.runId)!, records, caseFilter, cacheDir, fetcher: GH_HISTORY,
|
||
download: (match, maxBytes) => downloadRunArtifacts({ repo, run: fetched.find(run => run.id === parsed.runId)!, match, cacheDir, maxBytes }) })
|
||
.concat(formatCriticalPath(critical), historyError ? [] : [`guide: ${CENSUS_RED_GUIDE}`]);
|
||
if (asJson) console.log(JSON.stringify({ scope, historyError, view, lines }, null, 2));
|
||
else {
|
||
console.log(`scope: ${scope}`);
|
||
if (historyError) console.log(`history unavailable (${historyError})`);
|
||
for (const line of lines) console.log(line);
|
||
}
|
||
process.exit(historyError ? 1 : 0);
|
||
}
|
||
const full = analyzePassRates(records, { weeklyRuns, pooledRunIds, unattributed: [...unattributed].sort(), errors, manualReviews });
|
||
const alarms = [...full.alarms, ...(gate ? headroomAlarms(caseHeadroom) : [])].filter(alarm => !caseFilter || alarm.case === caseFilter);
|
||
const report = { ...full, cases: full.cases.filter(entry => !caseFilter || entry.case === caseFilter), alarms };
|
||
const ledger = readFreeLedger();
|
||
if (asJson) {
|
||
console.log(JSON.stringify({ repo, workflow, branch, scope, dirs, historyError, ...report, freeLedger: ledger }, null, 2));
|
||
} else {
|
||
console.log(`scope: ${scope}`);
|
||
if (historyError) console.log(`pass-rates: history unavailable (${historyError}); every label below is INCONCLUSIVE`);
|
||
console.log(formatPassRates(report, { caseFilter }));
|
||
if (ledger.length > 0) {
|
||
const byFile = new Map<string, number>();
|
||
for (const e of ledger) byFile.set(e.file, (byFile.get(e.file) ?? 0) + 1);
|
||
console.log(`free-suite flaky-passes (${flakeLedgerPath()}):`);
|
||
for (const [file, n] of [...byFile.entries()].sort((a, b) => b[1] - a[1])) {
|
||
console.log(` ${String(n).padStart(3)}x ${file}`);
|
||
}
|
||
}
|
||
}
|
||
if (gate && (historyError || report.alarms.length)) process.exit(1);
|
||
}
|