generated from coulomb/repo-seed
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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user