draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Service for generating and managing merge readiness checklists."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
from sqlalchemy.orm import selectinload
|
|
10
|
+
|
|
11
|
+
from app.exceptions import ResourceNotFoundError
|
|
12
|
+
from app.models.evidence import Evidence
|
|
13
|
+
from app.models.job import Job
|
|
14
|
+
from app.models.merge_checklist import MergeChecklist
|
|
15
|
+
from app.models.ticket import Ticket
|
|
16
|
+
from app.state_machine import JobStatus, TicketState
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MergeChecklistService:
|
|
22
|
+
"""Generate and track merge readiness checklist."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, db: AsyncSession):
|
|
25
|
+
self.db = db
|
|
26
|
+
|
|
27
|
+
async def generate_or_update_checklist(self, goal_id: str) -> MergeChecklist:
|
|
28
|
+
"""Generate or update the merge checklist for a goal.
|
|
29
|
+
|
|
30
|
+
Automatically checks:
|
|
31
|
+
- All tests passed
|
|
32
|
+
- Files/lines changed count
|
|
33
|
+
- Total cost
|
|
34
|
+
- Budget status
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
MergeChecklist instance
|
|
38
|
+
"""
|
|
39
|
+
# Get existing checklist or create new
|
|
40
|
+
result = await self.db.execute(
|
|
41
|
+
select(MergeChecklist).where(MergeChecklist.goal_id == goal_id)
|
|
42
|
+
)
|
|
43
|
+
checklist = result.scalar_one_or_none()
|
|
44
|
+
|
|
45
|
+
if not checklist:
|
|
46
|
+
checklist = MergeChecklist(goal_id=goal_id)
|
|
47
|
+
self.db.add(checklist)
|
|
48
|
+
|
|
49
|
+
# Run automatic checks
|
|
50
|
+
await self._update_auto_checks(checklist, goal_id)
|
|
51
|
+
|
|
52
|
+
# Generate rollback plan if not exists
|
|
53
|
+
if not checklist.rollback_plan_json:
|
|
54
|
+
rollback_plan = await self._generate_rollback_plan(goal_id)
|
|
55
|
+
checklist.rollback_plan_json = json.dumps(rollback_plan)
|
|
56
|
+
checklist.risk_level = rollback_plan["risk_level"]
|
|
57
|
+
|
|
58
|
+
# Update ready status
|
|
59
|
+
checklist.ready_to_merge = checklist.is_ready_to_merge()
|
|
60
|
+
|
|
61
|
+
await self.db.flush()
|
|
62
|
+
await self.db.refresh(checklist)
|
|
63
|
+
|
|
64
|
+
return checklist
|
|
65
|
+
|
|
66
|
+
async def _update_auto_checks(self, checklist: MergeChecklist, goal_id: str):
|
|
67
|
+
"""Update automatic checks by querying system state."""
|
|
68
|
+
# Get all tickets for this goal
|
|
69
|
+
result = await self.db.execute(
|
|
70
|
+
select(Ticket)
|
|
71
|
+
.where(Ticket.goal_id == goal_id)
|
|
72
|
+
.options(
|
|
73
|
+
selectinload(Ticket.jobs).selectinload(Job.evidence),
|
|
74
|
+
selectinload(Ticket.revisions),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
tickets = list(result.scalars().all())
|
|
78
|
+
|
|
79
|
+
# Check 1: All tests passed
|
|
80
|
+
checklist.all_tests_passed = await self._check_all_tests_passed(tickets)
|
|
81
|
+
|
|
82
|
+
# Check 2: Count files and lines changed
|
|
83
|
+
file_stats = await self._count_changes(tickets)
|
|
84
|
+
checklist.total_files_changed = file_stats["files"]
|
|
85
|
+
checklist.total_lines_changed = file_stats["lines"]
|
|
86
|
+
|
|
87
|
+
# Check 3: Calculate total cost
|
|
88
|
+
checklist.total_cost_usd = await self._calculate_cost(goal_id)
|
|
89
|
+
|
|
90
|
+
# Check 4: Check budget
|
|
91
|
+
checklist.budget_exceeded = await self._check_budget_exceeded(goal_id)
|
|
92
|
+
|
|
93
|
+
async def _check_all_tests_passed(self, tickets: list[Ticket]) -> bool:
|
|
94
|
+
"""Check if all verification jobs succeeded."""
|
|
95
|
+
for ticket in tickets:
|
|
96
|
+
# Skip tickets that aren't done yet
|
|
97
|
+
if ticket.state != TicketState.DONE.value:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Find most recent verify job
|
|
101
|
+
verify_jobs = [j for j in ticket.jobs if j.job_type == "verify"]
|
|
102
|
+
if not verify_jobs:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
latest_verify = max(verify_jobs, key=lambda j: j.created_at)
|
|
106
|
+
if latest_verify.status != JobStatus.SUCCEEDED.value:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
async def _count_changes(self, tickets: list[Ticket]) -> dict[str, int]:
|
|
112
|
+
"""Count total files and lines changed across all tickets."""
|
|
113
|
+
total_files = set()
|
|
114
|
+
total_lines = 0
|
|
115
|
+
|
|
116
|
+
for ticket in tickets:
|
|
117
|
+
# Get diffs from revisions
|
|
118
|
+
for revision in ticket.revisions:
|
|
119
|
+
if revision.diff_stat_evidence_id:
|
|
120
|
+
# Parse diff stat from evidence
|
|
121
|
+
stat_result = await self.db.execute(
|
|
122
|
+
select(Evidence).where(
|
|
123
|
+
Evidence.id == revision.diff_stat_evidence_id
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
stat_evidence = stat_result.scalar_one_or_none()
|
|
127
|
+
|
|
128
|
+
if stat_evidence and stat_evidence.stdout_path:
|
|
129
|
+
# Parse diff stat (format: "X files changed, Y insertions(+), Z deletions(-)")
|
|
130
|
+
try:
|
|
131
|
+
with open(stat_evidence.stdout_path) as f:
|
|
132
|
+
stat_line = f.read().strip()
|
|
133
|
+
# Simple parsing
|
|
134
|
+
if (
|
|
135
|
+
"files changed" in stat_line
|
|
136
|
+
or "file changed" in stat_line
|
|
137
|
+
):
|
|
138
|
+
parts = stat_line.split(",")
|
|
139
|
+
files_part = parts[0].strip().split()[0]
|
|
140
|
+
total_files.add(f"{ticket.id}:{files_part}")
|
|
141
|
+
|
|
142
|
+
# Count insertions and deletions
|
|
143
|
+
for part in parts[1:]:
|
|
144
|
+
if "insertion" in part or "deletion" in part:
|
|
145
|
+
count = int(part.strip().split()[0])
|
|
146
|
+
total_lines += count
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.warning(f"Failed to parse diff stat: {e}")
|
|
149
|
+
|
|
150
|
+
return {"files": len(total_files), "lines": total_lines}
|
|
151
|
+
|
|
152
|
+
async def _calculate_cost(self, goal_id: str) -> float:
|
|
153
|
+
"""Calculate total LLM API cost for all tickets."""
|
|
154
|
+
# TODO: Aggregate from agent_sessions table
|
|
155
|
+
return 0.0
|
|
156
|
+
|
|
157
|
+
async def _check_budget_exceeded(self, goal_id: str) -> bool:
|
|
158
|
+
"""Check if spending exceeded budget."""
|
|
159
|
+
# TODO: Check against cost_budget table
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
async def _generate_rollback_plan(self, goal_id: str) -> dict[str, Any]:
|
|
163
|
+
"""Generate rollback plan for all changes."""
|
|
164
|
+
result = await self.db.execute(
|
|
165
|
+
select(Ticket)
|
|
166
|
+
.where(Ticket.goal_id == goal_id)
|
|
167
|
+
.where(Ticket.state == TicketState.DONE.value)
|
|
168
|
+
)
|
|
169
|
+
tickets = list(result.scalars().all())
|
|
170
|
+
|
|
171
|
+
steps = []
|
|
172
|
+
|
|
173
|
+
# Step 1: Git revert for all merged changes
|
|
174
|
+
if tickets:
|
|
175
|
+
steps.append(
|
|
176
|
+
{
|
|
177
|
+
"order": 1,
|
|
178
|
+
"type": "git",
|
|
179
|
+
"description": f"Revert all commits for {len(tickets)} tickets",
|
|
180
|
+
"command": "git log --grep='ticket_id' --oneline | awk '{print $1}' | xargs git revert --no-commit",
|
|
181
|
+
"is_automated": True,
|
|
182
|
+
"risk": "low",
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Step 2: Check for database migrations
|
|
187
|
+
has_migrations = False # TODO: Detect if any ticket modified migrations
|
|
188
|
+
if has_migrations:
|
|
189
|
+
steps.append(
|
|
190
|
+
{
|
|
191
|
+
"order": 2,
|
|
192
|
+
"type": "migration",
|
|
193
|
+
"description": "Rollback database migrations",
|
|
194
|
+
"command": "alembic downgrade -1",
|
|
195
|
+
"is_automated": False,
|
|
196
|
+
"risk": "high",
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Step 3: Cache invalidation
|
|
201
|
+
steps.append(
|
|
202
|
+
{
|
|
203
|
+
"order": 3,
|
|
204
|
+
"type": "cache",
|
|
205
|
+
"description": "Clear application caches",
|
|
206
|
+
"command": "redis-cli FLUSHDB",
|
|
207
|
+
"is_automated": True,
|
|
208
|
+
"risk": "low",
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Assess overall risk
|
|
213
|
+
risk_level = "high" if has_migrations else "low"
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"steps": steps,
|
|
217
|
+
"risk_level": risk_level,
|
|
218
|
+
"estimated_time": "5-10 minutes",
|
|
219
|
+
"requires_human": any(s["risk"] == "high" for s in steps),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async def update_manual_check(
|
|
223
|
+
self, checklist_id: str, check_name: str, value: bool
|
|
224
|
+
) -> MergeChecklist:
|
|
225
|
+
"""Update a manual checklist item.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
checklist_id: Checklist ID
|
|
229
|
+
check_name: Name of check (code_reviewed, no_sensitive_data, etc.)
|
|
230
|
+
value: New value
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Updated checklist
|
|
234
|
+
"""
|
|
235
|
+
result = await self.db.execute(
|
|
236
|
+
select(MergeChecklist).where(MergeChecklist.id == checklist_id)
|
|
237
|
+
)
|
|
238
|
+
checklist = result.scalar_one_or_none()
|
|
239
|
+
|
|
240
|
+
if not checklist:
|
|
241
|
+
raise ResourceNotFoundError("MergeChecklist", checklist_id)
|
|
242
|
+
|
|
243
|
+
# Update the field
|
|
244
|
+
if hasattr(checklist, check_name):
|
|
245
|
+
setattr(checklist, check_name, value)
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError(f"Unknown check: {check_name}")
|
|
248
|
+
|
|
249
|
+
# Recalculate ready status
|
|
250
|
+
checklist.ready_to_merge = checklist.is_ready_to_merge()
|
|
251
|
+
|
|
252
|
+
await self.db.flush()
|
|
253
|
+
await self.db.refresh(checklist)
|
|
254
|
+
|
|
255
|
+
return checklist
|
|
256
|
+
|
|
257
|
+
async def get_checklist_by_goal(self, goal_id: str) -> MergeChecklist | None:
|
|
258
|
+
"""Get checklist for a goal."""
|
|
259
|
+
result = await self.db.execute(
|
|
260
|
+
select(MergeChecklist)
|
|
261
|
+
.where(MergeChecklist.goal_id == goal_id)
|
|
262
|
+
.options(selectinload(MergeChecklist.goal))
|
|
263
|
+
)
|
|
264
|
+
return result.scalar_one_or_none()
|