"""rule_moment_judgments — whether a rule belongs on a moment, and who said so (milestone 458 step 7, #4925) Revision ID: 0118 Revises: 0117 Create Date: 2026-10-05 One row per (rule, moment) with a state: `suggested` while a proposal waits for a judgment, `confirmed` or `rejected` once one is made. The mount itself stays in rule_moments; this records the proposals that are not mounts yet and the rejections that must stop them being proposed again. Moment '' is "no moment fits this rule". CASCADE on the rule — a judgment about a rule that no longer exists says nothing. No backfill: the mounts made before this table were judgments too, but recording them now would invent who made them. """ import sqlalchemy as sa from sqlalchemy.dialects import postgresql from alembic import op revision = "0118" down_revision = "0117" branch_labels = None depends_on = None # One place each, so the CHECKs and the model's JUDGMENT_STATES / SOURCES # cannot drift (rule 36: a new value later means DROP + ADD CONSTRAINT in the # same migration). _STATES = ("suggested", "confirmed", "rejected") _SOURCES = ("pass", "signal", "edit") def upgrade() -> None: op.create_table( "rule_moment_judgments", sa.Column("id", sa.BigInteger(), primary_key=True), sa.Column("rule_id", sa.BigInteger(), sa.ForeignKey("rules.id", ondelete="CASCADE"), nullable=False), sa.Column("moment", sa.Text(), nullable=False), sa.Column("state", sa.Text(), nullable=False, server_default="suggested"), sa.Column("source", sa.Text(), nullable=False, server_default="pass"), sa.Column("note", sa.Text(), nullable=True), sa.Column("evidence", postgresql.JSONB(), nullable=True), sa.Column("judged_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.UniqueConstraint("rule_id", "moment", name="uq_rule_moment_judgments_pair"), ) op.create_check_constraint( "ck_rule_moment_judgments_state", "rule_moment_judgments", "state IN (" + ", ".join(f"'{s}'" for s in _STATES) + ")", ) op.create_check_constraint( "ck_rule_moment_judgments_source", "rule_moment_judgments", "source IN (" + ", ".join(f"'{s}'" for s in _SOURCES) + ")", ) op.create_index("ix_rule_moment_judgments_rule_id", "rule_moment_judgments", ["rule_id"]) def downgrade() -> None: op.drop_index("ix_rule_moment_judgments_rule_id", table_name="rule_moment_judgments") op.drop_constraint("ck_rule_moment_judgments_source", "rule_moment_judgments", type_="check") op.drop_constraint("ck_rule_moment_judgments_state", "rule_moment_judgments", type_="check") op.drop_table("rule_moment_judgments")