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

1"""Suggested-patch output for verified findings (issue #10). 

2 

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: 

6 

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. 

12 

13Pure and deterministic: given the same groups it renders the same markdown. 

14""" 

15from __future__ import annotations 

16 

17from dataclasses import dataclass 

18 

19from .consensus import BUCKET_REJECTED, FindingGroup 

20 

21 

22@dataclass 

23class PatchSuggestion: 

24 file: str 

25 line: int | None 

26 severity: str 

27 claim: str 

28 suggested_fix: str 

29 

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 

35 

36 

37def patch_suggestions(groups: list[FindingGroup]) -> list[PatchSuggestion]: 

38 """Return one suggestion per VERIFIED group that carries a suggested fix. 

39 

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 

63 

64 

65def render_patch_suggestions(groups: list[FindingGroup]) -> str: 

66 """Render the "Suggested patches" markdown section, or "" when there are none. 

67 

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"