Spaces:
Paused
Paused
| // Delta Engine v2 β compares two synthesized sweep results and produces structured changes | |
| // Improvements: count metric thresholds, semantic TG dedup, configurable thresholds, null-safety | |
| import { createHash } from 'crypto'; | |
| // βββ Default Thresholds ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Override via config.delta.thresholds in crucix.config.mjs | |
| const DEFAULT_NUMERIC_THRESHOLDS = { | |
| vix: 5, // % change to flag | |
| hy_spread: 5, | |
| '10y2y': 10, | |
| wti: 3, | |
| brent: 3, | |
| natgas: 5, | |
| unemployment: 2, | |
| fed_funds: 1, | |
| '10y_yield': 3, | |
| usd_index: 1, | |
| mortgage: 2, | |
| }; | |
| const DEFAULT_COUNT_THRESHOLDS = { | |
| urgent_posts: 2, // need Β±2 to matter (was 0 β any change) | |
| thermal_total: 500, // Β±500 detections (was 0 β +1 was noise) | |
| air_total: 50, // Β±50 aircraft | |
| who_alerts: 1, // any new WHO alert matters | |
| conflict_events: 5, // Β±5 ACLED events | |
| conflict_fatalities: 10, // Β±10 fatalities | |
| sdr_online: 3, // Β±3 receivers | |
| news_count: 5, // Β±5 news items | |
| sources_ok: 1, // any source going down matters | |
| }; | |
| // βββ Metric Definitions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const NUMERIC_METRICS = [ | |
| { key: 'vix', extract: d => d.fred?.find(f => f.id === 'VIXCLS')?.value, label: 'VIX' }, | |
| { key: 'hy_spread', extract: d => d.fred?.find(f => f.id === 'BAMLH0A0HYM2')?.value, label: 'HY Spread' }, | |
| { key: '10y2y', extract: d => d.fred?.find(f => f.id === 'T10Y2Y')?.value, label: '10Y-2Y Spread' }, | |
| { key: 'wti', extract: d => d.energy?.wti, label: 'WTI Crude' }, | |
| { key: 'brent', extract: d => d.energy?.brent, label: 'Brent Crude' }, | |
| { key: 'natgas', extract: d => d.energy?.natgas, label: 'Natural Gas' }, | |
| { key: 'unemployment', extract: d => d.bls?.find(b => b.id === 'LNS14000000' || b.id === 'UNRATE')?.value, label: 'Unemployment' }, | |
| { key: 'fed_funds', extract: d => d.fred?.find(f => f.id === 'DFF')?.value, label: 'Fed Funds Rate' }, | |
| { key: '10y_yield', extract: d => d.fred?.find(f => f.id === 'DGS10')?.value, label: '10Y Yield' }, | |
| { key: 'usd_index', extract: d => d.fred?.find(f => f.id === 'DTWEXBGS')?.value, label: 'USD Index' }, | |
| { key: 'mortgage', extract: d => d.fred?.find(f => f.id === 'MORTGAGE30US')?.value, label: '30Y Mortgage' }, | |
| ]; | |
| const COUNT_METRICS = [ | |
| { key: 'urgent_posts', extract: d => d.tg?.urgent?.length || 0, label: 'Urgent OSINT Posts' }, | |
| { key: 'thermal_total', extract: d => d.thermal?.reduce((s, t) => s + t.det, 0) || 0, label: 'Thermal Detections' }, | |
| { key: 'air_total', extract: d => d.air?.reduce((s, a) => s + a.total, 0) || 0, label: 'Air Activity' }, | |
| { key: 'who_alerts', extract: d => d.who?.length || 0, label: 'WHO Alerts' }, | |
| { key: 'conflict_events', extract: d => d.acled?.totalEvents || 0, label: 'Conflict Events' }, | |
| { key: 'conflict_fatalities', extract: d => d.acled?.totalFatalities || 0, label: 'Conflict Fatalities' }, | |
| { key: 'sdr_online', extract: d => d.sdr?.online || 0, label: 'SDR Receivers' }, | |
| { key: 'news_count', extract: d => (d.news?.length ?? d.news?.count) || 0, label: 'News Items' }, | |
| { key: 'sources_ok', extract: d => d.meta?.sourcesOk || 0, label: 'Sources OK' }, | |
| ]; | |
| // Risk-sensitive keys: used for determining overall direction | |
| const RISK_KEYS = ['vix', 'hy_spread', 'urgent_posts', 'conflict_events', 'thermal_total']; | |
| // βββ Semantic Hashing for Telegram Posts βββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Produce a normalized hash of a post's content. | |
| * Strips timestamps, normalizes numbers, lowercases β so "BREAKING: 5 missiles at 14:32" | |
| * and "Breaking: 7 missiles at 15:01" produce the same hash (both are "missile strike" signals). | |
| */ | |
| function contentHash(text) { | |
| if (!text) return ''; | |
| const normalized = text | |
| .toLowerCase() | |
| .replace(/\d{1,2}:\d{2}(:\d{2})?/g, '') // strip times | |
| .replace(/\d+/g, 'N') // normalize all numbers | |
| .replace(/[^\w\s]/g, '') // strip punctuation | |
| .replace(/\s+/g, ' ') | |
| .trim() | |
| .substring(0, 100); | |
| return createHash('sha256').update(normalized).digest('hex').substring(0, 12); | |
| } | |
| // βββ Core Delta Computation ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * @param {object} current - current sweep's synthesized data | |
| * @param {object|null} previous - previous sweep's synthesized data (null on first run) | |
| * @param {object} [thresholdOverrides] - optional: { numeric: {...}, count: {...} } | |
| */ | |
| export function computeDelta(current, previous, thresholdOverrides = {}) { | |
| if (!previous) return null; | |
| if (!current) return null; | |
| const numThresholds = { ...DEFAULT_NUMERIC_THRESHOLDS, ...(thresholdOverrides.numeric || {}) }; | |
| const cntThresholds = { ...DEFAULT_COUNT_THRESHOLDS, ...(thresholdOverrides.count || {}) }; | |
| const signals = { new: [], escalated: [], deescalated: [], unchanged: [] }; | |
| let criticalChanges = 0; | |
| // βββ Numeric metrics: track % change βββββββββββββββββββββββββββββββββ | |
| for (const m of NUMERIC_METRICS) { | |
| const curr = m.extract(current); | |
| const prev = m.extract(previous); | |
| if (curr == null || prev == null) continue; | |
| const threshold = numThresholds[m.key] ?? 5; | |
| const pctChange = prev !== 0 ? ((curr - prev) / Math.abs(prev)) * 100 : 0; | |
| if (Math.abs(pctChange) > threshold) { | |
| const entry = { | |
| key: m.key, label: m.label, from: prev, to: curr, | |
| pctChange: parseFloat(pctChange.toFixed(2)), | |
| direction: pctChange > 0 ? 'up' : 'down', | |
| severity: Math.abs(pctChange) > threshold * 3 ? 'critical' : Math.abs(pctChange) > threshold * 2 ? 'high' : 'moderate', | |
| }; | |
| if (pctChange > 0) signals.escalated.push(entry); | |
| else signals.deescalated.push(entry); | |
| if (Math.abs(pctChange) > 10) criticalChanges++; | |
| } else { | |
| signals.unchanged.push(m.key); | |
| } | |
| } | |
| // βββ Count metrics: track absolute change (with minimum thresholds) ββ | |
| for (const m of COUNT_METRICS) { | |
| const curr = m.extract(current); | |
| const prev = m.extract(previous); | |
| const diff = curr - prev; | |
| const threshold = cntThresholds[m.key] ?? 1; | |
| if (Math.abs(diff) >= threshold) { | |
| const pctChange = prev > 0 ? ((diff / prev) * 100) : (diff > 0 ? 100 : 0); | |
| const entry = { | |
| key: m.key, label: m.label, from: prev, to: curr, | |
| change: diff, direction: diff > 0 ? 'up' : 'down', | |
| pctChange: parseFloat(pctChange.toFixed(1)), | |
| severity: Math.abs(diff) >= threshold * 5 ? 'critical' : Math.abs(diff) >= threshold * 2 ? 'high' : 'moderate', | |
| }; | |
| if (diff > 0) signals.escalated.push(entry); | |
| else signals.deescalated.push(entry); | |
| // Count metrics only critical if the change is extreme | |
| if (entry.severity === 'critical') criticalChanges++; | |
| } else { | |
| signals.unchanged.push(m.key); | |
| } | |
| } | |
| // βββ New urgent Telegram posts (semantic dedup) ββββββββββββββββββββββ | |
| const prevHashes = new Set( | |
| (previous.tg?.urgent || []).map(p => contentHash(p.text)) | |
| ); | |
| for (const post of (current.tg?.urgent || [])) { | |
| const hash = contentHash(post.text); | |
| if (hash && !prevHashes.has(hash)) { | |
| signals.new.push({ | |
| key: `tg_urgent:${hash}`, | |
| text: post.text?.substring(0, 120), | |
| item: post, | |
| reason: 'New urgent OSINT post', | |
| }); | |
| criticalChanges++; | |
| } | |
| } | |
| // βββ Nuclear anomaly state change ββββββββββββββββββββββββββββββββββββ | |
| const currAnom = current.nuke?.some(n => n.anom) || false; | |
| const prevAnom = previous.nuke?.some(n => n.anom) || false; | |
| if (currAnom && !prevAnom) { | |
| signals.new.push({ key: 'nuke_anomaly', reason: 'Nuclear anomaly detected', severity: 'critical' }); | |
| criticalChanges += 5; | |
| } else if (!currAnom && prevAnom) { | |
| signals.deescalated.push({ key: 'nuke_anomaly', label: 'Nuclear Anomaly', direction: 'resolved', severity: 'high' }); | |
| } | |
| // βββ Source health degradation βββββββββββββββββββββββββββββββββββββββ | |
| const currSourcesDown = current.health?.filter(s => s.err).length || 0; | |
| const prevSourcesDown = previous.health?.filter(s => s.err).length || 0; | |
| if (currSourcesDown > prevSourcesDown + 2) { | |
| signals.new.push({ | |
| key: 'source_degradation', | |
| reason: `${currSourcesDown - prevSourcesDown} additional sources failing (${currSourcesDown} total down)`, | |
| severity: currSourcesDown > 5 ? 'critical' : 'moderate', | |
| }); | |
| } | |
| // βββ Overall direction βββββββββββββββββββββββββββββββββββββββββββββββ | |
| let direction = 'mixed'; | |
| const riskUp = signals.escalated.filter(s => RISK_KEYS.includes(s.key)).length; | |
| const riskDown = signals.deescalated.filter(s => RISK_KEYS.includes(s.key)).length; | |
| if (riskUp > riskDown + 1) direction = 'risk-off'; | |
| else if (riskDown > riskUp + 1) direction = 'risk-on'; | |
| return { | |
| timestamp: current.meta?.timestamp || new Date().toISOString(), | |
| previous: previous.meta?.timestamp || null, | |
| signals, | |
| summary: { | |
| totalChanges: signals.new.length + signals.escalated.length + signals.deescalated.length, | |
| criticalChanges, | |
| direction, | |
| signalBreakdown: { | |
| new: signals.new.length, | |
| escalated: signals.escalated.length, | |
| deescalated: signals.deescalated.length, | |
| unchanged: signals.unchanged.length, | |
| }, | |
| }, | |
| }; | |
| } | |
| // Export thresholds for external config | |
| export { DEFAULT_NUMERIC_THRESHOLDS, DEFAULT_COUNT_THRESHOLDS }; | |