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
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
|
+
]
|