fix(pr-694): resolve master merge conflicts for #693 decision-lock isolation
Preserve #693 classification/correction helpers and integrate master #709 recovery provenance APIs; keep both capability-map entry sets.
This commit is contained in:
@@ -882,3 +882,252 @@ def assess_open_pr_decision_lock_recovery(
|
||||
"manual_file_delete_forbidden": True,
|
||||
"correction_as_generic_unlock_forbidden": True,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #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*.
|
||||
|
||||
When *expected_head_sha* is provided, a **recorded** terminal head must
|
||||
match. Legacy same-repo approve locks with no recorded head do **not**
|
||||
match (fail closed, #709 F3 residual / review 435) — callers must use the
|
||||
strict secondary path that refuses no-head clears.
|
||||
"""
|
||||
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:
|
||||
want = normalize_head_sha(expected_head_sha)
|
||||
if not want:
|
||||
return False
|
||||
locked = mutation_head_sha(last, lock)
|
||||
# Require recorded-head match; unrecorded head is not a match (#709 F3).
|
||||
if not locked or not heads_equal(locked, want):
|
||||
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 = None,
|
||||
# Deprecated kwargs retained only so stale call sites fail closed:
|
||||
operator_authorized: bool | None = None,
|
||||
# Required for merger-acceptable records (#709 F1):
|
||||
authorization: dict[str, Any] | None = None,
|
||||
incident_issue: int | None = None,
|
||||
incident_comment_id: int | None = None,
|
||||
destroyed_subject: str | None = None,
|
||||
historical_provenance_subject: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Truthful absence-of-proof record (#709 AC5). Never sets applied=True.
|
||||
|
||||
Caller-supplied ``operator_authorized`` is **ignored** as authorization
|
||||
evidence (review 434 F1). Prefer
|
||||
:func:`irrecoverable_provenance.build_irrecoverable_provenance_record`
|
||||
with a verified server-side authorization artifact.
|
||||
"""
|
||||
# Explicitly ignore deprecated self-assertable Boolean.
|
||||
_ = operator_authorized
|
||||
if authorization is not None and incident_issue is not None and incident_comment_id is not None:
|
||||
from irrecoverable_provenance import (
|
||||
build_irrecoverable_provenance_record as _build,
|
||||
)
|
||||
|
||||
return _build(
|
||||
pr_number=int(pr_number),
|
||||
head_sha=str(head_sha or ""),
|
||||
remote=str(remote or ""),
|
||||
org=str(org or ""),
|
||||
repo=str(repo or ""),
|
||||
actor_username=actor_username,
|
||||
profile_name=profile_name,
|
||||
reason=reason,
|
||||
incident_issue=int(incident_issue),
|
||||
incident_comment_id=int(incident_comment_id),
|
||||
authorization=authorization,
|
||||
destroyed_subject=destroyed_subject,
|
||||
historical_provenance_subject=historical_provenance_subject
|
||||
or destroyed_subject,
|
||||
)
|
||||
# Fail-closed skeleton when no server authorization is supplied: never
|
||||
# sets merger_may_accept True (even if operator_authorized was True).
|
||||
return {
|
||||
"event": "irrecoverable_decision_lock_provenance",
|
||||
"status": "provenance_irrecoverable",
|
||||
"record_type": "irrecoverable_decision_provenance",
|
||||
"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,
|
||||
"incident_issue": incident_issue,
|
||||
"incident_comment_id": incident_comment_id,
|
||||
"authorization_verified": False,
|
||||
"merger_may_accept": False,
|
||||
"acceptance_rule": (
|
||||
"Merger may accept this record only when a server-side "
|
||||
"authorization artifact verifies for remote/org/repo/PR/exact "
|
||||
"head/incident, the record is durable and read back, and normal "
|
||||
"merge gates still pass. Caller Booleans never authorize. 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)."""
|
||||
from irrecoverable_provenance import (
|
||||
format_irrecoverable_audit_comment as _fmt,
|
||||
)
|
||||
|
||||
return _fmt(record)
|
||||
|
||||
|
||||
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