STATE-WP-0070 T04 (partial): drop open_workstreams from /state/summary

Removes the redundant open_workstreams mirror field from StateSummary (schema,
router, MCP get_domain_summary reader, dashboard consumers + empty-state stub,
and tests). Consumers already preferred open_workplans, so this is the
low-risk half of T04.

Deferred (still have live callers — not yet retirement-ready):
- workstream_id query/body field alias on preferred routes — external
  session-close curls/scripts fleet-wide still send it.
- flows/workstream.yaml — /flows/workstream/{id} routes are still served and
  exercised by tests; retire only once no callers remain.

Staged on branch state-wp-0070-legacy-retirement — do not merge until the 7th
documented zero-usage window is captured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-13 10:04:38 +02:00
parent ef62cb3872
commit 6d3de59436
8 changed files with 9 additions and 16 deletions

View file

@ -199,7 +199,7 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
) )
open_ws = list(open_ws_rows.scalars().all()) open_ws = list(open_ws_rows.scalars().all())
# Task counts per workplan (used to enrich open_workplans / open_workstreams) # Task counts per workplan (used to enrich open_workplans)
task_per_ws: dict = {} task_per_ws: dict = {}
task_statuses_per_ws: dict = {} task_statuses_per_ws: dict = {}
for ws_id, tstat, cnt in await session.execute( for ws_id, tstat, cnt in await session.execute(
@ -430,7 +430,6 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
open_capability_requests=open_cap_req_count, open_capability_requests=open_cap_req_count,
ranked_suggestions=ranked_suggestions, ranked_suggestions=ranked_suggestions,
open_workplans=open_workplan_rows, open_workplans=open_workplan_rows,
open_workstreams=open_workplan_rows,
) )
return result return result
@ -793,7 +792,7 @@ async def _build_domain_summaries(session: AsyncSession) -> list[DomainSummary]:
async def get_deps(session: AsyncSession = Depends(get_session)) -> list[WorkstreamWithDeps]: async def get_deps(session: AsyncSession = Depends(get_session)) -> list[WorkstreamWithDeps]:
"""Lightweight dep-graph endpoint: open workstreams with their dependency edges only. """Lightweight dep-graph endpoint: open workstreams with their dependency edges only.
Returns the same structure as open_workstreams in /state/summary but skips Returns the same structure as open_workplans in /state/summary but skips
the 10-table full-summary computation. Task counts are omitted (all zero). the 10-table full-summary computation. Task counts are omitted (all zero).
Used by workstreams.md and dependencies.md which only need dep edges. Used by workstreams.md and dependencies.md which only need dep edges.
""" """

View file

@ -83,7 +83,6 @@ class StateSummary(BaseModel):
waiting_tasks: list[TaskRead] waiting_tasks: list[TaskRead]
blocked_tasks: list[TaskRead] = [] blocked_tasks: list[TaskRead] = []
recent_progress: list[ProgressEventRead] recent_progress: list[ProgressEventRead]
open_workstreams: list[WorkstreamWithDeps]
open_workplans: list[WorkstreamWithDeps] = [] open_workplans: list[WorkstreamWithDeps] = []
next_steps: list[NextStep] = [] next_steps: list[NextStep] = []
domains: list[DomainSummary] = [] domains: list[DomainSummary] = []

View file

@ -38,5 +38,4 @@ except urllib.error.URLError as e:
"blocked_tasks": [], "blocked_tasks": [],
"recent_progress": [], "recent_progress": [],
"open_workplans": [], "open_workplans": [],
"open_workstreams": [],
})) }))

View file

@ -437,7 +437,7 @@ display(html`<div class="grid grid-cols-3" style="gap:1rem;margin-bottom:1.5rem"
```js ```js
const waitingTasks = summary.waiting_tasks ?? summary.blocked_tasks ?? []; const waitingTasks = summary.waiting_tasks ?? summary.blocked_tasks ?? [];
const wsById = Object.fromEntries((summary.open_workplans ?? summary.open_workstreams ?? []).map(w => [w.id, w])); const wsById = Object.fromEntries((summary.open_workplans ?? []).map(w => [w.id, w]));
const todayCount = (summary.recent_progress ?? []).filter(e => const todayCount = (summary.recent_progress ?? []).filter(e =>
e.created_at?.startsWith(new Date().toISOString().slice(0, 10))).length; e.created_at?.startsWith(new Date().toISOString().slice(0, 10))).length;
const decCount = (decisions.open ?? 0) + (decisions.escalated ?? 0); const decCount = (decisions.open ?? 0) + (decisions.escalated ?? 0);

View file

@ -263,9 +263,7 @@ def get_domain_summary(domain_slug: str) -> str:
topic_id = topic["id"] topic_id = topic["id"]
state_summary = _get("/state/summary") state_summary = _get("/state/summary")
open_workplans = state_summary.get("open_workplans") or state_summary.get( open_workplans = state_summary.get("open_workplans", [])
"open_workstreams", []
)
workstreams = [ws for ws in open_workplans if ws.get("topic_id") == topic_id] workstreams = [ws for ws in open_workplans if ws.get("topic_id") == topic_id]
blocking = _get("/decisions", {"decision_type": "pending", "topic_id": topic_id}) blocking = _get("/decisions", {"decision_type": "pending", "topic_id": topic_id})
recent = _get("/progress", {"topic_id": topic_id, "limit": 5}) recent = _get("/progress", {"topic_id": topic_id, "limit": 5})

View file

@ -48,14 +48,14 @@ class TestGetStateSummary:
assert r.status_code == 200 assert r.status_code == 200
body = r.json() body = r.json()
# Required top-level fields # Required top-level fields
for key in ("open_workstreams", "blocking_decisions", "blocked_tasks", for key in ("open_workplans", "blocking_decisions", "blocked_tasks",
"domains", "contribution_counts", "licence_risk_count"): "domains", "contribution_counts", "licence_risk_count"):
assert key in body, f"missing key: {key}" assert key in body, f"missing key: {key}"
async def test_empty_db_returns_zero_counts(self, client): async def test_empty_db_returns_zero_counts(self, client):
r = await client.get("/state/summary") r = await client.get("/state/summary")
body = r.json() body = r.json()
assert body["open_workstreams"] == [] assert body["open_workplans"] == []
assert body["blocking_decisions"] == [] assert body["blocking_decisions"] == []
assert body["blocked_tasks"] == [] assert body["blocked_tasks"] == []

View file

@ -433,9 +433,7 @@ class TestStateSummary:
r = await client.get("/state/summary") r = await client.get("/state/summary")
assert r.status_code == 200 assert r.status_code == 200
body = r.json() body = r.json()
assert "open_workstreams" in body
assert "open_workplans" in body assert "open_workplans" in body
assert body["open_workplans"] == body["open_workstreams"]
assert "blocking_decisions" in body assert "blocking_decisions" in body
assert "blocked_tasks" in body assert "blocked_tasks" in body
assert "domains" in body assert "domains" in body
@ -471,7 +469,7 @@ class TestStateSummary:
r = await client.get("/state/summary") r = await client.get("/state/summary")
assert r.status_code == 200 assert r.status_code == 200
body = r.json() body = r.json()
summaries = {item["id"]: item for item in body["open_workstreams"]} summaries = {item["id"]: item for item in body["open_workplans"]}
assert summaries[blocked_ws["id"]]["status"] == "blocked" assert summaries[blocked_ws["id"]]["status"] == "blocked"
assert summaries[blocked_ws["id"]]["blocked_reasons"][0]["id"] == "dependencies.all_complete" assert summaries[blocked_ws["id"]]["blocked_reasons"][0]["id"] == "dependencies.all_complete"

View file

@ -142,7 +142,7 @@ def test_summary_cache_unit_progress_section():
blocking_decisions=[], blocking_decisions=[],
waiting_tasks=[], waiting_tasks=[],
recent_progress=[], recent_progress=[],
open_workstreams=[], open_workplans=[],
) )
cache.store(summary, rev) cache.store(summary, rev)
@ -181,7 +181,7 @@ def test_invalidate_summary_cache_scopes():
blocking_decisions=[], blocking_decisions=[],
waiting_tasks=[], waiting_tasks=[],
recent_progress=[], recent_progress=[],
open_workstreams=[], open_workplans=[],
) )
cache.store(summary, rev) cache.store(summary, rev)