Coordination is a view, not topology. Open workplans and wait tasks project into graph-explorer; citation edges recover depends_on from task prose. UI: /ui/graph-explorer?mode=coordination. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
188 lines
7 KiB
Python
188 lines
7 KiB
Python
"""Project State Hub workplans/tasks into the graph-explorer payload.
|
|
|
|
This is a *view* of State Hub coordination state, not Fabric topology.
|
|
State Hub remains the authoring surface for workplans; Fabric only renders.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
|
|
OPEN_WORKPLAN = frozenset({"proposed", "ready", "active", "blocked", "backlog"})
|
|
OPEN_TASK = frozenset({"todo", "progress", "wait"})
|
|
WP_ID_RE = re.compile(r"\b([A-Z]{2,12}(?:-[A-Z]+)?-WP-[A-Z0-9-]+)\b")
|
|
|
|
|
|
def coordination_graph_payload(
|
|
workplans: list[dict[str, Any]],
|
|
tasks: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""Build a GraphExplorerPayload of open workplans and their open tasks."""
|
|
open_wps = [wp for wp in workplans if str(wp.get("status") or "") in OPEN_WORKPLAN]
|
|
wp_by_id = {str(wp.get("id")): wp for wp in open_wps if wp.get("id")}
|
|
slug_to_id = {
|
|
str(wp.get("slug") or "").lower(): str(wp["id"])
|
|
for wp in open_wps
|
|
if wp.get("slug") and wp.get("id")
|
|
}
|
|
# also index record ids like CUST-WP-0071 from title/slug
|
|
record_to_id: dict[str, str] = {}
|
|
for wp in open_wps:
|
|
slug = str(wp.get("slug") or "")
|
|
title = str(wp.get("title") or "")
|
|
for token in WP_ID_RE.findall(slug.upper() + " " + title.upper()):
|
|
record_to_id[token] = str(wp["id"])
|
|
if slug:
|
|
record_to_id[slug.upper().replace("_", "-")] = str(wp["id"])
|
|
|
|
open_tasks = [
|
|
task
|
|
for task in tasks
|
|
if str(task.get("status") or "") in OPEN_TASK
|
|
and str(task.get("workplan_id") or task.get("workstream_id") or "") in wp_by_id
|
|
]
|
|
|
|
elements: list[dict[str, Any]] = []
|
|
for wp in open_wps:
|
|
wp_id = str(wp["id"])
|
|
status = str(wp.get("status") or "unknown")
|
|
elements.append(
|
|
{
|
|
"data": {
|
|
"id": f"workplan:{wp_id}",
|
|
"stableKey": f"workplan:{wp_id}",
|
|
"kind": "Workplan",
|
|
"layer": "workplan",
|
|
"label": str(wp.get("slug") or wp.get("title") or wp_id)[:48],
|
|
"name": wp.get("title"),
|
|
"lifecycle": status,
|
|
"status": status,
|
|
"repo": wp.get("repo") or wp.get("slug"),
|
|
"displayState": "show" if status != "blocked" else "highlight",
|
|
"unresolved": status in {"blocked", "proposed"},
|
|
"chokepoint": 0,
|
|
}
|
|
}
|
|
)
|
|
|
|
belongs = 0
|
|
wait_edges = 0
|
|
for task in open_tasks:
|
|
task_id = str(task["id"])
|
|
wp_id = str(task.get("workplan_id") or task.get("workstream_id"))
|
|
status = str(task.get("status") or "todo")
|
|
human = bool(task.get("needs_human"))
|
|
elements.append(
|
|
{
|
|
"data": {
|
|
"id": f"task:{task_id}",
|
|
"stableKey": f"task:{task_id}",
|
|
"kind": "Task",
|
|
"layer": "task",
|
|
"label": str(task.get("title") or task_id)[:56],
|
|
"name": task.get("title"),
|
|
"lifecycle": status,
|
|
"status": status,
|
|
"needsHuman": human,
|
|
"displayState": "highlight" if status == "wait" or human else "show",
|
|
"unresolved": status == "wait" or human,
|
|
}
|
|
}
|
|
)
|
|
elements.append(
|
|
{
|
|
"data": {
|
|
"id": f"edge:belongs:{task_id}",
|
|
"stableKey": f"edge:belongs:{task_id}",
|
|
"kind": "Edge",
|
|
"layer": "dependency",
|
|
"displayState": "show",
|
|
"source": f"task:{task_id}",
|
|
"target": f"workplan:{wp_id}",
|
|
"edgeType": "belongs_to",
|
|
"strength": "weak",
|
|
"sourceLayer": "task",
|
|
"targetLayer": "workplan",
|
|
}
|
|
}
|
|
)
|
|
belongs += 1
|
|
if status == "wait" or human:
|
|
wait_edges += 1
|
|
|
|
# Cross-workplan citations in wait/blocking text.
|
|
cited = 0
|
|
for task in open_tasks:
|
|
blob = " ".join(
|
|
str(task.get(key) or "")
|
|
for key in ("description", "blocking_reason", "intervention_note", "title")
|
|
)
|
|
src_wp = str(task.get("workplan_id") or task.get("workstream_id"))
|
|
for token in set(WP_ID_RE.findall(blob.upper())):
|
|
dst = record_to_id.get(token) or slug_to_id.get(token.lower())
|
|
if dst and dst != src_wp and dst in wp_by_id:
|
|
elements.append(
|
|
{
|
|
"data": {
|
|
"id": f"edge:cites:{task['id']}:{dst}",
|
|
"stableKey": f"edge:cites:{task['id']}:{dst}",
|
|
"kind": "Edge",
|
|
"layer": "dependency",
|
|
"displayState": "show",
|
|
"source": f"workplan:{src_wp}",
|
|
"target": f"workplan:{dst}",
|
|
"edgeType": "waits_on",
|
|
"strength": "strong",
|
|
"sourceLayer": "workplan",
|
|
"targetLayer": "workplan",
|
|
}
|
|
}
|
|
)
|
|
cited += 1
|
|
for el in elements:
|
|
if el["data"]["id"] == f"workplan:{dst}":
|
|
el["data"]["chokepoint"] = int(el["data"].get("chokepoint") or 0) + 1
|
|
|
|
generated = datetime.now(timezone.utc).isoformat()
|
|
return {
|
|
"apiVersion": "railiance.fabric/v1alpha1",
|
|
"kind": "GraphExplorerPayload",
|
|
"manifest_id": "railiance-fabric.coordination-map",
|
|
"generated_at": generated,
|
|
"mode": "coordination",
|
|
"metrics": {
|
|
"open_workplans": len(open_wps),
|
|
"open_tasks": len(open_tasks),
|
|
"belongs_to_edges": belongs,
|
|
"citation_edges": cited,
|
|
"wait_or_human_tasks": wait_edges,
|
|
},
|
|
"elements": elements,
|
|
"hidden_elements": [],
|
|
}
|
|
|
|
|
|
def fetch_hub_lists(api_base: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
base = api_base.rstrip("/")
|
|
workplans: list[dict[str, Any]] = []
|
|
for status in sorted(OPEN_WORKPLAN):
|
|
workplans.extend(_get_json(f"{base}/workplans/?{urlencode({'status': status})}"))
|
|
tasks = _get_json(f"{base}/tasks/")
|
|
return workplans, tasks
|
|
|
|
|
|
def _get_json(url: str) -> list[dict[str, Any]]:
|
|
with urllib.request.urlopen(url, timeout=60) as response:
|
|
payload = json.loads(response.read().decode())
|
|
if isinstance(payload, list):
|
|
return payload
|
|
raise RuntimeError(f"unexpected JSON from {url}")
|
|
|
|
|
|
def default_hub() -> str:
|
|
return os.environ.get("STATE_HUB_URL") or os.environ.get("API_BASE") or "http://127.0.0.1:8000"
|