mcp-vector-search 1.0.3__py3-none-any.whl → 1.1.22__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 (63) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +48 -1
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +35 -0
  7. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  8. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  9. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  10. mcp_vector_search/analysis/collectors/smells.py +325 -0
  11. mcp_vector_search/analysis/debt.py +516 -0
  12. mcp_vector_search/analysis/interpretation.py +685 -0
  13. mcp_vector_search/analysis/metrics.py +74 -1
  14. mcp_vector_search/analysis/reporters/__init__.py +3 -1
  15. mcp_vector_search/analysis/reporters/console.py +424 -0
  16. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  17. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  18. mcp_vector_search/analysis/storage/__init__.py +93 -0
  19. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  20. mcp_vector_search/analysis/storage/schema.py +245 -0
  21. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  22. mcp_vector_search/analysis/trends.py +308 -0
  23. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  24. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  25. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  26. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  27. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  28. mcp_vector_search/cli/commands/analyze.py +665 -11
  29. mcp_vector_search/cli/commands/chat.py +193 -0
  30. mcp_vector_search/cli/commands/index.py +600 -2
  31. mcp_vector_search/cli/commands/index_background.py +467 -0
  32. mcp_vector_search/cli/commands/search.py +194 -1
  33. mcp_vector_search/cli/commands/setup.py +64 -13
  34. mcp_vector_search/cli/commands/status.py +302 -3
  35. mcp_vector_search/cli/commands/visualize/cli.py +26 -10
  36. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
  37. mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
  38. mcp_vector_search/cli/commands/visualize/server.py +304 -15
  39. mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
  40. mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
  41. mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
  42. mcp_vector_search/cli/didyoumean.py +5 -0
  43. mcp_vector_search/cli/main.py +16 -5
  44. mcp_vector_search/cli/output.py +134 -5
  45. mcp_vector_search/config/thresholds.py +89 -1
  46. mcp_vector_search/core/__init__.py +16 -0
  47. mcp_vector_search/core/database.py +39 -2
  48. mcp_vector_search/core/embeddings.py +24 -0
  49. mcp_vector_search/core/git.py +380 -0
  50. mcp_vector_search/core/indexer.py +445 -84
  51. mcp_vector_search/core/llm_client.py +9 -4
  52. mcp_vector_search/core/models.py +88 -1
  53. mcp_vector_search/core/relationships.py +473 -0
  54. mcp_vector_search/core/search.py +1 -1
  55. mcp_vector_search/mcp/server.py +795 -4
  56. mcp_vector_search/parsers/python.py +285 -5
  57. mcp_vector_search/utils/gitignore.py +0 -3
  58. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
  59. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
  60. mcp_vector_search/cli/commands/visualize.py.original +0 -2536
  61. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
  62. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
  63. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,534 @@
1
+ """Transform analysis data to D3.js-friendly format for visualization.
2
+
3
+ This module converts AnalysisExport schema data into a format optimized for
4
+ D3.js force-directed graph visualization. It handles:
5
+ - Node transformation (files with metrics)
6
+ - Edge transformation (dependencies with coupling strength)
7
+ - Circular dependency detection and highlighting
8
+ - Module grouping for visual organization
9
+
10
+ The output format is designed for interactive dependency graphs with:
11
+ - Node size based on lines of code
12
+ - Node fill based on cognitive complexity (grayscale)
13
+ - Node border based on code smell severity (red scale)
14
+ - Edge thickness based on coupling strength
15
+ - Circular dependencies highlighted in red
16
+
17
+ Example:
18
+ >>> from mcp_vector_search.analysis.visualizer import JSONExporter
19
+ >>> from pathlib import Path
20
+ >>>
21
+ >>> exporter = JSONExporter(project_root=Path("/path/to/project"))
22
+ >>> export = exporter.export(project_metrics)
23
+ >>>
24
+ >>> from mcp_vector_search.analysis.visualizer.d3_data import transform_for_d3
25
+ >>> d3_data = transform_for_d3(export)
26
+ >>> # Returns: {"nodes": [...], "links": [...], "summary": {...}}
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from .schemas import AnalysisExport, FileDetail
36
+
37
+
38
+ @dataclass
39
+ class D3Node:
40
+ """Node data for D3 force graph.
41
+
42
+ Represents a single file in the codebase with visual properties
43
+ derived from code metrics.
44
+
45
+ Attributes:
46
+ id: Unique file path (relative to project root)
47
+ label: Display name (file name only)
48
+ module: Directory/module name for grouping
49
+ module_path: Full module path for cluster grouping (e.g., 'src/analysis')
50
+ loc: Lines of code (determines node size)
51
+ complexity: Cognitive complexity (determines fill color)
52
+ smell_count: Number of code smells detected
53
+ smell_severity: Worst smell severity level
54
+ cyclomatic_complexity: Cyclomatic complexity score
55
+ function_count: Number of functions in file
56
+ class_count: Number of classes in file
57
+ smells: List of smell details for detail panel
58
+ imports: List of imports (outgoing edges)
59
+ """
60
+
61
+ id: str
62
+ label: str
63
+ module: str
64
+ module_path: str
65
+ loc: int
66
+ complexity: float
67
+ smell_count: int
68
+ smell_severity: str
69
+ cyclomatic_complexity: int
70
+ function_count: int
71
+ class_count: int
72
+ smells: list[dict[str, Any]]
73
+ imports: list[str]
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ """Convert to dictionary for JSON serialization."""
77
+ return {
78
+ "id": self.id,
79
+ "label": self.label,
80
+ "module": self.module,
81
+ "module_path": self.module_path,
82
+ "loc": self.loc,
83
+ "complexity": self.complexity,
84
+ "smell_count": self.smell_count,
85
+ "smell_severity": self.smell_severity,
86
+ "cyclomatic_complexity": self.cyclomatic_complexity,
87
+ "function_count": self.function_count,
88
+ "class_count": self.class_count,
89
+ "smells": self.smells,
90
+ "imports": self.imports,
91
+ }
92
+
93
+
94
+ @dataclass
95
+ class D3Edge:
96
+ """Edge data for D3 force graph.
97
+
98
+ Represents a dependency relationship between two files with
99
+ visual properties derived from coupling metrics.
100
+
101
+ Attributes:
102
+ source: Source file path (relative to project root)
103
+ target: Target file path
104
+ coupling: Coupling strength (number of imports/dependencies)
105
+ circular: Whether this edge is part of a circular dependency
106
+ """
107
+
108
+ source: str
109
+ target: str
110
+ coupling: int
111
+ circular: bool
112
+
113
+ def to_dict(self) -> dict[str, Any]:
114
+ """Convert to dictionary for JSON serialization."""
115
+ return {
116
+ "source": self.source,
117
+ "target": self.target,
118
+ "coupling": self.coupling,
119
+ "circular": self.circular,
120
+ }
121
+
122
+
123
+ def transform_for_d3(export: AnalysisExport) -> dict[str, Any]:
124
+ """Transform AnalysisExport to D3-friendly JSON structure.
125
+
126
+ Creates a graph structure optimized for D3.js force-directed layout
127
+ with visual properties encoded in node and edge attributes.
128
+
129
+ Visual Encodings:
130
+ - Node size: Lines of code (LOC)
131
+ - Node fill: Cognitive complexity (grayscale: light to dark)
132
+ - 0-5: Very light gray (#f3f4f6)
133
+ - 6-10: Light gray (#9ca3af)
134
+ - 11-20: Medium gray (#4b5563)
135
+ - 21-30: Dark gray (#1f2937)
136
+ - 31+: Very dark gray (#111827)
137
+ - Node border: Code smell severity (red scale)
138
+ - none: Light gray (#e5e7eb)
139
+ - info: Light red (#fca5a5)
140
+ - warning: Medium red (#f87171)
141
+ - error: Dark red (#ef4444)
142
+ - critical: Very dark red (#dc2626) with glow
143
+ - Edge thickness: Coupling strength (number of imports)
144
+ - Edge color: Red if circular dependency, gray otherwise
145
+
146
+ Args:
147
+ export: Complete analysis export with files and dependencies
148
+
149
+ Returns:
150
+ Dictionary containing:
151
+ - nodes: List of node objects with visualization properties
152
+ - links: List of edge objects with source, target, coupling
153
+ - modules: List of module cluster definitions for hulls
154
+ - summary: Project summary statistics for context
155
+
156
+ Example:
157
+ >>> d3_data = transform_for_d3(export)
158
+ >>> d3_data.keys()
159
+ dict_keys(['nodes', 'links', 'modules', 'summary'])
160
+ >>> len(d3_data['nodes'])
161
+ 42
162
+ >>> d3_data['nodes'][0]
163
+ {'id': 'src/main.py', 'label': 'main.py', ...}
164
+ """
165
+ # Create nodes from files
166
+ nodes = [_create_node(file) for file in export.files]
167
+
168
+ # Identify circular dependency paths
169
+ circular_paths = _extract_circular_paths(export)
170
+
171
+ # Create edges from dependency graph
172
+ links = _create_edges(export, circular_paths)
173
+
174
+ # Group nodes by module for cluster hulls
175
+ modules = _create_module_groups(nodes)
176
+
177
+ # Calculate detailed statistics for dashboard panels
178
+ summary = _create_summary_stats(export, nodes, links)
179
+
180
+ return {
181
+ "nodes": [n.to_dict() for n in nodes],
182
+ "links": [e.to_dict() for e in links],
183
+ "modules": modules,
184
+ "summary": summary,
185
+ }
186
+
187
+
188
+ def _create_summary_stats(
189
+ export: AnalysisExport, nodes: list[D3Node], links: list[D3Edge]
190
+ ) -> dict[str, Any]:
191
+ """Create detailed summary statistics for dashboard panels.
192
+
193
+ Calculates comprehensive statistics including:
194
+ - Basic counts (files, functions, classes)
195
+ - Complexity metrics with grade distribution
196
+ - Smell breakdown by severity
197
+ - LOC distribution statistics
198
+ - Circular dependency information
199
+ - Complexity level distribution for legend
200
+
201
+ Args:
202
+ export: Complete analysis export
203
+ nodes: List of D3Node objects
204
+ links: List of D3Edge objects
205
+
206
+ Returns:
207
+ Dictionary containing detailed summary statistics
208
+ """
209
+ # Basic counts from export summary
210
+ basic_stats = {
211
+ "total_files": export.summary.total_files,
212
+ "total_functions": export.summary.total_functions,
213
+ "total_classes": export.summary.total_classes,
214
+ "total_lines": export.summary.total_lines,
215
+ "circular_dependencies": export.summary.circular_dependencies,
216
+ }
217
+
218
+ # Complexity statistics with grade
219
+ avg_complexity = export.summary.avg_cognitive_complexity
220
+ complexity_grade = _get_complexity_grade(avg_complexity)
221
+
222
+ complexity_stats = {
223
+ "avg_complexity": avg_complexity,
224
+ "avg_cyclomatic_complexity": export.summary.avg_complexity,
225
+ "complexity_grade": complexity_grade,
226
+ }
227
+
228
+ # Smell breakdown by severity
229
+ smells_by_severity = export.summary.smells_by_severity or {}
230
+ smell_stats = {
231
+ "total_smells": export.summary.total_smells,
232
+ "error_count": smells_by_severity.get("error", 0),
233
+ "warning_count": smells_by_severity.get("warning", 0),
234
+ "info_count": smells_by_severity.get("info", 0),
235
+ }
236
+
237
+ # LOC distribution statistics
238
+ if export.files:
239
+ locs = [f.lines_of_code for f in export.files]
240
+ loc_stats = {
241
+ "min_loc": min(locs),
242
+ "max_loc": max(locs),
243
+ "median_loc": sorted(locs)[len(locs) // 2],
244
+ "total_loc": sum(locs),
245
+ }
246
+ else:
247
+ loc_stats = {"min_loc": 0, "max_loc": 0, "median_loc": 0, "total_loc": 0}
248
+
249
+ # Complexity level distribution for legend (count nodes per level)
250
+ complexity_distribution = {
251
+ "low": 0, # 0-5
252
+ "moderate": 0, # 6-10
253
+ "high": 0, # 11-20
254
+ "very_high": 0, # 21-30
255
+ "critical": 0, # 31+
256
+ }
257
+
258
+ for node in nodes:
259
+ level = get_complexity_class(node.complexity)
260
+ # Map to underscore version for consistency
261
+ level_key = level.replace("-", "_")
262
+ if level_key in complexity_distribution:
263
+ complexity_distribution[level_key] += 1
264
+
265
+ # Smell severity distribution for legend (count nodes per severity)
266
+ smell_distribution = {
267
+ "none": 0,
268
+ "info": 0,
269
+ "warning": 0,
270
+ "error": 0,
271
+ "critical": 0,
272
+ }
273
+
274
+ for node in nodes:
275
+ severity = node.smell_severity
276
+ if severity in smell_distribution:
277
+ smell_distribution[severity] += 1
278
+
279
+ return {
280
+ **basic_stats,
281
+ **complexity_stats,
282
+ **smell_stats,
283
+ **loc_stats,
284
+ "complexity_distribution": complexity_distribution,
285
+ "smell_distribution": smell_distribution,
286
+ }
287
+
288
+
289
+ def _get_complexity_grade(avg_complexity: float) -> str:
290
+ """Get letter grade from average complexity score.
291
+
292
+ Args:
293
+ avg_complexity: Average cognitive complexity score
294
+
295
+ Returns:
296
+ Letter grade (A, B, C, D, or F)
297
+ """
298
+ if avg_complexity <= 5:
299
+ return "A"
300
+ elif avg_complexity <= 10:
301
+ return "B"
302
+ elif avg_complexity <= 20:
303
+ return "C"
304
+ elif avg_complexity <= 30:
305
+ return "D"
306
+ else:
307
+ return "F"
308
+
309
+
310
+ def _create_node(file: FileDetail) -> D3Node:
311
+ """Create a D3Node from FileDetail.
312
+
313
+ Args:
314
+ file: File metrics from analysis export
315
+
316
+ Returns:
317
+ D3Node with visual properties derived from metrics
318
+ """
319
+ file_path = Path(file.path)
320
+ label = file_path.name
321
+ module = file_path.parent.name if file_path.parent.name else "root"
322
+ module_path = str(file_path.parent) if file_path.parent.name else "root"
323
+
324
+ # Calculate worst smell severity
325
+ smell_severity = _calculate_worst_severity(file)
326
+
327
+ # Convert smells to dictionaries for JSON serialization
328
+ smells_data = [
329
+ {
330
+ "type": smell.smell_type,
331
+ "severity": smell.severity,
332
+ "message": smell.message,
333
+ "line": smell.line,
334
+ }
335
+ for smell in file.smells
336
+ ]
337
+
338
+ return D3Node(
339
+ id=file.path,
340
+ label=label,
341
+ module=module,
342
+ module_path=module_path,
343
+ loc=file.lines_of_code,
344
+ complexity=file.cognitive_complexity,
345
+ smell_count=len(file.smells),
346
+ smell_severity=smell_severity,
347
+ cyclomatic_complexity=file.cyclomatic_complexity,
348
+ function_count=file.function_count,
349
+ class_count=file.class_count,
350
+ smells=smells_data,
351
+ imports=file.imports or [],
352
+ )
353
+
354
+
355
+ def _calculate_worst_severity(file: FileDetail) -> str:
356
+ """Calculate worst smell severity for a file.
357
+
358
+ Severity levels in order of severity (low to high):
359
+ - none: No smells detected
360
+ - info: Informational smells only
361
+ - warning: Warning-level smells
362
+ - error: Error-level smells
363
+ - critical: Critical smells (not in current schema, reserved for future)
364
+
365
+ Args:
366
+ file: File detail with smell information
367
+
368
+ Returns:
369
+ Worst severity level as string
370
+ """
371
+ if not file.smells:
372
+ return "none"
373
+
374
+ severity_order = {"info": 1, "warning": 2, "error": 3}
375
+ worst_severity = "none"
376
+ worst_level = 0
377
+
378
+ for smell in file.smells:
379
+ level = severity_order.get(smell.severity, 0)
380
+ if level > worst_level:
381
+ worst_level = level
382
+ worst_severity = smell.severity
383
+
384
+ return worst_severity
385
+
386
+
387
+ def _extract_circular_paths(export: AnalysisExport) -> set[tuple[str, str]]:
388
+ """Extract all edges that are part of circular dependencies.
389
+
390
+ Creates a set of (source, target) tuples representing edges
391
+ that participate in any circular dependency cycle.
392
+
393
+ Args:
394
+ export: Analysis export with circular dependency data
395
+
396
+ Returns:
397
+ Set of (source, target) tuples for circular edges
398
+ """
399
+ circular_edges: set[tuple[str, str]] = set()
400
+
401
+ for cycle in export.dependencies.circular_dependencies:
402
+ # For each cycle, mark all edges in the cycle as circular
403
+ for i in range(len(cycle.cycle)):
404
+ source = cycle.cycle[i]
405
+ target = cycle.cycle[(i + 1) % len(cycle.cycle)]
406
+ circular_edges.add((source, target))
407
+
408
+ return circular_edges
409
+
410
+
411
+ def _create_edges(
412
+ export: AnalysisExport, circular_paths: set[tuple[str, str]]
413
+ ) -> list[D3Edge]:
414
+ """Create D3Edges from dependency graph.
415
+
416
+ Args:
417
+ export: Analysis export with dependency graph
418
+ circular_paths: Set of (source, target) tuples that are circular
419
+
420
+ Returns:
421
+ List of D3Edge objects with coupling and circularity info
422
+ """
423
+ edges: list[D3Edge] = []
424
+
425
+ # Count coupling strength (number of imports between each pair)
426
+ coupling_counts: dict[tuple[str, str], int] = {}
427
+
428
+ for edge in export.dependencies.edges:
429
+ key = (edge.source, edge.target)
430
+ coupling_counts[key] = coupling_counts.get(key, 0) + 1
431
+
432
+ # Create D3 edges with coupling strength and circularity flag
433
+ for (source, target), coupling in coupling_counts.items():
434
+ is_circular = (source, target) in circular_paths
435
+ edges.append(
436
+ D3Edge(
437
+ source=source, target=target, coupling=coupling, circular=is_circular
438
+ )
439
+ )
440
+
441
+ return edges
442
+
443
+
444
+ def get_complexity_class(complexity: float) -> str:
445
+ """Get CSS class name for complexity level.
446
+
447
+ Maps complexity scores to CSS class names for styling.
448
+
449
+ Complexity Thresholds:
450
+ - 0-5: low (very light gray)
451
+ - 6-10: moderate (light gray)
452
+ - 11-20: high (medium gray)
453
+ - 21-30: very-high (dark gray)
454
+ - 31+: critical (very dark gray)
455
+
456
+ Args:
457
+ complexity: Cognitive complexity score
458
+
459
+ Returns:
460
+ CSS class suffix (e.g., "low", "critical")
461
+ """
462
+ if complexity <= 5:
463
+ return "low"
464
+ elif complexity <= 10:
465
+ return "moderate"
466
+ elif complexity <= 20:
467
+ return "high"
468
+ elif complexity <= 30:
469
+ return "very-high"
470
+ else:
471
+ return "critical"
472
+
473
+
474
+ def get_smell_class(severity: str) -> str:
475
+ """Get CSS class name for smell severity.
476
+
477
+ Maps smell severity to CSS class names for border styling.
478
+
479
+ Args:
480
+ severity: Smell severity level (none, info, warning, error, critical)
481
+
482
+ Returns:
483
+ CSS class name (e.g., "smell-none", "smell-error")
484
+ """
485
+ return f"smell-{severity}"
486
+
487
+
488
+ def _create_module_groups(nodes: list[D3Node]) -> list[dict[str, Any]]:
489
+ """Group nodes by module path for cluster visualization.
490
+
491
+ Creates module cluster definitions that can be used to draw
492
+ convex hull polygons around related nodes.
493
+
494
+ Args:
495
+ nodes: List of D3Node objects
496
+
497
+ Returns:
498
+ List of module group dictionaries containing:
499
+ - name: Module path identifier
500
+ - node_ids: List of node IDs belonging to this module
501
+ - color: Hex color for the module cluster
502
+ """
503
+ # Group nodes by module_path
504
+ module_map: dict[str, list[str]] = {}
505
+ for node in nodes:
506
+ if node.module_path not in module_map:
507
+ module_map[node.module_path] = []
508
+ module_map[node.module_path].append(node.id)
509
+
510
+ # Assign colors to modules (cycle through a palette)
511
+ colors = [
512
+ "#3b82f6", # Blue
513
+ "#10b981", # Green
514
+ "#f59e0b", # Orange
515
+ "#8b5cf6", # Purple
516
+ "#ec4899", # Pink
517
+ "#14b8a6", # Teal
518
+ "#f97316", # Orange-red
519
+ "#06b6d4", # Cyan
520
+ ]
521
+
522
+ modules = []
523
+ for idx, (module_path, node_ids) in enumerate(sorted(module_map.items())):
524
+ # Only create module groups with 2+ nodes for meaningful hulls
525
+ if len(node_ids) >= 2:
526
+ modules.append(
527
+ {
528
+ "name": module_path,
529
+ "node_ids": node_ids,
530
+ "color": colors[idx % len(colors)],
531
+ }
532
+ )
533
+
534
+ return modules