shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.dev1__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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,768 @@
1
+ """Worker module for parallel file parsing.
2
+
3
+ This module provides the ParserWorker class and process_batch function
4
+ for parallel execution of file parsing across multiple processes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import os
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from tree_sitter import Node, Parser, Query, QueryCursor
16
+
17
+ from shotgun.codebase.core.extractors import LanguageExtractor, get_extractor
18
+ from shotgun.codebase.core.metrics_types import (
19
+ FileParseMetrics,
20
+ FileParseResult,
21
+ FileParseTask,
22
+ InheritanceData,
23
+ NodeData,
24
+ NodeLabel,
25
+ RawCallData,
26
+ RelationshipData,
27
+ RelationshipType,
28
+ WorkBatch,
29
+ )
30
+ from shotgun.codebase.core.parser_loader import load_parsers
31
+ from shotgun.logging_config import get_logger
32
+
33
+ logger = get_logger(__name__)
34
+
35
+ # Module-level lazy parser initialization (once per worker process)
36
+ _parsers: dict[str, Parser] | None = None
37
+ _queries: dict[str, dict[str, Query]] | None = None
38
+
39
+
40
+ def _ensure_parsers() -> tuple[dict[str, Parser], dict[str, Any]]:
41
+ """Initialize parsers lazily in worker process."""
42
+ global _parsers, _queries
43
+ if _parsers is None:
44
+ logger.debug("Initializing tree-sitter parsers in worker process")
45
+ _parsers, _queries = load_parsers()
46
+ return _parsers, _queries or {}
47
+
48
+
49
+ class ParserWorker:
50
+ """Handles file parsing in a worker process.
51
+
52
+ Extracts definitions, relationships, and registry data from source files
53
+ without database access, allowing the main process to aggregate results.
54
+ """
55
+
56
+ def __init__(self, worker_id: int = 0) -> None:
57
+ """Initialize the worker."""
58
+ self.worker_id = worker_id
59
+ self.parsers, self.queries = _ensure_parsers()
60
+
61
+ def process_file(self, task: FileParseTask) -> FileParseResult:
62
+ """Parse a single file and extract all data."""
63
+ start_time = time.perf_counter()
64
+ relative_path_str = str(task.relative_path).replace(os.sep, "/")
65
+
66
+ nodes: list[NodeData] = []
67
+ relationships: list[RelationshipData] = []
68
+ function_registry: dict[str, str] = {}
69
+ simple_name_lookup: dict[str, list[str]] = {}
70
+ raw_calls: list[RawCallData] = []
71
+ inheritance_data: list[InheritanceData] = []
72
+ ast_nodes_count = 0
73
+
74
+ try:
75
+ content, file_hash, mtime, file_size = self._read_file(task.file_path)
76
+
77
+ if not content.strip():
78
+ return self._empty_result(
79
+ task,
80
+ nodes,
81
+ relationships,
82
+ function_registry,
83
+ simple_name_lookup,
84
+ raw_calls,
85
+ inheritance_data,
86
+ file_hash,
87
+ mtime,
88
+ file_size,
89
+ start_time,
90
+ relative_path_str,
91
+ )
92
+
93
+ if task.language not in self.parsers:
94
+ return self._error_result(
95
+ task,
96
+ f"No parser for language: {task.language}",
97
+ file_hash,
98
+ mtime,
99
+ file_size,
100
+ start_time,
101
+ relative_path_str,
102
+ )
103
+
104
+ tree = self.parsers[task.language].parse(content)
105
+ root_node = tree.root_node
106
+ extractor = get_extractor(task.language)
107
+ ast_nodes_count = extractor.count_ast_nodes(root_node)
108
+
109
+ self._create_file_node(task, relative_path_str, nodes, relationships)
110
+ self._create_module_node(task, relative_path_str, nodes, relationships)
111
+ self._create_file_metadata_node(relative_path_str, file_hash, mtime, nodes)
112
+
113
+ self._extract_definitions(
114
+ root_node,
115
+ task.module_qn,
116
+ task.language,
117
+ relative_path_str,
118
+ extractor,
119
+ nodes,
120
+ relationships,
121
+ function_registry,
122
+ simple_name_lookup,
123
+ inheritance_data,
124
+ )
125
+
126
+ self._extract_calls(
127
+ root_node,
128
+ task.module_qn,
129
+ task.language,
130
+ extractor,
131
+ function_registry,
132
+ raw_calls,
133
+ )
134
+
135
+ return self._success_result(
136
+ task,
137
+ nodes,
138
+ relationships,
139
+ function_registry,
140
+ simple_name_lookup,
141
+ raw_calls,
142
+ inheritance_data,
143
+ file_hash,
144
+ mtime,
145
+ file_size,
146
+ ast_nodes_count,
147
+ start_time,
148
+ relative_path_str,
149
+ )
150
+
151
+ except Exception as e:
152
+ logger.error(f"Failed to process {task.file_path}: {e}")
153
+ return self._error_result(
154
+ task, str(e), "", 0, 0, start_time, relative_path_str
155
+ )
156
+
157
+ def process_batch(self, batch: WorkBatch) -> list[FileParseResult]:
158
+ """Process all files in a batch."""
159
+ return [self.process_file(task) for task in batch.tasks]
160
+
161
+ def _read_file(self, file_path: Path) -> tuple[bytes, str, int, int]:
162
+ """Read file and compute metadata."""
163
+ with open(file_path, "rb") as f:
164
+ content = f.read()
165
+ return (
166
+ content,
167
+ hashlib.sha256(content).hexdigest(),
168
+ int(file_path.stat().st_mtime),
169
+ len(content),
170
+ )
171
+
172
+ def _create_file_node(
173
+ self,
174
+ task: FileParseTask,
175
+ relative_path_str: str,
176
+ nodes: list[NodeData],
177
+ relationships: list[RelationshipData],
178
+ ) -> None:
179
+ """Create File node and containment relationship."""
180
+ nodes.append(
181
+ NodeData(
182
+ label=NodeLabel.FILE,
183
+ properties={
184
+ "path": relative_path_str,
185
+ "name": task.file_path.name,
186
+ "extension": task.file_path.suffix,
187
+ },
188
+ )
189
+ )
190
+
191
+ parent_rel_path = task.relative_path.parent
192
+ if parent_rel_path != Path("."):
193
+ relationships.append(
194
+ RelationshipData(
195
+ from_label=NodeLabel.FOLDER,
196
+ from_key="path",
197
+ from_value=str(parent_rel_path).replace(os.sep, "/"),
198
+ rel_type=RelationshipType.CONTAINS_FILE,
199
+ to_label=NodeLabel.FILE,
200
+ to_key="path",
201
+ to_value=relative_path_str,
202
+ )
203
+ )
204
+
205
+ def _create_file_metadata_node(
206
+ self,
207
+ relative_path_str: str,
208
+ file_hash: str,
209
+ mtime: int,
210
+ nodes: list[NodeData],
211
+ ) -> None:
212
+ """Create FileMetadata node for tracking file state."""
213
+ current_time = int(time.time())
214
+ nodes.append(
215
+ NodeData(
216
+ label=NodeLabel.FILE_METADATA,
217
+ properties={
218
+ "filepath": relative_path_str,
219
+ "mtime": mtime,
220
+ "hash": file_hash,
221
+ "last_updated": current_time,
222
+ },
223
+ )
224
+ )
225
+
226
+ def _create_module_node(
227
+ self,
228
+ task: FileParseTask,
229
+ relative_path_str: str,
230
+ nodes: list[NodeData],
231
+ relationships: list[RelationshipData],
232
+ ) -> None:
233
+ """Create Module node and containment relationship."""
234
+ current_time = int(time.time())
235
+ nodes.append(
236
+ NodeData(
237
+ label=NodeLabel.MODULE,
238
+ properties={
239
+ "qualified_name": task.module_qn,
240
+ "name": task.file_path.stem,
241
+ "path": relative_path_str,
242
+ "created_at": current_time,
243
+ "updated_at": current_time,
244
+ },
245
+ )
246
+ )
247
+
248
+ if task.container_qn:
249
+ relationships.append(
250
+ RelationshipData(
251
+ from_label=NodeLabel.PACKAGE,
252
+ from_key="qualified_name",
253
+ from_value=task.container_qn,
254
+ rel_type=RelationshipType.CONTAINS_MODULE,
255
+ to_label=NodeLabel.MODULE,
256
+ to_key="qualified_name",
257
+ to_value=task.module_qn,
258
+ )
259
+ )
260
+
261
+ # Add TRACKS_Module relationship from FileMetadata to Module
262
+ relationships.append(
263
+ RelationshipData(
264
+ from_label=NodeLabel.FILE_METADATA,
265
+ from_key="filepath",
266
+ from_value=relative_path_str,
267
+ rel_type=RelationshipType.TRACKS_MODULE,
268
+ to_label=NodeLabel.MODULE,
269
+ to_key="qualified_name",
270
+ to_value=task.module_qn,
271
+ )
272
+ )
273
+
274
+ def _extract_definitions(
275
+ self,
276
+ root_node: Node,
277
+ module_qn: str,
278
+ language: str,
279
+ relative_path_str: str,
280
+ extractor: LanguageExtractor,
281
+ nodes: list[NodeData],
282
+ relationships: list[RelationshipData],
283
+ function_registry: dict[str, str],
284
+ simple_name_lookup: dict[str, list[str]],
285
+ inheritance_data: list[InheritanceData],
286
+ ) -> None:
287
+ """Extract class and function definitions from AST."""
288
+ lang_queries = self.queries.get(language, {})
289
+
290
+ if "class_query" in lang_queries:
291
+ self._extract_classes(
292
+ root_node,
293
+ module_qn,
294
+ relative_path_str,
295
+ extractor,
296
+ lang_queries["class_query"],
297
+ nodes,
298
+ relationships,
299
+ function_registry,
300
+ simple_name_lookup,
301
+ inheritance_data,
302
+ )
303
+
304
+ if "function_query" in lang_queries:
305
+ self._extract_functions(
306
+ root_node,
307
+ module_qn,
308
+ relative_path_str,
309
+ extractor,
310
+ lang_queries["function_query"],
311
+ nodes,
312
+ relationships,
313
+ function_registry,
314
+ simple_name_lookup,
315
+ )
316
+
317
+ def _extract_classes(
318
+ self,
319
+ root_node: Node,
320
+ module_qn: str,
321
+ relative_path_str: str,
322
+ extractor: LanguageExtractor,
323
+ class_query: Query,
324
+ nodes: list[NodeData],
325
+ relationships: list[RelationshipData],
326
+ function_registry: dict[str, str],
327
+ simple_name_lookup: dict[str, list[str]],
328
+ inheritance_data: list[InheritanceData],
329
+ ) -> None:
330
+ """Extract class definitions."""
331
+ cursor = QueryCursor(class_query)
332
+
333
+ for match in cursor.matches(root_node):
334
+ class_node, class_name = self._get_class_from_match(match)
335
+ if not class_node or not class_name:
336
+ continue
337
+
338
+ class_qn = f"{module_qn}.{class_name}"
339
+ current_time = int(time.time())
340
+
341
+ nodes.append(
342
+ NodeData(
343
+ label=NodeLabel.CLASS,
344
+ properties={
345
+ "qualified_name": class_qn,
346
+ "name": class_name,
347
+ "decorators": extractor.extract_decorators(class_node),
348
+ "line_start": class_node.start_point.row + 1,
349
+ "line_end": class_node.end_point.row + 1,
350
+ "created_at": current_time,
351
+ "updated_at": current_time,
352
+ "docstring": extractor.extract_docstring(class_node),
353
+ },
354
+ )
355
+ )
356
+
357
+ relationships.extend(
358
+ [
359
+ RelationshipData(
360
+ from_label=NodeLabel.MODULE,
361
+ from_key="qualified_name",
362
+ from_value=module_qn,
363
+ rel_type=RelationshipType.DEFINES,
364
+ to_label=NodeLabel.CLASS,
365
+ to_key="qualified_name",
366
+ to_value=class_qn,
367
+ ),
368
+ RelationshipData(
369
+ from_label=NodeLabel.FILE_METADATA,
370
+ from_key="filepath",
371
+ from_value=relative_path_str,
372
+ rel_type=RelationshipType.TRACKS_CLASS,
373
+ to_label=NodeLabel.CLASS,
374
+ to_key="qualified_name",
375
+ to_value=class_qn,
376
+ ),
377
+ ]
378
+ )
379
+
380
+ function_registry[class_qn] = NodeLabel.CLASS
381
+ simple_name_lookup.setdefault(class_name, []).append(class_qn)
382
+
383
+ parent_names = extractor.extract_inheritance(class_node)
384
+ if parent_names:
385
+ inheritance_data.append(
386
+ InheritanceData(
387
+ child_class_qn=class_qn,
388
+ parent_simple_names=parent_names,
389
+ )
390
+ )
391
+
392
+ def _extract_functions(
393
+ self,
394
+ root_node: Node,
395
+ module_qn: str,
396
+ relative_path_str: str,
397
+ extractor: LanguageExtractor,
398
+ function_query: Query,
399
+ nodes: list[NodeData],
400
+ relationships: list[RelationshipData],
401
+ function_registry: dict[str, str],
402
+ simple_name_lookup: dict[str, list[str]],
403
+ ) -> None:
404
+ """Extract function and method definitions."""
405
+ cursor = QueryCursor(function_query)
406
+
407
+ for match in cursor.matches(root_node):
408
+ func_node, func_name = self._get_function_from_match(match)
409
+ if not func_node or not func_name:
410
+ continue
411
+
412
+ parent_class = extractor.find_parent_class(func_node, module_qn)
413
+ current_time = int(time.time())
414
+
415
+ if parent_class:
416
+ self._add_method(
417
+ func_node,
418
+ func_name,
419
+ parent_class,
420
+ extractor,
421
+ relative_path_str,
422
+ current_time,
423
+ nodes,
424
+ relationships,
425
+ function_registry,
426
+ simple_name_lookup,
427
+ )
428
+ else:
429
+ self._add_function(
430
+ func_node,
431
+ func_name,
432
+ module_qn,
433
+ extractor,
434
+ relative_path_str,
435
+ current_time,
436
+ nodes,
437
+ relationships,
438
+ function_registry,
439
+ simple_name_lookup,
440
+ )
441
+
442
+ def _add_method(
443
+ self,
444
+ func_node: Node,
445
+ func_name: str,
446
+ parent_class: str,
447
+ extractor: LanguageExtractor,
448
+ relative_path_str: str,
449
+ current_time: int,
450
+ nodes: list[NodeData],
451
+ relationships: list[RelationshipData],
452
+ function_registry: dict[str, str],
453
+ simple_name_lookup: dict[str, list[str]],
454
+ ) -> None:
455
+ """Add a method node and relationships."""
456
+ method_qn = f"{parent_class}.{func_name}"
457
+
458
+ nodes.append(
459
+ NodeData(
460
+ label=NodeLabel.METHOD,
461
+ properties={
462
+ "qualified_name": method_qn,
463
+ "name": func_name,
464
+ "decorators": extractor.extract_decorators(func_node),
465
+ "line_start": func_node.start_point.row + 1,
466
+ "line_end": func_node.end_point.row + 1,
467
+ "created_at": current_time,
468
+ "updated_at": current_time,
469
+ "docstring": extractor.extract_docstring(func_node),
470
+ },
471
+ )
472
+ )
473
+
474
+ relationships.extend(
475
+ [
476
+ RelationshipData(
477
+ from_label=NodeLabel.CLASS,
478
+ from_key="qualified_name",
479
+ from_value=parent_class,
480
+ rel_type=RelationshipType.DEFINES_METHOD,
481
+ to_label=NodeLabel.METHOD,
482
+ to_key="qualified_name",
483
+ to_value=method_qn,
484
+ ),
485
+ RelationshipData(
486
+ from_label=NodeLabel.FILE_METADATA,
487
+ from_key="filepath",
488
+ from_value=relative_path_str,
489
+ rel_type=RelationshipType.TRACKS_METHOD,
490
+ to_label=NodeLabel.METHOD,
491
+ to_key="qualified_name",
492
+ to_value=method_qn,
493
+ ),
494
+ ]
495
+ )
496
+
497
+ function_registry[method_qn] = NodeLabel.METHOD
498
+ simple_name_lookup.setdefault(func_name, []).append(method_qn)
499
+
500
+ def _add_function(
501
+ self,
502
+ func_node: Node,
503
+ func_name: str,
504
+ module_qn: str,
505
+ extractor: LanguageExtractor,
506
+ relative_path_str: str,
507
+ current_time: int,
508
+ nodes: list[NodeData],
509
+ relationships: list[RelationshipData],
510
+ function_registry: dict[str, str],
511
+ simple_name_lookup: dict[str, list[str]],
512
+ ) -> None:
513
+ """Add a function node and relationships."""
514
+ func_qn = f"{module_qn}.{func_name}"
515
+
516
+ nodes.append(
517
+ NodeData(
518
+ label=NodeLabel.FUNCTION,
519
+ properties={
520
+ "qualified_name": func_qn,
521
+ "name": func_name,
522
+ "decorators": extractor.extract_decorators(func_node),
523
+ "line_start": func_node.start_point.row + 1,
524
+ "line_end": func_node.end_point.row + 1,
525
+ "created_at": current_time,
526
+ "updated_at": current_time,
527
+ "docstring": extractor.extract_docstring(func_node),
528
+ },
529
+ )
530
+ )
531
+
532
+ relationships.extend(
533
+ [
534
+ RelationshipData(
535
+ from_label=NodeLabel.MODULE,
536
+ from_key="qualified_name",
537
+ from_value=module_qn,
538
+ rel_type=RelationshipType.DEFINES_FUNC,
539
+ to_label=NodeLabel.FUNCTION,
540
+ to_key="qualified_name",
541
+ to_value=func_qn,
542
+ ),
543
+ RelationshipData(
544
+ from_label=NodeLabel.FILE_METADATA,
545
+ from_key="filepath",
546
+ from_value=relative_path_str,
547
+ rel_type=RelationshipType.TRACKS_FUNCTION,
548
+ to_label=NodeLabel.FUNCTION,
549
+ to_key="qualified_name",
550
+ to_value=func_qn,
551
+ ),
552
+ ]
553
+ )
554
+
555
+ function_registry[func_qn] = NodeLabel.FUNCTION
556
+ simple_name_lookup.setdefault(func_name, []).append(func_qn)
557
+
558
+ def _extract_calls(
559
+ self,
560
+ root_node: Node,
561
+ module_qn: str,
562
+ language: str,
563
+ extractor: LanguageExtractor,
564
+ function_registry: dict[str, str],
565
+ raw_calls: list[RawCallData],
566
+ ) -> None:
567
+ """Extract raw call data for later resolution."""
568
+ lang_queries = self.queries.get(language, {})
569
+ if "call_query" not in lang_queries:
570
+ return
571
+
572
+ cursor = QueryCursor(lang_queries["call_query"])
573
+
574
+ for match in cursor.matches(root_node):
575
+ call_node = self._get_call_from_match(match)
576
+ if call_node:
577
+ self._extract_single_call(
578
+ call_node, module_qn, extractor, function_registry, raw_calls
579
+ )
580
+
581
+ def _extract_single_call(
582
+ self,
583
+ call_node: Node,
584
+ module_qn: str,
585
+ extractor: LanguageExtractor,
586
+ function_registry: dict[str, str],
587
+ raw_calls: list[RawCallData],
588
+ ) -> None:
589
+ """Extract data from a single call expression."""
590
+ callee_name, object_name = extractor.parse_call_node(call_node)
591
+ if not callee_name:
592
+ return
593
+
594
+ caller_qn = extractor.find_containing_function(call_node, module_qn)
595
+ if not caller_qn or caller_qn not in function_registry:
596
+ return
597
+
598
+ raw_calls.append(
599
+ RawCallData(
600
+ caller_qn=caller_qn,
601
+ callee_name=callee_name,
602
+ object_name=object_name,
603
+ line_number=call_node.start_point.row + 1,
604
+ module_qn=module_qn,
605
+ )
606
+ )
607
+
608
+ def _get_class_from_match(
609
+ self, match: tuple[int, dict[str, list[Node]]]
610
+ ) -> tuple[Node | None, str | None]:
611
+ """Extract class node and name from query match."""
612
+ class_node = None
613
+ class_name = None
614
+
615
+ for capture_name, capture_nodes in match[1].items():
616
+ for node in capture_nodes:
617
+ if capture_name in ["class", "interface", "type_alias"]:
618
+ class_node = node
619
+ elif capture_name == "class_name" and node.text:
620
+ class_name = node.text.decode("utf-8")
621
+
622
+ return class_node, class_name
623
+
624
+ def _get_function_from_match(
625
+ self, match: tuple[int, dict[str, list[Node]]]
626
+ ) -> tuple[Node | None, str | None]:
627
+ """Extract function node and name from query match."""
628
+ func_node = None
629
+ func_name = None
630
+
631
+ for capture_name, capture_nodes in match[1].items():
632
+ for node in capture_nodes:
633
+ if capture_name == "function":
634
+ func_node = node
635
+ elif capture_name == "function_name" and node.text:
636
+ func_name = node.text.decode("utf-8")
637
+
638
+ return func_node, func_name
639
+
640
+ def _get_call_from_match(
641
+ self, match: tuple[int, dict[str, list[Node]]]
642
+ ) -> Node | None:
643
+ """Extract call node from query match."""
644
+ for capture_name, capture_nodes in match[1].items():
645
+ for node in capture_nodes:
646
+ if capture_name == "call":
647
+ return node
648
+ return None
649
+
650
+ def _empty_result(
651
+ self,
652
+ task: FileParseTask,
653
+ nodes: list[NodeData],
654
+ relationships: list[RelationshipData],
655
+ function_registry: dict[str, str],
656
+ simple_name_lookup: dict[str, list[str]],
657
+ raw_calls: list[RawCallData],
658
+ inheritance_data: list[InheritanceData],
659
+ file_hash: str,
660
+ mtime: int,
661
+ file_size: int,
662
+ start_time: float,
663
+ relative_path_str: str,
664
+ ) -> FileParseResult:
665
+ """Create result for empty file."""
666
+ return FileParseResult(
667
+ task=task,
668
+ success=True,
669
+ nodes=nodes,
670
+ relationships=relationships,
671
+ function_registry_entries=function_registry,
672
+ simple_name_entries=simple_name_lookup,
673
+ raw_calls=raw_calls,
674
+ inheritance_data=inheritance_data,
675
+ file_hash=file_hash,
676
+ mtime=mtime,
677
+ metrics=FileParseMetrics(
678
+ file_path=relative_path_str,
679
+ language=task.language,
680
+ file_size_bytes=file_size,
681
+ parse_time_ms=(time.perf_counter() - start_time) * 1000,
682
+ ast_nodes=0,
683
+ definitions_extracted=0,
684
+ relationships_found=0,
685
+ worker_id=self.worker_id,
686
+ ),
687
+ )
688
+
689
+ def _error_result(
690
+ self,
691
+ task: FileParseTask,
692
+ error: str,
693
+ file_hash: str,
694
+ mtime: int,
695
+ file_size: int,
696
+ start_time: float,
697
+ relative_path_str: str,
698
+ ) -> FileParseResult:
699
+ """Create result for error case."""
700
+ return FileParseResult(
701
+ task=task,
702
+ success=False,
703
+ error=error,
704
+ file_hash=file_hash,
705
+ mtime=mtime,
706
+ metrics=FileParseMetrics(
707
+ file_path=relative_path_str,
708
+ language=task.language,
709
+ file_size_bytes=file_size,
710
+ parse_time_ms=(time.perf_counter() - start_time) * 1000,
711
+ ast_nodes=0,
712
+ definitions_extracted=0,
713
+ relationships_found=0,
714
+ worker_id=self.worker_id,
715
+ ),
716
+ )
717
+
718
+ def _success_result(
719
+ self,
720
+ task: FileParseTask,
721
+ nodes: list[NodeData],
722
+ relationships: list[RelationshipData],
723
+ function_registry: dict[str, str],
724
+ simple_name_lookup: dict[str, list[str]],
725
+ raw_calls: list[RawCallData],
726
+ inheritance_data: list[InheritanceData],
727
+ file_hash: str,
728
+ mtime: int,
729
+ file_size: int,
730
+ ast_nodes_count: int,
731
+ start_time: float,
732
+ relative_path_str: str,
733
+ ) -> FileParseResult:
734
+ """Create successful result."""
735
+ definitions_count = sum(
736
+ 1
737
+ for n in nodes
738
+ if n.label in [NodeLabel.CLASS, NodeLabel.FUNCTION, NodeLabel.METHOD]
739
+ )
740
+
741
+ return FileParseResult(
742
+ task=task,
743
+ success=True,
744
+ nodes=nodes,
745
+ relationships=relationships,
746
+ function_registry_entries=function_registry,
747
+ simple_name_entries=simple_name_lookup,
748
+ raw_calls=raw_calls,
749
+ inheritance_data=inheritance_data,
750
+ file_hash=file_hash,
751
+ mtime=mtime,
752
+ metrics=FileParseMetrics(
753
+ file_path=relative_path_str,
754
+ language=task.language,
755
+ file_size_bytes=file_size,
756
+ parse_time_ms=(time.perf_counter() - start_time) * 1000,
757
+ ast_nodes=ast_nodes_count,
758
+ definitions_extracted=definitions_count,
759
+ relationships_found=len(relationships) + len(raw_calls),
760
+ worker_id=self.worker_id,
761
+ ),
762
+ )
763
+
764
+
765
+ def process_batch(batch: WorkBatch, worker_id: int = 0) -> list[FileParseResult]:
766
+ """Entry point for worker processes."""
767
+ worker = ParserWorker(worker_id=worker_id)
768
+ return worker.process_batch(batch)