codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""V2 Review router - delegates to core modules.
|
|
2
|
+
|
|
3
|
+
This module provides v2-style API endpoints for code review operations
|
|
4
|
+
that delegate to core modules. It uses the v2 Workspace model.
|
|
5
|
+
|
|
6
|
+
The v1 router (review.py) remains for backwards compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Literal, Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from codeframe.core.workspace import Workspace
|
|
16
|
+
from codeframe.lib.rate_limiter import rate_limit_standard
|
|
17
|
+
from codeframe.core import review, git
|
|
18
|
+
from codeframe.ui.dependencies import get_v2_workspace
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/api/v2/review", tags=["review-v2"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Response Models
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReviewFindingResponse(BaseModel):
|
|
31
|
+
"""Response model for a review finding."""
|
|
32
|
+
|
|
33
|
+
category: str
|
|
34
|
+
severity: Literal["critical", "high", "medium", "low", "info"]
|
|
35
|
+
message: str
|
|
36
|
+
file_path: str
|
|
37
|
+
line_number: Optional[int] = None
|
|
38
|
+
suggestion: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ReviewResultResponse(BaseModel):
|
|
42
|
+
"""Response model for review result."""
|
|
43
|
+
|
|
44
|
+
status: Literal["approved", "changes_requested", "rejected"]
|
|
45
|
+
overall_score: float
|
|
46
|
+
findings: list[ReviewFindingResponse]
|
|
47
|
+
summary: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReviewSummaryResponse(BaseModel):
|
|
51
|
+
"""Response model for review summary."""
|
|
52
|
+
|
|
53
|
+
status: str
|
|
54
|
+
overall_score: float
|
|
55
|
+
total_findings: int
|
|
56
|
+
severity_counts: dict[str, int]
|
|
57
|
+
summary: str
|
|
58
|
+
has_blocking_issues: bool
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReviewFilesRequest(BaseModel):
|
|
62
|
+
"""Request model for reviewing files."""
|
|
63
|
+
|
|
64
|
+
files: list[str] = Field(..., min_length=1, description="Files to review")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ReviewTaskRequest(BaseModel):
|
|
68
|
+
"""Request model for reviewing a task's files."""
|
|
69
|
+
|
|
70
|
+
task_id: str = Field(..., description="Task ID")
|
|
71
|
+
files_modified: list[str] = Field(..., min_length=1, description="Modified files to review")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FileChangeResponse(BaseModel):
|
|
75
|
+
"""Per-file change statistics."""
|
|
76
|
+
|
|
77
|
+
path: str
|
|
78
|
+
change_type: str
|
|
79
|
+
insertions: int
|
|
80
|
+
deletions: int
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DiffStatsResponse(BaseModel):
|
|
84
|
+
"""Response model for diff with statistics."""
|
|
85
|
+
|
|
86
|
+
diff: str
|
|
87
|
+
files_changed: int
|
|
88
|
+
insertions: int
|
|
89
|
+
deletions: int
|
|
90
|
+
changed_files: list[FileChangeResponse]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PatchResponse(BaseModel):
|
|
94
|
+
"""Response model for patch export."""
|
|
95
|
+
|
|
96
|
+
patch: str
|
|
97
|
+
filename: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CommitMessageResponse(BaseModel):
|
|
101
|
+
"""Response model for generated commit message."""
|
|
102
|
+
|
|
103
|
+
message: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ============================================================================
|
|
107
|
+
# Review Endpoints
|
|
108
|
+
# ============================================================================
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.post("/files", response_model=ReviewResultResponse)
|
|
112
|
+
@rate_limit_standard()
|
|
113
|
+
async def review_files(
|
|
114
|
+
request: Request,
|
|
115
|
+
body: ReviewFilesRequest,
|
|
116
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
117
|
+
) -> ReviewResultResponse:
|
|
118
|
+
"""Run code review on specified files.
|
|
119
|
+
|
|
120
|
+
Performs complexity analysis, security scanning, and OWASP pattern
|
|
121
|
+
detection on the given files.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
request: HTTP request for rate limiting
|
|
125
|
+
body: ReviewFilesRequest with file list
|
|
126
|
+
workspace: v2 Workspace
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
ReviewResultResponse with findings and score
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
result = review.review_files(workspace, body.files)
|
|
133
|
+
|
|
134
|
+
return ReviewResultResponse(
|
|
135
|
+
status=result.status,
|
|
136
|
+
overall_score=result.overall_score,
|
|
137
|
+
findings=[
|
|
138
|
+
ReviewFindingResponse(
|
|
139
|
+
category=f.category,
|
|
140
|
+
severity=f.severity,
|
|
141
|
+
message=f.message,
|
|
142
|
+
file_path=f.file_path,
|
|
143
|
+
line_number=f.line_number,
|
|
144
|
+
suggestion=f.suggestion,
|
|
145
|
+
)
|
|
146
|
+
for f in result.findings
|
|
147
|
+
],
|
|
148
|
+
summary=result.summary,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.error(f"Failed to review files: {e}", exc_info=True)
|
|
153
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.post("/task", response_model=ReviewResultResponse)
|
|
157
|
+
@rate_limit_standard()
|
|
158
|
+
async def review_task(
|
|
159
|
+
request: Request,
|
|
160
|
+
body: ReviewTaskRequest,
|
|
161
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
162
|
+
) -> ReviewResultResponse:
|
|
163
|
+
"""Run code review for a task's modified files.
|
|
164
|
+
|
|
165
|
+
Convenience endpoint that wraps file review with task context.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
request: HTTP request for rate limiting
|
|
169
|
+
body: ReviewTaskRequest with task ID and modified files
|
|
170
|
+
workspace: v2 Workspace
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
ReviewResultResponse with findings and score
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
result = review.review_task(
|
|
177
|
+
workspace,
|
|
178
|
+
task_id=body.task_id,
|
|
179
|
+
files_modified=body.files_modified,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return ReviewResultResponse(
|
|
183
|
+
status=result.status,
|
|
184
|
+
overall_score=result.overall_score,
|
|
185
|
+
findings=[
|
|
186
|
+
ReviewFindingResponse(
|
|
187
|
+
category=f.category,
|
|
188
|
+
severity=f.severity,
|
|
189
|
+
message=f.message,
|
|
190
|
+
file_path=f.file_path,
|
|
191
|
+
line_number=f.line_number,
|
|
192
|
+
suggestion=f.suggestion,
|
|
193
|
+
)
|
|
194
|
+
for f in result.findings
|
|
195
|
+
],
|
|
196
|
+
summary=result.summary,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(f"Failed to review task: {e}", exc_info=True)
|
|
201
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.post("/files/summary", response_model=ReviewSummaryResponse)
|
|
205
|
+
@rate_limit_standard()
|
|
206
|
+
async def review_files_summary(
|
|
207
|
+
request: Request,
|
|
208
|
+
body: ReviewFilesRequest,
|
|
209
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
210
|
+
) -> ReviewSummaryResponse:
|
|
211
|
+
"""Run code review and return summary only.
|
|
212
|
+
|
|
213
|
+
Similar to review_files but returns a condensed summary without
|
|
214
|
+
individual findings. Useful for quick status checks.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
request: HTTP request for rate limiting
|
|
218
|
+
body: ReviewFilesRequest with file list
|
|
219
|
+
workspace: v2 Workspace
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
ReviewSummaryResponse with aggregated metrics
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
result = review.review_files(workspace, body.files)
|
|
226
|
+
summary = review.get_review_summary(result)
|
|
227
|
+
|
|
228
|
+
return ReviewSummaryResponse(
|
|
229
|
+
status=summary["status"],
|
|
230
|
+
overall_score=summary["overall_score"],
|
|
231
|
+
total_findings=summary["total_findings"],
|
|
232
|
+
severity_counts=summary["severity_counts"],
|
|
233
|
+
summary=summary["summary"],
|
|
234
|
+
has_blocking_issues=summary["has_blocking_issues"],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Failed to get review summary: {e}", exc_info=True)
|
|
239
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ============================================================================
|
|
243
|
+
# Diff & Patch Endpoints (for Review & Commit View)
|
|
244
|
+
# ============================================================================
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@router.get("/diff", response_model=DiffStatsResponse)
|
|
248
|
+
@rate_limit_standard()
|
|
249
|
+
async def get_review_diff(
|
|
250
|
+
request: Request,
|
|
251
|
+
staged: bool = False,
|
|
252
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
253
|
+
) -> DiffStatsResponse:
|
|
254
|
+
"""Get unified diff with parsed statistics.
|
|
255
|
+
|
|
256
|
+
Returns the raw diff plus per-file change counts for display
|
|
257
|
+
in the Review & Commit View.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
request: HTTP request for rate limiting
|
|
261
|
+
staged: If True, show staged changes; if False, show unstaged
|
|
262
|
+
workspace: v2 Workspace
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
DiffStatsResponse with diff text and statistics
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
stats = git.get_diff_stats(workspace, staged=staged)
|
|
269
|
+
|
|
270
|
+
return DiffStatsResponse(
|
|
271
|
+
diff=stats.diff,
|
|
272
|
+
files_changed=stats.files_changed,
|
|
273
|
+
insertions=stats.insertions,
|
|
274
|
+
deletions=stats.deletions,
|
|
275
|
+
changed_files=[
|
|
276
|
+
FileChangeResponse(
|
|
277
|
+
path=f.path,
|
|
278
|
+
change_type=f.change_type,
|
|
279
|
+
insertions=f.insertions,
|
|
280
|
+
deletions=f.deletions,
|
|
281
|
+
)
|
|
282
|
+
for f in stats.changed_files
|
|
283
|
+
],
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
except ValueError as e:
|
|
287
|
+
logger.error(f"Get review diff failed: {e}")
|
|
288
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Failed to get review diff: {e}", exc_info=True)
|
|
291
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@router.get("/patch", response_model=PatchResponse)
|
|
295
|
+
@rate_limit_standard()
|
|
296
|
+
async def get_review_patch(
|
|
297
|
+
request: Request,
|
|
298
|
+
staged: bool = False,
|
|
299
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
300
|
+
) -> PatchResponse:
|
|
301
|
+
"""Get patch-formatted diff for export.
|
|
302
|
+
|
|
303
|
+
Returns the diff in patch format suitable for `git apply`.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
request: HTTP request for rate limiting
|
|
307
|
+
staged: If True, show staged changes; if False, show unstaged
|
|
308
|
+
workspace: v2 Workspace
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
PatchResponse with patch content and suggested filename
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
patch_content = git.get_patch(workspace, staged=staged)
|
|
315
|
+
branch = git.get_current_branch(workspace)
|
|
316
|
+
filename = f"{branch.replace('/', '-')}.patch"
|
|
317
|
+
|
|
318
|
+
return PatchResponse(
|
|
319
|
+
patch=patch_content,
|
|
320
|
+
filename=filename,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
except ValueError as e:
|
|
324
|
+
logger.error(f"Get patch failed: {e}")
|
|
325
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"Failed to get patch: {e}", exc_info=True)
|
|
328
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@router.post("/commit-message", response_model=CommitMessageResponse)
|
|
332
|
+
@rate_limit_standard()
|
|
333
|
+
async def generate_commit_message(
|
|
334
|
+
request: Request,
|
|
335
|
+
staged: bool = False,
|
|
336
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
337
|
+
) -> CommitMessageResponse:
|
|
338
|
+
"""Generate a commit message from the current diff.
|
|
339
|
+
|
|
340
|
+
Analyzes changed files to suggest a conventional commit message.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
request: HTTP request for rate limiting
|
|
344
|
+
staged: If True, analyze staged changes; if False, unstaged
|
|
345
|
+
workspace: v2 Workspace
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
CommitMessageResponse with suggested message
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
message = git.generate_commit_message(workspace, staged=staged)
|
|
352
|
+
|
|
353
|
+
return CommitMessageResponse(message=message)
|
|
354
|
+
|
|
355
|
+
except ValueError as e:
|
|
356
|
+
logger.error(f"Generate commit message failed: {e}")
|
|
357
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Failed to generate commit message: {e}", exc_info=True)
|
|
360
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""V2 Schedule router - delegates to core modules.
|
|
2
|
+
|
|
3
|
+
This module provides v2-style API endpoints for schedule management that
|
|
4
|
+
delegate to core/schedule.py. It uses the v2 Workspace model.
|
|
5
|
+
|
|
6
|
+
The v1 router (schedule.py) remains for backwards compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from codeframe.core.workspace import Workspace
|
|
16
|
+
from codeframe.lib.rate_limiter import rate_limit_standard
|
|
17
|
+
from codeframe.core import schedule
|
|
18
|
+
from codeframe.ui.dependencies import get_v2_workspace
|
|
19
|
+
from codeframe.ui.response_models import api_error, ErrorCodes
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/api/v2/schedule", tags=["schedule-v2"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Request/Response Models
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TaskAssignmentResponse(BaseModel):
|
|
32
|
+
"""Response model for a single task assignment."""
|
|
33
|
+
|
|
34
|
+
task_id: str
|
|
35
|
+
title: str
|
|
36
|
+
start_time: float
|
|
37
|
+
end_time: float
|
|
38
|
+
assigned_agent: Optional[int] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScheduleResponse(BaseModel):
|
|
42
|
+
"""Response model for project schedule."""
|
|
43
|
+
|
|
44
|
+
task_assignments: list[TaskAssignmentResponse]
|
|
45
|
+
total_duration: float
|
|
46
|
+
agents_used: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CompletionPredictionResponse(BaseModel):
|
|
50
|
+
"""Response model for completion prediction."""
|
|
51
|
+
|
|
52
|
+
predicted_date: str
|
|
53
|
+
remaining_hours: float
|
|
54
|
+
completed_percentage: float
|
|
55
|
+
confidence_interval: dict[str, str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BottleneckResponse(BaseModel):
|
|
59
|
+
"""Response model for a scheduling bottleneck."""
|
|
60
|
+
|
|
61
|
+
task_id: str
|
|
62
|
+
task_title: str
|
|
63
|
+
bottleneck_type: str
|
|
64
|
+
impact_hours: float
|
|
65
|
+
recommendation: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# Schedule Endpoints
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get("", response_model=ScheduleResponse)
|
|
74
|
+
@rate_limit_standard()
|
|
75
|
+
async def get_schedule(
|
|
76
|
+
request: Request,
|
|
77
|
+
agents: int = Query(1, ge=1, le=10, description="Number of parallel agents/workers"),
|
|
78
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
79
|
+
) -> ScheduleResponse:
|
|
80
|
+
"""Get the schedule for a workspace.
|
|
81
|
+
|
|
82
|
+
Uses Critical Path Method to assign start/end times while
|
|
83
|
+
respecting dependency constraints and agent availability.
|
|
84
|
+
|
|
85
|
+
This is the v2 equivalent of `cf schedule show`.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
agents: Number of parallel agents/workers (default: 1)
|
|
89
|
+
workspace: v2 Workspace
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Schedule with task assignments
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
result = schedule.get_schedule(workspace, agents=agents)
|
|
96
|
+
|
|
97
|
+
return ScheduleResponse(
|
|
98
|
+
task_assignments=[
|
|
99
|
+
TaskAssignmentResponse(
|
|
100
|
+
task_id=a.task_id,
|
|
101
|
+
title=a.title,
|
|
102
|
+
start_time=a.start_time,
|
|
103
|
+
end_time=a.end_time,
|
|
104
|
+
assigned_agent=a.assigned_agent,
|
|
105
|
+
)
|
|
106
|
+
for a in result.task_assignments
|
|
107
|
+
],
|
|
108
|
+
total_duration=result.total_duration,
|
|
109
|
+
agents_used=result.agents_used,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=404,
|
|
115
|
+
detail=api_error("Schedule not found", ErrorCodes.NOT_FOUND, str(e)),
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Failed to get schedule: {e}", exc_info=True)
|
|
119
|
+
raise HTTPException(
|
|
120
|
+
status_code=500,
|
|
121
|
+
detail=api_error("Failed to get schedule", ErrorCodes.INTERNAL_ERROR, str(e)),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.get("/predict", response_model=CompletionPredictionResponse)
|
|
126
|
+
@rate_limit_standard()
|
|
127
|
+
async def predict_completion(
|
|
128
|
+
request: Request,
|
|
129
|
+
hours_per_day: float = Query(8.0, gt=0, le=24, description="Working hours per day"),
|
|
130
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
131
|
+
) -> CompletionPredictionResponse:
|
|
132
|
+
"""Predict project completion date.
|
|
133
|
+
|
|
134
|
+
This is the v2 equivalent of `cf schedule predict`.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
hours_per_day: Working hours per day (default: 8)
|
|
138
|
+
workspace: v2 Workspace
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Completion prediction with confidence interval
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
result = schedule.predict_completion(workspace, hours_per_day=hours_per_day)
|
|
145
|
+
|
|
146
|
+
return CompletionPredictionResponse(
|
|
147
|
+
predicted_date=result.predicted_date.isoformat(),
|
|
148
|
+
remaining_hours=result.remaining_hours,
|
|
149
|
+
completed_percentage=result.completed_percentage,
|
|
150
|
+
confidence_interval={
|
|
151
|
+
"early": result.confidence_early.isoformat(),
|
|
152
|
+
"late": result.confidence_late.isoformat(),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except ValueError as e:
|
|
157
|
+
raise HTTPException(
|
|
158
|
+
status_code=404,
|
|
159
|
+
detail=api_error("Prediction failed", ErrorCodes.NOT_FOUND, str(e)),
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Failed to predict completion: {e}", exc_info=True)
|
|
163
|
+
raise HTTPException(
|
|
164
|
+
status_code=500,
|
|
165
|
+
detail=api_error("Failed to predict completion", ErrorCodes.INTERNAL_ERROR, str(e)),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.get("/bottlenecks", response_model=list[BottleneckResponse])
|
|
170
|
+
@rate_limit_standard()
|
|
171
|
+
async def get_bottlenecks(
|
|
172
|
+
request: Request,
|
|
173
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
174
|
+
) -> list[BottleneckResponse]:
|
|
175
|
+
"""Identify scheduling bottlenecks for a workspace.
|
|
176
|
+
|
|
177
|
+
Identifies:
|
|
178
|
+
- Long duration tasks on critical path
|
|
179
|
+
- Tasks with many dependents causing delays
|
|
180
|
+
- Resource constraints limiting parallelization
|
|
181
|
+
|
|
182
|
+
This is the v2 equivalent of `cf schedule bottlenecks`.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
workspace: v2 Workspace
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of identified bottlenecks with recommendations
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
bottlenecks = schedule.get_bottlenecks(workspace)
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
BottleneckResponse(
|
|
195
|
+
task_id=bn.task_id,
|
|
196
|
+
task_title=bn.task_title,
|
|
197
|
+
bottleneck_type=bn.bottleneck_type,
|
|
198
|
+
impact_hours=bn.impact_hours,
|
|
199
|
+
recommendation=bn.recommendation,
|
|
200
|
+
)
|
|
201
|
+
for bn in bottlenecks
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
raise HTTPException(
|
|
206
|
+
status_code=404,
|
|
207
|
+
detail=api_error("Bottlenecks not found", ErrorCodes.NOT_FOUND, str(e)),
|
|
208
|
+
)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Failed to get bottlenecks: {e}", exc_info=True)
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=500,
|
|
213
|
+
detail=api_error("Failed to get bottlenecks", ErrorCodes.INTERNAL_ERROR, str(e)),
|
|
214
|
+
)
|