codebase-brain 0.1.0__tar.gz → 0.2.0__tar.gz

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 (26) hide show
  1. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/PKG-INFO +1 -1
  2. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/bug_detector.py +15 -14
  3. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/codebase_walker.py +11 -6
  4. codebase_brain-0.2.0/brain_parser/graph_builder.py +304 -0
  5. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/PKG-INFO +1 -1
  6. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/setup.py +1 -1
  7. codebase_brain-0.1.0/brain_parser/graph_builder.py +0 -190
  8. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/README.md +0 -0
  9. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_cli.py +0 -0
  10. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/__init__.py +0 -0
  11. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/ast_parser.py +0 -0
  12. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/file_watcher.py +0 -0
  13. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/llm_router.py +0 -0
  14. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/query_engine.py +0 -0
  15. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/root_cause.py +0 -0
  16. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/universal_parser.py +0 -0
  17. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/SOURCES.txt +0 -0
  18. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/dependency_links.txt +0 -0
  19. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/entry_points.txt +0 -0
  20. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/requires.txt +0 -0
  21. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/top_level.txt +0 -0
  22. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/setup.cfg +0 -0
  23. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_bugs.py +0 -0
  24. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_classes.py +0 -0
  25. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_parser.py +0 -0
  26. {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_treesitter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brain
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: An AI brain that lives inside your codebase
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: watchdog
@@ -9,32 +9,33 @@ def detect_circular_dependencies(brain, temp_path=None):
9
9
  G = build_graph(brain)
10
10
  bugs = []
11
11
 
12
- def clean(path):
12
+ def rel(path):
13
13
  p = path.replace('\\', '/')
14
14
  idx = p.find('/temp/')
15
15
  if idx != -1:
16
16
  after_temp = p[idx + 6:]
17
17
  parts = after_temp.split('/', 1)
18
- if len(parts) > 1:
19
- return parts[1]
20
- return parts[0]
21
- # fallback — just filename
22
- return p.split('/')[-1]
18
+ p = parts[1] if len(parts) > 1 else parts[0]
19
+ if '/src/' in p:
20
+ p = p[p.find('/src/') + 1:]
21
+ elif p.startswith('./'):
22
+ p = p[2:]
23
+ return p
23
24
 
24
25
  try:
25
- cycles = list(nx.simple_cycles(G))
26
- for cycle in cycles:
27
- clean_cycle = [clean(f) for f in cycle]
28
- # format as numbered list not one long line
29
- chain = '\n '.join([f"{i+1}. {f}" for i, f in enumerate(clean_cycle)])
30
- chain += f"\n → back to {clean_cycle[0]}"
26
+ sccs = [scc for scc in nx.strongly_connected_components(G) if len(scc) > 1]
27
+
28
+ for scc in sccs:
29
+ clean_files = sorted(list(set([rel(f).split('/')[-1] for f in scc])))
30
+ chain = '\n '.join([f"{i+1}. {f}" for i, f in enumerate(clean_files)])
31
31
  bugs.append({
32
32
  'type': 'circular_dependency',
33
33
  'severity': 'HIGH',
34
- 'files': clean_cycle,
35
- 'message': f"Circular dependency ({len(cycle)} files):\n {chain}",
34
+ 'files': clean_files,
35
+ 'message': f"Circular dependency group ({len(clean_files)} files):\n {chain}",
36
36
  'fix': 'Extract shared logic into a separate file that both can import from.'
37
37
  })
38
+
38
39
  except Exception as e:
39
40
  print(f"Cycle detection error: {e}")
40
41
 
@@ -53,18 +53,23 @@ def walk_codebase(root_path):
53
53
 
54
54
  return brain
55
55
 
56
- BRAIN_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'brain.json')
56
+ def get_brain_path():
57
+ """Always returns brain.json in the current working directory."""
58
+ return os.path.join(os.getcwd(), 'brain.json')
57
59
 
58
- def save_brain(brain, output_path=BRAIN_PATH):
60
+ def save_brain(brain, output_path=None):
61
+ if output_path is None:
62
+ output_path = get_brain_path()
59
63
  with open(output_path, "w") as f:
60
64
  json.dump(brain, f, indent=4)
61
65
  print(f"Brain saved to {output_path}")
62
66
 
63
- def load_brain(input_path=BRAIN_PATH):
64
- if not os.path.exists(input_path):
65
- print("No brain found. Run walker first.")
67
+ def load_brain(output_path=None):
68
+ if output_path is None:
69
+ output_path = get_brain_path()
70
+ if not os.path.exists(output_path):
66
71
  return None
67
- with open(input_path, "r") as f:
72
+ with open(output_path, 'r') as f:
68
73
  return json.load(f)
69
74
 
70
75
 
@@ -0,0 +1,304 @@
1
+ import networkx as nx
2
+ import matplotlib.pyplot as plt
3
+ from brain_parser.codebase_walker import load_brain
4
+ from pyvis.network import Network
5
+
6
+
7
+ def build_graph(brain):
8
+ G = nx.DiGraph()
9
+
10
+ for filepath, data in brain.items():
11
+ G.add_node(filepath)
12
+
13
+ for imp in data.get('imports', []):
14
+ raw = imp.get('name', '')
15
+
16
+ # extract module from "from x import y" or "import x"
17
+ if raw.startswith('from '):
18
+ module = raw.split('from ')[1].split(' import')[0].strip()
19
+ elif raw.startswith('import '):
20
+ module = raw.replace('import ', '').split(' as ')[0].strip()
21
+ else:
22
+ continue
23
+
24
+ # convert module path to file path format
25
+ # tests.circular_test.file_b → tests/circular_test/file_b
26
+ module_as_path = module.replace('.', '/').lower()
27
+
28
+ for other_file in brain.keys():
29
+ other_normalized = other_file.replace('\\', '/').lower()
30
+ # match whole filename not partial
31
+ filename = other_normalized.split('/')[-1].replace('.py', '').replace('.js', '').replace('.java',
32
+ '').replace(
33
+ '.ts', '')
34
+ if module_as_path == filename or other_normalized.endswith(module_as_path + '.py'):
35
+ G.add_edge(filepath, other_file)
36
+
37
+ return G
38
+
39
+
40
+ def visualize_graph(G):
41
+ risk = calculate_risk(G)
42
+
43
+ color_map = {
44
+ "HIGH": "red",
45
+ "MEDIUM": "orange",
46
+ "LOW": "lightgreen"
47
+ }
48
+
49
+ node_colors = [color_map[risk[node]] for node in G.nodes()]
50
+
51
+ plt.figure(figsize=(10, 7))
52
+ pos = nx.spring_layout(G)
53
+
54
+ nx.draw(G, pos,
55
+ with_labels=True,
56
+ node_color=node_colors,
57
+ node_size=2000,
58
+ font_size=8,
59
+ arrows=True,
60
+ edge_color='gray',
61
+ font_weight='bold'
62
+ )
63
+
64
+ plt.title("Live Codebase Brain - Risk Map")
65
+ plt.savefig("brain_graph.png")
66
+ plt.show()
67
+
68
+
69
+ def calculate_risk(G):
70
+ risk = {}
71
+
72
+ for node in G.nodes():
73
+ in_degree = G.in_degree(node)
74
+
75
+ if in_degree >= 2:
76
+ risk[node] = "HIGH"
77
+ elif in_degree == 1:
78
+ risk[node] = "MEDIUM"
79
+ else:
80
+ risk[node] = "LOW"
81
+
82
+ return risk
83
+
84
+ def visualize_interactive(G):
85
+ from pyvis.network import Network
86
+ import os
87
+
88
+ risk = calculate_risk(G)
89
+
90
+ net = Network(
91
+ height="100vh",
92
+ width="100%",
93
+ bgcolor="#0d0d0d",
94
+ font_color="#ffffff",
95
+ directed=True,
96
+ cdn_resources='in_line'
97
+ )
98
+
99
+ color_map = {
100
+ "HIGH": "#ff3860",
101
+ "MEDIUM": "#ffdd57",
102
+ "LOW": "#23d160"
103
+ }
104
+
105
+ # build node map first — ensures consistent keys for edges
106
+ node_map = {}
107
+ for node in G.nodes():
108
+ node_map[node] = str(node)
109
+
110
+ for node, node_id in node_map.items():
111
+ risk_level = risk.get(node, 'LOW')
112
+ color = color_map[risk_level]
113
+ label = node.replace('\\', '/').split('/')[-1]
114
+ size = 18 + (G.in_degree(node) * 5)
115
+
116
+ net.add_node(
117
+ node_id,
118
+ label=label,
119
+ color={
120
+ 'background': color,
121
+ 'border': color,
122
+ 'highlight': {'background': '#ffffff', 'border': color}
123
+ },
124
+ size=size,
125
+ font={'size': 14, 'color': '#ffffff', 'face': 'monospace'},
126
+ borderWidth=2,
127
+ shadow=True,
128
+ title=f"File: {label}\nRisk: {risk_level}\nConnections: {G.in_degree(node)}"
129
+ )
130
+
131
+ for edge in G.edges():
132
+ src = node_map.get(edge[0], str(edge[0]))
133
+ dst = node_map.get(edge[1], str(edge[1]))
134
+ if src in net.get_nodes() and dst in net.get_nodes():
135
+ net.add_edge(
136
+ src, dst,
137
+ color={'color': '#ffffff33'},
138
+ width=1.5,
139
+ arrows={'to': {'enabled': True, 'scaleFactor': 0.6}}
140
+ )
141
+
142
+ net.set_options("""
143
+ {
144
+ "nodes": {
145
+ "borderWidth": 2,
146
+ "shadow": {
147
+ "enabled": true,
148
+ "color": "rgba(0,0,0,0.8)",
149
+ "size": 15,
150
+ "x": 0,
151
+ "y": 0
152
+ }
153
+ },
154
+ "edges": {
155
+ "smooth": {
156
+ "type": "curvedCW",
157
+ "roundness": 0.2
158
+ },
159
+ "shadow": false
160
+ },
161
+ "interaction": {
162
+ "hover": true,
163
+ "navigationButtons": false,
164
+ "hideEdgesOnDrag": true,
165
+ "tooltipDelay": 100
166
+ },
167
+ "physics": {
168
+ "stabilization": {
169
+ "enabled": true,
170
+ "iterations": 200
171
+ },
172
+ "barnesHut": {
173
+ "gravitationalConstant": -12000,
174
+ "centralGravity": 0.1,
175
+ "springLength": 250,
176
+ "springConstant": 0.02,
177
+ "damping": 0.09
178
+ }
179
+ }
180
+ }
181
+ """)
182
+
183
+ output_path = os.path.join(os.getcwd(), 'brain_map.html')
184
+
185
+ html_content = net.generate_html()
186
+ with open(output_path, 'w', encoding='utf-8') as f:
187
+ f.write(html_content)
188
+
189
+ with open(output_path, 'r', encoding='utf-8') as f:
190
+ html = f.read()
191
+
192
+ custom_style = """
193
+ <style>
194
+ * { margin: 0; padding: 0; box-sizing: border-box; }
195
+ body { background: #0d0d0d; font-family: 'Courier New', monospace; }
196
+ #mynetwork {
197
+ background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d0d 100%);
198
+ border: none !important;
199
+ }
200
+ .legend {
201
+ position: fixed;
202
+ top: 20px;
203
+ left: 20px;
204
+ background: rgba(13,13,13,0.9);
205
+ border: 1px solid #333;
206
+ border-radius: 12px;
207
+ padding: 16px 20px;
208
+ color: #fff;
209
+ font-family: monospace;
210
+ font-size: 13px;
211
+ z-index: 1000;
212
+ backdrop-filter: blur(10px);
213
+ }
214
+ .legend-title {
215
+ color: #888;
216
+ font-size: 11px;
217
+ letter-spacing: 2px;
218
+ text-transform: uppercase;
219
+ margin-bottom: 12px;
220
+ }
221
+ .legend-item { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
222
+ .dot { width: 12px; height: 12px; border-radius: 50%; box-shadow: 0 0 8px currentColor; }
223
+ .high { background: #ff3860; color: #ff3860; }
224
+ .medium { background: #ffdd57; color: #ffdd57; }
225
+ .low { background: #23d160; color: #23d160; }
226
+ .brain-title {
227
+ position: fixed;
228
+ top: 20px;
229
+ right: 20px;
230
+ color: #444;
231
+ font-family: monospace;
232
+ font-size: 12px;
233
+ letter-spacing: 3px;
234
+ text-transform: uppercase;
235
+ z-index: 1000;
236
+ }
237
+ div.vis-tooltip {
238
+ background: #1a1a2e !important;
239
+ border: 1px solid #ff3860 !important;
240
+ border-radius: 8px !important;
241
+ color: #fff !important;
242
+ font-family: monospace !important;
243
+ font-size: 12px !important;
244
+ padding: 10px 14px !important;
245
+ }
246
+ </style>
247
+ <div class="legend">
248
+ <div class="legend-title">Risk Level</div>
249
+ <div class="legend-item"><div class="dot high"></div><span>High Risk</span></div>
250
+ <div class="legend-item"><div class="dot medium"></div><span>Medium Risk</span></div>
251
+ <div class="legend-item"><div class="dot low"></div><span>Low Risk</span></div>
252
+ </div>
253
+ <div class="brain-title">Codebase Brain</div>
254
+ """
255
+
256
+ html = html.replace('<body>', '<body>' + custom_style)
257
+
258
+ with open(output_path, 'w', encoding='utf-8') as f:
259
+ f.write(html)
260
+
261
+ print(f"Nodes in graph: {len(G.nodes())}")
262
+ print(f"Edges in graph: {len(G.edges())}")
263
+ print(f"Graph saved to {output_path}")
264
+
265
+
266
+ def get_impact(G, filepath):
267
+ import networkx as nx
268
+ import os
269
+
270
+ # Normalize incoming path
271
+ target_norm = os.path.abspath(filepath).replace('\\', '/')
272
+ target_rel = filepath.replace('\\', '/').lstrip('./')
273
+
274
+ # Find matching node — handles both absolute and relative keys
275
+ matched = None
276
+
277
+ for node in G.nodes():
278
+ node_norm = os.path.abspath(node).replace('\\', '/')
279
+ if node_norm == target_norm:
280
+ matched = node
281
+ break
282
+
283
+ if not matched:
284
+ return None
285
+
286
+ direct = list(G.predecessors(matched))
287
+ all_affected = nx.ancestors(G, matched)
288
+ indirect = [f for f in all_affected if f not in direct]
289
+
290
+ return {
291
+ 'target': matched,
292
+ 'direct': direct,
293
+ 'indirect': indirect,
294
+ 'total_affected': len(direct) + len(indirect)
295
+ }
296
+
297
+ if __name__ == "__main__":
298
+ brain = load_brain()
299
+ G = build_graph(brain)
300
+ visualize_interactive(G)
301
+
302
+ print("\nRisk Report:")
303
+ for node, level in calculate_risk(G).items():
304
+ print(f" {node.split(chr(92))[-1]}: {level}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brain
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: An AI brain that lives inside your codebase
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: watchdog
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="codebase-brain",
5
- version="0.1.0",
5
+ version="0.2.0",
6
6
  description="An AI brain that lives inside your codebase",
7
7
  package_dir={"": "."},
8
8
  packages=find_packages(where="."),
@@ -1,190 +0,0 @@
1
- import networkx as nx
2
- import matplotlib.pyplot as plt
3
- from brain_parser.codebase_walker import load_brain
4
- from pyvis.network import Network
5
-
6
-
7
- def build_graph(brain):
8
- G = nx.DiGraph()
9
-
10
- for filepath, data in brain.items():
11
- G.add_node(filepath)
12
-
13
- for imp in data.get('imports', []):
14
- raw = imp.get('name', '')
15
-
16
- # extract module from "from x import y" or "import x"
17
- if raw.startswith('from '):
18
- module = raw.split('from ')[1].split(' import')[0].strip()
19
- elif raw.startswith('import '):
20
- module = raw.replace('import ', '').split(' as ')[0].strip()
21
- else:
22
- continue
23
-
24
- # convert module path to file path format
25
- # tests.circular_test.file_b → tests/circular_test/file_b
26
- module_as_path = module.replace('.', '/').lower()
27
-
28
- for other_file in brain.keys():
29
- other_normalized = other_file.replace('\\', '/').lower()
30
- # match whole filename not partial
31
- filename = other_normalized.split('/')[-1].replace('.py', '').replace('.js', '').replace('.java',
32
- '').replace(
33
- '.ts', '')
34
- if module_as_path == filename or other_normalized.endswith(module_as_path + '.py'):
35
- G.add_edge(filepath, other_file)
36
-
37
- return G
38
-
39
-
40
- def visualize_graph(G):
41
- risk = calculate_risk(G)
42
-
43
- color_map = {
44
- "HIGH": "red",
45
- "MEDIUM": "orange",
46
- "LOW": "lightgreen"
47
- }
48
-
49
- node_colors = [color_map[risk[node]] for node in G.nodes()]
50
-
51
- plt.figure(figsize=(10, 7))
52
- pos = nx.spring_layout(G)
53
-
54
- nx.draw(G, pos,
55
- with_labels=True,
56
- node_color=node_colors,
57
- node_size=2000,
58
- font_size=8,
59
- arrows=True,
60
- edge_color='gray',
61
- font_weight='bold'
62
- )
63
-
64
- plt.title("Live Codebase Brain - Risk Map")
65
- plt.savefig("brain_graph.png")
66
- plt.show()
67
-
68
-
69
- def calculate_risk(G):
70
- risk = {}
71
-
72
- for node in G.nodes():
73
- in_degree = G.in_degree(node)
74
-
75
- if in_degree >= 2:
76
- risk[node] = "HIGH"
77
- elif in_degree == 1:
78
- risk[node] = "MEDIUM"
79
- else:
80
- risk[node] = "LOW"
81
-
82
- return risk
83
-
84
- def visualize_interactive(G):
85
- risk = calculate_risk(G)\
86
-
87
- color_map = {
88
- "HIGH": "#ff4444",
89
- "MEDIUM": "#ffa500",
90
- "LOW": "#44cc44"
91
- }
92
-
93
- net = Network(
94
- height="750px",
95
- width="100%",
96
- bgcolor="#1a1a2e",
97
- font_color="white",
98
- directed=True
99
- )
100
-
101
- for node in G.nodes():
102
- risk_level = risk[node]
103
- color = color_map[risk_level]
104
- label = node.split("\\")[-1].split("/")[-1]
105
-
106
- net.add_node(
107
- node,
108
- label=label,
109
- color=color,
110
- size=20 + (G.in_degree(node) * 3),
111
- title=f"File: {label}\nRisk: {risk_level}\nConnections: {G.in_degree(node)}"
112
- )
113
-
114
- for edge in G.edges():
115
- net.add_edge(edge[0], edge[1])
116
-
117
- net.set_options("""
118
- {
119
- "nodes": {
120
- "borderWidth": 2,
121
- "shadow": true
122
- },
123
- "edges": {
124
- "smooth": {
125
- "type": "dynamic"
126
- },
127
- "shadow": true
128
- },
129
- "interaction": {
130
- "hover": true,
131
- "navigationButtons": true,
132
- "hideEdgesOnDrag": true
133
- },
134
- "physics": {
135
- "stabilization": true,
136
- "barnesHut": {
137
- "gravitationalConstant": -8000,
138
- "springLength": 200,
139
- "springConstant": 0.02
140
- }
141
- }
142
- }
143
- """)
144
- net.save_graph("brain_map.html")
145
- print(f"Nodes in graph: {len(G.nodes())}")
146
- print(f"Edges in graph: {len(G.edges())}")
147
- import webbrowser
148
- webbrowser.open("brain_map.html")
149
- print("Interactive graph saved to brain_map.html")
150
-
151
-
152
- def get_impact(G, filepath):
153
- import networkx as nx
154
- import os
155
-
156
- # Normalize incoming path
157
- target_norm = os.path.abspath(filepath).replace('\\', '/')
158
- target_rel = filepath.replace('\\', '/').lstrip('./')
159
-
160
- # Find matching node — handles both absolute and relative keys
161
- matched = None
162
-
163
- for node in G.nodes():
164
- node_norm = os.path.abspath(node).replace('\\', '/')
165
- if node_norm == target_norm:
166
- matched = node
167
- break
168
-
169
- if not matched:
170
- return None
171
-
172
- direct = list(G.predecessors(matched))
173
- all_affected = nx.ancestors(G, matched)
174
- indirect = [f for f in all_affected if f not in direct]
175
-
176
- return {
177
- 'target': matched,
178
- 'direct': direct,
179
- 'indirect': indirect,
180
- 'total_affected': len(direct) + len(indirect)
181
- }
182
-
183
- if __name__ == "__main__":
184
- brain = load_brain()
185
- G = build_graph(brain)
186
- visualize_interactive(G)
187
-
188
- print("\nRisk Report:")
189
- for node, level in calculate_risk(G).items():
190
- print(f" {node.split(chr(92))[-1]}: {level}")
File without changes
File without changes