|
| 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 review pipeline: parse -> govern -> sandbox -> merge -> persist -> report.""" |
| 7 | +import json |
| 8 | +import time |
| 9 | +from dataclasses import dataclass, field |
| 10 | +from pathlib import Path |
| 11 | + |
| 12 | +from trpc_agent_sdk.skills import create_default_skill_repository |
| 13 | + |
| 14 | +from agent.agent import create_review_agent |
| 15 | +from filters_cr.governance_filter import GovernanceToolFilter |
| 16 | +from storage.store import ReviewStore |
| 17 | + |
| 18 | +from .findings import Finding, dedupe, gate, severity_distribution |
| 19 | +from .governance import DEFAULT_ALLOWED_SCRIPTS, GovernanceEngine |
| 20 | +from .llm_review import run_llm_review |
| 21 | +from .report import build_report, write_reports |
| 22 | +from .sandbox import DIFF_WS_PATH, SandboxSession, create_runtime |
| 23 | + |
| 24 | +DEFAULT_SKILL_ROOT = str(Path(__file__).resolve().parents[1] / "skills" / "code-review") |
| 25 | + |
| 26 | +CHECKERS = [ |
| 27 | + ("check_security.py", "security"), |
| 28 | + ("check_async_leak.py", "async_resource_leak"), |
| 29 | + ("check_db_lifecycle.py", "db_lifecycle"), |
| 30 | + ("check_tests_missing.py", "missing_test"), |
| 31 | + ("check_secrets.py", "secret_leak"), |
| 32 | +] |
| 33 | + |
| 34 | + |
| 35 | +@dataclass |
| 36 | +class ReviewOptions: |
| 37 | + diff_text: str |
| 38 | + input_type: str |
| 39 | + input_ref: str |
| 40 | + runtime: str = "local" |
| 41 | + dry_run: bool = True |
| 42 | + db_url: str = "sqlite:///code_review.db" |
| 43 | + output_dir: str = "out" |
| 44 | + skill_root: str = DEFAULT_SKILL_ROOT |
| 45 | + checkers: list = field(default_factory=lambda: list(CHECKERS)) |
| 46 | + allowed_scripts: tuple = DEFAULT_ALLOWED_SCRIPTS |
| 47 | + timeout_sec: float = 60.0 |
| 48 | + enable_llm: bool = True |
| 49 | + |
| 50 | + |
| 51 | +@dataclass |
| 52 | +class ReviewResult: |
| 53 | + task_id: str |
| 54 | + report: dict |
| 55 | + json_path: str |
| 56 | + md_path: str |
| 57 | + |
| 58 | + |
| 59 | +def _parse_script_findings(stdout: str, warnings: list) -> list[Finding]: |
| 60 | + try: |
| 61 | + payload = json.loads(stdout) |
| 62 | + except (json.JSONDecodeError, ValueError): |
| 63 | + warnings.append("checker produced invalid JSON output") |
| 64 | + return [] |
| 65 | + findings = [] |
| 66 | + for raw in payload.get("findings", []): |
| 67 | + try: |
| 68 | + findings.append(Finding(**raw)) |
| 69 | + except (TypeError, ValueError): |
| 70 | + warnings.append(f"skipped malformed finding: {raw!r}") |
| 71 | + return findings |
| 72 | + |
| 73 | + |
| 74 | +async def run_review(opts: ReviewOptions) -> ReviewResult: |
| 75 | + """Run the full review. Sandbox failures degrade, never crash.""" |
| 76 | + start = time.monotonic() |
| 77 | + store = ReviewStore(db_url=opts.db_url) |
| 78 | + engine = GovernanceEngine(allowed_scripts=opts.allowed_scripts) |
| 79 | + task_id = await store.create_task(opts.input_type, opts.input_ref, |
| 80 | + opts.runtime, opts.dry_run) |
| 81 | + warnings: list[str] = [] |
| 82 | + filter_events = [] |
| 83 | + sandbox_outcomes = [] |
| 84 | + raw_findings: list[Finding] = [] |
| 85 | + diff_summary: dict = {} |
| 86 | + error_distribution: dict[str, int] = {} |
| 87 | + sandbox_ms = 0 |
| 88 | + try: |
| 89 | + runtime = await create_runtime(opts.runtime) |
| 90 | + if opts.runtime == "local": |
| 91 | + warnings.append("local runtime is a development fallback; " |
| 92 | + "use container or cube in production") |
| 93 | + session = SandboxSession(runtime, opts.skill_root, timeout_sec=opts.timeout_sec) |
| 94 | + await session.open(f"cr_{task_id[:12]}") |
| 95 | + try: |
| 96 | + await session.put_diff(opts.diff_text) |
| 97 | + scripts = [("parse_diff.py", "parse")] + list(opts.checkers) |
| 98 | + for script, category in scripts: |
| 99 | + decision = engine.check_script(script, [DIFF_WS_PATH]) |
| 100 | + filter_events.append(decision) |
| 101 | + await store.add_filter_event(task_id, decision.target, decision.decision, |
| 102 | + decision.rule, decision.reason) |
| 103 | + if decision.decision != "allow": |
| 104 | + continue |
| 105 | + outcome = await session.run_script(script) |
| 106 | + engine.record_run(outcome.duration_ms / 1000.0) |
| 107 | + sandbox_ms += outcome.duration_ms |
| 108 | + sandbox_outcomes.append(outcome) |
| 109 | + await store.add_sandbox_run( |
| 110 | + task_id, script=script, category=category, status=outcome.status, |
| 111 | + exit_code=outcome.exit_code, duration_ms=outcome.duration_ms, |
| 112 | + timed_out=outcome.timed_out, stdout_summary=outcome.stdout[:4096], |
| 113 | + stderr_summary=outcome.stderr[:4096], error_type=outcome.error_type) |
| 114 | + if outcome.status != "ok": |
| 115 | + key = outcome.error_type or outcome.status |
| 116 | + error_distribution[key] = error_distribution.get(key, 0) + 1 |
| 117 | + warnings.append(f"{script} did not complete ({outcome.status}); " |
| 118 | + "coverage degraded") |
| 119 | + continue |
| 120 | + if script == "parse_diff.py": |
| 121 | + try: |
| 122 | + diff_summary = json.loads(outcome.stdout).get("summary", {}) |
| 123 | + except (json.JSONDecodeError, ValueError): |
| 124 | + warnings.append("parse_diff.py produced invalid JSON") |
| 125 | + else: |
| 126 | + raw_findings.extend(_parse_script_findings(outcome.stdout, warnings)) |
| 127 | + finally: |
| 128 | + await session.close() |
| 129 | + |
| 130 | + llm_findings: list[Finding] = [] |
| 131 | + llm_summary = "" |
| 132 | + llm_calls = 0 |
| 133 | + if opts.enable_llm: |
| 134 | + repository = create_default_skill_repository( |
| 135 | + str(Path(opts.skill_root).parent), workspace_runtime=runtime) |
| 136 | + gov_filter = GovernanceToolFilter(engine, on_event=filter_events.append) |
| 137 | + agent = create_review_agent(repository, opts.dry_run, [gov_filter]) |
| 138 | + llm_findings, llm_summary, llm_warnings = await run_llm_review( |
| 139 | + agent, opts.diff_text, raw_findings) |
| 140 | + warnings.extend(llm_warnings) |
| 141 | + llm_calls = 1 |
| 142 | + |
| 143 | + kept, dropped = dedupe(raw_findings + llm_findings) |
| 144 | + reported, needs_review = gate(kept) |
| 145 | + await store.add_findings(task_id, reported, status="reported") |
| 146 | + await store.add_findings(task_id, needs_review, status="needs_human_review") |
| 147 | + await store.add_findings(task_id, dropped, status="deduped") |
| 148 | + |
| 149 | + intercepts = sum(1 for e in filter_events if e.decision != "allow") |
| 150 | + metrics = { |
| 151 | + "total_duration_ms": int((time.monotonic() - start) * 1000), |
| 152 | + "sandbox_duration_ms": sandbox_ms, |
| 153 | + "tool_calls": len(sandbox_outcomes) + llm_calls, |
| 154 | + "intercepts": intercepts, |
| 155 | + "findings_total": len(reported), |
| 156 | + "severity_distribution": severity_distribution(reported), |
| 157 | + "error_distribution": error_distribution, |
| 158 | + } |
| 159 | + await store.add_metrics(task_id, metrics) |
| 160 | + |
| 161 | + report = build_report( |
| 162 | + task_id=task_id, input_ref=opts.input_ref, runtime=opts.runtime, |
| 163 | + dry_run=opts.dry_run, diff_summary=diff_summary, reported=reported, |
| 164 | + needs_review=needs_review, deduped_count=len(dropped), |
| 165 | + filter_events=filter_events, sandbox_outcomes=sandbox_outcomes, |
| 166 | + metrics=metrics, llm_summary=llm_summary, warnings=warnings) |
| 167 | + json_path, md_path = write_reports(report, opts.output_dir) |
| 168 | + await store.add_report(task_id, report, Path(md_path).read_text(encoding="utf-8")) |
| 169 | + await store.update_task(task_id, status="completed", |
| 170 | + diff_summary=diff_summary, finished=True) |
| 171 | + return ReviewResult(task_id=task_id, report=report, |
| 172 | + json_path=json_path, md_path=md_path) |
| 173 | + except Exception: |
| 174 | + await store.update_task(task_id, status="failed", finished=True) |
| 175 | + raise |
| 176 | + finally: |
| 177 | + await store.close() |
0 commit comments