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,316 @@
1
+ """Assess immutable implementation stages without executing models or analysis tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from pluginbench.assessment_metrics import (
13
+ compare_evidence,
14
+ compare_snapshots,
15
+ evaluate_evidence,
16
+ inspect_snapshot,
17
+ scoped_code_size,
18
+ scoped_test_size,
19
+ )
20
+ from pluginbench.assessment_models import (
21
+ ArtifactRef,
22
+ AssessmentInput,
23
+ Attempt,
24
+ CorrectnessReceipt,
25
+ Snapshot,
26
+ Stage,
27
+ )
28
+
29
+
30
+ def _document(path: Path) -> object:
31
+ return yaml.safe_load(path.read_text())
32
+
33
+
34
+ def _mapping(value: object) -> dict[str, object]:
35
+ if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
36
+ raise ValueError("assessment evidence must be an object")
37
+ return {key: item for key, item in value.items()}
38
+
39
+
40
+ def _artifact(base: Path, reference: ArtifactRef) -> Path:
41
+ path = base / reference.path
42
+ if path.is_symlink():
43
+ raise ValueError(f"assessment artifact is a symlink: {path}")
44
+ digest = "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
45
+ if digest != reference.sha256:
46
+ raise ValueError(f"assessment artifact digest mismatch: {path}")
47
+ return path.resolve()
48
+
49
+
50
+ def _snapshot(base: Path, reference: ArtifactRef) -> Snapshot:
51
+ path = base / reference.path
52
+ if inspect_snapshot(path)["digest"] != reference.sha256:
53
+ raise ValueError(f"snapshot digest mismatch: {path}")
54
+ return Snapshot(path.resolve(), reference.sha256)
55
+
56
+
57
+ def _snapshot_metadata(snapshot: Snapshot) -> dict[str, object]:
58
+ metadata = inspect_snapshot(snapshot.root)
59
+ if metadata["digest"] != snapshot.digest:
60
+ raise ValueError(f"snapshot changed during assessment: {snapshot.root}")
61
+ return metadata
62
+
63
+
64
+ def _load_manifest(path: Path) -> tuple[str, list[Attempt]]:
65
+ manifest = AssessmentInput.model_validate(_document(path))
66
+ base = path.parent
67
+ records = []
68
+ for entry in manifest.records:
69
+ stages = []
70
+ for item in entry.stages:
71
+ snapshot = _snapshot(base, item.snapshot) if item.snapshot is not None else None
72
+ correctness = None
73
+ if item.correctness is not None:
74
+ correctness = CorrectnessReceipt.model_validate(
75
+ _document(_artifact(base, item.correctness))
76
+ )
77
+ if snapshot is None or correctness.snapshot_digest != snapshot.digest:
78
+ raise ValueError(
79
+ f"correctness is bound to another snapshot: {entry.id}/{item.id}"
80
+ )
81
+ evidence: dict[str, str] | None = (
82
+ {kind: str(_artifact(base, ref)) for kind, ref in item.evidence.items()}
83
+ if item.evidence is not None
84
+ else None
85
+ )
86
+ stages.append(Stage(item.id, snapshot, correctness, evidence, item.work))
87
+ records.append(
88
+ Attempt(
89
+ entry.id,
90
+ entry.task_id,
91
+ entry.attempt,
92
+ entry.arm,
93
+ entry.model,
94
+ _snapshot(base, entry.before) if entry.before else None,
95
+ tuple(stages),
96
+ {
97
+ "input": "manifest",
98
+ "manifest": str(path),
99
+ **(entry.runtime.model_dump() if entry.runtime else {}),
100
+ },
101
+ (
102
+ {kind: str(_artifact(base, ref)) for kind, ref in entry.before_evidence.items()}
103
+ if entry.before_evidence is not None
104
+ else None
105
+ ),
106
+ )
107
+ )
108
+ return manifest.name, records
109
+
110
+
111
+ def _correctness(receipt: CorrectnessReceipt | None, required: list[str]) -> dict[str, object]:
112
+ if receipt is None:
113
+ return {"status": "unknown", "checks": {}, "reason": "no bound correctness receipt"}
114
+ checks = receipt.checks
115
+ missing = sorted(
116
+ {name for name in required if checks.get(name) is None}
117
+ | {name for name, value in checks.items() if value is None}
118
+ )
119
+ status = (
120
+ "fail"
121
+ if any(value is False for value in checks.values())
122
+ else "unknown"
123
+ if missing
124
+ else "pass"
125
+ )
126
+ return {
127
+ "status": status,
128
+ "checks": checks,
129
+ "missing_required_checks": missing,
130
+ "command": receipt.command,
131
+ "tool": receipt.tool,
132
+ "tool_version": receipt.tool_version,
133
+ "snapshot_digest": receipt.snapshot_digest,
134
+ }
135
+
136
+
137
+ def _code_size(
138
+ record: Attempt,
139
+ stage: Stage,
140
+ before_metrics: dict[str, object],
141
+ metrics: dict[str, object],
142
+ change: dict[str, object] | None,
143
+ ) -> dict[str, object]:
144
+ kinds = {
145
+ kind
146
+ for kind in ("fallow", "analysis", "coverage")
147
+ if kind in (record.before_evidence or {}) or kind in (stage.evidence or {})
148
+ }
149
+ if not kinds:
150
+ return {"status": "not configured", "reason": "no declared source measurement scope"}
151
+ scopes = []
152
+ for kind in sorted(kinds):
153
+ for measured in (before_metrics, metrics):
154
+ scope = _mapping(_mapping(measured.get(kind, {})).get("provenance", {})).get("scope")
155
+ if (
156
+ not isinstance(scope, list)
157
+ or not scope
158
+ or any(not isinstance(pattern, str) or not pattern for pattern in scope)
159
+ ):
160
+ return {"status": "unavailable", "reason": f"missing bound {kind} source scope"}
161
+ scopes.append(sorted({str(pattern).removeprefix("./") for pattern in scope}))
162
+ if any(scope != scopes[0] for scope in scopes):
163
+ return {"status": "unavailable", "reason": "bound source measurement scopes disagree"}
164
+ if record.before is None or stage.snapshot is None or change is None:
165
+ return {"status": "unavailable", "reason": "source-size comparison needs both snapshots"}
166
+ try:
167
+ return scoped_code_size(
168
+ before=record.before.root, after=stage.snapshot.root, scope=scopes[0], change=change
169
+ )
170
+ except ValueError as error:
171
+ return {"status": "unavailable", "reason": str(error)}
172
+
173
+
174
+ def _test_code_size(
175
+ record: Attempt,
176
+ stage: Stage,
177
+ before_metrics: dict[str, object],
178
+ metrics: dict[str, object],
179
+ change: dict[str, object] | None,
180
+ ) -> dict[str, object]:
181
+ if "coverage" not in (record.before_evidence or {}) and "coverage" not in (
182
+ stage.evidence or {}
183
+ ):
184
+ return {"status": "not configured", "reason": "no bound visible-test scope"}
185
+ scopes = []
186
+ for measured in (before_metrics, metrics):
187
+ scope = _mapping(_mapping(measured.get("coverage", {})).get("metrics") or {}).get(
188
+ "test_scope"
189
+ )
190
+ if (
191
+ not isinstance(scope, list)
192
+ or not scope
193
+ or any(not isinstance(pattern, str) or not pattern for pattern in scope)
194
+ ):
195
+ return {"status": "unavailable", "reason": "missing bound visible-test scope"}
196
+ scopes.append(sorted({str(pattern).removeprefix("./") for pattern in scope}))
197
+ if scopes[0] != scopes[1]:
198
+ return {"status": "unavailable", "reason": "bound visible-test scopes disagree"}
199
+ if record.before is None or stage.snapshot is None or change is None:
200
+ return {"status": "unavailable", "reason": "test-size comparison needs both snapshots"}
201
+ try:
202
+ return scoped_test_size(
203
+ before=record.before.root, after=stage.snapshot.root, scope=scopes[0], change=change
204
+ )
205
+ except ValueError as error:
206
+ return {"status": "unavailable", "reason": str(error)}
207
+
208
+
209
+ def _stage(
210
+ record: Attempt,
211
+ stage: Stage,
212
+ base: Path,
213
+ before_metrics: dict[str, object],
214
+ ) -> dict[str, object]:
215
+ correctness = _correctness(stage.correctness, ["behavior"])
216
+ if stage.snapshot is None:
217
+ return {
218
+ "id": stage.id,
219
+ "snapshot_digest": None,
220
+ "snapshot_metadata": None,
221
+ "correctness": correctness,
222
+ "work": stage.work.model_dump() if stage.work else None,
223
+ "metrics": None,
224
+ "change": None,
225
+ }
226
+ change = compare_snapshots(record.before.root, stage.snapshot.root) if record.before else None
227
+ metrics = evaluate_evidence(
228
+ stage.snapshot.root,
229
+ stage.evidence,
230
+ base,
231
+ )
232
+ return {
233
+ "id": stage.id,
234
+ "snapshot_digest": stage.snapshot.digest,
235
+ "snapshot_metadata": _snapshot_metadata(stage.snapshot),
236
+ "correctness": correctness,
237
+ "work": stage.work.model_dump() if stage.work else None,
238
+ "change": change,
239
+ "metrics": metrics,
240
+ "code_size": _code_size(record, stage, before_metrics, metrics, change),
241
+ "test_code_size": _test_code_size(record, stage, before_metrics, metrics, change),
242
+ "metric_comparison": compare_evidence(before_metrics, metrics, change=change),
243
+ }
244
+
245
+
246
+ def build_assessment(input_path: Path) -> dict[str, Any]:
247
+ """Read a native study manifest; never invoke auth or tools."""
248
+ source = input_path.expanduser().resolve()
249
+ name, records = _load_manifest(source)
250
+ attempts = []
251
+ for record in records:
252
+ base = source.parent
253
+ before_metrics = (
254
+ evaluate_evidence(
255
+ record.before.root,
256
+ record.before_evidence,
257
+ base,
258
+ )
259
+ if record.before
260
+ else {}
261
+ )
262
+ stages = [_stage(record, stage, base, before_metrics) for stage in record.stages]
263
+ attempts.append(
264
+ {
265
+ "id": record.id,
266
+ "task_id": record.task_id,
267
+ "attempt": record.attempt,
268
+ "arm": record.arm,
269
+ "model": record.model,
270
+ "before_digest": record.before.digest if record.before else None,
271
+ "before_snapshot_metadata": _snapshot_metadata(record.before)
272
+ if record.before
273
+ else None,
274
+ "before_metrics": before_metrics,
275
+ "configured_metrics": sorted(
276
+ set(record.before_evidence or {})
277
+ | {kind for stage in record.stages for kind in (stage.evidence or {})}
278
+ ),
279
+ "stages": stages,
280
+ "handoff": stages[0]["id"],
281
+ "provenance": record.provenance,
282
+ }
283
+ )
284
+ return {
285
+ "schema_version": 1,
286
+ "name": name,
287
+ "input": str(source),
288
+ "snapshot_roots": sorted(
289
+ {
290
+ str(snapshot.root)
291
+ for record in records
292
+ for snapshot in (record.before, *(stage.snapshot for stage in record.stages))
293
+ if snapshot is not None
294
+ }
295
+ ),
296
+ "attempts": attempts,
297
+ "limitations": [
298
+ "Every attempt is retained; independent repeats are not correction stages.",
299
+ "Bound receipts are supplied evidence, not new tool execution or proof of causality.",
300
+ "No composite score or semantic test-redundancy claim is inferred.",
301
+ "Missing metric, usage, or correctness evidence remains unavailable.",
302
+ ],
303
+ }
304
+
305
+
306
+ def write_assessment(report: dict[str, Any], out_dir: Path) -> Path:
307
+ """Persist the canonical assessment consumed by the public report renderer."""
308
+ out_dir = out_dir.expanduser().resolve()
309
+ if any(out_dir.is_relative_to(Path(root)) for root in report["snapshot_roots"]):
310
+ raise ValueError("assessment output must be outside every input snapshot")
311
+ json_path = out_dir / "assessment-report.json"
312
+ if json_path.is_symlink():
313
+ raise ValueError("assessment output cannot overwrite a symlink")
314
+ out_dir.mkdir(parents=True, exist_ok=True)
315
+ json_path.write_text(json.dumps(report, indent=2) + "\n")
316
+ return json_path