|
| 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() |
0 commit comments