From 46703eac3f7ed27761575080ed1831a005e5289b Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 04:46:48 -0400 Subject: [PATCH] feat: enforce proof wording for pagination and live claims (#330) Add assess_proof_wording to reject unsupported phrases such as inventory complete, live proof, same as master, and file edits none unless matching evidence is present. Wire into build_final_report and add doc-contract tests. Closes #330 --- review_proofs.py | 136 ++++++++++++++++++++++++++++++++++++ tests/test_proof_wording.py | 93 ++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 tests/test_proof_wording.py diff --git a/review_proofs.py b/review_proofs.py index be71582..a4eae3b 100644 --- a/review_proofs.py +++ b/review_proofs.py @@ -1174,6 +1174,11 @@ def build_final_report(checkout_proof, inventory, validation, contamination, ) empty_queue_report = assess_empty_queue_report(report_text) + proof_wording = ( + assess_proof_wording(report_text) + if report_text + else {"proven": True, "block": False, "reasons": [], "violations": []} + ) contamination_status = contamination.get("status", "unknown") checkout_proven = bool(checkout_proof.get("proven")) @@ -1303,6 +1308,11 @@ def build_final_report(checkout_proof, inventory, validation, contamination, "empty-queue report missing or failed trust-gate proof (#198)" ) downgrade_reasons.extend(empty_queue_report.get("reasons", [])) + if not proof_wording.get("proven"): + downgrade_reasons.append( + "unsupported proof wording in final report (#330)" + ) + downgrade_reasons.extend(proof_wording.get("reasons", [])) merge_allowed = ( identity_eligible @@ -1368,6 +1378,8 @@ def build_final_report(checkout_proof, inventory, validation, contamination, else True ), "empty_queue_trust_gate_status": empty_queue_report.get("status"), + "proof_wording_proven": bool(proof_wording.get("proven")), + "proof_wording_violations": list(proof_wording.get("violations") or []), } @@ -2816,3 +2828,127 @@ def assess_email_disclosure( for email in emails ], } + + +_PRIOR_PROOF_LABEL = re.compile( + r"prior (?:blocker|proof|request[- ]changes|feedback)|" + r"head sha unchanged since|prior blocker reused", + re.I, +) + +_PAGINATION_FINALITY_EVIDENCE = re.compile( + r"pagination_complete\s*:\s*true|inventory_complete\s*:\s*true|" + r"is_final_page\s*:\s*true|pages_fetched|has_more\s*:\s*false|" + r"no next page|final[- ]page|pr_inventory_trust_gate\.status|" + r"total_count\s*:|pagination.*(?:final|complete)", + re.I, +) + +_DEFAULT_PAGE_SIZE_ASSUMPTION = re.compile( + r"(?:less|fewer) than (?:the )?(?:default )?(?:gitea )?page[- ]?(?:size|limit)|" + r"default (?:gitea )?page[- ]?size|under (?:the )?50[- ]?(?:item )?limit|" + r"(?:complete|exhaustive).*(?:default )?page[- ]?size|" + r"page[- ]?size assumption", + re.I, +) + +_PROOF_WORDING_RULES: tuple[dict, ...] = ( + { + "id": "live_proof", + "pattern": re.compile(r"\blive (?:blocker )?proof\b", re.I), + "session_keys": ("live_session_proof", "session_proof_commands"), + "text_evidence": re.compile( + r"current[- ]session|ran in (?:the )?current session|" + r"proof (?:command|tool).*(?:current|this) session|" + r"revalidated in (?:the )?current session", + re.I, + ), + "allow_prior_label": True, + }, + { + "id": "inventory_complete", + "pattern": re.compile( + r"\binventory (?:is )?complete\b|\binventory completeness\b", + re.I, + ), + "session_keys": ("pagination_complete", "inventory_complete"), + "text_evidence": _PAGINATION_FINALITY_EVIDENCE, + "allow_prior_label": False, + }, + { + "id": "same_as_master", + "pattern": re.compile(r"\bsame as master\b", re.I), + "session_keys": ("baseline_worktree_proof", "clean_baseline_proof"), + "text_evidence": re.compile( + r"baseline worktree|clean baseline|diff base.*master|" + r"base[- ]equivalent.*master|worktree proof", + re.I, + ), + "allow_prior_label": False, + }, + { + "id": "no_file_edits", + "pattern": re.compile( + r"\b(?:no file edits|file edits none|workspace mutations none)\b", + re.I, + ), + "session_keys": ("no_file_edits_proven", "mutation_ledger_clean"), + "text_evidence": re.compile( + r"mutation ledger|worktree mutations:\s*none|" + r"file edits:\s*none|tracked file edits:\s*none|" + r"no tracked (?:file )?edits", + re.I, + ), + "allow_prior_label": False, + }, +) + + +def assess_proof_wording( + report_text: str | None, + *, + session_evidence: dict | None = None, +) -> dict: + """Issue #330: reject unsupported proof phrases in final reports. + + Forbidden wording such as ``inventory complete``, ``live proof``, and + ``same as master`` is allowed only when matching current-session evidence + appears in the report text or in *session_evidence*. + """ + text = report_text or "" + evidence = dict(session_evidence or {}) + violations: list[str] = [] + flagged_rules: list[str] = [] + + if _DEFAULT_PAGE_SIZE_ASSUMPTION.search(text): + if not _PAGINATION_FINALITY_EVIDENCE.search(text) and not ( + evidence.get("pagination_complete") or evidence.get("inventory_complete") + ): + violations.append( + "inventory pagination assumed from default page size without " + "final-page/no-next-page/total-count/traversal proof" + ) + flagged_rules.append("default_page_size_assumption") + + for rule in _PROOF_WORDING_RULES: + if not rule["pattern"].search(text): + continue + if rule.get("allow_prior_label") and _PRIOR_PROOF_LABEL.search(text): + continue + if any(evidence.get(key) for key in rule.get("session_keys") or ()): + continue + if rule["text_evidence"].search(text): + continue + violations.append( + f"forbidden proof phrase ({rule['id']}) without matching evidence" + ) + flagged_rules.append(rule["id"]) + + proven = not violations + return { + "proven": proven, + "block": not proven, + "violations": violations, + "flagged_rules": flagged_rules, + "reasons": violations, + } diff --git a/tests/test_proof_wording.py b/tests/test_proof_wording.py new file mode 100644 index 0000000..e59a99d --- /dev/null +++ b/tests/test_proof_wording.py @@ -0,0 +1,93 @@ +"""Doc-contract checks for proof wording enforcement (#330).""" +import unittest + +from review_proofs import assess_proof_wording, build_final_report + + +class TestProofWording(unittest.TestCase): + def test_rejects_inventory_complete_without_pagination_proof(self): + result = assess_proof_wording( + "Open PR inventory is complete because only 3 PRs were returned." + ) + self.assertFalse(result["proven"]) + self.assertTrue(result["block"]) + + def test_rejects_default_page_size_assumption(self): + result = assess_proof_wording( + "Inventory exhaustive: fewer than the default Gitea page size returned." + ) + self.assertFalse(result["proven"]) + self.assertIn("default_page_size_assumption", result["flagged_rules"]) + + def test_allows_inventory_complete_with_finality_metadata(self): + result = assess_proof_wording( + "Inventory complete. pagination_complete: true, is_final_page: true" + ) + self.assertTrue(result["proven"]) + + def test_allows_inventory_complete_with_session_evidence(self): + result = assess_proof_wording( + "Queue inventory complete.", + session_evidence={"pagination_complete": True}, + ) + self.assertTrue(result["proven"]) + + def test_rejects_live_proof_without_session_evidence(self): + result = assess_proof_wording("Live proof confirms mergeable state.") + self.assertFalse(result["proven"]) + self.assertIn("live_proof", result["flagged_rules"]) + + def test_allows_prior_blocker_reuse_wording(self): + result = assess_proof_wording( + "Prior blocker reused; head SHA unchanged since blocking review. " + "Live proof not claimed for this skip." + ) + self.assertTrue(result["proven"]) + + def test_allows_live_proof_with_current_session_evidence(self): + result = assess_proof_wording( + "Live proof: gitea_view_pr revalidated in the current session." + ) + self.assertTrue(result["proven"]) + + def test_rejects_same_as_master_without_baseline_proof(self): + result = assess_proof_wording("Diff is same as master.") + self.assertFalse(result["proven"]) + self.assertIn("same_as_master", result["flagged_rules"]) + + def test_allows_same_as_master_with_baseline_wording(self): + result = assess_proof_wording( + "Clean baseline worktree proof: diff base matches master." + ) + self.assertTrue(result["proven"]) + + def test_rejects_file_edits_none_without_ledger(self): + result = assess_proof_wording("Workspace mutations: file edits none.") + self.assertFalse(result["proven"]) + self.assertIn("no_file_edits", result["flagged_rules"]) + + def test_allows_file_edits_none_with_mutation_ledger(self): + result = assess_proof_wording( + "Mutation ledger: tracked file edits: none", + session_evidence={"mutation_ledger_clean": True}, + ) + self.assertTrue(result["proven"]) + + def test_build_final_report_downgrades_on_proof_wording_violation(self): + report = build_final_report( + {"proven": True}, + {"complete": True}, + {"claimable": True, "verdict": "strong"}, + {"status": "clean"}, + True, + False, + True, + report_text="Inventory complete after listing 4 open PRs.", + ) + self.assertEqual(report["grade"], "downgraded") + self.assertFalse(report["proof_wording_proven"]) + self.assertTrue(report["proof_wording_violations"]) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file