fix(workflow): cross-profile decision-lock cleanup and irrecoverable provenance (#709)
Prevent merger-local empty decision locks from standing in for reviewer terminal cleanup, refuse silent re-init overwrite of unresolved terminal evidence, record post-merge recovery-required state when audit fails, and add a truthful irrecoverable-provenance path that never claims applied=true. Closes #709
This commit is contained in:
@@ -438,3 +438,216 @@ def format_cleanup_audit_comment(audit: dict[str, Any]) -> str:
|
||||
"This path only clears a lock when the referenced PR is merged/closed.",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #709 cross-profile cleanup, overwrite protection, recovery provenance
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def has_unresolved_terminal_evidence(lock: dict | None) -> bool:
|
||||
"""True when *lock* still carries a terminal live mutation ledger."""
|
||||
return last_terminal_mutation(lock) is not None
|
||||
|
||||
|
||||
def assess_init_overwrite(
|
||||
existing_lock: dict | None,
|
||||
*,
|
||||
force: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Whether init_review_decision_lock may replace an existing durable lock (#709 AC2).
|
||||
|
||||
Unresolved terminal evidence must never be silently replaced by an empty
|
||||
initialized lock. *force* does not authorize destruction of terminal
|
||||
ledgers — only explicit cleanup / archive transitions may remove them.
|
||||
"""
|
||||
result: dict[str, Any] = {
|
||||
"overwrite_allowed": True,
|
||||
"has_existing": existing_lock is not None,
|
||||
"has_unresolved_terminal": False,
|
||||
"reasons": [],
|
||||
"last_terminal_pr": None,
|
||||
"last_terminal_action": None,
|
||||
"existing_profile_identity": None,
|
||||
}
|
||||
if existing_lock is None:
|
||||
result["reasons"].append("no existing lock; empty init allowed")
|
||||
return result
|
||||
result["existing_profile_identity"] = (
|
||||
existing_lock.get("profile_identity")
|
||||
or existing_lock.get("session_profile_lock")
|
||||
or existing_lock.get("session_profile")
|
||||
)
|
||||
last = last_terminal_mutation(existing_lock)
|
||||
if last is None:
|
||||
result["reasons"].append(
|
||||
"existing lock has no terminal mutations; re-init allowed"
|
||||
)
|
||||
return result
|
||||
result["has_unresolved_terminal"] = True
|
||||
result["overwrite_allowed"] = False
|
||||
result["last_terminal_pr"] = last.get("pr_number")
|
||||
result["last_terminal_action"] = last.get("action")
|
||||
result["reasons"].append(
|
||||
"refuse empty re-init: unresolved terminal decision-lock evidence "
|
||||
f"present ({last.get('action')} on PR #{last.get('pr_number')}); "
|
||||
"use gitea_cleanup_stale_review_decision_lock when moot, or archive "
|
||||
f"via sanctioned recovery — force={force!r} does not authorize overwrite "
|
||||
"(#709 AC2)"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def lock_targets_merged_pr_approval(
|
||||
lock: dict | None,
|
||||
*,
|
||||
pr_number: int,
|
||||
expected_head_sha: str | None = None,
|
||||
) -> bool:
|
||||
"""True when *lock*'s last terminal mutation is approve of *pr_number*."""
|
||||
last = last_terminal_mutation(lock)
|
||||
if last is None:
|
||||
return False
|
||||
if last.get("action") != "approve":
|
||||
return False
|
||||
if last.get("pr_number") != pr_number:
|
||||
return False
|
||||
if expected_head_sha:
|
||||
locked = mutation_head_sha(last, lock)
|
||||
if locked and not heads_equal(locked, expected_head_sha):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def build_post_merge_recovery_record(
|
||||
*,
|
||||
pr_number: int,
|
||||
head_sha: str | None,
|
||||
merge_commit_sha: str | None,
|
||||
target_profile_identity: str | None,
|
||||
failed_step: str,
|
||||
error: str | None,
|
||||
remote: str | None,
|
||||
org: str | None,
|
||||
repo: str | None,
|
||||
actor_username: str | None,
|
||||
profile_name: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Durable recovery-required payload after irreversible merge (#709 AC3)."""
|
||||
return {
|
||||
"event": "post_merge_decision_lock_recovery_required",
|
||||
"status": "recovery_required",
|
||||
"issue_ref": "#709",
|
||||
"recovery_critical": True,
|
||||
"applied": False, # never claim historical cleanup succeeded
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"pr_number": pr_number,
|
||||
"head_sha": normalize_head_sha(head_sha),
|
||||
"merge_commit_sha": merge_commit_sha,
|
||||
"target_profile_identity": target_profile_identity,
|
||||
"failed_step": failed_step,
|
||||
"error": error,
|
||||
"remote": remote,
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"actor_username": actor_username,
|
||||
"profile_name": profile_name,
|
||||
"required_recovery_action": (
|
||||
"retry cross-profile decision-lock cleanup and audit publication "
|
||||
"for the merged PR; if terminal evidence is gone, use "
|
||||
"gitea_record_irrecoverable_decision_lock_provenance"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_irrecoverable_provenance_record(
|
||||
*,
|
||||
pr_number: int,
|
||||
head_sha: str | None,
|
||||
remote: str | None,
|
||||
org: str | None,
|
||||
repo: str | None,
|
||||
actor_username: str | None,
|
||||
profile_name: str | None,
|
||||
reason: str,
|
||||
incident_ref: str | None,
|
||||
operator_authorized: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""Truthful absence-of-proof record (#709 AC5). Never sets applied=True."""
|
||||
return {
|
||||
"event": "irrecoverable_decision_lock_provenance",
|
||||
"status": "provenance_irrecoverable",
|
||||
"operator_recovery_required": True,
|
||||
"issue_ref": "#709",
|
||||
"recovery_critical": True,
|
||||
"applied": False,
|
||||
"historical_cleanup_proven": False,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"pr_number": pr_number,
|
||||
"head_sha": normalize_head_sha(head_sha),
|
||||
"remote": remote,
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"actor_username": actor_username,
|
||||
"profile_name": profile_name,
|
||||
"reason": reason,
|
||||
"incident_ref": incident_ref,
|
||||
"operator_authorized": bool(operator_authorized),
|
||||
"merger_may_accept": bool(operator_authorized),
|
||||
"acceptance_rule": (
|
||||
"Merger may accept this record only when operator_authorized=true, "
|
||||
"repository/PR/head match the live target, the record is durable "
|
||||
"and read back, and no conflicting terminal lock remains for a "
|
||||
"different PR/head. This does not prove historical cleanup."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def format_irrecoverable_audit_comment(record: dict[str, Any]) -> str:
|
||||
"""Markdown body for irrecoverable provenance audit (no applied=true claim)."""
|
||||
lines = [
|
||||
"## Irrecoverable decision-lock provenance (#709)",
|
||||
"",
|
||||
"Status: **PROVENANCE_IRRECOVERABLE** (not applied cleanup)",
|
||||
"",
|
||||
f"- actor: `{record.get('actor_username')}`",
|
||||
f"- profile: `{record.get('profile_name')}`",
|
||||
f"- timestamp: `{record.get('timestamp')}`",
|
||||
f"- PR: `#{record.get('pr_number')}`",
|
||||
f"- head_sha: `{record.get('head_sha')}`",
|
||||
f"- incident_ref: `{record.get('incident_ref')}`",
|
||||
f"- operator_authorized: `{record.get('operator_authorized')}`",
|
||||
f"- historical_cleanup_proven: `{record.get('historical_cleanup_proven')}`",
|
||||
f"- applied: `{record.get('applied')}` (must remain false)",
|
||||
"",
|
||||
f"Reason: {record.get('reason')}",
|
||||
"",
|
||||
"This record documents **absence of proof**, not successful cleanup.",
|
||||
"It must not be reused for a different PR or head (#709 AC6).",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_post_merge_recovery_comment(record: dict[str, Any]) -> str:
|
||||
"""Markdown body for post-merge recovery-required audit."""
|
||||
lines = [
|
||||
"## Post-merge decision-lock recovery required (#709)",
|
||||
"",
|
||||
"Status: **RECOVERY_REQUIRED** (merge is irreversible; cleanup/audit incomplete)",
|
||||
"",
|
||||
f"- actor: `{record.get('actor_username')}`",
|
||||
f"- profile: `{record.get('profile_name')}`",
|
||||
f"- timestamp: `{record.get('timestamp')}`",
|
||||
f"- PR: `#{record.get('pr_number')}`",
|
||||
f"- head_sha: `{record.get('head_sha')}`",
|
||||
f"- merge_commit_sha: `{record.get('merge_commit_sha')}`",
|
||||
f"- target_profile_identity: `{record.get('target_profile_identity')}`",
|
||||
f"- failed_step: `{record.get('failed_step')}`",
|
||||
f"- error: `{record.get('error')}`",
|
||||
"",
|
||||
f"Required action: {record.get('required_recovery_action')}",
|
||||
"",
|
||||
"applied=false — do not treat this as successful cleanup evidence.",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user