"""Fail-closed, UUID-addressed repository rename lifecycle (STATE-WP-0085-T03).""" from __future__ import annotations import base64 import hashlib import hmac import json import uuid from copy import deepcopy from dataclasses import dataclass from datetime import datetime, timedelta, timezone from typing import Any from sqlalchemy import or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from api.config import settings from api.models.agent_message import AgentMessage from api.models.capability_catalog import CapabilityCatalog from api.models.decision import Decision from api.models.fabric_graph import FabricGraphNode from api.models.interface_change import InterfaceChange from api.models.managed_repo import ManagedRepo from api.models.progress_event import ProgressEvent from api.models.repository_rename import ( RepositoryForgeIdentity, RepositoryRenameOperation, RepositorySlug, ) from api.models.sbom_entry import SBOMEntry from api.models.sbom_snapshot import SBOMSnapshot from api.models.service_catalog import ServiceFirstParty from api.models.task import Task from api.models.token_event import TokenEvent from api.models.workplan import Workplan from api.schemas.repository_rename import ( ForgeIdentityVerifyRequest, RepositoryRenameOperationCreate, RepositoryRenamePhaseApply, RepositoryRenamePreflightRequest, ) from api.services.forge_repository import ( ForgeRepositoryConflict, ForgeRepositoryGateway, ForgeRepositorySnapshot, ForgeRepositoryUnreadable, ) ACTIVE_OPERATION_PHASES = { "draft", "preflighted", "forge-renamed", "statehub-rebound", "source-synced", "consumers-verified", "rollback-preflight", } FORWARD_PHASES = [ "preflighted", "forge-renamed", "statehub-rebound", "source-synced", "consumers-verified", "completed", ] class RenameLifecycleError(RuntimeError): status_code = 409 code = "repository_rename_conflict" def __init__(self, message: str, *, details: dict[str, Any] | None = None): super().__init__(message) self.details = details or {} class RenameNotFound(RenameLifecycleError): status_code = 404 code = "repository_rename_not_found" class RenamePreconditionFailed(RenameLifecycleError): status_code = 412 code = "repository_rename_precondition_failed" class RenameServiceUnavailable(RenameLifecycleError): status_code = 503 code = "repository_rename_service_unavailable" def utcnow() -> datetime: return datetime.now(timezone.utc) def _json_default(value: Any) -> str: if isinstance(value, datetime): return value.astimezone(timezone.utc).isoformat() if isinstance(value, uuid.UUID): return str(value) if hasattr(value, "value"): return str(value.value) return str(value) def canonical_json(value: Any) -> bytes: return json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, default=_json_default, ).encode("utf-8") def checksum(value: Any) -> str: return hashlib.sha256(canonical_json(value)).hexdigest() def _b64encode(value: bytes) -> str: return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") def _b64decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) def _sign_preflight(payload: dict[str, Any]) -> str: secret = settings.repository_rename_preflight_secret if not secret: raise RenameServiceUnavailable( "Repository rename preflight signing is not configured" ) encoded = _b64encode(canonical_json(payload)) signature = hmac.new(secret.encode(), encoded.encode(), hashlib.sha256).digest() return f"{encoded}.{_b64encode(signature)}" def _verify_preflight_token(token: str) -> dict[str, Any]: secret = settings.repository_rename_preflight_secret if not secret: raise RenameServiceUnavailable( "Repository rename preflight signing is not configured" ) try: encoded, supplied = token.split(".", 1) expected = hmac.new(secret.encode(), encoded.encode(), hashlib.sha256).digest() if not hmac.compare_digest(expected, _b64decode(supplied)): raise ValueError("signature") payload = json.loads(_b64decode(encoded)) expires_at = datetime.fromisoformat(payload["expires_at"]) except (ValueError, KeyError, json.JSONDecodeError) as exc: raise RenamePreconditionFailed("Invalid repository rename preflight token") from exc if expires_at <= utcnow(): raise RenamePreconditionFailed("Repository rename preflight token has expired") return payload def confirmation_for(repo_id: uuid.UUID, old_slug: str, new_slug: str) -> str: return f"rename:{repo_id}:{old_slug}:{new_slug}" def rollback_confirmation_for(operation_id: uuid.UUID) -> str: return f"rollback:{operation_id}" async def _id_rows(session: AsyncSession, query) -> list[str]: result = await session.execute(query) return sorted(str(value) for value in result.scalars().all()) def _record_summary(ids: list[str]) -> dict[str, Any]: return {"count": len(ids), "ids": ids, "checksum": checksum(ids)} async def _relationship_rows( session: AsyncSession, query, fields: tuple[str, ...] ) -> list[dict[str, Any]]: def normalize(value: Any) -> Any: if value is None or isinstance(value, (str, int, float, bool)): return value if isinstance(value, datetime): return value.astimezone(timezone.utc).isoformat() if isinstance(value, uuid.UUID): return str(value) if hasattr(value, "value"): return value.value return str(value) result = await session.execute(query) rows = [ {field: normalize(getattr(row, field)) for field in fields} for row in result.all() ] return sorted(rows, key=lambda row: canonical_json(row)) def _relationship_summary( rows: list[dict[str, Any]], *, totals: dict[str, int] | None = None ) -> dict[str, Any]: ids = sorted(row["id"] for row in rows) result = { "count": len(rows), "ids": ids, "checksum": checksum(ids), "relationships": rows, "relationship_checksum": checksum(rows), } if totals is not None: result["totals"] = totals result["totals_checksum"] = checksum(totals) return result async def collect_continuity_baseline( session: AsyncSession, repo_id: uuid.UUID ) -> dict[str, Any]: workplans = await _relationship_rows( session, select( Workplan.id.label("id"), Workplan.repo_id.label("repo_id"), Workplan.slug.label("slug"), ).where(Workplan.repo_id == repo_id), ("id", "repo_id", "slug"), ) workplan_uuids = [uuid.UUID(row["id"]) for row in workplans] tasks = await _relationship_rows( session, select( Task.id.label("id"), Task.workplan_id.label("workplan_id"), Task.record_id.label("record_id"), ).where( Task.workplan_id.in_(workplan_uuids) if workplan_uuids else False ), ("id", "workplan_id", "record_id"), ) task_uuids = [uuid.UUID(row["id"]) for row in tasks] progress = await _relationship_rows( session, select( ProgressEvent.id.label("id"), ProgressEvent.workplan_id.label("workplan_id"), ProgressEvent.task_id.label("task_id"), ProgressEvent.decision_id.label("decision_id"), ).where( or_( ProgressEvent.workplan_id.in_(workplan_uuids) if workplan_uuids else False, ProgressEvent.task_id.in_(task_uuids) if task_uuids else False, ) ), ("id", "workplan_id", "task_id", "decision_id"), ) decisions = await _relationship_rows( session, select( Decision.id.label("id"), Decision.workplan_id.label("workplan_id"), ).where( Decision.workplan_id.in_(workplan_uuids) if workplan_uuids else False ), ("id", "workplan_id"), ) token_events = await _relationship_rows( session, select( TokenEvent.id.label("id"), TokenEvent.repo_id.label("repo_id"), TokenEvent.workplan_id.label("workplan_id"), TokenEvent.task_id.label("task_id"), TokenEvent.tokens_in.label("tokens_in"), TokenEvent.tokens_out.label("tokens_out"), TokenEvent.cached_input_tokens.label("cached_input_tokens"), TokenEvent.reasoning_output_tokens.label("reasoning_output_tokens"), TokenEvent.raw_total_tokens.label("raw_total_tokens"), ).where( or_( TokenEvent.repo_id == repo_id, TokenEvent.workplan_id.in_(workplan_uuids) if workplan_uuids else False, TokenEvent.task_id.in_(task_uuids) if task_uuids else False, ) ), ( "id", "repo_id", "workplan_id", "task_id", "tokens_in", "tokens_out", "cached_input_tokens", "reasoning_output_tokens", "raw_total_tokens", ), ) token_totals = { field: sum(int(row[field]) for row in token_events if row[field] is not None) for field in ( "tokens_in", "tokens_out", "cached_input_tokens", "reasoning_output_tokens", "raw_total_tokens", ) } sbom_snapshots = await _relationship_rows( session, select( SBOMSnapshot.id.label("id"), SBOMSnapshot.repo_id.label("repo_id"), ).where(SBOMSnapshot.repo_id == repo_id), ("id", "repo_id"), ) sbom_entries = await _relationship_rows( session, select( SBOMEntry.id.label("id"), SBOMEntry.repo_id.label("repo_id"), SBOMEntry.snapshot_id.label("snapshot_id"), ).where(SBOMEntry.repo_id == repo_id), ("id", "repo_id", "snapshot_id"), ) services = await _relationship_rows( session, select( ServiceFirstParty.service_id.label("id"), ServiceFirstParty.repo_id.label("repo_id"), ).where(ServiceFirstParty.repo_id == repo_id), ("id", "repo_id"), ) capabilities = await _relationship_rows( session, select( CapabilityCatalog.id.label("id"), CapabilityCatalog.repo_id.label("repo_id"), ).where(CapabilityCatalog.repo_id == repo_id), ("id", "repo_id"), ) interface_changes = await _relationship_rows( session, select( InterfaceChange.id.label("id"), InterfaceChange.repo_id.label("repo_id"), ).where(InterfaceChange.repo_id == repo_id), ("id", "repo_id"), ) bindings = await _relationship_rows( session, select( Workplan.id.label("id"), Workplan.repo_id.label("repo_id"), Workplan.backing_relative_path.label("backing_relative_path"), Workplan.backing_filename.label("backing_filename"), ).where( Workplan.repo_id == repo_id, Workplan.backing_relative_path.is_not(None), ), ("id", "repo_id", "backing_relative_path", "backing_filename"), ) slug_rows = await _relationship_rows( session, select( RepositorySlug.id.label("id"), RepositorySlug.repo_id.label("repo_id"), RepositorySlug.slug.label("slug"), RepositorySlug.kind.label("kind"), ).where(RepositorySlug.repo_id == repo_id), ("id", "repo_id", "slug", "kind"), ) known_slugs = [row["slug"] for row in slug_rows] messages = await _relationship_rows( session, select( AgentMessage.id.label("id"), AgentMessage.from_agent.label("from_agent"), AgentMessage.to_agent.label("to_agent"), AgentMessage.thread_id.label("thread_id"), ).where( or_( AgentMessage.from_agent.in_(known_slugs), AgentMessage.to_agent.in_(known_slugs), ) if known_slugs else False ), ("id", "from_agent", "to_agent", "thread_id"), ) active_work = await _active_work(session, repo_id) active_dispatch = [ {"id": row["id"], "repo_id": str(repo_id), "kind": "workplan"} for row in active_work["workplans"] ] + [ { "id": row["id"], "workplan_id": row["workplan_id"], "kind": "task", } for row in active_work["tasks"] ] active_dispatch = sorted(active_dispatch, key=lambda row: canonical_json(row)) records = { "repository": _relationship_summary( [{"id": str(repo_id), "repo_id": str(repo_id)}] ), "workplans": _relationship_summary(workplans), "tasks": _relationship_summary(tasks), "progress_events": _relationship_summary(progress), "decisions": _relationship_summary(decisions), "token_events": _relationship_summary(token_events, totals=token_totals), "sbom_snapshots": _relationship_summary(sbom_snapshots), "sbom_entries": _relationship_summary(sbom_entries), "services": _relationship_summary(services), "capabilities": _relationship_summary(capabilities), "interface_changes": _relationship_summary(interface_changes), "workplan_bindings": _relationship_summary(bindings), "active_dispatch": _relationship_summary(active_dispatch), "aliases": _relationship_summary(slug_rows), "messages": _relationship_summary(messages), } records["continuity_checksum"] = checksum(records) return records async def _active_work(session: AsyncSession, repo_id: uuid.UUID) -> dict[str, Any]: workplans_result = await session.execute( select(Workplan.id, Workplan.slug, Workplan.status) .where( Workplan.repo_id == repo_id, Workplan.status.not_in(("finished", "archived")), ) .order_by(Workplan.slug) ) workplans = [ {"id": str(row.id), "slug": row.slug, "status": row.status} for row in workplans_result.all() ] workplan_ids = [uuid.UUID(row["id"]) for row in workplans] tasks_result = await session.execute( select(Task.id, Task.workplan_id, Task.record_id, Task.status) .where( Task.workplan_id.in_(workplan_ids) if workplan_ids else False, Task.status.not_in(("done", "cancel")), ) .order_by(Task.id) ) tasks = [ { "id": str(row.id), "workplan_id": str(row.workplan_id), "record_id": row.record_id, "status": getattr(row.status, "value", str(row.status)), } for row in tasks_result.all() ] return {"workplans": workplans, "tasks": tasks} async def _affected_records( session: AsyncSession, repo_id: uuid.UUID, old_slug: str ) -> dict[str, Any]: messages = await _id_rows( session, select(AgentMessage.id).where( or_(AgentMessage.from_agent == old_slug, AgentMessage.to_agent == old_slug) ), ) interface_mentions = await _id_rows( session, select(InterfaceChange.id).where( InterfaceChange.affected_repo_slugs.contains([old_slug]) ), ) fabric_nodes = await _id_rows( session, select(FabricGraphNode.id).where( or_( FabricGraphNode.repo_slug == old_slug, FabricGraphNode.source_repo_slug == old_slug, ) ), ) return { "messages": _record_summary(messages), "interface_change_mentions": _record_summary(interface_mentions), "fabric_nodes": _record_summary(fabric_nodes), "external_handoffs": [ item for item, ids in ( ("interface-change-consumers", interface_mentions), ("fabric-graph-projections", fabric_nodes), ) if ids ], } def _new_remote_url(repo: ManagedRepo, identity: RepositoryForgeIdentity, new_slug: str) -> str: base = str(identity.forge_instance).rstrip("/") suffix = ".git" if (repo.remote_url or "").endswith(".git") else "" return f"{base}/{identity.forge_owner}/{new_slug}{suffix}" async def _load_repo_identity( session: AsyncSession, repo_id: uuid.UUID ) -> tuple[ManagedRepo, RepositoryForgeIdentity | None]: repo = await session.get(ManagedRepo, repo_id) if repo is None: raise RenameNotFound(f"Repository {repo_id} was not found") result = await session.execute( select(RepositoryForgeIdentity).where( RepositoryForgeIdentity.repo_id == repo_id ) ) return repo, result.scalar_one_or_none() async def verify_forge_identity( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, body: ForgeIdentityVerifyRequest, ) -> RepositoryForgeIdentity: repo, identity = await _load_repo_identity(session, repo_id) if identity is None: raise RenamePreconditionFailed("Repository has no Forge identity row") try: snapshot = await gateway.inspect( instance=body.forge_instance, owner=body.forge_owner, name=repo.slug, ) except ForgeRepositoryUnreadable as exc: raise RenamePreconditionFailed(str(exc)) from exc if snapshot is None: raise RenamePreconditionFailed("Forge repository is absent or unreadable") if snapshot.name != repo.slug: raise RenamePreconditionFailed( "Forge identity verification followed a repository redirect", details={"expected_name": repo.slug, "actual_name": snapshot.name}, ) if snapshot.repository_id != body.forge_repository_id: raise RenamePreconditionFailed( "Forge repository ID does not match the asserted immutable ID", details={ "expected_forge_repository_id": body.forge_repository_id, "actual_forge_repository_id": snapshot.repository_id, }, ) if identity.verification_state == "verified": actual = ( identity.provider, identity.forge_instance, identity.forge_owner, identity.forge_repository_id, ) supplied = ( body.provider, body.forge_instance.rstrip("/"), body.forge_owner, body.forge_repository_id, ) if actual != supplied: raise RenamePreconditionFailed("Verified Forge identity is immutable") return identity identity.provider = body.provider identity.forge_instance = body.forge_instance.rstrip("/") identity.forge_owner = body.forge_owner identity.forge_repository_id = body.forge_repository_id identity.verification_state = "verified" identity.verified_at = utcnow() identity.verified_by = body.verified_by identity.verification_evidence = { "forge_snapshot": snapshot.as_dict(), "verified_repo_slug": repo.slug, } await session.commit() await session.refresh(identity) return identity async def build_preflight( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, body: RepositoryRenamePreflightRequest, *, issue_token: bool = True, ) -> dict[str, Any]: repo, identity = await _load_repo_identity(session, repo_id) now = utcnow() blockers: list[dict[str, Any]] = [] warnings: list[dict[str, Any]] = [] old_snapshot: ForgeRepositorySnapshot | None = None target_snapshot: ForgeRepositorySnapshot | None = None if issue_token and not settings.repository_rename_preflight_secret: blockers.append( { "code": "preflight_signing_unavailable", "message": "Repository rename preflight signing is not configured", } ) if repo.slug == body.new_slug: blockers.append({"code": "same_slug", "message": "Target slug equals canonical slug"}) claimed = await session.execute( select(RepositorySlug).where(RepositorySlug.slug == body.new_slug) ) claimed_row = claimed.scalar_one_or_none() old_claim = await session.execute( select(RepositorySlug).where(RepositorySlug.slug == repo.slug) ) old_claim_row = old_claim.scalar_one_or_none() if ( old_claim_row is None or old_claim_row.repo_id != repo.id or old_claim_row.kind != "canonical" ): blockers.append( { "code": "statehub_canonical_inconsistent", "message": "Managed repository slug is not its protected canonical route", } ) if claimed_row is not None: blockers.append( { "code": "statehub_slug_claimed", "message": "Target slug is already protected in State Hub", "repo_id": str(claimed_row.repo_id), "kind": claimed_row.kind, } ) active_result = await session.execute( select(RepositoryRenameOperation).where( RepositoryRenameOperation.repo_id == repo_id, RepositoryRenameOperation.phase.in_(ACTIVE_OPERATION_PHASES), ) ) active_operations = list(active_result.scalars().all()) if active_operations: blockers.append( { "code": "active_rename_operation", "message": "Repository already has an active rename operation", "operation_ids": [str(item.id) for item in active_operations], } ) target_operation_result = await session.execute( select(RepositoryRenameOperation.id, RepositoryRenameOperation.repo_id).where( RepositoryRenameOperation.new_slug == body.new_slug, RepositoryRenameOperation.phase.in_(ACTIVE_OPERATION_PHASES), RepositoryRenameOperation.repo_id != repo_id, ) ) target_operations = list(target_operation_result.all()) if target_operations: blockers.append( { "code": "target_held_by_active_operation", "message": "Target slug is reserved by another active rename operation", "operations": [ {"operation_id": str(row.id), "repo_id": str(row.repo_id)} for row in target_operations ], } ) if body.queued_edge_writes: blockers.append( { "code": "queued_edge_writes", "message": "Caller reported edge writes that must be replayed or resolved first", "count": len(body.queued_edge_writes), } ) if identity is None or identity.verification_state != "verified": blockers.append( { "code": "forge_identity_unverified", "message": "Repository Forge identity is not verified", } ) else: try: old_snapshot = await gateway.inspect( instance=str(identity.forge_instance), owner=str(identity.forge_owner), name=repo.slug, ) target_snapshot = await gateway.inspect( instance=str(identity.forge_instance), owner=str(identity.forge_owner), name=body.new_slug, ) except ForgeRepositoryUnreadable as exc: blockers.append( {"code": "forge_unreadable", "message": str(exc)} ) if old_snapshot is None: blockers.append( {"code": "forge_source_absent", "message": "Canonical Forge repository is absent or unreadable"} ) else: if old_snapshot.name != repo.slug: blockers.append( { "code": "forge_redirected", "message": "Forge source lookup resolved to another coordinate", "expected": repo.slug, "actual": old_snapshot.name, } ) if old_snapshot.repository_id != identity.forge_repository_id: blockers.append( { "code": "wrong_forge_repository_id", "message": "Forge repository ID differs from verified identity", "expected": identity.forge_repository_id, "actual": old_snapshot.repository_id, } ) if not old_snapshot.projection_readable or not old_snapshot.projection_source_present: blockers.append( { "code": "projection_unreadable", "message": "Forge work-record projection source is not readable", } ) if target_snapshot is not None: blockers.append( { "code": "forge_target_claimed", "message": "Target Forge repository name is already claimed", "forge_repository_id": target_snapshot.repository_id, } ) baselines = await collect_continuity_baseline(session, repo_id) active_work = await _active_work(session, repo_id) affected = await _affected_records(session, repo_id, repo.slug) affected.update( { "workplan_bindings": baselines["workplan_bindings"], "workplans": baselines["workplans"], "tasks": baselines["tasks"], "progress_events": baselines["progress_events"], "decisions": baselines["decisions"], "token_events": baselines["token_events"], "interface_changes": baselines["interface_changes"], "sbom_snapshots": baselines["sbom_snapshots"], "sbom_entries": baselines["sbom_entries"], "services": baselines["services"], "capability_entries": baselines["capabilities"], } ) if active_work["workplans"]: warnings.append( { "code": "active_work_present", "message": "Active work must be quiesced or explicitly coordinated during cutover", "workplan_count": len(active_work["workplans"]), "task_count": len(active_work["tasks"]), } ) identity_dict = ( { "id": str(identity.id), "verification_state": identity.verification_state, "provider": identity.provider, "forge_instance": identity.forge_instance, "forge_owner": identity.forge_owner, "forge_repository_id": identity.forge_repository_id, } if identity is not None else None ) new_remote = _new_remote_url(repo, identity, body.new_slug) if identity else None proposed = [ {"phase": "forge-renamed", "system": "forgejo", "field": "name", "from": repo.slug, "to": body.new_slug}, {"phase": "statehub-rebound", "system": "state-hub", "field": "managed_repos.slug", "from": repo.slug, "to": body.new_slug}, {"phase": "statehub-rebound", "system": "state-hub", "field": "managed_repos.remote_url", "from": repo.remote_url, "to": new_remote}, {"phase": "statehub-rebound", "system": "state-hub", "field": "repository_slugs", "from": f"{repo.slug}:canonical", "to": f"{repo.slug}:alias,{body.new_slug}:canonical"}, ] retained = [ {"field": "managed_repos.id", "value": str(repo.id)}, {"field": "repository_forge_identities.forge_repository_id", "value": identity.forge_repository_id if identity else None}, {"field": "repository_slugs.alias", "value": repo.slug}, {"field": "managed_repos.local_path", "value": repo.local_path}, {"field": "managed_repos.host_paths", "value": repo.host_paths}, {"field": "telemetry_and_work_record_ids", "value": baselines["continuity_checksum"]}, ] stable_report = { "schema_version": "state-hub.repository-rename-preflight.v1", "repo_id": str(repo.id), "old_slug": repo.slug, "new_slug": body.new_slug, "safe_to_apply": not blockers, "blockers": blockers, "warnings": warnings, "current": { "statehub": { "repo_id": str(repo.id), "slug": repo.slug, "name": repo.name, "remote_url": repo.remote_url, "local_path": repo.local_path, "host_paths": repo.host_paths, "canonical_available": bool( old_claim_row and old_claim_row.repo_id == repo.id and old_claim_row.kind == "canonical" ), "slug_registry": ( { "id": str(old_claim_row.id), "kind": old_claim_row.kind, "protected": old_claim_row.protected, } if old_claim_row else None ), }, "forge_identity": identity_dict, "forge_available": old_snapshot is not None, "forge": old_snapshot.as_dict() if old_snapshot else None, }, "target": { "slug": body.new_slug, "statehub_available": claimed_row is None, "forge_available": target_snapshot is None, "forge": target_snapshot.as_dict() if target_snapshot else None, }, "baselines": baselines, "active_work": active_work, "affected": affected, "queued_edge_writes": [item.model_dump(mode="json") for item in body.queued_edge_writes], "proposed_mutations": proposed, "retained_history": retained, } report_checksum = checksum(stable_report) expires_at = now + timedelta(seconds=settings.repository_rename_preflight_ttl_seconds) token = None if issue_token and not blockers: token = _sign_preflight( { "version": 1, "repo_id": str(repo.id), "old_slug": repo.slug, "new_slug": body.new_slug, "forge_repository_id": identity.forge_repository_id if identity else None, "source_commit": old_snapshot.head_commit if old_snapshot else None, "report_checksum": report_checksum, "issued_at": now.isoformat(), "expires_at": expires_at.isoformat(), } ) return { **stable_report, "report_checksum": report_checksum, "preflight_token": token, "preflighted_at": now, "expires_at": expires_at if token else None, } async def create_operation( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, body: RepositoryRenameOperationCreate, ) -> tuple[RepositoryRenameOperation, bool]: if body.operation_id is not None: existing = await session.get(RepositoryRenameOperation, body.operation_id) if existing is not None: if ( existing.repo_id != repo_id or existing.new_slug != body.new_slug or existing.actor != body.actor or body.confirmation != confirmation_for(repo_id, existing.old_slug, existing.new_slug) ): raise RenamePreconditionFailed( "Repository rename operation ID is already bound to another request" ) return existing, True token = _verify_preflight_token(body.preflight_token) if token.get("repo_id") != str(repo_id) or token.get("new_slug") != body.new_slug: raise RenamePreconditionFailed("Preflight token does not address this rename") preflight = await build_preflight( session, gateway, repo_id, RepositoryRenamePreflightRequest( new_slug=body.new_slug, queued_edge_writes=body.queued_edge_writes ), issue_token=False, ) if not preflight["safe_to_apply"]: raise RenamePreconditionFailed( "Repository rename preflight is no longer safe", details={"blockers": preflight["blockers"]}, ) if preflight["report_checksum"] != token.get("report_checksum"): raise RenamePreconditionFailed( "Repository rename preflight evidence is stale", details={ "expected_report_checksum": token.get("report_checksum"), "actual_report_checksum": preflight["report_checksum"], }, ) old_slug = preflight["old_slug"] if body.confirmation != confirmation_for(repo_id, old_slug, body.new_slug): raise RenamePreconditionFailed("Explicit repository rename confirmation is incorrect") repo, identity = await _load_repo_identity(session, repo_id) if identity is None or identity.verification_state != "verified": raise RenamePreconditionFailed("Repository Forge identity is not verified") forge = preflight["current"]["forge"] now = utcnow() operation = RepositoryRenameOperation( id=body.operation_id or uuid.uuid4(), repo_id=repo.id, forge_identity_id=identity.id, forge_identity_state="verified", expected_provider=str(identity.provider), expected_forge_instance=str(identity.forge_instance), expected_forge_owner=str(identity.forge_owner), expected_forge_repository_id=int(identity.forge_repository_id), expected_source_commit=forge["head_commit"], expected_default_branch=forge["default_branch"], old_slug=repo.slug, new_slug=body.new_slug, old_coordinates={ "slug": repo.slug, "name": repo.name, "remote_url": repo.remote_url, "local_path": repo.local_path, "host_paths": repo.host_paths, }, new_coordinates={ "slug": body.new_slug, "name": body.new_slug, "remote_url": _new_remote_url(repo, identity, body.new_slug), "local_path": repo.local_path, "host_paths": repo.host_paths, }, phase="preflighted", actor=body.actor, phase_changed_at=now, preflighted_at=datetime.fromisoformat(token["issued_at"]), preflight_expires_at=datetime.fromisoformat(token["expires_at"]), evidence={ "preflight": { "report_checksum": preflight["report_checksum"], "baselines": preflight["baselines"], "affected": preflight["affected"], "warnings": preflight["warnings"], }, "phases": { "preflighted": {"at": now.isoformat(), "resumed": False} }, "telemetry": { "started_at": now.isoformat(), "phase_attempts": {"preflighted": 1}, "phase_durations_ms": {"preflighted": 0}, "retries": 0, "failures": 0, "rollback_attempts": 0, "verification_outcome": "pending", }, }, ) session.add(operation) try: await session.commit() except IntegrityError as exc: await session.rollback() if body.operation_id is not None: existing = await session.get(RepositoryRenameOperation, body.operation_id) if ( existing is not None and existing.repo_id == repo_id and existing.new_slug == body.new_slug and existing.actor == body.actor and body.confirmation == confirmation_for(repo_id, existing.old_slug, existing.new_slug) ): return existing, True raise RenamePreconditionFailed( "A conflicting repository rename operation was created" ) from exc await session.refresh(operation) return operation, False async def load_operation_by_id( session: AsyncSession, operation_id: uuid.UUID, ) -> RepositoryRenameOperation: operation = await session.get(RepositoryRenameOperation, operation_id) if operation is None: raise RenameNotFound( f"Repository rename operation {operation_id} was not found" ) return operation async def load_operation( session: AsyncSession, repo_id: uuid.UUID, operation_id: uuid.UUID, *, for_update: bool = False, ) -> RepositoryRenameOperation: query = select(RepositoryRenameOperation).where( RepositoryRenameOperation.id == operation_id, RepositoryRenameOperation.repo_id == repo_id, ) if for_update: query = query.with_for_update() result = await session.execute(query) operation = result.scalar_one_or_none() if operation is None: raise RenameNotFound(f"Repository rename operation {operation_id} was not found") return operation async def list_operations( session: AsyncSession, repo_id: uuid.UUID, *, active_only: bool = False ) -> list[RepositoryRenameOperation]: repo = await session.get(ManagedRepo, repo_id) if repo is None: raise RenameNotFound(f"Repository {repo_id} was not found") query = ( select(RepositoryRenameOperation) .where(RepositoryRenameOperation.repo_id == repo_id) .order_by(RepositoryRenameOperation.created_at.desc()) ) if active_only: query = query.where(RepositoryRenameOperation.phase.in_(ACTIVE_OPERATION_PHASES)) result = await session.execute(query) return list(result.scalars().all()) def _assert_snapshot( operation: RepositoryRenameOperation, snapshot: ForgeRepositorySnapshot, *, expected_name: str | None = None, ) -> None: if snapshot.repository_id != operation.expected_forge_repository_id: raise RenamePreconditionFailed( "Forge repository ID changed", details={ "expected": operation.expected_forge_repository_id, "actual": snapshot.repository_id, }, ) if snapshot.default_branch != operation.expected_default_branch: raise RenamePreconditionFailed("Forge default branch changed") if snapshot.head_commit != operation.expected_source_commit: raise RenamePreconditionFailed( "Forge default-branch head moved", details={ "expected": operation.expected_source_commit, "actual": snapshot.head_commit, }, ) if not snapshot.projection_readable or not snapshot.projection_source_present: raise RenamePreconditionFailed("Forge work-record projection is unreadable") if expected_name is not None and snapshot.name != expected_name: raise RenamePreconditionFailed( "Forge response resolved to a different repository coordinate", details={"expected_name": expected_name, "actual_name": snapshot.name}, ) async def _forge_at( gateway: ForgeRepositoryGateway, operation: RepositoryRenameOperation, name: str, ) -> ForgeRepositorySnapshot | None: try: return await gateway.inspect( instance=operation.expected_forge_instance, owner=operation.expected_forge_owner, name=name, ) except ForgeRepositoryUnreadable as exc: raise RenamePreconditionFailed(str(exc)) from exc async def _apply_forge_rename( gateway: ForgeRepositoryGateway, operation: RepositoryRenameOperation ) -> dict[str, Any]: old = await _forge_at(gateway, operation, operation.old_slug) new = await _forge_at(gateway, operation, operation.new_slug) if new is not None: _assert_snapshot(operation, new, expected_name=operation.new_slug) if old is not None: raise RenamePreconditionFailed("Both old and new Forge names are claimed") return {"resumed": True, "forge": new.as_dict()} if old is None: raise RenamePreconditionFailed("Forge repository is absent at both expected names") _assert_snapshot(operation, old, expected_name=operation.old_slug) try: renamed = await gateway.rename( instance=operation.expected_forge_instance, owner=operation.expected_forge_owner, old_name=operation.old_slug, new_name=operation.new_slug, ) except (ForgeRepositoryUnreadable, ForgeRepositoryConflict) as exc: raise RenamePreconditionFailed(str(exc)) from exc _assert_snapshot(operation, renamed, expected_name=operation.new_slug) return {"resumed": False, "forge": renamed.as_dict()} async def _apply_statehub_rebind( session: AsyncSession, operation: RepositoryRenameOperation ) -> dict[str, Any]: repo = await session.get(ManagedRepo, operation.repo_id, with_for_update=True) if repo is None: raise RenameNotFound("Managed repository disappeared") if repo.id != operation.repo_id: raise RenamePreconditionFailed("Managed repository UUID changed") old_result = await session.execute( select(RepositorySlug) .where(RepositorySlug.slug == operation.old_slug) .with_for_update() ) new_result = await session.execute( select(RepositorySlug) .where(RepositorySlug.slug == operation.new_slug) .with_for_update() ) old_slug = old_result.scalar_one_or_none() new_slug = new_result.scalar_one_or_none() if repo.slug == operation.new_slug: if ( old_slug is None or old_slug.repo_id != repo.id or old_slug.kind != "alias" or new_slug is None or new_slug.repo_id != repo.id or new_slug.kind != "canonical" ): raise RenamePreconditionFailed("State Hub rename state is internally inconsistent") return {"resumed": True, "repo_id": str(repo.id)} if repo.slug != operation.old_slug: raise RenamePreconditionFailed("Managed repository slug changed outside this operation") if old_slug is None or old_slug.repo_id != repo.id or old_slug.kind != "canonical": raise RenamePreconditionFailed("Old canonical slug record is missing") if new_slug is not None: raise RenamePreconditionFailed("Target State Hub slug became claimed") old_slug.kind = "alias" old_slug.protected = True old_slug.source_operation_id = operation.id await session.flush() session.add( RepositorySlug( repo_id=repo.id, slug=operation.new_slug, kind="canonical", protected=True, source_operation_id=operation.id, ) ) repo.slug = operation.new_slug repo.name = operation.new_coordinates["name"] repo.remote_url = operation.new_coordinates["remote_url"] await session.flush() return {"resumed": False, "repo_id": str(repo.id)} async def verify_operation( session: AsyncSession, gateway: ForgeRepositoryGateway, operation: RepositoryRenameOperation, ) -> dict[str, Any]: checks: list[dict[str, Any]] = [] def add(name: str, ok: bool, expected: Any, actual: Any) -> None: checks.append({"name": name, "ok": ok, "expected": expected, "actual": actual}) repo = await session.get(ManagedRepo, operation.repo_id) add("managed_repository_exists", repo is not None, str(operation.repo_id), str(repo.id) if repo else None) add("managed_repository_uuid", bool(repo and repo.id == operation.repo_id), str(operation.repo_id), str(repo.id) if repo else None) expected_name = operation.old_slug if operation.phase == "preflighted" else operation.new_slug if operation.phase == "rolled-back": expected_name = operation.old_slug try: forge = await _forge_at(gateway, operation, expected_name) except RenameLifecycleError as exc: forge = None add("forge_readable", False, True, str(exc)) else: add("forge_readable", forge is not None, True, forge is not None) if forge is not None: add("forge_coordinate", forge.name == expected_name, expected_name, forge.name) add("forge_repository_id", forge.repository_id == operation.expected_forge_repository_id, operation.expected_forge_repository_id, forge.repository_id) add("source_commit", forge.head_commit == operation.expected_source_commit, operation.expected_source_commit, forge.head_commit) add("default_branch", forge.default_branch == operation.expected_default_branch, operation.expected_default_branch, forge.default_branch) add("projection_readable", forge.projection_readable and forge.projection_source_present, True, forge.projection_readable and forge.projection_source_present) expected_canonical = operation.old_slug if operation.phase in {"preflighted", "forge-renamed", "rolled-back"} else operation.new_slug canonical_result = await session.execute( select(RepositorySlug).where( RepositorySlug.repo_id == operation.repo_id, RepositorySlug.kind == "canonical", ) ) canonical = canonical_result.scalar_one_or_none() add("canonical_route", bool(canonical and canonical.slug == expected_canonical), expected_canonical, canonical.slug if canonical else None) current = await collect_continuity_baseline(session, operation.repo_id) baseline = (operation.evidence.get("preflight") or {}).get("baselines") or {} expected_checksum = baseline.get("continuity_checksum", "") missing: dict[str, list[str]] = {} changed_relationships: dict[str, list[str]] = {} preserved_relationship_checksums: dict[str, str] = {} counts: dict[str, dict[str, int]] = {} for record_type, baseline_record in baseline.items(): if record_type == "continuity_checksum" or not isinstance(baseline_record, dict): continue current_record = current.get(record_type) or {"ids": [], "count": 0} absent = sorted(set(baseline_record.get("ids") or []) - set(current_record.get("ids") or [])) if absent: missing[record_type] = absent if record_type != "aliases": baseline_rows = { row["id"]: row for row in baseline_record.get("relationships") or [] } current_rows = { row["id"]: row for row in current_record.get("relationships") or [] } changed = sorted( record_id for record_id, baseline_row in baseline_rows.items() if current_rows.get(record_id) != baseline_row ) if changed: changed_relationships[record_type] = changed preserved_rows = [ current_rows[record_id] for record_id in baseline_rows if record_id in current_rows ] preserved_relationship_checksums[record_type] = checksum( sorted(preserved_rows, key=lambda row: canonical_json(row)) ) counts[record_type] = { "baseline": int(baseline_record.get("count") or 0), "current": int(current_record.get("count") or 0), } # New telemetry may legitimately arrive while a phased operation is being # executed. Continuity means every baseline identity still exists; it does # not freeze the repository's append-only history at the preflight count. add( "relationship_continuity", not missing and not changed_relationships, {"missing": {}, "changed": {}}, {"missing": missing, "changed": changed_relationships}, ) add( "record_counts_non_decreasing", all(item["current"] >= item["baseline"] for item in counts.values()), {name: item["baseline"] for name, item in counts.items()}, {name: item["current"] for name, item in counts.items()}, ) baseline_token_totals = (baseline.get("token_events") or {}).get("totals") or {} current_token_totals = (current.get("token_events") or {}).get("totals") or {} add( "token_totals_non_decreasing", all( int(current_token_totals.get(name) or 0) >= int(value or 0) for name, value in baseline_token_totals.items() ), baseline_token_totals, current_token_totals, ) slug_result = await session.execute( select(RepositorySlug).where( RepositorySlug.repo_id == operation.repo_id, RepositorySlug.slug.in_((operation.old_slug, operation.new_slug)), ) ) slug_routes = {row.slug: row.kind for row in slug_result.scalars().all()} if operation.phase in {"preflighted", "forge-renamed"}: expected_routes = {operation.old_slug: "canonical"} elif operation.phase == "rolled-back": expected_routes = { operation.old_slug: "canonical", operation.new_slug: "alias", } else: expected_routes = { operation.old_slug: "alias", operation.new_slug: "canonical", } add( "slug_routes", all(slug_routes.get(slug) == kind for slug, kind in expected_routes.items()), expected_routes, slug_routes, ) return { "operation_id": operation.id, "repo_id": operation.repo_id, "phase": operation.phase, "ok": all(item["ok"] for item in checks), "checks": checks, "baseline_checksum": expected_checksum, "current_checksum": current["continuity_checksum"], "relationship_checksums": { "baseline": { name: value.get("relationship_checksum") for name, value in baseline.items() if isinstance(value, dict) and name != "aliases" }, "preserved": preserved_relationship_checksums, }, } def _telemetry(operation: RepositoryRenameOperation) -> tuple[dict[str, Any], dict[str, Any]]: journal = deepcopy(operation.evidence or {}) telemetry = dict(journal.get("telemetry") or {}) telemetry.setdefault("started_at", operation.preflighted_at.isoformat()) telemetry.setdefault("phase_attempts", {}) telemetry.setdefault("phase_durations_ms", {}) telemetry.setdefault("retries", 0) telemetry.setdefault("failures", 0) telemetry.setdefault("rollback_attempts", 0) telemetry.setdefault("verification_outcome", "pending") return journal, telemetry def _record_retry(operation: RepositoryRenameOperation, phase: str) -> None: journal, telemetry = _telemetry(operation) attempts = dict(telemetry["phase_attempts"]) attempts[phase] = int(attempts.get(phase) or 0) + 1 telemetry["phase_attempts"] = attempts telemetry["retries"] = int(telemetry["retries"]) + 1 telemetry["last_retry"] = {"phase": phase, "at": utcnow().isoformat()} journal["telemetry"] = telemetry operation.evidence = journal def _record_failure( operation: RepositoryRenameOperation, phase: str, exc: RenameLifecycleError ) -> None: journal, telemetry = _telemetry(operation) failures_by_phase = dict(telemetry.get("failures_by_phase") or {}) failures_by_phase[phase] = int(failures_by_phase.get(phase) or 0) + 1 telemetry["failures_by_phase"] = failures_by_phase telemetry["failures"] = int(telemetry["failures"]) + 1 telemetry["last_failure"] = { "phase": phase, "code": exc.code, "at": utcnow().isoformat(), } if phase.startswith("rollback"): telemetry["rollback_outcome"] = "failed" journal["telemetry"] = telemetry operation.evidence = journal def _record_phase(operation: RepositoryRenameOperation, phase: str, evidence: dict[str, Any]) -> None: now = utcnow() journal, telemetry = _telemetry(operation) phases = dict(journal.get("phases") or {}) # Verification payloads use native UUID/datetime values for API response # typing. The operation journal is JSONB, so normalize at the boundary # instead of leaking storage concerns through every verifier. safe_evidence = json.loads(canonical_json(evidence)) phases[phase] = {"at": now.isoformat(), **safe_evidence} journal["phases"] = phases attempts = dict(telemetry["phase_attempts"]) attempts[phase] = int(attempts.get(phase) or 0) + 1 telemetry["phase_attempts"] = attempts durations = dict(telemetry["phase_durations_ms"]) durations[phase] = max( 0, int((now - operation.phase_changed_at).total_seconds() * 1000) ) telemetry["phase_durations_ms"] = durations telemetry["last_phase"] = phase if phase == "rollback-preflight": telemetry["rollback_attempts"] = int(telemetry["rollback_attempts"]) + 1 telemetry["rollback_outcome"] = "approved" elif phase == "rolled-back": telemetry["rollback_outcome"] = "completed" verification = evidence.get("verification") if isinstance(verification, dict): telemetry["verification_outcome"] = ( "passed" if verification.get("ok") else "failed" ) journal["telemetry"] = telemetry operation.evidence = journal operation.phase = phase operation.phase_changed_at = now operation.error_code = None operation.error_message = None operation.error_details = None operation.error_at = None if phase == "completed": operation.completed_at = now if phase == "rolled-back": operation.rolled_back_at = now def _phase_index(phase: str) -> int: try: return FORWARD_PHASES.index(phase) except ValueError: return -1 async def apply_phase( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, operation_id: uuid.UUID, requested_phase: str, body: RepositoryRenamePhaseApply, ) -> tuple[RepositoryRenameOperation, bool]: if requested_phase not in FORWARD_PHASES[1:]: raise RenamePreconditionFailed(f"Unsupported forward phase '{requested_phase}'") operation = await load_operation(session, repo_id, operation_id, for_update=True) if body.confirmation != confirmation_for(repo_id, operation.old_slug, operation.new_slug): raise RenamePreconditionFailed("Explicit repository rename confirmation is incorrect") current_index = _phase_index(operation.phase) requested_index = _phase_index(requested_phase) if current_index >= requested_index >= 0: _record_retry(operation, requested_phase) await session.commit() await session.refresh(operation) return operation, True if operation.phase != body.expected_phase: raise RenamePreconditionFailed( "Repository rename phase compare-and-set failed", details={"expected": body.expected_phase, "actual": operation.phase}, ) if requested_index != current_index + 1: raise RenamePreconditionFailed( "Repository rename phases cannot be skipped", details={"current": operation.phase, "requested": requested_phase}, ) try: if requested_phase == "forge-renamed": evidence = await _apply_forge_rename(gateway, operation) elif requested_phase == "statehub-rebound": forge = await _forge_at(gateway, operation, operation.new_slug) if forge is None: raise RenamePreconditionFailed("Forge rename is not observable") _assert_snapshot(operation, forge, expected_name=operation.new_slug) evidence = await _apply_statehub_rebind(session, operation) elif requested_phase == "source-synced": if not body.evidence or body.evidence.get("fresh_clone") is not True: raise RenamePreconditionFailed( "Source synchronization requires fresh-clone operator evidence" ) clone_repository_id = body.evidence.get("forge_repository_id") if clone_repository_id != operation.expected_forge_repository_id: raise RenamePreconditionFailed( "Fresh clone points at the wrong Forge repository ID", details={ "expected": operation.expected_forge_repository_id, "actual": clone_repository_id, }, ) clone_head = body.evidence.get("head_commit") if clone_head != operation.expected_source_commit: raise RenamePreconditionFailed( "Fresh clone head does not match the rename baseline", details={ "expected": operation.expected_source_commit, "actual": clone_head, }, ) forge = await _forge_at(gateway, operation, operation.new_slug) if forge is None: raise RenamePreconditionFailed("Renamed Forge repository is absent") _assert_snapshot(operation, forge, expected_name=operation.new_slug) evidence = {"forge": forge.as_dict(), "operator_evidence": body.evidence} elif requested_phase == "consumers-verified": if not body.checks or not all(body.checks.values()): raise RenamePreconditionFailed( "Consumer verification requires a non-empty set of passing checks", details={"checks": body.checks}, ) verification = await verify_operation(session, gateway, operation) if not verification["ok"]: raise RenamePreconditionFailed( "Continuity verification failed before consumer acceptance", details=verification, ) evidence = {"checks": body.checks, "verification": verification, "operator_evidence": body.evidence} else: # completed verification = await verify_operation(session, gateway, operation) if not verification["ok"]: raise RenamePreconditionFailed( "Repository rename verification failed", details=verification ) evidence = {"verification": verification} _record_phase(operation, requested_phase, evidence) await session.commit() await session.refresh(operation) return operation, False except RenameLifecycleError as exc: # Roll back every State Hub write attempted by the phase. A Forge # rename may already have committed externally; the unchanged journal # phase is precisely what lets the next request discover and resume it. await session.rollback() failed = await load_operation(session, repo_id, operation_id, for_update=True) _record_failure(failed, requested_phase, exc) failed.error_code = exc.code failed.error_message = str(exc) failed.error_details = json.loads(canonical_json(exc.details)) failed.error_at = utcnow() await session.commit() raise except IntegrityError as exc: await session.rollback() failure = RenamePreconditionFailed( "Repository rename phase lost a database compare-and-set race" ) failed = await load_operation(session, repo_id, operation_id, for_update=True) _record_failure(failed, requested_phase, failure) failed.error_code = failure.code failed.error_message = str(failure) failed.error_details = json.loads(canonical_json(failure.details)) failed.error_at = utcnow() await session.commit() raise failure from exc async def rollback_preflight( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, operation_id: uuid.UUID, *, expected_phase: str, confirmation: str, ) -> tuple[RepositoryRenameOperation, dict[str, Any]]: operation = await load_operation(session, repo_id, operation_id, for_update=True) if operation.phase == "rolled-back": return operation, {"rollback_from_phase": "rolled-back", "safe_to_rollback": True, "blockers": [], "irreversible": []} if confirmation != rollback_confirmation_for(operation.id): raise RenamePreconditionFailed("Explicit rollback confirmation is incorrect") if operation.phase != expected_phase: raise RenamePreconditionFailed( "Rollback preflight phase compare-and-set failed", details={"expected": expected_phase, "actual": operation.phase}, ) if operation.phase == "rollback-preflight": rollback_data = (operation.evidence or {}).get("rollback_preflight") or {} return operation, rollback_data competing_result = await session.execute( select(RepositoryRenameOperation.id).where( RepositoryRenameOperation.repo_id == repo_id, RepositoryRenameOperation.id != operation.id, RepositoryRenameOperation.phase.in_(ACTIVE_OPERATION_PHASES), ) ) competing = [str(value) for value in competing_result.scalars().all()] old = await _forge_at(gateway, operation, operation.old_slug) new = await _forge_at(gateway, operation, operation.new_slug) blockers: list[dict[str, Any]] = [] if competing: blockers.append( { "code": "newer_active_rename_operation", "operation_ids": competing, } ) rollback_from_phase = operation.phase if operation.phase == "preflighted": if old is not None and new is None: try: _assert_snapshot(operation, old, expected_name=operation.old_slug) except RenameLifecycleError as exc: blockers.append({"code": exc.code, "message": str(exc), **exc.details}) elif old is None and new is not None: # Forge committed but the phase journal did not. This is the # principal interruption case the operation ID must recover from. rollback_from_phase = "forge-renamed-unrecorded" try: _assert_snapshot(operation, new, expected_name=operation.new_slug) except RenameLifecycleError as exc: blockers.append({"code": exc.code, "message": str(exc), **exc.details}) elif old is None: blockers.append({"code": "forge_repository_absent_at_both_names"}) else: blockers.append({"code": "both_forge_slugs_claimed"}) else: if old is not None: blockers.append({"code": "old_forge_slug_claimed", "forge_repository_id": old.repository_id}) if new is None: blockers.append({"code": "renamed_forge_repository_absent"}) else: try: _assert_snapshot(operation, new, expected_name=operation.new_slug) except RenameLifecycleError as exc: blockers.append({"code": exc.code, "message": str(exc), **exc.details}) irreversible = [ {"code": "external_caches", "message": "External caches and redirects may retain the new coordinate."}, {"code": "consumer_changes", "message": "Consumer commits made after cutover are not rewritten automatically."}, {"code": "telemetry_history", "message": "The attempted operation and phase evidence remain durable."}, ] data = { "rollback_from_phase": rollback_from_phase, "safe_to_rollback": not blockers, "blockers": blockers, "irreversible": irreversible, } if blockers: failure = RenamePreconditionFailed( "Repository rename rollback is unsafe", details=data ) _record_failure(operation, "rollback-preflight", failure) operation.error_code = failure.code operation.error_message = str(failure) operation.error_details = json.loads(canonical_json(failure.details)) operation.error_at = utcnow() await session.commit() raise failure journal = deepcopy(operation.evidence or {}) journal["rollback_preflight"] = data operation.evidence = journal _record_phase(operation, "rollback-preflight", data) await session.commit() await session.refresh(operation) return operation, data async def _rollback_statehub( session: AsyncSession, operation: RepositoryRenameOperation ) -> dict[str, Any]: repo = await session.get(ManagedRepo, operation.repo_id, with_for_update=True) if repo is None: raise RenameNotFound("Managed repository disappeared") if repo.slug == operation.old_slug: return {"resumed": True, "repo_id": str(repo.id)} if repo.slug != operation.new_slug: raise RenamePreconditionFailed("Managed repository slug changed outside this operation") old_result = await session.execute( select(RepositorySlug).where(RepositorySlug.slug == operation.old_slug).with_for_update() ) new_result = await session.execute( select(RepositorySlug).where(RepositorySlug.slug == operation.new_slug).with_for_update() ) old_slug = old_result.scalar_one_or_none() new_slug = new_result.scalar_one_or_none() if not old_slug or old_slug.repo_id != repo.id or old_slug.kind != "alias": raise RenamePreconditionFailed("Protected old alias is unavailable for rollback") if not new_slug or new_slug.repo_id != repo.id or new_slug.kind != "canonical": raise RenamePreconditionFailed("New canonical slug is unavailable for rollback") new_slug.kind = "alias" new_slug.protected = True new_slug.source_operation_id = operation.id await session.flush() old_slug.kind = "canonical" old_slug.protected = True old_slug.source_operation_id = operation.id repo.slug = operation.old_slug repo.name = operation.old_coordinates["name"] repo.remote_url = operation.old_coordinates["remote_url"] await session.flush() return {"resumed": False, "repo_id": str(repo.id)} async def apply_rollback( session: AsyncSession, gateway: ForgeRepositoryGateway, repo_id: uuid.UUID, operation_id: uuid.UUID, *, expected_phase: str, confirmation: str, ) -> tuple[RepositoryRenameOperation, bool]: operation = await load_operation(session, repo_id, operation_id, for_update=True) if operation.phase == "rolled-back": _record_retry(operation, "rolled-back") await session.commit() await session.refresh(operation) return operation, True if operation.phase != expected_phase or operation.phase != "rollback-preflight": raise RenamePreconditionFailed( "Rollback phase compare-and-set failed", details={"expected": expected_phase, "actual": operation.phase}, ) if confirmation != rollback_confirmation_for(operation.id): raise RenamePreconditionFailed("Explicit rollback confirmation is incorrect") rollback_from = ((operation.evidence or {}).get("rollback_preflight") or {}).get("rollback_from_phase") if not rollback_from: raise RenamePreconditionFailed("Rollback preflight evidence is missing") try: old = await _forge_at(gateway, operation, operation.old_slug) new = await _forge_at(gateway, operation, operation.new_slug) forge_resumed = False if rollback_from != "preflighted": if old is not None: _assert_snapshot(operation, old, expected_name=operation.old_slug) if new is not None: raise RenamePreconditionFailed( "Both Forge coordinates are claimed during rollback" ) forge_resumed = True else: if new is None: raise RenamePreconditionFailed( "Forge repository is absent during rollback" ) _assert_snapshot(operation, new, expected_name=operation.new_slug) try: restored = await gateway.rename( instance=operation.expected_forge_instance, owner=operation.expected_forge_owner, old_name=operation.new_slug, new_name=operation.old_slug, ) except (ForgeRepositoryUnreadable, ForgeRepositoryConflict) as exc: raise RenamePreconditionFailed(str(exc)) from exc _assert_snapshot( operation, restored, expected_name=operation.old_slug ) statehub = await _rollback_statehub(session, operation) _record_phase( operation, "rolled-back", { "rollback_from_phase": rollback_from, "forge_resumed": forge_resumed, "statehub": statehub, }, ) await session.commit() await session.refresh(operation) return operation, False except RenameLifecycleError as exc: await session.rollback() failed = await load_operation(session, repo_id, operation_id, for_update=True) _record_failure(failed, "rolled-back", exc) failed.error_code = exc.code failed.error_message = str(exc) failed.error_details = json.loads(canonical_json(exc.details)) failed.error_at = utcnow() await session.commit() raise