anvil-dev-framework 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/VERSION +1 -1
- package/docs/command-reference.md +97 -16
- package/docs/system-architecture.md +15 -0
- package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
- package/global/api/openapi.yaml +357 -0
- package/global/api/ralph_api.py +528 -0
- package/global/commands/anvil-settings.md +44 -18
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/evidence.md +23 -6
- package/global/commands/hud.md +24 -0
- package/global/commands/orient.md +22 -21
- package/global/commands/weekly-review.md +21 -1
- package/global/config/notifications.yaml.template +50 -0
- package/global/hooks/ralph_stop.sh +33 -1
- package/global/hooks/statusline.sh +67 -2
- package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
- package/global/lib/coderabbit_metrics.py +647 -0
- package/global/lib/command_tracker.py +147 -0
- package/global/lib/log_rotation.py +287 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +28 -2
- package/global/lib/token_metrics.py +49 -3
- package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/test_command_tracker.py +172 -0
- package/global/tests/test_token_metrics.py +38 -0
- package/global/tools/README.md +153 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +86 -1
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
- package/global/tools/anvil-memory/src/commands/context.ts +322 -0
- package/global/tools/anvil-memory/src/db.ts +108 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/global/tools/orient_linear.py +159 -0
- package/global/tools/ralph-watch +423 -0
- package/package.json +2 -1
- package/project/.anvil-project.yaml.template +93 -0
- package/project/CLAUDE.md.template +343 -0
- package/project/agents/README.md +119 -0
- package/project/agents/cross-layer-debugger.md +217 -0
- package/project/agents/security-code-reviewer.md +162 -0
- package/project/constitution.md.template +235 -0
- package/project/coordination.md +103 -0
- package/project/docs/background-tasks.md +258 -0
- package/project/docs/skills-frontmatter.md +243 -0
- package/project/examples/README.md +106 -0
- package/project/examples/api-route-template.ts +171 -0
- package/project/examples/component-template.tsx +110 -0
- package/project/examples/hook-template.ts +152 -0
- package/project/examples/service-template.ts +207 -0
- package/project/examples/test-template.test.tsx +249 -0
- package/project/hooks/README.md +491 -0
- package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/project/hooks/notification.py +183 -0
- package/project/hooks/permission_request.py +438 -0
- package/project/hooks/post_tool_use.py +397 -0
- package/project/hooks/pre_compact.py +126 -0
- package/project/hooks/pre_tool_use.py +454 -0
- package/project/hooks/session_start.py +656 -0
- package/project/hooks/stop.py +356 -0
- package/project/hooks/subagent_start.py +223 -0
- package/project/hooks/subagent_stop.py +215 -0
- package/project/hooks/user_prompt_submit.py +110 -0
- package/project/hooks/utils/llm/anth.py +114 -0
- package/project/hooks/utils/llm/oai.py +114 -0
- package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
- package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
- package/project/hooks/utils/tts/openai_tts.py +92 -0
- package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
- package/project/linear.yaml.template +23 -0
- package/project/product.md.template +238 -0
- package/project/retros/README.md +126 -0
- package/project/rules/README.md +90 -0
- package/project/rules/debugging.md +139 -0
- package/project/rules/security-review.md +115 -0
- package/project/settings.yaml.template +185 -0
- package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
- package/project/templates/api-python/CLAUDE.md +547 -0
- package/project/templates/generic/CLAUDE.md +260 -0
- package/project/templates/saas/CLAUDE.md +478 -0
- package/project/tests/README.md +140 -0
- package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/project/tests/fixtures/sample-transcript.jsonl +21 -0
- package/project/tests/test-hooks.sh +259 -0
- package/project/tests/test-lib.sh +248 -0
- package/project/tests/test-statusline.sh +165 -0
- package/project/tests/test_transcript_parser.py +323 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeRabbit Metrics Service for Anvil Framework.
|
|
3
|
+
|
|
4
|
+
Provides instrumentation for tracking CodeRabbit review metrics across sessions.
|
|
5
|
+
Stores metrics in SQLite for analysis by /weekly-review and HUD panels.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from coderabbit_metrics import CodeRabbitMetrics
|
|
9
|
+
|
|
10
|
+
metrics = CodeRabbitMetrics()
|
|
11
|
+
metrics.record_review(
|
|
12
|
+
pr_number="123",
|
|
13
|
+
issue_count=3,
|
|
14
|
+
categories={"suggestion": 2, "warning": 1},
|
|
15
|
+
review_time_seconds=45,
|
|
16
|
+
)
|
|
17
|
+
summary = metrics.get_weekly_summary()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sqlite3
|
|
21
|
+
import os
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from datetime import datetime, timezone, timedelta
|
|
25
|
+
from typing import Optional, Dict, Any, List
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
|
|
29
|
+
# Module logger
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ReviewRecord:
|
|
35
|
+
"""Record of a single CodeRabbit review."""
|
|
36
|
+
id: int
|
|
37
|
+
pr_number: str
|
|
38
|
+
pr_url: Optional[str]
|
|
39
|
+
issue_count: int
|
|
40
|
+
categories: Dict[str, int]
|
|
41
|
+
review_time_seconds: float
|
|
42
|
+
reviewed_at: datetime
|
|
43
|
+
result: str # pass, issues, error
|
|
44
|
+
branch: Optional[str]
|
|
45
|
+
issue_key: Optional[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class FixRecord:
|
|
50
|
+
"""Record of a CodeRabbit fix application."""
|
|
51
|
+
id: int
|
|
52
|
+
pr_number: str
|
|
53
|
+
issues_fixed: int
|
|
54
|
+
issues_manual: int
|
|
55
|
+
fix_time_seconds: float
|
|
56
|
+
fixed_at: datetime
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class WeeklySummary:
|
|
61
|
+
"""Aggregated metrics for a week."""
|
|
62
|
+
total_reviews: int
|
|
63
|
+
total_issues_found: int
|
|
64
|
+
total_issues_fixed: int
|
|
65
|
+
avg_issues_per_review: float
|
|
66
|
+
avg_review_time_seconds: float
|
|
67
|
+
pass_rate: float # % of reviews with 0 issues
|
|
68
|
+
category_breakdown: Dict[str, int]
|
|
69
|
+
top_issue_prs: List[Dict[str, Any]]
|
|
70
|
+
trend: str # improving, stable, degrading
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Database Path
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
def get_db_path() -> Path:
|
|
78
|
+
"""Get the database path for CodeRabbit metrics."""
|
|
79
|
+
# Use ~/.anvil/coderabbit_metrics.db for persistence across projects
|
|
80
|
+
anvil_dir = Path.home() / ".anvil"
|
|
81
|
+
anvil_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return anvil_dir / "coderabbit_metrics.db"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# CodeRabbit Metrics Service
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
class CodeRabbitMetrics:
|
|
90
|
+
"""Service for tracking CodeRabbit review metrics."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, db_path: Optional[Path] = None):
|
|
93
|
+
"""Initialize the metrics service.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
db_path: Optional path to the database file.
|
|
97
|
+
Defaults to ~/.anvil/coderabbit_metrics.db
|
|
98
|
+
"""
|
|
99
|
+
self.db_path = db_path or get_db_path()
|
|
100
|
+
self._init_db()
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def _get_connection(self):
|
|
104
|
+
"""Context manager for database connections."""
|
|
105
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
106
|
+
conn.row_factory = sqlite3.Row
|
|
107
|
+
try:
|
|
108
|
+
yield conn
|
|
109
|
+
finally:
|
|
110
|
+
conn.close()
|
|
111
|
+
|
|
112
|
+
def _init_db(self):
|
|
113
|
+
"""Initialize database schema."""
|
|
114
|
+
with self._get_connection() as conn:
|
|
115
|
+
cursor = conn.cursor()
|
|
116
|
+
|
|
117
|
+
# Reviews table
|
|
118
|
+
cursor.execute("""
|
|
119
|
+
CREATE TABLE IF NOT EXISTS reviews (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
pr_number TEXT NOT NULL,
|
|
122
|
+
pr_url TEXT,
|
|
123
|
+
issue_count INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
categories_json TEXT,
|
|
125
|
+
review_time_seconds REAL DEFAULT 0,
|
|
126
|
+
reviewed_at TEXT NOT NULL,
|
|
127
|
+
result TEXT NOT NULL DEFAULT 'unknown',
|
|
128
|
+
branch TEXT,
|
|
129
|
+
issue_key TEXT,
|
|
130
|
+
project_path TEXT
|
|
131
|
+
)
|
|
132
|
+
""")
|
|
133
|
+
|
|
134
|
+
# Fixes table
|
|
135
|
+
cursor.execute("""
|
|
136
|
+
CREATE TABLE IF NOT EXISTS fixes (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
pr_number TEXT NOT NULL,
|
|
139
|
+
issues_fixed INTEGER NOT NULL DEFAULT 0,
|
|
140
|
+
issues_manual INTEGER NOT NULL DEFAULT 0,
|
|
141
|
+
fix_time_seconds REAL DEFAULT 0,
|
|
142
|
+
fixed_at TEXT NOT NULL,
|
|
143
|
+
project_path TEXT
|
|
144
|
+
)
|
|
145
|
+
""")
|
|
146
|
+
|
|
147
|
+
# Index for efficient queries
|
|
148
|
+
cursor.execute("""
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_reviewed_at
|
|
150
|
+
ON reviews(reviewed_at)
|
|
151
|
+
""")
|
|
152
|
+
cursor.execute("""
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_fixes_fixed_at
|
|
154
|
+
ON fixes(fixed_at)
|
|
155
|
+
""")
|
|
156
|
+
|
|
157
|
+
conn.commit()
|
|
158
|
+
|
|
159
|
+
# -------------------------------------------------------------------------
|
|
160
|
+
# Recording Methods
|
|
161
|
+
# -------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def record_review(
|
|
164
|
+
self,
|
|
165
|
+
pr_number: str,
|
|
166
|
+
issue_count: int,
|
|
167
|
+
categories: Optional[Dict[str, int]] = None,
|
|
168
|
+
review_time_seconds: float = 0,
|
|
169
|
+
pr_url: Optional[str] = None,
|
|
170
|
+
result: str = "unknown",
|
|
171
|
+
branch: Optional[str] = None,
|
|
172
|
+
issue_key: Optional[str] = None,
|
|
173
|
+
) -> int:
|
|
174
|
+
"""Record a CodeRabbit review.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
pr_number: PR number (e.g., "123")
|
|
178
|
+
issue_count: Total number of issues found
|
|
179
|
+
categories: Issue breakdown by category (suggestion, warning, error)
|
|
180
|
+
review_time_seconds: Time taken for review
|
|
181
|
+
pr_url: Full GitHub PR URL
|
|
182
|
+
result: Review result (pass, issues, error)
|
|
183
|
+
branch: Git branch name
|
|
184
|
+
issue_key: Associated Linear issue key
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The ID of the recorded review
|
|
188
|
+
"""
|
|
189
|
+
import json
|
|
190
|
+
|
|
191
|
+
categories = categories or {}
|
|
192
|
+
reviewed_at = datetime.now(timezone.utc).isoformat()
|
|
193
|
+
project_path = str(Path.cwd())
|
|
194
|
+
|
|
195
|
+
with self._get_connection() as conn:
|
|
196
|
+
cursor = conn.cursor()
|
|
197
|
+
cursor.execute("""
|
|
198
|
+
INSERT INTO reviews
|
|
199
|
+
(pr_number, pr_url, issue_count, categories_json, review_time_seconds,
|
|
200
|
+
reviewed_at, result, branch, issue_key, project_path)
|
|
201
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
202
|
+
""", (
|
|
203
|
+
pr_number,
|
|
204
|
+
pr_url,
|
|
205
|
+
issue_count,
|
|
206
|
+
json.dumps(categories),
|
|
207
|
+
review_time_seconds,
|
|
208
|
+
reviewed_at,
|
|
209
|
+
result,
|
|
210
|
+
branch,
|
|
211
|
+
issue_key,
|
|
212
|
+
project_path,
|
|
213
|
+
))
|
|
214
|
+
conn.commit()
|
|
215
|
+
return cursor.lastrowid
|
|
216
|
+
|
|
217
|
+
def record_fix(
|
|
218
|
+
self,
|
|
219
|
+
pr_number: str,
|
|
220
|
+
issues_fixed: int,
|
|
221
|
+
issues_manual: int = 0,
|
|
222
|
+
fix_time_seconds: float = 0,
|
|
223
|
+
) -> int:
|
|
224
|
+
"""Record a CodeRabbit fix application.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
pr_number: PR number
|
|
228
|
+
issues_fixed: Number of issues automatically fixed
|
|
229
|
+
issues_manual: Number of issues requiring manual fix
|
|
230
|
+
fix_time_seconds: Time taken to apply fixes
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The ID of the recorded fix
|
|
234
|
+
"""
|
|
235
|
+
fixed_at = datetime.now(timezone.utc).isoformat()
|
|
236
|
+
project_path = str(Path.cwd())
|
|
237
|
+
|
|
238
|
+
with self._get_connection() as conn:
|
|
239
|
+
cursor = conn.cursor()
|
|
240
|
+
cursor.execute("""
|
|
241
|
+
INSERT INTO fixes
|
|
242
|
+
(pr_number, issues_fixed, issues_manual, fix_time_seconds, fixed_at, project_path)
|
|
243
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
244
|
+
""", (
|
|
245
|
+
pr_number,
|
|
246
|
+
issues_fixed,
|
|
247
|
+
issues_manual,
|
|
248
|
+
fix_time_seconds,
|
|
249
|
+
fixed_at,
|
|
250
|
+
project_path,
|
|
251
|
+
))
|
|
252
|
+
conn.commit()
|
|
253
|
+
return cursor.lastrowid
|
|
254
|
+
|
|
255
|
+
# -------------------------------------------------------------------------
|
|
256
|
+
# Query Methods
|
|
257
|
+
# -------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def get_weekly_summary(self, weeks_ago: int = 0) -> WeeklySummary:
|
|
260
|
+
"""Get summary metrics for a specific week.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
weeks_ago: Number of weeks in the past (0 = current week)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
WeeklySummary with aggregated metrics
|
|
267
|
+
"""
|
|
268
|
+
import json
|
|
269
|
+
|
|
270
|
+
# Calculate date range
|
|
271
|
+
now = datetime.now(timezone.utc)
|
|
272
|
+
start_of_week = now - timedelta(days=now.weekday() + (weeks_ago * 7))
|
|
273
|
+
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
274
|
+
end_of_week = start_of_week + timedelta(days=7)
|
|
275
|
+
|
|
276
|
+
with self._get_connection() as conn:
|
|
277
|
+
cursor = conn.cursor()
|
|
278
|
+
|
|
279
|
+
# Get reviews for the week
|
|
280
|
+
cursor.execute("""
|
|
281
|
+
SELECT * FROM reviews
|
|
282
|
+
WHERE reviewed_at >= ? AND reviewed_at < ?
|
|
283
|
+
ORDER BY issue_count DESC
|
|
284
|
+
""", (start_of_week.isoformat(), end_of_week.isoformat()))
|
|
285
|
+
|
|
286
|
+
reviews = cursor.fetchall()
|
|
287
|
+
|
|
288
|
+
# Get fixes for the week
|
|
289
|
+
cursor.execute("""
|
|
290
|
+
SELECT SUM(issues_fixed) as total_fixed
|
|
291
|
+
FROM fixes
|
|
292
|
+
WHERE fixed_at >= ? AND fixed_at < ?
|
|
293
|
+
""", (start_of_week.isoformat(), end_of_week.isoformat()))
|
|
294
|
+
|
|
295
|
+
fixes_row = cursor.fetchone()
|
|
296
|
+
total_fixed = fixes_row["total_fixed"] or 0 if fixes_row else 0
|
|
297
|
+
|
|
298
|
+
# Calculate aggregates
|
|
299
|
+
total_reviews = len(reviews)
|
|
300
|
+
total_issues = sum(r["issue_count"] for r in reviews)
|
|
301
|
+
pass_count = sum(1 for r in reviews if r["issue_count"] == 0)
|
|
302
|
+
|
|
303
|
+
avg_issues = total_issues / total_reviews if total_reviews > 0 else 0
|
|
304
|
+
avg_time = sum(r["review_time_seconds"] for r in reviews) / total_reviews if total_reviews > 0 else 0
|
|
305
|
+
pass_rate = (pass_count / total_reviews * 100) if total_reviews > 0 else 0
|
|
306
|
+
|
|
307
|
+
# Category breakdown
|
|
308
|
+
category_breakdown: Dict[str, int] = {}
|
|
309
|
+
for review in reviews:
|
|
310
|
+
if review["categories_json"]:
|
|
311
|
+
try:
|
|
312
|
+
cats = json.loads(review["categories_json"])
|
|
313
|
+
for cat, count in cats.items():
|
|
314
|
+
category_breakdown[cat] = category_breakdown.get(cat, 0) + count
|
|
315
|
+
except json.JSONDecodeError:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
# Top issue PRs
|
|
319
|
+
top_prs = [
|
|
320
|
+
{
|
|
321
|
+
"pr_number": r["pr_number"],
|
|
322
|
+
"issue_count": r["issue_count"],
|
|
323
|
+
"branch": r["branch"],
|
|
324
|
+
"issue_key": r["issue_key"],
|
|
325
|
+
}
|
|
326
|
+
for r in reviews[:5]
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
# Determine trend by comparing to previous week
|
|
330
|
+
prev_summary = self._get_basic_weekly_stats(weeks_ago + 1)
|
|
331
|
+
if prev_summary["total_reviews"] == 0:
|
|
332
|
+
trend = "new"
|
|
333
|
+
elif avg_issues < prev_summary["avg_issues"] - 0.5:
|
|
334
|
+
trend = "improving"
|
|
335
|
+
elif avg_issues > prev_summary["avg_issues"] + 0.5:
|
|
336
|
+
trend = "degrading"
|
|
337
|
+
else:
|
|
338
|
+
trend = "stable"
|
|
339
|
+
|
|
340
|
+
return WeeklySummary(
|
|
341
|
+
total_reviews=total_reviews,
|
|
342
|
+
total_issues_found=total_issues,
|
|
343
|
+
total_issues_fixed=total_fixed,
|
|
344
|
+
avg_issues_per_review=round(avg_issues, 2),
|
|
345
|
+
avg_review_time_seconds=round(avg_time, 2),
|
|
346
|
+
pass_rate=round(pass_rate, 1),
|
|
347
|
+
category_breakdown=category_breakdown,
|
|
348
|
+
top_issue_prs=top_prs,
|
|
349
|
+
trend=trend,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _get_basic_weekly_stats(self, weeks_ago: int) -> Dict[str, Any]:
|
|
353
|
+
"""Get basic stats for trend comparison."""
|
|
354
|
+
now = datetime.now(timezone.utc)
|
|
355
|
+
start_of_week = now - timedelta(days=now.weekday() + (weeks_ago * 7))
|
|
356
|
+
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
357
|
+
end_of_week = start_of_week + timedelta(days=7)
|
|
358
|
+
|
|
359
|
+
with self._get_connection() as conn:
|
|
360
|
+
cursor = conn.cursor()
|
|
361
|
+
cursor.execute("""
|
|
362
|
+
SELECT COUNT(*) as count, SUM(issue_count) as total_issues
|
|
363
|
+
FROM reviews
|
|
364
|
+
WHERE reviewed_at >= ? AND reviewed_at < ?
|
|
365
|
+
""", (start_of_week.isoformat(), end_of_week.isoformat()))
|
|
366
|
+
|
|
367
|
+
row = cursor.fetchone()
|
|
368
|
+
|
|
369
|
+
total_reviews = row["count"] or 0
|
|
370
|
+
total_issues = row["total_issues"] or 0
|
|
371
|
+
avg_issues = total_issues / total_reviews if total_reviews > 0 else 0
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
"total_reviews": total_reviews,
|
|
375
|
+
"total_issues": total_issues,
|
|
376
|
+
"avg_issues": avg_issues,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def get_recent_reviews(self, limit: int = 10) -> List[ReviewRecord]:
|
|
380
|
+
"""Get the most recent reviews.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
limit: Maximum number of reviews to return
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of ReviewRecord objects
|
|
387
|
+
"""
|
|
388
|
+
import json
|
|
389
|
+
|
|
390
|
+
with self._get_connection() as conn:
|
|
391
|
+
cursor = conn.cursor()
|
|
392
|
+
cursor.execute("""
|
|
393
|
+
SELECT * FROM reviews
|
|
394
|
+
ORDER BY reviewed_at DESC
|
|
395
|
+
LIMIT ?
|
|
396
|
+
""", (limit,))
|
|
397
|
+
|
|
398
|
+
rows = cursor.fetchall()
|
|
399
|
+
|
|
400
|
+
return [
|
|
401
|
+
ReviewRecord(
|
|
402
|
+
id=row["id"],
|
|
403
|
+
pr_number=row["pr_number"],
|
|
404
|
+
pr_url=row["pr_url"],
|
|
405
|
+
issue_count=row["issue_count"],
|
|
406
|
+
categories=json.loads(row["categories_json"]) if row["categories_json"] else {},
|
|
407
|
+
review_time_seconds=row["review_time_seconds"],
|
|
408
|
+
reviewed_at=datetime.fromisoformat(row["reviewed_at"]),
|
|
409
|
+
result=row["result"],
|
|
410
|
+
branch=row["branch"],
|
|
411
|
+
issue_key=row["issue_key"],
|
|
412
|
+
)
|
|
413
|
+
for row in rows
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
def get_pr_history(self, pr_number: str) -> Dict[str, Any]:
|
|
417
|
+
"""Get all reviews and fixes for a specific PR.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
pr_number: The PR number to look up
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Dict with reviews and fixes lists
|
|
424
|
+
"""
|
|
425
|
+
import json
|
|
426
|
+
|
|
427
|
+
with self._get_connection() as conn:
|
|
428
|
+
cursor = conn.cursor()
|
|
429
|
+
|
|
430
|
+
# Get reviews
|
|
431
|
+
cursor.execute("""
|
|
432
|
+
SELECT * FROM reviews
|
|
433
|
+
WHERE pr_number = ?
|
|
434
|
+
ORDER BY reviewed_at ASC
|
|
435
|
+
""", (pr_number,))
|
|
436
|
+
review_rows = cursor.fetchall()
|
|
437
|
+
|
|
438
|
+
# Get fixes
|
|
439
|
+
cursor.execute("""
|
|
440
|
+
SELECT * FROM fixes
|
|
441
|
+
WHERE pr_number = ?
|
|
442
|
+
ORDER BY fixed_at ASC
|
|
443
|
+
""", (pr_number,))
|
|
444
|
+
fix_rows = cursor.fetchall()
|
|
445
|
+
|
|
446
|
+
reviews = [
|
|
447
|
+
{
|
|
448
|
+
"issue_count": r["issue_count"],
|
|
449
|
+
"categories": json.loads(r["categories_json"]) if r["categories_json"] else {},
|
|
450
|
+
"result": r["result"],
|
|
451
|
+
"reviewed_at": r["reviewed_at"],
|
|
452
|
+
}
|
|
453
|
+
for r in review_rows
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
fixes = [
|
|
457
|
+
{
|
|
458
|
+
"issues_fixed": f["issues_fixed"],
|
|
459
|
+
"issues_manual": f["issues_manual"],
|
|
460
|
+
"fixed_at": f["fixed_at"],
|
|
461
|
+
}
|
|
462
|
+
for f in fix_rows
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
"pr_number": pr_number,
|
|
467
|
+
"reviews": reviews,
|
|
468
|
+
"fixes": fixes,
|
|
469
|
+
"total_reviews": len(reviews),
|
|
470
|
+
"total_fixes": len(fixes),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# =============================================================================
|
|
475
|
+
# Linear Issue Automation (ANV-276)
|
|
476
|
+
# =============================================================================
|
|
477
|
+
|
|
478
|
+
def create_linear_issue_for_review(
|
|
479
|
+
pr_number: str,
|
|
480
|
+
issue_count: int,
|
|
481
|
+
categories: Dict[str, int],
|
|
482
|
+
pr_url: Optional[str] = None,
|
|
483
|
+
branch: Optional[str] = None,
|
|
484
|
+
) -> Optional[str]:
|
|
485
|
+
"""Create a Linear issue when CodeRabbit finds critical issues.
|
|
486
|
+
|
|
487
|
+
Creates an issue when:
|
|
488
|
+
- Security issues are found (always)
|
|
489
|
+
- Critical errors are found (always)
|
|
490
|
+
- More than 5 issues total (optional, based on config)
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
pr_number: PR number
|
|
494
|
+
issue_count: Total issues found
|
|
495
|
+
categories: Issue breakdown by category
|
|
496
|
+
pr_url: GitHub PR URL
|
|
497
|
+
branch: Git branch name
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Linear issue identifier if created, None otherwise
|
|
501
|
+
"""
|
|
502
|
+
# Check if we should create an issue
|
|
503
|
+
has_security = categories.get("security", 0) > 0
|
|
504
|
+
has_critical = categories.get("critical", 0) > 0
|
|
505
|
+
high_issue_count = issue_count >= 5
|
|
506
|
+
|
|
507
|
+
if not (has_security or has_critical or high_issue_count):
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
from linear_provider import LinearProvider
|
|
512
|
+
from issue_models import IssueStatus, Priority
|
|
513
|
+
except ImportError:
|
|
514
|
+
try:
|
|
515
|
+
from .linear_provider import LinearProvider
|
|
516
|
+
from .issue_models import IssueStatus, Priority
|
|
517
|
+
except ImportError:
|
|
518
|
+
logger.warning("Linear provider not available, skipping issue creation")
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
# Determine priority based on issue types
|
|
522
|
+
if has_security:
|
|
523
|
+
priority = Priority.URGENT
|
|
524
|
+
title_prefix = "🔒 Security"
|
|
525
|
+
elif has_critical:
|
|
526
|
+
priority = Priority.HIGH
|
|
527
|
+
title_prefix = "⚠️ Critical"
|
|
528
|
+
else:
|
|
529
|
+
priority = Priority.MEDIUM
|
|
530
|
+
title_prefix = "📋 Review"
|
|
531
|
+
|
|
532
|
+
# Build description
|
|
533
|
+
desc_parts = [
|
|
534
|
+
f"CodeRabbit found **{issue_count} issues** in PR #{pr_number}.",
|
|
535
|
+
"",
|
|
536
|
+
"### Issue Breakdown",
|
|
537
|
+
]
|
|
538
|
+
|
|
539
|
+
for cat, count in categories.items():
|
|
540
|
+
desc_parts.append(f"- {cat}: {count}")
|
|
541
|
+
|
|
542
|
+
if pr_url:
|
|
543
|
+
desc_parts.extend(["", f"**PR**: [{pr_url}]({pr_url})"])
|
|
544
|
+
|
|
545
|
+
if branch:
|
|
546
|
+
desc_parts.extend(["", f"**Branch**: `{branch}`"])
|
|
547
|
+
|
|
548
|
+
desc_parts.extend([
|
|
549
|
+
"",
|
|
550
|
+
"### Next Steps",
|
|
551
|
+
"1. Review CodeRabbit comments on the PR",
|
|
552
|
+
"2. Apply fixes using `/coderabbit-fix`",
|
|
553
|
+
"3. Re-run review to verify",
|
|
554
|
+
])
|
|
555
|
+
|
|
556
|
+
description = "\n".join(desc_parts)
|
|
557
|
+
title = f"{title_prefix}: CodeRabbit issues in PR #{pr_number}"
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
# Get team from environment or use default
|
|
561
|
+
team_key = os.environ.get("LINEAR_TEAM", "ANV")
|
|
562
|
+
provider = LinearProvider(team_key=team_key)
|
|
563
|
+
|
|
564
|
+
issue = provider.create_issue(
|
|
565
|
+
title=title,
|
|
566
|
+
description=description,
|
|
567
|
+
priority=priority,
|
|
568
|
+
labels=["coderabbit", "automated"],
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
logger.info(f"Created Linear issue {issue.key} for PR #{pr_number}")
|
|
572
|
+
return issue.key
|
|
573
|
+
except Exception as e:
|
|
574
|
+
logger.warning(f"Failed to create Linear issue: {e}")
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# =============================================================================
|
|
579
|
+
# CLI Interface
|
|
580
|
+
# =============================================================================
|
|
581
|
+
|
|
582
|
+
if __name__ == "__main__":
|
|
583
|
+
import sys
|
|
584
|
+
import json as json_module
|
|
585
|
+
|
|
586
|
+
if len(sys.argv) < 2:
|
|
587
|
+
print("Usage: coderabbit_metrics.py <command> [args...]")
|
|
588
|
+
print("Commands:")
|
|
589
|
+
print(" record-review <pr> <issues> [result] - Record a review")
|
|
590
|
+
print(" record-fix <pr> <fixed> [manual] - Record a fix")
|
|
591
|
+
print(" weekly [weeks_ago] - Get weekly summary")
|
|
592
|
+
print(" recent [limit] - Get recent reviews")
|
|
593
|
+
print(" pr-history <pr> - Get PR history")
|
|
594
|
+
sys.exit(1)
|
|
595
|
+
|
|
596
|
+
cmd = sys.argv[1]
|
|
597
|
+
metrics = CodeRabbitMetrics()
|
|
598
|
+
|
|
599
|
+
if cmd == "record-review":
|
|
600
|
+
if len(sys.argv) < 4:
|
|
601
|
+
print("Error: record-review requires pr and issues count")
|
|
602
|
+
sys.exit(1)
|
|
603
|
+
pr = sys.argv[2]
|
|
604
|
+
issues = int(sys.argv[3])
|
|
605
|
+
result = sys.argv[4] if len(sys.argv) > 4 else ("pass" if issues == 0 else "issues")
|
|
606
|
+
record_id = metrics.record_review(pr, issues, result=result)
|
|
607
|
+
print(f"Review recorded: ID {record_id}")
|
|
608
|
+
|
|
609
|
+
elif cmd == "record-fix":
|
|
610
|
+
if len(sys.argv) < 4:
|
|
611
|
+
print("Error: record-fix requires pr and fixed count")
|
|
612
|
+
sys.exit(1)
|
|
613
|
+
pr = sys.argv[2]
|
|
614
|
+
fixed = int(sys.argv[3])
|
|
615
|
+
manual = int(sys.argv[4]) if len(sys.argv) > 4 else 0
|
|
616
|
+
record_id = metrics.record_fix(pr, fixed, manual)
|
|
617
|
+
print(f"Fix recorded: ID {record_id}")
|
|
618
|
+
|
|
619
|
+
elif cmd == "weekly":
|
|
620
|
+
weeks_ago = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
621
|
+
summary = metrics.get_weekly_summary(weeks_ago)
|
|
622
|
+
print(f"Weekly Summary (week -{weeks_ago}):")
|
|
623
|
+
print(f" Reviews: {summary.total_reviews}")
|
|
624
|
+
print(f" Issues Found: {summary.total_issues_found}")
|
|
625
|
+
print(f" Issues Fixed: {summary.total_issues_fixed}")
|
|
626
|
+
print(f" Avg Issues/Review: {summary.avg_issues_per_review}")
|
|
627
|
+
print(f" Pass Rate: {summary.pass_rate}%")
|
|
628
|
+
print(f" Trend: {summary.trend}")
|
|
629
|
+
|
|
630
|
+
elif cmd == "recent":
|
|
631
|
+
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
|
632
|
+
reviews = metrics.get_recent_reviews(limit)
|
|
633
|
+
print(f"Recent {len(reviews)} reviews:")
|
|
634
|
+
for r in reviews:
|
|
635
|
+
print(f" PR #{r.pr_number}: {r.issue_count} issues ({r.result})")
|
|
636
|
+
|
|
637
|
+
elif cmd == "pr-history":
|
|
638
|
+
if len(sys.argv) < 3:
|
|
639
|
+
print("Error: pr-history requires pr number")
|
|
640
|
+
sys.exit(1)
|
|
641
|
+
pr = sys.argv[2]
|
|
642
|
+
history = metrics.get_pr_history(pr)
|
|
643
|
+
print(json_module.dumps(history, indent=2))
|
|
644
|
+
|
|
645
|
+
else:
|
|
646
|
+
print(f"Unknown command: {cmd}")
|
|
647
|
+
sys.exit(1)
|