Implement Workstream Health Index (WHI) KPI card

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 00:03:27 +01:00
parent fc7dfa1b64
commit efd13b13dd
3 changed files with 328 additions and 0 deletions

View File

@@ -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" },

View File

@@ -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 |
| 2040% | 🟠 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 |
| 2540% | 🟠 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 |
| 3060% | 🟠 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`*

View File

@@ -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`<div class="live-indicator">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
${_ok
@@ -57,6 +143,63 @@ const _liveEl = html`<div class="live-indicator">
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
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`<div class="kpi-infobox whi-box">
<div class="kpi-infobox-title">Workstream Health</div>
${_openCount === 0
? html`<div class="kpi-row"><span class="kpi-muted">No active workstreams</span></div>`
: html`
<div class="whi-score-row">
<span class="whi-value" style="color:${_whiColor(_WHI)}">${(_WHI*100).toFixed(0)}<span class="whi-pct">%</span></span>
<span class="whi-label" style="color:${_whiColor(_WHI)}">${_whiLabel(_WHI)}</span>
</div>
${_CPI === 1 ? html`<div class="whi-cycle-alert">⚠ Cycle detected — deadlock</div>` : ""}
<div class="whi-metrics">
${_whiMetrics.map(m => {
const lv = _warnLevel(m.name, m.val);
return html`<div class="whi-metric-row">
<span class="whi-metric-name" style="color:${_warnColor(lv)}">${m.name}</span>
<span class="whi-metric-val" style="color:${_warnColor(lv)}">${m.fmt(m.val)}</span>
</div>`;
})}
</div>
${_domainBreakdown.length > 1 ? html`
<div class="whi-domains">
<div class="whi-domain-header">by domain</div>
${_domainBreakdown.map(d => html`<div class="whi-domain-row">
<span class="whi-domain-dot" style="background:${_whiColor(d.whi)}"></span>
<span class="whi-domain-name">${d.domain}</span>
<span class="whi-domain-score" style="color:${_whiColor(d.whi)}">${(d.whi*100).toFixed(0)}%</span>
${d.cpi === 1 ? html`<span style="color:#d97706;font-size:0.7rem" title="cycle detected">⚠</span>` : ""}
</div>`)}
</div>` : ""}
`}
</div>`;
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) {
<style>
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
/* ── KPI infobox base (shared) ───────────────────────────────────────────── */
.kpi-infobox { background: var(--theme-background-alt, #f9f9f9); border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 10px; padding: 0.75rem 1rem; position: relative; box-shadow: 0 1px 6px rgba(0,0,0,0.07); margin-bottom: 1.25rem; }
.kpi-infobox-title { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-muted, #888); margin-bottom: 0.55rem; padding-right: 1.6rem; }
.kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; }
.kpi-muted { color: var(--theme-foreground-faint, #aaa); font-style: italic; font-size: 0.8rem; }
/* ── WHI card ────────────────────────────────────────────────────────────── */
.whi-score-row { display: flex; align-items: baseline; gap: 0.4rem; margin: 0.35rem 0 0.5rem; }
.whi-value { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.whi-pct { font-size: 1rem; font-weight: 600; }
.whi-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; align-self: center; }
.whi-cycle-alert { background: #fef2f2; color: #dc2626; border-radius: 4px; padding: 0.2rem 0.45rem; font-size: 0.72rem; font-weight: 600; margin-bottom: 0.4rem; }
.whi-metrics { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; margin-bottom: 0.35rem; }
.whi-metric-row { display: flex; justify-content: space-between; padding: 0.16rem 0; }
.whi-metric-name { font-family: monospace; font-size: 0.72rem; }
.whi-metric-val { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.78rem; }
.whi-domains { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; }
.whi-domain-header { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-faint, #aaa); margin-bottom: 0.2rem; }
.whi-domain-row { display: flex; align-items: center; gap: 0.3rem; padding: 0.1rem 0; }
.whi-domain-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.whi-domain-name { flex: 1; font-size: 0.75rem; color: var(--theme-foreground-muted, #666); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.whi-domain-score { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.75rem; }
.dim { color: gray; font-style: italic; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
.filter-owner { display: flex; align-items: center; }