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