feat: comment-then-stop policy for partial PR reconciliation (Closes #302)
Already-landed PR reconciliation could prove a PR should be closed but stop silently when gitea.pr.close was unavailable, even with comment capability present. Adopt Option B (comment-then-stop) as the durable policy and enforce it: - resolve_partial_reconciliation_plan maps proven capabilities to one of four outcomes: FULL_RECONCILE_CLOSE_ALLOWED (close_pr proven), PARTIAL_RECONCILE_COMMENT_THEN_STOP (comment_pr proven, close_pr missing; one reconciliation comment with PR head SHA, target branch SHA, ancestor proof, linked issue status, and the missing capability, then stop), RECOVERY_HANDOFF_ONLY (no comment capability; no Gitea mutation), and GATE_NOT_PROVEN (no ancestry proof; no mutation regardless of capability). Missing capability dicts fail closed. - assess_partial_reconciliation_report requires final reports to explain why the comment was or was not posted, name the missing capability, and state the close result or that no mutation ran. - workflows/reconcile-landed-pr.md gains section 12A documenting the policy; the workflow contract test locks the new markers. Tests cover close capability present, close missing with comment allowed, comment capability missing, all capabilities missing, unproven ancestry, fail-closed capability dict, and report checks for each outcome. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -3581,6 +3581,166 @@ def assess_reviewer_baseline_validation_proof(
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Partial reconciliation policy (#302)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Durable policy choice for already-landed PR reconciliation when
|
||||
# ``gitea.pr.close`` is unavailable: Option B, comment-then-stop.
|
||||
PARTIAL_RECONCILIATION_POLICY = "comment_then_stop"
|
||||
|
||||
PARTIAL_RECONCILIATION_COMMENT_FIELDS = (
|
||||
"PR head SHA",
|
||||
"target branch SHA",
|
||||
"ancestor proof",
|
||||
"linked issue status",
|
||||
"required missing capability",
|
||||
)
|
||||
|
||||
|
||||
def resolve_partial_reconciliation_plan(
|
||||
*,
|
||||
ancestor_proven: bool,
|
||||
capabilities: dict | None,
|
||||
) -> dict:
|
||||
"""#302: decide reconciliation mutations from proven capabilities.
|
||||
|
||||
Implements the comment-then-stop policy (Option B): with ancestor
|
||||
proof but no ``close_pr`` capability, a reconciliation comment with
|
||||
the full proof is posted when ``comment_pr`` is proven, then the
|
||||
workflow stops for an authorized close. Missing comment capability
|
||||
degrades to a recovery handoff with no Gitea mutation. Unproven
|
||||
ancestry blocks every mutation regardless of capability.
|
||||
|
||||
*capabilities* maps ``close_pr``/``comment_pr``/``close_issue``/
|
||||
``comment_issue`` to proven booleans; ``None`` or missing keys fail
|
||||
closed.
|
||||
"""
|
||||
caps = capabilities or {}
|
||||
if not ancestor_proven:
|
||||
return {
|
||||
"policy": PARTIAL_RECONCILIATION_POLICY,
|
||||
"outcome": "GATE_NOT_PROVEN",
|
||||
"allowed_mutations": (),
|
||||
"required_comment_fields": (),
|
||||
"report_requirements": (
|
||||
"state that ancestry was not proven and no Gitea mutation "
|
||||
"was performed",
|
||||
),
|
||||
"reasons": [
|
||||
"ancestor proof missing; no reconciliation mutation may "
|
||||
"run (#302)"
|
||||
],
|
||||
"safe_next_action": (
|
||||
"run the already-landed ancestry check before any "
|
||||
"reconciliation mutation"
|
||||
),
|
||||
}
|
||||
|
||||
if caps.get("close_pr") is True:
|
||||
return {
|
||||
"policy": PARTIAL_RECONCILIATION_POLICY,
|
||||
"outcome": "FULL_RECONCILE_CLOSE_ALLOWED",
|
||||
"allowed_mutations": ("close_pr", "comment_pr"),
|
||||
"required_comment_fields": (),
|
||||
"report_requirements": (
|
||||
"report the PR close result",
|
||||
),
|
||||
"reasons": [],
|
||||
"safe_next_action": (
|
||||
"close the already-landed PR with the proven close_pr "
|
||||
"capability and report the close result"
|
||||
),
|
||||
}
|
||||
|
||||
if caps.get("comment_pr") is True:
|
||||
return {
|
||||
"policy": PARTIAL_RECONCILIATION_POLICY,
|
||||
"outcome": "PARTIAL_RECONCILE_COMMENT_THEN_STOP",
|
||||
"allowed_mutations": ("comment_pr",),
|
||||
"required_comment_fields": PARTIAL_RECONCILIATION_COMMENT_FIELDS,
|
||||
"report_requirements": (
|
||||
"report that the reconciliation comment was posted",
|
||||
"report the missing close capability that prevented the "
|
||||
"PR close",
|
||||
),
|
||||
"reasons": [
|
||||
"close_pr capability missing; policy is comment-then-stop "
|
||||
"(#302)"
|
||||
],
|
||||
"safe_next_action": (
|
||||
"post one reconciliation comment carrying the ancestor "
|
||||
"proof, then stop for a human or authorized close"
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"policy": PARTIAL_RECONCILIATION_POLICY,
|
||||
"outcome": "RECOVERY_HANDOFF_ONLY",
|
||||
"allowed_mutations": (),
|
||||
"required_comment_fields": (),
|
||||
"report_requirements": (
|
||||
"report that no reconciliation comment was posted and name "
|
||||
"the missing capability",
|
||||
"report that no Gitea mutation was performed",
|
||||
),
|
||||
"reasons": [
|
||||
"close_pr and comment_pr capabilities missing; recovery "
|
||||
"handoff only (#302)"
|
||||
],
|
||||
"safe_next_action": (
|
||||
"produce a recovery handoff recording the intended comment "
|
||||
"and required capabilities; perform no Gitea mutation"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def assess_partial_reconciliation_report(
|
||||
report_text: str | None,
|
||||
*,
|
||||
plan: dict,
|
||||
) -> dict:
|
||||
"""#302: final reports must explain why comments were or were not posted."""
|
||||
text = (report_text or "").lower()
|
||||
outcome = (plan or {}).get("outcome", "")
|
||||
reasons: list[str] = []
|
||||
|
||||
if outcome == "FULL_RECONCILE_CLOSE_ALLOWED":
|
||||
if "close" not in text or "result" not in text:
|
||||
reasons.append(
|
||||
"full reconciliation report must state the PR close "
|
||||
"result (#302)"
|
||||
)
|
||||
elif outcome == "PARTIAL_RECONCILE_COMMENT_THEN_STOP":
|
||||
if "comment" not in text:
|
||||
reasons.append(
|
||||
"partial reconciliation report must state that the "
|
||||
"reconciliation comment was posted (#302)"
|
||||
)
|
||||
if "capability" not in text and "close_pr" not in text:
|
||||
reasons.append(
|
||||
"partial reconciliation report must name the missing "
|
||||
"close capability (#302)"
|
||||
)
|
||||
elif outcome == "RECOVERY_HANDOFF_ONLY":
|
||||
if "comment" not in text or "capability" not in text:
|
||||
reasons.append(
|
||||
"recovery handoff report must explain that no comment was "
|
||||
"posted and name the missing capability (#302)"
|
||||
)
|
||||
if "no gitea mutation" not in text and "no mutation" not in text:
|
||||
reasons.append(
|
||||
"recovery handoff report must state that no Gitea "
|
||||
"mutation was performed (#302)"
|
||||
)
|
||||
|
||||
return {
|
||||
"complete": not reasons,
|
||||
"block": bool(reasons),
|
||||
"reasons": reasons,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Identity disclosure (#305)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user