mcp-vector-search 1.0.3__py3-none-any.whl → 1.1.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +48 -1
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +35 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +74 -1
- mcp_vector_search/analysis/reporters/__init__.py +3 -1
- mcp_vector_search/analysis/reporters/console.py +424 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +665 -11
- mcp_vector_search/cli/commands/chat.py +193 -0
- mcp_vector_search/cli/commands/index.py +600 -2
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/search.py +194 -1
- mcp_vector_search/cli/commands/setup.py +64 -13
- mcp_vector_search/cli/commands/status.py +302 -3
- mcp_vector_search/cli/commands/visualize/cli.py +26 -10
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
- mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
- mcp_vector_search/cli/commands/visualize/server.py +304 -15
- mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
- mcp_vector_search/cli/didyoumean.py +5 -0
- mcp_vector_search/cli/main.py +16 -5
- mcp_vector_search/cli/output.py +134 -5
- mcp_vector_search/config/thresholds.py +89 -1
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/database.py +39 -2
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/indexer.py +445 -84
- mcp_vector_search/core/llm_client.py +9 -4
- mcp_vector_search/core/models.py +88 -1
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/search.py +1 -1
- mcp_vector_search/mcp/server.py +795 -4
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/gitignore.py +0 -3
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
- mcp_vector_search/cli/commands/visualize.py.original +0 -2536
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,2536 +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
|
-
code_only: bool = typer.Option(
|
|
39
|
-
False,
|
|
40
|
-
"--code-only",
|
|
41
|
-
help="Exclude documentation chunks (text, comment, docstring)",
|
|
42
|
-
),
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Export chunk relationships as JSON for D3.js visualization.
|
|
45
|
-
|
|
46
|
-
Examples:
|
|
47
|
-
# Export all chunks
|
|
48
|
-
mcp-vector-search visualize export
|
|
49
|
-
|
|
50
|
-
# Export from specific file
|
|
51
|
-
mcp-vector-search visualize export --file src/main.py
|
|
52
|
-
|
|
53
|
-
# Custom output location
|
|
54
|
-
mcp-vector-search visualize export -o graph.json
|
|
55
|
-
|
|
56
|
-
# Export only code chunks (exclude documentation)
|
|
57
|
-
mcp-vector-search visualize export --code-only
|
|
58
|
-
"""
|
|
59
|
-
asyncio.run(_export_chunks(output, file_path, code_only))
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def _export_chunks(
|
|
63
|
-
output: Path, file_filter: str | None, code_only: bool = False
|
|
64
|
-
) -> None:
|
|
65
|
-
"""Export chunk relationship data.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
output: Path to output JSON file
|
|
69
|
-
file_filter: Optional file pattern to filter chunks
|
|
70
|
-
code_only: If True, exclude documentation chunks (text, comment, docstring)
|
|
71
|
-
"""
|
|
72
|
-
try:
|
|
73
|
-
# Load project
|
|
74
|
-
project_manager = ProjectManager(Path.cwd())
|
|
75
|
-
|
|
76
|
-
if not project_manager.is_initialized():
|
|
77
|
-
console.print(
|
|
78
|
-
"[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
|
|
79
|
-
)
|
|
80
|
-
raise typer.Exit(1)
|
|
81
|
-
|
|
82
|
-
config = project_manager.load_config()
|
|
83
|
-
|
|
84
|
-
# Get database
|
|
85
|
-
embedding_function, _ = create_embedding_function(config.embedding_model)
|
|
86
|
-
database = ChromaVectorDatabase(
|
|
87
|
-
persist_directory=config.index_path,
|
|
88
|
-
embedding_function=embedding_function,
|
|
89
|
-
)
|
|
90
|
-
await database.initialize()
|
|
91
|
-
|
|
92
|
-
# Get all chunks with metadata
|
|
93
|
-
console.print("[cyan]Fetching chunks from database...[/cyan]")
|
|
94
|
-
chunks = await database.get_all_chunks()
|
|
95
|
-
|
|
96
|
-
# Store database reference for semantic search
|
|
97
|
-
# We'll pass it in metadata for the visualization
|
|
98
|
-
graph_database = database
|
|
99
|
-
|
|
100
|
-
if len(chunks) == 0:
|
|
101
|
-
console.print(
|
|
102
|
-
"[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]"
|
|
103
|
-
)
|
|
104
|
-
raise typer.Exit(1)
|
|
105
|
-
|
|
106
|
-
console.print(f"[green]✓[/green] Retrieved {len(chunks)} chunks")
|
|
107
|
-
|
|
108
|
-
# Apply file filter if specified
|
|
109
|
-
if file_filter:
|
|
110
|
-
from fnmatch import fnmatch
|
|
111
|
-
|
|
112
|
-
chunks = [c for c in chunks if fnmatch(str(c.file_path), file_filter)]
|
|
113
|
-
console.print(
|
|
114
|
-
f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]"
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Apply code-only filter if requested
|
|
118
|
-
if code_only:
|
|
119
|
-
original_count = len(chunks)
|
|
120
|
-
chunks = [
|
|
121
|
-
c
|
|
122
|
-
for c in chunks
|
|
123
|
-
if c.chunk_type not in ["text", "comment", "docstring"]
|
|
124
|
-
]
|
|
125
|
-
filtered_count = len(chunks)
|
|
126
|
-
console.print(
|
|
127
|
-
f"[dim]Filtered out {original_count - filtered_count} documentation chunks "
|
|
128
|
-
f"({original_count} → {filtered_count} chunks)[/dim]"
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
# Collect subprojects for monorepo support
|
|
132
|
-
subprojects = {}
|
|
133
|
-
for chunk in chunks:
|
|
134
|
-
if chunk.subproject_name and chunk.subproject_name not in subprojects:
|
|
135
|
-
subprojects[chunk.subproject_name] = {
|
|
136
|
-
"name": chunk.subproject_name,
|
|
137
|
-
"path": chunk.subproject_path,
|
|
138
|
-
"color": _get_subproject_color(
|
|
139
|
-
chunk.subproject_name, len(subprojects)
|
|
140
|
-
),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
# Build graph data structure
|
|
144
|
-
nodes = []
|
|
145
|
-
links = []
|
|
146
|
-
chunk_id_map = {} # Map chunk IDs to array indices
|
|
147
|
-
file_nodes = {} # Track file nodes by path
|
|
148
|
-
dir_nodes = {} # Track directory nodes by path
|
|
149
|
-
|
|
150
|
-
# Add subproject root nodes for monorepos
|
|
151
|
-
if subprojects:
|
|
152
|
-
console.print(
|
|
153
|
-
f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]"
|
|
154
|
-
)
|
|
155
|
-
for sp_name, sp_data in subprojects.items():
|
|
156
|
-
node = {
|
|
157
|
-
"id": f"subproject_{sp_name}",
|
|
158
|
-
"name": sp_name,
|
|
159
|
-
"type": "subproject",
|
|
160
|
-
"file_path": sp_data["path"] or "",
|
|
161
|
-
"start_line": 0,
|
|
162
|
-
"end_line": 0,
|
|
163
|
-
"complexity": 0,
|
|
164
|
-
"color": sp_data["color"],
|
|
165
|
-
"depth": 0,
|
|
166
|
-
}
|
|
167
|
-
nodes.append(node)
|
|
168
|
-
|
|
169
|
-
# Load directory index for enhanced directory metadata
|
|
170
|
-
console.print("[cyan]Loading directory index...[/cyan]")
|
|
171
|
-
from ...core.directory_index import DirectoryIndex
|
|
172
|
-
|
|
173
|
-
dir_index_path = (
|
|
174
|
-
project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
|
|
175
|
-
)
|
|
176
|
-
dir_index = DirectoryIndex(dir_index_path)
|
|
177
|
-
dir_index.load()
|
|
178
|
-
|
|
179
|
-
# Create directory nodes from directory index
|
|
180
|
-
console.print(
|
|
181
|
-
f"[green]✓[/green] Loaded {len(dir_index.directories)} directories"
|
|
182
|
-
)
|
|
183
|
-
for dir_path_str, directory in dir_index.directories.items():
|
|
184
|
-
dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
|
|
185
|
-
dir_nodes[dir_path_str] = {
|
|
186
|
-
"id": dir_id,
|
|
187
|
-
"name": directory.name,
|
|
188
|
-
"type": "directory",
|
|
189
|
-
"file_path": dir_path_str,
|
|
190
|
-
"start_line": 0,
|
|
191
|
-
"end_line": 0,
|
|
192
|
-
"complexity": 0,
|
|
193
|
-
"depth": directory.depth,
|
|
194
|
-
"dir_path": dir_path_str,
|
|
195
|
-
"file_count": directory.file_count,
|
|
196
|
-
"subdirectory_count": directory.subdirectory_count,
|
|
197
|
-
"total_chunks": directory.total_chunks,
|
|
198
|
-
"languages": directory.languages or {},
|
|
199
|
-
"is_package": directory.is_package,
|
|
200
|
-
"last_modified": directory.last_modified,
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# Create file nodes from chunks
|
|
204
|
-
for chunk in chunks:
|
|
205
|
-
file_path_str = str(chunk.file_path)
|
|
206
|
-
file_path = Path(file_path_str)
|
|
207
|
-
|
|
208
|
-
# Create file node with parent directory reference
|
|
209
|
-
if file_path_str not in file_nodes:
|
|
210
|
-
file_id = f"file_{hash(file_path_str) & 0xFFFFFFFF:08x}"
|
|
211
|
-
|
|
212
|
-
# Convert absolute path to relative path for parent directory lookup
|
|
213
|
-
try:
|
|
214
|
-
relative_file_path = file_path.relative_to(
|
|
215
|
-
project_manager.project_root
|
|
216
|
-
)
|
|
217
|
-
parent_dir = relative_file_path.parent
|
|
218
|
-
# Use relative path for parent directory (matches directory_index)
|
|
219
|
-
parent_dir_str = (
|
|
220
|
-
str(parent_dir) if parent_dir != Path(".") else None
|
|
221
|
-
)
|
|
222
|
-
except ValueError:
|
|
223
|
-
# File is outside project root
|
|
224
|
-
parent_dir_str = None
|
|
225
|
-
|
|
226
|
-
# Look up parent directory ID from dir_nodes (must match exactly)
|
|
227
|
-
parent_dir_id = None
|
|
228
|
-
if parent_dir_str and parent_dir_str in dir_nodes:
|
|
229
|
-
parent_dir_id = dir_nodes[parent_dir_str]["id"]
|
|
230
|
-
|
|
231
|
-
file_nodes[file_path_str] = {
|
|
232
|
-
"id": file_id,
|
|
233
|
-
"name": file_path.name,
|
|
234
|
-
"type": "file",
|
|
235
|
-
"file_path": file_path_str,
|
|
236
|
-
"start_line": 0,
|
|
237
|
-
"end_line": 0,
|
|
238
|
-
"complexity": 0,
|
|
239
|
-
"depth": len(file_path.parts) - 1,
|
|
240
|
-
"parent_dir_id": parent_dir_id,
|
|
241
|
-
"parent_dir_path": parent_dir_str,
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
# Add directory nodes to graph
|
|
245
|
-
for dir_node in dir_nodes.values():
|
|
246
|
-
nodes.append(dir_node)
|
|
247
|
-
|
|
248
|
-
# Add file nodes to graph
|
|
249
|
-
for file_node in file_nodes.values():
|
|
250
|
-
nodes.append(file_node)
|
|
251
|
-
|
|
252
|
-
# Compute semantic relationships for code chunks
|
|
253
|
-
console.print("[cyan]Computing semantic relationships...[/cyan]")
|
|
254
|
-
code_chunks = [
|
|
255
|
-
c for c in chunks if c.chunk_type in ["function", "method", "class"]
|
|
256
|
-
]
|
|
257
|
-
semantic_links = []
|
|
258
|
-
|
|
259
|
-
# Pre-compute top 5 semantic relationships for each code chunk
|
|
260
|
-
for i, chunk in enumerate(code_chunks):
|
|
261
|
-
if i % 20 == 0: # Progress indicator every 20 chunks
|
|
262
|
-
console.print(f"[dim]Processed {i}/{len(code_chunks)} chunks[/dim]")
|
|
263
|
-
|
|
264
|
-
try:
|
|
265
|
-
# Search for similar chunks using the chunk's content
|
|
266
|
-
similar_results = await graph_database.search(
|
|
267
|
-
query=chunk.content[:500], # Use first 500 chars for query
|
|
268
|
-
limit=6, # Get 6 (exclude self = 5)
|
|
269
|
-
similarity_threshold=0.3, # Lower threshold to catch more relationships
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
# Filter out self and create semantic links
|
|
273
|
-
for result in similar_results:
|
|
274
|
-
# Construct target chunk_id from file_path and line numbers
|
|
275
|
-
# This matches the chunk ID construction in the database
|
|
276
|
-
target_chunk = next(
|
|
277
|
-
(
|
|
278
|
-
c
|
|
279
|
-
for c in chunks
|
|
280
|
-
if str(c.file_path) == str(result.file_path)
|
|
281
|
-
and c.start_line == result.start_line
|
|
282
|
-
and c.end_line == result.end_line
|
|
283
|
-
),
|
|
284
|
-
None,
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
if not target_chunk:
|
|
288
|
-
continue
|
|
289
|
-
|
|
290
|
-
target_chunk_id = target_chunk.chunk_id or target_chunk.id
|
|
291
|
-
|
|
292
|
-
# Skip self-references
|
|
293
|
-
if target_chunk_id == (chunk.chunk_id or chunk.id):
|
|
294
|
-
continue
|
|
295
|
-
|
|
296
|
-
# Add semantic link with similarity score (use similarity_score from SearchResult)
|
|
297
|
-
if result.similarity_score >= 0.2:
|
|
298
|
-
semantic_links.append(
|
|
299
|
-
{
|
|
300
|
-
"source": chunk.chunk_id or chunk.id,
|
|
301
|
-
"target": target_chunk_id,
|
|
302
|
-
"type": "semantic",
|
|
303
|
-
"similarity": result.similarity_score,
|
|
304
|
-
}
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
# Only keep top 5
|
|
308
|
-
if (
|
|
309
|
-
len(
|
|
310
|
-
[
|
|
311
|
-
link
|
|
312
|
-
for link in semantic_links
|
|
313
|
-
if link["source"] == (chunk.chunk_id or chunk.id)
|
|
314
|
-
]
|
|
315
|
-
)
|
|
316
|
-
>= 5
|
|
317
|
-
):
|
|
318
|
-
break
|
|
319
|
-
|
|
320
|
-
except Exception as e:
|
|
321
|
-
logger.debug(
|
|
322
|
-
f"Failed to compute semantic relationships for {chunk.chunk_id}: {e}"
|
|
323
|
-
)
|
|
324
|
-
continue
|
|
325
|
-
|
|
326
|
-
console.print(
|
|
327
|
-
f"[green]✓[/green] Computed {len(semantic_links)} semantic relationships"
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# Compute external caller relationships
|
|
331
|
-
console.print("[cyan]Computing external caller relationships...[/cyan]")
|
|
332
|
-
caller_map = {} # Map chunk_id -> list of caller info
|
|
333
|
-
|
|
334
|
-
for chunk in code_chunks:
|
|
335
|
-
chunk_id = chunk.chunk_id or chunk.id
|
|
336
|
-
file_path = str(chunk.file_path)
|
|
337
|
-
function_name = chunk.function_name or chunk.class_name
|
|
338
|
-
|
|
339
|
-
if not function_name:
|
|
340
|
-
continue
|
|
341
|
-
|
|
342
|
-
# Search for other chunks that reference this function/class name
|
|
343
|
-
for other_chunk in chunks:
|
|
344
|
-
other_file_path = str(other_chunk.file_path)
|
|
345
|
-
|
|
346
|
-
# Only track EXTERNAL callers (different file)
|
|
347
|
-
if other_file_path == file_path:
|
|
348
|
-
continue
|
|
349
|
-
|
|
350
|
-
# Check if the other chunk's content mentions this function/class
|
|
351
|
-
if function_name in other_chunk.content:
|
|
352
|
-
other_chunk_id = other_chunk.chunk_id or other_chunk.id
|
|
353
|
-
other_name = (
|
|
354
|
-
other_chunk.function_name
|
|
355
|
-
or other_chunk.class_name
|
|
356
|
-
or f"L{other_chunk.start_line}"
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
if chunk_id not in caller_map:
|
|
360
|
-
caller_map[chunk_id] = []
|
|
361
|
-
|
|
362
|
-
# Store caller information
|
|
363
|
-
caller_map[chunk_id].append(
|
|
364
|
-
{
|
|
365
|
-
"file": other_file_path,
|
|
366
|
-
"chunk_id": other_chunk_id,
|
|
367
|
-
"name": other_name,
|
|
368
|
-
"type": other_chunk.chunk_type,
|
|
369
|
-
}
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
# Count total caller relationships
|
|
373
|
-
total_callers = sum(len(callers) for callers in caller_map.values())
|
|
374
|
-
console.print(
|
|
375
|
-
f"[green]✓[/green] Found {total_callers} external caller relationships"
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
# Detect circular dependencies in caller relationships
|
|
379
|
-
console.print("[cyan]Detecting circular dependencies...[/cyan]")
|
|
380
|
-
|
|
381
|
-
def detect_cycles(nodes_list, links_list):
|
|
382
|
-
"""Detect cycles in the call graph using DFS.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
List of cycles found, where each cycle is a list of node IDs in the cycle path.
|
|
386
|
-
"""
|
|
387
|
-
cycles_found = []
|
|
388
|
-
visited = set()
|
|
389
|
-
|
|
390
|
-
def dfs(node_id, path, path_set):
|
|
391
|
-
"""DFS traversal to detect cycles.
|
|
392
|
-
|
|
393
|
-
Args:
|
|
394
|
-
node_id: Current node ID being visited
|
|
395
|
-
path: List of node IDs in current path (for cycle reconstruction)
|
|
396
|
-
path_set: Set of node IDs in current path (for O(1) cycle detection)
|
|
397
|
-
|
|
398
|
-
Returns:
|
|
399
|
-
True if cycle detected in this path
|
|
400
|
-
"""
|
|
401
|
-
if node_id in path_set:
|
|
402
|
-
# Found a cycle! Record the cycle path
|
|
403
|
-
cycle_start = path.index(node_id)
|
|
404
|
-
cycle_nodes = path[cycle_start:]
|
|
405
|
-
cycles_found.append(cycle_nodes)
|
|
406
|
-
return True
|
|
407
|
-
|
|
408
|
-
if node_id in visited:
|
|
409
|
-
return False
|
|
410
|
-
|
|
411
|
-
path.append(node_id)
|
|
412
|
-
path_set.add(node_id)
|
|
413
|
-
visited.add(node_id)
|
|
414
|
-
|
|
415
|
-
# Follow caller links (external callers create directed edges)
|
|
416
|
-
if node_id in caller_map:
|
|
417
|
-
for caller_info in caller_map[node_id]:
|
|
418
|
-
caller_id = caller_info["chunk_id"]
|
|
419
|
-
dfs(caller_id, path, path_set)
|
|
420
|
-
|
|
421
|
-
path.pop()
|
|
422
|
-
path_set.remove(node_id)
|
|
423
|
-
return False
|
|
424
|
-
|
|
425
|
-
# Run DFS from each unvisited node
|
|
426
|
-
for node in nodes_list:
|
|
427
|
-
if node.chunk_id or node.id not in visited:
|
|
428
|
-
chunk_id = node.chunk_id or node.id
|
|
429
|
-
dfs(chunk_id, [], set())
|
|
430
|
-
|
|
431
|
-
return cycles_found
|
|
432
|
-
|
|
433
|
-
# Detect cycles
|
|
434
|
-
cycles = detect_cycles(chunks, [])
|
|
435
|
-
|
|
436
|
-
# Mark cycle links
|
|
437
|
-
cycle_links = []
|
|
438
|
-
if cycles:
|
|
439
|
-
console.print(
|
|
440
|
-
f"[yellow]⚠ Found {len(cycles)} circular dependencies[/yellow]"
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
# For each cycle, create links marking the cycle
|
|
444
|
-
for cycle in cycles:
|
|
445
|
-
# Create links for the cycle path: A → B → C → A
|
|
446
|
-
for i in range(len(cycle)):
|
|
447
|
-
source = cycle[i]
|
|
448
|
-
target = cycle[(i + 1) % len(cycle)] # Wrap around to form cycle
|
|
449
|
-
cycle_links.append(
|
|
450
|
-
{
|
|
451
|
-
"source": source,
|
|
452
|
-
"target": target,
|
|
453
|
-
"type": "caller",
|
|
454
|
-
"is_cycle": True,
|
|
455
|
-
}
|
|
456
|
-
)
|
|
457
|
-
else:
|
|
458
|
-
console.print("[green]✓[/green] No circular dependencies detected")
|
|
459
|
-
|
|
460
|
-
# Add chunk nodes
|
|
461
|
-
for chunk in chunks:
|
|
462
|
-
chunk_id = chunk.chunk_id or chunk.id
|
|
463
|
-
node = {
|
|
464
|
-
"id": chunk_id,
|
|
465
|
-
"name": chunk.function_name
|
|
466
|
-
or chunk.class_name
|
|
467
|
-
or f"L{chunk.start_line}",
|
|
468
|
-
"type": chunk.chunk_type,
|
|
469
|
-
"file_path": str(chunk.file_path),
|
|
470
|
-
"start_line": chunk.start_line,
|
|
471
|
-
"end_line": chunk.end_line,
|
|
472
|
-
"complexity": chunk.complexity_score,
|
|
473
|
-
"parent_id": chunk.parent_chunk_id,
|
|
474
|
-
"depth": chunk.chunk_depth,
|
|
475
|
-
"content": chunk.content, # Add content for code viewer
|
|
476
|
-
"docstring": chunk.docstring,
|
|
477
|
-
"language": chunk.language,
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
# Add caller information if available
|
|
481
|
-
if chunk_id in caller_map:
|
|
482
|
-
node["callers"] = caller_map[chunk_id]
|
|
483
|
-
|
|
484
|
-
# Add subproject info for monorepos
|
|
485
|
-
if chunk.subproject_name:
|
|
486
|
-
node["subproject"] = chunk.subproject_name
|
|
487
|
-
node["color"] = subprojects[chunk.subproject_name]["color"]
|
|
488
|
-
|
|
489
|
-
nodes.append(node)
|
|
490
|
-
chunk_id_map[node["id"]] = len(nodes) - 1
|
|
491
|
-
|
|
492
|
-
# Link directories to their parent directories (hierarchical structure)
|
|
493
|
-
for dir_path_str, dir_info in dir_index.directories.items():
|
|
494
|
-
if dir_info.parent_path:
|
|
495
|
-
parent_path_str = str(dir_info.parent_path)
|
|
496
|
-
if parent_path_str in dir_nodes:
|
|
497
|
-
parent_dir_id = f"dir_{hash(parent_path_str) & 0xFFFFFFFF:08x}"
|
|
498
|
-
child_dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
|
|
499
|
-
links.append(
|
|
500
|
-
{
|
|
501
|
-
"source": parent_dir_id,
|
|
502
|
-
"target": child_dir_id,
|
|
503
|
-
"type": "dir_hierarchy",
|
|
504
|
-
}
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
# Link directories to subprojects in monorepos (simple flat structure)
|
|
508
|
-
if subprojects:
|
|
509
|
-
for dir_path_str, dir_node in dir_nodes.items():
|
|
510
|
-
for sp_name, sp_data in subprojects.items():
|
|
511
|
-
if dir_path_str.startswith(sp_data.get("path", "")):
|
|
512
|
-
links.append(
|
|
513
|
-
{
|
|
514
|
-
"source": f"subproject_{sp_name}",
|
|
515
|
-
"target": dir_node["id"],
|
|
516
|
-
"type": "dir_containment",
|
|
517
|
-
}
|
|
518
|
-
)
|
|
519
|
-
break
|
|
520
|
-
|
|
521
|
-
# Link files to their parent directories
|
|
522
|
-
for _file_path_str, file_node in file_nodes.items():
|
|
523
|
-
if file_node.get("parent_dir_id"):
|
|
524
|
-
links.append(
|
|
525
|
-
{
|
|
526
|
-
"source": file_node["parent_dir_id"],
|
|
527
|
-
"target": file_node["id"],
|
|
528
|
-
"type": "dir_containment",
|
|
529
|
-
}
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
# Build hierarchical links from parent-child relationships
|
|
533
|
-
for chunk in chunks:
|
|
534
|
-
chunk_id = chunk.chunk_id or chunk.id
|
|
535
|
-
file_path = str(chunk.file_path)
|
|
536
|
-
|
|
537
|
-
# Link chunk to its file node if it has no parent (top-level chunks)
|
|
538
|
-
if not chunk.parent_chunk_id and file_path in file_nodes:
|
|
539
|
-
links.append(
|
|
540
|
-
{
|
|
541
|
-
"source": file_nodes[file_path]["id"],
|
|
542
|
-
"target": chunk_id,
|
|
543
|
-
"type": "file_containment",
|
|
544
|
-
}
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
# Link to subproject root if in monorepo
|
|
548
|
-
if chunk.subproject_name and not chunk.parent_chunk_id:
|
|
549
|
-
links.append(
|
|
550
|
-
{
|
|
551
|
-
"source": f"subproject_{chunk.subproject_name}",
|
|
552
|
-
"target": chunk_id,
|
|
553
|
-
}
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
# Link to parent chunk
|
|
557
|
-
if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
|
|
558
|
-
links.append(
|
|
559
|
-
{
|
|
560
|
-
"source": chunk.parent_chunk_id,
|
|
561
|
-
"target": chunk_id,
|
|
562
|
-
}
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
# Add semantic relationship links
|
|
566
|
-
links.extend(semantic_links)
|
|
567
|
-
|
|
568
|
-
# Add cycle links
|
|
569
|
-
links.extend(cycle_links)
|
|
570
|
-
|
|
571
|
-
# Parse inter-project dependencies for monorepos
|
|
572
|
-
if subprojects:
|
|
573
|
-
console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
|
|
574
|
-
dep_links = _parse_project_dependencies(
|
|
575
|
-
project_manager.project_root, subprojects
|
|
576
|
-
)
|
|
577
|
-
links.extend(dep_links)
|
|
578
|
-
if dep_links:
|
|
579
|
-
console.print(
|
|
580
|
-
f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies"
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
# Get stats
|
|
584
|
-
stats = await database.get_stats()
|
|
585
|
-
|
|
586
|
-
# Build final graph data
|
|
587
|
-
graph_data = {
|
|
588
|
-
"nodes": nodes,
|
|
589
|
-
"links": links,
|
|
590
|
-
"metadata": {
|
|
591
|
-
"total_chunks": len(chunks),
|
|
592
|
-
"total_files": stats.total_files,
|
|
593
|
-
"languages": stats.languages,
|
|
594
|
-
"is_monorepo": len(subprojects) > 0,
|
|
595
|
-
"subprojects": list(subprojects.keys()) if subprojects else [],
|
|
596
|
-
},
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
# Write to file
|
|
600
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
601
|
-
with open(output, "w") as f:
|
|
602
|
-
json.dump(graph_data, f, indent=2)
|
|
603
|
-
|
|
604
|
-
await database.close()
|
|
605
|
-
|
|
606
|
-
console.print()
|
|
607
|
-
cycle_warning = f"[yellow]Cycles: {len(cycles)} ⚠️[/yellow]\n" if cycles else ""
|
|
608
|
-
console.print(
|
|
609
|
-
Panel.fit(
|
|
610
|
-
f"[green]✓[/green] Exported graph data to [cyan]{output}[/cyan]\n\n"
|
|
611
|
-
f"Nodes: {len(graph_data['nodes'])}\n"
|
|
612
|
-
f"Links: {len(graph_data['links'])}\n"
|
|
613
|
-
f"{cycle_warning}"
|
|
614
|
-
f"{'Subprojects: ' + str(len(subprojects)) if subprojects else ''}\n\n"
|
|
615
|
-
f"[dim]Next: Run 'mcp-vector-search visualize serve' to view[/dim]",
|
|
616
|
-
title="Export Complete",
|
|
617
|
-
border_style="green",
|
|
618
|
-
)
|
|
619
|
-
)
|
|
620
|
-
|
|
621
|
-
except Exception as e:
|
|
622
|
-
logger.error(f"Export failed: {e}")
|
|
623
|
-
console.print(f"[red]✗ Export failed: {e}[/red]")
|
|
624
|
-
raise typer.Exit(1)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
def _get_subproject_color(subproject_name: str, index: int) -> str:
|
|
628
|
-
"""Get a consistent color for a subproject."""
|
|
629
|
-
# Color palette for subprojects (GitHub-style colors)
|
|
630
|
-
colors = [
|
|
631
|
-
"#238636", # Green
|
|
632
|
-
"#1f6feb", # Blue
|
|
633
|
-
"#d29922", # Yellow
|
|
634
|
-
"#8957e5", # Purple
|
|
635
|
-
"#da3633", # Red
|
|
636
|
-
"#bf8700", # Orange
|
|
637
|
-
"#1a7f37", # Dark green
|
|
638
|
-
"#0969da", # Dark blue
|
|
639
|
-
]
|
|
640
|
-
return colors[index % len(colors)]
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[dict]:
|
|
644
|
-
"""Parse package.json files to find inter-project dependencies.
|
|
645
|
-
|
|
646
|
-
Args:
|
|
647
|
-
project_root: Root directory of the monorepo
|
|
648
|
-
subprojects: Dictionary of subproject information
|
|
649
|
-
|
|
650
|
-
Returns:
|
|
651
|
-
List of dependency links between subprojects
|
|
652
|
-
"""
|
|
653
|
-
dependency_links = []
|
|
654
|
-
|
|
655
|
-
for sp_name, sp_data in subprojects.items():
|
|
656
|
-
package_json = project_root / sp_data["path"] / "package.json"
|
|
657
|
-
|
|
658
|
-
if not package_json.exists():
|
|
659
|
-
continue
|
|
660
|
-
|
|
661
|
-
try:
|
|
662
|
-
with open(package_json) as f:
|
|
663
|
-
package_data = json.load(f)
|
|
664
|
-
|
|
665
|
-
# Check all dependency types
|
|
666
|
-
all_deps = {}
|
|
667
|
-
for dep_type in ["dependencies", "devDependencies", "peerDependencies"]:
|
|
668
|
-
if dep_type in package_data:
|
|
669
|
-
all_deps.update(package_data[dep_type])
|
|
670
|
-
|
|
671
|
-
# Find dependencies on other subprojects
|
|
672
|
-
for dep_name in all_deps.keys():
|
|
673
|
-
# Check if this dependency is another subproject
|
|
674
|
-
for other_sp_name in subprojects.keys():
|
|
675
|
-
if other_sp_name != sp_name and dep_name == other_sp_name:
|
|
676
|
-
# Found inter-project dependency
|
|
677
|
-
dependency_links.append(
|
|
678
|
-
{
|
|
679
|
-
"source": f"subproject_{sp_name}",
|
|
680
|
-
"target": f"subproject_{other_sp_name}",
|
|
681
|
-
"type": "dependency",
|
|
682
|
-
}
|
|
683
|
-
)
|
|
684
|
-
|
|
685
|
-
except Exception as e:
|
|
686
|
-
logger.debug(f"Failed to parse {package_json}: {e}")
|
|
687
|
-
continue
|
|
688
|
-
|
|
689
|
-
return dependency_links
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
@app.command()
|
|
693
|
-
def serve(
|
|
694
|
-
port: int = typer.Option(
|
|
695
|
-
8080, "--port", "-p", help="Port for visualization server"
|
|
696
|
-
),
|
|
697
|
-
graph_file: Path = typer.Option(
|
|
698
|
-
Path("chunk-graph.json"),
|
|
699
|
-
"--graph",
|
|
700
|
-
"-g",
|
|
701
|
-
help="Graph JSON file to visualize",
|
|
702
|
-
),
|
|
703
|
-
code_only: bool = typer.Option(
|
|
704
|
-
False,
|
|
705
|
-
"--code-only",
|
|
706
|
-
help="Exclude documentation chunks (text, comment, docstring)",
|
|
707
|
-
),
|
|
708
|
-
) -> None:
|
|
709
|
-
"""Start local HTTP server for D3.js visualization.
|
|
710
|
-
|
|
711
|
-
Examples:
|
|
712
|
-
# Start server on default port 8080
|
|
713
|
-
mcp-vector-search visualize serve
|
|
714
|
-
|
|
715
|
-
# Custom port
|
|
716
|
-
mcp-vector-search visualize serve --port 3000
|
|
717
|
-
|
|
718
|
-
# Custom graph file
|
|
719
|
-
mcp-vector-search visualize serve --graph my-graph.json
|
|
720
|
-
|
|
721
|
-
# Serve with code-only filter
|
|
722
|
-
mcp-vector-search visualize serve --code-only
|
|
723
|
-
"""
|
|
724
|
-
import http.server
|
|
725
|
-
import os
|
|
726
|
-
import socket
|
|
727
|
-
import socketserver
|
|
728
|
-
import webbrowser
|
|
729
|
-
|
|
730
|
-
# Find free port in range 8080-8099
|
|
731
|
-
def find_free_port(start_port: int = 8080, end_port: int = 8099) -> int:
|
|
732
|
-
"""Find a free port in the given range."""
|
|
733
|
-
for test_port in range(start_port, end_port + 1):
|
|
734
|
-
try:
|
|
735
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
736
|
-
s.bind(("", test_port))
|
|
737
|
-
return test_port
|
|
738
|
-
except OSError:
|
|
739
|
-
continue
|
|
740
|
-
raise OSError(f"No free ports available in range {start_port}-{end_port}")
|
|
741
|
-
|
|
742
|
-
# Use specified port or find free one
|
|
743
|
-
if port == 8080: # Default port, try to find free one
|
|
744
|
-
try:
|
|
745
|
-
port = find_free_port(8080, 8099)
|
|
746
|
-
except OSError as e:
|
|
747
|
-
console.print(f"[red]✗ {e}[/red]")
|
|
748
|
-
raise typer.Exit(1)
|
|
749
|
-
|
|
750
|
-
# Get visualization directory - use project-local storage
|
|
751
|
-
project_manager = ProjectManager(Path.cwd())
|
|
752
|
-
if not project_manager.is_initialized():
|
|
753
|
-
console.print(
|
|
754
|
-
"[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
|
|
755
|
-
)
|
|
756
|
-
raise typer.Exit(1)
|
|
757
|
-
|
|
758
|
-
viz_dir = project_manager.project_root / ".mcp-vector-search" / "visualization"
|
|
759
|
-
|
|
760
|
-
if not viz_dir.exists():
|
|
761
|
-
console.print(
|
|
762
|
-
f"[yellow]Visualization directory not found. Creating at {viz_dir}...[/yellow]"
|
|
763
|
-
)
|
|
764
|
-
viz_dir.mkdir(parents=True, exist_ok=True)
|
|
765
|
-
|
|
766
|
-
# Always ensure index.html exists (regenerate if missing)
|
|
767
|
-
html_file = viz_dir / "index.html"
|
|
768
|
-
if not html_file.exists():
|
|
769
|
-
console.print("[yellow]Creating visualization HTML file...[/yellow]")
|
|
770
|
-
_create_visualization_html(html_file)
|
|
771
|
-
|
|
772
|
-
# Check if we need to regenerate the graph file
|
|
773
|
-
needs_regeneration = not graph_file.exists() or code_only
|
|
774
|
-
|
|
775
|
-
if graph_file.exists() and not needs_regeneration:
|
|
776
|
-
# Use existing unfiltered file
|
|
777
|
-
dest = viz_dir / "chunk-graph.json"
|
|
778
|
-
shutil.copy(graph_file, dest)
|
|
779
|
-
console.print(f"[green]✓[/green] Copied graph data to {dest}")
|
|
780
|
-
else:
|
|
781
|
-
# Generate new file (with filter if requested)
|
|
782
|
-
if graph_file.exists() and code_only:
|
|
783
|
-
console.print(
|
|
784
|
-
"[yellow]Regenerating filtered graph data (--code-only)...[/yellow]"
|
|
785
|
-
)
|
|
786
|
-
elif not graph_file.exists():
|
|
787
|
-
console.print(
|
|
788
|
-
f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
asyncio.run(_export_chunks(graph_file, None, code_only))
|
|
792
|
-
console.print()
|
|
793
|
-
|
|
794
|
-
# Copy the newly generated graph to visualization directory
|
|
795
|
-
if graph_file.exists():
|
|
796
|
-
dest = viz_dir / "chunk-graph.json"
|
|
797
|
-
shutil.copy(graph_file, dest)
|
|
798
|
-
console.print(f"[green]✓[/green] Copied graph data to {dest}")
|
|
799
|
-
|
|
800
|
-
# Change to visualization directory
|
|
801
|
-
os.chdir(viz_dir)
|
|
802
|
-
|
|
803
|
-
# Start server
|
|
804
|
-
handler = http.server.SimpleHTTPRequestHandler
|
|
805
|
-
try:
|
|
806
|
-
with socketserver.TCPServer(("", port), handler) as httpd:
|
|
807
|
-
url = f"http://localhost:{port}"
|
|
808
|
-
console.print()
|
|
809
|
-
console.print(
|
|
810
|
-
Panel.fit(
|
|
811
|
-
f"[green]✓[/green] Visualization server running\n\n"
|
|
812
|
-
f"URL: [cyan]{url}[/cyan]\n"
|
|
813
|
-
f"Directory: [dim]{viz_dir}[/dim]\n\n"
|
|
814
|
-
f"[dim]Press Ctrl+C to stop[/dim]",
|
|
815
|
-
title="Server Started",
|
|
816
|
-
border_style="green",
|
|
817
|
-
)
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
# Open browser
|
|
821
|
-
webbrowser.open(url)
|
|
822
|
-
|
|
823
|
-
try:
|
|
824
|
-
httpd.serve_forever()
|
|
825
|
-
except KeyboardInterrupt:
|
|
826
|
-
console.print("\n[yellow]Stopping server...[/yellow]")
|
|
827
|
-
|
|
828
|
-
except OSError as e:
|
|
829
|
-
if "Address already in use" in str(e):
|
|
830
|
-
console.print(
|
|
831
|
-
f"[red]✗ Port {port} is already in use. Try a different port with --port[/red]"
|
|
832
|
-
)
|
|
833
|
-
else:
|
|
834
|
-
console.print(f"[red]✗ Server error: {e}[/red]")
|
|
835
|
-
raise typer.Exit(1)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
def _create_visualization_html(html_file: Path) -> None:
|
|
839
|
-
"""Create the D3.js visualization HTML file."""
|
|
840
|
-
html_content = """<!DOCTYPE html>
|
|
841
|
-
<html>
|
|
842
|
-
<head>
|
|
843
|
-
<meta charset="utf-8">
|
|
844
|
-
<title>Code Chunk Relationship Graph</title>
|
|
845
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
846
|
-
<style>
|
|
847
|
-
body {
|
|
848
|
-
margin: 0;
|
|
849
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
850
|
-
background: #0d1117;
|
|
851
|
-
color: #c9d1d9;
|
|
852
|
-
overflow: hidden;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
#controls {
|
|
856
|
-
position: absolute;
|
|
857
|
-
top: 20px;
|
|
858
|
-
left: 20px;
|
|
859
|
-
background: rgba(13, 17, 23, 0.95);
|
|
860
|
-
border: 1px solid #30363d;
|
|
861
|
-
border-radius: 6px;
|
|
862
|
-
padding: 16px;
|
|
863
|
-
min-width: 250px;
|
|
864
|
-
max-height: 80vh;
|
|
865
|
-
overflow-y: auto;
|
|
866
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
h1 { margin: 0 0 16px 0; font-size: 18px; }
|
|
870
|
-
h3 { margin: 16px 0 8px 0; font-size: 14px; color: #8b949e; }
|
|
871
|
-
|
|
872
|
-
.control-group {
|
|
873
|
-
margin-bottom: 12px;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
label {
|
|
877
|
-
display: block;
|
|
878
|
-
margin-bottom: 4px;
|
|
879
|
-
font-size: 12px;
|
|
880
|
-
color: #8b949e;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
input[type="file"] {
|
|
884
|
-
width: 100%;
|
|
885
|
-
padding: 6px;
|
|
886
|
-
background: #161b22;
|
|
887
|
-
border: 1px solid #30363d;
|
|
888
|
-
border-radius: 6px;
|
|
889
|
-
color: #c9d1d9;
|
|
890
|
-
font-size: 12px;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
.legend {
|
|
894
|
-
font-size: 12px;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
.legend-item {
|
|
898
|
-
margin: 4px 0;
|
|
899
|
-
display: flex;
|
|
900
|
-
align-items: center;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
.legend-color {
|
|
904
|
-
width: 12px;
|
|
905
|
-
height: 12px;
|
|
906
|
-
border-radius: 50%;
|
|
907
|
-
margin-right: 8px;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
#graph {
|
|
911
|
-
width: 100vw;
|
|
912
|
-
height: 100vh;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
.node circle {
|
|
916
|
-
cursor: pointer;
|
|
917
|
-
stroke: #c9d1d9;
|
|
918
|
-
stroke-width: 2px;
|
|
919
|
-
pointer-events: all;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
.node.module circle { fill: #238636; }
|
|
923
|
-
.node.class circle { fill: #1f6feb; }
|
|
924
|
-
.node.function circle { fill: #d29922; }
|
|
925
|
-
.node.method circle { fill: #8957e5; }
|
|
926
|
-
.node.code circle { fill: #6e7681; }
|
|
927
|
-
.node.file circle {
|
|
928
|
-
fill: none;
|
|
929
|
-
stroke: #58a6ff;
|
|
930
|
-
stroke-width: 2px;
|
|
931
|
-
stroke-dasharray: 5,3;
|
|
932
|
-
opacity: 0.6;
|
|
933
|
-
}
|
|
934
|
-
.node.directory circle {
|
|
935
|
-
fill: none;
|
|
936
|
-
stroke: #79c0ff;
|
|
937
|
-
stroke-width: 2px;
|
|
938
|
-
stroke-dasharray: 3,3;
|
|
939
|
-
opacity: 0.5;
|
|
940
|
-
}
|
|
941
|
-
.node.subproject circle { fill: #da3633; stroke-width: 3px; }
|
|
942
|
-
|
|
943
|
-
/* Non-code document nodes - squares */
|
|
944
|
-
.node.docstring rect { fill: #8b949e; }
|
|
945
|
-
.node.comment rect { fill: #6e7681; }
|
|
946
|
-
.node rect {
|
|
947
|
-
cursor: pointer;
|
|
948
|
-
stroke: #c9d1d9;
|
|
949
|
-
stroke-width: 2px;
|
|
950
|
-
pointer-events: all;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/* File type icon styling */
|
|
954
|
-
.node path.file-icon {
|
|
955
|
-
fill: currentColor;
|
|
956
|
-
stroke: none;
|
|
957
|
-
pointer-events: all;
|
|
958
|
-
cursor: pointer;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
.node text {
|
|
962
|
-
font-size: 14px;
|
|
963
|
-
fill: #c9d1d9;
|
|
964
|
-
text-anchor: middle;
|
|
965
|
-
pointer-events: none;
|
|
966
|
-
user-select: none;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
.link {
|
|
970
|
-
stroke: #30363d;
|
|
971
|
-
stroke-opacity: 0.6;
|
|
972
|
-
stroke-width: 1.5px;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
.link.dependency {
|
|
976
|
-
stroke: #d29922;
|
|
977
|
-
stroke-opacity: 0.8;
|
|
978
|
-
stroke-width: 2px;
|
|
979
|
-
stroke-dasharray: 5,5;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
/* Semantic relationship links - colored by similarity */
|
|
983
|
-
.link.semantic {
|
|
984
|
-
stroke-opacity: 0.7;
|
|
985
|
-
stroke-dasharray: 4,4;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
.link.semantic.sim-high { stroke: #00ff00; stroke-width: 4px; }
|
|
989
|
-
.link.semantic.sim-medium-high { stroke: #88ff00; stroke-width: 3px; }
|
|
990
|
-
.link.semantic.sim-medium { stroke: #ffff00; stroke-width: 2.5px; }
|
|
991
|
-
.link.semantic.sim-low { stroke: #ffaa00; stroke-width: 2px; }
|
|
992
|
-
.link.semantic.sim-very-low { stroke: #ff0000; stroke-width: 1.5px; }
|
|
993
|
-
|
|
994
|
-
/* Circular dependency links - highest visual priority */
|
|
995
|
-
.link.cycle {
|
|
996
|
-
stroke: #ff4444 !important;
|
|
997
|
-
stroke-width: 3px !important;
|
|
998
|
-
stroke-dasharray: 8, 4;
|
|
999
|
-
stroke-opacity: 0.8;
|
|
1000
|
-
animation: pulse-cycle 2s infinite;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
@keyframes pulse-cycle {
|
|
1004
|
-
0%, 100% { stroke-opacity: 0.8; }
|
|
1005
|
-
50% { stroke-opacity: 1.0; }
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
.tooltip {
|
|
1009
|
-
position: absolute;
|
|
1010
|
-
padding: 12px;
|
|
1011
|
-
background: rgba(13, 17, 23, 0.95);
|
|
1012
|
-
border: 1px solid #30363d;
|
|
1013
|
-
border-radius: 6px;
|
|
1014
|
-
pointer-events: none;
|
|
1015
|
-
display: none;
|
|
1016
|
-
font-size: 12px;
|
|
1017
|
-
max-width: 300px;
|
|
1018
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
.stats {
|
|
1022
|
-
margin-top: 16px;
|
|
1023
|
-
padding-top: 16px;
|
|
1024
|
-
border-top: 1px solid #30363d;
|
|
1025
|
-
font-size: 12px;
|
|
1026
|
-
color: #8b949e;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
#content-pane {
|
|
1030
|
-
position: fixed;
|
|
1031
|
-
top: 0;
|
|
1032
|
-
right: 0;
|
|
1033
|
-
width: 600px;
|
|
1034
|
-
height: 100vh;
|
|
1035
|
-
background: rgba(13, 17, 23, 0.98);
|
|
1036
|
-
border-left: 1px solid #30363d;
|
|
1037
|
-
overflow-y: auto;
|
|
1038
|
-
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
|
|
1039
|
-
transform: translateX(100%);
|
|
1040
|
-
transition: transform 0.3s ease-in-out;
|
|
1041
|
-
z-index: 1000;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
#content-pane.visible {
|
|
1045
|
-
transform: translateX(0);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
#content-pane .pane-header {
|
|
1049
|
-
position: sticky;
|
|
1050
|
-
top: 0;
|
|
1051
|
-
background: rgba(13, 17, 23, 0.98);
|
|
1052
|
-
padding: 20px;
|
|
1053
|
-
border-bottom: 1px solid #30363d;
|
|
1054
|
-
z-index: 1;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
#content-pane .pane-title {
|
|
1058
|
-
font-size: 16px;
|
|
1059
|
-
font-weight: bold;
|
|
1060
|
-
color: #58a6ff;
|
|
1061
|
-
margin-bottom: 8px;
|
|
1062
|
-
padding-right: 30px;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
#content-pane .pane-meta {
|
|
1066
|
-
font-size: 12px;
|
|
1067
|
-
color: #8b949e;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
#content-pane .pane-footer {
|
|
1071
|
-
position: sticky;
|
|
1072
|
-
bottom: 0;
|
|
1073
|
-
background: rgba(13, 17, 23, 0.98);
|
|
1074
|
-
padding: 16px 20px;
|
|
1075
|
-
border-top: 1px solid #30363d;
|
|
1076
|
-
font-size: 11px;
|
|
1077
|
-
color: #8b949e;
|
|
1078
|
-
z-index: 1;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
#content-pane .pane-footer .footer-item {
|
|
1082
|
-
display: block;
|
|
1083
|
-
margin-bottom: 8px;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
#content-pane .pane-footer .footer-label {
|
|
1087
|
-
color: #c9d1d9;
|
|
1088
|
-
font-weight: 600;
|
|
1089
|
-
margin-right: 4px;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
#content-pane .pane-footer .footer-value {
|
|
1093
|
-
color: #8b949e;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
#content-pane .collapse-btn {
|
|
1097
|
-
position: absolute;
|
|
1098
|
-
top: 20px;
|
|
1099
|
-
right: 20px;
|
|
1100
|
-
cursor: pointer;
|
|
1101
|
-
color: #8b949e;
|
|
1102
|
-
font-size: 24px;
|
|
1103
|
-
line-height: 1;
|
|
1104
|
-
background: none;
|
|
1105
|
-
border: none;
|
|
1106
|
-
padding: 0;
|
|
1107
|
-
transition: color 0.2s;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
#content-pane .collapse-btn:hover {
|
|
1111
|
-
color: #c9d1d9;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
#content-pane .pane-content {
|
|
1115
|
-
padding: 20px;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
#content-pane pre {
|
|
1119
|
-
margin: 0;
|
|
1120
|
-
padding: 16px;
|
|
1121
|
-
background: #0d1117;
|
|
1122
|
-
border: 1px solid #30363d;
|
|
1123
|
-
border-radius: 6px;
|
|
1124
|
-
overflow-x: auto;
|
|
1125
|
-
font-size: 12px;
|
|
1126
|
-
line-height: 1.6;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
#content-pane code {
|
|
1130
|
-
color: #c9d1d9;
|
|
1131
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
#content-pane .directory-list {
|
|
1135
|
-
list-style: none;
|
|
1136
|
-
padding: 0;
|
|
1137
|
-
margin: 0;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
#content-pane .directory-list li {
|
|
1141
|
-
padding: 8px 12px;
|
|
1142
|
-
margin: 4px 0;
|
|
1143
|
-
background: #161b22;
|
|
1144
|
-
border: 1px solid #30363d;
|
|
1145
|
-
border-radius: 4px;
|
|
1146
|
-
font-size: 12px;
|
|
1147
|
-
display: flex;
|
|
1148
|
-
align-items: center;
|
|
1149
|
-
cursor: pointer;
|
|
1150
|
-
transition: background-color 0.2s;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
#content-pane .directory-list li:hover {
|
|
1154
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
#content-pane .directory-list .item-icon {
|
|
1158
|
-
margin-right: 8px;
|
|
1159
|
-
font-size: 14px;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
#content-pane .directory-list .item-type {
|
|
1163
|
-
margin-left: auto;
|
|
1164
|
-
padding-left: 12px;
|
|
1165
|
-
font-size: 10px;
|
|
1166
|
-
color: #8b949e;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
#content-pane .import-details {
|
|
1170
|
-
background: #161b22;
|
|
1171
|
-
border: 1px solid #30363d;
|
|
1172
|
-
border-radius: 6px;
|
|
1173
|
-
padding: 16px;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
#content-pane .import-details .import-statement {
|
|
1177
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
1178
|
-
font-size: 12px;
|
|
1179
|
-
color: #79c0ff;
|
|
1180
|
-
margin-bottom: 12px;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
#content-pane .import-details .detail-row {
|
|
1184
|
-
font-size: 11px;
|
|
1185
|
-
color: #8b949e;
|
|
1186
|
-
margin: 4px 0;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
#content-pane .import-details .detail-label {
|
|
1190
|
-
color: #c9d1d9;
|
|
1191
|
-
font-weight: 600;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
.node.highlighted circle,
|
|
1195
|
-
.node.highlighted rect {
|
|
1196
|
-
stroke: #f0e68c;
|
|
1197
|
-
stroke-width: 3px;
|
|
1198
|
-
filter: drop-shadow(0 0 8px #f0e68c);
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
.caller-link {
|
|
1202
|
-
color: #58a6ff;
|
|
1203
|
-
text-decoration: none;
|
|
1204
|
-
cursor: pointer;
|
|
1205
|
-
transition: color 0.2s;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
.caller-link:hover {
|
|
1209
|
-
color: #79c0ff;
|
|
1210
|
-
text-decoration: underline;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
#reset-view-btn {
|
|
1214
|
-
position: fixed;
|
|
1215
|
-
top: 20px;
|
|
1216
|
-
right: 460px;
|
|
1217
|
-
padding: 8px 16px;
|
|
1218
|
-
background: #21262d;
|
|
1219
|
-
border: 1px solid #30363d;
|
|
1220
|
-
border-radius: 6px;
|
|
1221
|
-
color: #c9d1d9;
|
|
1222
|
-
font-size: 14px;
|
|
1223
|
-
cursor: pointer;
|
|
1224
|
-
display: flex;
|
|
1225
|
-
align-items: center;
|
|
1226
|
-
gap: 8px;
|
|
1227
|
-
z-index: 100;
|
|
1228
|
-
transition: all 0.2s;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
#reset-view-btn:hover {
|
|
1232
|
-
background: #30363d;
|
|
1233
|
-
border-color: #58a6ff;
|
|
1234
|
-
transform: translateY(-1px);
|
|
1235
|
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/* Loading spinner animation */
|
|
1239
|
-
@keyframes spin {
|
|
1240
|
-
0% { transform: rotate(0deg); }
|
|
1241
|
-
100% { transform: rotate(360deg); }
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
.spinner {
|
|
1245
|
-
display: inline-block;
|
|
1246
|
-
width: 20px;
|
|
1247
|
-
height: 20px;
|
|
1248
|
-
border: 3px solid #30363d;
|
|
1249
|
-
border-top-color: #58a6ff;
|
|
1250
|
-
border-radius: 50%;
|
|
1251
|
-
animation: spin 0.8s linear infinite;
|
|
1252
|
-
margin-right: 8px;
|
|
1253
|
-
vertical-align: middle;
|
|
1254
|
-
}
|
|
1255
|
-
</style>
|
|
1256
|
-
</head>
|
|
1257
|
-
<body>
|
|
1258
|
-
<div id="controls">
|
|
1259
|
-
<h1>🔍 Code Graph</h1>
|
|
1260
|
-
|
|
1261
|
-
<div class="control-group" id="loading">
|
|
1262
|
-
<label>⏳ Loading graph data...</label>
|
|
1263
|
-
</div>
|
|
1264
|
-
|
|
1265
|
-
<h3>Legend</h3>
|
|
1266
|
-
<div class="legend">
|
|
1267
|
-
<div class="legend-item">
|
|
1268
|
-
<span class="legend-color" style="background: #da3633;"></span> Subproject
|
|
1269
|
-
</div>
|
|
1270
|
-
<div class="legend-item">
|
|
1271
|
-
📁 Directory
|
|
1272
|
-
</div>
|
|
1273
|
-
<div class="legend-item">
|
|
1274
|
-
📄 File (.py 🐍 .js/.ts 📜 .md 📝 .json/.yaml ⚙️ .sh 💻)
|
|
1275
|
-
</div>
|
|
1276
|
-
<div class="legend-item">
|
|
1277
|
-
<span class="legend-color" style="background: #238636;"></span> Module
|
|
1278
|
-
</div>
|
|
1279
|
-
<div class="legend-item">
|
|
1280
|
-
<span class="legend-color" style="background: #1f6feb;"></span> Class
|
|
1281
|
-
</div>
|
|
1282
|
-
<div class="legend-item">
|
|
1283
|
-
<span class="legend-color" style="background: #d29922;"></span> Function
|
|
1284
|
-
</div>
|
|
1285
|
-
<div class="legend-item">
|
|
1286
|
-
<span class="legend-color" style="background: #8957e5;"></span> Method
|
|
1287
|
-
</div>
|
|
1288
|
-
<div class="legend-item">
|
|
1289
|
-
<span class="legend-color" style="background: #6e7681;"></span> Code
|
|
1290
|
-
</div>
|
|
1291
|
-
<div class="legend-item" style="font-style: italic; color: #79c0ff;">
|
|
1292
|
-
<span class="legend-color" style="background: #6e7681;"></span> Import (L1)
|
|
1293
|
-
</div>
|
|
1294
|
-
<div class="legend-item">
|
|
1295
|
-
<span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
|
|
1296
|
-
</div>
|
|
1297
|
-
<div class="legend-item">
|
|
1298
|
-
<span class="legend-color" style="background: #6e7681; border-radius: 2px;"></span> Comment ▢
|
|
1299
|
-
</div>
|
|
1300
|
-
</div>
|
|
1301
|
-
|
|
1302
|
-
<h3>Relationships</h3>
|
|
1303
|
-
<div class="legend">
|
|
1304
|
-
<div class="legend-item" style="color: #ff4444;">
|
|
1305
|
-
⚠️ Circular Dependency (red pulsing)
|
|
1306
|
-
</div>
|
|
1307
|
-
<div class="legend-item" style="color: #00ff00;">
|
|
1308
|
-
— Semantic (green-yellow gradient)
|
|
1309
|
-
</div>
|
|
1310
|
-
<div class="legend-item" style="color: #30363d;">
|
|
1311
|
-
— Structural (gray)
|
|
1312
|
-
</div>
|
|
1313
|
-
</div>
|
|
1314
|
-
|
|
1315
|
-
<div id="subprojects-legend" style="display: none;">
|
|
1316
|
-
<h3>Subprojects</h3>
|
|
1317
|
-
<div class="legend" id="subprojects-list"></div>
|
|
1318
|
-
</div>
|
|
1319
|
-
|
|
1320
|
-
<div class="stats" id="stats"></div>
|
|
1321
|
-
</div>
|
|
1322
|
-
|
|
1323
|
-
<svg id="graph"></svg>
|
|
1324
|
-
<div id="tooltip" class="tooltip"></div>
|
|
1325
|
-
|
|
1326
|
-
<button id="reset-view-btn" title="Reset to home view">
|
|
1327
|
-
<span style="font-size: 18px;">🏠</span>
|
|
1328
|
-
<span>Reset View</span>
|
|
1329
|
-
</button>
|
|
1330
|
-
|
|
1331
|
-
<div id="content-pane">
|
|
1332
|
-
<div class="pane-header">
|
|
1333
|
-
<button class="collapse-btn" onclick="closeContentPane()">×</button>
|
|
1334
|
-
<div class="pane-title" id="pane-title"></div>
|
|
1335
|
-
<div class="pane-meta" id="pane-meta"></div>
|
|
1336
|
-
</div>
|
|
1337
|
-
<div class="pane-content" id="pane-content"></div>
|
|
1338
|
-
<div class="pane-footer" id="pane-footer"></div>
|
|
1339
|
-
</div>
|
|
1340
|
-
|
|
1341
|
-
<script>
|
|
1342
|
-
const width = window.innerWidth;
|
|
1343
|
-
const height = window.innerHeight;
|
|
1344
|
-
|
|
1345
|
-
// Create zoom behavior
|
|
1346
|
-
const zoom = d3.zoom().on("zoom", (event) => {
|
|
1347
|
-
g.attr("transform", event.transform);
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
const svg = d3.select("#graph")
|
|
1351
|
-
.attr("width", width)
|
|
1352
|
-
.attr("height", height)
|
|
1353
|
-
.call(zoom);
|
|
1354
|
-
|
|
1355
|
-
const g = svg.append("g");
|
|
1356
|
-
const tooltip = d3.select("#tooltip");
|
|
1357
|
-
let simulation;
|
|
1358
|
-
let allNodes = [];
|
|
1359
|
-
let allLinks = [];
|
|
1360
|
-
let visibleNodes = new Set();
|
|
1361
|
-
let collapsedNodes = new Set();
|
|
1362
|
-
let highlightedNode = null;
|
|
1363
|
-
let rootNodes = []; // NEW: Store root nodes for reset function
|
|
1364
|
-
|
|
1365
|
-
// Get file extension from path
|
|
1366
|
-
function getFileExtension(filePath) {
|
|
1367
|
-
if (!filePath) return '';
|
|
1368
|
-
const match = filePath.match(/\\.([^.]+)$/);
|
|
1369
|
-
return match ? match[1].toLowerCase() : '';
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Get SVG icon path for file type
|
|
1373
|
-
function getFileTypeIcon(node) {
|
|
1374
|
-
if (node.type === 'directory') {
|
|
1375
|
-
// Folder icon
|
|
1376
|
-
return 'M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z';
|
|
1377
|
-
}
|
|
1378
|
-
if (node.type === 'file') {
|
|
1379
|
-
const ext = getFileExtension(node.file_path);
|
|
1380
|
-
|
|
1381
|
-
// Python files
|
|
1382
|
-
if (ext === 'py') {
|
|
1383
|
-
return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z';
|
|
1384
|
-
}
|
|
1385
|
-
// JavaScript/TypeScript
|
|
1386
|
-
if (ext === 'js' || ext === 'jsx' || ext === 'ts' || ext === 'tsx') {
|
|
1387
|
-
return 'M3 3h18v18H3V3zm16 16V5H5v14h14zM7 7h2v2H7V7zm4 0h2v2h-2V7zm-4 4h2v2H7v-2zm4 0h6v2h-6v-2zm-4 4h10v2H7v-2z';
|
|
1388
|
-
}
|
|
1389
|
-
// Markdown
|
|
1390
|
-
if (ext === 'md' || ext === 'markdown') {
|
|
1391
|
-
return 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6zm10-10h-3v2h3v2h-3v2h3v2h-7V8h7v2z';
|
|
1392
|
-
}
|
|
1393
|
-
// JSON/YAML/Config files
|
|
1394
|
-
if (ext === 'json' || ext === 'yaml' || ext === 'yml' || ext === 'toml' || ext === 'ini' || ext === 'conf') {
|
|
1395
|
-
return 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm0 2l4 4h-4V4zM6 20V4h6v6h6v10H6zm4-4h4v2h-4v-2zm0-4h4v2h-4v-2z';
|
|
1396
|
-
}
|
|
1397
|
-
// Shell scripts
|
|
1398
|
-
if (ext === 'sh' || ext === 'bash' || ext === 'zsh') {
|
|
1399
|
-
return 'M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2zM6 10h8v2H6v-2z';
|
|
1400
|
-
}
|
|
1401
|
-
// Generic code file
|
|
1402
|
-
return 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm0 2l4 4h-4V4zM6 20V4h6v6h6v10H6zm3-4h6v2H9v-2zm0-4h6v2H9v-2z';
|
|
1403
|
-
}
|
|
1404
|
-
return null;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// Get color for file type icon
|
|
1408
|
-
function getFileTypeColor(node) {
|
|
1409
|
-
if (node.type === 'directory') return '#79c0ff';
|
|
1410
|
-
if (node.type === 'file') {
|
|
1411
|
-
const ext = getFileExtension(node.file_path);
|
|
1412
|
-
if (ext === 'py') return '#3776ab'; // Python blue
|
|
1413
|
-
if (ext === 'js' || ext === 'jsx') return '#f7df1e'; // JavaScript yellow
|
|
1414
|
-
if (ext === 'ts' || ext === 'tsx') return '#3178c6'; // TypeScript blue
|
|
1415
|
-
if (ext === 'md' || ext === 'markdown') return '#8b949e'; // Gray
|
|
1416
|
-
if (ext === 'json' || ext === 'yaml' || ext === 'yml') return '#90a4ae'; // Config gray
|
|
1417
|
-
if (ext === 'sh' || ext === 'bash' || ext === 'zsh') return '#4eaa25'; // Shell green
|
|
1418
|
-
return '#58a6ff'; // Default file color
|
|
1419
|
-
}
|
|
1420
|
-
return null;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
function visualizeGraph(data) {
|
|
1424
|
-
g.selectAll("*").remove();
|
|
1425
|
-
|
|
1426
|
-
allNodes = data.nodes;
|
|
1427
|
-
allLinks = data.links;
|
|
1428
|
-
|
|
1429
|
-
// Find root nodes - start with only top-level nodes
|
|
1430
|
-
let rootNodes;
|
|
1431
|
-
if (data.metadata && data.metadata.is_monorepo) {
|
|
1432
|
-
// In monorepos, subproject nodes are roots
|
|
1433
|
-
rootNodes = allNodes.filter(n => n.type === 'subproject');
|
|
1434
|
-
} else {
|
|
1435
|
-
// Regular projects: show root-level directories AND files
|
|
1436
|
-
const dirNodes = allNodes.filter(n => n.type === 'directory');
|
|
1437
|
-
const fileNodes = allNodes.filter(n => n.type === 'file');
|
|
1438
|
-
|
|
1439
|
-
// Find minimum depth for directories and files
|
|
1440
|
-
const minDirDepth = dirNodes.length > 0
|
|
1441
|
-
? Math.min(...dirNodes.map(n => n.depth))
|
|
1442
|
-
: Infinity;
|
|
1443
|
-
const minFileDepth = fileNodes.length > 0
|
|
1444
|
-
? Math.min(...fileNodes.map(n => n.depth))
|
|
1445
|
-
: Infinity;
|
|
1446
|
-
|
|
1447
|
-
// Include both root-level directories and root-level files
|
|
1448
|
-
rootNodes = [
|
|
1449
|
-
...dirNodes.filter(n => n.depth === minDirDepth),
|
|
1450
|
-
...fileNodes.filter(n => n.depth === minFileDepth)
|
|
1451
|
-
];
|
|
1452
|
-
|
|
1453
|
-
// Fallback to all files if nothing found
|
|
1454
|
-
if (rootNodes.length === 0) {
|
|
1455
|
-
rootNodes = fileNodes;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// Start with only root nodes visible, all collapsed
|
|
1460
|
-
rootNodes = rootNodes; // Store for reset function
|
|
1461
|
-
visibleNodes = new Set(rootNodes.map(n => n.id));
|
|
1462
|
-
collapsedNodes = new Set(rootNodes.map(n => n.id));
|
|
1463
|
-
|
|
1464
|
-
renderGraph();
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
function renderGraph() {
|
|
1468
|
-
const visibleNodesList = allNodes.filter(n => visibleNodes.has(n.id));
|
|
1469
|
-
const visibleLinks = allLinks.filter(l =>
|
|
1470
|
-
visibleNodes.has(l.source.id || l.source) &&
|
|
1471
|
-
visibleNodes.has(l.target.id || l.target)
|
|
1472
|
-
);
|
|
1473
|
-
|
|
1474
|
-
simulation = d3.forceSimulation(visibleNodesList)
|
|
1475
|
-
.force("link", d3.forceLink(visibleLinks).id(d => d.id).distance(100))
|
|
1476
|
-
.force("charge", d3.forceManyBody().strength(d => {
|
|
1477
|
-
// Check if node has any connections
|
|
1478
|
-
const hasConnections = allLinks.some(link =>
|
|
1479
|
-
link.source.id === d.id || link.target.id === d.id
|
|
1480
|
-
);
|
|
1481
|
-
// Connected nodes: spread out more (-200)
|
|
1482
|
-
// Unconnected nodes: cluster together (-50)
|
|
1483
|
-
return hasConnections ? -200 : -50;
|
|
1484
|
-
}))
|
|
1485
|
-
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.15))
|
|
1486
|
-
.force("collision", d3.forceCollide().radius(35));
|
|
1487
|
-
|
|
1488
|
-
g.selectAll("*").remove();
|
|
1489
|
-
|
|
1490
|
-
const link = g.append("g")
|
|
1491
|
-
.selectAll("line")
|
|
1492
|
-
.data(visibleLinks)
|
|
1493
|
-
.join("line")
|
|
1494
|
-
.attr("class", d => {
|
|
1495
|
-
// Cycle links have highest priority
|
|
1496
|
-
if (d.is_cycle) return "link cycle";
|
|
1497
|
-
if (d.type === "dependency") return "link dependency";
|
|
1498
|
-
if (d.type === "semantic") {
|
|
1499
|
-
// Color based on similarity score
|
|
1500
|
-
const sim = d.similarity || 0;
|
|
1501
|
-
let simClass = "sim-very-low";
|
|
1502
|
-
if (sim >= 0.8) simClass = "sim-high";
|
|
1503
|
-
else if (sim >= 0.6) simClass = "sim-medium-high";
|
|
1504
|
-
else if (sim >= 0.4) simClass = "sim-medium";
|
|
1505
|
-
else if (sim >= 0.2) simClass = "sim-low";
|
|
1506
|
-
return `link semantic ${simClass}`;
|
|
1507
|
-
}
|
|
1508
|
-
return "link";
|
|
1509
|
-
})
|
|
1510
|
-
.on("mouseover", showLinkTooltip)
|
|
1511
|
-
.on("mouseout", hideTooltip);
|
|
1512
|
-
|
|
1513
|
-
const node = g.append("g")
|
|
1514
|
-
.selectAll("g")
|
|
1515
|
-
.data(visibleNodesList)
|
|
1516
|
-
.join("g")
|
|
1517
|
-
.attr("class", d => {
|
|
1518
|
-
let classes = `node ${d.type}`;
|
|
1519
|
-
if (highlightedNode && d.id === highlightedNode.id) {
|
|
1520
|
-
classes += ' highlighted';
|
|
1521
|
-
}
|
|
1522
|
-
return classes;
|
|
1523
|
-
})
|
|
1524
|
-
.call(drag(simulation))
|
|
1525
|
-
.on("click", handleNodeClick)
|
|
1526
|
-
.on("mouseover", showTooltip)
|
|
1527
|
-
.on("mouseout", hideTooltip);
|
|
1528
|
-
|
|
1529
|
-
// Add shapes based on node type
|
|
1530
|
-
const isDocNode = d => ['docstring', 'comment'].includes(d.type);
|
|
1531
|
-
const isFileOrDir = d => d.type === 'file' || d.type === 'directory';
|
|
1532
|
-
|
|
1533
|
-
// Add circles for regular code nodes (not files/dirs/docs)
|
|
1534
|
-
node.filter(d => !isDocNode(d) && !isFileOrDir(d))
|
|
1535
|
-
.append("circle")
|
|
1536
|
-
.attr("r", d => {
|
|
1537
|
-
if (d.type === 'subproject') return 24;
|
|
1538
|
-
return d.complexity ? Math.min(12 + d.complexity * 2, 28) : 15;
|
|
1539
|
-
})
|
|
1540
|
-
.attr("stroke", d => {
|
|
1541
|
-
// Check if node has incoming caller/imports edges (dead code detection)
|
|
1542
|
-
const hasIncoming = allLinks.some(l =>
|
|
1543
|
-
(l.target.id || l.target) === d.id &&
|
|
1544
|
-
(l.type === 'caller' || l.type === 'imports')
|
|
1545
|
-
);
|
|
1546
|
-
if (!hasIncoming && (d.type === 'function' || d.type === 'class' || d.type === 'method')) {
|
|
1547
|
-
// Check if it's not an entry point (main, test, cli files)
|
|
1548
|
-
const isEntryPoint = d.file_path && (
|
|
1549
|
-
d.file_path.includes('main.py') ||
|
|
1550
|
-
d.file_path.includes('__main__.py') ||
|
|
1551
|
-
d.file_path.includes('cli.py') ||
|
|
1552
|
-
d.file_path.includes('test_')
|
|
1553
|
-
);
|
|
1554
|
-
if (!isEntryPoint) {
|
|
1555
|
-
return "#ff6b6b"; // Red border for potentially dead code
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return hasChildren(d) ? "#ffffff" : "none";
|
|
1559
|
-
})
|
|
1560
|
-
.attr("stroke-width", d => {
|
|
1561
|
-
const hasIncoming = allLinks.some(l =>
|
|
1562
|
-
(l.target.id || l.target) === d.id &&
|
|
1563
|
-
(l.type === 'caller' || l.type === 'imports')
|
|
1564
|
-
);
|
|
1565
|
-
if (!hasIncoming && (d.type === 'function' || d.type === 'class' || d.type === 'method')) {
|
|
1566
|
-
const isEntryPoint = d.file_path && (
|
|
1567
|
-
d.file_path.includes('main.py') ||
|
|
1568
|
-
d.file_path.includes('__main__.py') ||
|
|
1569
|
-
d.file_path.includes('cli.py') ||
|
|
1570
|
-
d.file_path.includes('test_')
|
|
1571
|
-
);
|
|
1572
|
-
if (!isEntryPoint) {
|
|
1573
|
-
return 3; // Thicker red border
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return hasChildren(d) ? 2 : 0;
|
|
1577
|
-
})
|
|
1578
|
-
.style("fill", d => d.color || null); // Use custom color if available
|
|
1579
|
-
|
|
1580
|
-
// Add rectangles for document nodes
|
|
1581
|
-
node.filter(d => isDocNode(d))
|
|
1582
|
-
.append("rect")
|
|
1583
|
-
.attr("width", d => {
|
|
1584
|
-
const size = d.complexity ? Math.min(12 + d.complexity * 2, 28) : 15;
|
|
1585
|
-
return size * 2;
|
|
1586
|
-
})
|
|
1587
|
-
.attr("height", d => {
|
|
1588
|
-
const size = d.complexity ? Math.min(12 + d.complexity * 2, 28) : 15;
|
|
1589
|
-
return size * 2;
|
|
1590
|
-
})
|
|
1591
|
-
.attr("x", d => {
|
|
1592
|
-
const size = d.complexity ? Math.min(12 + d.complexity * 2, 28) : 15;
|
|
1593
|
-
return -size;
|
|
1594
|
-
})
|
|
1595
|
-
.attr("y", d => {
|
|
1596
|
-
const size = d.complexity ? Math.min(12 + d.complexity * 2, 28) : 15;
|
|
1597
|
-
return -size;
|
|
1598
|
-
})
|
|
1599
|
-
.attr("rx", 2) // Rounded corners
|
|
1600
|
-
.attr("ry", 2)
|
|
1601
|
-
.attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
|
|
1602
|
-
.attr("stroke-width", d => hasChildren(d) ? 2 : 0)
|
|
1603
|
-
.style("fill", d => d.color || null);
|
|
1604
|
-
|
|
1605
|
-
// Add SVG icons for file and directory nodes
|
|
1606
|
-
node.filter(d => isFileOrDir(d))
|
|
1607
|
-
.append("path")
|
|
1608
|
-
.attr("class", "file-icon")
|
|
1609
|
-
.attr("d", d => getFileTypeIcon(d))
|
|
1610
|
-
.attr("transform", d => {
|
|
1611
|
-
const scale = d.type === 'directory' ? 1.8 : 1.5;
|
|
1612
|
-
return `translate(-12, -12) scale(${scale})`;
|
|
1613
|
-
})
|
|
1614
|
-
.style("color", d => getFileTypeColor(d))
|
|
1615
|
-
.attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
|
|
1616
|
-
.attr("stroke-width", d => hasChildren(d) ? 1 : 0);
|
|
1617
|
-
|
|
1618
|
-
// Add expand/collapse indicator - positioned to the left of label
|
|
1619
|
-
node.filter(d => hasChildren(d))
|
|
1620
|
-
.append("text")
|
|
1621
|
-
.attr("class", "expand-indicator")
|
|
1622
|
-
.attr("x", d => {
|
|
1623
|
-
const iconRadius = d.type === 'directory' ? 18 : (d.type === 'file' ? 15 : 15);
|
|
1624
|
-
return iconRadius + 5; // Just right of the icon (slightly more spacing)
|
|
1625
|
-
})
|
|
1626
|
-
.attr("y", 0) // Vertically center with icon
|
|
1627
|
-
.attr("dy", "0.6em") // Fine-tune vertical centering (shifted down)
|
|
1628
|
-
.attr("text-anchor", "start")
|
|
1629
|
-
.style("font-size", "14px")
|
|
1630
|
-
.style("font-weight", "bold")
|
|
1631
|
-
.style("fill", "#ffffff")
|
|
1632
|
-
.style("pointer-events", "none")
|
|
1633
|
-
.text(d => collapsedNodes.has(d.id) ? "+" : "−");
|
|
1634
|
-
|
|
1635
|
-
// Add labels (show actual import statement for L1 nodes)
|
|
1636
|
-
node.append("text")
|
|
1637
|
-
.text(d => {
|
|
1638
|
-
// L1 (depth 1) nodes are imports
|
|
1639
|
-
if (d.depth === 1 && d.type !== 'directory' && d.type !== 'file') {
|
|
1640
|
-
if (d.content) {
|
|
1641
|
-
// Extract first line of import statement
|
|
1642
|
-
const importLine = d.content.split('\\n')[0].trim();
|
|
1643
|
-
// Truncate if too long (max 60 chars)
|
|
1644
|
-
return importLine.length > 60 ? importLine.substring(0, 57) + '...' : importLine;
|
|
1645
|
-
}
|
|
1646
|
-
return d.name; // Fallback to name if no content
|
|
1647
|
-
}
|
|
1648
|
-
return d.name;
|
|
1649
|
-
})
|
|
1650
|
-
.attr("x", d => {
|
|
1651
|
-
const iconRadius = d.type === 'directory' ? 18 : (d.type === 'file' ? 15 : 15);
|
|
1652
|
-
const hasExpand = hasChildren(d);
|
|
1653
|
-
// Position after icon, plus expand indicator width if present (increased spacing)
|
|
1654
|
-
return iconRadius + 8 + (hasExpand ? 22 : 0);
|
|
1655
|
-
})
|
|
1656
|
-
.attr("y", 0) // Vertically center with icon
|
|
1657
|
-
.attr("dy", "0.6em") // Fine-tune vertical centering (shifted down to match expand indicator)
|
|
1658
|
-
.attr("text-anchor", "start");
|
|
1659
|
-
|
|
1660
|
-
simulation.on("tick", () => {
|
|
1661
|
-
link
|
|
1662
|
-
.attr("x1", d => d.source.x)
|
|
1663
|
-
.attr("y1", d => d.source.y)
|
|
1664
|
-
.attr("x2", d => d.target.x)
|
|
1665
|
-
.attr("y2", d => d.target.y);
|
|
1666
|
-
|
|
1667
|
-
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
1668
|
-
});
|
|
1669
|
-
|
|
1670
|
-
updateStats({nodes: visibleNodesList, links: visibleLinks, metadata: {total_files: allNodes.length}});
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
function hasChildren(node) {
|
|
1674
|
-
return allLinks.some(l => (l.source.id || l.source) === node.id);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// Zoom to fit all visible nodes
|
|
1678
|
-
function zoomToFit(duration = 750) {
|
|
1679
|
-
const visibleNodesList = allNodes.filter(n => visibleNodes.has(n.id));
|
|
1680
|
-
if (visibleNodesList.length === 0) return;
|
|
1681
|
-
|
|
1682
|
-
// Calculate bounding box of visible nodes
|
|
1683
|
-
const padding = 100;
|
|
1684
|
-
let minX = Infinity, minY = Infinity;
|
|
1685
|
-
let maxX = -Infinity, maxY = -Infinity;
|
|
1686
|
-
|
|
1687
|
-
visibleNodesList.forEach(d => {
|
|
1688
|
-
if (d.x !== undefined && d.y !== undefined) {
|
|
1689
|
-
minX = Math.min(minX, d.x);
|
|
1690
|
-
minY = Math.min(minY, d.y);
|
|
1691
|
-
maxX = Math.max(maxX, d.x);
|
|
1692
|
-
maxY = Math.max(maxY, d.y);
|
|
1693
|
-
}
|
|
1694
|
-
});
|
|
1695
|
-
|
|
1696
|
-
// Add padding
|
|
1697
|
-
minX -= padding;
|
|
1698
|
-
minY -= padding;
|
|
1699
|
-
maxX += padding;
|
|
1700
|
-
maxY += padding;
|
|
1701
|
-
|
|
1702
|
-
const boxWidth = maxX - minX;
|
|
1703
|
-
const boxHeight = maxY - minY;
|
|
1704
|
-
|
|
1705
|
-
// Calculate scale to fit
|
|
1706
|
-
const scale = Math.min(
|
|
1707
|
-
width / boxWidth,
|
|
1708
|
-
height / boxHeight,
|
|
1709
|
-
2 // Max zoom level
|
|
1710
|
-
) * 0.9; // Add 10% margin
|
|
1711
|
-
|
|
1712
|
-
// Calculate center translation
|
|
1713
|
-
const centerX = (minX + maxX) / 2;
|
|
1714
|
-
const centerY = (minY + maxY) / 2;
|
|
1715
|
-
const translateX = width / 2 - scale * centerX;
|
|
1716
|
-
const translateY = height / 2 - scale * centerY;
|
|
1717
|
-
|
|
1718
|
-
// Apply zoom transform with animation
|
|
1719
|
-
svg.transition()
|
|
1720
|
-
.duration(duration)
|
|
1721
|
-
.call(
|
|
1722
|
-
zoom.transform,
|
|
1723
|
-
d3.zoomIdentity
|
|
1724
|
-
.translate(translateX, translateY)
|
|
1725
|
-
.scale(scale)
|
|
1726
|
-
);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
function centerNode(node) {
|
|
1730
|
-
// Get current transform to maintain zoom level
|
|
1731
|
-
const transform = d3.zoomTransform(svg.node());
|
|
1732
|
-
|
|
1733
|
-
// Calculate translation to center the node in LEFT portion of viewport
|
|
1734
|
-
// Position at 30% from left to avoid code pane on right side
|
|
1735
|
-
const x = -node.x * transform.k + width * 0.3;
|
|
1736
|
-
const y = -node.y * transform.k + height / 2;
|
|
1737
|
-
|
|
1738
|
-
// Apply smooth animation to center the node
|
|
1739
|
-
svg.transition()
|
|
1740
|
-
.duration(750)
|
|
1741
|
-
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(transform.k));
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
function resetView() {
|
|
1745
|
-
// Reset to root level nodes only
|
|
1746
|
-
visibleNodes = new Set(rootNodes.map(n => n.id));
|
|
1747
|
-
collapsedNodes = new Set(rootNodes.map(n => n.id));
|
|
1748
|
-
highlightedNode = null;
|
|
1749
|
-
|
|
1750
|
-
// Re-render graph
|
|
1751
|
-
renderGraph();
|
|
1752
|
-
|
|
1753
|
-
// Zoom to fit after rendering
|
|
1754
|
-
setTimeout(() => {
|
|
1755
|
-
zoomToFit(750);
|
|
1756
|
-
}, 200);
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
function handleNodeClick(event, d) {
|
|
1760
|
-
event.stopPropagation();
|
|
1761
|
-
|
|
1762
|
-
// Always show content pane when clicking any node
|
|
1763
|
-
showContentPane(d);
|
|
1764
|
-
|
|
1765
|
-
// If node has children, also toggle expansion
|
|
1766
|
-
if (hasChildren(d)) {
|
|
1767
|
-
const wasCollapsed = collapsedNodes.has(d.id);
|
|
1768
|
-
if (wasCollapsed) {
|
|
1769
|
-
expandNode(d);
|
|
1770
|
-
} else {
|
|
1771
|
-
collapseNode(d);
|
|
1772
|
-
}
|
|
1773
|
-
renderGraph();
|
|
1774
|
-
|
|
1775
|
-
// After rendering and nodes have positions, zoom to fit ONLY visible nodes
|
|
1776
|
-
// Use a small delay to ensure D3 simulation has updated positions
|
|
1777
|
-
if (!wasCollapsed) {
|
|
1778
|
-
// Wait for simulation to stabilize before zooming
|
|
1779
|
-
setTimeout(() => {
|
|
1780
|
-
// Stop simulation to get final positions
|
|
1781
|
-
simulation.alphaTarget(0);
|
|
1782
|
-
zoomToFit(750);
|
|
1783
|
-
}, 200);
|
|
1784
|
-
} else {
|
|
1785
|
-
// For expansion, center the clicked node after a delay
|
|
1786
|
-
setTimeout(() => {
|
|
1787
|
-
centerNode(d);
|
|
1788
|
-
}, 200);
|
|
1789
|
-
}
|
|
1790
|
-
} else {
|
|
1791
|
-
// For nodes without children, center immediately after a small delay
|
|
1792
|
-
setTimeout(() => {
|
|
1793
|
-
centerNode(d);
|
|
1794
|
-
}, 100);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
function expandNode(node) {
|
|
1799
|
-
collapsedNodes.delete(node.id);
|
|
1800
|
-
|
|
1801
|
-
// Find direct children
|
|
1802
|
-
const children = allLinks
|
|
1803
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
1804
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1805
|
-
.filter(n => n);
|
|
1806
|
-
|
|
1807
|
-
children.forEach(child => {
|
|
1808
|
-
visibleNodes.add(child.id);
|
|
1809
|
-
collapsedNodes.add(child.id); // Children start collapsed
|
|
1810
|
-
});
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function collapseNode(node) {
|
|
1814
|
-
collapsedNodes.add(node.id);
|
|
1815
|
-
|
|
1816
|
-
// Hide all descendants recursively
|
|
1817
|
-
function hideDescendants(parentId) {
|
|
1818
|
-
const children = allLinks
|
|
1819
|
-
.filter(l => (l.source.id || l.source) === parentId)
|
|
1820
|
-
.map(l => l.target.id || l.target);
|
|
1821
|
-
|
|
1822
|
-
children.forEach(childId => {
|
|
1823
|
-
visibleNodes.delete(childId);
|
|
1824
|
-
collapsedNodes.delete(childId);
|
|
1825
|
-
hideDescendants(childId);
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
hideDescendants(node.id);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function showTooltip(event, d) {
|
|
1833
|
-
// Extract first 2-3 lines of docstring for preview
|
|
1834
|
-
let docPreview = '';
|
|
1835
|
-
if (d.docstring) {
|
|
1836
|
-
const lines = d.docstring.split('\\n').filter(l => l.trim());
|
|
1837
|
-
const previewLines = lines.slice(0, 3).join(' ');
|
|
1838
|
-
const truncated = previewLines.length > 150 ? previewLines.substring(0, 147) + '...' : previewLines;
|
|
1839
|
-
docPreview = `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; font-style: italic;">${truncated}</div>`;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
tooltip
|
|
1843
|
-
.style("display", "block")
|
|
1844
|
-
.style("left", (event.pageX + 10) + "px")
|
|
1845
|
-
.style("top", (event.pageY + 10) + "px")
|
|
1846
|
-
.html(`
|
|
1847
|
-
<div><strong>${d.name}</strong></div>
|
|
1848
|
-
<div>Type: ${d.type}</div>
|
|
1849
|
-
${d.complexity ? `<div>Complexity: ${d.complexity.toFixed(1)}</div>` : ''}
|
|
1850
|
-
${d.start_line ? `<div>Lines: ${d.start_line}-${d.end_line}</div>` : ''}
|
|
1851
|
-
<div>File: ${d.file_path}</div>
|
|
1852
|
-
${docPreview}
|
|
1853
|
-
`);
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
function showLinkTooltip(event, d) {
|
|
1857
|
-
const sourceName = allNodes.find(n => n.id === (d.source.id || d.source))?.name || 'Unknown';
|
|
1858
|
-
const targetName = allNodes.find(n => n.id === (d.target.id || d.target))?.name || 'Unknown';
|
|
1859
|
-
|
|
1860
|
-
// Special tooltip for cycle links
|
|
1861
|
-
if (d.is_cycle) {
|
|
1862
|
-
tooltip
|
|
1863
|
-
.style("display", "block")
|
|
1864
|
-
.style("left", (event.pageX + 10) + "px")
|
|
1865
|
-
.style("top", (event.pageY + 10) + "px")
|
|
1866
|
-
.html(`
|
|
1867
|
-
<div style="color: #ff4444;"><strong>⚠️ Circular Dependency Detected</strong></div>
|
|
1868
|
-
<div style="margin-top: 8px;">Path: ${sourceName} → ${targetName}</div>
|
|
1869
|
-
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; font-style: italic;">
|
|
1870
|
-
This indicates a circular call relationship that may lead to infinite recursion or tight coupling.
|
|
1871
|
-
</div>
|
|
1872
|
-
`);
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// Tooltip content based on link type
|
|
1877
|
-
let typeLabel = '';
|
|
1878
|
-
let typeDescription = '';
|
|
1879
|
-
let extraInfo = '';
|
|
1880
|
-
|
|
1881
|
-
switch(d.type) {
|
|
1882
|
-
case 'caller':
|
|
1883
|
-
typeLabel = '📞 Function Call';
|
|
1884
|
-
typeDescription = `${sourceName} calls ${targetName}`;
|
|
1885
|
-
extraInfo = 'This is a direct function call relationship, the most common type of code dependency.';
|
|
1886
|
-
break;
|
|
1887
|
-
case 'semantic':
|
|
1888
|
-
typeLabel = '🔗 Semantic Similarity';
|
|
1889
|
-
typeDescription = `${(d.similarity * 100).toFixed(1)}% similar`;
|
|
1890
|
-
extraInfo = `These code chunks have similar meaning or purpose based on their content.`;
|
|
1891
|
-
break;
|
|
1892
|
-
case 'imports':
|
|
1893
|
-
typeLabel = '📦 Import Dependency';
|
|
1894
|
-
typeDescription = `${sourceName} imports ${targetName}`;
|
|
1895
|
-
extraInfo = 'This is an explicit import/dependency declaration.';
|
|
1896
|
-
break;
|
|
1897
|
-
case 'file_containment':
|
|
1898
|
-
typeLabel = '📄 File Contains';
|
|
1899
|
-
typeDescription = `${sourceName} contains ${targetName}`;
|
|
1900
|
-
extraInfo = 'This file contains the code chunk or function.';
|
|
1901
|
-
break;
|
|
1902
|
-
case 'dir_containment':
|
|
1903
|
-
typeLabel = '📁 Directory Contains';
|
|
1904
|
-
typeDescription = `${sourceName} contains ${targetName}`;
|
|
1905
|
-
extraInfo = 'This directory contains the file or subdirectory.';
|
|
1906
|
-
break;
|
|
1907
|
-
case 'dir_hierarchy':
|
|
1908
|
-
typeLabel = '🗂️ Directory Hierarchy';
|
|
1909
|
-
typeDescription = `${sourceName} → ${targetName}`;
|
|
1910
|
-
extraInfo = 'Parent-child directory structure relationship.';
|
|
1911
|
-
break;
|
|
1912
|
-
case 'method':
|
|
1913
|
-
typeLabel = '⚙️ Method Relationship';
|
|
1914
|
-
typeDescription = `${sourceName} ↔ ${targetName}`;
|
|
1915
|
-
extraInfo = 'Class method relationship.';
|
|
1916
|
-
break;
|
|
1917
|
-
case 'module':
|
|
1918
|
-
typeLabel = '📚 Module Relationship';
|
|
1919
|
-
typeDescription = `${sourceName} ↔ ${targetName}`;
|
|
1920
|
-
extraInfo = 'Module-level relationship.';
|
|
1921
|
-
break;
|
|
1922
|
-
case 'dependency':
|
|
1923
|
-
typeLabel = '🔀 Dependency';
|
|
1924
|
-
typeDescription = `${sourceName} depends on ${targetName}`;
|
|
1925
|
-
extraInfo = 'General code dependency relationship.';
|
|
1926
|
-
break;
|
|
1927
|
-
default:
|
|
1928
|
-
typeLabel = `🔗 ${d.type || 'Unknown'}`;
|
|
1929
|
-
typeDescription = `${sourceName} → ${targetName}`;
|
|
1930
|
-
extraInfo = 'Code relationship.';
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
tooltip
|
|
1934
|
-
.style("display", "block")
|
|
1935
|
-
.style("left", (event.pageX + 10) + "px")
|
|
1936
|
-
.style("top", (event.pageY + 10) + "px")
|
|
1937
|
-
.html(`
|
|
1938
|
-
<div><strong>${typeLabel}</strong></div>
|
|
1939
|
-
<div style="margin-top: 4px;">${typeDescription}</div>
|
|
1940
|
-
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; font-style: italic;">
|
|
1941
|
-
${extraInfo}
|
|
1942
|
-
</div>
|
|
1943
|
-
`);
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
function hideTooltip() {
|
|
1947
|
-
tooltip.style("display", "none");
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
function drag(simulation) {
|
|
1951
|
-
function dragstarted(event) {
|
|
1952
|
-
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1953
|
-
event.subject.fx = event.subject.x;
|
|
1954
|
-
event.subject.fy = event.subject.y;
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
function dragged(event) {
|
|
1958
|
-
event.subject.fx = event.x;
|
|
1959
|
-
event.subject.fy = event.y;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
function dragended(event) {
|
|
1963
|
-
if (!event.active) simulation.alphaTarget(0);
|
|
1964
|
-
event.subject.fx = null;
|
|
1965
|
-
event.subject.fy = null;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
return d3.drag()
|
|
1969
|
-
.on("start", dragstarted)
|
|
1970
|
-
.on("drag", dragged)
|
|
1971
|
-
.on("end", dragended);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
function updateStats(data) {
|
|
1975
|
-
const stats = d3.select("#stats");
|
|
1976
|
-
stats.html(`
|
|
1977
|
-
<div>Nodes: ${data.nodes.length}</div>
|
|
1978
|
-
<div>Links: ${data.links.length}</div>
|
|
1979
|
-
${data.metadata ? `<div>Files: ${data.metadata.total_files || 'N/A'}</div>` : ''}
|
|
1980
|
-
${data.metadata && data.metadata.is_monorepo ? `<div>Monorepo: ${data.metadata.subprojects.length} subprojects</div>` : ''}
|
|
1981
|
-
`);
|
|
1982
|
-
|
|
1983
|
-
// Show subproject legend if monorepo
|
|
1984
|
-
if (data.metadata && data.metadata.is_monorepo && data.metadata.subprojects.length > 0) {
|
|
1985
|
-
const subprojectsLegend = d3.select("#subprojects-legend");
|
|
1986
|
-
const subprojectsList = d3.select("#subprojects-list");
|
|
1987
|
-
|
|
1988
|
-
subprojectsLegend.style("display", "block");
|
|
1989
|
-
|
|
1990
|
-
// Get subproject nodes with colors
|
|
1991
|
-
const subprojectNodes = allNodes.filter(n => n.type === 'subproject');
|
|
1992
|
-
|
|
1993
|
-
subprojectsList.html(
|
|
1994
|
-
subprojectNodes.map(sp =>
|
|
1995
|
-
`<div class="legend-item">
|
|
1996
|
-
<span class="legend-color" style="background: ${sp.color};"></span> ${sp.name}
|
|
1997
|
-
</div>`
|
|
1998
|
-
).join('')
|
|
1999
|
-
);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
function showContentPane(node) {
|
|
2004
|
-
// Highlight the node
|
|
2005
|
-
highlightedNode = node;
|
|
2006
|
-
renderGraph();
|
|
2007
|
-
|
|
2008
|
-
// Populate content pane
|
|
2009
|
-
const pane = document.getElementById('content-pane');
|
|
2010
|
-
const title = document.getElementById('pane-title');
|
|
2011
|
-
const meta = document.getElementById('pane-meta');
|
|
2012
|
-
const content = document.getElementById('pane-content');
|
|
2013
|
-
const footer = document.getElementById('pane-footer');
|
|
2014
|
-
|
|
2015
|
-
// Set title with actual import statement for L1 nodes
|
|
2016
|
-
if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
2017
|
-
if (node.content) {
|
|
2018
|
-
const importLine = node.content.split('\\n')[0].trim();
|
|
2019
|
-
title.textContent = importLine;
|
|
2020
|
-
} else {
|
|
2021
|
-
title.textContent = `Import: ${node.name}`;
|
|
2022
|
-
}
|
|
2023
|
-
} else {
|
|
2024
|
-
title.textContent = node.name;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Set metadata (type only in header)
|
|
2028
|
-
meta.textContent = node.type;
|
|
2029
|
-
|
|
2030
|
-
// Build footer with annotations
|
|
2031
|
-
let footerHtml = '';
|
|
2032
|
-
if (node.language) {
|
|
2033
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">Language:</span> ${node.language}</span>`;
|
|
2034
|
-
}
|
|
2035
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">File:</span> ${node.file_path}</span>`;
|
|
2036
|
-
|
|
2037
|
-
// Add line information and complexity
|
|
2038
|
-
if (node.start_line !== undefined && node.end_line !== undefined) {
|
|
2039
|
-
const totalLines = node.end_line - node.start_line + 1;
|
|
2040
|
-
|
|
2041
|
-
if (node.type === 'function' || node.type === 'class' || node.type === 'method') {
|
|
2042
|
-
// For functions/classes: show function lines
|
|
2043
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">Lines:</span> ${node.start_line}-${node.end_line} (${totalLines} lines)</span>`;
|
|
2044
|
-
} else if (node.type === 'file') {
|
|
2045
|
-
// For files: show file lines
|
|
2046
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">File Lines:</span> ${totalLines}</span>`;
|
|
2047
|
-
} else {
|
|
2048
|
-
// For other types: show location
|
|
2049
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">Location:</span> Lines ${node.start_line}-${node.end_line}</span>`;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
// Add cyclomatic complexity if available and > 0
|
|
2053
|
-
if (node.complexity && node.complexity > 0) {
|
|
2054
|
-
footerHtml += `<span class="footer-item"><span class="footer-label">Complexity:</span> ${node.complexity}</span>`;
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
footer.innerHTML = footerHtml;
|
|
2059
|
-
|
|
2060
|
-
// Display content based on node type
|
|
2061
|
-
if (node.type === 'directory') {
|
|
2062
|
-
showDirectoryContents(node, content, footer);
|
|
2063
|
-
} else if (node.type === 'file') {
|
|
2064
|
-
showFileContents(node, content);
|
|
2065
|
-
} else if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
2066
|
-
// L1 nodes are imports
|
|
2067
|
-
showImportDetails(node, content);
|
|
2068
|
-
} else {
|
|
2069
|
-
// Class, function, method, code nodes
|
|
2070
|
-
showCodeContent(node, content);
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
pane.classList.add('visible');
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
function showDirectoryContents(node, container, footer) {
|
|
2077
|
-
// Find all direct children of this directory
|
|
2078
|
-
const children = allLinks
|
|
2079
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
2080
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
2081
|
-
.filter(n => n);
|
|
2082
|
-
|
|
2083
|
-
if (children.length === 0) {
|
|
2084
|
-
container.innerHTML = '<p style="color: #8b949e;">Empty directory</p>';
|
|
2085
|
-
// Update footer with file path only
|
|
2086
|
-
footer.innerHTML = `<span class="footer-item"><span class="footer-label">File:</span> ${node.file_path}</span>`;
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
// Group by type
|
|
2091
|
-
const files = children.filter(n => n.type === 'file');
|
|
2092
|
-
const subdirs = children.filter(n => n.type === 'directory');
|
|
2093
|
-
const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
|
|
2094
|
-
|
|
2095
|
-
let html = '<ul class="directory-list">';
|
|
2096
|
-
|
|
2097
|
-
// Show subdirectories first
|
|
2098
|
-
subdirs.forEach(child => {
|
|
2099
|
-
html += `
|
|
2100
|
-
<li data-node-id="${child.id}">
|
|
2101
|
-
<span class="item-icon">📁</span>
|
|
2102
|
-
${child.name}
|
|
2103
|
-
</li>
|
|
2104
|
-
`;
|
|
2105
|
-
});
|
|
2106
|
-
|
|
2107
|
-
// Then files
|
|
2108
|
-
files.forEach(child => {
|
|
2109
|
-
html += `
|
|
2110
|
-
<li data-node-id="${child.id}">
|
|
2111
|
-
<span class="item-icon">📄</span>
|
|
2112
|
-
${child.name}
|
|
2113
|
-
</li>
|
|
2114
|
-
`;
|
|
2115
|
-
});
|
|
2116
|
-
|
|
2117
|
-
// Then code chunks
|
|
2118
|
-
chunks.forEach(child => {
|
|
2119
|
-
const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
|
|
2120
|
-
html += `
|
|
2121
|
-
<li data-node-id="${child.id}">
|
|
2122
|
-
<span class="item-icon">${icon}</span>
|
|
2123
|
-
${child.name}
|
|
2124
|
-
</li>
|
|
2125
|
-
`;
|
|
2126
|
-
});
|
|
2127
|
-
|
|
2128
|
-
html += '</ul>';
|
|
2129
|
-
|
|
2130
|
-
container.innerHTML = html;
|
|
2131
|
-
|
|
2132
|
-
// Add click handlers to list items
|
|
2133
|
-
const listItems = container.querySelectorAll('.directory-list li');
|
|
2134
|
-
listItems.forEach(item => {
|
|
2135
|
-
item.addEventListener('click', () => {
|
|
2136
|
-
const nodeId = item.getAttribute('data-node-id');
|
|
2137
|
-
const childNode = allNodes.find(n => n.id === nodeId);
|
|
2138
|
-
if (childNode) {
|
|
2139
|
-
showContentPane(childNode);
|
|
2140
|
-
}
|
|
2141
|
-
});
|
|
2142
|
-
});
|
|
2143
|
-
|
|
2144
|
-
// Update footer with file path and summary
|
|
2145
|
-
footer.innerHTML = `
|
|
2146
|
-
<span class="footer-item"><span class="footer-label">File:</span> ${node.file_path}</span>
|
|
2147
|
-
<span class="footer-item"><span class="footer-label">Total:</span> ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)</span>
|
|
2148
|
-
`;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
function showFileContents(node, container) {
|
|
2152
|
-
// Find all chunks in this file
|
|
2153
|
-
const fileChunks = allLinks
|
|
2154
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
2155
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
2156
|
-
.filter(n => n);
|
|
2157
|
-
|
|
2158
|
-
if (fileChunks.length === 0) {
|
|
2159
|
-
container.innerHTML = '<p style="color: #8b949e;">No code chunks found in this file</p>';
|
|
2160
|
-
return;
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
// Collect all content from chunks and sort by line number
|
|
2164
|
-
const sortedChunks = fileChunks
|
|
2165
|
-
.filter(c => c.content)
|
|
2166
|
-
.sort((a, b) => a.start_line - b.start_line);
|
|
2167
|
-
|
|
2168
|
-
if (sortedChunks.length === 0) {
|
|
2169
|
-
container.innerHTML = '<p style="color: #8b949e;">File content not available</p>';
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// Combine all chunks to show full file
|
|
2174
|
-
const fullContent = sortedChunks.map(c => c.content).join('\\n\\n');
|
|
2175
|
-
|
|
2176
|
-
container.innerHTML = `
|
|
2177
|
-
<p style="color: #8b949e; font-size: 11px; margin-bottom: 12px;">
|
|
2178
|
-
Contains ${fileChunks.length} code chunks
|
|
2179
|
-
</p>
|
|
2180
|
-
<pre><code>${escapeHtml(fullContent)}</code></pre>
|
|
2181
|
-
`;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
function showImportDetails(node, container) {
|
|
2185
|
-
// L1 nodes are import statements - show import content prominently
|
|
2186
|
-
// Note: File, Location, and Language are now in the footer
|
|
2187
|
-
const importHtml = `
|
|
2188
|
-
<div class="import-details">
|
|
2189
|
-
${node.content ? `
|
|
2190
|
-
<div style="margin-bottom: 16px;">
|
|
2191
|
-
<div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
|
|
2192
|
-
<pre><code>${escapeHtml(node.content)}</code></pre>
|
|
2193
|
-
</div>
|
|
2194
|
-
` : '<p style="color: #8b949e;">No import content available</p>'}
|
|
2195
|
-
</div>
|
|
2196
|
-
`;
|
|
2197
|
-
|
|
2198
|
-
container.innerHTML = importHtml;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
// Parse docstring sections (Args, Returns, Raises, etc.)
|
|
2202
|
-
function parseDocstring(docstring) {
|
|
2203
|
-
if (!docstring) return { brief: '', sections: {} };
|
|
2204
|
-
|
|
2205
|
-
const lines = docstring.split('\\n');
|
|
2206
|
-
const sections = {};
|
|
2207
|
-
let currentSection = 'brief';
|
|
2208
|
-
let currentContent = [];
|
|
2209
|
-
|
|
2210
|
-
for (let line of lines) {
|
|
2211
|
-
const trimmed = line.trim();
|
|
2212
|
-
// Check for section headers (Args:, Returns:, Raises:, etc.)
|
|
2213
|
-
const sectionMatch = trimmed.match(/^(Args?|Returns?|Yields?|Raises?|Note|Notes|Example|Examples|See Also|Docs?|Parameters?):?$/i);
|
|
2214
|
-
|
|
2215
|
-
if (sectionMatch) {
|
|
2216
|
-
// Save previous section
|
|
2217
|
-
if (currentContent.length > 0) {
|
|
2218
|
-
sections[currentSection] = currentContent.join('\\n').trim();
|
|
2219
|
-
}
|
|
2220
|
-
// Start new section
|
|
2221
|
-
currentSection = sectionMatch[1].toLowerCase();
|
|
2222
|
-
currentContent = [];
|
|
2223
|
-
} else {
|
|
2224
|
-
currentContent.push(line);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// Save last section
|
|
2229
|
-
if (currentContent.length > 0) {
|
|
2230
|
-
sections[currentSection] = currentContent.join('\\n').trim();
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
return { brief: sections.brief || '', sections };
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
function showCodeContent(node, container) {
|
|
2237
|
-
// Show code for function, class, method, or code chunks
|
|
2238
|
-
let html = '';
|
|
2239
|
-
|
|
2240
|
-
// Parse docstring to extract sections
|
|
2241
|
-
const docInfo = parseDocstring(node.docstring);
|
|
2242
|
-
|
|
2243
|
-
// Show brief description (non-sectioned part) in content area
|
|
2244
|
-
if (docInfo.brief && docInfo.brief.trim()) {
|
|
2245
|
-
html += `
|
|
2246
|
-
<div style="margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
|
|
2247
|
-
<div style="font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;">DESCRIPTION</div>
|
|
2248
|
-
<pre style="margin: 0; padding: 0; background: transparent; border: none; white-space: pre-wrap;"><code>${escapeHtml(docInfo.brief)}</code></pre>
|
|
2249
|
-
</div>
|
|
2250
|
-
`;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
if (node.content) {
|
|
2254
|
-
html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
|
|
2255
|
-
} else {
|
|
2256
|
-
html += '<p style="color: #8b949e;">No content available</p>';
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
container.innerHTML = html;
|
|
2260
|
-
|
|
2261
|
-
// Update footer with docstring sections
|
|
2262
|
-
const footer = document.getElementById('pane-footer');
|
|
2263
|
-
let footerHtml = '';
|
|
2264
|
-
|
|
2265
|
-
// Add existing footer items
|
|
2266
|
-
if (node.language) {
|
|
2267
|
-
footerHtml += `<div class="footer-item"><span class="footer-label">Language:</span> <span class="footer-value">${node.language}</span></div>`;
|
|
2268
|
-
}
|
|
2269
|
-
footerHtml += `<div class="footer-item"><span class="footer-label">File:</span> <span class="footer-value">${node.file_path}</span></div>`;
|
|
2270
|
-
if (node.start_line) {
|
|
2271
|
-
footerHtml += `<div class="footer-item"><span class="footer-label">Lines:</span> <span class="footer-value">${node.start_line}-${node.end_line}</span></div>`;
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// Add "Called By" section for external callers
|
|
2275
|
-
if (node.callers && node.callers.length > 0) {
|
|
2276
|
-
footerHtml += `<div class="footer-item" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d;">`;
|
|
2277
|
-
footerHtml += `<span class="footer-label">Called By:</span><br/>`;
|
|
2278
|
-
node.callers.forEach(caller => {
|
|
2279
|
-
const fileName = caller.file.split('/').pop();
|
|
2280
|
-
const callerDisplay = `${fileName}::${caller.name}`;
|
|
2281
|
-
footerHtml += `<span class="footer-value" style="display: block; margin-left: 8px; margin-top: 4px;">
|
|
2282
|
-
<a href="#" class="caller-link" data-chunk-id="${caller.chunk_id}" style="color: #58a6ff; text-decoration: none; cursor: pointer;">
|
|
2283
|
-
• ${escapeHtml(callerDisplay)}
|
|
2284
|
-
</a>
|
|
2285
|
-
</span>`;
|
|
2286
|
-
});
|
|
2287
|
-
footerHtml += `</div>`;
|
|
2288
|
-
} else if (node.type === 'function' || node.type === 'method' || node.type === 'class') {
|
|
2289
|
-
// Only show "no callers" message for callable entities
|
|
2290
|
-
footerHtml += `<div class="footer-item" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d;">`;
|
|
2291
|
-
footerHtml += `<span class="footer-label">Called By:</span> <span class="footer-value" style="font-style: italic; color: #6e7681;">(No external callers found)</span>`;
|
|
2292
|
-
footerHtml += `</div>`;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
// Add docstring sections to footer
|
|
2296
|
-
const sectionLabels = {
|
|
2297
|
-
'docs': 'Docs',
|
|
2298
|
-
'doc': 'Docs',
|
|
2299
|
-
'args': 'Args',
|
|
2300
|
-
'arg': 'Args',
|
|
2301
|
-
'parameters': 'Args',
|
|
2302
|
-
'parameter': 'Args',
|
|
2303
|
-
'returns': 'Returns',
|
|
2304
|
-
'return': 'Returns',
|
|
2305
|
-
'yields': 'Yields',
|
|
2306
|
-
'yield': 'Yields',
|
|
2307
|
-
'raises': 'Raises',
|
|
2308
|
-
'raise': 'Raises',
|
|
2309
|
-
'note': 'Note',
|
|
2310
|
-
'notes': 'Note',
|
|
2311
|
-
'example': 'Example',
|
|
2312
|
-
'examples': 'Example',
|
|
2313
|
-
};
|
|
2314
|
-
|
|
2315
|
-
for (let [key, content] of Object.entries(docInfo.sections)) {
|
|
2316
|
-
if (key === 'brief') continue; // Already shown above
|
|
2317
|
-
|
|
2318
|
-
const label = sectionLabels[key] || key.charAt(0).toUpperCase() + key.slice(1);
|
|
2319
|
-
// Truncate long sections for footer
|
|
2320
|
-
const truncated = content.length > 200 ? content.substring(0, 197) + '...' : content;
|
|
2321
|
-
|
|
2322
|
-
footerHtml += `<div class="footer-item"><span class="footer-label">${label}:</span> <span class="footer-value">${escapeHtml(truncated)}</span></div>`;
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
footer.innerHTML = footerHtml;
|
|
2326
|
-
|
|
2327
|
-
// Add click handlers to caller links
|
|
2328
|
-
const callerLinks = footer.querySelectorAll('.caller-link');
|
|
2329
|
-
callerLinks.forEach(link => {
|
|
2330
|
-
link.addEventListener('click', (e) => {
|
|
2331
|
-
e.preventDefault();
|
|
2332
|
-
const chunkId = link.getAttribute('data-chunk-id');
|
|
2333
|
-
const callerNode = allNodes.find(n => n.id === chunkId);
|
|
2334
|
-
if (callerNode) {
|
|
2335
|
-
// Navigate to the caller node
|
|
2336
|
-
navigateToNode(callerNode);
|
|
2337
|
-
}
|
|
2338
|
-
});
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
function escapeHtml(text) {
|
|
2343
|
-
const div = document.createElement('div');
|
|
2344
|
-
div.textContent = text;
|
|
2345
|
-
return div.innerHTML;
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
function navigateToNode(targetNode) {
|
|
2349
|
-
// Ensure the node is visible in the graph
|
|
2350
|
-
if (!visibleNodes.has(targetNode.id)) {
|
|
2351
|
-
// Expand parent nodes to make this node visible
|
|
2352
|
-
expandParentsToNode(targetNode);
|
|
2353
|
-
renderGraph();
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
// Show the content pane for this node
|
|
2357
|
-
showContentPane(targetNode);
|
|
2358
|
-
|
|
2359
|
-
// Zoom to the target node
|
|
2360
|
-
setTimeout(() => {
|
|
2361
|
-
// Find the node's position
|
|
2362
|
-
if (targetNode.x !== undefined && targetNode.y !== undefined) {
|
|
2363
|
-
const scale = 1.5; // Zoom level
|
|
2364
|
-
// Position at 30% from left to avoid code pane on right side
|
|
2365
|
-
const translateX = width * 0.3 - scale * targetNode.x;
|
|
2366
|
-
const translateY = height / 2 - scale * targetNode.y;
|
|
2367
|
-
|
|
2368
|
-
svg.transition()
|
|
2369
|
-
.duration(750)
|
|
2370
|
-
.call(
|
|
2371
|
-
zoom.transform,
|
|
2372
|
-
d3.zoomIdentity
|
|
2373
|
-
.translate(translateX, translateY)
|
|
2374
|
-
.scale(scale)
|
|
2375
|
-
);
|
|
2376
|
-
}
|
|
2377
|
-
}, 200);
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
function expandParentsToNode(targetNode) {
|
|
2381
|
-
// Build a path from root to target node
|
|
2382
|
-
const path = [];
|
|
2383
|
-
let current = targetNode;
|
|
2384
|
-
|
|
2385
|
-
while (current) {
|
|
2386
|
-
path.unshift(current);
|
|
2387
|
-
// Find parent
|
|
2388
|
-
const parentLink = allLinks.find(l =>
|
|
2389
|
-
(l.target.id || l.target) === current.id &&
|
|
2390
|
-
(l.type !== 'semantic' && l.type !== 'dependency')
|
|
2391
|
-
);
|
|
2392
|
-
if (parentLink) {
|
|
2393
|
-
const parentId = parentLink.source.id || parentLink.source;
|
|
2394
|
-
current = allNodes.find(n => n.id === parentId);
|
|
2395
|
-
} else {
|
|
2396
|
-
break;
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
// Expand all nodes in the path
|
|
2401
|
-
path.forEach(node => {
|
|
2402
|
-
if (!visibleNodes.has(node.id)) {
|
|
2403
|
-
visibleNodes.add(node.id);
|
|
2404
|
-
}
|
|
2405
|
-
if (collapsedNodes.has(node.id)) {
|
|
2406
|
-
expandNode(node);
|
|
2407
|
-
}
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
function closeContentPane() {
|
|
2412
|
-
const pane = document.getElementById('content-pane');
|
|
2413
|
-
pane.classList.remove('visible');
|
|
2414
|
-
|
|
2415
|
-
// Remove highlight
|
|
2416
|
-
highlightedNode = null;
|
|
2417
|
-
renderGraph();
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
// Auto-load graph data on page load with progress tracking
|
|
2421
|
-
window.addEventListener('DOMContentLoaded', () => {
|
|
2422
|
-
const loadingEl = document.getElementById('loading');
|
|
2423
|
-
|
|
2424
|
-
// Show initial loading message
|
|
2425
|
-
loadingEl.innerHTML = '<label style="color: #58a6ff;"><span class="spinner"></span>Loading graph data...</label><br>' +
|
|
2426
|
-
'<div style="margin-top: 8px; background: #21262d; border-radius: 4px; height: 20px; width: 250px; position: relative; overflow: hidden;">' +
|
|
2427
|
-
'<div id="progress-bar" style="background: #238636; height: 100%; width: 0%; transition: width 0.3s;"></div>' +
|
|
2428
|
-
'</div>' +
|
|
2429
|
-
'<small id="progress-text" style="color: #8b949e; margin-top: 4px; display: block;">Connecting...</small>';
|
|
2430
|
-
|
|
2431
|
-
// Create abort controller for timeout
|
|
2432
|
-
const controller = new AbortController();
|
|
2433
|
-
const timeout = setTimeout(() => controller.abort(), 60000); // 60s timeout for large files
|
|
2434
|
-
|
|
2435
|
-
fetch("chunk-graph.json", { signal: controller.signal })
|
|
2436
|
-
.then(response => {
|
|
2437
|
-
if (!response.ok) {
|
|
2438
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
const contentLength = response.headers.get('content-length');
|
|
2442
|
-
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
2443
|
-
let loaded = 0;
|
|
2444
|
-
|
|
2445
|
-
const progressBar = document.getElementById('progress-bar');
|
|
2446
|
-
const progressText = document.getElementById('progress-text');
|
|
2447
|
-
|
|
2448
|
-
// Update progress text based on file size
|
|
2449
|
-
if (total > 0) {
|
|
2450
|
-
const sizeMB = (total / (1024 * 1024)).toFixed(1);
|
|
2451
|
-
progressText.textContent = `Downloading ${sizeMB}MB...`;
|
|
2452
|
-
} else {
|
|
2453
|
-
progressText.textContent = 'Downloading...';
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
// Create a new response with progress tracking
|
|
2457
|
-
const reader = response.body.getReader();
|
|
2458
|
-
const stream = new ReadableStream({
|
|
2459
|
-
start(controller) {
|
|
2460
|
-
function push() {
|
|
2461
|
-
reader.read().then(({ done, value }) => {
|
|
2462
|
-
if (done) {
|
|
2463
|
-
controller.close();
|
|
2464
|
-
return;
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
loaded += value.byteLength;
|
|
2468
|
-
|
|
2469
|
-
// Update progress bar
|
|
2470
|
-
if (total > 0) {
|
|
2471
|
-
const percent = Math.round((loaded / total) * 100);
|
|
2472
|
-
progressBar.style.width = percent + '%';
|
|
2473
|
-
const loadedMB = (loaded / (1024 * 1024)).toFixed(1);
|
|
2474
|
-
const totalMB = (total / (1024 * 1024)).toFixed(1);
|
|
2475
|
-
progressText.textContent = `Downloaded ${loadedMB}MB / ${totalMB}MB (${percent}%)`;
|
|
2476
|
-
} else {
|
|
2477
|
-
const loadedMB = (loaded / (1024 * 1024)).toFixed(1);
|
|
2478
|
-
progressText.textContent = `Downloaded ${loadedMB}MB...`;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
controller.enqueue(value);
|
|
2482
|
-
push();
|
|
2483
|
-
}).catch(err => {
|
|
2484
|
-
console.error('Stream reading error:', err);
|
|
2485
|
-
controller.error(err);
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
push();
|
|
2489
|
-
}
|
|
2490
|
-
});
|
|
2491
|
-
|
|
2492
|
-
return new Response(stream);
|
|
2493
|
-
})
|
|
2494
|
-
.then(response => {
|
|
2495
|
-
clearTimeout(timeout);
|
|
2496
|
-
|
|
2497
|
-
// Update UI for parsing phase
|
|
2498
|
-
const progressText = document.getElementById('progress-text');
|
|
2499
|
-
const progressBar = document.getElementById('progress-bar');
|
|
2500
|
-
progressBar.style.width = '100%';
|
|
2501
|
-
progressText.textContent = 'Parsing JSON data...';
|
|
2502
|
-
|
|
2503
|
-
// Parse JSON (this may take time for large files)
|
|
2504
|
-
return response.json();
|
|
2505
|
-
})
|
|
2506
|
-
.then(data => {
|
|
2507
|
-
clearTimeout(timeout);
|
|
2508
|
-
loadingEl.innerHTML = '<label style="color: #238636;">✓ Graph loaded successfully</label>';
|
|
2509
|
-
setTimeout(() => loadingEl.style.display = 'none', 2000);
|
|
2510
|
-
visualizeGraph(data);
|
|
2511
|
-
})
|
|
2512
|
-
.catch(err => {
|
|
2513
|
-
clearTimeout(timeout);
|
|
2514
|
-
|
|
2515
|
-
let errorMsg = err.message;
|
|
2516
|
-
if (err.name === 'AbortError') {
|
|
2517
|
-
errorMsg = 'Loading timeout - file may be too large or server unresponsive';
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
loadingEl.innerHTML = `<label style="color: #f85149;">✗ Failed to load graph data</label><br>` +
|
|
2521
|
-
`<small style="color: #8b949e;">${errorMsg}</small><br>` +
|
|
2522
|
-
`<small style="color: #8b949e;">Run: mcp-vector-search visualize export</small>`;
|
|
2523
|
-
console.error("Failed to load graph:", err);
|
|
2524
|
-
});
|
|
2525
|
-
});
|
|
2526
|
-
|
|
2527
|
-
// Reset view button event handler
|
|
2528
|
-
document.getElementById('reset-view-btn').addEventListener('click', () => {
|
|
2529
|
-
resetView();
|
|
2530
|
-
});
|
|
2531
|
-
</script>
|
|
2532
|
-
</body>
|
|
2533
|
-
</html>"""
|
|
2534
|
-
|
|
2535
|
-
with open(html_file, "w") as f:
|
|
2536
|
-
f.write(html_content)
|