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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +48 -1
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +35 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +74 -1
- mcp_vector_search/analysis/reporters/__init__.py +3 -1
- mcp_vector_search/analysis/reporters/console.py +424 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +665 -11
- mcp_vector_search/cli/commands/chat.py +193 -0
- mcp_vector_search/cli/commands/index.py +600 -2
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/search.py +194 -1
- mcp_vector_search/cli/commands/setup.py +64 -13
- mcp_vector_search/cli/commands/status.py +302 -3
- mcp_vector_search/cli/commands/visualize/cli.py +26 -10
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
- mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
- mcp_vector_search/cli/commands/visualize/server.py +304 -15
- mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
- mcp_vector_search/cli/didyoumean.py +5 -0
- mcp_vector_search/cli/main.py +16 -5
- mcp_vector_search/cli/output.py +134 -5
- mcp_vector_search/config/thresholds.py +89 -1
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/database.py +39 -2
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/indexer.py +445 -84
- mcp_vector_search/core/llm_client.py +9 -4
- mcp_vector_search/core/models.py +88 -1
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/search.py +1 -1
- mcp_vector_search/mcp/server.py +795 -4
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/gitignore.py +0 -3
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
- mcp_vector_search/cli/commands/visualize.py.original +0 -2536
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Historical trend tracking for code metrics over time.
|
|
2
|
+
|
|
3
|
+
Stores daily snapshots of key metrics to track codebase evolution.
|
|
4
|
+
At most one entry per day - updates existing entry if reindexed same day.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TrendEntry:
|
|
20
|
+
"""Single trend snapshot for a specific date.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
date: ISO date string (YYYY-MM-DD)
|
|
24
|
+
timestamp: Full ISO timestamp when captured
|
|
25
|
+
metrics: Dictionary of metrics captured at this point
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
date: str
|
|
29
|
+
timestamp: str
|
|
30
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
"""Convert to dictionary for JSON serialization."""
|
|
34
|
+
return {
|
|
35
|
+
"date": self.date,
|
|
36
|
+
"timestamp": self.timestamp,
|
|
37
|
+
"metrics": self.metrics,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, data: dict[str, Any]) -> TrendEntry:
|
|
42
|
+
"""Create from dictionary."""
|
|
43
|
+
return cls(
|
|
44
|
+
date=data["date"],
|
|
45
|
+
timestamp=data["timestamp"],
|
|
46
|
+
metrics=data.get("metrics", {}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class TrendData:
|
|
52
|
+
"""Container for all trend entries.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
entries: List of trend snapshots (one per day)
|
|
56
|
+
last_updated: ISO timestamp of most recent update
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
entries: list[TrendEntry] = field(default_factory=list)
|
|
60
|
+
last_updated: str | None = None
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
"""Convert to dictionary for JSON serialization."""
|
|
64
|
+
return {
|
|
65
|
+
"entries": [entry.to_dict() for entry in self.entries],
|
|
66
|
+
"last_updated": self.last_updated,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict[str, Any]) -> TrendData:
|
|
71
|
+
"""Create from dictionary."""
|
|
72
|
+
return cls(
|
|
73
|
+
entries=[TrendEntry.from_dict(e) for e in data.get("entries", [])],
|
|
74
|
+
last_updated=data.get("last_updated"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TrendTracker:
|
|
79
|
+
"""Track code metrics over time with daily snapshots.
|
|
80
|
+
|
|
81
|
+
Features:
|
|
82
|
+
- One entry per day (updates existing if reindexed same day)
|
|
83
|
+
- Stores key metrics: files, chunks, lines, complexity, health
|
|
84
|
+
- JSON file storage in .mcp-vector-search/trends.json
|
|
85
|
+
- History retrieval for time series analysis
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, project_root: Path) -> None:
|
|
89
|
+
"""Initialize trend tracker.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
project_root: Root directory of the project
|
|
93
|
+
"""
|
|
94
|
+
self.project_root = project_root
|
|
95
|
+
self.trends_file = project_root / ".mcp-vector-search" / "trends.json"
|
|
96
|
+
self._ensure_directory()
|
|
97
|
+
|
|
98
|
+
def _ensure_directory(self) -> None:
|
|
99
|
+
"""Ensure .mcp-vector-search directory exists."""
|
|
100
|
+
self.trends_file.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
def load_trends(self) -> TrendData:
|
|
103
|
+
"""Load existing trends from file.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
TrendData with existing entries, or empty if file doesn't exist
|
|
107
|
+
"""
|
|
108
|
+
if not self.trends_file.exists():
|
|
109
|
+
logger.debug("No trends file found, returning empty TrendData")
|
|
110
|
+
return TrendData()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
with open(self.trends_file, encoding="utf-8") as f:
|
|
114
|
+
data = json.load(f)
|
|
115
|
+
return TrendData.from_dict(data)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Failed to load trends file: {e}")
|
|
118
|
+
return TrendData()
|
|
119
|
+
|
|
120
|
+
def save_trends(self, trends: TrendData) -> None:
|
|
121
|
+
"""Save trends to file.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
trends: TrendData to save
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
self._ensure_directory()
|
|
128
|
+
with open(self.trends_file, "w", encoding="utf-8") as f:
|
|
129
|
+
json.dump(trends.to_dict(), f, indent=2)
|
|
130
|
+
logger.debug(f"Saved trends to {self.trends_file}")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to save trends file: {e}")
|
|
133
|
+
|
|
134
|
+
def save_snapshot(self, metrics: dict[str, Any]) -> None:
|
|
135
|
+
"""Save or update today's metrics snapshot.
|
|
136
|
+
|
|
137
|
+
If an entry already exists for today, it's replaced (reindex case).
|
|
138
|
+
Otherwise, a new entry is appended.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
metrics: Dictionary of metrics to store
|
|
142
|
+
"""
|
|
143
|
+
trends = self.load_trends()
|
|
144
|
+
|
|
145
|
+
# Get current date and timestamp
|
|
146
|
+
now = datetime.now(UTC)
|
|
147
|
+
today_date = now.date().isoformat() # YYYY-MM-DD
|
|
148
|
+
timestamp = now.isoformat()
|
|
149
|
+
|
|
150
|
+
# Check if entry already exists for today
|
|
151
|
+
existing_index = None
|
|
152
|
+
for i, entry in enumerate(trends.entries):
|
|
153
|
+
if entry.date == today_date:
|
|
154
|
+
existing_index = i
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Create new entry
|
|
158
|
+
new_entry = TrendEntry(
|
|
159
|
+
date=today_date,
|
|
160
|
+
timestamp=timestamp,
|
|
161
|
+
metrics=metrics,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Replace existing or append new
|
|
165
|
+
if existing_index is not None:
|
|
166
|
+
logger.info(f"Updating existing trend entry for {today_date}")
|
|
167
|
+
trends.entries[existing_index] = new_entry
|
|
168
|
+
else:
|
|
169
|
+
logger.info(f"Creating new trend entry for {today_date}")
|
|
170
|
+
trends.entries.append(new_entry)
|
|
171
|
+
|
|
172
|
+
# Sort entries by date (oldest first)
|
|
173
|
+
trends.entries.sort(key=lambda e: e.date)
|
|
174
|
+
|
|
175
|
+
# Update last_updated timestamp
|
|
176
|
+
trends.last_updated = timestamp
|
|
177
|
+
|
|
178
|
+
# Save to file
|
|
179
|
+
self.save_trends(trends)
|
|
180
|
+
logger.info(
|
|
181
|
+
f"Saved trend snapshot with {len(metrics)} metrics for {today_date}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def get_history(self, days: int = 30) -> list[TrendEntry]:
|
|
185
|
+
"""Get recent trend history.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
days: Number of days to retrieve (default: 30)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of TrendEntry objects for the last N days
|
|
192
|
+
"""
|
|
193
|
+
trends = self.load_trends()
|
|
194
|
+
|
|
195
|
+
# Return last N entries
|
|
196
|
+
return trends.entries[-days:] if days > 0 else trends.entries
|
|
197
|
+
|
|
198
|
+
def get_trend_summary(self, days: int = 30) -> dict[str, Any]:
|
|
199
|
+
"""Get summary of trends for visualization.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
days: Number of days to include
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dictionary with trend summary data
|
|
206
|
+
"""
|
|
207
|
+
history = self.get_history(days)
|
|
208
|
+
|
|
209
|
+
if not history:
|
|
210
|
+
return {
|
|
211
|
+
"days": days,
|
|
212
|
+
"entries_count": 0,
|
|
213
|
+
"date_range": None,
|
|
214
|
+
"entries": [],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"days": days,
|
|
219
|
+
"entries_count": len(history),
|
|
220
|
+
"date_range": {
|
|
221
|
+
"start": history[0].date,
|
|
222
|
+
"end": history[-1].date,
|
|
223
|
+
},
|
|
224
|
+
"entries": [entry.to_dict() for entry in history],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
def compute_metrics_from_stats(
|
|
228
|
+
self, stats: dict[str, Any], chunks: list[Any] | None = None
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Compute metrics dictionary from database stats and chunks.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
stats: Database statistics (from database.get_stats())
|
|
234
|
+
chunks: Optional list of chunks for detailed metrics
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dictionary of metrics suitable for save_snapshot()
|
|
238
|
+
"""
|
|
239
|
+
metrics = {
|
|
240
|
+
"total_files": stats.get("total_files", 0),
|
|
241
|
+
"total_chunks": stats.get("total_chunks", 0),
|
|
242
|
+
"total_lines": 0, # Computed from chunks if available
|
|
243
|
+
"avg_complexity": 0.0,
|
|
244
|
+
"max_complexity": 0,
|
|
245
|
+
"health_score": 0,
|
|
246
|
+
"code_smells_count": 0,
|
|
247
|
+
"high_complexity_files": 0,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Compute detailed metrics from chunks if provided
|
|
251
|
+
if chunks:
|
|
252
|
+
total_lines = 0
|
|
253
|
+
complexities = []
|
|
254
|
+
smell_counts = 0
|
|
255
|
+
high_complexity_count = 0
|
|
256
|
+
|
|
257
|
+
for chunk in chunks:
|
|
258
|
+
# Lines of code
|
|
259
|
+
if hasattr(chunk, "lines_of_code") and chunk.lines_of_code:
|
|
260
|
+
total_lines += chunk.lines_of_code
|
|
261
|
+
else:
|
|
262
|
+
# Fallback: estimate from line range
|
|
263
|
+
total_lines += chunk.end_line - chunk.start_line + 1
|
|
264
|
+
|
|
265
|
+
# Complexity
|
|
266
|
+
if (
|
|
267
|
+
hasattr(chunk, "cognitive_complexity")
|
|
268
|
+
and chunk.cognitive_complexity
|
|
269
|
+
):
|
|
270
|
+
complexities.append(chunk.cognitive_complexity)
|
|
271
|
+
# High complexity = cognitive > 20
|
|
272
|
+
if chunk.cognitive_complexity > 20:
|
|
273
|
+
high_complexity_count += 1
|
|
274
|
+
|
|
275
|
+
# Code smells
|
|
276
|
+
if hasattr(chunk, "smell_count") and chunk.smell_count:
|
|
277
|
+
smell_counts += chunk.smell_count
|
|
278
|
+
|
|
279
|
+
metrics["total_lines"] = total_lines
|
|
280
|
+
|
|
281
|
+
if complexities:
|
|
282
|
+
metrics["avg_complexity"] = sum(complexities) / len(complexities)
|
|
283
|
+
metrics["max_complexity"] = max(complexities)
|
|
284
|
+
|
|
285
|
+
metrics["code_smells_count"] = smell_counts
|
|
286
|
+
metrics["high_complexity_files"] = high_complexity_count
|
|
287
|
+
|
|
288
|
+
# Compute health score (0-100)
|
|
289
|
+
# Formula: Base 100, penalty for complexity and smells
|
|
290
|
+
health = 100.0
|
|
291
|
+
|
|
292
|
+
# Penalty for average complexity
|
|
293
|
+
if metrics["avg_complexity"] > 30:
|
|
294
|
+
health -= 50
|
|
295
|
+
elif metrics["avg_complexity"] > 20:
|
|
296
|
+
health -= 30
|
|
297
|
+
elif metrics["avg_complexity"] > 10:
|
|
298
|
+
health -= 20
|
|
299
|
+
elif metrics["avg_complexity"] > 5:
|
|
300
|
+
health -= 10
|
|
301
|
+
|
|
302
|
+
# Penalty for code smells (5 points per smell, max 30 points)
|
|
303
|
+
smell_penalty = min(30, smell_counts * 5)
|
|
304
|
+
health -= smell_penalty
|
|
305
|
+
|
|
306
|
+
metrics["health_score"] = max(0, int(health))
|
|
307
|
+
|
|
308
|
+
return metrics
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Visualization and export schemas for code analysis results.
|
|
2
|
+
|
|
3
|
+
This module provides the JSON export format for structural code analysis,
|
|
4
|
+
enabling integration with visualization tools and external analysis platforms.
|
|
5
|
+
|
|
6
|
+
The export schema is versioned and designed to be stable across tool updates,
|
|
7
|
+
with support for:
|
|
8
|
+
- Complete metric snapshots (complexity, coupling, smells)
|
|
9
|
+
- Dependency graph visualization
|
|
10
|
+
- Historical trend tracking
|
|
11
|
+
- Git-aware context for time-series analysis
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from mcp_vector_search.analysis.visualizer.schemas import (
|
|
15
|
+
... AnalysisExport,
|
|
16
|
+
... ExportMetadata,
|
|
17
|
+
... MetricsSummary,
|
|
18
|
+
... generate_json_schema
|
|
19
|
+
... )
|
|
20
|
+
>>> from datetime import datetime
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Create export
|
|
23
|
+
>>> export = AnalysisExport(
|
|
24
|
+
... metadata=ExportMetadata(
|
|
25
|
+
... version="1.0.0",
|
|
26
|
+
... generated_at=datetime.now(),
|
|
27
|
+
... tool_version="0.19.0",
|
|
28
|
+
... project_root="/path/to/project"
|
|
29
|
+
... ),
|
|
30
|
+
... summary=MetricsSummary(...),
|
|
31
|
+
... files=[],
|
|
32
|
+
... dependencies=DependencyGraph(edges=[], circular_dependencies=[])
|
|
33
|
+
... )
|
|
34
|
+
>>>
|
|
35
|
+
>>> # Export to JSON
|
|
36
|
+
>>> json_data = export.model_dump_json(indent=2)
|
|
37
|
+
>>>
|
|
38
|
+
>>> # Generate schema for documentation
|
|
39
|
+
>>> schema = generate_json_schema()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from .d3_data import D3Edge, D3Node, transform_for_d3
|
|
43
|
+
from .exporter import JSONExporter
|
|
44
|
+
from .html_report import HTMLReportGenerator
|
|
45
|
+
from .schemas import (
|
|
46
|
+
AnalysisExport,
|
|
47
|
+
ClassMetrics,
|
|
48
|
+
CyclicDependency,
|
|
49
|
+
DependencyEdge,
|
|
50
|
+
DependencyGraph,
|
|
51
|
+
ExportMetadata,
|
|
52
|
+
FileDetail,
|
|
53
|
+
FunctionMetrics,
|
|
54
|
+
MetricsSummary,
|
|
55
|
+
MetricTrend,
|
|
56
|
+
SmellLocation,
|
|
57
|
+
TrendData,
|
|
58
|
+
TrendDataPoint,
|
|
59
|
+
generate_json_schema,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
# Exporters
|
|
64
|
+
"JSONExporter",
|
|
65
|
+
"HTMLReportGenerator",
|
|
66
|
+
# D3 visualization
|
|
67
|
+
"D3Node",
|
|
68
|
+
"D3Edge",
|
|
69
|
+
"transform_for_d3",
|
|
70
|
+
# Main export schema
|
|
71
|
+
"AnalysisExport",
|
|
72
|
+
# Metadata and summary
|
|
73
|
+
"ExportMetadata",
|
|
74
|
+
"MetricsSummary",
|
|
75
|
+
# File-level schemas
|
|
76
|
+
"FileDetail",
|
|
77
|
+
"FunctionMetrics",
|
|
78
|
+
"ClassMetrics",
|
|
79
|
+
"SmellLocation",
|
|
80
|
+
# Dependency analysis
|
|
81
|
+
"DependencyGraph",
|
|
82
|
+
"DependencyEdge",
|
|
83
|
+
"CyclicDependency",
|
|
84
|
+
# Trend tracking
|
|
85
|
+
"TrendData",
|
|
86
|
+
"MetricTrend",
|
|
87
|
+
"TrendDataPoint",
|
|
88
|
+
# Utilities
|
|
89
|
+
"generate_json_schema",
|
|
90
|
+
]
|