"""HeadMetricsSnapshot — a daily per-concept time-series point (#114). The "amount of change over time" reporting the operator asked for: once a day, record each concept's auto-applied VOLUME (current head_auto tags), cumulative misfires/under-fires, and the head's measured quality. Plotting these rows over time shows whether auto-apply is landing better/worse and whether tagging more is sharpening a concept — the signal for tuning the precision target + support floor. """ from datetime import datetime from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column from .base import Base class HeadMetricsSnapshot(Base): __tablename__ = "head_metrics_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True) tag_id: Mapped[int] = mapped_column( ForeignKey("tag.id", ondelete="CASCADE"), index=True ) # Denormalized so a snapshot stays readable even if the tag is later renamed. name: Mapped[str] = mapped_column(String(255), nullable=False) snapshot_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), index=True ) # Current count of source='head_auto' applications still standing. n_auto_applied: Mapped[int] = mapped_column(Integer, nullable=False, default=0) n_misfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0) n_underfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # The head's measured quality at snapshot time (null if no head exists). ap: Mapped[float | None] = mapped_column(Float, nullable=True) precision_cv: Mapped[float | None] = mapped_column(Float, nullable=True) recall: Mapped[float | None] = mapped_column(Float, nullable=True) n_pos: Mapped[int | None] = mapped_column(Integer, nullable=True)