Complete workplan state model cleanup

This commit is contained in:
2026-05-18 01:31:36 +02:00
parent 98b2cb6484
commit d6522a9a40
42 changed files with 789 additions and 310 deletions

View File

@@ -38,6 +38,11 @@ from api.schemas.task import TaskRead
from api.schemas.topic import TopicRead, TopicWithWorkstreams
from api.schemas.workstream import WorkstreamRead, WorkstreamWithTaskCounts, WorkstreamWithDeps
from api.schemas.workstream_dependency import WorkstreamDepStub
from api.workplan_status import (
CLOSED_WORKSTREAM_STATUSES,
OPEN_WORKSTREAM_STATUSES,
normalize_workstream_status,
)
from task_flow_engine import FlowEngine
router = APIRouter(prefix="/state", tags=["state"])
@@ -119,7 +124,7 @@ async def get_summary(
open_ws_rows = await session.execute(
select(Workstream)
.options(noload("*"))
.where(Workstream.status.in_(["active", "blocked"]))
.where(Workstream.status.in_(OPEN_WORKSTREAM_STATUSES))
.order_by(Workstream.due_date.asc().nullslast(), Workstream.created_at)
)
open_ws = list(open_ws_rows.scalars().all())
@@ -211,7 +216,7 @@ async def get_summary(
"workstation": w.status,
"tasks": [{"status": status} for status in task_statuses_per_ws.get(w.id, [])],
"dependencies": [
{"workstation": ws_lookup[d.to_workstream_id].status}
{"workstation": normalize_workstream_status(ws_lookup[d.to_workstream_id].status)}
for d in dep_rows
if d.from_workstream_id == w.id and d.to_workstream_id and d.to_workstream_id in ws_lookup
],
@@ -244,9 +249,16 @@ async def get_summary(
total=sum(topic_counts.values()),
),
workstreams=WorkstreamTotals(
proposed=ws_counts.get("proposed", 0),
ready=ws_counts.get("ready", 0) + ws_counts.get("todo", 0),
active=sum(1 for status in effective_status.values() if status == "active"),
blocked=sum(1 for status in effective_status.values() if status == "blocked"),
completed=ws_counts.get("completed", 0),
backlog=ws_counts.get("backlog", 0),
finished=(
ws_counts.get("finished", 0)
+ ws_counts.get("completed", 0)
+ ws_counts.get("accepted", 0)
),
archived=ws_counts.get("archived", 0),
total=sum(ws_counts.values()),
),
@@ -366,7 +378,7 @@ async def _build_domain_summaries(session: AsyncSession) -> list[DomainSummary]:
for domain_id, cnt in await session.execute(
select(Topic.domain_id, func.count(Workstream.id))
.join(Workstream, Workstream.topic_id == Topic.id)
.where(Workstream.status == "active")
.where(Workstream.status.in_(["active", "blocked"]))
.group_by(Topic.domain_id)
):
ws_per_domain[domain_id] = cnt
@@ -405,7 +417,7 @@ async def get_deps(session: AsyncSession = Depends(get_session)) -> list[Workstr
open_ws_rows = await session.execute(
select(Workstream)
.options(noload("*"))
.where(Workstream.status.in_(["active", "blocked"]))
.where(Workstream.status.in_(OPEN_WORKSTREAM_STATUSES))
.order_by(Workstream.due_date.asc().nullslast(), Workstream.created_at)
)
open_ws = list(open_ws_rows.scalars().all())
@@ -488,7 +500,7 @@ async def _derive_next_steps(session: AsyncSession) -> list[NextStep]:
Two signal sources:
1. Recently resolved decisions (last 7 days) → first open task in same workstream
2. Workstreams whose every dependency is now completed first todo task in that workstream
2. Workstreams whose every dependency is now finished -> first todo task in that workstream
"""
steps: list[NextStep] = []
seen_task_ids: set = set()
@@ -575,8 +587,11 @@ async def _derive_next_steps(session: AsyncSession) -> list[NextStep]:
ready_from_ws_ids = [
from_ws_id
for from_ws_id, to_ws_ids in dep_map.items()
if ws_info.get(from_ws_id, {}).get("status") in ("active", "blocked")
and all(ws_info.get(to_id, {}).get("status") == "completed" for to_id in to_ws_ids)
if normalize_workstream_status(ws_info.get(from_ws_id, {}).get("status")) in OPEN_WORKSTREAM_STATUSES
and all(
normalize_workstream_status(ws_info.get(to_id, {}).get("status")) in CLOSED_WORKSTREAM_STATUSES
for to_id in to_ws_ids
)
]
todo_by_ws: dict = {}
@@ -613,7 +628,7 @@ async def _derive_next_steps(session: AsyncSession) -> list[NextStep]:
task_id=task.id,
task_title=task.title,
message=(
f"All dependencies of '{from_ws['title']}' are completed ({blocker_slugs}) "
f"All dependencies of '{from_ws['title']}' are finished ({blocker_slugs}) -> "
f"'{task.title}' is ready to start"
),
))
@@ -650,7 +665,7 @@ async def get_next_steps(session: AsyncSession = Depends(get_session)) -> list[N
Returns suggestions based on:
- Recently resolved decisions → first open task in the same workstream
- Workstreams whose every dependency workstream is now completed first todo task
- Workstreams whose every dependency workstream is now finished -> first todo task
"""
return await _derive_next_steps(session)