From 1e6bd34d6f966d84ac0c21ff9b8613d9ef83fed8 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 05:22:08 -0400 Subject: [PATCH] feat: verify linked-issue consistency for the selected PR (Closes #314) Add assess_linked_issue_consistency to review_proofs: reviewer final reports must report a "Linked issue status" that matches the issue(s) the selected PR was live-verified to link this session. Without live proof the field may only say "not verified in this session"; with proof, every linked issue must be named, and any other "Issue #N" mention anywhere in the report fails as stale leakage from a previous PR (the PR #280 / Issue #260-vs-#275 incident). Missing field fails closed. Tests cover correct linked issue, stale previous issue in both the status field and diagnostics, missing proof, unverified wording, multiple linked issues, and PR-number mentions not being mistaken for issue mentions. Co-Authored-By: Claude Opus 4.8 (1M context) --- review_proofs.py | 83 +++++++++++++++++ tests/test_linked_issue_consistency.py | 118 +++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 tests/test_linked_issue_consistency.py diff --git a/review_proofs.py b/review_proofs.py index 233cfcc..1ca42f2 100644 --- a/review_proofs.py +++ b/review_proofs.py @@ -1052,6 +1052,89 @@ def _prs_from_list_response(list_prs_response: list | dict | None) -> list | Non return None +# ── Linked-issue consistency (Issue #314) ──────────────────────────────────── +# +# Stale linked-issue text from a previous PR must not leak into a final +# report: the "Linked issue status" field must match the issue(s) the +# selected PR actually links, verified live this session, and no other +# "Issue #N" mention may appear unless it is a verified linked issue. + +_ISSUE_MENTION_RE = re.compile(r"\bissue\s+#(\d+)", re.IGNORECASE) + + +def _report_labeled_fields(report_text): + """Return {lowercase label: value} for every 'Label: value' line.""" + fields = {} + for line in (report_text or "").splitlines(): + stripped = line.strip().lstrip("-*").strip() + if ":" in stripped: + k, v = stripped.split(":", 1) + fields[k.strip().lower()] = v.strip() + return fields + + +def _issue_numbers_mentioned(text): + """Return the set of ints referenced as 'Issue #N' in *text*.""" + return {int(n) for n in _ISSUE_MENTION_RE.findall(text or "")} + + +def assess_linked_issue_consistency(report_text, *, selected_pr=None, + linked_issues=None): + """#314: linked-issue status must match the selected PR, live-verified. + + *linked_issues* is the list of issue numbers the selected PR was + live-verified to link this session, or None when no live verification + happened. Without live proof the report may only say the status was + not verified; with proof, the ``Linked issue status`` field must name + every linked issue and no stale issue number may appear anywhere in + the report. + + Returns {'complete', 'downgraded', 'reasons'}; fails closed. + """ + fields = _report_labeled_fields(report_text) + status_value = fields.get("linked issue status") + + reasons = [] + + if status_value is None: + reasons.append( + "final report missing 'Linked issue status' field for the " + "selected PR" + ) + elif linked_issues is None: + if not status_value.strip().lower().startswith("not verified"): + reasons.append( + "linked issue status claimed without live proof; report " + "'Linked issue status: not verified in this session' or " + "verify the linked issue live" + ) + else: + allowed = set(linked_issues) + status_mentions = _issue_numbers_mentioned(status_value) + for missing in sorted(allowed - status_mentions): + reasons.append( + f"linked issue #{missing} of selected PR " + f"#{selected_pr} not reported in 'Linked issue status'" + ) + for stale in sorted(status_mentions - allowed): + reasons.append( + f"'Linked issue status' names issue #{stale}, which is " + f"not a live-verified linked issue of selected PR " + f"#{selected_pr}" + ) + for stale in sorted(_issue_numbers_mentioned(report_text) - allowed): + reasons.append( + f"stale issue #{stale} mentioned in final report but not " + f"live-verified as linked to selected PR #{selected_pr}" + ) + + return { + "complete": not reasons, + "downgraded": bool(reasons), + "reasons": reasons, + } + + def assess_role_boundary(proof=None, *, task_role=None, namespaces_used=None, justification=None): """Assess reviewer/author role separation for blind queue workflows. diff --git a/tests/test_linked_issue_consistency.py b/tests/test_linked_issue_consistency.py new file mode 100644 index 0000000..cf325bd --- /dev/null +++ b/tests/test_linked_issue_consistency.py @@ -0,0 +1,118 @@ +"""Tests for linked-issue consistency verifier (#314). + +Reviewer reports must verify the linked issue live for the selected PR; +stale issue numbers from a previous PR must not leak into the final +report, and linked-issue status must never be claimed without live proof. +""" +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from review_proofs import assess_linked_issue_consistency # noqa: E402 + + +class TestCorrectLinkedIssue(unittest.TestCase): + def test_matching_linked_issue_passes(self): + report = ( + "Selected PR: 280\n" + "Linked issue status: Issue #275 open, closes on merge\n" + ) + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275] + ) + self.assertTrue(result["complete"]) + self.assertFalse(result["downgraded"]) + self.assertEqual(result["reasons"], []) + + def test_multiple_linked_issues_all_reported_passes(self): + report = ( + "Linked issue status: Issue #275 and Issue #276 both open\n" + ) + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275, 276] + ) + self.assertTrue(result["complete"]) + + def test_multiple_linked_issues_one_missing_fails(self): + report = "Linked issue status: Issue #275 open\n" + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275, 276] + ) + self.assertFalse(result["complete"]) + self.assertTrue( + any("276" in r for r in result["reasons"]) + ) + + +class TestStaleIssueLeak(unittest.TestCase): + def test_stale_issue_in_status_field_fails(self): + # #314 observed behavior: PR #280 links Issue #275 but the report + # carries Issue #260 from the previous PR. + report = "Linked issue status: Issue #260 open\n" + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275] + ) + self.assertFalse(result["complete"]) + self.assertTrue(any("260" in r for r in result["reasons"])) + + def test_stale_issue_in_diagnostics_fails(self): + report = ( + "Linked issue status: Issue #275 open\n" + "Read-only diagnostics: viewed Issue #260 status\n" + ) + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275] + ) + self.assertFalse(result["complete"]) + self.assertTrue( + any("stale" in r.lower() and "260" in r for r in result["reasons"]) + ) + + def test_selected_pr_number_mention_is_not_stale(self): + # "PR #280" mentions must not be confused with issue mentions. + report = ( + "Selected PR: 280\n" + "Validation: ran tests at PR #280 head\n" + "Linked issue status: Issue #275 open\n" + ) + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275] + ) + self.assertTrue(result["complete"]) + + +class TestMissingProof(unittest.TestCase): + def test_status_claim_without_live_proof_fails(self): + report = "Linked issue status: Issue #275 open\n" + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=None + ) + self.assertFalse(result["complete"]) + self.assertTrue( + any("live proof" in r.lower() for r in result["reasons"]) + ) + + def test_unverified_wording_without_proof_passes(self): + report = ( + "Linked issue status: not verified in this session\n" + ) + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=None + ) + self.assertTrue(result["complete"]) + + def test_missing_field_fails(self): + report = "Selected PR: 280\n" + result = assess_linked_issue_consistency( + report, selected_pr=280, linked_issues=[275] + ) + self.assertFalse(result["complete"]) + self.assertTrue( + any("linked issue status" in r.lower() for r in result["reasons"]) + ) + + +if __name__ == "__main__": + unittest.main()