Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
2d746ac
[jwbron/live-eval-corpus] review: live-enabled corpus format and ten …
jwbron Jul 9, 2026
6727eb6
[jwbron/live-eval-producer-staging] review: live-producer prompt extr…
jwbron Jul 9, 2026
5c00cf3
[jwbron/live-eval-producer-staging] review: the live producer and SDK…
jwbron Jul 9, 2026
52073ef
[jwbron/live-eval-ab-runner] review: the live A/B runner (phase 3)
jwbron Jul 9, 2026
93770e6
[jwbron/live-eval-ab-ci] review: per-PR live A/B workflow (phase 4)
jwbron Jul 9, 2026
30e7527
[jwbron/live-eval-corpus] review: exclude eval-corpus trees from lint…
jwbron Jul 9, 2026
19f06cb
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
d1b5288
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
254dfc4
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
917cc11
[jwbron/live-eval-producer-staging] review: namespace live finding id…
jwbron Jul 9, 2026
0ee295a
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
58cd4ed
[jwbron/live-eval-ab-runner] review: judge failures degrade the A/B r…
jwbron Jul 9, 2026
b19b08d
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
bced291
[jwbron/review-trial-skill] review: add the review-trial skill (live …
jwbron Jul 9, 2026
da1115c
[tmp-refresh] Merge remote-tracking branch 'origin/main' into tmp-ref…
jwbron Jul 9, 2026
a6883be
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-c…
jwbron Jul 9, 2026
0d02672
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-p…
jwbron Jul 9, 2026
93d8dec
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-a…
jwbron Jul 9, 2026
e3b34eb
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-a…
jwbron Jul 9, 2026
491a983
[jwbron/review-rereview-accountability] review: re-review accountabil…
jwbron Jul 9, 2026
7b5318c
[jwbron/review-out-of-lane] review: hand off out-of-lane observations…
jwbron Jul 9, 2026
5dd182b
[jwbron/review-out-artifact-upload] review: fix the out/ artifact upl…
jwbron Jul 9, 2026
92bffa2
[jwbron/review-rereview-accountability] review: prettier-format the r…
jwbron Jul 9, 2026
2be8ede
[jwbron/review-out-of-lane] Merge branch 'jwbron/review-rereview-acco…
jwbron Jul 9, 2026
3a8fc5c
[jwbron/review-out-of-lane] review: prettier-format finding-schema
jwbron Jul 9, 2026
813767c
[jwbron/review-dispatch-tax] review: dedupe lens discipline snippets …
jwbron Jul 9, 2026
2812679
[jwbron/live-eval-corpus] review: route the specialist lens on each l…
jwbron Jul 9, 2026
2ce35a0
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
c0fece2
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
e02ac40
[jwbron/live-eval-ab-runner] review: carry agent-failure reasons into…
jwbron Jul 9, 2026
d225bd4
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
de239ee
[jwbron/rereview-mode-dial] review: the re-review mode dial: ROUTING …
jwbron Jul 9, 2026
1329297
[jwbron/rereview-mode-dial] review: expose keptBlockingCount from the…
jwbron Jul 9, 2026
45c6f6e
[jwbron/rereview-mode-dial] review: wire the re-review mode dial into…
jwbron Jul 9, 2026
976b925
[jwbron/rereview-mode-dial] review: prettier-format the mode-dial fil…
jwbron Jul 9, 2026
cbc838d
[jwbron/live-eval-ab-runner] review: close every A/B report with a pe…
jwbron Jul 9, 2026
63097f1
[jwbron/live-eval-corpus] Merge remote-tracking branch 'origin/jwbron…
jwbron Jul 9, 2026
9012508
[jwbron/live-eval-producer-staging] Merge remote-tracking branch 'ori…
jwbron Jul 9, 2026
25133b4
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
0a3d212
[jwbron/live-eval-ab-runner] Merge remote-tracking branch 'origin/jwb…
jwbron Jul 9, 2026
a547972
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
391151b
[jwbron/live-eval-ab-ci] Merge remote-tracking branch 'origin/jwbron/…
jwbron Jul 9, 2026
d2c4c70
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
082f580
[jwbron/rereview-live-corpus] review: re-review coverage for the eval…
jwbron Jul 9, 2026
48cdc39
[jwbron/rereview-live-corpus] review: the re-review mode sweep and th…
jwbron Jul 9, 2026
284fa40
[jwbron/rereview-live-corpus] review: dispatchable CI home for the re…
jwbron Jul 9, 2026
7f9ae36
[jwbron/rereview-live-corpus] review: manual trigger surface for ever…
jwbron Jul 9, 2026
996766f
[jwbron/live-eval-ab-runner] review: identity short-circuit, gate-fli…
jwbron Jul 10, 2026
fb81be8
[jwbron/live-eval-ab-runner] review: judge economics (Haiku pin, retr…
jwbron Jul 10, 2026
a659be8
[jwbron/live-eval-ab-ci] review: document why the baseline is the bas…
jwbron Jul 10, 2026
b7c3786
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 10, 2026
541e413
[jwbron/review-trial-skill] Merge branch 'jwbron/live-eval-ab-ci' int…
jwbron Jul 10, 2026
fd42efd
[jwbron/review-out-artifact-upload] Merge branch 'jwbron/review-trial…
jwbron Jul 10, 2026
6d3459a
[jwbron/review-rereview-accountability] Merge branch 'jwbron/review-o…
jwbron Jul 10, 2026
1438a67
[jwbron/review-out-of-lane] Merge branch 'jwbron/review-rereview-acco…
jwbron Jul 10, 2026
98157c2
[jwbron/review-dispatch-tax] Merge branch 'jwbron/review-out-of-lane'…
jwbron Jul 10, 2026
bbb624c
[jwbron/rereview-mode-dial] Merge branch 'jwbron/review-dispatch-tax'…
jwbron Jul 10, 2026
4d1ac62
[jwbron/rereview-live-corpus] Merge branch 'jwbron/rereview-mode-dial…
jwbron Jul 10, 2026
193ae69
[jwbron/live-eval-ab-runner] review: type the judge response via the …
jwbron Jul 10, 2026
7a2065c
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 10, 2026
00ce9d4
[jwbron/review-trial-skill] Merge branch 'jwbron/live-eval-ab-ci' int…
jwbron Jul 10, 2026
4bd445c
[jwbron/review-out-artifact-upload] Merge branch 'jwbron/review-trial…
jwbron Jul 10, 2026
da1e1db
[jwbron/review-rereview-accountability] Merge branch 'jwbron/review-o…
jwbron Jul 10, 2026
1bc9e08
[jwbron/review-out-of-lane] Merge branch 'jwbron/review-rereview-acco…
jwbron Jul 10, 2026
0123a92
[jwbron/review-dispatch-tax] Merge branch 'jwbron/review-out-of-lane'…
jwbron Jul 10, 2026
64f8115
[jwbron/rereview-mode-dial] Merge branch 'jwbron/review-dispatch-tax'…
jwbron Jul 10, 2026
5631477
[jwbron/rereview-live-corpus] Merge branch 'jwbron/rereview-mode-dial…
jwbron Jul 10, 2026
6a1a8bc
[jwbron/eval-measurement-tool] review: repeat aggregation, --repeats …
jwbron Jul 10, 2026
1dcc7c3
[jwbron/eval-measurement-tool] review: implement the fallback match a…
jwbron Jul 10, 2026
18eda9f
[jwbron/eval-measurement-tool] review: split live-ab report shapes/re…
jwbron Jul 10, 2026
26c8526
[jwbron/eval-measurement-tool] review: checkpoint the repeats artifac…
jwbron Jul 10, 2026
a63648f
[jwbron/eval-measurement-tool] review: publish the measured noise flo…
jwbron Jul 10, 2026
149bf14
[jwbron/eval-measurement-tool] review: raise the drift run's default …
jwbron Jul 10, 2026
52c954d
[jwbron/eval-measurement-tool] review: cover the arbiter, checkpoints…
jwbron Jul 10, 2026
de525c3
[jwbron/eval-measurement-tool] review: eval operator README; drift re…
jwbron Jul 10, 2026
64f5b14
[jwbron/eval-measurement-tool] review: link the eval operator guide f…
jwbron Jul 10, 2026
2369b86
[jwbron/eval-measurement-tool] review: ruler provenance, honest noise…
jwbron Jul 10, 2026
59f78c7
[jwbron/anchor-snap-provenance] review: anchor-snap fallback in the c…
jwbron Jul 10, 2026
3f96569
[jwbron/anchor-snap-provenance] review: pin anchor-snap in a determin…
jwbron Jul 10, 2026
820c735
[jwbron/eval-case-selection] review: exact --cases selection, fail-fa…
jwbron Jul 10, 2026
e0b347d
[jwbron/eval-case-selection] Merge remote-tracking branch 'origin/mai…
jwbron Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/review-eval-case-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"review": patch
---

Fail-fast case selection in the live A/B runner. An explicit `--cases` list is now an exact selection: it bypasses `--smoke-only` (which scopes unscoped runs only), preserves the requested order, runs duplicate ids once, and throws before any model spend when an id matches no live case. Previously the smoke scope filtered the corpus before the case filter, so a dispatch naming a non-smoke case silently dropped it: the 2026-07-10 anchor-snap powered run named two cases and the paid report covered one without saying so, and a typo'd case id would shrink a measurement the same way. The workflow needs no change; it already passes both flags and the case list now wins.
4 changes: 2 additions & 2 deletions workflows/review/eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pnpm test --run workflows/review/eval/
```sh
pnpm dlx tsx workflows/review/eval/live-ab.ts \
[--base-ref <ref>] # baseline review.md source (default: merge-base with origin/main)
[--cases <id,id>] # subset of live cases
[--smoke-only] # only live cases also tagged smoke (the per-PR default)
[--cases <id,id>] # EXACT selection: bypasses --smoke-only; unknown ids fail before spend
[--smoke-only] # only live cases also tagged smoke (the per-PR default; ignored under --cases)
[--repeats <n>] # n runs per arm in one invocation; pooled pass-rate report
[--force-arms] # run identical arms anyway (wobble control / noise floor)
[--max-usd <n>] # hard budget across all arm-runs (default 40)
Expand Down
47 changes: 47 additions & 0 deletions workflows/review/eval/live-ab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
renderMultiMarkdownReport,
retryGateFlips,
runArm,
selectCases,
type AbReport,
type ArmProduce,
type ArmRunReport,
Expand Down Expand Up @@ -125,6 +126,52 @@ const produceMiss: ArmProduce = async () => ({
],
});

describe("selectCases", () => {
const corpus = () => [
liveCase("smoke-a", {tags: ["live", "smoke"]}),
liveCase("smoke-b", {tags: ["live", "smoke"]}),
liveCase("holdout-c", {tags: ["live", "holdout"]}),
];

it("scopes unscoped runs by the smoke tag, or not at all", () => {
expect(
selectCases(corpus(), {smokeOnly: true}).map((c) => c.id),
).toEqual(["smoke-a", "smoke-b"]);
expect(
selectCases(corpus(), {smokeOnly: false}).map((c) => c.id),
).toEqual(["smoke-a", "smoke-b", "holdout-c"]);
});

it("treats an explicit case list as exact: it bypasses the smoke scope", () => {
// The 2026-07-10 footgun: a powered dispatch named a non-smoke case
// and the smoke scope silently dropped it from a paid measurement.
expect(
selectCases(corpus(), {
smokeOnly: true,
caseFilter: ["smoke-a", "holdout-c"],
}).map((c) => c.id),
).toEqual(["smoke-a", "holdout-c"]);
});

it("preserves requested order and runs duplicate ids once", () => {
expect(
selectCases(corpus(), {
smokeOnly: false,
caseFilter: ["holdout-c", "smoke-a", "holdout-c"],
}).map((c) => c.id),
).toEqual(["holdout-c", "smoke-a"]);
});

it("fails before any spend on an id that matches no live case", () => {
expect(() =>
selectCases(corpus(), {
smokeOnly: false,
caseFilter: ["smoke-a", "not-a-case"],
}),
).toThrow(/not in the live corpus: not-a-case/);
});
});

describe("runArm", () => {
it("scores cases, accounts cost, and reports agent failures", async () => {
const report = await runArm(
Expand Down
70 changes: 58 additions & 12 deletions workflows/review/eval/live-ab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
* pnpm dlx tsx workflows/review/eval/live-ab.ts
* [--base-ref <ref>] baseline review.md source (default: merge-base
* of HEAD and origin/main)
* [--cases <id,id,...>] subset of live cases (default: every live case)
* [--cases <id,id,...>] EXACT selection of live cases: bypasses
* --smoke-only, and an id matching no live case
* fails the run before any model spend
* [--smoke-only] only live cases also tagged smoke (the per-PR
* default in CI; a full-eval label lifts it)
* default in CI; a full-eval label lifts it);
* scopes unscoped runs only — ignored when
* --cases names the selection
* [--max-usd <n>] total hard budget across both arms (default 40)
* [--no-judge] skip judge quality scoring
* [--no-match-arbiter] deterministic spec matching only (skip the
Expand Down Expand Up @@ -107,6 +111,50 @@ export type {
MultiAbReport,
} from "./live-ab-report";

/* -------------------------------------------------------------------------- */
/* Case selection */
/* -------------------------------------------------------------------------- */

/**
* Select the cases a run scores. An explicit case list is an EXACT
* selection: it bypasses the smoke scope (naming a non-smoke case in
* `--cases` selects it; the smoke tag scopes only unscoped runs) and throws
* on any id that matches no live case. A typo'd or non-live id must fail
* the dispatch BEFORE any model spend — the alternative already happened:
* the 2026-07-10 anchor-snap powered run named two cases, the smoke scope
* silently dropped the non-smoke one, and the paid report covered half the
* measurement without saying so. Requested order is preserved; duplicate
* ids run once.
*/
export const selectCases = (
allLive: CorpusCase[],
options: {smokeOnly: boolean; caseFilter?: string[]},
): CorpusCase[] => {
const {caseFilter} = options;
if (caseFilter !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The empty-caseFilter ([]) branch is untested. It is distinct from undefined — it takes this if branch, bypasses the smoke scope, and returns [] — and is reachable in practice, since main()'s new --cases trim/filter turns --cases "" or --cases ,, into []. A future refactor that treated [] as "no filter" and fell back to the full corpus would defeat the fail-fast intent, and no current test would catch it. Consider adding to the selectCases block:

it("an empty case list selects nothing, even under a smoke scope", () => {
    expect(selectCases(corpus(), {smokeOnly: true, caseFilter: []})).toEqual([]);
});

const byId = new Map(allLive.map((c) => [c.id, c]));
const unknown = caseFilter.filter((id) => !byId.has(id));
if (unknown.length > 0) {
throw new Error(
`--cases: not in the live corpus: ${unknown.join(", ")} ` +
`(live case ids: ${allLive.map((c) => c.id).join(", ")})`,
);
}
const seen = new Set<string>();
return caseFilter.flatMap((id) => {
if (seen.has(id)) {
return [];
}
seen.add(id);
const found = byId.get(id);
return found === undefined ? [] : [found];
});
}
return options.smokeOnly
? allLive.filter((c) => c.tags.includes(SMOKE_TAG))
: [...allLive];
};

/* -------------------------------------------------------------------------- */
/* One arm */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -408,7 +456,10 @@ const main = async (): Promise<void> => {
const outPath = argValue("--out") ?? "out/live-ab-report.json";
const stageRoot =
argValue("--stage-root") ?? mkdtempSync(`${tmpdir()}/review-ab-`);
const caseFilter = argValue("--cases")?.split(",");
const caseFilter = argValue("--cases")
?.split(",")
.map((id) => id.trim())
.filter((id) => id !== "");
const withJudge = !process.argv.includes("--no-judge");

const reviewMdPath = "workflows/review/review.md";
Expand Down Expand Up @@ -452,15 +503,10 @@ const main = async (): Promise<void> => {
return;
}

const allCases = loadLiveCorpus().filter(
(c) =>
!process.argv.includes("--smoke-only") ||
c.tags.includes(SMOKE_TAG),
);
const cases =
caseFilter === undefined
? allCases
: allCases.filter((c) => caseFilter.includes(c.id));
const cases = selectCases(loadLiveCorpus(), {
smokeOnly: process.argv.includes("--smoke-only"),
...(caseFilter !== undefined ? {caseFilter} : {}),
});
if (cases.length === 0) {
throw new Error("no live cases selected");
}
Expand Down
Loading