Artifex/control_plane/projects/models.py

252 lines
10 KiB
Python
Raw Normal View History

2026-08-15 13:50:24 +07:00
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")