state-hub/api/routers/work_record_projection.py
tegwick 34f5cb3fc3
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
feat: add fast forge work-record reconciliation
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
2026-08-30 22:38:54 +02:00

151 lines
4.9 KiB
Python

"""Primary-only fast path for one forge-derived repository projection."""
from __future__ import annotations
import asyncio
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from api.config import settings
from api.database import get_session
from api.models.task import Task
from api.models.workplan import Workplan
from api.models.workplan_dependency import WorkplanDependency
from api.schemas.work_record_projection import (
RepositoryProjectionReconcile,
RepositoryProjectionSnapshot,
)
from api.services.forge_projection import (
ForgeDeriveError,
ForgeUnreadableError,
derive_from_forge,
reset_repository_projection,
)
from api.services.repository_aliases import resolve_repository_slug
router = APIRouter(prefix="/repos", tags=["repository-work-record-projection"])
@router.get(
"/{slug}/work-record-projection/snapshot",
response_model=RepositoryProjectionSnapshot,
)
async def repository_work_record_snapshot(
slug: str,
session: AsyncSession = Depends(get_session),
) -> RepositoryProjectionSnapshot:
"""Return all consistency-check inputs for one repository in one request."""
resolution = await resolve_repository_slug(session, slug)
workplans = list(
(
await session.execute(
select(Workplan)
.where(Workplan.repo_id == resolution.repo.id)
.order_by(Workplan.slug)
)
).scalars()
)
workplan_ids = [workplan.id for workplan in workplans]
tasks: list[Task] = []
dependencies: list[WorkplanDependency] = []
if workplan_ids:
tasks = list(
(
await session.execute(
select(Task)
.where(Task.workplan_id.in_(workplan_ids))
.order_by(Task.workplan_id, Task.id)
)
).scalars()
)
dependencies = list(
(
await session.execute(
select(WorkplanDependency)
.where(WorkplanDependency.from_workplan_id.in_(workplan_ids))
.order_by(
WorkplanDependency.from_workplan_id, WorkplanDependency.id
)
)
).scalars()
)
return RepositoryProjectionSnapshot(
schema="state-hub.repository-projection-snapshot.v1",
repo_slug=resolution.canonical_slug,
repo_id=resolution.repo.id,
workplans=workplans,
tasks=tasks,
dependencies=dependencies,
)
@router.post("/{slug}/work-record-projection/reconcile")
async def reconcile_repository_work_records(
slug: str,
body: RepositoryProjectionReconcile,
session: AsyncSession = Depends(get_session),
) -> dict:
"""Derive an exact pushed commit centrally and apply it transactionally."""
if settings.state_hub_instance_role != "primary":
raise HTTPException(
status_code=409,
detail={
"message": "repository projection writes require the primary State Hub",
"instance_role": settings.state_hub_instance_role,
"instance_label": settings.state_hub_instance_label,
},
)
try:
derived = await asyncio.to_thread(derive_from_forge, slug)
except ForgeUnreadableError as exc:
raise HTTPException(
status_code=424,
detail={
"message": "repository is unreadable from the forge",
"detail": str(exc)[:300],
},
) from exc
except ForgeDeriveError as exc:
raise HTTPException(
status_code=502,
detail={
"message": "forge projection derivation failed",
"detail": str(exc)[:300],
},
) from exc
if derived.commit.lower() != body.expected_commit:
raise HTTPException(
status_code=409,
detail={
"message": "forge default branch is not at the expected commit",
"expected_commit": body.expected_commit,
"derived_commit": derived.commit,
},
)
outcome = await reset_repository_projection(
session,
slug,
acknowledge_retirements=body.acknowledge_retirements,
derived=derived,
)
if outcome.status in {"applied", "noop"} or outcome.released:
await session.commit()
else:
await session.rollback()
from api.routers.workstreams import _invalidate_workplan_index_cache
_invalidate_workplan_index_cache()
return {
"schema": "state-hub.repository-projection-reconcile.v1",
"instance_role": settings.state_hub_instance_role,
"instance_label": settings.state_hub_instance_label,
"expected_commit": body.expected_commit,
"derived_commit": derived.commit,
"outcome": outcome.to_dict(),
}