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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ brain = brain_cli:main
@@ -0,0 +1,2 @@
1
+ brain_cli
2
+ brain_parser