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.
Files changed (117) hide show
  1. package/README.md +48 -18
  2. package/VERSION +1 -1
  3. package/docs/command-reference.md +97 -16
  4. package/docs/system-architecture.md +15 -0
  5. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  6. package/global/api/openapi.yaml +357 -0
  7. package/global/api/ralph_api.py +528 -0
  8. package/global/commands/anvil-settings.md +44 -18
  9. package/global/commands/coderabbit-fix.md +282 -0
  10. package/global/commands/evidence.md +23 -6
  11. package/global/commands/hud.md +24 -0
  12. package/global/commands/orient.md +22 -21
  13. package/global/commands/weekly-review.md +21 -1
  14. package/global/config/notifications.yaml.template +50 -0
  15. package/global/hooks/ralph_stop.sh +33 -1
  16. package/global/hooks/statusline.sh +67 -2
  17. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  18. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  19. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  20. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  21. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  22. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  23. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  24. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  25. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  26. package/global/lib/coderabbit_metrics.py +647 -0
  27. package/global/lib/command_tracker.py +147 -0
  28. package/global/lib/log_rotation.py +287 -0
  29. package/global/lib/ralph_events.py +398 -0
  30. package/global/lib/ralph_notifier.py +366 -0
  31. package/global/lib/ralph_webhooks.py +470 -0
  32. package/global/lib/state_manager.py +121 -0
  33. package/global/lib/token_analyzer.py +28 -2
  34. package/global/lib/token_metrics.py +49 -3
  35. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  36. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  37. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  38. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  39. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  40. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  41. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  42. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  43. package/global/tests/test_command_tracker.py +172 -0
  44. package/global/tests/test_token_metrics.py +38 -0
  45. package/global/tools/README.md +153 -0
  46. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  47. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  48. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  49. package/global/tools/anvil-hud.py +86 -1
  50. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  51. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  52. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  53. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  54. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  55. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  56. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  57. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  58. package/global/tools/anvil-memory/src/db.ts +108 -0
  59. package/global/tools/anvil-memory/src/index.ts +2 -8
  60. package/global/tools/orient_linear.py +159 -0
  61. package/global/tools/ralph-watch +423 -0
  62. package/package.json +2 -1
  63. package/project/.anvil-project.yaml.template +93 -0
  64. package/project/CLAUDE.md.template +343 -0
  65. package/project/agents/README.md +119 -0
  66. package/project/agents/cross-layer-debugger.md +217 -0
  67. package/project/agents/security-code-reviewer.md +162 -0
  68. package/project/constitution.md.template +235 -0
  69. package/project/coordination.md +103 -0
  70. package/project/docs/background-tasks.md +258 -0
  71. package/project/docs/skills-frontmatter.md +243 -0
  72. package/project/examples/README.md +106 -0
  73. package/project/examples/api-route-template.ts +171 -0
  74. package/project/examples/component-template.tsx +110 -0
  75. package/project/examples/hook-template.ts +152 -0
  76. package/project/examples/service-template.ts +207 -0
  77. package/project/examples/test-template.test.tsx +249 -0
  78. package/project/hooks/README.md +491 -0
  79. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  80. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  81. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  82. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  83. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  84. package/project/hooks/notification.py +183 -0
  85. package/project/hooks/permission_request.py +438 -0
  86. package/project/hooks/post_tool_use.py +397 -0
  87. package/project/hooks/pre_compact.py +126 -0
  88. package/project/hooks/pre_tool_use.py +454 -0
  89. package/project/hooks/session_start.py +656 -0
  90. package/project/hooks/stop.py +356 -0
  91. package/project/hooks/subagent_start.py +223 -0
  92. package/project/hooks/subagent_stop.py +215 -0
  93. package/project/hooks/user_prompt_submit.py +110 -0
  94. package/project/hooks/utils/llm/anth.py +114 -0
  95. package/project/hooks/utils/llm/oai.py +114 -0
  96. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  97. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  98. package/project/hooks/utils/tts/openai_tts.py +92 -0
  99. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  100. package/project/linear.yaml.template +23 -0
  101. package/project/product.md.template +238 -0
  102. package/project/retros/README.md +126 -0
  103. package/project/rules/README.md +90 -0
  104. package/project/rules/debugging.md +139 -0
  105. package/project/rules/security-review.md +115 -0
  106. package/project/settings.yaml.template +185 -0
  107. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  108. package/project/templates/api-python/CLAUDE.md +547 -0
  109. package/project/templates/generic/CLAUDE.md +260 -0
  110. package/project/templates/saas/CLAUDE.md +478 -0
  111. package/project/tests/README.md +140 -0
  112. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  113. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  114. package/project/tests/test-hooks.sh +259 -0
  115. package/project/tests/test-lib.sh +248 -0
  116. package/project/tests/test-statusline.sh +165 -0
  117. 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)