Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
475 lines
17 KiB
Python
475 lines
17 KiB
Python
"""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()"))
|