Merge pull request 'feat: enforce reconciler PR-close proof fields in final reports (Closes #306)' (#423) from feat/issue-306-reconciler-close-proof into master

This commit was merged in pull request #423.
This commit is contained in:
2026-07-08 21:32:32 -05:00
3 changed files with 159 additions and 0 deletions
+66
View File
@@ -961,6 +961,69 @@ def _rule_reconcile_linked_issue_live(
return []
_PR_CLOSE_NEGATIVE = frozenset({"", "none", "n/a", "na", "0", "no", "not closed", "not performed"})
_ANCESTOR_AFFIRMATIVE_RE = re.compile(
r"ancestor|passed|true|verified|confirmed", re.IGNORECASE
)
def _reconciler_pr_close_performed(fields: dict[str, str], lock: dict) -> bool:
"""True when the report/session indicates a reconciler PR close happened."""
if lock.get("pr_closed") is True:
return True
value = (fields.get("prs closed", "") or "").strip().lower()
if value in _PR_CLOSE_NEGATIVE:
return False
# A closed PR is reported by number (e.g. "#99") or an affirmative result.
return bool(re.search(r"#\s*\d+|\b(?:closed|success|done)\b", value))
def _rule_reconcile_close_proof(
report_text: str,
*,
reconciler_close_lock: dict | None = None,
) -> list[dict[str, str]]:
"""#306: a reconciler PR close must carry exact proof fields.
Read-only/comment-only reconciliations are untouched. Once a PR close is
reported (via the ``PRs closed`` field or a session close lock), the
handoff must prove the close capability, ancestor landing, PR close
result, and the linked-issue result — narrative alone fails closed.
"""
fields = _handoff_fields(report_text)
lock = reconciler_close_lock or {}
if not _reconciler_pr_close_performed(fields, lock):
return []
missing: list[str] = []
capabilities = fields.get("capabilities proven", "")
if "gitea.pr.close" not in capabilities.lower():
missing.append("close capability proof (gitea.pr.close)")
ancestor = fields.get("ancestor proof", "")
if not _ANCESTOR_AFFIRMATIVE_RE.search(ancestor):
missing.append("ancestor proof")
prs_closed = (fields.get("prs closed", "") or "").strip().lower()
if prs_closed in _PR_CLOSE_NEGATIVE and lock.get("pr_closed") is True:
missing.append("PR close result")
linked = fields.get("linked issue live status", "") or fields.get("issues closed", "")
if not linked.strip():
missing.append("linked issue result")
if not missing:
return []
return [
validator_finding(
"reconcile.close_proof_fields",
"block",
"Reconciler close proof",
"reconciler PR close reported without required proof field(s): "
+ ", ".join(missing),
"include close capability proof, ancestor proof, PR close result, "
"and linked issue result in the handoff",
)
]
def _rule_reconcile_pagination_proof(report_text: str) -> list[dict[str, str]]:
text = report_text or ""
if not _INVENTORY_COMPLETE_RE.search(text):
@@ -1253,6 +1316,7 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
_rule_reconcile_stale_author_fields,
_rule_reconcile_eligible_reviewed,
_rule_reconcile_linked_issue_live,
_rule_reconcile_close_proof,
_rule_reconcile_pagination_proof,
_rule_reviewer_git_fetch_readonly,
_rule_reviewer_legacy_workspace_mutations,
@@ -1352,6 +1416,7 @@ def assess_final_report_validator(
issue_filing_lock: dict | None = None,
session_pr_opened: bool = False,
validation_session: dict | None = None,
reconciler_close_lock: dict | None = None,
) -> dict[str, Any]:
"""Validate final-report text against task-specific proof rules (#327).
@@ -1408,6 +1473,7 @@ def assess_final_report_validator(
"local_edits": local_edits,
"session_pr_opened": session_pr_opened,
"validation_session": validation_session,
"reconciler_close_lock": reconciler_close_lock,
}
for rule in _RULES_BY_TASK.get(normalized_kind, ()):