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,68 @@
1
+ """Pydantic models for agent API."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class AgentMode(str, Enum):
10
+ """Agent operation modes."""
11
+
12
+ CODE = "code"
13
+ RESEARCH = "research"
14
+ REVIEW = "review"
15
+ SPEC = "spec"
16
+ PLAN = "plan"
17
+
18
+
19
+ class ImageData(BaseModel):
20
+ """Image data for vision-capable models."""
21
+
22
+ data: str = Field(..., description="Base64 encoded image data")
23
+ format: str = Field(default="png", description="Image format (png, jpg, etc.)")
24
+
25
+
26
+ class AgentChatOptions(BaseModel):
27
+ """Options for agent chat."""
28
+
29
+ max_iterations: int = Field(default=50, description="Maximum agent iterations")
30
+ verbose: bool = Field(default=True, description="Enable verbose output")
31
+ mode: AgentMode = Field(default=AgentMode.CODE, description="Agent mode")
32
+ context_threshold: float = Field(
33
+ default=0.6,
34
+ description="Context window threshold for summarization (0-1)"
35
+ )
36
+
37
+
38
+ class AgentChatRequest(BaseModel):
39
+ """Request for agent chat endpoint."""
40
+
41
+ message: str = Field(..., description="User message/task")
42
+ session_id: Optional[str] = Field(
43
+ default=None,
44
+ description="Session ID for conversation continuity"
45
+ )
46
+ model: Optional[str] = Field(
47
+ default=None,
48
+ description="Model to use (defaults to server config)"
49
+ )
50
+ images: list[ImageData] = Field(
51
+ default_factory=list,
52
+ description="Images for vision-capable models"
53
+ )
54
+ options: AgentChatOptions = Field(
55
+ default_factory=AgentChatOptions,
56
+ description="Agent options"
57
+ )
58
+
59
+
60
+ class SessionInfo(BaseModel):
61
+ """Information about an agent session."""
62
+
63
+ session_id: str
64
+ agent_name: str
65
+ model: str
66
+ created_at: str
67
+ message_count: int
68
+ is_active: bool
@@ -0,0 +1,77 @@
1
+ """Pydantic models for indexing API."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class IndexOptions(BaseModel):
10
+ """Options for indexing operation."""
11
+
12
+ incremental: bool = Field(
13
+ default=False,
14
+ description="Only index changed files"
15
+ )
16
+ changed_only: bool = Field(
17
+ default=False,
18
+ description="Detect and index only modified files"
19
+ )
20
+ skip_git: bool = Field(
21
+ default=False,
22
+ description="Skip git history analysis"
23
+ )
24
+ pr_limit: int = Field(
25
+ default=100,
26
+ description="Maximum PRs to fetch"
27
+ )
28
+ detect_communities: bool = Field(
29
+ default=True,
30
+ description="Run community detection"
31
+ )
32
+ skip_embeddings: bool = Field(
33
+ default=False,
34
+ description="Skip embedding generation"
35
+ )
36
+
37
+
38
+ class IndexRequest(BaseModel):
39
+ """Request to start indexing."""
40
+
41
+ repo_path: str = Field(..., description="Path to repository")
42
+ options: IndexOptions = Field(
43
+ default_factory=IndexOptions,
44
+ description="Indexing options"
45
+ )
46
+
47
+
48
+ class IndexStats(BaseModel):
49
+ """Statistics about indexed content."""
50
+
51
+ files: int = Field(default=0, description="Number of files indexed")
52
+ functions: int = Field(default=0, description="Number of functions")
53
+ classes: int = Field(default=0, description="Number of classes")
54
+ relationships: int = Field(default=0, description="Number of relationships")
55
+ communities: int = Field(default=0, description="Number of communities detected")
56
+
57
+
58
+ class IndexStatus(BaseModel):
59
+ """Status of indexing operation."""
60
+
61
+ is_running: bool = Field(default=False, description="Whether indexing is in progress")
62
+ last_indexed: Optional[datetime] = Field(
63
+ default=None,
64
+ description="Last successful index timestamp"
65
+ )
66
+ last_commit: Optional[str] = Field(
67
+ default=None,
68
+ description="Last indexed commit hash"
69
+ )
70
+ stats: IndexStats = Field(
71
+ default_factory=IndexStats,
72
+ description="Index statistics"
73
+ )
74
+ error: Optional[str] = Field(
75
+ default=None,
76
+ description="Error message if failed"
77
+ )
@@ -0,0 +1,113 @@
1
+ """Pydantic models for query API."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class EntityType(str, Enum):
10
+ """Types of code entities."""
11
+
12
+ FILE = "File"
13
+ CLASS = "Class"
14
+ FUNCTION = "Function"
15
+ METHOD = "Method"
16
+ MODULE = "Module"
17
+
18
+
19
+ class SearchType(str, Enum):
20
+ """Types of search operations."""
21
+
22
+ SEMANTIC = "semantic"
23
+ TEXT = "text"
24
+ GREP = "grep"
25
+
26
+
27
+ class SearchFilters(BaseModel):
28
+ """Filters for search results."""
29
+
30
+ entity_types: list[EntityType] = Field(
31
+ default_factory=list,
32
+ description="Filter by entity types"
33
+ )
34
+ limit: int = Field(default=20, description="Maximum results to return")
35
+ min_score: float = Field(default=0.0, description="Minimum similarity score")
36
+ file_patterns: list[str] = Field(
37
+ default_factory=list,
38
+ description="Glob patterns to filter files"
39
+ )
40
+
41
+
42
+ class SearchRequest(BaseModel):
43
+ """Request for search endpoint."""
44
+
45
+ query: str = Field(..., description="Search query")
46
+ type: SearchType = Field(default=SearchType.SEMANTIC, description="Search type")
47
+ filters: SearchFilters = Field(
48
+ default_factory=SearchFilters,
49
+ description="Search filters"
50
+ )
51
+
52
+
53
+ class SearchResult(BaseModel):
54
+ """A single search result."""
55
+
56
+ qualified_name: str = Field(..., description="Fully qualified name")
57
+ name: str = Field(..., description="Short name")
58
+ type: EntityType = Field(..., description="Entity type")
59
+ file_path: str = Field(..., description="File path relative to repo")
60
+ line_number: Optional[int] = Field(default=None, description="Line number")
61
+ score: float = Field(..., description="Relevance score (0-1)")
62
+ snippet: Optional[str] = Field(default=None, description="Code snippet")
63
+
64
+
65
+ class SearchResponse(BaseModel):
66
+ """Response from search endpoint."""
67
+
68
+ results: list[SearchResult] = Field(default_factory=list)
69
+ total: int = Field(..., description="Total number of matches")
70
+ query: str = Field(..., description="Original query")
71
+
72
+
73
+ class ExpandRequest(BaseModel):
74
+ """Request to expand a node."""
75
+
76
+ node_type: EntityType = Field(..., description="Type of node to expand")
77
+ identifier: str = Field(..., description="Qualified name or identifier")
78
+ max_hops: int = Field(default=2, description="Maximum traversal depth")
79
+ include_source: bool = Field(default=True, description="Include source code")
80
+
81
+
82
+ class CallersRequest(BaseModel):
83
+ """Request to get callers of a function."""
84
+
85
+ qualified_name: str = Field(..., description="Qualified name of function")
86
+ max_depth: int = Field(default=1, description="Maximum call depth")
87
+
88
+
89
+ class CalleesRequest(BaseModel):
90
+ """Request to get callees of a function."""
91
+
92
+ qualified_name: str = Field(..., description="Qualified name of function")
93
+ max_depth: int = Field(default=1, description="Maximum call depth")
94
+
95
+
96
+ class HierarchyRequest(BaseModel):
97
+ """Request to get class hierarchy."""
98
+
99
+ class_name: str = Field(..., description="Qualified name of class")
100
+ direction: str = Field(
101
+ default="both",
102
+ description="Direction: 'up' (parents), 'down' (children), 'both'"
103
+ )
104
+
105
+
106
+ class DependenciesRequest(BaseModel):
107
+ """Request to get file dependencies."""
108
+
109
+ file_path: str = Field(..., description="File path to analyze")
110
+ direction: str = Field(
111
+ default="both",
112
+ description="Direction: 'imports', 'imported_by', 'both'"
113
+ )
@@ -0,0 +1,7 @@
1
+ """Planning and context building for AI agents."""
2
+
3
+ from .similarity import SimilaritySearch
4
+ from .context_builder import ContextBuilder, PlanningContext
5
+ from .agent_api import AgentAPI
6
+
7
+ __all__ = ["SimilaritySearch", "ContextBuilder", "PlanningContext", "AgentAPI"]
@@ -0,0 +1,413 @@
1
+ """Graph traversal API for AI agents."""
2
+
3
+ from typing import Optional
4
+
5
+ from ..graph.connection import KuzuConnection, get_connection
6
+ from ..utils.logger import log
7
+
8
+
9
+ class AgentAPI:
10
+ """Graph traversal API for AI coding agents."""
11
+
12
+ def __init__(self, connection: Optional[KuzuConnection] = None):
13
+ """Initialize agent API.
14
+
15
+ Args:
16
+ connection: Neo4j connection. If None, uses global connection.
17
+ """
18
+ self.connection = connection or get_connection()
19
+
20
+ def get_file_dependencies(self, file_path: str) -> dict:
21
+ """Get files that import/are imported by this file.
22
+
23
+ Args:
24
+ file_path: Path to the file
25
+
26
+ Returns:
27
+ Dictionary with imports and imported_by lists
28
+ """
29
+ with self.connection.session() as session:
30
+ # Get files this file imports
31
+ imports_result = session.run("""
32
+ MATCH (f:File)-[:IMPORTS]->(m:Module)
33
+ WHERE f.path ENDS WITH $file_path
34
+ RETURN m.name as module_name,
35
+ m.is_external as is_external
36
+ """, file_path=file_path)
37
+ imports = [dict(r) for r in imports_result]
38
+
39
+ # Get files that import modules from this file
40
+ # Query functions and classes separately (Kuzu doesn't support | in rel types)
41
+ func_result = session.run("""
42
+ MATCH (f:File)-[:CONTAINS_FUNCTION]->(entity:Function)
43
+ WHERE f.path ENDS WITH $file_path
44
+ WITH entity.qualified_name as qn
45
+ MATCH (other:File)-[:IMPORTS]->(m:Module)
46
+ WHERE m.name CONTAINS qn OR m.import_path CONTAINS qn
47
+ RETURN DISTINCT other.path as file_path
48
+ """, file_path=file_path)
49
+
50
+ class_result = session.run("""
51
+ MATCH (f:File)-[:CONTAINS_CLASS]->(entity:Class)
52
+ WHERE f.path ENDS WITH $file_path
53
+ WITH entity.qualified_name as qn
54
+ MATCH (other:File)-[:IMPORTS]->(m:Module)
55
+ WHERE m.name CONTAINS qn OR m.import_path CONTAINS qn
56
+ RETURN DISTINCT other.path as file_path
57
+ """, file_path=file_path)
58
+
59
+ imported_by = list(set(
60
+ [r["file_path"] for r in func_result] +
61
+ [r["file_path"] for r in class_result]
62
+ ))
63
+
64
+ return {
65
+ "file_path": file_path,
66
+ "imports": imports,
67
+ "imported_by": imported_by,
68
+ }
69
+
70
+ def get_function_callers(self, qualified_name: str) -> list[dict]:
71
+ """Find all functions that call this function.
72
+
73
+ Args:
74
+ qualified_name: Qualified name of the function
75
+
76
+ Returns:
77
+ List of calling functions with metadata
78
+ """
79
+ with self.connection.session() as session:
80
+ result = session.run("""
81
+ MATCH (caller:Function)-[:CALLS]->(f:Function {qualified_name: $qualified_name})
82
+ RETURN caller.name as name,
83
+ caller.qualified_name as qualified_name,
84
+ caller.file_path as file_path,
85
+ caller.is_method as is_method
86
+ ORDER BY caller.name
87
+ """, qualified_name=qualified_name)
88
+
89
+ return [dict(r) for r in result]
90
+
91
+ def get_function_callees(self, qualified_name: str) -> list[dict]:
92
+ """Find all functions called by this function.
93
+
94
+ Args:
95
+ qualified_name: Qualified name of the function
96
+
97
+ Returns:
98
+ List of called functions with metadata
99
+ """
100
+ with self.connection.session() as session:
101
+ result = session.run("""
102
+ MATCH (f:Function {qualified_name: $qualified_name})-[:CALLS]->(callee:Function)
103
+ RETURN callee.name as name,
104
+ callee.qualified_name as qualified_name,
105
+ callee.file_path as file_path,
106
+ callee.is_method as is_method
107
+ ORDER BY callee.name
108
+ """, qualified_name=qualified_name)
109
+
110
+ return [dict(r) for r in result]
111
+
112
+ def get_class_hierarchy(self, class_name: str) -> dict:
113
+ """Get inheritance tree for a class.
114
+
115
+ Args:
116
+ class_name: Name or qualified name of the class
117
+
118
+ Returns:
119
+ Dictionary with parents and children
120
+ """
121
+ with self.connection.session() as session:
122
+ # Get parent classes
123
+ parents_result = session.run("""
124
+ MATCH (c:Class)-[:INHERITS_FROM]->(parent:Class)
125
+ WHERE c.name = $class_name OR c.qualified_name = $class_name
126
+ RETURN parent.name as name,
127
+ parent.qualified_name as qualified_name,
128
+ parent.file_path as file_path
129
+ """, class_name=class_name)
130
+ parents = [dict(r) for r in parents_result]
131
+
132
+ # Get child classes
133
+ children_result = session.run("""
134
+ MATCH (child:Class)-[:INHERITS_FROM]->(c:Class)
135
+ WHERE c.name = $class_name OR c.qualified_name = $class_name
136
+ RETURN child.name as name,
137
+ child.qualified_name as qualified_name,
138
+ child.file_path as file_path
139
+ """, class_name=class_name)
140
+ children = [dict(r) for r in children_result]
141
+
142
+ return {
143
+ "class_name": class_name,
144
+ "parents": parents,
145
+ "children": children,
146
+ }
147
+
148
+ def get_file_history(self, file_path: str, limit: int = 10) -> dict:
149
+ """Get recent commits that modified this file.
150
+
151
+ Args:
152
+ file_path: Path to the file
153
+ limit: Maximum number of commits to return
154
+
155
+ Returns:
156
+ Dictionary with file_path and commits list
157
+ """
158
+ with self.connection.session() as session:
159
+ result = session.run("""
160
+ MATCH (c:GitCommit)-[mod:COMMIT_MODIFIES]->(f:File)
161
+ WHERE f.path ENDS WITH $file_path
162
+ RETURN c.sha as sha,
163
+ c.message as message,
164
+ c.author_name as author,
165
+ c.timestamp as timestamp,
166
+ mod.change_type as change_type,
167
+ mod.insertions as insertions,
168
+ mod.deletions as deletions
169
+ ORDER BY c.timestamp DESC
170
+ LIMIT $limit
171
+ """, file_path=file_path, limit=limit)
172
+
173
+ commits = [dict(r) for r in result]
174
+
175
+ return {
176
+ "file_path": file_path,
177
+ "commits": commits,
178
+ }
179
+
180
+ def get_community_overview(self, community_id: int) -> dict:
181
+ """Get summary of a code community.
182
+
183
+ Args:
184
+ community_id: The community ID
185
+
186
+ Returns:
187
+ Dictionary with community summary
188
+ """
189
+ with self.connection.session() as session:
190
+ # Get member counts by type
191
+ result = session.run("""
192
+ MATCH (n)
193
+ WHERE n.community = $community_id
194
+ AND (n:Class OR n:Function)
195
+ WITH label(n) as type, n
196
+ RETURN type,
197
+ count(n) as count,
198
+ collect(n.name)[0:10] as sample_names
199
+ """, community_id=community_id)
200
+
201
+ members_by_type = {r["type"]: {"count": r["count"], "samples": r["sample_names"]}
202
+ for r in result}
203
+
204
+ # Get files in this community (separate queries for Kuzu compatibility)
205
+ func_files = session.run("""
206
+ MATCH (f:File)-[:CONTAINS_FUNCTION]->(n:Function)
207
+ WHERE n.community = $community_id
208
+ RETURN DISTINCT f.path as file_path
209
+ LIMIT 10
210
+ """, community_id=community_id)
211
+
212
+ class_files = session.run("""
213
+ MATCH (f:File)-[:CONTAINS_CLASS]->(n:Class)
214
+ WHERE n.community = $community_id
215
+ RETURN DISTINCT f.path as file_path
216
+ LIMIT 10
217
+ """, community_id=community_id)
218
+
219
+ files = list(set(
220
+ [r["file_path"] for r in func_files] +
221
+ [r["file_path"] for r in class_files]
222
+ ))[:10]
223
+
224
+ return {
225
+ "community_id": community_id,
226
+ "members_by_type": members_by_type,
227
+ "sample_files": files,
228
+ }
229
+
230
+ def get_author_expertise(self, email: str) -> dict:
231
+ """Get files and areas an author has worked on.
232
+
233
+ Args:
234
+ email: Author's email
235
+
236
+ Returns:
237
+ Dictionary with author expertise summary
238
+ """
239
+ with self.connection.session() as session:
240
+ # Get author info
241
+ author_result = session.run("""
242
+ MATCH (a:Author {email: $email})
243
+ RETURN a.name as name,
244
+ a.total_commits as total_commits,
245
+ a.total_lines_added as lines_added,
246
+ a.total_lines_deleted as lines_deleted
247
+ """, email=email)
248
+ author = author_result.single()
249
+
250
+ if not author:
251
+ return {"error": f"Author not found: {email}"}
252
+
253
+ # Get most modified files
254
+ files_result = session.run("""
255
+ MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
256
+ WITH f.path as file_path, count(c) as commit_count
257
+ RETURN file_path, commit_count
258
+ ORDER BY commit_count DESC
259
+ LIMIT 10
260
+ """, email=email)
261
+ top_files = [dict(r) for r in files_result]
262
+
263
+ # Get communities the author has worked in (separate queries for Kuzu)
264
+ func_communities = session.run("""
265
+ MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
266
+ MATCH (f)-[:CONTAINS_FUNCTION]->(entity:Function)
267
+ WHERE entity.community IS NOT NULL
268
+ WITH entity.community as community_id, count(DISTINCT c) as commit_count
269
+ RETURN community_id, commit_count
270
+ ORDER BY commit_count DESC
271
+ LIMIT 5
272
+ """, email=email)
273
+
274
+ class_communities = session.run("""
275
+ MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
276
+ MATCH (f)-[:CONTAINS_CLASS]->(entity:Class)
277
+ WHERE entity.community IS NOT NULL
278
+ WITH entity.community as community_id, count(DISTINCT c) as commit_count
279
+ RETURN community_id, commit_count
280
+ ORDER BY commit_count DESC
281
+ LIMIT 5
282
+ """, email=email)
283
+
284
+ # Combine and deduplicate by community_id, keeping highest commit_count
285
+ community_map = {}
286
+ for r in list(func_communities) + list(class_communities):
287
+ cid = r["community_id"]
288
+ cc = r["commit_count"]
289
+ if cid not in community_map or cc > community_map[cid]:
290
+ community_map[cid] = cc
291
+ communities = [
292
+ {"community_id": cid, "commit_count": cc}
293
+ for cid, cc in sorted(community_map.items(), key=lambda x: -x[1])[:5]
294
+ ]
295
+
296
+ return {
297
+ "email": email,
298
+ "name": author["name"],
299
+ "total_commits": author["total_commits"],
300
+ "lines_added": author["lines_added"],
301
+ "lines_deleted": author["lines_deleted"],
302
+ "top_files": top_files,
303
+ "communities": communities,
304
+ }
305
+
306
+ def expand_from_files(
307
+ self,
308
+ file_paths: list[str],
309
+ hops: int = 1,
310
+ ) -> dict:
311
+ """Expand to related files within N relationship hops.
312
+
313
+ Args:
314
+ file_paths: Starting file paths
315
+ hops: Number of relationship hops to follow
316
+
317
+ Returns:
318
+ Dictionary with expanded file set and relationships
319
+ """
320
+ with self.connection.session() as session:
321
+ # Get directly related files (via imports, function calls)
322
+ result = session.run("""
323
+ UNWIND $file_paths as fp
324
+ MATCH (f:File)
325
+ WHERE f.path ENDS WITH fp
326
+
327
+ // Follow imports
328
+ OPTIONAL MATCH (f)-[:IMPORTS]->(m:Module)<-[:IMPORTS]-(related:File)
329
+ WHERE related.path <> f.path
330
+
331
+ // Follow function calls
332
+ OPTIONAL MATCH (f)-[:CONTAINS_FUNCTION]->(func:Function)-[:CALLS]->(called:Function)<-[:CONTAINS_FUNCTION]-(related2:File)
333
+ WHERE related2.path <> f.path
334
+
335
+ WITH collect(DISTINCT related.path) + collect(DISTINCT related2.path) as related_paths
336
+ UNWIND related_paths as rp
337
+ WITH rp WHERE rp IS NOT NULL
338
+ RETURN DISTINCT rp as file_path
339
+ LIMIT 20
340
+ """, file_paths=file_paths)
341
+
342
+ related_files = [r["file_path"] for r in result]
343
+
344
+ return {
345
+ "starting_files": file_paths,
346
+ "hops": hops,
347
+ "related_files": related_files,
348
+ "total_files": len(file_paths) + len(related_files),
349
+ }
350
+
351
+ def get_impact_analysis(self, file_path: str) -> dict:
352
+ """Analyze potential impact of changing a file.
353
+
354
+ Args:
355
+ file_path: Path to the file
356
+
357
+ Returns:
358
+ Dictionary with impact analysis
359
+ """
360
+ with self.connection.session() as session:
361
+ # Get functions in this file and their callers
362
+ callers_result = session.run("""
363
+ MATCH (f:File)-[:CONTAINS_FUNCTION]->(func:Function)
364
+ WHERE f.path ENDS WITH $file_path
365
+ OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
366
+ RETURN func.name as function_name,
367
+ func.qualified_name as qualified_name,
368
+ collect(DISTINCT caller.qualified_name) as called_by
369
+ """, file_path=file_path)
370
+
371
+ functions_impact = []
372
+ total_callers = set()
373
+ for r in callers_result:
374
+ callers = [c for c in r["called_by"] if c is not None]
375
+ total_callers.update(callers)
376
+ functions_impact.append({
377
+ "name": r["function_name"],
378
+ "qualified_name": r["qualified_name"],
379
+ "caller_count": len(callers),
380
+ })
381
+
382
+ # Get files that import from this file (separate queries for Kuzu)
383
+ func_names = session.run("""
384
+ MATCH (f:File)-[:CONTAINS_FUNCTION]->(entity:Function)
385
+ WHERE f.path ENDS WITH $file_path
386
+ RETURN DISTINCT entity.name as name
387
+ """, file_path=file_path)
388
+
389
+ class_names = session.run("""
390
+ MATCH (f:File)-[:CONTAINS_CLASS]->(entity:Class)
391
+ WHERE f.path ENDS WITH $file_path
392
+ RETURN DISTINCT entity.name as name
393
+ """, file_path=file_path)
394
+
395
+ exported_names = [r["name"] for r in func_names] + [r["name"] for r in class_names]
396
+
397
+ # Find files that import these names
398
+ dependent_files = []
399
+ if exported_names:
400
+ dependents_result = session.run("""
401
+ MATCH (other:File)-[:IMPORTS]->(m:Module)
402
+ WHERE any(name IN $exported_names WHERE m.name CONTAINS name)
403
+ RETURN DISTINCT other.path as file_path
404
+ """, exported_names=exported_names)
405
+ dependent_files = [r["file_path"] for r in dependents_result]
406
+
407
+ return {
408
+ "file_path": file_path,
409
+ "functions": functions_impact,
410
+ "total_callers": len(total_callers),
411
+ "dependent_files": dependent_files,
412
+ "risk_level": "high" if len(total_callers) > 10 else "medium" if len(total_callers) > 3 else "low",
413
+ }