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.
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/PKG-INFO +1 -1
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/bug_detector.py +15 -14
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/codebase_walker.py +11 -6
- codebase_brain-0.2.0/brain_parser/graph_builder.py +304 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/PKG-INFO +1 -1
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/setup.py +1 -1
- codebase_brain-0.1.0/brain_parser/graph_builder.py +0 -190
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/README.md +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_cli.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/__init__.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/ast_parser.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/file_watcher.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/llm_router.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/query_engine.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/root_cause.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/brain_parser/universal_parser.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/SOURCES.txt +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/dependency_links.txt +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/entry_points.txt +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/requires.txt +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/codebase_brain.egg-info/top_level.txt +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/setup.cfg +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_bugs.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_classes.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_parser.py +0 -0
- {codebase_brain-0.1.0 → codebase_brain-0.2.0}/tests/test_treesitter.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
chain = '\n '.join([f"{i+1}. {f}" for i, f in enumerate(
|
|
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':
|
|
35
|
-
'message': f"Circular dependency ({len(
|
|
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
|
-
|
|
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=
|
|
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(
|
|
64
|
-
if
|
|
65
|
-
|
|
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(
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|