forked from cardosofelipe/pragma-stack
Reformatted multiline function calls, object definitions, and queries for improved code readability and consistency. Adjusted imports and constraints where necessary.
177 lines
5.2 KiB
Python
177 lines
5.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,
|
|
Date,
|
|
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, IssueType, 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,
|
|
)
|
|
|
|
# Parent issue for hierarchy (Epic -> Story -> Task)
|
|
parent_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("issues.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
# Issue type (Epic, Story, Task, Bug)
|
|
type: Column[IssueType] = Column(
|
|
Enum(IssueType),
|
|
default=IssueType.TASK,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# Reporter (who created this issue - can be user or agent)
|
|
reporter_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
nullable=True, # System-generated issues may have no reporter
|
|
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)
|
|
|
|
# Due date for the issue
|
|
due_date = Column(Date, nullable=True, index=True)
|
|
|
|
# External tracker integration
|
|
external_tracker_type = Column(
|
|
String(50),
|
|
nullable=True,
|
|
index=True,
|
|
) # 'gitea', 'github', 'gitlab'
|
|
|
|
external_issue_id = Column(String(255), nullable=True) # External system's ID
|
|
remote_url = Column(String(1000), nullable=True) # Link to external issue
|
|
external_issue_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")
|
|
parent = relationship("Issue", remote_side="Issue.id", backref="children")
|
|
|
|
__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_type",
|
|
"external_issue_id",
|
|
),
|
|
Index("ix_issues_sync_status", "sync_status"),
|
|
Index("ix_issues_project_agent", "project_id", "assigned_agent_id"),
|
|
Index("ix_issues_project_type", "project_id", "type"),
|
|
Index("ix_issues_project_status_priority", "project_id", "status", "priority"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<Issue {self.id} title='{self.title[:30]}...' "
|
|
f"status={self.status.value} priority={self.priority.value}>"
|
|
)
|