Critic false-pass baseline (roadmap SL-2)¶
The claim this measures: anvil's review gate is only as trustworthy as its critic. If the critic waves bad diffs through, every downstream "PASS" is a lie. SL-2 makes that risk measurable: a fault-injection harness feeds a corpus of known-bad diffs to the critic and counts how many it approves — the false-pass rate. You cannot improve the critic until you can score it.
Roadmap:
docs/roadmap.md§ Wave 1 → SL-2. Acceptance: a reproducible script plus a committed baseline false-pass number indocs/.
What "false pass" means¶
For each corpus case we know the ground truth (bad or good). A backend
returns a verdict per diff — PASS (the critic approved / waved it through)
or FAIL (the critic rejected it). The oracle is two pure ratios:
false_pass_rate = (# BAD cases the critic PASSED) / (# BAD cases) <- the headline
false_fail_rate = (# GOOD cases the critic FAILED) / (# GOOD cases)
A false pass is the dangerous one: a defect the critic missed. The good controls catch the opposite failure — a critic so trigger-happy it rejects clean diffs (which would make a low false-pass rate meaningless).
The corpus¶
Lives at benchmarks/critic_corpus/. Each case
is a tiny, self-evident <id>.diff plus a <id>.json carrying id, label,
defect_class, and description. The four bad classes are exactly the ones the
roadmap names; two good controls guard against over-rejection.
| id | label | defect_class | what it injects |
|---|---|---|---|
off_by_one |
bad | off-by-one | range(len(x)) → range(len(x) + 1); indexes past the end |
dropped_null_check |
bad | dropped-null-check | deletes the if user is None guard |
assertion_free_test |
bad | assertion-free-test | adds a def test_* that calls but never asserts |
deleted_assertion |
bad | deleted-assertion | deletes the only assert in a passing test |
good_bugfix |
good | none | a correct empty-list guard; should PASS |
good_test |
good | none | a real test that calls and asserts; should PASS |
How to run¶
The harness lives at benchmarks/critic_falsepass.py.
Run it from bin/ so it uses the synced anvil venv (uv only):
cd bin
uv run python ../benchmarks/critic_falsepass.py # mock backend (default)
uv run python ../benchmarks/critic_falsepass.py --backend mock --json
uv run python ../benchmarks/critic_falsepass.py --backend api # real critic (manual)
It is backend-agnostic. A backend is just a callable Case -> Verdict:
mock— a deterministic, dependency-free rule-set (a cheap linter). It catches the classes a simple textual rule can see and is blind, by design, to ones needing semantic reasoning. Makes no API call. Its number is the committed self-test reference below.api— the real critic agent (agents/critic.md), an LLM that needsANTHROPIC_API_KEYand is non-deterministic. It therefore cannot run in deterministic pytest CI. It is a documented stub that raisesNotImplementedErroruntil the Agent-SDK call is wired in — it never silently fabricates a number.
Mock backend — committed self-test reference¶
The mock backend's rules are fixed, so its result is stable across runs and is
asserted by tests/test_critic_falsepass.py
(no network, no LLM). The mock catches deleted-assertion, dropped-null-check,
and assertion-free-test, but has no arithmetic rule, so it waves the
off_by_one diff through — one false pass out of four bad cases:
backend: mock
false_pass_rate = 1/4 = 0.25 (off_by_one missed)
false_fail_rate = 0/2 = 0.0 (both good controls correctly PASS)
This is not a claim about the real critic. It is a fixture that proves the harness arithmetic — and it documents the methodology a reader will re-apply to the real critic's verdicts.
Real critic baseline — TODO (run with API access)¶
PLACEHOLDER — not yet measured. The real
false_pass_ratefor the LLM critic is TBD. Populate it by wiringapi_backend()inbenchmarks/critic_falsepass.pyto invokeagents/critic.mdvia the Claude Agent SDK, then running:cd bin ANTHROPIC_API_KEY=... uv run python ../benchmarks/critic_falsepass.py --backend api --jsonRecord the resulting
false_pass_rate(andfalse_fail_rate), the date, the model id, and the corpus revision in the table below. Because the critic is non-deterministic, capture the number over several runs and note the spread — mirror the "reproducible-aggregate, not bit-identical" caveat from the coordination benchmark (benchmarks/README.md).
| date | model | corpus rev | false_pass_rate | false_fail_rate | notes |
|---|---|---|---|---|---|
| TBD | TBD | TBD | TBD | TBD | run --backend api to populate |