Qualify wait states in the coordination graph (CUST-WP-0074-T04)
Task nodes carry wait_kind (external | human | both | unqualified, null unless wait): the hub field wins, otherwise derived from needs_human and dependency edges (task-level depends_on, or a workplan dependency row whose target the task cites). Workplan nodes carry blocked_kind (human | external | none, null unless blocked). Task-level depends_on becomes a task -> workplan/task depends_on edge; depends_on edges carry edge_kind commitment (cyan #0891b2), human gates are marked on the node (humanGate, rose #be123c). A needs_human=true query / "Needs human" toggle / --needs-human flag keeps only blocked_kind=human workplans and their human-wait tasks; the parameter survives in the shareable URL and mode switch like include_residuals. Fabric stays a read of State Hub. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826
This commit is contained in:
parent
b47dd43b04
commit
9dbb8f0bcb
7 changed files with 426 additions and 33 deletions
|
|
@ -91,6 +91,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
action="store_true",
|
||||
help="Include flavor=residual workplans in --format coordination (default omits them)",
|
||||
)
|
||||
export.add_argument(
|
||||
"--needs-human",
|
||||
action="store_true",
|
||||
help="Only blocked_kind=human workplans and their human-wait tasks in --format coordination",
|
||||
)
|
||||
|
||||
scan = sub.add_parser("scan", help="Scan a repo for deterministic discovery candidates.")
|
||||
scan.add_argument("path", nargs="?", type=Path, default=Path("."))
|
||||
|
|
@ -363,6 +368,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
workplans,
|
||||
tasks,
|
||||
include_residuals=args.include_residuals,
|
||||
needs_human_only=args.needs_human,
|
||||
),
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ FLAVOR_COLORS = {
|
|||
"extension": "#0f766e",
|
||||
"residual": "#64748b",
|
||||
}
|
||||
# CUST-WP-0074 qualified waits: external commitment (Case A) versus human gate (Case B).
|
||||
WAIT_KIND_COLORS = {
|
||||
"external": "#0891b2",
|
||||
"human": "#be123c",
|
||||
}
|
||||
WAIT_KINDS = frozenset({"external", "human", "both", "unqualified"})
|
||||
BLOCKED_KINDS = frozenset({"external", "human", "none"})
|
||||
HUMAN_WAIT_KINDS = frozenset({"human", "both"})
|
||||
|
||||
|
||||
def is_residual_flavor(value: Any) -> bool:
|
||||
|
|
@ -52,6 +60,7 @@ def _indexed_depends_targets(workplan: dict[str, Any]) -> list[str]:
|
|||
target = (
|
||||
item.get("workplan_id")
|
||||
or item.get("workstream_id")
|
||||
or item.get("task_id")
|
||||
or item.get("id")
|
||||
)
|
||||
else:
|
||||
|
|
@ -66,6 +75,88 @@ def _indexed_depends_targets(workplan: dict[str, Any]) -> list[str]:
|
|||
return targets
|
||||
|
||||
|
||||
def _task_depends_targets(task: dict[str, Any]) -> list[str]:
|
||||
"""Task-level depends_on ids (workplan or task ids), when the payload carries them."""
|
||||
return _indexed_depends_targets({"depends_on": task.get("depends_on")})
|
||||
|
||||
|
||||
def _cited_tokens(task: dict[str, Any]) -> set[str]:
|
||||
blob = " ".join(
|
||||
str(task.get(key) or "")
|
||||
for key in ("description", "blocking_reason", "intervention_note", "title")
|
||||
)
|
||||
return set(WP_ID_RE.findall(blob.upper()))
|
||||
|
||||
|
||||
def _hub_kind(value: Any, allowed: frozenset[str]) -> str | None:
|
||||
"""Accept the hub field as-is or with a `blocked-`/`waiting-` prefix."""
|
||||
text = str(value or "").strip().lower()
|
||||
for prefix in ("blocked-", "waiting-"):
|
||||
if text.startswith(prefix):
|
||||
text = text[len(prefix):]
|
||||
return text if text in allowed else None
|
||||
|
||||
|
||||
def task_wait_kind(
|
||||
task: dict[str, Any],
|
||||
*,
|
||||
has_external: bool,
|
||||
) -> str | None:
|
||||
"""`external` | `human` | `both` | `unqualified` for a wait task, else None.
|
||||
|
||||
The hub's `wait_kind` wins when present; otherwise derive from
|
||||
`needs_human` (Case B) and dependency edges (Case A).
|
||||
"""
|
||||
if str(task.get("status") or "") != "wait":
|
||||
return None
|
||||
hub = _hub_kind(task.get("wait_kind"), WAIT_KINDS)
|
||||
if hub:
|
||||
return hub
|
||||
human = bool(task.get("needs_human"))
|
||||
if human and has_external:
|
||||
return "both"
|
||||
if human:
|
||||
return "human"
|
||||
if has_external:
|
||||
return "external"
|
||||
return "unqualified"
|
||||
|
||||
|
||||
def workplan_blocked_kind(workplan: dict[str, Any], wait_kinds: list[str | None]) -> str | None:
|
||||
"""`human` when any wait task is a human gate, else `external`, else `none`."""
|
||||
if str(workplan.get("status") or "") != "blocked":
|
||||
return None
|
||||
hub = _hub_kind(workplan.get("blocked_kind"), BLOCKED_KINDS)
|
||||
if hub:
|
||||
return hub
|
||||
kinds = {kind for kind in wait_kinds if kind}
|
||||
if kinds & HUMAN_WAIT_KINDS:
|
||||
return "human"
|
||||
if "external" in kinds:
|
||||
return "external"
|
||||
return "none"
|
||||
|
||||
|
||||
def _index_workplans(
|
||||
open_wps: list[dict[str, Any]],
|
||||
) -> tuple[dict[str, dict[str, Any]], dict[str, str], dict[str, str]]:
|
||||
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")
|
||||
}
|
||||
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"])
|
||||
return wp_by_id, slug_to_id, record_to_id
|
||||
|
||||
|
||||
TASK_NODE_SIZE = 26
|
||||
WORKPLAN_BASE_SIZE = 44
|
||||
WORKPLAN_SIZE_STEP = 10
|
||||
|
|
@ -83,28 +174,23 @@ def coordination_graph_payload(
|
|||
tasks: list[dict[str, Any]],
|
||||
*,
|
||||
include_residuals: bool = False,
|
||||
needs_human_only: bool = False,
|
||||
dependencies: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a GraphExplorerPayload of open workplans and their open tasks."""
|
||||
"""Build a GraphExplorerPayload of open workplans and their open tasks.
|
||||
|
||||
`dependencies` are raw `/workplans/{id}/dependencies/` rows
|
||||
(`from_workplan_id`, `to_workplan_id`, `to_task_id`); they qualify a wait
|
||||
task as external when the task cites the row's target. `needs_human_only`
|
||||
keeps only `blocked_kind: human` workplans and their human-wait tasks.
|
||||
"""
|
||||
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")
|
||||
}
|
||||
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"])
|
||||
wp_by_id, slug_to_id, record_to_id = _index_workplans(open_wps)
|
||||
|
||||
open_tasks = [
|
||||
task
|
||||
|
|
@ -114,6 +200,51 @@ def coordination_graph_payload(
|
|||
and (include_residuals or not is_residual_flavor(_flavor(task)))
|
||||
]
|
||||
|
||||
# Dependency targets per workplan: indexed stubs plus raw dependency rows.
|
||||
dep_targets: dict[str, set[str]] = {}
|
||||
for wp in open_wps:
|
||||
dep_targets[str(wp["id"])] = set(_indexed_depends_targets(wp))
|
||||
for row in dependencies or []:
|
||||
src = str(row.get("from_workplan_id") or row.get("from_workstream_id") or "")
|
||||
dst = row.get("to_workplan_id") or row.get("to_workstream_id") or row.get("to_task_id")
|
||||
if src in dep_targets and dst:
|
||||
dep_targets[src].add(str(dst))
|
||||
|
||||
def _resolve(raw: str) -> str | None:
|
||||
if raw in wp_by_id:
|
||||
return raw
|
||||
return record_to_id.get(raw.upper()) or slug_to_id.get(raw.lower())
|
||||
|
||||
wait_kind_by_task: dict[str, str | None] = {}
|
||||
for task in open_tasks:
|
||||
wp_id = str(task.get("workplan_id") or task.get("workstream_id"))
|
||||
external = bool(_task_depends_targets(task))
|
||||
if not external and str(task.get("status") or "") == "wait":
|
||||
resolved = {_resolve(dst) or dst for dst in dep_targets.get(wp_id, set())}
|
||||
cited = {_resolve(token) or token for token in _cited_tokens(task)}
|
||||
external = bool(resolved & cited)
|
||||
wait_kind_by_task[str(task["id"])] = task_wait_kind(task, has_external=external)
|
||||
blocked_kind_by_wp: dict[str, str | None] = {}
|
||||
for wp in open_wps:
|
||||
wp_id = str(wp["id"])
|
||||
kinds = [
|
||||
wait_kind_by_task[str(task["id"])]
|
||||
for task in open_tasks
|
||||
if str(task.get("workplan_id") or task.get("workstream_id")) == wp_id
|
||||
]
|
||||
blocked_kind_by_wp[wp_id] = workplan_blocked_kind(wp, kinds)
|
||||
|
||||
if needs_human_only:
|
||||
open_wps = [wp for wp in open_wps if blocked_kind_by_wp[str(wp["id"])] == "human"]
|
||||
wp_by_id, slug_to_id, record_to_id = _index_workplans(open_wps)
|
||||
open_tasks = [
|
||||
task
|
||||
for task in open_tasks
|
||||
if str(task.get("workplan_id") or task.get("workstream_id")) in wp_by_id
|
||||
and wait_kind_by_task[str(task["id"])] in HUMAN_WAIT_KINDS
|
||||
]
|
||||
task_ids = {str(task["id"]) for task in open_tasks}
|
||||
|
||||
elements: list[dict[str, Any]] = []
|
||||
for wp in open_wps:
|
||||
wp_id = str(wp["id"])
|
||||
|
|
@ -135,6 +266,7 @@ def coordination_graph_payload(
|
|||
"repo": wp.get("repo") or wp.get("slug"),
|
||||
"displayState": "show" if status != "blocked" else "highlight",
|
||||
"unresolved": status in {"blocked", "proposed"},
|
||||
"blocked_kind": blocked_kind_by_wp[wp_id],
|
||||
"chokepoint": 0,
|
||||
"color": FLAVOR_COLORS.get(flavor or "", "#1d4ed8"),
|
||||
}
|
||||
|
|
@ -143,12 +275,17 @@ def coordination_graph_payload(
|
|||
|
||||
belongs = 0
|
||||
wait_edges = 0
|
||||
wait_kind_counts = {kind: 0 for kind in sorted(WAIT_KINDS)}
|
||||
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"))
|
||||
flavor = _flavor(task) or _flavor(wp_by_id.get(wp_id) or {})
|
||||
wait_kind = wait_kind_by_task[task_id]
|
||||
human_gate = wait_kind in HUMAN_WAIT_KINDS
|
||||
if wait_kind:
|
||||
wait_kind_counts[wait_kind] += 1
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
|
|
@ -163,12 +300,19 @@ def coordination_graph_payload(
|
|||
"flavor": flavor,
|
||||
"nodeClass": flavor or "unspecified",
|
||||
"needsHuman": human,
|
||||
"wait_kind": wait_kind,
|
||||
# Human gates have no edge target; the marker lives on the node.
|
||||
"humanGate": human_gate,
|
||||
"displayState": "highlight" if status == "wait" or human else "show",
|
||||
"unresolved": status == "wait" or human,
|
||||
"visualSize": TASK_NODE_SIZE,
|
||||
}
|
||||
}
|
||||
)
|
||||
if human_gate:
|
||||
elements[-1]["data"]["color"] = WAIT_KIND_COLORS["human"]
|
||||
elif wait_kind == "external":
|
||||
elements[-1]["data"]["color"] = WAIT_KIND_COLORS["external"]
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
|
|
@ -198,12 +342,7 @@ def coordination_graph_payload(
|
|||
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())
|
||||
)
|
||||
dst = _resolve(dst_raw)
|
||||
if not dst or dst == src or dst not in wp_by_id:
|
||||
continue
|
||||
pair = (src, dst)
|
||||
|
|
@ -222,6 +361,7 @@ def coordination_graph_payload(
|
|||
"target": f"workplan:{dst}",
|
||||
"edgeType": "depends_on",
|
||||
"edgeSource": "indexed",
|
||||
"edge_kind": "commitment",
|
||||
"strength": "strong",
|
||||
"sourceLayer": "workplan",
|
||||
"targetLayer": "workplan",
|
||||
|
|
@ -231,14 +371,49 @@ def coordination_graph_payload(
|
|||
depends_count += 1
|
||||
chokepoint[dst] = chokepoint.get(dst, 0) + 1
|
||||
|
||||
# Task-level depends_on (CUST-WP-0074 Case A): task -> workplan or task.
|
||||
task_pairs: set[tuple[str, str]] = set()
|
||||
task_depends_count = 0
|
||||
for task in open_tasks:
|
||||
task_id = str(task["id"])
|
||||
src_wp = str(task.get("workplan_id") or task.get("workstream_id"))
|
||||
for dst_raw in _task_depends_targets(task):
|
||||
if dst_raw in task_ids and dst_raw != task_id:
|
||||
target, target_layer = f"task:{dst_raw}", "task"
|
||||
else:
|
||||
dst = _resolve(dst_raw)
|
||||
if not dst or dst == src_wp or dst not in wp_by_id:
|
||||
continue
|
||||
target, target_layer = f"workplan:{dst}", "workplan"
|
||||
chokepoint[dst] = chokepoint.get(dst, 0) + 1
|
||||
if (task_id, target) in task_pairs:
|
||||
continue
|
||||
task_pairs.add((task_id, target))
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": f"edge:task-depends:{task_id}:{target}",
|
||||
"stableKey": f"edge:task-depends:{task_id}:{target}",
|
||||
"kind": "Edge",
|
||||
"layer": "dependency",
|
||||
"displayState": "show",
|
||||
"source": f"task:{task_id}",
|
||||
"target": target,
|
||||
"edgeType": "depends_on",
|
||||
"edgeSource": "task",
|
||||
"edge_kind": "commitment",
|
||||
"strength": "strong",
|
||||
"sourceLayer": "task",
|
||||
"targetLayer": target_layer,
|
||||
}
|
||||
}
|
||||
)
|
||||
task_depends_count += 1
|
||||
|
||||
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())):
|
||||
for token in _cited_tokens(task):
|
||||
dst = record_to_id.get(token) or slug_to_id.get(token.lower())
|
||||
if not dst or dst == src_wp or dst not in wp_by_id:
|
||||
continue
|
||||
|
|
@ -256,6 +431,7 @@ def coordination_graph_payload(
|
|||
"target": f"workplan:{dst}",
|
||||
"edgeType": "waits_on",
|
||||
"edgeSource": "citation",
|
||||
"edge_kind": "citation",
|
||||
"strength": "strong",
|
||||
"sourceLayer": "workplan",
|
||||
"targetLayer": "workplan",
|
||||
|
|
@ -290,10 +466,13 @@ def coordination_graph_payload(
|
|||
"open_tasks": len(open_tasks),
|
||||
"belongs_to_edges": belongs,
|
||||
"depends_on_edges": depends_count,
|
||||
"task_depends_on_edges": task_depends_count,
|
||||
"citation_edges": cited,
|
||||
"wait_or_human_tasks": wait_edges,
|
||||
**{f"wait_{kind}_tasks": count for kind, count in wait_kind_counts.items()},
|
||||
"residual_open_workplans": residual_open,
|
||||
"include_residuals": include_residuals,
|
||||
"needs_human_only": needs_human_only,
|
||||
},
|
||||
"elements": elements,
|
||||
"hidden_elements": [],
|
||||
|
|
@ -301,6 +480,7 @@ def coordination_graph_payload(
|
|||
|
||||
|
||||
def fetch_hub_lists(api_base: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Open workplans and all tasks; `/state/deps` stubs carry the dependency rows."""
|
||||
base = api_base.rstrip("/")
|
||||
workplans: list[dict[str, Any]] = []
|
||||
for status in sorted(OPEN_WORKPLAN):
|
||||
|
|
|
|||
|
|
@ -409,6 +409,10 @@ def graph_explorer_page() -> str:
|
|||
<span class="field-label">Residuals <button type="button" class="help-tip" aria-label="Residuals help" data-help-title="Include residuals" data-help="Default coordination view omits flavor=residual workplans. Enable this to show them. Unspecified flavor stays visible. Titles containing the word residual are not treated as residual.">?</button></span>
|
||||
<input type="checkbox" id="include-residuals" aria-label="Include residual workplans">
|
||||
</label>
|
||||
<label id="needs-human-wrap" class="field" style="display:none;align-self:end">
|
||||
<span class="field-label">Needs human <button type="button" class="help-tip" aria-label="Needs human help" data-help-title="Needs human only" data-help="Show only workplans blocked on a human gate (blocked_kind=human) and their human-wait tasks. External commitments (depends_on) are hidden. This is the operator queue.">?</button></span>
|
||||
<input type="checkbox" id="needs-human" aria-label="Needs human only">
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">Layout <button type="button" class="help-tip" aria-label="Layout help" data-help-title="Layout" data-help="Layout redraws the map arrangement. Cose uses relationship strength and repo affinity; circle, grid, concentric, and breadthfirst are simpler alternate arrangements.">?</button></span>
|
||||
<select id="layout-select" aria-label="Graph layout">
|
||||
|
|
@ -572,8 +576,13 @@ def graph_explorer_page() -> str:
|
|||
let includeResidualsParam = ["1", "true", "yes"].includes(
|
||||
(pageParams.get("include_residuals") || "").toLowerCase()
|
||||
);
|
||||
let needsHumanParam = ["1", "true", "yes"].includes(
|
||||
(pageParams.get("needs_human") || "").toLowerCase()
|
||||
);
|
||||
const coordinationUrl = (includeResiduals, needsHuman) =>
|
||||
`/exports/graph-explorer?mode=coordination${includeResiduals ? "&include_residuals=true" : ""}${needsHuman ? "&needs_human=true" : ""}`;
|
||||
const graphUrl = graphMode === "coordination"
|
||||
? `/exports/graph-explorer?mode=coordination${includeResidualsParam ? "&include_residuals=true" : ""}`
|
||||
? coordinationUrl(includeResidualsParam, needsHumanParam)
|
||||
: "/exports/graph-explorer";
|
||||
const canvas = document.getElementById("graph-canvas");
|
||||
const zoneOverlay = document.getElementById("zone-overlay");
|
||||
|
|
@ -2142,6 +2151,7 @@ def graph_explorer_page() -> str:
|
|||
if (state.search) params.set("search", state.search);
|
||||
if (state.mode && state.mode !== "full") params.set("mode", state.mode);
|
||||
if (state.mode === "coordination" && includeResidualsParam) params.set("include_residuals", "true");
|
||||
if (state.mode === "coordination" && needsHumanParam) params.set("needs_human", "true");
|
||||
if (state.layout && state.layout !== "cose") params.set("layout", state.layout);
|
||||
if (state.labelMode && state.labelMode !== "auto") params.set("labelMode", state.labelMode);
|
||||
if (state.nodeTypes.length !== allNodeTypes.length) params.set("nodeTypes", state.nodeTypes.join(","));
|
||||
|
|
@ -2203,7 +2213,7 @@ def graph_explorer_page() -> str:
|
|||
|
||||
const applyViewState = (state, options = {}) => {
|
||||
if (cy && "mode" in state && ((state.mode === "coordination") !== (graphMode === "coordination"))) {
|
||||
switchGraph(state.mode, includeResidualsParam, state);
|
||||
switchGraph(state.mode, includeResidualsParam, state, needsHumanParam);
|
||||
return;
|
||||
}
|
||||
if ("search" in state) searchInput.value = state.search || "";
|
||||
|
|
@ -2836,6 +2846,15 @@ def graph_explorer_page() -> str:
|
|||
switchGraph("coordination", residualsToggle.checked);
|
||||
});
|
||||
}
|
||||
const needsHumanWrap = document.getElementById("needs-human-wrap");
|
||||
const needsHumanToggle = document.getElementById("needs-human");
|
||||
if (needsHumanWrap && needsHumanToggle) {
|
||||
needsHumanWrap.style.display = graphMode === "coordination" ? "" : "none";
|
||||
needsHumanToggle.checked = needsHumanParam;
|
||||
needsHumanToggle.addEventListener("change", () => {
|
||||
switchGraph("coordination", includeResidualsParam, null, needsHumanToggle.checked);
|
||||
});
|
||||
}
|
||||
profiles = loadProfiles();
|
||||
renderProfiles();
|
||||
const elements = (payload.elements || []).map((element) => ({
|
||||
|
|
@ -2903,6 +2922,10 @@ def graph_explorer_page() -> str:
|
|||
{selector: "node[flavor = 'refactoring']", style: {"background-color": "#b45309"}},
|
||||
{selector: "node[flavor = 'extension']", style: {"background-color": "#0f766e"}},
|
||||
{selector: "node[flavor = 'residual']", style: {"background-color": "#64748b"}},
|
||||
{selector: "edge[edge_kind = 'commitment']", style: {"line-color": "#0891b2", "target-arrow-color": "#0891b2"}},
|
||||
{selector: "node[wait_kind = 'external']", style: {"background-color": "#0891b2"}},
|
||||
{selector: "node[humanGate = true]", style: {"background-color": "#be123c", "border-color": "#be123c", "border-style": "solid", "border-width": 3}},
|
||||
{selector: "node[wait_kind = 'both']", style: {"border-color": "#0891b2"}},
|
||||
{selector: "edge[zoneCollapse = true]", style: {"line-style": "dashed", "line-color": "#0f766e", "target-arrow-color": "#0f766e"}},
|
||||
{selector: "node.rule-highlight", style: {
|
||||
"border-color": "#2563eb",
|
||||
|
|
@ -2974,11 +2997,11 @@ def graph_explorer_page() -> str:
|
|||
};
|
||||
|
||||
let graphRequest = 0;
|
||||
const switchGraph = async (mode, includeResiduals = includeResidualsParam, viewState = null) => {
|
||||
const switchGraph = async (mode, includeResiduals = includeResidualsParam, viewState = null, needsHuman = needsHumanParam) => {
|
||||
const request = ++graphRequest;
|
||||
const coordination = mode === "coordination";
|
||||
const url = coordination
|
||||
? `/exports/graph-explorer?mode=coordination${includeResiduals ? "&include_residuals=true" : ""}`
|
||||
? coordinationUrl(includeResiduals, needsHuman)
|
||||
: "/exports/graph-explorer";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
|
@ -2992,10 +3015,13 @@ def graph_explorer_page() -> str:
|
|||
}));
|
||||
graphMode = coordination ? "coordination" : "full";
|
||||
includeResidualsParam = includeResiduals;
|
||||
needsHumanParam = needsHuman;
|
||||
activeMode = mode;
|
||||
modeSelect.value = mode;
|
||||
document.getElementById("include-residuals-wrap").style.display = coordination ? "" : "none";
|
||||
document.getElementById("include-residuals").checked = includeResiduals;
|
||||
document.getElementById("needs-human-wrap").style.display = coordination ? "" : "none";
|
||||
document.getElementById("needs-human").checked = needsHuman;
|
||||
selected = null;
|
||||
selectedAnchor = null;
|
||||
selectedZoneId = "";
|
||||
|
|
@ -3031,6 +3057,7 @@ def graph_explorer_page() -> str:
|
|||
if (request !== graphRequest) return;
|
||||
modeSelect.value = activeMode;
|
||||
document.getElementById("include-residuals").checked = includeResidualsParam;
|
||||
document.getElementById("needs-human").checked = needsHumanParam;
|
||||
profileSummary.textContent = `Could not load graph: ${error.message}`;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -144,8 +144,13 @@ class RegistryHandler(BaseHTTPRequestHandler):
|
|||
"true",
|
||||
"yes",
|
||||
}
|
||||
needs_human = (_query_optional(query, "needs_human") or "").lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
}
|
||||
return HTTPStatus.OK, coordination_graph_payload(
|
||||
workplans, tasks, include_residuals=include
|
||||
workplans, tasks, include_residuals=include, needs_human_only=needs_human
|
||||
)
|
||||
return HTTPStatus.OK, fabric_graph_explorer_payload(
|
||||
self.store.combined_graph(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue