"""Duplicate clusters, their members, and not-duplicate negative links. A cluster is a reviewable grouping produced by a detection ``method`` (exact, pixel, or perceptual) at a ``confidence`` band. Exact/pixel clusters can be decided automatically; perceptual clusters stay open for human review. Every member carries the evidence behind its inclusion. Negative links record ``not_duplicate`` decisions so rescans do not re-suggest a rejected pair. """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column from photo_pipeline.db import Base class DuplicateCluster(Base): __tablename__ = "duplicate_clusters" id: Mapped[str] = mapped_column(String, primary_key=True) method: Mapped[str] = mapped_column(String, nullable=False) # exact|pixel|perceptual confidence: Mapped[str] = mapped_column(String, nullable=False) # band name state: Mapped[str] = mapped_column(String, nullable=False) # open|decided|dismissed|reopened decision: Mapped[str | None] = mapped_column(String) # canonical|not_duplicate|deferred canonical_asset_id: Mapped[str | None] = mapped_column(ForeignKey("assets.id")) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class DuplicateMember(Base): __tablename__ = "duplicate_members" cluster_id: Mapped[str] = mapped_column( ForeignKey("duplicate_clusters.id"), primary_key=True ) asset_id: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True) role: Mapped[str] = mapped_column(String, nullable=False, default="member") # member|canonical|variant distance: Mapped[int | None] = mapped_column(Integer) # phash distance to representative evidence: Mapped[str | None] = mapped_column(String) # JSON blob explaining inclusion added_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class DuplicateNegativeLink(Base): __tablename__ = "duplicate_negative_links" asset_a: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True) asset_b: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True) reason: Mapped[str | None] = mapped_column(String) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )