roma-debug 0.1.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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Dependency graph builder for project analysis.
|
|
2
|
+
|
|
3
|
+
Builds a graph of module dependencies for understanding code relationships.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, Dict, Set
|
|
10
|
+
|
|
11
|
+
from roma_debug.core.models import Language, Import, FileContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DependencyNode:
|
|
16
|
+
"""A node in the dependency graph representing a file/module."""
|
|
17
|
+
filepath: str
|
|
18
|
+
language: Language
|
|
19
|
+
imports: List[Import] = field(default_factory=list)
|
|
20
|
+
imported_by: List[str] = field(default_factory=list) # Files that import this one
|
|
21
|
+
symbols: List[str] = field(default_factory=list) # Exported symbols
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def filename(self) -> str:
|
|
25
|
+
return Path(self.filepath).name
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def module_name(self) -> str:
|
|
29
|
+
"""Derive module name from filepath."""
|
|
30
|
+
path = Path(self.filepath)
|
|
31
|
+
# Remove extension and convert to module format
|
|
32
|
+
stem = path.stem
|
|
33
|
+
if stem == '__init__':
|
|
34
|
+
return path.parent.name
|
|
35
|
+
return stem
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DependencyGraph:
|
|
39
|
+
"""Graph of module dependencies in a project.
|
|
40
|
+
|
|
41
|
+
Tracks which files import which other files, allowing us to:
|
|
42
|
+
- Find all files that depend on a given file
|
|
43
|
+
- Find all dependencies of a file
|
|
44
|
+
- Identify potential root cause locations
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_root: Optional[str] = None):
|
|
48
|
+
"""Initialize the dependency graph.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
project_root: Root directory of the project
|
|
52
|
+
"""
|
|
53
|
+
self.project_root = Path(project_root) if project_root else Path.cwd()
|
|
54
|
+
self._nodes: Dict[str, DependencyNode] = {}
|
|
55
|
+
self._edges: Dict[str, Set[str]] = defaultdict(set) # from -> to
|
|
56
|
+
self._reverse_edges: Dict[str, Set[str]] = defaultdict(set) # to -> from
|
|
57
|
+
|
|
58
|
+
def add_file(self, filepath: str, language: Language, imports: List[Import]):
|
|
59
|
+
"""Add a file to the dependency graph.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
filepath: Path to the file
|
|
63
|
+
language: Language of the file
|
|
64
|
+
imports: List of imports from the file
|
|
65
|
+
"""
|
|
66
|
+
filepath = str(Path(filepath).resolve())
|
|
67
|
+
|
|
68
|
+
if filepath not in self._nodes:
|
|
69
|
+
self._nodes[filepath] = DependencyNode(
|
|
70
|
+
filepath=filepath,
|
|
71
|
+
language=language,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
node = self._nodes[filepath]
|
|
75
|
+
node.imports = imports
|
|
76
|
+
|
|
77
|
+
# Add edges for resolved imports
|
|
78
|
+
for imp in imports:
|
|
79
|
+
if imp.resolved_path:
|
|
80
|
+
resolved = str(Path(imp.resolved_path).resolve())
|
|
81
|
+
self._edges[filepath].add(resolved)
|
|
82
|
+
self._reverse_edges[resolved].add(filepath)
|
|
83
|
+
|
|
84
|
+
# Ensure the target node exists
|
|
85
|
+
if resolved not in self._nodes:
|
|
86
|
+
# Infer language from extension
|
|
87
|
+
target_lang = Language.from_extension(Path(resolved).suffix)
|
|
88
|
+
self._nodes[resolved] = DependencyNode(
|
|
89
|
+
filepath=resolved,
|
|
90
|
+
language=target_lang,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Update imported_by
|
|
94
|
+
self._nodes[resolved].imported_by.append(filepath)
|
|
95
|
+
|
|
96
|
+
def add_file_context(self, context: FileContext):
|
|
97
|
+
"""Add a FileContext to the graph.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
context: FileContext object with imports
|
|
101
|
+
"""
|
|
102
|
+
self.add_file(context.filepath, context.language, context.imports)
|
|
103
|
+
|
|
104
|
+
def get_dependencies(self, filepath: str) -> List[str]:
|
|
105
|
+
"""Get all files that the given file imports.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
filepath: Path to the file
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of file paths that are imported
|
|
112
|
+
"""
|
|
113
|
+
filepath = str(Path(filepath).resolve())
|
|
114
|
+
return list(self._edges.get(filepath, set()))
|
|
115
|
+
|
|
116
|
+
def get_dependents(self, filepath: str) -> List[str]:
|
|
117
|
+
"""Get all files that import the given file.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
filepath: Path to the file
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of file paths that import this file
|
|
124
|
+
"""
|
|
125
|
+
filepath = str(Path(filepath).resolve())
|
|
126
|
+
return list(self._reverse_edges.get(filepath, set()))
|
|
127
|
+
|
|
128
|
+
def get_transitive_dependencies(self, filepath: str, max_depth: int = 10) -> List[str]:
|
|
129
|
+
"""Get all transitive dependencies of a file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
filepath: Path to the file
|
|
133
|
+
max_depth: Maximum recursion depth
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of all files that the given file depends on (directly or indirectly)
|
|
137
|
+
"""
|
|
138
|
+
filepath = str(Path(filepath).resolve())
|
|
139
|
+
visited = set()
|
|
140
|
+
result = []
|
|
141
|
+
|
|
142
|
+
def visit(path: str, depth: int):
|
|
143
|
+
if depth > max_depth or path in visited:
|
|
144
|
+
return
|
|
145
|
+
visited.add(path)
|
|
146
|
+
|
|
147
|
+
for dep in self._edges.get(path, set()):
|
|
148
|
+
if dep not in visited:
|
|
149
|
+
result.append(dep)
|
|
150
|
+
visit(dep, depth + 1)
|
|
151
|
+
|
|
152
|
+
visit(filepath, 0)
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def get_transitive_dependents(self, filepath: str, max_depth: int = 10) -> List[str]:
|
|
156
|
+
"""Get all files that transitively depend on the given file.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
filepath: Path to the file
|
|
160
|
+
max_depth: Maximum recursion depth
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of all files that depend on this file (directly or indirectly)
|
|
164
|
+
"""
|
|
165
|
+
filepath = str(Path(filepath).resolve())
|
|
166
|
+
visited = set()
|
|
167
|
+
result = []
|
|
168
|
+
|
|
169
|
+
def visit(path: str, depth: int):
|
|
170
|
+
if depth > max_depth or path in visited:
|
|
171
|
+
return
|
|
172
|
+
visited.add(path)
|
|
173
|
+
|
|
174
|
+
for dep in self._reverse_edges.get(path, set()):
|
|
175
|
+
if dep not in visited:
|
|
176
|
+
result.append(dep)
|
|
177
|
+
visit(dep, depth + 1)
|
|
178
|
+
|
|
179
|
+
visit(filepath, 0)
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
def get_path_between(self, source: str, target: str) -> Optional[List[str]]:
|
|
183
|
+
"""Find the import path between two files.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
source: Starting file
|
|
187
|
+
target: Target file
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of files forming the path, or None if no path exists
|
|
191
|
+
"""
|
|
192
|
+
source = str(Path(source).resolve())
|
|
193
|
+
target = str(Path(target).resolve())
|
|
194
|
+
|
|
195
|
+
if source == target:
|
|
196
|
+
return [source]
|
|
197
|
+
|
|
198
|
+
# BFS to find shortest path
|
|
199
|
+
visited = {source}
|
|
200
|
+
queue = [(source, [source])]
|
|
201
|
+
|
|
202
|
+
while queue:
|
|
203
|
+
current, path = queue.pop(0)
|
|
204
|
+
|
|
205
|
+
for neighbor in self._edges.get(current, set()):
|
|
206
|
+
if neighbor == target:
|
|
207
|
+
return path + [neighbor]
|
|
208
|
+
|
|
209
|
+
if neighbor not in visited:
|
|
210
|
+
visited.add(neighbor)
|
|
211
|
+
queue.append((neighbor, path + [neighbor]))
|
|
212
|
+
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def find_common_dependencies(self, files: List[str]) -> List[str]:
|
|
216
|
+
"""Find files that are imported by all given files.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
files: List of file paths
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of files that are common dependencies
|
|
223
|
+
"""
|
|
224
|
+
if not files:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
# Get dependencies of first file
|
|
228
|
+
common = set(self.get_transitive_dependencies(files[0]))
|
|
229
|
+
|
|
230
|
+
# Intersect with dependencies of other files
|
|
231
|
+
for filepath in files[1:]:
|
|
232
|
+
deps = set(self.get_transitive_dependencies(filepath))
|
|
233
|
+
common &= deps
|
|
234
|
+
|
|
235
|
+
return list(common)
|
|
236
|
+
|
|
237
|
+
def get_node(self, filepath: str) -> Optional[DependencyNode]:
|
|
238
|
+
"""Get the node for a file.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
filepath: Path to the file
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
DependencyNode or None
|
|
245
|
+
"""
|
|
246
|
+
filepath = str(Path(filepath).resolve())
|
|
247
|
+
return self._nodes.get(filepath)
|
|
248
|
+
|
|
249
|
+
def get_all_files(self) -> List[str]:
|
|
250
|
+
"""Get all files in the graph.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of all file paths
|
|
254
|
+
"""
|
|
255
|
+
return list(self._nodes.keys())
|
|
256
|
+
|
|
257
|
+
def get_summary(self) -> str:
|
|
258
|
+
"""Get a text summary of the dependency graph.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Human-readable summary string
|
|
262
|
+
"""
|
|
263
|
+
lines = [
|
|
264
|
+
f"Dependency Graph Summary:",
|
|
265
|
+
f" Files: {len(self._nodes)}",
|
|
266
|
+
f" Direct Dependencies: {sum(len(deps) for deps in self._edges.values())}",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
# Top imported files
|
|
270
|
+
import_counts = [(f, len(deps)) for f, deps in self._reverse_edges.items()]
|
|
271
|
+
import_counts.sort(key=lambda x: x[1], reverse=True)
|
|
272
|
+
|
|
273
|
+
if import_counts:
|
|
274
|
+
lines.append("\n Most Imported Files:")
|
|
275
|
+
for filepath, count in import_counts[:5]:
|
|
276
|
+
lines.append(f" {Path(filepath).name}: imported by {count} files")
|
|
277
|
+
|
|
278
|
+
return "\n".join(lines)
|
|
279
|
+
|
|
280
|
+
def to_dict(self) -> dict:
|
|
281
|
+
"""Serialize the graph to a dictionary.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dictionary representation of the graph
|
|
285
|
+
"""
|
|
286
|
+
return {
|
|
287
|
+
"project_root": str(self.project_root),
|
|
288
|
+
"nodes": {
|
|
289
|
+
path: {
|
|
290
|
+
"filepath": node.filepath,
|
|
291
|
+
"language": node.language.value,
|
|
292
|
+
"imports": [imp.module_name for imp in node.imports],
|
|
293
|
+
"imported_by": node.imported_by,
|
|
294
|
+
}
|
|
295
|
+
for path, node in self._nodes.items()
|
|
296
|
+
},
|
|
297
|
+
"edges": {k: list(v) for k, v in self._edges.items()},
|
|
298
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Error analyzer for understanding errors without explicit tracebacks.
|
|
2
|
+
|
|
3
|
+
Analyzes error messages to:
|
|
4
|
+
- Identify error type and category
|
|
5
|
+
- Find relevant files in the project
|
|
6
|
+
- Provide targeted context for AI fixing
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional, List, Dict, Tuple
|
|
12
|
+
|
|
13
|
+
from roma_debug.core.models import Language, FileContext
|
|
14
|
+
from roma_debug.tracing.project_scanner import ProjectScanner, ProjectInfo, ProjectFile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ErrorAnalysis:
|
|
19
|
+
"""Result of analyzing an error message."""
|
|
20
|
+
error_type: str # 'http', 'runtime', 'syntax', 'import', 'config', 'unknown'
|
|
21
|
+
error_category: str # More specific: '404', 'type_error', 'module_not_found', etc.
|
|
22
|
+
error_message: str
|
|
23
|
+
suggested_language: Optional[Language] = None
|
|
24
|
+
relevant_files: List[ProjectFile] = field(default_factory=list)
|
|
25
|
+
affected_routes: List[str] = field(default_factory=list)
|
|
26
|
+
keywords: List[str] = field(default_factory=list)
|
|
27
|
+
confidence: float = 0.0 # 0-1 confidence in analysis
|
|
28
|
+
|
|
29
|
+
def to_context_string(self) -> str:
|
|
30
|
+
"""Format analysis as context string for AI."""
|
|
31
|
+
lines = [
|
|
32
|
+
"## ERROR ANALYSIS",
|
|
33
|
+
f"Type: {self.error_type}",
|
|
34
|
+
f"Category: {self.error_category}",
|
|
35
|
+
f"Message: {self.error_message}",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
if self.suggested_language:
|
|
39
|
+
lines.append(f"Language: {self.suggested_language.value}")
|
|
40
|
+
|
|
41
|
+
if self.affected_routes:
|
|
42
|
+
lines.append(f"Affected Routes: {', '.join(self.affected_routes)}")
|
|
43
|
+
|
|
44
|
+
if self.relevant_files:
|
|
45
|
+
lines.append("\n### Relevant Files:")
|
|
46
|
+
for f in self.relevant_files:
|
|
47
|
+
lines.append(f"- {f.path}")
|
|
48
|
+
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Error pattern matchers
|
|
53
|
+
ERROR_PATTERNS = {
|
|
54
|
+
# HTTP/Web errors
|
|
55
|
+
'http_404': [
|
|
56
|
+
(r'cannot\s+(?:get|post|put|delete|patch)\s+[/\w]+', 0.9),
|
|
57
|
+
(r'404\s+(?:not\s+found|\(not\s+found\))', 0.95),
|
|
58
|
+
(r'get\s+http://.*\s+404', 0.95),
|
|
59
|
+
(r'route\s+not\s+found', 0.9),
|
|
60
|
+
(r'no\s+route\s+matches', 0.9),
|
|
61
|
+
],
|
|
62
|
+
'http_500': [
|
|
63
|
+
(r'500\s+internal\s+server\s+error', 0.95),
|
|
64
|
+
(r'internal\s+server\s+error', 0.8),
|
|
65
|
+
],
|
|
66
|
+
'http_400': [
|
|
67
|
+
(r'400\s+bad\s+request', 0.95),
|
|
68
|
+
(r'bad\s+request', 0.7),
|
|
69
|
+
],
|
|
70
|
+
'http_401': [
|
|
71
|
+
(r'401\s+unauthorized', 0.95),
|
|
72
|
+
(r'authentication\s+required', 0.85),
|
|
73
|
+
(r'not\s+authenticated', 0.8),
|
|
74
|
+
],
|
|
75
|
+
'http_403': [
|
|
76
|
+
(r'403\s+forbidden', 0.95),
|
|
77
|
+
(r'permission\s+denied', 0.8),
|
|
78
|
+
(r'access\s+denied', 0.8),
|
|
79
|
+
],
|
|
80
|
+
'static_file': [
|
|
81
|
+
(r'cannot\s+(?:get|find|serve)\s+.*\.(?:html|css|js|png|jpg|svg)', 0.9),
|
|
82
|
+
(r'static\s+file\s+not\s+found', 0.9),
|
|
83
|
+
(r'failed\s+to\s+load\s+resource', 0.85),
|
|
84
|
+
(r'enoent.*index\.html', 0.95),
|
|
85
|
+
(r'enoent.*public', 0.9),
|
|
86
|
+
(r'enoent.*static', 0.9),
|
|
87
|
+
(r'no\s+such\s+file.*\.html', 0.95),
|
|
88
|
+
(r'no\s+such\s+file.*public', 0.9),
|
|
89
|
+
],
|
|
90
|
+
'file_not_found': [
|
|
91
|
+
(r'enoent', 0.9),
|
|
92
|
+
(r'no\s+such\s+file\s+or\s+directory', 0.95),
|
|
93
|
+
(r'file\s+not\s+found', 0.9),
|
|
94
|
+
(r'cannot\s+find\s+(?:file|path)', 0.85),
|
|
95
|
+
],
|
|
96
|
+
|
|
97
|
+
# Python errors
|
|
98
|
+
'python_import': [
|
|
99
|
+
(r'modulenotfounderror', 0.95),
|
|
100
|
+
(r'importerror', 0.9),
|
|
101
|
+
(r'no\s+module\s+named', 0.95),
|
|
102
|
+
(r'cannot\s+import\s+name', 0.9),
|
|
103
|
+
],
|
|
104
|
+
'python_attribute': [
|
|
105
|
+
(r'attributeerror', 0.95),
|
|
106
|
+
(r'has\s+no\s+attribute', 0.9),
|
|
107
|
+
],
|
|
108
|
+
'python_type': [
|
|
109
|
+
(r'typeerror', 0.95),
|
|
110
|
+
(r'expected\s+\w+\s+got\s+\w+', 0.8),
|
|
111
|
+
],
|
|
112
|
+
'python_value': [
|
|
113
|
+
(r'valueerror', 0.95),
|
|
114
|
+
(r'invalid\s+value', 0.7),
|
|
115
|
+
],
|
|
116
|
+
'python_key': [
|
|
117
|
+
(r'keyerror', 0.95),
|
|
118
|
+
],
|
|
119
|
+
'python_index': [
|
|
120
|
+
(r'indexerror', 0.95),
|
|
121
|
+
(r'list\s+index\s+out\s+of\s+range', 0.95),
|
|
122
|
+
],
|
|
123
|
+
'python_name': [
|
|
124
|
+
(r'nameerror', 0.95),
|
|
125
|
+
(r'name\s+[\'"]?\w+[\'"]?\s+is\s+not\s+defined', 0.9),
|
|
126
|
+
],
|
|
127
|
+
'python_syntax': [
|
|
128
|
+
(r'syntaxerror', 0.95),
|
|
129
|
+
(r'invalid\s+syntax', 0.9),
|
|
130
|
+
],
|
|
131
|
+
|
|
132
|
+
# JavaScript/Node errors
|
|
133
|
+
'js_reference': [
|
|
134
|
+
(r'referenceerror', 0.95),
|
|
135
|
+
(r'is\s+not\s+defined', 0.8),
|
|
136
|
+
],
|
|
137
|
+
'js_type': [
|
|
138
|
+
(r'typeerror.*undefined', 0.9),
|
|
139
|
+
(r'cannot\s+read\s+propert', 0.9),
|
|
140
|
+
(r'is\s+not\s+a\s+function', 0.9),
|
|
141
|
+
],
|
|
142
|
+
'js_syntax': [
|
|
143
|
+
(r'syntaxerror.*javascript', 0.9),
|
|
144
|
+
(r'unexpected\s+token', 0.85),
|
|
145
|
+
],
|
|
146
|
+
'js_module': [
|
|
147
|
+
(r'cannot\s+find\s+module', 0.95),
|
|
148
|
+
(r'module\s+not\s+found', 0.9),
|
|
149
|
+
],
|
|
150
|
+
|
|
151
|
+
# Go errors
|
|
152
|
+
'go_panic': [
|
|
153
|
+
(r'panic:', 0.95),
|
|
154
|
+
(r'runtime\s+error:', 0.9),
|
|
155
|
+
],
|
|
156
|
+
'go_nil': [
|
|
157
|
+
(r'nil\s+pointer', 0.95),
|
|
158
|
+
(r'invalid\s+memory\s+address', 0.9),
|
|
159
|
+
],
|
|
160
|
+
|
|
161
|
+
# Rust errors
|
|
162
|
+
'rust_panic': [
|
|
163
|
+
(r'thread\s+.*\s+panicked', 0.95),
|
|
164
|
+
(r'called\s+`option::unwrap\(\)`', 0.9),
|
|
165
|
+
],
|
|
166
|
+
|
|
167
|
+
# Database errors
|
|
168
|
+
'database': [
|
|
169
|
+
(r'database\s+error', 0.85),
|
|
170
|
+
(r'sql\s+error', 0.9),
|
|
171
|
+
(r'connection\s+refused.*(?:5432|3306|27017)', 0.9), # Common DB ports
|
|
172
|
+
(r'operationalerror.*database', 0.9),
|
|
173
|
+
],
|
|
174
|
+
|
|
175
|
+
# Config/Environment errors
|
|
176
|
+
'config': [
|
|
177
|
+
(r'api\s*key\s+(?:not\s+(?:set|found|valid)|invalid)', 0.9),
|
|
178
|
+
(r'missing\s+(?:env|environment)\s+variable', 0.9),
|
|
179
|
+
(r'configuration\s+error', 0.85),
|
|
180
|
+
(r'\.env\s+(?:not\s+found|missing)', 0.9),
|
|
181
|
+
],
|
|
182
|
+
|
|
183
|
+
# Connection errors
|
|
184
|
+
'connection': [
|
|
185
|
+
(r'connection\s+refused', 0.9),
|
|
186
|
+
(r'econnrefused', 0.95),
|
|
187
|
+
(r'connection\s+timed?\s*out', 0.9),
|
|
188
|
+
(r'network\s+error', 0.8),
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Map error categories to error types
|
|
193
|
+
CATEGORY_TO_TYPE = {
|
|
194
|
+
'http_404': 'http',
|
|
195
|
+
'http_500': 'http',
|
|
196
|
+
'http_400': 'http',
|
|
197
|
+
'http_401': 'http',
|
|
198
|
+
'http_403': 'http',
|
|
199
|
+
'static_file': 'static',
|
|
200
|
+
'file_not_found': 'filesystem',
|
|
201
|
+
'python_import': 'import',
|
|
202
|
+
'python_attribute': 'runtime',
|
|
203
|
+
'python_type': 'runtime',
|
|
204
|
+
'python_value': 'runtime',
|
|
205
|
+
'python_key': 'runtime',
|
|
206
|
+
'python_index': 'runtime',
|
|
207
|
+
'python_name': 'runtime',
|
|
208
|
+
'python_syntax': 'syntax',
|
|
209
|
+
'js_reference': 'runtime',
|
|
210
|
+
'js_type': 'runtime',
|
|
211
|
+
'js_syntax': 'syntax',
|
|
212
|
+
'js_module': 'import',
|
|
213
|
+
'go_panic': 'runtime',
|
|
214
|
+
'go_nil': 'runtime',
|
|
215
|
+
'rust_panic': 'runtime',
|
|
216
|
+
'database': 'database',
|
|
217
|
+
'config': 'config',
|
|
218
|
+
'connection': 'connection',
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Language hints from error patterns
|
|
222
|
+
CATEGORY_TO_LANGUAGE = {
|
|
223
|
+
'python_import': Language.PYTHON,
|
|
224
|
+
'python_attribute': Language.PYTHON,
|
|
225
|
+
'python_type': Language.PYTHON,
|
|
226
|
+
'python_value': Language.PYTHON,
|
|
227
|
+
'python_key': Language.PYTHON,
|
|
228
|
+
'python_index': Language.PYTHON,
|
|
229
|
+
'python_name': Language.PYTHON,
|
|
230
|
+
'python_syntax': Language.PYTHON,
|
|
231
|
+
'js_reference': Language.JAVASCRIPT,
|
|
232
|
+
'js_type': Language.JAVASCRIPT,
|
|
233
|
+
'js_syntax': Language.JAVASCRIPT,
|
|
234
|
+
'js_module': Language.JAVASCRIPT,
|
|
235
|
+
'go_panic': Language.GO,
|
|
236
|
+
'go_nil': Language.GO,
|
|
237
|
+
'rust_panic': Language.RUST,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ErrorAnalyzer:
|
|
242
|
+
"""Analyzes errors to extract meaning and find relevant files."""
|
|
243
|
+
|
|
244
|
+
def __init__(self, project_scanner: Optional[ProjectScanner] = None):
|
|
245
|
+
"""Initialize the analyzer.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
project_scanner: Optional project scanner for file discovery
|
|
249
|
+
"""
|
|
250
|
+
self.scanner = project_scanner
|
|
251
|
+
|
|
252
|
+
def analyze(self, error_message: str) -> ErrorAnalysis:
|
|
253
|
+
"""Analyze an error message.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
error_message: The error message or log to analyze
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
ErrorAnalysis with extracted information
|
|
260
|
+
"""
|
|
261
|
+
error_lower = error_message.lower()
|
|
262
|
+
|
|
263
|
+
# Detect error category
|
|
264
|
+
category, confidence = self._detect_category(error_lower)
|
|
265
|
+
error_type = CATEGORY_TO_TYPE.get(category, 'unknown')
|
|
266
|
+
suggested_lang = CATEGORY_TO_LANGUAGE.get(category)
|
|
267
|
+
|
|
268
|
+
# Extract routes from HTTP errors
|
|
269
|
+
affected_routes = self._extract_routes(error_message)
|
|
270
|
+
|
|
271
|
+
# Extract keywords
|
|
272
|
+
keywords = self._extract_keywords(error_message)
|
|
273
|
+
|
|
274
|
+
# Find relevant files if scanner available
|
|
275
|
+
relevant_files = []
|
|
276
|
+
if self.scanner:
|
|
277
|
+
relevant_files = self.scanner.find_relevant_files(error_message, limit=5)
|
|
278
|
+
|
|
279
|
+
# If no language detected, use project's primary language
|
|
280
|
+
if not suggested_lang:
|
|
281
|
+
info = self.scanner.scan()
|
|
282
|
+
suggested_lang = info.primary_language
|
|
283
|
+
|
|
284
|
+
return ErrorAnalysis(
|
|
285
|
+
error_type=error_type,
|
|
286
|
+
error_category=category,
|
|
287
|
+
error_message=error_message[:500], # Truncate long messages
|
|
288
|
+
suggested_language=suggested_lang,
|
|
289
|
+
relevant_files=relevant_files,
|
|
290
|
+
affected_routes=affected_routes,
|
|
291
|
+
keywords=keywords,
|
|
292
|
+
confidence=confidence,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _detect_category(self, error_lower: str) -> Tuple[str, float]:
|
|
296
|
+
"""Detect error category from message.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Tuple of (category, confidence)
|
|
300
|
+
"""
|
|
301
|
+
best_category = 'unknown'
|
|
302
|
+
best_confidence = 0.0
|
|
303
|
+
|
|
304
|
+
for category, patterns in ERROR_PATTERNS.items():
|
|
305
|
+
for pattern, conf in patterns:
|
|
306
|
+
if re.search(pattern, error_lower):
|
|
307
|
+
if conf > best_confidence:
|
|
308
|
+
best_category = category
|
|
309
|
+
best_confidence = conf
|
|
310
|
+
|
|
311
|
+
return best_category, best_confidence
|
|
312
|
+
|
|
313
|
+
def _extract_routes(self, error_message: str) -> List[str]:
|
|
314
|
+
"""Extract URL routes from error message."""
|
|
315
|
+
routes = []
|
|
316
|
+
|
|
317
|
+
# Pattern for routes in "Cannot GET /path" style
|
|
318
|
+
route_patterns = [
|
|
319
|
+
r'cannot\s+(?:get|post|put|delete|patch)\s+([/\w\-\.]+)',
|
|
320
|
+
r'(?:get|post|put|delete|patch)\s+([/\w\-\.]+)\s+(?:404|failed)',
|
|
321
|
+
r'route\s+[\'"]?([/\w\-\.]+)[\'"]?',
|
|
322
|
+
r'path\s+[\'"]?([/\w\-\.]+)[\'"]?',
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
for pattern in route_patterns:
|
|
326
|
+
matches = re.findall(pattern, error_message, re.IGNORECASE)
|
|
327
|
+
routes.extend(matches)
|
|
328
|
+
|
|
329
|
+
# Deduplicate
|
|
330
|
+
return list(dict.fromkeys(routes))
|
|
331
|
+
|
|
332
|
+
def _extract_keywords(self, error_message: str) -> List[str]:
|
|
333
|
+
"""Extract relevant keywords from error."""
|
|
334
|
+
keywords = []
|
|
335
|
+
|
|
336
|
+
# Extract quoted strings
|
|
337
|
+
quoted = re.findall(r'[\'"]([^\'"]{2,30})[\'"]', error_message)
|
|
338
|
+
keywords.extend(quoted)
|
|
339
|
+
|
|
340
|
+
# Extract identifiers
|
|
341
|
+
identifiers = re.findall(r'\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b', error_message)
|
|
342
|
+
keywords.extend(identifiers)
|
|
343
|
+
|
|
344
|
+
identifiers = re.findall(r'\b([a-z]+(?:_[a-z]+)+)\b', error_message)
|
|
345
|
+
keywords.extend(identifiers)
|
|
346
|
+
|
|
347
|
+
# Extract file references
|
|
348
|
+
files = re.findall(r'[\w\-]+\.(?:py|js|ts|go|rs|java)', error_message)
|
|
349
|
+
keywords.extend(files)
|
|
350
|
+
|
|
351
|
+
# Deduplicate and limit
|
|
352
|
+
return list(dict.fromkeys(keywords))[:20]
|
|
353
|
+
|
|
354
|
+
def get_fix_context(
|
|
355
|
+
self,
|
|
356
|
+
error_message: str,
|
|
357
|
+
include_project_structure: bool = True,
|
|
358
|
+
include_file_contents: bool = True,
|
|
359
|
+
max_files: int = 3,
|
|
360
|
+
) -> str:
|
|
361
|
+
"""Get comprehensive context for AI to fix the error.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
error_message: The error message
|
|
365
|
+
include_project_structure: Include project structure info
|
|
366
|
+
include_file_contents: Include relevant file contents
|
|
367
|
+
max_files: Maximum file contents to include
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Formatted context string for AI
|
|
371
|
+
"""
|
|
372
|
+
analysis = self.analyze(error_message)
|
|
373
|
+
parts = []
|
|
374
|
+
|
|
375
|
+
# Error analysis
|
|
376
|
+
parts.append(analysis.to_context_string())
|
|
377
|
+
|
|
378
|
+
# Project structure
|
|
379
|
+
if include_project_structure and self.scanner:
|
|
380
|
+
parts.append("")
|
|
381
|
+
parts.append(self.scanner.get_project_context(max_files=2))
|
|
382
|
+
|
|
383
|
+
# Relevant file contents
|
|
384
|
+
if include_file_contents and self.scanner and analysis.relevant_files:
|
|
385
|
+
parts.append("")
|
|
386
|
+
parts.append("## RELEVANT SOURCE FILES")
|
|
387
|
+
|
|
388
|
+
for pf in analysis.relevant_files[:max_files]:
|
|
389
|
+
content = self.scanner.get_file_content(pf.path)
|
|
390
|
+
if content:
|
|
391
|
+
parts.append(f"\n### {pf.path}")
|
|
392
|
+
parts.append(f"```{pf.language.value}")
|
|
393
|
+
# Truncate long files
|
|
394
|
+
if len(content) > 3000:
|
|
395
|
+
content = content[:3000] + "\n... (truncated)"
|
|
396
|
+
parts.append(content)
|
|
397
|
+
parts.append("```")
|
|
398
|
+
|
|
399
|
+
return "\n".join(parts)
|