feat: converge mixed identifier projections
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
tegwick 2026-08-31 01:27:21 +02:00
parent 6f64f1aab8
commit 151fdcf397
5 changed files with 256 additions and 25 deletions

View file

@ -4,18 +4,92 @@ 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,