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.
- pluginbench/__init__.py +3 -0
- pluginbench/__main__.py +3 -0
- pluginbench/assessment-charts.html +790 -0
- pluginbench/assessment.py +316 -0
- pluginbench/assessment_charts.py +1225 -0
- pluginbench/assessment_metrics.py +1956 -0
- pluginbench/assessment_models.py +127 -0
- pluginbench/baselines.py +792 -0
- pluginbench/claude.py +374 -0
- pluginbench/cli.py +672 -0
- pluginbench/code_diff.py +207 -0
- pluginbench/codex.py +451 -0
- pluginbench/codex_events.py +585 -0
- pluginbench/collectors.py +591 -0
- pluginbench/compare.py +927 -0
- pluginbench/components.py +129 -0
- pluginbench/coverage_report.py +610 -0
- pluginbench/datasets.py +207 -0
- pluginbench/evaluation.example.yaml +45 -0
- pluginbench/evaluation.py +1651 -0
- pluginbench/evaluation_report.py +1004 -0
- pluginbench/fallow_parse.mjs +101 -0
- pluginbench/harbor_backend.py +2946 -0
- pluginbench/isolation.py +314 -0
- pluginbench/judge.py +683 -0
- pluginbench/language_analysis.py +915 -0
- pluginbench/ledger.py +217 -0
- pluginbench/measurement_profiles.py +616 -0
- pluginbench/native_treatment_runtime.js +221 -0
- pluginbench/native_treatments.py +155 -0
- pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
- pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
- pluginbench/observer_data/go-analysis-v1/main.go +64 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
- pluginbench/parallel.py +101 -0
- pluginbench/processes.py +100 -0
- pluginbench/remeasure.py +992 -0
- pluginbench/run_lock.py +227 -0
- pluginbench/run_status.py +518 -0
- pluginbench/study.py +1858 -0
- pluginbench/study_review.py +924 -0
- pluginbench/submissions.py +486 -0
- pluginbench/task_sources.py +403 -0
- pluginbench/treatments.py +388 -0
- pluginbench/trial_containers.py +94 -0
- pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench/vendor/ZRENDER-LICENSE +29 -0
- pluginbench/vendor/echarts.min.js +48 -0
- pluginbench-0.5.0.dist-info/METADATA +294 -0
- pluginbench-0.5.0.dist-info/RECORD +58 -0
- pluginbench-0.5.0.dist-info/WHEEL +4 -0
- pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
- pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
- 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
|