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")