Add values_callable to all enum columns so SQLAlchemy serializes using the enum's .value (lowercase) instead of .name (uppercase). PostgreSQL enum types defined in migrations use lowercase values. Fixes: invalid input value for enum autonomy_level: "MILESTONE" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
191 lines
5.6 KiB
Python
191 lines
5.6 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, name="issue_type", values_callable=lambda x: [e.value for e in x]
|
|
),
|
|
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,
|
|
name="issue_status",
|
|
values_callable=lambda x: [e.value for e in x],
|
|
),
|
|
default=IssueStatus.OPEN,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
priority: Column[IssuePriority] = Column(
|
|
Enum(
|
|
IssuePriority,
|
|
name="issue_priority",
|
|
values_callable=lambda x: [e.value for e in x],
|
|
),
|
|
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,
|
|
name="sync_status",
|
|
values_callable=lambda x: [e.value for e in x],
|
|
),
|
|
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}>"
|
|
)
|