"""HeadMetric — running correction counters per concept (#114 observability). Earned auto-apply fires graduated heads; to TUNE it we need to know how often a head's auto-applied tag was wrong (the operator removed it = a MISFIRE) and how often the operator had to add a tag a head exists for by hand (an UNDER-FIRE, the head missed it). image_tag.source is lost when a row is deleted, so these are captured as durable cumulative counters at correction time — they survive head retrain/prune (keyed by tag, not by the head row). The daily snapshot reads them into the time-series. """ from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Integer, func from sqlalchemy.orm import Mapped, mapped_column from .base import Base class HeadMetric(Base): __tablename__ = "head_metric" tag_id: Mapped[int] = mapped_column( ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True ) # An auto-applied (source='head_auto') tag the operator later REMOVED. n_misfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # A tag with a head that the operator added by HAND (the head missed it). n_underfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )