pluginbench 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. pluginbench/__init__.py +3 -0
  2. pluginbench/__main__.py +3 -0
  3. pluginbench/assessment-charts.html +790 -0
  4. pluginbench/assessment.py +316 -0
  5. pluginbench/assessment_charts.py +1225 -0
  6. pluginbench/assessment_metrics.py +1956 -0
  7. pluginbench/assessment_models.py +127 -0
  8. pluginbench/baselines.py +792 -0
  9. pluginbench/claude.py +374 -0
  10. pluginbench/cli.py +672 -0
  11. pluginbench/code_diff.py +207 -0
  12. pluginbench/codex.py +451 -0
  13. pluginbench/codex_events.py +585 -0
  14. pluginbench/collectors.py +591 -0
  15. pluginbench/compare.py +927 -0
  16. pluginbench/components.py +129 -0
  17. pluginbench/coverage_report.py +610 -0
  18. pluginbench/datasets.py +207 -0
  19. pluginbench/evaluation.example.yaml +45 -0
  20. pluginbench/evaluation.py +1651 -0
  21. pluginbench/evaluation_report.py +1004 -0
  22. pluginbench/fallow_parse.mjs +101 -0
  23. pluginbench/harbor_backend.py +2946 -0
  24. pluginbench/isolation.py +314 -0
  25. pluginbench/judge.py +683 -0
  26. pluginbench/language_analysis.py +915 -0
  27. pluginbench/ledger.py +217 -0
  28. pluginbench/measurement_profiles.py +616 -0
  29. pluginbench/native_treatment_runtime.js +221 -0
  30. pluginbench/native_treatments.py +155 -0
  31. pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
  32. pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
  33. pluginbench/observer_data/go-analysis-v1/main.go +64 -0
  34. pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
  35. pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
  36. pluginbench/parallel.py +101 -0
  37. pluginbench/processes.py +100 -0
  38. pluginbench/remeasure.py +992 -0
  39. pluginbench/run_lock.py +227 -0
  40. pluginbench/run_status.py +518 -0
  41. pluginbench/study.py +1858 -0
  42. pluginbench/study_review.py +924 -0
  43. pluginbench/submissions.py +486 -0
  44. pluginbench/task_sources.py +403 -0
  45. pluginbench/treatments.py +388 -0
  46. pluginbench/trial_containers.py +94 -0
  47. pluginbench/vendor/ECHARTS-LICENSE +222 -0
  48. pluginbench/vendor/ECHARTS-NOTICE +5 -0
  49. pluginbench/vendor/ZRENDER-LICENSE +29 -0
  50. pluginbench/vendor/echarts.min.js +48 -0
  51. pluginbench-0.5.0.dist-info/METADATA +294 -0
  52. pluginbench-0.5.0.dist-info/RECORD +58 -0
  53. pluginbench-0.5.0.dist-info/WHEEL +4 -0
  54. pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
  55. pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
  56. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
  57. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
  58. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
@@ -0,0 +1,1004 @@
1
+ """Compact, selected views of canonical evidence for one evaluation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import statistics
7
+ from pathlib import Path
8
+
9
+ from pluginbench.measurement_profiles import REMOVED_TEST_OVERLAP, readiness_state
10
+ from pluginbench.assessment_charts import (
11
+ DIAGNOSTIC_PANELS,
12
+ METRICS,
13
+ _map,
14
+ _net_lines,
15
+ _number,
16
+ _rows,
17
+ _signed,
18
+ chart_artifacts_from_data,
19
+ chart_data,
20
+ execution_outcome,
21
+ )
22
+
23
+ MEASUREMENT_METRICS = {
24
+ "tokens": (
25
+ "Input tokens",
26
+ "Cached input tokens",
27
+ "Uncached input tokens",
28
+ "Output tokens",
29
+ "Reasoning output tokens",
30
+ "Coding time",
31
+ ),
32
+ "lines": (
33
+ "Source lines added",
34
+ "Source lines removed",
35
+ "Source lines net",
36
+ "Test lines added",
37
+ "Test lines removed",
38
+ "Duplicated added test lines",
39
+ ),
40
+ "cyclomatic": (
41
+ "Net cyclomatic complexity added",
42
+ "Functions added",
43
+ "Source files added",
44
+ ),
45
+ "cognitive": (
46
+ "Net cognitive complexity added",
47
+ "Complexity hotspots added",
48
+ ),
49
+ "dead_code": ("Introduced dead findings", "Introduced clone groups"),
50
+ "coverage": (
51
+ "Changed-line coverage %",
52
+ "Changed-branch coverage %",
53
+ "Redundant added tests",
54
+ "Line coverage %",
55
+ "Statement coverage %",
56
+ "Branch coverage %",
57
+ "Function coverage %",
58
+ ),
59
+ "review": (
60
+ "Total review findings",
61
+ "P0 review findings",
62
+ "P1 review findings",
63
+ "P2 review findings",
64
+ "P3 review findings",
65
+ "Correctness review findings",
66
+ "Scope review findings",
67
+ "Maintainability review findings",
68
+ "Tests review findings",
69
+ "Judge input tokens",
70
+ "Judge output tokens",
71
+ ),
72
+ }
73
+ OBSERVATION_FIELDS = (
74
+ "attempt_id",
75
+ "task",
76
+ "arm",
77
+ "panel",
78
+ "metric",
79
+ "family",
80
+ "value",
81
+ "unit",
82
+ "correctness",
83
+ "status",
84
+ "reason",
85
+ "compatibility",
86
+ "model",
87
+ "host",
88
+ "harness",
89
+ "harness_version",
90
+ "reasoning_effort",
91
+ "requested_reasoning_effort",
92
+ "runtime_source",
93
+ "role",
94
+ "stage",
95
+ "snapshot_digest",
96
+ "before_digest",
97
+ "per_test_count",
98
+ "elapsed_seconds",
99
+ "provenance",
100
+ )
101
+ PROVENANCE_FIELDS = (
102
+ "language",
103
+ "analyzers",
104
+ "metric_settings",
105
+ "tool",
106
+ "tool_version",
107
+ "method",
108
+ "config_digest",
109
+ "scope",
110
+ "token_format",
111
+ "clock",
112
+ "complete_for_declared_scope",
113
+ "reported_functions",
114
+ "limitations",
115
+ "test_scope",
116
+ "covered",
117
+ "total",
118
+ "selection_policy",
119
+ "branch_denominator_digest",
120
+ "function_denominator_digest",
121
+ "line_denominator_digest",
122
+ "statement_denominator_digest",
123
+ "judge_model",
124
+ "judge_harness",
125
+ "judge_reasoning_effort",
126
+ "judge_comparison_reasoning_effort",
127
+ "judge_effort_evidence_source",
128
+ "judge_harness_version",
129
+ "review_stage",
130
+ "criteria_sha256",
131
+ "reasoning_comparison",
132
+ "execution_status",
133
+ "execution_reason",
134
+ )
135
+
136
+ TRUSTED_TEST_OVERLAP = "trusted verifier tests overwrite authored patch paths"
137
+
138
+
139
+ def correctness_state(status: object, grading: object) -> str:
140
+ """Correctness of one attempt, separating overlap passes from unknowns.
141
+
142
+ PluginBench withholds confirmation when trusted verifier tests overwrite paths the agent
143
+ changed. The verifier still grades the agent's production code against its own tests, so
144
+ reward 1 with every fail-to-pass and pass-to-pass test passing is an overlap pass, and
145
+ reward 0 is a fail. One exception stays unknown: reward 0 with no pass-to-pass test passing.
146
+ A replaced test file can remove code the agent's other test files use, so the existing suite
147
+ may fail to build before any test runs. Any other withheld result stays unknown.
148
+ """
149
+ grading = _map(grading)
150
+ if status not in {"unknown", "fail"} or grading.get("reason") != TRUSTED_TEST_OVERLAP:
151
+ return str(status)
152
+ rewards = _map(_map(grading.get("native_verifier_result")).get("rewards"))
153
+ if status == "fail" and rewards.get("reward") != 0:
154
+ # A fail the receipt recorded itself, such as a run that did not complete.
155
+ return "fail"
156
+ if rewards.get("reward") == 0:
157
+ # Also a saved report's earlier verdict, which compare and judge read back.
158
+ p2p_total = rewards.get("p2p_total")
159
+ no_existing_test_ran = (
160
+ isinstance(p2p_total, int) and p2p_total > 0 and rewards.get("p2p_passed") == 0
161
+ )
162
+ return "unknown" if no_existing_test_ran else "fail"
163
+ every_test_passed = all(
164
+ isinstance(rewards.get(f"{kind}_total"), int)
165
+ and rewards.get(f"{kind}_passed") == rewards.get(f"{kind}_total")
166
+ for kind in ("f2p", "p2p")
167
+ )
168
+ return "overlap_pass" if rewards.get("reward") == 1 and every_test_passed else "unknown"
169
+
170
+
171
+ COMPLETENESS_COUNTS = {
172
+ "tokens": "Input/output tokens",
173
+ "lines": "Source/test lines added/removed",
174
+ "cognitive": "Complexity population",
175
+ "cyclomatic": "Complexity population",
176
+ "dead_code": "Introduced dead findings",
177
+ "coverage": "Coverage covered/total counts",
178
+ }
179
+
180
+
181
+ def _selected_completeness(
182
+ *,
183
+ data: dict[str, object],
184
+ projection: dict[str, object],
185
+ selected: list[object],
186
+ skipped: dict[tuple[str, str], object],
187
+ ) -> list[dict[str, object]]:
188
+ source = {
189
+ row["attempt_id"]: row
190
+ for row in _rows(projection["measurement_completeness"])
191
+ if row["role"] == "handoff"
192
+ }
193
+ observations = _rows(data["observations"])
194
+ required = {COMPLETENESS_COUNTS[str(family)] for family in selected if family != "review"}
195
+ results = []
196
+ for attempt in _rows(data["attempts"]):
197
+ record = source.get(attempt["id"], {})
198
+ measured = {
199
+ row["metric"]: row for row in observations if row["attempt_id"] == attempt["id"]
200
+ }
201
+
202
+ def captured(metrics: tuple[str, ...]) -> bool:
203
+ return all(
204
+ metric in measured and measured[metric]["value"] is not None for metric in metrics
205
+ )
206
+
207
+ counts = {name: _map(record.get("counts")).get(name) is True for name in sorted(required)}
208
+ # Same populations as the collector-backed projection count: totals and line changes only.
209
+ for family, metrics in (
210
+ ("tokens", ("Input tokens", "Output tokens")),
211
+ ("lines", MEASUREMENT_METRICS["lines"][:4]),
212
+ ):
213
+ if family in selected and not record:
214
+ counts[COMPLETENESS_COUNTS[family]] = captured(metrics)
215
+ complexity_metrics = [
216
+ metric
217
+ for family in ("cognitive", "cyclomatic")
218
+ if family in selected
219
+ for metric in MEASUREMENT_METRICS[family]
220
+ ]
221
+ if complexity_metrics:
222
+ # Code-added sums are defined (zero) for complete empty populations; unknown stays None.
223
+ counts["Complexity population"] = captured(tuple(complexity_metrics))
224
+ if "review" in selected:
225
+ counts["Review findings"] = captured(
226
+ tuple(
227
+ metric
228
+ for metric in MEASUREMENT_METRICS["review"]
229
+ if not metric.startswith("Judge ")
230
+ )
231
+ )
232
+ counts["Review input/output tokens"] = captured(
233
+ ("Judge input tokens", "Judge output tokens")
234
+ )
235
+ # A pair the check marked as not measured is incomplete, whatever its collector counted.
236
+ for measure in map(str, selected):
237
+ if (str(attempt["task"]), measure) in skipped and measure in COMPLETENESS_COUNTS:
238
+ counts[COMPLETENESS_COUNTS[measure]] = False
239
+ results.append(
240
+ {
241
+ **record,
242
+ "attempt_id": attempt["id"],
243
+ "task": attempt["task"],
244
+ "arm": attempt["guidance"],
245
+ "role": "handoff",
246
+ "counts": counts,
247
+ "complete": bool(counts) and all(counts.values()),
248
+ }
249
+ )
250
+ return results
251
+
252
+
253
+ def _completeness_matrix(
254
+ observations: list[dict[str, object]], capabilities: dict[str, object]
255
+ ) -> list[dict[str, object]]:
256
+ """Task × measure × arm: measured, unknown with its reasons, or not configured for the task."""
257
+ groups: dict[tuple[str, str, str], dict[str, list[dict[str, object]]]] = {}
258
+ for row in observations:
259
+ key = (str(row["task"]), str(row["family"]), str(row["metric"]))
260
+ groups.setdefault(key, {}).setdefault(str(row["arm"]), []).append(row)
261
+ matrix: list[dict[str, object]] = []
262
+ for (task, family, metric), arms in sorted(groups.items()):
263
+ capability = _map(_map(capabilities.get(task)).get(family))
264
+ configured = not capability or readiness_state(capability) != "not_configured"
265
+ cells: dict[str, object] = {}
266
+ for arm, rows in sorted(arms.items()):
267
+ measured = sum(row["status"] == "measured" for row in rows)
268
+ reasons = sorted({str(row["reason"]) for row in rows if row["status"] != "measured"})
269
+ state = (
270
+ "not_configured"
271
+ if not configured
272
+ else "measured"
273
+ if measured == len(rows)
274
+ else "unknown"
275
+ )
276
+ text = (
277
+ state
278
+ if state != "unknown"
279
+ else f"unknown: {'; '.join(reasons)} ({measured} of {len(rows)} measured)"
280
+ )
281
+ cells[arm] = {
282
+ "state": state,
283
+ "measured": measured,
284
+ "assigned": len(rows),
285
+ "reasons": reasons,
286
+ "text": text,
287
+ }
288
+ matrix.append({"task": task, "family": family, "metric": metric, "cells": cells})
289
+ return matrix
290
+
291
+
292
+ def _assigned_counts(
293
+ observations: list[dict[str, object]],
294
+ ) -> dict[tuple[object, object, object], int]:
295
+ attempts: dict[tuple[object, object, object], set[object]] = {}
296
+ for row in observations:
297
+ key = (row.get("task"), row.get("metric"), row.get("arm"))
298
+ attempts.setdefault(key, set()).add(row["attempt_id"])
299
+ return {key: len(ids) for key, ids in attempts.items()}
300
+
301
+
302
+ def _comparison_rows(data: dict[str, object]) -> list[dict[str, object]]:
303
+ observations = _rows(data["observations"])
304
+ assigned_counts = _assigned_counts(observations)
305
+ medians = _rows(data["medians"])
306
+ lookup = {
307
+ (row.get("task"), row.get("metric"), row.get("compatibility"), row.get("arm")): row
308
+ for row in medians
309
+ }
310
+ profiles = {
311
+ (row.get("task"), row.get("metric"), row.get("arm"), row.get("compatibility"))
312
+ for row in observations
313
+ if row.get("arm") not in {"baseline", "unknown"}
314
+ }
315
+ results = []
316
+ for task, metric, arm, compatibility in sorted(profiles, key=str):
317
+ baseline = lookup.get((task, metric, compatibility, "baseline"), {})
318
+ guided = lookup.get((task, metric, compatibility, arm), {})
319
+ baseline_value, guided_value = baseline.get("value"), guided.get("value")
320
+ absolute = (
321
+ float(guided_value) - float(baseline_value)
322
+ if compatibility is not None
323
+ and isinstance(baseline_value, (int, float))
324
+ and isinstance(guided_value, (int, float))
325
+ else None
326
+ )
327
+ known = absolute is not None
328
+ percentage = (
329
+ absolute / float(baseline_value) * 100
330
+ if absolute is not None
331
+ and isinstance(baseline_value, (int, float))
332
+ # A percentage of a zero or negative baseline has no meaning.
333
+ and baseline_value > 0
334
+ else None
335
+ )
336
+ assigned = {
337
+ group: assigned_counts.get((task, metric, group), 0) for group in ("baseline", arm)
338
+ }
339
+ baseline_measured, guided_measured = baseline.get("measured", 0), guided.get("measured", 0)
340
+ complete = baseline_measured == assigned["baseline"] and guided_measured == assigned[arm]
341
+ metric_guide = _map(_map(data.get("metric_guide")).get(str(metric)))
342
+ unit = metric_guide.get("unit")
343
+ results.append(
344
+ {
345
+ "task": task,
346
+ "absolute_delta_unit": "percentage points" if unit == "% covered" else unit,
347
+ "metric": metric,
348
+ "guidance": arm,
349
+ "compatibility": compatibility,
350
+ "baseline_median": baseline_value,
351
+ "guidance_median": guided_value,
352
+ "absolute_delta_vs_vanilla": absolute,
353
+ "percentage_delta_vs_vanilla": percentage,
354
+ "baseline_assigned": assigned["baseline"],
355
+ "baseline_measured": baseline_measured,
356
+ "guidance_assigned": assigned[arm],
357
+ "guidance_measured": guided_measured,
358
+ "status": "complete" if known and complete else "partial" if known else "unknown",
359
+ "reason": "Guidance median minus this evaluation's vanilla median; lower is not automatically better."
360
+ if known
361
+ else "No matching measured vanilla and guidance profile; unknown is not zero.",
362
+ "percentage_reason": "Undefined when vanilla is zero or either median is unknown."
363
+ if percentage is None
364
+ else None,
365
+ }
366
+ )
367
+ return results
368
+
369
+
370
+ def _aggregate_groups(data: dict[str, object]) -> list[tuple[str, str | None, list[str]]]:
371
+ tasks = sorted({str(row["task"]) for row in _rows(data["attempts"])})
372
+ observations = _rows(data["observations"])
373
+ groups: dict[tuple[str, str | None], set[str]] = {}
374
+ task_analyzers: dict[tuple[str, str], set[str]] = {}
375
+ unbound_values: set[tuple[str, str]] = set()
376
+ for row in observations:
377
+ group = None
378
+ if row.get("family") in {"cognitive", "cyclomatic", "dead_code"}:
379
+ known = task_analyzers.setdefault((str(row["metric"]), str(row["task"])), set())
380
+ provenance = _map(row.get("provenance"))
381
+ analyzers = _map(provenance.get("analyzers"))
382
+ language = str(
383
+ provenance.get("language")
384
+ or ("JS/TS" if provenance.get("tool") == "fallow" else "unknown language")
385
+ )
386
+ family = str(row.get("family"))
387
+ tool_name = (
388
+ {"python": "vulture", "go": "staticcheck", "rust": "rustc"}.get(language, "fallow")
389
+ if family == "dead_code"
390
+ else "go_analysis"
391
+ if language == "go"
392
+ else "rca"
393
+ )
394
+ tool = _map(analyzers.get(tool_name))
395
+ version = tool.get("version", provenance.get("tool_version"))
396
+ if not version or version == "unknown" or (not tool and not provenance.get("tool")):
397
+ if row.get("value") is not None:
398
+ unbound_values.add((str(row["metric"]), str(row["task"])))
399
+ continue
400
+ version = str(version)
401
+ if tool_name == "go_analysis" and "gocognit=" in version:
402
+ part = "gocognit=" if family == "cognitive" else "gocyclo="
403
+ name = part[:-1] + " " + version.split(part)[-1].split()[0]
404
+ else:
405
+ name = (
406
+ version
407
+ if tool
408
+ else str(provenance.get("tool", "unknown analyzer")) + " " + version
409
+ )
410
+ group = f"{language.title()} · {name}"
411
+ known.add(group)
412
+ continue
413
+ groups.setdefault((str(row["metric"]), group), set()).add(str(row["task"]))
414
+ for (metric, task), known in task_analyzers.items():
415
+ group = (
416
+ "Conflicting analyzers"
417
+ if len(known) > 1
418
+ else next(iter(known))
419
+ if known and (metric, task) not in unbound_values
420
+ else "Unknown analyzer"
421
+ )
422
+ groups.setdefault((metric, group), set()).add(task)
423
+ return [
424
+ (metric, group, sorted(members) if group else tasks)
425
+ for (metric, group), members in sorted(groups.items(), key=str)
426
+ ]
427
+
428
+
429
+ def _aggregate_rows(data: dict[str, object]) -> list[dict[str, object]]:
430
+ """Give each assigned task one median, independent of its repeat count."""
431
+ observations = _rows(data["observations"])
432
+ assigned_counts = _assigned_counts(observations)
433
+ medians = _rows(data["medians"])
434
+ raw_medians = _rows(data["raw_medians"])
435
+ comparisons = _rows(data["comparisons"])
436
+ median_rows: dict[tuple[object, object, object], list[dict[str, object]]] = {}
437
+ raw_rows: dict[tuple[object, object, object], list[dict[str, object]]] = {}
438
+ comparison_rows: dict[tuple[object, object, object], list[dict[str, object]]] = {}
439
+ for row in medians:
440
+ median_rows.setdefault((row["task"], row["metric"], row["arm"]), []).append(row)
441
+ for row in raw_medians:
442
+ raw_rows.setdefault((row["task"], row["metric"], row["arm"]), []).append(row)
443
+ for row in comparisons:
444
+ if row["absolute_delta_vs_vanilla"] is not None:
445
+ comparison_rows.setdefault((row["task"], row["metric"], row["guidance"]), []).append(
446
+ row
447
+ )
448
+ arms = sorted(
449
+ {str(row["arm"]) for row in observations}, key=lambda arm: (arm != "baseline", arm)
450
+ )
451
+ results = []
452
+ for metric, analysis_group, tasks in _aggregate_groups(data):
453
+ unresolved_analyzer = analysis_group in {"Unknown analyzer", "Conflicting analyzers"}
454
+ baseline_values = {task: median_rows.get((task, metric, "baseline"), []) for task in tasks}
455
+ for arm in arms:
456
+ task_values = []
457
+ task_statuses = []
458
+ comparison_statuses = []
459
+ matching_baselines = []
460
+ for task in tasks:
461
+ rows = [] if unresolved_analyzer else raw_rows.get((task, metric, arm), [])
462
+ value = rows[0].get("value") if len(rows) == 1 else None
463
+ task_values.append({"task": task, "value": value})
464
+ matches = (
465
+ [] if unresolved_analyzer else comparison_rows.get((task, metric, arm), [])
466
+ )
467
+ baseline = baseline_values[task]
468
+ measured = rows[0].get("measured", 0) if len(rows) == 1 else 0
469
+ assigned = assigned_counts.get((task, metric, arm), 0)
470
+ raw_status = "complete" if value is not None and measured == assigned else "partial"
471
+ task_statuses.append(raw_status)
472
+ if arm == "baseline":
473
+ matching_baselines.append(value)
474
+ comparison_statuses.append(raw_status)
475
+ else:
476
+ match = matches[0] if len(matches) == 1 and len(baseline) == 1 else {}
477
+ matching_baselines.append(match.get("baseline_median"))
478
+ comparison_statuses.append(str(match.get("status", "unknown")))
479
+ known_values = [
480
+ float(row["value"]) for row in task_values if isinstance(row["value"], (int, float))
481
+ ]
482
+ value = sum(known_values) / len(known_values) if known_values else None
483
+ known_baselines = [
484
+ float(value) for value in matching_baselines if isinstance(value, (int, float))
485
+ ]
486
+ all_tasks_matched = len(known_values) == len(known_baselines) == len(tasks) and bool(
487
+ tasks
488
+ )
489
+ baseline_value = sum(known_baselines) / len(tasks) if all_tasks_matched else None
490
+ delta = (
491
+ value - baseline_value if value is not None and baseline_value is not None else None
492
+ )
493
+ guide = _map(_map(data["metric_guide"]).get(metric))
494
+ results.append(
495
+ {
496
+ "metric": metric,
497
+ "analysis_group": analysis_group,
498
+ "guidance": arm,
499
+ "value": value,
500
+ "unit": guide.get("unit"),
501
+ "method": "equal-weight mean of per-task medians",
502
+ "assigned_tasks": len(tasks),
503
+ "measured_tasks": len(known_values),
504
+ "task_medians": task_values,
505
+ "status": "complete"
506
+ if len(known_values) == len(tasks)
507
+ and all(status == "complete" for status in task_statuses)
508
+ else "partial"
509
+ if known_values
510
+ else "unknown",
511
+ "comparison_status": "complete"
512
+ if all_tasks_matched
513
+ and all(status == "complete" for status in comparison_statuses)
514
+ else "partial"
515
+ if all_tasks_matched
516
+ else "unknown",
517
+ "baseline_value": baseline_value,
518
+ "absolute_delta_vs_vanilla": delta,
519
+ "percentage_delta_vs_vanilla": delta / baseline_value * 100
520
+ if delta is not None and baseline_value
521
+ else None,
522
+ "absolute_delta_unit": "percentage points"
523
+ if guide.get("unit") == "% covered"
524
+ else guide.get("unit"),
525
+ "reason": "Analyzer provenance is missing or conflicting; overview values are unavailable. Raw observations and task medians remain separate."
526
+ if unresolved_analyzer
527
+ else "Each task has equal weight, regardless of repetitions. Deltas require matching vanilla and guidance medians for every assigned task. Partial raw averages describe the measured tasks only.",
528
+ }
529
+ )
530
+ return results
531
+
532
+
533
+ REFERENCE_METRICS = (
534
+ "Net cognitive complexity added",
535
+ "Net cyclomatic complexity added",
536
+ "Functions added",
537
+ "Source files added",
538
+ "Complexity hotspots added",
539
+ "Introduced dead findings",
540
+ "Introduced clone groups",
541
+ "Source lines added",
542
+ "Source lines removed",
543
+ "Source lines net",
544
+ )
545
+
546
+
547
+ def _reference_values(report: dict[str, object]) -> list[dict[str, object]]:
548
+ """Code measures of each task's reference solution, measured like a submission."""
549
+ specs = {spec[1]: spec for spec in METRICS}
550
+ rows: list[dict[str, object]] = []
551
+ for task, value in sorted(_map(report.get("references")).items()):
552
+ reference = _map(value)
553
+ comparison = _map(reference.get("comparison"))
554
+ static = (
555
+ "analysis"
556
+ if _map(comparison.get("analysis")).get("status") == "available"
557
+ else "fallow"
558
+ )
559
+ for metric in REFERENCE_METRICS:
560
+ _, _, kind, key = specs[metric]
561
+ number = None
562
+ if reference.get("status") == "available":
563
+ if kind == "fallow_change":
564
+ section, field = key.split(".")
565
+ detail = _map(_map(comparison.get(static)).get(section))
566
+ number = (
567
+ _signed(detail.get(field)) if detail.get("status") == "available" else None
568
+ )
569
+ elif kind == "loc_change":
570
+ size = _map(reference.get("code_size"))
571
+ number = (
572
+ _net_lines(size) if key == "source_lines_net" else _number(size.get(key))
573
+ )
574
+ rows.append(
575
+ {
576
+ "task": task,
577
+ "metric": metric,
578
+ "value": number,
579
+ "status": "measured" if number is not None else "unknown",
580
+ "reason": None
581
+ if number is not None
582
+ else str(reference.get("reason") or "reference measurement unavailable"),
583
+ }
584
+ )
585
+ return rows
586
+
587
+
588
+ def _reference_comparisons(data: dict[str, object]) -> list[dict[str, object]]:
589
+ """Attempt task medians as a ratio to the reference; a zero reference reports a difference."""
590
+ references = {(row["task"], row["metric"]): row for row in _rows(data["reference_values"])}
591
+ rows: list[dict[str, object]] = []
592
+ for median in _rows(data["raw_medians"]):
593
+ reference = references.get((median.get("task"), median.get("metric")))
594
+ if median.get("role") != "handoff" or reference is None:
595
+ continue
596
+ value, base = _signed(median.get("value")), _signed(reference.get("value"))
597
+ ratio = difference = None
598
+ if value is not None and base is not None:
599
+ difference = value - base
600
+ # A ratio is only meaningful against a positive reference; otherwise report a difference.
601
+ ratio = value / base if base > 0 else None
602
+ rows.append(
603
+ {
604
+ "task": median["task"],
605
+ "guidance": median["arm"],
606
+ "metric": median["metric"],
607
+ "value": value,
608
+ "reference": base,
609
+ "ratio_to_reference": ratio,
610
+ "difference_vs_reference": difference,
611
+ "status": "unknown"
612
+ if difference is None
613
+ else "ratio"
614
+ if ratio is not None
615
+ else "difference",
616
+ }
617
+ )
618
+ return rows
619
+
620
+
621
+ def _reference_aggregates(rows: list[dict[str, object]]) -> list[dict[str, object]]:
622
+ """Equal-weight mean of per-task ratios; tasks without a defined ratio stay out and are counted."""
623
+ groups: dict[tuple[str, str], dict[str, list[float | None]]] = {}
624
+ for row in rows:
625
+ tasks = groups.setdefault((str(row["guidance"]), str(row["metric"])), {})
626
+ tasks.setdefault(str(row["task"]), []).append(_signed(row["ratio_to_reference"]))
627
+ result: list[dict[str, object]] = []
628
+ for (guidance, metric), tasks in sorted(groups.items()):
629
+ # A task with several incompatible profiles has no single ratio.
630
+ ratios = [
631
+ values[0] for values in tasks.values() if len(values) == 1 and values[0] is not None
632
+ ]
633
+ result.append(
634
+ {
635
+ "guidance": guidance,
636
+ "metric": metric,
637
+ "ratio_to_reference": sum(ratios) / len(ratios) if ratios else None,
638
+ "measured_tasks": len(ratios),
639
+ "assigned_tasks": len(tasks),
640
+ "method": "equal-weight mean of per-task median ÷ reference",
641
+ }
642
+ )
643
+ return result
644
+
645
+
646
+ def _passing_summaries(
647
+ data: dict[str, object],
648
+ ) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
649
+ """Task medians over passing attempts only, and equal-weight task means per analyzer group."""
650
+ groups: dict[tuple[str, str, str], dict[object, list[float]]] = {}
651
+ arms = sorted({str(row["arm"]) for row in _rows(data["observations"])})
652
+ for row in _rows(data["observations"]):
653
+ value = _signed(row.get("value"))
654
+ if row.get("correctness") != "pass" or value is None or row.get("compatibility") is None:
655
+ continue
656
+ key = (str(row["task"]), str(row["arm"]), str(row["metric"]))
657
+ groups.setdefault(key, {}).setdefault(row["compatibility"], []).append(value)
658
+ medians: list[dict[str, object]] = []
659
+ for (task, arm, metric), profiles in sorted(groups.items()):
660
+ if len(profiles) != 1:
661
+ continue # unlike measurement profiles are never pooled
662
+ [values] = profiles.values()
663
+ medians.append(
664
+ {
665
+ "task": task,
666
+ "arm": arm,
667
+ "metric": metric,
668
+ "value": statistics.median(values),
669
+ "passing": len(values),
670
+ }
671
+ )
672
+ by_key = {(row["task"], row["arm"], row["metric"]): float(str(row["value"])) for row in medians}
673
+ aggregates: list[dict[str, object]] = []
674
+ # Same task partition as the all-attempt aggregates: unlike analyzer scores are never averaged.
675
+ for metric, group, tasks in _aggregate_groups(data):
676
+ # Like the all-attempt overview, unresolved analyzer groups get no pooled value.
677
+ unresolved = group in {"Unknown analyzer", "Conflicting analyzers"}
678
+ for arm in arms:
679
+ values = [
680
+ by_key[(task, arm, metric)]
681
+ for task in tasks
682
+ if (task, arm, metric) in by_key and not unresolved
683
+ ]
684
+ aggregates.append(
685
+ {
686
+ "arm": arm,
687
+ "metric": metric,
688
+ "analysis_group": group,
689
+ "value": sum(values) / len(values) if values else None,
690
+ "measured_tasks": len(values),
691
+ "assigned_tasks": len(tasks),
692
+ "method": "passing attempts only; equal-weight mean of per-task medians",
693
+ }
694
+ )
695
+ return medians, aggregates
696
+
697
+
698
+ def _cost_rows(
699
+ observations: list[dict[str, object]], prices: dict[str, object]
700
+ ) -> list[dict[str, object]]:
701
+ """Coding dollars per attempt from a dated per-model price table; no table means unknown."""
702
+ tokens: dict[str, dict[str, object]] = {}
703
+ for row in observations:
704
+ if row.get("family") == "tokens":
705
+ tokens.setdefault(
706
+ str(row["attempt_id"]),
707
+ {"task": row["task"], "arm": row["arm"], "model": row["model"]},
708
+ )[str(row["metric"])] = _number(row.get("value"))
709
+ rows = []
710
+ for attempt_id, values in sorted(tokens.items()):
711
+ price = _map(prices.get(str(values["model"])))
712
+ parts = [
713
+ _number(values.get(name))
714
+ for name in ("Uncached input tokens", "Cached input tokens", "Output tokens")
715
+ ]
716
+ rates = [
717
+ _number(price.get(name))
718
+ for name in ("input_per_million", "cached_input_per_million", "output_per_million")
719
+ ]
720
+ known_parts = [part for part in parts if part is not None]
721
+ known_rates = [rate for rate in rates if rate is not None]
722
+ dated = isinstance(price.get("as_of"), str) and bool(price.get("as_of"))
723
+ known = len(known_parts) == len(known_rates) == 3 and dated
724
+ rows.append(
725
+ {
726
+ "attempt_id": attempt_id,
727
+ "task": values["task"],
728
+ "arm": values["arm"],
729
+ "model": values["model"],
730
+ "dollars": sum(
731
+ part * rate for part, rate in zip(known_parts, known_rates, strict=True)
732
+ )
733
+ / 1_000_000
734
+ if known
735
+ else None,
736
+ "price_as_of": price.get("as_of"),
737
+ "reason": None
738
+ if known
739
+ else "no price for this model"
740
+ if not price
741
+ else "price table entry lacks an as_of date"
742
+ if not dated
743
+ else "price table entry lacks a rate"
744
+ if len(known_rates) < 3
745
+ else "token counts unavailable",
746
+ "limitations": "uncached input is priced at the input rate; Claude cache writes, which cost more, are included there",
747
+ }
748
+ )
749
+ return rows
750
+
751
+
752
+ def evaluation_data(report: dict[str, object], config: dict[str, object]) -> dict[str, object]:
753
+ """Select measurements and derive baseline comparisons without scanning or model calls."""
754
+ selected = config.get("measurements")
755
+ if isinstance(selected, list) and "test_overlap" in selected:
756
+ raise ValueError(REMOVED_TEST_OVERLAP)
757
+ if (
758
+ not isinstance(selected, list)
759
+ or not selected
760
+ or any(not isinstance(name, str) or name not in MEASUREMENT_METRICS for name in selected)
761
+ ):
762
+ raise ValueError("measurements must select supported measurement families")
763
+ families = {metric: family for family in selected for metric in MEASUREMENT_METRICS[family]}
764
+ projection_source = (
765
+ {**report, "independent_review": {"status": "unknown"}}
766
+ if "review" in selected and not report.get("independent_review")
767
+ else report
768
+ )
769
+ projection = chart_data(projection_source)
770
+ guide = _map(projection["metric_guide"])
771
+ handoffs = {
772
+ attempt.get("id"): next(
773
+ row for row in _rows(attempt.get("stages")) if row.get("id") == attempt.get("handoff")
774
+ )
775
+ for attempt in _rows(report.get("attempts"))
776
+ }
777
+ states = {
778
+ attempt_id: correctness_state(
779
+ _map(stage.get("correctness")).get("status", "unknown"), stage.get("task_result")
780
+ )
781
+ for attempt_id, stage in handoffs.items()
782
+ }
783
+ # Pairs the check recorded as not measured. One collector serves several measures, so a
784
+ # marked pair's collector can still run; its values are hidden, not reported.
785
+ skipped = {
786
+ (str(row["task"]), str(row["measure"])): row.get("reason")
787
+ for row in _rows(config.get("not_measured", []))
788
+ }
789
+ observations = []
790
+ for source in _rows(projection["observations"]):
791
+ if source.get("metric") not in families:
792
+ continue
793
+ metric = str(source["metric"])
794
+ row = {field: source.get(field) for field in OBSERVATION_FIELDS}
795
+ row.update(family=families[metric], unit=_map(guide.get(metric)).get("unit"))
796
+ if row.get("role") == "handoff" and row.get("attempt_id") in states:
797
+ row["correctness"] = states[row["attempt_id"]]
798
+ capability = _map(
799
+ _map(_map(config.get("measurement_capabilities")).get(str(row.get("task")))).get(
800
+ families[metric]
801
+ )
802
+ )
803
+ if row.get("value") is None and capability.get("status") == "unavailable":
804
+ row.update(status="unavailable", reason=capability.get("reason"))
805
+ row["provenance"] = {
806
+ key: value
807
+ for key, value in _map(source.get("provenance")).items()
808
+ if key in PROVENANCE_FIELDS
809
+ }
810
+ if (str(row.get("task")), families[metric]) in skipped:
811
+ # A masked row keeps no measurement result: no value, counts, or population.
812
+ row.update(
813
+ value=None,
814
+ status="unavailable",
815
+ reason=skipped[str(row.get("task")), families[metric]],
816
+ compatibility=None,
817
+ per_test_count=None,
818
+ provenance={},
819
+ )
820
+ observations.append(row)
821
+
822
+ def shown(median: dict[str, object]) -> bool:
823
+ metric = median.get("metric")
824
+ return metric in families and (str(median.get("task")), families[metric]) not in skipped
825
+
826
+ attempts = []
827
+ origins = _map(report.get("baseline_origin"))
828
+ for attempt in _rows(report.get("attempts")):
829
+ stage = handoffs[attempt.get("id")]
830
+ provenance = _map(attempt.get("provenance"))
831
+ execution = _map(stage.get("execution_provenance"))
832
+ attempts.append(
833
+ {
834
+ "id": attempt.get("id"),
835
+ "repeat": attempt.get("attempt"),
836
+ "task": attempt.get("task_id"),
837
+ "guidance": attempt.get("arm"),
838
+ "baseline_origin": {
839
+ key: _map(origins.get(str(attempt.get("id")))).get(key)
840
+ # origin_run is the absolute path historical runs recorded.
841
+ for key in (
842
+ "cache_key", "origin_cohort", "origin_run", "origin_attempt", "evidence"
843
+ )
844
+ }
845
+ if str(attempt.get("id")) in origins
846
+ else None,
847
+ "correctness": states[attempt.get("id")],
848
+ "native_grading": stage.get("task_result"),
849
+ "execution": {
850
+ **execution_outcome(execution),
851
+ **{
852
+ key: execution.get(key)
853
+ for key in (
854
+ "successful_native_completion",
855
+ "timed_out",
856
+ "exit_code",
857
+ "stream_errors",
858
+ "partial_usage",
859
+ "usage_accounting",
860
+ "actual_descendant_models",
861
+ "native_treatment",
862
+ "native_session_usage",
863
+ "error",
864
+ "attempt_status",
865
+ )
866
+ },
867
+ },
868
+ "actual_model": attempt.get("model"),
869
+ "runtime": {
870
+ name: provenance.get(name)
871
+ for name in (
872
+ "host",
873
+ "harness",
874
+ "harness_version",
875
+ "reasoning_effort",
876
+ "requested_reasoning_effort",
877
+ "effort_evidence_source",
878
+ )
879
+ },
880
+ "requested_model": provenance.get(
881
+ "assigned_model", _map(config.get("runner")).get("model")
882
+ ),
883
+ "artifacts": {
884
+ "execution": _map(stage.get("execution_provenance")).get("artifact"),
885
+ "snapshot_digest": stage.get("snapshot_digest"),
886
+ "before_digest": attempt.get("before_digest"),
887
+ },
888
+ }
889
+ )
890
+ limitations = [
891
+ "Each guidance set is compared with the same vanilla cohort. Compare separate evaluations through compatible baseline deltas.",
892
+ "Deltas are differences in observed medians, not a winner or composite quality score. A negative delta is not automatically better.",
893
+ ]
894
+ environments = _map(_map(config.get("comparison_identity")).get("task_environment"))
895
+ for task, environment in sorted(environments.items()):
896
+ supplied = _map(_map(environment).get("supplied_git_identity"))
897
+ if supplied:
898
+ limitations.append(
899
+ f"Task {task} sets no git identity, so PluginBench supplied "
900
+ f"{supplied.get('name')} <{supplied.get('email')}> to its agents."
901
+ )
902
+ unfinished = _map(config.get("unfinished_run"))
903
+ if unfinished:
904
+ limitations.insert(
905
+ 0,
906
+ f"This run is not finished: {unfinished['complete']} of {unfinished['assigned']} "
907
+ "attempts are complete. This report is partial: results that are missing stay "
908
+ "unknown.",
909
+ )
910
+ if origins:
911
+ limitations.append(
912
+ f"{len(origins)} vanilla attempts were reused from earlier evaluations. Their tokens and time are original observations, not fresh independent samples. Grading, measurements, and reviews run again."
913
+ )
914
+ if {"cyclomatic", "cognitive"}.intersection(selected):
915
+ limitations.append(
916
+ "Complexity counts what each submission added to source code: changed functions after the edit minus the same functions before. Unlike analyzers are reported separately; ratios compare each task with its reference solution."
917
+ )
918
+ if "coverage" in selected:
919
+ limitations.append(
920
+ "Coverage counts are proxies, not proof of correct assertions or unnecessary tests."
921
+ )
922
+ if "review" in selected:
923
+ limitations.append(
924
+ "AI review findings are judgments, not deterministic defect counts. The reviewer is not calibrated against human judgments; recorded reviews do not measure human review churn."
925
+ )
926
+ if config.get("review_projection") is not None:
927
+ limitations.append(
928
+ "This report selects a separate review revision. Judge charts show that revision's usage; bound prior review receipts retain earlier calls, failures, and usage."
929
+ )
930
+ data = {
931
+ "schema_version": 1,
932
+ "kind": "evaluation",
933
+ "name": report.get("name"),
934
+ "comparison_identity": config.get("comparison_identity"),
935
+ "guidance_packages": config.get("guidance_packages", {}),
936
+ "artifact_base": config.get("artifact_base", "."),
937
+ "runner": config.get("runner"),
938
+ "jobs": config.get("jobs", 1),
939
+ "reviewer": config.get("reviewer") if "review" in selected else None,
940
+ "measurements": selected,
941
+ "measurement_collection": config.get("measurement_collection"),
942
+ "measurement_capabilities": config.get("measurement_capabilities", {}),
943
+ "measurement_readiness": config.get("measurement_readiness", {}),
944
+ "assigned": len(attempts),
945
+ "coding_invocations": _map(report.get("study_progress")).get("native_invocations"),
946
+ "reused_baseline_attempts": len(origins),
947
+ "attempts": attempts,
948
+ "observations": observations,
949
+ "observation_fields": list(OBSERVATION_FIELDS),
950
+ "medians": [row for row in _rows(projection["medians"]) if shown(row)],
951
+ "raw_medians": [row for row in _rows(projection["raw_medians"]) if shown(row)],
952
+ "metric_guide": {metric: guide[metric] for metric in families},
953
+ "independent_review": projection.get("independent_review")
954
+ if "review" in selected
955
+ else None,
956
+ "review_findings": projection.get("review_findings") if "review" in selected else [],
957
+ "download_csv": "results.csv",
958
+ "diagnostic_panels": sorted(DIAGNOSTIC_PANELS),
959
+ "limitations": limitations,
960
+ }
961
+ if unfinished:
962
+ data["unfinished_run"] = unfinished
963
+ data["comparisons"] = _comparison_rows(data)
964
+ data["aggregates"] = _aggregate_rows(data)
965
+ data["passing_medians"], data["passing_aggregates"] = _passing_summaries(data)
966
+ owners = {metric: name for name, metrics in MEASUREMENT_METRICS.items() for metric in metrics}
967
+ data["reference_values"] = [
968
+ # A listed pair's reference is masked like its observations, with the same reason.
969
+ {**row, "value": None, "status": "unavailable", "reason": skipped[key]}
970
+ if (key := (str(row["task"]), owners.get(str(row["metric"]), ""))) in skipped
971
+ else row
972
+ for row in _reference_values(report)
973
+ ]
974
+ data["reference_comparisons"] = _reference_comparisons(data)
975
+ data["reference_aggregates"] = _reference_aggregates(_rows(data["reference_comparisons"]))
976
+ prices = _map(config.get("prices"))
977
+ data["prices"] = prices or None
978
+ data["cost"] = _cost_rows(observations, prices) if "tokens" in selected else []
979
+ data["measurement_completeness"] = _selected_completeness(
980
+ data=data, projection=projection, selected=selected, skipped=skipped
981
+ )
982
+ data["completeness_matrix"] = _completeness_matrix(
983
+ observations, _map(config.get("measurement_capabilities"))
984
+ )
985
+ if config.get("review_projection") is not None:
986
+ data["review_projection"] = config["review_projection"]
987
+ return data
988
+
989
+
990
+ def write_evaluation_report(
991
+ report: dict[str, object], config: dict[str, object], out_dir: Path
992
+ ) -> Path:
993
+ if out_dir.is_symlink() or any(
994
+ (out_dir / name).is_symlink() for name in ("results.json", "results.csv", "report.html")
995
+ ):
996
+ raise ValueError("report outputs must not be symlinks")
997
+ data = evaluation_data(report, config)
998
+ html, csv_text = chart_artifacts_from_data(data)
999
+ out_dir.mkdir(parents=True, exist_ok=True)
1000
+ result = out_dir / "results.json"
1001
+ result.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n")
1002
+ (out_dir / "results.csv").write_text(csv_text)
1003
+ (out_dir / "report.html").write_text(html)
1004
+ return result