Rebased onto current prgs/master; merged with #396 validation failure history by keeping both validator rules and workflow handoff fields. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
233 lines
7.5 KiB
Python
233 lines
7.5 KiB
Python
"""Explicit worktree and cwd proof for PR review validation (#398)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
_FULL_SHA = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
|
|
|
|
_PWD_RE = re.compile(
|
|
r"(?:^|\n)\s*(?:pwd|working\s+directory|cwd)\s*:\s*(\S+)",
|
|
re.IGNORECASE,
|
|
)
|
|
_HEAD_RE = re.compile(
|
|
r"(?:git\s+rev-parse\s+head|observed\s+head\s+sha)\s*:\s*([0-9a-f]{7,40})",
|
|
re.IGNORECASE | re.MULTILINE,
|
|
)
|
|
_EXPECTED_HEAD_RE = re.compile(
|
|
r"(?:expected\s+(?:pr\s+)?head\s+sha|candidate\s+head\s+sha|pinned\s+head)\s*:\s*([0-9a-f]{7,40})",
|
|
re.IGNORECASE,
|
|
)
|
|
_STATUS_RE = re.compile(
|
|
r"git\s+status\s+(?:--short\s+--branch|--short|-sb)\s*:\s*(.+)",
|
|
re.IGNORECASE,
|
|
)
|
|
_VALIDATION_CMD_RE = re.compile(
|
|
r"validation\s+command\s*:\s*(.+)",
|
|
re.IGNORECASE,
|
|
)
|
|
_GIT_C_CMD_RE = re.compile(r"git\s+-C\s+\S+", re.IGNORECASE)
|
|
_CD_CMD_RE = re.compile(r"(?:^|&&\s*)cd\s+\S+", re.IGNORECASE)
|
|
_BASELINE_CWD_RE = re.compile(
|
|
r"baseline\s+(?:worktree|working\s+directory|cwd)\s*:\s*(\S+)",
|
|
re.IGNORECASE,
|
|
)
|
|
_BASELINE_SHA_RE = re.compile(
|
|
r"baseline\s+(?:target\s+)?sha\s*:\s*([0-9a-f]{7,40})",
|
|
re.IGNORECASE,
|
|
)
|
|
_BASELINE_CMD_RE = re.compile(
|
|
r"baseline\s+validation\s+command\s*:\s*(.+)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def _normalize_path(path: str) -> str:
|
|
return (path or "").replace("\\", "/").rstrip("/")
|
|
|
|
|
|
def _path_under_branches(path: str, project_root: str | None = None) -> bool:
|
|
normalized = _normalize_path(path)
|
|
if not normalized:
|
|
return False
|
|
if "/branches/" in f"{normalized}/":
|
|
return True
|
|
if normalized.endswith("/branches"):
|
|
return True
|
|
if project_root:
|
|
root = _normalize_path(project_root)
|
|
if normalized.startswith(f"{root}/"):
|
|
rel = normalized[len(root) + 1 :]
|
|
return rel == "branches" or rel.startswith("branches/")
|
|
return False
|
|
|
|
|
|
def _expand_sha(sha: str) -> str:
|
|
return (sha or "").strip().lower()
|
|
|
|
|
|
def _sha_matches(expected: str, observed: str) -> bool:
|
|
exp = _expand_sha(expected)
|
|
obs = _expand_sha(observed)
|
|
if not exp or not obs:
|
|
return False
|
|
if len(exp) == 40 and len(obs) == 40:
|
|
return exp == obs
|
|
return obs.startswith(exp) or exp.startswith(obs)
|
|
|
|
|
|
def _command_has_explicit_cwd(command: str, cwd: str) -> bool:
|
|
text = (command or "").strip()
|
|
if not text:
|
|
return False
|
|
if _GIT_C_CMD_RE.search(text):
|
|
return True
|
|
if _CD_CMD_RE.search(text):
|
|
return True
|
|
if cwd and cwd in text:
|
|
return True
|
|
return False
|
|
|
|
|
|
def assess_validation_cwd_proof_report(
|
|
report_text: str,
|
|
*,
|
|
validation_session: dict | None = None,
|
|
project_root: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Require cwd/HEAD proof before reviewer validation claims (#398)."""
|
|
text = report_text or ""
|
|
session = dict(validation_session or {})
|
|
reasons: list[str] = []
|
|
violations: list[str] = []
|
|
|
|
claims_validation = bool(
|
|
session.get("validation_ran")
|
|
or _VALIDATION_CMD_RE.search(text)
|
|
or session.get("command")
|
|
)
|
|
if not claims_validation:
|
|
return {
|
|
"proven": True,
|
|
"block": False,
|
|
"claims_validation": False,
|
|
"reasons": [],
|
|
"violations": [],
|
|
"safe_next_action": "proceed",
|
|
}
|
|
|
|
expected_head = (
|
|
session.get("expected_head_sha")
|
|
or session.get("candidate_head_sha")
|
|
or ""
|
|
).strip()
|
|
if not expected_head:
|
|
match = _EXPECTED_HEAD_RE.search(text)
|
|
expected_head = (match.group(1) if match else "").strip()
|
|
|
|
observed_head = (session.get("observed_head_sha") or "").strip()
|
|
if not observed_head:
|
|
match = _HEAD_RE.search(text)
|
|
observed_head = (match.group(1) if match else "").strip()
|
|
|
|
cwd = (
|
|
session.get("working_directory")
|
|
or session.get("cwd")
|
|
or session.get("pwd")
|
|
or ""
|
|
).strip()
|
|
if not cwd:
|
|
match = _PWD_RE.search(text)
|
|
cwd = (match.group(1) if match else "").strip().rstrip(",.;")
|
|
|
|
command = (session.get("command") or "").strip()
|
|
if not command:
|
|
match = _VALIDATION_CMD_RE.search(text)
|
|
command = (match.group(1) if match else "").strip().rstrip(".;")
|
|
|
|
if not cwd:
|
|
reasons.append(
|
|
"validation claimed without pwd/working-directory proof (#398)"
|
|
)
|
|
elif not _path_under_branches(cwd, project_root):
|
|
violations.append(
|
|
f"validation cwd {cwd!r} is not under branches/ (#398)"
|
|
)
|
|
reasons.append(
|
|
"reviewer validation must run from a branches/ worktree, "
|
|
"not the main checkout (#398)"
|
|
)
|
|
|
|
if not observed_head:
|
|
reasons.append(
|
|
"validation claimed without git rev-parse HEAD / observed HEAD SHA "
|
|
"proof (#398)"
|
|
)
|
|
elif expected_head and not _sha_matches(expected_head, observed_head):
|
|
violations.append(
|
|
f"observed HEAD {observed_head} does not match expected "
|
|
f"PR head {expected_head} (#398)"
|
|
)
|
|
reasons.append("validation HEAD SHA must match pinned PR head (#398)")
|
|
|
|
if not _STATUS_RE.search(text) and session.get("git_status") is None:
|
|
reasons.append(
|
|
"validation claimed without git status --short --branch proof (#398)"
|
|
)
|
|
|
|
if command and cwd and not _command_has_explicit_cwd(command, cwd):
|
|
if session.get("tool_working_directory") is not True:
|
|
reasons.append(
|
|
"validation command must use git -C <worktree>, "
|
|
"cd <worktree> && ..., or tool-provided cwd metadata (#398)"
|
|
)
|
|
|
|
baseline_ran = bool(
|
|
session.get("baseline_validation_ran")
|
|
or _BASELINE_CMD_RE.search(text)
|
|
)
|
|
if baseline_ran:
|
|
baseline_cwd = (session.get("baseline_worktree_path") or "").strip()
|
|
if not baseline_cwd:
|
|
match = _BASELINE_CWD_RE.search(text)
|
|
baseline_cwd = (match.group(1) if match else "").strip().rstrip(",.;")
|
|
if not baseline_cwd or not _path_under_branches(baseline_cwd, project_root):
|
|
reasons.append(
|
|
"baseline validation claimed without baseline worktree cwd "
|
|
"under branches/ (#398)"
|
|
)
|
|
baseline_sha = (session.get("baseline_target_sha") or "").strip()
|
|
if not baseline_sha:
|
|
match = _BASELINE_SHA_RE.search(text)
|
|
baseline_sha = (match.group(1) if match else "").strip()
|
|
if not baseline_sha:
|
|
reasons.append(
|
|
"baseline validation claimed without baseline target SHA (#398)"
|
|
)
|
|
baseline_cmd = (session.get("baseline_command") or "").strip()
|
|
if not baseline_cmd:
|
|
match = _BASELINE_CMD_RE.search(text)
|
|
baseline_cmd = (match.group(1) if match else "").strip()
|
|
if not baseline_cmd:
|
|
reasons.append(
|
|
"baseline validation claimed without exact baseline command (#398)"
|
|
)
|
|
|
|
proven = not reasons and not violations
|
|
return {
|
|
"proven": proven,
|
|
"block": bool(violations) or not proven,
|
|
"claims_validation": True,
|
|
"expected_head_sha": expected_head or None,
|
|
"observed_head_sha": observed_head or None,
|
|
"working_directory": cwd or None,
|
|
"reasons": reasons,
|
|
"violations": violations,
|
|
"safe_next_action": (
|
|
"before validation record pwd, git rev-parse HEAD, git status, "
|
|
"expected PR head SHA; run commands with git -C or cd in the same line"
|
|
if not proven
|
|
else "proceed"
|
|
),
|
|
} |