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,480 @@
1
+ """Markdown reporter for code analysis results.
2
+
3
+ Generates two markdown reports:
4
+ 1. Full analysis report with metrics and visualizations
5
+ 2. Agent-actionable fixes report for refactoring tasks
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from ..collectors.smells import CodeSmell
16
+ from ..metrics import ProjectMetrics
17
+
18
+
19
+ class MarkdownReporter:
20
+ """Markdown reporter for generating analysis reports."""
21
+
22
+ def generate_analysis_report(
23
+ self,
24
+ metrics: ProjectMetrics,
25
+ smells: list[CodeSmell] | None = None,
26
+ output_path: Path | None = None,
27
+ ) -> str:
28
+ """Generate full analysis report in markdown format.
29
+
30
+ Args:
31
+ metrics: Project metrics to report
32
+ smells: Optional list of detected code smells
33
+ output_path: Optional output file path (defaults to current directory)
34
+
35
+ Returns:
36
+ Path to generated markdown file
37
+ """
38
+ if output_path is None:
39
+ output_path = Path.cwd()
40
+
41
+ # Determine output directory
42
+ if output_path.is_dir():
43
+ output_dir = output_path
44
+ elif output_path.exists() and output_path.is_file():
45
+ # If file exists, use parent directory
46
+ output_dir = output_path.parent
47
+ else:
48
+ # If path doesn't exist, check if parent exists
49
+ if output_path.parent.exists() and output_path.parent.is_dir():
50
+ output_dir = output_path.parent
51
+ else:
52
+ # Default to current directory
53
+ output_dir = Path.cwd()
54
+
55
+ output_file = output_dir / "mcp-vector-search-analysis.md"
56
+
57
+ # Generate markdown content
58
+ content = self._build_analysis_markdown(metrics, smells or [])
59
+
60
+ # Write to file
61
+ output_file.write_text(content, encoding="utf-8")
62
+
63
+ return str(output_file)
64
+
65
+ def generate_fixes_report(
66
+ self,
67
+ metrics: ProjectMetrics,
68
+ smells: list[CodeSmell],
69
+ output_path: Path | None = None,
70
+ ) -> str:
71
+ """Generate agent-actionable fixes report.
72
+
73
+ Args:
74
+ metrics: Project metrics to analyze
75
+ smells: List of detected code smells
76
+ output_path: Optional output file path (defaults to current directory)
77
+
78
+ Returns:
79
+ Path to generated markdown file
80
+ """
81
+ if output_path is None:
82
+ output_path = Path.cwd()
83
+
84
+ # Determine output directory
85
+ if output_path.is_dir():
86
+ output_dir = output_path
87
+ elif output_path.exists() and output_path.is_file():
88
+ # If file exists, use parent directory
89
+ output_dir = output_path.parent
90
+ else:
91
+ # If path doesn't exist, check if parent exists
92
+ if output_path.parent.exists() and output_path.parent.is_dir():
93
+ output_dir = output_path.parent
94
+ else:
95
+ # Default to current directory
96
+ output_dir = Path.cwd()
97
+
98
+ output_file = output_dir / "mcp-vector-search-analysis-fixes.md"
99
+
100
+ # Generate markdown content
101
+ content = self._build_fixes_markdown(metrics, smells)
102
+
103
+ # Write to file
104
+ output_file.write_text(content, encoding="utf-8")
105
+
106
+ return str(output_file)
107
+
108
+ def _build_analysis_markdown(
109
+ self, metrics: ProjectMetrics, smells: list[CodeSmell]
110
+ ) -> str:
111
+ """Build full analysis markdown content.
112
+
113
+ Args:
114
+ metrics: Project metrics
115
+ smells: Detected code smells
116
+
117
+ Returns:
118
+ Markdown content as string
119
+ """
120
+ lines: list[str] = []
121
+
122
+ # Header
123
+ lines.append("# MCP Vector Search - Code Analysis Report")
124
+ lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
125
+ lines.append("")
126
+
127
+ # Summary section
128
+ lines.append("## Summary")
129
+ lines.append("")
130
+ lines.append(f"- **Files analyzed**: {metrics.total_files}")
131
+ lines.append(f"- **Total lines**: {metrics.total_lines:,}")
132
+ lines.append(f"- **Total functions**: {metrics.total_functions}")
133
+ lines.append(f"- **Total classes**: {metrics.total_classes}")
134
+ lines.append(f"- **Average complexity**: {metrics.avg_file_complexity:.1f}")
135
+ lines.append("")
136
+
137
+ # Complexity distribution
138
+ distribution = metrics._compute_grade_distribution()
139
+ total_chunks = sum(distribution.values())
140
+
141
+ if total_chunks > 0:
142
+ lines.append("## Complexity Distribution")
143
+ lines.append("")
144
+ lines.append("| Grade | Description | Count | Percentage |")
145
+ lines.append("|-------|------------|-------|------------|")
146
+
147
+ grade_info = {
148
+ "A": "Excellent (0-5)",
149
+ "B": "Good (6-10)",
150
+ "C": "Acceptable (11-20)",
151
+ "D": "Needs Improvement (21-30)",
152
+ "F": "Refactor Required (31+)",
153
+ }
154
+
155
+ for grade in ["A", "B", "C", "D", "F"]:
156
+ count = distribution.get(grade, 0)
157
+ percentage = (count / total_chunks * 100) if total_chunks > 0 else 0
158
+ description = grade_info[grade]
159
+ lines.append(
160
+ f"| {grade} | {description} | {count} | {percentage:.1f}% |"
161
+ )
162
+
163
+ lines.append("")
164
+
165
+ # Code smells summary
166
+ if smells:
167
+ from ..collectors.smells import SmellSeverity
168
+
169
+ error_count = sum(1 for s in smells if s.severity == SmellSeverity.ERROR)
170
+ warning_count = sum(
171
+ 1 for s in smells if s.severity == SmellSeverity.WARNING
172
+ )
173
+ info_count = sum(1 for s in smells if s.severity == SmellSeverity.INFO)
174
+
175
+ lines.append("## Code Smells Summary")
176
+ lines.append("")
177
+ lines.append("| Severity | Count |")
178
+ lines.append("|----------|-------|")
179
+ lines.append(f"| ERROR | {error_count} |")
180
+ lines.append(f"| WARNING | {warning_count} |")
181
+ lines.append(f"| INFO | {info_count} |")
182
+ lines.append(f"| **Total** | **{len(smells)}** |")
183
+ lines.append("")
184
+
185
+ # Count by smell type
186
+ smell_types: dict[str, int] = {}
187
+ for smell in smells:
188
+ smell_types[smell.name] = smell_types.get(smell.name, 0) + 1
189
+
190
+ lines.append("### By Type")
191
+ lines.append("")
192
+ lines.append("| Type | Count | Severity |")
193
+ lines.append("|------|-------|----------|")
194
+
195
+ # Sort by count descending
196
+ for smell_type, count in sorted(
197
+ smell_types.items(), key=lambda x: x[1], reverse=True
198
+ ):
199
+ # Get most common severity for this type
200
+ type_smells = [s for s in smells if s.name == smell_type]
201
+ severities = [s.severity.value for s in type_smells]
202
+ common_severity = max(set(severities), key=severities.count)
203
+ lines.append(f"| {smell_type} | {count} | {common_severity} |")
204
+
205
+ lines.append("")
206
+
207
+ # Top complexity hotspots
208
+ hotspots = metrics.get_hotspots(limit=10)
209
+
210
+ if hotspots:
211
+ lines.append("## Top Complexity Hotspots")
212
+ lines.append("")
213
+
214
+ for rank, file_metrics in enumerate(hotspots, 1):
215
+ # Compute average grade
216
+ if file_metrics.chunks:
217
+ grades = [chunk.complexity_grade for chunk in file_metrics.chunks]
218
+ avg_grade = max(set(grades), key=grades.count)
219
+ else:
220
+ avg_grade = "N/A"
221
+
222
+ lines.append(
223
+ f"{rank}. **{file_metrics.file_path}** - "
224
+ f"Complexity: {file_metrics.avg_complexity:.1f}, "
225
+ f"Grade: {avg_grade}, "
226
+ f"Lines: {file_metrics.total_lines}, "
227
+ f"Functions: {len(file_metrics.chunks)}"
228
+ )
229
+
230
+ lines.append("")
231
+
232
+ # Detailed smells (top 20)
233
+ if smells:
234
+ from ..collectors.smells import SmellSeverity
235
+
236
+ lines.append("## Detailed Code Smells")
237
+ lines.append("")
238
+ lines.append("Showing top 20 most critical issues:")
239
+ lines.append("")
240
+
241
+ # Prioritize errors, then warnings, then info
242
+ sorted_smells = sorted(
243
+ smells,
244
+ key=lambda s: (
245
+ (
246
+ 0
247
+ if s.severity == SmellSeverity.ERROR
248
+ else 1
249
+ if s.severity == SmellSeverity.WARNING
250
+ else 2
251
+ ),
252
+ -s.metric_value,
253
+ ),
254
+ )[:20]
255
+
256
+ for i, smell in enumerate(sorted_smells, 1):
257
+ lines.append(f"### {i}. {smell.name} ({smell.severity.value.upper()})")
258
+ lines.append("")
259
+ lines.append(f"- **Location**: `{smell.location}`")
260
+ lines.append(f"- **Description**: {smell.description}")
261
+ lines.append(
262
+ f"- **Metric**: {smell.metric_value} (threshold: {smell.threshold})"
263
+ )
264
+ if smell.suggestion:
265
+ lines.append(f"- **Suggestion**: {smell.suggestion}")
266
+ lines.append("")
267
+
268
+ # Health metrics
269
+ avg_health = metrics._compute_avg_health_score()
270
+ files_needing_attention = metrics._count_files_needing_attention()
271
+
272
+ lines.append("## Health Metrics")
273
+ lines.append("")
274
+ lines.append(f"- **Average health score**: {avg_health:.2f} / 1.00")
275
+ lines.append(
276
+ f"- **Files needing attention**: {files_needing_attention} "
277
+ f"({files_needing_attention / max(metrics.total_files, 1) * 100:.1f}%)"
278
+ )
279
+ lines.append("")
280
+
281
+ # Footer
282
+ lines.append("---")
283
+ lines.append("")
284
+ lines.append("*Generated by mcp-vector-search analyze command*")
285
+ lines.append("")
286
+
287
+ return "\n".join(lines)
288
+
289
+ def _build_fixes_markdown(
290
+ self, metrics: ProjectMetrics, smells: list[CodeSmell]
291
+ ) -> str:
292
+ """Build agent-actionable fixes markdown content.
293
+
294
+ Args:
295
+ metrics: Project metrics
296
+ smells: Detected code smells
297
+
298
+ Returns:
299
+ Markdown content as string
300
+ """
301
+
302
+ lines: list[str] = []
303
+
304
+ # Header
305
+ lines.append("# MCP Vector Search - Refactoring Tasks")
306
+ lines.append("")
307
+ lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
308
+ lines.append("")
309
+ lines.append(
310
+ "This report contains actionable refactoring tasks prioritized "
311
+ "by severity and impact."
312
+ )
313
+ lines.append("")
314
+
315
+ # Priority 1: God Classes
316
+ god_class_smells = [s for s in smells if s.name == "God Class"]
317
+
318
+ if god_class_smells:
319
+ lines.append("## Priority 1: God Classes")
320
+ lines.append("")
321
+ lines.append(
322
+ "Large classes with too many responsibilities. "
323
+ "These should be split into smaller, focused classes."
324
+ )
325
+ lines.append("")
326
+
327
+ for i, smell in enumerate(god_class_smells, 1):
328
+ # Extract file path and line info
329
+ location_parts = smell.location.split(":")
330
+ file_path = location_parts[0]
331
+
332
+ # Get file metrics for more context
333
+ file_metrics = metrics.files.get(file_path)
334
+
335
+ lines.append(f"### {i}. {Path(file_path).name}")
336
+ lines.append("")
337
+ lines.append(f"- **Location**: `{smell.location}`")
338
+
339
+ if file_metrics:
340
+ lines.append(f"- **Lines**: {file_metrics.total_lines}")
341
+ lines.append(f"- **Methods**: {file_metrics.method_count}")
342
+ lines.append(f"- **Complexity**: {file_metrics.avg_complexity:.1f}")
343
+
344
+ lines.append(f"- **Task**: {smell.suggestion}")
345
+ lines.append("")
346
+ lines.append("**Suggested refactoring:**")
347
+ lines.append("")
348
+
349
+ if file_metrics and file_metrics.method_count > 0:
350
+ # Suggest splitting based on method groups
351
+ suggested_classes = min(max(file_metrics.method_count // 10, 2), 5)
352
+ lines.append(f"- Extract into {suggested_classes} separate classes")
353
+ lines.append("- Group related methods by functionality")
354
+ lines.append("- Move configuration/setup to separate config class")
355
+ lines.append("- Consider using composition over inheritance")
356
+
357
+ lines.append("")
358
+
359
+ # Priority 2: Long Methods (>100 lines)
360
+ long_method_smells = [
361
+ s for s in smells if s.name == "Long Method" and s.metric_value > 100
362
+ ]
363
+
364
+ if long_method_smells:
365
+ # Sort by metric value descending
366
+ long_method_smells.sort(key=lambda s: s.metric_value, reverse=True)
367
+
368
+ lines.append("## Priority 2: Long Methods (>100 lines)")
369
+ lines.append("")
370
+ lines.append(
371
+ "Methods that are too long and should be broken down into "
372
+ "smaller, focused functions."
373
+ )
374
+ lines.append("")
375
+
376
+ for i, smell in enumerate(long_method_smells[:10], 1):
377
+ lines.append(f"### {i}. Method at {smell.location}")
378
+ lines.append("")
379
+ lines.append(f"- **Metric**: {smell.metric_value:.0f} lines/complexity")
380
+ lines.append(f"- **Task**: {smell.suggestion}")
381
+ lines.append("")
382
+
383
+ # Priority 3: High Complexity (>20)
384
+ complex_method_smells = [
385
+ s for s in smells if s.name == "Complex Method" and s.metric_value > 20
386
+ ]
387
+
388
+ if complex_method_smells:
389
+ # Sort by metric value descending
390
+ complex_method_smells.sort(key=lambda s: s.metric_value, reverse=True)
391
+
392
+ lines.append("## Priority 3: High Complexity (>20)")
393
+ lines.append("")
394
+ lines.append(
395
+ "Methods with high cyclomatic complexity that need simplification."
396
+ )
397
+ lines.append("")
398
+
399
+ for i, smell in enumerate(complex_method_smells[:10], 1):
400
+ lines.append(f"### {i}. Method at {smell.location}")
401
+ lines.append("")
402
+ lines.append(f"- **Cyclomatic Complexity**: {smell.metric_value:.0f}")
403
+ lines.append(f"- **Task**: {smell.suggestion}")
404
+ lines.append("")
405
+
406
+ # Priority 4: Deep Nesting
407
+ nesting_smells = [s for s in smells if s.name == "Deep Nesting"]
408
+
409
+ if nesting_smells:
410
+ # Sort by metric value descending
411
+ nesting_smells.sort(key=lambda s: s.metric_value, reverse=True)
412
+
413
+ lines.append("## Priority 4: Deep Nesting")
414
+ lines.append("")
415
+ lines.append("Methods with excessive nesting that reduce readability.")
416
+ lines.append("")
417
+
418
+ for i, smell in enumerate(nesting_smells[:10], 1):
419
+ lines.append(f"### {i}. Method at {smell.location}")
420
+ lines.append("")
421
+ lines.append(f"- **Nesting Depth**: {smell.metric_value:.0f}")
422
+ lines.append(f"- **Task**: {smell.suggestion}")
423
+ lines.append("")
424
+
425
+ # Priority 5: Long Parameter Lists
426
+ param_smells = [s for s in smells if s.name == "Long Parameter List"]
427
+
428
+ if param_smells:
429
+ # Sort by metric value descending
430
+ param_smells.sort(key=lambda s: s.metric_value, reverse=True)
431
+
432
+ lines.append("## Priority 5: Long Parameter Lists")
433
+ lines.append("")
434
+ lines.append(
435
+ "Functions with too many parameters that should use "
436
+ "parameter objects or builders."
437
+ )
438
+ lines.append("")
439
+
440
+ for i, smell in enumerate(param_smells[:10], 1):
441
+ lines.append(f"### {i}. Function at {smell.location}")
442
+ lines.append("")
443
+ lines.append(f"- **Parameter Count**: {smell.metric_value:.0f}")
444
+ lines.append(f"- **Task**: {smell.suggestion}")
445
+ lines.append("")
446
+
447
+ # Summary
448
+ lines.append("## Summary")
449
+ lines.append("")
450
+ lines.append(f"- **Total issues**: {len(smells)}")
451
+ lines.append(
452
+ f"- **God Classes**: {len(god_class_smells)} (Priority 1 - highest impact)"
453
+ )
454
+ lines.append(
455
+ f"- **Long Methods**: {len(long_method_smells)} "
456
+ "(Priority 2 - moderate impact)"
457
+ )
458
+ lines.append(
459
+ f"- **Complex Methods**: {len(complex_method_smells)} "
460
+ "(Priority 3 - moderate impact)"
461
+ )
462
+ lines.append(
463
+ f"- **Deep Nesting**: {len(nesting_smells)} (Priority 4 - readability)"
464
+ )
465
+ lines.append(
466
+ f"- **Long Parameters**: {len(param_smells)} (Priority 5 - API design)"
467
+ )
468
+ lines.append("")
469
+
470
+ # Footer
471
+ lines.append("---")
472
+ lines.append("")
473
+ lines.append("*Generated by mcp-vector-search analyze command*")
474
+ lines.append("")
475
+ lines.append(
476
+ "**Note**: Address Priority 1 and 2 items first for maximum impact."
477
+ )
478
+ lines.append("")
479
+
480
+ return "\n".join(lines)