Coverage for src/ai_jury/incremental.py: 100%
26 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 23:18 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 23:18 +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"""
14from __future__ import annotations
16import re
18# Hidden marker embedded in the jury summary comment recording the SHA that
19# was reviewed, so a later run can compute the incremental range.
20_MARKER_RE = re.compile(r"<!--\s*arc-reviewed-sha:([0-9a-fA-F]{7,40})\s*-->")
22MODE_FULL = "full"
23MODE_INCREMENTAL = "incremental"
26def reviewed_sha_marker(sha: str) -> str:
27 """Return the hidden HTML-comment marker recording the reviewed head SHA."""
28 return f"<!-- arc-reviewed-sha:{sha} -->"
31def parse_reviewed_sha(comment_bodies) -> str | None:
32 """Return the most recent reviewed SHA across jury comment bodies, or None.
34 Scans every body for the marker and returns the LAST match found (later
35 comments override earlier ones), so a fresh re-review marker wins.
36 """
37 last: str | None = None
38 for body in comment_bodies or []:
39 for m in _MARKER_RE.finditer(body or ""):
40 last = m.group(1)
41 return last
44def decide_review(prev_sha: str | None, head_sha: str | None) -> tuple[str, str]:
45 """Decide review scope from the previous reviewed SHA and the current head.
47 Returns ``(mode, reason)`` where ``mode`` is ``"full"`` or ``"incremental"``.
48 Falls back to a full review whenever incremental is not safely possible: no
49 prior marker, unknown head, or an unchanged head (nothing new to review).
50 """
51 if not prev_sha:
52 return MODE_FULL, "no prior jury marker found — full review"
53 if not head_sha:
54 return MODE_FULL, "current head SHA unavailable — full review"
55 if prev_sha == head_sha:
56 return MODE_FULL, "head unchanged since last review — full review"
57 return (
58 MODE_INCREMENTAL,
59 f"incremental: reviewing {prev_sha[:7]}..{head_sha[:7]}",
60 )
63def compare_range(prev_sha: str, head_sha: str) -> str:
64 """Return the ``base...head`` range spec for the GitHub compare API."""
65 return f"{prev_sha}...{head_sha}"
68def scope_note(mode: str, reason: str) -> str:
69 """Render the user-facing review-scope line for the report (issue #9)."""
70 label = "Incremental" if mode == MODE_INCREMENTAL else "Full"
71 return f"**Review scope:** {label} — {reason}"