Skip to content

Commit 4dba7dc

Browse files
feat: persist code review results with sqlite
1 parent 2323114 commit 4dba7dc

3 files changed

Lines changed: 194 additions & 3 deletions

File tree

examples/skills_code_review_agent/README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Skills Code Review Agent
22

3-
This example implements the first phase of issue #92 as a deterministic,
3+
This example implements the early phases of issue #92 as a deterministic,
44
local-only code review loop. It reads a unified diff, scans added lines with a
55
small static rule set, redacts likely secrets, and writes JSON and Markdown
6-
reports.
6+
reports. When `--db-path` is provided, it also persists the review task,
7+
findings, and report metadata into SQLite.
78

89
It intentionally does not call an LLM, remote sandbox, or SDK core extension.
910

@@ -29,6 +30,33 @@ You can also run the clean fixture:
2930
python examples/skills_code_review_agent/run_agent.py --diff-file examples/skills_code_review_agent/fixtures/clean.diff --output-dir examples/skills_code_review_agent/output_clean --dry-run
3031
```
3132

33+
## Run With SQLite
34+
35+
```bash
36+
python examples/skills_code_review_agent/run_agent.py --diff-file examples/skills_code_review_agent/fixtures/security.diff --output-dir examples/skills_code_review_agent/output --db-path examples/skills_code_review_agent/output/reviews.sqlite3 --dry-run
37+
```
38+
39+
When `--db-path` is set, the command prints the database path and generated
40+
task id. The database contains three tables:
41+
42+
- `review_tasks`
43+
- `findings`
44+
- `reports`
45+
46+
## Verify SQLite Records
47+
48+
Using Python:
49+
50+
```bash
51+
python -c "import sqlite3; db='examples/skills_code_review_agent/output/reviews.sqlite3'; con=sqlite3.connect(db); print(con.execute('select task_id,total_findings from review_tasks order by created_at desc limit 1').fetchone()); print(con.execute('select severity,category,file,line,title from findings order by id limit 5').fetchall())"
52+
```
53+
54+
Using the `sqlite3` CLI:
55+
56+
```bash
57+
sqlite3 examples/skills_code_review_agent/output/reviews.sqlite3 "select severity, category, file, line, title from findings order by id;"
58+
```
59+
3260
## Current Scope
3361

3462
- Parses unified diff hunks and added line numbers.
@@ -40,12 +68,12 @@ python examples/skills_code_review_agent/run_agent.py --diff-file examples/skill
4068
- `open(...)` without `with`
4169
- Redacts likely API keys, tokens, secrets, and passwords before writing reports.
4270
- Produces `review_report.json` and `review_report.md`.
71+
- Optionally persists review tasks, findings, and report metadata to SQLite.
4372

4473
## Not Implemented Yet
4574

4675
- Real LLM review.
4776
- Real remote sandbox or container execution.
4877
- SDK core integration.
49-
- SQLite storage.
5078
- Filter governance and telemetry.
5179
- Multi-agent orchestration.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""SQLite persistence for deterministic code review runs."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import sqlite3
7+
import uuid
8+
from pathlib import Path
9+
from typing import Any
10+
11+
12+
def init_db(db_path: Path) -> None:
13+
"""Create the SQLite database and tables if they do not already exist."""
14+
15+
db_path.parent.mkdir(parents=True, exist_ok=True)
16+
with sqlite3.connect(db_path) as conn:
17+
conn.execute("PRAGMA foreign_keys = ON")
18+
conn.execute(
19+
"""
20+
CREATE TABLE IF NOT EXISTS review_tasks (
21+
task_id TEXT PRIMARY KEY,
22+
created_at TEXT NOT NULL,
23+
diff_file TEXT NOT NULL,
24+
dry_run INTEGER NOT NULL,
25+
files_scanned TEXT NOT NULL,
26+
total_findings INTEGER NOT NULL
27+
)
28+
"""
29+
)
30+
conn.execute(
31+
"""
32+
CREATE TABLE IF NOT EXISTS findings (
33+
id INTEGER PRIMARY KEY AUTOINCREMENT,
34+
task_id TEXT NOT NULL,
35+
severity TEXT NOT NULL,
36+
category TEXT NOT NULL,
37+
file TEXT NOT NULL,
38+
line INTEGER NOT NULL,
39+
title TEXT NOT NULL,
40+
evidence TEXT NOT NULL,
41+
recommendation TEXT NOT NULL,
42+
confidence REAL NOT NULL,
43+
source TEXT NOT NULL,
44+
FOREIGN KEY (task_id) REFERENCES review_tasks(task_id) ON DELETE CASCADE
45+
)
46+
"""
47+
)
48+
conn.execute(
49+
"""
50+
CREATE TABLE IF NOT EXISTS reports (
51+
task_id TEXT PRIMARY KEY,
52+
json_report_path TEXT NOT NULL,
53+
markdown_report_path TEXT NOT NULL,
54+
summary_json TEXT NOT NULL,
55+
FOREIGN KEY (task_id) REFERENCES review_tasks(task_id) ON DELETE CASCADE
56+
)
57+
"""
58+
)
59+
60+
61+
def persist_review(
62+
*,
63+
db_path: Path,
64+
report: dict[str, Any],
65+
json_report_path: Path,
66+
markdown_report_path: Path,
67+
) -> str:
68+
"""Persist one review task, its findings, and report metadata."""
69+
70+
init_db(db_path)
71+
task_id = str(uuid.uuid4())
72+
summary = report["summary"]
73+
findings = report["findings"]
74+
75+
with sqlite3.connect(db_path) as conn:
76+
conn.execute("PRAGMA foreign_keys = ON")
77+
conn.execute(
78+
"""
79+
INSERT INTO review_tasks (
80+
task_id,
81+
created_at,
82+
diff_file,
83+
dry_run,
84+
files_scanned,
85+
total_findings
86+
) VALUES (?, ?, ?, ?, ?, ?)
87+
""",
88+
(
89+
task_id,
90+
summary["generated_at"],
91+
summary["diff_file"],
92+
1 if summary["dry_run"] else 0,
93+
json.dumps(summary["files_scanned"], ensure_ascii=False),
94+
int(summary["total_findings"]),
95+
),
96+
)
97+
conn.executemany(
98+
"""
99+
INSERT INTO findings (
100+
task_id,
101+
severity,
102+
category,
103+
file,
104+
line,
105+
title,
106+
evidence,
107+
recommendation,
108+
confidence,
109+
source
110+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111+
""",
112+
[
113+
(
114+
task_id,
115+
finding["severity"],
116+
finding["category"],
117+
finding["file"],
118+
int(finding["line"]),
119+
finding["title"],
120+
finding["evidence"],
121+
finding["recommendation"],
122+
float(finding["confidence"]),
123+
finding["source"],
124+
)
125+
for finding in findings
126+
],
127+
)
128+
conn.execute(
129+
"""
130+
INSERT INTO reports (
131+
task_id,
132+
json_report_path,
133+
markdown_report_path,
134+
summary_json
135+
) VALUES (?, ?, ?, ?)
136+
""",
137+
(
138+
task_id,
139+
str(json_report_path),
140+
str(markdown_report_path),
141+
json.dumps(summary, ensure_ascii=False),
142+
),
143+
)
144+
145+
return task_id

examples/skills_code_review_agent/run_agent.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from agent.report import build_report
1818
from agent.report import write_reports
1919
from agent.rules import run_static_rules
20+
from agent.storage import persist_review
2021

2122

2223
def _resolve_path(raw: str) -> Path:
@@ -47,13 +48,19 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
4748
action="store_true",
4849
help="Record the run as dry-run. No external services are called either way.",
4950
)
51+
parser.add_argument(
52+
"--db-path",
53+
default="",
54+
help="Optional SQLite database path for persisting review tasks, findings, and reports.",
55+
)
5056
return parser.parse_args(argv)
5157

5258

5359
def main(argv: Sequence[str] | None = None) -> int:
5460
args = parse_args(argv)
5561
diff_file = _resolve_path(args.diff_file)
5662
output_dir = _resolve_path(args.output_dir)
63+
db_path = _resolve_path(args.db_path) if args.db_path else None
5764

5865
if not diff_file.exists():
5966
raise FileNotFoundError(f"diff file not found: {diff_file}")
@@ -68,6 +75,14 @@ def main(argv: Sequence[str] | None = None) -> int:
6875
dry_run=args.dry_run,
6976
)
7077
json_path, md_path = write_reports(report, output_dir)
78+
task_id = ""
79+
if db_path is not None:
80+
task_id = persist_review(
81+
db_path=db_path,
82+
report=report,
83+
json_report_path=json_path,
84+
markdown_report_path=md_path,
85+
)
7186

7287
high_count = report["summary"]["severity_counts"].get("high", 0)
7388
medium_count = report["summary"]["severity_counts"].get("medium", 0)
@@ -79,6 +94,9 @@ def main(argv: Sequence[str] | None = None) -> int:
7994
print(f"Findings: high={high_count} medium={medium_count} low={low_count}")
8095
print(f"JSON report: {json_path}")
8196
print(f"Markdown report: {md_path}")
97+
if db_path is not None:
98+
print(f"Database: {db_path}")
99+
print(f"Task ID: {task_id}")
82100
return 0
83101

84102

0 commit comments

Comments
 (0)