#!/usr/bin/env python3 """Validate issue QA workflow artifacts.""" from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ISSUES_DIR = ROOT / "issues" REQUIRED_SECTIONS = ( "Title", "Severity", "Environment", "Summary", "Reproduction Steps", "Expected Result", "Actual Result", "Root Cause Analysis", "Fix Plan", "Implementation Notes", "Verification", "Status History", "Closure", ) CLOSED_STATES = {"Verification Passed", "Closed"} def issue_files() -> list[Path]: return sorted(ISSUES_DIR.glob("issue[0-9]*.md")) def has_section(content: str, name: str) -> bool: pattern = rf"(?m)^##\s+{re.escape(name)}\s*$" return bool(re.search(pattern, content)) def current_status(content: str) -> str | None: match = re.search(r"(?m)^- Current Status:\s*`?([^`\n]+)`?\s*$", content) return match.group(1).strip() if match else None def screenshot_refs(content: str) -> list[str]: pattern = r"issues/screenshots/[A-Za-z0-9._-]+\.png" return sorted(set(re.findall(pattern, content))) def has_before_ref(refs: list[str], issue_num: str) -> bool: legacy = f"issues/screenshots/issue{issue_num}.png" return any("-before-" in ref for ref in refs) or legacy in refs def has_after_ref(refs: list[str]) -> bool: return any("-after-" in ref for ref in refs) def command_check_present(content: str) -> bool: required = ( "`cargo check --workspace`", "`cargo test --workspace`", "`just qa-validate`", ) return all(token in content for token in required) def verify_issue(path: Path) -> list[str]: errors: list[str] = [] content = path.read_text(encoding="utf-8") issue_num_match = re.search(r"issue([0-9]+)\.md$", path.name) issue_num = issue_num_match.group(1) if issue_num_match else "?" for section in REQUIRED_SECTIONS: if not has_section(content, section): errors.append(f"{path}: missing section '## {section}'") refs = screenshot_refs(content) for ref in refs: if not (ROOT / ref).exists(): errors.append(f"{path}: missing screenshot file: {ref}") if not has_before_ref(refs, issue_num): errors.append( f"{path}: missing before screenshot reference " "(use issueN-before-...png or legacy issueN.png)" ) status = current_status(content) if status in CLOSED_STATES and not has_after_ref(refs): errors.append( f"{path}: status '{status}' requires at least one after screenshot reference" ) if status in CLOSED_STATES and not command_check_present(content): errors.append( f"{path}: status '{status}' requires command checklist entries for " "cargo check, cargo test, and just qa-validate" ) return errors def main() -> int: files = issue_files() if not files: print("error: no issue files found under issues/", file=sys.stderr) return 1 failures: list[str] = [] for path in files: failures.extend(verify_issue(path)) if failures: for failure in failures: print(f"error: {failure}", file=sys.stderr) return 1 print(f"qa validation passed ({len(files)} issue file(s) checked)") return 0 if __name__ == "__main__": raise SystemExit(main())