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,475 @@
|
|
|
1
|
+
"""Analyze module relationships and coherence."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Set, Tuple, Optional
|
|
4
|
+
import networkx as nx
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
import community as community_louvain
|
|
7
|
+
|
|
8
|
+
from coding_assistant.utils.logger import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModuleAnalyzer:
|
|
14
|
+
"""
|
|
15
|
+
Analyze and identify logical modules in codebase.
|
|
16
|
+
|
|
17
|
+
Uses graph analysis techniques to:
|
|
18
|
+
- Detect logical module groupings (community detection)
|
|
19
|
+
- Compute cohesion and coupling metrics
|
|
20
|
+
- Identify core/central files
|
|
21
|
+
- Analyze architectural patterns
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, dependency_graph: nx.DiGraph):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the module analyzer.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
dependency_graph: Dependency graph from DependencyGraphBuilder
|
|
30
|
+
"""
|
|
31
|
+
self.graph = dependency_graph
|
|
32
|
+
|
|
33
|
+
def detect_modules(self, resolution: float = 1.0) -> Dict[str, List[str]]:
|
|
34
|
+
"""
|
|
35
|
+
Detect logical modules using community detection.
|
|
36
|
+
|
|
37
|
+
Uses the Louvain algorithm for community detection, which finds
|
|
38
|
+
densely connected groups of nodes (files that work closely together).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
resolution: Resolution parameter for Louvain algorithm.
|
|
42
|
+
Higher values -> more smaller communities
|
|
43
|
+
Lower values -> fewer larger communities
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary mapping module names to lists of file paths
|
|
47
|
+
"""
|
|
48
|
+
if self.graph.number_of_nodes() == 0:
|
|
49
|
+
logger.warning("Empty graph, cannot detect modules")
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
logger.info(f"Detecting modules using Louvain algorithm (resolution={resolution})")
|
|
53
|
+
|
|
54
|
+
# Convert to undirected graph for community detection
|
|
55
|
+
undirected = self.graph.to_undirected()
|
|
56
|
+
|
|
57
|
+
# Apply Louvain community detection
|
|
58
|
+
try:
|
|
59
|
+
partition = community_louvain.best_partition(
|
|
60
|
+
undirected,
|
|
61
|
+
resolution=resolution
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Group nodes by community
|
|
65
|
+
communities: Dict[int, List[str]] = defaultdict(list)
|
|
66
|
+
for node, community_id in partition.items():
|
|
67
|
+
communities[community_id].append(node)
|
|
68
|
+
|
|
69
|
+
# Convert to named modules
|
|
70
|
+
modules = {}
|
|
71
|
+
for i, (community_id, files) in enumerate(sorted(communities.items())):
|
|
72
|
+
# Try to name module based on common path prefix
|
|
73
|
+
module_name = self._infer_module_name(files) or f"module_{i}"
|
|
74
|
+
modules[module_name] = files
|
|
75
|
+
|
|
76
|
+
logger.info(f"Detected {len(modules)} modules")
|
|
77
|
+
|
|
78
|
+
# Log module sizes
|
|
79
|
+
for module_name, files in modules.items():
|
|
80
|
+
logger.info(f" {module_name}: {len(files)} files")
|
|
81
|
+
|
|
82
|
+
return modules
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Failed to detect modules: {e}")
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
def _infer_module_name(self, files: List[str]) -> Optional[str]:
|
|
89
|
+
"""
|
|
90
|
+
Infer a meaningful module name from a list of files.
|
|
91
|
+
|
|
92
|
+
Looks for common path prefix or directory name.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
files: List of file paths in the module
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Inferred module name or None
|
|
99
|
+
"""
|
|
100
|
+
if not files:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Find common path prefix
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
paths = [Path(f).parts for f in files]
|
|
107
|
+
|
|
108
|
+
if len(paths) == 1:
|
|
109
|
+
# Single file, use its parent directory
|
|
110
|
+
return Path(files[0]).parent.name
|
|
111
|
+
|
|
112
|
+
# Find longest common prefix
|
|
113
|
+
common_prefix = []
|
|
114
|
+
for parts in zip(*paths):
|
|
115
|
+
if len(set(parts)) == 1:
|
|
116
|
+
common_prefix.append(parts[0])
|
|
117
|
+
else:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
if common_prefix:
|
|
121
|
+
# Filter out common parts like 'src', 'coding_assistant'
|
|
122
|
+
filtered = [p for p in common_prefix if p not in ['src', 'home', 'projects']]
|
|
123
|
+
if filtered:
|
|
124
|
+
return '.'.join(filtered)
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def compute_cohesion(self, module_files: List[str]) -> float:
|
|
129
|
+
"""
|
|
130
|
+
Compute cohesion score for a module.
|
|
131
|
+
|
|
132
|
+
Cohesion measures how closely related the files within a module are.
|
|
133
|
+
Higher cohesion = better module design.
|
|
134
|
+
|
|
135
|
+
Formula: (internal edges) / (possible internal edges)
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
module_files: List of file paths in the module
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Cohesion score between 0 and 1
|
|
142
|
+
"""
|
|
143
|
+
if len(module_files) < 2:
|
|
144
|
+
return 1.0 # Single file is perfectly cohesive
|
|
145
|
+
|
|
146
|
+
# Create subgraph for this module
|
|
147
|
+
try:
|
|
148
|
+
subgraph = self.graph.subgraph(module_files)
|
|
149
|
+
|
|
150
|
+
# Count actual internal edges
|
|
151
|
+
internal_edges = subgraph.number_of_edges()
|
|
152
|
+
|
|
153
|
+
# Count possible edges (for directed graph: n * (n-1))
|
|
154
|
+
n = len(module_files)
|
|
155
|
+
possible_edges = n * (n - 1)
|
|
156
|
+
|
|
157
|
+
if possible_edges == 0:
|
|
158
|
+
return 1.0
|
|
159
|
+
|
|
160
|
+
cohesion = internal_edges / possible_edges
|
|
161
|
+
|
|
162
|
+
return min(cohesion, 1.0) # Cap at 1.0
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Failed to compute cohesion: {e}")
|
|
166
|
+
return 0.0
|
|
167
|
+
|
|
168
|
+
def compute_coupling(self, module1: List[str], module2: List[str]) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Compute coupling between two modules.
|
|
171
|
+
|
|
172
|
+
Coupling measures how dependent two modules are on each other.
|
|
173
|
+
Lower coupling = better module design (loose coupling).
|
|
174
|
+
|
|
175
|
+
Formula: (edges between modules) / (total possible edges between modules)
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
module1: Files in first module
|
|
179
|
+
module2: Files in second module
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Coupling score between 0 and 1
|
|
183
|
+
"""
|
|
184
|
+
if not module1 or not module2:
|
|
185
|
+
return 0.0
|
|
186
|
+
|
|
187
|
+
# Count edges from module1 to module2 and vice versa
|
|
188
|
+
edges_between = 0
|
|
189
|
+
|
|
190
|
+
for file1 in module1:
|
|
191
|
+
for file2 in module2:
|
|
192
|
+
if self.graph.has_edge(file1, file2):
|
|
193
|
+
edges_between += 1
|
|
194
|
+
if self.graph.has_edge(file2, file1):
|
|
195
|
+
edges_between += 1
|
|
196
|
+
|
|
197
|
+
# Total possible edges
|
|
198
|
+
possible_edges = len(module1) * len(module2) * 2 # Both directions
|
|
199
|
+
|
|
200
|
+
if possible_edges == 0:
|
|
201
|
+
return 0.0
|
|
202
|
+
|
|
203
|
+
coupling = edges_between / possible_edges
|
|
204
|
+
|
|
205
|
+
return min(coupling, 1.0)
|
|
206
|
+
|
|
207
|
+
def identify_core_files(self, top_n: int = 10) -> List[Tuple[str, float]]:
|
|
208
|
+
"""
|
|
209
|
+
Identify the most central/important files using betweenness centrality.
|
|
210
|
+
|
|
211
|
+
Core files are those that connect different parts of the codebase
|
|
212
|
+
and are crucial for information flow.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
top_n: Number of core files to return
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of (file_path, centrality_score) tuples, sorted by score
|
|
219
|
+
"""
|
|
220
|
+
if self.graph.number_of_nodes() == 0:
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
# Compute betweenness centrality
|
|
225
|
+
centrality = nx.betweenness_centrality(self.graph)
|
|
226
|
+
|
|
227
|
+
# Sort by centrality
|
|
228
|
+
sorted_files = sorted(
|
|
229
|
+
centrality.items(),
|
|
230
|
+
key=lambda x: x[1],
|
|
231
|
+
reverse=True
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
core_files = sorted_files[:top_n]
|
|
235
|
+
|
|
236
|
+
logger.info(f"Identified {len(core_files)} core files")
|
|
237
|
+
for file_path, score in core_files[:5]:
|
|
238
|
+
logger.info(f" {file_path}: {score:.4f}")
|
|
239
|
+
|
|
240
|
+
return core_files
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Failed to identify core files: {e}")
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
def compute_module_metrics(self, modules: Dict[str, List[str]]) -> Dict[str, Dict]:
|
|
247
|
+
"""
|
|
248
|
+
Compute comprehensive metrics for each module.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
modules: Dictionary mapping module names to file lists
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dictionary mapping module names to their metrics
|
|
255
|
+
"""
|
|
256
|
+
module_metrics = {}
|
|
257
|
+
|
|
258
|
+
for module_name, files in modules.items():
|
|
259
|
+
metrics = {
|
|
260
|
+
'file_count': len(files),
|
|
261
|
+
'cohesion': self.compute_cohesion(files),
|
|
262
|
+
'avg_coupling': 0.0,
|
|
263
|
+
'total_lines': 0,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Compute total lines of code
|
|
267
|
+
for file_path in files:
|
|
268
|
+
if self.graph.has_node(file_path):
|
|
269
|
+
node_data = self.graph.nodes[file_path]
|
|
270
|
+
metrics['total_lines'] += node_data.get('line_count', 0)
|
|
271
|
+
|
|
272
|
+
# Compute average coupling with other modules
|
|
273
|
+
couplings = []
|
|
274
|
+
for other_module, other_files in modules.items():
|
|
275
|
+
if other_module != module_name:
|
|
276
|
+
coupling = self.compute_coupling(files, other_files)
|
|
277
|
+
couplings.append(coupling)
|
|
278
|
+
|
|
279
|
+
if couplings:
|
|
280
|
+
metrics['avg_coupling'] = sum(couplings) / len(couplings)
|
|
281
|
+
|
|
282
|
+
module_metrics[module_name] = metrics
|
|
283
|
+
|
|
284
|
+
return module_metrics
|
|
285
|
+
|
|
286
|
+
def analyze_module_dependencies(self, modules: Dict[str, List[str]]) -> Dict[str, List[str]]:
|
|
287
|
+
"""
|
|
288
|
+
Analyze dependencies between modules.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
modules: Dictionary mapping module names to file lists
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Dictionary mapping module names to lists of modules they depend on
|
|
295
|
+
"""
|
|
296
|
+
module_deps: Dict[str, Set[str]] = defaultdict(set)
|
|
297
|
+
|
|
298
|
+
# Create reverse mapping: file -> module
|
|
299
|
+
file_to_module = {}
|
|
300
|
+
for module_name, files in modules.items():
|
|
301
|
+
for file_path in files:
|
|
302
|
+
file_to_module[file_path] = module_name
|
|
303
|
+
|
|
304
|
+
# Analyze dependencies
|
|
305
|
+
for source_module, files in modules.items():
|
|
306
|
+
for file_path in files:
|
|
307
|
+
if self.graph.has_node(file_path):
|
|
308
|
+
# Check all outgoing edges
|
|
309
|
+
for _, target_file in self.graph.out_edges(file_path):
|
|
310
|
+
target_module = file_to_module.get(target_file)
|
|
311
|
+
if target_module and target_module != source_module:
|
|
312
|
+
module_deps[source_module].add(target_module)
|
|
313
|
+
|
|
314
|
+
# Convert sets to sorted lists
|
|
315
|
+
return {
|
|
316
|
+
module: sorted(list(deps))
|
|
317
|
+
for module, deps in module_deps.items()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
def detect_layered_architecture(self, modules: Dict[str, List[str]]) -> Dict[int, List[str]]:
|
|
321
|
+
"""
|
|
322
|
+
Detect layered architecture by analyzing module dependencies.
|
|
323
|
+
|
|
324
|
+
Identifies layers where:
|
|
325
|
+
- Layer 0: Modules with no dependencies
|
|
326
|
+
- Layer 1: Modules depending only on Layer 0
|
|
327
|
+
- Layer N: Modules depending on Layers 0 to N-1
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
modules: Dictionary mapping module names to file lists
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dictionary mapping layer number to list of module names
|
|
334
|
+
"""
|
|
335
|
+
module_deps = self.analyze_module_dependencies(modules)
|
|
336
|
+
|
|
337
|
+
layers: Dict[int, List[str]] = defaultdict(list)
|
|
338
|
+
assigned = set()
|
|
339
|
+
|
|
340
|
+
current_layer = 0
|
|
341
|
+
|
|
342
|
+
while len(assigned) < len(modules):
|
|
343
|
+
current_layer_modules = []
|
|
344
|
+
|
|
345
|
+
for module_name in modules.keys():
|
|
346
|
+
if module_name in assigned:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
deps = set(module_deps.get(module_name, []))
|
|
350
|
+
|
|
351
|
+
# Check if all dependencies are in previous layers
|
|
352
|
+
if not deps or deps.issubset(assigned):
|
|
353
|
+
current_layer_modules.append(module_name)
|
|
354
|
+
|
|
355
|
+
if not current_layer_modules:
|
|
356
|
+
# Circular dependencies or disconnected components
|
|
357
|
+
# Assign remaining modules to current layer
|
|
358
|
+
for module_name in modules.keys():
|
|
359
|
+
if module_name not in assigned:
|
|
360
|
+
current_layer_modules.append(module_name)
|
|
361
|
+
|
|
362
|
+
layers[current_layer] = current_layer_modules
|
|
363
|
+
assigned.update(current_layer_modules)
|
|
364
|
+
current_layer += 1
|
|
365
|
+
|
|
366
|
+
logger.info(f"Detected {len(layers)} architectural layers")
|
|
367
|
+
for layer_num, layer_modules in layers.items():
|
|
368
|
+
logger.info(f" Layer {layer_num}: {', '.join(layer_modules)}")
|
|
369
|
+
|
|
370
|
+
return dict(layers)
|
|
371
|
+
|
|
372
|
+
def identify_architectural_patterns(self, modules: Dict[str, List[str]]) -> List[str]:
|
|
373
|
+
"""
|
|
374
|
+
Identify common architectural patterns in the codebase.
|
|
375
|
+
|
|
376
|
+
Detects patterns like:
|
|
377
|
+
- MVC (Model-View-Controller)
|
|
378
|
+
- Layered architecture
|
|
379
|
+
- Microservices/modular
|
|
380
|
+
- Monolithic
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
modules: Dictionary mapping module names to file lists
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of detected pattern names
|
|
387
|
+
"""
|
|
388
|
+
patterns = []
|
|
389
|
+
|
|
390
|
+
# Check for layered architecture
|
|
391
|
+
layers = self.detect_layered_architecture(modules)
|
|
392
|
+
if len(layers) >= 3:
|
|
393
|
+
patterns.append("Layered Architecture")
|
|
394
|
+
|
|
395
|
+
# Check for MVC pattern (common module names)
|
|
396
|
+
module_names_lower = [m.lower() for m in modules.keys()]
|
|
397
|
+
|
|
398
|
+
mvc_keywords = {'model', 'view', 'controller'}
|
|
399
|
+
if any(keyword in name for name in module_names_lower for keyword in mvc_keywords):
|
|
400
|
+
patterns.append("MVC Pattern")
|
|
401
|
+
|
|
402
|
+
# Check for modular architecture (many loosely coupled modules)
|
|
403
|
+
if len(modules) >= 5:
|
|
404
|
+
metrics = self.compute_module_metrics(modules)
|
|
405
|
+
avg_coupling = sum(m['avg_coupling'] for m in metrics.values()) / len(metrics)
|
|
406
|
+
|
|
407
|
+
if avg_coupling < 0.2:
|
|
408
|
+
patterns.append("Modular Architecture (Low Coupling)")
|
|
409
|
+
|
|
410
|
+
# Check for monolithic (few modules, high coupling)
|
|
411
|
+
if len(modules) <= 2:
|
|
412
|
+
patterns.append("Monolithic Architecture")
|
|
413
|
+
|
|
414
|
+
logger.info(f"Detected architectural patterns: {', '.join(patterns) if patterns else 'None'}")
|
|
415
|
+
|
|
416
|
+
return patterns
|
|
417
|
+
|
|
418
|
+
def generate_module_summary(self, modules: Dict[str, List[str]]) -> str:
|
|
419
|
+
"""
|
|
420
|
+
Generate a human-readable summary of module analysis.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
modules: Dictionary mapping module names to file lists
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Formatted summary string
|
|
427
|
+
"""
|
|
428
|
+
metrics = self.compute_module_metrics(modules)
|
|
429
|
+
dependencies = self.analyze_module_dependencies(modules)
|
|
430
|
+
patterns = self.identify_architectural_patterns(modules)
|
|
431
|
+
core_files = self.identify_core_files(top_n=5)
|
|
432
|
+
|
|
433
|
+
summary_lines = [
|
|
434
|
+
"=" * 60,
|
|
435
|
+
"MODULE ANALYSIS SUMMARY",
|
|
436
|
+
"=" * 60,
|
|
437
|
+
"",
|
|
438
|
+
f"Total Modules: {len(modules)}",
|
|
439
|
+
f"Total Files: {sum(len(files) for files in modules.values())}",
|
|
440
|
+
"",
|
|
441
|
+
"Detected Patterns:",
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
for pattern in patterns:
|
|
445
|
+
summary_lines.append(f" - {pattern}")
|
|
446
|
+
|
|
447
|
+
summary_lines.extend([
|
|
448
|
+
"",
|
|
449
|
+
"Module Metrics:",
|
|
450
|
+
])
|
|
451
|
+
|
|
452
|
+
for module_name, module_metrics in metrics.items():
|
|
453
|
+
summary_lines.append(
|
|
454
|
+
f" {module_name}:"
|
|
455
|
+
)
|
|
456
|
+
summary_lines.append(
|
|
457
|
+
f" Files: {module_metrics['file_count']}, "
|
|
458
|
+
f"LOC: {module_metrics['total_lines']}, "
|
|
459
|
+
f"Cohesion: {module_metrics['cohesion']:.2f}, "
|
|
460
|
+
f"Avg Coupling: {module_metrics['avg_coupling']:.2f}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
summary_lines.extend([
|
|
464
|
+
"",
|
|
465
|
+
"Core Files (Highest Centrality):",
|
|
466
|
+
])
|
|
467
|
+
|
|
468
|
+
for file_path, centrality in core_files:
|
|
469
|
+
from pathlib import Path
|
|
470
|
+
filename = Path(file_path).name
|
|
471
|
+
summary_lines.append(f" {filename}: {centrality:.4f}")
|
|
472
|
+
|
|
473
|
+
summary_lines.append("=" * 60)
|
|
474
|
+
|
|
475
|
+
return "\n".join(summary_lines)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Documentation writers for various output formats.
|
|
2
|
+
|
|
3
|
+
This module provides writers that convert generated documentation
|
|
4
|
+
and diagrams into different output formats (markdown, HTML, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .markdown_writer import MarkdownWriter
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'MarkdownWriter',
|
|
11
|
+
]
|