222 lines
9.1 KiB
Python
222 lines
9.1 KiB
Python
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")
|