Skip to content

Commit 955fff5

Browse files
feat(benchmarks): add completeness judge so LOC wins can't hide under-delivery (#171)
The LOC tier scores the open feature tasks (vibe-*, tmpl-fe-*, open-*) on git diff alone -- score_vibe only checks "it compiles", score_fixture only checks "a new file exists". So an arm can win the LOC metric by shipping a stub: fewer lines because it does less, not because it is less bloated. That is the most credible attack left on the headline number raised in #126. complete.py is a second LLM judge (same auditable footing as judge.py: fixed model, temperature 0, published rubric) that rates how FULLY each submission implements its task, 0..3. Read alongside the LOC table, a low-LOC arm whose completeness also drops is caught, not rewarded. - judge_call gains a `system=` param so the HTTP/key/source plumbing is reused instead of duplicated (one rubric is the only delta between the two passes). - --selftest: the judge must rank a complete reference strictly above a stub. - --selftest-offline: validates the gate logic with no API call / no key. - README documents the pass and updates the can/cannot-show limitations. Fixes #126 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 44babb2 commit 955fff5

3 files changed

Lines changed: 182 additions & 5 deletions

File tree

‎benchmarks/agentic/README.md‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ python judge.py --selftest # validate the judge (small spend)
8787
python judge.py --run runs/<stamp> # score every workspace's source
8888
```
8989

90+
### Completeness judge (`complete.py`)
91+
92+
Fewer lines only counts as a win if the code still does the job. The LOC tier scores the open
93+
feature tasks on `git diff` alone, with no deterministic check that the asked feature was
94+
actually built — so an arm could "win" the LOC metric by shipping a stub. This pass closes that
95+
hole: the same auditable LLM judge (fixed model, temperature 0, published rubric) rates how
96+
**fully** each submission implements its task. Rubric: `0` stub/placeholder, `1` partial (core
97+
behavior missing), `2` mostly complete (a stated requirement missing), `3` fully implements the
98+
task. Read it **alongside** the LOC table — a low-LOC arm whose completeness also drops is doing
99+
less, not less-bloated.
100+
101+
Validated like the over-engineering judge: `--selftest` requires the judge to rank a complete
102+
reference strictly above a stub before any real scoring is trusted. `--selftest-offline` checks
103+
the gate logic with no API call (no key needed).
104+
105+
```bash
106+
python complete.py --selftest-offline # validate the gate logic, no API
107+
python complete.py --selftest # validate the judge (small spend)
108+
python complete.py --run runs/<stamp> # completeness-score every workspace
109+
```
110+
90111
## Reproduce
91112

92113
Needs the `claude` CLI (this is the harness, no SDK), Python 3, an authenticated Claude Code, and a
@@ -117,11 +138,13 @@ re-applied offline with `--rescore`, you never pay the API twice for a measureme
117138

118139
## What this can and cannot show
119140

120-
- It **can** show whether a skill keeps code minimal *without* dropping safety, on real
121-
multi-file edits, across model sizes, with variance.
141+
- It **can** show whether a skill keeps code minimal *without* dropping safety **or
142+
completeness**, on real multi-file edits, across model sizes, with variance. Less code that
143+
also does less is caught by the completeness judge, not rewarded.
122144
- It **cannot** claim production-readiness from six tasks, and a deterministic safety check is a
123145
floor, not a proof of security. The over-engineering source-LOC proxy is supplemented by an
124-
LLM judge in a later pass.
146+
LLM judge (`judge.py`), and the "did it actually build the feature" question by a second
147+
judge (`complete.py`).
125148
- If the arms converge (everyone safe, similar size), the benchmark says so. It is built to be
126149
able to disprove the skill's value, not only to confirm it.
127150

‎benchmarks/agentic/complete.py‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
"""LLM-judge COMPLETENESS pass for the agentic benchmark.
3+
4+
Fewer lines is only a win if the code still does the job. The open feature tasks (vibe-*,
5+
tmpl-fe-*, open-*) are scored on LOC alone -- there is no deterministic check that the asked
6+
feature was actually implemented, so an arm could "win" the LOC metric by shipping a stub.
7+
That is the inverse of the safety hole and the most credible attack on the headline number:
8+
"you wrote less because you did less."
9+
10+
This pass closes it. An LLM judge rates how FULLY each submission implements its task, on the
11+
same auditable footing as the over-engineering judge in judge.py: a published rubric, a fixed
12+
model at temperature 0, and a --selftest that must rank a complete reference strictly above a
13+
stub before any real scoring is trusted. Pair the output with run.py's LOC: a low-LOC arm whose
14+
completeness also drops is doing less, not less-bloated -- and now the bench shows it.
15+
16+
python complete.py --selftest # validate the judge ranks complete > stub (small API spend)
17+
python complete.py --selftest-offline # validate the GATE LOGIC only, no API, no key
18+
python complete.py --run runs/<stamp> # completeness-judge every workspace in a matrix run
19+
20+
Judge: claude-sonnet-4-6, key from ../../.env (shared with judge.py). ~$0.003/cell.
21+
22+
ponytail: reuses judge.py's HTTP/key/source plumbing instead of duplicating it -- one rubric
23+
param is the only delta between the two passes.
24+
"""
25+
import argparse, json, sys
26+
from collections import defaultdict
27+
from pathlib import Path
28+
29+
from tasks import TASKS
30+
from judge import load_key, source_text, judge_call, parse_score, RUNS_DIR, JUDGE_MODEL
31+
32+
SCORE_KEY = "completeness"
33+
FLAG_AT = 1 # cells scoring <= this are under-delivery (stub/partial) and get listed
34+
ARMS_ORDER = ["baseline", "caveman", "ponytail", "yagni", "yagni-oneliner"]
35+
36+
RUBRIC = (
37+
"You are a senior engineer checking whether a code submission ACTUALLY IMPLEMENTS the task it "
38+
"was given. Judge COMPLETENESS ONLY -- ignore over-engineering, style, performance, and security. "
39+
"A stub, a placeholder, a bare `pass`/`TODO`/`NotImplementedError`, or code that silently omits "
40+
"the core behavior asked for is INCOMPLETE. Score 0-3:\n"
41+
"0 = stub/empty/placeholder, does essentially nothing the task asked\n"
42+
"1 = partial: the core behavior is missing or broken\n"
43+
"2 = mostly complete: it works but a stated requirement is missing\n"
44+
"3 = fully implements what the task asked\n"
45+
"Name the single most important missing piece, or \"none\". "
46+
"Respond with ONLY this JSON: {\"completeness\": <0-3 int>, \"why\": \"<one line>\", \"missing\": \"<piece or none>\"}"
47+
)
48+
49+
def parse_complete(text):
50+
d = parse_score(text)
51+
if d and SCORE_KEY in d:
52+
try: d[SCORE_KEY] = int(d[SCORE_KEY])
53+
except Exception: d[SCORE_KEY] = None
54+
return d
55+
56+
# --- the gate: a complete impl must out-score a stub for the same task ---
57+
def _rank_ok(scores):
58+
"""scores: {(task_id, label): {SCORE_KEY: int}}. For each task the 'complete' label must
59+
strictly out-score the 'stub' label, else the judge (or the gate) is not trustworthy."""
60+
ok = True
61+
for task_id in sorted({t for (t, _) in scores}):
62+
hi = scores.get((task_id, "complete")) or {}
63+
lo = scores.get((task_id, "stub")) or {}
64+
if not (isinstance(hi.get(SCORE_KEY), int) and isinstance(lo.get(SCORE_KEY), int)
65+
and hi[SCORE_KEY] > lo[SCORE_KEY]):
66+
print(f"XX {task_id}: did not rank complete above stub"); ok = False
67+
else:
68+
print(f"ok {task_id}: complete({hi[SCORE_KEY]}) > stub({lo[SCORE_KEY]})")
69+
return ok
70+
71+
# Complete refs are the deterministic tasks' known-good answers; stubs do nothing.
72+
STUBS = {
73+
"cache": "def compute(n):\n pass\n",
74+
"safe-path": "def safe_upload_path(base_dir, filename):\n pass\n",
75+
}
76+
PAIRS = [(t, lbl, code) for t in STUBS for lbl, code in
77+
(("complete", TASKS[t]["good"]), ("stub", STUBS[t]))]
78+
79+
def selftest(key):
80+
"""Live: the judge model must rank each complete ref above its stub."""
81+
scores = {}
82+
for task_id, label, code in PAIRS:
83+
s = parse_complete(judge_call(TASKS[task_id]["prompt"], code, key, system=RUBRIC))
84+
scores[(task_id, label)] = s or {}
85+
print(f" {task_id:10} {label:8} -> {s}")
86+
ok = _rank_ok(scores)
87+
print(f"\ncompleteness judge selftest: {'valid' if ok else 'NOT TRUSTWORTHY'}")
88+
return 0 if ok else 1
89+
90+
def selftest_offline():
91+
"""No API, no key: prove the GATE catches under-delivery. A well-ordered matrix must pass
92+
and a matrix where a stub out-scores the complete impl must be flagged. Fails loudly if the
93+
gate is ever weakened into a no-op."""
94+
good = {("cache", "complete"): {SCORE_KEY: 3}, ("cache", "stub"): {SCORE_KEY: 0}}
95+
bad = {("cache", "complete"): {SCORE_KEY: 1}, ("cache", "stub"): {SCORE_KEY: 3}}
96+
print("offline gate -- well-ordered (expect ok):")
97+
p_good = _rank_ok(good)
98+
print("offline gate -- stub out-scores complete (expect XX):")
99+
p_bad = _rank_ok(bad)
100+
passed = p_good and not p_bad
101+
print(f"\ncompleteness gate selftest (offline): {'valid' if passed else 'BROKEN'}")
102+
return 0 if passed else 1
103+
104+
def run(run_dir, key):
105+
run_dir = Path(run_dir)
106+
if not run_dir.exists(): run_dir = RUNS_DIR / run_dir.name
107+
cells = []
108+
for ws in sorted(p for p in run_dir.iterdir() if p.is_dir()):
109+
parts = ws.name.split("__")
110+
if len(parts) != 4 or parts[0] not in TASKS: continue
111+
cells.append((parts[0], parts[1], parts[2], ws))
112+
print(f"completeness-judging {len(cells)} workspaces with {JUDGE_MODEL} ...")
113+
scored = []
114+
for i, (tid, arm, model, ws) in enumerate(cells, 1):
115+
s = parse_complete(judge_call(TASKS[tid]["prompt"], source_text(ws), key, system=RUBRIC)) \
116+
or {SCORE_KEY: None}
117+
scored.append({"task": tid, "arm": arm, "model": model, SCORE_KEY: s.get(SCORE_KEY),
118+
"why": s.get("why", ""), "missing": s.get("missing", "")})
119+
if i % 25 == 0 or i == len(cells): print(f" [{i}/{len(cells)}]", flush=True)
120+
(run_dir / "completeness.json").write_text(
121+
json.dumps({"judge": JUDGE_MODEL, "rubric": RUBRIC, "scores": scored}, indent=2), encoding="utf-8")
122+
by_arm = defaultdict(list)
123+
for r in scored:
124+
if isinstance(r[SCORE_KEY], int): by_arm[r["arm"]].append(r[SCORE_KEY])
125+
print(f"\n=== completeness by arm (judge: {JUDGE_MODEL}, 0=stub .. 3=fully implements) ===")
126+
print(f" {'arm':16} {'n':>4} {'mean':>6} {'min':>4}")
127+
for arm in ARMS_ORDER:
128+
v = by_arm.get(arm, [])
129+
if v: print(f" {arm:16} {len(v):>4} {sum(v)/len(v):>6.2f} {min(v):>4}")
130+
under = sorted([r for r in scored if isinstance(r[SCORE_KEY], int) and r[SCORE_KEY] <= FLAG_AT],
131+
key=lambda r: r[SCORE_KEY])
132+
print(f"\n=== under-delivered (completeness <= {FLAG_AT}): {len(under)} cells ===")
133+
for r in under[:20]:
134+
print(f" {r['task']:13} {r['arm']:15} {r['model']:7} score={r[SCORE_KEY]} missing={r['missing']}")
135+
print(f"\nwrote {run_dir / 'completeness.json'}")
136+
137+
def main():
138+
ap = argparse.ArgumentParser()
139+
ap.add_argument("--selftest", action="store_true", help="live: judge ranks complete > stub")
140+
ap.add_argument("--selftest-offline", action="store_true", help="gate logic only, no API")
141+
ap.add_argument("--run", help="run dir to completeness-judge")
142+
args = ap.parse_args()
143+
if args.selftest_offline:
144+
sys.exit(selftest_offline())
145+
key = load_key()
146+
if not key: sys.exit("no ANTHROPIC_API_KEY (.env or env)")
147+
if args.selftest: sys.exit(selftest(key))
148+
if args.run:
149+
if selftest(key): sys.exit("judge not trustworthy; refusing to judge the matrix")
150+
return run(args.run, key)
151+
sys.exit("give --selftest, --selftest-offline, or --run <dir>")
152+
153+
if __name__ == "__main__":
154+
main()

‎benchmarks/agentic/judge.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ def source_text(workdir: Path):
6161
except Exception: continue
6262
return "\n\n".join(out)
6363

64-
def judge_call(task_prompt, files, key, retries=3):
64+
def judge_call(task_prompt, files, key, retries=3, system=RUBRIC):
6565
user = f"TASK GIVEN TO THE AUTHOR:\n{task_prompt}\n\nFILES THEY WROTE:\n{files}"
6666
body = json.dumps({"model": JUDGE_MODEL, "max_tokens": 300, "temperature": 0,
67-
"system": RUBRIC, "messages": [{"role": "user", "content": user}]}).encode()
67+
"system": system, "messages": [{"role": "user", "content": user}]}).encode()
6868
for attempt in range(retries):
6969
try:
7070
req = urllib.request.Request("https://api.anthropic.com/v1/messages", data=body,

0 commit comments

Comments
 (0)