codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tree-sitter language and parser management module.
|
|
3
|
+
|
|
4
|
+
This module provides thread-safe, cached access to tree-sitter languages and parsers.
|
|
5
|
+
It handles the migration from tree-sitter-languages to tree-sitter-language-pack.
|
|
6
|
+
|
|
7
|
+
Key design principles:
|
|
8
|
+
1. Cache languages, not parsers (parsers are NOT thread-safe)
|
|
9
|
+
2. Handle language name aliasing
|
|
10
|
+
3. Provide clear error messages for missing languages
|
|
11
|
+
4. Support optional tree-sitter dependency
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Dict, Optional
|
|
15
|
+
import threading
|
|
16
|
+
|
|
17
|
+
from tree_sitter import Language, Parser
|
|
18
|
+
from tree_sitter_language_pack import get_language
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Language name aliases for compatibility
|
|
22
|
+
LANGUAGE_ALIASES = {
|
|
23
|
+
# Common aliases
|
|
24
|
+
"py": "python",
|
|
25
|
+
"js": "javascript",
|
|
26
|
+
"ts": "typescript",
|
|
27
|
+
"c++": "cpp",
|
|
28
|
+
"c#": "c_sharp",
|
|
29
|
+
"csharp": "c_sharp",
|
|
30
|
+
"cs": "c_sharp",
|
|
31
|
+
"rb": "ruby",
|
|
32
|
+
"rs": "rust",
|
|
33
|
+
"go": "go",
|
|
34
|
+
"php": "php",
|
|
35
|
+
".php": "php",
|
|
36
|
+
|
|
37
|
+
# Canonical names (map to themselves for consistency)
|
|
38
|
+
"python": "python",
|
|
39
|
+
"javascript": "javascript",
|
|
40
|
+
"typescript": "typescript",
|
|
41
|
+
"tsx": "tsx",
|
|
42
|
+
"cpp": "cpp",
|
|
43
|
+
"c_sharp": "c_sharp",
|
|
44
|
+
"c": "c",
|
|
45
|
+
"java": "java",
|
|
46
|
+
"haskell": "haskell",
|
|
47
|
+
"ruby": "ruby",
|
|
48
|
+
"rust": "rust",
|
|
49
|
+
"kt": "kotlin",
|
|
50
|
+
"kotlin": "kotlin",
|
|
51
|
+
"scala": "scala",
|
|
52
|
+
".scala": "scala",
|
|
53
|
+
"swift": "swift",
|
|
54
|
+
".swift": "swift",
|
|
55
|
+
"dart": "dart",
|
|
56
|
+
"perl": "perl",
|
|
57
|
+
"pl": "perl",
|
|
58
|
+
"pm": "perl",
|
|
59
|
+
"elixir": "elixir",
|
|
60
|
+
"ex": "elixir",
|
|
61
|
+
"exs": "elixir",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Canonical names that differ from tree-sitter-language-pack names
|
|
65
|
+
LANGUAGE_PACK_NAMES = {
|
|
66
|
+
"c_sharp": "csharp",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TreeSitterManager:
|
|
71
|
+
"""
|
|
72
|
+
Manages tree-sitter language loading and parser creation.
|
|
73
|
+
|
|
74
|
+
This class provides:
|
|
75
|
+
- Thread-safe language caching
|
|
76
|
+
- Language name aliasing
|
|
77
|
+
- Parser lifecycle management
|
|
78
|
+
- Clear error handling
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self):
|
|
82
|
+
"""Initialize the tree-sitter manager."""
|
|
83
|
+
self._language_cache: Dict[str, Language] = {}
|
|
84
|
+
self._cache_lock = threading.Lock()
|
|
85
|
+
|
|
86
|
+
def _normalize_language_name(self, lang: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Normalize a language name to its canonical form.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
lang: Language name (e.g., "py", "python", "c++")
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Canonical language name (e.g., "python", "cpp")
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If language name is not recognized
|
|
98
|
+
"""
|
|
99
|
+
normalized = LANGUAGE_ALIASES.get(lang.lower())
|
|
100
|
+
if normalized is None:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Unknown language: {lang}. "
|
|
103
|
+
f"Supported languages: {', '.join(sorted(set(LANGUAGE_ALIASES.values())))}"
|
|
104
|
+
)
|
|
105
|
+
return normalized
|
|
106
|
+
|
|
107
|
+
def get_language_safe(self, lang: str) -> Language:
|
|
108
|
+
"""
|
|
109
|
+
Get a cached Language object for the specified language.
|
|
110
|
+
|
|
111
|
+
This method is thread-safe and caches languages to avoid repeated loading.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
lang: Language name (supports aliases like "py", "c++", etc.)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tree-sitter Language object
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If language is not supported
|
|
121
|
+
Exception: If language loading fails
|
|
122
|
+
"""
|
|
123
|
+
# Normalize the language name
|
|
124
|
+
canonical_name = self._normalize_language_name(lang)
|
|
125
|
+
|
|
126
|
+
# Check cache first (fast path, no lock needed for reads)
|
|
127
|
+
if canonical_name in self._language_cache:
|
|
128
|
+
return self._language_cache[canonical_name]
|
|
129
|
+
|
|
130
|
+
# Load language with lock (slow path)
|
|
131
|
+
with self._cache_lock:
|
|
132
|
+
# Double-check after acquiring lock
|
|
133
|
+
if canonical_name in self._language_cache:
|
|
134
|
+
return self._language_cache[canonical_name]
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Map canonical name to language-pack name where they differ
|
|
138
|
+
pack_name = LANGUAGE_PACK_NAMES.get(canonical_name, canonical_name)
|
|
139
|
+
language = get_language(pack_name)
|
|
140
|
+
|
|
141
|
+
self._language_cache[canonical_name] = language
|
|
142
|
+
return language
|
|
143
|
+
except (KeyError, ModuleNotFoundError):
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Language '{canonical_name}' is not available in tree-sitter-language-pack. "
|
|
146
|
+
f"This may be due to a missing or experimental grammar."
|
|
147
|
+
)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise Exception(
|
|
150
|
+
f"Failed to load language '{canonical_name}': {e}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def create_parser(self, lang: str) -> Parser:
|
|
154
|
+
"""
|
|
155
|
+
Create a new Parser instance for the specified language.
|
|
156
|
+
|
|
157
|
+
IMPORTANT: Parsers are NOT thread-safe and should not be shared across threads.
|
|
158
|
+
Each thread should create its own parser using this method.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
lang: Language name (supports aliases)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A new Parser instance configured for the language
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If language is not supported
|
|
168
|
+
Exception: If parser creation fails
|
|
169
|
+
"""
|
|
170
|
+
language = self.get_language_safe(lang)
|
|
171
|
+
# In tree-sitter 0.25+, Parser takes language in constructor
|
|
172
|
+
parser = Parser(language)
|
|
173
|
+
return parser
|
|
174
|
+
|
|
175
|
+
def is_language_available(self, lang: str) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Check if a language is available without raising exceptions.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
lang: Language name
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if language is available, False otherwise
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
self.get_language_safe(lang)
|
|
187
|
+
return True
|
|
188
|
+
except (ValueError, Exception):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def get_supported_languages(self) -> list[str]:
|
|
192
|
+
"""
|
|
193
|
+
Get a list of all supported language names.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Sorted list of canonical language names
|
|
197
|
+
"""
|
|
198
|
+
return sorted(set(LANGUAGE_ALIASES.values()))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Global singleton instance
|
|
202
|
+
_manager_instance: Optional[TreeSitterManager] = None
|
|
203
|
+
_instance_lock = threading.Lock()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_tree_sitter_manager() -> TreeSitterManager:
|
|
207
|
+
"""
|
|
208
|
+
Get the global TreeSitterManager instance (singleton pattern).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The global TreeSitterManager instance
|
|
212
|
+
"""
|
|
213
|
+
global _manager_instance
|
|
214
|
+
|
|
215
|
+
if _manager_instance is not None:
|
|
216
|
+
return _manager_instance
|
|
217
|
+
|
|
218
|
+
with _instance_lock:
|
|
219
|
+
if _manager_instance is None:
|
|
220
|
+
_manager_instance = TreeSitterManager()
|
|
221
|
+
return _manager_instance
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Convenience functions for backward compatibility
|
|
225
|
+
def get_language_safe(lang: str) -> Language:
|
|
226
|
+
"""Get a cached Language object. Thread-safe."""
|
|
227
|
+
return get_tree_sitter_manager().get_language_safe(lang)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def create_parser(lang: str) -> Parser:
|
|
231
|
+
"""Create a new Parser for the language. Each call returns a new parser."""
|
|
232
|
+
return get_tree_sitter_manager().create_parser(lang)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def execute_query(language: Language, query_string: str, node):
|
|
236
|
+
"""
|
|
237
|
+
Execute a tree-sitter query and return captures in backward-compatible format.
|
|
238
|
+
|
|
239
|
+
This function provides compatibility with the old tree-sitter 0.20.x API where
|
|
240
|
+
you could call query.captures(node). The new 0.25+ API uses QueryCursor.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
language: Tree-sitter Language object
|
|
244
|
+
query_string: Query string in tree-sitter query syntax
|
|
245
|
+
node: Tree-sitter Node to query
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of (node, capture_name) tuples, compatible with old API
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
>>> from tree_sitter_language_pack import get_language
|
|
252
|
+
>>> lang = get_language('python')
|
|
253
|
+
>>> parser = Parser(lang)
|
|
254
|
+
>>> tree = parser.parse(b'def hello(): pass')
|
|
255
|
+
>>> captures = execute_query(lang, '(function_definition) @func', tree.root_node)
|
|
256
|
+
>>> for node, name in captures:
|
|
257
|
+
... print(f'{name}: {node.type}')
|
|
258
|
+
"""
|
|
259
|
+
from tree_sitter import Query, QueryCursor
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Create query and cursor
|
|
263
|
+
query = Query(language, query_string)
|
|
264
|
+
cursor = QueryCursor(query)
|
|
265
|
+
|
|
266
|
+
# Execute query and convert to old format
|
|
267
|
+
captures = []
|
|
268
|
+
|
|
269
|
+
# Use matches() which returns (pattern_index, captures_dict) tuples
|
|
270
|
+
for pattern_index, captures_dict in cursor.matches(node):
|
|
271
|
+
# captures_dict is {capture_name: [nodes]}
|
|
272
|
+
for capture_name, nodes in captures_dict.items():
|
|
273
|
+
for captured_node in nodes:
|
|
274
|
+
# Old format: (node, capture_name)
|
|
275
|
+
captures.append((captured_node, capture_name))
|
|
276
|
+
|
|
277
|
+
return captures
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
# Provide helpful error message
|
|
281
|
+
raise Exception(
|
|
282
|
+
f"Failed to execute query: {e}\n"
|
|
283
|
+
f"Query string: {query_string[:100]}..."
|
|
284
|
+
)
|
|
285
|
+
|
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException, Query, Request
|
|
2
|
+
from fastapi.staticfiles import StaticFiles
|
|
3
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
4
|
+
from fastapi.responses import HTMLResponse, FileResponse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import uvicorn
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
|
|
12
|
+
from ..core.database import DatabaseManager
|
|
13
|
+
from ..utils.debug_log import debug_log
|
|
14
|
+
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
|
|
17
|
+
# Enable CORS for development
|
|
18
|
+
app.add_middleware(
|
|
19
|
+
CORSMiddleware,
|
|
20
|
+
allow_origins=["*"],
|
|
21
|
+
allow_methods=["*"],
|
|
22
|
+
allow_headers=["*"],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Global database manager (will be initialized when server starts)
|
|
26
|
+
db_manager: Optional[DatabaseManager] = None
|
|
27
|
+
# Path to static directory
|
|
28
|
+
_static_dir: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
def set_db_manager(manager: DatabaseManager):
|
|
31
|
+
global db_manager
|
|
32
|
+
db_manager = manager
|
|
33
|
+
|
|
34
|
+
@app.get("/api/graph")
|
|
35
|
+
async def get_graph(repo_path: Optional[str] = None, cypher_query: Optional[str] = None):
|
|
36
|
+
if not db_manager:
|
|
37
|
+
raise HTTPException(status_code=500, detail="Database not initialized")
|
|
38
|
+
|
|
39
|
+
def get_eid(element):
|
|
40
|
+
if element is None: return None
|
|
41
|
+
if isinstance(element, (int, str)):
|
|
42
|
+
return str(element)
|
|
43
|
+
|
|
44
|
+
# If element is a dict (like Neo4j returned items or KuzuDB node/rel dicts)
|
|
45
|
+
if isinstance(element, dict):
|
|
46
|
+
# KuzuDB _src / _dst are directly {'offset': X, 'table': Y}
|
|
47
|
+
if 'offset' in element and 'table' in element:
|
|
48
|
+
return f"{element.get('table')}_{element.get('offset')}"
|
|
49
|
+
|
|
50
|
+
for key in ['_id', 'id', 'element_id']:
|
|
51
|
+
if key in element:
|
|
52
|
+
val = element[key]
|
|
53
|
+
if val is not None:
|
|
54
|
+
# KuzuDB returns dict IDs like {'offset': 1, 'table': 0} inside nodes
|
|
55
|
+
if isinstance(val, dict):
|
|
56
|
+
return f"{val.get('table', 0)}_{val.get('offset', 0)}"
|
|
57
|
+
return str(val)
|
|
58
|
+
return str(id(element))
|
|
59
|
+
|
|
60
|
+
# Try various ways to get ID (Neo4j, FalkorDB, etc. objects)
|
|
61
|
+
for attr in ['element_id', 'id', '_id']:
|
|
62
|
+
if hasattr(element, attr):
|
|
63
|
+
val = getattr(element, attr)
|
|
64
|
+
if val is not None:
|
|
65
|
+
# KuzuDB objects if any
|
|
66
|
+
if isinstance(val, dict):
|
|
67
|
+
return f"{val.get('table', 0)}_{val.get('offset', 0)}"
|
|
68
|
+
return str(val)
|
|
69
|
+
return str(id(element))
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
nodes_dict = {}
|
|
73
|
+
edges = []
|
|
74
|
+
|
|
75
|
+
print(f"DEBUG: Starting get_graph with repo_path={repo_path}", flush=True)
|
|
76
|
+
|
|
77
|
+
with db_manager.get_driver().session() as session:
|
|
78
|
+
if cypher_query:
|
|
79
|
+
print(f"DEBUG: Executing custom query: {cypher_query}", flush=True)
|
|
80
|
+
result = session.run(cypher_query)
|
|
81
|
+
elif repo_path:
|
|
82
|
+
repo_path = str(Path(repo_path).resolve())
|
|
83
|
+
print(f"DEBUG: Fetching subgraph for: {repo_path}", flush=True)
|
|
84
|
+
query = """
|
|
85
|
+
MATCH (r:Repository {path: $repo_path})
|
|
86
|
+
OPTIONAL MATCH (r)-[:CONTAINS*0..]->(n)
|
|
87
|
+
WITH DISTINCT n
|
|
88
|
+
WHERE n IS NOT NULL
|
|
89
|
+
OPTIONAL MATCH (n)-[rel]->(m)
|
|
90
|
+
RETURN n, rel, m
|
|
91
|
+
"""
|
|
92
|
+
result = session.run(query, repo_path=repo_path)
|
|
93
|
+
else:
|
|
94
|
+
print("DEBUG: Fetching global graph", flush=True)
|
|
95
|
+
query = "MATCH (n) OPTIONAL MATCH (n)-[rel]->(m) RETURN n, rel, m LIMIT 50000"
|
|
96
|
+
result = session.run(query)
|
|
97
|
+
|
|
98
|
+
record_count = 0
|
|
99
|
+
for record in result:
|
|
100
|
+
record_count += 1
|
|
101
|
+
# Use .get() to avoid KeyError if the query doesn't return all fields (n, rel, m)
|
|
102
|
+
for key in ['n', 'm']:
|
|
103
|
+
try:
|
|
104
|
+
node = record.get(key)
|
|
105
|
+
if node:
|
|
106
|
+
eid = get_eid(node)
|
|
107
|
+
if eid and eid not in nodes_dict:
|
|
108
|
+
# Extract labels
|
|
109
|
+
labels = []
|
|
110
|
+
if isinstance(node, dict):
|
|
111
|
+
# KuzuDB node label is under '_label'
|
|
112
|
+
if '_label' in node:
|
|
113
|
+
labels = [node['_label']]
|
|
114
|
+
elif 'label' in node:
|
|
115
|
+
labels = [node['label']]
|
|
116
|
+
else:
|
|
117
|
+
for label_attr in ['_labels', 'labels']:
|
|
118
|
+
if hasattr(node, label_attr):
|
|
119
|
+
attr_val = getattr(node, label_attr)
|
|
120
|
+
if attr_val:
|
|
121
|
+
labels = list(attr_val)
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
# Extract properties
|
|
125
|
+
props = {}
|
|
126
|
+
if isinstance(node, dict):
|
|
127
|
+
props = {k: v for k, v in node.items() if not k.startswith('_')}
|
|
128
|
+
else:
|
|
129
|
+
for prop_attr in ['properties', '_properties']:
|
|
130
|
+
if hasattr(node, prop_attr):
|
|
131
|
+
attr_val = getattr(node, prop_attr)
|
|
132
|
+
if attr_val:
|
|
133
|
+
props = dict(attr_val)
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
# Fallback if props still empty but node acts like dict
|
|
137
|
+
if not props and hasattr(node, 'items'):
|
|
138
|
+
try:
|
|
139
|
+
props = dict(node.items())
|
|
140
|
+
except: pass
|
|
141
|
+
|
|
142
|
+
# Extract name/label for frontend
|
|
143
|
+
# Prefer 'name' property, fallback to 'label', then 'path' or 'Unknown'
|
|
144
|
+
display_name = str(props.get('name', props.get('label', props.get('path', 'Unknown'))))
|
|
145
|
+
|
|
146
|
+
nodes_dict[eid] = {
|
|
147
|
+
"id": eid,
|
|
148
|
+
"name": display_name,
|
|
149
|
+
"label": display_name,
|
|
150
|
+
"type": str(labels[0]).capitalize() if labels else "Other",
|
|
151
|
+
"file": str(props.get('path', props.get('file', ''))),
|
|
152
|
+
"val": 4 if (labels and labels[0] in ['Repository', 'Class', 'Interface', 'Trait']) else 2,
|
|
153
|
+
"properties": props
|
|
154
|
+
}
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"DEBUG: Error parsing node: {e}", file=sys.stderr, flush=True)
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
rel = record.get('rel')
|
|
161
|
+
if rel is not None:
|
|
162
|
+
# One-shot debug: print type and keys/attrs of first rel
|
|
163
|
+
if not edges:
|
|
164
|
+
if isinstance(rel, dict):
|
|
165
|
+
print(f"DEBUG rel (dict): keys={list(rel.keys())}", file=sys.stderr, flush=True)
|
|
166
|
+
else:
|
|
167
|
+
print(f"DEBUG rel (obj): type={type(rel).__name__}, attrs={[a for a in dir(rel) if not a.startswith('__')]}", file=sys.stderr, flush=True)
|
|
168
|
+
|
|
169
|
+
# FalkorDB / KuzuDB may return rels as dicts OR objects
|
|
170
|
+
if isinstance(rel, dict):
|
|
171
|
+
rid = get_eid(rel)
|
|
172
|
+
# KuzuDB uses _src and _dst, FalkorDB uses src_node/dest_node
|
|
173
|
+
src = rel.get('_src', rel.get('src_node'))
|
|
174
|
+
dst = rel.get('_dst', rel.get('dest_node'))
|
|
175
|
+
|
|
176
|
+
source = get_eid(src) if src is not None else None
|
|
177
|
+
target = get_eid(dst) if dst is not None else None
|
|
178
|
+
rel_type = str(rel.get('_label', rel.get('relation', rel.get('type', 'RELATED')))).upper()
|
|
179
|
+
else:
|
|
180
|
+
rid = get_eid(rel)
|
|
181
|
+
start_node = None
|
|
182
|
+
end_node = None
|
|
183
|
+
for src_attr in ['start_node', 'src_node', '_src_node']:
|
|
184
|
+
if hasattr(rel, src_attr):
|
|
185
|
+
start_node = getattr(rel, src_attr)
|
|
186
|
+
break
|
|
187
|
+
for dest_attr in ['end_node', 'dest_node', '_dest_node']:
|
|
188
|
+
if hasattr(rel, dest_attr):
|
|
189
|
+
end_node = getattr(rel, dest_attr)
|
|
190
|
+
break
|
|
191
|
+
source = get_eid(start_node) if start_node is not None else None
|
|
192
|
+
target = get_eid(end_node) if end_node is not None else None
|
|
193
|
+
rel_type = "RELATED"
|
|
194
|
+
for rel_attr in ['type', 'relation', '_relation']:
|
|
195
|
+
if hasattr(rel, rel_attr):
|
|
196
|
+
rel_type = str(getattr(rel, rel_attr)).upper()
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if source and target:
|
|
200
|
+
edges.append({
|
|
201
|
+
"id": rid,
|
|
202
|
+
"source": source,
|
|
203
|
+
"target": target,
|
|
204
|
+
"type": rel_type
|
|
205
|
+
})
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"DEBUG: Error parsing relationship: {e}", file=sys.stderr, flush=True)
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
print(f"DEBUG: Processed {record_count} records. extracted {len(nodes_dict)} nodes and {len(edges)} edges.", file=sys.stderr, flush=True)
|
|
211
|
+
|
|
212
|
+
# Build a list of unique file paths from File-type nodes for the tree
|
|
213
|
+
file_paths = []
|
|
214
|
+
for n in nodes_dict.values():
|
|
215
|
+
if n.get("file") and str(n.get("type", "")).lower() == "file":
|
|
216
|
+
file_paths.append(str(n["file"]))
|
|
217
|
+
file_paths = sorted(list(set(file_paths)))
|
|
218
|
+
|
|
219
|
+
# Read file contents so the frontend never needs a separate API call
|
|
220
|
+
file_contents: dict[str, str] = {}
|
|
221
|
+
for fp in file_paths:
|
|
222
|
+
try:
|
|
223
|
+
with open(fp, "r", encoding="utf-8", errors="replace") as f:
|
|
224
|
+
file_contents[fp] = f.read()
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
response_data = {
|
|
229
|
+
"nodes": list(nodes_dict.values()),
|
|
230
|
+
"links": edges,
|
|
231
|
+
"files": file_paths,
|
|
232
|
+
"fileContents": file_contents,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
print(f"API SUCCESS: Returning graph with {len(response_data['nodes'])} nodes and {len(response_data['links'])} links.", file=sys.stderr, flush=True)
|
|
236
|
+
return response_data
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
debug_log(f"Error fetching graph: {str(e)}")
|
|
240
|
+
import traceback
|
|
241
|
+
traceback.print_exc()
|
|
242
|
+
# Still return a valid structure so the frontend doesn't crash, but with 500 status if raised
|
|
243
|
+
# Actually, let's just return a 500 error but with JSON body if possible
|
|
244
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
245
|
+
|
|
246
|
+
@app.get("/api/file")
|
|
247
|
+
async def get_file(path: str):
|
|
248
|
+
file_path = Path(path)
|
|
249
|
+
if not file_path.exists():
|
|
250
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
254
|
+
return {"content": f.read()}
|
|
255
|
+
except Exception as e:
|
|
256
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
257
|
+
|
|
258
|
+
# SPA fallback handler
|
|
259
|
+
@app.get("/{full_path:path}")
|
|
260
|
+
async def spa_fallback(request: Request, full_path: str):
|
|
261
|
+
global _static_dir
|
|
262
|
+
if not _static_dir:
|
|
263
|
+
return HTMLResponse("Static directory not configured", status_code=500)
|
|
264
|
+
|
|
265
|
+
# Filesystem path
|
|
266
|
+
file_path = Path(_static_dir) / full_path
|
|
267
|
+
|
|
268
|
+
# If the file exists and is a file, serve it normally
|
|
269
|
+
if file_path.exists() and file_path.is_file():
|
|
270
|
+
return FileResponse(file_path)
|
|
271
|
+
|
|
272
|
+
# Otherwise serve index.html (Standard SPA routing)
|
|
273
|
+
index_path = Path(_static_dir) / "index.html"
|
|
274
|
+
if index_path.exists():
|
|
275
|
+
return FileResponse(index_path)
|
|
276
|
+
|
|
277
|
+
return HTMLResponse("Not Found (Built UI not found in viz/dist)", status_code=404)
|
|
278
|
+
|
|
279
|
+
def run_server(host: str = "127.0.0.1", port: int = 8000, static_dir: Optional[str] = None):
|
|
280
|
+
global _static_dir
|
|
281
|
+
_static_dir = static_dir
|
|
282
|
+
uvicorn.run(app, host=host, port=port)
|