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,39 @@
|
|
|
1
|
+
"""SQLite-backed job queue model (replaces Celery/Redis broker)."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import DateTime, Index, Integer, String, Text, func
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from app.models.base import Base
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JobQueueEntry(Base):
|
|
12
|
+
"""A queued task for the SQLite worker to process.
|
|
13
|
+
|
|
14
|
+
Replaces the Celery broker queue. Tasks are claimed atomically
|
|
15
|
+
via UPDATE...WHERE status='pending' ORDER BY priority DESC, created_at ASC.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "job_queue"
|
|
19
|
+
__table_args__ = (
|
|
20
|
+
Index("ix_job_queue_claim_order", "status", "priority", "created_at"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
24
|
+
task_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
25
|
+
args_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
26
|
+
status: Mapped[str] = mapped_column(
|
|
27
|
+
String(20), nullable=False, default="pending", index=True
|
|
28
|
+
)
|
|
29
|
+
claimed_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
30
|
+
claimed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
31
|
+
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
32
|
+
result_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
33
|
+
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
34
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
35
|
+
DateTime, nullable=False, server_default=func.now()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"<JobQueueEntry(id={self.id}, task={self.task_name}, status={self.status})>"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""SQLite-backed key-value store model (replaces Redis GET/SET)."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import DateTime, String, Text, func
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from app.models.base import Base
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KVStoreEntry(Base):
|
|
12
|
+
"""Generic key-value store with optional TTL.
|
|
13
|
+
|
|
14
|
+
Replaces Redis GET/SET for queued messages and follow-up prompts.
|
|
15
|
+
Expired entries are cleaned up inline on access.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "kv_store"
|
|
19
|
+
|
|
20
|
+
key: Mapped[str] = mapped_column(String(512), primary_key=True)
|
|
21
|
+
value: Mapped[str] = mapped_column(Text, nullable=False)
|
|
22
|
+
expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
23
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
24
|
+
DateTime, nullable=False, server_default=func.now()
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"<KVStoreEntry(key={self.key[:30]}...)>"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Merge checklist model for tracking merge readiness."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from app.models.base import Base
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from app.models.goal import Goal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MergeChecklist(Base):
|
|
17
|
+
"""Tracks merge readiness for a goal's tickets.
|
|
18
|
+
|
|
19
|
+
Combines automatic checks (tests, cost) and manual checks (review, security).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__tablename__ = "merge_checklists"
|
|
23
|
+
|
|
24
|
+
id: Mapped[str] = mapped_column(
|
|
25
|
+
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
|
26
|
+
)
|
|
27
|
+
goal_id: Mapped[str] = mapped_column(
|
|
28
|
+
String(36),
|
|
29
|
+
ForeignKey("goals.id", ondelete="CASCADE"),
|
|
30
|
+
nullable=False,
|
|
31
|
+
index=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Auto-checks (computed from system state)
|
|
35
|
+
all_tests_passed: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
36
|
+
total_files_changed: Mapped[int] = mapped_column(Integer, default=0)
|
|
37
|
+
total_lines_changed: Mapped[int] = mapped_column(Integer, default=0)
|
|
38
|
+
total_cost_usd: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
39
|
+
budget_exceeded: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
40
|
+
|
|
41
|
+
# Manual checks (require human confirmation)
|
|
42
|
+
code_reviewed: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
43
|
+
no_sensitive_data: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
44
|
+
rollback_plan_understood: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
45
|
+
documentation_updated: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
46
|
+
|
|
47
|
+
# Rollback plan
|
|
48
|
+
rollback_plan_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
49
|
+
risk_level: Mapped[str] = mapped_column(
|
|
50
|
+
String(20),
|
|
51
|
+
default="low", # low, medium, high
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Status
|
|
55
|
+
ready_to_merge: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
56
|
+
merged_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
57
|
+
|
|
58
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
59
|
+
DateTime, server_default=func.now(), nullable=False
|
|
60
|
+
)
|
|
61
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
62
|
+
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Relationships
|
|
66
|
+
goal: Mapped["Goal"] = relationship("Goal", back_populates="merge_checklist")
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f"<MergeChecklist(id={self.id}, goal_id={self.goal_id}, ready={self.ready_to_merge})>"
|
|
70
|
+
|
|
71
|
+
def is_ready_to_merge(self) -> bool:
|
|
72
|
+
"""Check if all conditions are met for merging."""
|
|
73
|
+
# All auto-checks must pass
|
|
74
|
+
if not self.all_tests_passed:
|
|
75
|
+
return False
|
|
76
|
+
if self.budget_exceeded:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# All manual checks must be confirmed
|
|
80
|
+
if not (
|
|
81
|
+
self.code_reviewed
|
|
82
|
+
and self.no_sensitive_data
|
|
83
|
+
and self.rollback_plan_understood
|
|
84
|
+
):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return True
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Database models for normalized log entries."""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
JSON,
|
|
8
|
+
Boolean,
|
|
9
|
+
Column,
|
|
10
|
+
DateTime,
|
|
11
|
+
Enum,
|
|
12
|
+
ForeignKey,
|
|
13
|
+
Index,
|
|
14
|
+
Integer,
|
|
15
|
+
String,
|
|
16
|
+
Text,
|
|
17
|
+
)
|
|
18
|
+
from sqlalchemy.orm import relationship
|
|
19
|
+
|
|
20
|
+
from app.models.base import Base
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LogEntryType(enum.StrEnum):
|
|
24
|
+
"""Semantic types for normalized log entries."""
|
|
25
|
+
|
|
26
|
+
THINKING = "thinking"
|
|
27
|
+
ASSISTANT_MESSAGE = "assistant_message" # Agent's response/reasoning
|
|
28
|
+
FILE_EDIT = "file_edit"
|
|
29
|
+
FILE_CREATE = "file_create"
|
|
30
|
+
FILE_DELETE = "file_delete"
|
|
31
|
+
COMMAND_RUN = "command_run"
|
|
32
|
+
TOOL_CALL = "tool_call"
|
|
33
|
+
ERROR = "error"
|
|
34
|
+
USER_MESSAGE = "user_message"
|
|
35
|
+
SYSTEM_MESSAGE = "system_message"
|
|
36
|
+
LOADING = "loading"
|
|
37
|
+
TODO_LIST = "todo_list" # Agent's todo/task list
|
|
38
|
+
|
|
39
|
+
# UDAR agent entry types
|
|
40
|
+
AGENT_UNDERSTANDING = "agent_understanding" # Context gathered in Understand phase
|
|
41
|
+
AGENT_DECISION = "agent_decision" # LLM reasoning in Decide phase
|
|
42
|
+
AGENT_VALIDATION = "agent_validation" # Validation results in Validate phase
|
|
43
|
+
AGENT_TOOL_CALL = "agent_tool_call" # Tool invocations (deterministic)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NormalizedLogEntry(Base):
|
|
47
|
+
"""Structured, semantic log entry parsed from raw agent output."""
|
|
48
|
+
|
|
49
|
+
__tablename__ = "normalized_log_entries"
|
|
50
|
+
|
|
51
|
+
id = Column(
|
|
52
|
+
String(36), primary_key=True, default=lambda: str(__import__("uuid").uuid4())
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Foreign key to job
|
|
56
|
+
job_id = Column(String, ForeignKey("jobs.id"), nullable=False, index=True)
|
|
57
|
+
job = relationship("Job", back_populates="normalized_logs")
|
|
58
|
+
|
|
59
|
+
# Sequence number for ordering
|
|
60
|
+
sequence = Column(Integer, nullable=False)
|
|
61
|
+
|
|
62
|
+
# Timestamp of the entry
|
|
63
|
+
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
|
|
64
|
+
|
|
65
|
+
# Entry type (semantic category)
|
|
66
|
+
entry_type = Column(Enum(LogEntryType), nullable=False, index=True)
|
|
67
|
+
|
|
68
|
+
# Text content (can be markdown, code, etc.)
|
|
69
|
+
content = Column(Text, nullable=False)
|
|
70
|
+
|
|
71
|
+
# Structured metadata (JSON)
|
|
72
|
+
# Examples:
|
|
73
|
+
# - file_edit: {"file_path": "app/auth.py", "diff": "...", "language": "python"}
|
|
74
|
+
# - command_run: {"command": "pytest", "exit_code": 0, "output": "..."}
|
|
75
|
+
# - tool_call: {"tool_name": "web_search", "args": {...}, "result": {...}}
|
|
76
|
+
# - error: {"error_type": "SyntaxError", "traceback": "..."}
|
|
77
|
+
# Note: "metadata" is reserved in SQLAlchemy, so using "entry_metadata"
|
|
78
|
+
entry_metadata = Column(JSON, nullable=True)
|
|
79
|
+
|
|
80
|
+
# Display flags
|
|
81
|
+
collapsed = Column(Boolean, default=False) # Start collapsed?
|
|
82
|
+
highlight = Column(Boolean, default=False) # Highlight in UI?
|
|
83
|
+
|
|
84
|
+
__table_args__ = (
|
|
85
|
+
Index("ix_normalized_log_entries_job_sequence", "job_id", "sequence"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def to_dict(self):
|
|
89
|
+
"""Convert to dictionary for API responses."""
|
|
90
|
+
return {
|
|
91
|
+
"id": self.id,
|
|
92
|
+
"job_id": self.job_id,
|
|
93
|
+
"sequence": self.sequence,
|
|
94
|
+
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
|
95
|
+
"entry_type": self.entry_type.value if self.entry_type else None,
|
|
96
|
+
"content": self.content,
|
|
97
|
+
"metadata": self.entry_metadata or {}, # Return as "metadata" for frontend
|
|
98
|
+
"collapsed": self.collapsed,
|
|
99
|
+
"highlight": self.highlight,
|
|
100
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Planner lock model for ensuring single-tick execution."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import DateTime, String, func
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from app.models.base import Base
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlannerLock(Base):
|
|
12
|
+
"""Single-row lock table to prevent concurrent planner ticks.
|
|
13
|
+
|
|
14
|
+
This ensures that only one planner tick can run at a time,
|
|
15
|
+
preventing race conditions where two ticks might both see
|
|
16
|
+
"no executing ticket" and both enqueue jobs.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
- Acquire: INSERT with lock_key="planner_tick"
|
|
20
|
+
- Release: DELETE the row
|
|
21
|
+
- Check: SELECT to see if lock is held
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__tablename__ = "planner_locks"
|
|
25
|
+
|
|
26
|
+
# Fixed key for the planner lock
|
|
27
|
+
lock_key: Mapped[str] = mapped_column(
|
|
28
|
+
String(50),
|
|
29
|
+
primary_key=True,
|
|
30
|
+
default="planner_tick",
|
|
31
|
+
)
|
|
32
|
+
acquired_at: Mapped[datetime] = mapped_column(
|
|
33
|
+
DateTime,
|
|
34
|
+
server_default=func.now(),
|
|
35
|
+
nullable=False,
|
|
36
|
+
)
|
|
37
|
+
owner_id: Mapped[str | None] = mapped_column(
|
|
38
|
+
String(255),
|
|
39
|
+
nullable=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"<PlannerLock(key={self.lock_key}, acquired={self.acquired_at})>"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""SQLite-backed rate limit entry model (replaces Redis sorted sets)."""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Float, Integer, String
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
|
|
6
|
+
from app.models.base import Base
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RateLimitEntry(Base):
|
|
10
|
+
"""A rate limit cost entry.
|
|
11
|
+
|
|
12
|
+
Replaces Redis sorted sets. Entries are cleaned up inline
|
|
13
|
+
(DELETE WHERE expires_at < now) before checking budget.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "rate_limit_entries"
|
|
17
|
+
|
|
18
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
19
|
+
client_key: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
20
|
+
cost: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
21
|
+
recorded_at: Mapped[float] = mapped_column(Float, nullable=False)
|
|
22
|
+
expires_at: Mapped[float] = mapped_column(Float, nullable=False, index=True)
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"<RateLimitEntry(key={self.client_key}, cost={self.cost})>"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Repo model - global repository registry."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import DateTime, String, Text, func
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
8
|
+
|
|
9
|
+
from app.models.base import Base
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from app.models.board_repo import BoardRepo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Repo(Base):
|
|
16
|
+
"""Global repository registry.
|
|
17
|
+
|
|
18
|
+
Each Repo represents a git repository on the filesystem.
|
|
19
|
+
Repos can be associated with multiple boards via BoardRepo junction table.
|
|
20
|
+
|
|
21
|
+
Key properties:
|
|
22
|
+
- path is unique - only one Repo per filesystem path
|
|
23
|
+
- display_name is user-friendly name for UI
|
|
24
|
+
- Optional scripts for setup/cleanup/dev server
|
|
25
|
+
- Git metadata (default_branch, remote_url) cached for performance
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__tablename__ = "repos"
|
|
29
|
+
|
|
30
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
31
|
+
|
|
32
|
+
# Filesystem path to git repository (unique)
|
|
33
|
+
path: Mapped[str] = mapped_column(
|
|
34
|
+
String(1024), unique=True, nullable=False, index=True
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Repository name (derived from path, e.g., "my-project")
|
|
38
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
39
|
+
|
|
40
|
+
# User-friendly display name (editable)
|
|
41
|
+
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
42
|
+
|
|
43
|
+
# Optional per-repo scripts
|
|
44
|
+
setup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
45
|
+
cleanup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
46
|
+
dev_server_script: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
47
|
+
|
|
48
|
+
# Git metadata (cached)
|
|
49
|
+
default_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
50
|
+
remote_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
|
51
|
+
|
|
52
|
+
# Timestamps
|
|
53
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
54
|
+
DateTime(timezone=True), server_default=func.now()
|
|
55
|
+
)
|
|
56
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
57
|
+
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Relationships
|
|
61
|
+
board_repos: Mapped[list["BoardRepo"]] = relationship(
|
|
62
|
+
"BoardRepo", back_populates="repo", cascade="all, delete-orphan"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return f"<Repo(id={self.id}, name={self.name}, path={self.path})>"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""ReviewComment model for inline comments on revision diffs."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
10
|
+
|
|
11
|
+
from app.models.base import Base
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from app.models.revision import Revision
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthorType(StrEnum):
|
|
18
|
+
"""Enum representing the type of author for a review comment."""
|
|
19
|
+
|
|
20
|
+
HUMAN = "human"
|
|
21
|
+
AGENT = "agent"
|
|
22
|
+
SYSTEM = "system"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReviewComment(Base):
|
|
26
|
+
"""ReviewComment model representing inline comments on a revision diff.
|
|
27
|
+
|
|
28
|
+
Comments are anchored using a sha1 hash of (file_path + hunk_header + line_content)
|
|
29
|
+
to survive small line shifts between revisions.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__tablename__ = "review_comments"
|
|
33
|
+
|
|
34
|
+
id: Mapped[str] = mapped_column(
|
|
35
|
+
String(36),
|
|
36
|
+
primary_key=True,
|
|
37
|
+
default=lambda: str(uuid.uuid4()),
|
|
38
|
+
)
|
|
39
|
+
revision_id: Mapped[str] = mapped_column(
|
|
40
|
+
String(36),
|
|
41
|
+
ForeignKey("revisions.id", ondelete="CASCADE"),
|
|
42
|
+
nullable=False,
|
|
43
|
+
index=True,
|
|
44
|
+
)
|
|
45
|
+
file_path: Mapped[str] = mapped_column(
|
|
46
|
+
String(500),
|
|
47
|
+
nullable=False,
|
|
48
|
+
)
|
|
49
|
+
line_number: Mapped[int] = mapped_column(
|
|
50
|
+
Integer,
|
|
51
|
+
nullable=False,
|
|
52
|
+
)
|
|
53
|
+
anchor: Mapped[str] = mapped_column(
|
|
54
|
+
String(40), # sha1 hex digest (truncated to 16 chars in practice)
|
|
55
|
+
nullable=False,
|
|
56
|
+
index=True,
|
|
57
|
+
)
|
|
58
|
+
line_content: Mapped[str | None] = mapped_column(
|
|
59
|
+
Text,
|
|
60
|
+
nullable=True,
|
|
61
|
+
)
|
|
62
|
+
body: Mapped[str] = mapped_column(
|
|
63
|
+
Text,
|
|
64
|
+
nullable=False,
|
|
65
|
+
)
|
|
66
|
+
author_type: Mapped[str] = mapped_column(
|
|
67
|
+
String(20),
|
|
68
|
+
default=AuthorType.HUMAN.value,
|
|
69
|
+
nullable=False,
|
|
70
|
+
)
|
|
71
|
+
resolved: Mapped[bool] = mapped_column(
|
|
72
|
+
Boolean,
|
|
73
|
+
default=False,
|
|
74
|
+
nullable=False,
|
|
75
|
+
)
|
|
76
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
77
|
+
DateTime,
|
|
78
|
+
server_default=func.now(),
|
|
79
|
+
nullable=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Relationships
|
|
83
|
+
revision: Mapped["Revision"] = relationship("Revision", back_populates="comments")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def author_type_enum(self) -> AuthorType:
|
|
87
|
+
"""Get the author_type as an AuthorType enum."""
|
|
88
|
+
return AuthorType(self.author_type)
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return f"<ReviewComment(id={self.id}, file={self.file_path}, line={self.line_number}, resolved={self.resolved})>"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""ReviewSummary model for overall review decisions on revisions."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
10
|
+
|
|
11
|
+
from app.models.base import Base
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from app.models.revision import Revision
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReviewDecision(StrEnum):
|
|
18
|
+
"""Enum representing the review decision."""
|
|
19
|
+
|
|
20
|
+
APPROVED = "approved"
|
|
21
|
+
CHANGES_REQUESTED = "changes_requested"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReviewSummary(Base):
|
|
25
|
+
"""ReviewSummary model representing the overall review decision for a revision.
|
|
26
|
+
|
|
27
|
+
Only one ReviewSummary per revision is allowed.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__tablename__ = "review_summaries"
|
|
31
|
+
|
|
32
|
+
id: Mapped[str] = mapped_column(
|
|
33
|
+
String(36),
|
|
34
|
+
primary_key=True,
|
|
35
|
+
default=lambda: str(uuid.uuid4()),
|
|
36
|
+
)
|
|
37
|
+
revision_id: Mapped[str] = mapped_column(
|
|
38
|
+
String(36),
|
|
39
|
+
ForeignKey("revisions.id", ondelete="CASCADE"),
|
|
40
|
+
nullable=False,
|
|
41
|
+
unique=True,
|
|
42
|
+
index=True,
|
|
43
|
+
)
|
|
44
|
+
decision: Mapped[str] = mapped_column(
|
|
45
|
+
String(30),
|
|
46
|
+
nullable=False,
|
|
47
|
+
)
|
|
48
|
+
body: Mapped[str] = mapped_column(
|
|
49
|
+
Text,
|
|
50
|
+
nullable=False,
|
|
51
|
+
)
|
|
52
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
53
|
+
DateTime,
|
|
54
|
+
server_default=func.now(),
|
|
55
|
+
nullable=False,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Relationships
|
|
59
|
+
revision: Mapped["Revision"] = relationship(
|
|
60
|
+
"Revision", back_populates="review_summary"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def decision_enum(self) -> ReviewDecision:
|
|
65
|
+
"""Get the decision as a ReviewDecision enum."""
|
|
66
|
+
return ReviewDecision(self.decision)
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f"<ReviewSummary(id={self.id}, revision_id={self.revision_id}, decision={self.decision})>"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Revision model for tracking agent code change iterations."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
10
|
+
|
|
11
|
+
from app.models.base import Base
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from app.models.evidence import Evidence
|
|
15
|
+
from app.models.job import Job
|
|
16
|
+
from app.models.review_comment import ReviewComment
|
|
17
|
+
from app.models.review_summary import ReviewSummary
|
|
18
|
+
from app.models.ticket import Ticket
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RevisionStatus(StrEnum):
|
|
22
|
+
"""Enum representing the status of a revision."""
|
|
23
|
+
|
|
24
|
+
OPEN = "open"
|
|
25
|
+
CHANGES_REQUESTED = "changes_requested"
|
|
26
|
+
APPROVED = "approved"
|
|
27
|
+
SUPERSEDED = "superseded"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Revision(Base):
|
|
31
|
+
"""Revision model representing one agent iteration for a ticket.
|
|
32
|
+
|
|
33
|
+
A revision is similar to a GitHub PR snapshot - it contains:
|
|
34
|
+
- A diff (stat + patch as evidence)
|
|
35
|
+
- Review comments
|
|
36
|
+
- A final review decision
|
|
37
|
+
|
|
38
|
+
Only one revision per ticket can be 'open' at a time.
|
|
39
|
+
When a new revision is created, previous open revision becomes 'superseded'.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
__tablename__ = "revisions"
|
|
43
|
+
__table_args__ = (
|
|
44
|
+
UniqueConstraint("ticket_id", "number", name="uq_revision_ticket_number"),
|
|
45
|
+
UniqueConstraint("ticket_id", "job_id", name="uq_revision_ticket_job"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
id: Mapped[str] = mapped_column(
|
|
49
|
+
String(36),
|
|
50
|
+
primary_key=True,
|
|
51
|
+
default=lambda: str(uuid.uuid4()),
|
|
52
|
+
)
|
|
53
|
+
ticket_id: Mapped[str] = mapped_column(
|
|
54
|
+
String(36),
|
|
55
|
+
ForeignKey("tickets.id", ondelete="CASCADE"),
|
|
56
|
+
nullable=False,
|
|
57
|
+
index=True,
|
|
58
|
+
)
|
|
59
|
+
job_id: Mapped[str] = mapped_column(
|
|
60
|
+
String(36),
|
|
61
|
+
ForeignKey("jobs.id", ondelete="CASCADE"),
|
|
62
|
+
nullable=False,
|
|
63
|
+
index=True,
|
|
64
|
+
)
|
|
65
|
+
number: Mapped[int] = mapped_column(
|
|
66
|
+
Integer,
|
|
67
|
+
nullable=False,
|
|
68
|
+
)
|
|
69
|
+
status: Mapped[str] = mapped_column(
|
|
70
|
+
String(50),
|
|
71
|
+
default=RevisionStatus.OPEN.value,
|
|
72
|
+
nullable=False,
|
|
73
|
+
index=True,
|
|
74
|
+
)
|
|
75
|
+
diff_stat_evidence_id: Mapped[str | None] = mapped_column(
|
|
76
|
+
String(36),
|
|
77
|
+
ForeignKey("evidence.id", ondelete="SET NULL"),
|
|
78
|
+
nullable=True,
|
|
79
|
+
)
|
|
80
|
+
diff_patch_evidence_id: Mapped[str | None] = mapped_column(
|
|
81
|
+
String(36),
|
|
82
|
+
ForeignKey("evidence.id", ondelete="SET NULL"),
|
|
83
|
+
nullable=True,
|
|
84
|
+
)
|
|
85
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
86
|
+
DateTime,
|
|
87
|
+
server_default=func.now(),
|
|
88
|
+
nullable=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Relationships
|
|
92
|
+
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="revisions")
|
|
93
|
+
job: Mapped["Job"] = relationship(
|
|
94
|
+
"Job",
|
|
95
|
+
back_populates="revision",
|
|
96
|
+
foreign_keys=[job_id],
|
|
97
|
+
)
|
|
98
|
+
diff_stat_evidence: Mapped["Evidence | None"] = relationship(
|
|
99
|
+
"Evidence",
|
|
100
|
+
foreign_keys=[diff_stat_evidence_id],
|
|
101
|
+
)
|
|
102
|
+
diff_patch_evidence: Mapped["Evidence | None"] = relationship(
|
|
103
|
+
"Evidence",
|
|
104
|
+
foreign_keys=[diff_patch_evidence_id],
|
|
105
|
+
)
|
|
106
|
+
comments: Mapped[list["ReviewComment"]] = relationship(
|
|
107
|
+
"ReviewComment",
|
|
108
|
+
back_populates="revision",
|
|
109
|
+
cascade="all, delete-orphan",
|
|
110
|
+
order_by="ReviewComment.created_at",
|
|
111
|
+
)
|
|
112
|
+
review_summary: Mapped["ReviewSummary | None"] = relationship(
|
|
113
|
+
"ReviewSummary",
|
|
114
|
+
back_populates="revision",
|
|
115
|
+
cascade="all, delete-orphan",
|
|
116
|
+
uselist=False,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def status_enum(self) -> RevisionStatus:
|
|
121
|
+
"""Get the status as a RevisionStatus enum."""
|
|
122
|
+
return RevisionStatus(self.status)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def unresolved_comment_count(self) -> int:
|
|
126
|
+
"""Get count of unresolved comments."""
|
|
127
|
+
return sum(1 for c in self.comments if not c.resolved)
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return f"<Revision(id={self.id}, ticket_id={self.ticket_id}, number={self.number}, status={self.status})>"
|