STATE-WP-0069 T04/T06: body metering and workplan-first state internals
Share LegacyWorkstreamIdBodyMixin across create schemas; meter POST /tasks/ and /decisions/ workstream_id bodies. State summary uses workplan flow; NextStep dual-writes workplan_* fields alongside legacy workstream_*.
This commit is contained in:
parent
6e5e150803
commit
388b330809
11 changed files with 157 additions and 36 deletions
|
|
@ -53,6 +53,22 @@ from api.services.summary_cache import (
|
|||
get_summary_cache,
|
||||
register_summary_cache_invalidation,
|
||||
)
|
||||
|
||||
|
||||
def _dual_workplan_refs(
|
||||
workplan_id,
|
||||
title: str | None,
|
||||
slug: str | None,
|
||||
) -> dict:
|
||||
"""Dual-write workplan-first and legacy workstream fields for NextStep."""
|
||||
return {
|
||||
"workplan_id": workplan_id,
|
||||
"workplan_title": title,
|
||||
"workplan_slug": slug,
|
||||
"workstream_id": workplan_id,
|
||||
"workstream_title": title,
|
||||
"workstream_slug": slug,
|
||||
}
|
||||
from api.task_status import TERMINAL_TASK_STATUSES, status_value
|
||||
from api.workplan_status import (
|
||||
CLOSED_WORKPLAN_STATUSES,
|
||||
|
|
@ -183,7 +199,7 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
|
|||
)
|
||||
open_ws = list(open_ws_rows.scalars().all())
|
||||
|
||||
# Task counts per workstream (used to enrich open_workstreams)
|
||||
# Task counts per workplan (used to enrich open_workplans / open_workstreams)
|
||||
task_per_ws: dict = {}
|
||||
task_statuses_per_ws: dict = {}
|
||||
for ws_id, tstat, cnt in await session.execute(
|
||||
|
|
@ -260,7 +276,7 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
|
|||
description=d.description,
|
||||
))
|
||||
|
||||
workstream_flow = load_flow("workstream")
|
||||
workplan_flow = load_flow("workplan")
|
||||
flow_engine = FlowEngine()
|
||||
effective_status: dict = {}
|
||||
blocked_reasons: dict = {}
|
||||
|
|
@ -275,7 +291,7 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
|
|||
if d.from_workplan_id == w.id and d.to_workplan_id and d.to_workplan_id in ws_lookup
|
||||
],
|
||||
}
|
||||
flow_result = flow_engine.evaluate(flow_obj, workstream_flow)
|
||||
flow_result = flow_engine.evaluate(flow_obj, workplan_flow)
|
||||
effective_status[w.id] = "blocked" if flow_result.exit_blocked else w.status
|
||||
blocked_reasons[w.id] = [
|
||||
assertion_result_to_dict(item) for item in flow_result.blocking_assertions
|
||||
|
|
@ -529,7 +545,7 @@ async def _build_dashboard_overview(session: AsyncSession) -> DashboardOverview:
|
|||
dep_rows = list(dep_result.scalars().all())
|
||||
|
||||
ws_lookup = {w.id: w for w in workstreams_all}
|
||||
workstream_flow = load_flow("workstream")
|
||||
workplan_flow = load_flow("workplan")
|
||||
flow_engine = FlowEngine()
|
||||
effective_status: dict = {}
|
||||
for w in open_ws:
|
||||
|
|
@ -543,7 +559,7 @@ async def _build_dashboard_overview(session: AsyncSession) -> DashboardOverview:
|
|||
if d.from_workplan_id == w.id and d.to_workplan_id and d.to_workplan_id in ws_lookup
|
||||
],
|
||||
}
|
||||
flow_result = flow_engine.evaluate(flow_obj, workstream_flow)
|
||||
flow_result = flow_engine.evaluate(flow_obj, workplan_flow)
|
||||
effective_status[w.id] = "blocked" if flow_result.exit_blocked else normalize_workplan_status(w.status)
|
||||
|
||||
topic_counts = {r[0]: r[1] for r in await session.execute(
|
||||
|
|
@ -923,13 +939,15 @@ async def _derive_next_steps(session: AsyncSession) -> tuple[list[NextStep], lis
|
|||
if task.id in seen_task_ids:
|
||||
continue
|
||||
ws = await session.get(Workplan, decision.workplan_id, options=[noload("*")])
|
||||
domain_slug = await _get_domain_slug_for_workstream(ws, session)
|
||||
domain_slug = await _get_domain_slug_for_workplan(ws, session)
|
||||
steps.append(NextStep(
|
||||
type="resolved_decision",
|
||||
domain=domain_slug,
|
||||
workstream_id=ws.id if ws else None,
|
||||
workstream_title=ws.title if ws else None,
|
||||
workstream_slug=ws.slug if ws else None,
|
||||
**_dual_workplan_refs(
|
||||
ws.id if ws else None,
|
||||
ws.title if ws else None,
|
||||
ws.slug if ws else None,
|
||||
),
|
||||
task_id=task.id,
|
||||
task_title=task.title,
|
||||
message=(
|
||||
|
|
@ -1015,9 +1033,7 @@ async def _derive_next_steps(session: AsyncSession) -> tuple[list[NextStep], lis
|
|||
steps.append(NextStep(
|
||||
type="dependency_cleared",
|
||||
domain=domain_slug,
|
||||
workstream_id=from_ws_id,
|
||||
workstream_title=from_ws["title"],
|
||||
workstream_slug=from_ws["slug"],
|
||||
**_dual_workplan_refs(from_ws_id, from_ws["title"], from_ws["slug"]),
|
||||
task_id=task.id,
|
||||
task_title=task.title,
|
||||
message=(
|
||||
|
|
@ -1040,9 +1056,7 @@ async def _derive_next_steps(session: AsyncSession) -> tuple[list[NextStep], lis
|
|||
steps.append(NextStep(
|
||||
type="open_suggestion",
|
||||
domain=suggestion.domain_slug,
|
||||
workstream_id=suggestion.workplan_id,
|
||||
workstream_title=None,
|
||||
workstream_slug=suggestion.origin_ref,
|
||||
**_dual_workplan_refs(suggestion.workplan_id, None, suggestion.origin_ref),
|
||||
task_id=None,
|
||||
task_title=None,
|
||||
message=(
|
||||
|
|
@ -1054,8 +1068,8 @@ async def _derive_next_steps(session: AsyncSession) -> tuple[list[NextStep], lis
|
|||
return steps, open_suggestions
|
||||
|
||||
|
||||
async def _get_domain_slug_for_workstream(ws: Workplan | None, session: AsyncSession) -> str | None:
|
||||
"""Get the domain slug for a workstream via its topic."""
|
||||
async def _get_domain_slug_for_workplan(ws: Workplan | None, session: AsyncSession) -> str | None:
|
||||
"""Get the domain slug for a workplan via its topic."""
|
||||
if ws is None or ws.topic_id is None:
|
||||
return None
|
||||
return await _get_domain_slug_for_topic(ws.topic_id, session)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue