Skip to content

Commit 4dc5db0

Browse files
committed
benchmark: add a standalone syntax-only check mode
1 parent e835a5d commit 4dc5db0

4 files changed

Lines changed: 238 additions & 70 deletions

File tree

jumper_extension/adapters/ai_reviewer/benchmark/orchestrator.py

Lines changed: 153 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
import logging
99
from concurrent.futures import Future, ThreadPoolExecutor, wait, FIRST_COMPLETED
10+
from functools import partial
1011
from typing import Callable
1112

1213
from jumper_extension.adapters.ai_reviewer.benchmark import fingerprint
@@ -68,20 +69,30 @@ def __init__(
6869
self._position: dict[str, int] = {}
6970

7071
def run(self, baseline_code: str, variants: list[tuple[str, str]]) -> dict:
71-
"""Benchmark *baseline_code*, then each ``(label, code)`` variant.
72-
73-
Returns ``{label: BenchmarkResult}``; the baseline is under
74-
``BASELINE_LABEL``. An empty dict means the baseline itself would not
75-
run, which leaves nothing to compare against.
72+
"""Score each ``(label, code)`` variant the best the active checks allow.
73+
74+
With the timed run on, this measures *baseline_code* and every variant
75+
and compares their results, returning ``{label: BenchmarkResult}`` with
76+
the baseline under ``BASELINE_LABEL``. With the run turned off but
77+
``validate_syntax`` still on, it falls back to a syntax-only pass that
78+
still repairs broken suggestions - just without timing or comparison,
79+
and without a baseline entry. With neither active there is nothing to
80+
do. An empty dict also means the baseline itself would not run, leaving
81+
nothing to compare against.
7682
"""
77-
if not self.checks.run.active:
78-
# No timed replay: the only thing left worth measuring is gone, so
79-
# there is nothing to compare. resolve_checks already warned why.
80-
logger.info(
81-
"[JUmPER]: benchmark timed run is off; nothing to measure or compare."
82-
)
83-
return {}
83+
if self.checks.run.active:
84+
return self._run_timed(baseline_code, variants)
85+
if self.checks.validate_syntax.active:
86+
return self._run_syntax_only(variants)
87+
# Both the run and the syntax gate are off: resolve_checks already
88+
# explained why, and there is genuinely nothing left to do here.
89+
logger.info(
90+
"[JUmPER]: benchmark has no active checks; nothing to measure or verify."
91+
)
92+
return {}
8493

94+
def _run_timed(self, baseline_code: str, variants: list[tuple[str, str]]) -> dict:
95+
"""The full benchmark: measure the baseline, then time and verify each variant."""
8596
self.progress = BenchmarkProgress(len(variants), self.runs)
8697
self._position = {label: index for index, (label, _) in enumerate(variants, start=1)}
8798

@@ -111,74 +122,63 @@ def run(self, baseline_code: str, variants: list[tuple[str, str]]) -> dict:
111122
timeout = self._timeout_from(baseline_runs, duration)
112123

113124
results = {BASELINE_LABEL: baseline}
114-
results.update(self._benchmark_variants(variants, timeout, duration, prints))
125+
results.update(
126+
self._drive(
127+
variants,
128+
partial(
129+
self._process_timed,
130+
timeout=timeout,
131+
baseline_duration=duration,
132+
baseline_prints=prints,
133+
),
134+
)
135+
)
115136
self.progress.finished()
116137
return results
117138

118-
def _benchmark_variants(
119-
self,
120-
variants: list[tuple[str, str]],
121-
timeout: float,
122-
baseline_duration: float,
123-
baseline_prints: dict,
124-
) -> dict:
139+
def _run_syntax_only(self, variants: list[tuple[str, str]]) -> dict:
140+
"""Check each suggestion parses, repairing what does not - no timing.
141+
142+
The timed run is off, so there is nothing to measure or fingerprint, but
143+
a broken suggestion can still be caught and handed to the same repair
144+
loop. Each surviving variant is reported OK with ``UNVERIFIED``
145+
correctness and its valid (possibly repaired) code in ``final_code``;
146+
ones that never parse are reported FAILED.
147+
"""
148+
logger.info(
149+
f"[JUmPER]: benchmark: validating the syntax of {len(variants)} "
150+
"suggestion(s), with the timed run off."
151+
)
152+
self.progress = BenchmarkProgress(len(variants), self.runs)
153+
self._position = {label: index for index, (label, _) in enumerate(variants, start=1)}
154+
155+
results = self._drive(variants, self._process_syntax)
156+
157+
valid = sum(1 for result in results.values() if result.ok)
158+
logger.info(
159+
f"[JUmPER]: benchmark: syntax validated - {valid} valid, "
160+
f"{len(results) - valid} unfixable."
161+
)
162+
return results
163+
164+
def _drive(self, variants: list[tuple[str, str]], process: Callable) -> dict:
165+
"""Run *process* over each variant, draining repairs the shared way.
166+
167+
Every mode differs only in what it does with a fresh candidate; the
168+
repair round-trip - fold a returned fix back into the queue, or settle a
169+
candidate whose repair produced nothing - is identical, so it lives here
170+
once. *process* is called as ``process(results, candidate, pending,
171+
repairing, pool)`` and is responsible for settling the candidate or
172+
submitting it for repair.
173+
"""
125174
results: dict = {}
126175
pending = [_Candidate(label, code, self.fix_attempts) for label, code in variants]
127176
repairing: dict[Future, _Candidate] = {}
128177

129178
with ThreadPoolExecutor(max_workers=max(1, len(pending))) as pool:
130179
while pending or repairing:
131180
if pending:
132-
candidate = pending.pop(0)
133-
position = self._position[candidate.label]
134-
135-
# Syntax is checkable for free, so broken code never costs a
136-
# replay - whether it came from the model or from a repair.
137-
if not self._syntax_ok(candidate):
138-
self._reject(results, candidate, position, pool, repairing)
139-
continue
140-
141-
outcome = self._measure(
142-
candidate.code,
143-
candidate.label,
144-
timeout,
145-
on_run=lambda index: self.progress.variant_run(position, index),
146-
)
147-
outstanding = len(pending) + len(repairing)
148-
if isinstance(outcome, list):
149-
verdict = self._verdict(
150-
candidate, outcome, baseline_duration, baseline_prints
151-
)
152-
candidate.last_verdict = verdict
153-
candidate.last_code = candidate.code
154-
155-
# A wrong answer is worth repairing too: it costs the
156-
# review more than a crash, since nothing else catches it.
157-
if verdict.correctness == fingerprint.DIFFERS:
158-
candidate.error = fingerprint.describe_divergence(
159-
baseline_prints,
160-
outcome[-1].fingerprints,
161-
verdict.differing_names,
162-
)
163-
if self._submit_fix(candidate, pool, repairing):
164-
self.progress.variant_diverged(
165-
position,
166-
verdict.differing_names,
167-
candidate.attempts,
168-
self.fix_attempts,
169-
)
170-
continue
171-
172-
self._settle(results, candidate, verdict)
173-
self.progress.variant_done(
174-
position,
175-
_summarize(verdict),
176-
_walls(outcome),
177-
outstanding=outstanding,
178-
)
179-
else:
180-
candidate.error = outcome.error
181-
self._reject(results, candidate, position, pool, repairing)
181+
process(results, pending.pop(0), pending, repairing, pool)
182182
continue
183183

184184
done, _ = wait(list(repairing), return_when=FIRST_COMPLETED)
@@ -193,6 +193,89 @@ def _benchmark_variants(
193193
pending.append(candidate)
194194
return results
195195

196+
def _process_timed(
197+
self,
198+
results: dict,
199+
candidate: _Candidate,
200+
pending: list,
201+
repairing: dict,
202+
pool: ThreadPoolExecutor,
203+
*,
204+
timeout: float,
205+
baseline_duration: float,
206+
baseline_prints: dict,
207+
) -> None:
208+
"""Time one candidate against the baseline, repairing a crash or a divergence."""
209+
position = self._position[candidate.label]
210+
211+
# Syntax is checkable for free, so broken code never costs a replay -
212+
# whether it came from the model or from a repair.
213+
if not self._syntax_ok(candidate):
214+
self._reject(results, candidate, position, pool, repairing)
215+
return
216+
217+
outcome = self._measure(
218+
candidate.code,
219+
candidate.label,
220+
timeout,
221+
on_run=lambda index: self.progress.variant_run(position, index),
222+
)
223+
outstanding = len(pending) + len(repairing)
224+
if isinstance(outcome, list):
225+
verdict = self._verdict(candidate, outcome, baseline_duration, baseline_prints)
226+
candidate.last_verdict = verdict
227+
candidate.last_code = candidate.code
228+
229+
# A wrong answer is worth repairing too: it costs the review more
230+
# than a crash, since nothing else catches it.
231+
if verdict.correctness == fingerprint.DIFFERS:
232+
candidate.error = fingerprint.describe_divergence(
233+
baseline_prints,
234+
outcome[-1].fingerprints,
235+
verdict.differing_names,
236+
)
237+
if self._submit_fix(candidate, pool, repairing):
238+
self.progress.variant_diverged(
239+
position,
240+
verdict.differing_names,
241+
candidate.attempts,
242+
self.fix_attempts,
243+
)
244+
return
245+
246+
self._settle(results, candidate, verdict)
247+
self.progress.variant_done(
248+
position,
249+
_summarize(verdict),
250+
_walls(outcome),
251+
outstanding=outstanding,
252+
)
253+
else:
254+
candidate.error = outcome.error
255+
self._reject(results, candidate, position, pool, repairing)
256+
257+
def _process_syntax(
258+
self,
259+
results: dict,
260+
candidate: _Candidate,
261+
pending: list,
262+
repairing: dict,
263+
pool: ThreadPoolExecutor,
264+
) -> None:
265+
"""Parse one candidate, repairing it if it does not - never running it."""
266+
position = self._position[candidate.label]
267+
if self._syntax_ok(candidate):
268+
self.final_code[candidate.label] = candidate.code
269+
results[candidate.label] = BenchmarkResult(
270+
label=candidate.label,
271+
status=OK,
272+
attempts=candidate.attempts,
273+
correctness=fingerprint.UNVERIFIED,
274+
)
275+
self.progress.variant_validated(position, candidate.attempts)
276+
else:
277+
self._reject(results, candidate, position, pool, repairing)
278+
196279
def _reject(
197280
self,
198281
results: dict,

jumper_extension/adapters/ai_reviewer/benchmark/progress.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ def variant_done(self, position: int, summary: str, walls: list[float], outstand
6161
f"{self._remaining(outstanding, prefix='; ', suffix=' left')}"
6262
)
6363

64+
def variant_validated(self, position: int, attempts: int) -> None:
65+
"""A suggestion parsed (syntax-only mode: no timing to report)."""
66+
repaired = f" (repaired {attempts - 1}x)" if attempts > 1 else ""
67+
logger.info(
68+
f"[JUmPER]: benchmark: option {position}/{self.total_variants} "
69+
f"syntax valid{repaired}"
70+
)
71+
6472
def variant_failed(self, position: int, error: str, attempt: int, attempts: int) -> None:
6573
logger.info(
6674
f"[JUmPER]: benchmark: option {position}/{self.total_variants} failed "

jumper_extension/adapters/ai_reviewer/ui/review_display.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ def verdict_of(state: OptimizationState, index: int) -> dict | None:
7575
"notes": [],
7676
}
7777

78+
# No timed run (e.g. --check validate_syntax): nothing was measured or
79+
# compared, so report only that the code parses, plus any repair it took.
80+
if result.duration_s is None:
81+
notes = []
82+
if result.attempts > 1:
83+
notes.append(f"repaired after {result.attempts - 1} failed attempt(s)")
84+
return {
85+
"headline": "Syntax valid",
86+
"tone": "warn",
87+
"detail": "not timed",
88+
"notes": notes,
89+
}
90+
7891
notes = []
7992
tone = "good"
8093
if result.correctness == fingerprint.DIFFERS:

tests/ai_reviewer/test_benchmark_orchestrator.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from unittest.mock import Mock
22

33
from jumper_extension.adapters.ai_reviewer.benchmark import fingerprint
4+
from jumper_extension.adapters.ai_reviewer.benchmark.checks import CheckPlan, Decision
45
from jumper_extension.adapters.ai_reviewer.benchmark.models import FAILED, OK, RunOutcome
56
from jumper_extension.adapters.ai_reviewer.benchmark.orchestrator import (
67
BASELINE_LABEL,
78
BenchmarkOrchestrator,
89
)
910

11+
12+
def _syntax_only_checks() -> CheckPlan:
13+
"""run off, validate_syntax on - the standalone syntax-check plan."""
14+
return CheckPlan(validate_syntax=Decision(True), run=Decision(False))
15+
1016
_PRINTS = {"y": {"kind": "scalar", "value": 42.0}}
1117

1218

@@ -183,6 +189,64 @@ def test_an_unrepairable_divergence_keeps_the_measurement_it_did_get():
183189
assert results["1"].correctness == fingerprint.DIFFERS
184190

185191

192+
def test_syntax_only_mode_validates_without_running_anything():
193+
log = []
194+
orchestrator = _orchestrator(
195+
lambda code: 1.0,
196+
log=log,
197+
checks=_syntax_only_checks(),
198+
)
199+
200+
results = orchestrator.run("base", [("1", "y = 1")])
201+
202+
assert log == [] # not even the baseline was replayed
203+
assert BASELINE_LABEL not in results # nothing to compare against
204+
assert results["1"].status == OK
205+
assert results["1"].correctness == fingerprint.UNVERIFIED
206+
assert results["1"].duration_s is None
207+
assert orchestrator.final_code["1"] == "y = 1"
208+
209+
210+
def test_syntax_only_mode_repairs_a_broken_suggestion_without_running_it():
211+
log = []
212+
orchestrator = _orchestrator(
213+
lambda code: 1.0,
214+
fix_fn=lambda code, error, label: "y = 1",
215+
log=log,
216+
checks=_syntax_only_checks(),
217+
)
218+
219+
results = orchestrator.run("base", [("1", "def broken(:")])
220+
221+
assert log == [] # the fix was checked statically, never executed
222+
assert results["1"].status == OK
223+
assert results["1"].attempts == 2
224+
assert orchestrator.final_code["1"] == "y = 1"
225+
226+
227+
def test_syntax_only_mode_gives_up_on_an_unfixable_suggestion():
228+
orchestrator = _orchestrator(
229+
lambda code: 1.0,
230+
fix_fn=lambda code, error, label: "def still_broken(:",
231+
fix_attempts=2,
232+
checks=_syntax_only_checks(),
233+
)
234+
235+
results = orchestrator.run("base", [("1", "def broken(:")])
236+
237+
assert results["1"].status == FAILED
238+
assert "1" not in orchestrator.final_code
239+
240+
241+
def test_nothing_runs_when_every_check_is_off():
242+
orchestrator = _orchestrator(
243+
lambda code: 1.0,
244+
checks=CheckPlan(validate_syntax=Decision(False), run=Decision(False)),
245+
)
246+
247+
assert orchestrator.run("base", [("1", "y = 1")]) == {}
248+
249+
186250
def test_timeout_budget_covers_the_prefix_and_room_to_be_slow():
187251
orchestrator = _orchestrator(lambda code: 4.0, timeout_factor=10.0)
188252

0 commit comments

Comments
 (0)