/** * Stack Observer Service * Reads divergences from observer.db (SQLite) * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import sqlite3 from 'sqlite3' import { open } from 'sqlite' import path from 'path' import { fileURLToPath } from 'url' import os from 'os' const __dirname = path.dirname(fileURLToPath(import.meta.url)) interface Evaluation { id: number source: string entity_id: string site_sig: string result_real: string result_nominal: string tokens: number reincidence_count: number judge_method: string confidence: number evaluated_at: number category: string } interface DivergenceStats { total_divergences: number by_category: Record by_judge: Record recent_24h: number high_confidence: number } /** * Open observer.db connection * DB is located at ~/Dados/Dev/stack-observer/observer.db */ async function openObserverDb() { const dbPath = path.join( os.homedir(), 'Dados/Dev/stack-observer/observer.db' ) return open({ filename: dbPath, driver: sqlite3.Database }) } /** * Get all divergences (evaluations) * Filters for result_real != result_nominal */ export async function getDivergences() { const db = await openObserverDb() try { // Get divergences with divergence flag const divergences = await db.all(` SELECT id, source, entity_id, site_sig, result_real, result_nominal, tokens, reincidence_count, judge_method, confidence, evaluated_at, category FROM evaluations WHERE result_real IS NOT NULL AND category IN ('automation-liveness', 'automation-failure') ORDER BY evaluated_at DESC LIMIT 100 `) // Calculate stats const now = Math.floor(Date.now() / 1000) const day_ago = now - 86400 const stats: DivergenceStats = { total_divergences: 0, by_category: {}, by_judge: {}, recent_24h: 0, high_confidence: 0 } if (divergences) { stats.total_divergences = divergences.length for (const div of divergences) { // Count by category stats.by_category[div.category] = (stats.by_category[div.category] ?? 0) + 1 // Count by judge method stats.by_judge[div.judge_method] = (stats.by_judge[div.judge_method] ?? 0) + 1 // Count recent if (div.evaluated_at >= day_ago) { stats.recent_24h++ } // Count high confidence if (div.confidence >= 0.9) { stats.high_confidence++ } } } return { divergences: divergences ?? [], stats, generated_at: new Date().toISOString() } } finally { await db.close() } } /** * Get single divergence by ID */ export async function getDivergenceById(id: string) { const db = await openObserverDb() try { const evaluation = await db.get(` SELECT * FROM evaluations WHERE id = ? `, [id]) return evaluation ?? null } finally { await db.close() } } /** * Get divergence trend (last 7 days) */ export async function getDivergenceTrend() { const db = await openObserverDb() try { interface TrendRow { date: string count: number } const trend = await db.all(` SELECT DATE(evaluated_at, 'unixepoch') as date, COUNT(*) as count FROM evaluations WHERE result_real IS NOT NULL AND category IN ('automation-liveness', 'automation-failure') AND evaluated_at >= (unixepoch() - 604800) GROUP BY date ORDER BY date DESC `) return trend ?? [] } finally { await db.close() } } /** * Get list of automations (hermes_jobs) * F1b: Colector lê de ~/.hermes/data/action-log.db */ export async function getAutomations() { const db = await openObserverDb() try { const automations = await db.all(` SELECT action_id, who, what, where_field, when_ts, result, result_real, why, how, detail, session_id, created_at, ingested_at FROM hermes_jobs ORDER BY created_at DESC LIMIT 100 `) return automations ?? [] } finally { await db.close() } } /** * Get list of gate events * F1a: Colector lê de /tmp/stack-observer/gate-events.queue */ export async function getGateEvents() { const db = await openObserverDb() try { const events = await db.all(` SELECT id, gate_id, event_type, payload, timestamp, ingested_at FROM gate_events ORDER BY timestamp DESC LIMIT 100 `) return events ?? [] } finally { await db.close() } }