// Phase-1 intelligence memory. Uses node:sqlite when available and degrades to no-op. import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; export class IntelligenceStore { constructor(dbPath) { this.dbPath = dbPath; this.db = null; this.available = false; this.reason = null; } async init() { const dir = dirname(this.dbPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); try { const sqlite = await import('node:sqlite'); const DatabaseSync = sqlite.DatabaseSync; this.db = new DatabaseSync(this.dbPath); this.db.exec(` CREATE TABLE IF NOT EXISTS runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, sources_ok INTEGER DEFAULT 0, sources_degraded INTEGER DEFAULT 0, sources_failed INTEGER DEFAULT 0, direction TEXT, summary_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS predictions ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL, title TEXT NOT NULL, type TEXT, confidence TEXT, source TEXT, payload_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, first_seen TEXT NOT NULL, last_seen TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL, count INTEGER DEFAULT 1, UNIQUE(name, kind) ); `); this.available = true; } catch (err) { this.available = false; this.reason = err.message; if (!existsSync(this.dbPath)) { writeFileSync(this.dbPath, ''); } } return this; } recordRun(data, delta) { if (!this.available || !this.db) return; const meta = data.meta || {}; const timestamp = meta.timestamp || new Date().toISOString(); this.db.prepare(`INSERT INTO runs (timestamp, sources_ok, sources_degraded, sources_failed, direction, summary_json) VALUES (?, ?, ?, ?, ?, ?)`).run( timestamp, meta.sourcesOk || 0, meta.sourcesDegraded || 0, meta.sourcesFailed || 0, delta?.summary?.direction || null, JSON.stringify({ meta, delta: delta?.summary || null }), ); for (const idea of data.ideas || []) { this.db.prepare(`INSERT INTO predictions (created_at, title, type, confidence, source, payload_json) VALUES (?, ?, ?, ?, ?, ?)`).run( timestamp, idea.title || 'Untitled idea', idea.type || null, idea.confidence || null, idea.source || data.ideasSource || null, JSON.stringify(idea), ); } this._recordEntities(data, timestamp); } status() { return { available: this.available, path: this.dbPath, reason: this.reason }; } _recordEntities(data, timestamp) { const names = []; for (const item of data.acled?.deadliestEvents || []) { if (item.country) names.push([item.country, 'country']); if (item.location) names.push([item.location, 'location']); } for (const item of data.news || []) { if (item.region) names.push([item.region, 'region']); } for (const [name, kind] of names.slice(0, 200)) { this.db.prepare(`INSERT INTO entities (first_seen, last_seen, name, kind, count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(name, kind) DO UPDATE SET last_seen=excluded.last_seen, count=count+1`).run( timestamp, timestamp, String(name).slice(0, 160), kind, ); } } }