mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +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>
353 lines
20 KiB
TypeScript
353 lines
20 KiB
TypeScript
/**
|
||
* ship-measure sweep — the weekly off-ship qualification sweep on main
|
||
* (approved by Garry 2026-10-06; docs/TESTING_INTERNALS.md#ship-measure-sweep).
|
||
*
|
||
* With ~130 gate verdicts, an all-green gate is unlikely even when every case
|
||
* is healthy, so flaky cases are measured on main before any ship hits them.
|
||
* The sweep reads eval:pass-rates history (evals-periodic.yml on main plus the
|
||
* branch census runs that pool into it), ranks the gate cases with a failed
|
||
* trial by how much each costs the gate's all-green probability Π(1 − p_i)
|
||
* (p_i: its red-verdict rate shrunk toward the pooled rate, as
|
||
* `eval:pass-rates --reds` computes it; ties broken by the lower per-trial
|
||
* pass rate), and measures the top K at their kind's N on the
|
||
* checked-out head with the same runner and measurement bar /ship uses
|
||
* (scripts/ship-measure.ts, scripts/lib/measure-bar.ts).
|
||
*
|
||
* Spend: ship_measure_sweep_budget_usd (default $150) is a weekly cap enforced
|
||
* through the same admission budget as /ship's loop, shared across the
|
||
* sweep's cases and every sweep report of the last 7 days found under --prior
|
||
* (CI downloads the earlier runs' reports there) and the local sweep root.
|
||
* When the cap is exhausted the sweep stops and lists what it skipped.
|
||
*
|
||
* Every trial is diagnostic: it never becomes a verdict and never enters
|
||
* EVAL_POLICY pooling or series history. The sweep never pushes, commits or
|
||
* opens a pull request; an agent fixes from its report.
|
||
*/
|
||
import * as fs from 'node:fs';
|
||
import * as path from 'node:path';
|
||
import { spawnSync } from 'node:child_process';
|
||
import { CASE_QUARANTINE } from '../test/helpers/periodic-exclude-data';
|
||
import { E2E_KINDS, E2E_TIERS, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
|
||
import { parseCliFlags } from './lib/shard-engine';
|
||
import { RED_RATE_SHRINK, verdictsOf } from './lib/eval-history';
|
||
import { trialPassed } from './lib/measure-bar';
|
||
import { repoSlug } from './lib/ci-history';
|
||
import { loadPassRateHistory, type TrialRecord as HistoryRecord } from './eval-flake-rank';
|
||
import {
|
||
caseSelectionPreflight, caseSlug, commandRunner, gstackRunner, kindPlan, measureCase, measurementIdentity, readMeasureConfig,
|
||
type MeasureConfig, type MeasureKind, type Measurement, type SpendPool, type TrialRunner,
|
||
} from './ship-measure';
|
||
|
||
const ROOT = path.resolve(import.meta.dir, '..');
|
||
const WEEK_MS = 7 * 24 * 3600_000;
|
||
export const SWEEP_SCHEMA = 'gstack-ship-measure-sweep/1';
|
||
|
||
export interface SweepCandidate {
|
||
case: string;
|
||
kind: MeasureKind;
|
||
/** Red-verdict rate over gate history, shrunk toward the pooled rate (K = RED_RATE_SHRINK, as eval:pass-rates --reds). */
|
||
redRate: number;
|
||
reds: number;
|
||
verdicts: number;
|
||
/** Per-trial pass rate over the history window, or null without scored trials. */
|
||
passRate: number | null;
|
||
scoredTrials: number;
|
||
/** All-green probability gained if this case never went red: P / (1 − p_i) − P. */
|
||
contribution: number;
|
||
/** Mean known cost per trial from history (else the gate-wide mean), or null. */
|
||
costPerTrialUsd: number | null;
|
||
}
|
||
|
||
export interface SweepPlan {
|
||
candidates: SweepCandidate[];
|
||
picked: SweepCandidate[];
|
||
/** All-green probability over the latest census's gate verdicts, from history. */
|
||
allGreen: number | null;
|
||
censusCases: string[];
|
||
pooledRate: number;
|
||
/** Per-case shrunk red rates for every case in the latest census. */
|
||
rates: Record<string, number>;
|
||
}
|
||
|
||
const liveCase = (id: string) => Object.hasOwn(E2E_TIERS, id) || Object.hasOwn(LLM_JUDGE_TOUCHFILES, id);
|
||
export const sweepKind = (id: string): MeasureKind => (E2E_KINDS[id] as MeasureKind | undefined) ?? (Object.hasOwn(E2E_TIERS, id) ? 'rule' : 'judge');
|
||
|
||
/**
|
||
* Rank the gate cases history flags: every live, non-quarantined case of the
|
||
* latest gate census with a failed trial in the window, by its contribution
|
||
* to the all-green probability, then by the lower per-trial pass rate. A red
|
||
* verdict here is a lane-failing verdict with at least one failed trial (an
|
||
* INCOMPLETE record-count verdict is a reporting fault, not a flake). Picks
|
||
* the top k.
|
||
*/
|
||
export function planSweep(records: readonly HistoryRecord[], k: number, opts: { quarantine?: Record<string, unknown> } = {}): SweepPlan {
|
||
const quarantine = opts.quarantine ?? CASE_QUARANTINE;
|
||
const gate = records.filter(r => r.tier === 'gate' && r.policy_version >= 1);
|
||
const verdicts = verdictsOf(gate).filter(v => v.status !== 'SKIPPED');
|
||
const red = (v: (typeof verdicts)[number]) => v.failsLane && v.trials.some(t => t.outcome === 'failed');
|
||
const pooledRate = verdicts.length ? verdicts.filter(red).length / verdicts.length : 0;
|
||
const byCase = new Map<string, { verdicts: number; reds: number }>();
|
||
for (const v of verdicts) {
|
||
const e = byCase.get(v.case) ?? { verdicts: 0, reds: 0 };
|
||
e.verdicts++; if (red(v)) e.reds++;
|
||
byCase.set(v.case, e);
|
||
}
|
||
const rates: Record<string, number> = Object.fromEntries([...byCase].map(([id, e]) => [id, (e.reds + RED_RATE_SHRINK * pooledRate) / (e.verdicts + RED_RATE_SHRINK)]));
|
||
const latestRun = verdicts.map(v => v.run).filter(run => run !== 'local').sort().at(-1) ?? 'local';
|
||
const censusCases = [...new Set(verdicts.filter(v => v.run === latestRun).map(v => v.case))].sort();
|
||
const allGreen = censusCases.length ? censusCases.reduce((p, id) => p * (1 - rates[id]!), 1) : null;
|
||
const scored = gate.filter(r => r.execution === 'executed' && (r.outcome === 'passed' || r.outcome === 'failed'));
|
||
const knownCost = (r: HistoryRecord) => r.execution === 'executed' && r.cost_known !== false && r.cost_usd > 0;
|
||
const mean = (xs: number[]) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null;
|
||
const gateCost = mean(records.filter(r => r.tier === 'gate' && knownCost(r)).map(r => r.cost_usd));
|
||
const candidates: SweepCandidate[] = [];
|
||
for (const id of censusCases) {
|
||
if (!liveCase(id) || Object.hasOwn(quarantine, id)) continue;
|
||
const trials = scored.filter(r => r.case === id);
|
||
const failedTrials = trials.filter(r => r.outcome === 'failed').length;
|
||
const e = byCase.get(id)!;
|
||
if (failedTrials === 0) continue;
|
||
candidates.push({
|
||
case: id, kind: sweepKind(id), redRate: rates[id]!, reds: e.reds, verdicts: e.verdicts,
|
||
passRate: trials.length ? (trials.length - failedTrials) / trials.length : null, scoredTrials: trials.length,
|
||
contribution: allGreen === null ? 0 : allGreen / (1 - rates[id]!) - allGreen,
|
||
costPerTrialUsd: mean(records.filter(r => r.case === id && knownCost(r)).map(r => r.cost_usd)) ?? gateCost,
|
||
});
|
||
}
|
||
candidates.sort((a, b) => b.contribution - a.contribution || b.redRate - a.redRate || (a.passRate ?? 1) - (b.passRate ?? 1) || (a.case < b.case ? -1 : 1));
|
||
return { candidates, picked: candidates.slice(0, k), allGreen, censusCases, pooledRate, rates };
|
||
}
|
||
|
||
/**
|
||
* A measured case's predicted red-verdict rate: its measured per-trial failure
|
||
* fraction shrunk toward the pooled rate with the same K as history, and for a
|
||
* behavior case the 3-trial panel's red risk 1 − P(at least 2 of 3 pass).
|
||
*/
|
||
export function measuredRedRate(kind: MeasureKind, failures: number, trials: number, pooledRate: number): number {
|
||
const q = (failures + RED_RATE_SHRINK * pooledRate) / (trials + RED_RATE_SHRINK);
|
||
return kind === 'behavior' ? 1 - ((1 - q) ** 3 + 3 * (1 - q) ** 2 * q) : q;
|
||
}
|
||
|
||
/** Π(1 − p_i) over the census cases, with measured cases' rates replacing history's. */
|
||
export function predictedAllGreen(plan: SweepPlan, measured: Record<string, number>): number | null {
|
||
if (plan.allGreen === null) return null;
|
||
return plan.censusCases.reduce((p, id) => p * (1 - (measured[id] ?? plan.rates[id] ?? plan.pooledRate)), 1);
|
||
}
|
||
|
||
export interface SweepRow {
|
||
rank: number;
|
||
case: string;
|
||
kind: MeasureKind;
|
||
historyPassRate: number | null;
|
||
historyTrials: number;
|
||
reds: number;
|
||
verdicts: number;
|
||
redRate: number;
|
||
status: 'planned' | 'measured' | 'budget_exhausted' | 'skipped' | 'error';
|
||
decision: string;
|
||
measuredPasses: number | null;
|
||
measuredTrials: number | null;
|
||
estimatedUsd: number | null;
|
||
actualUsd: number;
|
||
costUnknownTrials: number;
|
||
captures: string | null;
|
||
note: string;
|
||
}
|
||
|
||
export interface SweepReport {
|
||
schema: typeof SWEEP_SCHEMA;
|
||
label: 'diagnostic';
|
||
verdict: null;
|
||
sweepId: string;
|
||
startedAt: string;
|
||
finishedAt: string | null;
|
||
ref: string;
|
||
sha: string;
|
||
dryRun: boolean;
|
||
k: number;
|
||
capUsd: number;
|
||
priorWeekUsd: number;
|
||
/** Charged against the weekly cap by this sweep: known costs plus the estimate for trials whose cost is unknown. */
|
||
chargedUsd: number;
|
||
historyScope: string;
|
||
allGreenBefore: number | null;
|
||
allGreenAfter: number | null;
|
||
censusVerdicts: number;
|
||
rows: SweepRow[];
|
||
}
|
||
|
||
/** Spend charged by sweep reports under dirs started within the last 7 days (deduplicated by sweep id). */
|
||
export function weeklySpent(dirs: readonly string[], now: number, excludeId?: string): number {
|
||
const seen = new Map<string, number>();
|
||
for (const dir of dirs) {
|
||
if (!fs.existsSync(dir)) continue;
|
||
for (const name of fs.readdirSync(dir, { recursive: true }) as string[]) {
|
||
if (path.basename(name) !== 'sweep-report.json') continue;
|
||
let report: SweepReport;
|
||
try { report = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')) as SweepReport; } catch { continue; }
|
||
if (report.schema !== SWEEP_SCHEMA || report.sweepId === excludeId || report.dryRun) continue;
|
||
const started = Date.parse(report.startedAt);
|
||
if (!Number.isFinite(started) || now - started > WEEK_MS || started > now) continue;
|
||
seen.set(report.sweepId, Math.max(seen.get(report.sweepId) ?? 0, report.chargedUsd));
|
||
}
|
||
}
|
||
return [...seen.values()].reduce((a, b) => a + b, 0);
|
||
}
|
||
|
||
const pct = (x: number | null) => x === null ? 'n/a' : `${Math.round(x * 1000) / 10}%`;
|
||
const prob = (x: number | null) => x === null || x >= 0.001 || x === 0 ? pct(x) : `${(x * 100).toPrecision(2)}%`;
|
||
const money = (x: number | null) => x === null ? 'unknown' : `$${x.toFixed(2)}`;
|
||
const cell = (text: string) => text.replace(/\|/g, '/').replace(/\n/g, ' ');
|
||
|
||
export function formatSweepReport(r: SweepReport): string {
|
||
const rows = r.rows.map(row => `| ${row.rank} | ${cell(row.case)} | ${row.kind} | ${pct(row.historyPassRate)} of ${row.historyTrials} trials; ${row.reds}/${row.verdicts} red verdicts (rate ${pct(row.redRate)}) | `
|
||
+ `${row.measuredTrials === null ? '—' : `${row.measuredPasses}/${row.measuredTrials}`} | ${cell(row.decision)} | ${money(row.estimatedUsd)} / ${money(row.actualUsd)}${row.costUnknownTrials ? ` (+${row.costUnknownTrials} unknown)` : ''} | ${row.captures ? `\`${cell(row.captures)}\`` : '—'} |`);
|
||
const noted = r.rows.filter(row => row.status !== 'measured');
|
||
return [
|
||
`## ship-measure sweep${r.dryRun ? ' (dry run: nothing ran, $0)' : ''}: diagnostic measurements, not verdicts`,
|
||
'',
|
||
`Ref ${r.ref} at ${r.sha.slice(0, 12)}; K=${r.k}; weekly cap $${r.capUsd} (charged earlier this week $${r.priorWeekUsd.toFixed(2)}, this sweep $${r.chargedUsd.toFixed(2)}). History: ${r.historyScope}.`,
|
||
'',
|
||
`Predicted gate all-green probability (approximation, assumes independent verdicts, over the latest census's ${r.censusVerdicts} gate verdicts): before ${prob(r.allGreenBefore)} → after ${prob(r.allGreenAfter)}${r.dryRun ? ' (no measurements yet)' : ' (measured cases use this sweep\'s rates)'}.`,
|
||
'',
|
||
'| # | Case | Kind | History rate | Measured | Decision | Cost (est / actual) | Captures |',
|
||
'|---|---|---|---|---|---|---|---|',
|
||
...(rows.length ? rows : ['| — | no case in the latest gate census has a failed trial or red verdict | | | | | | |']),
|
||
...(noted.length ? ['', r.dryRun ? 'Plan:' : 'Skipped or stopped:', ...noted.map(row => `- ${row.case}: ${row.note}`)] : []),
|
||
'',
|
||
'Every trial is diagnostic: it never becomes a verdict and never enters pass-rate history. The sweep never pushes or opens a pull request. '
|
||
+ 'Fix a BELOW case at its cause, classify a needs-classify case (`ship-measure classify --out <captures root> --case <id> --round baseline ...`), and measure again on the fix.',
|
||
].join('\n');
|
||
}
|
||
|
||
export interface SweepOptions {
|
||
records: readonly HistoryRecord[];
|
||
historyScope: string;
|
||
k: number;
|
||
capUsd: number;
|
||
config: MeasureConfig;
|
||
outDir: string;
|
||
priorDirs: string[];
|
||
dryRun: boolean;
|
||
parallel: number;
|
||
ref: string;
|
||
sha: string;
|
||
now?: number;
|
||
/** One runner for every case (default: gstack's single-case runner after a selection preflight). */
|
||
runner?: TrialRunner;
|
||
runnerId?: string;
|
||
preflight?: (caseId: string) => { ok: boolean; detail: string };
|
||
identity?: (kind: MeasureKind, trials: number) => Record<string, string>;
|
||
evalDir?: string;
|
||
log?: (line: string) => void;
|
||
}
|
||
|
||
/** Plan, measure in rank order under the weekly cap, and write sweep-report.json/.md after every case. */
|
||
export async function runSweep(o: SweepOptions): Promise<SweepReport> {
|
||
const log = o.log ?? ((line: string) => console.log(line));
|
||
const now = o.now ?? Date.now();
|
||
const sweepId = new Date(now).toISOString().replace(/[:.]/g, '-');
|
||
const dir = path.join(o.outDir, `sweep-${sweepId}`);
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
const plan = planSweep(o.records, o.k);
|
||
const priorWeekUsd = weeklySpent([...o.priorDirs, o.outDir], now, sweepId);
|
||
const pool: SpendPool = { label: `weekly sweep cap`, capUsd: o.capUsd, spentUsd: priorWeekUsd };
|
||
const report: SweepReport = {
|
||
schema: SWEEP_SCHEMA, label: 'diagnostic', verdict: null, sweepId, startedAt: new Date(now).toISOString(), finishedAt: null, ref: o.ref, sha: o.sha,
|
||
dryRun: o.dryRun, k: o.k, capUsd: o.capUsd, priorWeekUsd, chargedUsd: 0, historyScope: o.historyScope,
|
||
allGreenBefore: plan.allGreen, allGreenAfter: plan.allGreen, censusVerdicts: plan.censusCases.length, rows: [],
|
||
};
|
||
const save = () => {
|
||
report.chargedUsd = Math.max(0, pool.spentUsd - priorWeekUsd);
|
||
fs.writeFileSync(path.join(dir, 'sweep-report.json'), `${JSON.stringify(report, null, 2)}\n`);
|
||
fs.writeFileSync(path.join(dir, 'sweep-report.md'), `${formatSweepReport(report)}\n`);
|
||
};
|
||
const measuredRates: Record<string, number> = {};
|
||
for (const [index, c] of plan.picked.entries()) {
|
||
const n = kindPlan(c.kind, o.config).trials;
|
||
const row: SweepRow = {
|
||
rank: index + 1, case: c.case, kind: c.kind, historyPassRate: c.passRate, historyTrials: c.scoredTrials, reds: c.reds, verdicts: c.verdicts, redRate: c.redRate,
|
||
status: 'planned', decision: 'planned', measuredPasses: null, measuredTrials: null,
|
||
estimatedUsd: c.costPerTrialUsd === null ? null : c.costPerTrialUsd * n, actualUsd: 0, costUnknownTrials: 0, captures: null, note: '',
|
||
};
|
||
report.rows.push(row);
|
||
if (o.dryRun) { row.note = `would measure ${n} trials at ${c.costPerTrialUsd === null ? 'an unknown cost (one calibration trial first)' : `${money(c.costPerTrialUsd)}/trial`}`; save(); continue; }
|
||
if (pool.spentUsd >= pool.capUsd) {
|
||
Object.assign(row, { status: 'skipped', decision: 'skipped', note: `weekly cap $${o.capUsd} exhausted ($${pool.spentUsd.toFixed(2)} charged in the last 7 days)` });
|
||
save();
|
||
continue;
|
||
}
|
||
const selected = (o.preflight ?? caseSelectionPreflight)(c.case);
|
||
if (!selected.ok) { Object.assign(row, { status: 'skipped', decision: 'skipped', note: `selection preflight failed (no paid call made): ${selected.detail}` }); save(); continue; }
|
||
let m: Measurement;
|
||
try {
|
||
m = await measureCase({
|
||
caseId: c.case, kind: c.kind, round: 'baseline', config: o.config, outDir: dir, parallel: o.parallel, costPerTrialUsd: c.costPerTrialUsd, approved: true,
|
||
runner: o.runner ?? gstackRunner(), pool, log,
|
||
identity: () => (o.identity ?? ((kind, trials) => measurementIdentity(kind, trials, o.runnerId ?? 'gstack')))(c.kind, n),
|
||
...(o.evalDir ? { evalDir: o.evalDir } : {}),
|
||
});
|
||
} catch (error) {
|
||
Object.assign(row, { status: 'error', decision: 'error', note: (error as Error).message });
|
||
save();
|
||
continue;
|
||
}
|
||
const captures = path.relative(o.outDir, path.join(dir, caseSlug(c.case), 'baseline'));
|
||
const decided = m.status === 'measured' && m.decision !== 'void' && m.decision !== 'incomplete';
|
||
Object.assign(row, {
|
||
status: m.status === 'measured' ? 'measured' : 'budget_exhausted', decision: m.status === 'measured' ? m.decision : m.status.replace('_', ' '),
|
||
...(decided ? { measuredPasses: m.passes, measuredTrials: m.counted } : { measuredPasses: m.trials.filter(t => trialPassed(c.kind, t)).length, measuredTrials: m.trials.length }),
|
||
actualUsd: m.actualUsd, costUnknownTrials: m.costUnknownTrials, captures,
|
||
note: m.status === 'measured' ? m.decisionReason : m.reason ?? '',
|
||
});
|
||
if (decided) {
|
||
measuredRates[c.case] = measuredRedRate(c.kind, m.counted - m.passes, m.counted, plan.pooledRate);
|
||
report.allGreenAfter = predictedAllGreen(plan, measuredRates);
|
||
}
|
||
save();
|
||
}
|
||
report.finishedAt = new Date().toISOString();
|
||
save();
|
||
log(formatSweepReport(report));
|
||
log(`[ship-measure] sweep report: ${path.join(dir, 'sweep-report.md')}`);
|
||
return report;
|
||
}
|
||
|
||
function gitText(args: string[]): string {
|
||
const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8', timeout: 30_000 });
|
||
return r.status === 0 ? r.stdout.trim() : 'unknown';
|
||
}
|
||
|
||
/** `ship-measure sweep` CLI. Exits 0 when the sweep completed (whatever it measured), 1 when history was unavailable, 4 on usage errors. */
|
||
export async function sweepMain(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
||
const flags: Record<string, string> = {};
|
||
const historyDirs: string[] = [];
|
||
const priorDirs: string[] = [];
|
||
const value = (name: string) => (next: () => string | undefined) => { const v = next(); if (v === undefined) throw new Error(`${name} needs a value`); flags[name] = v; };
|
||
const list = (into: string[], name: string) => (next: () => string | undefined) => { const v = next(); if (v === undefined) throw new Error(`${name} needs a value`); into.push(path.resolve(v)); };
|
||
parseCliFlags(argv, {
|
||
...Object.fromEntries(['--k', '--cap-usd', '--command', '--jobs', '--out', '--runs', '--repo'].map(name => [name, value(name)])),
|
||
'--history-dir': list(historyDirs, '--history-dir'), '--prior': list(priorDirs, '--prior'), '--dry-run': () => { flags['--dry-run'] = '1'; },
|
||
}, 'Usage: ship-measure sweep [--k K] [--cap-usd USD] [--dry-run] [--history-dir DIR]... [--prior DIR]... [--runs N] [--command \'CMD {case}\'] [--jobs N] [--out DIR]');
|
||
const config = readMeasureConfig();
|
||
const int = (raw: string | undefined, flag: string, fallback: number) => {
|
||
if (raw === undefined) return fallback;
|
||
if (!/^[1-9][0-9]*$/.test(raw)) throw new Error(`${flag} needs a positive integer. Received: ${raw}`);
|
||
return Number(raw);
|
||
};
|
||
const capUsd = flags['--cap-usd'] === undefined ? config.sweepBudgetUsd : Number(flags['--cap-usd']);
|
||
if (!Number.isFinite(capUsd) || capUsd <= 0) throw new Error(`--cap-usd needs a positive amount in USD. Received: ${flags['--cap-usd']}`);
|
||
const repo = flags['--repo'] ?? repoSlug(ROOT);
|
||
const history = loadPassRateHistory({ repo, workflow: 'evals-periodic.yml', branch: 'main', runsLimit: int(flags['--runs'], '--runs', 10), dirs: historyDirs, sinceDays: 60 });
|
||
if (history.historyError) { console.error(`[ship-measure] sweep: pass-rate history unavailable (${history.historyError}); nothing measured`); return 1; }
|
||
const outDir = path.resolve(flags['--out'] ?? path.join(process.cwd(), '.context', 'ship-measure'));
|
||
const report = await runSweep({
|
||
records: history.records, historyScope: history.scope, k: int(flags['--k'], '--k', config.sweepCases), capUsd, config, outDir, priorDirs,
|
||
dryRun: flags['--dry-run'] === '1', parallel: int(flags['--jobs'], '--jobs', Number(env.EVALS_JOBS) || 4),
|
||
ref: env.GITHUB_REF_NAME ?? gitText(['rev-parse', '--abbrev-ref', 'HEAD']), sha: env.GITHUB_SHA ?? gitText(['rev-parse', 'HEAD']),
|
||
...(flags['--command'] ? { runner: commandRunner(flags['--command']), runnerId: `command:${flags['--command']}`, preflight: () => ({ ok: true, detail: 'documented command' }) } : {}),
|
||
});
|
||
if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, `${formatSweepReport(report)}\n`);
|
||
return 0;
|
||
}
|