Add WSJF triage dashboard review page

This commit is contained in:
2026-06-03 09:54:24 +02:00
parent 746cd00028
commit 8137c98a1f
9 changed files with 805 additions and 8 deletions

View File

@@ -71,6 +71,7 @@ export default {
{ name: "Interventions", path: "/interventions" },
{ name: "Tasks", path: "/tasks" },
{ name: "UI Feedback", path: "/ui-feedback" },
{ name: "WSJF Triage", path: "/wsjf-triage" },
],
},
// ── Reference (always last) ───────────────────────────────────────────────
@@ -110,6 +111,7 @@ export default {
{ name: "Workstream Health", path: "/docs/workstream-health-index" },
{ name: "Workstream Lifecycle", path: "/docs/workstream-lifecycle" },
{ name: "Workstreams", path: "/docs/workstreams" },
{ name: "WSJF Triage", path: "/docs/wsjf-triage" },
],
},
],

View File

@@ -0,0 +1,216 @@
export const ACTION_META = {
"work-next": {
tone: "good",
label: "work-next",
description: "Best next executable task.",
},
"close-out": {
tone: "good",
label: "close-out",
description: "Finish closure review and mark done when appropriate.",
},
revisit: {
tone: "warn",
label: "revisit",
description: "Re-read and refresh before execution.",
},
split: {
tone: "warn",
label: "split",
description: "Break an oversized workplan into smaller plans.",
},
park: {
tone: "muted",
label: "park",
description: "Move out of active focus.",
},
"needs-human": {
tone: "bad",
label: "needs-human",
description: "Human decision or approval needed.",
},
"needs-cross-agent": {
tone: "bad",
label: "needs-cross-agent",
description: "Another repo or agent is the right owner.",
},
"needs-consistency-sync": {
tone: "bad",
label: "needs-consistency-sync",
description: "File, DB, or index state should be reconciled.",
},
};
const WORKPLAN_ID_RE = /([a-z0-9]+-wp-\d+[a-z]?)/i;
const ADHOC_ID_RE = /(adhoc-\d{4}-\d{2}-\d{2})/i;
export function normalizeCandidate(value) {
return String(value ?? "")
.trim()
.replace(/\.md$/i, "")
.replace(/^workplans\//i, "")
.replace(/^archived\//i, "")
.replace(/_/g, "-")
.replace(/\s+/g, "-")
.toLowerCase();
}
function filenameStem(filename) {
const base = String(filename ?? "").split("/").pop() ?? "";
return base.replace(/\.md$/i, "").replace(/^\d{6}-/, "");
}
export function candidateKeysForWorkplan(item = {}) {
const stem = filenameStem(item.filename ?? item.relative_path);
const keys = new Set();
const normalizedStem = normalizeCandidate(stem);
if (normalizedStem) keys.add(normalizedStem);
const idMatch = stem.match(WORKPLAN_ID_RE) ?? stem.match(ADHOC_ID_RE);
if (idMatch) {
const idKey = normalizeCandidate(idMatch[1]);
keys.add(idKey);
const suffix = normalizeCandidate(stem.slice(idMatch.index + idMatch[1].length).replace(/^-/, ""));
if (suffix) keys.add(suffix);
}
return [...keys].filter(Boolean);
}
export function buildCandidateIndex(workplanIndex = {}) {
const byCandidate = new Map();
const workstreams = workplanIndex.workstreams ?? {};
for (const [id, item] of Object.entries(workstreams)) {
const resolved = {id, ...item};
byCandidate.set(normalizeCandidate(id), resolved);
for (const key of candidateKeysForWorkplan(item)) {
if (!byCandidate.has(key)) byCandidate.set(key, resolved);
}
}
return byCandidate;
}
export function resolveCandidate(candidate, candidateIndex) {
if (!candidateIndex) return null;
const key = normalizeCandidate(candidate);
return candidateIndex.get(key) ?? null;
}
export function actionMeta(action) {
return ACTION_META[normalizeCandidate(action)] ?? {
tone: "muted",
label: String(action ?? "unknown"),
description: "Unrecognized recommendation action.",
};
}
export function normalizeTriageReports(events = []) {
return events
.map((event, index) => {
const detail = event?.detail ?? {};
const report = detail.report ?? {};
const recommendations = Array.isArray(report.recommendations)
? report.recommendations.map((rec, recIndex) => ({
rank: recIndex + 1,
candidate: String(rec?.candidate ?? "").trim(),
action: normalizeCandidate(rec?.action) || "unknown",
confidence: String(rec?.confidence ?? "").trim() || "unknown",
why: String(rec?.why ?? "").trim(),
}))
: [];
const scheduledFor = detail.scheduled_for ?? null;
const runId = detail.activity_core_run_id ?? null;
const dateSource = scheduledFor ?? event?.created_at ?? "";
const date = String(dateSource).slice(0, 10) || null;
return {
id: String(event?.id ?? `${event?.created_at ?? "report"}-${index}`),
created_at: event?.created_at ?? null,
date,
summary: String(report.summary ?? event?.summary ?? "").trim(),
recommendations,
scheduled_for: scheduledFor,
activity_core_run_id: runId,
instruction_id: detail.instruction_id ?? null,
activity_id: detail.activity_id ?? null,
author: event?.author ?? null,
memory_path: detail.memory_path ?? detail.working_memory_path ?? deriveMemoryPath(date, runId),
};
})
.sort((a, b) => String(b.created_at ?? "").localeCompare(String(a.created_at ?? "")));
}
export function deriveMemoryPath(date, runId) {
if (!date || !runId) return null;
return `the-custodian/memory/working/daily-triage-${date}-${String(runId).slice(0, 8)}.md`;
}
export function truncateSummary(summary, maxLength = 120) {
const text = String(summary ?? "").trim();
if (text.length <= maxLength) return text;
return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
}
export function topAction(recommendations = []) {
const counts = new Map();
for (const rec of recommendations) {
const action = normalizeCandidate(rec?.action) || "unknown";
counts.set(action, (counts.get(action) ?? 0) + 1);
}
const [action, count] = [...counts.entries()].sort((a, b) => {
const countCompare = b[1] - a[1];
return countCompare || a[0].localeCompare(b[0]);
})[0] ?? [];
return action ? {action, count} : null;
}
export function formatActionCount(row) {
return row ? `${row.action} x${row.count}` : "-";
}
export function isWithinDays(iso, days, now = new Date()) {
if (!iso) return false;
const ts = new Date(iso).getTime();
if (Number.isNaN(ts)) return false;
return now.getTime() - ts <= days * 24 * 60 * 60 * 1000;
}
export function buildPatternRows(reports = []) {
const rows = new Map();
for (const report of reports) {
for (const rec of report.recommendations ?? []) {
const key = normalizeCandidate(rec.candidate);
if (!key) continue;
const row = rows.get(key) ?? {
candidate: rec.candidate,
candidateKey: key,
count: 0,
actionCounts: new Map(),
};
row.count += 1;
const action = normalizeCandidate(rec.action) || "unknown";
row.actionCounts.set(action, (row.actionCounts.get(action) ?? 0) + 1);
rows.set(key, row);
}
}
return [...rows.values()]
.map(row => {
const top = [...row.actionCounts.entries()].sort((a, b) => {
const countCompare = b[1] - a[1];
return countCompare || a[0].localeCompare(b[0]);
})[0];
return {
candidate: row.candidate,
candidateKey: row.candidateKey,
count: row.count,
action: top?.[0] ?? "unknown",
actionCount: top?.[1] ?? 0,
};
})
.sort((a, b) => {
const countCompare = b.count - a.count;
return countCompare || a.candidateKey.localeCompare(b.candidateKey);
});
}

View File

@@ -172,6 +172,7 @@ withDocHelp(_liveEl, "/docs/live-data");
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/decisions"); }
display(html`<p class="dim" style="margin-top:-0.25rem"><a href="/wsjf-triage">Daily WSJF triage</a> surfaces recurring decision and human-approval recommendations.</p>`);
// ── Inject into TOC sidebar: KPI first (prepend → bottom), live last (→ top) ─
const _toc = document.querySelector("#observablehq-toc");

View File

@@ -10,6 +10,11 @@ belongs to exactly one project domain. The Workstreams page gives you a
filtered, visual overview of active work, derived blocked state, and the
dependency graph between workstreams.
The [Daily WSJF Triage](/wsjf-triage) page is a companion review surface for
activity-core's daily recommendations. It links recommendation candidates back
to workstream detail pages when the candidate can be resolved through the
workplan index.
---
## Workstation Distribution chart

View File

@@ -0,0 +1,57 @@
---
title: WSJF Triage
---
# WSJF Triage
The Daily WSJF Triage page reviews the append-only reports produced by the
Custodian daily triage runner. The producer is tracked by `CUST-WP-0044`; the
dashboard page is only a read surface for `daily_triage` progress events.
The page loads the latest reports from:
```text
GET /progress/?event_type=daily_triage&limit=14
```
Each event carries the report under `detail.report`, with a summary and a list
of recommendations. Candidate values are resolved through
`/workstreams/workplan-index` so file-backed workplans can link to their
workstream detail pages.
## How to read recommendations
Recommendations are advisory, not authoritative. They help decide where to look
first, but they do not change workplan status, task status, ownership, or
priority. Acting on a recommendation still belongs to the operator, the
workplan owner, or a follow-up implementation workplan.
Confidence labels mean:
| Confidence | Meaning |
|---|---|
| high | State Hub summary, workplan file, and recent progress agree |
| medium | Summary data is supported by at least one corroborating source |
| low | The signal is stale, incomplete, or mostly inferred |
## Action vocabulary
| Action | Meaning |
|---|---|
| `work-next` | Best next executable task |
| `close-out` | Finish closure review and mark done when appropriate |
| `revisit` | Re-read and refresh before execution |
| `split` | Break an oversized workplan into smaller plans |
| `park` | Move out of active focus |
| `needs-human` | Human decision or approval needed |
| `needs-cross-agent` | Another repo or agent is the right owner |
| `needs-consistency-sync` | File, DB, or index state should be reconciled |
## Pattern view
The pattern table aggregates recommendations in the loaded 14-day window. It is
useful for spotting recurring human gates, stale revisit signals, or workstreams
that keep surfacing as the next best piece of work.
No write controls live on this page. It is intentionally a review page so the
daily runner remains a focus surface, not an execution loop.

View File

@@ -38,6 +38,7 @@ convention used in the Custodian State Hub.
| [Todo](/docs/todo) | Internal/Ecosystem/Third-party classification, data sources |
| [Workstream Health](/docs/workstream-health-index) | WHI formula, six base metrics, per-domain breakdown |
| [Workstreams](/docs/workstreams) | Workstream statuses, dependency edges, WHI KPI card |
| [WSJF Triage](/docs/wsjf-triage) | Daily triage reports, action vocabulary, advisory review workflow |
---

View File

@@ -0,0 +1,398 @@
---
title: Daily WSJF Triage
---
```js
import {POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
import {
ACTION_META,
actionMeta,
buildCandidateIndex,
buildPatternRows,
isWithinDays,
normalizeTriageReports,
resolveCandidate,
topAction,
truncateSummary,
} from "./components/wsjf-triage.js";
```
```js
const triageState = (async function*() {
let failures = 0;
while (true) {
let events = [], workplanIndex = {workstreams: {}}, ok = false;
try {
const [reportsResp, indexResp] = await Promise.all([
apiFetch("/progress/?event_type=daily_triage&limit=14"),
apiFetch("/workstreams/workplan-index"),
]);
ok = reportsResp.ok && indexResp.ok;
events = reportsResp.ok ? await reportsResp.json() : [];
workplanIndex = indexResp.ok ? await indexResp.json() : {workstreams: {}};
} catch {}
failures = ok ? 0 : failures + 1;
yield {events, workplanIndex, ok, ts: new Date()};
await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
const reports = normalizeTriageReports(triageState.events ?? []);
const candidateIndex = buildCandidateIndex(triageState.workplanIndex ?? {workstreams: {}});
const _ok = triageState.ok ?? false;
const _ts = triageState.ts;
const latestReport = reports[0] ?? null;
```
# Daily WSJF Triage
```js
function fmtDateTime(iso) {
if (!iso) return "-";
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
}
function fmtDate(iso) {
if (!iso) return "-";
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? String(iso).slice(0, 10) : d.toLocaleDateString(undefined, {year: "numeric", month: "short", day: "numeric"});
}
function candidateNode(candidate, index) {
const resolved = resolveCandidate(candidate, index);
return resolved
? html`<a href="/workstreams/${resolved.id}" title=${resolved.filename ?? resolved.id}>${candidate}</a>`
: html`<span>${candidate || "-"}</span>`;
}
function actionBadge(action, extraText = "") {
const meta = actionMeta(action);
return html`<span class="triage-action triage-action-${meta.tone}" title=${meta.description}>${meta.label}${extraText}</span>`;
}
function recommendationTable(report, index) {
const recommendations = report.recommendations ?? [];
if (recommendations.length === 0) {
return html`<p class="triage-muted">No recommendations were recorded for this report.</p>`;
}
return html`<div>
<table class="triage-table triage-recommendations">
<thead><tr><th>#</th><th>Candidate</th><th>Action</th><th>Confidence</th><th>Why</th></tr></thead>
<tbody>${recommendations.map(rec => html`<tr>
<td>${rec.rank}</td>
<td>${candidateNode(rec.candidate, index)}</td>
<td>${actionBadge(rec.action)}</td>
<td><span class="triage-confidence">${rec.confidence}</span></td>
<td>${rec.why || "-"}</td>
</tr>`)}</tbody>
</table>
<div class="triage-legend">
${Object.values(ACTION_META).map(meta => html`<span>${actionBadge(meta.label)} ${meta.description}</span>`)}
</div>
</div>`;
}
function reportMetadata(report) {
return html`<div class="triage-metadata">
<div><span>Scheduled for</span><strong>${fmtDateTime(report.scheduled_for)}</strong></div>
<div><span>Created</span><strong>${fmtDateTime(report.created_at)}</strong></div>
<div><span>Instruction</span><strong>${report.instruction_id ?? "-"}</strong></div>
<div><span>Activity run</span><strong>${report.activity_core_run_id ?? "-"}</strong></div>
<div><span>Activity</span><strong>${report.activity_id ?? "-"}</strong></div>
<div><span>Memory note</span><strong>${report.memory_path ?? "-"}</strong></div>
</div>`;
}
function renderReportDetail(report, index) {
return html`<div>
<div class="triage-section-heading">
<h2>Report Detail</h2>
<span>${fmtDate(report.scheduled_for ?? report.created_at)}</span>
</div>
<section class="triage-detail-block">
<h3>Summary</h3>
<p>${report.summary || "-"}</p>
</section>
<section class="triage-detail-block">
<h3>Recommendations</h3>
${recommendationTable(report, index)}
</section>
<section class="triage-detail-block">
<h3>Run Metadata</h3>
${reportMetadata(report)}
</section>
</div>`;
}
function renderPatterns(reports, index) {
const windowReports = reports.filter(report => isWithinDays(report.created_at, 14));
const rows = buildPatternRows(windowReports);
return html`<section class="triage-section">
<div class="triage-section-heading">
<h2>Patterns</h2>
<span>Last 14 days</span>
</div>
${rows.length === 0
? html`<p class="triage-muted">No repeated recommendations are visible in the loaded 14-day window.</p>`
: html`<table class="triage-table">
<thead><tr><th>Workstream</th><th>Times Recommended</th><th>Most Frequent Action</th></tr></thead>
<tbody>${rows.map(row => html`<tr>
<td>${candidateNode(row.candidate, index)}</td>
<td>${row.count} / ${Math.max(1, windowReports.length)} reports</td>
<td>${actionBadge(row.action, ` x${row.actionCount}`)}</td>
</tr>`)}</tbody>
</table>`}
</section>`;
}
function renderExplorer(reports, index) {
const root = html`<div class="triage-explorer"></div>`;
const detail = html`<section id="triage-report-detail" class="triage-section"></section>`;
const tableBody = html`<tbody></tbody>`;
const rows = [];
let selectedId = reports[0]?.id;
function selectReport(report, {scroll = true} = {}) {
selectedId = report.id;
for (const row of rows) {
row.classList.toggle("is-selected", row.dataset.reportId === selectedId);
row.setAttribute("aria-selected", row.dataset.reportId === selectedId ? "true" : "false");
}
detail.replaceChildren(renderReportDetail(report, index));
if (scroll) detail.scrollIntoView({behavior: "smooth", block: "start"});
}
for (const report of reports) {
const top = topAction(report.recommendations);
const row = html`<tr class="triage-report-row" tabindex="0" role="button" data-report-id=${report.id} aria-selected="false">
<td>${fmtDate(report.scheduled_for ?? report.created_at)}</td>
<td>${truncateSummary(report.summary)}</td>
<td>${report.recommendations.length}</td>
<td>${top ? actionBadge(top.action, ` x${top.count}`) : "-"}</td>
</tr>`;
row.addEventListener("click", () => selectReport(report));
row.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectReport(report);
}
});
rows.push(row);
}
tableBody.append(...rows);
root.append(
html`<section class="triage-section">
<div class="triage-section-heading">
<h2>Recent Reports</h2>
<span>${reports.length} loaded</span>
</div>
<table class="triage-table triage-reports">
<thead><tr><th>Date</th><th>Summary</th><th># Recs</th><th>Top Action</th></tr></thead>
${tableBody}
</table>
</section>`,
detail,
renderPatterns(reports, index),
);
if (reports[0]) selectReport(reports[0], {scroll: false});
return root;
}
```
```js
const _liveEl = html`<div class="live-indicator">
<span class="live-dot" style="background:${_ok ? "var(--theme-foreground-focus)" : "red"}"></span>
${_ok
? `Live - updated ${_ts?.toLocaleTimeString()} - ${reports.length} triage reports`
: html`<span style="color:red">Offline - run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/wsjf-triage"); }
display(html`<p class="triage-subtitle">Daily State Hub triage from activity-core. Recommendations are advisory; the operator and workplan owners decide what to act on.</p>`);
display(html`<div class="triage-latest">
<span>Last updated</span>
<strong>${latestReport ? fmtDateTime(latestReport.created_at) : "No daily_triage events yet"}</strong>
</div>`);
if (reports.length === 0) {
const empty = html`<div class="triage-empty">
<h2>No daily triage reports yet.</h2>
<p>The next run is scheduled for 07:20 Europe/Berlin (activity-core <code>daily-statehub-wsjf-triage</code>).</p>
</div>`;
withDocHelp(empty, "/docs/wsjf-triage");
display(empty);
} else {
display(renderExplorer(reports, candidateIndex));
}
```
<style>
.live-indicator {
color: gray;
font-size: 0.8rem;
margin-bottom: 0.75rem;
padding: 0.55rem 1.8rem 0.55rem 0.7rem;
position: relative;
}
.live-dot {
border-radius: 50%;
display: inline-block;
height: 0.5rem;
margin-right: 0.35rem;
width: 0.5rem;
}
.triage-subtitle {
color: var(--theme-foreground-muted, #666);
margin-top: -0.2rem;
max-width: 820px;
}
.triage-latest {
align-items: baseline;
border: 1px solid var(--theme-foreground-faint, #ddd);
border-radius: 6px;
display: inline-flex;
gap: 0.45rem;
margin: 0.2rem 0 1rem;
padding: 0.35rem 0.55rem;
}
.triage-latest span,
.triage-section-heading span,
.triage-metadata span {
color: var(--theme-foreground-muted, #666);
font-size: 0.72rem;
text-transform: uppercase;
}
.triage-empty {
border: 1px solid var(--theme-foreground-faint, #ddd);
border-radius: 6px;
margin-top: 1rem;
max-width: 760px;
padding: 1rem;
position: relative;
}
.triage-empty h2 {
margin-top: 0;
}
.triage-section {
margin: 1.4rem 0 1.8rem;
}
.triage-section-heading {
align-items: baseline;
display: flex;
gap: 0.65rem;
justify-content: space-between;
margin-bottom: 0.45rem;
}
.triage-section-heading h2 {
margin: 0;
}
.triage-detail-block {
margin: 0.9rem 0 1.2rem;
}
.triage-detail-block h3 {
font-size: 0.95rem;
margin-bottom: 0.35rem;
}
.triage-table {
border-collapse: collapse;
width: 100%;
}
.triage-table th,
.triage-table td {
border-bottom: 1px solid var(--theme-foreground-faint, #ddd);
padding: 0.45rem 0.5rem;
text-align: left;
vertical-align: top;
}
.triage-reports td:nth-child(2),
.triage-recommendations td:nth-child(5) {
max-width: 520px;
}
.triage-report-row {
cursor: pointer;
}
.triage-report-row:hover,
.triage-report-row:focus {
background: var(--theme-background-alt, #f7f7f7);
outline: none;
}
.triage-report-row.is-selected {
background: color-mix(in srgb, var(--theme-foreground-focus, steelblue) 9%, transparent);
}
.triage-action {
border-radius: 4px;
display: inline-block;
font-size: 0.74rem;
font-weight: 700;
line-height: 1.2;
padding: 0.14rem 0.4rem;
white-space: nowrap;
}
.triage-action-good {
background: #dcfce7;
color: #166534;
}
.triage-action-warn {
background: #fef3c7;
color: #92400e;
}
.triage-action-muted {
background: #f3f4f6;
color: #4b5563;
}
.triage-action-bad {
background: #fee2e2;
color: #991b1b;
}
.triage-confidence {
color: var(--theme-foreground-muted, #666);
font-size: 0.82rem;
}
.triage-legend {
color: var(--theme-foreground-muted, #666);
display: flex;
flex-wrap: wrap;
gap: 0.45rem 0.8rem;
margin-top: 0.6rem;
}
.triage-legend span {
font-size: 0.76rem;
}
.triage-metadata {
display: grid;
gap: 0.65rem 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.triage-metadata div {
min-width: 0;
}
.triage-metadata strong {
display: block;
font-size: 0.86rem;
overflow-wrap: anywhere;
}
.triage-muted {
color: var(--theme-foreground-muted, #666);
}
@media (max-width: 760px) {
.triage-section-heading,
.triage-latest {
align-items: flex-start;
flex-direction: column;
}
.triage-metadata {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,117 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildCandidateIndex,
buildPatternRows,
candidateKeysForWorkplan,
formatActionCount,
normalizeTriageReports,
resolveCandidate,
topAction,
truncateSummary,
} from "../src/components/wsjf-triage.js";
test("candidate keys resolve workplan ids and filename slugs", () => {
assert.deepEqual(
candidateKeysForWorkplan({filename: "CUST-WP-0003-whi-kpi-card.md"}).sort(),
["cust-wp-0003", "cust-wp-0003-whi-kpi-card", "whi-kpi-card"],
);
assert.deepEqual(
candidateKeysForWorkplan({filename: "260501-CUST-WP-0028-e2e-sandbox-framework.md"}).sort(),
["cust-wp-0028", "cust-wp-0028-e2e-sandbox-framework", "e2e-sandbox-framework"],
);
});
test("candidate index resolves daily report candidates to workstream ids", () => {
const index = buildCandidateIndex({
workstreams: {
"d9d9a3ec-f736-4041-beac-bb92c7ad314e": {
filename: "CUST-WP-0045-activity-core-daily-triage-runner.md",
},
"9cc32158-2f5c-4ef6-9713-aacce4623d5e": {
filename: "CUST-WP-0003-whi-kpi-card.md",
},
"36162ff0-9b47-47c4-8602-56767f9b7a1c": {
filename: "ADHOC-2026-06-01.md",
},
},
});
assert.equal(resolveCandidate("cust-wp-0045", index).id, "d9d9a3ec-f736-4041-beac-bb92c7ad314e");
assert.equal(resolveCandidate("whi-kpi-card", index).id, "9cc32158-2f5c-4ef6-9713-aacce4623d5e");
assert.equal(resolveCandidate("adhoc-2026-06-01", index).id, "36162ff0-9b47-47c4-8602-56767f9b7a1c");
assert.equal(resolveCandidate("missing-wp-0001", index), null);
});
test("triage events normalize reports and action summaries", () => {
const [report] = normalizeTriageReports([
{
id: "935244fa-b438-488c-a11a-42e1a84e3d59",
summary: "event summary",
created_at: "2026-06-02T12:52:14.460214Z",
author: "activity-core",
detail: {
scheduled_for: "2026-06-02T12:52:01.690214+00:00",
instruction_id: "daily-triage-report",
activity_core_run_id: "f9b97749-c1d0-5746-ab18-89932bef47c1",
report: {
summary: "daily summary",
recommendations: [
{candidate: "cust-wp-0045", action: "work-next", confidence: "high", why: "runner"},
{candidate: "cust-wp-0046", action: "needs-human", confidence: "high", why: "blocked"},
{candidate: "whi-kpi-card", action: "work-next", confidence: "medium", why: "health"},
],
},
},
},
]);
assert.equal(report.summary, "daily summary");
assert.equal(report.date, "2026-06-02");
assert.equal(report.recommendations.length, 3);
assert.equal(report.memory_path, "the-custodian/memory/working/daily-triage-2026-06-02-f9b97749.md");
assert.deepEqual(topAction(report.recommendations), {action: "work-next", count: 2});
assert.equal(formatActionCount(topAction(report.recommendations)), "work-next x2");
});
test("pattern rows aggregate repeated candidates by action", () => {
const reports = normalizeTriageReports([
{
id: "one",
created_at: "2026-06-03T06:00:00Z",
detail: {
report: {
recommendations: [
{candidate: "cust-wp-0046", action: "needs-human"},
{candidate: "cust-wp-0045", action: "work-next"},
],
},
},
},
{
id: "two",
created_at: "2026-06-02T06:00:00Z",
detail: {
report: {
recommendations: [
{candidate: "cust-wp-0046", action: "needs-human"},
{candidate: "cust-wp-0046", action: "revisit"},
],
},
},
},
]);
const [first, second] = buildPatternRows(reports);
assert.equal(first.candidateKey, "cust-wp-0046");
assert.equal(first.count, 3);
assert.equal(first.action, "needs-human");
assert.equal(first.actionCount, 2);
assert.equal(second.candidateKey, "cust-wp-0045");
});
test("summary truncation keeps compact table rows", () => {
assert.equal(truncateSummary("short", 120), "short");
assert.equal(truncateSummary("abcdefghijklmnopqrstuvwxyz", 10), "abcdefg...");
});

View File

@@ -4,11 +4,11 @@ type: workplan
title: "WSJF Triage Review Page (Workstreams section)"
domain: custodian
repo: state-hub
status: proposed
status: finished
owner: custodian
topic_slug: custodian
created: "2026-06-02"
updated: "2026-06-02"
updated: "2026-06-03"
state_hub_workstream_id: "0cca23a9-9640-491a-92db-6414db891019"
---
@@ -182,7 +182,7 @@ dashboard); fall back to plain text if the slug is not indexed.
```task
id: STATE-WP-0053-T01
status: todo
status: done
priority: medium
state_hub_task_id: "6d33aeed-03d2-4a04-95f4-d28fe35e22c7"
```
@@ -198,7 +198,7 @@ state in a dev preview.
```task
id: STATE-WP-0053-T02
status: todo
status: done
priority: medium
depends_on: [STATE-WP-0053-T01]
state_hub_task_id: "b3e569df-861f-4a65-8aa5-b011547e1fdb"
@@ -215,7 +215,7 @@ in the table, summary truncated to ~120 characters.
```task
id: STATE-WP-0053-T03
status: todo
status: done
priority: medium
depends_on: [STATE-WP-0053-T02]
state_hub_task_id: "fcbaca4d-3186-4192-aa79-a2ebb12074ed"
@@ -233,7 +233,7 @@ workstream detail page on the existing dashboard.
```task
id: STATE-WP-0053-T04
status: todo
status: done
priority: low
depends_on: [STATE-WP-0053-T03]
state_hub_task_id: "45441829-6f01-4b53-8ca7-f4d6f644f77c"
@@ -252,7 +252,7 @@ and the legend matches the action vocabulary documented in
```task
id: STATE-WP-0053-T05
status: todo
status: done
priority: medium
depends_on: [STATE-WP-0053-T02]
state_hub_task_id: "8403b3b5-9f37-4ae7-a141-6fc7afbe8053"
@@ -269,7 +269,7 @@ three or more days surfaces near the top of the pattern table.
```task
id: STATE-WP-0053-T06
status: todo
status: done
priority: low
depends_on: [STATE-WP-0053-T05]
state_hub_task_id: "ce0abbbd-9418-4016-9679-3b3abe0efd6b"