Add assess_reconcile_linked_issue_report verifier to block open/closed/resolved linked issue status claims without gitea_view_issue proof in the current session, require not-verified wording when the issue is missing, and gate recommended issue close/reopen/comment actions on exact capability proof. Wire into build_final_report and export from review_proofs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""Reconciliation linked-issue live proof verifier (#300)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
_HANDOFF_SECTION_RE = re.compile(r"^##\s*Controller Handoff\s*$", re.I | re.M)
|
|
|
|
_LINKED_ISSUE_FIELD_RE = re.compile(
|
|
r"linked issue\s*:\s*(.+)$",
|
|
re.I | re.M,
|
|
)
|
|
_LINKED_ISSUE_STATUS_RE = re.compile(
|
|
r"linked issue(?:\s+live)?\s+status\s*:\s*(.+)$",
|
|
re.I | re.M,
|
|
)
|
|
|
|
_STATUS_CLAIM_RE = re.compile(
|
|
r"\b(?:issue\s+#?\d+\s*\()?(open|closed|resolved)\b|"
|
|
r"issue\s+(?:is\s+)?(open|closed|resolved)\b|"
|
|
r"linked issue.*\b(open|closed|resolved)\b",
|
|
re.I,
|
|
)
|
|
_NOT_VERIFIED_RE = re.compile(r"not verified in this session", re.I)
|
|
|
|
_LIVE_FETCH_PROOF_RE = re.compile(
|
|
r"gitea_view_issue|issue fetched live|live issue fetch|"
|
|
r"fetched linked issue.*(?:current|this) session|"
|
|
r"linked issue (?:fetch|proof).*(?:current|this) session|"
|
|
r"live linked issue proof",
|
|
re.I,
|
|
)
|
|
|
|
_ISSUE_ACTION_RECOMMEND_RE = re.compile(
|
|
r"(?:recommend(?:ed)?|should|must)\s+(?:close|reopen|comment on)\s+"
|
|
r"(?:the\s+)?(?:linked\s+)?issue|"
|
|
r"(?:close|reopen|comment on)\s+linked issue\s+#?\d+",
|
|
re.I,
|
|
)
|
|
_CAPABILITY_PROOF_RE = re.compile(
|
|
r"capability proof|exact capability|gitea_close_issue|"
|
|
r"gitea_mark_issue|gitea_create_issue_comment|gitea_edit_issue",
|
|
re.I,
|
|
)
|
|
|
|
_NO_LINKED_ISSUE_VALUES = frozenset({
|
|
"",
|
|
"none",
|
|
"n/a",
|
|
"not applicable",
|
|
"no linked issue",
|
|
"unknown",
|
|
})
|
|
|
|
|
|
def _handoff_field_map(report_text: str) -> dict[str, str]:
|
|
text = report_text or ""
|
|
match = _HANDOFF_SECTION_RE.search(text)
|
|
if not match:
|
|
return {}
|
|
fields: dict[str, str] = {}
|
|
for line in text[match.end() :].splitlines():
|
|
stripped = line.strip().lstrip("-*").strip()
|
|
if ":" not in stripped:
|
|
continue
|
|
key, value = stripped.split(":", 1)
|
|
fields[key.strip().lower()] = value.strip()
|
|
return fields
|
|
|
|
|
|
def _extract_linked_issue_number(text: str, fields: dict[str, str]) -> int | None:
|
|
linked = fields.get("linked issue", "")
|
|
if linked.lower() in _NO_LINKED_ISSUE_VALUES:
|
|
return None
|
|
match = re.search(r"#?(\d+)", linked)
|
|
if match:
|
|
return int(match.group(1))
|
|
field_match = _LINKED_ISSUE_FIELD_RE.search(text)
|
|
if field_match:
|
|
num = re.search(r"#?(\d+)", field_match.group(1))
|
|
if num:
|
|
return int(num.group(1))
|
|
return None
|
|
|
|
|
|
def _status_value(text: str, fields: dict[str, str]) -> str:
|
|
for key in ("linked issue live status", "linked issue status"):
|
|
if fields.get(key):
|
|
return fields[key]
|
|
match = _LINKED_ISSUE_STATUS_RE.search(text)
|
|
return match.group(1).strip() if match else ""
|
|
|
|
|
|
def _live_proof_present(text: str, session: dict, issue_number: int | None) -> bool:
|
|
if session.get("live_fetched") or session.get("verified_live"):
|
|
return True
|
|
if session.get("issue_fetch_proof"):
|
|
return True
|
|
if _LIVE_FETCH_PROOF_RE.search(text):
|
|
return True
|
|
if issue_number is not None:
|
|
pattern = re.compile(
|
|
rf"gitea_view_issue.*#?{issue_number}|"
|
|
rf"issue\s+#?{issue_number}.*(?:fetched|viewed).*(?:current|this) session",
|
|
re.I,
|
|
)
|
|
if pattern.search(text):
|
|
return True
|
|
return False
|
|
|
|
|
|
def assess_reconcile_linked_issue_report(
|
|
report_text: str,
|
|
*,
|
|
reconcile_session: dict | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Validate linked issue status claims in reconciliation handoffs (#300)."""
|
|
text = report_text or ""
|
|
session = dict(reconcile_session or {})
|
|
fields = _handoff_field_map(text)
|
|
reasons: list[str] = []
|
|
|
|
issue_number = session.get("linked_issue_number")
|
|
if issue_number is None:
|
|
issue_number = _extract_linked_issue_number(text, fields)
|
|
|
|
if issue_number is None:
|
|
status = _status_value(text, fields)
|
|
if status and status.lower() not in _NO_LINKED_ISSUE_VALUES:
|
|
if _STATUS_CLAIM_RE.search(status):
|
|
reasons.append(
|
|
"linked issue status reported without identifying the linked issue (#300)"
|
|
)
|
|
if not reasons:
|
|
return {
|
|
"proven": True,
|
|
"block": False,
|
|
"reasons": [],
|
|
"linked_issue_number": None,
|
|
"safe_next_action": "proceed",
|
|
}
|
|
|
|
status = _status_value(text, fields)
|
|
status_lower = status.lower()
|
|
|
|
if session.get("issue_missing"):
|
|
if status_lower and "not verified" not in status_lower:
|
|
reasons.append(
|
|
"missing linked issue must be reported as "
|
|
"'not verified in this session' (#300)"
|
|
)
|
|
elif status:
|
|
if _NOT_VERIFIED_RE.search(status):
|
|
pass
|
|
elif _STATUS_CLAIM_RE.search(status):
|
|
if not _live_proof_present(text, session, issue_number):
|
|
reasons.append(
|
|
"linked issue open/closed/resolved status requires live "
|
|
"gitea_view_issue proof in the current session (#300)"
|
|
)
|
|
elif status_lower not in _NO_LINKED_ISSUE_VALUES:
|
|
if not _live_proof_present(text, session, issue_number):
|
|
reasons.append(
|
|
"linked issue status claim requires live fetch proof or "
|
|
"'not verified in this session' (#300)"
|
|
)
|
|
|
|
if _ISSUE_ACTION_RECOMMEND_RE.search(text):
|
|
if not _CAPABILITY_PROOF_RE.search(text) and not session.get(
|
|
"issue_action_capability_proven"
|
|
):
|
|
reasons.append(
|
|
"recommended linked issue close/reopen/comment requires "
|
|
"exact capability proof (#300)"
|
|
)
|
|
|
|
proven = not reasons
|
|
return {
|
|
"proven": proven,
|
|
"block": not proven,
|
|
"reasons": list(dict.fromkeys(reasons)),
|
|
"linked_issue_number": issue_number,
|
|
"live_proof_present": _live_proof_present(text, session, issue_number),
|
|
"safe_next_action": (
|
|
"fetch linked issue with gitea_view_issue in this session or "
|
|
"report 'Linked issue status: not verified in this session'"
|
|
if reasons
|
|
else "proceed"
|
|
),
|
|
} |