Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions alembic/versions/20260529_0036_ain303_outcome_source.py
Original file line number Diff line number Diff line change
@@ -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")
Loading