Add the ranked legacy-interface backlog (T01), rename dashboard navigation and user-facing copy to workplan while preserving wire-compat API keys (T02), and activate the retirement workplan with T01/T02 marked done.
216 lines
6.8 KiB
JavaScript
216 lines
6.8 KiB
JavaScript
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 workplans = workplanIndex.workplans ?? workplanIndex.workstreams ?? {};
|
|
for (const [id, item] of Object.entries(workplans)) {
|
|
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);
|
|
});
|
|
}
|