diff --git a/scripts/check_review_completeness.py b/scripts/check_review_completeness.py new file mode 100644 index 00000000..f60b3560 --- /dev/null +++ b/scripts/check_review_completeness.py @@ -0,0 +1,80 @@ +"""Check review requests for completeness before submission.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +REQUIRED_FIELDS = { + "request_id": "Unique identifier for the review request", + "reviewer_role": "Role being requested (e.g., domain_expert, safety_reviewer)", + "artifact_refs": "List of artifacts to review (PR URLs, doc paths)", + "claim_boundary": "What claim the review is expected to evaluate", + "decision_needed": "What decision is needed (approve, reject, defer)", + "requested_by": "Name or ID of the person requesting review", +} + +OPTIONAL_FIELDS = { + "deadline": "ISO 8601 deadline for the review", + "context_notes": "Additional context for the reviewer", + "safety_relevant": "Whether the request touches safety policy", +} + + +def check_completeness(request_path: str) -> dict: + p = Path(request_path) + if not p.exists(): + return {"error": f"Request file not found: {request_path}"} + + try: + data = json.loads(p.read_text()) + except json.JSONDecodeError as e: + return {"error": f"Invalid JSON: {e}"} + + if not isinstance(data, dict): + return {"error": "Request must be a JSON object"} + + missing = [] + present = [] + + for field, desc in REQUIRED_FIELDS.items(): + if field not in data or data[field] is None or (isinstance(data[field], str) and not data[field].strip()): + missing.append({"field": field, "description": desc}) + else: + present.append(field) + + optional_present = [f for f in OPTIONAL_FIELDS if f in data and data[f]] + + return { + "request_path": request_path, + "valid_json": True, + "is_complete": len(missing) == 0, + "required_fields_present": len(present), + "required_fields_total": len(REQUIRED_FIELDS), + "optional_fields_present": len(optional_present), + "missing_fields": missing, + "fields_present": present, + } + + +def main() -> int: + import argparse + parser = argparse.ArgumentParser(description="Check review request completeness") + parser.add_argument("--request", required=True, help="Path to review request JSON") + args = parser.parse_args() + + result = check_completeness(args.request) + if "error" in result: + print(f"Error: {result['error']}", file=sys.stderr) + return 2 + + print(json.dumps(result, indent=2)) + if not result["is_complete"]: + print(f"\nMissing {len(result['missing_fields'])} required fields.") + return 3 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_check_review_completeness.py b/tests/test_check_review_completeness.py new file mode 100644 index 00000000..a2f260d9 --- /dev/null +++ b/tests/test_check_review_completeness.py @@ -0,0 +1,80 @@ +"""Tests for review request completeness checker.""" +import json +import subprocess +import sys +from pathlib import Path +import pytest + +from scripts.check_review_completeness import check_completeness + + +def _make_request(tmp_path, data: dict) -> str: + p = Path(tmp_path) / "request.json" + p.write_text(json.dumps(data)) + return str(p) + + +def _valid_request(): + return { + "request_id": "REV-001", + "reviewer_role": "domain_expert", + "artifact_refs": ["docs/review/EXPERT_REVIEW_PACK.md"], + "claim_boundary": "Evaluate whether the candidate panel is credible", + "decision_needed": "approve", + "requested_by": "PI Name", + "deadline": "2026-08-01", + "context_notes": "First pilot panel review", + } + + +def test_complete_request(tmp_path): + r = check_completeness(_make_request(tmp_path, _valid_request())) + assert r["is_complete"] is True + + +def test_missing_required_field(tmp_path): + d = _valid_request() + del d["decision_needed"] + r = check_completeness(_make_request(tmp_path, d)) + assert r["is_complete"] is False + + +def test_empty_required_field(tmp_path): + d = _valid_request() + d["reviewer_role"] = "" + r = check_completeness(_make_request(tmp_path, d)) + assert r["is_complete"] is False + + +def test_missing_multiple_fields(tmp_path): + d = _valid_request() + del d["artifact_refs"] + del d["claim_boundary"] + r = check_completeness(_make_request(tmp_path, d)) + assert len(r["missing_fields"]) >= 2 + + +def test_missing_file(): + r = check_completeness("/nonexistent.json") + assert "error" in r + + +def test_invalid_json(tmp_path): + p = Path(tmp_path) / "bad.json" + p.write_text("not json") + r = check_completeness(str(p)) + assert "error" in r + + +def test_cli_valid(tmp_path): + p = _make_request(tmp_path, _valid_request()) + res = subprocess.run([sys.executable, "scripts/check_review_completeness.py", "--request", p], + capture_output=True, text=True) + assert res.returncode == 0 + + +def test_cli_invalid(tmp_path): + p = _make_request(tmp_path, {"id": "incomplete"}) + res = subprocess.run([sys.executable, "scripts/check_review_completeness.py", "--request", p], + capture_output=True, text=True) + assert res.returncode == 3