Coverage for src/ai_jury/patches.py: 100%
41 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"""Suggested-patch output for verified findings (issue #10).
3The jury identifies issues; this renders a *separate*, opt-in "suggested
4patches" section that turns verified findings into concrete, inspectable fix
5suggestions. It is deliberately conservative:
7- only VERIFIED findings (a consensus group the verifier confirmed) produce a
8 suggestion — unverified or rejected findings never do;
9- suggestions are rendered as clearly-labelled blocks tied to one finding;
10- nothing is ever applied automatically (read-only by design); the output is for
11 a human to inspect, copy, or adapt.
13Pure and deterministic: given the same groups it renders the same markdown.
14"""
15from __future__ import annotations
17from dataclasses import dataclass
19from .consensus import BUCKET_REJECTED, FindingGroup
22@dataclass
23class PatchSuggestion:
24 file: str
25 line: int | None
26 severity: str
27 claim: str
28 suggested_fix: str
30 def location(self) -> str:
31 loc = self.file or "?"
32 if self.line is not None:
33 loc = f"{loc}:{self.line}"
34 return loc
37def patch_suggestions(groups: list[FindingGroup]) -> list[PatchSuggestion]:
38 """Return one suggestion per VERIFIED group that carries a suggested fix.
40 A group qualifies only when the verifier marked it ``verified`` (not
41 unsupported/disputed and not merely unverified) AND its representative
42 finding has a non-empty ``suggested_fix``. Order follows the input group
43 order (already severity-sorted by the consensus pass).
44 """
45 out: list[PatchSuggestion] = []
46 for g in groups:
47 if getattr(g, "status", "") != "verified" or g.bucket == BUCKET_REJECTED:
48 continue
49 rep = g.representative
50 fix = (getattr(rep, "suggested_fix", "") or "").strip()
51 if not rep or not fix:
52 continue
53 out.append(
54 PatchSuggestion(
55 file=rep.file or "",
56 line=rep.line,
57 severity=g.severity,
58 claim=(rep.claim or "").strip(),
59 suggested_fix=fix,
60 )
61 )
62 return out
65def render_patch_suggestions(groups: list[FindingGroup]) -> str:
66 """Render the "Suggested patches" markdown section, or "" when there are none.
68 Kept separate from the default report so the standard review flow stays
69 read-only; the CLI emits this only under ``--suggest-patches``.
70 """
71 suggestions = patch_suggestions(groups)
72 if not suggestions:
73 return ""
74 lines = [
75 "## Suggested patches",
76 "",
77 "_Opt-in, read-only suggestions for **verified** findings only. Inspect "
78 "before applying — nothing here is applied automatically._",
79 "",
80 ]
81 for s in suggestions:
82 lines.append(f"### {s.location()} — [{s.severity}] {s.claim}")
83 lines.append("")
84 lines.append("> Verified by the jury.")
85 lines.append("")
86 lines.append("```suggestion")
87 lines.append(s.suggested_fix)
88 lines.append("```")
89 lines.append("")
90 return "\n".join(lines).rstrip() + "\n"