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

@@ -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);
});
}