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.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. 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
+ )