gtg 0.4.0__py3-none-any.whl
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.
- goodtogo/__init__.py +66 -0
- goodtogo/adapters/__init__.py +22 -0
- goodtogo/adapters/agent_state.py +490 -0
- goodtogo/adapters/cache_memory.py +208 -0
- goodtogo/adapters/cache_sqlite.py +305 -0
- goodtogo/adapters/github.py +523 -0
- goodtogo/adapters/time_provider.py +123 -0
- goodtogo/cli.py +311 -0
- goodtogo/container.py +313 -0
- goodtogo/core/__init__.py +0 -0
- goodtogo/core/analyzer.py +982 -0
- goodtogo/core/errors.py +100 -0
- goodtogo/core/interfaces.py +388 -0
- goodtogo/core/models.py +312 -0
- goodtogo/core/validation.py +144 -0
- goodtogo/parsers/__init__.py +0 -0
- goodtogo/parsers/claude.py +188 -0
- goodtogo/parsers/coderabbit.py +352 -0
- goodtogo/parsers/cursor.py +135 -0
- goodtogo/parsers/generic.py +192 -0
- goodtogo/parsers/greptile.py +249 -0
- gtg-0.4.0.dist-info/METADATA +278 -0
- gtg-0.4.0.dist-info/RECORD +27 -0
- gtg-0.4.0.dist-info/WHEEL +5 -0
- gtg-0.4.0.dist-info/entry_points.txt +2 -0
- gtg-0.4.0.dist-info/licenses/LICENSE +21 -0
- gtg-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""CodeRabbit comment parser for GoodToMerge.
|
|
2
|
+
|
|
3
|
+
This module implements the ReviewerParser interface for parsing comments
|
|
4
|
+
from CodeRabbit, an AI-powered automated code review tool.
|
|
5
|
+
|
|
6
|
+
CodeRabbit uses specific patterns to indicate comment severity and type:
|
|
7
|
+
- Severity indicators with emojis and labels
|
|
8
|
+
- Fingerprinting comments (internal metadata)
|
|
9
|
+
- Resolution status markers
|
|
10
|
+
- Outside diff range notifications
|
|
11
|
+
- Summary/walkthrough sections (non-actionable)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
from goodtogo.core.interfaces import ReviewerParser
|
|
19
|
+
from goodtogo.core.models import CommentClassification, Priority, ReviewerType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CodeRabbitParser(ReviewerParser):
|
|
23
|
+
"""Parser for CodeRabbit automated code review comments.
|
|
24
|
+
|
|
25
|
+
CodeRabbit posts comments with structured severity indicators that
|
|
26
|
+
can be deterministically parsed to classify comment actionability.
|
|
27
|
+
|
|
28
|
+
Patterns recognized:
|
|
29
|
+
- _Potential issue_ | _Critical/Major/Minor_: ACTIONABLE
|
|
30
|
+
- _Trivial_: NON_ACTIONABLE
|
|
31
|
+
- _Nitpick_: NON_ACTIONABLE
|
|
32
|
+
- Fingerprinting HTML comments: NON_ACTIONABLE
|
|
33
|
+
- Addressed checkmarks: NON_ACTIONABLE
|
|
34
|
+
- Outside diff range mentions: ACTIONABLE (MINOR)
|
|
35
|
+
- Summary/walkthrough sections: NON_ACTIONABLE
|
|
36
|
+
- Tip/info boxes: NON_ACTIONABLE
|
|
37
|
+
- All other: AMBIGUOUS
|
|
38
|
+
|
|
39
|
+
Author detection:
|
|
40
|
+
- Primary: author == "coderabbitai[bot]"
|
|
41
|
+
- Fallback: body contains CodeRabbit signature comment
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Author pattern for CodeRabbit bot
|
|
45
|
+
CODERABBIT_AUTHOR = "coderabbitai[bot]"
|
|
46
|
+
|
|
47
|
+
# Body pattern for CodeRabbit signature (fallback detection)
|
|
48
|
+
CODERABBIT_SIGNATURE_PATTERN = re.compile(
|
|
49
|
+
r"<!-- This is an auto-generated comment.*by coderabbit\.ai -->",
|
|
50
|
+
re.IGNORECASE | re.DOTALL,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Severity patterns - using re.escape for literal characters
|
|
54
|
+
# Pattern: _Potential issue_ | _Critical/Major/Minor_
|
|
55
|
+
CRITICAL_PATTERN = re.compile(
|
|
56
|
+
r"_\u26a0\ufe0f\s*Potential issue_\s*\|\s*_\U0001f534\s*Critical_",
|
|
57
|
+
re.IGNORECASE,
|
|
58
|
+
)
|
|
59
|
+
MAJOR_PATTERN = re.compile(
|
|
60
|
+
r"_\u26a0\ufe0f\s*Potential issue_\s*\|\s*_\U0001f7e0\s*Major_",
|
|
61
|
+
re.IGNORECASE,
|
|
62
|
+
)
|
|
63
|
+
MINOR_PATTERN = re.compile(
|
|
64
|
+
r"_\u26a0\ufe0f\s*Potential issue_\s*\|\s*_\U0001f7e1\s*Minor_",
|
|
65
|
+
re.IGNORECASE,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Non-actionable patterns
|
|
69
|
+
TRIVIAL_PATTERN = re.compile(r"_\U0001f535\s*Trivial_", re.IGNORECASE)
|
|
70
|
+
NITPICK_PATTERN = re.compile(r"_\U0001f9f9\s*Nitpick_", re.IGNORECASE)
|
|
71
|
+
|
|
72
|
+
# Fingerprinting comments (internal CodeRabbit metadata)
|
|
73
|
+
FINGERPRINT_PATTERN = re.compile(r"<!--\s*fingerprinting:", re.IGNORECASE)
|
|
74
|
+
|
|
75
|
+
# Addressed status marker
|
|
76
|
+
ADDRESSED_PATTERN = re.compile(r"\u2705\s*Addressed", re.IGNORECASE)
|
|
77
|
+
|
|
78
|
+
# Acknowledgment patterns (thank-you replies indicating issue was addressed)
|
|
79
|
+
# These are reply comments from CodeRabbit confirming a fix was applied
|
|
80
|
+
# Note: GitHub usernames can contain hyphens, so we use [\w-] instead of \w
|
|
81
|
+
ACKNOWLEDGMENT_PATTERNS = [
|
|
82
|
+
# "@username Thank you for the fix/catch/suggestion/addressing"
|
|
83
|
+
re.compile(
|
|
84
|
+
r"`?@[\w-]+`?\s+Thank\s+you\s+for\s+(the\s+)?(fix|catch|suggestion|addressing)",
|
|
85
|
+
re.IGNORECASE,
|
|
86
|
+
),
|
|
87
|
+
# "Thank you for addressing this"
|
|
88
|
+
re.compile(r"Thank\s+you\s+for\s+addressing\s+this", re.IGNORECASE),
|
|
89
|
+
# Starts with "Thank you" and contains keywords like fix, addressed, suggestion
|
|
90
|
+
re.compile(
|
|
91
|
+
r"^`?@?[\w-]*`?\s*,?\s*[Tt]hank\s+you.*?(fix|addressed|updated|resolved|correct|suggestion)",
|
|
92
|
+
re.IGNORECASE,
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# Outside diff range (in review body)
|
|
97
|
+
OUTSIDE_DIFF_PATTERN = re.compile(r"Outside diff range", re.IGNORECASE)
|
|
98
|
+
|
|
99
|
+
# Summary/walkthrough patterns (non-actionable informational content)
|
|
100
|
+
# These are overview sections that don't require action
|
|
101
|
+
SUMMARY_PATTERNS = [
|
|
102
|
+
# Walkthrough header
|
|
103
|
+
re.compile(r"^##\s*Walkthrough", re.MULTILINE),
|
|
104
|
+
# Changes summary header
|
|
105
|
+
re.compile(r"^##\s*Changes", re.MULTILINE),
|
|
106
|
+
# Summary header
|
|
107
|
+
re.compile(r"^##\s*Summary", re.MULTILINE),
|
|
108
|
+
# PR summary pattern
|
|
109
|
+
re.compile(r"^##\s*PR\s+Summary", re.MULTILINE | re.IGNORECASE),
|
|
110
|
+
# Review summary
|
|
111
|
+
re.compile(r"^##\s*Review\s+Summary", re.MULTILINE | re.IGNORECASE),
|
|
112
|
+
# File summary table pattern (CodeRabbit specific)
|
|
113
|
+
re.compile(r"\|\s*File\s*\|\s*Changes\s*\|", re.IGNORECASE),
|
|
114
|
+
# Sequence diagram indicators
|
|
115
|
+
re.compile(r"```mermaid", re.IGNORECASE),
|
|
116
|
+
# PR Objectives section
|
|
117
|
+
re.compile(r"^##\s*Objectives", re.MULTILINE | re.IGNORECASE),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
# Tip/info box patterns (non-actionable)
|
|
121
|
+
TIP_PATTERNS = [
|
|
122
|
+
re.compile(r"^>\s*\[!TIP\]", re.MULTILINE),
|
|
123
|
+
re.compile(r"^>\s*\[!NOTE\]", re.MULTILINE),
|
|
124
|
+
re.compile(r"^>\s*\[!INFO\]", re.MULTILINE),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
# PR-level summary comment patterns (non-actionable)
|
|
128
|
+
# These are posted at the PR level (path=None, line=None) and contain
|
|
129
|
+
# overview information. The actual actionable items are in inline comments.
|
|
130
|
+
PR_SUMMARY_PATTERNS = [
|
|
131
|
+
# "Actionable comments posted: N" pattern
|
|
132
|
+
re.compile(r"Actionable comments posted:\s*\d+", re.IGNORECASE),
|
|
133
|
+
# <details> sections with summaries
|
|
134
|
+
re.compile(r"<details>.*?<summary>.*?</summary>", re.IGNORECASE | re.DOTALL),
|
|
135
|
+
# CodeRabbit auto-generated comment signature
|
|
136
|
+
re.compile(
|
|
137
|
+
r"<!-- This is an auto-generated comment.*?by coderabbit",
|
|
138
|
+
re.IGNORECASE | re.DOTALL,
|
|
139
|
+
),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def reviewer_type(self) -> ReviewerType:
|
|
144
|
+
"""Return the reviewer type this parser handles.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
ReviewerType.CODERABBIT
|
|
148
|
+
"""
|
|
149
|
+
return ReviewerType.CODERABBIT
|
|
150
|
+
|
|
151
|
+
def can_parse(self, author: str, body: str) -> bool:
|
|
152
|
+
"""Check if this parser can handle the comment.
|
|
153
|
+
|
|
154
|
+
Identifies CodeRabbit comments by:
|
|
155
|
+
1. Author being "coderabbitai[bot]" (primary method)
|
|
156
|
+
2. Body containing CodeRabbit signature HTML comment (fallback)
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
author: Comment author's username/login.
|
|
160
|
+
body: Comment body text.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if this is a CodeRabbit comment, False otherwise.
|
|
164
|
+
"""
|
|
165
|
+
# Primary detection: check author
|
|
166
|
+
if author == self.CODERABBIT_AUTHOR:
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
# Fallback detection: check body for signature
|
|
170
|
+
if body and self.CODERABBIT_SIGNATURE_PATTERN.search(body):
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def parse(self, comment: dict) -> tuple[CommentClassification, Priority, bool]:
|
|
176
|
+
"""Parse CodeRabbit comment and return classification.
|
|
177
|
+
|
|
178
|
+
Analyzes the comment body to determine classification and priority
|
|
179
|
+
based on CodeRabbit's severity indicators.
|
|
180
|
+
|
|
181
|
+
Classification rules (in order of precedence):
|
|
182
|
+
1. PR-level summary comments -> NON_ACTIONABLE
|
|
183
|
+
2. Fingerprinting comments -> NON_ACTIONABLE
|
|
184
|
+
3. Addressed marker -> NON_ACTIONABLE
|
|
185
|
+
4. Critical severity -> ACTIONABLE, CRITICAL
|
|
186
|
+
5. Major severity -> ACTIONABLE, MAJOR
|
|
187
|
+
6. Minor severity -> ACTIONABLE, MINOR
|
|
188
|
+
7. Trivial severity -> NON_ACTIONABLE, TRIVIAL
|
|
189
|
+
8. Nitpick marker -> NON_ACTIONABLE, TRIVIAL
|
|
190
|
+
9. Outside diff range -> ACTIONABLE, MINOR
|
|
191
|
+
10. Summary/walkthrough sections -> NON_ACTIONABLE
|
|
192
|
+
11. Tip/info boxes -> NON_ACTIONABLE
|
|
193
|
+
12. All other -> AMBIGUOUS, UNKNOWN, requires_investigation=True
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
comment: Dictionary containing comment data with 'body' key,
|
|
197
|
+
and optionally 'path' and 'line' keys for inline comments.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Tuple of (classification, priority, requires_investigation):
|
|
201
|
+
- classification: CommentClassification enum value
|
|
202
|
+
- priority: Priority enum value
|
|
203
|
+
- requires_investigation: Boolean, True for AMBIGUOUS comments
|
|
204
|
+
"""
|
|
205
|
+
# Check for PR-level summary comments first (highest precedence)
|
|
206
|
+
# These are posted at the PR level and contain overview information
|
|
207
|
+
if self._is_pr_summary_comment(comment):
|
|
208
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
209
|
+
|
|
210
|
+
body = comment.get("body", "")
|
|
211
|
+
|
|
212
|
+
# Early exit for empty body
|
|
213
|
+
if not body:
|
|
214
|
+
return (CommentClassification.AMBIGUOUS, Priority.UNKNOWN, True)
|
|
215
|
+
|
|
216
|
+
# Check fingerprinting comments first (internal metadata, ignore)
|
|
217
|
+
if self.FINGERPRINT_PATTERN.search(body):
|
|
218
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
219
|
+
|
|
220
|
+
# Check addressed marker
|
|
221
|
+
if self.ADDRESSED_PATTERN.search(body):
|
|
222
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
223
|
+
|
|
224
|
+
# Check acknowledgment patterns (thank-you replies)
|
|
225
|
+
if self._is_acknowledgment(body):
|
|
226
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
227
|
+
|
|
228
|
+
# Check severity patterns (most specific first)
|
|
229
|
+
if self.CRITICAL_PATTERN.search(body):
|
|
230
|
+
return (CommentClassification.ACTIONABLE, Priority.CRITICAL, False)
|
|
231
|
+
|
|
232
|
+
if self.MAJOR_PATTERN.search(body):
|
|
233
|
+
return (CommentClassification.ACTIONABLE, Priority.MAJOR, False)
|
|
234
|
+
|
|
235
|
+
if self.MINOR_PATTERN.search(body):
|
|
236
|
+
return (CommentClassification.ACTIONABLE, Priority.MINOR, False)
|
|
237
|
+
|
|
238
|
+
# Check non-actionable patterns
|
|
239
|
+
if self.TRIVIAL_PATTERN.search(body):
|
|
240
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.TRIVIAL, False)
|
|
241
|
+
|
|
242
|
+
if self.NITPICK_PATTERN.search(body):
|
|
243
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.TRIVIAL, False)
|
|
244
|
+
|
|
245
|
+
# Check outside diff range (actionable but lower priority)
|
|
246
|
+
if self.OUTSIDE_DIFF_PATTERN.search(body):
|
|
247
|
+
return (CommentClassification.ACTIONABLE, Priority.MINOR, False)
|
|
248
|
+
|
|
249
|
+
# Check for summary/walkthrough sections (non-actionable informational)
|
|
250
|
+
if self._is_summary_content(body):
|
|
251
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
252
|
+
|
|
253
|
+
# Check for tip/info boxes (non-actionable)
|
|
254
|
+
if self._is_tip_content(body):
|
|
255
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
256
|
+
|
|
257
|
+
# Default: AMBIGUOUS - requires investigation
|
|
258
|
+
return (CommentClassification.AMBIGUOUS, Priority.UNKNOWN, True)
|
|
259
|
+
|
|
260
|
+
def _is_summary_content(self, body: str) -> bool:
|
|
261
|
+
"""Check if the body is a summary/walkthrough section.
|
|
262
|
+
|
|
263
|
+
Summary sections are informational overviews that don't require
|
|
264
|
+
specific action. They include walkthroughs, change summaries,
|
|
265
|
+
and file tables.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
body: Comment body text.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if the body appears to be a summary section.
|
|
272
|
+
"""
|
|
273
|
+
for pattern in self.SUMMARY_PATTERNS:
|
|
274
|
+
if pattern.search(body):
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def _is_tip_content(self, body: str) -> bool:
|
|
279
|
+
"""Check if the body is a tip/info box.
|
|
280
|
+
|
|
281
|
+
Tip boxes are informational callouts that provide helpful
|
|
282
|
+
context but don't require action.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
body: Comment body text.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if the body appears to be a tip/info box.
|
|
289
|
+
"""
|
|
290
|
+
for pattern in self.TIP_PATTERNS:
|
|
291
|
+
if pattern.search(body):
|
|
292
|
+
return True
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def _is_acknowledgment(self, body: str) -> bool:
|
|
296
|
+
"""Check if the body is an acknowledgment/thank-you reply.
|
|
297
|
+
|
|
298
|
+
Acknowledgment comments are replies from CodeRabbit confirming
|
|
299
|
+
that a fix or suggestion was addressed. These don't require action.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
body: Comment body text.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if the body appears to be an acknowledgment.
|
|
306
|
+
"""
|
|
307
|
+
for pattern in self.ACKNOWLEDGMENT_PATTERNS:
|
|
308
|
+
if pattern.search(body):
|
|
309
|
+
return True
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def _is_pr_summary_comment(self, comment: dict) -> bool:
|
|
313
|
+
"""Check if this is a PR-level summary comment.
|
|
314
|
+
|
|
315
|
+
PR-level summary comments are posted at the PR level (not inline)
|
|
316
|
+
and contain overview information like "Actionable comments posted: N",
|
|
317
|
+
walkthrough sections, or CodeRabbit signatures. These should be
|
|
318
|
+
classified as NON_ACTIONABLE because the actual actionable items
|
|
319
|
+
are in inline comments.
|
|
320
|
+
|
|
321
|
+
Key criteria:
|
|
322
|
+
1. Must be a PR-level comment (path=None or missing)
|
|
323
|
+
2. Must match one of the PR summary patterns
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
comment: Dictionary containing comment data with 'body' key,
|
|
327
|
+
and optionally 'path' and 'line' keys.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
True if this is a PR-level summary comment that should be
|
|
331
|
+
classified as NON_ACTIONABLE.
|
|
332
|
+
"""
|
|
333
|
+
# Only filter PR-level comments (path=None or missing)
|
|
334
|
+
# Never filter inline comments (they have path/line set)
|
|
335
|
+
path = comment.get("path")
|
|
336
|
+
if path is not None:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
body = comment.get("body", "")
|
|
340
|
+
if not body:
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
# Check for PR summary patterns
|
|
344
|
+
for pattern in self.PR_SUMMARY_PATTERNS:
|
|
345
|
+
if pattern.search(body):
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
# Also check for summary/walkthrough content at PR level
|
|
349
|
+
if self._is_summary_content(body):
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
return False
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Cursor/Bugbot comment parser.
|
|
2
|
+
|
|
3
|
+
This module implements the ReviewerParser interface for parsing comments
|
|
4
|
+
from Cursor's Bugbot automated code reviewer.
|
|
5
|
+
|
|
6
|
+
Cursor/Bugbot uses severity-based classification:
|
|
7
|
+
- Critical Severity: Must fix immediately
|
|
8
|
+
- High Severity: Must fix before merge
|
|
9
|
+
- Medium Severity: Should fix
|
|
10
|
+
- Low Severity: Nice to fix (non-actionable)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from goodtogo.core.interfaces import ReviewerParser
|
|
18
|
+
from goodtogo.core.models import CommentClassification, Priority, ReviewerType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CursorBugbotParser(ReviewerParser):
|
|
22
|
+
"""Parser for Cursor/Bugbot automated code review comments.
|
|
23
|
+
|
|
24
|
+
Detects comments from Cursor's Bugbot based on author patterns and
|
|
25
|
+
body content signatures. Classifies based on severity indicators
|
|
26
|
+
in the comment body.
|
|
27
|
+
|
|
28
|
+
Author patterns:
|
|
29
|
+
- "cursor[bot]"
|
|
30
|
+
- "cursor-bot"
|
|
31
|
+
|
|
32
|
+
Body signatures:
|
|
33
|
+
- cursor.com links
|
|
34
|
+
- Cursor-specific formatting
|
|
35
|
+
|
|
36
|
+
Severity mapping:
|
|
37
|
+
- Critical Severity -> ACTIONABLE, CRITICAL
|
|
38
|
+
- High Severity -> ACTIONABLE, MAJOR
|
|
39
|
+
- Medium Severity -> ACTIONABLE, MINOR
|
|
40
|
+
- Low Severity -> NON_ACTIONABLE, TRIVIAL
|
|
41
|
+
- No severity indicator -> AMBIGUOUS, UNKNOWN
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Author patterns that identify Cursor/Bugbot comments
|
|
45
|
+
_AUTHOR_PATTERNS = frozenset({"cursor[bot]", "cursor-bot"})
|
|
46
|
+
|
|
47
|
+
# Patterns in body that indicate Cursor/Bugbot origin
|
|
48
|
+
_CURSOR_BODY_SIGNATURES = (re.compile(r"cursor\.com", re.IGNORECASE),)
|
|
49
|
+
|
|
50
|
+
# Severity patterns and their classifications
|
|
51
|
+
_SEVERITY_PATTERNS = (
|
|
52
|
+
(
|
|
53
|
+
re.compile(r"Critical\s+Severity", re.IGNORECASE),
|
|
54
|
+
CommentClassification.ACTIONABLE,
|
|
55
|
+
Priority.CRITICAL,
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
re.compile(r"High\s+Severity", re.IGNORECASE),
|
|
59
|
+
CommentClassification.ACTIONABLE,
|
|
60
|
+
Priority.MAJOR,
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
re.compile(r"Medium\s+Severity", re.IGNORECASE),
|
|
64
|
+
CommentClassification.ACTIONABLE,
|
|
65
|
+
Priority.MINOR,
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
re.compile(r"Low\s+Severity", re.IGNORECASE),
|
|
69
|
+
CommentClassification.NON_ACTIONABLE,
|
|
70
|
+
Priority.TRIVIAL,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def reviewer_type(self) -> ReviewerType:
|
|
76
|
+
"""Return the reviewer type this parser handles.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ReviewerType.CURSOR
|
|
80
|
+
"""
|
|
81
|
+
return ReviewerType.CURSOR
|
|
82
|
+
|
|
83
|
+
def can_parse(self, author: str, body: str) -> bool:
|
|
84
|
+
"""Check if this parser can handle the comment.
|
|
85
|
+
|
|
86
|
+
A comment can be parsed by this parser if:
|
|
87
|
+
1. The author matches known Cursor/Bugbot patterns, OR
|
|
88
|
+
2. The body contains Cursor-specific signatures
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
author: Comment author's username/login.
|
|
92
|
+
body: Comment body text.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if this parser can handle the comment, False otherwise.
|
|
96
|
+
"""
|
|
97
|
+
# Check author patterns (case-insensitive)
|
|
98
|
+
author_lower = author.lower()
|
|
99
|
+
if author_lower in self._AUTHOR_PATTERNS:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
# Check body signatures
|
|
103
|
+
for pattern in self._CURSOR_BODY_SIGNATURES:
|
|
104
|
+
if pattern.search(body):
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def parse(self, comment: dict) -> tuple[CommentClassification, Priority, bool]:
|
|
110
|
+
"""Parse comment and return classification.
|
|
111
|
+
|
|
112
|
+
Analyzes the comment body for severity indicators to determine
|
|
113
|
+
classification and priority. Comments without a recognized
|
|
114
|
+
severity pattern are classified as AMBIGUOUS.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
comment: Dictionary containing comment data with at least:
|
|
118
|
+
- 'body': Comment text content
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (classification, priority, requires_investigation):
|
|
122
|
+
- classification: CommentClassification enum value
|
|
123
|
+
- priority: Priority enum value
|
|
124
|
+
- requires_investigation: True if AMBIGUOUS, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
body = comment.get("body", "")
|
|
127
|
+
|
|
128
|
+
# Check each severity pattern in order of priority
|
|
129
|
+
for pattern, classification, priority in self._SEVERITY_PATTERNS:
|
|
130
|
+
if pattern.search(body):
|
|
131
|
+
return classification, priority, False
|
|
132
|
+
|
|
133
|
+
# No recognized severity pattern - classify as ambiguous
|
|
134
|
+
# This requires investigation by the agent
|
|
135
|
+
return CommentClassification.AMBIGUOUS, Priority.UNKNOWN, True
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Generic fallback parser for unknown reviewers.
|
|
2
|
+
|
|
3
|
+
This module provides the GenericParser class, which serves as a fallback
|
|
4
|
+
parser for comments that don't match any specific automated reviewer pattern.
|
|
5
|
+
It handles human comments and unknown reviewer types.
|
|
6
|
+
|
|
7
|
+
Per the design specification, the Generic Parser classification rules are:
|
|
8
|
+
- Thread is resolved -> NON_ACTIONABLE
|
|
9
|
+
- Thread is outdated -> NON_ACTIONABLE
|
|
10
|
+
- Reply confirmation patterns -> NON_ACTIONABLE (acknowledging fixes)
|
|
11
|
+
- Approval patterns (LGTM, etc.) -> NON_ACTIONABLE
|
|
12
|
+
- All other -> AMBIGUOUS with requires_investigation=True
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from goodtogo.core.interfaces import ReviewerParser
|
|
20
|
+
from goodtogo.core.models import (
|
|
21
|
+
CommentClassification,
|
|
22
|
+
Priority,
|
|
23
|
+
ReviewerType,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GenericParser(ReviewerParser):
|
|
28
|
+
"""Fallback parser for unknown reviewers and human comments.
|
|
29
|
+
|
|
30
|
+
This parser is used when no specific reviewer parser matches the comment.
|
|
31
|
+
It applies conservative classification rules, marking most comments as
|
|
32
|
+
AMBIGUOUS to ensure nothing is silently skipped.
|
|
33
|
+
|
|
34
|
+
The GenericParser serves two purposes:
|
|
35
|
+
1. Handle comments from human reviewers (ReviewerType.HUMAN)
|
|
36
|
+
2. Act as a fallback for any unrecognized automated reviewers
|
|
37
|
+
|
|
38
|
+
Classification logic:
|
|
39
|
+
- Resolved threads -> NON_ACTIONABLE (already addressed)
|
|
40
|
+
- Outdated threads -> NON_ACTIONABLE (code has changed)
|
|
41
|
+
- Reply confirmation patterns -> NON_ACTIONABLE (acknowledging fixes)
|
|
42
|
+
- Approval patterns -> NON_ACTIONABLE (LGTM, looks good, etc.)
|
|
43
|
+
- All other comments -> AMBIGUOUS with requires_investigation=True
|
|
44
|
+
|
|
45
|
+
This conservative approach ensures that AI agents never miss potentially
|
|
46
|
+
important feedback by automatically dismissing it.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Patterns indicating a reply that confirms something was addressed
|
|
50
|
+
# These are typically non-actionable acknowledgments
|
|
51
|
+
REPLY_CONFIRMATION_PATTERNS = [
|
|
52
|
+
# Explicit fix confirmations
|
|
53
|
+
re.compile(r"^(good\s+catch|great\s+catch|nice\s+catch)", re.IGNORECASE),
|
|
54
|
+
re.compile(r"^fixed\s+(in\s+)?(commit|[a-f0-9]{7})", re.IGNORECASE),
|
|
55
|
+
re.compile(r"^done[.!]?\s*$", re.IGNORECASE),
|
|
56
|
+
re.compile(r"^addressed[.!]?\s*$", re.IGNORECASE),
|
|
57
|
+
re.compile(r"^resolved[.!]?\s*$", re.IGNORECASE),
|
|
58
|
+
# Acknowledgments and thanks
|
|
59
|
+
re.compile(r"^thanks[!.,]?\s*$", re.IGNORECASE),
|
|
60
|
+
re.compile(r"^thank\s+you[!.,]?\s*$", re.IGNORECASE),
|
|
61
|
+
re.compile(r"^will\s+(fix|do|address|update)", re.IGNORECASE),
|
|
62
|
+
re.compile(r"^updated[.!]?\s*$", re.IGNORECASE),
|
|
63
|
+
re.compile(r"^applied[.!]?\s*$", re.IGNORECASE),
|
|
64
|
+
# Agreement patterns - must be complete confirmations, not prefixes
|
|
65
|
+
re.compile(r"^(yep|yeah|yes)[,.]?\s+(fixed|done|updated|addressed)", re.IGNORECASE),
|
|
66
|
+
re.compile(r"^agreed[,.]?\s*(fixed|done|updated)?[.!]?\s*$", re.IGNORECASE),
|
|
67
|
+
re.compile(r"^makes\s+sense[.!]?\s*$", re.IGNORECASE),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Patterns indicating approval or positive feedback (non-actionable)
|
|
71
|
+
APPROVAL_PATTERNS = [
|
|
72
|
+
re.compile(r"^lgtm[!.]?\s*$", re.IGNORECASE),
|
|
73
|
+
re.compile(r"^looks\s+good(\s+to\s+me)?[!.]?\s*$", re.IGNORECASE),
|
|
74
|
+
re.compile(r"^ship\s+it[!.]?\s*$", re.IGNORECASE),
|
|
75
|
+
re.compile(r"^\+1\s*$"),
|
|
76
|
+
re.compile(r"^:?\+1:?\s*$"), # emoji format
|
|
77
|
+
re.compile(r"^approved[!.]?\s*$", re.IGNORECASE),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def reviewer_type(self) -> ReviewerType:
|
|
82
|
+
"""Return ReviewerType.HUMAN.
|
|
83
|
+
|
|
84
|
+
The generic parser is used for human reviewers and as a fallback
|
|
85
|
+
for unknown reviewer types. HUMAN is returned as it's the most
|
|
86
|
+
appropriate classification for non-automated reviews.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
ReviewerType.HUMAN
|
|
90
|
+
"""
|
|
91
|
+
return ReviewerType.HUMAN
|
|
92
|
+
|
|
93
|
+
def can_parse(self, author: str, body: str) -> bool:
|
|
94
|
+
"""Return True for all comments.
|
|
95
|
+
|
|
96
|
+
As the fallback parser, GenericParser accepts all comments that
|
|
97
|
+
weren't matched by more specific parsers. This ensures no comments
|
|
98
|
+
are dropped or unhandled.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
author: Comment author's username/login (ignored).
|
|
102
|
+
body: Comment body text (ignored).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Always True - this is the catch-all parser.
|
|
106
|
+
"""
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def parse(self, comment: dict) -> tuple[CommentClassification, Priority, bool]:
|
|
110
|
+
"""Parse comment and return classification.
|
|
111
|
+
|
|
112
|
+
Classification logic:
|
|
113
|
+
1. Resolved threads -> NON_ACTIONABLE
|
|
114
|
+
2. Outdated threads -> NON_ACTIONABLE
|
|
115
|
+
3. Reply confirmation patterns -> NON_ACTIONABLE
|
|
116
|
+
4. Approval patterns -> NON_ACTIONABLE
|
|
117
|
+
5. All other -> AMBIGUOUS with requires_investigation=True
|
|
118
|
+
|
|
119
|
+
Note: This parser intentionally does NOT try to interpret comment
|
|
120
|
+
content for actionability. That would be unreliable for human comments.
|
|
121
|
+
Instead, it relies on metadata (resolved/outdated status) and simple
|
|
122
|
+
patterns that indicate the comment has been addressed.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
comment: Dictionary containing comment data with:
|
|
126
|
+
- 'body': Comment text content (optional)
|
|
127
|
+
- 'is_resolved': Boolean indicating if thread is resolved
|
|
128
|
+
- 'is_outdated': Boolean indicating if comment is outdated
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (classification, priority, requires_investigation):
|
|
132
|
+
- classification: CommentClassification enum value
|
|
133
|
+
- priority: Priority.UNKNOWN (generic parser doesn't assess priority)
|
|
134
|
+
- requires_investigation: True for AMBIGUOUS, False otherwise
|
|
135
|
+
"""
|
|
136
|
+
# Check if thread is resolved
|
|
137
|
+
if comment.get("is_resolved", False):
|
|
138
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
139
|
+
|
|
140
|
+
# Check if thread is outdated
|
|
141
|
+
if comment.get("is_outdated", False):
|
|
142
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
143
|
+
|
|
144
|
+
body = comment.get("body", "")
|
|
145
|
+
|
|
146
|
+
# Check for reply confirmation patterns (acknowledging fixes)
|
|
147
|
+
if self._is_reply_confirmation(body):
|
|
148
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
149
|
+
|
|
150
|
+
# Check for approval patterns (LGTM, looks good, etc.)
|
|
151
|
+
if self._is_approval(body):
|
|
152
|
+
return (CommentClassification.NON_ACTIONABLE, Priority.UNKNOWN, False)
|
|
153
|
+
|
|
154
|
+
# All other cases: AMBIGUOUS with requires_investigation=True
|
|
155
|
+
# Critical: AMBIGUOUS comments MUST always have requires_investigation=True
|
|
156
|
+
return (CommentClassification.AMBIGUOUS, Priority.UNKNOWN, True)
|
|
157
|
+
|
|
158
|
+
def _is_reply_confirmation(self, body: str) -> bool:
|
|
159
|
+
"""Check if the body is a reply confirmation.
|
|
160
|
+
|
|
161
|
+
Reply confirmations are comments that acknowledge something was
|
|
162
|
+
addressed or fixed, such as "Good catch!", "Fixed in commit abc123", etc.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
body: Comment body text.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if the body appears to be a reply confirmation.
|
|
169
|
+
"""
|
|
170
|
+
body_stripped = body.strip()
|
|
171
|
+
for pattern in self.REPLY_CONFIRMATION_PATTERNS:
|
|
172
|
+
if pattern.search(body_stripped):
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _is_approval(self, body: str) -> bool:
|
|
177
|
+
"""Check if the body is an approval comment.
|
|
178
|
+
|
|
179
|
+
Approval comments are positive feedback that indicate the code
|
|
180
|
+
is acceptable, such as "LGTM", "Looks good", "+1", etc.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
body: Comment body text.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if the body appears to be an approval.
|
|
187
|
+
"""
|
|
188
|
+
body_stripped = body.strip()
|
|
189
|
+
for pattern in self.APPROVAL_PATTERNS:
|
|
190
|
+
if pattern.search(body_stripped):
|
|
191
|
+
return True
|
|
192
|
+
return False
|