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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- 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
|