From eaead549c3d76f9219c5a1799e67d389a02a82ca Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Sun, 5 Jul 2026 16:43:02 -0400 Subject: [PATCH] feat(reviewer-preflight): check Workspace mutations handoff fields --- review_proofs.py | 34 +++++++++++++++++++++++----------- tests/test_review_proofs.py | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/review_proofs.py b/review_proofs.py index 718061d..040e0c6 100644 --- a/review_proofs.py +++ b/review_proofs.py @@ -530,6 +530,7 @@ HANDOFF_BASE_FIELDS = ( ("Files changed", ("files changed", "changed", "files")), ("Validation", ("validation",)), ("Mutations", ("mutations",)), + ("Workspace mutations", ("workspace mutations",)), ("Current status", ("current status", "status")), ("Blockers", ("blockers",)), ("Next", ("next",)), @@ -580,7 +581,7 @@ def _handoff_section_lines(report_text): return lines[start:] -def assess_controller_handoff(report_text, role=None): +def assess_controller_handoff(report_text, role=None, local_edits=False): """Issue #182: final reports without a Controller Handoff downgrade. Verdicts: @@ -592,6 +593,7 @@ def assess_controller_handoff(report_text, role=None): The handoff supplements the full report; this helper never validates the full report body, only the continuation summary. """ + import re section = _handoff_section_lines(report_text) if section is None: return { @@ -605,10 +607,14 @@ def assess_controller_handoff(report_text, role=None): } labels = [] + fields_dict = {} for line in section: stripped = line.strip().lstrip("-*").strip() if ":" in stripped: - labels.append(stripped.split(":", 1)[0].strip().lower()) + k, v = stripped.split(":", 1) + label = k.strip().lower() + labels.append(label) + fields_dict[label] = v.strip() required = list(HANDOFF_BASE_FIELDS) required.extend(HANDOFF_ROLE_FIELDS.get(role or "", ())) @@ -629,13 +635,6 @@ def assess_controller_handoff(report_text, role=None): } # Validate issue/PR references for exact number and no forbidden terms (Issue #194 / #196) - fields_dict = {} - for line in section: - stripped = line.strip().lstrip("-*").strip() - if ":" in stripped: - k, v = stripped.split(":", 1) - fields_dict[k.strip().lower()] = v.strip() - for alias in ("selected issue", "pr number opened", "pr opened", "pr number", "selected pr"): val = fields_dict.get(alias) if val: @@ -643,8 +642,8 @@ def assess_controller_handoff(report_text, role=None): has_forbidden = any(term in val.lower() for term in ("equivalent", "related", "same", "/")) if len(numbers) != 1 or has_forbidden: field_name = "Selected issue/PR" - for name, aliases in list(HANDOFF_BASE_FIELDS) + list(HANDOFF_ROLE_FIELDS.get(role or "", ())): - if alias in aliases: + for name, aliases_list in required: + if alias in aliases_list: field_name = name break return { @@ -656,6 +655,18 @@ def assess_controller_handoff(report_text, role=None): ], } + if local_edits: + workspace_mutations_val = fields_dict.get("workspace mutations", "").strip().lower() + if not workspace_mutations_val or workspace_mutations_val == "none": + return { + "verdict": "incomplete", + "downgraded": True, + "missing_fields": ["Workspace mutations"], + "reasons": [ + "Workspace mutations cannot be 'none' or empty when local edits exist" + ], + } + return { "verdict": "complete", "downgraded": False, @@ -664,6 +675,7 @@ def assess_controller_handoff(report_text, role=None): } + # ── PR Inventory Trust Gate (Issue #194) ────────────────────────────────────── # # A reviewer agent may not convert an empty PR list response into a definitive diff --git a/tests/test_review_proofs.py b/tests/test_review_proofs.py index 9570d5d..b36c81b 100644 --- a/tests/test_review_proofs.py +++ b/tests/test_review_proofs.py @@ -710,6 +710,7 @@ class TestControllerHandoff(unittest.TestCase): "- Files changed: review_proofs.py", "- Validation: 700 passed, 6 skipped", "- Mutations: one PR opened", + "- Workspace mutations: none", "- Current status: PR open", "- Blockers: none", "- Next: review PR #999", @@ -830,6 +831,28 @@ class TestControllerHandoff(unittest.TestCase): self.assertIn("assess_controller_handoff", skill) self.assertIn("issue #182", skill) + def test_handoff_rejects_none_workspace_mutations_when_local_edits_exist(self): + # 1. Workspace mutations: none is rejected when local_edits is True + incomplete_eq = self.BASE_HANDOFF + "\n" + "\n".join([ + "- Selected issue: #196", + "- Claim/comment status: comment-claimed", + "- PR number opened: #203", + "- No review/merge: confirmed", + ]) + res = assess_controller_handoff(incomplete_eq, role="author", local_edits=True) + self.assertEqual(res["verdict"], "incomplete") + self.assertIn("Workspace mutations", res["missing_fields"]) + + # 2. Workspace mutations: edited files is allowed when local_edits is True + complete_eq = self.BASE_HANDOFF.replace("- Workspace mutations: none", "- Workspace mutations: edited review_proofs.py") + "\n" + "\n".join([ + "- Selected issue: #196", + "- Claim/comment status: comment-claimed", + "- PR number opened: #203", + "- No review/merge: confirmed", + ]) + res2 = assess_controller_handoff(complete_eq, role="author", local_edits=True) + self.assertEqual(res2["verdict"], "complete") + class TestPRInventoryTrustGate(unittest.TestCase): """Issue #194: unit tests for the PR inventory trust gate."""