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,428 @@
|
|
|
1
|
+
"""State management system for visualization V2.0.
|
|
2
|
+
|
|
3
|
+
This module implements the hierarchical list-based navigation state,
|
|
4
|
+
including expansion paths, node visibility, and layout modes.
|
|
5
|
+
|
|
6
|
+
Design Principles:
|
|
7
|
+
- Sibling Exclusivity: Only one child expanded per depth level
|
|
8
|
+
- List/Fan Modes: Root list view vs. horizontal fan expansion
|
|
9
|
+
- AST-Only Edges: Show only function calls within expanded files
|
|
10
|
+
- Explicit State: No implicit behavior, all state transitions documented
|
|
11
|
+
|
|
12
|
+
Reference: docs/development/VISUALIZATION_ARCHITECTURE_V2.md
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ViewMode(str, Enum):
|
|
24
|
+
"""View mode for visualization layout.
|
|
25
|
+
|
|
26
|
+
Design Decision: Tree-based view modes for file explorer metaphor
|
|
27
|
+
|
|
28
|
+
Rationale: Replaced fan-based modes with tree-based modes to match
|
|
29
|
+
traditional file system navigation (Finder, Explorer). Tree modes
|
|
30
|
+
provide clearer hierarchical relationships and familiar UX patterns.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
TREE_ROOT: Root list view (vertical alphabetical list, no edges)
|
|
34
|
+
TREE_EXPANDED: Tree with expanded directories (rightward expansion)
|
|
35
|
+
FILE_DETAIL: File with AST chunks (function call edges visible)
|
|
36
|
+
|
|
37
|
+
Migration from V1:
|
|
38
|
+
- LIST → TREE_ROOT (same behavior, clearer naming)
|
|
39
|
+
- DIRECTORY_FAN → TREE_EXPANDED (fan arc → tree levels)
|
|
40
|
+
- FILE_FAN → FILE_DETAIL (fan arc → rightward tree + edges)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
TREE_ROOT = "tree_root" # Vertical list of root nodes, no edges
|
|
44
|
+
TREE_EXPANDED = "tree_expanded" # Tree with expanded nodes, hierarchical edges
|
|
45
|
+
FILE_DETAIL = "file_detail" # File with AST chunks and call edges
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class NodeState:
|
|
50
|
+
"""State of a single node in the visualization.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
node_id: Unique identifier for the node
|
|
54
|
+
expanded: Whether this node's children are visible
|
|
55
|
+
visible: Whether this node is currently visible
|
|
56
|
+
children_visible: Whether this node's children are visible
|
|
57
|
+
position_override: Optional fixed position for layout
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
node_id: str
|
|
61
|
+
expanded: bool = False
|
|
62
|
+
visible: bool = True
|
|
63
|
+
children_visible: bool = False
|
|
64
|
+
position_override: tuple[float, float] | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class VisualizationState:
|
|
69
|
+
"""Core state manager for visualization V2.0.
|
|
70
|
+
|
|
71
|
+
Manages expansion paths, node visibility, and layout modes using
|
|
72
|
+
explicit state transitions. Enforces sibling exclusivity and
|
|
73
|
+
AST-only edge filtering.
|
|
74
|
+
|
|
75
|
+
Design Decision: Sibling Exclusivity
|
|
76
|
+
When expanding a node at depth D, any previously expanded sibling
|
|
77
|
+
at depth D is automatically collapsed. This reduces visual clutter
|
|
78
|
+
and maintains a single focused path through the hierarchy.
|
|
79
|
+
|
|
80
|
+
Trade-offs:
|
|
81
|
+
- Simplicity: Only one path visible at a time
|
|
82
|
+
- Focus: Clear navigation context
|
|
83
|
+
- Limitation: Cannot compare siblings side-by-side
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
view_mode: Current view mode (tree_root, tree_expanded, file_detail)
|
|
87
|
+
expansion_path: Ordered list of expanded node IDs (root to current)
|
|
88
|
+
node_states: Map of node ID to NodeState
|
|
89
|
+
visible_edges: Set of visible AST call edges (source_id, target_id)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
view_mode: ViewMode = ViewMode.TREE_ROOT
|
|
93
|
+
expansion_path: list[str] = field(default_factory=list)
|
|
94
|
+
node_states: dict[str, NodeState] = field(default_factory=dict)
|
|
95
|
+
visible_edges: set[tuple[str, str]] = field(default_factory=set)
|
|
96
|
+
|
|
97
|
+
def _get_or_create_state(self, node_id: str) -> NodeState:
|
|
98
|
+
"""Get or create node state.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
node_id: Node identifier
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
NodeState for the given node
|
|
105
|
+
"""
|
|
106
|
+
if node_id not in self.node_states:
|
|
107
|
+
self.node_states[node_id] = NodeState(node_id=node_id)
|
|
108
|
+
return self.node_states[node_id]
|
|
109
|
+
|
|
110
|
+
def expand_node(
|
|
111
|
+
self,
|
|
112
|
+
node_id: str,
|
|
113
|
+
node_type: str,
|
|
114
|
+
children: list[str],
|
|
115
|
+
parent_id: str | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Expand a node (directory or file).
|
|
118
|
+
|
|
119
|
+
When expanding a node:
|
|
120
|
+
1. Check if a sibling at same depth is already expanded
|
|
121
|
+
2. If yes, collapse that sibling first (sibling exclusivity)
|
|
122
|
+
3. Expand this node and show its children
|
|
123
|
+
4. Update view mode based on node type
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
node_id: ID of node to expand
|
|
127
|
+
node_type: Type of node ("directory" or "file")
|
|
128
|
+
children: List of child node IDs
|
|
129
|
+
parent_id: Optional parent node ID (for depth calculation)
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If node_type is not "directory" or "file"
|
|
133
|
+
"""
|
|
134
|
+
if node_type not in ("directory", "file"):
|
|
135
|
+
raise ValueError(f"Cannot expand node type: {node_type}")
|
|
136
|
+
|
|
137
|
+
logger.debug(
|
|
138
|
+
f"Expanding {node_type} node: {node_id} with {len(children)} children"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Get node state
|
|
142
|
+
node_state = self._get_or_create_state(node_id)
|
|
143
|
+
|
|
144
|
+
# Calculate depth (distance from root)
|
|
145
|
+
if parent_id and parent_id in self.expansion_path:
|
|
146
|
+
# If parent is in path, depth is parent_index + 1
|
|
147
|
+
parent_index = self.expansion_path.index(parent_id)
|
|
148
|
+
depth = parent_index + 1
|
|
149
|
+
else:
|
|
150
|
+
# No parent or parent not in path: this is a root-level sibling
|
|
151
|
+
depth = 0
|
|
152
|
+
|
|
153
|
+
# Sibling exclusivity: Check if another sibling is expanded at this depth
|
|
154
|
+
if depth < len(self.expansion_path):
|
|
155
|
+
# There's already an expanded node at this depth
|
|
156
|
+
old_sibling = self.expansion_path[depth]
|
|
157
|
+
if old_sibling != node_id:
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Sibling exclusivity: Collapsing {old_sibling} "
|
|
160
|
+
f"before expanding {node_id}"
|
|
161
|
+
)
|
|
162
|
+
# Collapse old path from this depth onward
|
|
163
|
+
nodes_to_collapse = self.expansion_path[depth:]
|
|
164
|
+
self.expansion_path = self.expansion_path[:depth]
|
|
165
|
+
for old_node in nodes_to_collapse:
|
|
166
|
+
self._collapse_node_internal(old_node)
|
|
167
|
+
|
|
168
|
+
# Mark node as expanded
|
|
169
|
+
node_state.expanded = True
|
|
170
|
+
node_state.children_visible = True
|
|
171
|
+
|
|
172
|
+
# Add to expansion path
|
|
173
|
+
if node_id not in self.expansion_path:
|
|
174
|
+
self.expansion_path.append(node_id)
|
|
175
|
+
|
|
176
|
+
# Make children visible
|
|
177
|
+
for child_id in children:
|
|
178
|
+
child_state = self._get_or_create_state(child_id)
|
|
179
|
+
child_state.visible = True
|
|
180
|
+
|
|
181
|
+
# Update view mode based on node type
|
|
182
|
+
if node_type == "directory":
|
|
183
|
+
self.view_mode = ViewMode.TREE_EXPANDED
|
|
184
|
+
elif node_type == "file":
|
|
185
|
+
self.view_mode = ViewMode.FILE_DETAIL
|
|
186
|
+
|
|
187
|
+
logger.info(
|
|
188
|
+
f"Expanded {node_type} {node_id}, "
|
|
189
|
+
f"path: {' > '.join(self.expansion_path)}, "
|
|
190
|
+
f"mode: {self.view_mode.value}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _collapse_node_internal(self, node_id: str) -> None:
|
|
194
|
+
"""Internal collapse without path manipulation.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
node_id: Node to collapse
|
|
198
|
+
"""
|
|
199
|
+
node_state = self.node_states.get(node_id)
|
|
200
|
+
if not node_state:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Mark as collapsed
|
|
204
|
+
node_state.expanded = False
|
|
205
|
+
node_state.children_visible = False
|
|
206
|
+
|
|
207
|
+
logger.debug(f"Collapsed node: {node_id}")
|
|
208
|
+
|
|
209
|
+
def collapse_node(self, node_id: str, all_nodes: dict[str, dict]) -> None:
|
|
210
|
+
"""Collapse a node and hide all its descendants.
|
|
211
|
+
|
|
212
|
+
Recursively hides all descendants of the collapsed node.
|
|
213
|
+
If the expansion path becomes empty, revert to LIST view.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
node_id: ID of node to collapse
|
|
217
|
+
all_nodes: Dictionary of all nodes (id -> node_data) for traversal
|
|
218
|
+
|
|
219
|
+
Performance:
|
|
220
|
+
Time Complexity: O(d) where d = number of descendants
|
|
221
|
+
Space Complexity: O(d) for recursion stack
|
|
222
|
+
"""
|
|
223
|
+
logger.debug(f"Collapsing node: {node_id}")
|
|
224
|
+
|
|
225
|
+
# Remove from expansion path
|
|
226
|
+
if node_id in self.expansion_path:
|
|
227
|
+
path_index = self.expansion_path.index(node_id)
|
|
228
|
+
# Also remove all descendants in path
|
|
229
|
+
self.expansion_path = self.expansion_path[:path_index]
|
|
230
|
+
|
|
231
|
+
# Mark node as collapsed
|
|
232
|
+
self._collapse_node_internal(node_id)
|
|
233
|
+
|
|
234
|
+
# Hide all descendants recursively
|
|
235
|
+
def hide_descendants(parent_id: str) -> None:
|
|
236
|
+
"""Recursively hide all descendants.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
parent_id: Parent node ID
|
|
240
|
+
"""
|
|
241
|
+
# Find children
|
|
242
|
+
node_data = all_nodes.get(parent_id)
|
|
243
|
+
if not node_data:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Get children IDs (implementation depends on node structure)
|
|
247
|
+
# This is a placeholder - actual implementation needs to find children
|
|
248
|
+
children = [] # TODO: Extract from node data or links
|
|
249
|
+
|
|
250
|
+
for child_id in children:
|
|
251
|
+
child_state = self.node_states.get(child_id)
|
|
252
|
+
if child_state:
|
|
253
|
+
child_state.visible = False
|
|
254
|
+
child_state.expanded = False
|
|
255
|
+
child_state.children_visible = False
|
|
256
|
+
|
|
257
|
+
# Recurse
|
|
258
|
+
hide_descendants(child_id)
|
|
259
|
+
|
|
260
|
+
hide_descendants(node_id)
|
|
261
|
+
|
|
262
|
+
# Update view mode if path is empty
|
|
263
|
+
if len(self.expansion_path) == 0:
|
|
264
|
+
self.view_mode = ViewMode.TREE_ROOT
|
|
265
|
+
logger.info("Collapsed to root, switching to TREE_ROOT view")
|
|
266
|
+
|
|
267
|
+
def switch_sibling(
|
|
268
|
+
self,
|
|
269
|
+
old_node_id: str,
|
|
270
|
+
new_node_id: str,
|
|
271
|
+
new_node_type: str,
|
|
272
|
+
new_children: list[str],
|
|
273
|
+
all_nodes: dict[str, dict],
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Close old sibling path, open new sibling path.
|
|
276
|
+
|
|
277
|
+
This is a convenience method that combines collapse and expand
|
|
278
|
+
for switching between siblings at the same depth.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
old_node_id: ID of currently expanded sibling
|
|
282
|
+
new_node_id: ID of sibling to expand
|
|
283
|
+
new_node_type: Type of new node ("directory" or "file")
|
|
284
|
+
new_children: Children of new node
|
|
285
|
+
all_nodes: Dictionary of all nodes for traversal
|
|
286
|
+
"""
|
|
287
|
+
logger.debug(f"Switching sibling: {old_node_id} → {new_node_id}")
|
|
288
|
+
|
|
289
|
+
# Collapse old sibling
|
|
290
|
+
self.collapse_node(old_node_id, all_nodes)
|
|
291
|
+
|
|
292
|
+
# Expand new sibling
|
|
293
|
+
self.expand_node(new_node_id, new_node_type, new_children)
|
|
294
|
+
|
|
295
|
+
def get_visible_nodes(self) -> list[str]:
|
|
296
|
+
"""Return list of currently visible node IDs.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of node IDs that should be rendered
|
|
300
|
+
"""
|
|
301
|
+
visible = []
|
|
302
|
+
for node_id, state in self.node_states.items():
|
|
303
|
+
if state.visible:
|
|
304
|
+
visible.append(node_id)
|
|
305
|
+
return visible
|
|
306
|
+
|
|
307
|
+
def get_visible_edges(
|
|
308
|
+
self, all_edges: list[dict], expanded_file_id: str | None = None
|
|
309
|
+
) -> set[tuple[str, str]]:
|
|
310
|
+
"""Return set of visible edges (AST calls only).
|
|
311
|
+
|
|
312
|
+
Edge Filtering Rules:
|
|
313
|
+
- TREE_ROOT mode: No edges shown
|
|
314
|
+
- TREE_EXPANDED mode: No edges shown
|
|
315
|
+
- FILE_DETAIL mode: Only AST call edges within expanded file
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
all_edges: List of all edge dictionaries
|
|
319
|
+
expanded_file_id: ID of currently expanded file (if in FILE_DETAIL mode)
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Set of (source_id, target_id) tuples for visible edges
|
|
323
|
+
"""
|
|
324
|
+
if self.view_mode != ViewMode.FILE_DETAIL or not expanded_file_id:
|
|
325
|
+
# No edges in TREE_ROOT or TREE_EXPANDED modes
|
|
326
|
+
return set()
|
|
327
|
+
|
|
328
|
+
visible = set()
|
|
329
|
+
for edge in all_edges:
|
|
330
|
+
# Only show "caller" type edges (function calls)
|
|
331
|
+
if edge.get("type") != "caller":
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
source_id = edge.get("source")
|
|
335
|
+
target_id = edge.get("target")
|
|
336
|
+
|
|
337
|
+
if not source_id or not target_id:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Both nodes must be visible
|
|
341
|
+
source_state = self.node_states.get(source_id)
|
|
342
|
+
target_state = self.node_states.get(target_id)
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
source_state
|
|
346
|
+
and target_state
|
|
347
|
+
and source_state.visible
|
|
348
|
+
and target_state.visible
|
|
349
|
+
):
|
|
350
|
+
visible.add((source_id, target_id))
|
|
351
|
+
|
|
352
|
+
return visible
|
|
353
|
+
|
|
354
|
+
def to_dict(self) -> dict:
|
|
355
|
+
"""Serialize state for JavaScript.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Dictionary representation suitable for JSON serialization
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
{
|
|
362
|
+
"view_mode": "directory_fan",
|
|
363
|
+
"expansion_path": ["dir1", "dir2"],
|
|
364
|
+
"visible_nodes": ["node1", "node2", "node3"],
|
|
365
|
+
"visible_edges": [["func1", "func2"], ["func2", "func3"]]
|
|
366
|
+
}
|
|
367
|
+
"""
|
|
368
|
+
return {
|
|
369
|
+
"view_mode": self.view_mode.value,
|
|
370
|
+
"expansion_path": self.expansion_path.copy(),
|
|
371
|
+
"visible_nodes": self.get_visible_nodes(),
|
|
372
|
+
"visible_edges": [list(edge) for edge in self.visible_edges],
|
|
373
|
+
"node_states": {
|
|
374
|
+
node_id: {
|
|
375
|
+
"expanded": state.expanded,
|
|
376
|
+
"visible": state.visible,
|
|
377
|
+
"children_visible": state.children_visible,
|
|
378
|
+
"position_override": state.position_override,
|
|
379
|
+
}
|
|
380
|
+
for node_id, state in self.node_states.items()
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def from_dict(cls, data: dict) -> VisualizationState:
|
|
386
|
+
"""Deserialize state from JavaScript.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
data: Dictionary from JavaScript state (via JSON)
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Reconstructed VisualizationState instance
|
|
393
|
+
"""
|
|
394
|
+
state = cls()
|
|
395
|
+
|
|
396
|
+
# Handle view mode with backward compatibility
|
|
397
|
+
view_mode_str = data.get("view_mode", "tree_root")
|
|
398
|
+
|
|
399
|
+
# Map old view modes to new ones
|
|
400
|
+
view_mode_migration = {
|
|
401
|
+
"list": "tree_root",
|
|
402
|
+
"directory_fan": "tree_expanded",
|
|
403
|
+
"file_fan": "file_detail",
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Apply migration if needed
|
|
407
|
+
if view_mode_str in view_mode_migration:
|
|
408
|
+
view_mode_str = view_mode_migration[view_mode_str]
|
|
409
|
+
|
|
410
|
+
state.view_mode = ViewMode(view_mode_str)
|
|
411
|
+
state.expansion_path = data.get("expansion_path", [])
|
|
412
|
+
|
|
413
|
+
# Reconstruct node states
|
|
414
|
+
node_states_data = data.get("node_states", {})
|
|
415
|
+
for node_id, node_data in node_states_data.items():
|
|
416
|
+
state.node_states[node_id] = NodeState(
|
|
417
|
+
node_id=node_id,
|
|
418
|
+
expanded=node_data.get("expanded", False),
|
|
419
|
+
visible=node_data.get("visible", True),
|
|
420
|
+
children_visible=node_data.get("children_visible", False),
|
|
421
|
+
position_override=node_data.get("position_override"),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Reconstruct visible edges
|
|
425
|
+
visible_edges_data = data.get("visible_edges", [])
|
|
426
|
+
state.visible_edges = {tuple(edge) for edge in visible_edges_data}
|
|
427
|
+
|
|
428
|
+
return state
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Templates for visualization HTML generation.
|
|
2
|
+
|
|
3
|
+
This package contains modular template components for generating
|
|
4
|
+
the D3.js visualization HTML page.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import generate_html_template, inject_data
|
|
8
|
+
from .scripts import get_all_scripts
|
|
9
|
+
from .styles import get_all_styles
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"generate_html_template",
|
|
13
|
+
"inject_data",
|
|
14
|
+
"get_all_scripts",
|
|
15
|
+
"get_all_styles",
|
|
16
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""HTML template generation for the visualization.
|
|
2
|
+
|
|
3
|
+
This module combines CSS and JavaScript from other template modules
|
|
4
|
+
to generate the complete HTML page for the D3.js visualization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from mcp_vector_search import __build__, __version__
|
|
10
|
+
|
|
11
|
+
from .scripts import get_all_scripts
|
|
12
|
+
from .styles import get_all_styles
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_html_template() -> str:
|
|
16
|
+
"""Generate the complete HTML template for visualization.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Complete HTML string with embedded CSS and JavaScript
|
|
20
|
+
"""
|
|
21
|
+
# Add timestamp for cache busting
|
|
22
|
+
build_timestamp = int(time.time())
|
|
23
|
+
|
|
24
|
+
html = f"""<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<title>Code Chunk Relationship Graph</title>
|
|
29
|
+
<meta http-cache="no-cache, no-store, must-revalidate">
|
|
30
|
+
<meta http-pragma="no-cache">
|
|
31
|
+
<meta http-expires="0">
|
|
32
|
+
<!-- Build: {build_timestamp} -->
|
|
33
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
34
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
35
|
+
<script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
|
|
36
|
+
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
37
|
+
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
|
38
|
+
<!-- Highlight.js for syntax highlighting -->
|
|
39
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
40
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
41
|
+
<style>
|
|
42
|
+
{get_all_styles()}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="controls">
|
|
47
|
+
<h1>🔍 Code Tree</h1>
|
|
48
|
+
<div class="version-badge">v{__version__} (build {__build__})</div>
|
|
49
|
+
|
|
50
|
+
<div class="control-group">
|
|
51
|
+
<label style="color: var(--text-primary); margin-bottom: 8px;">Layout Mode</label>
|
|
52
|
+
<div class="toggle-switch-container">
|
|
53
|
+
<span class="toggle-label">Linear</span>
|
|
54
|
+
<label class="toggle-switch">
|
|
55
|
+
<input type="checkbox" id="layout-toggle" onchange="toggleLayout()">
|
|
56
|
+
<span class="toggle-slider"></span>
|
|
57
|
+
</label>
|
|
58
|
+
<span class="toggle-label">Circular</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="control-group">
|
|
63
|
+
<label style="color: var(--text-primary); margin-bottom: 8px;">Show Files</label>
|
|
64
|
+
<div class="filter-buttons">
|
|
65
|
+
<button class="filter-btn active" data-filter="all" onclick="setFileFilter('all')">All</button>
|
|
66
|
+
<button class="filter-btn" data-filter="code" onclick="setFileFilter('code')">Code</button>
|
|
67
|
+
<button class="filter-btn" data-filter="docs" onclick="setFileFilter('docs')">Docs</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<h3>Legend</h3>
|
|
72
|
+
<div class="legend">
|
|
73
|
+
<div class="legend-category">
|
|
74
|
+
<div class="legend-title">Node Types</div>
|
|
75
|
+
<div class="legend-item">
|
|
76
|
+
<svg width="16" height="16" style="margin-right: 8px;">
|
|
77
|
+
<circle cx="8" cy="8" r="6" fill="#3498db"/>
|
|
78
|
+
</svg>
|
|
79
|
+
<span>Directory (expanded)</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="legend-item">
|
|
82
|
+
<svg width="16" height="16" style="margin-right: 8px;">
|
|
83
|
+
<circle cx="8" cy="8" r="6" fill="#f39c12"/>
|
|
84
|
+
</svg>
|
|
85
|
+
<span>Directory (collapsed)</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="legend-item">
|
|
88
|
+
<svg width="16" height="16" style="margin-right: 8px;">
|
|
89
|
+
<circle cx="8" cy="8" r="6" fill="#95a5a6"/>
|
|
90
|
+
</svg>
|
|
91
|
+
<span>File</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="legend-category">
|
|
96
|
+
<div class="legend-title">Call Relationships</div>
|
|
97
|
+
<div class="legend-item">
|
|
98
|
+
<svg width="40" height="16" style="margin-right: 8px;">
|
|
99
|
+
<line x1="0" y1="8" x2="35" y2="8" stroke="#58a6ff" stroke-width="2" stroke-dasharray="4,2"/>
|
|
100
|
+
<text x="38" y="12" fill="#58a6ff" font-size="12">←</text>
|
|
101
|
+
</svg>
|
|
102
|
+
<span>Inbound calls (called by)</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="legend-item">
|
|
105
|
+
<svg width="40" height="16" style="margin-right: 8px;">
|
|
106
|
+
<line x1="0" y1="8" x2="35" y2="8" stroke="#f0883e" stroke-width="2" stroke-dasharray="4,2"/>
|
|
107
|
+
<text x="38" y="12" fill="#f0883e" font-size="12">→</text>
|
|
108
|
+
</svg>
|
|
109
|
+
<span>Outbound calls (calls to)</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="legend-item" style="margin-top: 8px;">
|
|
112
|
+
<label class="toggle-switch">
|
|
113
|
+
<input type="checkbox" id="show-call-lines" checked onchange="toggleCallLines(this.checked)">
|
|
114
|
+
<span class="toggle-slider"></span>
|
|
115
|
+
</label>
|
|
116
|
+
<span style="margin-left: 8px;">Show call lines</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="legend-category">
|
|
121
|
+
<div class="legend-title">Interactions</div>
|
|
122
|
+
<div class="legend-item" style="padding-left: 16px;">
|
|
123
|
+
<span>Click directory → expand/collapse</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="legend-item" style="padding-left: 16px;">
|
|
126
|
+
<span>Click file → view info</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="legend-item" style="padding-left: 16px;">
|
|
129
|
+
<span>Click chunk → view code</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="legend-item" style="padding-left: 16px;">
|
|
132
|
+
<span>Scroll → zoom in/out</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Search Section -->
|
|
138
|
+
<h3>🔎 Search</h3>
|
|
139
|
+
<div class="search-container">
|
|
140
|
+
<input type="text" id="search-input" placeholder="Search nodes..." oninput="handleSearchInput(event)" onkeydown="handleSearchKeydown(event)">
|
|
141
|
+
<div id="search-results" class="search-results"></div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Options Section -->
|
|
145
|
+
<h3>📋 Reports</h3>
|
|
146
|
+
<div class="legend" style="margin-top: 8px;">
|
|
147
|
+
<div class="legend-category" style="border-bottom: none;">
|
|
148
|
+
<div class="legend-item report-btn" onclick="showComplexityReport()">
|
|
149
|
+
<span class="report-icon">📊</span>
|
|
150
|
+
<span>Complexity</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="legend-item report-btn" onclick="showCodeSmells()">
|
|
153
|
+
<span class="report-icon">🔍</span>
|
|
154
|
+
<span>Code Smells</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="legend-item report-btn" onclick="showDependencies()">
|
|
157
|
+
<span class="report-icon">🔗</span>
|
|
158
|
+
<span>Dependencies</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="legend-item report-btn" onclick="showTrends()">
|
|
161
|
+
<span class="report-icon">📈</span>
|
|
162
|
+
<span>Trends</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="legend-item report-btn" onclick="generateRemediationReport()">
|
|
165
|
+
<span class="report-icon">📋</span>
|
|
166
|
+
<span>Remediation</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<h3 style="margin-top: 16px;">Options</h3>
|
|
172
|
+
<div class="legend" style="margin-top: 8px;">
|
|
173
|
+
<div class="legend-category" style="border-bottom: none;">
|
|
174
|
+
<!-- Theme Toggle -->
|
|
175
|
+
<div class="legend-item" style="margin-bottom: 12px; padding: 0;">
|
|
176
|
+
<button class="theme-toggle-icon-btn" onclick="toggleTheme()" title="Toggle dark/light theme">
|
|
177
|
+
<span class="theme-icon" id="theme-icon">🌙</span>
|
|
178
|
+
</button>
|
|
179
|
+
<span style="margin-left: 8px; color: var(--text-secondary); font-size: 12px;">Theme</span>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="stats" id="stats"></div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div id="main-container">
|
|
188
|
+
<svg id="graph"></svg>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div id="viewer-panel" class="viewer-panel">
|
|
192
|
+
<div class="viewer-header">
|
|
193
|
+
<div class="viewer-header-buttons">
|
|
194
|
+
<button class="viewer-expand-btn" onclick="toggleViewerExpand()" title="Expand/Collapse panel">
|
|
195
|
+
<span id="expand-icon">⬅</span>
|
|
196
|
+
</button>
|
|
197
|
+
<button class="viewer-close-btn" onclick="closeViewerPanel()" title="Close panel">×</button>
|
|
198
|
+
</div>
|
|
199
|
+
<h2 class="viewer-title" id="viewer-title">Viewer</h2>
|
|
200
|
+
<div class="section-nav" id="section-nav">
|
|
201
|
+
<select id="section-dropdown" onchange="jumpToSection(this.value)" title="Jump to section">
|
|
202
|
+
<option value="">Jump to section...</option>
|
|
203
|
+
</select>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="viewer-content" id="viewer-content">
|
|
207
|
+
<p style="color: #8b949e; text-align: center; padding: 40px;">Select a node to view details</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<script>
|
|
212
|
+
{get_all_scripts()}
|
|
213
|
+
</script>
|
|
214
|
+
</body>
|
|
215
|
+
</html>"""
|
|
216
|
+
return html
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def inject_data(html: str, data: dict) -> str:
|
|
220
|
+
"""Inject graph data into HTML template (not currently used for static export).
|
|
221
|
+
|
|
222
|
+
This function is provided for potential future use where data might be
|
|
223
|
+
embedded directly in the HTML rather than loaded from a separate JSON file.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
html: HTML template string
|
|
227
|
+
data: Graph data dictionary
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
HTML with embedded data
|
|
231
|
+
"""
|
|
232
|
+
# For now, we load data from external JSON file
|
|
233
|
+
# This function can be enhanced later if inline data embedding is needed
|
|
234
|
+
return html
|