mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 18:50:18 +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>
203 lines
13 KiB
TypeScript
203 lines
13 KiB
TypeScript
/**
|
|
* Free contracts for the weekly off-ship qualification sweep
|
|
* (scripts/ship-measure-sweep.ts): which gate cases it picks from pass-rate
|
|
* history, the predicted all-green probability before and after, the weekly
|
|
* spend cap enforced through the admission budget across earlier sweep
|
|
* reports, the report it writes after every case, and a $0 CLI run (dry run,
|
|
* and a fake single-case command). Nothing here makes a paid call.
|
|
*/
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { EVAL_POLICY } from '../test/helpers/periodic-exclude-data';
|
|
import { TRIAL_OUTCOME_SCHEMA, type TrialOutcomeRecord } from '../test/helpers/eval-store';
|
|
import { MEASURE_DEFAULTS, type TrialRequest, type TrialResult } from '../scripts/ship-measure';
|
|
import { SWEEP_SCHEMA, measuredRedRate, planSweep, predictedAllGreen, runSweep, weeklySpent, type SweepReport } from '../scripts/ship-measure-sweep';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const temps: string[] = [];
|
|
const tmp = (prefix: string) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); temps.push(dir); return dir; };
|
|
afterAll(() => { for (const dir of temps) fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
const A = 'ship-skipped-queued-finding';
|
|
const B = 'investigate-owned-completion';
|
|
const C = 'investigate-owned-abort';
|
|
const Q = 'investigate-owned-ending-error';
|
|
|
|
function record(id: string, run: number, failed: boolean, extra: Partial<TrialOutcomeRecord> = {}): TrialOutcomeRecord {
|
|
return {
|
|
schema: TRIAL_OUTCOME_SCHEMA, case: id, file: `test/${id}.test.ts`, tier: 'gate', kind: 'rule', trial: 1, panel: { n: 1, k: 1 }, attempt: 1,
|
|
outcome: failed ? 'failed' : 'passed', ...(failed ? { failure_class: 'assertion' as const } : {}), duration_ms: 1000, cost_usd: 1,
|
|
policy_version: EVAL_POLICY.version, quarantined: false, execution: 'executed', source: 'shard', run_id: String(run), lane: 'gate',
|
|
series_identity: 's1', model: 'm', cli_version: 'c', recorded_at: `2026-09-${10 + run}T00:00:00Z`, ...extra,
|
|
} as TrialOutcomeRecord;
|
|
}
|
|
|
|
/** Three weekly runs: A red twice, B red once, C never, a periodic case red (not gate), a retired id red. */
|
|
function history(): TrialOutcomeRecord[] {
|
|
const out: TrialOutcomeRecord[] = [];
|
|
for (const run of [1, 2, 3]) {
|
|
out.push(record(A, run, run !== 3), record(B, run, run === 2), record(C, run, false));
|
|
out.push(record('retired-case-id', run, true), record('some-periodic-case', run, true, { tier: 'periodic', lane: 'periodic' }));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe('picking cases from gate history', () => {
|
|
test('ranks live, non-quarantined gate cases with a red by their cost to the all-green probability, and picks K', () => {
|
|
const withQuarantined = [...history(), ...[1, 2, 3].map(run => record(Q, run, true))];
|
|
expect(planSweep(withQuarantined, 3, { quarantine: {} }).picked.map(c => c.case)).toEqual([Q, A, B]);
|
|
const plan = planSweep(withQuarantined, 2, { quarantine: { [Q]: {} } });
|
|
expect(plan.candidates.map(c => c.case)).toEqual([A, B]);
|
|
expect(plan.picked.map(c => c.case)).toEqual([A, B]);
|
|
const [a, b] = plan.candidates;
|
|
expect(a).toMatchObject({ reds: 2, verdicts: 3, scoredTrials: 3, costPerTrialUsd: 1, kind: 'rule' });
|
|
expect(a!.passRate).toBeCloseTo(1 / 3, 5);
|
|
expect(a!.redRate).toBeGreaterThan(b!.redRate);
|
|
expect(a!.contribution).toBeGreaterThan(b!.contribution);
|
|
expect(plan.censusCases).toEqual([A, B, C, Q, 'retired-case-id'].sort());
|
|
const product = plan.censusCases.reduce((p, id) => p * (1 - plan.rates[id]!), 1);
|
|
expect(plan.allGreen).toBeCloseTo(product, 10);
|
|
expect(planSweep(history(), 1).picked.map(c => c.case)).toEqual([A]);
|
|
});
|
|
|
|
test('a measured case replaces its history rate; a behavior rate converts to the 3-trial panel red risk', () => {
|
|
expect(measuredRedRate('rule', 0, 10, 0)).toBe(0);
|
|
expect(measuredRedRate('rule', 1, 10, 0.05)).toBeCloseTo((1 + 4 * 0.05) / 14, 10);
|
|
const q = 2 / 16;
|
|
expect(measuredRedRate('behavior', 2, 12, 0)).toBeCloseTo(1 - ((1 - q) ** 3 + 3 * (1 - q) ** 2 * q), 10);
|
|
const plan = planSweep(history(), 2);
|
|
const after = predictedAllGreen(plan, { [A]: 0 })!;
|
|
expect(after).toBeCloseTo(plan.allGreen! / (1 - plan.rates[A]!), 10);
|
|
});
|
|
});
|
|
|
|
describe('the weekly cap', () => {
|
|
const report = (dir: string, id: string, startedAt: string, chargedUsd: number, dryRun = false) => {
|
|
fs.mkdirSync(path.join(dir, id), { recursive: true });
|
|
fs.writeFileSync(path.join(dir, id, 'sweep-report.json'), JSON.stringify({ schema: SWEEP_SCHEMA, sweepId: id, startedAt, chargedUsd, dryRun }));
|
|
};
|
|
test('sums the last 7 days of sweep reports once each, skipping dry runs, older reports and this sweep', () => {
|
|
const dir = tmp('sweep-prior-');
|
|
const now = Date.parse('2026-10-12T12:00:00Z');
|
|
report(dir, 'a', '2026-10-10T00:00:00Z', 40);
|
|
report(path.join(dir, 'copy'), 'a', '2026-10-10T00:00:00Z', 40);
|
|
report(dir, 'b', '2026-10-06T00:00:00Z', 25);
|
|
report(dir, 'old', '2026-10-01T00:00:00Z', 100);
|
|
report(dir, 'dry', '2026-10-11T00:00:00Z', 0, true);
|
|
report(dir, 'self', '2026-10-12T11:00:00Z', 9);
|
|
expect(weeklySpent([dir], now, 'self')).toBe(65);
|
|
expect(weeklySpent([path.join(dir, 'missing')], now)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('running the sweep with a fake runner', () => {
|
|
const fake = (fail: (r: TrialRequest) => boolean) => {
|
|
const calls: TrialRequest[] = [];
|
|
const runner = async (r: TrialRequest): Promise<TrialResult> => { calls.push(r); return fail(r) ? { passed: false, costUsd: 1, failureCause: 'assertion' } : { passed: true, costUsd: 1 }; };
|
|
return { calls, runner };
|
|
};
|
|
const base = (outDir: string, runner: (r: TrialRequest) => Promise<TrialResult>, extra: Partial<Parameters<typeof runSweep>[0]> = {}) => runSweep({
|
|
records: history(), historyScope: 'fixture history', k: 2, capUsd: 150, config: MEASURE_DEFAULTS, outDir, priorDirs: [], dryRun: false, parallel: 5,
|
|
ref: 'main', sha: 'a'.repeat(40), runner, runnerId: 'fake', preflight: () => ({ ok: true, detail: 'fake' }), identity: () => ({ tree: 'fixed' }),
|
|
evalDir: tmp('sweep-history-'), log: () => {}, now: Date.parse('2026-10-12T12:00:00Z'), ...extra,
|
|
});
|
|
|
|
test('measures each picked case at its kind\'s N with the bar, writes the report, and predicts the all-green probability after', async () => {
|
|
const outDir = tmp('sweep-out-');
|
|
const run = fake(r => r.caseId === A && r.trial <= 4);
|
|
const report = await base(outDir, run.runner);
|
|
expect(run.calls.filter(c => c.caseId === A)).toHaveLength(10);
|
|
expect(run.calls.filter(c => c.caseId === B)).toHaveLength(10);
|
|
expect(report.rows.map(r => [r.case, r.status, r.decision, `${r.measuredPasses}/${r.measuredTrials}`])).toEqual([[A, 'measured', 'BELOW', '6/10'], [B, 'measured', 'MEETS', '10/10']]);
|
|
expect(report).toMatchObject({ label: 'diagnostic', verdict: null, chargedUsd: 20, priorWeekUsd: 0, k: 2 });
|
|
expect(report.allGreenAfter).not.toBe(report.allGreenBefore);
|
|
const dir = path.join(outDir, `sweep-${report.sweepId}`);
|
|
const saved = JSON.parse(fs.readFileSync(path.join(dir, 'sweep-report.json'), 'utf8')) as SweepReport;
|
|
expect(saved.rows[0]!.captures).toBe(path.join(`sweep-${report.sweepId}`, A, 'baseline'));
|
|
expect(fs.existsSync(path.join(outDir, saved.rows[0]!.captures!, 't01', 'trial.json'))).toBe(true);
|
|
const md = fs.readFileSync(path.join(dir, 'sweep-report.md'), 'utf8');
|
|
expect(md).toContain('| # | Case | Kind | History rate | Measured | Decision | Cost (est / actual) | Captures |');
|
|
expect(md).toMatch(/Predicted gate all-green probability .*before \d+(\.\d)?% → after \d+(\.\d)?%/);
|
|
expect(md).toContain(`| 1 | ${A} | rule | 33.3% of 3 trials; 2/3 red verdicts`);
|
|
expect(md).toContain('never pushes or opens a pull request');
|
|
});
|
|
|
|
test('stops at the weekly cap: a partly funded case stops at the budget, the rest are skipped and listed', async () => {
|
|
const outDir = tmp('sweep-cap-');
|
|
const prior = tmp('sweep-cap-prior-');
|
|
fs.writeFileSync(path.join(prior, 'sweep-report.json'), JSON.stringify({ schema: SWEEP_SCHEMA, sweepId: 'earlier', startedAt: '2026-10-08T00:00:00Z', chargedUsd: 136, dryRun: false }));
|
|
const run = fake(() => false);
|
|
const report = await base(outDir, run.runner, { priorDirs: [prior], capUsd: 150, k: 3, records: history().map(r => r.case === C && r.run_id === '1' ? record(C, 1, true) : r) });
|
|
expect(report.priorWeekUsd).toBe(136);
|
|
// B and C tie (one red in three); ties fall to the case id, so C ranks before B.
|
|
expect(report.rows.map(r => [r.case, r.status])).toEqual([[A, 'measured'], [C, 'budget_exhausted'], [B, 'skipped']]);
|
|
// A partly funded case reports the trials that ran, not the planned N.
|
|
expect([report.rows[1]!.measuredPasses, report.rows[1]!.measuredTrials]).toEqual([4, 4]);
|
|
expect(run.calls).toHaveLength(14);
|
|
expect(report.chargedUsd).toBe(14);
|
|
expect(report.rows[2]!.note).toContain('weekly cap $150 exhausted');
|
|
expect(fs.readFileSync(path.join(outDir, `sweep-${report.sweepId}`, 'sweep-report.md'), 'utf8')).toContain(`- ${B}: weekly cap $150 exhausted`);
|
|
});
|
|
|
|
test('a dry run plans and estimates without running anything or charging the cap', async () => {
|
|
const outDir = tmp('sweep-dry-');
|
|
const run = fake(() => false);
|
|
const report = await base(outDir, run.runner, { dryRun: true });
|
|
expect(run.calls).toHaveLength(0);
|
|
expect(report.rows.map(r => [r.case, r.status, r.estimatedUsd])).toEqual([[A, 'planned', 10], [B, 'planned', 10]]);
|
|
expect(report.chargedUsd).toBe(0);
|
|
expect(weeklySpent([outDir], Date.parse('2026-10-12T13:00:00Z'))).toBe(0);
|
|
});
|
|
|
|
test('a case whose selection preflight fails is skipped before any paid call', async () => {
|
|
const run = fake(() => false);
|
|
const report = await base(tmp('sweep-preflight-'), run.runner, { k: 1, preflight: () => ({ ok: false, detail: '--list planned nothing' }) });
|
|
expect(report.rows[0]).toMatchObject({ status: 'skipped', note: 'selection preflight failed (no paid call made): --list planned nothing' });
|
|
expect(run.calls).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('ship-measure sweep CLI ($0)', () => {
|
|
const historyDir = () => {
|
|
const dir = tmp('sweep-cli-history-');
|
|
fs.writeFileSync(path.join(dir, 'trial-outcomes.jsonl'), `${history().map(r => JSON.stringify(r)).join('\n')}\n`);
|
|
return dir;
|
|
};
|
|
const cli = (cwd: string, ...args: string[]) => spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/ship-measure.ts'), 'sweep', ...args], {
|
|
cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, GSTACK_STATE_ROOT: tmp('sweep-cli-state-'), GITHUB_STEP_SUMMARY: '' },
|
|
});
|
|
|
|
test('--dry-run reads local history, prints the plan and the before probability, and spends nothing', () => {
|
|
const cwd = tmp('sweep-cli-');
|
|
const r = cli(cwd, '--history-dir', historyDir(), '--k', '2', '--dry-run', '--out', path.join(cwd, 'out'));
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toContain('(dry run: nothing ran, $0)');
|
|
expect(r.stdout).toContain(`| 1 | ${A} | rule |`);
|
|
expect(r.stdout).toContain('would measure 10 trials');
|
|
});
|
|
|
|
test('a fake single-case command runs the whole sweep end to end at $0', () => {
|
|
const cwd = tmp('sweep-cli-fake-');
|
|
// A Bun script, not a shell stub, so the fake runner also executes on Windows.
|
|
const script = path.join(cwd, 'fake-trial.ts');
|
|
fs.writeFileSync(script, 'console.log("cost_usd: 0");\nif (process.env.GSTACK_SHIP_MEASURE_TRIAL === "3") { console.log("failure_cause: assertion"); process.exit(1); }\nconsole.log(`PASS ${process.argv[2]}`);\n');
|
|
const bun = /\s/.test(process.execPath) ? 'bun' : process.execPath;
|
|
const r = cli(cwd, '--history-dir', historyDir(), '--k', '1', '--cap-usd', '1', '--command', `${bun} ${script} {case}`, '--jobs', '5', '--out', path.join(cwd, 'out'));
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toContain(`DIAGNOSTIC ${A} baseline: observed 9/10 trials (baseline); MEETS`);
|
|
expect(r.stdout).toMatch(new RegExp(`\\| 1 \\| ${A} \\| rule \\| [^|]+ \\| 9/10 \\| MEETS \\|`));
|
|
});
|
|
|
|
test('history that cannot be read fails closed with nothing measured', () => {
|
|
const cwd = tmp('sweep-cli-nohistory-');
|
|
const r = spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/ship-measure.ts'), 'sweep', '--dry-run', '--repo', 'garrytan/no-such-repo-xyz', '--out', path.join(cwd, 'out')], {
|
|
cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, PATH: path.dirname(process.execPath), GSTACK_STATE_ROOT: tmp('sweep-cli-state-') },
|
|
});
|
|
expect(r.status).toBe(1);
|
|
expect(r.stderr).toContain('pass-rate history unavailable');
|
|
});
|
|
});
|