Files
gitea-agent/harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py
T

713 lines
35 KiB
Python

#!/usr/bin/env python3
"""Pure, standard-library support for implementation-spec review artifacts."""
from __future__ import annotations
import argparse, grp, hashlib, json, math, os, pwd, re, stat, sys
from typing import NoReturn
from pathlib import Path
SCHEMA = "gitea-agent.implementation-spec-review-result/v1"
TOP = ("schema", "target", "contract_sha256", "verdict", "criteria", "scope", "conclusion")
TARGET = ("owner", "repository", "issue", "pull", "head")
CRITERION = ("criterion", "contract", "implementation_paths", "test_paths", "evidence", "result", "reason")
SCOPE = ("missing", "excess", "spec_conflicts")
VERDICTS = {"approve", "request_changes", "blocked_spec"}
RESULTS = {"satisfied", "unsatisfied", "indeterminate"}
HEX40 = re.compile(r"^[0-9a-f]{40}$")
HEX64 = re.compile(r"^[0-9a-f]{64}$")
IDENT = re.compile(r"^[^\s/\\\x00<>`=@]+$")
CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]")
MARKUP = re.compile(r"[`<>\[\]{}]|(?:^|\s)[#*!|]|[a-z][a-z0-9+.-]*://|www\.", re.I)
def _require_adoption(declaration, revision, stage):
if declaration is None:
_fail("delegation adoption declaration required")
try:
repo_root = Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from harness.lib.delegation_adoption import AdoptionError, require_adoption
except Exception:
_fail("delegation adoption unavailable")
try:
return require_adoption(declaration, expected_source={"kind": "git_tree", "revision": revision}, expected_stage=stage)
except AdoptionError as exc:
_fail(str(exc))
except Exception:
_fail("delegation adoption unavailable")
class ValidationError(ValueError):
pass
def _fail(msg="invalid review artifact") -> NoReturn:
raise ValidationError(msg)
def _obj(v, keys, label):
if type(v) is not dict or set(v) != set(keys) or len(v) != len(keys): _fail("invalid " + label)
return v
def _str(v, label, lo=1, hi=4000, human=False):
if type(v) is not str or not lo <= len(v) <= hi: _fail("invalid " + label)
if CONTROL.search(v) or v != v.rstrip(): _fail("invalid " + label)
try:
v.encode("utf-8")
except UnicodeError:
_fail("invalid " + label)
if human and (MARKUP.search(v) or "@" in v): _fail("invalid " + label)
return v
def _array(v, label, hi=512, human=False):
if type(v) is not list or len(v) > hi: _fail("invalid " + label)
out=[]; seen=set()
for x in v:
_str(x, label + " item", 1, 4000, human)
if x in seen: _fail("duplicate " + label)
seen.add(x); out.append(x)
return out
def loads_json(text):
if type(text) is not str: _fail("invalid JSON")
def hook(pairs):
d={}
for k,v in pairs:
if k in d: _fail("duplicate JSON key")
d[k]=v
return d
try:
return json.loads(text, object_pairs_hook=hook, parse_constant=lambda x: _fail("nonfinite JSON"),
parse_float=lambda x: float(x) if math.isfinite(float(x)) else _fail("nonfinite JSON"))
except ValidationError: raise
except Exception: _fail("invalid JSON")
def _path(p):
_str(p, "path", 1, 1024)
if p.startswith("/") or "\\" in p or "\x00" in p: _fail("invalid path")
bits=p.split("/")
if any(x in ("", ".", "..") for x in bits): _fail("invalid path")
return p
def validate_result(result, allowed_paths, sensitive_values=()):
_obj(result, TOP, "result schema")
if result["schema"] != SCHEMA: _fail("invalid schema")
t=_obj(result["target"], TARGET, "target")
for k in ("owner", "repository"):
_str(t[k], k, 1, 255);
if not IDENT.fullmatch(t[k]) or t[k] in (".", ".."): _fail("invalid target")
for k in ("issue", "pull"):
if type(t[k]) is not int or t[k] < 1: _fail("invalid target")
if type(t["head"]) is not str or not HEX40.fullmatch(t["head"]): _fail("invalid target")
if type(result["contract_sha256"]) is not str or not HEX64.fullmatch(result["contract_sha256"]): _fail("invalid contract hash")
if type(result["verdict"]) is not str or result["verdict"] not in VERDICTS: _fail("invalid verdict")
if type(allowed_paths) not in (list, tuple, set, frozenset): _fail("invalid allowed paths")
for path in allowed_paths:
_path(path)
allowed = set(allowed_paths)
if len(allowed) != len(allowed_paths): _fail("duplicate allowed path")
criteria=result["criteria"]
if type(criteria) is not list or not 1 <= len(criteria) <= 512: _fail("invalid criteria")
ids=set()
for c in criteria:
_obj(c, CRITERION, "criterion")
_str(c["criterion"], "criterion", 1, 200, True)
if c["criterion"] in ids: _fail("duplicate criterion")
ids.add(c["criterion"])
for k in ("contract", "evidence", "reason"): _str(c[k], k, 1, 4000, True)
for k in ("implementation_paths", "test_paths"):
for p in _array(c[k], k):
_path(p)
if p not in allowed: _fail("path outside allowed set")
if type(c["result"]) is not str or c["result"] not in RESULTS: _fail("invalid criterion result")
s=_obj(result["scope"], SCOPE, "scope")
for k in SCOPE: _array(s[k], k, 512, True)
_str(result["conclusion"], "conclusion", 1, 4000, True)
human = [result["conclusion"]] + [c[k] for c in criteria for k in ("criterion", "contract", "evidence", "reason")]
human += [x for k in SCOPE for x in s[k]]
for secret in sensitive_values:
if type(secret) is not str: _fail("invalid sensitive values")
if secret and any(secret in value for value in human):
_fail("sensitive value in human data")
states={c["result"] for c in criteria}
if result["verdict"] == "approve" and (states != {"satisfied"} or any(s[k] for k in SCOPE)): _fail("verdict contradiction")
if result["verdict"] == "request_changes" and not ({"unsatisfied", "indeterminate"} & states): _fail("verdict contradiction")
if result["verdict"] == "blocked_spec" and not (s["spec_conflicts"] or "indeterminate" in states): _fail("verdict contradiction")
return result
def _sha(b): return hashlib.sha256(b).hexdigest()
def _human_paths(xs):
if not xs:
return "없음"
# Quote complete list elements only when plain comma-separated prose is ambiguous.
unsafe = "<>@`[]{}*|~"
if any(", " in x or "; " in x or x == "없음" or '"' in x or any(c in x for c in unsafe) for x in xs):
def quoted(value):
text = json.dumps(value, ensure_ascii=False)
for char in unsafe:
text = text.replace(char, "\\u%04x" % ord(char))
return text
return ", ".join(quoted(x) for x in xs)
return ", ".join(xs)
def _parse_list(text):
if text == "없음":
return []
if not text.startswith('"'):
return text.split(", ")
decoder = json.JSONDecoder()
out = []
while text:
try:
value, end = decoder.raw_decode(text)
except ValueError:
_fail("invalid list encoding")
if type(value) is not str:
_fail("invalid list encoding")
out.append(value)
text = text[end:]
if text:
if not text.startswith(", "):
_fail("invalid list encoding")
text = text[2:]
if not text:
_fail("invalid list encoding")
return out
def _parse_evidence(text):
# Find the first path/evidence separator outside quoted path elements.
quoted = escaped = False
for i, char in enumerate(text):
if quoted:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
quoted = False
elif char == '"':
quoted = True
elif text[i:i + 2] == "; ":
return _parse_list(text[:i]), text[i + 2:]
_fail("invalid criterion block")
def _marker(r, digest):
t=r["target"]
raw="\x00".join([t["owner"],t["repository"],str(t["issue"]),str(t["pull"]),t["head"],r["contract_sha256"],digest]).encode()
corr=_sha(raw)
return ("<!-- gitea-agent:implementation-spec-review/v1 owner=%s repository=%s issue=%d pr=%d head=%s contract-sha256=%s result-sha256=%s correlation=%s -->" %
(t["owner"],t["repository"],t["issue"],t["pull"],t["head"],r["contract_sha256"],digest,corr))
def render_review(result, allowed_paths, sensitive_values=(), *, declaration=None):
if type(result) is dict and type(result.get("target")) is dict:
_require_adoption(declaration, result["target"].get("head"), "spec-review")
r=validate_result(result, allowed_paths, sensitive_values); t=r["target"]
lines=["## 구현 명세 적합성 검토", "", f"- Source Issue: #{t['issue']}", f"- Pull Request: #{t['pull']}",
f"- Reviewed head: `{t['head']}`", f"- Contract SHA-256: `{r['contract_sha256']}`", f"- Verdict: `{r['verdict']}`", "",
"### Acceptance criteria 판정", ""]
for c in r["criteria"]:
lines += [f"- `{c['criterion']}` — `{c['result']}`", f" - 계약: {c['contract']}",
f" - 구현 근거: {_human_paths(c['implementation_paths'])}; {c['evidence']}", f" - 테스트 근거: {_human_paths(c['test_paths'])}",
f" - 판정 근거: {c['reason']}", ""]
s=r["scope"]
lines += ["### 범위 판정", "", f"- 누락: {_human_paths(s['missing'])}", f"- 범위 초과: {_human_paths(s['excess'])}",
f"- 명세 모순: {_human_paths(s['spec_conflicts'])}", "", "### 결론", "", r["conclusion"], ""]
body="\n".join(lines)
return body + "\n" + _marker(r, _sha(body.encode("utf-8"))) + "\n"
def parse_review(body, allowed_paths, sensitive_values=()):
if type(body) is not str or not body.endswith("\n"): _fail("invalid review body")
lines=body.splitlines()
if not lines or not lines[-1].startswith("<!-- gitea-agent:implementation-spec-review/v1 ") or not lines[-1].endswith(" -->"): _fail("invalid review marker")
marker=lines[-1]; m=re.fullmatch(r"<!-- gitea-agent:implementation-spec-review/v1 owner=([^ ]+) repository=([^ ]+) issue=(\d+) pr=(\d+) head=([0-9a-f]{40}) contract-sha256=([0-9a-f]{64}) result-sha256=([0-9a-f]{64}) correlation=([0-9a-f]{64}) -->", marker)
if not m: _fail("invalid review marker")
# Parse the stable prose emitted by render_review, then validate by re-rendering.
try:
if lines[0] != "## 구현 명세 적합성 검토" or lines[8] != "### Acceptance criteria 판정": _fail("invalid review body")
issue=int(lines[2][len("- Source Issue: #") :]); pull=int(lines[3][len("- Pull Request: #"):])
head=lines[4][len("- Reviewed head: `"):-1]; contract=lines[5][len("- Contract SHA-256: `"):-1]; verdict=lines[6][len("- Verdict: `"):-1]
i=9; criteria=[]
while i < len(lines) and lines[i] != "### 범위 판정":
if not lines[i]: i+=1; continue
cm=re.fullmatch(r"- `([^`]+)` — `([^`]+)`", lines[i]);
if not cm: _fail("invalid criterion block")
c={"criterion":cm.group(1),"result":cm.group(2)}
for key,prefix in (("contract"," - 계약: "),("implementation_paths"," - 구현 근거: "),("test_paths"," - 테스트 근거: "),("reason"," - 판정 근거: ")):
i+=1
if i>=len(lines) or not lines[i].startswith(prefix): _fail("invalid criterion block")
val=lines[i][len(prefix):]
if key == "implementation_paths":
c[key], c["evidence"] = _parse_evidence(val)
else:
c[key] = _parse_list(val) if key.endswith("paths") else val
c={"criterion":c["criterion"],"contract":c["contract"],"implementation_paths":c["implementation_paths"],"test_paths":c["test_paths"],"evidence":c["evidence"],"result":c["result"],"reason":c["reason"]}
criteria.append(c); i+=1
if i+6 >= len(lines) or lines[i+1] != "": _fail("invalid scope block")
scope={}; i += 1
for key,prefix in (("missing","- 누락: "),("excess","- 범위 초과: "),("spec_conflicts","- 명세 모순: ")):
i += 1
if not lines[i].startswith(prefix): _fail("invalid scope block")
scope[key] = _parse_list(lines[i][len(prefix):])
i+=2
if lines[i] != "### 결론" or lines[i+1] != "": _fail("invalid conclusion")
conclusion=lines[i+2]
except (IndexError, ValueError): _fail("invalid review body")
r={"schema":SCHEMA,"target":{"owner":m.group(1),"repository":m.group(2),"issue":issue,"pull":pull,"head":head},"contract_sha256":contract,"verdict":verdict,"criteria":criteria,"scope":scope,"conclusion":conclusion}
validate_result(r, allowed_paths, sensitive_values)
canonical=render_review(r, allowed_paths, sensitive_values, declaration={"mode": "none"})
if canonical != body or _sha(canonical[:canonical.rfind("<!--") - 1].encode()) != m.group(7): _fail("review digest mismatch")
if _marker(r,m.group(7)) != marker: _fail("review marker mismatch")
return r
def _lstat(path, kind=None):
try: st=os.lstat(path)
except OSError: _fail("cache path unavailable")
if stat.S_ISLNK(st.st_mode): _fail("cache symlink")
if kind and not kind(st.st_mode): _fail("cache path type")
return st
def _safe_name(x):
if type(x) is not str or not x or x in (".", "..") or "/" in x or "\\" in x or CONTROL.search(x):
_fail("invalid cache identifier")
def _role_group_ok(st, uid):
if not (st.st_mode & stat.S_IWGRP):
return True
try:
group = grp.getgrgid(st.st_gid)
role = pwd.getpwuid(uid)
accounts = pwd.getpwall()
if group.gr_gid != st.st_gid or role.pw_uid != uid or not accounts:
return False
members = set(group.gr_mem)
members.update(account.pw_name for account in accounts if account.pw_gid == st.st_gid)
if role.pw_gid == st.st_gid:
members.add(role.pw_name)
return members == {role.pw_name}
except (KeyError, OSError):
return False
def _optional_stat(path, kind):
try:
os.lstat(path)
except FileNotFoundError:
return None
except OSError:
_fail("cache path unavailable")
return _lstat(path, kind)
def validate_cache_paths(contract_path, owner, repository, issue, uid=None):
"""Inspect existing paths only; acquire locks and verify Git separately."""
_safe_name(owner)
_safe_name(repository)
if type(issue) is not int or issue < 1:
_fail("invalid cache issue")
uid = os.getuid() if uid is None else uid
if type(uid) is not int or uid < 0:
_fail("invalid role UID")
try:
raw = os.fspath(contract_path)
except TypeError:
_fail("invalid cache contract path")
if type(raw) is not str or not raw.startswith("/") or any(x in (".", "..", "") for x in raw.split("/")[1:]):
_fail("invalid cache contract path")
cp = Path(raw)
if tuple(cp.parts[-3:]) != ("harness", "operations", "repository-cache.md"):
_fail("invalid cache contract path")
root = cp.parent.parent.parent
for ancestor in reversed(cp.parents):
_lstat(ancestor, stat.S_ISDIR)
_lstat(cp, stat.S_ISREG)
# NFS permits a different root owner, but never a world-writable root.
if os.lstat(root).st_mode & stat.S_IWOTH:
_fail("invalid harness root")
rs = root / "repositories"
rst = _lstat(rs, stat.S_ISDIR)
if rst.st_uid != uid or rst.st_mode & stat.S_IWOTH or not _role_group_ok(rst, uid):
_fail("invalid repositories directory")
base = rs / owner / repository
for path in (rs / owner, base, base / "locks"):
st = _lstat(path, stat.S_ISDIR)
if st.st_uid != uid or stat.S_IMODE(st.st_mode) != 0o700:
_fail("invalid cache directory")
git = base / "repository.git"
gst = _lstat(git, stat.S_ISDIR)
if gst.st_uid != uid or gst.st_mode & stat.S_IWOTH:
_fail("invalid repository cache")
# Bootstrap paths are optional for an already-existing cache.
bootstrap_root = rs / ".locks"
if _optional_stat(bootstrap_root, stat.S_ISDIR) is not None:
for path in (bootstrap_root, bootstrap_root / owner):
st = _optional_stat(path, stat.S_ISDIR)
if st is not None and (st.st_uid != uid or stat.S_IMODE(st.st_mode) != 0o700):
_fail("invalid bootstrap directory")
bootstrap = bootstrap_root / owner / (repository + ".bootstrap.lock")
st = _optional_stat(bootstrap, stat.S_ISREG)
if st is not None and (st.st_uid != uid or stat.S_IMODE(st.st_mode) != 0o600):
_fail("invalid bootstrap lock")
out = {"root": str(root), "repositories": str(rs), "owner": str(rs / owner),
"repository": str(base), "repository_git": str(git), "locks": str(base / "locks")}
for name in (f"issue-{issue}.lock", "repository.lock"):
path = base / "locks" / name
st = _optional_stat(path, stat.S_ISREG)
if st is not None:
if st.st_uid != uid or stat.S_IMODE(st.st_mode) != 0o600:
_fail("invalid lock file")
out[name] = str(path)
return out
def collect_pages(fetch_page, key, page_size=50, max_pages=10000):
"""Consume provider-verified envelopes with one-based page numbers."""
if key not in ("id", "filename", "sha") or type(key) is not str:
_fail("invalid identity key")
if type(page_size) is not int or page_size < 1 or type(max_pages) is not int or max_pages < 1:
_fail("invalid pagination limits")
all_items, seen, total = [], set(), None
for page in range(1, max_pages + 1):
try:
env = fetch_page(page)
except Exception:
_fail("page read failed")
if type(env) is not dict or "items" not in env or set(env) - {"items", "total_count", "has_more"}:
_fail("invalid page envelope")
declared, more = env.get("total_count"), env.get("has_more")
if declared is not None and (type(declared) is not int or declared < 0):
_fail("invalid page count")
if more is not None and type(more) is not bool:
_fail("invalid page metadata")
if declared is not None:
if total is not None and total != declared:
_fail("conflicting page count")
total = declared
items = env["items"]
if items is None and declared == 0 and more is False:
items = []
if type(items) is not list or len(items) > page_size:
_fail("invalid page items")
for item in items:
if type(item) is not dict or key not in item:
_fail("invalid page identity")
ident = item[key]
if key == "id":
_positive_id(ident)
elif key == "filename":
_path(ident)
elif type(ident) is not str or not HEX40.fullmatch(ident):
_fail("invalid page identity")
if ident in seen:
_fail("duplicate page identity")
seen.add(ident)
all_items.append(item)
if total is not None and len(all_items) > total:
_fail("page count exceeded")
if more is True:
if len(items) != page_size or (total is not None and len(all_items) >= total):
_fail("truncated or inconsistent continuation")
continue
done = more is False or (more is None and (
(total is not None and len(all_items) == total) or len(items) < page_size))
if done:
if total is not None and len(all_items) != total:
_fail("truncated pages")
return all_items
_fail("pagination limit reached")
def _json_bytes(value):
def check(item):
if type(item) is dict:
if any(type(key) is not str for key in item):
_fail("invalid JSON object")
for child in item.values():
check(child)
elif type(item) is list:
for child in item:
check(child)
elif item is not None and type(item) not in (str, int, float, bool):
_fail("invalid JSON value")
check(value)
try:
return json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False,
separators=(",", ":")).encode("utf-8")
except (TypeError, ValueError, UnicodeError):
_fail("invalid JSON value")
def _same(a, b):
return _json_bytes(a) == _json_bytes(b)
def verify_unchanged(before, after):
if not _same(before, after):
_fail("snapshot changed")
return True
def _positive_id(value):
if type(value) is not int or value < 1:
_fail("invalid identity")
return value
def _user_id(record):
if type(record) is not dict or type(record.get("user")) is not dict:
_fail("invalid author identity")
return _positive_id(record["user"].get("id"))
def _index(collection, key="id"):
if type(collection) is not list:
_fail("invalid collection")
out = {}
for item in collection:
if type(item) is not dict or key not in item:
_fail("invalid collection identity")
ident = item[key]
if key == "id":
_positive_id(ident)
elif key == "filename":
_path(ident)
_str(item.get("status"), "file status", 1, 100)
if ident in out:
_fail("duplicate collection identity")
out[ident] = item
return out
def _timestamp(value):
from datetime import datetime
if type(value) is not str or not re.fullmatch(
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})", value):
_fail("invalid review timestamp")
try:
datetime.fromisoformat(value)
except ValueError:
_fail("invalid review timestamp")
return value
def _without(value, fields):
return {key: child for key, child in value.items() if key not in fields}
def _target_snapshot(snapshot, target):
fields = ("issue", "issue_comments", "pr", "pr_comments", "reviews", "timeline", "files")
_obj(snapshot, fields, "publication snapshot")
issue, pr = snapshot["issue"], snapshot["pr"]
if type(issue) is not dict or type(pr) is not dict:
_fail("invalid source snapshot")
if _positive_id(issue.get("number")) != target["issue"] or _positive_id(pr.get("number")) != target["pull"]:
_fail("snapshot target mismatch")
fullname = target["owner"] + "/" + target["repository"]
if type(issue.get("repository")) is not dict or issue["repository"].get("full_name") != fullname:
_fail("snapshot repository mismatch")
if type(pr.get("base")) is not dict or type(pr["base"].get("repo")) is not dict or pr["base"]["repo"].get("full_name") != fullname:
_fail("snapshot repository mismatch")
if type(pr.get("head")) is not dict or pr["head"].get("sha") != target["head"]:
_fail("snapshot head mismatch")
if pr.get("state") != "open" or pr.get("merged") is not False:
_fail("snapshot PR state mismatch")
from urllib.parse import urlsplit
url = pr.get("html_url")
if type(url) is not str:
_fail("invalid PR URL")
try:
parts = urlsplit(url)
except ValueError:
_fail("invalid PR URL")
if parts.scheme not in ("https", "http") or not parts.hostname or parts.username or parts.password or parts.query or parts.fragment or not parts.path.endswith("/" + fullname + "/pulls/" + str(target["pull"])):
_fail("snapshot PR URL mismatch")
for key in ("issue_comments", "pr_comments", "reviews", "timeline"):
_index(snapshot[key])
_index(snapshot["files"], "filename")
return url
def _projection_contract(contract):
if contract is None:
return False, False, None, False
if type(contract) is not dict or type(contract.get("version")) is not dict or contract["version"].get("version") != "1.27.3":
_fail("unsupported provider projection contract")
flags = []
for key in ("supersession_same_author_only", "already_dismissed_review_timestamp_projection",
"requested_reviewers_presentation_order"):
value = contract.get(key, False)
if type(value) is not bool:
_fail("invalid provider projection contract")
flags.append(value)
official = contract.get("expected_official")
if official is not None and type(official) is not bool:
_fail("invalid provider review policy")
return flags[0], flags[1], official, flags[2]
def verify_publication(before, after, exact_review, result, allowed_paths, reviewer_id, inline_comments, provider_contract=None, *, expected_pr_url):
"""Verify saved readbacks of one native create; never authenticate or publish."""
validate_result(result, allowed_paths)
_positive_id(reviewer_id)
if type(inline_comments) is not list or inline_comments:
_fail("inline comments are not supported")
target = result["target"]
_str(expected_pr_url, "canonical PR URL", 1, 4000)
url = _target_snapshot(before, target)
if url != expected_pr_url or _target_snapshot(after, target) != expected_pr_url:
_fail("snapshot PR URL mismatch")
if _user_id(before["pr"]) == reviewer_id:
_fail("reviewer is PR author")
supersede, retouch, expected_official, reviewer_presentation_order = _projection_contract(provider_contract)
old_reviews, reviews = _index(before["reviews"]), _index(after["reviews"])
if type(exact_review) is not dict:
_fail("invalid exact review")
rid = _positive_id(exact_review.get("id"))
if set(reviews) - set(old_reviews) != {rid} or set(old_reviews) - set(reviews):
_fail("review cardinality mismatch")
if [x for x in reviews if x != rid] != list(old_reviews):
_fail("previous review order changed")
er = reviews[rid]
if not _same(er, exact_review):
_fail("exact review readback mismatch")
expected = {"approve": "APPROVED", "request_changes": "REQUEST_CHANGES", "blocked_spec": "COMMENT"}[result["verdict"]]
if er.get("body") != render_review(result, allowed_paths, declaration={"mode": "none"}) or er.get("commit_id") != target["head"] or er.get("state") != expected:
_fail("review binding mismatch")
if _user_id(er) != reviewer_id or er.get("pull_request_url") != url:
_fail("review identity mismatch")
if er.get("stale") is not False or er.get("dismissed") is not False or type(er.get("official")) is not bool:
_fail("review status mismatch")
if type(er.get("comments_count")) is not int or er["comments_count"] != 0:
_fail("review comment count mismatch")
if expected_official is not None and er["official"] != expected_official:
_fail("review policy mismatch")
submitted = _timestamp(er.get("submitted_at"))
_timestamp(er.get("updated_at"))
old_events, events = _index(before["timeline"]), _index(after["timeline"])
additions = set(events) - set(old_events)
if len(additions) != 1 or set(old_events) - set(events) or not _same(after["timeline"][:len(before["timeline"])], before["timeline"]):
_fail("timeline cardinality or preservation mismatch")
event = after["timeline"][-1]
if event["id"] not in additions or _positive_id(event.get("review_id")) != rid or event.get("type") != "review":
_fail("timeline review binding mismatch")
if _user_id(event) != reviewer_id or event.get("body") != er["body"] or event.get("pull_request_url") != url:
_fail("timeline author or body mismatch")
created = _timestamp(event.get("created_at"))
event_updated = _timestamp(event.get("updated_at"))
# Bind the event by exact record ID/author/body, not coincident wall clocks.
causal_times = {submitted, created, event_updated}
for section in ("issue", "pr"):
b, a = before[section], after[section]
permitted = {"updated_at"} if section == "issue" else {"updated_at", "review_comments", "requested_reviewers"}
if not _same(_without(b, permitted), _without(a, permitted)) or set(b) != set(a):
_fail("source or PR drift")
if not _same(b.get("updated_at"), a.get("updated_at")) and a.get("updated_at") not in causal_times:
_fail("timestamp projection mismatch")
bpr, apr = before["pr"], after["pr"]
old_count, count = bpr.get("review_comments"), apr.get("review_comments")
if type(old_count) is not int or old_count < 0 or type(count) is not int or count != old_count + 1:
_fail("review count projection mismatch")
old_requested = _index(bpr.get("requested_reviewers"))
requested = _index(apr.get("requested_reviewers"))
if reviewer_presentation_order:
# Only this explicitly version-bound collection has presentation order.
if set(old_requested) - set(requested) or (set(requested) - set(old_requested)) - {reviewer_id}:
_fail("requested reviewers projection mismatch")
for old_id, old in old_requested.items():
if not _same(old, requested[old_id]):
_fail("previous requested reviewers changed")
if reviewer_id in set(requested) - set(old_requested) and not _same(requested[reviewer_id], exact_review.get("user")):
_fail("new requested reviewer user mismatch")
elif not _same(bpr["requested_reviewers"], apr["requested_reviewers"]):
if reviewer_id in old_requested or set(requested) - set(old_requested) != {reviewer_id} or set(old_requested) - set(requested):
_fail("requested reviewers projection mismatch")
if not _same([x for x in apr["requested_reviewers"] if x["id"] != reviewer_id], bpr["requested_reviewers"]):
_fail("previous requested reviewers changed")
for collection in ("issue_comments", "pr_comments"):
if not _same(before[collection], after[collection]):
_fail("comment collection changed")
if not _same(_index(before["files"], "filename"), _index(after["files"], "filename")):
_fail("changed file set drift")
projected = []
for old_id, old in old_reviews.items():
current = reviews[old_id]
if _same(old, current):
continue
if _user_id(old) != reviewer_id or old.get("pull_request_url") != url or old.get("state") not in ("APPROVED", "REQUEST_CHANGES"):
_fail("previous review drift")
fields = {"official", "dismissed", "updated_at"}
if set(old) != set(current) or not _same(_without(old, fields), _without(current, fields)) or current.get("updated_at") not in causal_times:
_fail("previous review drift")
superseded = supersede and old.get("official") is True and old.get("dismissed") is False
retouched = retouch and old.get("official") is False and old.get("dismissed") is True
if not (superseded or retouched) or current.get("official") is not False or current.get("dismissed") is not True:
_fail("previous review projection not authorized")
projected.append(old_id)
return {"review_id": rid, "state": expected, "review_records_added": 1,
"timeline_events_added": 1, "projected_review_ids": projected, "read_only": True}
def _read(path):
try:
with open(path, encoding="utf-8", newline="") as stream:
return stream.read()
except (OSError, ValueError, UnicodeError):
_fail("input unavailable")
def _json_file(path): return loads_json(_read(path))
def _emit_error(exc):
sys.stderr.write(json.dumps({"error":"invalid input"}, ensure_ascii=False)+"\n"); return 2
class _Parser(argparse.ArgumentParser):
def error(self, message):
_fail("invalid command arguments")
def main(argv=None):
p=_Parser(prog="review_support", add_help=True); sub=p.add_subparsers(dest="cmd",required=True)
def common(name):
q=sub.add_parser(name); q.add_argument("--result",required=True); q.add_argument("--allowed-paths",required=True); return q
common("validate-result"); common("render")
q=sub.add_parser("parse"); q.add_argument("--body",required=True); q.add_argument("--allowed-paths",required=True)
render_parser = sub.choices["render"]
adoption = render_parser.add_mutually_exclusive_group(required=True)
adoption.add_argument("--no-delegated-evidence", action="store_true")
adoption.add_argument("--delegation-adoption")
q=sub.add_parser("check-cache"); q.add_argument("--contract-path",required=True); q.add_argument("--owner",required=True); q.add_argument("--repository",required=True); q.add_argument("--issue",required=True,type=int)
q=sub.add_parser("check-pages"); q.add_argument("--pages",required=True); q.add_argument("--key",required=True)
q.add_argument("--page-size", type=int, default=50); q.add_argument("--max-pages", type=int, default=10000)
q=sub.add_parser("check-unchanged"); q.add_argument("--before",required=True); q.add_argument("--after",required=True)
q=sub.add_parser("verify-publication");
for n in ("before","after","exact-review","result","allowed-paths","inline-comments"): q.add_argument("--"+n,required=True)
q.add_argument("--reviewer-id",required=True,type=int); q.add_argument("--provider-contract")
q.add_argument("--expected-pr-url", required=True)
try:
a=p.parse_args(argv)
if a.cmd in ("validate-result","render"):
r=_json_file(a.result); paths=_json_file(a.allowed_paths); validate_result(r,paths)
if a.cmd == "validate-result": out=r
else:
if a.no_delegated_evidence:
declaration={"mode":"none"}
else:
repo_root=Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0,str(repo_root))
from harness.lib.delegation_evidence import _json_file as load_declaration
declaration=load_declaration(a.delegation_adoption)
out=render_review(r,paths,declaration=declaration)
print(json.dumps(out,ensure_ascii=False,sort_keys=True) if a.cmd=="validate-result" else out,end="" if a.cmd=="render" else "\n")
elif a.cmd=="parse": print(json.dumps(parse_review(_read(a.body),_json_file(a.allowed_paths)),ensure_ascii=False,sort_keys=True))
elif a.cmd=="check-cache": print(json.dumps(validate_cache_paths(a.contract_path,a.owner,a.repository,a.issue),sort_keys=True))
elif a.cmd=="check-pages":
pages=_json_file(a.pages)
if type(pages) is not list: _fail("invalid pages")
used = []
def f(i):
used.append(i)
return pages[i - 1]
items = collect_pages(f, a.key, a.page_size, a.max_pages)
if len(used) != len(pages): _fail("extra saved pages")
print(json.dumps(items,ensure_ascii=False,sort_keys=True))
elif a.cmd=="check-unchanged": print(json.dumps({"unchanged":verify_unchanged(_json_file(a.before),_json_file(a.after))}))
else:
print(json.dumps(verify_publication(_json_file(a.before),_json_file(a.after),_json_file(a.exact_review),_json_file(a.result),_json_file(a.allowed_paths),a.reviewer_id,_json_file(a.inline_comments),_json_file(a.provider_contract) if a.provider_contract else None, expected_pr_url=a.expected_pr_url),sort_keys=True))
return 0
except (ValidationError, OSError, ValueError, TypeError): return _emit_error(None)
if __name__ == "__main__": sys.exit(main())