Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions scripts/check_review_completeness.py
Original file line number Diff line number Diff line change
@@ -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())
80 changes: 80 additions & 0 deletions tests/test_check_review_completeness.py
Original file line number Diff line number Diff line change
@@ -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
Loading