252 lines
10 KiB
Python
252 lines
10 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from django.db import models
|
||
|
|
|
||
|
|
from control_plane.common import TimestampedModel
|
||
|
|
|
||
|
|
|
||
|
|
class ProjectStatus(models.TextChoices):
|
||
|
|
PLANNING = "PLANNING"
|
||
|
|
READY = "READY"
|
||
|
|
ARCHAEOLOGY = "ARCHAEOLOGY"
|
||
|
|
BUILDING = "BUILDING"
|
||
|
|
PAUSING = "PAUSING"
|
||
|
|
PAUSED = "PAUSED"
|
||
|
|
BLOCKED = "BLOCKED"
|
||
|
|
VERIFYING = "VERIFYING"
|
||
|
|
FINISHED = "FINISHED"
|
||
|
|
FAILED = "FAILED"
|
||
|
|
|
||
|
|
|
||
|
|
class Project(TimestampedModel):
|
||
|
|
name = models.CharField(max_length=200)
|
||
|
|
project_type = models.CharField(max_length=100, default="WEB_APP")
|
||
|
|
goal = models.TextField()
|
||
|
|
repository_url = models.TextField(blank=True)
|
||
|
|
repository_path = models.TextField(blank=True)
|
||
|
|
status = models.CharField(max_length=32, choices=ProjectStatus.choices, default=ProjectStatus.PLANNING)
|
||
|
|
current_plan_version = models.PositiveIntegerField(default=0)
|
||
|
|
architecture_summary = models.TextField(blank=True)
|
||
|
|
constraints = models.JSONField(default=dict, blank=True)
|
||
|
|
budget = models.JSONField(default=dict, blank=True)
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
return self.name
|
||
|
|
|
||
|
|
|
||
|
|
class ProjectPlan(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="plans")
|
||
|
|
version = models.PositiveIntegerField()
|
||
|
|
goal = models.TextField()
|
||
|
|
scope = models.TextField(blank=True)
|
||
|
|
stack = models.JSONField(default=dict, blank=True)
|
||
|
|
architecture = models.JSONField(default=dict, blank=True)
|
||
|
|
constraints = models.JSONField(default=dict, blank=True)
|
||
|
|
acceptance_criteria = models.JSONField(default=list, blank=True)
|
||
|
|
permissions = models.JSONField(default=dict, blank=True)
|
||
|
|
budget = models.JSONField(default=dict, blank=True)
|
||
|
|
open_decisions = models.JSONField(default=list, blank=True)
|
||
|
|
approved_at = models.DateTimeField(null=True, blank=True)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
constraints = [models.UniqueConstraint(fields=["project", "version"], name="unique_plan_version")]
|
||
|
|
|
||
|
|
|
||
|
|
class MilestoneStatus(models.TextChoices):
|
||
|
|
QUEUED = "QUEUED"
|
||
|
|
READY = "READY"
|
||
|
|
RUNNING = "RUNNING"
|
||
|
|
VERIFYING = "VERIFYING"
|
||
|
|
COMPLETE = "COMPLETE"
|
||
|
|
BLOCKED = "BLOCKED"
|
||
|
|
FAILED = "FAILED"
|
||
|
|
|
||
|
|
|
||
|
|
class Milestone(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="milestones")
|
||
|
|
plan = models.ForeignKey(ProjectPlan, on_delete=models.PROTECT, related_name="milestones")
|
||
|
|
key = models.CharField(max_length=50)
|
||
|
|
title = models.CharField(max_length=200)
|
||
|
|
goal = models.TextField()
|
||
|
|
status = models.CharField(max_length=32, choices=MilestoneStatus.choices, default=MilestoneStatus.QUEUED)
|
||
|
|
dependencies = models.ManyToManyField("self", symmetrical=False, blank=True)
|
||
|
|
verification_contract = models.JSONField(default=dict, blank=True)
|
||
|
|
order = models.PositiveIntegerField(default=0)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
ordering = ["order", "created_at"]
|
||
|
|
constraints = [models.UniqueConstraint(fields=["project", "key"], name="unique_milestone_key")]
|
||
|
|
|
||
|
|
|
||
|
|
class Feature(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="features")
|
||
|
|
milestone = models.ForeignKey(Milestone, on_delete=models.CASCADE, related_name="features")
|
||
|
|
title = models.CharField(max_length=200)
|
||
|
|
description = models.TextField(blank=True)
|
||
|
|
acceptance_criteria = models.JSONField(default=list, blank=True)
|
||
|
|
status = models.CharField(max_length=32, choices=MilestoneStatus.choices, default=MilestoneStatus.QUEUED)
|
||
|
|
|
||
|
|
|
||
|
|
class TaskStatus(models.TextChoices):
|
||
|
|
QUEUED = "QUEUED"
|
||
|
|
READY = "READY"
|
||
|
|
RUNNING = "RUNNING"
|
||
|
|
WAITING_TOOL = "WAITING_TOOL"
|
||
|
|
REVIEW = "REVIEW"
|
||
|
|
BLOCKED = "BLOCKED"
|
||
|
|
FAILED = "FAILED"
|
||
|
|
COMPLETE = "COMPLETE"
|
||
|
|
|
||
|
|
|
||
|
|
class TaskPriority(models.IntegerChoices):
|
||
|
|
LOW = 10
|
||
|
|
NORMAL = 50
|
||
|
|
HIGH = 80
|
||
|
|
CRITICAL = 100
|
||
|
|
|
||
|
|
|
||
|
|
class Task(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="tasks")
|
||
|
|
milestone = models.ForeignKey(Milestone, on_delete=models.PROTECT, related_name="tasks")
|
||
|
|
feature = models.ForeignKey(Feature, on_delete=models.PROTECT, related_name="tasks", null=True, blank=True)
|
||
|
|
task_type = models.CharField(max_length=80)
|
||
|
|
status = models.CharField(max_length=32, choices=TaskStatus.choices, default=TaskStatus.QUEUED)
|
||
|
|
priority = models.PositiveSmallIntegerField(choices=TaskPriority.choices, default=TaskPriority.NORMAL)
|
||
|
|
goal = models.TextField()
|
||
|
|
acceptance_criteria = models.JSONField(default=list, blank=True)
|
||
|
|
assigned_agent_version = models.ForeignKey(
|
||
|
|
"agents.AgentVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="tasks"
|
||
|
|
)
|
||
|
|
retry_count = models.PositiveIntegerField(default=0)
|
||
|
|
max_retries = models.PositiveIntegerField(default=2)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
indexes = [models.Index(fields=["project", "status", "priority"])]
|
||
|
|
|
||
|
|
|
||
|
|
class TaskDependency(TimestampedModel):
|
||
|
|
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="dependency_edges")
|
||
|
|
depends_on = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="dependent_edges")
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
constraints = [models.UniqueConstraint(fields=["task", "depends_on"], name="unique_task_dependency")]
|
||
|
|
|
||
|
|
|
||
|
|
class Action(TimestampedModel):
|
||
|
|
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="actions")
|
||
|
|
action_type = models.CharField(max_length=80)
|
||
|
|
status = models.CharField(max_length=32, default="PENDING")
|
||
|
|
contract = models.JSONField(default=dict, blank=True)
|
||
|
|
result = models.JSONField(default=dict, blank=True)
|
||
|
|
started_at = models.DateTimeField(null=True, blank=True)
|
||
|
|
finished_at = models.DateTimeField(null=True, blank=True)
|
||
|
|
|
||
|
|
|
||
|
|
class Worktree(TimestampedModel):
|
||
|
|
task = models.OneToOneField(Task, on_delete=models.CASCADE, related_name="worktree")
|
||
|
|
repository_path = models.TextField()
|
||
|
|
worktree_path = models.TextField()
|
||
|
|
branch_name = models.CharField(max_length=255)
|
||
|
|
base_ref = models.CharField(max_length=255, default="main")
|
||
|
|
status = models.CharField(max_length=32, default="ACTIVE")
|
||
|
|
|
||
|
|
|
||
|
|
class CommitRecord(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="commits")
|
||
|
|
task = models.ForeignKey(Task, on_delete=models.SET_NULL, null=True, blank=True, related_name="commits")
|
||
|
|
worktree = models.ForeignKey(Worktree, on_delete=models.SET_NULL, null=True, blank=True, related_name="commits")
|
||
|
|
coder = models.ForeignKey(
|
||
|
|
"agents.AgentVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="coded_commits"
|
||
|
|
)
|
||
|
|
reviewer = models.ForeignKey(
|
||
|
|
"agents.AgentVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_commits"
|
||
|
|
)
|
||
|
|
judge = models.ForeignKey(
|
||
|
|
"agents.AgentVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="judged_commits"
|
||
|
|
)
|
||
|
|
test_run = models.ForeignKey(
|
||
|
|
"verification.TestRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="commits"
|
||
|
|
)
|
||
|
|
review = models.ForeignKey(
|
||
|
|
"verification.Review", on_delete=models.SET_NULL, null=True, blank=True, related_name="commits"
|
||
|
|
)
|
||
|
|
verification = models.ForeignKey(
|
||
|
|
"verification.Verification", on_delete=models.SET_NULL, null=True, blank=True, related_name="commits"
|
||
|
|
)
|
||
|
|
sha = models.CharField(max_length=64)
|
||
|
|
branch_name = models.CharField(max_length=255)
|
||
|
|
message = models.TextField()
|
||
|
|
|
||
|
|
|
||
|
|
class TaskAttempt(TimestampedModel):
|
||
|
|
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="attempts")
|
||
|
|
attempt_number = models.PositiveIntegerField()
|
||
|
|
coder = models.ForeignKey("agents.AgentVersion", on_delete=models.PROTECT, related_name="task_attempts")
|
||
|
|
status = models.CharField(max_length=32, default="RUNNING")
|
||
|
|
context_snapshot = models.JSONField(default=dict, blank=True)
|
||
|
|
coder_result = models.JSONField(default=dict, blank=True)
|
||
|
|
review_findings = models.JSONField(default=list, blank=True)
|
||
|
|
judge_findings = models.JSONField(default=list, blank=True)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
constraints = [models.UniqueConstraint(fields=["task", "attempt_number"], name="unique_task_attempt")]
|
||
|
|
|
||
|
|
|
||
|
|
class Decision(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="decisions")
|
||
|
|
decision_type = models.CharField(max_length=80)
|
||
|
|
decision = models.TextField()
|
||
|
|
reason = models.TextField(blank=True)
|
||
|
|
evidence = models.JSONField(default=list, blank=True)
|
||
|
|
project_plan_version = models.PositiveIntegerField(default=0)
|
||
|
|
actor = models.CharField(max_length=120)
|
||
|
|
|
||
|
|
|
||
|
|
class Artifact(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="artifacts")
|
||
|
|
task = models.ForeignKey(Task, on_delete=models.SET_NULL, null=True, blank=True, related_name="artifacts")
|
||
|
|
artifact_type = models.CharField(max_length=80)
|
||
|
|
name = models.CharField(max_length=255)
|
||
|
|
uri = models.TextField(blank=True)
|
||
|
|
content = models.JSONField(default=dict, blank=True)
|
||
|
|
generated_by = models.CharField(max_length=120, blank=True)
|
||
|
|
|
||
|
|
|
||
|
|
class RoadmapStatus(models.TextChoices):
|
||
|
|
INBOX = "INBOX"
|
||
|
|
NOW = "NOW"
|
||
|
|
NEXT = "NEXT"
|
||
|
|
LATER = "LATER"
|
||
|
|
EXPLORING = "EXPLORING"
|
||
|
|
DECLINED = "DECLINED"
|
||
|
|
DONE = "DONE"
|
||
|
|
|
||
|
|
|
||
|
|
class RoadmapItem(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="roadmap_items")
|
||
|
|
title = models.CharField(max_length=200)
|
||
|
|
description = models.TextField(blank=True)
|
||
|
|
source = models.CharField(max_length=80, default="user")
|
||
|
|
status = models.CharField(max_length=32, choices=RoadmapStatus.choices, default=RoadmapStatus.INBOX)
|
||
|
|
priority = models.PositiveSmallIntegerField(default=50)
|
||
|
|
|
||
|
|
|
||
|
|
class Finding(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="findings")
|
||
|
|
source = models.CharField(max_length=80)
|
||
|
|
finding_type = models.CharField(max_length=80)
|
||
|
|
severity = models.CharField(max_length=32, default="INFO")
|
||
|
|
title = models.CharField(max_length=255)
|
||
|
|
description = models.TextField(blank=True)
|
||
|
|
evidence = models.JSONField(default=list, blank=True)
|
||
|
|
status = models.CharField(max_length=32, default="OPEN")
|
||
|
|
|
||
|
|
|
||
|
|
class Scenario(TimestampedModel):
|
||
|
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="scenarios", null=True, blank=True)
|
||
|
|
name = models.CharField(max_length=200)
|
||
|
|
target_type = models.CharField(max_length=80)
|
||
|
|
target_id = models.CharField(max_length=120)
|
||
|
|
definition = models.JSONField(default=dict, blank=True)
|
||
|
|
status = models.CharField(max_length=32, default="DRAFT")
|