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,325 @@
1
+ """Code smell detection based on structural metrics.
2
+
3
+ This module provides smell detection functionality that identifies common
4
+ code smells based on complexity metrics and structural analysis.
5
+
6
+ Supported Smells:
7
+ - Long Method: Functions/methods with too many lines or complexity
8
+ - Deep Nesting: Excessive nesting depth
9
+ - Long Parameter List: Too many function parameters
10
+ - God Class: Classes with too many methods and lines
11
+ - Complex Method: High cyclomatic complexity
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from ...config.thresholds import ThresholdConfig
22
+ from ..metrics import ChunkMetrics, FileMetrics
23
+
24
+
25
+ class SmellSeverity(Enum):
26
+ """Severity level for code smells.
27
+
28
+ Attributes:
29
+ INFO: Informational smell, no immediate action needed
30
+ WARNING: Warning smell, should be addressed during refactoring
31
+ ERROR: Error-level smell, requires immediate attention
32
+ """
33
+
34
+ INFO = "info"
35
+ WARNING = "warning"
36
+ ERROR = "error"
37
+
38
+ def __str__(self) -> str:
39
+ """Return string representation of severity."""
40
+ return self.value
41
+
42
+
43
+ @dataclass
44
+ class CodeSmell:
45
+ """Represents a detected code smell.
46
+
47
+ Attributes:
48
+ name: Human-readable smell name (e.g., "Long Method")
49
+ description: Detailed description of the smell
50
+ severity: Severity level (INFO, WARNING, ERROR)
51
+ location: File location in format "file:line" or "file:line-range"
52
+ metric_value: Actual metric value that triggered the smell
53
+ threshold: Threshold value that was exceeded
54
+ suggestion: Optional suggestion for fixing the smell
55
+ """
56
+
57
+ name: str
58
+ description: str
59
+ severity: SmellSeverity
60
+ location: str
61
+ metric_value: float
62
+ threshold: float
63
+ suggestion: str = ""
64
+
65
+ def __str__(self) -> str:
66
+ """Return string representation of code smell."""
67
+ return (
68
+ f"[{self.severity.value.upper()}] {self.name} at {self.location}: "
69
+ f"{self.description} (value: {self.metric_value}, threshold: {self.threshold})"
70
+ )
71
+
72
+
73
+ class SmellDetector:
74
+ """Detects code smells based on structural metrics.
75
+
76
+ This detector analyzes ChunkMetrics and FileMetrics to identify common
77
+ code smells that indicate maintainability issues. Detection rules are
78
+ configurable via ThresholdConfig.
79
+
80
+ Detection Rules:
81
+ - Long Method: lines > 50 OR cognitive_complexity > 15
82
+ - Deep Nesting: max_nesting_depth > 4
83
+ - Long Parameter List: parameter_count > 5
84
+ - God Class: method_count > 20 AND lines > 500
85
+ - Complex Method: cyclomatic_complexity > 10
86
+
87
+ Example:
88
+ detector = SmellDetector()
89
+ smells = detector.detect(chunk_metrics)
90
+
91
+ for smell in smells:
92
+ print(f"{smell.severity}: {smell.name} at {smell.location}")
93
+ """
94
+
95
+ def __init__(self, thresholds: ThresholdConfig | None = None) -> None:
96
+ """Initialize smell detector.
97
+
98
+ Args:
99
+ thresholds: Optional custom threshold configuration.
100
+ If None, uses default thresholds from ThresholdConfig.
101
+ """
102
+ if thresholds is None:
103
+ # Import here to avoid circular dependency
104
+ from ...config.thresholds import ThresholdConfig
105
+
106
+ thresholds = ThresholdConfig()
107
+
108
+ self.thresholds = thresholds
109
+
110
+ def detect(
111
+ self, metrics: ChunkMetrics, file_path: str = "", start_line: int = 0
112
+ ) -> list[CodeSmell]:
113
+ """Detect code smells in a single chunk (function/method/class).
114
+
115
+ Analyzes the provided metrics and returns a list of detected smells.
116
+ Each smell includes location information and severity level.
117
+
118
+ Args:
119
+ metrics: Chunk metrics to analyze
120
+ file_path: Path to the file containing this chunk
121
+ start_line: Starting line number of the chunk
122
+
123
+ Returns:
124
+ List of detected CodeSmell objects (empty if no smells found)
125
+ """
126
+ smells: list[CodeSmell] = []
127
+
128
+ # Location string for reporting
129
+ location = f"{file_path}:{start_line}" if file_path else f"line {start_line}"
130
+
131
+ # 1. Long Method Detection
132
+ # Rule: lines > 50 OR cognitive_complexity > 15
133
+ long_method_lines_threshold = self.thresholds.smells.long_method_lines
134
+ high_complexity_threshold = self.thresholds.smells.high_complexity
135
+
136
+ is_long_by_lines = metrics.lines_of_code > long_method_lines_threshold
137
+ is_long_by_complexity = metrics.cognitive_complexity > high_complexity_threshold
138
+
139
+ if is_long_by_lines or is_long_by_complexity:
140
+ description_parts = []
141
+ if is_long_by_lines:
142
+ description_parts.append(
143
+ f"{metrics.lines_of_code} lines (threshold: {long_method_lines_threshold})"
144
+ )
145
+ if is_long_by_complexity:
146
+ description_parts.append(
147
+ f"cognitive complexity {metrics.cognitive_complexity} (threshold: {high_complexity_threshold})"
148
+ )
149
+
150
+ smells.append(
151
+ CodeSmell(
152
+ name="Long Method",
153
+ description=f"Method/function is too long: {', '.join(description_parts)}",
154
+ severity=SmellSeverity.WARNING,
155
+ location=location,
156
+ metric_value=float(
157
+ max(metrics.lines_of_code, metrics.cognitive_complexity)
158
+ ),
159
+ threshold=float(
160
+ max(long_method_lines_threshold, high_complexity_threshold)
161
+ ),
162
+ suggestion="Consider breaking this method into smaller, focused functions",
163
+ )
164
+ )
165
+
166
+ # 2. Deep Nesting Detection
167
+ # Rule: max_nesting_depth > 4
168
+ nesting_threshold = self.thresholds.smells.deep_nesting_depth
169
+
170
+ if metrics.max_nesting_depth > nesting_threshold:
171
+ smells.append(
172
+ CodeSmell(
173
+ name="Deep Nesting",
174
+ description=f"Excessive nesting depth: {metrics.max_nesting_depth} levels (threshold: {nesting_threshold})",
175
+ severity=SmellSeverity.WARNING,
176
+ location=location,
177
+ metric_value=float(metrics.max_nesting_depth),
178
+ threshold=float(nesting_threshold),
179
+ suggestion="Consider extracting nested logic into separate functions or using early returns",
180
+ )
181
+ )
182
+
183
+ # 3. Long Parameter List Detection
184
+ # Rule: parameter_count > 5
185
+ param_threshold = self.thresholds.smells.too_many_parameters
186
+
187
+ if metrics.parameter_count > param_threshold:
188
+ smells.append(
189
+ CodeSmell(
190
+ name="Long Parameter List",
191
+ description=f"Too many parameters: {metrics.parameter_count} parameters (threshold: {param_threshold})",
192
+ severity=SmellSeverity.WARNING,
193
+ location=location,
194
+ metric_value=float(metrics.parameter_count),
195
+ threshold=float(param_threshold),
196
+ suggestion="Consider introducing a parameter object or using builder pattern",
197
+ )
198
+ )
199
+
200
+ # 4. Complex Method Detection
201
+ # Rule: cyclomatic_complexity > 10
202
+ cyclomatic_threshold = self.thresholds.complexity.cyclomatic_moderate
203
+
204
+ if metrics.cyclomatic_complexity > cyclomatic_threshold:
205
+ smells.append(
206
+ CodeSmell(
207
+ name="Complex Method",
208
+ description=f"High cyclomatic complexity: {metrics.cyclomatic_complexity} (threshold: {cyclomatic_threshold})",
209
+ severity=SmellSeverity.WARNING,
210
+ location=location,
211
+ metric_value=float(metrics.cyclomatic_complexity),
212
+ threshold=float(cyclomatic_threshold),
213
+ suggestion="Simplify control flow by extracting complex conditions into separate functions",
214
+ )
215
+ )
216
+
217
+ return smells
218
+
219
+ def detect_god_class(
220
+ self, file_metrics: FileMetrics, file_path: str = ""
221
+ ) -> list[CodeSmell]:
222
+ """Detect God Class smell at file level.
223
+
224
+ A God Class has too many responsibilities, indicated by high method
225
+ count and large file size.
226
+
227
+ Rule: method_count > 20 AND lines > 500
228
+
229
+ Args:
230
+ file_metrics: File-level metrics to analyze
231
+ file_path: Path to the file being analyzed
232
+
233
+ Returns:
234
+ List containing CodeSmell if God Class detected (empty otherwise)
235
+ """
236
+ smells: list[CodeSmell] = []
237
+
238
+ # God Class Detection
239
+ # Rule: method_count > 20 AND total_lines > 500
240
+ method_threshold = self.thresholds.smells.god_class_methods
241
+ lines_threshold = self.thresholds.smells.god_class_lines
242
+
243
+ is_god_class = (
244
+ file_metrics.method_count > method_threshold
245
+ and file_metrics.total_lines > lines_threshold
246
+ )
247
+
248
+ if is_god_class:
249
+ location = file_path if file_path else "file"
250
+ smells.append(
251
+ CodeSmell(
252
+ name="God Class",
253
+ description=(
254
+ f"Class has too many responsibilities: "
255
+ f"{file_metrics.method_count} methods (threshold: {method_threshold}), "
256
+ f"{file_metrics.total_lines} lines (threshold: {lines_threshold})"
257
+ ),
258
+ severity=SmellSeverity.ERROR,
259
+ location=location,
260
+ metric_value=float(file_metrics.method_count),
261
+ threshold=float(method_threshold),
262
+ suggestion="Split this class into smaller, focused classes following Single Responsibility Principle",
263
+ )
264
+ )
265
+
266
+ return smells
267
+
268
+ def detect_all(
269
+ self, file_metrics: FileMetrics, file_path: str = ""
270
+ ) -> list[CodeSmell]:
271
+ """Detect all code smells across an entire file.
272
+
273
+ Analyzes both chunk-level smells (Long Method, Deep Nesting, etc.)
274
+ and file-level smells (God Class).
275
+
276
+ Args:
277
+ file_metrics: File metrics containing all chunks
278
+ file_path: Path to the file being analyzed
279
+
280
+ Returns:
281
+ List of all detected CodeSmell objects
282
+ """
283
+ all_smells: list[CodeSmell] = []
284
+
285
+ # Detect chunk-level smells
286
+ for i, chunk in enumerate(file_metrics.chunks):
287
+ # Estimate start line for each chunk
288
+ # This is a rough approximation - ideally we'd have actual line numbers
289
+ estimated_start_line = (
290
+ 1 + i * 20
291
+ ) # Rough estimate assuming 20 lines per chunk
292
+
293
+ chunk_smells = self.detect(chunk, file_path, estimated_start_line)
294
+ all_smells.extend(chunk_smells)
295
+
296
+ # Detect file-level smells (God Class)
297
+ file_smells = self.detect_god_class(file_metrics, file_path)
298
+ all_smells.extend(file_smells)
299
+
300
+ return all_smells
301
+
302
+ def get_smell_summary(self, smells: list[CodeSmell]) -> dict[str, int]:
303
+ """Generate summary statistics for detected smells.
304
+
305
+ Args:
306
+ smells: List of detected code smells
307
+
308
+ Returns:
309
+ Dictionary with counts by severity and smell type
310
+ """
311
+ summary: dict[str, int] = {
312
+ "total": len(smells),
313
+ "error": sum(1 for s in smells if s.severity == SmellSeverity.ERROR),
314
+ "warning": sum(1 for s in smells if s.severity == SmellSeverity.WARNING),
315
+ "info": sum(1 for s in smells if s.severity == SmellSeverity.INFO),
316
+ }
317
+
318
+ # Count by smell type
319
+ smell_types = {}
320
+ for smell in smells:
321
+ smell_types[smell.name] = smell_types.get(smell.name, 0) + 1
322
+
323
+ summary["by_type"] = smell_types
324
+
325
+ return summary