feat(tags): system tags — is_system column, seeded hygiene tags, protection guards
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Failing after 4m49s

Training hygiene step 1 (milestone #128). Migration 0075 adds
tag.is_system and seeds wip / banner / editor screenshot (kind=general),
ADOPTING an existing same-(name,kind) tag case-insensitively instead of
duplicating. These rows drive the upcoming training exclusions, so they
are protected: rename and merge-away refuse system tags (merge-INTO
stays allowed — folding an operator's old hygiene tag into the system
row is the intended move; merge is the only tag-delete path, so that
guard covers deletion). is_system rides every tag serialization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-02 23:14:49 -04:00
parent 581b778528
commit e9891ee9f3
7 changed files with 187 additions and 7 deletions
+60
View File
@@ -0,0 +1,60 @@
"""tag.is_system + seed the three hygiene system tags
Training hygiene (operator 2026-07-03, milestone #128): rough WIPs tagged as a
character poison that character's head and CCIP references; banners/editor
screenshots pollute whole-image similarity. The fix keys on SYSTEM tags the
product ships — not operator configuration — so the seed lives here.
Seeding ADOPTS an existing same-(name, kind=general) tag (case-insensitive,
matching TagService.rename's collision stance) instead of inserting a
duplicate, so an operator who already tagged `wip` keeps their applications.
Revision ID: 0075
Revises: 0074
Create Date: 2026-07-03
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0075"
down_revision: Union[str, None] = "0074"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
SYSTEM_TAG_NAMES = ("wip", "banner", "editor screenshot")
def upgrade() -> None:
op.add_column(
"tag",
sa.Column(
"is_system", sa.Boolean(), nullable=False,
server_default=sa.false(),
),
)
conn = op.get_bind()
for name in SYSTEM_TAG_NAMES:
adopted = conn.execute(
sa.text(
"UPDATE tag SET is_system = true "
"WHERE lower(name) = lower(:name) AND kind = 'general'"
),
{"name": name},
)
if adopted.rowcount == 0:
conn.execute(
sa.text(
"INSERT INTO tag (name, kind, is_system) "
"VALUES (:name, 'general', true)"
),
{"name": name},
)
def downgrade() -> None:
# The seeded rows survive as ordinary general tags — dropping the flag is
# enough to disarm the mechanism, and deleting rows would orphan any
# operator applications made while the flag existed.
op.drop_column("tag", "is_system")