Add author_duplicate_work_gate and enforce it at claim, lock, and PR creation. Expose gitea_assess_author_duplicate_work for pre-commit/push checks and extend work-issue final-report verification. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
198 lines
6.4 KiB
Python
198 lines
6.4 KiB
Python
"""Early duplicate-work detection for author work-issue flows (#400)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from issue_claim_heartbeat import (
|
|
_linked_open_pr,
|
|
_matching_branch_names,
|
|
classify_issue_claim,
|
|
)
|
|
|
|
STAGES = (
|
|
"claim",
|
|
"lock",
|
|
"worktree",
|
|
"edit",
|
|
"commit",
|
|
"push",
|
|
"create_pr",
|
|
)
|
|
|
|
ELIGIBILITY_OPEN_PR_EXISTS = "OPEN_PR_EXISTS"
|
|
ELIGIBILITY_DUPLICATE_BRANCH_EXISTS = "DUPLICATE_BRANCH_EXISTS"
|
|
ELIGIBILITY_ACTIVE_CLAIM = "ACTIVE_CLAIM_BY_OTHER"
|
|
ELIGIBILITY_CLEAR = "CLEAR"
|
|
|
|
|
|
def assess_author_duplicate_work(
|
|
issue_number: int,
|
|
*,
|
|
stage: str,
|
|
open_prs: list[dict] | None = None,
|
|
branch_names: list[str] | None = None,
|
|
claim_entry: dict | None = None,
|
|
matching_branches: list[str] | None = None,
|
|
allow_stale_takeover: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Fail closed when duplicate work is detected before author mutations (#400)."""
|
|
stage_norm = (stage or "").strip().lower()
|
|
if stage_norm not in STAGES:
|
|
return {
|
|
"allowed": False,
|
|
"block": True,
|
|
"eligibility_class": "INVALID_STAGE",
|
|
"stage": stage_norm or None,
|
|
"reasons": [f"unknown duplicate-work stage {stage!r}"],
|
|
"safe_next_action": f"use one of: {', '.join(STAGES)}",
|
|
}
|
|
|
|
prs = list(open_prs or [])
|
|
linked_pr = _linked_open_pr(int(issue_number), prs)
|
|
branches = list(
|
|
matching_branches
|
|
if matching_branches is not None
|
|
else _matching_branch_names(int(issue_number), list(branch_names or []))
|
|
)
|
|
|
|
reasons: list[str] = []
|
|
eligibility = ELIGIBILITY_CLEAR
|
|
|
|
if linked_pr:
|
|
eligibility = ELIGIBILITY_OPEN_PR_EXISTS
|
|
reasons.append(
|
|
f"open PR #{linked_pr.get('number')} already covers issue "
|
|
f"#{issue_number}"
|
|
)
|
|
|
|
if branches and stage_norm in {"claim", "lock", "worktree", "edit"}:
|
|
if eligibility == ELIGIBILITY_CLEAR:
|
|
eligibility = ELIGIBILITY_DUPLICATE_BRANCH_EXISTS
|
|
reasons.append(
|
|
f"remote branch(es) already exist for issue #{issue_number}: "
|
|
f"{', '.join(branches)}"
|
|
)
|
|
|
|
entry = claim_entry or {}
|
|
claim_status = (entry.get("status") or "").strip()
|
|
if claim_status in {"active", "awaiting_review"} and stage_norm == "claim":
|
|
if entry.get("reclaimable") and allow_stale_takeover:
|
|
pass
|
|
elif claim_status == "active" and not entry.get("reclaimable"):
|
|
if eligibility == ELIGIBILITY_CLEAR:
|
|
eligibility = ELIGIBILITY_ACTIVE_CLAIM
|
|
reasons.append(
|
|
f"issue #{issue_number} has active claim "
|
|
f"(status={claim_status})"
|
|
)
|
|
elif claim_status == "awaiting_review" and stage_norm == "claim":
|
|
if not linked_pr:
|
|
reasons.append(
|
|
f"issue #{issue_number} is awaiting_review but no linked open PR "
|
|
"was supplied for duplicate-work proof"
|
|
)
|
|
|
|
allowed = not reasons
|
|
outcome = "duplicate_work_prevented" if not allowed else "clear"
|
|
if stage_norm == "create_pr" and not allowed:
|
|
outcome = "duplicate_pr_prevented"
|
|
elif stage_norm in {"commit", "push"} and not allowed:
|
|
outcome = "duplicate_push_prevented" if stage_norm == "push" else "duplicate_commit_prevented"
|
|
|
|
return {
|
|
"allowed": allowed,
|
|
"block": not allowed,
|
|
"eligibility_class": eligibility if not allowed else ELIGIBILITY_CLEAR,
|
|
"stage": stage_norm,
|
|
"linked_open_pr": linked_pr.get("number") if linked_pr else None,
|
|
"matching_branches": branches,
|
|
"claim_status": claim_status or None,
|
|
"outcome": outcome,
|
|
"reasons": reasons,
|
|
"safe_next_action": (
|
|
"stop without edits/commit/push/PR; produce reconciliation handoff "
|
|
"preserving local work only"
|
|
if not allowed
|
|
else "proceed"
|
|
),
|
|
}
|
|
|
|
|
|
def build_claim_entry_from_classification(classification: dict) -> dict:
|
|
"""Map ``classify_issue_claim`` output to gate claim metadata."""
|
|
return {
|
|
"status": classification.get("status"),
|
|
"reclaimable": classification.get("reclaimable"),
|
|
"linked_open_pr": classification.get("linked_open_pr"),
|
|
}
|
|
|
|
|
|
_DUPLICATE_OUTCOME_RE = re.compile(
|
|
r"(duplicate\s+(?:pr|branch|commit|push)\s+prevented|"
|
|
r"duplicate\s+work\s+not\s+prevented|reconciliation\s+handoff)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def assess_work_issue_duplicate_prevention_report(report_text: str) -> dict:
|
|
"""#400: work-issue reports must state duplicate-work prevention outcome."""
|
|
text = report_text or ""
|
|
if "duplicate work" not in text.lower() and "duplicate pr" not in text.lower():
|
|
return {
|
|
"proven": True,
|
|
"block": False,
|
|
"reasons": [],
|
|
"safe_next_action": "proceed",
|
|
}
|
|
if _DUPLICATE_OUTCOME_RE.search(text):
|
|
return {
|
|
"proven": True,
|
|
"block": False,
|
|
"reasons": [],
|
|
"safe_next_action": "proceed",
|
|
}
|
|
return {
|
|
"proven": False,
|
|
"block": True,
|
|
"reasons": [
|
|
"duplicate-work discussion must name a prevention outcome "
|
|
"(duplicate PR/branch/commit/push prevented, or duplicate work "
|
|
"not prevented, or reconciliation handoff)"
|
|
],
|
|
"safe_next_action": "state exact duplicate-work prevention class in final report",
|
|
}
|
|
|
|
|
|
def classify_and_assess(
|
|
issue: dict,
|
|
*,
|
|
stage: str,
|
|
comments: list[dict] | None = None,
|
|
open_prs: list[dict] | None = None,
|
|
branch_names: list[str] | None = None,
|
|
allow_stale_takeover: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Combine claim classification with duplicate-work gate assessment."""
|
|
issue_number = int(issue.get("number") or 0)
|
|
claim = classify_issue_claim(
|
|
issue=issue,
|
|
comments=comments or [],
|
|
open_prs=open_prs or [],
|
|
branch_names=branch_names or [],
|
|
)
|
|
assessment = assess_author_duplicate_work(
|
|
issue_number,
|
|
stage=stage,
|
|
open_prs=open_prs,
|
|
branch_names=branch_names,
|
|
claim_entry=build_claim_entry_from_classification(claim),
|
|
matching_branches=claim.get("matching_branches"),
|
|
allow_stale_takeover=allow_stale_takeover,
|
|
)
|
|
return {
|
|
"issue_number": issue_number,
|
|
"claim": claim,
|
|
"duplicate_work": assessment,
|
|
} |