from sqlalchemy import Index, Integer, Text, text from sqlalchemy.orm import Mapped, mapped_column from scribe.models import Base from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin): """A GLOBAL area name — the shared vocabulary every project's Systems can point at (milestone 307, decision note 3026). A `System` is per-project and NOT NULL on project_id, so nothing outside a project can reference one: a rule that applies across projects has no way to say "this is about CI" without chaining itself to one project's row. This table is that join key. It carries no `user_id` on purpose — a shared project must INHERIT the vocabulary rather than re-earn it, so the catalog is global and the same word means the same thing in every install. It is a convergence aid, never a gate: `systems.canonical_id` is nullable, an unmapped System stays fully usable, and `record_systems` never sees this table at all — the local name is a legitimate local label and is never rewritten to match. `slug` is the match key, not a display value. It folds the spelling differences that produced four names for one area on the author's own instance ("CI & Release" / "CI and Release" / "CI & release"): an exact slug hit maps automatically, and anything short of that becomes a proposal for a human to confirm. See services/canonical_systems.canonical_slug. """ __tablename__ = "canonical_systems" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(Text, nullable=False) # Normalized match key — unique among LIVE rows, so a soft-deleted entry # doesn't block recreating the same area (the partial-unique convention # rules/topics already use). slug: Mapped[str] = mapped_column(Text, nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) order_index: Mapped[int] = mapped_column(Integer, default=0, server_default="0") __table_args__ = ( Index( "uq_canonical_systems_slug", "slug", unique=True, postgresql_where=text("deleted_at IS NULL"), ), ) def to_dict(self) -> dict: return { "id": self.id, "name": self.name, "slug": self.slug, "description": self.description, "order_index": self.order_index, "created_at": iso(self.created_at), "updated_at": iso(self.updated_at), }