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,486 @@
1
+ """Canonical source for explicitly reviewed, patch-grading native contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import tomllib
13
+ from pathlib import Path, PurePosixPath
14
+ from typing import Any
15
+
16
+ from pluginbench.assessment_metrics import EXCLUDED_NAMES, inspect_snapshot
17
+ from pluginbench.code_diff import _file_changes
18
+
19
+ SOURCE_POLICY = "reviewed-native-patch-source-v2"
20
+ GRADER_STARTUP_POLICY = "reviewed-native-grader-startup-v1"
21
+ _GRADER_ENVIRONMENT = {
22
+ "APP_DIR": "/app",
23
+ "TESTS_DIR": "/tests",
24
+ "ARTIFACTS_DIR": "/logs/artifacts",
25
+ "VERIFIER_DIR": "/logs/verifier",
26
+ }
27
+ _GRADER_SHA256 = "47cc9eaadf21e636323c360ec4fa786f0733ec9fd1d21ea5a5717ff9f8c4077c"
28
+ # Reviewed complete verifier trees, including task entrypoint, config and hidden patch.
29
+ # A shared grader filename or even its fingerprint alone is insufficient admission.
30
+ _REVIEWED_TESTS = {
31
+ "ac4aadf99cfbda751fb12f9faef03cc6a3ccbeed8cf1ccda9441c811ef247acc": "d8fbebc75de4991a32354518beff1abf628d0b07",
32
+ "9caf86b1898a4e1f9077e7ea9ed7f7e46c16bec89526916cf56a1d50187acfb5": "88e3d965c0b1628642a30a841745b410d6835052",
33
+ "0eaf29b862c00fe04c8d5f76b63297fd15e8193da13b548f9a68e57b8ccf276d": "a691069fcadf9131e5f7a5a130a022dc678f3e1d",
34
+ "5712990cdc674f4a167fc86ac9104daa7070cadbd051eeb329589a0fa1b174db": "c981f9c7aa91b83cc9bd33a09ecee9e751b06e8d",
35
+ # happy-dom-abort-pending-body-reads and testem-per-launcher-reports at DeepSWE 0b9fabbb.
36
+ "9d5f5c01aeb6935be963a7858beeacc97f231b92ccd34925a94ba5a46454673c": "82a0888cb2c87a6123e05424b528f8e8c9b3e426",
37
+ "eda69e4b08e0976c74e59f4556e97ac0721b0ff0e154f58b6c3ec20b52249f10": "158f61ea91c9613d2011c41ee9be40ada1d7a307",
38
+ # go-critic-doc-link-checker at DeepSWE 0b9fabbb.
39
+ "7aadc441e56f0c2a29a10e560a04720c2e41a71751b67d2a93981ce9edcbdcb1": "9aea378c4dccd6f4394196ad8f0873b3e84678c8",
40
+ # oxvg-structural-selector-preservation at DeepSWE 0b9fabbb.
41
+ "f34b8dfebb3f2a53d473f0b7914a4afe8fae1ca6003fbd0b12ed80dd1cdd5f4d": "1fd7fab851ecc975e008be0e3e279568ce4e2b51",
42
+ # termenv-preserve-ansi-resets at DeepSWE 0b9fabbb.
43
+ "02d9f47229ee4e271c31e627484f0182ed0a37ad75ee2af65590f6817a640bc1": "368a3572b8146cc038b3f240da6792003d7e42c5",
44
+ # ts-pattern-match-each at DeepSWE 0b9fabbb.
45
+ "5ac2f376722eda2df307bfa4c86ba1fd3e93267928a212da5840c9f7f11f851e": "f66fc061fde4f764b113ededa09be63dae564159",
46
+ # cattrs-partial-structuring-recovery at DeepSWE 0b9fabbb.
47
+ "62c19e25862ba6465d0fba5be783857e4b874457850813b5d8fe588146d3e70c": "6bc4708fb9b2ac52d9a18997e923da6a58916102",
48
+ # skrub-duration-encoding at DeepSWE 0b9fabbb.
49
+ "6a75f354bd229f0f45b7b51ee9fb74fb6d40de576ae9ec707a870cfeafd8879c": "24c4466fea94f551fb73d21eba54038dc5b346d3",
50
+ # wasmi-trap-coredumps at DeepSWE 0b9fabbb.
51
+ "b93c2efb48680988a72a0c617ec66d81b0e4d9ccd9a5432052183011d9bd12d9": "e1f76e285b9ad68a952b7cf5297bbb7ab91e6028",
52
+ # fd-deterministic-multi-key-sorting at DeepSWE 0b9fabbb.
53
+ "29a16328fb01fb1deec3abde7c841279169ac5eca614d9293131f5261f055390": "227883606023d62275fb48701aeac90f2b604143",
54
+ # boa-hierarchical-evaluation-cancellation at DeepSWE 0b9fabbb.
55
+ "70754686d59a946aa5823e134462a23b300687db4e4b7bffe756258fa667a959": "70409a5052984325dccfdc5f6520818568a81f39",
56
+ # tengo-destructuring-bindings at DeepSWE 0b9fabbb.
57
+ "277fcdba6cd4102362294c1f44fda1fe6fdd7a7bb6fe1bfc7b8bb6e6401fab66": "3cad0da7a51b1206c6f01e3f4fbb44b976d5275c",
58
+ # tomlkit-toml-table-converters at DeepSWE 0b9fabbb.
59
+ "263d0a5fb6423b68487bed4a9de25c1cfeb73960732dcd33534ef9615c15cc8d": "dd05eebc8ed9e30fc6c223088a5a450cb54c1cab",
60
+ # abs-stepped-slices at DeepSWE 0b9fabbb.
61
+ "39effb9fa65a618d116f608e75b3e49c220b58e9a4441f3329f28c8d2ea9a7b7": "cb1b3b671d0ee9fa9da9f7b02f86967953ffd10a",
62
+ # expr-try-catch-errors at DeepSWE 0b9fabbb.
63
+ "98c702c7f932c4407d44448022978af1d5cb73cd1bf8cc80dd8bbbb77a9d2475": "851b241a301f7c74646e65e4009c69cf290993a8",
64
+ # kysely-window-grouping-helpers at DeepSWE 0b9fabbb.
65
+ "a9ced82977aea08738b3d6b865be2cd1cc63709f31aa872c9d23854f6d40f10f": "91cf3733b2a419f5b17dff118cedb7052ab5300d",
66
+ # actionlint-action-pinning-lint at DeepSWE 0b9fabbb.
67
+ "5762d47f0d98aebf38297b3fc166b270237e94e72e02a9b588b3bbf9d7ca6d8b": "0bdc95715fa58f64e3fd6e63b0f89be8733cbbab",
68
+ # etree-xml-diff-patch at DeepSWE 0b9fabbb.
69
+ "cc9c0afada1f2fb84ef17c9ad7832090d385fc8803e5b1b4f0d476ccae84a1d3": "4032e04c8f2e2f35e43ce5d772fcef14a5df4d74",
70
+ # returns-validated-error-accumulation at DeepSWE 0b9fabbb.
71
+ "e92a9be1e52f9fb15aef1d6264122298a88c5d9f9c80d68fe2fcf2cd48fb424b": "41607fae1289de2787523c452d75212206b9c7c0",
72
+ # anko-default-function-arguments at DeepSWE 0b9fabbb.
73
+ "7b9bfaaa953eef4e6052b163b2004dc18f072f0b9b465348ba1976db9ba050d5": "9d2d84bb1564e9513287998c56ccf16c01c19008",
74
+ # textual-richlog-follow-state at DeepSWE 0b9fabbb.
75
+ "078133321f346427078548e9f0f21aa45b0b28d3ead4fc6df918dfd5522ace51": "0f0849fd37fbd0d4d6f81889476c22340129df67",
76
+ # bandit-structured-nosec-directives at DeepSWE 0b9fabbb.
77
+ "ad33fe4854693f57f9ba084d7e4443e9513524051ea8401e1f2ea778ab86dcc4": "b46fa3a2723635aa29cc012538df4867ac2ac006",
78
+ # anko-typed-variable-bindings at DeepSWE 0b9fabbb.
79
+ "ed5a0a7465b87cdeedc4e27e5d68c8daf6e399495da52364868e59cb4fe42ea9": "3f269a72ff69398b1250c584171f32d12c0d8085",
80
+ # yjs-map-conflict-detection at DeepSWE 0b9fabbb.
81
+ "656e5337e72b1e03aa3f870f99d8387caad74613fc8eb157e9d09ce8933761f1": "7795050a749bd1111cbbdd9d0219b27226a8e710",
82
+ # sql-formatter-bigquery-pipe-formatting at DeepSWE 0b9fabbb.
83
+ "2d470eedde92f4ce193c895b3eb851dd794ff188d6321e77c70756d7b471c7dc": "954e5a474b9e3d45ca58f02a3a4eac8e1947acc5",
84
+ }
85
+ PATCH_ARTIFACT = "logs/artifacts/model.patch"
86
+
87
+
88
+ def _digest(value: object) -> str:
89
+ return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
90
+
91
+
92
+ def portable_qualification(proof: object) -> dict[str, object]:
93
+ """Bind source semantics without container IDs, cleanup timestamps or run locations."""
94
+ if not isinstance(proof, dict):
95
+ return {"status": "unknown", "reason": "legacy preparation"}
96
+ cleanup = proof.get("cleanup")
97
+ return {
98
+ **{
99
+ key: value
100
+ for key, value in proof.items()
101
+ if key not in {"base_tree", "export", "cleanup"}
102
+ },
103
+ "cleanup_status": cleanup.get("status") if isinstance(cleanup, dict) else "unavailable",
104
+ }
105
+
106
+
107
+ def grader_startup(*, config: object, image_id: str) -> dict[str, object]:
108
+ """Admit the startup semantics Harbor actually inherits from a prebuilt grader."""
109
+ if not isinstance(config, dict):
110
+ raise ValueError("native grader image configuration unavailable")
111
+ if config.get("Entrypoint") not in (None, []):
112
+ raise ValueError("native grader image inherited entrypoint is unsupported")
113
+ if config.get("WorkingDir") != "/app":
114
+ raise ValueError("native grader image workdir differs from reviewed /app contract")
115
+ variables = config.get("Env")
116
+ if variables is None:
117
+ variables = []
118
+ if not isinstance(variables, list) or not all(isinstance(value, str) for value in variables):
119
+ raise ValueError("native grader image environment unavailable")
120
+ environment: dict[str, str] = {}
121
+ for variable in variables:
122
+ key, separator, value = variable.partition("=")
123
+ if key in _GRADER_ENVIRONMENT:
124
+ if not separator or key in environment or value != _GRADER_ENVIRONMENT[key]:
125
+ raise ValueError("native grader image redirects reviewed environment: " + key)
126
+ environment[key] = value
127
+ return {
128
+ "policy": GRADER_STARTUP_POLICY,
129
+ "image_id": image_id,
130
+ "workdir": "/app",
131
+ "entrypoint": [],
132
+ "environment": environment,
133
+ }
134
+
135
+
136
+ def reviewed_contract(task_dir: Path) -> dict[str, object]:
137
+ """Admit only the reviewed verifier and exact artifact collection semantics."""
138
+ from pluginbench.harbor_backend import native_task_assets
139
+
140
+ native_task_assets(task_dir)
141
+ tests = task_dir / "tests"
142
+ tests_digest = native_task_assets(tests)
143
+ base = _REVIEWED_TESTS.get(tests_digest)
144
+ if base is None:
145
+ raise ValueError("unsupported native patch grading contract: verifier tree unreviewed")
146
+ if hashlib.sha256((tests / "grader.py").read_bytes()).hexdigest() != _GRADER_SHA256:
147
+ raise ValueError("unsupported native patch grading contract: grader changed")
148
+ config = json.loads((tests / "config.json").read_text())
149
+ if config.get("base_commit") != base:
150
+ raise ValueError("reviewed verifier base commit changed")
151
+ task = tomllib.loads((task_dir / "task.toml").read_text())
152
+ verifier = task.get("verifier", {})
153
+ collection = [
154
+ {
155
+ "command": "cd /app && mkdir -p /logs/artifacts && git config --global --add "
156
+ "safe.directory /app && git diff --binary "
157
+ + base
158
+ + " HEAD > /logs/artifacts/model.patch",
159
+ "service": "main",
160
+ "timeout_sec": 300.0,
161
+ }
162
+ ]
163
+ collect = verifier.get("collect")
164
+ if isinstance(collect, list) and all(isinstance(entry, dict) for entry in collect):
165
+ # Harbor runs a collect command in the main service when the task names none.
166
+ collect = [{"service": "main", **entry} for entry in collect]
167
+ if (
168
+ task.get("artifacts") != ["/logs/artifacts/model.patch"]
169
+ or verifier.get("environment_mode") != "separate"
170
+ or verifier.get("network_mode") != "no-network"
171
+ or collect != collection
172
+ or verifier.get("env", {}) != {}
173
+ or verifier.get("environment", {}).get("env", {}) != {}
174
+ ):
175
+ raise ValueError(
176
+ "unsupported native patch grading contract: collection or environment changed"
177
+ )
178
+ payload = {
179
+ name: hashlib.sha256((tests / name).read_bytes()).hexdigest()
180
+ for name in ("test.sh", "test.patch", "grader.py", "config.json")
181
+ }
182
+ return {
183
+ "base_commit": base,
184
+ "tests_sha256": tests_digest,
185
+ "collection": collection,
186
+ "grader_sha256": _GRADER_SHA256,
187
+ "patch_artifact": PATCH_ARTIFACT,
188
+ "verifier_payload": payload,
189
+ }
190
+
191
+
192
+ def base_tree_manifest(output: str) -> dict[str, dict[str, str]]:
193
+ """Parse finite git ls-tree -r -z BASE output without losing blob identities."""
194
+ entries: dict[str, dict[str, str]] = {}
195
+ for record in output.split("\0"):
196
+ if not record:
197
+ continue
198
+ header, separator, path = record.partition("\t")
199
+ match = re.fullmatch(r"([0-7]{6}) (blob|commit) ([0-9a-f]{40})", header)
200
+ if not separator or match is None or path in entries:
201
+ raise ValueError("invalid or duplicate native base tree entry")
202
+ _safe_path(path)
203
+ entries[path] = {"mode": match[1], "type": match[2], "blob": match[3]}
204
+ if not entries:
205
+ raise ValueError("native base tree inventory is empty")
206
+ return entries
207
+
208
+
209
+ def _safe_path(value: str, *, included: bool = False) -> None:
210
+ path = PurePosixPath(value)
211
+ if (
212
+ not value
213
+ or path.is_absolute()
214
+ or path.as_posix() != value
215
+ or ".." in path.parts
216
+ or "\\" in value
217
+ or any(ord(char) < 32 for char in value)
218
+ or (included and any(part in EXCLUDED_NAMES for part in path.parts))
219
+ ):
220
+ raise ValueError("unsafe or excluded native patch path: " + value)
221
+
222
+
223
+ def _patch_paths(patch: str) -> set[str]:
224
+ # The reviewed grader does not reliably parse Git's C-quoted paths.
225
+ if any(line.startswith("diff --git ") and '"' in line for line in patch.splitlines()):
226
+ raise ValueError("unsupported quoted native patch path")
227
+ for line in patch.splitlines():
228
+ if re.match(r"(?:old mode|new mode|new file mode|deleted file mode) ", line):
229
+ if line.rsplit(" ", 1)[-1] not in {"100644", "100755"}:
230
+ raise ValueError("unsupported native patch file mode")
231
+ changes = _file_changes(patch)
232
+ paths = {
233
+ path
234
+ for change in changes
235
+ for path in (change.old_path, change.new_path)
236
+ if path is not None
237
+ }
238
+ sections = iter(changes)
239
+ current = None
240
+ for line in patch.splitlines():
241
+ if line.startswith("diff --git "):
242
+ current = next(sections)
243
+ header = _file_changes(line)[0]
244
+ for effective, declared in (
245
+ (current.old_path, header.old_path),
246
+ (current.new_path, header.new_path),
247
+ ):
248
+ if effective is not None and effective != declared:
249
+ raise ValueError("inconsistent native patch header and marker paths")
250
+ elif current is not None:
251
+ for prefix, expected in (
252
+ ("rename from ", current.old_path),
253
+ ("rename to ", current.new_path),
254
+ ("copy from ", current.old_path),
255
+ ("copy to ", current.new_path),
256
+ ):
257
+ if line.startswith(prefix):
258
+ path = line.removeprefix(prefix)
259
+ _safe_path(path, included=True)
260
+ if path != expected:
261
+ raise ValueError("inconsistent native patch rename or copy paths")
262
+ paths.add(path)
263
+ for path in paths:
264
+ _safe_path(path, included=True)
265
+ return paths
266
+
267
+
268
+ def _proof(*, before: Path, task_dir: Path, identity: dict[str, object]) -> dict[str, Any]:
269
+ from pluginbench.harbor_backend import native_task_assets
270
+
271
+ if native_task_assets(task_dir) != identity.get("task_assets_sha256"):
272
+ raise ValueError("frozen native task assets changed")
273
+ contract = reviewed_contract(task_dir)
274
+ proof = identity.get("source_qualification")
275
+ if not isinstance(proof, dict) or proof.get("status") != "complete":
276
+ raise ValueError("native patch source qualification unavailable or incomplete")
277
+ if (
278
+ identity.get("source_policy") != SOURCE_POLICY
279
+ or proof.get("source_policy") != SOURCE_POLICY
280
+ ):
281
+ raise ValueError("native patch source policy incompatible")
282
+ if proof.get("contract") != contract or proof.get("grader_head") != contract["base_commit"]:
283
+ raise ValueError("native patch source qualification contract changed")
284
+ if proof.get("grader_tests_sha256") != _digest(contract["verifier_payload"]):
285
+ raise ValueError("native grader image verifier payload unqualified")
286
+ startup = proof.get("grader_startup")
287
+ grader_image = identity.get("grader_image_identity")
288
+ if not isinstance(startup, dict) or not isinstance(grader_image, dict):
289
+ raise ValueError("native grader startup qualification unavailable")
290
+ environment = startup.get("environment")
291
+ if not isinstance(environment, dict) or not all(
292
+ isinstance(key, str) and isinstance(value, str) for key, value in environment.items()
293
+ ):
294
+ raise ValueError("native grader startup environment qualification unavailable")
295
+ admitted_startup = grader_startup(
296
+ config={
297
+ "Entrypoint": startup.get("entrypoint"),
298
+ "WorkingDir": startup.get("workdir"),
299
+ "Env": [key + "=" + value for key, value in environment.items()],
300
+ },
301
+ image_id=str(grader_image.get("image_id")),
302
+ )
303
+ if startup != admitted_startup:
304
+ raise ValueError("native grader startup qualification changed")
305
+ for key, proof_key in (
306
+ ("image_identity", "coding_image_id"),
307
+ ("grader_image_identity", "grader_image_id"),
308
+ ):
309
+ image = identity.get(key)
310
+ if not isinstance(image, dict) or image.get("image_id") != proof.get(proof_key):
311
+ raise ValueError("native patch source qualification image changed")
312
+ if (
313
+ inspect_snapshot(before)["digest"] != proof.get("fixture_digest")
314
+ or native_task_assets(before) != proof.get("fixture_metadata_digest")
315
+ or identity.get("source_digest") != proof.get("fixture_digest")
316
+ ):
317
+ raise ValueError("frozen native patch fixture changed")
318
+ tree = proof.get("base_tree")
319
+ if not isinstance(tree, dict) or not tree or _digest(tree) != proof.get("base_tree_sha256"):
320
+ raise ValueError("native patch base inventory changed or unavailable")
321
+ cleanup = proof.get("cleanup")
322
+ if not isinstance(cleanup, dict) or cleanup.get("status") != "complete":
323
+ raise ValueError("native patch qualification cleanup incomplete")
324
+ return proof
325
+
326
+
327
+ def reconstruct(
328
+ *,
329
+ before: Path,
330
+ task_dir: Path,
331
+ identity: dict[str, object],
332
+ artifacts: Path,
333
+ execution: dict[str, Any],
334
+ destination: Path,
335
+ ) -> dict[str, object]:
336
+ """Reconstruct the admitted hash-bound artifact; never substitute a stopped export."""
337
+ from pluginbench.harbor_backend import native_task_assets
338
+
339
+ if destination.exists() or destination.is_symlink():
340
+ raise ValueError("canonical native destination already exists")
341
+ if any(parent.is_symlink() for parent in destination.parents):
342
+ raise ValueError("canonical native destination is linked")
343
+ proof = _proof(before=before, task_dir=task_dir, identity=identity)
344
+ hashes = execution.get("native_artifact_hashes")
345
+ if not isinstance(hashes, dict) or not isinstance(hashes.get(PATCH_ARTIFACT), str):
346
+ raise ValueError("admitted native graded patch hash unavailable")
347
+ patch = artifacts / "harbor/trial/artifacts" / PATCH_ARTIFACT
348
+ if (
349
+ not patch.is_file()
350
+ or patch.is_symlink()
351
+ or any(parent.is_symlink() for parent in patch.parents)
352
+ ):
353
+ raise ValueError("native graded patch unavailable or linked")
354
+ patch_bytes = patch.read_bytes()
355
+ digest = hashlib.sha256(patch_bytes).hexdigest()
356
+ if digest != hashes[PATCH_ARTIFACT]:
357
+ raise ValueError("native graded patch changed")
358
+ text = patch_bytes.decode("utf-8", errors="strict")
359
+ paths = _patch_paths(text)
360
+ tree = proof["base_tree"]
361
+ for path in paths:
362
+ preimage = before / path
363
+ entry = tree.get(path)
364
+ if entry is None:
365
+ if preimage.exists():
366
+ raise ValueError("native patch creation collides with frozen fixture: " + path)
367
+ continue
368
+ if (
369
+ not isinstance(entry, dict)
370
+ or entry.get("type") != "blob"
371
+ or entry.get("mode") not in {"100644", "100755"}
372
+ or not preimage.is_file()
373
+ or preimage.is_symlink()
374
+ ):
375
+ raise ValueError("native patch preimage unavailable or unsupported: " + path)
376
+ content = preimage.read_bytes()
377
+ blob = hashlib.sha1(b"blob " + str(len(content)).encode() + b"\0" + content).hexdigest()
378
+ mode = "100755" if preimage.stat().st_mode & 0o111 else "100644"
379
+ if blob != entry.get("blob") or mode != entry["mode"]:
380
+ raise ValueError("native patch preimage differs from verifier base: " + path)
381
+ hidden = _patch_paths((task_dir / "tests/test.patch").read_text())
382
+ with tempfile.TemporaryDirectory(prefix="pluginbench-canonical-patch-") as temporary:
383
+ source = Path(temporary) / "source"
384
+ shutil.copytree(before, source)
385
+ try:
386
+ applied = subprocess.run(
387
+ [
388
+ "git",
389
+ "apply",
390
+ "--binary",
391
+ "--allow-empty",
392
+ "--whitespace=nowarn",
393
+ str(patch.resolve()),
394
+ ],
395
+ cwd=source,
396
+ capture_output=True,
397
+ text=True,
398
+ check=False,
399
+ timeout=60,
400
+ env={
401
+ "PATH": os.environ["PATH"],
402
+ "GIT_CONFIG_GLOBAL": os.devnull,
403
+ "GIT_CONFIG_NOSYSTEM": "1",
404
+ "GIT_CEILING_DIRECTORIES": temporary,
405
+ },
406
+ )
407
+ except subprocess.TimeoutExpired as error:
408
+ raise ValueError("graded patch reconstruction timed out after 60 seconds") from error
409
+ if applied.returncode:
410
+ raise ValueError("graded patch could not be safely reconstructed: " + applied.stderr)
411
+ source_digest = inspect_snapshot(source)["digest"]
412
+ metadata_digest = native_task_assets(source)
413
+ # Copy only after all validation, and never leave a partially admitted destination.
414
+ try:
415
+ shutil.copytree(source, destination)
416
+ except BaseException:
417
+ if destination.is_dir():
418
+ shutil.rmtree(destination)
419
+ raise
420
+ return {
421
+ "status": "complete",
422
+ "source_policy": SOURCE_POLICY,
423
+ "binding": "reviewed model.patch over qualified frozen fixture",
424
+ "snapshot_digest": source_digest,
425
+ "source_metadata_digest": metadata_digest,
426
+ "patch_artifact": PATCH_ARTIFACT,
427
+ "patch_sha256": digest,
428
+ "preparation_proof_sha256": _digest(portable_qualification(proof)),
429
+ "trusted_test_overlap": sorted(paths & hidden),
430
+ }
431
+
432
+
433
+ def validate(
434
+ *,
435
+ before: Path,
436
+ task_dir: Path,
437
+ identity: dict[str, object],
438
+ artifacts: Path,
439
+ execution: dict[str, Any],
440
+ snapshot: Path,
441
+ ) -> dict[str, object]:
442
+ """Independently reconstruct and compare the complete canonical snapshot metadata."""
443
+ from pluginbench.harbor_backend import native_task_assets
444
+
445
+ with tempfile.TemporaryDirectory(prefix="pluginbench-validate-patch-") as temporary:
446
+ receipt = reconstruct(
447
+ before=before,
448
+ task_dir=task_dir,
449
+ identity=identity,
450
+ artifacts=artifacts,
451
+ execution=execution,
452
+ destination=Path(temporary).resolve() / "source",
453
+ )
454
+ if (
455
+ inspect_snapshot(snapshot)["digest"] != receipt["snapshot_digest"]
456
+ or native_task_assets(snapshot) != receipt["source_metadata_digest"]
457
+ ):
458
+ raise ValueError("canonical graded source changed")
459
+ return receipt
460
+
461
+
462
+ def validate_export(
463
+ *, diagnostic: Path, artifacts: Path, execution: dict[str, Any]
464
+ ) -> dict[str, object]:
465
+ """Validate stopped-source diagnostics separately from the canonical submission."""
466
+ from pluginbench.harbor_backend import native_task_assets, _native_artifact_hashes
467
+
468
+ receipt_path = artifacts / "submission-source.json"
469
+ for path in (diagnostic, receipt_path, artifacts / "harbor/trial/artifacts"):
470
+ if path.is_symlink() or any(parent.is_symlink() for parent in path.parents):
471
+ raise ValueError("native stopped export evidence is linked")
472
+ receipt = json.loads(receipt_path.read_text())
473
+ if not isinstance(receipt, dict):
474
+ raise ValueError("native stopped export receipt must be an object")
475
+ digest = inspect_snapshot(diagnostic)["digest"]
476
+ if digest != receipt.get("snapshot_digest") or digest != execution.get(
477
+ "submission_export_digest"
478
+ ):
479
+ raise ValueError("native stopped export changed")
480
+ if native_task_assets(diagnostic) != receipt.get("source_metadata_digest"):
481
+ raise ValueError("native stopped export metadata changed")
482
+ artifact_dir = artifacts / "harbor/trial/artifacts"
483
+ native_task_assets(artifact_dir)
484
+ if _native_artifact_hashes(artifact_dir) != execution.get("native_artifact_hashes"):
485
+ raise ValueError("native graded artifacts changed")
486
+ return receipt