mcp-vector-search 0.15.7__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.

Potentially problematic release.


This version of mcp-vector-search might be problematic. Click here for more details.

Files changed (86) hide show
  1. mcp_vector_search/__init__.py +10 -0
  2. mcp_vector_search/cli/__init__.py +1 -0
  3. mcp_vector_search/cli/commands/__init__.py +1 -0
  4. mcp_vector_search/cli/commands/auto_index.py +397 -0
  5. mcp_vector_search/cli/commands/chat.py +534 -0
  6. mcp_vector_search/cli/commands/config.py +393 -0
  7. mcp_vector_search/cli/commands/demo.py +358 -0
  8. mcp_vector_search/cli/commands/index.py +762 -0
  9. mcp_vector_search/cli/commands/init.py +658 -0
  10. mcp_vector_search/cli/commands/install.py +869 -0
  11. mcp_vector_search/cli/commands/install_old.py +700 -0
  12. mcp_vector_search/cli/commands/mcp.py +1254 -0
  13. mcp_vector_search/cli/commands/reset.py +393 -0
  14. mcp_vector_search/cli/commands/search.py +796 -0
  15. mcp_vector_search/cli/commands/setup.py +1133 -0
  16. mcp_vector_search/cli/commands/status.py +584 -0
  17. mcp_vector_search/cli/commands/uninstall.py +404 -0
  18. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  19. mcp_vector_search/cli/commands/visualize/cli.py +265 -0
  20. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  21. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  22. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
  23. mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
  24. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  25. mcp_vector_search/cli/commands/visualize/server.py +201 -0
  26. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  27. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  28. mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
  29. mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
  30. mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
  31. mcp_vector_search/cli/commands/visualize.py.original +2536 -0
  32. mcp_vector_search/cli/commands/watch.py +287 -0
  33. mcp_vector_search/cli/didyoumean.py +520 -0
  34. mcp_vector_search/cli/export.py +320 -0
  35. mcp_vector_search/cli/history.py +295 -0
  36. mcp_vector_search/cli/interactive.py +342 -0
  37. mcp_vector_search/cli/main.py +484 -0
  38. mcp_vector_search/cli/output.py +414 -0
  39. mcp_vector_search/cli/suggestions.py +375 -0
  40. mcp_vector_search/config/__init__.py +1 -0
  41. mcp_vector_search/config/constants.py +24 -0
  42. mcp_vector_search/config/defaults.py +200 -0
  43. mcp_vector_search/config/settings.py +146 -0
  44. mcp_vector_search/core/__init__.py +1 -0
  45. mcp_vector_search/core/auto_indexer.py +298 -0
  46. mcp_vector_search/core/config_utils.py +394 -0
  47. mcp_vector_search/core/connection_pool.py +360 -0
  48. mcp_vector_search/core/database.py +1237 -0
  49. mcp_vector_search/core/directory_index.py +318 -0
  50. mcp_vector_search/core/embeddings.py +294 -0
  51. mcp_vector_search/core/exceptions.py +89 -0
  52. mcp_vector_search/core/factory.py +318 -0
  53. mcp_vector_search/core/git_hooks.py +345 -0
  54. mcp_vector_search/core/indexer.py +1002 -0
  55. mcp_vector_search/core/llm_client.py +453 -0
  56. mcp_vector_search/core/models.py +294 -0
  57. mcp_vector_search/core/project.py +350 -0
  58. mcp_vector_search/core/scheduler.py +330 -0
  59. mcp_vector_search/core/search.py +952 -0
  60. mcp_vector_search/core/watcher.py +322 -0
  61. mcp_vector_search/mcp/__init__.py +5 -0
  62. mcp_vector_search/mcp/__main__.py +25 -0
  63. mcp_vector_search/mcp/server.py +752 -0
  64. mcp_vector_search/parsers/__init__.py +8 -0
  65. mcp_vector_search/parsers/base.py +296 -0
  66. mcp_vector_search/parsers/dart.py +605 -0
  67. mcp_vector_search/parsers/html.py +413 -0
  68. mcp_vector_search/parsers/javascript.py +643 -0
  69. mcp_vector_search/parsers/php.py +694 -0
  70. mcp_vector_search/parsers/python.py +502 -0
  71. mcp_vector_search/parsers/registry.py +223 -0
  72. mcp_vector_search/parsers/ruby.py +678 -0
  73. mcp_vector_search/parsers/text.py +186 -0
  74. mcp_vector_search/parsers/utils.py +265 -0
  75. mcp_vector_search/py.typed +1 -0
  76. mcp_vector_search/utils/__init__.py +42 -0
  77. mcp_vector_search/utils/gitignore.py +250 -0
  78. mcp_vector_search/utils/gitignore_updater.py +212 -0
  79. mcp_vector_search/utils/monorepo.py +339 -0
  80. mcp_vector_search/utils/timing.py +338 -0
  81. mcp_vector_search/utils/version.py +47 -0
  82. mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
  83. mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
  84. mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
  85. mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
  86. mcp_vector_search-0.15.7.dist-info/licenses/LICENSE +21 -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,218 @@
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 .scripts import get_all_scripts
10
+ from .styles import get_all_styles
11
+
12
+
13
+ def generate_html_template() -> str:
14
+ """Generate the complete HTML template for visualization.
15
+
16
+ Returns:
17
+ Complete HTML string with embedded CSS and JavaScript
18
+ """
19
+ # Add timestamp for cache busting
20
+ build_timestamp = int(time.time())
21
+
22
+ html = f"""<!DOCTYPE html>
23
+ <html>
24
+ <head>
25
+ <meta charset="utf-8">
26
+ <title>Code Chunk Relationship Graph</title>
27
+ <meta http-cache="no-cache, no-store, must-revalidate">
28
+ <meta http-pragma="no-cache">
29
+ <meta http-expires="0">
30
+ <!-- Build: {build_timestamp} -->
31
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
32
+ <script src="https://d3js.org/d3.v7.min.js"></script>
33
+ <script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
34
+ <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
35
+ <script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
36
+ <style>
37
+ {get_all_styles()}
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div id="controls">
42
+ <h1>🔍 Code Graph</h1>
43
+
44
+ <div class="control-group" id="loading">
45
+ <label>⏳ Loading graph data...</label>
46
+ </div>
47
+
48
+ <div class="control-group" id="layout-controls" style="display: none;">
49
+ <h3 style="margin: 12px 0 8px 0;">Layout</h3>
50
+ <select id="layoutSelector" style="width: 100%; padding: 6px; background: #161b22; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 12px;">
51
+ <option value="force">Force-Directed</option>
52
+ <option value="dagre">Hierarchical (Dagre)</option>
53
+ <option value="circle">Circular</option>
54
+ </select>
55
+ </div>
56
+
57
+ <div class="control-group" id="edge-filters" style="display: none;">
58
+ <h3 style="margin: 12px 0 8px 0;">Edge Filters</h3>
59
+ <div style="font-size: 12px;">
60
+ <label style="display: block; margin-bottom: 6px; cursor: pointer;">
61
+ <input type="checkbox" id="filter-containment" checked style="margin-right: 6px;">
62
+ Containment (dir/file)
63
+ </label>
64
+ <label style="display: block; margin-bottom: 6px; cursor: pointer;">
65
+ <input type="checkbox" id="filter-calls" checked style="margin-right: 6px;">
66
+ Function Calls
67
+ </label>
68
+ <label style="display: block; margin-bottom: 6px; cursor: pointer;">
69
+ <input type="checkbox" id="filter-imports" style="margin-right: 6px;">
70
+ Imports
71
+ </label>
72
+ <label style="display: block; margin-bottom: 6px; cursor: pointer;">
73
+ <input type="checkbox" id="filter-semantic" style="margin-right: 6px;">
74
+ Semantic Links
75
+ </label>
76
+ <label style="display: block; margin-bottom: 6px; cursor: pointer;">
77
+ <input type="checkbox" id="filter-cycles" checked style="margin-right: 6px;">
78
+ Circular Dependencies
79
+ </label>
80
+ </div>
81
+ </div>
82
+
83
+ <h3>Legend</h3>
84
+ <div class="legend">
85
+ <div class="legend-category">
86
+ <div class="legend-title">Code Elements</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="#d29922"/>
90
+ </svg>
91
+ <span>Function</span>
92
+ </div>
93
+ <div class="legend-item">
94
+ <svg width="16" height="16" style="margin-right: 8px;">
95
+ <circle cx="8" cy="8" r="6" fill="#1f6feb"/>
96
+ </svg>
97
+ <span>Class</span>
98
+ </div>
99
+ <div class="legend-item">
100
+ <svg width="16" height="16" style="margin-right: 8px;">
101
+ <circle cx="8" cy="8" r="6" fill="#8957e5"/>
102
+ </svg>
103
+ <span>Method</span>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="legend-category">
108
+ <div class="legend-title">File</div>
109
+ <div class="legend-item" style="padding-left: 16px;">
110
+ <span style="margin-right: 6px;">📄</span>
111
+ <span>.py (Python) 🐍</span>
112
+ </div>
113
+ <div class="legend-item" style="padding-left: 16px;">
114
+ <span style="margin-right: 6px;">📄</span>
115
+ <span>.js (JavaScript) 📜</span>
116
+ </div>
117
+ <div class="legend-item" style="padding-left: 16px;">
118
+ <span style="margin-right: 6px;">📄</span>
119
+ <span>.ts (TypeScript) 📜</span>
120
+ </div>
121
+ <div class="legend-item" style="padding-left: 16px;">
122
+ <span style="margin-right: 6px;">📄</span>
123
+ <span>.md (Markdown) 📝</span>
124
+ </div>
125
+ <div class="legend-item" style="padding-left: 16px;">
126
+ <span style="margin-right: 6px;">📄</span>
127
+ <span>.json (JSON) ⚙️</span>
128
+ </div>
129
+ <div class="legend-item" style="padding-left: 16px;">
130
+ <span style="margin-right: 6px;">📄</span>
131
+ <span>.yaml (YAML) ⚙️</span>
132
+ </div>
133
+ <div class="legend-item" style="padding-left: 16px;">
134
+ <span style="margin-right: 6px;">📄</span>
135
+ <span>.sh (Shell) 💻</span>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="legend-category">
140
+ <div class="legend-title">Indicators</div>
141
+ <div class="legend-item">
142
+ <svg width="16" height="16" style="margin-right: 8px;">
143
+ <circle cx="8" cy="8" r="6" fill="#d29922" stroke="#ff6b6b" stroke-width="2"/>
144
+ </svg>
145
+ <span>Dead Code (red border)</span>
146
+ </div>
147
+ <div class="legend-item">
148
+ <svg width="16" height="16" style="margin-right: 8px;">
149
+ <line x1="2" y1="8" x2="14" y2="8" stroke="#ff4444" stroke-width="2" stroke-dasharray="4,2"/>
150
+ </svg>
151
+ <span>Circular Dependency (red dashed)</span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div id="subprojects-legend" style="display: none;">
157
+ <h3>Subprojects</h3>
158
+ <div class="legend" id="subprojects-list"></div>
159
+ </div>
160
+
161
+ <div class="stats" id="stats"></div>
162
+ </div>
163
+
164
+ <svg id="graph"></svg>
165
+ <div id="tooltip" class="tooltip"></div>
166
+
167
+ <button id="reset-view-btn" title="Reset to home view">
168
+ <span style="font-size: 18px;">🏠</span>
169
+ <span>Reset View</span>
170
+ </button>
171
+
172
+ <div id="content-pane">
173
+ <div class="pane-header">
174
+ <button class="collapse-btn" onclick="closeContentPane()">×</button>
175
+ <div class="code-viewer-nav">
176
+ <button id="navBack" disabled title="Back (Alt+Left)">
177
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
178
+ <path d="M9.78 12.78a.75.75 0 0 1-1.06 0L4.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L6.06 8l3.72 3.72a.75.75 0 0 1 0 1.06Z"></path>
179
+ </svg>
180
+ </button>
181
+ <button id="navForward" disabled title="Forward (Alt+Right)">
182
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
183
+ <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"></path>
184
+ </svg>
185
+ </button>
186
+ <span id="navPosition"></span>
187
+ </div>
188
+ <div class="pane-title" id="pane-title"></div>
189
+ <div class="pane-meta" id="pane-meta"></div>
190
+ </div>
191
+ <div class="pane-content" id="pane-content"></div>
192
+ <div class="pane-footer" id="pane-footer"></div>
193
+ </div>
194
+
195
+ <script>
196
+ {get_all_scripts()}
197
+ </script>
198
+ </body>
199
+ </html>"""
200
+ return html
201
+
202
+
203
+ def inject_data(html: str, data: dict) -> str:
204
+ """Inject graph data into HTML template (not currently used for static export).
205
+
206
+ This function is provided for potential future use where data might be
207
+ embedded directly in the HTML rather than loaded from a separate JSON file.
208
+
209
+ Args:
210
+ html: HTML template string
211
+ data: Graph data dictionary
212
+
213
+ Returns:
214
+ HTML with embedded data
215
+ """
216
+ # For now, we load data from external JSON file
217
+ # This function can be enhanced later if inline data embedding is needed
218
+ return html