feat: retire the sketch/doodle WIP title tier, and the review strip asks "Is this a WIP?" (milestone 430)
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m35s
CI and images / smoke-web (push) Successful in 56s
CI and images / promote (push) Successful in 1s

The soft tier (#1474) tagged `wip` on any post titled sketch, doodle or
scribble: 6,096 of the library's 8,876 wip tags. Its conflict audit flagged
most of them, because finished art scores >= 0.5 on some content head,
which filled the Gallery strip with 2,086 cards. A "sketch" is usually
finished work, so the operator retired the tier. The artist's own
"WIP" / "work in progress" title rule and human wip tags stay.

- Removed: the soft matcher, source and prefilter; the importer and backfill
  soft branches; soft_wip_conflict_audit with its task and beat; the
  wip_soft_title_tagging_enabled setting, API field and toggle; and
  wip_title_soft from _AUTO_SOURCES.
- Migration 0112: a soft tag the operator confirmed, or kept from the strip,
  becomes `manual`. Every other soft tag is deleted, along with the open
  review cards whose tag is gone. The column is dropped.
- Review strip (#4424): each card asks "Is this a WIP?" (or banner, and so on),
  and the buttons read "Is a WIP" / "Is not a WIP". The content tag it also
  scored on is shown as the reason it was flagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-25 08:20:28 -04:00
co-authored by Claude Opus 5.5
parent 83e1382812
commit 7b1570f2a5
16 changed files with 220 additions and 283 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Migration 0112 (milestone 430, #4428): retiring the sketch/doodle WIP title tier.
Runs the migration's data step against real rows. The soft tags the operator stood
behind (confirmed, or kept through the review strip) survive as `manual`; the rest go,
and so do the unresolved review cards whose tag went with them. Hard title tags and
their flags are untouched.
"""
import importlib.util
from datetime import UTC, datetime
from pathlib import Path
import pytest
from sqlalchemy import select
from backend.app.models import PresentationReview, TagPositiveConfirmation
from backend.app.models.tag import image_tag
from backend.app.services.wip_title import resolve_wip_tag_id
from tests.factories import make_image as _img
pytestmark = pytest.mark.integration
_MIGRATION = (
Path(__file__).resolve().parents[1]
/ "alembic" / "versions" / "0112_retire_soft_wip_title.py"
)
def _retire():
spec = importlib.util.spec_from_file_location("m0112", _MIGRATION)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.retire_soft_wip_tags
def _source(db, image_id, tag_id):
return db.execute(
select(image_tag.c.source)
.where(image_tag.c.image_record_id == image_id)
.where(image_tag.c.tag_id == tag_id)
).scalar_one_or_none()
def _review(db, image_id, tag_id):
return db.execute(
select(PresentationReview)
.where(PresentationReview.image_record_id == image_id)
.where(PresentationReview.tag_id == tag_id)
).scalar_one_or_none()
def _flag(db, image_id, tag_id, *, resolved):
db.add(PresentationReview(
image_record_id=image_id, tag_id=tag_id, conflict_score=0.8, mode="process",
resolved_at=datetime.now(UTC) if resolved else None,
))
def test_retire_soft_wip_keeps_human_judged_and_clears_the_rest(db_sync):
wip = resolve_wip_tag_id(db_sync)
plain = _img(db_sync, "a" * 64) # soft, never looked at → removed
confirmed = _img(db_sync, "b" * 64) # soft + confirmed → kept as manual
kept = _img(db_sync, "c" * 64) # soft + "Keep tag" in the strip → manual
pending = _img(db_sync, "d" * 64) # soft + open card → tag and card removed
hard = _img(db_sync, "e" * 64) # hard title tag + open card → untouched
for img in (plain, confirmed, kept, pending):
db_sync.execute(image_tag.insert().values(
image_record_id=img.id, tag_id=wip, source="wip_title_soft"))
db_sync.execute(image_tag.insert().values(
image_record_id=hard.id, tag_id=wip, source="wip_title"))
db_sync.add(TagPositiveConfirmation(image_record_id=confirmed.id, tag_id=wip))
_flag(db_sync, kept.id, wip, resolved=True)
_flag(db_sync, pending.id, wip, resolved=False)
_flag(db_sync, hard.id, wip, resolved=False)
db_sync.flush()
_retire()(db_sync.connection())
db_sync.expire_all()
assert _source(db_sync, plain.id, wip) is None
assert _source(db_sync, confirmed.id, wip) == "manual"
assert _source(db_sync, kept.id, wip) == "manual"
assert _source(db_sync, pending.id, wip) is None
assert _source(db_sync, hard.id, wip) == "wip_title"
assert _review(db_sync, pending.id, wip) is None
assert _review(db_sync, kept.id, wip) is not None # resolved history stays
assert _review(db_sync, hard.id, wip) is not None # its tag is still on
assert db_sync.execute(
select(image_tag.c.image_record_id).where(image_tag.c.source == "wip_title_soft")
).first() is None