-
Notifications
You must be signed in to change notification settings - Fork 1
Evaluation Methodology
Normative version, with thresholds, bucket shares, and the full metric list:
docs/eval-methodology.md.
The tempting setup is: write some deliberately flaky tests, hand-label them, run the classifier, report accuracy. It would report something like 96%.
That number would be worthless. The fixtures were authored to be classifiable, by the same person who wrote the rubric and the prompt. A test that fails because the author put an assertion immediately after a drag event with no wait is identifiable from the stack trace alone — no language model required.
Any AI project can produce a high number this way. Four mechanisms are what make these numbers mean something.
A deliberately simple heuristic — roughly sixty lines — classifies the same fixtures:
no assertion reached (crash / bind / install) → environment
the commit changes any non-test source file → app_code
locator or timeout error, not a value → test_code
otherwise → app_code
The second rule says any changed non-test source path, not "the file under test". A heuristic cannot map a test to the implementation it exercises, and the looser wording pointed in two opposite directions at once.
Every agent number is reported next to it, and the delta is the headline, not the absolute.
Two outcomes, both fine. The agent wins → there is evidence the model earns its cost. The baseline wins → that is a genuinely interesting result, it goes in the README, and the project demonstrates something rarer than a working classifier.
The baseline has a second job: on fork pull requests there is no API key, so it is the only classifier available. That forces it to be good enough for a human to read, which also makes it a fair control rather than a straw man.
Against the 33 committed fixtures, with 95% Wilson intervals:
| Metric | Baseline | Macro-F1 |
|---|---|---|
| Joint (both axes) | 36.4% [22.2–53.4] | — |
owner |
54.5% [38.0–70.2] | 0.36 |
determinism |
78.8% [62.2–89.3] | 0.77 |
Joint accuracy sits 18pp below the owner axis and 42pp below determinism — reporting either
axis alone would describe a classifier that half works.
test_code has an F1 of exactly 0: the baseline never once identifies a test-code failure.
Accuracy hides that; macro-F1 is what makes it visible.
And a classifier that answers app_code unconditionally beats it on owner accuracy — 63.6%
against 54.5%, because 21 of 33 fixtures are app_code. That is the adversarial dataset doing its
job rather than a defect in the baseline: on macro-F1 the constant classifier scores 0.26 against
the baseline's 0.36. The intervals overlap heavily, so at n=33 neither result is evidence of a
difference — which is the argument for growing the dataset to 60.
Every number here is pinned in eval/metrics.test.ts, so a change to the rules or the dataset
fails the build with the old and new values side by side.
Roughly 60 fixtures, weighted towards the cases that defeat obvious shortcuts:
| Bucket | Share |
|---|---|
Hard quadrant (app_code + intermittent) |
~20% |
| Misleading history | ~10% |
| Environment dressed as regression | ~10% |
| Stale tests after refactors | ~15% |
| Cross-file state leaks | ~10% |
| Straightforward | ~25% |
| Genuinely ambiguous (excluded from headline) | ~10% |
Fixtures carry their provenance: synthetic (hand-authored), captured (from a real CI run,
labelled after the fact), mutated (a real run with an injected defect). Metrics are broken down by
provenance, so a classifier that only performs on invented failures is visible rather than
flattered.
"Punishes diff-only reasoning" is a claim, not a fact, until something is measured against it. Scoring the baseline per bucket is that measurement:
| Bucket | n | Joint | owner |
determinism |
|---|---|---|---|---|
straightforward |
8 | 100.0% | 100.0% | 100.0% |
hard-quadrant |
10 | 30.0% | 60.0% | 70.0% |
stale-test |
7 | 0.0% | 0.0% | 100.0% |
misleading-history |
4 | 0.0% | 75.0% | 0.0% |
environment-as-regression |
4 | 25.0% | 25.0% | 100.0% |
Each adversarial bucket defeats the baseline on the axis it was built to attack, and only that
axis. stale-test takes owner to 0% and leaves determinism at 100%. misleading-history is the
mirror image — determinism to 0%, owner still at 75%.
That orthogonality is the point. Buckets that all failed together would mean the fixtures were merely hard, or badly written. Failing on precisely the intended axis is what separates an adversarial dataset from a difficult one.
straightforward at 100% is the control on the control: the baseline is a working classifier, so
the failures above are properties of the fixtures rather than of the code being measured.
Two independent sources of noise, both measured rather than ignored.
Small-sample noise. Accuracy on 60 fixtures has a 95% Wilson interval of about ±11pp at 90% accuracy. On 15 fixtures it is ±20pp — wide enough that a real regression is invisible. Every proportion ships with its interval, and the CI gate compares lower bounds, never point estimates.
Model non-determinism. Temperature 0 is not determinism. Each fixture is classified five times, and the harness reports mean accuracy, variance, and per-fixture label stability.
Self-consistency is a first-class metric. A classifier that is 85% accurate and 100% stable is more useful than one that is 88% accurate and flips on a third of fixtures — because the second cannot be trusted at the level of an individual pull-request comment, which is the only level at which anyone actually reads it.
There is an obvious irony in a flaky-test triage tool whose own evaluation is mildly flaky. It is documented rather than hidden.
A model asked for a confidence number returns 0.85 for nearly everything. That number gates whether the expensive root-cause agent runs, so if it carries no information the gate is decorative.
The harness bins predictions by stated confidence, measures observed accuracy per bin, and reports a reliability curve plus Expected Calibration Error. Two consequences:
- If calibration is reasonable, the threshold is read off the curve at the point where observed accuracy crosses the target — not guessed.
- If ECE shows confidence is noise, the honest move is to drop the field and gate on something else. That result would be published, not worked around.
eval/report.md is regenerated by npm run eval and committed, so a regression appears as a diff:
per-axis and joint accuracy with intervals · agent vs baseline deltas · per-class precision/recall/F1 · two confusion matrices · per-quadrant accuracy with the hard quadrant called out · breakdown by provenance · self-consistency · reliability curve and ECE · cost per fixture and projected cost of a 50-failure run · and the prompt version, model ID, dataset revision and date behind every number.
Joint accuracy — both axes correct on the same fixture — is the headline. It is always the lowest number reported, and reporting the axes separately without it would be the most natural way to accidentally flatter the classifier.
npm run eval:ablation re-runs the dataset with parts of the context removed: no history, no diff,
no test source, error message only, baseline, multi-step agent variant, smaller model.
This is the part that turns "I built an AI classifier" into "I measured which parts of it work". It is also how the context bundle gets pruned — a field that contributes nothing is cost and latency with no return, and the only way to know which is to remove them one at a time.
Three of these run as tracked experiments: history · multi-step agent · model tier.
Stated plainly, because a methodology page claiming none is not credible.
- Author bias. The fixtures, the rubric, and the prompt share an author. Mitigated by captured fixtures from real runs and by labelling rules mechanical enough for someone else to reproduce.
- Small n. Sixty fixtures is not a benchmark. Hence the intervals.
- Distribution shift. TaskFlow's failure modes are not every codebase's. Nothing here claims to generalise beyond this repository.
- Prompt overfitting. Iterating against the same fixtures fits them. Mitigated by a held-out slice consulted rarely and reported separately.
- Label leakage. A lint step checks that no ground-truth term appears in a fixture payload, filename, or test title. It catches the careless cases; semantic leakage survives any word list.
Start here
Design
Quality
Working on it