diff --git a/alembic/versions/20260529_0036_ain303_outcome_source.py b/alembic/versions/20260529_0036_ain303_outcome_source.py new file mode 100644 index 0000000..15e6bf3 --- /dev/null +++ b/alembic/versions/20260529_0036_ain303_outcome_source.py @@ -0,0 +1,50 @@ +"""AIN-303 · routing_outcomes.source — synthetic/shadow tagging (INVARIANT 1). + +Revision ID: 20260529_0036 +Revises: 20260529_0035 +Create Date: 2026-05-29 + +The synthetic cold-start loop writes outcomes that must NEVER feed a prod +routing-policy promotion. This adds a `source` discriminator so the wall is +enforceable in SQL: prod-policy refits filter `source = 'prod'`; the synthetic +warmup loop tags its rows `source = 'synthetic'` (and shadow-replay rows +`'shadow'`). Existing rows are real traffic → backfilled to 'prod'. + +Additive: NOT NULL with server_default 'prod' (existing 147 rows become 'prod'), +a CHECK constraint pinning the vocabulary, and an index for the source-filtered +corpus reads. No scoring/auth/candidate-set change (Disc #12 intact). +""" + +from __future__ import annotations + +import sqlalchemy as sa + +from alembic import op + +revision = "20260529_0036" +down_revision = "20260529_0035" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "routing_outcomes", + sa.Column("source", sa.Text(), nullable=False, server_default="prod"), + ) + op.create_check_constraint( + "ck_routing_outcomes_source", + "routing_outcomes", + "source IN ('prod','synthetic','shadow')", + ) + op.create_index( + "ix_routing_outcomes_source", + "routing_outcomes", + ["source"], + ) + + +def downgrade() -> None: + op.drop_index("ix_routing_outcomes_source", table_name="routing_outcomes") + op.drop_constraint("ck_routing_outcomes_source", "routing_outcomes", type_="check") + op.drop_column("routing_outcomes", "source")