emdash-core 0.1.7__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 (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,646 @@
1
+ """Feature graph expansion using AST relationships."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+ from ..graph.connection import KuzuConnection, get_connection
7
+ from ..utils.logger import log
8
+
9
+
10
+ @dataclass
11
+ class FeatureGraph:
12
+ """Complete AST graph for a feature."""
13
+
14
+ root_node: dict = field(default_factory=dict)
15
+ functions: list[dict] = field(default_factory=list)
16
+ classes: list[dict] = field(default_factory=list)
17
+ files: list[dict] = field(default_factory=list)
18
+ call_graph: list[dict] = field(default_factory=list)
19
+ inheritance: list[dict] = field(default_factory=list)
20
+ imports: list[dict] = field(default_factory=list)
21
+
22
+ def to_dict(self) -> dict:
23
+ """Convert to dictionary."""
24
+ return {
25
+ "root_node": self.root_node,
26
+ "functions": self.functions,
27
+ "classes": self.classes,
28
+ "files": self.files,
29
+ "call_graph": self.call_graph,
30
+ "inheritance": self.inheritance,
31
+ "imports": self.imports,
32
+ }
33
+
34
+ def to_context_string(self) -> str:
35
+ """Convert to a readable string for LLM context."""
36
+ lines = []
37
+
38
+ # Root node
39
+ lines.append(f"## Root: {self.root_node.get('name', 'Unknown')}")
40
+ lines.append(f"Type: {self.root_node.get('type', 'Unknown')}")
41
+ if self.root_node.get('docstring'):
42
+ lines.append(f"Description: {self.root_node['docstring'][:200]}")
43
+ lines.append("")
44
+
45
+ # Call graph
46
+ if self.call_graph:
47
+ lines.append("## Call Graph")
48
+ for call in self.call_graph[:20]:
49
+ lines.append(f" {call['caller']} -> {call['callee']}")
50
+ lines.append("")
51
+
52
+ # Classes
53
+ if self.classes:
54
+ lines.append("## Classes")
55
+ for cls in self.classes[:10]:
56
+ lines.append(f" - {cls['name']}: {cls.get('docstring', 'No description')[:100]}")
57
+ lines.append("")
58
+
59
+ # Functions
60
+ if self.functions:
61
+ lines.append("## Functions")
62
+ for func in self.functions[:15]:
63
+ lines.append(f" - {func['name']}: {func.get('docstring', 'No description')[:100]}")
64
+ lines.append("")
65
+
66
+ # Inheritance
67
+ if self.inheritance:
68
+ lines.append("## Inheritance")
69
+ for inh in self.inheritance[:10]:
70
+ lines.append(f" {inh['child']} extends {inh['parent']}")
71
+ lines.append("")
72
+
73
+ # Files
74
+ if self.files:
75
+ lines.append("## Files")
76
+ for f in self.files[:10]:
77
+ lines.append(f" - {f.get('path', f.get('name', 'Unknown'))}")
78
+
79
+ return "\n".join(lines)
80
+
81
+
82
+ class FeatureExpander:
83
+ """Expands from a starting node to full feature graph."""
84
+
85
+ def __init__(self, connection: Optional[KuzuConnection] = None):
86
+ """Initialize feature expander.
87
+
88
+ Args:
89
+ connection: Neo4j connection. If None, uses global connection.
90
+ """
91
+ self.connection = connection or get_connection()
92
+
93
+ def expand_from_function(
94
+ self,
95
+ qualified_name: str,
96
+ max_hops: int = 2
97
+ ) -> FeatureGraph:
98
+ """Expand from a function node.
99
+
100
+ Traverses:
101
+ - Callers (who calls this function?)
102
+ - Callees (what does this function call?)
103
+ - Parent class (if method)
104
+ - Containing file
105
+ - Sibling functions in same file
106
+
107
+ Args:
108
+ qualified_name: Function's qualified name
109
+ max_hops: Maximum relationship depth to traverse
110
+
111
+ Returns:
112
+ FeatureGraph with expanded context
113
+ """
114
+ log.debug(f"Expanding from function: {qualified_name}")
115
+
116
+ with self.connection.session() as session:
117
+ # Get the root function and immediate relationships
118
+ result = session.run("""
119
+ MATCH (f:Function {qualified_name: $qualified_name})
120
+ OPTIONAL MATCH (f)<-[:CALLS]-(caller:Function)
121
+ OPTIONAL MATCH (f)-[:CALLS]->(callee:Function)
122
+ OPTIONAL MATCH (c:Class)-[:HAS_METHOD]->(f)
123
+ OPTIONAL MATCH (file:File)-[:CONTAINS_FUNCTION]->(f)
124
+ RETURN f as func,
125
+ collect(DISTINCT caller) as callers,
126
+ collect(DISTINCT callee) as callees,
127
+ c as parent_class,
128
+ file
129
+ """, qualified_name=qualified_name)
130
+
131
+ record = result.single()
132
+ if not record or not record["func"]:
133
+ return FeatureGraph()
134
+
135
+ func = dict(record["func"])
136
+ callers = [dict(c) for c in (record["callers"] or []) if c]
137
+ callees = [dict(c) for c in (record["callees"] or []) if c]
138
+ parent_class = dict(record["parent_class"]) if record["parent_class"] else None
139
+ file_node = dict(record["file"]) if record["file"] else None
140
+
141
+ # Build root node
142
+ root_node = {
143
+ "type": "Function",
144
+ "name": func.get("name"),
145
+ "qualified_name": func.get("qualified_name"),
146
+ "file_path": func.get("file_path"),
147
+ "docstring": func.get("docstring"),
148
+ "line_start": func.get("line_start"),
149
+ "line_end": func.get("line_end"),
150
+ }
151
+
152
+ # Build call graph
153
+ call_graph = []
154
+ for caller in callers:
155
+ call_graph.append({
156
+ "caller": caller.get("name"),
157
+ "caller_qualified": caller.get("qualified_name"),
158
+ "callee": func.get("name"),
159
+ "callee_qualified": func.get("qualified_name"),
160
+ })
161
+ for callee in callees:
162
+ call_graph.append({
163
+ "caller": func.get("name"),
164
+ "caller_qualified": func.get("qualified_name"),
165
+ "callee": callee.get("name"),
166
+ "callee_qualified": callee.get("qualified_name"),
167
+ })
168
+
169
+ # Collect functions (callers + callees + root)
170
+ functions = [root_node]
171
+ for c in callers + callees:
172
+ functions.append({
173
+ "name": c.get("name"),
174
+ "qualified_name": c.get("qualified_name"),
175
+ "file_path": c.get("file_path"),
176
+ "docstring": c.get("docstring"),
177
+ })
178
+
179
+ # If max_hops > 1, expand further
180
+ if max_hops > 1:
181
+ expanded = self._expand_call_graph(session, qualified_name, max_hops)
182
+ existing_qns = {f.get("qualified_name") for f in functions}
183
+ for func in expanded.get("functions", []):
184
+ if func.get("qualified_name") not in existing_qns:
185
+ functions.append(func)
186
+ existing_qns.add(func.get("qualified_name"))
187
+ call_graph.extend(expanded.get("calls", []))
188
+
189
+ # Collect classes
190
+ classes = []
191
+ if parent_class:
192
+ classes.append({
193
+ "name": parent_class.get("name"),
194
+ "qualified_name": parent_class.get("qualified_name"),
195
+ "file_path": parent_class.get("file_path"),
196
+ "docstring": parent_class.get("docstring"),
197
+ })
198
+
199
+ # Get class hierarchy
200
+ inheritance = self._get_class_hierarchy(session, parent_class.get("qualified_name"))
201
+ else:
202
+ inheritance = []
203
+
204
+ # Collect files
205
+ files = []
206
+ if file_node:
207
+ files.append({
208
+ "path": file_node.get("path"),
209
+ "name": file_node.get("name"),
210
+ })
211
+
212
+ # Get sibling functions from same file
213
+ if file_node:
214
+ siblings = self._get_file_functions(session, file_node.get("path"))
215
+ for sib in siblings:
216
+ if sib.get("qualified_name") != qualified_name:
217
+ if sib not in functions:
218
+ functions.append(sib)
219
+
220
+ return FeatureGraph(
221
+ root_node=root_node,
222
+ functions=functions,
223
+ classes=classes,
224
+ files=files,
225
+ call_graph=call_graph,
226
+ inheritance=inheritance,
227
+ imports=[],
228
+ )
229
+
230
+ def expand_from_class(
231
+ self,
232
+ qualified_name: str,
233
+ max_hops: int = 2
234
+ ) -> FeatureGraph:
235
+ """Expand from a class node.
236
+
237
+ Traverses:
238
+ - Methods (HAS_METHOD)
239
+ - Parent classes (INHERITS_FROM)
240
+ - Child classes (reverse INHERITS_FROM)
241
+ - Containing file
242
+ - Method call graphs
243
+
244
+ Args:
245
+ qualified_name: Class's qualified name
246
+ max_hops: Maximum relationship depth to traverse
247
+
248
+ Returns:
249
+ FeatureGraph with expanded context
250
+ """
251
+ log.debug(f"Expanding from class: {qualified_name}")
252
+
253
+ with self.connection.session() as session:
254
+ # Get the root class and relationships
255
+ result = session.run("""
256
+ MATCH (c:Class {qualified_name: $qualified_name})
257
+ OPTIONAL MATCH (c)-[:HAS_METHOD]->(m:Function)
258
+ OPTIONAL MATCH (c)-[:INHERITS_FROM]->(parent:Class)
259
+ OPTIONAL MATCH (child:Class)-[:INHERITS_FROM]->(c)
260
+ OPTIONAL MATCH (file:File)-[:CONTAINS_CLASS]->(c)
261
+ RETURN c as cls,
262
+ collect(DISTINCT m) as methods,
263
+ collect(DISTINCT parent) as parents,
264
+ collect(DISTINCT child) as children,
265
+ file
266
+ """, qualified_name=qualified_name)
267
+
268
+ record = result.single()
269
+ if not record or not record["cls"]:
270
+ return FeatureGraph()
271
+
272
+ cls = dict(record["cls"])
273
+ methods = [dict(m) for m in (record["methods"] or []) if m]
274
+ parents = [dict(p) for p in (record["parents"] or []) if p]
275
+ children = [dict(c) for c in (record["children"] or []) if c]
276
+ file_node = dict(record["file"]) if record["file"] else None
277
+
278
+ # Build root node
279
+ root_node = {
280
+ "type": "Class",
281
+ "name": cls.get("name"),
282
+ "qualified_name": cls.get("qualified_name"),
283
+ "file_path": cls.get("file_path"),
284
+ "docstring": cls.get("docstring"),
285
+ }
286
+
287
+ # Build classes list
288
+ classes = [root_node]
289
+ for p in parents:
290
+ classes.append({
291
+ "name": p.get("name"),
292
+ "qualified_name": p.get("qualified_name"),
293
+ "file_path": p.get("file_path"),
294
+ "docstring": p.get("docstring"),
295
+ })
296
+ for c in children:
297
+ classes.append({
298
+ "name": c.get("name"),
299
+ "qualified_name": c.get("qualified_name"),
300
+ "file_path": c.get("file_path"),
301
+ "docstring": c.get("docstring"),
302
+ })
303
+
304
+ # Build inheritance
305
+ inheritance = []
306
+ for p in parents:
307
+ inheritance.append({
308
+ "child": cls.get("name"),
309
+ "child_qualified": cls.get("qualified_name"),
310
+ "parent": p.get("name"),
311
+ "parent_qualified": p.get("qualified_name"),
312
+ })
313
+ for c in children:
314
+ inheritance.append({
315
+ "child": c.get("name"),
316
+ "child_qualified": c.get("qualified_name"),
317
+ "parent": cls.get("name"),
318
+ "parent_qualified": cls.get("qualified_name"),
319
+ })
320
+
321
+ # Build functions list from methods
322
+ functions = []
323
+ for m in methods:
324
+ functions.append({
325
+ "name": m.get("name"),
326
+ "qualified_name": m.get("qualified_name"),
327
+ "file_path": m.get("file_path"),
328
+ "docstring": m.get("docstring"),
329
+ })
330
+
331
+ # Get method call graphs
332
+ call_graph = []
333
+ for m in methods:
334
+ method_calls = self._get_function_calls(session, m.get("qualified_name"))
335
+ call_graph.extend(method_calls)
336
+
337
+ # Collect files
338
+ files = []
339
+ if file_node:
340
+ files.append({
341
+ "path": file_node.get("path"),
342
+ "name": file_node.get("name"),
343
+ })
344
+
345
+ return FeatureGraph(
346
+ root_node=root_node,
347
+ functions=functions,
348
+ classes=classes,
349
+ files=files,
350
+ call_graph=call_graph,
351
+ inheritance=inheritance,
352
+ imports=[],
353
+ )
354
+
355
+ def expand_from_file(
356
+ self,
357
+ file_path: str,
358
+ max_hops: int = 2
359
+ ) -> FeatureGraph:
360
+ """Expand from a file node.
361
+
362
+ Traverses:
363
+ - All classes and functions (CONTAINS)
364
+ - Imports (IMPORTS)
365
+ - Files that import this file
366
+
367
+ Args:
368
+ file_path: File path
369
+ max_hops: Maximum relationship depth to traverse
370
+
371
+ Returns:
372
+ FeatureGraph with expanded context
373
+ """
374
+ log.debug(f"Expanding from file: {file_path}")
375
+
376
+ with self.connection.session() as session:
377
+ # Get the file and its contents
378
+ result = session.run("""
379
+ MATCH (f:File)
380
+ WHERE f.path ENDS WITH $file_path OR f.path = $file_path
381
+ OPTIONAL MATCH (f)-[:CONTAINS_CLASS]->(cls:Class)
382
+ OPTIONAL MATCH (f)-[:CONTAINS_FUNCTION]->(func:Function)
383
+ OPTIONAL MATCH (f)-[:IMPORTS]->(m:Module)
384
+ RETURN f as file,
385
+ collect(DISTINCT cls) as classes,
386
+ collect(DISTINCT func) as functions,
387
+ collect(DISTINCT m) as imports
388
+ """, file_path=file_path)
389
+
390
+ record = result.single()
391
+ if not record or not record["file"]:
392
+ return FeatureGraph()
393
+
394
+ file_node = dict(record["file"])
395
+ file_classes = [dict(c) for c in (record["classes"] or []) if c]
396
+ file_functions = [dict(f) for f in (record["functions"] or []) if f]
397
+ file_imports = [dict(m) for m in (record["imports"] or []) if m]
398
+
399
+ # Build root node
400
+ root_node = {
401
+ "type": "File",
402
+ "name": file_node.get("name"),
403
+ "path": file_node.get("path"),
404
+ }
405
+
406
+ # Build classes list
407
+ classes = []
408
+ for c in file_classes:
409
+ classes.append({
410
+ "name": c.get("name"),
411
+ "qualified_name": c.get("qualified_name"),
412
+ "file_path": c.get("file_path"),
413
+ "docstring": c.get("docstring"),
414
+ })
415
+
416
+ # Build functions list
417
+ functions = []
418
+ for f in file_functions:
419
+ functions.append({
420
+ "name": f.get("name"),
421
+ "qualified_name": f.get("qualified_name"),
422
+ "file_path": f.get("file_path"),
423
+ "docstring": f.get("docstring"),
424
+ })
425
+
426
+ # Build imports list
427
+ imports = []
428
+ for m in file_imports:
429
+ imports.append({
430
+ "module": m.get("name"),
431
+ "is_external": m.get("is_external", False),
432
+ })
433
+
434
+ # Get call graph for all functions in file
435
+ call_graph = []
436
+ for f in file_functions:
437
+ calls = self._get_function_calls(session, f.get("qualified_name"))
438
+ call_graph.extend(calls)
439
+
440
+ # Get inheritance for all classes
441
+ inheritance = []
442
+ for c in file_classes:
443
+ inh = self._get_class_hierarchy(session, c.get("qualified_name"))
444
+ inheritance.extend(inh)
445
+
446
+ # Files list
447
+ files = [{
448
+ "path": file_node.get("path"),
449
+ "name": file_node.get("name"),
450
+ }]
451
+
452
+ return FeatureGraph(
453
+ root_node=root_node,
454
+ functions=functions,
455
+ classes=classes,
456
+ files=files,
457
+ call_graph=call_graph,
458
+ inheritance=inheritance,
459
+ imports=imports,
460
+ )
461
+
462
+ def _expand_call_graph(self, session, qualified_name: str, max_hops: int) -> dict:
463
+ """Expand call graph to multiple hops.
464
+
465
+ Uses Kuzu-compatible syntax (no startNode/endNode/relationships functions).
466
+
467
+ Returns:
468
+ Dict with 'functions' list and 'calls' list
469
+ """
470
+ calls = []
471
+ functions = []
472
+ seen_funcs = set()
473
+ seen_calls = set()
474
+
475
+ # Get the starting function
476
+ seen_funcs.add(qualified_name)
477
+
478
+ # Iteratively expand the call graph up to max_hops
479
+ current_functions = {qualified_name}
480
+
481
+ for hop in range(max_hops):
482
+ if not current_functions:
483
+ break
484
+
485
+ # Find all calls from/to current set of functions
486
+ # Outgoing calls
487
+ out_result = session.run("""
488
+ MATCH (caller:Function)-[:CALLS]->(callee:Function)
489
+ WHERE caller.qualified_name IN $func_names
490
+ RETURN caller.name as caller_name,
491
+ caller.qualified_name as caller_qualified,
492
+ callee.name as callee_name,
493
+ callee.qualified_name as callee_qualified,
494
+ callee.file_path as callee_file,
495
+ callee.docstring as callee_docstring
496
+ """, func_names=list(current_functions))
497
+
498
+ next_functions = set()
499
+ for record in out_result:
500
+ call_key = (record["caller_qualified"], record["callee_qualified"])
501
+ if call_key not in seen_calls:
502
+ seen_calls.add(call_key)
503
+ calls.append({
504
+ "caller": record["caller_name"],
505
+ "caller_qualified": record["caller_qualified"],
506
+ "callee": record["callee_name"],
507
+ "callee_qualified": record["callee_qualified"],
508
+ })
509
+
510
+ callee_qn = record["callee_qualified"]
511
+ if callee_qn and callee_qn not in seen_funcs:
512
+ seen_funcs.add(callee_qn)
513
+ next_functions.add(callee_qn)
514
+ functions.append({
515
+ "name": record["callee_name"],
516
+ "qualified_name": callee_qn,
517
+ "file_path": record["callee_file"],
518
+ "docstring": record["callee_docstring"],
519
+ })
520
+
521
+ # Incoming calls
522
+ in_result = session.run("""
523
+ MATCH (caller:Function)-[:CALLS]->(callee:Function)
524
+ WHERE callee.qualified_name IN $func_names
525
+ RETURN caller.name as caller_name,
526
+ caller.qualified_name as caller_qualified,
527
+ caller.file_path as caller_file,
528
+ caller.docstring as caller_docstring,
529
+ callee.name as callee_name,
530
+ callee.qualified_name as callee_qualified
531
+ """, func_names=list(current_functions))
532
+
533
+ for record in in_result:
534
+ call_key = (record["caller_qualified"], record["callee_qualified"])
535
+ if call_key not in seen_calls:
536
+ seen_calls.add(call_key)
537
+ calls.append({
538
+ "caller": record["caller_name"],
539
+ "caller_qualified": record["caller_qualified"],
540
+ "callee": record["callee_name"],
541
+ "callee_qualified": record["callee_qualified"],
542
+ })
543
+
544
+ caller_qn = record["caller_qualified"]
545
+ if caller_qn and caller_qn not in seen_funcs:
546
+ seen_funcs.add(caller_qn)
547
+ next_functions.add(caller_qn)
548
+ functions.append({
549
+ "name": record["caller_name"],
550
+ "qualified_name": caller_qn,
551
+ "file_path": record["caller_file"],
552
+ "docstring": record["caller_docstring"],
553
+ })
554
+
555
+ current_functions = next_functions
556
+
557
+ return {"functions": functions, "calls": calls}
558
+
559
+ def _get_class_hierarchy(self, session, qualified_name: str) -> list[dict]:
560
+ """Get inheritance hierarchy for a class."""
561
+ # Get ancestors (classes this class inherits from)
562
+ result = session.run("""
563
+ MATCH (c:Class {qualified_name: $qualified_name})
564
+ OPTIONAL MATCH (c)-[:INHERITS_FROM*1..3]->(ancestor:Class)
565
+ WITH c, collect(DISTINCT ancestor) as ancestors
566
+ UNWIND ancestors as a
567
+ RETURN c.name as child, a.name as parent
568
+ """, qualified_name=qualified_name)
569
+
570
+ inheritance = []
571
+ for record in result:
572
+ if record["child"] and record["parent"]:
573
+ inheritance.append({
574
+ "child": record["child"],
575
+ "parent": record["parent"],
576
+ })
577
+
578
+ # Get descendants (classes that inherit from this class)
579
+ result = session.run("""
580
+ MATCH (c:Class {qualified_name: $qualified_name})
581
+ OPTIONAL MATCH (descendant:Class)-[:INHERITS_FROM*1..3]->(c)
582
+ WITH c, collect(DISTINCT descendant) as descendants
583
+ UNWIND descendants as d
584
+ RETURN d.name as child, c.name as parent
585
+ """, qualified_name=qualified_name)
586
+
587
+ for record in result:
588
+ if record["child"] and record["parent"]:
589
+ inheritance.append({
590
+ "child": record["child"],
591
+ "parent": record["parent"],
592
+ })
593
+
594
+ return inheritance
595
+
596
+ def _get_function_calls(self, session, qualified_name: str) -> list[dict]:
597
+ """Get call relationships for a function."""
598
+ calls = []
599
+
600
+ # Get functions this function calls (outgoing)
601
+ result = session.run("""
602
+ MATCH (f:Function {qualified_name: $qualified_name})-[:CALLS]->(callee:Function)
603
+ RETURN f.name as caller, callee.name as callee
604
+ """, qualified_name=qualified_name)
605
+
606
+ for record in result:
607
+ if record["caller"] and record["callee"]:
608
+ calls.append({
609
+ "caller": record["caller"],
610
+ "callee": record["callee"],
611
+ })
612
+
613
+ # Get functions that call this function (incoming)
614
+ result = session.run("""
615
+ MATCH (caller:Function)-[:CALLS]->(f:Function {qualified_name: $qualified_name})
616
+ RETURN caller.name as caller, f.name as callee
617
+ """, qualified_name=qualified_name)
618
+
619
+ for record in result:
620
+ if record["caller"] and record["callee"]:
621
+ calls.append({
622
+ "caller": record["caller"],
623
+ "callee": record["callee"],
624
+ })
625
+
626
+ return calls
627
+
628
+ def _get_file_functions(self, session, file_path: str) -> list[dict]:
629
+ """Get all functions in a file."""
630
+ result = session.run("""
631
+ MATCH (f:File {path: $file_path})-[:CONTAINS_FUNCTION]->(func:Function)
632
+ RETURN func.name as name,
633
+ func.qualified_name as qualified_name,
634
+ func.file_path as file_path,
635
+ func.docstring as docstring
636
+ """, file_path=file_path)
637
+
638
+ functions = []
639
+ for record in result:
640
+ functions.append({
641
+ "name": record["name"],
642
+ "qualified_name": record["qualified_name"],
643
+ "file_path": record["file_path"],
644
+ "docstring": record["docstring"],
645
+ })
646
+ return functions