Skip to content

Commit 2df9a88

Browse files
igerberclaude
andcommitted
Fix findings element validation, one-to-one fallback matching
P1: Filter non-dict elements from findings list in parse_review_state(). Entries like strings or integers are silently dropped instead of crashing downstream code that calls f.get(...). P2: Track consumed current candidates in merge_findings() pass 2b so two prior no-location findings can't both match the same current finding. Ensures one-to-one matching in the reverse fallback pass. P2: Add 2 regression tests: non-dict findings filtering, duplicate no-location findings one-to-one matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e3585eb commit 2df9a88

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

.claude/scripts/openai_review.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ def parse_review_state(path: str) -> "tuple[list[dict], int]":
391391
file=sys.stderr,
392392
)
393393
return ([], 0)
394+
# Filter to dict elements only — non-dict entries (e.g., strings) would
395+
# crash downstream code that calls f.get(...)
396+
findings = [f for f in findings if isinstance(f, dict)]
394397

395398
review_round = data.get("review_round", 0)
396399
if not isinstance(review_round, int):
@@ -669,12 +672,14 @@ def merge_findings(
669672
merged_pass2.append(f)
670673

671674
# 2b: Unconsumed previous findings without file paths → try to match
672-
# current findings that DO have file paths (reverse direction)
675+
# current findings that DO have file paths (reverse direction).
676+
# Track consumed current candidates to ensure one-to-one matching.
673677
current_by_fallback: dict[tuple, list[dict]] = {}
674678
for f in merged_pass2:
675679
_, fallback = _finding_keys(f)
676680
current_by_fallback.setdefault(fallback, []).append(f)
677681

682+
consumed_current_ids: set[str] = set()
678683
for f in previous:
679684
fid = f.get("id", "")
680685
if fid in consumed_ids:
@@ -684,9 +689,14 @@ def merge_findings(
684689
if has_file:
685690
continue # Has file path — should have matched in pass 1 if possible
686691
# Previous finding without file path — try fallback against current
687-
candidates = current_by_fallback.get(fallback, [])
692+
# Exclude already-consumed current candidates for one-to-one matching
693+
candidates = [
694+
c for c in current_by_fallback.get(fallback, [])
695+
if c.get("id", "") not in consumed_current_ids
696+
]
688697
if len(candidates) == 1:
689698
consumed_ids.add(fid)
699+
consumed_current_ids.add(candidates[0].get("id", ""))
690700

691701
# Mark unconsumed previous findings as addressed
692702
for f in previous:

tests/test_openai_review.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,20 @@ def test_non_int_round_defaults_to_zero(self, review_mod, tmp_path):
767767
assert findings == []
768768
assert round_num == 0
769769

770+
def test_non_dict_findings_filtered(self, review_mod, tmp_path):
771+
"""Non-dict elements in findings list are filtered out, not crash."""
772+
state_file = tmp_path / "review-state.json"
773+
state = {
774+
"schema_version": 1,
775+
"findings": ["oops", {"id": "R1-P1-1", "severity": "P1"}, 42],
776+
"review_round": 1,
777+
}
778+
state_file.write_text(json.dumps(state))
779+
findings, round_num = review_mod.parse_review_state(str(state_file))
780+
assert len(findings) == 1
781+
assert findings[0]["id"] == "R1-P1-1"
782+
assert round_num == 1
783+
770784

771785
class TestWriteReviewState:
772786
def test_writes_valid_json(self, review_mod, tmp_path):
@@ -1070,6 +1084,28 @@ def test_multiple_findings_same_key(self, review_mod):
10701084
assert len(open_findings) == 1
10711085
assert len(addressed) == 1
10721086

1087+
def test_duplicate_no_location_findings_one_to_one(self, review_mod):
1088+
"""Two prior no-location findings should not both match one current finding."""
1089+
previous = [
1090+
{"id": "R1-P1-1", "severity": "P1", "location": "",
1091+
"section": "Code Quality", "summary": "Missing NaN guard",
1092+
"status": "open"},
1093+
{"id": "R1-P1-2", "severity": "P1", "location": "",
1094+
"section": "Methodology", "summary": "Missing NaN guard",
1095+
"status": "open"},
1096+
]
1097+
current = [
1098+
{"id": "R2-P1-1", "severity": "P1", "location": "foo.py:L10",
1099+
"section": "Code Quality", "summary": "Missing NaN guard",
1100+
"status": "open"},
1101+
]
1102+
merged = review_mod.merge_findings(previous, current)
1103+
open_findings = [f for f in merged if f["status"] == "open"]
1104+
addressed = [f for f in merged if f["status"] == "addressed"]
1105+
# One current + one prior matched = 1 open; one prior unmatched = 1 addressed
1106+
assert len(open_findings) == 1
1107+
assert len(addressed) == 1
1108+
10731109
def test_previous_missing_location_current_has_location(self, review_mod):
10741110
"""Previous finding with no location, current has one → should match."""
10751111
previous = [

0 commit comments

Comments
 (0)