"""Merge-simulation worktree mutation verifier for reviewer reports (#317).""" from __future__ import annotations import re from typing import Any _WORKTREE_INDEX_FIELD_RE = re.compile( r"^\s*worktree/index mutations\s*:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE, ) _READONLY_DIAG_RE = re.compile( r"read[- ]only diagnostics\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE, ) _WORKTREE_PATH_RE = re.compile( r"(?:worktree path|diagnostic worktree|simulation worktree)\s*:", re.IGNORECASE, ) _PRE_CLEAN_RE = re.compile( r"(?:pre[- ]simulation|before simulation|clean before).*(?:clean|status)", re.IGNORECASE, ) _MERGE_RESULT_RE = re.compile( r"(?:merge result|simulation result|conflict status|merge conflict)", re.IGNORECASE, ) _ABORT_RE = re.compile( r"(?:merge --abort|abort/reset command|abort command|git merge --abort)", re.IGNORECASE, ) _POST_CLEAN_RE = re.compile( r"(?:post[- ]abort|after abort|clean after).*(?:clean|status)", re.IGNORECASE, ) def _command_text(entry: Any) -> str: if isinstance(entry, dict): return str(entry.get("command") or "").strip() return str(entry or "").strip() def _git_subcommand_line(command: str) -> str | None: tokens = command.split() if not tokens or tokens[0] != "git": return None return " ".join(tokens[1:]) def merge_simulation_commands(command_log: list | None) -> list[str]: """Return logged commands that perform local merge simulation (#317).""" out: list[str] = [] for entry in command_log or []: command = _command_text(entry) rest = _git_subcommand_line(command) if rest is None: continue lower = command.lower() if rest.startswith("merge"): if "--no-commit" in lower or ( "--no-ff" in lower and "merge" in lower and "--commit" not in lower ): out.append(command) elif "--abort" in lower: out.append(command) return out def assess_merge_simulation_report( report_text: str, *, command_log: list | None = None, ) -> dict[str, Any]: """Reject merge simulation classified as read-only; require worktree proof (#317).""" text = report_text or "" lower = text.lower() reasons: list[str] = [] simulation_commands = merge_simulation_commands(command_log) has_abort = any("--abort" in c.lower() for c in simulation_commands) has_simulation = any( "--no-commit" in c.lower() or ( "--no-ff" in c.lower() and "--abort" not in c.lower() ) for c in simulation_commands ) if not simulation_commands: return { "proven": True, "block": False, "reasons": [], "simulation_commands": [], "safe_next_action": "proceed", } field = _WORKTREE_INDEX_FIELD_RE.search(text) value = (field.group(1).strip().lower() if field else "") or "" if not value or value in {"none", "n/a", "no"}: reasons.append( "merge simulation commands ran but report lacks " "'Worktree/index mutations' entry" ) elif "merge" not in value and "simulation" not in value: if not any(cmd.lower() in lower for cmd in simulation_commands): reasons.append( "Worktree/index mutations must document merge simulation commands" ) readonly = _READONLY_DIAG_RE.search(text) if readonly: readonly_value = readonly.group(1) for command in simulation_commands: if command.lower() in readonly_value.lower(): reasons.append( "merge simulation may not be listed under read-only diagnostics" ) if has_simulation and "merge simulation" in readonly_value.lower(): reasons.append( "merge simulation may not be classified as read-only diagnostics" ) if has_simulation: if not _WORKTREE_PATH_RE.search(text): reasons.append("merge simulation report missing worktree path") if not _PRE_CLEAN_RE.search(text): reasons.append("merge simulation report missing pre-simulation clean status") if not _MERGE_RESULT_RE.search(text): reasons.append("merge simulation report missing merge result/conflict status") if has_abort or has_simulation: if has_abort and not _ABORT_RE.search(text): reasons.append("merge simulation report missing abort/reset command proof") if has_abort and not _POST_CLEAN_RE.search(text): reasons.append("merge simulation report missing post-abort clean status") proven = not reasons return { "proven": proven, "block": not proven, "reasons": reasons, "simulation_commands": simulation_commands, "safe_next_action": ( "classify merge simulation under Worktree/index mutations with full " "pre/merge/abort/post-clean proof; never list as read-only diagnostics" if reasons else "proceed" ), }