"""Canonical post-merge (moot) validation outcome and wording gate (#529). When a PR is already merged or closed *before* a reviewer submits their verdict, an ordinary "approve" is misleading: the review never gated the merge, so a normal active approval overstates controller confidence. The sanctioned outcome for that case is a **post-merge moot validation** — the reviewer records what validation found, but classifies it as moot rather than an active approval. This module defines the four canonical validation distinctions #529 requires and enforces the post-merge case: - ``CLEAN_PASS_OUTCOME`` — clean validation pass on an open, reviewable PR. - ``BASELINE_ACCEPTED_OUTCOME`` — validation pass with a baseline-proven unrelated failure (proof handled by :mod:`premerge_baseline_proof`). - ``BLOCKED_OUTCOME`` — validation blocked by an unresolved failure. - ``POST_MERGE_MOOT_OUTCOME`` — the PR was already merged/closed before review submission; validation is recorded as post-merge moot, not an active approval. The gate blocks two mistakes: 1. Claiming an *active approval* on a PR that was already merged/closed before review submission (must be recorded as post-merge moot validation instead). 2. Claiming post-merge moot validation without proof the PR is actually merged/closed (a merged-state field plus a merge commit SHA). Each outcome maps to a durable ``validation:*`` process-state label so PRs and issues carry the distinction (#529 acceptance criterion). """ from __future__ import annotations import re from typing import Any CLEAN_PASS_OUTCOME = "clean validation pass" BASELINE_ACCEPTED_OUTCOME = "validation pass with baseline-proven unrelated failure" BLOCKED_OUTCOME = "validation blocked by unresolved failure" POST_MERGE_MOOT_OUTCOME = "post-merge moot validation" # Canonical validation status label a reviewer writes in the report body for the # post-merge case; kept in sync with validation_status_vocabulary. POST_MERGE_MOOT_STATUS = "post-merge moot validation" # Durable process-state labels (registered in issue_workflow_labels). LABEL_CLEAN_PASS = "validation:clean-pass" LABEL_BASELINE_ACCEPTED = "validation:baseline-accepted" LABEL_BLOCKED = "validation:blocked" LABEL_POST_MERGE_MOOT = "validation:post-merge-moot" _OUTCOME_TO_LABEL: dict[str, str] = { CLEAN_PASS_OUTCOME: LABEL_CLEAN_PASS, BASELINE_ACCEPTED_OUTCOME: LABEL_BASELINE_ACCEPTED, BLOCKED_OUTCOME: LABEL_BLOCKED, POST_MERGE_MOOT_OUTCOME: LABEL_POST_MERGE_MOOT, } # The PR was already merged/closed before the review was submitted. _MERGED_BEFORE_REVIEW_RE = re.compile( r"(?:already[- ]merged" r"|merged\s+before\s+review" r"|closed\s+before\s+review" r"|pr\s+(?:is|was)\s+(?:already\s+)?(?:merged|closed)" r"|pr\s+state\s*[:=]\s*(?:merged|closed)" r"|merged\s*/\s*closed)", re.IGNORECASE, ) # An active approval verdict is being claimed. _ACTIVE_APPROVAL_RE = re.compile( r"(?:review\s+decision\s*[:=]\s*approve" r"|submitted\s+['\"]?approve['\"]?" r"|active\s+approval" r"|approving\s+the\s+pr" r"|posting\s+an?\s+approval)", re.IGNORECASE, ) # The sanctioned post-merge moot validation wording. _POST_MERGE_MOOT_RE = re.compile( r"post[- ]merge\s+(?:moot\s+)?validation" r"|post[- ]merge\s+moot" r"|moot\s+validation", re.IGNORECASE, ) # Proof the PR is genuinely merged/closed. _MERGED_STATE_PROOF_RE = re.compile( r"(?:pr\s+state\s*[:=]\s*(?:merged|closed)" r"|merged_at\s*[:=]\s*\S" r"|closed_at\s*[:=]\s*\S" r"|state\s*[:=]\s*(?:merged|closed))", re.IGNORECASE, ) _MERGE_COMMIT_SHA_RE = re.compile( r"merge\s+commit(?:\s+sha)?\s*[:=]\s*[0-9a-f]{7,40}", re.IGNORECASE, ) POST_MERGE_VALIDATION_TEMPLATE = ( "Validation status: post-merge moot validation\n" "PR state: merged\n" "Merge commit sha: <40-hex merge commit>\n" "Validation finding: \n" "Note: PR merged/closed before review submission; recorded as post-merge " "moot validation, not an active approval." ) def process_state_label(outcome: str) -> str | None: """Return the durable ``validation:*`` label for a canonical outcome.""" return _OUTCOME_TO_LABEL.get(outcome) def assess_post_merge_validation( report_text: str, *, pr_merged_or_closed: bool | None = None, ) -> dict[str, Any]: """Assess post-merge moot validation wording and proof (#529). Args: report_text: The reviewer final report text. pr_merged_or_closed: Optional structured signal that the PR is already merged/closed. When ``True`` it forces the merged-before-review path even if the report text omits the phrasing; proof fields are still required in the report body for durability. Returns a dict with ``proven``, ``block``, ``outcome``, ``process_state_label``, ``reasons``, ``skipped`` and ``safe_next_action``. Only blocks when the report either claims an active approval on a merged/closed PR, or claims post-merge moot validation without proof. """ text = report_text or "" merged_signal = bool(_MERGED_BEFORE_REVIEW_RE.search(text)) or bool( pr_merged_or_closed ) active_approval = bool(_ACTIVE_APPROVAL_RE.search(text)) moot_wording = bool(_POST_MERGE_MOOT_RE.search(text)) # Nothing about merged-state or moot validation — not applicable here. if not merged_signal and not moot_wording: return { "proven": True, "block": False, "outcome": None, "process_state_label": None, "reasons": [], "skipped": True, "safe_next_action": "", } # An active approval on a PR already merged/closed before review, without # the sanctioned moot wording, overstates confidence. if merged_signal and active_approval and not moot_wording: return { "proven": False, "block": True, "outcome": POST_MERGE_MOOT_OUTCOME, "process_state_label": LABEL_POST_MERGE_MOOT, "reasons": [ "PR was already merged/closed before review submission; an " "active approval overstates confidence — record 'post-merge " "moot validation' instead of a normal approval" ], "skipped": False, "safe_next_action": ( "classify the outcome as post-merge moot validation (not an " "active approval); cite PR state merged/closed and the merge " "commit SHA. Template:\n" + POST_MERGE_VALIDATION_TEMPLATE ), } # Post-merge moot validation claimed — require merged-state + commit proof. if moot_wording: reasons: list[str] = [] if not _MERGED_STATE_PROOF_RE.search(text): reasons.append( "post-merge moot validation claimed without merged/closed state " "proof (state: merged/closed, or merged_at/closed_at)" ) if not _MERGE_COMMIT_SHA_RE.search(text): reasons.append( "post-merge moot validation claimed without a merge commit SHA" ) if reasons: return { "proven": False, "block": True, "outcome": POST_MERGE_MOOT_OUTCOME, "process_state_label": LABEL_POST_MERGE_MOOT, "reasons": reasons, "skipped": False, "safe_next_action": ( "prove the PR is merged/closed: cite PR state and the merge " "commit SHA. Template:\n" + POST_MERGE_VALIDATION_TEMPLATE ), } return { "proven": True, "block": False, "outcome": POST_MERGE_MOOT_OUTCOME, "process_state_label": LABEL_POST_MERGE_MOOT, "reasons": [], "skipped": False, "safe_next_action": "", } # Merged signal present but no approval claim and no moot wording yet — # guide toward the moot outcome without blocking. return { "proven": True, "block": False, "outcome": POST_MERGE_MOOT_OUTCOME, "process_state_label": LABEL_POST_MERGE_MOOT, "reasons": [], "skipped": False, "safe_next_action": ( "PR appears merged/closed; if submitting a verdict, record " "post-merge moot validation rather than an active approval" ), }