init commit
This commit is contained in:
221
backend/app/models.py
Normal file
221
backend/app/models.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import enum
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Enum, ForeignKey, Index, Integer, String, Text, Boolean, DateTime, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class Priority(str, enum.Enum):
|
||||
critical = "critical"
|
||||
high = "high"
|
||||
medium = "medium"
|
||||
low = "low"
|
||||
|
||||
|
||||
class Status(str, enum.Enum):
|
||||
idea = "idea"
|
||||
draft = "draft"
|
||||
todo = "todo"
|
||||
in_progress = "in_progress"
|
||||
done = "done"
|
||||
|
||||
|
||||
class NodeType(str, enum.Enum):
|
||||
pillar = "pillar"
|
||||
requirement = "requirement"
|
||||
sub_requirement = "sub_requirement"
|
||||
leaf = "leaf"
|
||||
planning = "planning"
|
||||
|
||||
|
||||
class CodeLanguage(str, enum.Enum):
|
||||
python = "python"
|
||||
cpp = "cpp"
|
||||
typescript = "typescript"
|
||||
|
||||
|
||||
class TestStatus(str, enum.Enum):
|
||||
not_written = "not_written"
|
||||
written = "written"
|
||||
passing = "passing"
|
||||
failing = "failing"
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class RequirementNode(Base):
|
||||
__tablename__ = "requirement_nodes"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
priority: Mapped[Priority] = mapped_column(Enum(Priority), default=Priority.medium)
|
||||
phase: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status: Mapped[Status] = mapped_column(Enum(Status), default=Status.draft)
|
||||
node_type: Mapped[NodeType] = mapped_column(Enum(NodeType), default=NodeType.requirement)
|
||||
parent_id: Mapped[str | None] = mapped_column(
|
||||
String(64), ForeignKey("requirement_nodes.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
project_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
|
||||
# Quality gate flags
|
||||
traceable: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
measurable: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
consistent: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
single_purpose: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Issue tracker integration
|
||||
gitea_issue_number: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gitea_repo: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
|
||||
# Acceptance criteria and code (all node types)
|
||||
acceptance_criteria: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
test_spec: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
test_code: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
impl_code: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
code_language: Mapped[CodeLanguage | None] = mapped_column(
|
||||
Enum(CodeLanguage), nullable=True
|
||||
)
|
||||
target_test_file: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
target_impl_file: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
test_status: Mapped[TestStatus] = mapped_column(
|
||||
Enum(TestStatus), default=TestStatus.not_written
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
# Relationships
|
||||
parent: Mapped["RequirementNode | None"] = relationship(
|
||||
"RequirementNode", remote_side="RequirementNode.id", back_populates="children"
|
||||
)
|
||||
children: Mapped[list["RequirementNode"]] = relationship(
|
||||
"RequirementNode", back_populates="parent", cascade="all, delete-orphan",
|
||||
order_by="RequirementNode.sort_order"
|
||||
)
|
||||
project: Mapped["Project | None"] = relationship("Project", back_populates="requirements")
|
||||
|
||||
# Cross-pillar links (outgoing)
|
||||
outgoing_links: Mapped[list["CrossPillarLink"]] = relationship(
|
||||
"CrossPillarLink", foreign_keys="CrossPillarLink.source_id",
|
||||
back_populates="source", cascade="all, delete-orphan"
|
||||
)
|
||||
incoming_links: Mapped[list["CrossPillarLink"]] = relationship(
|
||||
"CrossPillarLink", foreign_keys="CrossPillarLink.target_id",
|
||||
back_populates="target", cascade="all, delete-orphan"
|
||||
)
|
||||
history: Mapped[list["RequirementHistory"]] = relationship(
|
||||
"RequirementHistory", back_populates="requirement",
|
||||
cascade="all, delete-orphan", order_by="RequirementHistory.version"
|
||||
)
|
||||
|
||||
|
||||
class CrossPillarLink(Base):
|
||||
__tablename__ = "cross_pillar_links"
|
||||
|
||||
source_id: Mapped[str] = mapped_column(
|
||||
String(64), ForeignKey("requirement_nodes.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
target_id: Mapped[str] = mapped_column(
|
||||
String(64), ForeignKey("requirement_nodes.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
relationship_type: Mapped[str] = mapped_column(
|
||||
String(64), default="shared_parent"
|
||||
)
|
||||
|
||||
source: Mapped[RequirementNode] = relationship(
|
||||
"RequirementNode", foreign_keys=[source_id], back_populates="outgoing_links"
|
||||
)
|
||||
target: Mapped[RequirementNode] = relationship(
|
||||
"RequirementNode", foreign_keys=[target_id], back_populates="incoming_links"
|
||||
)
|
||||
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
repo_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
repo_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
requirements_yaml_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
requirements: Mapped[list[RequirementNode]] = relationship(
|
||||
"RequirementNode", back_populates="project", cascade="all, delete-orphan"
|
||||
)
|
||||
layouts: Mapped[list["GraphLayout"]] = relationship(
|
||||
"GraphLayout", back_populates="project", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class RequirementHistory(Base):
|
||||
__tablename__ = "requirement_history"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("requirement_id", "version", name="uq_requirement_version"),
|
||||
Index("idx_req_history_req_version", "requirement_id", "version"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
requirement_id: Mapped[str] = mapped_column(
|
||||
String(64), ForeignKey("requirement_nodes.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# Snapshot of editable fields
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
priority: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
phase: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
node_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
acceptance_criteria: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
test_spec: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
test_code: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
impl_code: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
code_language: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
target_test_file: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
target_impl_file: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
measurable: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
consistent: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
single_purpose: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
gitea_issue_number: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gitea_repo: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
|
||||
# Metadata
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
change_summary: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
|
||||
requirement: Mapped[RequirementNode] = relationship(
|
||||
"RequirementNode", back_populates="history"
|
||||
)
|
||||
|
||||
|
||||
class GraphLayout(Base):
|
||||
__tablename__ = "graph_layouts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
project_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
node_positions: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
viewport: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
project: Mapped["Project | None"] = relationship("Project", back_populates="layouts")
|
||||
Reference in New Issue
Block a user