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
pluginbench/ledger.py ADDED
@@ -0,0 +1,217 @@
1
+ """Single-writer SQLite ledger of coding attempt and stage states.
2
+
3
+ Evidence files stay the record of what happened; the ledger only says how far each attempt got.
4
+ One coordinator thread writes it. Every write is its own transaction, and a state is marked
5
+ complete only after the receipt it points to is on disk. The default rollback journal leaves
6
+ no sidecar files once a transaction ends.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sqlite3
13
+ from collections.abc import Iterable
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ SCHEMA_VERSION = 1
19
+ LEDGER_NAME = "ledger.sqlite"
20
+
21
+ # pending -> running -> complete | interrupted. A resumed run moves a started but unfinished
22
+ # attempt to discarded-interrupted and gives its slot to a new pending attempt.
23
+ ATTEMPT_STATES = ("pending", "running", "complete", "interrupted", "discarded-interrupted")
24
+
25
+ _SCHEMA = f"""
26
+ CREATE TABLE attempts (
27
+ id TEXT PRIMARY KEY,
28
+ slot INTEGER NOT NULL,
29
+ task_id TEXT NOT NULL,
30
+ arm TEXT NOT NULL,
31
+ repeat INTEGER NOT NULL,
32
+ state TEXT NOT NULL CHECK (state IN {ATTEMPT_STATES}),
33
+ record TEXT,
34
+ replaced_by TEXT REFERENCES attempts (id),
35
+ updated_at TEXT NOT NULL
36
+ );
37
+ CREATE UNIQUE INDEX one_live_attempt_per_slot ON attempts (slot)
38
+ WHERE state != 'discarded-interrupted';
39
+ CREATE TABLE stages (
40
+ attempt_id TEXT NOT NULL REFERENCES attempts (id),
41
+ stage TEXT NOT NULL,
42
+ state TEXT NOT NULL CHECK (state IN ('started', 'complete')),
43
+ receipt TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL,
45
+ PRIMARY KEY (attempt_id, stage)
46
+ );
47
+ """
48
+
49
+
50
+ def _now() -> str:
51
+ return datetime.now(UTC).isoformat()
52
+
53
+
54
+ class Ledger:
55
+ def __init__(self, connection: sqlite3.Connection) -> None:
56
+ self._db = connection
57
+
58
+ @classmethod
59
+ def create(
60
+ cls, path: Path, entries: Iterable[dict[str, Any]], states: dict[str, str]
61
+ ) -> Ledger:
62
+ """A new ledger with one row per plan entry, in plan (slot) order.
63
+
64
+ It is built in a temporary file and renamed into place, so a killed build never leaves
65
+ a ledger that looks valid but lacks attempts.
66
+ """
67
+ if path.exists():
68
+ raise ValueError(f"ledger already exists: {path}")
69
+ temporary = path.with_name(f".{path.name}.tmp")
70
+ # Left by a build that was killed; a stale journal must not roll into the new file.
71
+ temporary.unlink(missing_ok=True)
72
+ temporary.with_name(temporary.name + "-journal").unlink(missing_ok=True)
73
+ connection = sqlite3.connect(temporary)
74
+ with connection:
75
+ connection.executescript(_SCHEMA)
76
+ connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
77
+ connection.executemany(
78
+ "INSERT INTO attempts (id, slot, task_id, arm, repeat, state, updated_at)"
79
+ " VALUES (?, ?, ?, ?, ?, ?, ?)",
80
+ [
81
+ (
82
+ entry["id"],
83
+ slot,
84
+ entry["task_id"],
85
+ entry["arm"],
86
+ entry["attempt"],
87
+ states[entry["id"]],
88
+ _now(),
89
+ )
90
+ for slot, entry in enumerate(entries)
91
+ ],
92
+ )
93
+ connection.close()
94
+ temporary.replace(path)
95
+ return cls.open(path)
96
+
97
+ @classmethod
98
+ def open(cls, path: Path) -> Ledger:
99
+ if not path.is_file():
100
+ raise ValueError(f"ledger is missing: {path}")
101
+ connection = sqlite3.connect(path)
102
+ version = connection.execute("PRAGMA user_version").fetchone()[0]
103
+ if version != SCHEMA_VERSION:
104
+ connection.close()
105
+ raise ValueError(f"unsupported ledger schema version {version}")
106
+ return cls(connection)
107
+
108
+ @classmethod
109
+ def read(cls, path: Path) -> Ledger:
110
+ """Open read-only, for observers such as `status`; any write raises sqlite3.Error.
111
+
112
+ PluginBench ledgers use the rollback journal. A WAL database is refused, because even a
113
+ read-only connection to one creates -wal and -shm files.
114
+ """
115
+ with path.open("rb") as stream:
116
+ header = stream.read(20)
117
+ # Bytes 18 and 19 are the write and read format versions: 2 means WAL.
118
+ if len(header) == 20 and header[18:20] != b"\x01\x01":
119
+ raise ValueError(f"ledger is not in rollback-journal mode: {path}")
120
+ connection = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=2)
121
+ try:
122
+ version = connection.execute("PRAGMA user_version").fetchone()[0]
123
+ except sqlite3.Error:
124
+ connection.close()
125
+ raise
126
+ if version != SCHEMA_VERSION:
127
+ connection.close()
128
+ raise ValueError(f"unsupported ledger schema version {version}")
129
+ return cls(connection)
130
+
131
+ def close(self) -> None:
132
+ self._db.close()
133
+
134
+ def set_state(self, attempt_id: str, state: str) -> None:
135
+ with self._db:
136
+ changed = self._db.execute(
137
+ "UPDATE attempts SET state = ?, updated_at = ? WHERE id = ?"
138
+ " AND state NOT IN ('complete', 'discarded-interrupted')",
139
+ (state, _now(), attempt_id),
140
+ ).rowcount
141
+ if changed != 1:
142
+ raise ValueError(f"ledger cannot move attempt {attempt_id} to {state}")
143
+
144
+ def stage_started(self, attempt_id: str, stage: str, receipt: dict[str, str]) -> None:
145
+ with self._db:
146
+ self._db.execute(
147
+ "INSERT INTO stages (attempt_id, stage, state, receipt, updated_at)"
148
+ " VALUES (?, ?, 'started', ?, ?)",
149
+ (attempt_id, stage, json.dumps(receipt, sort_keys=True), _now()),
150
+ )
151
+
152
+ def complete(self, attempt_id: str, record: dict[str, str], stages: Iterable[str]) -> None:
153
+ """Mark complete; `record` references the attempt's record receipt, already on disk."""
154
+ encoded = json.dumps(record, sort_keys=True)
155
+ with self._db:
156
+ changed = self._db.execute(
157
+ "UPDATE attempts SET state = 'complete', record = ?, updated_at = ?"
158
+ " WHERE id = ? AND state IN ('pending', 'running', 'interrupted')",
159
+ (encoded, _now(), attempt_id),
160
+ ).rowcount
161
+ if changed != 1:
162
+ raise ValueError(f"ledger cannot complete attempt {attempt_id}")
163
+ for stage in stages:
164
+ self._db.execute(
165
+ "INSERT INTO stages (attempt_id, stage, state, receipt, updated_at)"
166
+ " VALUES (?, ?, 'complete', ?, ?) ON CONFLICT (attempt_id, stage)"
167
+ " DO UPDATE SET state = 'complete', receipt = excluded.receipt,"
168
+ " updated_at = excluded.updated_at",
169
+ (attempt_id, stage, encoded, _now()),
170
+ )
171
+
172
+ def replace(self, attempt_id: str, replacement: dict[str, Any]) -> None:
173
+ """Discard an interrupted attempt and give its slot to a new pending attempt."""
174
+ with self._db:
175
+ row = self._db.execute(
176
+ "SELECT slot FROM attempts WHERE id = ? AND state IN ('running', 'interrupted')",
177
+ (attempt_id,),
178
+ ).fetchone()
179
+ if row is None:
180
+ raise ValueError(f"ledger cannot discard attempt {attempt_id}")
181
+ self._db.execute(
182
+ "UPDATE attempts SET state = 'discarded-interrupted', replaced_by = ?,"
183
+ " updated_at = ? WHERE id = ?",
184
+ (replacement["id"], _now(), attempt_id),
185
+ )
186
+ self._db.execute(
187
+ "INSERT INTO attempts (id, slot, task_id, arm, repeat, state, updated_at)"
188
+ " VALUES (?, ?, ?, ?, ?, 'pending', ?)",
189
+ (
190
+ replacement["id"],
191
+ row[0],
192
+ replacement["task_id"],
193
+ replacement["arm"],
194
+ replacement["attempt"],
195
+ _now(),
196
+ ),
197
+ )
198
+
199
+ def attempts(self) -> list[dict[str, Any]]:
200
+ """Every attempt, discarded ones included, in slot then insertion order."""
201
+ rows = self._db.execute(
202
+ "SELECT id, slot, task_id, arm, repeat, state, record, replaced_by"
203
+ " FROM attempts ORDER BY slot, rowid"
204
+ ).fetchall()
205
+ return [
206
+ {
207
+ "id": row[0],
208
+ "slot": row[1],
209
+ "task_id": row[2],
210
+ "arm": row[3],
211
+ "attempt": row[4],
212
+ "state": row[5],
213
+ "record": json.loads(row[6]) if row[6] else None,
214
+ "replaced_by": row[7],
215
+ }
216
+ for row in rows
217
+ ]