76 lines
3.3 KiB
Python
76 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Validate that a code-review report gives evidence-backed Review advice."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
|
||
REVIEW_PREFIX = "**Review 建议:**"
|
||
LEGACY_PREFIX = "**建议:**"
|
||
REVIEW_PATTERN = re.compile(
|
||
r"^\*\*Review 建议:\*\*\s*"
|
||
r"<span\b[^>]*><strong>([^<]+)</strong></span>\s*"
|
||
r"\*\*\[([^\]]+)\]\*\*:\s*(\S.*)$"
|
||
)
|
||
REVIEW_DRAFT_HEADING = re.compile(r"^### PR #\d+ Review 建议草稿(未提交)\s*$")
|
||
EVIDENCE_MARKERS = ("依据", "测试", "Diff", "文件", "行号", "CR-", "RV-", "实现", "影响", "主线", "API", "命令", "已核验", "未发现", "通过", "失败", "缺少", "需要")
|
||
PLACEHOLDER_MARKERS = ("<结论>", "<具体", "<status-", "<decision_")
|
||
|
||
|
||
def validate_report(text: str, require_review: bool = False) -> list[str]:
|
||
errors: list[str] = []
|
||
review_lines: list[int] = []
|
||
lines = text.splitlines()
|
||
for line_number, line in enumerate(lines, start=1):
|
||
stripped = line.strip()
|
||
if stripped.startswith(LEGACY_PREFIX):
|
||
errors.append(f"line {line_number}: legacy conclusion-only field is not allowed")
|
||
if not stripped.startswith(REVIEW_PREFIX):
|
||
continue
|
||
review_lines.append(line_number)
|
||
match = REVIEW_PATTERN.match(stripped)
|
||
if not match:
|
||
errors.append(f"line {line_number}: Review decision requires a status tag and rationale")
|
||
continue
|
||
conclusion, status, rationale = match.groups()
|
||
if not conclusion.strip() or not status.strip() or len(rationale.strip()) < 30:
|
||
errors.append(f"line {line_number}: Review rationale is too short")
|
||
if not any(marker in rationale for marker in EVIDENCE_MARKERS):
|
||
errors.append(f"line {line_number}: Review rationale lacks evidence or an actionable next step")
|
||
if any(marker in rationale for marker in PLACEHOLDER_MARKERS):
|
||
errors.append(f"line {line_number}: unresolved template placeholder in rationale")
|
||
for index, line in enumerate(lines):
|
||
if REVIEW_DRAFT_HEADING.match(line.strip()) and not any(candidate.strip().startswith(REVIEW_PREFIX) for candidate in lines[index + 1:index + 7]):
|
||
errors.append(f"line {index + 1}: Review draft must start with a substantive Review suggestion")
|
||
if require_review and not review_lines:
|
||
errors.append("PR review mode requires at least one substantive Review decision")
|
||
return errors
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Validate substantive Review decisions.")
|
||
parser.add_argument("--report", required=True, type=Path)
|
||
parser.add_argument("--require-review", action="store_true")
|
||
args = parser.parse_args()
|
||
try:
|
||
text = args.report.read_text(encoding="utf-8", errors="strict")
|
||
except (OSError, UnicodeError) as exc:
|
||
print(f"review report validation failed: {exc}", file=sys.stderr)
|
||
return 2
|
||
errors = validate_report(text, args.require_review)
|
||
if errors:
|
||
print("review report validation failed:", file=sys.stderr)
|
||
for error in errors:
|
||
print(f"- {error}", file=sys.stderr)
|
||
return 1
|
||
print("review report validation passed")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|