Adapt the coordination graph to flavor and indexed depends_on.
Omit flavor=residual by default, draw hub depends_on first, and keep citation waits_on as a tagged fallback. Explorer checkbox and CLI --include-residuals match State Hub include_residuals. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
This commit is contained in:
parent
0b0abedbe8
commit
725e440b1e
8 changed files with 410 additions and 39 deletions
|
|
@ -15,22 +15,76 @@ from urllib.parse import urlencode
|
|||
|
||||
OPEN_WORKPLAN = frozenset({"proposed", "ready", "active", "blocked", "backlog"})
|
||||
OPEN_TASK = frozenset({"todo", "progress", "wait"})
|
||||
RESIDUAL_FLAVOR = "residual"
|
||||
WP_ID_RE = re.compile(r"\b([A-Z]{2,12}(?:-[A-Z]+)?-WP-[A-Z0-9-]+)\b")
|
||||
FLAVOR_COLORS = {
|
||||
"planning": "#6366f1",
|
||||
"implementation": "#1d4ed8",
|
||||
"refactoring": "#b45309",
|
||||
"extension": "#0f766e",
|
||||
"residual": "#64748b",
|
||||
}
|
||||
|
||||
|
||||
def is_residual_flavor(value: Any) -> bool:
|
||||
"""True only for the closed flavor token. Titles are not consulted."""
|
||||
return str(value or "").strip().lower() == RESIDUAL_FLAVOR
|
||||
|
||||
|
||||
def _flavor(record: dict[str, Any]) -> str | None:
|
||||
text = str(record.get("flavor") or "").strip().lower()
|
||||
return text or None
|
||||
|
||||
|
||||
def _indexed_depends_targets(workplan: dict[str, Any]) -> list[str]:
|
||||
"""Hub-indexed workplan ids from depends_on stubs or id lists."""
|
||||
raw = workplan.get("depends_on")
|
||||
if raw is None:
|
||||
raw = workplan.get("depends_on_workplans")
|
||||
if raw is None:
|
||||
return []
|
||||
if not isinstance(raw, list):
|
||||
raw = [raw]
|
||||
targets: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
target = (
|
||||
item.get("workplan_id")
|
||||
or item.get("workstream_id")
|
||||
or item.get("id")
|
||||
)
|
||||
else:
|
||||
target = item
|
||||
if not target:
|
||||
continue
|
||||
key = str(target)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
targets.append(key)
|
||||
return targets
|
||||
|
||||
|
||||
def coordination_graph_payload(
|
||||
workplans: list[dict[str, Any]],
|
||||
tasks: list[dict[str, Any]],
|
||||
*,
|
||||
include_residuals: bool = False,
|
||||
) -> 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]
|
||||
open_all = [wp for wp in workplans if str(wp.get("status") or "") in OPEN_WORKPLAN]
|
||||
residual_open = sum(1 for wp in open_all if is_residual_flavor(_flavor(wp)))
|
||||
if include_residuals:
|
||||
open_wps = open_all
|
||||
else:
|
||||
open_wps = [wp for wp in open_all if not is_residual_flavor(_flavor(wp))]
|
||||
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 "")
|
||||
|
|
@ -45,12 +99,14 @@ def coordination_graph_payload(
|
|||
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
|
||||
and (include_residuals or not is_residual_flavor(_flavor(task)))
|
||||
]
|
||||
|
||||
elements: list[dict[str, Any]] = []
|
||||
for wp in open_wps:
|
||||
wp_id = str(wp["id"])
|
||||
status = str(wp.get("status") or "unknown")
|
||||
flavor = _flavor(wp)
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
|
|
@ -62,10 +118,13 @@ def coordination_graph_payload(
|
|||
"name": wp.get("title"),
|
||||
"lifecycle": status,
|
||||
"status": status,
|
||||
"flavor": flavor,
|
||||
"nodeClass": flavor or "unspecified",
|
||||
"repo": wp.get("repo") or wp.get("slug"),
|
||||
"displayState": "show" if status != "blocked" else "highlight",
|
||||
"unresolved": status in {"blocked", "proposed"},
|
||||
"chokepoint": 0,
|
||||
"color": FLAVOR_COLORS.get(flavor or "", "#1d4ed8"),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -77,6 +136,7 @@ def coordination_graph_payload(
|
|||
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"))
|
||||
flavor = _flavor(task) or _flavor(wp_by_id.get(wp_id) or {})
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
|
|
@ -88,6 +148,8 @@ def coordination_graph_payload(
|
|||
"name": task.get("title"),
|
||||
"lifecycle": status,
|
||||
"status": status,
|
||||
"flavor": flavor,
|
||||
"nodeClass": flavor or "unspecified",
|
||||
"needsHuman": human,
|
||||
"displayState": "highlight" if status == "wait" or human else "show",
|
||||
"unresolved": status == "wait" or human,
|
||||
|
|
@ -105,6 +167,7 @@ def coordination_graph_payload(
|
|||
"source": f"task:{task_id}",
|
||||
"target": f"workplan:{wp_id}",
|
||||
"edgeType": "belongs_to",
|
||||
"edgeSource": "membership",
|
||||
"strength": "weak",
|
||||
"sourceLayer": "task",
|
||||
"targetLayer": "workplan",
|
||||
|
|
@ -115,7 +178,46 @@ def coordination_graph_payload(
|
|||
if status == "wait" or human:
|
||||
wait_edges += 1
|
||||
|
||||
# Cross-workplan citations in wait/blocking text.
|
||||
indexed_pairs: set[tuple[str, str]] = set()
|
||||
depends_count = 0
|
||||
chokepoint: dict[str, int] = {str(wp["id"]): 0 for wp in open_wps}
|
||||
|
||||
for wp in open_wps:
|
||||
src = str(wp["id"])
|
||||
for dst_raw in _indexed_depends_targets(wp):
|
||||
dst = (
|
||||
dst_raw
|
||||
if dst_raw in wp_by_id
|
||||
else record_to_id.get(dst_raw.upper())
|
||||
or slug_to_id.get(dst_raw.lower())
|
||||
)
|
||||
if not dst or dst == src or dst not in wp_by_id:
|
||||
continue
|
||||
pair = (src, dst)
|
||||
if pair in indexed_pairs:
|
||||
continue
|
||||
indexed_pairs.add(pair)
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": f"edge:depends:{src}:{dst}",
|
||||
"stableKey": f"edge:depends:{src}:{dst}",
|
||||
"kind": "Edge",
|
||||
"layer": "dependency",
|
||||
"displayState": "show",
|
||||
"source": f"workplan:{src}",
|
||||
"target": f"workplan:{dst}",
|
||||
"edgeType": "depends_on",
|
||||
"edgeSource": "indexed",
|
||||
"strength": "strong",
|
||||
"sourceLayer": "workplan",
|
||||
"targetLayer": "workplan",
|
||||
}
|
||||
}
|
||||
)
|
||||
depends_count += 1
|
||||
chokepoint[dst] = chokepoint.get(dst, 0) + 1
|
||||
|
||||
cited = 0
|
||||
for task in open_tasks:
|
||||
blob = " ".join(
|
||||
|
|
@ -125,28 +227,38 @@ def coordination_graph_payload(
|
|||
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",
|
||||
}
|
||||
if not dst or dst == src_wp or dst not in wp_by_id:
|
||||
continue
|
||||
if (src_wp, dst) in indexed_pairs:
|
||||
continue
|
||||
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",
|
||||
"edgeSource": "citation",
|
||||
"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
|
||||
}
|
||||
)
|
||||
cited += 1
|
||||
chokepoint[dst] = chokepoint.get(dst, 0) + 1
|
||||
indexed_pairs.add((src_wp, dst))
|
||||
|
||||
for el in elements:
|
||||
node_id = el["data"]["id"]
|
||||
if not node_id.startswith("workplan:"):
|
||||
continue
|
||||
wp_id = node_id.split(":", 1)[1]
|
||||
el["data"]["chokepoint"] = int(chokepoint.get(wp_id) or 0)
|
||||
|
||||
generated = datetime.now(timezone.utc).isoformat()
|
||||
return {
|
||||
|
|
@ -159,8 +271,11 @@ def coordination_graph_payload(
|
|||
"open_workplans": len(open_wps),
|
||||
"open_tasks": len(open_tasks),
|
||||
"belongs_to_edges": belongs,
|
||||
"depends_on_edges": depends_count,
|
||||
"citation_edges": cited,
|
||||
"wait_or_human_tasks": wait_edges,
|
||||
"residual_open_workplans": residual_open,
|
||||
"include_residuals": include_residuals,
|
||||
},
|
||||
"elements": elements,
|
||||
"hidden_elements": [],
|
||||
|
|
@ -173,6 +288,20 @@ def fetch_hub_lists(api_base: str) -> tuple[list[dict[str, Any]], list[dict[str,
|
|||
for status in sorted(OPEN_WORKPLAN):
|
||||
workplans.extend(_get_json(f"{base}/workplans/?{urlencode({'status': status})}"))
|
||||
tasks = _get_json(f"{base}/tasks/")
|
||||
dep_rows = _get_json_optional(f"{base}/state/deps?include_residuals=true")
|
||||
by_id = {
|
||||
str(row.get("id")): row
|
||||
for row in dep_rows
|
||||
if isinstance(row, dict) and row.get("id")
|
||||
}
|
||||
for wp in workplans:
|
||||
extra = by_id.get(str(wp.get("id")))
|
||||
if not extra:
|
||||
continue
|
||||
if extra.get("depends_on") and not wp.get("depends_on"):
|
||||
wp["depends_on"] = extra["depends_on"]
|
||||
if extra.get("flavor") and not wp.get("flavor"):
|
||||
wp["flavor"] = extra["flavor"]
|
||||
return workplans, tasks
|
||||
|
||||
|
||||
|
|
@ -184,5 +313,12 @@ def _get_json(url: str) -> list[dict[str, Any]]:
|
|||
raise RuntimeError(f"unexpected JSON from {url}")
|
||||
|
||||
|
||||
def _get_json_optional(url: str) -> list[dict[str, Any]]:
|
||||
try:
|
||||
return _get_json(url)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def default_hub() -> str:
|
||||
return os.environ.get("STATE_HUB_URL") or os.environ.get("API_BASE") or "http://127.0.0.1:8000"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue