ai-coding-assistant 0.5.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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Build comprehensive dependency graphs for code repositories."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Set, Tuple, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import networkx as nx
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from coding_assistant.codebase.parser import CodeParser
|
|
10
|
+
from coding_assistant.codebase.crawler import CodebaseCrawler
|
|
11
|
+
from coding_assistant.utils.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CodeEntity:
|
|
18
|
+
"""Represents a code entity (file, module, class, function)."""
|
|
19
|
+
type: str # 'file', 'module', 'class', 'function'
|
|
20
|
+
name: str
|
|
21
|
+
path: str
|
|
22
|
+
imports: List[str] = field(default_factory=list)
|
|
23
|
+
exports: List[str] = field(default_factory=list)
|
|
24
|
+
calls: List[str] = field(default_factory=list) # Function/method calls
|
|
25
|
+
line_count: int = 0
|
|
26
|
+
language: str = 'python'
|
|
27
|
+
metadata: Dict = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def __hash__(self):
|
|
30
|
+
"""Make entity hashable for use in sets and as dict keys."""
|
|
31
|
+
return hash((self.type, self.name, self.path))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DependencyGraphBuilder:
|
|
35
|
+
"""
|
|
36
|
+
Build multi-level dependency graphs for code analysis.
|
|
37
|
+
|
|
38
|
+
Creates three types of graphs:
|
|
39
|
+
1. File-level: Dependencies between files based on imports
|
|
40
|
+
2. Module-level: Dependencies between logical modules/packages
|
|
41
|
+
3. Call-level: Function/method call relationships
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, codebase_path: Optional[Path] = None):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the dependency graph builder.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
codebase_path: Path to the codebase root (defaults to current directory)
|
|
50
|
+
"""
|
|
51
|
+
self.codebase_path = codebase_path or Path.cwd()
|
|
52
|
+
self.file_graph = nx.DiGraph()
|
|
53
|
+
self.module_graph = nx.DiGraph()
|
|
54
|
+
self.call_graph = nx.DiGraph()
|
|
55
|
+
self.parser = CodeParser()
|
|
56
|
+
self.crawler = CodebaseCrawler(self.codebase_path)
|
|
57
|
+
|
|
58
|
+
# Cache for parsed files
|
|
59
|
+
self._parsed_cache: Dict[str, Dict] = {}
|
|
60
|
+
self._entities: Dict[str, CodeEntity] = {}
|
|
61
|
+
|
|
62
|
+
def build_file_graph(self, codebase_path: Optional[Path] = None) -> nx.DiGraph:
|
|
63
|
+
"""
|
|
64
|
+
Build file-level dependency graph based on imports.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
codebase_path: Path to analyze (overrides instance path if provided)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
NetworkX DiGraph with files as nodes and imports as edges
|
|
71
|
+
"""
|
|
72
|
+
path = codebase_path or self.codebase_path
|
|
73
|
+
logger.info(f"Building file dependency graph for {path}")
|
|
74
|
+
|
|
75
|
+
# Update crawler path if different
|
|
76
|
+
if path != self.codebase_path:
|
|
77
|
+
self.crawler = CodebaseCrawler(path)
|
|
78
|
+
self.codebase_path = path
|
|
79
|
+
|
|
80
|
+
# Crawl for all code files
|
|
81
|
+
file_info_list = self.crawler.scan(max_files=1000)
|
|
82
|
+
|
|
83
|
+
# Filter for Python files (we'll add multi-language support later)
|
|
84
|
+
python_files = [
|
|
85
|
+
f['absolute_path'] for f in file_info_list
|
|
86
|
+
if f['extension'] == '.py'
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
logger.info(f"Found {len(python_files)} Python files to analyze")
|
|
90
|
+
|
|
91
|
+
# Parse each file and extract imports
|
|
92
|
+
for file_path in python_files:
|
|
93
|
+
try:
|
|
94
|
+
self._process_file_for_graph(file_path)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to process {file_path}: {e}")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Build edges based on imports
|
|
100
|
+
self._build_file_edges()
|
|
101
|
+
|
|
102
|
+
logger.info(
|
|
103
|
+
f"File graph built: {self.file_graph.number_of_nodes()} nodes, "
|
|
104
|
+
f"{self.file_graph.number_of_edges()} edges"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return self.file_graph
|
|
108
|
+
|
|
109
|
+
def _process_file_for_graph(self, file_path: str):
|
|
110
|
+
"""Process a single file and add to graph."""
|
|
111
|
+
try:
|
|
112
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
113
|
+
content = f.read()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Could not read {file_path}: {e}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Parse file
|
|
119
|
+
parsed = self.parser.parse_file(file_path, content)
|
|
120
|
+
self._parsed_cache[file_path] = parsed
|
|
121
|
+
|
|
122
|
+
# Create entity for this file
|
|
123
|
+
entity = CodeEntity(
|
|
124
|
+
type='file',
|
|
125
|
+
name=Path(file_path).name,
|
|
126
|
+
path=file_path,
|
|
127
|
+
imports=parsed.get('imports', []),
|
|
128
|
+
line_count=len(content.split('\n')),
|
|
129
|
+
language='python',
|
|
130
|
+
metadata={
|
|
131
|
+
'functions': len(parsed.get('functions', [])),
|
|
132
|
+
'classes': len(parsed.get('classes', []))
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._entities[file_path] = entity
|
|
137
|
+
|
|
138
|
+
# Add node to graph
|
|
139
|
+
self.file_graph.add_node(
|
|
140
|
+
file_path,
|
|
141
|
+
entity=entity,
|
|
142
|
+
label=entity.name,
|
|
143
|
+
imports=entity.imports,
|
|
144
|
+
line_count=entity.line_count
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _build_file_edges(self):
|
|
148
|
+
"""Build edges between files based on import relationships."""
|
|
149
|
+
# Build mapping of module names to file paths
|
|
150
|
+
module_to_file = self._build_module_mapping()
|
|
151
|
+
|
|
152
|
+
for file_path, entity in self._entities.items():
|
|
153
|
+
for import_stmt in entity.imports:
|
|
154
|
+
# Parse import statement to extract module name
|
|
155
|
+
imported_module = self._extract_module_from_import(import_stmt)
|
|
156
|
+
|
|
157
|
+
if imported_module in module_to_file:
|
|
158
|
+
target_file = module_to_file[imported_module]
|
|
159
|
+
if target_file != file_path: # Avoid self-loops
|
|
160
|
+
self.file_graph.add_edge(
|
|
161
|
+
file_path,
|
|
162
|
+
target_file,
|
|
163
|
+
import_type='direct'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _build_module_mapping(self) -> Dict[str, str]:
|
|
167
|
+
"""
|
|
168
|
+
Build mapping from Python module names to file paths.
|
|
169
|
+
|
|
170
|
+
Example: 'coding_assistant.llm.client' -> 'src/coding_assistant/llm/client.py'
|
|
171
|
+
"""
|
|
172
|
+
module_to_file = {}
|
|
173
|
+
|
|
174
|
+
for file_path in self._entities.keys():
|
|
175
|
+
# Convert file path to module name
|
|
176
|
+
path_obj = Path(file_path)
|
|
177
|
+
|
|
178
|
+
# Find the root of the Python package
|
|
179
|
+
parts = path_obj.parts
|
|
180
|
+
|
|
181
|
+
# Look for src/ or the project root
|
|
182
|
+
try:
|
|
183
|
+
if 'src' in parts:
|
|
184
|
+
src_idx = parts.index('src')
|
|
185
|
+
module_parts = parts[src_idx + 1:]
|
|
186
|
+
else:
|
|
187
|
+
# Use relative to codebase path
|
|
188
|
+
rel_path = path_obj.relative_to(self.codebase_path)
|
|
189
|
+
module_parts = rel_path.parts
|
|
190
|
+
|
|
191
|
+
# Remove .py extension
|
|
192
|
+
if module_parts[-1].endswith('.py'):
|
|
193
|
+
module_parts = list(module_parts[:-1]) + [module_parts[-1][:-3]]
|
|
194
|
+
|
|
195
|
+
# Remove __init__ from module name
|
|
196
|
+
if module_parts[-1] == '__init__':
|
|
197
|
+
module_parts = module_parts[:-1]
|
|
198
|
+
|
|
199
|
+
module_name = '.'.join(module_parts)
|
|
200
|
+
module_to_file[module_name] = file_path
|
|
201
|
+
|
|
202
|
+
except ValueError:
|
|
203
|
+
# Path is not relative to codebase_path, skip
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
return module_to_file
|
|
207
|
+
|
|
208
|
+
def _extract_module_from_import(self, import_stmt: str) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Extract module name from import statement.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
'import foo.bar' -> 'foo.bar'
|
|
214
|
+
'from foo.bar import baz' -> 'foo.bar'
|
|
215
|
+
'from . import something' -> '' (relative import, skip)
|
|
216
|
+
"""
|
|
217
|
+
import_stmt = import_stmt.strip()
|
|
218
|
+
|
|
219
|
+
if import_stmt.startswith('from '):
|
|
220
|
+
# from X import Y
|
|
221
|
+
parts = import_stmt.split()
|
|
222
|
+
if len(parts) >= 2:
|
|
223
|
+
module = parts[1]
|
|
224
|
+
# Skip relative imports for now
|
|
225
|
+
if module.startswith('.'):
|
|
226
|
+
return ''
|
|
227
|
+
return module
|
|
228
|
+
elif import_stmt.startswith('import '):
|
|
229
|
+
# import X
|
|
230
|
+
parts = import_stmt.split()
|
|
231
|
+
if len(parts) >= 2:
|
|
232
|
+
module = parts[1]
|
|
233
|
+
# Handle 'import X as Y'
|
|
234
|
+
if 'as' in parts:
|
|
235
|
+
as_idx = parts.index('as')
|
|
236
|
+
module = parts[as_idx - 1]
|
|
237
|
+
return module
|
|
238
|
+
|
|
239
|
+
return ''
|
|
240
|
+
|
|
241
|
+
def build_module_graph(self, file_graph: Optional[nx.DiGraph] = None) -> nx.DiGraph:
|
|
242
|
+
"""
|
|
243
|
+
Build module-level graph by clustering related files.
|
|
244
|
+
|
|
245
|
+
Groups files into logical modules (packages/directories) and
|
|
246
|
+
creates module-level dependencies.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
file_graph: File graph to use (uses instance graph if not provided)
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
NetworkX DiGraph with modules as nodes
|
|
253
|
+
"""
|
|
254
|
+
graph = file_graph or self.file_graph
|
|
255
|
+
|
|
256
|
+
if graph.number_of_nodes() == 0:
|
|
257
|
+
logger.warning("File graph is empty, building it first")
|
|
258
|
+
self.build_file_graph()
|
|
259
|
+
graph = self.file_graph
|
|
260
|
+
|
|
261
|
+
logger.info("Building module-level graph")
|
|
262
|
+
|
|
263
|
+
# Group files by directory (module)
|
|
264
|
+
module_files: Dict[str, List[str]] = defaultdict(list)
|
|
265
|
+
|
|
266
|
+
for file_path in graph.nodes():
|
|
267
|
+
module_name = self._get_module_name(file_path)
|
|
268
|
+
module_files[module_name].append(file_path)
|
|
269
|
+
|
|
270
|
+
# Create module nodes
|
|
271
|
+
for module_name, files in module_files.items():
|
|
272
|
+
total_lines = sum(
|
|
273
|
+
graph.nodes[f].get('line_count', 0) for f in files
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
self.module_graph.add_node(
|
|
277
|
+
module_name,
|
|
278
|
+
files=files,
|
|
279
|
+
file_count=len(files),
|
|
280
|
+
line_count=total_lines
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Create module edges based on file dependencies
|
|
284
|
+
for source_file, target_file in graph.edges():
|
|
285
|
+
source_module = self._get_module_name(source_file)
|
|
286
|
+
target_module = self._get_module_name(target_file)
|
|
287
|
+
|
|
288
|
+
if source_module != target_module:
|
|
289
|
+
# Add or strengthen edge
|
|
290
|
+
if self.module_graph.has_edge(source_module, target_module):
|
|
291
|
+
self.module_graph[source_module][target_module]['weight'] += 1
|
|
292
|
+
else:
|
|
293
|
+
self.module_graph.add_edge(
|
|
294
|
+
source_module,
|
|
295
|
+
target_module,
|
|
296
|
+
weight=1
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
logger.info(
|
|
300
|
+
f"Module graph built: {self.module_graph.number_of_nodes()} modules, "
|
|
301
|
+
f"{self.module_graph.number_of_edges()} dependencies"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return self.module_graph
|
|
305
|
+
|
|
306
|
+
def _get_module_name(self, file_path: str) -> str:
|
|
307
|
+
"""
|
|
308
|
+
Get module name from file path.
|
|
309
|
+
|
|
310
|
+
Example: 'src/coding_assistant/llm/client.py' -> 'coding_assistant.llm'
|
|
311
|
+
"""
|
|
312
|
+
path_obj = Path(file_path)
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
parts = path_obj.parts
|
|
316
|
+
|
|
317
|
+
# Find src or project root
|
|
318
|
+
if 'src' in parts:
|
|
319
|
+
src_idx = parts.index('src')
|
|
320
|
+
module_parts = parts[src_idx + 1:-1] # Exclude filename
|
|
321
|
+
else:
|
|
322
|
+
rel_path = path_obj.relative_to(self.codebase_path)
|
|
323
|
+
module_parts = rel_path.parts[:-1] # Exclude filename
|
|
324
|
+
|
|
325
|
+
if not module_parts:
|
|
326
|
+
return 'root'
|
|
327
|
+
|
|
328
|
+
return '.'.join(module_parts)
|
|
329
|
+
|
|
330
|
+
except ValueError:
|
|
331
|
+
return 'external'
|
|
332
|
+
|
|
333
|
+
def detect_cycles(self, graph: Optional[nx.DiGraph] = None) -> List[List[str]]:
|
|
334
|
+
"""
|
|
335
|
+
Detect circular dependencies in the graph.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
graph: Graph to analyze (defaults to file_graph)
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of cycles, where each cycle is a list of node names
|
|
342
|
+
"""
|
|
343
|
+
graph = graph or self.file_graph
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
cycles = list(nx.simple_cycles(graph))
|
|
347
|
+
|
|
348
|
+
if cycles:
|
|
349
|
+
logger.warning(f"Found {len(cycles)} circular dependencies")
|
|
350
|
+
for i, cycle in enumerate(cycles[:5]): # Log first 5
|
|
351
|
+
logger.warning(f" Cycle {i+1}: {' -> '.join(cycle)}")
|
|
352
|
+
else:
|
|
353
|
+
logger.info("No circular dependencies found")
|
|
354
|
+
|
|
355
|
+
return cycles
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"Error detecting cycles: {e}")
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
def compute_metrics(self, graph: Optional[nx.DiGraph] = None) -> Dict:
|
|
362
|
+
"""
|
|
363
|
+
Compute graph metrics (centrality, coupling, complexity, etc.).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
graph: Graph to analyze (defaults to file_graph)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Dictionary of metrics
|
|
370
|
+
"""
|
|
371
|
+
graph = graph or self.file_graph
|
|
372
|
+
|
|
373
|
+
if graph.number_of_nodes() == 0:
|
|
374
|
+
return {
|
|
375
|
+
'nodes': 0,
|
|
376
|
+
'edges': 0,
|
|
377
|
+
'density': 0.0,
|
|
378
|
+
'avg_degree': 0.0,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
metrics = {
|
|
382
|
+
'nodes': graph.number_of_nodes(),
|
|
383
|
+
'edges': graph.number_of_edges(),
|
|
384
|
+
'density': nx.density(graph),
|
|
385
|
+
'avg_degree': sum(dict(graph.degree()).values()) / graph.number_of_nodes(),
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Weakly connected components
|
|
389
|
+
if graph.is_directed():
|
|
390
|
+
metrics['connected_components'] = nx.number_weakly_connected_components(graph)
|
|
391
|
+
else:
|
|
392
|
+
metrics['connected_components'] = nx.number_connected_components(graph)
|
|
393
|
+
|
|
394
|
+
# Compute centrality for important nodes
|
|
395
|
+
try:
|
|
396
|
+
centrality = nx.betweenness_centrality(graph)
|
|
397
|
+
metrics['max_centrality'] = max(centrality.values()) if centrality else 0
|
|
398
|
+
metrics['avg_centrality'] = sum(centrality.values()) / len(centrality) if centrality else 0
|
|
399
|
+
except:
|
|
400
|
+
metrics['max_centrality'] = 0
|
|
401
|
+
metrics['avg_centrality'] = 0
|
|
402
|
+
|
|
403
|
+
logger.info(f"Graph metrics: {metrics}")
|
|
404
|
+
|
|
405
|
+
return metrics
|
|
406
|
+
|
|
407
|
+
def get_important_files(self, top_n: int = 10, graph: Optional[nx.DiGraph] = None) -> List[Tuple[str, float]]:
|
|
408
|
+
"""
|
|
409
|
+
Get the most important/central files in the codebase.
|
|
410
|
+
|
|
411
|
+
Uses betweenness centrality to identify files that are
|
|
412
|
+
crucial connectors in the dependency graph.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
top_n: Number of files to return
|
|
416
|
+
graph: Graph to analyze (defaults to file_graph)
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
List of (file_path, centrality_score) tuples
|
|
420
|
+
"""
|
|
421
|
+
graph = graph or self.file_graph
|
|
422
|
+
|
|
423
|
+
if graph.number_of_nodes() == 0:
|
|
424
|
+
return []
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
centrality = nx.betweenness_centrality(graph)
|
|
428
|
+
sorted_files = sorted(
|
|
429
|
+
centrality.items(),
|
|
430
|
+
key=lambda x: x[1],
|
|
431
|
+
reverse=True
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return sorted_files[:top_n]
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error(f"Error computing important files: {e}")
|
|
438
|
+
return []
|
|
439
|
+
|
|
440
|
+
def export_graph(self, output_path: str, graph: Optional[nx.DiGraph] = None, format: str = 'gml'):
|
|
441
|
+
"""
|
|
442
|
+
Export graph to file for visualization or external analysis.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
output_path: Path to save the graph
|
|
446
|
+
graph: Graph to export (defaults to file_graph)
|
|
447
|
+
format: Output format ('gml', 'graphml', 'json')
|
|
448
|
+
"""
|
|
449
|
+
graph = graph or self.file_graph
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
if format == 'gml':
|
|
453
|
+
nx.write_gml(graph, output_path)
|
|
454
|
+
elif format == 'graphml':
|
|
455
|
+
nx.write_graphml(graph, output_path)
|
|
456
|
+
elif format == 'json':
|
|
457
|
+
from networkx.readwrite import json_graph
|
|
458
|
+
import json
|
|
459
|
+
data = json_graph.node_link_data(graph)
|
|
460
|
+
with open(output_path, 'w') as f:
|
|
461
|
+
json.dump(data, f, indent=2)
|
|
462
|
+
else:
|
|
463
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
464
|
+
|
|
465
|
+
logger.info(f"Graph exported to {output_path}")
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Failed to export graph: {e}")
|