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,719 @@
1
+ """SemanticChunkBuilder domain service - creates semantic code chunks."""
2
+ import re
3
+ from pathlib import Path
4
+
5
+ from src.domain.entities.ast_node import ASTNode
6
+ from src.domain.entities.code_chunk import CodeChunk
7
+ from src.domain.value_objects.chunk_type import ChunkType
8
+
9
+
10
+ class SemanticChunkBuilder:
11
+ """
12
+ Domain service for creating semantic code chunks from AST.
13
+
14
+ Creates method-based chunks with context enrichment (javadoc, imports, class signature).
15
+ Groups small methods (<20 lines) together (up to 3 methods).
16
+ Keeps large methods (>500 lines) as single chunks.
17
+
18
+ Extensions for frontend (Tasks 5.1-5.2):
19
+ - TypeScript/JavaScript functions with JSDoc
20
+ - React/Angular/Vue components
21
+ - Hooks detection (use* functions)
22
+ """
23
+
24
+ JAVADOC_PATTERN = re.compile(r'/\*\*.*?\*/', re.DOTALL)
25
+ JSDOC_PATTERN = re.compile(r'/\*\*.*?\*/', re.DOTALL) # Same as JavaDoc
26
+ SMALL_METHOD_THRESHOLD = 20 # lines
27
+ LARGE_METHOD_THRESHOLD = 500 # lines
28
+ MAX_METHODS_PER_CHUNK = 3
29
+
30
+ def build_chunks(
31
+ self,
32
+ ast_nodes: list[ASTNode],
33
+ source_code: dict[str, str]
34
+ ) -> list[CodeChunk]:
35
+ """
36
+ Build semantic chunks from AST nodes and source code.
37
+
38
+ Args:
39
+ ast_nodes: Parsed AST nodes
40
+ source_code: Source code by file path
41
+
42
+ Returns:
43
+ List of CodeChunk with metadata
44
+ """
45
+ chunks = []
46
+
47
+ # Separate nodes by type
48
+ method_nodes = [n for n in ast_nodes if n.node_type in ("METHOD", "CONSTRUCTOR")]
49
+ class_nodes = [n for n in ast_nodes if n.node_type == "CLASS"]
50
+ import_nodes = [n for n in ast_nodes if n.node_type == "IMPORT"]
51
+
52
+ # Build chunks from methods
53
+ chunks.extend(self._build_method_chunks(method_nodes, source_code))
54
+
55
+ # Build context chunks if no methods
56
+ if not method_nodes and (class_nodes or import_nodes):
57
+ chunks.extend(self._build_context_chunks(class_nodes, import_nodes, source_code))
58
+
59
+ return chunks
60
+
61
+ def _build_method_chunks(
62
+ self,
63
+ method_nodes: list[ASTNode],
64
+ source_code: dict[str, str]
65
+ ) -> list[CodeChunk]:
66
+ """Build chunks from method nodes."""
67
+ chunks = []
68
+
69
+ # Group small methods together
70
+ small_methods = []
71
+ large_methods = []
72
+
73
+ for node in method_nodes:
74
+ method_size = node.line_end - node.line_start + 1
75
+
76
+ if method_size < self.SMALL_METHOD_THRESHOLD:
77
+ small_methods.append(node)
78
+ else:
79
+ large_methods.append(node)
80
+
81
+ # Process small methods (group up to 3)
82
+ chunks.extend(self._group_small_methods(small_methods, source_code))
83
+
84
+ # Process large methods (one per chunk)
85
+ for node in large_methods:
86
+ chunk = self._create_chunk_from_method(node, source_code)
87
+ if chunk:
88
+ chunks.append(chunk)
89
+
90
+ return chunks
91
+
92
+ def _group_small_methods(
93
+ self,
94
+ small_methods: list[ASTNode],
95
+ source_code: dict[str, str]
96
+ ) -> list[CodeChunk]:
97
+ """Group small methods together (up to 3 per chunk)."""
98
+ chunks = []
99
+ i = 0
100
+
101
+ while i < len(small_methods):
102
+ # Take up to 3 methods
103
+ group = small_methods[i:i + self.MAX_METHODS_PER_CHUNK]
104
+
105
+ if len(group) == 1:
106
+ # Single method
107
+ chunk = self._create_chunk_from_method(group[0], source_code)
108
+ if chunk:
109
+ chunks.append(chunk)
110
+ else:
111
+ # Multiple methods grouped
112
+ chunk = self._create_grouped_chunk(group, source_code)
113
+ if chunk:
114
+ chunks.append(chunk)
115
+
116
+ i += self.MAX_METHODS_PER_CHUNK
117
+
118
+ return chunks
119
+
120
+ def _create_chunk_from_method(
121
+ self,
122
+ node: ASTNode,
123
+ source_code: dict[str, str]
124
+ ) -> CodeChunk | None:
125
+ """Create a chunk from a single method."""
126
+ # Find source file
127
+ source_file = self._find_source_file(node, source_code)
128
+ if not source_file:
129
+ return None
130
+
131
+ content = source_code[source_file]
132
+ lines = content.split('\n')
133
+
134
+ # Extract method content (with bounds checking)
135
+ # Line numbers in AST are 1-indexed, but list is 0-indexed
136
+ start_idx = max(0, node.line_start - 1)
137
+ end_idx = min(len(lines), node.line_end)
138
+
139
+ method_content = '\n'.join(lines[start_idx:end_idx])
140
+
141
+ # If method content doesn't contain method name, try expanding range
142
+ if node.name not in method_content and start_idx > 0:
143
+ start_idx = max(0, start_idx - 5)
144
+ method_content = '\n'.join(lines[start_idx:end_idx])
145
+
146
+ # Try to include javadoc (look backwards from method start)
147
+ javadoc_start = max(0, start_idx - 10)
148
+ preceding_lines = '\n'.join(lines[javadoc_start:start_idx])
149
+ javadoc_match = self.JAVADOC_PATTERN.search(preceding_lines)
150
+ if javadoc_match:
151
+ method_content = javadoc_match.group() + '\n' + method_content
152
+
153
+ # Determine chunk type
154
+ chunk_type = ChunkType.CONSTRUCTOR.value if node.node_type == "CONSTRUCTOR" else ChunkType.METHOD.value
155
+
156
+ # Build metadata
157
+ metadata = {
158
+ "name": node.name,
159
+ "qualified_name": node.qualified_name,
160
+ "parameters": node.metadata.get("parameters", []),
161
+ "return_type": node.metadata.get("return_type"),
162
+ "parent_class": node.metadata.get("parent_class"),
163
+ "line_start": node.line_start,
164
+ "line_end": node.line_end
165
+ }
166
+
167
+ return CodeChunk(
168
+ content=method_content,
169
+ chunk_type=chunk_type,
170
+ metadata=metadata
171
+ )
172
+
173
+ def _create_grouped_chunk(
174
+ self,
175
+ nodes: list[ASTNode],
176
+ source_code: dict[str, str]
177
+ ) -> CodeChunk | None:
178
+ """Create a chunk from multiple grouped methods."""
179
+ if not nodes:
180
+ return None
181
+
182
+ # Find source file from first node
183
+ source_file = self._find_source_file(nodes[0], source_code)
184
+ if not source_file:
185
+ return None
186
+
187
+ content = source_code[source_file]
188
+ lines = content.split('\n')
189
+
190
+ # Extract all methods
191
+ all_content = []
192
+ for node in nodes:
193
+ start_idx = max(0, node.line_start - 1)
194
+ end_idx = min(len(lines), node.line_end)
195
+ method_content = '\n'.join(lines[start_idx:end_idx])
196
+ all_content.append(method_content)
197
+
198
+ grouped_content = '\n\n'.join(all_content)
199
+
200
+ # Use first node's metadata as base
201
+ first_node = nodes[0]
202
+ metadata = {
203
+ "name": f"grouped_{len(nodes)}_methods",
204
+ "qualified_name": first_node.metadata.get("parent_class", "unknown"),
205
+ "parent_class": first_node.metadata.get("parent_class"),
206
+ "line_start": nodes[0].line_start,
207
+ "line_end": nodes[-1].line_end,
208
+ "method_count": len(nodes)
209
+ }
210
+
211
+ return CodeChunk(
212
+ content=grouped_content,
213
+ chunk_type=ChunkType.METHOD.value,
214
+ metadata=metadata
215
+ )
216
+
217
+ def _build_context_chunks(
218
+ self,
219
+ class_nodes: list[ASTNode],
220
+ import_nodes: list[ASTNode],
221
+ source_code: dict[str, str]
222
+ ) -> list[CodeChunk]:
223
+ """Build context chunks for files with only imports and class declaration."""
224
+ chunks = []
225
+
226
+ # If we have class nodes but no methods, create CLASS chunk
227
+ for class_node in class_nodes:
228
+ source_file = self._find_source_file(class_node, source_code)
229
+ if not source_file:
230
+ continue
231
+
232
+ content = source_code[source_file]
233
+ lines = content.split('\n')
234
+
235
+ # Extract class declaration and imports
236
+ start_idx = max(0, class_node.line_start - 1)
237
+ end_idx = min(len(lines), class_node.line_end)
238
+
239
+ class_content = '\n'.join(lines[start_idx:end_idx])
240
+
241
+ # Include imports
242
+ import_content = ""
243
+ for imp_node in import_nodes:
244
+ imp_start = max(0, imp_node.line_start - 1)
245
+ imp_end = min(len(lines), imp_node.line_end)
246
+ import_content += '\n'.join(lines[imp_start:imp_end]) + '\n'
247
+
248
+ full_content = import_content + '\n' + class_content
249
+
250
+ metadata = {
251
+ "name": class_node.name,
252
+ "qualified_name": class_node.qualified_name,
253
+ "line_start": class_node.line_start,
254
+ "line_end": class_node.line_end
255
+ }
256
+
257
+ chunk = CodeChunk(
258
+ content=full_content,
259
+ chunk_type=ChunkType.CLASS.value,
260
+ metadata=metadata
261
+ )
262
+ chunks.append(chunk)
263
+
264
+ # If only imports, create IMPORT_BLOCK chunk
265
+ if not class_nodes and import_nodes:
266
+ source_file = self._find_source_file(import_nodes[0], source_code)
267
+ if source_file:
268
+ content = source_code[source_file]
269
+ lines = content.split('\n')
270
+
271
+ import_lines = []
272
+ for imp_node in import_nodes:
273
+ start_idx = max(0, imp_node.line_start - 1)
274
+ end_idx = min(len(lines), imp_node.line_end)
275
+ import_lines.extend(lines[start_idx:end_idx])
276
+
277
+ chunk = CodeChunk(
278
+ content='\n'.join(import_lines),
279
+ chunk_type=ChunkType.IMPORT_BLOCK.value,
280
+ metadata={"import_count": len(import_nodes)}
281
+ )
282
+ chunks.append(chunk)
283
+
284
+ return chunks
285
+
286
+ def _find_source_file(self, node: ASTNode, source_code: dict[str, str]) -> str | None:
287
+ """Find the source file for a given node."""
288
+ # Try to match by qualified name
289
+ qualified_parts = node.qualified_name.split('.')
290
+
291
+ for filename in source_code.keys():
292
+ # Simple heuristic: match class name in filename
293
+ if any(part in filename for part in qualified_parts):
294
+ return filename
295
+
296
+ # Fallback: return first available file
297
+ return next(iter(source_code.keys()), None)
298
+
299
+
300
+ def build_typescript_chunks(
301
+ self,
302
+ parse_results: list,
303
+ source_code: dict[str, str]
304
+ ) -> list[CodeChunk]:
305
+ """
306
+ Build semantic chunks from TypeScript/JavaScript parse results (Tasks 5.1-5.2).
307
+
308
+ Uses Tree-sitter parsed data to extract REAL code content.
309
+
310
+ Args:
311
+ parse_results: List of ParseResult from TreeSitterTypeScriptParser
312
+ source_code: Source code by file path {file_path: code_string}
313
+
314
+ Returns:
315
+ List of CodeChunk with actual TypeScript/JavaScript code
316
+ """
317
+ chunks = []
318
+
319
+ for parse_result in parse_results:
320
+ file_path = str(parse_result.file_path)
321
+ code = source_code.get(file_path, "")
322
+
323
+ if not code:
324
+ # Try reading from file if not in dict
325
+ try:
326
+ from pathlib import Path
327
+ code = Path(file_path).read_text(encoding='utf-8')
328
+ except Exception:
329
+ continue
330
+
331
+ code_lines = code.split('\n')
332
+
333
+ # Build chunks from methods/components (extracted by tree-sitter)
334
+ for method in parse_result.methods:
335
+ chunk = self._create_typescript_chunk(method, code_lines, file_path)
336
+ if chunk:
337
+ chunks.append(chunk)
338
+
339
+ return chunks
340
+
341
+ def _create_typescript_chunk(
342
+ self,
343
+ method: dict,
344
+ code_lines: list[str],
345
+ file_path: str
346
+ ) -> CodeChunk | None:
347
+ """
348
+ Create a single chunk from a TypeScript method/component.
349
+
350
+ Args:
351
+ method: Method metadata from tree-sitter (name, line, type, framework, etc)
352
+ code_lines: Source code split by lines
353
+ file_path: Path to source file
354
+
355
+ Returns:
356
+ CodeChunk with real code content
357
+ """
358
+ name = method.get('name', 'anonymous')
359
+ start_line = method.get('line', 1) - 1 # Convert to 0-indexed
360
+
361
+ # Estimate end line (in production, tree-sitter should provide this)
362
+ # For now, extract ~30 lines or until next function/class
363
+ end_line = self._find_function_end(code_lines, start_line)
364
+
365
+ # Extract JSDoc comment if present (look backwards up to 10 lines)
366
+ comment_start = max(0, start_line - 10)
367
+ has_jsdoc = False
368
+ jsdoc_lines = []
369
+
370
+ for i in range(start_line - 1, comment_start - 1, -1):
371
+ if i < 0 or i >= len(code_lines):
372
+ break
373
+ line = code_lines[i].strip()
374
+ if '*/' in line:
375
+ has_jsdoc = True
376
+ jsdoc_lines.insert(0, code_lines[i])
377
+ elif has_jsdoc and ('/**' in line or '*' in line):
378
+ jsdoc_lines.insert(0, code_lines[i])
379
+ if '/**' in line:
380
+ break
381
+ elif has_jsdoc:
382
+ break
383
+
384
+ # Extract actual code content using tree-sitter line info
385
+ if has_jsdoc:
386
+ content_lines = jsdoc_lines + code_lines[start_line:end_line]
387
+ else:
388
+ content_lines = code_lines[start_line:end_line]
389
+
390
+ content = '\n'.join(content_lines)
391
+
392
+ if not content.strip():
393
+ return None
394
+
395
+ # Detect chunk type based on tree-sitter metadata
396
+ chunk_type = self._detect_typescript_chunk_type(method)
397
+
398
+ # Build rich metadata
399
+ metadata = {
400
+ 'language': 'typescript',
401
+ 'file_path': file_path,
402
+ 'function_name': name,
403
+ 'start_line': start_line + 1,
404
+ 'end_line': end_line,
405
+ 'has_jsdoc': has_jsdoc
406
+ }
407
+
408
+ # Add framework-specific metadata
409
+ if method.get('framework'):
410
+ metadata['framework'] = method['framework']
411
+
412
+ if method.get('type'):
413
+ metadata['component_type'] = method['type']
414
+
415
+ # React hooks detection (Task 5.1)
416
+ if name.startswith('use') and len(name) > 3 and name[3].isupper():
417
+ metadata['is_hook'] = True
418
+
419
+ # JSX children for components (Task 5.2)
420
+ if 'jsx_children' in method:
421
+ metadata['jsx_children'] = method['jsx_children']
422
+
423
+ chunk = CodeChunk(
424
+ content=content,
425
+ chunk_type=chunk_type,
426
+ metadata=metadata
427
+ )
428
+
429
+ return chunk
430
+
431
+ def _find_function_end(self, code_lines: list[str], start_line: int) -> int:
432
+ """
433
+ Find the end line of a function (heuristic-based).
434
+
435
+ Args:
436
+ code_lines: Source code lines
437
+ start_line: Starting line (0-indexed)
438
+
439
+ Returns:
440
+ End line number (0-indexed)
441
+ """
442
+ brace_count = 0
443
+ in_function = False
444
+
445
+ for i in range(start_line, min(len(code_lines), start_line + 100)):
446
+ line = code_lines[i]
447
+
448
+ # Count braces to find function end
449
+ if '{' in line:
450
+ brace_count += line.count('{')
451
+ in_function = True
452
+ if '}' in line:
453
+ brace_count -= line.count('}')
454
+
455
+ # Arrow functions: const X = () => ...
456
+ if '=>' in line and not in_function:
457
+ in_function = True
458
+ if '{' not in line:
459
+ # Single-line arrow function
460
+ return i + 1
461
+
462
+ # Function ended when braces balanced
463
+ if in_function and brace_count == 0:
464
+ return i + 1
465
+
466
+ # Stop at next function/class declaration
467
+ if i > start_line and any(keyword in line for keyword in ['function ', 'class ', 'const ', 'export ']):
468
+ stripped = line.strip()
469
+ if stripped.startswith(('function', 'class', 'export function', 'export class', 'const')):
470
+ return i
471
+
472
+ # Default: take 30 lines
473
+ return min(len(code_lines), start_line + 30)
474
+
475
+ def _detect_typescript_chunk_type(self, method: dict) -> str:
476
+ """
477
+ Detect chunk type from TypeScript method metadata (Tasks 5.1-5.2).
478
+
479
+ Args:
480
+ method: Method dictionary from TreeSitterTypeScriptParser
481
+
482
+ Returns:
483
+ ChunkType enum value
484
+ """
485
+ method_type = method.get('type', '')
486
+ name = method.get('name', '')
487
+
488
+ # React/Angular/Vue components (Task 5.2)
489
+ if method_type in ('function_component', 'class_component', 'angular_component'):
490
+ return ChunkType.CLASS.value # Use CLASS for components
491
+
492
+ # React hooks (Task 5.1)
493
+ if name.startswith('use') and len(name) > 3 and name[3].isupper():
494
+ return ChunkType.METHOD.value # Use METHOD for hooks
495
+
496
+ # Regular functions
497
+ return ChunkType.METHOD.value
498
+
499
+ def _build_function_chunks(
500
+ self,
501
+ function_nodes: list[ASTNode],
502
+ source_code: dict[str, str]
503
+ ) -> list[CodeChunk]:
504
+ """
505
+ Build chunks from TypeScript/JavaScript functions (Task 15.1).
506
+
507
+ Applies same grouping strategy as Java methods:
508
+ - Small functions (<20 lines): group up to 3 per chunk
509
+ - Large functions (≥20 lines): one per chunk
510
+ - React hooks (name starts with 'use'): tagged as HOOK
511
+
512
+ Args:
513
+ function_nodes: List of FUNCTION nodes from AST
514
+ source_code: Source code by file path
515
+
516
+ Returns:
517
+ List of CodeChunk instances
518
+ """
519
+ chunks = []
520
+
521
+ # Group by size
522
+ small_functions = []
523
+ large_functions = []
524
+
525
+ for node in function_nodes:
526
+ function_size = node.line_end - node.line_start + 1
527
+
528
+ if function_size < self.SMALL_METHOD_THRESHOLD:
529
+ small_functions.append(node)
530
+ else:
531
+ large_functions.append(node)
532
+
533
+ # Process small functions (group up to 3)
534
+ i = 0
535
+ while i < len(small_functions):
536
+ group = small_functions[i:i + self.MAX_METHODS_PER_CHUNK]
537
+
538
+ if len(group) == 1:
539
+ chunk = self._create_function_chunk(group[0], source_code)
540
+ if chunk:
541
+ chunks.append(chunk)
542
+ else:
543
+ chunk = self._create_grouped_function_chunk(group, source_code)
544
+ if chunk:
545
+ chunks.append(chunk)
546
+
547
+ i += self.MAX_METHODS_PER_CHUNK
548
+
549
+ # Process large functions (one per chunk)
550
+ for node in large_functions:
551
+ chunk = self._create_function_chunk(node, source_code)
552
+ if chunk:
553
+ chunks.append(chunk)
554
+
555
+ return chunks
556
+
557
+ def _create_function_chunk(
558
+ self,
559
+ node: ASTNode,
560
+ source_code: dict[str, str]
561
+ ) -> CodeChunk | None:
562
+ """Create chunk from a single TypeScript/JavaScript function."""
563
+ file_path = node.metadata.get("file_path")
564
+ if not file_path or file_path not in source_code:
565
+ return None
566
+
567
+ content = source_code[file_path]
568
+ lines = content.split('\n')
569
+
570
+ # Extract function content
571
+ start_idx = max(0, node.line_start - 1)
572
+ end_idx = min(len(lines), node.line_end)
573
+ function_content = '\n'.join(lines[start_idx:end_idx])
574
+
575
+ # Try to include JSDoc
576
+ jsdoc_start = max(0, start_idx - 10)
577
+ preceding_lines = '\n'.join(lines[jsdoc_start:start_idx])
578
+ jsdoc_match = self.JSDOC_PATTERN.search(preceding_lines)
579
+ if jsdoc_match:
580
+ function_content = jsdoc_match.group() + '\n' + function_content
581
+
582
+ # Detect React hooks
583
+ is_hook = node.name.startswith('use') and len(node.name) > 3 and node.name[3].isupper()
584
+ chunk_type = ChunkType.HOOK.value if is_hook else ChunkType.FUNCTION.value
585
+
586
+ metadata = {
587
+ "name": node.name,
588
+ "qualified_name": node.qualified_name,
589
+ "line_start": node.line_start,
590
+ "line_end": node.line_end,
591
+ "language": node.metadata.get("language", "typescript"),
592
+ "is_hook": is_hook
593
+ }
594
+
595
+ return CodeChunk(
596
+ content=function_content,
597
+ chunk_type=chunk_type,
598
+ metadata=metadata
599
+ )
600
+
601
+ def _create_grouped_function_chunk(
602
+ self,
603
+ nodes: list[ASTNode],
604
+ source_code: dict[str, str]
605
+ ) -> CodeChunk | None:
606
+ """Create chunk from multiple small functions grouped together."""
607
+ if not nodes:
608
+ return None
609
+
610
+ file_path = nodes[0].metadata.get("file_path")
611
+ if not file_path or file_path not in source_code:
612
+ return None
613
+
614
+ content = source_code[file_path]
615
+ lines = content.split('\n')
616
+
617
+ # Extract all functions
618
+ grouped_content = []
619
+ for node in nodes:
620
+ start_idx = max(0, node.line_start - 1)
621
+ end_idx = min(len(lines), node.line_end)
622
+ function_content = '\n'.join(lines[start_idx:end_idx])
623
+ grouped_content.append(function_content)
624
+
625
+ metadata = {
626
+ "name": f"grouped_{len(nodes)}_functions",
627
+ "qualified_name": nodes[0].qualified_name.rsplit('.', 1)[0],
628
+ "line_start": nodes[0].line_start,
629
+ "line_end": nodes[-1].line_end,
630
+ "function_count": len(nodes),
631
+ "language": nodes[0].metadata.get("language", "typescript")
632
+ }
633
+
634
+ return CodeChunk(
635
+ content='\n\n'.join(grouped_content),
636
+ chunk_type=ChunkType.FUNCTION.value,
637
+ metadata=metadata
638
+ )
639
+
640
+ def _build_component_chunks(
641
+ self,
642
+ component_nodes: list[ASTNode],
643
+ source_code: dict[str, str]
644
+ ) -> list[CodeChunk]:
645
+ """
646
+ Build chunks from React/Angular/Vue components (Task 15.2).
647
+
648
+ Includes:
649
+ - Props interface for React
650
+ - @Component decorator for Angular
651
+ - Script + template for Vue
652
+ - JSDoc comments
653
+
654
+ Args:
655
+ component_nodes: List of COMPONENT nodes from AST
656
+ source_code: Source code by file path
657
+
658
+ Returns:
659
+ List of CodeChunk instances
660
+ """
661
+ chunks = []
662
+
663
+ for node in component_nodes:
664
+ chunk = self._create_component_chunk(node, source_code)
665
+ if chunk:
666
+ chunks.append(chunk)
667
+
668
+ return chunks
669
+
670
+ def _create_component_chunk(
671
+ self,
672
+ node: ASTNode,
673
+ source_code: dict[str, str]
674
+ ) -> CodeChunk | None:
675
+ """Create chunk from a single component."""
676
+ file_path = node.metadata.get("file_path")
677
+ if not file_path or file_path not in source_code:
678
+ return None
679
+
680
+ content = source_code[file_path]
681
+ lines = content.split('\n')
682
+
683
+ # Extract component content
684
+ start_idx = max(0, node.line_start - 1)
685
+ end_idx = min(len(lines), node.line_end)
686
+ component_content = '\n'.join(lines[start_idx:end_idx])
687
+
688
+ # Try to include JSDoc
689
+ jsdoc_start = max(0, start_idx - 10)
690
+ preceding_lines = '\n'.join(lines[jsdoc_start:start_idx])
691
+ jsdoc_match = self.JSDOC_PATTERN.search(preceding_lines)
692
+ if jsdoc_match:
693
+ component_content = jsdoc_match.group() + '\n' + component_content
694
+
695
+ framework = node.metadata.get("framework", "unknown")
696
+
697
+ metadata = {
698
+ "name": node.name,
699
+ "qualified_name": node.qualified_name,
700
+ "line_start": node.line_start,
701
+ "line_end": node.line_end,
702
+ "framework": framework,
703
+ "language": node.metadata.get("language", "typescript")
704
+ }
705
+
706
+ # Add framework-specific metadata
707
+ if "props_interface" in node.metadata:
708
+ metadata["props_interface"] = node.metadata["props_interface"]
709
+ if "selector" in node.metadata:
710
+ metadata["selector"] = node.metadata["selector"]
711
+ if "has_script_setup" in node.metadata:
712
+ metadata["has_script_setup"] = node.metadata["has_script_setup"]
713
+
714
+ return CodeChunk(
715
+ content=component_content,
716
+ chunk_type=ChunkType.COMPONENT.value,
717
+ metadata=metadata
718
+ )
719
+