- Add Project model with slug, description, autonomy level, and settings - Add AgentType model for agent templates with model config and failover - Add AgentInstance model for running agents with status and memory - Add Issue model with external tracker sync (Gitea/GitHub/GitLab) - Add Sprint model with velocity tracking and lifecycle management - Add comprehensive Pydantic schemas with validation - Add full CRUD operations for all models with filtering/sorting - Add 280+ tests for models, schemas, and CRUD operations Implements #23, #24, #25, #26, #27 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
# app/models/syndarix/issue.py
|
|
"""
|
|
Issue model for Syndarix AI consulting platform.
|
|
|
|
An Issue represents a unit of work that can be assigned to agents or humans,
|
|
with optional synchronization to external issue trackers (Gitea, GitHub, GitLab).
|
|
"""
|
|
|
|
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Index, Integer, String, Text
|
|
from sqlalchemy.dialects.postgresql import (
|
|
JSONB,
|
|
UUID as PGUUID,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
|
|
|
from .enums import IssuePriority, IssueStatus, SyncStatus
|
|
|
|
|
|
class Issue(Base, UUIDMixin, TimestampMixin):
|
|
"""
|
|
Issue model representing a unit of work in a project.
|
|
|
|
Features:
|
|
- Standard issue fields (title, body, status, priority)
|
|
- Assignment to agent instances or human assignees
|
|
- Sprint association for backlog management
|
|
- External tracker synchronization (Gitea, GitHub, GitLab)
|
|
"""
|
|
|
|
__tablename__ = "issues"
|
|
|
|
# Foreign key to project
|
|
project_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# Issue content
|
|
title = Column(String(500), nullable=False)
|
|
body = Column(Text, nullable=False, default="")
|
|
|
|
# Status and priority
|
|
status: Column[IssueStatus] = Column(
|
|
Enum(IssueStatus),
|
|
default=IssueStatus.OPEN,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
priority: Column[IssuePriority] = Column(
|
|
Enum(IssuePriority),
|
|
default=IssuePriority.MEDIUM,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# Labels for categorization (e.g., ["bug", "frontend", "urgent"])
|
|
labels = Column(JSONB, default=list, nullable=False)
|
|
|
|
# Assignment - either to an agent or a human (mutually exclusive)
|
|
assigned_agent_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("agent_instances.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
# Human assignee (username or email, not a FK to allow external users)
|
|
human_assignee = Column(String(255), nullable=True, index=True)
|
|
|
|
# Sprint association
|
|
sprint_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("sprints.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
# Story points for estimation
|
|
story_points = Column(Integer, nullable=True)
|
|
|
|
# External tracker integration
|
|
external_tracker = Column(
|
|
String(50),
|
|
nullable=True,
|
|
index=True,
|
|
) # 'gitea', 'github', 'gitlab'
|
|
|
|
external_id = Column(String(255), nullable=True) # External system's ID
|
|
external_url = Column(String(1000), nullable=True) # Link to external issue
|
|
external_number = Column(Integer, nullable=True) # Issue number (e.g., #123)
|
|
|
|
# Sync status with external tracker
|
|
sync_status: Column[SyncStatus] = Column(
|
|
Enum(SyncStatus),
|
|
default=SyncStatus.SYNCED,
|
|
nullable=False,
|
|
# Note: Index defined in __table_args__ as ix_issues_sync_status
|
|
)
|
|
|
|
last_synced_at = Column(DateTime(timezone=True), nullable=True)
|
|
external_updated_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
# Lifecycle timestamp
|
|
closed_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
|
|
|
# Relationships
|
|
project = relationship("Project", back_populates="issues")
|
|
assigned_agent = relationship(
|
|
"AgentInstance",
|
|
back_populates="assigned_issues",
|
|
foreign_keys=[assigned_agent_id],
|
|
)
|
|
sprint = relationship("Sprint", back_populates="issues")
|
|
|
|
__table_args__ = (
|
|
Index("ix_issues_project_status", "project_id", "status"),
|
|
Index("ix_issues_project_priority", "project_id", "priority"),
|
|
Index("ix_issues_project_sprint", "project_id", "sprint_id"),
|
|
Index("ix_issues_external_tracker_id", "external_tracker", "external_id"),
|
|
Index("ix_issues_sync_status", "sync_status"),
|
|
Index("ix_issues_project_agent", "project_id", "assigned_agent_id"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<Issue {self.id} title='{self.title[:30]}...' "
|
|
f"status={self.status.value} priority={self.priority.value}>"
|
|
)
|