Skip to content

Commit af0d65d

Browse files
committed
agent: add code review dry-run prototype
This change adds a deterministic code review Agent example that can parse diffs, run fake-model review rules, apply sandbox governance, simulate sandbox execution, persist review data to SQLite, and render JSON and Markdown reports. It includes fixtures and tests for clean diffs, hard-coded secrets, async task handling, database lifecycle issues, missing tests, duplicate findings, sandbox failures, and secret redaction. Updates #92 RELEASE NOTES: Added a code review Agent dry-run example with fake sandbox execution and SQLite-backed reports.
1 parent dc34455 commit af0d65d

41 files changed

Lines changed: 3142 additions & 220 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/code_review_agent/README.md

Lines changed: 145 additions & 220 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Code review agent example package."""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Dry-run code review agent MVP helpers."""
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Small unified diff parser for the code review dry-run example."""
7+
8+
from __future__ import annotations
9+
10+
import re
11+
from pathlib import Path
12+
13+
from .schemas import ChangedLine
14+
from .schemas import ChangedLineKind
15+
from .schemas import DiffFile
16+
from .schemas import DiffHunk
17+
from .schemas import ParsedDiff
18+
19+
_HUNK_RE = re.compile(
20+
r"^@@ -(?P<old_start>\d+)(?:,(?P<old_count>\d+))? "
21+
r"\+(?P<new_start>\d+)(?:,(?P<new_count>\d+))? @@(?P<section>.*)$"
22+
)
23+
24+
25+
def read_diff_file(path: Path) -> str:
26+
"""Read a unified diff file as UTF-8 text."""
27+
return path.read_text(encoding="utf-8")
28+
29+
30+
def parse_unified_diff(diff_text: str) -> ParsedDiff:
31+
"""Parse a small, git-style unified diff.
32+
33+
The parser intentionally supports the subset needed by the dry-run MVP:
34+
git file headers, old/new file markers, hunk headers, line anchors, binary
35+
markers, and rename metadata.
36+
"""
37+
files: list[DiffFile] = []
38+
current_file: DiffFile | None = None
39+
current_hunk: DiffHunk | None = None
40+
old_line: int | None = None
41+
new_line: int | None = None
42+
43+
def finish_file() -> None:
44+
nonlocal current_file, current_hunk, old_line, new_line
45+
if current_file is not None:
46+
_finalize_status(current_file)
47+
files.append(current_file)
48+
current_file = None
49+
current_hunk = None
50+
old_line = None
51+
new_line = None
52+
53+
for raw_line in diff_text.splitlines():
54+
if raw_line.startswith("diff --git "):
55+
finish_file()
56+
old_path, new_path = _parse_git_header(raw_line)
57+
current_file = DiffFile(old_path=old_path, new_path=new_path, status="modified")
58+
continue
59+
60+
if current_file is None:
61+
continue
62+
63+
if raw_line.startswith("rename from "):
64+
current_file.old_path = raw_line.removeprefix("rename from ").strip()
65+
current_file.status = "renamed"
66+
continue
67+
68+
if raw_line.startswith("rename to "):
69+
current_file.new_path = raw_line.removeprefix("rename to ").strip()
70+
current_file.status = "renamed"
71+
continue
72+
73+
if raw_line.startswith("Binary files ") or raw_line == "GIT binary patch":
74+
current_file.is_binary = True
75+
current_file.status = "binary"
76+
continue
77+
78+
if raw_line.startswith("--- "):
79+
current_file.old_path = _normalize_marker_path(raw_line[4:].strip())
80+
continue
81+
82+
if raw_line.startswith("+++ "):
83+
current_file.new_path = _normalize_marker_path(raw_line[4:].strip())
84+
continue
85+
86+
hunk_match = _HUNK_RE.match(raw_line)
87+
if hunk_match:
88+
current_hunk = DiffHunk(
89+
old_start=int(hunk_match.group("old_start")),
90+
old_count=int(hunk_match.group("old_count") or "1"),
91+
new_start=int(hunk_match.group("new_start")),
92+
new_count=int(hunk_match.group("new_count") or "1"),
93+
section=hunk_match.group("section").strip(),
94+
)
95+
current_file.hunks.append(current_hunk)
96+
old_line = current_hunk.old_start
97+
new_line = current_hunk.new_start
98+
continue
99+
100+
if current_hunk is None:
101+
continue
102+
103+
if raw_line == r"\ No newline at end of file":
104+
continue
105+
106+
prefix = raw_line[:1]
107+
text = raw_line[1:] if prefix in {" ", "+", "-"} else raw_line
108+
109+
if prefix == "+":
110+
current_hunk.changed_lines.append(
111+
ChangedLine(old_line_number=None, new_line_number=new_line, kind=ChangedLineKind.ADDED, text=text)
112+
)
113+
if new_line is not None:
114+
new_line += 1
115+
continue
116+
117+
if prefix == "-":
118+
current_hunk.changed_lines.append(
119+
ChangedLine(old_line_number=old_line, new_line_number=None, kind=ChangedLineKind.REMOVED, text=text)
120+
)
121+
if old_line is not None:
122+
old_line += 1
123+
continue
124+
125+
current_hunk.changed_lines.append(
126+
ChangedLine(old_line_number=old_line, new_line_number=new_line, kind=ChangedLineKind.CONTEXT, text=text)
127+
)
128+
if old_line is not None:
129+
old_line += 1
130+
if new_line is not None:
131+
new_line += 1
132+
133+
finish_file()
134+
return ParsedDiff(files=files)
135+
136+
137+
def _parse_git_header(line: str) -> tuple[str | None, str | None]:
138+
parts = line.split()
139+
if len(parts) < 4:
140+
return None, None
141+
return _strip_ab_prefix(parts[2]), _strip_ab_prefix(parts[3])
142+
143+
144+
def _normalize_marker_path(path: str) -> str | None:
145+
if path == "/dev/null":
146+
return None
147+
return _strip_ab_prefix(path.split("\t", 1)[0])
148+
149+
150+
def _strip_ab_prefix(path: str) -> str:
151+
if path.startswith("a/") or path.startswith("b/"):
152+
return path[2:]
153+
return path
154+
155+
156+
def _finalize_status(diff_file: DiffFile) -> None:
157+
if diff_file.is_binary:
158+
diff_file.status = "binary"
159+
elif diff_file.old_path is None and diff_file.new_path is not None:
160+
diff_file.status = "added"
161+
elif diff_file.old_path is not None and diff_file.new_path is None:
162+
diff_file.status = "deleted"
163+
elif diff_file.status != "renamed":
164+
diff_file.status = "modified"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Deterministic fake reviewer for code review dry-run tests."""
7+
8+
from __future__ import annotations
9+
10+
from .rules import review_with_rules
11+
from .schemas import ParsedDiff
12+
from .schemas import ReviewFinding
13+
14+
15+
def review_with_fake_model(parsed_diff: ParsedDiff) -> list[ReviewFinding]:
16+
"""Return deterministic findings for added lines in a parsed diff."""
17+
return review_with_rules(parsed_diff)

0 commit comments

Comments
 (0)