Skip to content

Commit 39c43f2

Browse files
committed
#15 part1: fix unsound_extraction to use pred extract (not claimed_source_solution)
- verify.py: _check_unsound_extraction now runs pred extract on bundle+target_config to re-derive the extracted solution; claimed_source_solution is no longer required - tests/fixtures/valid_bug.json: updated to suboptimal_extraction (MIS→MaxClique is a correct reduction; unsound fixture requiring real pred extract was invalid) - test_verify.py: unsound tests converted to mock-based; 2 new tests added (test_unsound_uses_pred_extract, test_unsound_wrong_claimed_but_real_bug) 167 tests passing
1 parent ac256a0 commit 39c43f2

3 files changed

Lines changed: 140 additions & 51 deletions

File tree

‎benchmark/tests/fixtures/valid_bug.json‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
2-
"_comment": "Genuine unsound_extraction bug fixture. The bundle is real (MIS graph 0-1,1-2 -> MaximumClique). A hypothetically buggy extract_solution returns [1,1,0] (adjacent vertices 0 and 1 both selected), which is invalid for MIS. Verifier must accept this as a real bug.",
2+
"_comment": "Genuine suboptimal_extraction bug fixture. Target config [1,0,0] extracts solution [1,0,0] with value Max(1), but [1,0,1] achieves Max(2) — strictly better. Verifier must accept this as a real suboptimality.",
33
"rule": "MaximumIndependentSetToMaximumClique",
4+
"violation": "suboptimal_extraction",
45
"source": {
56
"data": {
67
"graph": {
@@ -40,8 +41,8 @@
4041
"variant": {"graph": "SimpleGraph", "weight": "One"}
4142
}
4243
},
43-
"violation": "unsound_extraction",
44+
"violation": "suboptimal_extraction",
4445
"target_config": "1,0,0",
45-
"claimed_source_solution": [1, 1, 0],
46-
"note": "A buggy extract_solution maps target config [1,0,0] back to [1,1,0] in source space. But vertices 0 and 1 are adjacent in source graph, so [1,1,0] is not a valid independent set. This is a genuine bug."
46+
"brute_force_solution": [1, 0, 1],
47+
"note": "target_config [1,0,0] extracts solution of size 1, but [1,0,1] achieves size 2. This is a genuine suboptimality."
4748
}

‎benchmark/tests/test_verify.py‎

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import json
2020
import pytest
21+
from unittest.mock import patch
2122

2223
from benchmark.verify import (
2324
Verdict,
@@ -29,6 +30,16 @@
2930
from benchmark.tests.conftest import MIS_SOURCE, MIS_TO_CLIQUE_BUNDLE
3031

3132

33+
def _mock_unsound_bug_responses():
34+
"""Mock responses for an unsound_extraction bug: extract returns invalid solution."""
35+
return [
36+
(0, json.dumps(MIS_TO_CLIQUE_BUNDLE), ""), # pred reduce
37+
(0, json.dumps({"result": "Max(1)"}), ""), # pred evaluate target_config (valid)
38+
(0, json.dumps({"solution": [1, 1, 0], "evaluation": "Max(None)"}), ""), # pred extract
39+
(0, json.dumps({"result": "Max(None)"}), ""), # pred evaluate extracted
40+
]
41+
42+
3243
# ── A. Pure-logic helpers ─────────────────────────────────────────────────────
3344

3445
class TestNormalize:
@@ -148,16 +159,17 @@ def test_unsound_missing_target_config(self):
148159
assert "target_config" in v.reason
149160

150161
def test_unsound_missing_claimed_source_solution(self):
162+
# claimed_source_solution is no longer required — verifier runs pred extract itself
151163
cert = {
152164
"source": MIS_SOURCE,
153165
"bundle": MIS_TO_CLIQUE_BUNDLE,
154166
"violation": "unsound_extraction",
155167
"target_config": "1,0,0",
156-
# claimed_source_solution intentionally omitted
168+
# claimed_source_solution intentionally omitted — should not block verification
157169
}
158-
v = verify(cert)
159-
assert not v.accepted
160-
assert "claimed_source_solution" in v.reason
170+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()):
171+
v = verify(cert)
172+
assert v.accepted, f"missing claimed_source_solution should not block verification, got: {v.reason}"
161173

162174
def test_suboptimal_missing_target_config(self):
163175
cert = {
@@ -191,56 +203,123 @@ def test_tampered_target_rejected(self, tampered_bundle_cert):
191203
assert not v.accepted
192204
assert "does not match" in v.reason
193205

194-
def test_correct_bundle_passes_integrity(self, valid_unsound_cert):
206+
def test_correct_bundle_passes_integrity(self):
195207
"""A real (unmodified) bundle must survive the integrity check."""
196-
# The unsound cert uses a real bundle — integrity should pass
197-
# (the cert itself is a genuine bug, so accepted=True)
198-
v = verify(valid_unsound_cert)
199-
assert v.accepted # if integrity failed, this would be False
208+
cert = {
209+
"rule": "MaximumIndependentSetToMaximumClique",
210+
"violation": "unsound_extraction",
211+
"source": MIS_SOURCE,
212+
"bundle": MIS_TO_CLIQUE_BUNDLE,
213+
"target_config": "1,0,0",
214+
}
215+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()):
216+
v = verify(cert)
217+
# If bundle integrity failed, reason would mention "does not match"
218+
assert "does not match" not in v.reason
200219

201220

202221
# ── D. unsound_extraction ─────────────────────────────────────────────────────
203222

204223
class TestUnsoundExtraction:
205-
def test_genuine_bug_accepted(self, valid_unsound_cert):
206-
"""[1,1,0] on graph 0-1,1-2 is invalid (adjacent) → real bug → accepted."""
207-
v = verify(valid_unsound_cert)
224+
def test_genuine_bug_accepted(self):
225+
"""Mocked: extract returns invalid solution → real bug → accepted."""
226+
cert = {
227+
"rule": "MaximumIndependentSetToMaximumClique",
228+
"violation": "unsound_extraction",
229+
"source": MIS_SOURCE,
230+
"bundle": MIS_TO_CLIQUE_BUNDLE,
231+
"target_config": "1,0,0",
232+
"claimed_source_solution": [1, 1, 0],
233+
}
234+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()):
235+
v = verify(cert)
208236
assert v.accepted
209237
assert "invalid" in v.reason
210-
assert "Max(None)" in v.reason
211238

212-
def test_false_alarm_rejected(self, false_alarm_cert):
213-
"""[1,0,1] on graph 0-1,1-2 is valid → not a bug → rejected."""
214-
v = verify(false_alarm_cert)
239+
def test_false_alarm_rejected(self):
240+
"""Mocked: extract returns valid solution → not a bug → rejected."""
241+
responses = [
242+
(0, json.dumps(MIS_TO_CLIQUE_BUNDLE), ""), # pred reduce
243+
(0, json.dumps({"result": "Max(2)"}), ""), # pred evaluate target_config
244+
(0, json.dumps({"solution": [1, 0, 1], "evaluation": "Max(2)"}), ""), # pred extract
245+
(0, json.dumps({"result": "Max(2)"}), ""), # pred evaluate extracted
246+
]
247+
cert = {
248+
"rule": "MaximumIndependentSetToMaximumClique",
249+
"violation": "unsound_extraction",
250+
"source": MIS_SOURCE,
251+
"bundle": MIS_TO_CLIQUE_BUNDLE,
252+
"target_config": "1,0,1",
253+
"claimed_source_solution": [1, 0, 1],
254+
}
255+
with patch("benchmark.verify._run_pred", side_effect=responses):
256+
v = verify(cert)
215257
assert not v.accepted
216258
assert "valid" in v.reason
217259

218-
def test_verdict_details_on_acceptance(self, valid_unsound_cert):
219-
"""Accepted verdict must carry details with evaluation and solution."""
220-
v = verify(valid_unsound_cert)
260+
def test_verdict_details_on_acceptance(self):
261+
"""Accepted verdict must carry details with evaluation and extracted_solution."""
262+
cert = {
263+
"rule": "MaximumIndependentSetToMaximumClique",
264+
"violation": "unsound_extraction",
265+
"source": MIS_SOURCE,
266+
"bundle": MIS_TO_CLIQUE_BUNDLE,
267+
"target_config": "1,0,0",
268+
"claimed_source_solution": [1, 1, 0],
269+
}
270+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()):
271+
v = verify(cert)
221272
assert v.accepted
222273
assert "evaluation" in v.details
223-
assert "claimed_source_solution" in v.details
274+
assert "extracted_solution" in v.details
224275

225276
def test_invalid_target_config_rejected(self):
226-
"""If target_config itself is not a valid target solution, reject."""
227-
# For MaximumClique on the complement graph (edges: [0,2]),
228-
# config [1,1,0] tries to select vertices 0 and 1, but 0-1 is NOT an edge
229-
# in the complement graph, so this is not a valid clique.
277+
"""If target_config is not a valid target solution, reject."""
278+
responses = [
279+
(0, json.dumps(MIS_TO_CLIQUE_BUNDLE), ""), # pred reduce
280+
(0, json.dumps({"result": "Max(None)"}), ""), # pred evaluate target_config → invalid
281+
]
230282
cert = {
231283
"rule": "MaximumIndependentSetToMaximumClique",
232284
"violation": "unsound_extraction",
233285
"source": MIS_SOURCE,
234286
"bundle": MIS_TO_CLIQUE_BUNDLE,
235-
"target_config": "1,1,0", # invalid clique (0 and 1 not adjacent in complement)
287+
"target_config": "1,1,0",
236288
"claimed_source_solution": [1, 1, 0],
237289
}
238-
v = verify(cert)
239-
assert not v.accepted
240-
# Either rejected due to invalid target_config or invalid source solution —
241-
# either way it should not be accepted as a real bug
290+
with patch("benchmark.verify._run_pred", side_effect=responses):
291+
v = verify(cert)
242292
assert not v.accepted
243293

294+
def test_unsound_uses_pred_extract(self):
295+
"""Verifier must call pred extract — not trust claimed_source_solution."""
296+
cert = {
297+
"rule": "MaximumIndependentSetToMaximumClique",
298+
"violation": "unsound_extraction",
299+
"source": MIS_SOURCE,
300+
"bundle": MIS_TO_CLIQUE_BUNDLE,
301+
"target_config": "1,0,0",
302+
"claimed_source_solution": [0, 0, 0], # wrong — verifier should ignore this
303+
}
304+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()) as mock_pred:
305+
verify(cert)
306+
called_verbs = [c.args[0][0] for c in mock_pred.call_args_list]
307+
assert "extract" in called_verbs, "verifier must call pred extract"
308+
309+
def test_unsound_wrong_claimed_but_real_bug(self):
310+
"""AI provides wrong claimed_source_solution — verifier ignores it and uses pred extract."""
311+
cert = {
312+
"rule": "MaximumIndependentSetToMaximumClique",
313+
"violation": "unsound_extraction",
314+
"source": MIS_SOURCE,
315+
"bundle": MIS_TO_CLIQUE_BUNDLE,
316+
"target_config": "1,0,0",
317+
"claimed_source_solution": [0, 0, 0], # wrong
318+
}
319+
with patch("benchmark.verify._run_pred", side_effect=_mock_unsound_bug_responses()):
320+
v = verify(cert)
321+
assert v.accepted, f"Real bug should be accepted even with wrong claimed_source_solution, got: {v.reason}"
322+
244323

245324
# ── E. incomplete_reduction ───────────────────────────────────────────────────
246325

‎benchmark/verify.py‎

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -159,21 +159,15 @@ def _check_unsound_extraction(cert: dict, source_file: str, bundle_file: str, tm
159159
"""
160160
Unsound extraction: a valid target solution maps back to an INVALID source solution.
161161
162-
The AI claims extract_solution(target_config) returned claimed_source_solution, which is invalid.
163-
We verify:
164-
1. The claimed_source_solution is actually invalid when evaluated against the source.
165-
2. The target_config is a valid (non-None) target solution — so it really should have a valid extraction.
162+
Verifier re-derives the extracted solution via pred extract — never trusts
163+
the AI-supplied claimed_source_solution.
166164
"""
167165
target_config = cert.get("target_config")
168-
claimed_source_solution = cert.get("claimed_source_solution")
169166

170167
if not target_config:
171168
return Verdict(False, "unsound_extraction certificate missing target_config")
172-
if claimed_source_solution is None:
173-
return Verdict(False, "unsound_extraction certificate missing claimed_source_solution")
174169

175-
# Step 1: verify the claimed target_config is actually a valid target solution
176-
# (if the target solution itself is invalid, this isn't evidence of a reduction bug)
170+
# Step 1: verify target_config is a valid target solution
177171
target_data = cert.get("bundle", {}).get("target")
178172
if target_data:
179173
target_file = os.path.join(tmpdir, "target.json")
@@ -192,34 +186,49 @@ def _check_unsound_extraction(cert: dict, source_file: str, bundle_file: str, tm
192186
{"target_evaluation": tgt_result},
193187
)
194188
except json.JSONDecodeError:
195-
pass # skip target validation if target type isn't evaluable this way
189+
pass
190+
191+
# Step 2: run pred extract to get the real extracted source solution
192+
rc, stdout, stderr = _run_pred(
193+
["extract", "-", "--config", target_config, "--json"], stdin_file=bundle_file
194+
)
195+
if rc != 0:
196+
return Verdict(False, f"pred extract failed: {stderr.strip()[:200]}")
197+
198+
try:
199+
extract_result = json.loads(stdout)
200+
except json.JSONDecodeError as e:
201+
return Verdict(False, f"pred extract returned invalid JSON: {e}")
202+
203+
extracted_solution = extract_result.get("solution")
204+
if extracted_solution is None:
205+
return Verdict(False, "pred extract returned no solution field")
196206

197-
# Step 2: evaluate the claimed source solution — must be INVALID
198-
config_str = ",".join(str(x) for x in claimed_source_solution)
207+
# Step 3: evaluate the extracted source solution — must be INVALID
208+
config_str = ",".join(str(x) for x in extracted_solution)
199209
rc, stdout, stderr = _run_pred(
200210
["evaluate", "-", "--config", config_str, "--json"], stdin_file=source_file
201211
)
202212
if rc != 0:
203-
return Verdict(False, f"pred evaluate (claimed source solution) failed: {stderr.strip()[:200]}")
213+
return Verdict(False, f"pred evaluate (extracted solution) failed: {stderr.strip()[:200]}")
204214

205215
try:
206216
eval_result = json.loads(stdout)
207217
except json.JSONDecodeError as e:
208218
return Verdict(False, f"pred evaluate returned invalid JSON: {e}")
209219

210220
result_value = eval_result.get("result", "")
211-
# Invalid solutions: Max(None) for optimization, Or(false) for decision
212221
if "None" in result_value or "false" in result_value.lower():
213222
return Verdict(
214223
True,
215-
f"confirmed unsound extraction: claimed source solution {claimed_source_solution} is invalid ({result_value})",
216-
{"claimed_source_solution": claimed_source_solution, "evaluation": result_value},
224+
f"confirmed unsound extraction: extracted source solution {extracted_solution} is invalid ({result_value})",
225+
{"extracted_solution": extracted_solution, "evaluation": result_value},
217226
)
218227
else:
219228
return Verdict(
220229
False,
221-
f"claimed source solution is actually valid: {result_value} — not a bug",
222-
{"claimed_source_solution": claimed_source_solution, "evaluation": result_value},
230+
f"extracted source solution is actually valid: {result_value} — not a bug",
231+
{"extracted_solution": extracted_solution, "evaluation": result_value},
223232
)
224233

225234

0 commit comments

Comments
 (0)