mcp-vector-search 0.12.6__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 +111 -0
- 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 +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -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 +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -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 +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.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
|