#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // SIL Observability — Dashboard och metriker för SIL självt // Erik-krav: "Bygg dashboards och mätvärden för SIL, inte bara för produkten" // ═══════════════════════════════════════════════════════════════════════════ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; import { execSync } from 'child_process'; const LOGS = { shadow: '/home/bernt/.openclaw/workspace/SIL/shadow-log.jsonl', gold: '/home/bernt/.openclaw/workspace/SIL/gold-set.jsonl', stability: '/home/bernt/.openclaw/workspace/SIL/stability-log.jsonl', versions: '/home/bernt/.openclaw/workspace/SIL/graph-version-log.jsonl', webhooks: '/home/bernt/.openclaw/workspace/SIL/webhook-events.jsonl', decisions: '/home/bernt/.openclaw/workspace/SIL/decision-log.jsonl' }; const DASHBOARD_DIR = '/home/bernt/.openclaw/workspace/SIL/dashboard'; class SILObservability { constructor() { if (!existsSync(DASHBOARD_DIR)) { mkdirSync(DASHBOARD_DIR, { recursive: true }); } } loadLog(path) { if (!existsSync(path)) return []; return readFileSync(path, 'utf8') .split('\n') .filter(line => line.trim()) .map(line => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); } // ── Daglig sammanfattning ─────────────────────────────────────────────── generateDailyReport() { const now = new Date(); const today = now.toISOString().split('T')[0]; const shadowEntries = this.loadLog(LOGS.shadow); const goldEntries = this.loadLog(LOGS.gold); const stabilityEntries = this.loadLog(LOGS.stability); const versionEntries = this.loadLog(LOGS.versions); const webhookEntries = this.loadLog(LOGS.webhooks); // Filtrera dagens entries const todayEntries = shadowEntries.filter(e => e.timestamp?.startsWith(today) ); const report = { date: today, generatedAt: now.toISOString(), // Beslut decisions: { total: todayEntries.length, byType: this.countBy(todayEntries, 'status'), laterDisproven: this.countDisproven(goldEntries, today) }, // Osäkerhet uncertainty: { unknown: todayEntries.filter(e => e.uncertaintySummary?.unknown > 0).length, uncertain: todayEntries.filter(e => e.uncertaintySummary?.uncertain > 0).length, certain: todayEntries.filter(e => e.uncertaintySummary?.certain > 0).length }, // Graf-hälsa graph: { versions: versionEntries.length, latestVersion: versionEntries[versionEntries.length - 1]?.id || 'none', nodes: versionEntries[versionEntries.length - 1]?.metadata?.nodeCount || 0, edges: versionEntries[versionEntries.length - 1]?.metadata?.edgeCount || 0 }, // Stabilitet stability: { testsRun: stabilityEntries.length, deterministic: stabilityEntries.filter(e => e.stability?.isDeterministic ).length, avgStability: this.calculateAvgStability(stabilityEntries) }, // Webhooks webhooks: { events: webhookEntries.filter(e => e.timestamp?.startsWith(today) ).length, queued: webhookEntries.filter(e => e.status === 'queued' && e.timestamp?.startsWith(today) ).length, completed: webhookEntries.filter(e => e.status === 'completed' && e.timestamp?.startsWith(today) ).length }, // Kvalitetsmetriker över tid quality: this.calculateQualityTrend(goldEntries) }; this.saveDailyReport(report); this.printDailyReport(report); return report; } // ── Veckotrend ────────────────────────────────────────────────────────── calculateQualityTrend(goldEntries) { if (goldEntries.length === 0) return null; // Gruppera per vecka const byWeek = {}; for (const entry of goldEntries) { const date = new Date(entry.date || entry.timestamp); const week = this.getWeekKey(date); if (!byWeek[week]) { byWeek[week] = { entries: [], tp: 0, fp: 0, fn: 0 }; } byWeek[week].entries.push(entry); const predicted = new Set(entry.predicted || []); const actual = new Set(entry.actual || []); for (const comp of actual) { if (predicted.has(comp)) byWeek[week].tp++; else byWeek[week].fn++; } for (const comp of predicted) { if (!actual.has(comp)) byWeek[week].fp++; } } const trends = []; for (const [week, data] of Object.entries(byWeek).sort()) { const recall = data.tp + data.fn > 0 ? data.tp / (data.tp + data.fn) : 0; const precision = data.tp + data.fp > 0 ? data.tp / (data.tp + data.fp) : 0; trends.push({ week, prs: data.entries.length, recall: Math.round(recall * 100) + '%', precision: Math.round(precision * 100) + '%', fnr: data.tp + data.fn > 0 ? Math.round(data.fn / (data.tp + data.fn) * 100) + '%' : 'N/A' }); } return trends; } // ── Regelanvändning ───────────────────────────────────────────────────── analyzeRuleUsage() { const shadowEntries = this.loadLog(LOGS.shadow); const ruleCounts = {}; const unusedRules = new Set([ 'auth', 'wallet', 'mission', 'kyc', 'user', 'notification', 'api', 'database', 'tests', 'config', 'frontend', 'infrastructure' ]); for (const entry of shadowEntries) { const components = entry.components || entry.changedComponents || []; if (!Array.isArray(components)) continue; for (const comp of components) { const compId = typeof comp === 'string' ? comp : comp.id; if (compId) { ruleCounts[compId] = (ruleCounts[compId] || 0) + 1; unusedRules.delete(compId); } } } return { used: Object.entries(ruleCounts) .sort((a, b) => b[1] - a[1]) .map(([rule, count]) => ({ rule, count })), unused: [...unusedRules], totalRules: 12, utilizationRate: Math.round((12 - unusedRules.size) / 12 * 100) + '%' }; } // ── Confidence-fördelning ─────────────────────────────────────────────── analyzeConfidence() { const shadowEntries = this.loadLog(LOGS.shadow); const distribution = { '0.0-0.5': 0, '0.5-0.7': 0, '0.7-0.9': 0, '0.9-1.0': 0 }; for (const entry of shadowEntries) { const conf = entry.confidence || 0; if (conf >= 0.9) distribution['0.9-1.0']++; else if (conf >= 0.7) distribution['0.7-0.9']++; else if (conf >= 0.5) distribution['0.5-0.7']++; else distribution['0.0-0.5']++; } return distribution; } // ── Helpers ───────────────────────────────────────────────────────────── countBy(entries, key) { const counts = {}; for (const entry of entries) { const val = entry[key] || 'unknown'; counts[val] = (counts[val] || 0) + 1; } return counts; } countDisproven(goldEntries, date) { // Räkna hur många beslut som senare visade sig felaktiga let disproven = 0; for (const entry of goldEntries) { const predictions = entry.predictions || []; const incorrect = predictions.filter(p => p.verified === false).length; disproven += incorrect; } return disproven; } calculateAvgStability(entries) { if (entries.length === 0) return 'N/A'; const scores = entries .map(e => parseFloat(e.stability?.stabilityScore)) .filter(s => !isNaN(s)); if (scores.length === 0) return 'N/A'; return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%'; } getWeekKey(date) { const d = new Date(date); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - d.getDay() + 1); // Måndag return d.toISOString().split('T')[0]; } // ── Output ────────────────────────────────────────────────────────────── saveDailyReport(report) { const filename = `${DASHBOARD_DIR}/daily-${report.date}.json`; writeFileSync(filename, JSON.stringify(report, null, 2)); } printDailyReport(report) { console.log('╔═══════════════════════════════════════════════════════════════╗'); console.log('║ SIL OBSERVABILITY DASHBOARD ║'); console.log(`║ ${report.date} ║`); console.log('╚═══════════════════════════════════════════════════════════════╝'); console.log(); // Beslut console.log('📊 BESLUT IDAG'); console.log(); console.log(` Totala analyser: ${report.decisions.total}`); console.log(` Senare motbevisade: ${report.decisions.laterDisproven}`); console.log(); // Osäkerhet console.log('❓ OSÄKERHET'); console.log(); console.log(` UNKNOWN: ${report.uncertainty.unknown}`); console.log(` UNCERTAIN: ${report.uncertainty.uncertain}`); console.log(` CERTAIN: ${report.uncertainty.certain}`); console.log(); // Graf console.log('🕸️ GRAF-HÄLSA'); console.log(); console.log(` Versioner: ${report.graph.versions}`); console.log(` Senaste: ${report.graph.latestVersion}`); console.log(` Noder: ${report.graph.nodes}`); console.log(` Kanter: ${report.graph.edges}`); console.log(); // Stabilitet console.log('🔄 STABILITET'); console.log(); console.log(` Tester körda: ${report.stability.testsRun}`); console.log(` Deterministiska: ${report.stability.deterministic}`); console.log(` Genomsnitt: ${report.stability.avgStability}`); console.log(); // Webhooks console.log('🌐 WEBHOOKS'); console.log(); console.log(` Events: ${report.webhooks.events}`); console.log(` Köade: ${report.webhooks.queued}`); console.log(` Klara: ${report.webhooks.completed}`); console.log(); // Regelanvändning const rules = this.analyzeRuleUsage(); console.log('📋 REGELANVÄNDNING'); console.log(); console.log(` Utilization: ${rules.utilizationRate}`); console.log(` Oanvända: ${rules.unused.join(', ') || 'none'}`); console.log(); // Confidence const conf = this.analyzeConfidence(); console.log('🎯 CONFIDENCE-FÖRDELNING'); console.log(); for (const [range, count] of Object.entries(conf)) { console.log(` ${range}: ${count}`); } console.log(); // Trend if (report.quality) { console.log('📈 KVALITETSTREND (veckovis)'); console.log(); for (const week of report.quality.slice(-4)) { console.log(` ${week.week}: recall=${week.recall} precision=${week.precision} (${week.prs} PR:er)`); } console.log(); } console.log('═══════════════════════════════════════════════════════════════'); console.log(); } // ── HTML Dashboard ────────────────────────────────────────────────────── generateHTMLDashboard() { const report = this.generateDailyReport(); const html = `
${report.date} — Genererad ${report.generatedAt}
Version: ${report.graph.latestVersion}
Noder: ${report.graph.nodes} | Kanter: ${report.graph.edges}
| Vecka | PR:er | Recall | Precision | FNR |
|---|---|---|---|---|
| ${w.week} | ${w.prs} | ${w.recall} | ${w.precision} | ${w.fnr} |