generated from coulomb/repo-seed
Add WSJF triage dashboard review page
This commit is contained in:
216
dashboard/src/components/wsjf-triage.js
Normal file
216
dashboard/src/components/wsjf-triage.js
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user