codebase-brain 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.
- brain_cli.py +331 -0
- brain_parser/__init__.py +0 -0
- brain_parser/ast_parser.py +62 -0
- brain_parser/bug_detector.py +212 -0
- brain_parser/codebase_walker.py +101 -0
- brain_parser/file_watcher.py +81 -0
- brain_parser/graph_builder.py +190 -0
- brain_parser/llm_router.py +75 -0
- brain_parser/query_engine.py +127 -0
- brain_parser/root_cause.py +162 -0
- brain_parser/universal_parser.py +215 -0
- codebase_brain-0.1.0.dist-info/METADATA +13 -0
- codebase_brain-0.1.0.dist-info/RECORD +16 -0
- codebase_brain-0.1.0.dist-info/WHEEL +5 -0
- codebase_brain-0.1.0.dist-info/entry_points.txt +2 -0
- codebase_brain-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
from brain_parser.codebase_walker import load_brain
|
|
4
|
+
from brain_parser.graph_builder import build_graph, calculate_risk
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Patterns for each language stacktrace format
|
|
8
|
+
TRACE_PATTERNS = [
|
|
9
|
+
# JavaScript/Node: at functionName (file.js:34:12)
|
|
10
|
+
r'at\s+(?:\S+\s+)?\((.+?\.(?:js|ts)):(\d+)',
|
|
11
|
+
# Python: File "file.py", line 34
|
|
12
|
+
r'File\s+"(.+?\.py)",\s+line\s+(\d+)',
|
|
13
|
+
# Java: at com.example.Class(File.java:34)
|
|
14
|
+
r'at\s+[\w.]+\((\w+\.java):(\d+)\)',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_frames(stacktrace):
|
|
19
|
+
"""Extract (filepath, line_number) pairs from any stacktrace."""
|
|
20
|
+
frames = []
|
|
21
|
+
for pattern in TRACE_PATTERNS:
|
|
22
|
+
matches = re.findall(pattern, stacktrace)
|
|
23
|
+
for filepath, line in matches:
|
|
24
|
+
frames.append({
|
|
25
|
+
'file': filepath.strip(),
|
|
26
|
+
'line': int(line)
|
|
27
|
+
})
|
|
28
|
+
# deduplicate preserving order
|
|
29
|
+
seen = set()
|
|
30
|
+
unique = []
|
|
31
|
+
for f in frames:
|
|
32
|
+
key = (f['file'], f['line'])
|
|
33
|
+
if key not in seen:
|
|
34
|
+
seen.add(key)
|
|
35
|
+
unique.append(f)
|
|
36
|
+
return unique
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def match_frame_to_graph(frame_file, G):
|
|
40
|
+
"""Match a short filename from stacktrace to full path in graph."""
|
|
41
|
+
frame_norm = frame_file.replace('\\', '/')
|
|
42
|
+
candidates = []
|
|
43
|
+
|
|
44
|
+
for node in G.nodes():
|
|
45
|
+
node_norm = node.replace('\\', '/')
|
|
46
|
+
# exact suffix match — auth/middleware.js matches ./src/auth/middleware.js
|
|
47
|
+
if node_norm.endswith(frame_norm):
|
|
48
|
+
candidates.append(node)
|
|
49
|
+
|
|
50
|
+
if not candidates:
|
|
51
|
+
return None
|
|
52
|
+
# prefer shortest path — most specific match
|
|
53
|
+
return min(candidates, key=len)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def find_root_cause(stacktrace):
|
|
57
|
+
"""Main function — paste stacktrace, get root cause analysis."""
|
|
58
|
+
brain = load_brain()
|
|
59
|
+
if not brain:
|
|
60
|
+
return {"error": "No brain found. Run 'brain start' first."}
|
|
61
|
+
|
|
62
|
+
G = build_graph(brain)
|
|
63
|
+
risk = calculate_risk(G)
|
|
64
|
+
|
|
65
|
+
frames = extract_frames(stacktrace)
|
|
66
|
+
if not frames:
|
|
67
|
+
return {"error": "No file references found in stacktrace. Paste the full error including file paths."}
|
|
68
|
+
|
|
69
|
+
results = []
|
|
70
|
+
for frame in frames:
|
|
71
|
+
matched_node = match_frame_to_graph(frame['file'], G)
|
|
72
|
+
if not matched_node:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# walk backwards — what files feed into this file
|
|
76
|
+
import networkx as nx
|
|
77
|
+
predecessors = list(G.predecessors(matched_node))
|
|
78
|
+
ancestors = list(nx.ancestors(G, matched_node))
|
|
79
|
+
|
|
80
|
+
# find functions in this file near the error line
|
|
81
|
+
file_data = brain.get(matched_node, {})
|
|
82
|
+
functions = file_data.get('functions', [])
|
|
83
|
+
nearby_functions = [
|
|
84
|
+
f for f in functions
|
|
85
|
+
if abs(f['line'] - frame['line']) <= 10
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
results.append({
|
|
89
|
+
'file': matched_node,
|
|
90
|
+
'line': frame['line'],
|
|
91
|
+
'risk': risk.get(matched_node, 'LOW'),
|
|
92
|
+
'nearby_functions': nearby_functions,
|
|
93
|
+
'direct_causes': predecessors,
|
|
94
|
+
'all_causes': ancestors,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if not results:
|
|
98
|
+
return {"error": "Files in stacktrace not found in brain. Run 'brain start' to rebuild."}
|
|
99
|
+
|
|
100
|
+
return _format_result(results, risk, stacktrace)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _format_result(results, risk,stacktrace=""):
|
|
104
|
+
lines = []
|
|
105
|
+
lines.append("ROOT CAUSE ANALYSIS")
|
|
106
|
+
lines.append("=" * 50)
|
|
107
|
+
|
|
108
|
+
for i, r in enumerate(results, 1):
|
|
109
|
+
lines.append(f"\n[{i}] {r['file']} line {r['line']} [{r['risk']}]")
|
|
110
|
+
|
|
111
|
+
if r['nearby_functions']:
|
|
112
|
+
lines.append(" Functions at error site:")
|
|
113
|
+
for f in r['nearby_functions']:
|
|
114
|
+
lines.append(f" → {f['name']}() line {f['line']}")
|
|
115
|
+
|
|
116
|
+
if r['direct_causes']:
|
|
117
|
+
lines.append(" Direct causes (files that feed this):")
|
|
118
|
+
for f in r['direct_causes']:
|
|
119
|
+
lines.append(f" ← {f} [{risk.get(f, 'LOW')}]")
|
|
120
|
+
|
|
121
|
+
if r['all_causes']:
|
|
122
|
+
lines.append(f" Full cause chain: {len(r['all_causes'])} files upstream")
|
|
123
|
+
|
|
124
|
+
# Add Groq explanation
|
|
125
|
+
lines.append("\n" + "=" * 50)
|
|
126
|
+
lines.append("BRAIN DIAGNOSIS")
|
|
127
|
+
lines.append("=" * 50)
|
|
128
|
+
lines.append(_ask_groq_diagnosis(results, risk, stacktrace))
|
|
129
|
+
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _ask_groq_diagnosis(results, risk, stacktrace=""):
|
|
134
|
+
from brain_parser.llm_router import call_llm
|
|
135
|
+
|
|
136
|
+
context = ""
|
|
137
|
+
for r in results:
|
|
138
|
+
context += f"\nFile: {r['file']} (line {r['line']}, risk: {r['risk']})\n"
|
|
139
|
+
if r['nearby_functions']:
|
|
140
|
+
context += f"Functions: {[f['name'] for f in r['nearby_functions']]}\n"
|
|
141
|
+
if r['direct_causes']:
|
|
142
|
+
context += f"Fed by: {r['direct_causes']}\n"
|
|
143
|
+
|
|
144
|
+
prompt = f"""You are a senior developer doing root cause analysis.
|
|
145
|
+
|
|
146
|
+
Original error:
|
|
147
|
+
{stacktrace}
|
|
148
|
+
|
|
149
|
+
Dependency chain:
|
|
150
|
+
{context}
|
|
151
|
+
|
|
152
|
+
In 3 sentences max:
|
|
153
|
+
1. Which file is the root cause and exactly why based on the error message
|
|
154
|
+
2. What the developer should check first — name the specific function
|
|
155
|
+
3. What likely went wrong — be specific about the error type
|
|
156
|
+
|
|
157
|
+
Name files and functions. No generic answers."""
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
return call_llm(prompt, max_tokens=200)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return f"Could not get AI diagnosis: {str(e)}"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tree_sitter_python as tspython
|
|
3
|
+
import tree_sitter_javascript as tsjavascript
|
|
4
|
+
import tree_sitter_java as tsjava
|
|
5
|
+
import tree_sitter_typescript as tstypescript
|
|
6
|
+
import hashlib
|
|
7
|
+
from tree_sitter import Language, Parser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
UNKNOWN_FILES = []
|
|
11
|
+
|
|
12
|
+
PY_LANGUAGE = Language(tspython.language())
|
|
13
|
+
JS_LANGUAGE = Language(tsjavascript.language())
|
|
14
|
+
JAVA_LANGUAGE=Language(tsjava.language())
|
|
15
|
+
TS_LANGUAGE = Language(tstypescript.language_typescript())
|
|
16
|
+
|
|
17
|
+
LANGUAGE_MAP = {
|
|
18
|
+
'python': PY_LANGUAGE,
|
|
19
|
+
'javascript': JS_LANGUAGE,
|
|
20
|
+
'java': JAVA_LANGUAGE,
|
|
21
|
+
'typescript':TS_LANGUAGE
|
|
22
|
+
}
|
|
23
|
+
FUNCTION_NODES = {
|
|
24
|
+
'python': ['function_definition'],
|
|
25
|
+
'javascript': ['function_declaration', 'arrow_function', 'function_expression'],
|
|
26
|
+
'java': ['method_declaration'],
|
|
27
|
+
'typescript': ['function_declaration', 'arrow_function', 'function_expression'],
|
|
28
|
+
}
|
|
29
|
+
CLASS_NODES = {
|
|
30
|
+
'python': ['class_definition'],
|
|
31
|
+
'javascript': ['class_declaration'],
|
|
32
|
+
'java': ['class_declaration'],
|
|
33
|
+
'typescript': ['class_declaration'],
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
IMPORT_NODES = {
|
|
37
|
+
'python': ['import_statement', 'import_from_statement'],
|
|
38
|
+
'javascript': ['import_statement'],
|
|
39
|
+
'java': ['import_declaration'],
|
|
40
|
+
'typescript': ['import_statement'],
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def detect_language(filepath):
|
|
45
|
+
filepath = os.path.abspath(filepath).replace("\\", "/")
|
|
46
|
+
|
|
47
|
+
ext_map = {
|
|
48
|
+
'.py': 'python',
|
|
49
|
+
'.js': 'javascript',
|
|
50
|
+
'.ts': 'typescript',
|
|
51
|
+
'.java': 'java',
|
|
52
|
+
'.go': 'go',
|
|
53
|
+
'.rb': 'ruby',
|
|
54
|
+
'.cpp': 'cpp',
|
|
55
|
+
'.cs': 'csharp',
|
|
56
|
+
'.html': 'html',
|
|
57
|
+
'.css': 'css',
|
|
58
|
+
'.json': 'json',
|
|
59
|
+
'.yaml': 'yaml',
|
|
60
|
+
'.yml': 'yaml',
|
|
61
|
+
'.md': 'markdown',
|
|
62
|
+
'.sql': 'sql',
|
|
63
|
+
'.php': 'php',
|
|
64
|
+
'.swift': 'swift',
|
|
65
|
+
'.kt': 'kotlin',
|
|
66
|
+
'.rs': 'rust',
|
|
67
|
+
'.c': 'c',
|
|
68
|
+
'.sh': 'shell',
|
|
69
|
+
'.env': 'env',
|
|
70
|
+
'.xml': 'xml',
|
|
71
|
+
'.toml': 'toml',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ext = os.path.splitext(filepath)[1].lower()
|
|
75
|
+
language = ext_map.get(ext, 'unknown')
|
|
76
|
+
|
|
77
|
+
if language == 'unknown':
|
|
78
|
+
UNKNOWN_FILES.append(filepath)
|
|
79
|
+
|
|
80
|
+
return language
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_file_hash(content):
|
|
84
|
+
# generate md5 hash of file content
|
|
85
|
+
return hashlib.md5(content.encode('utf-8')).hexdigest()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def extract_functions(tree, code_bytes, node_types):
|
|
89
|
+
functions = []
|
|
90
|
+
|
|
91
|
+
def walk(node):
|
|
92
|
+
if node.type in node_types:
|
|
93
|
+
name_node = node.child_by_field_name('name')
|
|
94
|
+
if name_node:
|
|
95
|
+
functions.append({
|
|
96
|
+
'name': code_bytes[name_node.start_byte:name_node.end_byte].decode('utf-8'),
|
|
97
|
+
'line': node.start_point[0] + 1
|
|
98
|
+
})
|
|
99
|
+
elif node.parent and node.parent.type == 'variable_declarator':
|
|
100
|
+
name_node = node.parent.child_by_field_name('name')
|
|
101
|
+
if name_node:
|
|
102
|
+
functions.append({
|
|
103
|
+
'name': code_bytes[name_node.start_byte:name_node.end_byte].decode('utf-8'),
|
|
104
|
+
'line': node.start_point[0] + 1
|
|
105
|
+
})
|
|
106
|
+
for child in node.children:
|
|
107
|
+
walk(child)
|
|
108
|
+
|
|
109
|
+
walk(tree.root_node)
|
|
110
|
+
return functions
|
|
111
|
+
|
|
112
|
+
def extract_classes(tree, code_bytes,node_types):
|
|
113
|
+
# loop tree → find class_definition nodes → extract name and line number
|
|
114
|
+
classes = []
|
|
115
|
+
|
|
116
|
+
def walk(node):
|
|
117
|
+
if node.type in node_types:
|
|
118
|
+
name_node = node.child_by_field_name('name')
|
|
119
|
+
if name_node:
|
|
120
|
+
classes.append({
|
|
121
|
+
'name': code_bytes[name_node.start_byte:name_node.end_byte].decode('utf-8'),
|
|
122
|
+
'line': node.start_point[0] + 1
|
|
123
|
+
})
|
|
124
|
+
for child in node.children:
|
|
125
|
+
walk(child)
|
|
126
|
+
|
|
127
|
+
walk(tree.root_node)
|
|
128
|
+
return classes
|
|
129
|
+
|
|
130
|
+
def extract_imports(tree, code_bytes,node_types):
|
|
131
|
+
# loop tree → find import nodes → extract module name and line number
|
|
132
|
+
imports = []
|
|
133
|
+
|
|
134
|
+
def walk(node):
|
|
135
|
+
if node.type in node_types:
|
|
136
|
+
imports.append({
|
|
137
|
+
'name': code_bytes[node.start_byte:node.end_byte].decode('utf-8').strip(),
|
|
138
|
+
'line': node.start_point[0] + 1
|
|
139
|
+
})
|
|
140
|
+
for child in node.children:
|
|
141
|
+
walk(child)
|
|
142
|
+
|
|
143
|
+
walk(tree.root_node)
|
|
144
|
+
return imports
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def summarize_file(filepath, content, language):
|
|
148
|
+
# call Groq → get 3 line summary → store instead of raw content
|
|
149
|
+
try:
|
|
150
|
+
from groq import Groq
|
|
151
|
+
from dotenv import load_dotenv
|
|
152
|
+
load_dotenv()
|
|
153
|
+
|
|
154
|
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
|
155
|
+
|
|
156
|
+
filename = filepath.split('/')[-1]
|
|
157
|
+
prompt = f"""Summarize this {language} file in exactly 3 sentences.
|
|
158
|
+
Focus on: what it does, what functions matter, what it connects to.
|
|
159
|
+
File: {filename}
|
|
160
|
+
Code:
|
|
161
|
+
{content[:3000]}"""
|
|
162
|
+
|
|
163
|
+
response = client.chat.completions.create(
|
|
164
|
+
model="llama-3.3-70b-versatile",
|
|
165
|
+
messages=[{"role": "user", "content": prompt}],
|
|
166
|
+
max_tokens=150
|
|
167
|
+
)
|
|
168
|
+
return response.choices[0].message.content
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Could not summarize: {str(e)}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_file(filepath):
|
|
174
|
+
filepath = os.path.abspath(filepath).replace("\\", "/")
|
|
175
|
+
language = detect_language(filepath)
|
|
176
|
+
|
|
177
|
+
# unknown files → log and return empty structure
|
|
178
|
+
if language == 'unknown':
|
|
179
|
+
return {
|
|
180
|
+
'filepath': filepath,
|
|
181
|
+
'language': 'unknown',
|
|
182
|
+
'imports': [],
|
|
183
|
+
'functions': [],
|
|
184
|
+
'classes': [],
|
|
185
|
+
'summary': 'unknown file - could not parse'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# read raw file content
|
|
189
|
+
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
190
|
+
content = f.read()
|
|
191
|
+
|
|
192
|
+
code_bytes = content.encode('utf-8')
|
|
193
|
+
functions = []
|
|
194
|
+
classes = []
|
|
195
|
+
imports = []
|
|
196
|
+
|
|
197
|
+
# if language supported by tree-sitter → parse and extract functions
|
|
198
|
+
if language in LANGUAGE_MAP:
|
|
199
|
+
parser = Parser(LANGUAGE_MAP[language])
|
|
200
|
+
tree = parser.parse(code_bytes)
|
|
201
|
+
functions = extract_functions(tree, code_bytes, FUNCTION_NODES.get(language, []))
|
|
202
|
+
classes = extract_classes(tree, code_bytes,CLASS_NODES.get(language,[]))
|
|
203
|
+
imports = extract_imports(tree, code_bytes, IMPORT_NODES.get(language, []))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
'filepath': filepath,
|
|
208
|
+
'language': language,
|
|
209
|
+
'imports': imports,
|
|
210
|
+
'functions': functions,
|
|
211
|
+
'classes': classes,
|
|
212
|
+
'summary': '',
|
|
213
|
+
'content': content ,
|
|
214
|
+
'hash': get_file_hash(content)
|
|
215
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codebase-brain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An AI brain that lives inside your codebase
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: watchdog
|
|
7
|
+
Requires-Dist: networkx
|
|
8
|
+
Requires-Dist: matplotlib
|
|
9
|
+
Requires-Dist: pyvis
|
|
10
|
+
Requires-Dist: groq
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
brain_cli.py,sha256=sdJ5r60B4ELMruWR8B9azejpnizFkhdDDv0IrPhMLvE,11580
|
|
2
|
+
brain_parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
brain_parser/ast_parser.py,sha256=3Vy1XXyInjgA2xn6IBYv4K1juEdtFwAo58upqukCHXI,1799
|
|
4
|
+
brain_parser/bug_detector.py,sha256=gvXBUFFk3RYoNQC-mToo80ytR7VZr_t1TfFEiSStmvo,7652
|
|
5
|
+
brain_parser/codebase_walker.py,sha256=6uU1yJ6leqmHcdlSm-xcq1DOG1mdv4F0-JupSMZrSwE,3582
|
|
6
|
+
brain_parser/file_watcher.py,sha256=cuok95kDJy6uKfsz_HByKWLBvRIPUAz7djAaXQ-2T3Q,2565
|
|
7
|
+
brain_parser/graph_builder.py,sha256=fpQqY46oq4uqTzysPyhbUAaAwwJGV_yH5kQBW4xtuSs,5218
|
|
8
|
+
brain_parser/llm_router.py,sha256=f86O4tUw3MV1s2dq6axEzwJHEQfyboaqPr_QDR8CtK0,2447
|
|
9
|
+
brain_parser/query_engine.py,sha256=Lunb_-9z1Lk2VYOE7f_-jF8X2AQtby1tPFXgFUjqnpA,3671
|
|
10
|
+
brain_parser/root_cause.py,sha256=jvRB9OzFREU0oguGhfoU8wV2mCJ7A85y2BohybzIQ-0,5326
|
|
11
|
+
brain_parser/universal_parser.py,sha256=47StXj-GPGJb4g0WzM5VYgM3tIc1tOzwEa8mcL_q9AY,6694
|
|
12
|
+
codebase_brain-0.1.0.dist-info/METADATA,sha256=Eep4Qd7cPOarafSDhm6aysPe6lbhMk1fcPWJGqd3UVg,327
|
|
13
|
+
codebase_brain-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
codebase_brain-0.1.0.dist-info/entry_points.txt,sha256=O9yHP4zy_uoVPxmewGbdNjNksmxRYUv34MQJDRXXodc,41
|
|
15
|
+
codebase_brain-0.1.0.dist-info/top_level.txt,sha256=RmSvznU63-vmm5zLSKAlYYsFzQ8X3RyPrB-m0q0uQfI,23
|
|
16
|
+
codebase_brain-0.1.0.dist-info/RECORD,,
|