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.
Files changed (92) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +111 -0
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +74 -0
  7. mcp_vector_search/analysis/collectors/base.py +164 -0
  8. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  9. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  10. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  11. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  12. mcp_vector_search/analysis/collectors/smells.py +325 -0
  13. mcp_vector_search/analysis/debt.py +516 -0
  14. mcp_vector_search/analysis/interpretation.py +685 -0
  15. mcp_vector_search/analysis/metrics.py +414 -0
  16. mcp_vector_search/analysis/reporters/__init__.py +7 -0
  17. mcp_vector_search/analysis/reporters/console.py +646 -0
  18. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  19. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  20. mcp_vector_search/analysis/storage/__init__.py +93 -0
  21. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  22. mcp_vector_search/analysis/storage/schema.py +245 -0
  23. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  24. mcp_vector_search/analysis/trends.py +308 -0
  25. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  26. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  27. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  28. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  29. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  30. mcp_vector_search/cli/commands/analyze.py +1062 -0
  31. mcp_vector_search/cli/commands/chat.py +1455 -0
  32. mcp_vector_search/cli/commands/index.py +621 -5
  33. mcp_vector_search/cli/commands/index_background.py +467 -0
  34. mcp_vector_search/cli/commands/init.py +13 -0
  35. mcp_vector_search/cli/commands/install.py +597 -335
  36. mcp_vector_search/cli/commands/install_old.py +8 -4
  37. mcp_vector_search/cli/commands/mcp.py +78 -6
  38. mcp_vector_search/cli/commands/reset.py +68 -26
  39. mcp_vector_search/cli/commands/search.py +224 -8
  40. mcp_vector_search/cli/commands/setup.py +1184 -0
  41. mcp_vector_search/cli/commands/status.py +339 -5
  42. mcp_vector_search/cli/commands/uninstall.py +276 -357
  43. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  44. mcp_vector_search/cli/commands/visualize/cli.py +292 -0
  45. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  46. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  47. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
  48. mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
  49. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  50. mcp_vector_search/cli/commands/visualize/server.py +600 -0
  51. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  52. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  53. mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
  54. mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
  55. mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
  56. mcp_vector_search/cli/didyoumean.py +27 -2
  57. mcp_vector_search/cli/main.py +127 -160
  58. mcp_vector_search/cli/output.py +158 -13
  59. mcp_vector_search/config/__init__.py +4 -0
  60. mcp_vector_search/config/default_thresholds.yaml +52 -0
  61. mcp_vector_search/config/settings.py +12 -0
  62. mcp_vector_search/config/thresholds.py +273 -0
  63. mcp_vector_search/core/__init__.py +16 -0
  64. mcp_vector_search/core/auto_indexer.py +3 -3
  65. mcp_vector_search/core/boilerplate.py +186 -0
  66. mcp_vector_search/core/config_utils.py +394 -0
  67. mcp_vector_search/core/database.py +406 -94
  68. mcp_vector_search/core/embeddings.py +24 -0
  69. mcp_vector_search/core/exceptions.py +11 -0
  70. mcp_vector_search/core/git.py +380 -0
  71. mcp_vector_search/core/git_hooks.py +4 -4
  72. mcp_vector_search/core/indexer.py +632 -54
  73. mcp_vector_search/core/llm_client.py +756 -0
  74. mcp_vector_search/core/models.py +91 -1
  75. mcp_vector_search/core/project.py +17 -0
  76. mcp_vector_search/core/relationships.py +473 -0
  77. mcp_vector_search/core/scheduler.py +11 -11
  78. mcp_vector_search/core/search.py +179 -29
  79. mcp_vector_search/mcp/server.py +819 -9
  80. mcp_vector_search/parsers/python.py +285 -5
  81. mcp_vector_search/utils/__init__.py +2 -0
  82. mcp_vector_search/utils/gitignore.py +0 -3
  83. mcp_vector_search/utils/gitignore_updater.py +212 -0
  84. mcp_vector_search/utils/monorepo.py +66 -4
  85. mcp_vector_search/utils/timing.py +10 -6
  86. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
  87. mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
  88. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
  89. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
  90. mcp_vector_search/cli/commands/visualize.py +0 -1467
  91. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  92. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -1,1467 +0,0 @@
1
- """Visualization commands for MCP Vector Search."""
2
-
3
- import asyncio
4
- import json
5
- import shutil
6
- from pathlib import Path
7
-
8
- import typer
9
- from loguru import logger
10
- from rich.console import Console
11
- from rich.panel import Panel
12
-
13
- from ...core.database import ChromaVectorDatabase
14
- from ...core.embeddings import create_embedding_function
15
- from ...core.project import ProjectManager
16
-
17
- app = typer.Typer(
18
- help="Visualize code chunk relationships",
19
- no_args_is_help=True,
20
- )
21
- console = Console()
22
-
23
-
24
- @app.command()
25
- def export(
26
- output: Path = typer.Option(
27
- Path("chunk-graph.json"),
28
- "--output",
29
- "-o",
30
- help="Output file for chunk relationship data",
31
- ),
32
- file_path: str | None = typer.Option(
33
- None,
34
- "--file",
35
- "-f",
36
- help="Export only chunks from specific file (supports wildcards)",
37
- ),
38
- ) -> None:
39
- """Export chunk relationships as JSON for D3.js visualization.
40
-
41
- Examples:
42
- # Export all chunks
43
- mcp-vector-search visualize export
44
-
45
- # Export from specific file
46
- mcp-vector-search visualize export --file src/main.py
47
-
48
- # Custom output location
49
- mcp-vector-search visualize export -o graph.json
50
- """
51
- asyncio.run(_export_chunks(output, file_path))
52
-
53
-
54
- async def _export_chunks(output: Path, file_filter: str | None) -> None:
55
- """Export chunk relationship data."""
56
- try:
57
- # Load project
58
- project_manager = ProjectManager(Path.cwd())
59
-
60
- if not project_manager.is_initialized():
61
- console.print(
62
- "[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
63
- )
64
- raise typer.Exit(1)
65
-
66
- config = project_manager.load_config()
67
-
68
- # Get database
69
- embedding_function, _ = create_embedding_function(config.embedding_model)
70
- database = ChromaVectorDatabase(
71
- persist_directory=config.index_path,
72
- embedding_function=embedding_function,
73
- )
74
- await database.initialize()
75
-
76
- # Get all chunks with metadata
77
- console.print("[cyan]Fetching chunks from database...[/cyan]")
78
- chunks = await database.get_all_chunks()
79
-
80
- if len(chunks) == 0:
81
- console.print(
82
- "[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]"
83
- )
84
- raise typer.Exit(1)
85
-
86
- console.print(f"[green]✓[/green] Retrieved {len(chunks)} chunks")
87
-
88
- # Apply file filter if specified
89
- if file_filter:
90
- from fnmatch import fnmatch
91
-
92
- chunks = [c for c in chunks if fnmatch(str(c.file_path), file_filter)]
93
- console.print(
94
- f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]"
95
- )
96
-
97
- # Collect subprojects for monorepo support
98
- subprojects = {}
99
- for chunk in chunks:
100
- if chunk.subproject_name and chunk.subproject_name not in subprojects:
101
- subprojects[chunk.subproject_name] = {
102
- "name": chunk.subproject_name,
103
- "path": chunk.subproject_path,
104
- "color": _get_subproject_color(
105
- chunk.subproject_name, len(subprojects)
106
- ),
107
- }
108
-
109
- # Build graph data structure
110
- nodes = []
111
- links = []
112
- chunk_id_map = {} # Map chunk IDs to array indices
113
- file_nodes = {} # Track file nodes by path
114
- dir_nodes = {} # Track directory nodes by path
115
-
116
- # Add subproject root nodes for monorepos
117
- if subprojects:
118
- console.print(
119
- f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]"
120
- )
121
- for sp_name, sp_data in subprojects.items():
122
- node = {
123
- "id": f"subproject_{sp_name}",
124
- "name": sp_name,
125
- "type": "subproject",
126
- "file_path": sp_data["path"] or "",
127
- "start_line": 0,
128
- "end_line": 0,
129
- "complexity": 0,
130
- "color": sp_data["color"],
131
- "depth": 0,
132
- }
133
- nodes.append(node)
134
-
135
- # Load directory index for enhanced directory metadata
136
- console.print("[cyan]Loading directory index...[/cyan]")
137
- from ...core.directory_index import DirectoryIndex
138
-
139
- dir_index_path = (
140
- project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
141
- )
142
- dir_index = DirectoryIndex(dir_index_path)
143
- dir_index.load()
144
-
145
- # Create directory nodes from directory index
146
- console.print(
147
- f"[green]✓[/green] Loaded {len(dir_index.directories)} directories"
148
- )
149
- for dir_path_str, directory in dir_index.directories.items():
150
- dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
151
- dir_nodes[dir_path_str] = {
152
- "id": dir_id,
153
- "name": directory.name,
154
- "type": "directory",
155
- "file_path": dir_path_str,
156
- "start_line": 0,
157
- "end_line": 0,
158
- "complexity": 0,
159
- "depth": directory.depth,
160
- "dir_path": dir_path_str,
161
- "file_count": directory.file_count,
162
- "subdirectory_count": directory.subdirectory_count,
163
- "total_chunks": directory.total_chunks,
164
- "languages": directory.languages or {},
165
- "is_package": directory.is_package,
166
- "last_modified": directory.last_modified,
167
- }
168
-
169
- # Create file nodes from chunks
170
- for chunk in chunks:
171
- file_path_str = str(chunk.file_path)
172
- file_path = Path(file_path_str)
173
-
174
- # Create file node with parent directory reference
175
- if file_path_str not in file_nodes:
176
- file_id = f"file_{hash(file_path_str) & 0xFFFFFFFF:08x}"
177
-
178
- # Convert absolute path to relative path for parent directory lookup
179
- try:
180
- relative_file_path = file_path.relative_to(
181
- project_manager.project_root
182
- )
183
- parent_dir = relative_file_path.parent
184
- # Use relative path for parent directory (matches directory_index)
185
- parent_dir_str = (
186
- str(parent_dir) if parent_dir != Path(".") else None
187
- )
188
- except ValueError:
189
- # File is outside project root
190
- parent_dir_str = None
191
-
192
- # Look up parent directory ID from dir_nodes (must match exactly)
193
- parent_dir_id = None
194
- if parent_dir_str and parent_dir_str in dir_nodes:
195
- parent_dir_id = dir_nodes[parent_dir_str]["id"]
196
-
197
- file_nodes[file_path_str] = {
198
- "id": file_id,
199
- "name": file_path.name,
200
- "type": "file",
201
- "file_path": file_path_str,
202
- "start_line": 0,
203
- "end_line": 0,
204
- "complexity": 0,
205
- "depth": len(file_path.parts) - 1,
206
- "parent_dir_id": parent_dir_id,
207
- "parent_dir_path": parent_dir_str,
208
- }
209
-
210
- # Add directory nodes to graph
211
- for dir_node in dir_nodes.values():
212
- nodes.append(dir_node)
213
-
214
- # Add file nodes to graph
215
- for file_node in file_nodes.values():
216
- nodes.append(file_node)
217
-
218
- # Add chunk nodes
219
- for chunk in chunks:
220
- node = {
221
- "id": chunk.chunk_id or chunk.id,
222
- "name": chunk.function_name
223
- or chunk.class_name
224
- or f"L{chunk.start_line}",
225
- "type": chunk.chunk_type,
226
- "file_path": str(chunk.file_path),
227
- "start_line": chunk.start_line,
228
- "end_line": chunk.end_line,
229
- "complexity": chunk.complexity_score,
230
- "parent_id": chunk.parent_chunk_id,
231
- "depth": chunk.chunk_depth,
232
- "content": chunk.content, # Add content for code viewer
233
- "docstring": chunk.docstring,
234
- "language": chunk.language,
235
- }
236
-
237
- # Add subproject info for monorepos
238
- if chunk.subproject_name:
239
- node["subproject"] = chunk.subproject_name
240
- node["color"] = subprojects[chunk.subproject_name]["color"]
241
-
242
- nodes.append(node)
243
- chunk_id_map[node["id"]] = len(nodes) - 1
244
-
245
- # Link directories to their parent directories (hierarchical structure)
246
- for dir_path_str, dir_info in dir_index.directories.items():
247
- if dir_info.parent_path:
248
- parent_path_str = str(dir_info.parent_path)
249
- if parent_path_str in dir_nodes:
250
- parent_dir_id = f"dir_{hash(parent_path_str) & 0xFFFFFFFF:08x}"
251
- child_dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
252
- links.append(
253
- {
254
- "source": parent_dir_id,
255
- "target": child_dir_id,
256
- "type": "dir_hierarchy",
257
- }
258
- )
259
-
260
- # Link directories to subprojects in monorepos (simple flat structure)
261
- if subprojects:
262
- for dir_path_str, dir_node in dir_nodes.items():
263
- for sp_name, sp_data in subprojects.items():
264
- if dir_path_str.startswith(sp_data.get("path", "")):
265
- links.append(
266
- {
267
- "source": f"subproject_{sp_name}",
268
- "target": dir_node["id"],
269
- "type": "dir_containment",
270
- }
271
- )
272
- break
273
-
274
- # Link files to their parent directories
275
- for _file_path_str, file_node in file_nodes.items():
276
- if file_node.get("parent_dir_id"):
277
- links.append(
278
- {
279
- "source": file_node["parent_dir_id"],
280
- "target": file_node["id"],
281
- "type": "dir_containment",
282
- }
283
- )
284
-
285
- # Build hierarchical links from parent-child relationships
286
- for chunk in chunks:
287
- chunk_id = chunk.chunk_id or chunk.id
288
- file_path = str(chunk.file_path)
289
-
290
- # Link chunk to its file node if it has no parent (top-level chunks)
291
- if not chunk.parent_chunk_id and file_path in file_nodes:
292
- links.append(
293
- {
294
- "source": file_nodes[file_path]["id"],
295
- "target": chunk_id,
296
- "type": "file_containment",
297
- }
298
- )
299
-
300
- # Link to subproject root if in monorepo
301
- if chunk.subproject_name and not chunk.parent_chunk_id:
302
- links.append(
303
- {
304
- "source": f"subproject_{chunk.subproject_name}",
305
- "target": chunk_id,
306
- }
307
- )
308
-
309
- # Link to parent chunk
310
- if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
311
- links.append(
312
- {
313
- "source": chunk.parent_chunk_id,
314
- "target": chunk_id,
315
- }
316
- )
317
-
318
- # Parse inter-project dependencies for monorepos
319
- if subprojects:
320
- console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
321
- dep_links = _parse_project_dependencies(
322
- project_manager.project_root, subprojects
323
- )
324
- links.extend(dep_links)
325
- if dep_links:
326
- console.print(
327
- f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies"
328
- )
329
-
330
- # Get stats
331
- stats = await database.get_stats()
332
-
333
- # Build final graph data
334
- graph_data = {
335
- "nodes": nodes,
336
- "links": links,
337
- "metadata": {
338
- "total_chunks": len(chunks),
339
- "total_files": stats.total_files,
340
- "languages": stats.languages,
341
- "is_monorepo": len(subprojects) > 0,
342
- "subprojects": list(subprojects.keys()) if subprojects else [],
343
- },
344
- }
345
-
346
- # Write to file
347
- output.parent.mkdir(parents=True, exist_ok=True)
348
- with open(output, "w") as f:
349
- json.dump(graph_data, f, indent=2)
350
-
351
- await database.close()
352
-
353
- console.print()
354
- console.print(
355
- Panel.fit(
356
- f"[green]✓[/green] Exported graph data to [cyan]{output}[/cyan]\n\n"
357
- f"Nodes: {len(graph_data['nodes'])}\n"
358
- f"Links: {len(graph_data['links'])}\n"
359
- f"{'Subprojects: ' + str(len(subprojects)) if subprojects else ''}\n\n"
360
- f"[dim]Next: Run 'mcp-vector-search visualize serve' to view[/dim]",
361
- title="Export Complete",
362
- border_style="green",
363
- )
364
- )
365
-
366
- except Exception as e:
367
- logger.error(f"Export failed: {e}")
368
- console.print(f"[red]✗ Export failed: {e}[/red]")
369
- raise typer.Exit(1)
370
-
371
-
372
- def _get_subproject_color(subproject_name: str, index: int) -> str:
373
- """Get a consistent color for a subproject."""
374
- # Color palette for subprojects (GitHub-style colors)
375
- colors = [
376
- "#238636", # Green
377
- "#1f6feb", # Blue
378
- "#d29922", # Yellow
379
- "#8957e5", # Purple
380
- "#da3633", # Red
381
- "#bf8700", # Orange
382
- "#1a7f37", # Dark green
383
- "#0969da", # Dark blue
384
- ]
385
- return colors[index % len(colors)]
386
-
387
-
388
- def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[dict]:
389
- """Parse package.json files to find inter-project dependencies.
390
-
391
- Args:
392
- project_root: Root directory of the monorepo
393
- subprojects: Dictionary of subproject information
394
-
395
- Returns:
396
- List of dependency links between subprojects
397
- """
398
- dependency_links = []
399
-
400
- for sp_name, sp_data in subprojects.items():
401
- package_json = project_root / sp_data["path"] / "package.json"
402
-
403
- if not package_json.exists():
404
- continue
405
-
406
- try:
407
- with open(package_json) as f:
408
- package_data = json.load(f)
409
-
410
- # Check all dependency types
411
- all_deps = {}
412
- for dep_type in ["dependencies", "devDependencies", "peerDependencies"]:
413
- if dep_type in package_data:
414
- all_deps.update(package_data[dep_type])
415
-
416
- # Find dependencies on other subprojects
417
- for dep_name in all_deps.keys():
418
- # Check if this dependency is another subproject
419
- for other_sp_name in subprojects.keys():
420
- if other_sp_name != sp_name and dep_name == other_sp_name:
421
- # Found inter-project dependency
422
- dependency_links.append(
423
- {
424
- "source": f"subproject_{sp_name}",
425
- "target": f"subproject_{other_sp_name}",
426
- "type": "dependency",
427
- }
428
- )
429
-
430
- except Exception as e:
431
- logger.debug(f"Failed to parse {package_json}: {e}")
432
- continue
433
-
434
- return dependency_links
435
-
436
-
437
- @app.command()
438
- def serve(
439
- port: int = typer.Option(
440
- 8080, "--port", "-p", help="Port for visualization server"
441
- ),
442
- graph_file: Path = typer.Option(
443
- Path("chunk-graph.json"),
444
- "--graph",
445
- "-g",
446
- help="Graph JSON file to visualize",
447
- ),
448
- ) -> None:
449
- """Start local HTTP server for D3.js visualization.
450
-
451
- Examples:
452
- # Start server on default port 8080
453
- mcp-vector-search visualize serve
454
-
455
- # Custom port
456
- mcp-vector-search visualize serve --port 3000
457
-
458
- # Custom graph file
459
- mcp-vector-search visualize serve --graph my-graph.json
460
- """
461
- import http.server
462
- import os
463
- import socket
464
- import socketserver
465
- import webbrowser
466
-
467
- # Find free port in range 8080-8099
468
- def find_free_port(start_port: int = 8080, end_port: int = 8099) -> int:
469
- """Find a free port in the given range."""
470
- for test_port in range(start_port, end_port + 1):
471
- try:
472
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
473
- s.bind(("", test_port))
474
- return test_port
475
- except OSError:
476
- continue
477
- raise OSError(f"No free ports available in range {start_port}-{end_port}")
478
-
479
- # Use specified port or find free one
480
- if port == 8080: # Default port, try to find free one
481
- try:
482
- port = find_free_port(8080, 8099)
483
- except OSError as e:
484
- console.print(f"[red]✗ {e}[/red]")
485
- raise typer.Exit(1)
486
-
487
- # Get visualization directory
488
- viz_dir = Path(__file__).parent.parent.parent / "visualization"
489
-
490
- if not viz_dir.exists():
491
- console.print(
492
- f"[yellow]Visualization directory not found. Creating at {viz_dir}...[/yellow]"
493
- )
494
- viz_dir.mkdir(parents=True, exist_ok=True)
495
-
496
- # Create index.html if it doesn't exist
497
- html_file = viz_dir / "index.html"
498
- if not html_file.exists():
499
- console.print("[yellow]Creating visualization HTML file...[/yellow]")
500
- _create_visualization_html(html_file)
501
-
502
- # Copy graph file to visualization directory if it exists
503
- if graph_file.exists():
504
- dest = viz_dir / "chunk-graph.json"
505
- shutil.copy(graph_file, dest)
506
- console.print(f"[green]✓[/green] Copied graph data to {dest}")
507
- else:
508
- # Auto-generate graph file if it doesn't exist
509
- console.print(
510
- f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
511
- )
512
- asyncio.run(_export_chunks(graph_file, None))
513
- console.print()
514
-
515
- # Copy the newly generated graph to visualization directory
516
- if graph_file.exists():
517
- dest = viz_dir / "chunk-graph.json"
518
- shutil.copy(graph_file, dest)
519
- console.print(f"[green]✓[/green] Copied graph data to {dest}")
520
-
521
- # Change to visualization directory
522
- os.chdir(viz_dir)
523
-
524
- # Start server
525
- handler = http.server.SimpleHTTPRequestHandler
526
- try:
527
- with socketserver.TCPServer(("", port), handler) as httpd:
528
- url = f"http://localhost:{port}"
529
- console.print()
530
- console.print(
531
- Panel.fit(
532
- f"[green]✓[/green] Visualization server running\n\n"
533
- f"URL: [cyan]{url}[/cyan]\n"
534
- f"Directory: [dim]{viz_dir}[/dim]\n\n"
535
- f"[dim]Press Ctrl+C to stop[/dim]",
536
- title="Server Started",
537
- border_style="green",
538
- )
539
- )
540
-
541
- # Open browser
542
- webbrowser.open(url)
543
-
544
- try:
545
- httpd.serve_forever()
546
- except KeyboardInterrupt:
547
- console.print("\n[yellow]Stopping server...[/yellow]")
548
-
549
- except OSError as e:
550
- if "Address already in use" in str(e):
551
- console.print(
552
- f"[red]✗ Port {port} is already in use. Try a different port with --port[/red]"
553
- )
554
- else:
555
- console.print(f"[red]✗ Server error: {e}[/red]")
556
- raise typer.Exit(1)
557
-
558
-
559
- def _create_visualization_html(html_file: Path) -> None:
560
- """Create the D3.js visualization HTML file."""
561
- html_content = """<!DOCTYPE html>
562
- <html>
563
- <head>
564
- <meta charset="utf-8">
565
- <title>Code Chunk Relationship Graph</title>
566
- <script src="https://d3js.org/d3.v7.min.js"></script>
567
- <style>
568
- body {
569
- margin: 0;
570
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
571
- background: #0d1117;
572
- color: #c9d1d9;
573
- overflow: hidden;
574
- }
575
-
576
- #controls {
577
- position: absolute;
578
- top: 20px;
579
- left: 20px;
580
- background: rgba(13, 17, 23, 0.95);
581
- border: 1px solid #30363d;
582
- border-radius: 6px;
583
- padding: 16px;
584
- min-width: 250px;
585
- max-height: 80vh;
586
- overflow-y: auto;
587
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
588
- }
589
-
590
- h1 { margin: 0 0 16px 0; font-size: 18px; }
591
- h3 { margin: 16px 0 8px 0; font-size: 14px; color: #8b949e; }
592
-
593
- .control-group {
594
- margin-bottom: 12px;
595
- }
596
-
597
- label {
598
- display: block;
599
- margin-bottom: 4px;
600
- font-size: 12px;
601
- color: #8b949e;
602
- }
603
-
604
- input[type="file"] {
605
- width: 100%;
606
- padding: 6px;
607
- background: #161b22;
608
- border: 1px solid #30363d;
609
- border-radius: 6px;
610
- color: #c9d1d9;
611
- font-size: 12px;
612
- }
613
-
614
- .legend {
615
- font-size: 12px;
616
- }
617
-
618
- .legend-item {
619
- margin: 4px 0;
620
- display: flex;
621
- align-items: center;
622
- }
623
-
624
- .legend-color {
625
- width: 12px;
626
- height: 12px;
627
- border-radius: 50%;
628
- margin-right: 8px;
629
- }
630
-
631
- #graph {
632
- width: 100vw;
633
- height: 100vh;
634
- }
635
-
636
- .node circle {
637
- cursor: pointer;
638
- stroke: #c9d1d9;
639
- stroke-width: 1.5px;
640
- }
641
-
642
- .node.module circle { fill: #238636; }
643
- .node.class circle { fill: #1f6feb; }
644
- .node.function circle { fill: #d29922; }
645
- .node.method circle { fill: #8957e5; }
646
- .node.code circle { fill: #6e7681; }
647
- .node.file circle {
648
- fill: none;
649
- stroke: #58a6ff;
650
- stroke-width: 2px;
651
- stroke-dasharray: 5,3;
652
- opacity: 0.6;
653
- }
654
- .node.directory circle {
655
- fill: none;
656
- stroke: #79c0ff;
657
- stroke-width: 2px;
658
- stroke-dasharray: 3,3;
659
- opacity: 0.5;
660
- }
661
- .node.subproject circle { fill: #da3633; stroke-width: 3px; }
662
-
663
- /* Non-code document nodes - squares */
664
- .node.docstring rect { fill: #8b949e; }
665
- .node.comment rect { fill: #6e7681; }
666
- .node rect {
667
- stroke: #c9d1d9;
668
- stroke-width: 1.5px;
669
- }
670
-
671
- .node text {
672
- font-size: 11px;
673
- fill: #c9d1d9;
674
- text-anchor: middle;
675
- pointer-events: none;
676
- user-select: none;
677
- }
678
-
679
- .link {
680
- stroke: #30363d;
681
- stroke-opacity: 0.6;
682
- stroke-width: 1.5px;
683
- }
684
-
685
- .link.dependency {
686
- stroke: #d29922;
687
- stroke-opacity: 0.8;
688
- stroke-width: 2px;
689
- stroke-dasharray: 5,5;
690
- }
691
-
692
- .tooltip {
693
- position: absolute;
694
- padding: 12px;
695
- background: rgba(13, 17, 23, 0.95);
696
- border: 1px solid #30363d;
697
- border-radius: 6px;
698
- pointer-events: none;
699
- display: none;
700
- font-size: 12px;
701
- max-width: 300px;
702
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
703
- }
704
-
705
- .stats {
706
- margin-top: 16px;
707
- padding-top: 16px;
708
- border-top: 1px solid #30363d;
709
- font-size: 12px;
710
- color: #8b949e;
711
- }
712
-
713
- #content-pane {
714
- position: fixed;
715
- top: 0;
716
- right: 0;
717
- width: 600px;
718
- height: 100vh;
719
- background: rgba(13, 17, 23, 0.98);
720
- border-left: 1px solid #30363d;
721
- overflow-y: auto;
722
- box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
723
- transform: translateX(100%);
724
- transition: transform 0.3s ease-in-out;
725
- z-index: 1000;
726
- }
727
-
728
- #content-pane.visible {
729
- transform: translateX(0);
730
- }
731
-
732
- #content-pane .pane-header {
733
- position: sticky;
734
- top: 0;
735
- background: rgba(13, 17, 23, 0.98);
736
- padding: 20px;
737
- border-bottom: 1px solid #30363d;
738
- z-index: 1;
739
- }
740
-
741
- #content-pane .pane-title {
742
- font-size: 16px;
743
- font-weight: bold;
744
- color: #58a6ff;
745
- margin-bottom: 8px;
746
- padding-right: 30px;
747
- }
748
-
749
- #content-pane .pane-meta {
750
- font-size: 12px;
751
- color: #8b949e;
752
- }
753
-
754
- #content-pane .collapse-btn {
755
- position: absolute;
756
- top: 20px;
757
- right: 20px;
758
- cursor: pointer;
759
- color: #8b949e;
760
- font-size: 24px;
761
- line-height: 1;
762
- background: none;
763
- border: none;
764
- padding: 0;
765
- transition: color 0.2s;
766
- }
767
-
768
- #content-pane .collapse-btn:hover {
769
- color: #c9d1d9;
770
- }
771
-
772
- #content-pane .pane-content {
773
- padding: 20px;
774
- }
775
-
776
- #content-pane pre {
777
- margin: 0;
778
- padding: 16px;
779
- background: #0d1117;
780
- border: 1px solid #30363d;
781
- border-radius: 6px;
782
- overflow-x: auto;
783
- font-size: 12px;
784
- line-height: 1.6;
785
- }
786
-
787
- #content-pane code {
788
- color: #c9d1d9;
789
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
790
- }
791
-
792
- #content-pane .directory-list {
793
- list-style: none;
794
- padding: 0;
795
- margin: 0;
796
- }
797
-
798
- #content-pane .directory-list li {
799
- padding: 8px 12px;
800
- margin: 4px 0;
801
- background: #161b22;
802
- border: 1px solid #30363d;
803
- border-radius: 4px;
804
- font-size: 12px;
805
- display: flex;
806
- align-items: center;
807
- }
808
-
809
- #content-pane .directory-list .item-icon {
810
- margin-right: 8px;
811
- font-size: 14px;
812
- }
813
-
814
- #content-pane .directory-list .item-type {
815
- margin-left: auto;
816
- padding-left: 12px;
817
- font-size: 10px;
818
- color: #8b949e;
819
- }
820
-
821
- #content-pane .import-details {
822
- background: #161b22;
823
- border: 1px solid #30363d;
824
- border-radius: 6px;
825
- padding: 16px;
826
- }
827
-
828
- #content-pane .import-details .import-statement {
829
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
830
- font-size: 12px;
831
- color: #79c0ff;
832
- margin-bottom: 12px;
833
- }
834
-
835
- #content-pane .import-details .detail-row {
836
- font-size: 11px;
837
- color: #8b949e;
838
- margin: 4px 0;
839
- }
840
-
841
- #content-pane .import-details .detail-label {
842
- color: #c9d1d9;
843
- font-weight: 600;
844
- }
845
-
846
- .node.highlighted circle,
847
- .node.highlighted rect {
848
- stroke: #f0e68c;
849
- stroke-width: 3px;
850
- filter: drop-shadow(0 0 8px #f0e68c);
851
- }
852
- </style>
853
- </head>
854
- <body>
855
- <div id="controls">
856
- <h1>🔍 Code Graph</h1>
857
-
858
- <div class="control-group" id="loading">
859
- <label>⏳ Loading graph data...</label>
860
- </div>
861
-
862
- <h3>Legend</h3>
863
- <div class="legend">
864
- <div class="legend-item">
865
- <span class="legend-color" style="background: #da3633;"></span> Subproject
866
- </div>
867
- <div class="legend-item">
868
- <span class="legend-color" style="border: 2px dashed #79c0ff; border-radius: 50%; background: transparent;"></span> Directory
869
- </div>
870
- <div class="legend-item">
871
- <span class="legend-color" style="border: 2px dashed #58a6ff; border-radius: 50%; background: transparent;"></span> File
872
- </div>
873
- <div class="legend-item">
874
- <span class="legend-color" style="background: #238636;"></span> Module
875
- </div>
876
- <div class="legend-item">
877
- <span class="legend-color" style="background: #1f6feb;"></span> Class
878
- </div>
879
- <div class="legend-item">
880
- <span class="legend-color" style="background: #d29922;"></span> Function
881
- </div>
882
- <div class="legend-item">
883
- <span class="legend-color" style="background: #8957e5;"></span> Method
884
- </div>
885
- <div class="legend-item">
886
- <span class="legend-color" style="background: #6e7681;"></span> Code
887
- </div>
888
- <div class="legend-item" style="font-style: italic; color: #79c0ff;">
889
- <span class="legend-color" style="background: #6e7681;"></span> Import (L1)
890
- </div>
891
- <div class="legend-item">
892
- <span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
893
- </div>
894
- <div class="legend-item">
895
- <span class="legend-color" style="background: #6e7681; border-radius: 2px;"></span> Comment ▢
896
- </div>
897
- </div>
898
-
899
- <div id="subprojects-legend" style="display: none;">
900
- <h3>Subprojects</h3>
901
- <div class="legend" id="subprojects-list"></div>
902
- </div>
903
-
904
- <div class="stats" id="stats"></div>
905
- </div>
906
-
907
- <svg id="graph"></svg>
908
- <div id="tooltip" class="tooltip"></div>
909
-
910
- <div id="content-pane">
911
- <div class="pane-header">
912
- <button class="collapse-btn" onclick="closeContentPane()">×</button>
913
- <div class="pane-title" id="pane-title"></div>
914
- <div class="pane-meta" id="pane-meta"></div>
915
- </div>
916
- <div class="pane-content" id="pane-content"></div>
917
- </div>
918
-
919
- <script>
920
- const width = window.innerWidth;
921
- const height = window.innerHeight;
922
-
923
- const svg = d3.select("#graph")
924
- .attr("width", width)
925
- .attr("height", height)
926
- .call(d3.zoom().on("zoom", (event) => {
927
- g.attr("transform", event.transform);
928
- }));
929
-
930
- const g = svg.append("g");
931
- const tooltip = d3.select("#tooltip");
932
- let simulation;
933
- let allNodes = [];
934
- let allLinks = [];
935
- let visibleNodes = new Set();
936
- let collapsedNodes = new Set();
937
- let highlightedNode = null;
938
-
939
- function visualizeGraph(data) {
940
- g.selectAll("*").remove();
941
-
942
- allNodes = data.nodes;
943
- allLinks = data.links;
944
-
945
- // Find root nodes - start with only top-level nodes
946
- let rootNodes;
947
- if (data.metadata && data.metadata.is_monorepo) {
948
- // In monorepos, subproject nodes are roots
949
- rootNodes = allNodes.filter(n => n.type === 'subproject');
950
- } else {
951
- // Regular projects: show root-level directories AND files
952
- const dirNodes = allNodes.filter(n => n.type === 'directory');
953
- const fileNodes = allNodes.filter(n => n.type === 'file');
954
-
955
- // Find minimum depth for directories and files
956
- const minDirDepth = dirNodes.length > 0
957
- ? Math.min(...dirNodes.map(n => n.depth))
958
- : Infinity;
959
- const minFileDepth = fileNodes.length > 0
960
- ? Math.min(...fileNodes.map(n => n.depth))
961
- : Infinity;
962
-
963
- // Include both root-level directories and root-level files
964
- rootNodes = [
965
- ...dirNodes.filter(n => n.depth === minDirDepth),
966
- ...fileNodes.filter(n => n.depth === minFileDepth)
967
- ];
968
-
969
- // Fallback to all files if nothing found
970
- if (rootNodes.length === 0) {
971
- rootNodes = fileNodes;
972
- }
973
- }
974
-
975
- // Start with only root nodes visible, all collapsed
976
- visibleNodes = new Set(rootNodes.map(n => n.id));
977
- collapsedNodes = new Set(rootNodes.map(n => n.id));
978
-
979
- renderGraph();
980
- }
981
-
982
- function renderGraph() {
983
- const visibleNodesList = allNodes.filter(n => visibleNodes.has(n.id));
984
- const visibleLinks = allLinks.filter(l =>
985
- visibleNodes.has(l.source.id || l.source) &&
986
- visibleNodes.has(l.target.id || l.target)
987
- );
988
-
989
- simulation = d3.forceSimulation(visibleNodesList)
990
- .force("link", d3.forceLink(visibleLinks).id(d => d.id).distance(100))
991
- .force("charge", d3.forceManyBody().strength(-400))
992
- .force("center", d3.forceCenter(width / 2, height / 2))
993
- .force("collision", d3.forceCollide().radius(40));
994
-
995
- g.selectAll("*").remove();
996
-
997
- const link = g.append("g")
998
- .selectAll("line")
999
- .data(visibleLinks)
1000
- .join("line")
1001
- .attr("class", d => d.type === "dependency" ? "link dependency" : "link");
1002
-
1003
- const node = g.append("g")
1004
- .selectAll("g")
1005
- .data(visibleNodesList)
1006
- .join("g")
1007
- .attr("class", d => {
1008
- let classes = `node ${d.type}`;
1009
- if (highlightedNode && d.id === highlightedNode.id) {
1010
- classes += ' highlighted';
1011
- }
1012
- return classes;
1013
- })
1014
- .call(drag(simulation))
1015
- .on("click", handleNodeClick)
1016
- .on("mouseover", showTooltip)
1017
- .on("mouseout", hideTooltip);
1018
-
1019
- // Add shapes based on node type (circles for code, squares for docs)
1020
- const isDocNode = d => ['docstring', 'comment'].includes(d.type);
1021
-
1022
- // Add circles for code nodes
1023
- node.filter(d => !isDocNode(d))
1024
- .append("circle")
1025
- .attr("r", d => {
1026
- if (d.type === 'subproject') return 20;
1027
- if (d.type === 'directory') return 40; // Largest for directory containers
1028
- if (d.type === 'file') return 30; // Larger transparent circle for files
1029
- return d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1030
- })
1031
- .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
1032
- .attr("stroke-width", d => hasChildren(d) ? 2 : 0)
1033
- .style("fill", d => d.color || null); // Use custom color if available
1034
-
1035
- // Add rectangles for document nodes
1036
- node.filter(d => isDocNode(d))
1037
- .append("rect")
1038
- .attr("width", d => {
1039
- const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1040
- return size * 2;
1041
- })
1042
- .attr("height", d => {
1043
- const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1044
- return size * 2;
1045
- })
1046
- .attr("x", d => {
1047
- const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1048
- return -size;
1049
- })
1050
- .attr("y", d => {
1051
- const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1052
- return -size;
1053
- })
1054
- .attr("rx", 2) // Rounded corners
1055
- .attr("ry", 2)
1056
- .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
1057
- .attr("stroke-width", d => hasChildren(d) ? 2 : 0)
1058
- .style("fill", d => d.color || null);
1059
-
1060
- // Add expand/collapse indicator
1061
- node.filter(d => hasChildren(d))
1062
- .append("text")
1063
- .attr("class", "expand-indicator")
1064
- .attr("text-anchor", "middle")
1065
- .attr("dy", 5)
1066
- .style("font-size", "16px")
1067
- .style("font-weight", "bold")
1068
- .style("fill", "#ffffff")
1069
- .style("pointer-events", "none")
1070
- .text(d => collapsedNodes.has(d.id) ? "+" : "−");
1071
-
1072
- // Add labels (show actual import statement for L1 nodes)
1073
- node.append("text")
1074
- .text(d => {
1075
- // L1 (depth 1) nodes are imports
1076
- if (d.depth === 1 && d.type !== 'directory' && d.type !== 'file') {
1077
- if (d.content) {
1078
- // Extract first line of import statement
1079
- const importLine = d.content.split('\n')[0].trim();
1080
- // Truncate if too long (max 60 chars)
1081
- return importLine.length > 60 ? importLine.substring(0, 57) + '...' : importLine;
1082
- }
1083
- return d.name; // Fallback to name if no content
1084
- }
1085
- return d.name;
1086
- })
1087
- .attr("dy", 30);
1088
-
1089
- simulation.on("tick", () => {
1090
- link
1091
- .attr("x1", d => d.source.x)
1092
- .attr("y1", d => d.source.y)
1093
- .attr("x2", d => d.target.x)
1094
- .attr("y2", d => d.target.y);
1095
-
1096
- node.attr("transform", d => `translate(${d.x},${d.y})`);
1097
- });
1098
-
1099
- updateStats({nodes: visibleNodesList, links: visibleLinks, metadata: {total_files: allNodes.length}});
1100
- }
1101
-
1102
- function hasChildren(node) {
1103
- return allLinks.some(l => (l.source.id || l.source) === node.id);
1104
- }
1105
-
1106
- function handleNodeClick(event, d) {
1107
- event.stopPropagation();
1108
-
1109
- // Always show content pane when clicking any node
1110
- showContentPane(d);
1111
-
1112
- // If node has children, also toggle expansion
1113
- if (hasChildren(d)) {
1114
- if (collapsedNodes.has(d.id)) {
1115
- expandNode(d);
1116
- } else {
1117
- collapseNode(d);
1118
- }
1119
- renderGraph();
1120
- }
1121
- }
1122
-
1123
- function expandNode(node) {
1124
- collapsedNodes.delete(node.id);
1125
-
1126
- // Find direct children
1127
- const children = allLinks
1128
- .filter(l => (l.source.id || l.source) === node.id)
1129
- .map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
1130
- .filter(n => n);
1131
-
1132
- children.forEach(child => {
1133
- visibleNodes.add(child.id);
1134
- collapsedNodes.add(child.id); // Children start collapsed
1135
- });
1136
- }
1137
-
1138
- function collapseNode(node) {
1139
- collapsedNodes.add(node.id);
1140
-
1141
- // Hide all descendants recursively
1142
- function hideDescendants(parentId) {
1143
- const children = allLinks
1144
- .filter(l => (l.source.id || l.source) === parentId)
1145
- .map(l => l.target.id || l.target);
1146
-
1147
- children.forEach(childId => {
1148
- visibleNodes.delete(childId);
1149
- collapsedNodes.delete(childId);
1150
- hideDescendants(childId);
1151
- });
1152
- }
1153
-
1154
- hideDescendants(node.id);
1155
- }
1156
-
1157
- function showTooltip(event, d) {
1158
- tooltip
1159
- .style("display", "block")
1160
- .style("left", (event.pageX + 10) + "px")
1161
- .style("top", (event.pageY + 10) + "px")
1162
- .html(`
1163
- <div><strong>${d.name}</strong></div>
1164
- <div>Type: ${d.type}</div>
1165
- ${d.complexity ? `<div>Complexity: ${d.complexity.toFixed(1)}</div>` : ''}
1166
- ${d.start_line ? `<div>Lines: ${d.start_line}-${d.end_line}</div>` : ''}
1167
- <div>File: ${d.file_path}</div>
1168
- `);
1169
- }
1170
-
1171
- function hideTooltip() {
1172
- tooltip.style("display", "none");
1173
- }
1174
-
1175
- function drag(simulation) {
1176
- function dragstarted(event) {
1177
- if (!event.active) simulation.alphaTarget(0.3).restart();
1178
- event.subject.fx = event.subject.x;
1179
- event.subject.fy = event.subject.y;
1180
- }
1181
-
1182
- function dragged(event) {
1183
- event.subject.fx = event.x;
1184
- event.subject.fy = event.y;
1185
- }
1186
-
1187
- function dragended(event) {
1188
- if (!event.active) simulation.alphaTarget(0);
1189
- event.subject.fx = null;
1190
- event.subject.fy = null;
1191
- }
1192
-
1193
- return d3.drag()
1194
- .on("start", dragstarted)
1195
- .on("drag", dragged)
1196
- .on("end", dragended);
1197
- }
1198
-
1199
- function updateStats(data) {
1200
- const stats = d3.select("#stats");
1201
- stats.html(`
1202
- <div>Nodes: ${data.nodes.length}</div>
1203
- <div>Links: ${data.links.length}</div>
1204
- ${data.metadata ? `<div>Files: ${data.metadata.total_files || 'N/A'}</div>` : ''}
1205
- ${data.metadata && data.metadata.is_monorepo ? `<div>Monorepo: ${data.metadata.subprojects.length} subprojects</div>` : ''}
1206
- `);
1207
-
1208
- // Show subproject legend if monorepo
1209
- if (data.metadata && data.metadata.is_monorepo && data.metadata.subprojects.length > 0) {
1210
- const subprojectsLegend = d3.select("#subprojects-legend");
1211
- const subprojectsList = d3.select("#subprojects-list");
1212
-
1213
- subprojectsLegend.style("display", "block");
1214
-
1215
- // Get subproject nodes with colors
1216
- const subprojectNodes = allNodes.filter(n => n.type === 'subproject');
1217
-
1218
- subprojectsList.html(
1219
- subprojectNodes.map(sp =>
1220
- `<div class="legend-item">
1221
- <span class="legend-color" style="background: ${sp.color};"></span> ${sp.name}
1222
- </div>`
1223
- ).join('')
1224
- );
1225
- }
1226
- }
1227
-
1228
- function showContentPane(node) {
1229
- // Highlight the node
1230
- highlightedNode = node;
1231
- renderGraph();
1232
-
1233
- // Populate content pane
1234
- const pane = document.getElementById('content-pane');
1235
- const title = document.getElementById('pane-title');
1236
- const meta = document.getElementById('pane-meta');
1237
- const content = document.getElementById('pane-content');
1238
-
1239
- // Set title with actual import statement for L1 nodes
1240
- if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
1241
- if (node.content) {
1242
- const importLine = node.content.split('\n')[0].trim();
1243
- title.textContent = importLine;
1244
- } else {
1245
- title.textContent = `Import: ${node.name}`;
1246
- }
1247
- } else {
1248
- title.textContent = node.name;
1249
- }
1250
-
1251
- // Set metadata
1252
- let metaText = `${node.type} • ${node.file_path}`;
1253
- if (node.start_line) {
1254
- metaText += ` • Lines ${node.start_line}-${node.end_line}`;
1255
- }
1256
- if (node.language) {
1257
- metaText += ` • ${node.language}`;
1258
- }
1259
- meta.textContent = metaText;
1260
-
1261
- // Display content based on node type
1262
- if (node.type === 'directory') {
1263
- showDirectoryContents(node, content);
1264
- } else if (node.type === 'file') {
1265
- showFileContents(node, content);
1266
- } else if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
1267
- // L1 nodes are imports
1268
- showImportDetails(node, content);
1269
- } else {
1270
- // Class, function, method, code nodes
1271
- showCodeContent(node, content);
1272
- }
1273
-
1274
- pane.classList.add('visible');
1275
- }
1276
-
1277
- function showDirectoryContents(node, container) {
1278
- // Find all direct children of this directory
1279
- const children = allLinks
1280
- .filter(l => (l.source.id || l.source) === node.id)
1281
- .map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
1282
- .filter(n => n);
1283
-
1284
- if (children.length === 0) {
1285
- container.innerHTML = '<p style="color: #8b949e;">Empty directory</p>';
1286
- return;
1287
- }
1288
-
1289
- // Group by type
1290
- const files = children.filter(n => n.type === 'file');
1291
- const subdirs = children.filter(n => n.type === 'directory');
1292
- const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
1293
-
1294
- let html = '<ul class="directory-list">';
1295
-
1296
- // Show subdirectories first
1297
- subdirs.forEach(child => {
1298
- html += `
1299
- <li>
1300
- <span class="item-icon">📁</span>
1301
- ${child.name}
1302
- <span class="item-type">directory</span>
1303
- </li>
1304
- `;
1305
- });
1306
-
1307
- // Then files
1308
- files.forEach(child => {
1309
- html += `
1310
- <li>
1311
- <span class="item-icon">📄</span>
1312
- ${child.name}
1313
- <span class="item-type">file</span>
1314
- </li>
1315
- `;
1316
- });
1317
-
1318
- // Then code chunks
1319
- chunks.forEach(child => {
1320
- const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
1321
- html += `
1322
- <li>
1323
- <span class="item-icon">${icon}</span>
1324
- ${child.name}
1325
- <span class="item-type">${child.type}</span>
1326
- </li>
1327
- `;
1328
- });
1329
-
1330
- html += '</ul>';
1331
-
1332
- // Add summary
1333
- const summary = `<p style="color: #8b949e; font-size: 11px; margin-top: 16px;">
1334
- Total: ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)
1335
- </p>`;
1336
-
1337
- container.innerHTML = html + summary;
1338
- }
1339
-
1340
- function showFileContents(node, container) {
1341
- // Find all chunks in this file
1342
- const fileChunks = allLinks
1343
- .filter(l => (l.source.id || l.source) === node.id)
1344
- .map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
1345
- .filter(n => n);
1346
-
1347
- if (fileChunks.length === 0) {
1348
- container.innerHTML = '<p style="color: #8b949e;">No code chunks found in this file</p>';
1349
- return;
1350
- }
1351
-
1352
- // Collect all content from chunks and sort by line number
1353
- const sortedChunks = fileChunks
1354
- .filter(c => c.content)
1355
- .sort((a, b) => a.start_line - b.start_line);
1356
-
1357
- if (sortedChunks.length === 0) {
1358
- container.innerHTML = '<p style="color: #8b949e;">File content not available</p>';
1359
- return;
1360
- }
1361
-
1362
- // Combine all chunks to show full file
1363
- const fullContent = sortedChunks.map(c => c.content).join('\n\n');
1364
-
1365
- container.innerHTML = `
1366
- <p style="color: #8b949e; font-size: 11px; margin-bottom: 12px;">
1367
- Contains ${fileChunks.length} code chunks
1368
- </p>
1369
- <pre><code>${escapeHtml(fullContent)}</code></pre>
1370
- `;
1371
- }
1372
-
1373
- function showImportDetails(node, container) {
1374
- // L1 nodes are import statements - show import content prominently
1375
- const importHtml = `
1376
- <div class="import-details">
1377
- ${node.content ? `
1378
- <div style="margin-bottom: 16px;">
1379
- <div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
1380
- <pre><code>${escapeHtml(node.content)}</code></pre>
1381
- </div>
1382
- ` : '<p style="color: #8b949e;">No import content available</p>'}
1383
- <div class="detail-row">
1384
- <span class="detail-label">File:</span> ${node.file_path}
1385
- </div>
1386
- ${node.start_line ? `
1387
- <div class="detail-row">
1388
- <span class="detail-label">Location:</span> Lines ${node.start_line}-${node.end_line}
1389
- </div>
1390
- ` : ''}
1391
- ${node.language ? `
1392
- <div class="detail-row">
1393
- <span class="detail-label">Language:</span> ${node.language}
1394
- </div>
1395
- ` : ''}
1396
- </div>
1397
- `;
1398
-
1399
- container.innerHTML = importHtml;
1400
- }
1401
-
1402
- function showCodeContent(node, container) {
1403
- // Show code for function, class, method, or code chunks
1404
- let html = '';
1405
-
1406
- if (node.docstring) {
1407
- html += `
1408
- <div style="margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
1409
- <div style="font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;">DOCSTRING</div>
1410
- <pre style="margin: 0; padding: 0; background: transparent; border: none;"><code>${escapeHtml(node.docstring)}</code></pre>
1411
- </div>
1412
- `;
1413
- }
1414
-
1415
- if (node.content) {
1416
- html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
1417
- } else {
1418
- html += '<p style="color: #8b949e;">No content available</p>';
1419
- }
1420
-
1421
- container.innerHTML = html;
1422
- }
1423
-
1424
- function escapeHtml(text) {
1425
- const div = document.createElement('div');
1426
- div.textContent = text;
1427
- return div.innerHTML;
1428
- }
1429
-
1430
- function closeContentPane() {
1431
- const pane = document.getElementById('content-pane');
1432
- pane.classList.remove('visible');
1433
-
1434
- // Remove highlight
1435
- highlightedNode = null;
1436
- renderGraph();
1437
- }
1438
-
1439
- // Auto-load graph data on page load
1440
- window.addEventListener('DOMContentLoaded', () => {
1441
- const loadingEl = document.getElementById('loading');
1442
-
1443
- fetch("chunk-graph.json")
1444
- .then(response => {
1445
- if (!response.ok) {
1446
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1447
- }
1448
- return response.json();
1449
- })
1450
- .then(data => {
1451
- loadingEl.innerHTML = '<label style="color: #238636;">✓ Graph loaded successfully</label>';
1452
- setTimeout(() => loadingEl.style.display = 'none', 2000);
1453
- visualizeGraph(data);
1454
- })
1455
- .catch(err => {
1456
- loadingEl.innerHTML = `<label style="color: #f85149;">✗ Failed to load graph data</label><br>` +
1457
- `<small style="color: #8b949e;">${err.message}</small><br>` +
1458
- `<small style="color: #8b949e;">Run: mcp-vector-search visualize export</small>`;
1459
- console.error("Failed to load graph:", err);
1460
- });
1461
- });
1462
- </script>
1463
- </body>
1464
- </html>"""
1465
-
1466
- with open(html_file, "w") as f:
1467
- f.write(html_content)