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,79 @@
|
|
|
1
|
+
"""Board model - the primary permission and scoping boundary."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import JSON, DateTime, ForeignKey, 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
|
+
from app.models.goal import Goal
|
|
14
|
+
from app.models.job import Job
|
|
15
|
+
from app.models.ticket import Ticket
|
|
16
|
+
from app.models.user import User
|
|
17
|
+
from app.models.workspace import Workspace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Board(Base):
|
|
21
|
+
"""Board represents a project/repository boundary.
|
|
22
|
+
|
|
23
|
+
Key properties:
|
|
24
|
+
- Single repo per board (repo_root is authoritative)
|
|
25
|
+
- All goals, tickets, jobs, workspaces belong to a board
|
|
26
|
+
- board_id is the permission boundary for all operations
|
|
27
|
+
|
|
28
|
+
This prevents cross-tenant/cross-project data access.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__tablename__ = "boards"
|
|
32
|
+
|
|
33
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
34
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
35
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
36
|
+
|
|
37
|
+
# The authoritative repo root for this board
|
|
38
|
+
# All file operations use this path - NOT client-provided paths
|
|
39
|
+
repo_root: Mapped[str] = mapped_column(String(1024), nullable=False)
|
|
40
|
+
|
|
41
|
+
# Optional: default branch for this repo
|
|
42
|
+
default_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
43
|
+
|
|
44
|
+
# Board-level configuration overrides (JSON)
|
|
45
|
+
# Overrides settings from draft.yaml
|
|
46
|
+
config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
47
|
+
|
|
48
|
+
# Owner (nullable for backward compat with single-user setups)
|
|
49
|
+
owner_id: Mapped[str | None] = mapped_column(
|
|
50
|
+
String(36),
|
|
51
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
52
|
+
nullable=True,
|
|
53
|
+
index=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
57
|
+
DateTime(timezone=True), server_default=func.now()
|
|
58
|
+
)
|
|
59
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
60
|
+
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Relationships
|
|
64
|
+
owner: Mapped["User | None"] = relationship("User", back_populates="boards")
|
|
65
|
+
goals: Mapped[list["Goal"]] = relationship(
|
|
66
|
+
"Goal", back_populates="board", cascade="all, delete-orphan"
|
|
67
|
+
)
|
|
68
|
+
tickets: Mapped[list["Ticket"]] = relationship(
|
|
69
|
+
"Ticket", back_populates="board", cascade="all, delete-orphan"
|
|
70
|
+
)
|
|
71
|
+
jobs: Mapped[list["Job"]] = relationship(
|
|
72
|
+
"Job", back_populates="board", cascade="all, delete-orphan"
|
|
73
|
+
)
|
|
74
|
+
workspaces: Mapped[list["Workspace"]] = relationship(
|
|
75
|
+
"Workspace", back_populates="board", cascade="all, delete-orphan"
|
|
76
|
+
)
|
|
77
|
+
board_repos: Mapped[list["BoardRepo"]] = relationship(
|
|
78
|
+
"BoardRepo", back_populates="board", cascade="all, delete-orphan"
|
|
79
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""BoardRepo junction model - Board <-> Repo many-to-many relationship."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
Boolean,
|
|
8
|
+
DateTime,
|
|
9
|
+
ForeignKey,
|
|
10
|
+
String,
|
|
11
|
+
Text,
|
|
12
|
+
UniqueConstraint,
|
|
13
|
+
func,
|
|
14
|
+
)
|
|
15
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
16
|
+
|
|
17
|
+
from app.models.base import Base
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from app.models.board import Board
|
|
21
|
+
from app.models.repo import Repo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BoardRepo(Base):
|
|
25
|
+
"""Junction table linking Boards to Repos (many-to-many).
|
|
26
|
+
|
|
27
|
+
Allows a board to have multiple repositories and a repo to be
|
|
28
|
+
shared across multiple boards.
|
|
29
|
+
|
|
30
|
+
Key properties:
|
|
31
|
+
- board_id + repo_id must be unique (one entry per board-repo pair)
|
|
32
|
+
- is_primary marks the primary repo for a board (used as default for operations)
|
|
33
|
+
- custom_setup_script allows per-board repo configuration overrides
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__tablename__ = "board_repos"
|
|
37
|
+
|
|
38
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
39
|
+
|
|
40
|
+
# Foreign keys
|
|
41
|
+
board_id: Mapped[str] = mapped_column(
|
|
42
|
+
ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True
|
|
43
|
+
)
|
|
44
|
+
repo_id: Mapped[str] = mapped_column(
|
|
45
|
+
ForeignKey("repos.id", ondelete="CASCADE"), nullable=False, index=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Primary repo flag - each board should have exactly one primary repo
|
|
49
|
+
is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
50
|
+
|
|
51
|
+
# Per-board repo overrides (optional)
|
|
52
|
+
custom_setup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
53
|
+
custom_cleanup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
54
|
+
|
|
55
|
+
# Timestamps
|
|
56
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
57
|
+
DateTime(timezone=True), server_default=func.now()
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Relationships
|
|
61
|
+
board: Mapped["Board"] = relationship("Board", back_populates="board_repos")
|
|
62
|
+
repo: Mapped["Repo"] = relationship("Repo", back_populates="board_repos")
|
|
63
|
+
|
|
64
|
+
# Constraints
|
|
65
|
+
__table_args__ = (UniqueConstraint("board_id", "repo_id", name="uq_board_repo"),)
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return f"<BoardRepo(board_id={self.board_id}, repo_id={self.repo_id}, is_primary={self.is_primary})>"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Cost budget model for tracking spending limits."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, String
|
|
7
|
+
from sqlalchemy.orm import relationship
|
|
8
|
+
|
|
9
|
+
from app.models.base import Base
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CostBudget(Base):
|
|
13
|
+
"""Budget configuration for cost tracking."""
|
|
14
|
+
|
|
15
|
+
__tablename__ = "cost_budgets"
|
|
16
|
+
|
|
17
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
|
18
|
+
goal_id = Column(
|
|
19
|
+
String(36),
|
|
20
|
+
ForeignKey("goals.id", ondelete="CASCADE"),
|
|
21
|
+
nullable=True,
|
|
22
|
+
index=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Budget limits (None = unlimited)
|
|
26
|
+
daily_budget = Column(Float, nullable=True)
|
|
27
|
+
weekly_budget = Column(Float, nullable=True)
|
|
28
|
+
monthly_budget = Column(Float, nullable=True)
|
|
29
|
+
total_budget = Column(Float, nullable=True)
|
|
30
|
+
|
|
31
|
+
# Alert settings
|
|
32
|
+
warning_threshold = Column(Float, default=0.8, nullable=False) # 80% by default
|
|
33
|
+
pause_on_exceed = Column(Boolean, default=False, nullable=False)
|
|
34
|
+
|
|
35
|
+
# Timestamps
|
|
36
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
37
|
+
updated_at = Column(
|
|
38
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Relationships
|
|
42
|
+
goal = relationship("Goal", back_populates="budget")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Shared enumerations for Draft models.
|
|
2
|
+
|
|
3
|
+
This file contains event types and other enums that are NOT part of the
|
|
4
|
+
ticket state machine. The state machine (TicketState, transitions) lives
|
|
5
|
+
in state_machine.py. Event types are audit log entries, not state rules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventType(StrEnum):
|
|
12
|
+
"""Enum representing types of ticket events.
|
|
13
|
+
|
|
14
|
+
These are audit log event types, NOT state transition rules.
|
|
15
|
+
State transitions are governed by state_machine.py.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Core lifecycle events
|
|
19
|
+
CREATED = "created"
|
|
20
|
+
TRANSITIONED = "transitioned"
|
|
21
|
+
UPDATED = "updated"
|
|
22
|
+
COMMENT = "comment"
|
|
23
|
+
|
|
24
|
+
# Merge lifecycle events
|
|
25
|
+
MERGE_REQUESTED = "merge_requested"
|
|
26
|
+
MERGE_SUCCEEDED = "merge_succeeded"
|
|
27
|
+
MERGE_FAILED = "merge_failed"
|
|
28
|
+
|
|
29
|
+
# Cleanup events (distinct types for clear analytics/UX)
|
|
30
|
+
WORKTREE_CLEANED = "worktree_cleaned"
|
|
31
|
+
WORKTREE_CLEANUP_FAILED = "worktree_cleanup_failed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ActorType(StrEnum):
|
|
35
|
+
"""Enum representing who performed an action."""
|
|
36
|
+
|
|
37
|
+
HUMAN = "human"
|
|
38
|
+
PLANNER = "planner"
|
|
39
|
+
SYSTEM = "system"
|
|
40
|
+
EXECUTOR = "executor"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Evidence model for storing verification command results."""
|
|
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, 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.job import Job
|
|
15
|
+
from app.models.ticket import Ticket
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EvidenceKind(StrEnum):
|
|
19
|
+
"""Enum representing the kind of evidence.
|
|
20
|
+
|
|
21
|
+
Metadata evidence (JSON in stdout_path):
|
|
22
|
+
- EXECUTOR_META: JSON with exit_code, duration, executor_type, mode, command
|
|
23
|
+
- VERIFY_META: JSON with exit_code, commands_run, duration, results per command
|
|
24
|
+
|
|
25
|
+
Evidence types for execution:
|
|
26
|
+
- EXECUTOR_STDOUT: stdout from executor CLI (Claude/Cursor)
|
|
27
|
+
- EXECUTOR_STDERR: stderr from executor CLI
|
|
28
|
+
- GIT_DIFF_STAT: output of `git diff --stat`
|
|
29
|
+
- GIT_DIFF_PATCH: full git diff patch
|
|
30
|
+
|
|
31
|
+
Evidence types for verification:
|
|
32
|
+
- VERIFY_STDOUT: stdout from verification command
|
|
33
|
+
- VERIFY_STDERR: stderr from verification command
|
|
34
|
+
|
|
35
|
+
Legacy types (kept for backwards compatibility):
|
|
36
|
+
- COMMAND_LOG: generic command output
|
|
37
|
+
- TEST_REPORT: test framework report
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Metadata evidence (JSON)
|
|
41
|
+
EXECUTOR_META = (
|
|
42
|
+
"executor_meta" # JSON: exit_code, duration_ms, executor_type, mode, command
|
|
43
|
+
)
|
|
44
|
+
VERIFY_META = "verify_meta" # JSON: exit_code, commands, duration_ms, results
|
|
45
|
+
|
|
46
|
+
# Executor evidence
|
|
47
|
+
EXECUTOR_STDOUT = "executor_stdout"
|
|
48
|
+
EXECUTOR_STDERR = "executor_stderr"
|
|
49
|
+
|
|
50
|
+
# Git diff evidence
|
|
51
|
+
GIT_DIFF_STAT = "git_diff_stat"
|
|
52
|
+
GIT_DIFF_PATCH = "git_diff_patch"
|
|
53
|
+
|
|
54
|
+
# Verification evidence
|
|
55
|
+
VERIFY_STDOUT = "verify_stdout"
|
|
56
|
+
VERIFY_STDERR = "verify_stderr"
|
|
57
|
+
|
|
58
|
+
# Merge evidence
|
|
59
|
+
MERGE_STDOUT = "merge_stdout"
|
|
60
|
+
MERGE_STDERR = "merge_stderr"
|
|
61
|
+
MERGE_META = "merge_meta" # JSON: strategy, branch, base_branch, exit_code, duration_ms, evidence_ids
|
|
62
|
+
|
|
63
|
+
# Legacy types (backwards compatibility)
|
|
64
|
+
COMMAND_LOG = "command_log"
|
|
65
|
+
TEST_REPORT = "test_report"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Evidence(Base):
|
|
69
|
+
"""Evidence model representing verification command execution results."""
|
|
70
|
+
|
|
71
|
+
__tablename__ = "evidence"
|
|
72
|
+
|
|
73
|
+
id: Mapped[str] = mapped_column(
|
|
74
|
+
String(36),
|
|
75
|
+
primary_key=True,
|
|
76
|
+
default=lambda: str(uuid.uuid4()),
|
|
77
|
+
)
|
|
78
|
+
ticket_id: Mapped[str] = mapped_column(
|
|
79
|
+
String(36),
|
|
80
|
+
ForeignKey("tickets.id", ondelete="CASCADE"),
|
|
81
|
+
nullable=False,
|
|
82
|
+
index=True,
|
|
83
|
+
)
|
|
84
|
+
job_id: Mapped[str] = mapped_column(
|
|
85
|
+
String(36),
|
|
86
|
+
ForeignKey("jobs.id", ondelete="CASCADE"),
|
|
87
|
+
nullable=False,
|
|
88
|
+
index=True,
|
|
89
|
+
)
|
|
90
|
+
kind: Mapped[str] = mapped_column(
|
|
91
|
+
String(50),
|
|
92
|
+
nullable=False,
|
|
93
|
+
default=EvidenceKind.COMMAND_LOG.value,
|
|
94
|
+
)
|
|
95
|
+
command: Mapped[str] = mapped_column(
|
|
96
|
+
Text,
|
|
97
|
+
nullable=False,
|
|
98
|
+
)
|
|
99
|
+
exit_code: Mapped[int] = mapped_column(
|
|
100
|
+
Integer,
|
|
101
|
+
nullable=False,
|
|
102
|
+
)
|
|
103
|
+
stdout_path: Mapped[str | None] = mapped_column(
|
|
104
|
+
Text,
|
|
105
|
+
nullable=True,
|
|
106
|
+
)
|
|
107
|
+
stderr_path: Mapped[str | None] = mapped_column(
|
|
108
|
+
Text,
|
|
109
|
+
nullable=True,
|
|
110
|
+
)
|
|
111
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
112
|
+
DateTime,
|
|
113
|
+
server_default=func.now(),
|
|
114
|
+
nullable=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Relationships
|
|
118
|
+
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="evidence")
|
|
119
|
+
job: Mapped["Job"] = relationship("Job", back_populates="evidence")
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def kind_enum(self) -> EvidenceKind:
|
|
123
|
+
"""Get the kind as an EvidenceKind enum."""
|
|
124
|
+
return EvidenceKind(self.kind)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def succeeded(self) -> bool:
|
|
128
|
+
"""Check if the command succeeded (exit code 0)."""
|
|
129
|
+
return self.exit_code == 0
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
return f"<Evidence(id={self.id}, command={self.command[:30]}..., exit_code={self.exit_code})>"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Goal model for Draft."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Boolean, DateTime, 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.agent_conversation_history import AgentConversationHistory
|
|
14
|
+
from app.models.board import Board
|
|
15
|
+
from app.models.cost_budget import CostBudget
|
|
16
|
+
from app.models.merge_checklist import MergeChecklist
|
|
17
|
+
from app.models.ticket import Ticket
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Goal(Base):
|
|
21
|
+
"""Goal model representing a high-level objective.
|
|
22
|
+
|
|
23
|
+
IMPORTANT: Goals are scoped by board_id for permission enforcement.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__tablename__ = "goals"
|
|
27
|
+
|
|
28
|
+
id: Mapped[str] = mapped_column(
|
|
29
|
+
String(36),
|
|
30
|
+
primary_key=True,
|
|
31
|
+
default=lambda: str(uuid.uuid4()),
|
|
32
|
+
)
|
|
33
|
+
board_id: Mapped[str | None] = mapped_column(
|
|
34
|
+
String(36),
|
|
35
|
+
ForeignKey("boards.id", ondelete="CASCADE"),
|
|
36
|
+
nullable=True, # Nullable for migration compatibility
|
|
37
|
+
index=True,
|
|
38
|
+
)
|
|
39
|
+
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
40
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
41
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
42
|
+
DateTime,
|
|
43
|
+
server_default=func.now(),
|
|
44
|
+
nullable=False,
|
|
45
|
+
)
|
|
46
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
47
|
+
DateTime,
|
|
48
|
+
server_default=func.now(),
|
|
49
|
+
onupdate=func.now(),
|
|
50
|
+
nullable=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Autonomy fields
|
|
54
|
+
autonomy_enabled: Mapped[bool] = mapped_column(
|
|
55
|
+
Boolean, default=False, server_default="0", nullable=False
|
|
56
|
+
)
|
|
57
|
+
auto_approve_tickets: Mapped[bool] = mapped_column(
|
|
58
|
+
Boolean, default=False, server_default="0", nullable=False
|
|
59
|
+
)
|
|
60
|
+
auto_approve_revisions: Mapped[bool] = mapped_column(
|
|
61
|
+
Boolean, default=False, server_default="0", nullable=False
|
|
62
|
+
)
|
|
63
|
+
auto_merge: Mapped[bool] = mapped_column(
|
|
64
|
+
Boolean, default=False, server_default="0", nullable=False
|
|
65
|
+
)
|
|
66
|
+
auto_approve_followups: Mapped[bool] = mapped_column(
|
|
67
|
+
Boolean, default=False, server_default="0", nullable=False
|
|
68
|
+
)
|
|
69
|
+
max_auto_approvals: Mapped[int | None] = mapped_column(
|
|
70
|
+
Integer, default=None, nullable=True
|
|
71
|
+
)
|
|
72
|
+
auto_approval_count: Mapped[int] = mapped_column(
|
|
73
|
+
Integer, default=0, server_default="0", nullable=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Relationships
|
|
77
|
+
board: Mapped["Board | None"] = relationship("Board", back_populates="goals")
|
|
78
|
+
tickets: Mapped[list["Ticket"]] = relationship(
|
|
79
|
+
"Ticket",
|
|
80
|
+
back_populates="goal",
|
|
81
|
+
cascade="all, delete-orphan",
|
|
82
|
+
)
|
|
83
|
+
budget: Mapped["CostBudget | None"] = relationship(
|
|
84
|
+
"CostBudget",
|
|
85
|
+
back_populates="goal",
|
|
86
|
+
uselist=False,
|
|
87
|
+
cascade="all, delete-orphan",
|
|
88
|
+
)
|
|
89
|
+
merge_checklist: Mapped["MergeChecklist | None"] = relationship(
|
|
90
|
+
"MergeChecklist",
|
|
91
|
+
back_populates="goal",
|
|
92
|
+
uselist=False,
|
|
93
|
+
cascade="all, delete-orphan",
|
|
94
|
+
)
|
|
95
|
+
agent_conversation_history: Mapped[list["AgentConversationHistory"]] = relationship(
|
|
96
|
+
"AgentConversationHistory",
|
|
97
|
+
back_populates="goal",
|
|
98
|
+
cascade="all, delete-orphan",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
return f"<Goal(id={self.id}, title={self.title})>"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""SQLite-backed idempotency cache model (replaces Redis SETNX)."""
|
|
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 IdempotencyEntry(Base):
|
|
12
|
+
"""Idempotency lock and result cache entry.
|
|
13
|
+
|
|
14
|
+
Replaces Redis SETNX + result cache. Lock acquisition is atomic
|
|
15
|
+
via INSERT OR IGNORE (rowcount == 1 means acquired).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "idempotency_cache"
|
|
19
|
+
|
|
20
|
+
cache_key: Mapped[str] = mapped_column(String(512), primary_key=True)
|
|
21
|
+
lock_value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
22
|
+
result_value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
23
|
+
lock_expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
24
|
+
result_expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
25
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
26
|
+
DateTime, nullable=False, server_default=func.now()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return f"<IdempotencyEntry(key={self.cache_key[:30]}...)>"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Job model for tracking long-running task executions."""
|
|
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, 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.board import Board
|
|
15
|
+
from app.models.evidence import Evidence
|
|
16
|
+
from app.models.normalized_log import NormalizedLogEntry
|
|
17
|
+
from app.models.revision import Revision
|
|
18
|
+
from app.models.ticket import Ticket
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JobKind(StrEnum):
|
|
22
|
+
"""Enum representing the kind of job."""
|
|
23
|
+
|
|
24
|
+
EXECUTE = "execute"
|
|
25
|
+
VERIFY = "verify"
|
|
26
|
+
RESUME = "resume" # Resume after interactive (human) completion
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JobStatus(StrEnum):
|
|
30
|
+
"""Enum representing the status of a job."""
|
|
31
|
+
|
|
32
|
+
QUEUED = "queued"
|
|
33
|
+
RUNNING = "running"
|
|
34
|
+
SUCCEEDED = "succeeded"
|
|
35
|
+
FAILED = "failed"
|
|
36
|
+
CANCELED = "canceled"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Job(Base):
|
|
40
|
+
"""Job model representing a long-running task execution.
|
|
41
|
+
|
|
42
|
+
IMPORTANT: Jobs are scoped by board_id for permission enforcement.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
__tablename__ = "jobs"
|
|
46
|
+
|
|
47
|
+
id: Mapped[str] = mapped_column(
|
|
48
|
+
String(36),
|
|
49
|
+
primary_key=True,
|
|
50
|
+
default=lambda: str(uuid.uuid4()),
|
|
51
|
+
)
|
|
52
|
+
board_id: Mapped[str | None] = mapped_column(
|
|
53
|
+
String(36),
|
|
54
|
+
ForeignKey("boards.id", ondelete="CASCADE"),
|
|
55
|
+
nullable=True, # Nullable for migration compatibility
|
|
56
|
+
index=True,
|
|
57
|
+
)
|
|
58
|
+
ticket_id: Mapped[str] = mapped_column(
|
|
59
|
+
String(36),
|
|
60
|
+
ForeignKey("tickets.id", ondelete="CASCADE"),
|
|
61
|
+
nullable=False,
|
|
62
|
+
index=True,
|
|
63
|
+
)
|
|
64
|
+
kind: Mapped[str] = mapped_column(
|
|
65
|
+
String(20),
|
|
66
|
+
nullable=False,
|
|
67
|
+
)
|
|
68
|
+
status: Mapped[str] = mapped_column(
|
|
69
|
+
String(20),
|
|
70
|
+
default=JobStatus.QUEUED.value,
|
|
71
|
+
nullable=False,
|
|
72
|
+
index=True,
|
|
73
|
+
)
|
|
74
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
75
|
+
DateTime,
|
|
76
|
+
server_default=func.now(),
|
|
77
|
+
nullable=False,
|
|
78
|
+
)
|
|
79
|
+
started_at: Mapped[datetime | None] = mapped_column(
|
|
80
|
+
DateTime,
|
|
81
|
+
nullable=True,
|
|
82
|
+
)
|
|
83
|
+
finished_at: Mapped[datetime | None] = mapped_column(
|
|
84
|
+
DateTime,
|
|
85
|
+
nullable=True,
|
|
86
|
+
)
|
|
87
|
+
exit_code: Mapped[int | None] = mapped_column(
|
|
88
|
+
Integer,
|
|
89
|
+
nullable=True,
|
|
90
|
+
)
|
|
91
|
+
log_path: Mapped[str | None] = mapped_column(
|
|
92
|
+
Text,
|
|
93
|
+
nullable=True,
|
|
94
|
+
)
|
|
95
|
+
celery_task_id: Mapped[str | None] = mapped_column(
|
|
96
|
+
String(255),
|
|
97
|
+
nullable=True,
|
|
98
|
+
)
|
|
99
|
+
# For jobs triggered by review feedback, tracks which revision is being addressed
|
|
100
|
+
source_revision_id: Mapped[str | None] = mapped_column(
|
|
101
|
+
String(36),
|
|
102
|
+
ForeignKey("revisions.id", ondelete="SET NULL"),
|
|
103
|
+
nullable=True,
|
|
104
|
+
index=True,
|
|
105
|
+
)
|
|
106
|
+
# Session ID for executor session resume (e.g. Claude --resume)
|
|
107
|
+
session_id: Mapped[str | None] = mapped_column(
|
|
108
|
+
String(255),
|
|
109
|
+
nullable=True,
|
|
110
|
+
)
|
|
111
|
+
# Health monitoring fields
|
|
112
|
+
last_heartbeat_at: Mapped[datetime | None] = mapped_column(
|
|
113
|
+
DateTime,
|
|
114
|
+
nullable=True,
|
|
115
|
+
index=True,
|
|
116
|
+
)
|
|
117
|
+
timeout_seconds: Mapped[int | None] = mapped_column(
|
|
118
|
+
Integer,
|
|
119
|
+
nullable=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Relationships
|
|
123
|
+
board: Mapped["Board | None"] = relationship("Board", back_populates="jobs")
|
|
124
|
+
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="jobs")
|
|
125
|
+
evidence: Mapped[list["Evidence"]] = relationship(
|
|
126
|
+
"Evidence",
|
|
127
|
+
back_populates="job",
|
|
128
|
+
cascade="all, delete-orphan",
|
|
129
|
+
order_by="Evidence.created_at.desc()",
|
|
130
|
+
)
|
|
131
|
+
# The revision created by this job (via Revision.job_id -> Job.id)
|
|
132
|
+
revision: Mapped["Revision | None"] = relationship(
|
|
133
|
+
"Revision",
|
|
134
|
+
back_populates="job",
|
|
135
|
+
uselist=False,
|
|
136
|
+
foreign_keys="Revision.job_id",
|
|
137
|
+
)
|
|
138
|
+
# For jobs triggered by review, the revision being addressed
|
|
139
|
+
source_revision: Mapped["Revision | None"] = relationship(
|
|
140
|
+
"Revision",
|
|
141
|
+
foreign_keys="Job.source_revision_id",
|
|
142
|
+
viewonly=True, # Don't allow writes through this relationship
|
|
143
|
+
)
|
|
144
|
+
# Normalized log entries for this job
|
|
145
|
+
normalized_logs: Mapped[list["NormalizedLogEntry"]] = relationship(
|
|
146
|
+
"NormalizedLogEntry",
|
|
147
|
+
back_populates="job",
|
|
148
|
+
cascade="all, delete-orphan",
|
|
149
|
+
order_by="NormalizedLogEntry.sequence",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def kind_enum(self) -> JobKind:
|
|
154
|
+
"""Get the kind as a JobKind enum."""
|
|
155
|
+
return JobKind(self.kind)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def status_enum(self) -> JobStatus:
|
|
159
|
+
"""Get the status as a JobStatus enum."""
|
|
160
|
+
return JobStatus(self.status)
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
return f"<Job(id={self.id}, kind={self.kind}, status={self.status})>"
|