import { getDb } from './db'; import { isGenAvailable, generate } from './search/ollamaGen'; export interface SummaryData { generatedAt: number; overview: string; components: { title: string; status?: string; summary?: string }[]; decisions: { title: string; status?: string }[]; tasks: { todo: string[]; in_progress: string[]; done: string[] }; memories: { byTag: Record }; stats: { total: number; stale: number; orphans: number; edges: number }; } // Ensure cache table exists function ensureCacheTable(): void { const db = getDb(); db.exec(` CREATE TABLE IF NOT EXISTS system_cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ) `); } export function getCachedSummary(): SummaryData | null { ensureCacheTable(); const db = getDb(); const row = db.prepare('SELECT value FROM system_cache WHERE key = ?').get('summary') as { value: string } | undefined; if (!row) return null; try { return JSON.parse(row.value); } catch { return null; } } export function cacheSummary(data: SummaryData): void { ensureCacheTable(); const db = getDb(); db.prepare(` INSERT INTO system_cache (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at `).run('summary', JSON.stringify(data), Date.now()); } export async function generateSummary(): Promise { const db = getDb(); const now = Date.now(); // Gather all active nodes const rows = db.prepare(` SELECT id, kind, title, status, tags, metadata, content FROM nodes WHERE is_stale = 0 ORDER BY kind, title `).all() as any[]; // Stats const totalNodes = rows.length; const staleCount = (db.prepare('SELECT COUNT(*) as c FROM nodes WHERE is_stale = 1').get() as any).c; const edgeCount = (db.prepare('SELECT COUNT(*) as c FROM edges').get() as any).c; const edgeRows = db.prepare('SELECT from_id, to_id FROM edges').all() as any[]; const hasEdge = new Set(); for (const e of edgeRows) { hasEdge.add(e.from_id); hasEdge.add(e.to_id); } const orphanCount = rows.filter(r => !hasEdge.has(r.id)).length; // Categorize nodes const components: SummaryData['components'] = []; const decisions: SummaryData['decisions'] = []; const tasks: SummaryData['tasks'] = { todo: [], in_progress: [], done: [] }; const memoryTags: Record = {}; for (const row of rows) { const meta = JSON.parse(row.metadata || '{}'); if (row.kind === 'component') { components.push({ title: row.title, status: row.status, summary: meta.summary || (row.content?.slice(0, 100) + (row.content?.length > 100 ? '...' : '')), }); } else if (row.kind === 'decision') { decisions.push({ title: row.title, status: row.status }); } else if (row.kind === 'task') { const status = (row.status || 'todo').toLowerCase(); if (status === 'done' || status === 'completed') { tasks.done.push(row.title); } else if (status === 'in_progress' || status === 'in-progress') { tasks.in_progress.push(row.title); } else { tasks.todo.push(row.title); } } else if (row.kind === 'memory') { const tags: string[] = JSON.parse(row.tags || '[]'); // Group by first tag, or 'untagged' const primaryTag = tags[0] || 'untagged'; if (!memoryTags[primaryTag]) memoryTags[primaryTag] = []; memoryTags[primaryTag].push(row.title); } } // Sort tags by count descending, limit to top 10 const sortedTags = Object.entries(memoryTags) .sort((a, b) => b[1].length - a[1].length) .slice(0, 10); const limitedMemoryTags: Record = {}; for (const [tag, titles] of sortedTags) { // Limit to 5 titles per tag limitedMemoryTags[tag] = titles.slice(0, 5); if (titles.length > 5) { limitedMemoryTags[tag].push(`+${titles.length - 5} more`); } } // Generate overview let overview: string; const aiAvailable = await isGenAvailable(); if (aiAvailable) { const prompt = `Write a single sentence (max 30 words) summarizing this knowledge graph for a developer starting a coding session. Stats: ${totalNodes} nodes (${components.length} components, ${decisions.length} decisions, ${tasks.todo.length + tasks.in_progress.length + tasks.done.length} tasks, ${totalNodes - components.length - decisions.length - tasks.todo.length - tasks.in_progress.length - tasks.done.length} memories), ${edgeCount} edges. Components: ${components.map(c => c.title).join(', ')} Active tasks: ${[...tasks.todo, ...tasks.in_progress].join(', ') || 'none'} Output ONLY the summary sentence.`; const aiOverview = await generate(prompt); overview = aiOverview || buildFallbackOverview(totalNodes, components.length, decisions.length, tasks, edgeCount); } else { overview = buildFallbackOverview(totalNodes, components.length, decisions.length, tasks, edgeCount); } const summary: SummaryData = { generatedAt: now, overview, components, decisions, tasks, memories: { byTag: limitedMemoryTags }, stats: { total: totalNodes, stale: staleCount, orphans: orphanCount, edges: edgeCount }, }; cacheSummary(summary); return summary; } function buildFallbackOverview( total: number, components: number, decisions: number, tasks: SummaryData['tasks'], edges: number ): string { const pending = tasks.todo.length + tasks.in_progress.length; const parts = [`${total} nodes`, `${components} components`, `${decisions} decisions`]; if (pending > 0) parts.push(`${pending} open tasks`); parts.push(`${edges} edges`); return parts.join(', ') + '.'; }