"""repository identity, protected slugs, and rename journal (STATE-WP-0085-T02) The existing repository UUID remains the durable State Hub identity. This migration adds only new rows around it: an explicit unverified Forge identity, one canonical slug record, and empty operation history. It deliberately does not infer Forge coordinates from a slug, remote URL, checkout, or Git commit. Revision ID: f3c4d5e6a7b8 Revises: e2b3c4d5f6a7 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB, UUID revision = "f3c4d5e6a7b8" down_revision = "e2b3c4d5f6a7" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "repository_forge_identities", sa.Column( "id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()"), ), sa.Column( "repo_id", UUID(as_uuid=True), sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("provider", sa.String(length=40), nullable=True), sa.Column("forge_instance", sa.String(length=255), nullable=True), sa.Column("forge_owner", sa.String(length=255), nullable=True), sa.Column("forge_repository_id", sa.BigInteger(), nullable=True), sa.Column( "verification_state", sa.String(length=20), nullable=False, server_default="unverified", ), sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True), sa.Column("verified_by", sa.String(length=160), nullable=True), sa.Column( "verification_evidence", JSONB(), nullable=False, server_default="{}" ), 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( "repo_id", name="uq_repository_forge_identities_repo_id" ), sa.UniqueConstraint( "id", "repo_id", "verification_state", name="uq_repository_forge_identity_verified_ref", ), sa.UniqueConstraint( "provider", "forge_instance", "forge_owner", "forge_repository_id", name="uq_repository_forge_identity_tuple", ), sa.CheckConstraint( "verification_state IN ('unverified', 'verified')", name="ck_repository_forge_identity_state", ), sa.CheckConstraint( "forge_repository_id IS NULL OR forge_repository_id > 0", name="ck_repository_forge_id_positive", ), sa.CheckConstraint( "verification_state != 'verified' OR " "(provider IS NOT NULL AND forge_instance IS NOT NULL " "AND forge_owner IS NOT NULL AND forge_repository_id IS NOT NULL " "AND verified_at IS NOT NULL AND verified_by IS NOT NULL)", name="ck_repository_forge_verified_complete", ), sa.CheckConstraint( "verification_state = 'verified' OR " "(verified_at IS NULL AND verified_by IS NULL)", name="ck_repository_forge_unverified_has_no_attestation", ), ) op.create_table( "repository_rename_operations", sa.Column( "id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()"), ), sa.Column( "repo_id", UUID(as_uuid=True), sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("forge_identity_id", UUID(as_uuid=True), nullable=False), sa.Column( "forge_identity_state", sa.String(length=20), nullable=False, server_default="verified", ), sa.Column("expected_provider", sa.String(length=40), nullable=False), sa.Column( "expected_forge_instance", sa.String(length=255), nullable=False ), sa.Column("expected_forge_owner", sa.String(length=255), nullable=False), sa.Column("expected_forge_repository_id", sa.BigInteger(), nullable=False), sa.Column("expected_source_commit", sa.String(length=64), nullable=False), sa.Column("expected_default_branch", sa.String(length=255), nullable=False), sa.Column("old_slug", sa.String(length=100), nullable=False), sa.Column("new_slug", sa.String(length=100), nullable=False), sa.Column("old_coordinates", JSONB(), nullable=False), sa.Column("new_coordinates", JSONB(), nullable=False), sa.Column( "phase", sa.String(length=32), nullable=False, server_default="draft" ), sa.Column("actor", sa.String(length=160), nullable=False), sa.Column("phase_changed_at", sa.DateTime(timezone=True), nullable=False), sa.Column("preflighted_at", sa.DateTime(timezone=True), nullable=True), sa.Column("preflight_expires_at", sa.DateTime(timezone=True), nullable=True), sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("rolled_back_at", sa.DateTime(timezone=True), nullable=True), sa.Column("evidence", JSONB(), nullable=False, server_default="{}"), sa.Column("error_code", sa.String(length=80), nullable=True), sa.Column("error_message", sa.Text(), nullable=True), sa.Column("error_details", JSONB(), nullable=True), sa.Column("error_at", sa.DateTime(timezone=True), nullable=True), 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.CheckConstraint( "phase IN ('draft', 'preflighted', 'forge-renamed', " "'statehub-rebound', 'source-synced', 'consumers-verified', " "'completed', 'rollback-preflight', 'rolled-back')", name="ck_repository_rename_phase", ), sa.CheckConstraint( "old_slug != new_slug", name="ck_repository_rename_distinct_slugs" ), sa.CheckConstraint( "expected_forge_repository_id > 0", name="ck_repository_rename_forge_id_positive", ), sa.CheckConstraint( "forge_identity_state = 'verified'", name="ck_repository_rename_verified_identity", ), sa.CheckConstraint( "phase = 'draft' OR preflighted_at IS NOT NULL", name="ck_repository_rename_preflight_timestamp", ), sa.CheckConstraint( "phase != 'completed' OR completed_at IS NOT NULL", name="ck_repository_rename_completed_timestamp", ), sa.CheckConstraint( "phase != 'rolled-back' OR rolled_back_at IS NOT NULL", name="ck_repository_rename_rollback_timestamp", ), sa.ForeignKeyConstraint( ["forge_identity_id", "repo_id", "forge_identity_state"], [ "repository_forge_identities.id", "repository_forge_identities.repo_id", "repository_forge_identities.verification_state", ], name="fk_repository_rename_verified_identity", ondelete="RESTRICT", ), ) op.create_index( "ix_repository_rename_operations_repo_id", "repository_rename_operations", ["repo_id"], ) op.create_index( "ix_repository_rename_operations_old_slug", "repository_rename_operations", ["old_slug"], ) op.create_index( "ix_repository_rename_operations_new_slug", "repository_rename_operations", ["new_slug"], ) op.create_index( "ix_repository_rename_phase", "repository_rename_operations", ["phase"], ) op.create_index( "uq_repository_rename_active_repo", "repository_rename_operations", ["repo_id"], unique=True, postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"), ) op.create_index( "uq_repository_rename_active_new_slug", "repository_rename_operations", ["new_slug"], unique=True, postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"), ) op.create_table( "repository_slugs", sa.Column( "id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()"), ), sa.Column( "repo_id", UUID(as_uuid=True), sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("slug", sa.String(length=100), nullable=False), sa.Column("kind", sa.String(length=16), nullable=False), sa.Column( "protected", sa.Boolean(), nullable=False, server_default=sa.true() ), sa.Column( "source_operation_id", UUID(as_uuid=True), sa.ForeignKey("repository_rename_operations.id", ondelete="RESTRICT"), nullable=True, ), 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("slug", name="uq_repository_slugs_slug"), sa.CheckConstraint( "kind IN ('canonical', 'alias')", name="ck_repository_slug_kind" ), sa.CheckConstraint( "kind != 'alias' OR protected", name="ck_repository_slug_alias_protected", ), ) op.create_index( "ix_repository_slugs_source_operation_id", "repository_slugs", ["source_operation_id"], ) op.create_index( "ix_repository_slugs_repo_kind", "repository_slugs", ["repo_id", "kind"], ) op.create_index( "uq_repository_slugs_one_canonical", "repository_slugs", ["repo_id"], unique=True, postgresql_where=sa.text("kind = 'canonical'"), ) # Explicitly mark every existing repository unverified. Do not parse or # infer Forge coordinates from current mutable fields. op.execute( sa.text( """ INSERT INTO repository_forge_identities (id, repo_id, verification_state, verification_evidence, created_at, updated_at) SELECT gen_random_uuid(), id, 'unverified', '{}'::jsonb, now(), now() FROM managed_repos """ ) ) op.execute( sa.text( """ CREATE FUNCTION guard_repository_forge_identity_history() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'repository Forge identity rows are durable'; END IF; IF OLD.verification_state = 'verified' AND ROW( NEW.repo_id, NEW.provider, NEW.forge_instance, NEW.forge_owner, NEW.forge_repository_id, NEW.verification_state, NEW.verified_at, NEW.verified_by, NEW.verification_evidence ) IS DISTINCT FROM ROW( OLD.repo_id, OLD.provider, OLD.forge_instance, OLD.forge_owner, OLD.forge_repository_id, OLD.verification_state, OLD.verified_at, OLD.verified_by, OLD.verification_evidence ) THEN RAISE EXCEPTION 'verified repository Forge identity is immutable'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql """ ) ) op.execute( sa.text( """ CREATE TRIGGER repository_forge_identity_history_guard BEFORE UPDATE OR DELETE ON repository_forge_identities FOR EACH ROW EXECUTE FUNCTION guard_repository_forge_identity_history() """ ) ) op.execute( sa.text( """ CREATE FUNCTION guard_repository_slug_history() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'repository slug rows are durable'; END IF; IF NEW.repo_id IS DISTINCT FROM OLD.repo_id OR NEW.slug IS DISTINCT FROM OLD.slug THEN RAISE EXCEPTION 'repository slug ownership is immutable'; END IF; IF OLD.kind = 'alias' AND ROW( NEW.kind, NEW.protected, NEW.source_operation_id ) IS DISTINCT FROM ROW( OLD.kind, OLD.protected, OLD.source_operation_id ) THEN RAISE EXCEPTION 'protected repository alias is immutable'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql """ ) ) op.execute( sa.text( """ CREATE TRIGGER repository_slug_history_guard BEFORE UPDATE OR DELETE ON repository_slugs FOR EACH ROW EXECUTE FUNCTION guard_repository_slug_history() """ ) ) op.execute( sa.text( """ CREATE FUNCTION guard_repository_rename_operation_history() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'repository rename operation rows are durable'; END IF; IF ROW( NEW.repo_id, NEW.forge_identity_id, NEW.forge_identity_state, NEW.expected_provider, NEW.expected_forge_instance, NEW.expected_forge_owner, NEW.expected_forge_repository_id, NEW.expected_source_commit, NEW.expected_default_branch, NEW.old_slug, NEW.new_slug, NEW.old_coordinates, NEW.new_coordinates, NEW.actor, NEW.created_at ) IS DISTINCT FROM ROW( OLD.repo_id, OLD.forge_identity_id, OLD.forge_identity_state, OLD.expected_provider, OLD.expected_forge_instance, OLD.expected_forge_owner, OLD.expected_forge_repository_id, OLD.expected_source_commit, OLD.expected_default_branch, OLD.old_slug, OLD.new_slug, OLD.old_coordinates, OLD.new_coordinates, OLD.actor, OLD.created_at ) THEN RAISE EXCEPTION 'repository rename operation assertions are immutable'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql """ ) ) op.execute( sa.text( """ CREATE TRIGGER repository_rename_operation_history_guard BEFORE UPDATE OR DELETE ON repository_rename_operations FOR EACH ROW EXECUTE FUNCTION guard_repository_rename_operation_history() """ ) ) op.execute( sa.text( """ INSERT INTO repository_slugs (id, repo_id, slug, kind, protected, created_at, updated_at) SELECT gen_random_uuid(), id, slug, 'canonical', true, now(), now() FROM managed_repos """ ) ) def downgrade() -> None: # These tables are additive. Dropping them restores the prior schema and # cannot rewrite or delete any pre-existing work or telemetry row. op.drop_table("repository_slugs") op.drop_table("repository_rename_operations") op.drop_table("repository_forge_identities") op.execute(sa.text("DROP FUNCTION guard_repository_slug_history()")) op.execute(sa.text("DROP FUNCTION guard_repository_rename_operation_history()")) op.execute(sa.text("DROP FUNCTION guard_repository_forge_identity_history()"))