"""Project inception: the decision record + always-on rulebook exclusions (milestone 297) Revision ID: 0085 Revises: 0084 Create Date: 2026-08-22 `projects.inception` is the WHY a project inherits what it does — NULL until someone decides, at which point enter_project stops asking. The new association `project_rulebook_exclusions` is the opt-out of a whole always-on rulebook for one project (the sibling of the rule/topic suppressions). Backfill: every project that exists when this runs is stamped via="legacy" with its CURRENT standing (no exclusions, its subscriptions, its design_system_id, no seed) — so the ask fires only for projects created after the step shipped, and nothing a running install relies on changes. """ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision = "0085" down_revision = "0084" branch_labels = None depends_on = None def upgrade() -> None: op.add_column( "projects", sa.Column("inception", postgresql.JSONB(), nullable=True), ) op.create_table( "project_rulebook_exclusions", sa.Column( "project_id", sa.BigInteger(), sa.ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True, nullable=False, ), sa.Column( "rulebook_id", sa.BigInteger(), sa.ForeignKey("rulebooks.id", ondelete="CASCADE"), primary_key=True, nullable=False, ), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), ) # Legacy stamp: what each existing project inherits today, recorded as a # decision so the inception ask does not fire on a project that has been # running for months. op.execute(sa.text(""" UPDATE projects p SET inception = jsonb_build_object( 'via', 'legacy', 'decided_at', to_jsonb(now()), 'decided_by', NULL, 'choices', jsonb_build_object( 'exclude_always_on_rulebooks', '[]'::jsonb, 'subscribe_rulebooks', COALESCE( (SELECT jsonb_agg(s.rulebook_id ORDER BY s.rulebook_id) FROM project_rulebook_subscriptions s WHERE s.project_id = p.id), '[]'::jsonb), 'design_system_id', to_jsonb(p.design_system_id), 'seed_systems', false ) ) WHERE p.inception IS NULL """)) def downgrade() -> None: op.drop_table("project_rulebook_exclusions") op.drop_column("projects", "inception")