Improved workplan dependency management facilities

This commit is contained in:
2026-05-04 11:45:24 +02:00
parent 4d6164e81b
commit bfed370a6e
10 changed files with 380 additions and 29 deletions

View File

@@ -97,9 +97,13 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
# Build a slug+title lookup for all workstreams referenced in deps
dep_ws_ids = set()
dep_task_ids = set()
for d in dep_rows:
dep_ws_ids.add(d.from_workstream_id)
dep_ws_ids.add(d.to_workstream_id)
if d.to_workstream_id:
dep_ws_ids.add(d.to_workstream_id)
if d.to_task_id:
dep_task_ids.add(d.to_task_id)
ws_lookup: dict = {w.id: w for w in open_ws}
extra_ids = dep_ws_ids - set(ws_lookup.keys())
if extra_ids:
@@ -108,22 +112,39 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
)
for w in extra_rows.scalars():
ws_lookup[w.id] = w
task_lookup: dict = {}
if dep_task_ids:
task_rows = await session.execute(select(Task).where(Task.id.in_(dep_task_ids)))
task_lookup = {t.id: t for t in task_rows.scalars().all()}
# Index: workstream_id → (depends_on stubs, blocks stubs)
dep_index: dict = {w.id: {"depends_on": [], "blocks": []} for w in open_ws}
for d in dep_rows:
from_id, to_id = d.from_workstream_id, d.to_workstream_id
if from_id in dep_index and to_id in ws_lookup:
from_id, to_id, task_id = d.from_workstream_id, d.to_workstream_id, d.to_task_id
if from_id in dep_index and to_id and to_id in ws_lookup:
dep_index[from_id]["depends_on"].append(WorkstreamDepStub(
dep_id=d.id,
target_type="workstream",
relationship_type=d.relationship_type,
workstream_id=to_id,
workstream_slug=ws_lookup[to_id].slug,
workstream_title=ws_lookup[to_id].title,
description=d.description,
))
if to_id in dep_index and from_id in ws_lookup:
if from_id in dep_index and task_id and task_id in task_lookup:
dep_index[from_id]["depends_on"].append(WorkstreamDepStub(
dep_id=d.id,
target_type="task",
relationship_type=d.relationship_type,
task_id=task_id,
task_title=task_lookup[task_id].title,
description=d.description,
))
if to_id and to_id in dep_index and from_id in ws_lookup:
dep_index[to_id]["blocks"].append(WorkstreamDepStub(
dep_id=d.id,
target_type="workstream",
relationship_type=d.relationship_type,
workstream_id=from_id,
workstream_slug=ws_lookup[from_id].slug,
workstream_title=ws_lookup[from_id].title,
@@ -142,7 +163,7 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
"dependencies": [
{"workstation": ws_lookup[d.to_workstream_id].status}
for d in dep_rows
if d.from_workstream_id == w.id and d.to_workstream_id in ws_lookup
if d.from_workstream_id == w.id and d.to_workstream_id and d.to_workstream_id in ws_lookup
],
}
flow_result = flow_engine.evaluate(flow_obj, workstream_flow)

View File

@@ -5,6 +5,7 @@ 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.models.workstream_dependency import WorkstreamDependency
from api.schemas.workstream_dependency import WorkstreamDependencyCreate, WorkstreamDependencyRead
@@ -22,17 +23,27 @@ async def create_dependency(
body: WorkstreamDependencyCreate,
session: AsyncSession = Depends(get_session),
) -> WorkstreamDependency:
"""Record that workstream_id depends on body.to_workstream_id."""
"""Record that workstream_id depends on another workstream or a task."""
if await session.get(Workstream, workstream_id) is None:
raise HTTPException(status_code=404, detail="from workstream not found")
if await session.get(Workstream, body.to_workstream_id) is None:
raise HTTPException(status_code=404, detail="to workstream not found")
has_workstream_target = body.to_workstream_id is not None
has_task_target = body.to_task_id is not None
if has_workstream_target == has_task_target:
raise HTTPException(status_code=422, detail="provide exactly one dependency target")
if body.to_workstream_id and await session.get(Workstream, body.to_workstream_id) is None:
raise HTTPException(status_code=404, detail="target workstream not found")
if body.to_task_id and await session.get(Task, body.to_task_id) is None:
raise HTTPException(status_code=404, detail="target task not found")
if workstream_id == body.to_workstream_id:
raise HTTPException(status_code=422, detail="a workstream cannot depend on itself")
dep = WorkstreamDependency(
from_workstream_id=workstream_id,
to_workstream_id=body.to_workstream_id,
to_task_id=body.to_task_id,
relationship_type=body.relationship_type,
description=body.description,
)
session.add(dep)

View File

@@ -82,7 +82,11 @@ async def list_workstreams(
q = q.where(Workstream.owner == owner)
if slug:
q = q.where(Workstream.slug == slug)
q = q.order_by(Workstream.updated_at.desc())
q = q.order_by(
Workstream.planning_priority.asc().nullslast(),
Workstream.planning_order.asc().nullslast(),
Workstream.updated_at.desc(),
)
result = await session.execute(q)
return list(result.scalars().all())