from dataclasses import asdict from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.config import settings from api.schemas.identifier_migration import ( IdentifierMigrationApplySubmit, SealedProjectionRepairReceiptRead, SealedProjectionRepairSubmit, ) from api.services.work_record_identifier_migration import ( IdentifierMigrationError, apply_repository_identifier_migration, repair_absent_prederivation_projection, reverse_repository_identifier_migration, ) router = APIRouter(prefix="/identifier-migrations", tags=["identifier-migrations"]) @router.post("/repositories/{repo_slug}/apply") async def apply_identifier_migration( repo_slug: str, body: IdentifierMigrationApplySubmit, session: AsyncSession = Depends(get_session), ) -> dict: """Apply one sealed repository-atomic identifier convergence transaction.""" if settings.state_hub_instance_role != "primary": raise HTTPException( status_code=409, detail={ "message": "identifier migration writes require the primary State Hub", "instance_role": settings.state_hub_instance_role, "instance_label": settings.state_hub_instance_label, }, ) if not body.primary_confirmed: raise HTTPException(status_code=409, detail="explicit primary confirmation is required") if body.plan.get("plan_sha256") != body.expected_plan_sha256: raise HTTPException(status_code=409, detail="explicit plan SHA-256 does not match sealed plan") try: result = await apply_repository_identifier_migration(session, body.plan, repo_slug) except IdentifierMigrationError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc from api.routers.workstreams import _invalidate_workplan_index_cache _invalidate_workplan_index_cache() return { "schema": "state-hub.identifier-migration-apply.v1", "instance_role": settings.state_hub_instance_role, "instance_label": settings.state_hub_instance_label, "result": asdict(result), } @router.post("/repositories/{repo_slug}/reverse") async def reverse_identifier_migration( repo_slug: str, body: IdentifierMigrationApplySubmit, session: AsyncSession = Depends(get_session), ) -> dict: """Reverse one sealed repository migration after a failed file phase.""" if settings.state_hub_instance_role != "primary": raise HTTPException( status_code=409, detail={ "message": "identifier migration writes require the primary State Hub", "instance_role": settings.state_hub_instance_role, "instance_label": settings.state_hub_instance_label, }, ) if not body.primary_confirmed: raise HTTPException(status_code=409, detail="explicit primary confirmation is required") if body.plan.get("plan_sha256") != body.expected_plan_sha256: raise HTTPException(status_code=409, detail="explicit plan SHA-256 does not match sealed plan") try: result = await reverse_repository_identifier_migration(session, body.plan, repo_slug) except IdentifierMigrationError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc from api.routers.workstreams import _invalidate_workplan_index_cache _invalidate_workplan_index_cache() return { "schema": "state-hub.identifier-migration-reverse.v1", "instance_role": settings.state_hub_instance_role, "instance_label": settings.state_hub_instance_label, "result": asdict(result), } @router.post( "/sealed-projection-repairs", response_model=SealedProjectionRepairReceiptRead, ) async def repair_sealed_projection( body: SealedProjectionRepairSubmit, session: AsyncSession = Depends(get_session), ) -> dict: """Restore one exact pre-derivation workplan unit into an absent projection.""" try: receipt = await repair_absent_prederivation_projection( session, body.plan, body.repo_slug, body.unit, expected_plan_sha256=body.expected_plan_sha256, source_revision=body.source_revision, source_fingerprint=body.source_fingerprint, source_clean=body.source_clean, source_synchronized=body.source_synchronized, primary_confirmed=body.primary_confirmed, projection_identity=body.projection_identity, ) except IdentifierMigrationError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc return asdict(receipt)