T01: Fix datetime.utcnow() → datetime.now(tz=timezone.utc) in MCP server T02: Wrap _get/_post/_patch/_delete with try/except; return error dicts T03: Log warnings when write_log skips missing project path T04: Add priority + due_date_before filters to GET /tasks/ T05: Add owner + slug filters to GET /workstreams/ T06: Add offset param to GET /progress/ for proper pagination T07: Low-severity bundle: - CORS origins from CORS_ORIGINS env var (TD-017) - seed.py upsert domains+topics on re-run (TD-011) - normalise filter bar CSS → filter-text-input everywhere (TD-016) - add 30.5 avg-days-per-month comment in decisions.md (TD-019) - TD-009, TD-018 already resolved by existing code Closes CUST-WP-0018. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
364 lines
18 KiB
Markdown
364 lines
18 KiB
Markdown
---
|
|
title: Workstreams
|
|
---
|
|
|
|
```js
|
|
import {API, POLL} from "./components/config.js";
|
|
```
|
|
|
|
```js
|
|
// Fetch workstreams + topics + summary (for dep graph) in parallel
|
|
const wsState = (async function*() {
|
|
while (true) {
|
|
let data = [], openWs = [], ok = false;
|
|
try {
|
|
const [rw, rt, rr, rs] = await Promise.all([
|
|
fetch(`${API}/workstreams/`),
|
|
fetch(`${API}/topics/`),
|
|
fetch(`${API}/repos/`),
|
|
fetch(`${API}/state/summary`),
|
|
]);
|
|
ok = rw.ok && rt.ok && rr.ok && rs.ok;
|
|
if (ok) {
|
|
const [wsList, topicList, repoList, summary] = await Promise.all([rw.json(), rt.json(), rr.json(), rs.json()]);
|
|
const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
|
|
const repoMap = Object.fromEntries(repoList.map(r => [r.id, r]));
|
|
data = wsList.map(w => ({
|
|
...w,
|
|
domain: repoMap[w.repo_id]?.domain_slug ?? topicMap[w.topic_id]?.domain_slug ?? "unknown",
|
|
topic_title: topicMap[w.topic_id]?.title ?? "—",
|
|
}));
|
|
// open_workstreams from summary carry depends_on / blocks lists
|
|
openWs = summary.open_workstreams ?? [];
|
|
}
|
|
} catch {}
|
|
yield {data, openWs, ok, ts: new Date()};
|
|
await new Promise(res => setTimeout(res, POLL));
|
|
}
|
|
})();
|
|
```
|
|
|
|
```js
|
|
const data = wsState.data ?? [];
|
|
const openWs = wsState.openWs ?? [];
|
|
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";
|
|
import "./components/help-tip.js";
|
|
import {openEntityModal, buildEntityTable} from "./components/entity-modal.js";
|
|
|
|
// ── Live indicator ────────────────────────────────────────────────────────────
|
|
const _liveEl = html`<div class="live-indicator">
|
|
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
|
${_ok
|
|
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
|
: 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), label: "Dependency Density", desc: "Average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
|
|
{name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio", desc: "Share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
|
|
{name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk", desc: "Share of workstreams depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
|
|
{name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workstreams with zero blocking dependencies that could start or continue immediately."},
|
|
{name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", label: "Cross-Domain Dependency Ratio", desc: "Share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
|
|
];
|
|
|
|
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">
|
|
<help-tip class="whi-metric-name" style="color:${_warnColor(lv)}" label="${m.label}" description="${m.desc}" doc="/docs/workstream-health-index">${m.name}</help-tip>
|
|
<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>
|
|
<help-tip class="whi-domain-name"
|
|
label="${d.domain.replaceAll('_', ' ')}"
|
|
description="Domain-scoped WHI (intra-domain edges only). Open: ${d.openCount} · Blocked: ${(d.br*100).toFixed(0)}% · Runnable: ${(d.pep*100).toFixed(0)}%"
|
|
doc="/docs/workstream-health-index">${d.domain}</help-tip>
|
|
<span class="whi-domain-score" style="color:${_whiColor(d.whi)}">${(d.whi*100).toFixed(0)}%</span>
|
|
${d.cpi === 1 ? html`<help-tip style="color:#d97706;font-size:0.7rem" label="Dependency Cycle" description="A circular dependency exists within this domain — workstreams are waiting on each other and cannot all proceed." doc="/docs/workstream-health-index">⚠</help-tip>` : ""}
|
|
</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");
|
|
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/workstreams"); }
|
|
```
|
|
|
|
```js
|
|
import {MultiSelect} from "./components/multiselect.js";
|
|
|
|
// Load domain slugs from API (dynamic — works with new domains after v0.5)
|
|
const _domainsResp = await fetch(`${API}/domains/?status=active`).catch(() => null);
|
|
const DOMAINS = _domainsResp?.ok
|
|
? (await _domainsResp.json()).map(d => d.slug)
|
|
: ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
|
|
const STATUSES = ["active", "blocked", "completed", "archived"];
|
|
|
|
// Create filter form without displaying — shown below the chart
|
|
const _filtersForm = Inputs.form(
|
|
{
|
|
domain: MultiSelect(DOMAINS, {label: "Domain", placeholder: "All domains"}),
|
|
status: MultiSelect(STATUSES, {label: "Status", placeholder: "All statuses"}),
|
|
owner: Inputs.text({placeholder: "Owner…", style: "width:120px"}),
|
|
},
|
|
{
|
|
template: ({domain, status, owner}) => html`<div class="filter-bar">
|
|
${domain}${status}
|
|
<div class="filter-text-input">${owner}</div>
|
|
</div>`,
|
|
}
|
|
);
|
|
```
|
|
|
|
```js
|
|
const filters = Generators.input(_filtersForm);
|
|
```
|
|
|
|
```js
|
|
// Empty array = no filter applied (show all)
|
|
const filtered = data.filter(w =>
|
|
(filters.domain.length === 0 || filters.domain.includes(w.domain)) &&
|
|
(filters.status.length === 0 || filters.status.includes(w.status)) &&
|
|
(!filters.owner || (w.owner ?? "").toLowerCase().includes(filters.owner.toLowerCase()))
|
|
);
|
|
```
|
|
|
|
## Status Distribution
|
|
|
|
```js
|
|
import * as Plot from "npm:@observablehq/plot";
|
|
|
|
const byStatus = Object.entries(
|
|
filtered.reduce((acc, w) => { acc[w.status] = (acc[w.status] ?? 0) + 1; return acc; }, {})
|
|
).map(([status, count]) => ({status, count}));
|
|
|
|
display(Plot.plot({
|
|
marks: [
|
|
Plot.barX(byStatus, {y: "status", x: "count", fill: "status", tip: true}),
|
|
Plot.ruleX([0]),
|
|
],
|
|
marginLeft: 80,
|
|
width: 500,
|
|
}));
|
|
```
|
|
|
|
## All Workstreams
|
|
|
|
```js
|
|
display(_filtersForm);
|
|
|
|
{
|
|
// Enrich each workstream with tasks/deps data from open_workstreams summary
|
|
const _openWsMap = Object.fromEntries(openWs.map(w => [w.id, w]));
|
|
const _wsTable = buildEntityTable(
|
|
filtered,
|
|
[
|
|
{label: "Title", key: "title", cls: "et-title-col et-title-cell",
|
|
render: w => w.title},
|
|
{label: "Domain", key: "domain"},
|
|
{label: "Status", key: "status"},
|
|
{label: "Owner", render: w => w.owner ?? "—"},
|
|
{label: "Due", render: w => w.due_date ?? "—"},
|
|
{label: "Updated", render: w => new Date(w.updated_at).toLocaleDateString()},
|
|
],
|
|
w => openEntityModal({...w, ..._openWsMap[w.id]}, "workstream"),
|
|
);
|
|
display(_wsTable);
|
|
}
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
```js
|
|
// Build dep cards from the enriched open_workstreams in the summary
|
|
const wsWithDeps = openWs.filter(w => {
|
|
const domain = data.find(d => d.id === w.id)?.domain ?? "unknown";
|
|
return (filters.domain.length === 0 || filters.domain.includes(domain)) &&
|
|
(filters.status.length === 0 || filters.status.includes(w.status)) &&
|
|
(w.depends_on.length > 0 || w.blocks.length > 0);
|
|
});
|
|
|
|
if (wsWithDeps.length === 0) {
|
|
display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workstreams.</p>`);
|
|
} else {
|
|
display(html`<div class="dep-grid">${wsWithDeps.map(w => {
|
|
const depRows = w.depends_on.map(d =>
|
|
html`<div class="dep-row dep-on">↳ depends on <strong>${d.workstream_title}</strong>${d.description ? html` <span class="dep-desc">— ${d.description}</span>` : ""}</div>`
|
|
);
|
|
const blockRows = w.blocks.map(d =>
|
|
html`<div class="dep-row dep-block">⊳ blocks <strong>${d.workstream_title}</strong>${d.description ? html` <span class="dep-desc">— ${d.description}</span>` : ""}</div>`
|
|
);
|
|
return html`<div class="dep-card">
|
|
<div class="dep-title">${w.title}</div>
|
|
<div class="dep-status dep-status-${w.status}">${w.status}</div>
|
|
${depRows}${blockRows}
|
|
</div>`;
|
|
})}</div>`);
|
|
}
|
|
```
|
|
|
|
<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-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; }
|
|
.dep-grid { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
.dep-card { border: 1px solid #e0e0e0; border-radius: 6px; padding: 0.75rem 1rem; background: var(--theme-background-alt, #fafafa); }
|
|
.dep-title { font-weight: 600; margin-bottom: 0.25rem; }
|
|
.dep-status { display: inline-block; font-size: 0.7rem; padding: 1px 6px; border-radius: 10px; margin-bottom: 0.5rem; text-transform: uppercase; }
|
|
.dep-status-active { background: #d4edda; color: #155724; }
|
|
.dep-status-blocked { background: #f8d7da; color: #721c24; }
|
|
.dep-status-completed { background: #cce5ff; color: #004085; }
|
|
.dep-row { font-size: 0.85rem; margin: 0.2rem 0 0 0.5rem; color: #444; }
|
|
.dep-on { color: #1a5276; }
|
|
.dep-block { color: #6e2f00; }
|
|
.dep-desc { color: #888; font-size: 0.8rem; }
|
|
</style>
|