generated from coulomb/repo-seed
100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.database import get_session
|
|
from api.models.task import Task
|
|
from api.models.workstream import Workstream
|
|
from api.schemas.reconciliation import StateChangeRequest, StateChangeResponse
|
|
from api.services.lifecycle import status_value
|
|
from api.services.reconciliation import (
|
|
classify_task_status_change,
|
|
classify_workstream_status_change,
|
|
)
|
|
from api.workplan_status import normalize_workstream_status
|
|
|
|
router = APIRouter(prefix="/reconciliation", tags=["reconciliation"])
|
|
|
|
|
|
def _bool_or_default(value: bool | None, default: bool) -> bool:
|
|
return default if value is None else value
|
|
|
|
|
|
async def _workstream_tasks_terminal(session: AsyncSession, workstream_id: uuid.UUID) -> bool:
|
|
result = await session.execute(select(Task.status).where(Task.workstream_id == workstream_id))
|
|
statuses = [status_value(row[0]) for row in result.all()]
|
|
return bool(statuses) and all(status in {"done", "cancelled"} for status in statuses)
|
|
|
|
|
|
@router.post("/state-change", response_model=StateChangeResponse)
|
|
async def classify_state_change(
|
|
body: StateChangeRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> StateChangeResponse:
|
|
if body.target_type == "workstream":
|
|
ws = await session.get(Workstream, body.target_id)
|
|
if ws is None:
|
|
raise HTTPException(status_code=404, detail="Workstream not found")
|
|
|
|
file_backed = _bool_or_default(body.file_backed, ws.repo_id is not None)
|
|
archived_file = _bool_or_default(body.archived_file, False)
|
|
tasks_terminal = (
|
|
body.tasks_terminal
|
|
if body.tasks_terminal is not None
|
|
else await _workstream_tasks_terminal(session, ws.id)
|
|
)
|
|
classification = classify_workstream_status_change(
|
|
current_status=ws.status,
|
|
target_status=body.target_status,
|
|
file_backed=file_backed,
|
|
archived_file=archived_file,
|
|
tasks_terminal=tasks_terminal,
|
|
)
|
|
return StateChangeResponse(
|
|
target_type=body.target_type,
|
|
target_id=body.target_id,
|
|
actor=body.actor,
|
|
intent=body.intent,
|
|
current_status=normalize_workstream_status(ws.status),
|
|
target_status=normalize_workstream_status(body.target_status),
|
|
file_backed=file_backed,
|
|
archived_file=archived_file,
|
|
tasks_terminal=tasks_terminal,
|
|
reconciliation_class=classification.reconciliation_class,
|
|
reason=classification.reason,
|
|
follow_up=classification.follow_up,
|
|
)
|
|
|
|
task = await session.get(Task, body.target_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
ws = await session.get(Workstream, task.workstream_id)
|
|
file_backed = _bool_or_default(body.file_backed, bool(ws and ws.repo_id))
|
|
archived_file = _bool_or_default(body.archived_file, False)
|
|
task_linked = _bool_or_default(body.task_linked, True)
|
|
classification = classify_task_status_change(
|
|
current_status=task.status,
|
|
target_status=body.target_status,
|
|
file_backed=file_backed,
|
|
archived_file=archived_file,
|
|
task_linked=task_linked,
|
|
blocking_reason=body.blocking_reason,
|
|
)
|
|
return StateChangeResponse(
|
|
target_type=body.target_type,
|
|
target_id=body.target_id,
|
|
actor=body.actor,
|
|
intent=body.intent,
|
|
current_status=status_value(task.status),
|
|
target_status=status_value(body.target_status),
|
|
file_backed=file_backed,
|
|
archived_file=archived_file,
|
|
task_linked=task_linked,
|
|
reconciliation_class=classification.reconciliation_class,
|
|
reason=classification.reason,
|
|
follow_up=classification.follow_up,
|
|
)
|