From 05e379263a80e9762dc0e1f9b758adc2a07966a2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 21:16:24 -0400 Subject: [PATCH] =?UTF-8?q?feat(rulebook):=20migration=200055=20=E2=80=94?= =?UTF-8?q?=20rulebooks,=20topics,=20rules,=20subscriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alembic/versions/0055_rulebook_tables.py | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 alembic/versions/0055_rulebook_tables.py diff --git a/alembic/versions/0055_rulebook_tables.py b/alembic/versions/0055_rulebook_tables.py new file mode 100644 index 0000000..54f4846 --- /dev/null +++ b/alembic/versions/0055_rulebook_tables.py @@ -0,0 +1,153 @@ +"""rulebook tables: rulebooks, rulebook_topics, rules, project_rulebook_subscriptions + +Revision ID: 0055 +Revises: 0054 +Create Date: 2026-05-27 + +Adds the Scribe Rulebook hierarchy as a fourth top-level entity, sibling +to Project. Rules carry a structural (statement, why, how_to_apply) +triple. Projects subscribe to Rulebooks (many-to-many). See the design +doc at docs/superpowers/specs/2026-05-27-scribe-rulebook-design.md. +""" +from alembic import op +import sqlalchemy as sa + + +revision = "0055" +down_revision = "0054" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "rulebooks", + sa.Column("id", sa.BigInteger, primary_key=True), + sa.Column( + "owner_user_id", + sa.BigInteger, + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("title", sa.Text, nullable=False), + sa.Column("description", sa.Text), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + ) + op.create_index("ix_rulebooks_owner", "rulebooks", ["owner_user_id"]) + + op.create_table( + "rulebook_topics", + sa.Column("id", sa.BigInteger, primary_key=True), + sa.Column( + "rulebook_id", + sa.BigInteger, + sa.ForeignKey("rulebooks.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("title", sa.Text, nullable=False), + sa.Column("description", sa.Text), + sa.Column( + "order_index", sa.Integer, nullable=False, server_default="0", + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.UniqueConstraint("rulebook_id", "title", name="uq_topic_per_rulebook"), + ) + op.create_index( + "ix_rulebook_topics_rulebook", "rulebook_topics", ["rulebook_id"], + ) + + op.create_table( + "rules", + sa.Column("id", sa.BigInteger, primary_key=True), + sa.Column( + "topic_id", + sa.BigInteger, + sa.ForeignKey("rulebook_topics.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("title", sa.Text, nullable=False), + sa.Column("statement", sa.Text, nullable=False), + sa.Column("why", sa.Text), + sa.Column("how_to_apply", sa.Text), + sa.Column( + "order_index", sa.Integer, nullable=False, server_default="0", + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.UniqueConstraint("topic_id", "title", name="uq_rule_per_topic"), + ) + op.create_index("ix_rules_topic", "rules", ["topic_id"]) + + op.create_table( + "project_rulebook_subscriptions", + sa.Column( + "project_id", + sa.BigInteger, + sa.ForeignKey("projects.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "rulebook_id", + sa.BigInteger, + sa.ForeignKey("rulebooks.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.PrimaryKeyConstraint("project_id", "rulebook_id"), + ) + op.create_index( + "ix_project_rulebook_subs_rulebook", + "project_rulebook_subscriptions", + ["rulebook_id"], + ) + + +def downgrade() -> None: + op.drop_index( + "ix_project_rulebook_subs_rulebook", + table_name="project_rulebook_subscriptions", + ) + op.drop_table("project_rulebook_subscriptions") + op.drop_index("ix_rules_topic", table_name="rules") + op.drop_table("rules") + op.drop_index("ix_rulebook_topics_rulebook", table_name="rulebook_topics") + op.drop_table("rulebook_topics") + op.drop_index("ix_rulebooks_owner", table_name="rulebooks") + op.drop_table("rulebooks")