Qualify wait states in the coordination graph (CUST-WP-0074-T04)
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

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:
codex 2026-09-28 22:26:44 +02:00
parent b47dd43b04
commit 9dbb8f0bcb
7 changed files with 426 additions and 33 deletions

View file

@ -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}`;
}
};