codegraph-cli 2.0.0__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.
- codegraph_cli/__init__.py +4 -0
- codegraph_cli/agents.py +191 -0
- codegraph_cli/bug_detector.py +386 -0
- codegraph_cli/chat_agent.py +352 -0
- codegraph_cli/chat_session.py +220 -0
- codegraph_cli/cli.py +330 -0
- codegraph_cli/cli_chat.py +367 -0
- codegraph_cli/cli_diagnose.py +133 -0
- codegraph_cli/cli_refactor.py +230 -0
- codegraph_cli/cli_setup.py +470 -0
- codegraph_cli/cli_test.py +177 -0
- codegraph_cli/cli_v2.py +267 -0
- codegraph_cli/codegen_agent.py +265 -0
- codegraph_cli/config.py +31 -0
- codegraph_cli/config_manager.py +341 -0
- codegraph_cli/context_manager.py +500 -0
- codegraph_cli/crew_agents.py +123 -0
- codegraph_cli/crew_chat.py +159 -0
- codegraph_cli/crew_tools.py +497 -0
- codegraph_cli/diff_engine.py +265 -0
- codegraph_cli/embeddings.py +241 -0
- codegraph_cli/graph_export.py +144 -0
- codegraph_cli/llm.py +642 -0
- codegraph_cli/models.py +47 -0
- codegraph_cli/models_v2.py +185 -0
- codegraph_cli/orchestrator.py +49 -0
- codegraph_cli/parser.py +800 -0
- codegraph_cli/performance_analyzer.py +223 -0
- codegraph_cli/project_context.py +230 -0
- codegraph_cli/rag.py +200 -0
- codegraph_cli/refactor_agent.py +452 -0
- codegraph_cli/security_scanner.py +366 -0
- codegraph_cli/storage.py +390 -0
- codegraph_cli/templates/graph_interactive.html +257 -0
- codegraph_cli/testgen_agent.py +316 -0
- codegraph_cli/validation_engine.py +285 -0
- codegraph_cli/vector_store.py +293 -0
- codegraph_cli-2.0.0.dist-info/METADATA +318 -0
- codegraph_cli-2.0.0.dist-info/RECORD +43 -0
- codegraph_cli-2.0.0.dist-info/WHEEL +5 -0
- codegraph_cli-2.0.0.dist-info/entry_points.txt +2 -0
- codegraph_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_cli-2.0.0.dist-info/top_level.txt +1 -0
codegraph_cli/storage.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Persistence layer for project-specific code graph memory.
|
|
2
|
+
|
|
3
|
+
Architecture:
|
|
4
|
+
- **SQLite** for structured data (nodes, edges) and graph traversal queries.
|
|
5
|
+
- **LanceDB** (via :class:`~codegraph_cli.vector_store.VectorStore`) for
|
|
6
|
+
vector similarity search.
|
|
7
|
+
|
|
8
|
+
This hybrid approach gives the best of both worlds: fast relational queries
|
|
9
|
+
for graph traversal and fast ANN search for semantic retrieval.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import sqlite3
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from .config import MEMORY_DIR, STATE_FILE, ensure_base_dirs
|
|
21
|
+
from .models import Edge, Node
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from .vector_store import VectorStore
|
|
27
|
+
VECTOR_STORE_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
VECTOR_STORE_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ===================================================================
|
|
33
|
+
# ProjectManager (unchanged – manages directories / active project)
|
|
34
|
+
# ===================================================================
|
|
35
|
+
|
|
36
|
+
class ProjectManager:
|
|
37
|
+
"""Manage project memory directories and active project state."""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
ensure_base_dirs()
|
|
41
|
+
|
|
42
|
+
def list_projects(self) -> List[str]:
|
|
43
|
+
if not MEMORY_DIR.exists():
|
|
44
|
+
return []
|
|
45
|
+
return sorted([p.name for p in MEMORY_DIR.iterdir() if p.is_dir()])
|
|
46
|
+
|
|
47
|
+
def project_dir(self, project_name: str) -> Path:
|
|
48
|
+
return MEMORY_DIR / project_name
|
|
49
|
+
|
|
50
|
+
def create_or_get_project(self, project_name: str) -> Path:
|
|
51
|
+
path = self.project_dir(project_name)
|
|
52
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
def set_current_project(self, project_name: str) -> None:
|
|
56
|
+
ensure_base_dirs()
|
|
57
|
+
STATE_FILE.write_text(
|
|
58
|
+
json.dumps({"current_project": project_name}, indent=2),
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_current_project(self) -> Optional[str]:
|
|
63
|
+
if not STATE_FILE.exists():
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
payload = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
return None
|
|
69
|
+
return payload.get("current_project")
|
|
70
|
+
|
|
71
|
+
def unload_project(self) -> None:
|
|
72
|
+
ensure_base_dirs()
|
|
73
|
+
STATE_FILE.write_text(
|
|
74
|
+
json.dumps({"current_project": None}, indent=2),
|
|
75
|
+
encoding="utf-8",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def delete_project(self, project_name: str) -> bool:
|
|
79
|
+
path = self.project_dir(project_name)
|
|
80
|
+
if not path.exists():
|
|
81
|
+
return False
|
|
82
|
+
for child in sorted(path.glob("**/*"), reverse=True):
|
|
83
|
+
if child.is_file():
|
|
84
|
+
child.unlink()
|
|
85
|
+
elif child.is_dir():
|
|
86
|
+
child.rmdir()
|
|
87
|
+
path.rmdir()
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ===================================================================
|
|
92
|
+
# GraphStore (SQLite + LanceDB hybrid)
|
|
93
|
+
# ===================================================================
|
|
94
|
+
|
|
95
|
+
class GraphStore:
|
|
96
|
+
"""Hybrid store: SQLite for structure, LanceDB for vectors.
|
|
97
|
+
|
|
98
|
+
Public API is backward-compatible with the legacy SQLite-only store.
|
|
99
|
+
The ``vector_store`` attribute exposes the underlying
|
|
100
|
+
:class:`~codegraph_cli.vector_store.VectorStore` for direct vector
|
|
101
|
+
search when needed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, project_dir: Path) -> None:
|
|
105
|
+
self.project_dir = project_dir
|
|
106
|
+
self.db_path = project_dir / "graph.db"
|
|
107
|
+
self.meta_path = project_dir / "project.json"
|
|
108
|
+
self.conn = sqlite3.connect(str(self.db_path))
|
|
109
|
+
self.conn.row_factory = sqlite3.Row
|
|
110
|
+
self._init_schema()
|
|
111
|
+
|
|
112
|
+
# Initialise LanceDB vector store
|
|
113
|
+
self.vector_store: Optional[VectorStore] = None
|
|
114
|
+
if VECTOR_STORE_AVAILABLE:
|
|
115
|
+
try:
|
|
116
|
+
self.vector_store = VectorStore(project_dir)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.warning("LanceDB vector store unavailable: %s", exc)
|
|
119
|
+
|
|
120
|
+
def close(self) -> None:
|
|
121
|
+
self.conn.close()
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# Schema
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def _init_schema(self) -> None:
|
|
128
|
+
cur = self.conn.cursor()
|
|
129
|
+
cur.execute("""
|
|
130
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
131
|
+
node_id TEXT PRIMARY KEY,
|
|
132
|
+
node_type TEXT NOT NULL,
|
|
133
|
+
name TEXT NOT NULL,
|
|
134
|
+
qualname TEXT NOT NULL,
|
|
135
|
+
file_path TEXT NOT NULL,
|
|
136
|
+
start_line INTEGER NOT NULL,
|
|
137
|
+
end_line INTEGER NOT NULL,
|
|
138
|
+
code TEXT NOT NULL,
|
|
139
|
+
docstring TEXT,
|
|
140
|
+
embedding TEXT,
|
|
141
|
+
metadata TEXT
|
|
142
|
+
)
|
|
143
|
+
""")
|
|
144
|
+
cur.execute("""
|
|
145
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
146
|
+
src TEXT NOT NULL,
|
|
147
|
+
dst TEXT NOT NULL,
|
|
148
|
+
edge_type TEXT NOT NULL
|
|
149
|
+
)
|
|
150
|
+
""")
|
|
151
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src)")
|
|
152
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst)")
|
|
153
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)")
|
|
154
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_nodes_qualname ON nodes(qualname)")
|
|
155
|
+
self.conn.commit()
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Clear / metadata
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def clear(self) -> None:
|
|
162
|
+
cur = self.conn.cursor()
|
|
163
|
+
cur.execute("DELETE FROM edges")
|
|
164
|
+
cur.execute("DELETE FROM nodes")
|
|
165
|
+
self.conn.commit()
|
|
166
|
+
if self.vector_store is not None:
|
|
167
|
+
try:
|
|
168
|
+
self.vector_store.clear()
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def set_metadata(self, payload: Dict[str, Any]) -> None:
|
|
173
|
+
self.meta_path.write_text(
|
|
174
|
+
json.dumps(payload, indent=2), encoding="utf-8",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
178
|
+
if not self.meta_path.exists():
|
|
179
|
+
return {}
|
|
180
|
+
try:
|
|
181
|
+
return json.loads(self.meta_path.read_text(encoding="utf-8"))
|
|
182
|
+
except json.JSONDecodeError:
|
|
183
|
+
return {}
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Insert
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def insert_nodes(self, rows: Iterable[Tuple[Node, List[float]]]) -> None:
|
|
190
|
+
"""Insert nodes with their embedding vectors.
|
|
191
|
+
|
|
192
|
+
Each element of *rows* is a ``(Node, embedding)`` tuple. Data is
|
|
193
|
+
written to both SQLite (for structured queries) and LanceDB (for
|
|
194
|
+
vector search).
|
|
195
|
+
"""
|
|
196
|
+
rows_list = list(rows)
|
|
197
|
+
if not rows_list:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# ---- SQLite -----------------------------------------------------
|
|
201
|
+
self.conn.executemany(
|
|
202
|
+
"""
|
|
203
|
+
INSERT OR REPLACE INTO nodes (
|
|
204
|
+
node_id, node_type, name, qualname, file_path,
|
|
205
|
+
start_line, end_line, code, docstring, embedding, metadata
|
|
206
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
|
+
""",
|
|
208
|
+
[
|
|
209
|
+
(
|
|
210
|
+
node.node_id,
|
|
211
|
+
node.node_type,
|
|
212
|
+
node.name,
|
|
213
|
+
node.qualname,
|
|
214
|
+
node.file_path,
|
|
215
|
+
node.start_line,
|
|
216
|
+
node.end_line,
|
|
217
|
+
node.code,
|
|
218
|
+
node.docstring,
|
|
219
|
+
json.dumps(embedding),
|
|
220
|
+
json.dumps(node.metadata) if node.metadata else None,
|
|
221
|
+
)
|
|
222
|
+
for node, embedding in rows_list
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
self.conn.commit()
|
|
226
|
+
|
|
227
|
+
# ---- LanceDB (vector store) ------------------------------------
|
|
228
|
+
if self.vector_store is not None:
|
|
229
|
+
try:
|
|
230
|
+
node_ids = [node.node_id for node, _ in rows_list]
|
|
231
|
+
embeddings = [emb for _, emb in rows_list]
|
|
232
|
+
metadatas = [
|
|
233
|
+
{
|
|
234
|
+
"node_type": node.node_type,
|
|
235
|
+
"file_path": node.file_path,
|
|
236
|
+
"qualname": node.qualname,
|
|
237
|
+
"name": node.name,
|
|
238
|
+
}
|
|
239
|
+
for node, _ in rows_list
|
|
240
|
+
]
|
|
241
|
+
documents = [node.code for node, _ in rows_list]
|
|
242
|
+
self.vector_store.add_nodes(
|
|
243
|
+
node_ids, embeddings, metadatas, documents,
|
|
244
|
+
)
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
logger.warning("Failed to sync nodes to LanceDB: %s", exc)
|
|
247
|
+
|
|
248
|
+
def insert_edges(self, edges: Iterable[Edge]) -> None:
|
|
249
|
+
cur = self.conn.cursor()
|
|
250
|
+
cur.executemany(
|
|
251
|
+
"INSERT INTO edges (src, dst, edge_type) VALUES (?, ?, ?)",
|
|
252
|
+
[(e.src, e.dst, e.edge_type) for e in edges],
|
|
253
|
+
)
|
|
254
|
+
self.conn.commit()
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
# Read (structured)
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def get_nodes(self) -> List[sqlite3.Row]:
|
|
261
|
+
return self.conn.execute("SELECT * FROM nodes").fetchall()
|
|
262
|
+
|
|
263
|
+
def get_node(self, node_id_or_name: str) -> Optional[sqlite3.Row]:
|
|
264
|
+
return self.conn.execute(
|
|
265
|
+
"SELECT * FROM nodes WHERE node_id = ? OR qualname = ? OR name = ? LIMIT 1",
|
|
266
|
+
(node_id_or_name, node_id_or_name, node_id_or_name),
|
|
267
|
+
).fetchone()
|
|
268
|
+
|
|
269
|
+
def get_edges(self) -> List[sqlite3.Row]:
|
|
270
|
+
return self.conn.execute("SELECT * FROM edges").fetchall()
|
|
271
|
+
|
|
272
|
+
def neighbors(self, src_node_id: str) -> List[sqlite3.Row]:
|
|
273
|
+
return self.conn.execute(
|
|
274
|
+
"SELECT * FROM edges WHERE src = ?", (src_node_id,),
|
|
275
|
+
).fetchall()
|
|
276
|
+
|
|
277
|
+
def reverse_neighbors(self, dst_node_id: str) -> List[sqlite3.Row]:
|
|
278
|
+
return self.conn.execute(
|
|
279
|
+
"SELECT * FROM edges WHERE dst = ?", (dst_node_id,),
|
|
280
|
+
).fetchall()
|
|
281
|
+
|
|
282
|
+
def all_by_file(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
283
|
+
by_file: Dict[str, List[Dict[str, Any]]] = {}
|
|
284
|
+
for row in self.get_nodes():
|
|
285
|
+
payload = dict(row)
|
|
286
|
+
by_file.setdefault(payload["file_path"], []).append(payload)
|
|
287
|
+
return by_file
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
# Vector search (convenience wrappers)
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def search_vectors(
|
|
294
|
+
self,
|
|
295
|
+
query_vector: List[float],
|
|
296
|
+
top_k: int = 10,
|
|
297
|
+
where: Optional[Dict[str, str]] = None,
|
|
298
|
+
) -> List[Dict[str, Any]]:
|
|
299
|
+
"""Semantic search via LanceDB.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
query_vector: Embedding of the search query.
|
|
303
|
+
top_k: Max results.
|
|
304
|
+
where: Metadata filter dict, e.g. ``{"node_type": "function"}``.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List of result dicts with ``id``, ``_distance``, ``document``, etc.
|
|
308
|
+
"""
|
|
309
|
+
if self.vector_store is None:
|
|
310
|
+
return []
|
|
311
|
+
result = self.vector_store.search(query_vector, n_results=top_k, where=where)
|
|
312
|
+
# Flatten the nested Chroma-compat format into a plain list
|
|
313
|
+
out: List[Dict[str, Any]] = []
|
|
314
|
+
if result["ids"] and result["ids"][0]:
|
|
315
|
+
for i, nid in enumerate(result["ids"][0]):
|
|
316
|
+
out.append({
|
|
317
|
+
"id": nid,
|
|
318
|
+
"_distance": result["distances"][0][i] if result["distances"][0] else 0.0,
|
|
319
|
+
"metadata": result["metadatas"][0][i] if result["metadatas"][0] else {},
|
|
320
|
+
"document": result["documents"][0][i] if result["documents"][0] else "",
|
|
321
|
+
})
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
def hybrid_search(
|
|
325
|
+
self,
|
|
326
|
+
query_vector: List[float],
|
|
327
|
+
top_k: int = 10,
|
|
328
|
+
where_sql: Optional[str] = None,
|
|
329
|
+
) -> List[Dict[str, Any]]:
|
|
330
|
+
"""Hybrid search: vector + SQL filter (e.g. ``file_path LIKE 'src/%'``).
|
|
331
|
+
|
|
332
|
+
Falls back to :meth:`search_vectors` when where_sql is ``None``.
|
|
333
|
+
"""
|
|
334
|
+
if self.vector_store is None:
|
|
335
|
+
return []
|
|
336
|
+
return self.vector_store.hybrid_search(
|
|
337
|
+
query_vector, n_results=top_k, where_sql=where_sql,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
# Merge (cross-project)
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
def merge_from(self, other: "GraphStore", source_project: str) -> None:
|
|
345
|
+
current_nodes = self.conn.execute("SELECT node_id FROM nodes").fetchall()
|
|
346
|
+
existing = {r[0] for r in current_nodes}
|
|
347
|
+
|
|
348
|
+
node_rows: List[Dict[str, Any]] = []
|
|
349
|
+
id_map: Dict[str, str] = {}
|
|
350
|
+
for row in other.get_nodes():
|
|
351
|
+
row_dict = dict(row)
|
|
352
|
+
original_id = row_dict["node_id"]
|
|
353
|
+
new_id = original_id if original_id not in existing else f"{source_project}:{original_id}"
|
|
354
|
+
existing.add(new_id)
|
|
355
|
+
id_map[original_id] = new_id
|
|
356
|
+
metadata = json.loads(row_dict.get("metadata") or "{}")
|
|
357
|
+
metadata["merged_from"] = source_project
|
|
358
|
+
row_dict["node_id"] = new_id
|
|
359
|
+
row_dict["metadata"] = json.dumps(metadata)
|
|
360
|
+
node_rows.append(row_dict)
|
|
361
|
+
|
|
362
|
+
cur = self.conn.cursor()
|
|
363
|
+
cur.executemany(
|
|
364
|
+
"""
|
|
365
|
+
INSERT OR REPLACE INTO nodes (
|
|
366
|
+
node_id, node_type, name, qualname, file_path,
|
|
367
|
+
start_line, end_line, code, docstring, embedding, metadata
|
|
368
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
369
|
+
""",
|
|
370
|
+
[
|
|
371
|
+
(
|
|
372
|
+
n["node_id"], n["node_type"], n["name"], n["qualname"],
|
|
373
|
+
n["file_path"], n["start_line"], n["end_line"], n["code"],
|
|
374
|
+
n["docstring"], n["embedding"], n["metadata"],
|
|
375
|
+
)
|
|
376
|
+
for n in node_rows
|
|
377
|
+
],
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
edge_rows: List[Tuple[str, str, str]] = []
|
|
381
|
+
for edge_row in other.get_edges():
|
|
382
|
+
e = dict(edge_row)
|
|
383
|
+
src = id_map.get(e["src"], e["src"])
|
|
384
|
+
dst = id_map.get(e["dst"], e["dst"])
|
|
385
|
+
edge_rows.append((src, dst, e["edge_type"]))
|
|
386
|
+
cur.executemany(
|
|
387
|
+
"INSERT INTO edges (src, dst, edge_type) VALUES (?, ?, ?)",
|
|
388
|
+
edge_rows,
|
|
389
|
+
)
|
|
390
|
+
self.conn.commit()
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>CodeGraph Interactive Visualization</title>
|
|
7
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#header {
|
|
17
|
+
background: white;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
margin-bottom: 20px;
|
|
21
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
h1 {
|
|
25
|
+
margin: 0 0 10px 0;
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#stats {
|
|
30
|
+
color: #666;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#graph {
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 700px;
|
|
37
|
+
background: white;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#legend {
|
|
43
|
+
background: white;
|
|
44
|
+
padding: 15px;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
margin-top: 20px;
|
|
47
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.legend-item {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
margin-right: 20px;
|
|
53
|
+
margin-bottom: 10px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.legend-color {
|
|
57
|
+
display: inline-block;
|
|
58
|
+
width: 20px;
|
|
59
|
+
height: 20px;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
margin-right: 8px;
|
|
62
|
+
vertical-align: middle;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#info {
|
|
66
|
+
background: white;
|
|
67
|
+
padding: 15px;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
margin-top: 20px;
|
|
70
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
|
|
76
|
+
<body>
|
|
77
|
+
<div id="header">
|
|
78
|
+
<h1>🔍 CodeGraph Interactive Visualization</h1>
|
|
79
|
+
<div id="stats"></div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div id="graph"></div>
|
|
83
|
+
|
|
84
|
+
<div id="legend">
|
|
85
|
+
<strong>Legend:</strong>
|
|
86
|
+
<div class="legend-item">
|
|
87
|
+
<span class="legend-color" style="background: #4A90E2;"></span>
|
|
88
|
+
<span>Module</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="legend-item">
|
|
91
|
+
<span class="legend-color" style="background: #7ED321;"></span>
|
|
92
|
+
<span>Class</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="legend-item">
|
|
95
|
+
<span class="legend-color" style="background: #F5A623;"></span>
|
|
96
|
+
<span>Function</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="legend-item">
|
|
99
|
+
<span style="color: #999; margin-right: 8px;">━━━</span>
|
|
100
|
+
<span>Contains</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="legend-item">
|
|
103
|
+
<span style="color: #E74C3C; margin-right: 8px;">━━━▶</span>
|
|
104
|
+
<span>Calls</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="legend-item">
|
|
107
|
+
<span style="color: #9B59B6; margin-right: 8px;">- - -▶</span>
|
|
108
|
+
<span>Depends On</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div id="info"></div>
|
|
113
|
+
|
|
114
|
+
<script type="text/javascript">
|
|
115
|
+
// Graph data will be injected here
|
|
116
|
+
const graphData = {{ GRAPH_DATA }};
|
|
117
|
+
|
|
118
|
+
// Transform nodes
|
|
119
|
+
const nodes = graphData.nodes.map(node => {
|
|
120
|
+
let color = '#4A90E2'; // Module
|
|
121
|
+
let shape = 'box';
|
|
122
|
+
|
|
123
|
+
if (node.id.startsWith('class:')) {
|
|
124
|
+
color = '#7ED321'; // Class
|
|
125
|
+
shape = 'ellipse';
|
|
126
|
+
} else if (node.id.startsWith('function:')) {
|
|
127
|
+
color = '#F5A623'; // Function
|
|
128
|
+
shape = 'box';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id: node.id,
|
|
133
|
+
label: node.label.split(':')[1] || node.label,
|
|
134
|
+
title: `${node.label}\n${node.title}`,
|
|
135
|
+
color: {
|
|
136
|
+
background: color,
|
|
137
|
+
border: color,
|
|
138
|
+
highlight: {
|
|
139
|
+
background: color,
|
|
140
|
+
border: '#333'
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
shape: shape,
|
|
144
|
+
font: {
|
|
145
|
+
color: 'white',
|
|
146
|
+
size: 14
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Transform edges
|
|
152
|
+
const edges = graphData.edges.map(edge => {
|
|
153
|
+
let color = '#999';
|
|
154
|
+
let dashes = false;
|
|
155
|
+
let arrows = '';
|
|
156
|
+
|
|
157
|
+
if (edge.edge_type === 'calls') {
|
|
158
|
+
color = '#E74C3C';
|
|
159
|
+
arrows = 'to';
|
|
160
|
+
} else if (edge.edge_type === 'depends_on') {
|
|
161
|
+
color = '#9B59B6';
|
|
162
|
+
dashes = true;
|
|
163
|
+
arrows = 'to';
|
|
164
|
+
} else if (edge.edge_type === 'contains') {
|
|
165
|
+
color = '#999';
|
|
166
|
+
arrows = '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
from: edge.src,
|
|
171
|
+
to: edge.dst,
|
|
172
|
+
color: color,
|
|
173
|
+
dashes: dashes,
|
|
174
|
+
arrows: arrows,
|
|
175
|
+
title: edge.edge_type
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update stats
|
|
180
|
+
const callEdges = graphData.edges.filter(e => e.edge_type === 'calls').length;
|
|
181
|
+
const containsEdges = graphData.edges.filter(e => e.edge_type === 'contains').length;
|
|
182
|
+
const dependsEdges = graphData.edges.filter(e => e.edge_type === 'depends_on').length;
|
|
183
|
+
|
|
184
|
+
document.getElementById('stats').innerHTML = `
|
|
185
|
+
<strong>${nodes.length}</strong> nodes |
|
|
186
|
+
<strong>${edges.length}</strong> edges
|
|
187
|
+
(<strong>${callEdges}</strong> calls,
|
|
188
|
+
<strong>${containsEdges}</strong> contains,
|
|
189
|
+
<strong>${dependsEdges}</strong> dependencies)
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
// Create network
|
|
193
|
+
const container = document.getElementById('graph');
|
|
194
|
+
const data = {
|
|
195
|
+
nodes: new vis.DataSet(nodes),
|
|
196
|
+
edges: new vis.DataSet(edges)
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const options = {
|
|
200
|
+
physics: {
|
|
201
|
+
enabled: true,
|
|
202
|
+
barnesHut: {
|
|
203
|
+
gravitationalConstant: -8000,
|
|
204
|
+
centralGravity: 0.3,
|
|
205
|
+
springLength: 150,
|
|
206
|
+
springConstant: 0.04
|
|
207
|
+
},
|
|
208
|
+
stabilization: {
|
|
209
|
+
iterations: 200
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
interaction: {
|
|
213
|
+
hover: true,
|
|
214
|
+
tooltipDelay: 100,
|
|
215
|
+
zoomView: true,
|
|
216
|
+
dragView: true
|
|
217
|
+
},
|
|
218
|
+
layout: {
|
|
219
|
+
hierarchical: {
|
|
220
|
+
enabled: false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const network = new vis.Network(container, data, options);
|
|
226
|
+
|
|
227
|
+
// Click handler
|
|
228
|
+
network.on('click', function (params) {
|
|
229
|
+
if (params.nodes.length > 0) {
|
|
230
|
+
const nodeId = params.nodes[0];
|
|
231
|
+
const node = graphData.nodes.find(n => n.id === nodeId);
|
|
232
|
+
|
|
233
|
+
if (node) {
|
|
234
|
+
const infoDiv = document.getElementById('info');
|
|
235
|
+
infoDiv.style.display = 'block';
|
|
236
|
+
infoDiv.innerHTML = `
|
|
237
|
+
<h3>${node.label}</h3>
|
|
238
|
+
<p><strong>Type:</strong> ${node.id.split(':')[0]}</p>
|
|
239
|
+
<p><strong>File:</strong> ${node.title}</p>
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Stabilization progress
|
|
246
|
+
network.on('stabilizationProgress', function (params) {
|
|
247
|
+
const progress = Math.round((params.iterations / params.total) * 100);
|
|
248
|
+
document.getElementById('stats').innerHTML += ` | Rendering: ${progress}%`;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
network.on('stabilizationIterationsDone', function () {
|
|
252
|
+
document.getElementById('stats').innerHTML = document.getElementById('stats').innerHTML.replace(/\| Rendering: \d+%/, '');
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
</body>
|
|
256
|
+
|
|
257
|
+
</html>
|