"""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" ), }