graph-dependency-analyzer 0.2.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.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IndexRepositoryUseCase Implementation (Task 8.1)
|
|
3
|
+
|
|
4
|
+
Orchestrates the complete indexing pipeline for a single repository.
|
|
5
|
+
Executes 8 sequential stages from validation to Neo4j storage.
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from src.application.dtos.index_result import IndexResult
|
|
12
|
+
from src.application.services.file_scanner_service import FileScannerService
|
|
13
|
+
from src.application.services.progress_tracker import ProgressTracker, ProgressUpdate
|
|
14
|
+
from src.infrastructure.parsing.tree_sitter_java_parser import TreeSitterJavaParser, ParseError
|
|
15
|
+
from src.infrastructure.parsing.tree_sitter_typescript_parser import TreeSitterTypeScriptParser # NEW: Task 7.1
|
|
16
|
+
from src.domain.services.dependency_extractor import DependencyExtractor
|
|
17
|
+
from src.domain.services.semantic_chunk_builder import SemanticChunkBuilder
|
|
18
|
+
from src.infrastructure.external_apis.openai_embedding_provider import OpenAIEmbeddingProvider
|
|
19
|
+
from src.infrastructure.persistence.qdrant_vector_repository import QdrantVectorRepository
|
|
20
|
+
from src.infrastructure.persistence.neo4j_dependency_repository import Neo4jDependencyGraphRepository
|
|
21
|
+
|
|
22
|
+
logger = structlog.get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IndexRepositoryUseCase:
|
|
26
|
+
"""
|
|
27
|
+
Use case for indexing a single repository through complete pipeline.
|
|
28
|
+
|
|
29
|
+
Executes 8 sequential stages:
|
|
30
|
+
1. Validation (0%)
|
|
31
|
+
2. File scanning (5%)
|
|
32
|
+
3. AST parsing (25%)
|
|
33
|
+
4. Dependency extraction (50%)
|
|
34
|
+
5. Chunking (65%)
|
|
35
|
+
6. Embedding generation (77%)
|
|
36
|
+
7. Vector storage in Qdrant (90%)
|
|
37
|
+
8. Graph storage in Neo4j (97%)
|
|
38
|
+
|
|
39
|
+
Requirements: 3.1, 3.2, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
file_scanner: FileScannerService,
|
|
45
|
+
java_parser: TreeSitterJavaParser,
|
|
46
|
+
typescript_parser=None,
|
|
47
|
+
dependency_extractor=None,
|
|
48
|
+
chunk_builder=None,
|
|
49
|
+
embedding_provider=None,
|
|
50
|
+
qdrant_repository=None,
|
|
51
|
+
neo4j_repository=None,
|
|
52
|
+
sql_parser=None,
|
|
53
|
+
python_parser=None,
|
|
54
|
+
groovy_parser=None,
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Initialize use case with all dependencies.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_scanner: Service for scanning source files
|
|
61
|
+
java_parser: Parser for Java AST
|
|
62
|
+
typescript_parser: Optional parser for TypeScript/JavaScript AST
|
|
63
|
+
dependency_extractor: Service for extracting dependencies
|
|
64
|
+
chunk_builder: Service for creating semantic chunks
|
|
65
|
+
embedding_provider: Provider for generating embeddings
|
|
66
|
+
qdrant_repository: Repository for vector storage
|
|
67
|
+
neo4j_repository: Repository for graph storage
|
|
68
|
+
sql_parser: Optional parser for SQL/DB schema files
|
|
69
|
+
"""
|
|
70
|
+
self.file_scanner = file_scanner
|
|
71
|
+
self.java_parser = java_parser
|
|
72
|
+
self.typescript_parser = typescript_parser
|
|
73
|
+
self.sql_parser = sql_parser
|
|
74
|
+
self.python_parser = python_parser
|
|
75
|
+
self.groovy_parser = groovy_parser
|
|
76
|
+
self.dependency_extractor = dependency_extractor
|
|
77
|
+
self.chunk_builder = chunk_builder
|
|
78
|
+
self.embedding_provider = embedding_provider
|
|
79
|
+
self.qdrant_repository = qdrant_repository
|
|
80
|
+
self.neo4j_repository = neo4j_repository
|
|
81
|
+
|
|
82
|
+
async def execute(
|
|
83
|
+
self,
|
|
84
|
+
repository_path: Path,
|
|
85
|
+
progress_callback: Callable[[ProgressUpdate], None] | None = None
|
|
86
|
+
) -> IndexResult:
|
|
87
|
+
"""
|
|
88
|
+
Execute complete indexing pipeline for one repository.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
repository_path: Absolute path to repository root
|
|
92
|
+
progress_callback: Optional callback for progress updates
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
IndexResult with status and counts
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If repository_path does not exist
|
|
99
|
+
"""
|
|
100
|
+
# Convert to Path if string
|
|
101
|
+
if isinstance(repository_path, str):
|
|
102
|
+
repository_path = Path(repository_path)
|
|
103
|
+
|
|
104
|
+
# Get repository name
|
|
105
|
+
repository_name = repository_path.name
|
|
106
|
+
|
|
107
|
+
# Create progress tracker
|
|
108
|
+
tracker = ProgressTracker(repository_name, progress_callback)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# STAGE 1: Validation (0%)
|
|
112
|
+
tracker.update("validation", "Validating repository path")
|
|
113
|
+
if not repository_path.exists():
|
|
114
|
+
raise ValueError(f"Repository path does not exist: {repository_path}")
|
|
115
|
+
|
|
116
|
+
logger.info(
|
|
117
|
+
"indexing_started",
|
|
118
|
+
repository=repository_name,
|
|
119
|
+
path=str(repository_path)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# STAGE 2: File Scanning (5%) - Task 6.1 extension
|
|
123
|
+
tracker.update("scan", f"Scanning files in {repository_name}")
|
|
124
|
+
scanned_files = self.file_scanner.scan(repository_path)
|
|
125
|
+
|
|
126
|
+
# Extract separate counts for progress tracking
|
|
127
|
+
java_files = scanned_files.get("java", [])
|
|
128
|
+
typescript_files = scanned_files.get("typescript", [])
|
|
129
|
+
javascript_files = scanned_files.get("javascript", [])
|
|
130
|
+
sql_files = scanned_files.get("sql", [])
|
|
131
|
+
python_files = scanned_files.get("python", [])
|
|
132
|
+
groovy_files = scanned_files.get("groovy", [])
|
|
133
|
+
|
|
134
|
+
total_files = len(java_files) + len(typescript_files) + len(javascript_files) + len(sql_files) + len(python_files) + len(groovy_files)
|
|
135
|
+
|
|
136
|
+
logger.info(
|
|
137
|
+
"files_scanned",
|
|
138
|
+
repository=repository_name,
|
|
139
|
+
java_files_count=len(java_files),
|
|
140
|
+
typescript_files_count=len(typescript_files),
|
|
141
|
+
javascript_files_count=len(javascript_files),
|
|
142
|
+
sql_files_count=len(sql_files),
|
|
143
|
+
total_files=total_files
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# STAGE 3: AST Parsing (25%)
|
|
147
|
+
tracker.update("ast", f"Parsing {len(java_files)} Java, {len(typescript_files)} TypeScript, {len(javascript_files)} JS, {len(sql_files)} SQL files")
|
|
148
|
+
|
|
149
|
+
parsed_results = []
|
|
150
|
+
parse_errors = {"java": 0, "typescript": 0}
|
|
151
|
+
|
|
152
|
+
# Task 7.2: Parallel parsing using asyncio.gather()
|
|
153
|
+
import asyncio
|
|
154
|
+
|
|
155
|
+
async def parse_java_files():
|
|
156
|
+
"""Parse all Java files"""
|
|
157
|
+
java_results = []
|
|
158
|
+
java_errors = 0
|
|
159
|
+
for java_file in java_files:
|
|
160
|
+
try:
|
|
161
|
+
parsed = self.java_parser.parse_file(java_file) # FIXED: Renamed
|
|
162
|
+
java_results.append(parsed)
|
|
163
|
+
except ParseError as e:
|
|
164
|
+
java_errors += 1
|
|
165
|
+
logger.warning(
|
|
166
|
+
"parse_error_skipped",
|
|
167
|
+
language="java",
|
|
168
|
+
file=str(java_file),
|
|
169
|
+
error=str(e)
|
|
170
|
+
)
|
|
171
|
+
return java_results, java_errors
|
|
172
|
+
|
|
173
|
+
async def parse_typescript_files():
|
|
174
|
+
"""Parse all TypeScript/JavaScript files"""
|
|
175
|
+
ts_results = []
|
|
176
|
+
ts_errors = 0
|
|
177
|
+
if self.typescript_parser:
|
|
178
|
+
for ts_file in (typescript_files + javascript_files):
|
|
179
|
+
try:
|
|
180
|
+
parsed = self.typescript_parser.parse_file(ts_file)
|
|
181
|
+
ts_results.append(parsed)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
ts_errors += 1
|
|
184
|
+
logger.warning(
|
|
185
|
+
"parse_error_skipped",
|
|
186
|
+
language="typescript",
|
|
187
|
+
file=str(ts_file),
|
|
188
|
+
error=str(e)
|
|
189
|
+
)
|
|
190
|
+
return ts_results, ts_errors
|
|
191
|
+
|
|
192
|
+
async def parse_sql_files_fn():
|
|
193
|
+
"""Parse all SQL/DB schema files"""
|
|
194
|
+
sql_results = []
|
|
195
|
+
sql_errors = 0
|
|
196
|
+
if self.sql_parser and sql_files:
|
|
197
|
+
for sql_file in sql_files:
|
|
198
|
+
try:
|
|
199
|
+
parsed = self.sql_parser.parse_file(sql_file)
|
|
200
|
+
sql_results.append(parsed)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
sql_errors += 1
|
|
203
|
+
logger.warning(
|
|
204
|
+
"parse_error_skipped",
|
|
205
|
+
language="sql",
|
|
206
|
+
file=str(sql_file),
|
|
207
|
+
error=str(e)
|
|
208
|
+
)
|
|
209
|
+
return sql_results, sql_errors
|
|
210
|
+
|
|
211
|
+
async def parse_python_files_fn():
|
|
212
|
+
"""Parse all Python source files"""
|
|
213
|
+
py_results = []
|
|
214
|
+
py_errors = 0
|
|
215
|
+
if self.python_parser and python_files:
|
|
216
|
+
for py_file in python_files:
|
|
217
|
+
try:
|
|
218
|
+
parsed = self.python_parser.parse_file(py_file)
|
|
219
|
+
py_results.append(parsed)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
py_errors += 1
|
|
222
|
+
logger.warning(
|
|
223
|
+
"parse_error_skipped",
|
|
224
|
+
language="python",
|
|
225
|
+
file=str(py_file),
|
|
226
|
+
error=str(e)
|
|
227
|
+
)
|
|
228
|
+
return py_results, py_errors
|
|
229
|
+
|
|
230
|
+
async def parse_groovy_files_fn():
|
|
231
|
+
"""Parse all Groovy/Jenkinsfile source files"""
|
|
232
|
+
groovy_results = []
|
|
233
|
+
groovy_errors = 0
|
|
234
|
+
if self.groovy_parser and groovy_files:
|
|
235
|
+
for groovy_file in groovy_files:
|
|
236
|
+
try:
|
|
237
|
+
parsed = self.groovy_parser.parse_file(groovy_file)
|
|
238
|
+
groovy_results.append(parsed)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
groovy_errors += 1
|
|
241
|
+
logger.warning(
|
|
242
|
+
"parse_error_skipped",
|
|
243
|
+
language="groovy",
|
|
244
|
+
file=str(groovy_file),
|
|
245
|
+
error=str(e)
|
|
246
|
+
)
|
|
247
|
+
return groovy_results, groovy_errors
|
|
248
|
+
|
|
249
|
+
# Execute parsing in parallel
|
|
250
|
+
(java_parsed, java_errors), (ts_parsed, ts_errors), \
|
|
251
|
+
(sql_parsed, sql_errors), (py_parsed, py_errors), \
|
|
252
|
+
(groovy_parsed, groovy_errors) = \
|
|
253
|
+
await asyncio.gather(
|
|
254
|
+
parse_java_files(),
|
|
255
|
+
parse_typescript_files(),
|
|
256
|
+
parse_sql_files_fn(),
|
|
257
|
+
parse_python_files_fn(),
|
|
258
|
+
parse_groovy_files_fn(),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Combine results
|
|
262
|
+
parsed_results = java_parsed + ts_parsed + sql_parsed + py_parsed + groovy_parsed
|
|
263
|
+
parse_errors["java"] = java_errors
|
|
264
|
+
parse_errors["typescript"] = ts_errors
|
|
265
|
+
parse_errors["sql"] = sql_errors
|
|
266
|
+
|
|
267
|
+
logger.info(
|
|
268
|
+
"ast_parsing_complete",
|
|
269
|
+
repository=repository_name,
|
|
270
|
+
files_parsed=len(parsed_results),
|
|
271
|
+
java_parse_errors=parse_errors["java"],
|
|
272
|
+
typescript_parse_errors=parse_errors["typescript"]
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# STAGE 4: Dependency Extraction (50%)
|
|
276
|
+
tracker.update("deps", "Extracting dependencies from AST")
|
|
277
|
+
|
|
278
|
+
# Convert parsed results to AST nodes format expected by DependencyExtractor
|
|
279
|
+
from src.domain.entities.ast_node import ASTNode
|
|
280
|
+
|
|
281
|
+
ast_nodes = []
|
|
282
|
+
for parse_result in parsed_results:
|
|
283
|
+
# Convert classes to ASTNode objects
|
|
284
|
+
for cls in parse_result.classes:
|
|
285
|
+
if isinstance(cls, dict):
|
|
286
|
+
# Add repository metadata
|
|
287
|
+
cls_metadata = cls.copy()
|
|
288
|
+
cls_metadata["repository"] = repository_name
|
|
289
|
+
cls_metadata["file_path"] = str(parse_result.file_path)
|
|
290
|
+
|
|
291
|
+
# Ensure qualified_name exists (use name as fallback)
|
|
292
|
+
qualified_name = cls.get("qualified_name", cls.get("name", "Unknown"))
|
|
293
|
+
if not qualified_name or qualified_name == "Unknown":
|
|
294
|
+
file_path_str = str(parse_result.file_path)
|
|
295
|
+
component_name = cls.get("name", Path(file_path_str).stem)
|
|
296
|
+
qualified_name = f"{repository_name}.{component_name}"
|
|
297
|
+
|
|
298
|
+
# Preserve DB object types from SqlSchemaParser; default to CLASS
|
|
299
|
+
_db_types = {"DB_TABLE","DB_VIEW","DB_PROCEDURE","DB_PACKAGE",
|
|
300
|
+
"DB_TRIGGER","DB_SEQUENCE","DB_FUNCTION","DB_INDEX",
|
|
301
|
+
"DB_TYPE","DB_COLLECTION"}
|
|
302
|
+
raw_node_type = cls.get("node_type", "CLASS")
|
|
303
|
+
node_type = raw_node_type if raw_node_type in _db_types else "CLASS"
|
|
304
|
+
|
|
305
|
+
ast_node = ASTNode(
|
|
306
|
+
node_type=node_type,
|
|
307
|
+
name=cls.get("name", "Unknown"),
|
|
308
|
+
qualified_name=qualified_name,
|
|
309
|
+
line_start=cls.get("line_start", cls.get("line", 0)),
|
|
310
|
+
line_end=cls.get("line_end", cls.get("line", 0)),
|
|
311
|
+
metadata=cls_metadata
|
|
312
|
+
)
|
|
313
|
+
ast_nodes.append(ast_node)
|
|
314
|
+
else:
|
|
315
|
+
ast_nodes.append(cls) # Already an ASTNode
|
|
316
|
+
|
|
317
|
+
# Convert methods to ASTNode objects
|
|
318
|
+
for method in parse_result.methods:
|
|
319
|
+
if isinstance(method, dict):
|
|
320
|
+
# Add repository metadata
|
|
321
|
+
method_metadata = method.copy()
|
|
322
|
+
method_metadata["repository"] = repository_name
|
|
323
|
+
method_metadata["file_path"] = str(parse_result.file_path)
|
|
324
|
+
|
|
325
|
+
# Add string_literals from ParseResult for API detection (Task 4.2)
|
|
326
|
+
# TypeScript parser extracts all string literals from the file
|
|
327
|
+
if hasattr(parse_result, 'string_literals') and parse_result.string_literals:
|
|
328
|
+
method_metadata["string_literals"] = parse_result.string_literals
|
|
329
|
+
|
|
330
|
+
# Generate qualified_name if not present (TypeScript components/functions)
|
|
331
|
+
method_name = method.get("name", "Unknown")
|
|
332
|
+
qualified_name = method.get("qualified_name", "")
|
|
333
|
+
if not qualified_name:
|
|
334
|
+
# Generate from repository + file path + method name
|
|
335
|
+
file_path_str = str(parse_result.file_path)
|
|
336
|
+
file_name = Path(file_path_str).stem
|
|
337
|
+
qualified_name = f"{repository_name}.{file_name}.{method_name}"
|
|
338
|
+
|
|
339
|
+
ast_node = ASTNode(
|
|
340
|
+
node_type="METHOD",
|
|
341
|
+
name=method_name,
|
|
342
|
+
qualified_name=qualified_name,
|
|
343
|
+
line_start=method.get("line_start", method.get("line", 0)),
|
|
344
|
+
line_end=method.get("line_end", method.get("line", 0)),
|
|
345
|
+
metadata=method_metadata
|
|
346
|
+
)
|
|
347
|
+
ast_nodes.append(ast_node)
|
|
348
|
+
else:
|
|
349
|
+
ast_nodes.append(method)
|
|
350
|
+
|
|
351
|
+
# Convert Spring endpoints and HTTP wrapper calls to ASTNode objects
|
|
352
|
+
for call in (parse_result.method_calls if hasattr(parse_result, 'method_calls') else []):
|
|
353
|
+
if not isinstance(call, dict):
|
|
354
|
+
continue
|
|
355
|
+
file_stem = Path(str(parse_result.file_path)).stem
|
|
356
|
+
|
|
357
|
+
# Spring REST endpoint extracted by Java parser
|
|
358
|
+
if call.get("_is_endpoint"):
|
|
359
|
+
ep_metadata = call.copy()
|
|
360
|
+
ep_metadata["repository"] = repository_name
|
|
361
|
+
ep_metadata["file_path"] = str(parse_result.file_path)
|
|
362
|
+
qualified_name = f"{repository_name}.{file_stem}"
|
|
363
|
+
ast_node = ASTNode(
|
|
364
|
+
node_type="BACKEND_ENDPOINT",
|
|
365
|
+
name=call.get("path", ""),
|
|
366
|
+
qualified_name=qualified_name,
|
|
367
|
+
line_start=call.get("line", 0),
|
|
368
|
+
line_end=call.get("line", 0),
|
|
369
|
+
metadata=ep_metadata
|
|
370
|
+
)
|
|
371
|
+
ast_nodes.append(ast_node)
|
|
372
|
+
|
|
373
|
+
# Frontend HTTP wrapper call
|
|
374
|
+
elif call.get("is_api_call") and call.get("api_path"):
|
|
375
|
+
call_metadata = call.copy()
|
|
376
|
+
call_metadata["repository"] = repository_name
|
|
377
|
+
call_metadata["file_path"] = str(parse_result.file_path)
|
|
378
|
+
qualified_name = f"{repository_name}.{file_stem}"
|
|
379
|
+
ast_node = ASTNode(
|
|
380
|
+
node_type="API_CALL",
|
|
381
|
+
name=call.get("callee", ""),
|
|
382
|
+
qualified_name=qualified_name,
|
|
383
|
+
line_start=call.get("line", 0),
|
|
384
|
+
line_end=call.get("line", 0),
|
|
385
|
+
metadata=call_metadata
|
|
386
|
+
)
|
|
387
|
+
ast_nodes.append(ast_node)
|
|
388
|
+
|
|
389
|
+
# Determine the source class for imports in this file.
|
|
390
|
+
# Use the first class defined in the file as the owning class.
|
|
391
|
+
source_class_qname = None
|
|
392
|
+
for cls in parse_result.classes:
|
|
393
|
+
if isinstance(cls, dict):
|
|
394
|
+
qn = cls.get("qualified_name") or cls.get("name")
|
|
395
|
+
if qn and qn not in ("Unknown", ""):
|
|
396
|
+
source_class_qname = qn
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
# Convert imports to ASTNode objects
|
|
400
|
+
for imp in parse_result.imports:
|
|
401
|
+
if not isinstance(imp, dict):
|
|
402
|
+
ast_nodes.append(imp)
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# Skip SQL cross-refs (handled separately below)
|
|
406
|
+
if imp.get("type") in {"READS_TABLE", "WRITES_TABLE", "CALLS_PROCEDURE"}:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# Resolve import path — Java parser uses "import_path",
|
|
410
|
+
# TypeScript parser uses "module_path".
|
|
411
|
+
module_path = (
|
|
412
|
+
imp.get("import_path") # Java: com.medline.wms.Foo
|
|
413
|
+
or imp.get("module_path") # TypeScript: ../services/Foo
|
|
414
|
+
or imp.get("qualified_name")
|
|
415
|
+
or ""
|
|
416
|
+
)
|
|
417
|
+
if not module_path:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
imp_metadata = imp.copy()
|
|
421
|
+
imp_metadata["repository"] = repository_name
|
|
422
|
+
imp_metadata["file_path"] = str(parse_result.file_path)
|
|
423
|
+
imp_metadata["import_path"] = module_path
|
|
424
|
+
|
|
425
|
+
# Use the owning class as source (for CLASS→CLASS IMPORTS edge).
|
|
426
|
+
# Fall back to file-based package only if no class found.
|
|
427
|
+
if source_class_qname:
|
|
428
|
+
imp_metadata["current_package"] = source_class_qname
|
|
429
|
+
else:
|
|
430
|
+
file_path_str = str(parse_result.file_path)
|
|
431
|
+
if "src" in file_path_str:
|
|
432
|
+
parts = file_path_str.split("src")[-1].strip("/\\").replace("\\", "/")
|
|
433
|
+
package_parts = parts.split("/")[:-1]
|
|
434
|
+
imp_metadata["current_package"] = ".".join(package_parts) if package_parts else "src"
|
|
435
|
+
else:
|
|
436
|
+
imp_metadata["current_package"] = repository_name
|
|
437
|
+
|
|
438
|
+
ast_nodes.append(ASTNode(
|
|
439
|
+
node_type="IMPORT",
|
|
440
|
+
name=imp.get("name", module_path),
|
|
441
|
+
qualified_name=module_path,
|
|
442
|
+
line_start=imp.get("line", imp.get("line_start", 0)),
|
|
443
|
+
line_end=imp.get("line", imp.get("line_end", 0)),
|
|
444
|
+
metadata=imp_metadata,
|
|
445
|
+
))
|
|
446
|
+
|
|
447
|
+
# Convert SQL cross-references (reads/writes/calls between DB objects)
|
|
448
|
+
# These come from SqlSchemaParser as parse_result.imports = list[dict]
|
|
449
|
+
# with keys: type (READS_TABLE/WRITES_TABLE/CALLS_PROCEDURE), target, line
|
|
450
|
+
for ref in (parse_result.imports if hasattr(parse_result, 'imports') else []):
|
|
451
|
+
if not isinstance(ref, dict):
|
|
452
|
+
continue
|
|
453
|
+
ref_type = ref.get("type", "")
|
|
454
|
+
if ref_type not in {"READS_TABLE", "WRITES_TABLE", "CALLS_PROCEDURE"}:
|
|
455
|
+
continue
|
|
456
|
+
target = ref.get("target", "")
|
|
457
|
+
if not target:
|
|
458
|
+
continue
|
|
459
|
+
ref_metadata = {
|
|
460
|
+
"repository": repository_name,
|
|
461
|
+
"file_path": str(parse_result.file_path),
|
|
462
|
+
"ref_type": ref_type,
|
|
463
|
+
"target": target,
|
|
464
|
+
}
|
|
465
|
+
ast_nodes.append(ASTNode(
|
|
466
|
+
node_type="DB_REF",
|
|
467
|
+
name=target,
|
|
468
|
+
qualified_name=target,
|
|
469
|
+
line_start=ref.get("line", 0),
|
|
470
|
+
line_end=ref.get("line", 0),
|
|
471
|
+
metadata=ref_metadata,
|
|
472
|
+
))
|
|
473
|
+
|
|
474
|
+
# Scan build files (pom.xml, build.gradle) and config files
|
|
475
|
+
build_files: dict[str, str] = {}
|
|
476
|
+
config_files: dict[str, str] = {}
|
|
477
|
+
_BUILD_NAMES = {"pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"}
|
|
478
|
+
_SKIP_DIRS = {"node_modules", ".git", "target", "dist", ".next", "__pycache__"}
|
|
479
|
+
for f in repository_path.rglob("*"):
|
|
480
|
+
if not f.is_file():
|
|
481
|
+
continue
|
|
482
|
+
# skip unwanted directories
|
|
483
|
+
if _SKIP_DIRS & set(f.parts):
|
|
484
|
+
continue
|
|
485
|
+
fname = f.name
|
|
486
|
+
fname_lower = fname.lower()
|
|
487
|
+
try:
|
|
488
|
+
# Build files — exact name match
|
|
489
|
+
if fname in _BUILD_NAMES:
|
|
490
|
+
build_files[fname] = f.read_text(encoding="utf-8", errors="ignore")
|
|
491
|
+
# Config files — pattern match (all profiles and formats)
|
|
492
|
+
elif (
|
|
493
|
+
fname_lower.startswith("application") and fname_lower.endswith((".properties", ".yml", ".yaml"))
|
|
494
|
+
or fname_lower.startswith("bootstrap") and fname_lower.endswith((".properties", ".yml", ".yaml"))
|
|
495
|
+
or fname_lower in (".env", ".env.production", ".env.staging", ".env.development", ".env.local")
|
|
496
|
+
or fname_lower.startswith(".env.") and not fname_lower.endswith((".js", ".ts", ".py"))
|
|
497
|
+
# SQL query properties files (e.g. webServiceSql.properties, commonSql.properties)
|
|
498
|
+
or fname_lower.endswith("sql.properties")
|
|
499
|
+
or fname_lower.endswith("sql.props")
|
|
500
|
+
):
|
|
501
|
+
# Use relative path as key to avoid collisions between profiles
|
|
502
|
+
rel_key = str(f.relative_to(repository_path))
|
|
503
|
+
config_files[rel_key] = f.read_text(encoding="utf-8", errors="ignore")
|
|
504
|
+
except Exception:
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
dependency_graph = self.dependency_extractor.extract_dependencies(
|
|
508
|
+
ast_nodes=ast_nodes,
|
|
509
|
+
build_files=build_files,
|
|
510
|
+
config_files=config_files,
|
|
511
|
+
repository=repository_name,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Cross-language linking: match frontend API calls to backend endpoints
|
|
515
|
+
cross_links = self.dependency_extractor.link_cross_language(dependency_graph)
|
|
516
|
+
|
|
517
|
+
logger.info(
|
|
518
|
+
"dependencies_extracted",
|
|
519
|
+
repository=repository_name,
|
|
520
|
+
nodes_count=len(dependency_graph.nodes) if hasattr(dependency_graph, 'nodes') else 0,
|
|
521
|
+
cross_language_links=cross_links
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# STAGE 5: Chunking (65%)
|
|
525
|
+
tracker.update("chunking", "Creating semantic code chunks")
|
|
526
|
+
|
|
527
|
+
# Build source_code dictionary from all files (Java + TypeScript + JavaScript)
|
|
528
|
+
source_code = {}
|
|
529
|
+
|
|
530
|
+
# Add Java files
|
|
531
|
+
for java_file in java_files:
|
|
532
|
+
try:
|
|
533
|
+
source_code[str(java_file)] = java_file.read_text(encoding='utf-8')
|
|
534
|
+
except Exception as e:
|
|
535
|
+
logger.warning(
|
|
536
|
+
"source_code_read_failed",
|
|
537
|
+
file=str(java_file),
|
|
538
|
+
error=str(e)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Add TypeScript files
|
|
542
|
+
for ts_file in typescript_files:
|
|
543
|
+
try:
|
|
544
|
+
source_code[str(ts_file)] = ts_file.read_text(encoding='utf-8')
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.warning(
|
|
547
|
+
"source_code_read_failed",
|
|
548
|
+
file=str(ts_file),
|
|
549
|
+
error=str(e)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Add JavaScript files
|
|
553
|
+
for js_file in javascript_files:
|
|
554
|
+
try:
|
|
555
|
+
source_code[str(js_file)] = js_file.read_text(encoding='utf-8')
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.warning(
|
|
558
|
+
"source_code_read_failed",
|
|
559
|
+
file=str(js_file),
|
|
560
|
+
error=str(e)
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Build chunks from Java files (existing logic)
|
|
564
|
+
chunks = self.chunk_builder.build_chunks(
|
|
565
|
+
ast_nodes=ast_nodes,
|
|
566
|
+
source_code=source_code
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Build chunks from TypeScript/JavaScript files (Task 5.1-5.2)
|
|
570
|
+
if self.typescript_parser and (typescript_files or javascript_files):
|
|
571
|
+
ts_chunks = self.chunk_builder.build_typescript_chunks(
|
|
572
|
+
parse_results=ts_parsed,
|
|
573
|
+
source_code=source_code
|
|
574
|
+
)
|
|
575
|
+
chunks.extend(ts_chunks)
|
|
576
|
+
|
|
577
|
+
logger.info(
|
|
578
|
+
"chunks_created",
|
|
579
|
+
repository=repository_name,
|
|
580
|
+
chunks_count=len(chunks)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# STAGE 6: Embedding Generation (77%)
|
|
584
|
+
tracker.update("embeddings", f"Generating embeddings for {len(chunks)} chunks")
|
|
585
|
+
|
|
586
|
+
# Extract chunk contents for embedding
|
|
587
|
+
chunk_contents = [chunk.content for chunk in chunks]
|
|
588
|
+
|
|
589
|
+
# Generate embeddings with batch_size=10
|
|
590
|
+
embeddings = await self.embedding_provider.embed_batch(
|
|
591
|
+
chunk_contents,
|
|
592
|
+
batch_size=10
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
logger.info(
|
|
596
|
+
"embeddings_generated",
|
|
597
|
+
repository=repository_name,
|
|
598
|
+
embeddings_count=len(embeddings)
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# STAGE 7: Qdrant Storage (90%)
|
|
602
|
+
tracker.update("qdrant", "Storing vectors in Qdrant")
|
|
603
|
+
|
|
604
|
+
# Stamp repository name on every chunk before storing
|
|
605
|
+
for chunk in chunks:
|
|
606
|
+
if not chunk.metadata.get("repository"):
|
|
607
|
+
chunk.metadata["repository"] = repository_name
|
|
608
|
+
|
|
609
|
+
qdrant_result = await self.qdrant_repository.upsert_batch(
|
|
610
|
+
chunks,
|
|
611
|
+
embeddings,
|
|
612
|
+
batch_size=1000
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
logger.info(
|
|
616
|
+
"vectors_stored",
|
|
617
|
+
repository=repository_name,
|
|
618
|
+
points_stored=qdrant_result.points_count if hasattr(qdrant_result, 'points_count') else len(chunks)
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# STAGE 8: Neo4j Storage (97%)
|
|
622
|
+
tracker.update("neo4j", "Creating dependency graph in Neo4j")
|
|
623
|
+
|
|
624
|
+
neo4j_result = await self.neo4j_repository.store_dependency_graph(
|
|
625
|
+
dependency_graph,
|
|
626
|
+
repository_name
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
logger.info(
|
|
630
|
+
"graph_stored",
|
|
631
|
+
repository=repository_name,
|
|
632
|
+
nodes_created=neo4j_result.nodes_created,
|
|
633
|
+
relationships_created=neo4j_result.relationships_created
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# COMPLETE (100%)
|
|
637
|
+
tracker.complete(f"Repository {repository_name} indexed successfully")
|
|
638
|
+
|
|
639
|
+
logger.info(
|
|
640
|
+
"indexing_completed",
|
|
641
|
+
repository=repository_name,
|
|
642
|
+
status="success"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Return success result
|
|
646
|
+
return IndexResult(
|
|
647
|
+
status="success",
|
|
648
|
+
repository_name=repository_name,
|
|
649
|
+
files_processed=len(java_files),
|
|
650
|
+
chunks_created=len(chunks),
|
|
651
|
+
nodes_created=neo4j_result.nodes_created,
|
|
652
|
+
relationships_created=neo4j_result.relationships_created
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
except ValueError as e:
|
|
656
|
+
# Validation errors
|
|
657
|
+
logger.error(
|
|
658
|
+
"indexing_failed_validation",
|
|
659
|
+
repository=repository_name,
|
|
660
|
+
error=str(e)
|
|
661
|
+
)
|
|
662
|
+
tracker.fail(f"Validation error: {str(e)}")
|
|
663
|
+
raise
|
|
664
|
+
|
|
665
|
+
except Exception as e:
|
|
666
|
+
# Critical errors
|
|
667
|
+
logger.error(
|
|
668
|
+
"indexing_failed",
|
|
669
|
+
repository=repository_name,
|
|
670
|
+
error=str(e),
|
|
671
|
+
error_type=type(e).__name__
|
|
672
|
+
)
|
|
673
|
+
tracker.fail(f"Indexing failed: {str(e)}")
|
|
674
|
+
|
|
675
|
+
return IndexResult(
|
|
676
|
+
status="error",
|
|
677
|
+
repository_name=repository_name,
|
|
678
|
+
files_processed=0,
|
|
679
|
+
chunks_created=0,
|
|
680
|
+
nodes_created=0,
|
|
681
|
+
relationships_created=0,
|
|
682
|
+
error_message=str(e)
|
|
683
|
+
)
|