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.
Files changed (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. 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
+ ]