feat(ml): auto-applied tags don't train a head unless confirmed (milestone 139)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m41s

Makes auto-apply truly "soft" for heads: _ids_with_tag (head positives) and
_eligible_tag_ids (graduation count) now count human-applied + operator-confirmed
tags only, via a shared _AUTO_SOURCES (head_auto/ccip_auto/ml_auto) exclusion.
Unconfirmed auto-applied tags no longer train the head that judges them, so a
misfire can't reinforce itself and the retraction sweep can actually drop it.
Confirming a tag (TagPositiveConfirmation) promotes it to a positive AND protects
it from retraction. sklearn-free tests. CCIP reference exclusion is the companion
piece, next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 18:28:25 -04:00
parent 0de726ed48
commit 2d44a26bdf
3 changed files with 108 additions and 6 deletions
+27 -2
View File
@@ -17,9 +17,20 @@ from typing import Any
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from ...models import ImageRecord, Tag, TagSuggestionRejection
from ...models import (
ImageRecord,
Tag,
TagPositiveConfirmation,
TagSuggestionRejection,
)
from ...models.tag import image_tag
# Auto-apply sources whose tags are PROVISIONAL: they never train a head (or seed
# a CCIP reference) unless the operator confirms them (milestone 139). Keeping
# auto-applied predictions out of training is what makes them "soft" — a misfire
# can't reinforce itself, so the retraction sweep can actually drop it.
_AUTO_SOURCES = ("head_auto", "ccip_auto", "ml_auto")
def _hygiene_excluded_ids(session: Session) -> set[int]:
"""Ids of images carrying ANY system tag (wip / banner / editor
@@ -45,9 +56,23 @@ def _hygiene_excluded_ids(session: Session) -> set[int]:
def _ids_with_tag(session: Session, tag_id: int) -> list[int]:
"""Image ids that count as POSITIVES for this tag's head: human-applied
(manual / accepted) tags PLUS any auto-applied tag the operator explicitly
confirmed (TagPositiveConfirmation). Unconfirmed auto-applied tags are
EXCLUDED — they are provisional and must not train the head that judges
them (milestone 139)."""
confirmed = (
select(TagPositiveConfirmation.image_record_id)
.where(TagPositiveConfirmation.tag_id == tag_id)
)
return [
r[0] for r in session.execute(
select(image_tag.c.image_record_id).where(image_tag.c.tag_id == tag_id)
select(image_tag.c.image_record_id)
.where(image_tag.c.tag_id == tag_id)
.where(
image_tag.c.source.not_in(_AUTO_SOURCES)
| image_tag.c.image_record_id.in_(confirmed)
)
).all()
]