"""Add evidence-bound lifecycle, rollback, retention and cleanup records. Revision ID: 20260826_0018 Revises: 20260826_0017 """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "20260826_0018" down_revision: str | None = "20260826_0017" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def id_column() -> sa.Column[object]: return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False) def created_at() -> sa.Column[object]: return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()) def upgrade() -> None: op.create_table( "lifecycle_subjects", id_column(), sa.Column("target_type", sa.String(48), nullable=False), sa.Column("target_ref", sa.String(255), nullable=False), sa.Column("environment", sa.String(32), nullable=False), sa.Column("state", sa.String(48), nullable=False), sa.Column("version", sa.Integer(), nullable=False, server_default="1"), sa.Column("superseded_by_ref", sa.String(255)), created_at(), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.CheckConstraint("version >= 1", name="ck_lifecycle_subject_version"), sa.UniqueConstraint( "target_type", "target_ref", "environment", name="uq_lifecycle_subject_target" ), ) op.create_index("ix_lifecycle_subjects_target_type", "lifecycle_subjects", ["target_type"]) op.create_index("ix_lifecycle_subjects_target_ref", "lifecycle_subjects", ["target_ref"]) op.create_index("ix_lifecycle_subjects_environment", "lifecycle_subjects", ["environment"]) op.create_index("ix_lifecycle_subjects_state", "lifecycle_subjects", ["state"]) op.create_table( "lifecycle_policy_revisions", id_column(), sa.Column("key", sa.String(128), nullable=False), sa.Column("revision", sa.Integer(), nullable=False), sa.Column("scope", sa.String(48), nullable=False), sa.Column("requirements", sa.JSON(), nullable=False), sa.Column("fingerprint", sa.String(64), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("created_by", sa.String(255), nullable=False), created_at(), sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.UniqueConstraint("key", "revision", name="uq_lifecycle_policy_revision"), sa.UniqueConstraint("fingerprint", name="uq_lifecycle_policy_fingerprint"), ) op.create_index("ix_lifecycle_policy_revisions_key", "lifecycle_policy_revisions", ["key"]) op.create_index("ix_lifecycle_policy_revisions_scope", "lifecycle_policy_revisions", ["scope"]) op.create_index( "ix_lifecycle_policy_revisions_active", "lifecycle_policy_revisions", ["active"] ) op.create_table( "lifecycle_approval_requests", id_column(), sa.Column( "policy_revision_id", sa.Uuid(), sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "subject_id", sa.Uuid(), sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"), ), sa.Column("target_type", sa.String(48), nullable=False), sa.Column("target_ref", sa.String(255), nullable=False), sa.Column("environment", sa.String(32), nullable=False), sa.Column("requested_transition", sa.String(48), nullable=False), sa.Column("evidence_snapshot", sa.JSON(), nullable=False), sa.Column("evidence_fingerprint", sa.String(64), nullable=False), sa.Column("status", sa.String(32), nullable=False), sa.Column("blockers", sa.JSON(), nullable=False), sa.Column("warnings", sa.JSON(), nullable=False), sa.Column("requested_by", sa.String(255), nullable=False), sa.Column("approved_by", sa.String(255)), sa.Column("reason", sa.Text(), nullable=False), sa.Column("expires_at", sa.DateTime(timezone=True)), sa.Column("stale_at", sa.DateTime(timezone=True)), sa.Column("decided_at", sa.DateTime(timezone=True)), sa.Column("version", sa.Integer(), nullable=False, server_default="1"), created_at(), sa.CheckConstraint("version >= 1", name="ck_lifecycle_approval_version"), ) for column in ( "policy_revision_id", "subject_id", "target_type", "target_ref", "environment", "evidence_fingerprint", "status", "expires_at", "created_at", ): op.create_index( f"ix_lifecycle_approval_requests_{column}", "lifecycle_approval_requests", [column] ) op.create_table( "lifecycle_approval_evidence", sa.Column( "approval_request_id", sa.Uuid(), sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"), primary_key=True, ), sa.Column("evidence_type", sa.String(64), primary_key=True), sa.Column("evidence_ref", sa.String(255), primary_key=True), sa.Column("evidence_digest", sa.String(64), nullable=False), ) op.create_index( "ix_lifecycle_approval_evidence_evidence_ref", "lifecycle_approval_evidence", ["evidence_ref"], ) op.create_table( "lifecycle_promotion_plans", id_column(), sa.Column( "approval_request_id", sa.Uuid(), sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "subject_id", sa.Uuid(), sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("current_state", sa.String(48), nullable=False), sa.Column("desired_state", sa.String(48), nullable=False), sa.Column("migration_class", sa.String(32), nullable=False), sa.Column( "candidate_deployment_id", sa.Uuid(), sa.ForeignKey("capability_deployments.id", ondelete="RESTRICT"), ), sa.Column("rollback_target_ref", sa.String(255), nullable=False), sa.Column("project_consumers", sa.JSON(), nullable=False), sa.Column("affected_identities", sa.JSON(), nullable=False), sa.Column("impact_analysis", sa.JSON(), nullable=False), sa.Column( "migration_id", sa.Uuid(), sa.ForeignKey("embedding_migrations.id", ondelete="RESTRICT"), ), sa.Column("canary_strategy", sa.JSON(), nullable=False), sa.Column("drain_strategy", sa.JSON(), nullable=False), sa.Column("health_gates", sa.JSON(), nullable=False), sa.Column("automatic_abort_conditions", sa.JSON(), nullable=False), sa.Column("plan_fingerprint", sa.String(64), nullable=False), sa.Column("status", sa.String(32), nullable=False, server_default="DRAFT"), sa.Column("version", sa.Integer(), nullable=False, server_default="1"), sa.Column("created_by", sa.String(255), nullable=False), sa.Column("approved_by", sa.String(255)), sa.Column("immutable_at", sa.DateTime(timezone=True)), created_at(), sa.CheckConstraint("version >= 1", name="ck_lifecycle_plan_version"), sa.UniqueConstraint("plan_fingerprint", name="uq_lifecycle_plan_fingerprint"), ) for column in ( "approval_request_id", "subject_id", "migration_class", "candidate_deployment_id", "migration_id", "status", "created_at", ): op.create_index( f"ix_lifecycle_promotion_plans_{column}", "lifecycle_promotion_plans", [column] ) op.create_table( "lifecycle_rollback_snapshots", id_column(), sa.Column( "promotion_plan_id", sa.Uuid(), sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("snapshot", sa.JSON(), nullable=False), sa.Column("snapshot_digest", sa.String(64), nullable=False), created_at(), sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.UniqueConstraint("promotion_plan_id", name="uq_lifecycle_rollback_plan"), sa.UniqueConstraint("snapshot_digest", name="uq_lifecycle_rollback_digest"), ) op.create_index( "ix_lifecycle_rollback_snapshots_promotion_plan_id", "lifecycle_rollback_snapshots", ["promotion_plan_id"], ) op.create_table( "lifecycle_operations", id_column(), sa.Column( "promotion_plan_id", sa.Uuid(), sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("stage", sa.String(32), nullable=False), sa.Column("idempotency_key", sa.String(128), nullable=False), sa.Column("expected_subject_version", sa.Integer(), nullable=False), sa.Column("requester", sa.String(255), nullable=False), sa.Column("approver", sa.String(255), nullable=False), sa.Column("executor", sa.String(255), nullable=False), sa.Column("failure_code", sa.String(64)), sa.Column("failure_details", sa.JSON(), nullable=False), sa.Column("rollback_duration_ms", sa.Float()), sa.Column("version", sa.Integer(), nullable=False, server_default="1"), sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("finished_at", sa.DateTime(timezone=True)), sa.CheckConstraint("version >= 1", name="ck_lifecycle_operation_version"), sa.UniqueConstraint("idempotency_key", name="uq_lifecycle_operation_idempotency"), ) op.create_index( "ix_lifecycle_operations_promotion_plan_id", "lifecycle_operations", ["promotion_plan_id"] ) op.create_index("ix_lifecycle_operations_stage", "lifecycle_operations", ["stage"]) op.create_table( "lifecycle_canary_runs", id_column(), sa.Column( "operation_id", sa.Uuid(), sa.ForeignKey("lifecycle_operations.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("mode", sa.String(48), nullable=False), sa.Column("traffic_percent", sa.Integer(), nullable=False, server_default="0"), sa.Column("eligible_clients", sa.JSON(), nullable=False), sa.Column("eligible_projects", sa.JSON(), nullable=False), sa.Column("target_request_count", sa.Integer(), nullable=False), sa.Column("thresholds", sa.JSON(), nullable=False), sa.Column("status", sa.String(48), nullable=False), sa.Column("request_count", sa.Integer(), nullable=False, server_default="0"), sa.Column("error_count", sa.Integer(), nullable=False, server_default="0"), sa.Column("latency_p95_ms", sa.Float()), sa.Column("abort_trigger", sa.String(64)), sa.Column("result", sa.JSON(), nullable=False), sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("finished_at", sa.DateTime(timezone=True)), sa.UniqueConstraint("operation_id", name="uq_lifecycle_canary_operation"), ) op.create_index( "ix_lifecycle_canary_runs_operation_id", "lifecycle_canary_runs", ["operation_id"] ) op.create_index("ix_lifecycle_canary_runs_status", "lifecycle_canary_runs", ["status"]) op.create_table( "retention_policy_revisions", id_column(), sa.Column("key", sa.String(128), nullable=False), sa.Column("revision", sa.Integer(), nullable=False), sa.Column("minimum_rollback_days", sa.Integer(), nullable=False), sa.Column("requirements", sa.JSON(), nullable=False), sa.Column("fingerprint", sa.String(64), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("created_by", sa.String(255), nullable=False), created_at(), sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.UniqueConstraint("key", "revision", name="uq_retention_policy_revision"), sa.UniqueConstraint("fingerprint", name="uq_retention_policy_fingerprint"), ) op.create_index("ix_retention_policy_revisions_key", "retention_policy_revisions", ["key"]) op.create_index( "ix_retention_policy_revisions_active", "retention_policy_revisions", ["active"] ) op.create_table( "lifecycle_retention_records", id_column(), sa.Column( "policy_revision_id", sa.Uuid(), sa.ForeignKey("retention_policy_revisions.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("target_type", sa.String(48), nullable=False), sa.Column("target_ref", sa.String(255), nullable=False), sa.Column("state", sa.String(32), nullable=False), sa.Column("retained_until", sa.DateTime(timezone=True)), sa.Column("legal_hold", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("reason", sa.Text(), nullable=False), created_at(), sa.UniqueConstraint("target_type", "target_ref", name="uq_lifecycle_retention_target"), ) for column in ("policy_revision_id", "target_type", "target_ref", "state", "retained_until"): op.create_index( f"ix_lifecycle_retention_records_{column}", "lifecycle_retention_records", [column] ) op.create_table( "lifecycle_cleanup_plans", id_column(), sa.Column("target_type", sa.String(48), nullable=False), sa.Column("target_ref", sa.String(255), nullable=False), sa.Column("action", sa.String(48), nullable=False), sa.Column("dependencies", sa.JSON(), nullable=False), sa.Column("dependency_digest", sa.String(64), nullable=False), sa.Column("reclaimable_bytes", sa.BigInteger(), nullable=False), sa.Column("retention_state", sa.String(32), nullable=False), sa.Column("blockers", sa.JSON(), nullable=False), sa.Column("status", sa.String(32), nullable=False), sa.Column("created_by", sa.String(255), nullable=False), created_at(), sa.Column("executed_at", sa.DateTime(timezone=True)), ) for column in ("target_type", "target_ref", "status", "created_at"): op.create_index(f"ix_lifecycle_cleanup_plans_{column}", "lifecycle_cleanup_plans", [column]) op.create_table( "artifact_location_removal_records", id_column(), sa.Column( "artifact_location_id", sa.Uuid(), sa.ForeignKey("artifact_locations.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "cleanup_plan_id", sa.Uuid(), sa.ForeignKey("lifecycle_cleanup_plans.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("prior_status", sa.String(32), nullable=False), sa.Column("prior_observed_sha256", sa.String(64)), sa.Column("prior_size_bytes", sa.BigInteger()), sa.Column("removed_by", sa.String(255), nullable=False), sa.Column("removed_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_index( "ix_artifact_location_removal_records_artifact_location_id", "artifact_location_removal_records", ["artifact_location_id"], ) op.create_index( "ix_artifact_location_removal_records_cleanup_plan_id", "artifact_location_removal_records", ["cleanup_plan_id"], ) op.create_table( "lifecycle_events", id_column(), sa.Column("event_type", sa.String(64), nullable=False), sa.Column("object_type", sa.String(48), nullable=False), sa.Column("object_ref", sa.String(255), nullable=False), sa.Column("from_state", sa.String(48)), sa.Column("to_state", sa.String(48)), sa.Column("actor", sa.String(255), nullable=False), sa.Column("actor_role", sa.String(32), nullable=False), sa.Column( "policy_revision_id", sa.Uuid(), sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"), ), sa.Column("evidence_ids", sa.JSON(), nullable=False), sa.Column("reason", sa.Text(), nullable=False), sa.Column("change_id", sa.String(64), nullable=False), sa.Column("details", sa.JSON(), nullable=False), sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) for column in ( "event_type", "object_type", "object_ref", "policy_revision_id", "change_id", "occurred_at", ): op.create_index(f"ix_lifecycle_events_{column}", "lifecycle_events", [column]) op.create_index( "uq_capability_deployment_one_production_stable", "capability_deployments", ["capability_contract_id"], unique=True, postgresql_where=sa.text("production = true AND status = 'stable'"), sqlite_where=sa.text("production = 1 AND status = 'stable'"), ) def downgrade() -> None: op.drop_index( "uq_capability_deployment_one_production_stable", table_name="capability_deployments", ) for table in ( "lifecycle_events", "artifact_location_removal_records", "lifecycle_cleanup_plans", "lifecycle_retention_records", "retention_policy_revisions", "lifecycle_canary_runs", "lifecycle_operations", "lifecycle_rollback_snapshots", "lifecycle_promotion_plans", "lifecycle_approval_evidence", "lifecycle_approval_requests", "lifecycle_policy_revisions", "lifecycle_subjects", ): op.drop_table(table)