Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
151 lines
4.9 KiB
Python
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(),
|
|
}
|