Adds #529 acceptance criteria 1/4/5. - post_merge_validation.py: assess_post_merge_validation defines the four canonical validation distinctions (clean pass / baseline-accepted / blocked / post-merge moot) and fails closed on two mistakes: (a) an active approval on a PR already merged/closed before review submission, and (b) post-merge moot validation claimed without merged-state + merge-commit proof. - issue_workflow_labels.py: durable validation:* process-state labels (clean-pass / baseline-accepted / blocked / post-merge-moot) registered in CANONICAL_LABEL_SPECS so manage_labels creates them. - final_report_validator.py: _rule_reviewer_post_merge_validation wired into the review_pr rule set. - tests/test_post_merge_validation.py: module behavior, label registration, and the wired review rule. Refs #529 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
223 lines
8.4 KiB
Python
223 lines
8.4 KiB
Python
"""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: <what the validation run showed, for the record>\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"
|
|
),
|
|
}
|