From efd13b13dd67eadbbcbff3a4fe0c746bdbd49918 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 27 Feb 2026 00:03:27 +0100 Subject: [PATCH] Implement Workstream Health Index (WHI) KPI card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a live WHI card to the Workstreams page TOC sidebar. All six base metrics from the spec (workstream-kpi.md) computed client-side from existing data — no API or schema changes required. Computation (workstreams.md): - DD: dependency edges / open workstreams (normalised at DD_crit=1.0) - BR: blocked workstreams / open workstreams - SPR: max inbound deps on one incomplete workstream / open count - PEP: active workstreams with all deps completed / open count - CDDR: cross-domain edges / total edges - CPI: DFS cycle detection (back-edge = 1, halves WHI as hard penalty) - WHI = 0.30(1-DDnorm) + 0.25(1-BR) + 0.15(1-SPR) + 0.20·PEP + 0.10(1-CDDR) - Per-domain breakdown using intra-domain edges only Card UI: global WHI % with green/orange/red health label, sub-metric rows with per-spec warning thresholds, cycle alert panel, per-domain breakdown rows with coloured dots. Also add src/docs/workstream-health-index.md reference page (formula, thresholds, improvement guidance) and wire ? button on the card. Add "Workstream Health" to Reference nav. Co-Authored-By: Claude Sonnet 4.6 --- state-hub/dashboard/observablehq.config.js | 1 + .../src/docs/workstream-health-index.md | 161 +++++++++++++++++ state-hub/dashboard/src/workstreams.md | 166 ++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 state-hub/dashboard/src/docs/workstream-health-index.md diff --git a/state-hub/dashboard/observablehq.config.js b/state-hub/dashboard/observablehq.config.js index 28ff4f4..54ade8e 100644 --- a/state-hub/dashboard/observablehq.config.js +++ b/state-hub/dashboard/observablehq.config.js @@ -11,6 +11,7 @@ export default { pages: [ { name: "Live Data", path: "/docs/live-data" }, { name: "Workstreams", path: "/docs/workstreams" }, + { name: "Workstream Health", path: "/docs/workstream-health-index" }, { name: "Decisions", path: "/docs/decisions" }, { name: "Decision Health", path: "/docs/decisions-kpi" }, { name: "Progress Log", path: "/docs/progress-log" }, diff --git a/state-hub/dashboard/src/docs/workstream-health-index.md b/state-hub/dashboard/src/docs/workstream-health-index.md new file mode 100644 index 0000000..dca138b --- /dev/null +++ b/state-hub/dashboard/src/docs/workstream-health-index.md @@ -0,0 +1,161 @@ +--- +title: Workstream Health Index — Reference +--- + +# Workstream Health Index (WHI) + +The **Workstream Health Index** is a composite score in the range [0, 1] that measures how well the workstream network is structured for parallel execution and stable progress. It is displayed as a live KPI card in the right margin of the Workstreams page and recomputes on every poll (every 15 seconds). + +**1.0 = ideal independence · 0.0 = severe systemic dysfunction** + +--- + +## Health states + +| Score | Color | Label | Meaning | +|---|---|---|---| +| ≥ 0.75 | 🟢 green | Healthy | Parallel execution effective, delays localized | +| 0.50 – 0.74 | 🟠 orange | Optimizable | Noticeable coordination cost; review decomposition | +| < 0.50 | 🔴 red | Critical | Serial execution dominates; immediate replanning required | + +--- + +## The six base metrics + +### DD — Dependency Density + +``` +DD = total dependency edges / (active + blocked workstreams) +``` + +Measures structural coupling. Low DD means independent, parallelizable work. Completed and archived workstreams are excluded — they no longer constrain progress. + +| DD | Warning | +|---|---| +| > 1.0 | 🔴 red — more than one dependency per workstream on average | +| 0.5 – 1.0 | 🟠 orange | +| ≤ 0.5 | ok | + +--- + +### BR — Blocked Ratio + +``` +BR = blocked workstreams / (active + blocked workstreams) +``` + +Measures immediate operational impact. BR ≈ 0 means flow is unobstructed. + +| BR | Warning | +|---|---| +| > 40% | 🔴 red | +| 20–40% | 🟠 orange | +| ≤ 20% | ok | + +--- + +### SPR — Single-Point Risk + +``` +SPR = max dependents on one incomplete workstream / (active + blocked) +``` + +Detects concentration of blocking power. High SPR means one delay propagates widely — a structural SPOF. + +| SPR | Warning | +|---|---| +| > 40% | 🔴 red | +| 25–40% | 🟠 orange | +| ≤ 25% | ok | + +--- + +### PEP — Parallel Execution Potential + +``` +PEP = active workstreams with all deps completed / (active + blocked) +``` + +Estimates how much work can proceed right now. A workstream is eligible if its status is `active` and every workstream it depends on has reached `completed` or `archived`. + +| PEP | Warning | +|---|---| +| < 30% | 🔴 red | +| 30–60% | 🟠 orange | +| ≥ 60% | ok | + +--- + +### CDDR — Cross-Domain Dependency Ratio + +``` +CDDR = dependency edges crossing domain boundaries / total edges +``` + +Measures architectural entanglement. High CDDR indicates loss of modularity across the six project domains. + +| CDDR | Warning | +|---|---| +| > 40% | 🟠 orange | + +--- + +### CPI — Cycle Presence Indicator + +``` +CPI = 0 → no cycles +CPI = 1 → at least one circular dependency detected +``` + +Detected via DFS with inStack colouring. Any cycle means no feasible execution order exists — a structural deadlock. When CPI = 1, the final WHI score is **halved** as a hard penalty. + +--- + +## Aggregation formula + +``` +DDnorm = min(1, DD / 1.0) ← saturates at DD_critical = 1.0 + +WHI = 0.30 × (1 − DDnorm) + + 0.25 × (1 − BR) + + 0.15 × (1 − SPR) + + 0.20 × PEP + + 0.10 × (1 − CDDR) + +if CPI = 1: WHI = WHI × 0.5 +``` + +Result is clamped to [0, 1]. + +--- + +## Domain breakdown + +The card also shows a per-domain WHI computed using **intra-domain workstreams and intra-domain edges only**. This measures each domain's internal autonomy — how well its workstreams are decomposed relative to each other, independent of cross-domain dependencies. + +A domain with WHI = 100% is fully self-contained and parallelizable internally. Its global contribution to the program-level WHI may still be reduced by cross-domain dependencies (captured in CDDR). + +The domain breakdown is shown when at least two domains have active workstreams. + +--- + +## How to improve a poor score + +| Symptom | Action | +|---|---| +| High DD | Decompose tightly coupled workstreams; remove unnecessary dependencies | +| High BR | Unblock workstreams — resolve the blocking condition, or mark dependency as completed if done | +| High SPR | Split the bottleneck workstream into independent deliverables | +| Low PEP | Complete prerequisite workstreams or re-sequence work | +| High CDDR | Refactor cross-domain dependencies into shared contracts or invert the dependency | +| CPI = 1 | Find and break the cycle — identify which dependency edge is incorrect and remove it | + +--- + +## What WHI is not + +WHI measures **structural health of the work graph** — not individual performance, not velocity, not burn-down rate. A team can be moving fast with a poor WHI (serially but quickly), or slowly with a perfect WHI (fully parallel but under-resourced). Use WHI alongside velocity metrics, not instead of them. + +--- + +*Specification: `state-hub/dashboard/src/docs/workstream-kpi.md`* diff --git a/state-hub/dashboard/src/workstreams.md b/state-hub/dashboard/src/workstreams.md index 20856a7..b564998 100644 --- a/state-hub/dashboard/src/workstreams.md +++ b/state-hub/dashboard/src/workstreams.md @@ -44,12 +44,98 @@ const _ok = wsState.ok ?? false; const _ts = wsState.ts; ``` +```js +// ── Workstream Health Index (WHI) ──────────────────────────────────────────── +const _idToDomain = Object.fromEntries(data.map(w => [w.id, w.domain ?? "unknown"])); +const _completedIds = new Set(data.filter(w => w.status === "completed" || w.status === "archived").map(w => w.id)); +const _openCount = openWs.length; +const _allEdges = openWs.flatMap(w => w.depends_on.map(d => ({from: w.id, to: d.workstream_id}))); +const _totalEdges = _allEdges.length; + +// Dependency Density +const _DD = _openCount > 0 ? _totalEdges / _openCount : 0; + +// Blocked Ratio +const _BR = _openCount > 0 ? openWs.filter(w => w.status === "blocked").length / _openCount : 0; + +// Single-Point Risk — max inbound edges on one incomplete workstream +const _inbound = {}; +for (const e of _allEdges) { + if (!_completedIds.has(e.to)) _inbound[e.to] = (_inbound[e.to] ?? 0) + 1; +} +const _SPR = _openCount > 0 + ? (Object.keys(_inbound).length > 0 ? Math.max(...Object.values(_inbound)) : 0) / _openCount + : 0; + +// Parallel Execution Potential — active workstreams with all deps completed +const _PEP = _openCount > 0 + ? openWs.filter(w => w.status === "active" && w.depends_on.every(d => _completedIds.has(d.workstream_id))).length / _openCount + : 0; + +// Cross-Domain Dependency Ratio +const _crossEdges = _allEdges.filter(e => (_idToDomain[e.from] ?? "?") !== (_idToDomain[e.to] ?? "?")).length; +const _CDDR = _totalEdges > 0 ? _crossEdges / _totalEdges : 0; + +// Cycle Presence Indicator — DFS with visited/inStack colouring +function _detectCycle(nodes, edges) { + const adj = Object.fromEntries(nodes.map(n => [n.id, []])); + for (const e of edges) { if (adj[e.from] !== undefined) adj[e.from].push(e.to); } + const visited = new Set(), inStack = new Set(); + function dfs(id) { + if (inStack.has(id)) return true; + if (visited.has(id)) return false; + visited.add(id); inStack.add(id); + for (const nx of (adj[id] ?? [])) { if (dfs(nx)) return true; } + inStack.delete(id); + return false; + } + for (const n of nodes) { if (!visited.has(n.id) && dfs(n.id)) return 1; } + return 0; +} +const _CPI = _detectCycle(openWs, _allEdges); + +// WHI aggregation — DD normalised at DD_critical = 1.0, CPI halves the score +const _DDnorm = Math.min(1, _DD / 1.0); +let _WHI = 0.30*(1 - _DDnorm) + 0.25*(1 - _BR) + 0.15*(1 - _SPR) + 0.20*_PEP + 0.10*(1 - _CDDR); +if (_CPI === 1) _WHI *= 0.5; +_WHI = Math.max(0, Math.min(1, _WHI)); + +// Per-domain breakdown — intra-domain edges only (measures domain autonomy) +const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unknown"))].sort().map(domain => { + const nodes = openWs.filter(w => (_idToDomain[w.id] ?? "unknown") === domain); + const edges = nodes.flatMap(w => + w.depends_on + .filter(d => (_idToDomain[d.workstream_id] ?? "unknown") === domain) + .map(d => ({from: w.id, to: d.workstream_id})) + ); + const oc = nodes.length; + if (oc === 0) return null; + const te = edges.length; + const dd = oc > 0 ? te / oc : 0; + const br = oc > 0 ? nodes.filter(w => w.status === "blocked").length / oc : 0; + const pep = oc > 0 ? nodes.filter(w => { + if (w.status !== "active") return false; + const intraDeps = w.depends_on.filter(d => (_idToDomain[d.workstream_id] ?? "unknown") === domain); + return intraDeps.every(d => _completedIds.has(d.workstream_id)); + }).length / oc : 0; + const inb = {}; + for (const e of edges) inb[e.to] = (inb[e.to] ?? 0) + 1; + const spr = oc > 0 ? (Object.keys(inb).length > 0 ? Math.max(...Object.values(inb)) : 0) / oc : 0; + const cpi = _detectCycle(nodes, edges); + const ddN = Math.min(1, dd / 1.0); + let whi = 0.30*(1 - ddN) + 0.25*(1 - br) + 0.15*(1 - spr) + 0.20*pep + 0.10; // CDDR=0 within domain + if (cpi === 1) whi *= 0.5; + return {domain, whi: Math.max(0, Math.min(1, whi)), br, pep, cpi, openCount: oc}; +}).filter(Boolean); +``` + # Workstreams ```js import {injectTocTop} from "./components/toc-sidebar.js"; import {withDocHelp} from "./components/doc-overlay.js"; +// ── Live indicator ──────────────────────────────────────────────────────────── const _liveEl = html`
${_ok @@ -57,6 +143,63 @@ const _liveEl = html`
: html`Offline — run: make api`}
`; withDocHelp(_liveEl, "/docs/live-data"); + +// ── WHI card ────────────────────────────────────────────────────────────────── +function _whiColor(v) { return v >= 0.75 ? "#16a34a" : v >= 0.50 ? "#d97706" : "#dc2626"; } +function _whiLabel(v) { return v >= 0.75 ? "Healthy" : v >= 0.50 ? "Optimizable" : "Critical"; } +function _warnLevel(name, val) { + if (name === "PEP") return val < 0.30 ? 2 : val < 0.60 ? 1 : 0; + if (name === "DD") return val > 1.0 ? 2 : val > 0.50 ? 1 : 0; + if (name === "BR") return val > 0.40 ? 2 : val > 0.20 ? 1 : 0; + if (name === "SPR") return val > 0.40 ? 2 : val > 0.25 ? 1 : 0; + if (name === "CDDR") return val > 0.40 ? 1 : 0; + return 0; +} +function _warnColor(lv) { return lv === 2 ? "#dc2626" : lv === 1 ? "#d97706" : "var(--theme-foreground-muted, #666)"; } + +const _whiMetrics = [ + {name: "DD", val: _DD, fmt: v => v.toFixed(2)}, + {name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%"}, + {name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%"}, + {name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%"}, + {name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%"}, +]; + +const _whiBox = html`
+
Workstream Health
+ ${_openCount === 0 + ? html`
No active workstreams
` + : html` +
+ ${(_WHI*100).toFixed(0)}% + ${_whiLabel(_WHI)} +
+ ${_CPI === 1 ? html`
⚠ Cycle detected — deadlock
` : ""} +
+ ${_whiMetrics.map(m => { + const lv = _warnLevel(m.name, m.val); + return html`
+ ${m.name} + ${m.fmt(m.val)} +
`; + })} +
+ ${_domainBreakdown.length > 1 ? html` +
+
by domain
+ ${_domainBreakdown.map(d => html`
+ + ${d.domain} + ${(d.whi*100).toFixed(0)}% + ${d.cpi === 1 ? html`` : ""} +
`)} +
` : ""} + `} +
`; +withDocHelp(_whiBox, "/docs/workstream-health-index"); + +// ── Inject into TOC sidebar: WHI first (lower), live last (top) ─────────────── +injectTocTop("whi-kpi-box", _whiBox); injectTocTop("live-indicator", _liveEl); const _h1 = document.querySelector("#observablehq-main h1"); @@ -165,6 +308,29 @@ if (wsWithDeps.length === 0) {