ossa 0.1.0a1__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.
ossa/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """OSSA toolbox: the deterministic half of a safety-first open-source contribution crew."""
2
+ __version__ = "0.1.0-alpha.1"
ossa/arena.py ADDED
@@ -0,0 +1,591 @@
1
+ """Breaker Arena: score Breaker on real merged fixes and their plausible-but-wrong twins. No LLM here.
2
+
3
+ An instance is a merged PR that closes an issue opened after the models' training cutoff and adds
4
+ tests (replay.mine_instances). The human fix is the reference, and its tests stay hidden.
5
+
6
+ mine instances from a repo's merged PRs (hidden: runs/arena/hidden/<instance>/)
7
+ env a frozen copy of the repo at the instance's base (history ends there, so the later fix
8
+ can't be found) and an env attempt; Sherlock builds the sandbox on it with `ossa env`
9
+ prepare checks the hidden tests fail on base and pass with the human fix (F2P, P2P), then makes
10
+ wrong candidates: mutants of the human fix (flipped comparisons, booleans, constants,
11
+ arithmetic) and partial fixes (a hunk, a file or a line left out). A candidate counts
12
+ only if it is plausible (every P2P test still passes) and wrong (an F2P test fails).
13
+ next one candidate at a time (the human fix or a wrong one, in a shuffled order) becomes a
14
+ blind duel attempt: base + candidate as the "verified fix", the issue text, no tests.
15
+ Breaker attacks it with the ordinary `ossa attack`, as in a real duel.
16
+ score re-runs every attack against the hidden human fix (the verifiable reward):
17
+ caught fails on a wrong candidate, passes on the human fix +1
18
+ false_alarm fails on the human fix -1
19
+ missed holds on a wrong candidate 0
20
+ held holds on the human fix (as it should) 0
21
+ invalid never exercised the code, or flaky 0
22
+ solve Patch's side: an attempt at base from the issue alone; `grade` runs the hidden tests on
23
+ its verified fix (resolved = every F2P passes and no P2P breaks)
24
+ report catch rate (wrong candidates with at least one caught attack), false-alarm rate (human
25
+ fixes with at least one false alarm), by Breaker prompt version and model.
26
+ The crew never reads runs/arena/hidden/ (the guard blocks it); everything there is for scoring.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import json
32
+ import random
33
+ import re
34
+ import shutil
35
+ from pathlib import Path, PurePosixPath
36
+ from typing import Any
37
+
38
+ from ossa.schemas import Attempt, IssueRef, State
39
+ from ossa.store import now
40
+ from ossa.toolbox import ToolError, _attempt, _pristine
41
+
42
+ WRONG_PER_INSTANCE = 3
43
+ MAX_TRIES = 12
44
+ HUNK = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$")
45
+ OPERATORS: list[tuple[str, str, Any]] = [ # (name, pattern, replacement): one change per mutant
46
+ ("== to !=", r"==", "!="), ("!= to ==", r"!=", "=="),
47
+ ("<= to <", r"<=", "<"), (">= to >", r">=", ">"),
48
+ ("< to <=", r"(?<![<\-=])<(?![<=\-])", "<="), ("> to >=", r"(?<![>\-=])>(?![>=])", ">="),
49
+ ("and to or", r"\band\b", "or"), ("or to and", r"\bor\b", "and"),
50
+ ("&& to ||", r"&&", "||"), ("|| to &&", r"\|\|", "&&"),
51
+ ("drop not", r"\bnot\s+", ""), ("drop !", r"!(?=[A-Za-z_(])", ""),
52
+ ("+ to -", r"(?<=\s)\+(?=\s)", "-"), ("- to +", r"(?<=\s)-(?=\s)", "+"),
53
+ ("True to False", r"\bTrue\b", "False"), ("False to True", r"\bFalse\b", "True"),
54
+ ("true to false", r"\btrue\b", "false"), ("false to true", r"\bfalse\b", "true"),
55
+ ("is None to is not None", r"\bis None\b", "is not None"), ("is not None to is None", r"\bis not None\b", "is None"),
56
+ ("n to n+1", r"(?<![\w.])(\d{1,4})(?![\w.])", lambda m: str(int(m.group(1)) + 1)),
57
+ ]
58
+ COMMENT = re.compile(r"^\s*(#|//|/\*|\*)")
59
+ OPENER = re.compile(r"(:|\{|\(|\[|,)\s*$|^\s*(\}|\)|\]|else\b|elif\b|except\b|finally\b|try\b|def |class |@|return\s*$)")
60
+
61
+
62
+ def _hash(text: str) -> str:
63
+ return hashlib.sha1(text.encode()).hexdigest()[:12]
64
+
65
+
66
+ def _arena(ctx) -> Path:
67
+ return ctx.config.path("runs") / "arena"
68
+
69
+
70
+ def hidden_dir(ctx, iid: str) -> Path:
71
+ return _arena(ctx) / "hidden" / iid
72
+
73
+
74
+ def _row(ctx, iid: str) -> dict:
75
+ rows = ctx.store.query("SELECT * FROM arena_instances WHERE id=?", (iid,))
76
+ if not rows:
77
+ raise ToolError(f"no Arena instance {iid} (`ossa arena status` lists them)")
78
+ d = dict(rows[0])
79
+ d["f2p"] = json.loads(d.pop("f2p_json") or "[]")
80
+ d["p2p"] = json.loads(d.pop("p2p_json") or "[]")
81
+ return d
82
+
83
+
84
+ def _update(ctx, iid: str, **fields: Any) -> None:
85
+ fields["updated_at"] = now()
86
+ sets = ", ".join(f"{k}=?" for k in fields)
87
+ ctx.store.execute(f"UPDATE arena_instances SET {sets} WHERE id=?", (*fields.values(), iid))
88
+
89
+
90
+ def _instance(ctx, iid: str) -> dict:
91
+ return json.loads((hidden_dir(ctx, iid) / "instance.json").read_text())
92
+
93
+
94
+ def _blind_issue(inst: dict) -> IssueRef:
95
+ """The issue as it was reported: no thread, no URL (nothing that points at the fix)."""
96
+ i = IssueRef.model_validate(inst["issue"])
97
+ return i.model_copy(update={"comments": [], "url": ""})
98
+
99
+
100
+ def _move(a: Attempt, dest: Path) -> None:
101
+ """Put an attempt's artifacts somewhere else (blind, hidden, solve folders) and remember it."""
102
+ old = Path(a.artifacts_dir)
103
+ dest.parent.mkdir(parents=True, exist_ok=True)
104
+ if old.exists() and old != dest:
105
+ shutil.move(str(old), str(dest))
106
+ dest.mkdir(parents=True, exist_ok=True)
107
+ a.artifacts_dir = str(dest)
108
+ a.ctx.store.save_attempt(a)
109
+
110
+
111
+ # ------------------------------------------------------------------ mining
112
+ def mine(ctx, repo: str, n: int = 5, allow_contaminated: bool = False) -> list[dict]:
113
+ """New instances from `repo`'s merged PRs (newest first). Already-mined ones are listed again."""
114
+ from ossa import replay
115
+ out = []
116
+ for inst in replay.mine_instances(ctx, repo, n, allow_contaminated=allow_contaminated):
117
+ iid = f"{repo.replace('/', '__')}-pr{inst['pr']}"
118
+ summary = {"instance": iid, "pr": inst["pr"], "issue": inst["issue"]["number"],
119
+ "contaminated": inst["contaminated"]}
120
+ if ctx.store.query("SELECT id FROM arena_instances WHERE id=?", (iid,)):
121
+ out.append({**summary, "new": False})
122
+ continue
123
+ h = hidden_dir(ctx, iid)
124
+ h.mkdir(parents=True, exist_ok=True)
125
+ (h / "instance.json").write_text(json.dumps(inst, indent=1))
126
+ ctx.store.execute(
127
+ "INSERT INTO arena_instances(id, repo, pr, issue, base_sha, issue_created_at, contaminated, state, "
128
+ "created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)",
129
+ (iid, repo, inst["pr"], inst["issue"]["number"], inst["base_sha"], inst.get("issue_created_at"),
130
+ int(bool(inst["contaminated"])), "mined", now(), now()))
131
+ out.append({**summary, "new": True})
132
+ return out
133
+
134
+
135
+ # ------------------------------------------------------------------ the frozen repo and the env
136
+ def freeze(ctx, repo: str, base_sha: str) -> Path:
137
+ """A repo whose only history ends at `base_sha`. It borrows the upstream clone's objects
138
+ (git alternates), so it costs no disk, but no ref in it leads past the base."""
139
+ from ossa import gitops
140
+ from ossa.stages import s03_workspace as W
141
+ dest = W.frozen_dir(ctx, repo, base_sha)
142
+ if (dest / ".git").exists():
143
+ return dest
144
+ upstream = W.clone_dir(ctx, repo)
145
+ if not ((upstream / ".git").exists() and W._has_commit(upstream, base_sha)):
146
+ gitops.ensure_clone(W.clone_url(repo), upstream)
147
+ if not W._has_commit(upstream, base_sha):
148
+ raise ToolError(f"{base_sha[:12]} isn't in {repo}'s history")
149
+ dest.mkdir(parents=True, exist_ok=True)
150
+ gitops.git(dest, "init", "-q")
151
+ (dest / ".git" / "objects" / "info" / "alternates").write_text(f"{(upstream / '.git' / 'objects').resolve()}\n")
152
+ gitops.git(dest, "update-ref", "refs/heads/base", base_sha)
153
+ gitops.git(dest, "checkout", "-q", "--detach", base_sha)
154
+ return dest
155
+
156
+
157
+ def _copy_env(src: Attempt, dst: Attempt) -> None:
158
+ for name in ("env.json", "env/image.txt", "env/Dockerfile", "baseline.json"):
159
+ text = src.ctx.store.read_artifact(src, name)
160
+ if text is not None:
161
+ dst.ctx.store.write_artifact(dst, name, text)
162
+
163
+
164
+ def _new(ctx, row: dict, kind: str, folder: Path, branch: str) -> Attempt:
165
+ a = ctx.store.new_attempt(row["repo"], f"{row['repo']}#{row['issue']}", kind=kind)
166
+ a.ctx, a.base_sha, a.branch = ctx, row["base_sha"], f"{branch}/{a.id[-10:].lower()}"
167
+ _move(a, folder / a.id)
168
+ ctx.store.write_artifact(a, "issue.json", _blind_issue(_instance(ctx, row["id"])).model_dump(mode="json"))
169
+ return a
170
+
171
+
172
+ def env_attempt(ctx, iid: str) -> Attempt:
173
+ """The attempt Sherlock builds the instance's sandbox on (`ossa env <attempt> ...`)."""
174
+ from ossa.stages import s03_workspace
175
+ row = _row(ctx, iid)
176
+ if row["env_attempt"]:
177
+ return _attempt(ctx, row["env_attempt"])
178
+ freeze(ctx, row["repo"], row["base_sha"])
179
+ a = _new(ctx, row, "arena-env", _arena(ctx) / "env", "arena-env")
180
+ s03_workspace.prepare_worktree(a, _blind_issue(_instance(ctx, iid)))
181
+ a.state = State.ISSUE_SELECTED
182
+ ctx.store.save_attempt(a)
183
+ _update(ctx, iid, env_attempt=a.id)
184
+ return a
185
+
186
+
187
+ def _env(ctx, row: dict) -> Attempt:
188
+ from ossa.stages import s03_workspace
189
+ if not row["env_attempt"]:
190
+ raise ToolError(f"no sandbox yet: `ossa arena env {row['id']}`, then `ossa env <attempt> ...`")
191
+ a = _attempt(ctx, row["env_attempt"])
192
+ try:
193
+ s03_workspace.load_env(a)
194
+ except Exception: # noqa: BLE001 - Blocked: env.json missing
195
+ raise ToolError(f"build the sandbox first: `ossa env {a.id} --dockerfile ... --test ...`") from None
196
+ return a
197
+
198
+
199
+ def _suite(env: Attempt, patches: list[str]):
200
+ """The full suite on base + patches in a clean worktree; None when a patch doesn't apply."""
201
+ from ossa import gitops
202
+ from ossa.stages import s03_workspace
203
+ spec, image = s03_workspace.load_env(env)
204
+ wt = _pristine(env, with_repro=False)
205
+ for p in patches:
206
+ if p.strip():
207
+ ok, _ = gitops.apply_patch(wt, p)
208
+ if not ok:
209
+ return None
210
+ try:
211
+ return env.ctx.sandbox.run_tests(wt, image, spec)
212
+ finally:
213
+ gitops.git(wt, "clean", "-fdq", check=False)
214
+
215
+
216
+ # ------------------------------------------------------------------ wrong candidates
217
+ def recount(patch: str) -> str:
218
+ """Fix every hunk header's line counts after lines were added or dropped."""
219
+ lines, out, i = patch.splitlines(keepends=True), [], 0
220
+ while i < len(lines):
221
+ m = HUNK.match(lines[i].rstrip("\n"))
222
+ if not m:
223
+ out.append(lines[i])
224
+ i += 1
225
+ continue
226
+ j, old, new = i + 1, 0, 0
227
+ while j < len(lines) and not lines[j].startswith(("@@", "diff --git")):
228
+ line = lines[j]
229
+ if line.startswith("\\"):
230
+ pass
231
+ elif line.startswith("+"):
232
+ new += 1
233
+ elif line.startswith("-"):
234
+ old += 1
235
+ else:
236
+ old += 1
237
+ new += 1
238
+ j += 1
239
+ out.append(f"@@ -{m.group(1)},{old} +{m.group(2)},{new} @@{m.group(3)}\n")
240
+ out.extend(lines[i + 1:j])
241
+ i = j
242
+ return "".join(out)
243
+
244
+
245
+ def _sections(patch: str) -> list[str]:
246
+ """Per-file sections of a git diff."""
247
+ parts = re.split(r"(?m)^(?=diff --git )", patch)
248
+ return [p for p in parts if p.strip()]
249
+
250
+
251
+ def _hunks(section: str) -> tuple[str, list[str]]:
252
+ """(file header, hunks) of one file section."""
253
+ parts = re.split(r"(?m)^(?=@@ )", section)
254
+ return parts[0], parts[1:]
255
+
256
+
257
+ def _in_string(line: str, pos: int) -> bool:
258
+ before = line[:pos]
259
+ return (before.count('"') - before.count('\\"')) % 2 == 1 or (before.count("'") - before.count("\\'")) % 2 == 1
260
+
261
+
262
+ def mutants(patch: str) -> list[tuple[str, str]]:
263
+ """(label, patch) for every single-change mutant and partial fix of `patch`, deduplicated."""
264
+ out: dict[str, str] = {}
265
+ sections = _sections(patch)
266
+ if len(sections) > 1:
267
+ for i, sec in enumerate(sections):
268
+ path = re.search(r"^diff --git a/(\S+)", sec, re.M)
269
+ out.setdefault("".join(sections[:i] + sections[i + 1:]),
270
+ f"partial: leaves out {path.group(1) if path else f'file {i + 1}'}")
271
+ for si, sec in enumerate(sections):
272
+ head, hunks = _hunks(sec)
273
+ if len(hunks) > 1:
274
+ for hi in range(len(hunks)):
275
+ rest = head + "".join(hunks[:hi] + hunks[hi + 1:])
276
+ out.setdefault("".join(sections[:si] + [rest] + sections[si + 1:]), f"partial: leaves out hunk {hi + 1}")
277
+ lines = patch.splitlines(keepends=True)
278
+ in_hunk = False
279
+ for idx, line in enumerate(lines):
280
+ if line.startswith("@@"):
281
+ in_hunk = True
282
+ continue
283
+ if line.startswith("diff --git"):
284
+ in_hunk = False
285
+ if not in_hunk or not line.startswith("+") or line.startswith("+++"):
286
+ continue
287
+ body = line[1:]
288
+ if not body.strip() or COMMENT.match(body):
289
+ continue
290
+ for name, pattern, repl in OPERATORS:
291
+ m = next((m for m in re.finditer(pattern, body) if not _in_string(body, m.start())), None)
292
+ if m is None:
293
+ continue
294
+ new = body[:m.start()] + (repl(m) if callable(repl) else repl) + body[m.end():]
295
+ text = "".join(lines[:idx] + ["+" + new] + lines[idx + 1:])
296
+ out.setdefault(text, f"mutant: {name} on line {idx + 1}")
297
+ if not OPENER.search(body.rstrip("\n")):
298
+ text = recount("".join(lines[:idx] + lines[idx + 1:]))
299
+ out.setdefault(text, f"partial: drops line {idx + 1}")
300
+ out.pop(patch, None)
301
+ return [(label, text) for text, label in out.items() if not is_noop(text)]
302
+
303
+
304
+ def is_noop(patch: str) -> bool:
305
+ """True when every hunk adds back exactly the lines it removes (a "fix" that changes nothing)."""
306
+ for sec in _sections(patch):
307
+ for hunk in _hunks(sec)[1]:
308
+ rows = hunk.splitlines()[1:]
309
+ if sorted(r[1:] for r in rows if r.startswith("-")) != sorted(r[1:] for r in rows if r.startswith("+")):
310
+ return False
311
+ return True
312
+
313
+
314
+ def plausible_and_wrong(tests: dict[str, str], f2p: list[str], p2p: list[str]) -> bool:
315
+ return all(tests.get(t) == "passed" for t in p2p) and any(tests.get(t) != "passed" for t in f2p)
316
+
317
+
318
+ def prepare(ctx, iid: str, wrong: int = WRONG_PER_INSTANCE, tries: int = MAX_TRIES) -> dict:
319
+ """Validate the instance in the sandbox and make its candidates (shuffled; labels stay hidden)."""
320
+ from ossa import replay
321
+ row = _row(ctx, iid)
322
+ env, inst = _env(ctx, row), _instance(ctx, iid)
323
+ human, tests = inst["human_patch"], inst["human_tests_patch"]
324
+ on_base, with_fix = _suite(env, [tests]), _suite(env, [human, tests])
325
+ if on_base is None or with_fix is None:
326
+ _update(ctx, iid, state="invalid", note="the human patch or tests don't apply to base")
327
+ return {"instance": iid, "valid": False, "why": "the human patch or tests don't apply to base"}
328
+ f2p, p2p = replay.f2p_p2p(on_base.tests, with_fix.tests)
329
+ if not f2p:
330
+ _update(ctx, iid, state="invalid", note="no hidden test fails on base and passes with the fix")
331
+ return {"instance": iid, "valid": False, "why": "no hidden test fails on base and passes with the fix"}
332
+ rng = random.Random(iid)
333
+ pool = mutants(human)
334
+ rng.shuffle(pool)
335
+ kept, tried = [], 0
336
+ for label, patch in pool[:tries]:
337
+ if len(kept) >= wrong:
338
+ break
339
+ tried += 1
340
+ result = _suite(env, [patch, tests])
341
+ if result is not None and plausible_and_wrong(result.tests, f2p, p2p):
342
+ kept.append((label, patch, "partial" if label.startswith("partial") else "mutant"))
343
+ cands = [("human fix", human, "human")] + kept
344
+ rng.shuffle(cands)
345
+ ctx.store.execute("DELETE FROM arena_candidates WHERE instance_id=? AND state='waiting'", (iid,))
346
+ cdir = hidden_dir(ctx, iid) / "candidates"
347
+ cdir.mkdir(parents=True, exist_ok=True)
348
+ for k, (label, patch, kind) in enumerate(cands, 1):
349
+ (cdir / f"{k}.patch").write_text(patch)
350
+ ctx.store.execute("INSERT OR REPLACE INTO arena_candidates(instance_id, k, kind, label, patch_hash, state) "
351
+ "VALUES(?,?,?,?,?,?)", (iid, k, kind, label, _hash(patch), "waiting"))
352
+ _update(ctx, iid, state="ready", f2p_json=json.dumps(f2p), p2p_json=json.dumps(p2p), note="")
353
+ return {"instance": iid, "valid": True, "f2p": len(f2p), "p2p": len(p2p), "candidates": len(cands),
354
+ "mutants_tried": tried}
355
+
356
+
357
+ def add_candidate(ctx, iid: str, patch_file: str, label: str) -> dict:
358
+ """Add a wrong fix by hand (for example one of Patch's failed candidates). It must be plausible
359
+ and wrong, like the generated ones."""
360
+ row = _row(ctx, iid)
361
+ if row["state"] != "ready":
362
+ raise ToolError("prepare the instance first: `ossa arena prepare`")
363
+ patch = Path(patch_file).read_text()
364
+ result = _suite(_env(ctx, row), [patch, _instance(ctx, iid)["human_tests_patch"]])
365
+ if result is None or not plausible_and_wrong(result.tests, row["f2p"], row["p2p"]):
366
+ raise ToolError("not a usable wrong candidate: it must apply, keep every P2P test passing and fail an F2P test")
367
+ k = ctx.store.query("SELECT COALESCE(MAX(k), 0) AS m FROM arena_candidates WHERE instance_id=?", (iid,))[0]["m"] + 1
368
+ (hidden_dir(ctx, iid) / "candidates" / f"{k}.patch").write_text(patch)
369
+ ctx.store.execute("INSERT INTO arena_candidates(instance_id, k, kind, label, patch_hash, state) VALUES(?,?,?,?,?,?)",
370
+ (iid, k, "added", label[:200], _hash(patch), "waiting"))
371
+ return {"instance": iid, "added": True}
372
+
373
+
374
+ # ------------------------------------------------------------------ rounds
375
+ def next_candidate(ctx, iid: str = "") -> dict:
376
+ """Turn the next waiting candidate into a blind duel attempt for Breaker. One at a time."""
377
+ from ossa import gitops
378
+ from ossa.stages import s03_workspace
379
+ active = ctx.store.query("SELECT attempt_id FROM arena_candidates WHERE state='active'")
380
+ if active:
381
+ raise ToolError(f"score the current candidate first: `ossa arena score {active[0]['attempt_id']}`")
382
+ sql = ("SELECT c.instance_id, c.k FROM arena_candidates c JOIN arena_instances i ON i.id=c.instance_id "
383
+ "WHERE c.state='waiting' AND i.state='ready'" + (" AND c.instance_id=?" if iid else "")
384
+ + " ORDER BY i.created_at, c.k LIMIT 1")
385
+ rows = ctx.store.query(sql, (iid,) if iid else ())
386
+ if not rows:
387
+ return {"attempt": None, "note": "no candidates waiting: mine and prepare more instances"}
388
+ iid, k = rows[0]["instance_id"], rows[0]["k"]
389
+ row = _row(ctx, iid)
390
+ env = _env(ctx, row)
391
+ patch = (hidden_dir(ctx, iid) / "candidates" / f"{k}.patch").read_text()
392
+ a = _new(ctx, row, "arena", _arena(ctx) / "blind", "arena")
393
+ _copy_env(env, a)
394
+ for name in ("fix.patch", "final.patch"):
395
+ ctx.store.write_artifact(a, name, patch)
396
+ ctx.store.write_artifact(a, "verify.json", {"verified": True})
397
+ wt = gitops.add_worktree(s03_workspace.repo_dir(a), s03_workspace.worktree_for(a), a.branch, a.base_sha)
398
+ ok, msg = gitops.apply_patch(wt, patch)
399
+ if not ok:
400
+ raise ToolError(f"candidate {k} of {iid} no longer applies: {msg[:200]}")
401
+ a.state = State.FIXED
402
+ ctx.store.save_attempt(a)
403
+ ctx.store.execute("UPDATE arena_candidates SET state='active', attempt_id=? WHERE instance_id=? AND k=?",
404
+ (a.id, iid, k))
405
+ return {"attempt": a.id, "worktree": str(wt), "artifacts": a.artifacts_dir,
406
+ "next": "start the breaker subagent with only the attempt id, the artifacts dir and the worktree; "
407
+ f"when its duel is done, `ossa arena score {a.id}`"}
408
+
409
+
410
+ def _attack_on(env: Attempt, patch: str, dest: str, text: str, command: str) -> str:
411
+ """passes | fails | bad: one attack test on base + `patch` (the hidden reference)."""
412
+ from ossa import gitops
413
+ from ossa.stages import s03_workspace
414
+ from ossa.stages.s06_reproduce import classify_failure
415
+ _, image = s03_workspace.load_env(env)
416
+ wt = _pristine(env, with_repro=False)
417
+ ok, _ = gitops.apply_patch(wt, patch)
418
+ if not ok:
419
+ return "bad"
420
+ (wt / dest).parent.mkdir(parents=True, exist_ok=True)
421
+ (wt / dest).write_text(text)
422
+ r = env.ctx.sandbox.run(wt, image, command, network=False, timeout=900)
423
+ gitops.git(wt, "clean", "-fdq", check=False)
424
+ return classify_failure(r.exit_code, r.output, getattr(r, "timed_out", False))[0]
425
+
426
+
427
+ def verdict(kind: str, candidate_outcome: str, status: str, reference: str) -> str:
428
+ if status == "invalid" or candidate_outcome in ("invalid", "flaky") or reference == "bad":
429
+ return "invalid"
430
+ if reference == "fails":
431
+ return "false_alarm"
432
+ if kind == "human":
433
+ return "held"
434
+ return "caught" if candidate_outcome == "broke" else "missed"
435
+
436
+
437
+ def score(ctx, attempt_id: str) -> dict:
438
+ """Score the active candidate's attacks against the hidden human fix, then hide the candidate."""
439
+ from ossa import breaker, gitops
440
+ from ossa.stages import s03_workspace
441
+ a = _attempt(ctx, attempt_id)
442
+ rows = ctx.store.query("SELECT * FROM arena_candidates WHERE attempt_id=?", (a.id,))
443
+ if not rows or rows[0]["state"] != "active":
444
+ raise ToolError(f"{a.id} is not an Arena candidate waiting to be scored")
445
+ cand = dict(rows[0])
446
+ attacks = breaker.attacks(ctx, a.id)
447
+ if not attacks:
448
+ raise ToolError("no attacks on this candidate yet: Breaker attacks first")
449
+ iid, kind = cand["instance_id"], cand["kind"]
450
+ env, human = _env(ctx, _row(ctx, iid)), _instance(ctx, iid)["human_patch"]
451
+ verdicts = []
452
+ for r in attacks:
453
+ reference = ""
454
+ if r["status"] != "invalid" and r["outcome"] not in ("invalid", "flaky"):
455
+ text = ctx.store.read_artifact(a, f"attacks/{r['n']}/{PurePosixPath(r['test_path']).name}") or ""
456
+ reference = _attack_on(env, human, r["test_path"], text, r["command"])
457
+ v = verdict(kind, r["outcome"], r["status"], reference)
458
+ verdicts.append(v)
459
+ ctx.store.execute(
460
+ "INSERT OR REPLACE INTO arena_scores(attempt_id, n, instance_id, k, kind, category, candidate_outcome, "
461
+ "reference_outcome, verdict, breaker_version, model, ts) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
462
+ (a.id, r["n"], iid, cand["k"], kind, r["category"], r["outcome"], reference, v,
463
+ r.get("breaker_version") or "", r.get("model") or "", now()))
464
+ if kind == "human":
465
+ result = "false_alarm" if "false_alarm" in verdicts else "clean"
466
+ else:
467
+ result = "caught" if "caught" in verdicts else "missed"
468
+ duel = breaker.duel(ctx, a.id)["verdict"]
469
+ ctx.store.execute("UPDATE arena_candidates SET state='scored', result=?, duel_verdict=? WHERE attempt_id=?",
470
+ (result, duel, a.id))
471
+ for wt in (s03_workspace.worktree_for(a), s03_workspace.worktree_for(a, 0)):
472
+ gitops.remove_worktree(s03_workspace.repo_dir(a), wt)
473
+ _move(a, hidden_dir(ctx, iid) / "scored" / a.id)
474
+ a.state = State.CLOSED
475
+ ctx.store.save_attempt(a)
476
+ left = ctx.store.query("SELECT COUNT(*) AS n FROM arena_candidates WHERE instance_id=? AND state='waiting'",
477
+ (iid,))[0]["n"]
478
+ return {"scored": a.id, "attacks": len(attacks), "candidates_left": int(left),
479
+ "next": "`ossa arena next`" if left else "`ossa arena report`"}
480
+
481
+
482
+ # ------------------------------------------------------------------ Patch's side
483
+ def solve(ctx, iid: str) -> Attempt:
484
+ """An attempt at the instance's base with only the issue: Sherlock reproduces, Patch fixes,
485
+ `ossa verify` checks it, and `ossa arena grade` scores it with the hidden tests."""
486
+ from ossa.stages import s03_workspace
487
+ row = _row(ctx, iid)
488
+ if row["state"] != "ready":
489
+ raise ToolError("prepare the instance first: `ossa arena prepare`")
490
+ env = _env(ctx, row)
491
+ a = _new(ctx, row, "arena-solve", _arena(ctx) / "solve", "arena-solve")
492
+ _copy_env(env, a)
493
+ s03_workspace.prepare_worktree(a, _blind_issue(_instance(ctx, iid)))
494
+ a.state = State.WORKSPACE_READY
495
+ ctx.store.save_attempt(a)
496
+ ctx.store.execute("INSERT INTO arena_solves(attempt_id, instance_id, ts) VALUES(?,?,?)", (a.id, iid, now()))
497
+ return a
498
+
499
+
500
+ def grade(ctx, attempt_id: str) -> dict:
501
+ from ossa import replay
502
+ from ossa.gitops import diff_stats
503
+ a = _attempt(ctx, attempt_id)
504
+ if a.kind != "arena-solve":
505
+ raise ToolError(f"{a.id} is not an Arena solve attempt")
506
+ if not (ctx.store.read_json(a, "verify.json") or {}).get("verified"):
507
+ raise ToolError("not verified yet: `ossa verify` first")
508
+ iid = ctx.store.query("SELECT instance_id FROM arena_solves WHERE attempt_id=?", (a.id,))[0]["instance_id"]
509
+ row = _row(ctx, iid)
510
+ fix = ctx.store.read_artifact(a, "fix.patch") or ""
511
+ result = _suite(_env(ctx, row), [fix, _instance(ctx, iid)["human_tests_patch"]])
512
+ tests = result.tests if result else {}
513
+ resolved = bool(result) and replay.is_resolved(row["f2p"], row["p2p"], tests)
514
+ out = {"attempt": a.id, "instance": iid, "resolved": resolved,
515
+ "f2p_passed": sum(tests.get(t) == "passed" for t in row["f2p"]), "f2p_total": len(row["f2p"]),
516
+ "p2p_broken": sum(tests.get(t) != "passed" for t in row["p2p"]), "lines": diff_stats(fix)[0],
517
+ "applies": result is not None}
518
+ ctx.store.execute("INSERT OR REPLACE INTO arena_solves VALUES(?,?,?,?,?,?,?,?)",
519
+ (a.id, iid, int(resolved), out["f2p_passed"], out["f2p_total"], out["p2p_broken"],
520
+ out["lines"], now()))
521
+ return out
522
+
523
+
524
+ # ------------------------------------------------------------------ status and report
525
+ def status(ctx) -> list[dict]:
526
+ """Instances and how far each got (no labels: those stay hidden)."""
527
+ out = []
528
+ for r in ctx.store.query("SELECT * FROM arena_instances ORDER BY created_at"):
529
+ counts = {s: 0 for s in ("waiting", "active", "scored")}
530
+ for c in ctx.store.query("SELECT state, COUNT(*) AS n FROM arena_candidates WHERE instance_id=? GROUP BY state",
531
+ (r["id"],)):
532
+ counts[c["state"]] = c["n"]
533
+ out.append({"instance": r["id"], "repo": r["repo"], "pr": r["pr"], "issue": r["issue"], "state": r["state"],
534
+ "env_attempt": r["env_attempt"], "contaminated": bool(r["contaminated"]), "note": r["note"],
535
+ "candidates": counts})
536
+ return out
537
+
538
+
539
+ def report(ctx) -> dict:
540
+ """Catch and false-alarm rates by Breaker version and model; Patch's resolve rate."""
541
+ cands = [dict(r) for r in ctx.store.query("SELECT * FROM arena_candidates WHERE state='scored'")]
542
+ scores = [dict(r) for r in ctx.store.query("SELECT * FROM arena_scores")]
543
+ by: dict[tuple[str, str], dict] = {}
544
+ for c in cands:
545
+ mine = [s for s in scores if s["attempt_id"] == c["attempt_id"]]
546
+ key = (mine[0]["breaker_version"] if mine else "", mine[0]["model"] if mine else "")
547
+ g = by.setdefault(key, {"wrong": 0, "caught": 0, "human": 0, "false_alarm": 0, "ship_on_wrong": 0,
548
+ "attacks": 0, "verdicts": {}, "categories": {}})
549
+ g["attacks"] += len(mine)
550
+ for s in mine:
551
+ g["verdicts"][s["verdict"]] = g["verdicts"].get(s["verdict"], 0) + 1
552
+ cat = g["categories"].setdefault(s["category"] or "?", {"attacks": 0, "caught": 0, "false_alarm": 0})
553
+ cat["attacks"] += 1
554
+ if s["verdict"] in ("caught", "false_alarm"):
555
+ cat[s["verdict"]] += 1
556
+ if c["kind"] == "human":
557
+ g["human"] += 1
558
+ g["false_alarm"] += c["result"] == "false_alarm"
559
+ else:
560
+ g["wrong"] += 1
561
+ g["caught"] += c["result"] == "caught"
562
+ g["ship_on_wrong"] += c["duel_verdict"] == "SHIP"
563
+ rows = []
564
+ for (version, model), g in sorted(by.items()):
565
+ rows.append({"breaker_version": version, "model": model, **g,
566
+ "catch_rate": round(g["caught"] / g["wrong"], 3) if g["wrong"] else None,
567
+ "false_alarm_rate": round(g["false_alarm"] / g["human"], 3) if g["human"] else None})
568
+ solves = ctx.store.query("SELECT COUNT(*) AS n, COALESCE(SUM(resolved), 0) AS r FROM arena_solves "
569
+ "WHERE resolved IS NOT NULL")[0]
570
+ inst = {s: n for s, n in ((r["state"], r["n"]) for r in ctx.store.query(
571
+ "SELECT state, COUNT(*) AS n FROM arena_instances GROUP BY state"))}
572
+ out = {"instances": inst, "breaker": rows,
573
+ "patch": {"solves": solves["n"], "resolved": solves["r"],
574
+ "resolve_rate": round(solves["r"] / solves["n"], 3) if solves["n"] else None}}
575
+ _arena(ctx).mkdir(parents=True, exist_ok=True)
576
+ (_arena(ctx) / "REPORT.md").write_text(render(out))
577
+ return out
578
+
579
+
580
+ def render(rep: dict) -> str:
581
+ pct = lambda x: "-" if x is None else f"{x:.0%}" # noqa: E731
582
+ lines = ["# Breaker Arena", "", "Instances: " + (", ".join(f"{n} {s}" for s, n in rep["instances"].items()) or "none"),
583
+ "", "| Breaker version | model | wrong fixes | caught | catch rate | human fixes | false alarms "
584
+ "| false-alarm rate | SHIP on a wrong fix | attacks |", "|---|---|---|---|---|---|---|---|---|---|"]
585
+ for r in rep["breaker"]:
586
+ lines.append(f"| {r['breaker_version'] or '?'} | {r['model'] or '?'} | {r['wrong']} | {r['caught']} | "
587
+ f"{pct(r['catch_rate'])} | {r['human']} | {r['false_alarm']} | {pct(r['false_alarm_rate'])} | "
588
+ f"{r['ship_on_wrong']} | {r['attacks']} |")
589
+ p = rep["patch"]
590
+ lines += ["", f"Patch: {p['resolved']} of {p['solves']} solves resolved ({pct(p['resolve_rate'])})."]
591
+ return "\n".join(lines) + "\n"