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,265 @@
|
|
|
1
|
+
"""Data models for EmDash entities."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ============================================================================
|
|
10
|
+
# Layer A: Code Structure Entities
|
|
11
|
+
# ============================================================================
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FileEntity:
|
|
16
|
+
"""Represents a source code file."""
|
|
17
|
+
|
|
18
|
+
path: str
|
|
19
|
+
name: str
|
|
20
|
+
extension: str
|
|
21
|
+
size_bytes: int
|
|
22
|
+
lines_of_code: int
|
|
23
|
+
hash: str
|
|
24
|
+
last_modified: Optional[datetime] = None
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_path(cls, file_path: Path, content_hash: str) -> "FileEntity":
|
|
28
|
+
"""Create a FileEntity from a file path."""
|
|
29
|
+
stat = file_path.stat()
|
|
30
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
31
|
+
lines = len(f.readlines())
|
|
32
|
+
|
|
33
|
+
return cls(
|
|
34
|
+
path=str(file_path),
|
|
35
|
+
name=file_path.name,
|
|
36
|
+
extension=file_path.suffix,
|
|
37
|
+
size_bytes=stat.st_size,
|
|
38
|
+
lines_of_code=lines,
|
|
39
|
+
hash=content_hash,
|
|
40
|
+
last_modified=datetime.fromtimestamp(stat.st_mtime),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ClassEntity:
|
|
46
|
+
"""Represents a class definition."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
qualified_name: str # e.g., "module.ClassName"
|
|
50
|
+
file_path: str
|
|
51
|
+
line_start: int
|
|
52
|
+
line_end: int
|
|
53
|
+
docstring: Optional[str] = None
|
|
54
|
+
is_abstract: bool = False
|
|
55
|
+
decorators: list[str] = field(default_factory=list)
|
|
56
|
+
base_classes: list[str] = field(default_factory=list) # For inheritance
|
|
57
|
+
attributes: list[str] = field(default_factory=list)
|
|
58
|
+
methods: list[str] = field(default_factory=list) # Qualified names of methods
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class FunctionEntity:
|
|
63
|
+
"""Represents a function or method definition."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
qualified_name: str # e.g., "module.function" or "module.Class.method"
|
|
67
|
+
file_path: str
|
|
68
|
+
line_start: int
|
|
69
|
+
line_end: int
|
|
70
|
+
docstring: Optional[str] = None
|
|
71
|
+
parameters: list[str] = field(default_factory=list)
|
|
72
|
+
return_annotation: Optional[str] = None
|
|
73
|
+
is_async: bool = False
|
|
74
|
+
is_method: bool = False
|
|
75
|
+
is_static: bool = False
|
|
76
|
+
is_classmethod: bool = False
|
|
77
|
+
decorators: list[str] = field(default_factory=list)
|
|
78
|
+
cyclomatic_complexity: int = 1
|
|
79
|
+
calls: list[str] = field(default_factory=list) # Qualified names of called functions
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ModuleEntity:
|
|
84
|
+
"""Represents a Python module (imported)."""
|
|
85
|
+
|
|
86
|
+
name: str
|
|
87
|
+
import_path: str
|
|
88
|
+
is_external: bool = False # stdlib or third-party
|
|
89
|
+
package: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ImportStatement:
|
|
94
|
+
"""Represents an import statement."""
|
|
95
|
+
|
|
96
|
+
file_path: str
|
|
97
|
+
line_number: int
|
|
98
|
+
module: str
|
|
99
|
+
imported_names: list[str] = field(default_factory=list)
|
|
100
|
+
alias: Optional[str] = None
|
|
101
|
+
import_type: str = "import" # "import" or "from_import"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Layer B: Git History Entities
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class CommitEntity:
|
|
111
|
+
"""Represents a git commit."""
|
|
112
|
+
|
|
113
|
+
sha: str
|
|
114
|
+
message: str
|
|
115
|
+
timestamp: datetime
|
|
116
|
+
author_name: str
|
|
117
|
+
author_email: str
|
|
118
|
+
committer_name: str
|
|
119
|
+
committer_email: str
|
|
120
|
+
insertions: int = 0
|
|
121
|
+
deletions: int = 0
|
|
122
|
+
files_changed: int = 0
|
|
123
|
+
is_merge: bool = False
|
|
124
|
+
parent_shas: list[str] = field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class FileModification:
|
|
129
|
+
"""Represents a file modification in a commit."""
|
|
130
|
+
|
|
131
|
+
commit_sha: str
|
|
132
|
+
file_path: str
|
|
133
|
+
change_type: str # "added", "modified", "deleted", "renamed"
|
|
134
|
+
insertions: int = 0
|
|
135
|
+
deletions: int = 0
|
|
136
|
+
old_path: Optional[str] = None # For renames
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class AuthorEntity:
|
|
141
|
+
"""Represents a code contributor."""
|
|
142
|
+
|
|
143
|
+
email: str
|
|
144
|
+
name: str
|
|
145
|
+
first_commit: Optional[datetime] = None
|
|
146
|
+
last_commit: Optional[datetime] = None
|
|
147
|
+
total_commits: int = 0
|
|
148
|
+
total_lines_added: int = 0
|
|
149
|
+
total_lines_deleted: int = 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class PullRequestEntity:
|
|
154
|
+
"""Represents a GitHub pull request."""
|
|
155
|
+
|
|
156
|
+
number: int
|
|
157
|
+
title: str
|
|
158
|
+
state: str # "open", "closed", "merged"
|
|
159
|
+
created_at: datetime
|
|
160
|
+
author: str
|
|
161
|
+
description: Optional[str] = None # Full PR body text
|
|
162
|
+
merged_at: Optional[datetime] = None
|
|
163
|
+
reviewers: list[str] = field(default_factory=list)
|
|
164
|
+
labels: list[str] = field(default_factory=list)
|
|
165
|
+
additions: int = 0
|
|
166
|
+
deletions: int = 0
|
|
167
|
+
files_changed: list[str] = field(default_factory=list) # File paths modified
|
|
168
|
+
commit_shas: list[str] = field(default_factory=list)
|
|
169
|
+
base_branch: str = "main"
|
|
170
|
+
head_branch: str = ""
|
|
171
|
+
embedding: Optional[list[float]] = None # Vector embedding for similarity
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class TaskEntity:
|
|
176
|
+
"""Represents a subtask extracted from a PR description."""
|
|
177
|
+
|
|
178
|
+
id: str # Format: "pr_{number}_task_{index}"
|
|
179
|
+
pr_number: int
|
|
180
|
+
description: str
|
|
181
|
+
is_completed: bool
|
|
182
|
+
order: int # Position in the task list
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class RepositoryEntity:
|
|
187
|
+
"""Represents a repository."""
|
|
188
|
+
|
|
189
|
+
url: str
|
|
190
|
+
name: str
|
|
191
|
+
owner: Optional[str] = None
|
|
192
|
+
default_branch: str = "main"
|
|
193
|
+
last_ingested: Optional[datetime] = None
|
|
194
|
+
ingestion_status: str = "pending"
|
|
195
|
+
commit_count: int = 0
|
|
196
|
+
file_count: int = 0
|
|
197
|
+
primary_language: str = "Python"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ============================================================================
|
|
201
|
+
# Layer C: Analytics Entities
|
|
202
|
+
# ============================================================================
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class ClusterEntity:
|
|
207
|
+
"""Represents a detected code cluster/community."""
|
|
208
|
+
|
|
209
|
+
cluster_id: int
|
|
210
|
+
algorithm: str # "louvain", "label_propagation"
|
|
211
|
+
modularity_score: float
|
|
212
|
+
size: int # Number of nodes in cluster
|
|
213
|
+
members: list[str] = field(default_factory=list) # Qualified names
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ============================================================================
|
|
217
|
+
# Aggregate Models
|
|
218
|
+
# ============================================================================
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class CodebaseEntities:
|
|
223
|
+
"""Aggregate of all entities extracted from a codebase."""
|
|
224
|
+
|
|
225
|
+
files: list[FileEntity] = field(default_factory=list)
|
|
226
|
+
classes: list[ClassEntity] = field(default_factory=list)
|
|
227
|
+
functions: list[FunctionEntity] = field(default_factory=list)
|
|
228
|
+
modules: list[ModuleEntity] = field(default_factory=list)
|
|
229
|
+
imports: list[ImportStatement] = field(default_factory=list)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def merge(cls, entities_list: list["FileEntities"]) -> "CodebaseEntities":
|
|
233
|
+
"""Merge multiple FileEntities into a single CodebaseEntities."""
|
|
234
|
+
result = cls()
|
|
235
|
+
for file_entities in entities_list:
|
|
236
|
+
if file_entities:
|
|
237
|
+
if file_entities.file:
|
|
238
|
+
result.files.append(file_entities.file)
|
|
239
|
+
result.classes.extend(file_entities.classes)
|
|
240
|
+
result.functions.extend(file_entities.functions)
|
|
241
|
+
result.modules.extend(file_entities.modules)
|
|
242
|
+
result.imports.extend(file_entities.imports)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class FileEntities:
|
|
248
|
+
"""Entities extracted from a single file."""
|
|
249
|
+
|
|
250
|
+
file: Optional[FileEntity] = None
|
|
251
|
+
classes: list[ClassEntity] = field(default_factory=list)
|
|
252
|
+
functions: list[FunctionEntity] = field(default_factory=list)
|
|
253
|
+
modules: list[ModuleEntity] = field(default_factory=list)
|
|
254
|
+
imports: list[ImportStatement] = field(default_factory=list)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class GitData:
|
|
259
|
+
"""Aggregate of all Git history data."""
|
|
260
|
+
|
|
261
|
+
repository: RepositoryEntity
|
|
262
|
+
commits: list[CommitEntity] = field(default_factory=list)
|
|
263
|
+
modifications: list[FileModification] = field(default_factory=list)
|
|
264
|
+
authors: list[AuthorEntity] = field(default_factory=list)
|
|
265
|
+
pull_requests: list[PullRequestEntity] = field(default_factory=list)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Configuration loader for PR review settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReviewConfig(BaseModel):
|
|
14
|
+
"""Configuration for PR review command."""
|
|
15
|
+
|
|
16
|
+
max_files: int = Field(default=50, ge=1, le=500)
|
|
17
|
+
max_tokens: int = Field(default=50000, ge=1000, le=200000)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find_config_path() -> Optional[Path]:
|
|
21
|
+
"""Find the review config file path."""
|
|
22
|
+
env_path = os.getenv("EMDASH_CONFIG_PATH")
|
|
23
|
+
if env_path:
|
|
24
|
+
return Path(env_path)
|
|
25
|
+
|
|
26
|
+
cwd = Path.cwd()
|
|
27
|
+
candidates = [
|
|
28
|
+
cwd / "emdash.config.json",
|
|
29
|
+
cwd / ".emdash-rules" / "config.json",
|
|
30
|
+
]
|
|
31
|
+
for path in candidates:
|
|
32
|
+
if path.is_file():
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_review_config() -> ReviewConfig:
|
|
39
|
+
"""Load review configuration from file if present."""
|
|
40
|
+
path = _find_config_path()
|
|
41
|
+
if not path or not path.is_file():
|
|
42
|
+
return ReviewConfig()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
46
|
+
raw = json.load(handle)
|
|
47
|
+
except (json.JSONDecodeError, OSError):
|
|
48
|
+
return ReviewConfig()
|
|
49
|
+
|
|
50
|
+
review = raw.get("review", {}) if isinstance(raw, dict) else {}
|
|
51
|
+
if not isinstance(review, dict):
|
|
52
|
+
return ReviewConfig()
|
|
53
|
+
|
|
54
|
+
return ReviewConfig(
|
|
55
|
+
max_files=review.get("max_files", 50),
|
|
56
|
+
max_tokens=review.get("max_tokens", 50000),
|
|
57
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Database layer for EmDash.
|
|
2
|
+
|
|
3
|
+
Provides an abstraction over database backends with Supabase as the default implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
Feature,
|
|
11
|
+
FeatureAssignee,
|
|
12
|
+
FeaturePR,
|
|
13
|
+
FeatureStatus,
|
|
14
|
+
PRStatus,
|
|
15
|
+
Project,
|
|
16
|
+
TeamMember,
|
|
17
|
+
)
|
|
18
|
+
from .provider import DatabaseProvider
|
|
19
|
+
from .providers.supabase import SupabaseProvider
|
|
20
|
+
from .auth import AuthProvider, User, get_auth
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Models
|
|
24
|
+
"Feature",
|
|
25
|
+
"FeatureAssignee",
|
|
26
|
+
"FeaturePR",
|
|
27
|
+
"FeatureStatus",
|
|
28
|
+
"PRStatus",
|
|
29
|
+
"Project",
|
|
30
|
+
"TeamMember",
|
|
31
|
+
"User",
|
|
32
|
+
# Provider
|
|
33
|
+
"DatabaseProvider",
|
|
34
|
+
"SupabaseProvider",
|
|
35
|
+
# Auth
|
|
36
|
+
"AuthProvider",
|
|
37
|
+
"get_auth",
|
|
38
|
+
# Factory
|
|
39
|
+
"get_provider",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_provider_instance: Optional[DatabaseProvider] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_provider() -> DatabaseProvider:
|
|
46
|
+
"""Get the configured database provider instance.
|
|
47
|
+
|
|
48
|
+
Returns a singleton instance of the database provider.
|
|
49
|
+
Currently only supports Supabase.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
DatabaseProvider instance
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If required environment variables are not set
|
|
56
|
+
"""
|
|
57
|
+
global _provider_instance
|
|
58
|
+
|
|
59
|
+
if _provider_instance is None:
|
|
60
|
+
provider_type = os.getenv("EMDASH_DB_PROVIDER", "supabase")
|
|
61
|
+
|
|
62
|
+
if provider_type == "supabase":
|
|
63
|
+
_provider_instance = SupabaseProvider()
|
|
64
|
+
else:
|
|
65
|
+
raise ValueError(f"Unknown database provider: {provider_type}")
|
|
66
|
+
|
|
67
|
+
return _provider_instance
|
emdash_core/db/auth.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Supabase authentication with GitHub OAuth."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from supabase import create_client, Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class User:
|
|
12
|
+
"""Authenticated user from Supabase Auth."""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
email: Optional[str]
|
|
16
|
+
github_handle: Optional[str]
|
|
17
|
+
avatar_url: Optional[str]
|
|
18
|
+
access_token: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthProvider:
|
|
22
|
+
"""Handle Supabase authentication with GitHub OAuth."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, url: Optional[str] = None, key: Optional[str] = None):
|
|
25
|
+
self.url = url or os.getenv("SUPABASE_URL")
|
|
26
|
+
self.key = key or os.getenv("SUPABASE_KEY")
|
|
27
|
+
|
|
28
|
+
if not self.url or not self.key:
|
|
29
|
+
raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set")
|
|
30
|
+
|
|
31
|
+
self.client: Client = create_client(self.url, self.key)
|
|
32
|
+
|
|
33
|
+
def get_github_login_url(self, redirect_to: Optional[str] = None) -> str:
|
|
34
|
+
"""Get the GitHub OAuth login URL.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
redirect_to: URL to redirect after login (for web apps)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
GitHub OAuth URL to redirect user to
|
|
41
|
+
"""
|
|
42
|
+
options = {}
|
|
43
|
+
if redirect_to:
|
|
44
|
+
options["redirect_to"] = redirect_to
|
|
45
|
+
|
|
46
|
+
response = self.client.auth.sign_in_with_oauth(
|
|
47
|
+
{"provider": "github", "options": options}
|
|
48
|
+
)
|
|
49
|
+
return response.url
|
|
50
|
+
|
|
51
|
+
def sign_in_with_github_token(self, access_token: str) -> Optional[User]:
|
|
52
|
+
"""Sign in with an existing GitHub access token.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
access_token: GitHub OAuth access token
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
User if successful, None otherwise
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
response = self.client.auth.sign_in_with_id_token(
|
|
62
|
+
{"provider": "github", "token": access_token}
|
|
63
|
+
)
|
|
64
|
+
return self._session_to_user(response)
|
|
65
|
+
except Exception:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_session(self) -> Optional[User]:
|
|
69
|
+
"""Get the current authenticated user from session.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
User if authenticated, None otherwise
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
response = self.client.auth.get_session()
|
|
76
|
+
if response and response.user:
|
|
77
|
+
return self._response_to_user(response)
|
|
78
|
+
return None
|
|
79
|
+
except Exception:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def sign_out(self) -> bool:
|
|
83
|
+
"""Sign out the current user.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if successful
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
self.client.auth.sign_out()
|
|
90
|
+
return True
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def _session_to_user(self, session) -> Optional[User]:
|
|
95
|
+
"""Convert Supabase session to User model."""
|
|
96
|
+
if not session or not session.user:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
user_meta = session.user.user_metadata or {}
|
|
100
|
+
|
|
101
|
+
return User(
|
|
102
|
+
id=session.user.id,
|
|
103
|
+
email=session.user.email,
|
|
104
|
+
github_handle=user_meta.get("user_name") or user_meta.get("preferred_username"),
|
|
105
|
+
avatar_url=user_meta.get("avatar_url"),
|
|
106
|
+
access_token=session.access_token if hasattr(session, "access_token") else None,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _response_to_user(self, response) -> Optional[User]:
|
|
110
|
+
"""Convert Supabase auth response to User model."""
|
|
111
|
+
if not response or not response.user:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
user_meta = response.user.user_metadata or {}
|
|
115
|
+
|
|
116
|
+
return User(
|
|
117
|
+
id=response.user.id,
|
|
118
|
+
email=response.user.email,
|
|
119
|
+
github_handle=user_meta.get("user_name") or user_meta.get("preferred_username"),
|
|
120
|
+
avatar_url=user_meta.get("avatar_url"),
|
|
121
|
+
access_token=response.session.access_token if response.session else None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Singleton instance
|
|
126
|
+
_auth_instance: Optional[AuthProvider] = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_auth() -> AuthProvider:
|
|
130
|
+
"""Get the auth provider instance."""
|
|
131
|
+
global _auth_instance
|
|
132
|
+
if _auth_instance is None:
|
|
133
|
+
_auth_instance = AuthProvider()
|
|
134
|
+
return _auth_instance
|
emdash_core/db/models.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Data models for the database layer."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FeatureStatus(str, Enum):
|
|
10
|
+
"""Status of a feature in the workflow."""
|
|
11
|
+
|
|
12
|
+
TODO = "todo"
|
|
13
|
+
IN_DESIGN_REVIEW = "in_design_review"
|
|
14
|
+
IN_PROGRESS = "in_progress"
|
|
15
|
+
IN_PR = "in_pr"
|
|
16
|
+
DONE = "done"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PRStatus(str, Enum):
|
|
20
|
+
"""Status of a pull request."""
|
|
21
|
+
|
|
22
|
+
OPEN = "open"
|
|
23
|
+
MERGED = "merged"
|
|
24
|
+
CLOSED = "closed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Project:
|
|
29
|
+
"""A project in the system."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
name: str
|
|
33
|
+
repo_url: Optional[str] = None
|
|
34
|
+
owner_id: Optional[str] = None
|
|
35
|
+
created_at: Optional[datetime] = None
|
|
36
|
+
updated_at: Optional[datetime] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TeamMember:
|
|
41
|
+
"""A team member belonging to a project."""
|
|
42
|
+
|
|
43
|
+
id: str
|
|
44
|
+
project_id: str
|
|
45
|
+
name: str
|
|
46
|
+
email: Optional[str] = None
|
|
47
|
+
github_handle: Optional[str] = None
|
|
48
|
+
role: Optional[str] = None
|
|
49
|
+
user_id: Optional[str] = None # Links to auth.users
|
|
50
|
+
created_at: Optional[datetime] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Feature:
|
|
55
|
+
"""A feature belonging to a project."""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
project_id: str
|
|
59
|
+
title: str
|
|
60
|
+
description: Optional[str] = None
|
|
61
|
+
status: FeatureStatus = FeatureStatus.TODO
|
|
62
|
+
spec: Optional[str] = None
|
|
63
|
+
plan: Optional[str] = None
|
|
64
|
+
tasks: Optional[str] = None
|
|
65
|
+
created_at: Optional[datetime] = None
|
|
66
|
+
updated_at: Optional[datetime] = None
|
|
67
|
+
# Populated by joins
|
|
68
|
+
assignees: list["TeamMember"] = field(default_factory=list)
|
|
69
|
+
prs: list["FeaturePR"] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class FeatureAssignee:
|
|
74
|
+
"""Junction table for feature-assignee many-to-many relationship."""
|
|
75
|
+
|
|
76
|
+
feature_id: str
|
|
77
|
+
team_member_id: str
|
|
78
|
+
assigned_at: Optional[datetime] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class FeaturePR:
|
|
83
|
+
"""A pull request linked to a feature."""
|
|
84
|
+
|
|
85
|
+
id: str
|
|
86
|
+
feature_id: str
|
|
87
|
+
pr_url: str
|
|
88
|
+
pr_number: int
|
|
89
|
+
title: Optional[str] = None
|
|
90
|
+
status: PRStatus = PRStatus.OPEN
|
|
91
|
+
created_at: Optional[datetime] = None
|