Skip to content

review: make the live eval a measurement tool (repeat aggregation, powered runs, drift watch)#252

Open
jwbron wants to merge 13 commits into
jwbron/rereview-live-corpusfrom
jwbron/eval-measurement-tool
Open

review: make the live eval a measurement tool (repeat aggregation, powered runs, drift watch)#252
jwbron wants to merge 13 commits into
jwbron/rereview-live-corpusfrom
jwbron/eval-measurement-tool

Conversation

@jwbron

@jwbron jwbron commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Lands the tuning memo's rev-2 measurement items (memo: "Dialing in the evals", rev 2). The per-PR A/B stays a tripwire; this adds the machinery that turns repeats into resolution.

What's here

Repeat-aggregation report (eval/aggregate.ts, memo item 3). Deterministic core + thin CLI: pools N live-ab-report.json artifacts (local paths or gh run ids) into per-case pass rates per arm with 95% Wilson intervals, pooled recall/verdict/noise rows, and true-miss vs found-but-dropped classification per gate bucket. Warns when a pool mixes review.md shas. Validated against the three 07-10 cumulative dispatches (29065968954 / 29065976210 / 29065977160): reproduces the memo's hand-computed numbers exactly (baseline 21/24 specs, 28/30 verdicts; candidate 22/24, 28/30).

--repeats <n> in live-ab.ts (memo sizing section). One dispatch runs the selected cases n times per arm (budget split per arm-run, fresh staging per repeat) and reports through the aggregation core instead of single-run percentages; this is the ~$29 targeted-repeats powered run (--cases <two anchor-fragile cases> --repeats 10). The adversarial gate is decided by strict majority across repeats; the single-run best-of-three retry is skipped since the repeat structure buys the same flake evidence. The A/B workflow exposes cases / repeats / force_arms dispatch inputs.

Scheduled drift watch (review-eval-drift.yml). Weekly cron: full live corpus x3 repeats, both arms pinned to main's review.md (--force-arms past the identity short-circuit), aggregate published to job summary + artifact. Identical arms mean 6 arm-samples of the same prompt per run, so every drift run also refreshes the noise-floor bands, which the aggregate renders as data (memo item 6).

incident-sql-missing-index diagnosed and fixed (memo item 2). Read every recorded transcript in the wave + cumulative artifacts (28 arm-runs): the reviewer found the missing index in 28/28. The 8/16 catch rate was the spec accepting only the migration-file anchor while the reviewer anchored the same blocking finding at the hot query in src/models/order.ts (11 "misses") or had it provenance-dropped on an out-of-file mis-anchor (2, the adversarial-injection-approve pathology). Neither mechanism alternates nor lens routing were at fault; the case measured anchor-site preference. Specs gain altLocations (validated like the primary location; matcher, fallback filter, and drop classification all honor them) and the case accepts the query-site window. Replayed against all 28 recorded runs: 26 caught, 2 found-but-dropped, 0 true misses; the residual failure class is exactly the anchor-snap production defect, now correctly bucketed.

Notes

  • pnpm test --run (879 tests) and pnpm typecheck pass; eslint can't run in this worktree (dot-dir ignore), prettier applied.
  • The aggregate CLI degrades per source: an unreadable artifact is listed and skipped, never fatal.
  • Out of scope per the memo: the anchor-snap provenance-gate fallback (production reviewer change) and review.md semantics.

…powered runs, drift watch, sql-index case fix

The eval instrument's percentage deltas sit below the one-case noise floor;
this lands the memo's measurement items. eval/aggregate.ts pools N report
artifacts into per-case pass rates with Wilson intervals and pooled rows
(reproduces the 07-10 cumulative numbers exactly); live-ab.ts --repeats runs
one powered dispatch with a strict-majority adversarial gate; the weekly
review-eval-drift.yml workflow runs full corpus x3 on main and publishes the
aggregate; identical-arm pools render noise-floor bands as data.

incident-sql-missing-index diagnosed against all 28 recorded arm-runs: the
reviewer found the missing index every single time; the 8/16 catch rate was
the spec accepting only the migration-file anchor while the reviewer anchored
the same blocking finding at the hot query (11 runs) or had it provenance-
dropped on a mis-anchor (2 runs). Specs gain altLocations; the case accepts
the query-site anchor and its residual misses now classify found-but-dropped
(the anchor-snap defect class), not recall.
@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e48bc52

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

No reviewable delta: review.md is byte-identical in both arms (baseline origin/jwbron/rereview-live-corpus, sha 255373d9458d), so the extracted prompts and the orchestrator body match and no arms were run. Pass --force-arms for a deliberate wobble control.

@khan-actions-bot khan-actions-bot requested review from a team, kevinb-khan and somewhatabstract and removed request for a team July 10, 2026 04:31
…rbiter on the pinned Haiku snapshot

Fills the MatchFallback seam in live-match.ts: when a spec stays unmatched
after the deterministic pass and a posted candidate shares its file, a
pinned claude-haiku-4-5-20251001 answers one yes/no question (same defect,
same root cause?), biased to no since a false yes inflates recall. All seam
guard rails inherited: capped per case, same-file only, matches recorded
via 'fallback' for audit. API failures degrade to non-matches through
onError; both arms share the matcher so the A/B delta stays unbiased.
On by default in live-ab.ts; --no-match-arbiter restores deterministic-only
matching. This targets the 54-69% 'unmatched posted' readings, implausibly
high as true noise.
@khan-actions-bot khan-actions-bot requested a review from a team July 10, 2026 04:37
jwbron added 4 commits July 9, 2026 21:41
…nderers into live-ab-report.ts

CI's max-lines (1000) caught live-ab.ts at 1011 after the repeats and
arbiter work; this worktree cannot run eslint locally (dot-dir ignore).
The report types (ArmRunReport, AbReport, MultiAbReport, gate types) and
both markdown renderers move to a leaf module with no runner dependency;
live-ab.ts re-exports them so the import surface is unchanged.
…t after every repeat

A multi-repeat run carries tens of dollars of spend but wrote its artifact
only at the end, so a crash or cancellation on repeat n forfeited repeats
1..n-1 (the exact dies-with-nothing-emitted failure mode the plan forbids;
the workflow's always() artifact upload had nothing to grab). Each completed
repeat now overwrites the out path with the accumulated partial payload,
which aggregate.ts already pools via its repeats-field handling.
…or in the report footer

Bought per the memo's item 6: run 29069228968 (--force-arms --repeats 3,
full 14-case corpus, $58.71 under the $60 clamp) gives 6 arm-samples of
one review.md. Measured bands: must-catch recall 54-86%, verdict agreement
75-100%, noise 50-60%, judge quality 0.82-0.86. Every single-run report now
renders these as data with provenance, replacing prose guesswork; a delta
whose arms both sit inside a band is wobble, and the footer points at
--repeats for smaller effects. Pre-arbiter measurement; the weekly drift
run re-measures with the arbiter active.
…budget to $85

The noise-floor dispatch showed the live corpus is now 14 cases (the #251
golden re-review set), and the $60 default trimmed case-runs on both arms
(38 and 36 of 42; $58.71 spent at the clamp). $85 covers 6 full arm-runs
at the measured ~$10/arm-run with landing headroom, so the weekly aggregate
never carries budget-skip asymmetry.
@jwbron

jwbron commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Post-review additions on this branch since the opening description:

  • Fallback match arbiter (eval/match-arbiter.ts, pinned claude-haiku-4-5-20251001): fills the MatchFallback seam; unmatched specs only, same-file, capped 10/case, via: "fallback" audit trail, biased to refuse, degrades to non-match on API failure. On by default; --no-match-arbiter opts out.
  • Module split: report shapes/renderers moved to eval/live-ab-report.ts (CI max-lines caught live-ab.ts at 1011; this worktree cannot run eslint locally).
  • Per-repeat artifact checkpoints: a multi-repeat run now overwrites its out path after each repeat, so cancellation/crash cannot forfeit completed repeats (the always() artifact upload always has something to grab).
  • Measured noise floor published: run 29069228968 (--force-arms --repeats 3, full 14-case corpus, $58.71 at the $60 clamp; 6 arm-samples of one prompt) measured recall 54-86%, verdict agreement 75-100%, noise 50-60%, judge quality 0.82-0.86. These bands render as data in every single-run report footer with provenance. The run also live-validated --repeats + the new dispatch inputs end to end.
  • Drift default budget raised to $85: the corpus is 14 live cases on this stack and $60 trimmed case-runs (38/36 of 42).

Not landed, deliberately: baseline-arm reuse. Actions cache entries are unreadable across sibling/child PR runs, which is exactly the stacked topology carrying the 44% duplicate spend; the memo (rev 3) records the analysis and three candidate alternatives.

jwbron added 3 commits July 10, 2026 11:06
…port opens a visibility PR

workflows/review/eval/README.md is the guide an agent or engineer needs to
run the eval system cold: the three tiers, every live-ab flag and CI entry
point, powered-run recipes with real costs, corpus-growth rules (target the
20-80% band), report-reading guidance anchored on the measured noise floor,
model pins, and the harness's historical limits (nothing before the
2026-07-08 structured-agent architecture runs under it).

The weekly drift run now also opens a PR committing the report markdown and
compact aggregate under .github/review-eval/drift/ (changeset-gate-excluded,
outside the A/B path filter): a job summary nobody opens is not a drift
watch, and merged report PRs accumulate an in-repo time series.
@jwbron

jwbron commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Main vs stack tip: the cumulative measurement, and how to read it

Pooled from the three 2026-07-10 full-corpus dispatches (29065968954 / 29065976210 / 29065977160; baseline origin/main review.md e54d8145, candidate stack-top review.md e403408c, 3 repeats per arm, $42.83; table reproduced by aggregate.ts <run-id> <run-id> <run-id>):

Metric main 95% CI stack tip 95% CI
Must-catch recall 21/24 (88%) 69-96% 22/24 (92%) 74-98%
Verdict agreement 28/30 (93%) 79-98% 28/30 (93%) 79-98%
Noise (unmatched posted) 24/45 (53%) 39-67% 23/45 (51%) 37-65%
Misses (true / dropped) 1 true / 2 provenance 0 true / 2 provenance
Judge mean quality 0.91 0.90

Verdict: recall-neutral, which is the designed outcome. Batch two is process and output-quality work (accountability, rendering, re-review machinery); the A/B's job for that class of change is to prove nothing silently regressed across a heavy prompt rework, and it did: the candidate arm has zero true misses. All movement sits inside the measured noise floor (recall wobbles 54-86% on byte-identical arms; see the report footer this PR adds).

What the run actually taught us:

  • The instrument was undercounting the reviewer. Both weak cases were ruler error or gate pathology, not detection failure: the reviewer found the sql-index defect in 28/28 recorded runs (this PR fixes that spec via altLocations) and the injected auth defect in every adversarial run; what varied was anchor placement.
  • Anchor-snap is where the real gain is waiting. Every observed drop (both arms, and production main itself) is right-file, right-mechanism, wrong-line, and each golden-request-changes-authz drop flips a verdict. Expected gain: +1-2 pooled specs and most of the verdict wobble, priceable at ~$29 with cases=adversarial-injection-approve,golden-request-changes-authz repeats=10.
  • The discrimination band has moved to the re-review cases. On the 14-case corpus the identical-arm run pooled ~71% recall vs ~90% on these 10 cases: the review: re-review coverage for the eval corpus (open-PR cases + lifecycle chain) #251 golden re-review lifecycle set is where recall-affecting work (mode dial, reconciliation) will show real movement next.

Reading future reports: the next drift runs will show recall and noise 'improving' because this PR fixed the ruler (altLocations) and de-noises matching (arbiter), not because the reviewer changed. Compare like with like via the review.md shas and the noise-floor provenance in each report.

…-floor statistics, rolling repeat budgets

Findings from an adversarial pass over the instrument's own claims:

Reports now stamp their ruler (matcher configuration + a content hash of
the loaded corpus); the aggregate prints it, warns when a pool mixes
rulers, and the drift series stays interpretable across instrument
upgrades like the arbiter default or corpus growth, which move every rate
without the reviewer changing.

Noise-floor bands gain SD (min/max are extreme-value statistics that only
widen as samples accumulate; mean +/- sd is the stable band) and a loud
case-asymmetry warning: the 2026-07-10 measurement had budget skips, so
its published v1 bands fold case-mix variance in on top of wobble; the
footer constants and provenance string now say so.

Repeat budgets roll: each arm-run's slice is the remaining budget over the
remaining arm-runs, so cheap early arms donate headroom forward instead of
stranding it (fixed slices caused the 38/36-of-42 skip asymmetry that
contaminated the noise-floor run). README gains a statistical-honesty
section: clustered-interval optimism, the repeats-gate relaxation, and the
arbiter's uncalibrated refuse bias.
@jwbron

jwbron commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

One more commit (2369b86) from an adversarial pass over the instrument's own claims:

  • Ruler provenance: every report now stamps its matcher configuration and a corpus content hash; the aggregate prints the ruler and warns when a pool mixes rulers. Without this, the drift series would show an unexplained step change whenever the instrument itself changes (arbiter default, corpus growth).
  • Honest noise-floor statistics: bands now carry SD alongside min/max (min/max are extreme-value stats that only widen as samples accumulate), and identical-arm pools whose samples scored unequal case sets get a loud warning. The published v1 bands from run 29069228968 are so annotated: budget skips (38/36 of 42 case-runs) mean they fold case-mix variance in on top of wobble. Recall sd is 10 points; the 32-point min/max spread overstated pure wobble.
  • Rolling repeat budgets: each arm-run's slice is now remaining-budget over remaining-arm-runs, so cheap early arms donate headroom forward; fixed slices were what caused the skip asymmetry above.
  • README statistical-honesty section: clustered-interval optimism (pooled CIs are a lower bound on uncertainty), the repeats-gate being more permissive than single-run best-of-three (deliberate, but a relaxation reviewers should sign off on), and the arbiter's refuse bias being uncalibrated (audit via: "fallback" on close calls).

Known-open items live in the tuning memo rather than this stack: arbiter false-positive calibration, the gate-policy decision, a post-merge validation dispatch of the drift PR flow, and a clean re-measurement of the noise floor at the $85 budget.

@github-actions github-actions Bot left a comment

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.

Changes requested — see inline comments.

Comment thread workflows/review/eval/live-ab.ts Outdated
* prose. The weekly drift run re-measures these bands (with the arbiter
* active); update the constants when they move materially.
*/
export const MEASURED_NOISE_FLOOR = {

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.

thought (non-blocking): MEASURED_NOISE_FLOOR's provenance says "pre-arbiter", but this PR makes the Haiku match arbiter the default matcher, so every single-run report footer prices arbiter-era deltas against pre-arbiter wobble bands until someone hand-edits the constant. Consider warning when the renderer's provenance.matcher differs from the noise floor's ruler, or sourcing the bands from the latest committed drift aggregate.

# the memo's answer: the FULL live corpus, 3 repeats per arm, both arms
# pinned to main's review.md (`--force-arms` past the identity
# short-circuit), aggregated into per-case pass rates with binomial
# intervals. Week-over-week comparison happens across artifacts; within one

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 drift watch emits weekly snapshots but nothing computes the week-over-week comparison — the header notes it "happens across artifacts" (i.e. by a human), so a slow recall regression on main would sail under the within-run adversarial gate. aggregate.ts already accepts run ids, so feeding the current run plus the last N committed drift JSONs through it would render prior-week bands next to this week's and make the watch self-comparing.

STAMP="$(date -u +%Y-%m-%d)"
DIR=".github/review-eval/drift"
BRANCH="drift-report/${STAMP}-${{ github.run_id }}"
mkdir -p "$DIR"

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.

note (non-blocking): This workflow grants contents: write + pull-requests: write on a schedule trigger and opens a PR on every run (if: always()). The interpolations are all repo-controlled, so there is no injection surface — flagging only for visibility that the write-scope and the per-run branch/PR accumulation are intended.

// Fallback: only candidates sharing a spec file, in posted order.
for (const candidate of posted) {
if (claimed.has(candidate.id) || candidate.path !== spec.path) {
if (claimed.has(candidate.id) || !onSpecFile(candidate, spec)) {

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): onSpecFile's new altLocations awareness (this fallback gate and the drop classifier at line 254) is only exercised via anchorAgrees; every matchCase test uses a candidate on the primary spec path. A spec whose real finding lands on the alt file only would be scored a true miss rather than a fallback/found-but-dropped catch, and that path is unverified. Consider a matchCase test with an alt-file candidate.

…array, not the scalar count

The per-repeat checkpoint wrote {repeats: <n>} where the recovery path
in aggregate.ts expects {repeats: [reports]}; a crash mid-run therefore
forfeited every completed repeat, which is exactly what the checkpoint
exists to prevent. Write the reports array, matching the final artifact
shape, and lock the checkpoint-to-extractSamples contract with a test.
@github-actions

This comment has been minimized.

let armRunsDone = 0;
let spentUsd = 0;
const nextArmBudget = (): number =>
Math.max(0, maxUsd - spentUsd) / (totalArmRuns - armRunsDone);

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.

note (non-blocking): Judge and arbiter spend are excluded from --max-usd and scale with --repeats; gate-retry spend is also untracked. The rolling budget only accounts for arm-producer cost (spentUsd += report.usd), and nextArmBudget divides only the remaining maxUsd - spentUsd. Judge cost (liveJudgeModel, one paid call per finding per arm) and the Haiku arbiter cost (haikuMatchArbiter) never flow into spentUsd, so --repeats n multiplies that untracked spend across 2*n arm-runs. Gate-retry cost is likewise untracked (though it runs only on the single-run path). Consider folding these into spentUsd, or documenting that --max-usd bounds arm-producer spend only.

…; pin github-script to v8

Two review nitpicks from the stack: live-match.ts imported two types from
./corpus/loader in separate statements where the rest of the eval dir
merges them, and the sticky-comment step pinned github-script v7 while
node-ci.yml and release.yml pin v8 (ed597411). Align both.
@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (7 files)
File Reason
review-eval-ab.yml Adds workflow_dispatch inputs (cases/repeats/force_arms) wired to CLI flags and re-pins github-script to a v8 SHA; the permissions block is untouched.
review-eval-drift.yml New scheduled workflow with contents:write + pull-requests:write that opens a visibility PR on every run.
aggregate.ts New deterministic aggregator; its pooled pass-rates and intervals feed the drift watch and powered-run decisions.
live-ab-report.ts Report types and renderers extracted here from live-ab.ts; the single-run footer prices deltas against the noise-floor bands.
live-ab.ts The eval runner; owns the rolling --max-usd budget, the --repeats loop, and checkpointing, and parses the new CLI flags.
live-match.ts The matcher; altLocations now widens both anchor agreement and the fallback file gate.
match-arbiter.ts The Haiku match arbiter, now the default matcher; degrades to a non-match on API error.

Common patterns

4 files: altLocations support — LiveDefectSpec gains an altLocations field (type + parser in live.ts, matcher in live-match.ts), validated in loader.test.ts and used in the smoke case fixture.

2 files: Report types and renderers extracted from live-ab.ts into the new live-ab-report.ts leaf module; live-ab.ts re-exports them to preserve the import surface.

2 files: One-line re-export propagation — LiveSpecLocation added to the loader barrel and the altLocations field added to the smoke case.json.

Excluded from review (6 files)

Not individually reviewed — generated, formatting-only, or fully explained by a common pattern above:

  • .changeset/review-eval-measurement.md — formatting-only
  • workflows/review/README.md — formatting-only
  • workflows/review/eval/README.md — formatting-only
  • workflows/review/eval/corpus/loader.ts — pattern-only
  • workflows/review/eval/corpus/smoke/incident-sql-missing-index/case.json — pattern-only
  • workflows/review/eval/live-match.test.ts — pattern-only

- name: Post the sticky PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8

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.

note (non-blocking): This v8 SHA matches the pin already used by node-ci.yml and release.yml, so the alignment is correct. Heads up that .github/workflows/review-rereview-sweep.yml:79 is now the last workflow still on the v7 SHA — a trivial follow-up would bring the repo to a single pinned version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant