feat: converge mixed identifier projections
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
6f64f1aab8
commit
151fdcf397
5 changed files with 256 additions and 25 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue