Coverage for src/ai_jury/incremental.py: 100%
26 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 20:29 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 20:29 +0000
1"""Incremental review mode for updated PRs (issue #9).
3Re-reviewing a large PR's full diff on every push is slow and costly. Incremental
4mode reviews only what changed since the jury last ran: it records the
5reviewed head SHA in a hidden marker on its summary comment, and on the next run
6compares that SHA to the current head to fetch just the new range.
8This module holds the PURE, network-free core — embedding/parsing the marker and
9deciding the review scope — so it is fully unit-testable. The thin GitHub calls
10(fetch head SHA, fetch comment bodies, fetch the range diff) live in
11``github.py`` and the CLI; this module never touches the network.
12"""
13from __future__ import annotations
15import re
17# Hidden marker embedded in the jury summary comment recording the SHA that
18# was reviewed, so a later run can compute the incremental range.
19_MARKER_RE = re.compile(r"<!--\s*arc-reviewed-sha:([0-9a-fA-F]{7,40})\s*-->")
21MODE_FULL = "full"
22MODE_INCREMENTAL = "incremental"
25def reviewed_sha_marker(sha: str) -> str:
26 """Return the hidden HTML-comment marker recording the reviewed head SHA."""
27 return f"<!-- arc-reviewed-sha:{sha} -->"
30def parse_reviewed_sha(comment_bodies) -> str | None:
31 """Return the most recent reviewed SHA across jury comment bodies, or None.
33 Scans every body for the marker and returns the LAST match found (later
34 comments override earlier ones), so a fresh re-review marker wins.
35 """
36 last: str | None = None
37 for body in comment_bodies or []:
38 for m in _MARKER_RE.finditer(body or ""):
39 last = m.group(1)
40 return last
43def decide_review(prev_sha: str | None, head_sha: str | None) -> tuple[str, str]:
44 """Decide review scope from the previous reviewed SHA and the current head.
46 Returns ``(mode, reason)`` where ``mode`` is ``"full"`` or ``"incremental"``.
47 Falls back to a full review whenever incremental is not safely possible: no
48 prior marker, unknown head, or an unchanged head (nothing new to review).
49 """
50 if not prev_sha:
51 return MODE_FULL, "no prior jury marker found — full review"
52 if not head_sha:
53 return MODE_FULL, "current head SHA unavailable — full review"
54 if prev_sha == head_sha:
55 return MODE_FULL, "head unchanged since last review — full review"
56 return (
57 MODE_INCREMENTAL,
58 f"incremental: reviewing {prev_sha[:7]}..{head_sha[:7]}",
59 )
62def compare_range(prev_sha: str, head_sha: str) -> str:
63 """Return the ``base...head`` range spec for the GitHub compare API."""
64 return f"{prev_sha}...{head_sha}"
67def scope_note(mode: str, reason: str) -> str:
68 """Render the user-facing review-scope line for the report (issue #9)."""
69 label = "Incremental" if mode == MODE_INCREMENTAL else "Full"
70 return f"**Review scope:** {label} — {reason}"