26-source OSINT intelligence engine with live Jarvis dashboard, auto-refresh via SSE, optional LLM layer (4 providers), delta/memory system, and Telegram breaking news alerts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
5.2 KiB
JavaScript
118 lines
5.2 KiB
JavaScript
// Delta Engine — compares two synthesized sweep results and produces structured changes
|
|
|
|
// Metrics we track for delta computation
|
|
const NUMERIC_METRICS = [
|
|
{ key: 'vix', extract: d => d.fred?.find(f => f.id === 'VIXCLS')?.value, label: 'VIX', threshold: 5 },
|
|
{ key: 'hy_spread', extract: d => d.fred?.find(f => f.id === 'BAMLH0A0HYM2')?.value, label: 'HY Spread', threshold: 5 },
|
|
{ key: '10y2y', extract: d => d.fred?.find(f => f.id === 'T10Y2Y')?.value, label: '10Y-2Y Spread', threshold: 10 },
|
|
{ key: 'wti', extract: d => d.energy?.wti, label: 'WTI Crude', threshold: 3 },
|
|
{ key: 'brent', extract: d => d.energy?.brent, label: 'Brent Crude', threshold: 3 },
|
|
{ key: 'natgas', extract: d => d.energy?.natgas, label: 'Natural Gas', threshold: 5 },
|
|
{ key: 'unemployment', extract: d => d.bls?.find(b => b.id === 'LNS14000000' || b.id === 'UNRATE')?.value, label: 'Unemployment', threshold: 2 },
|
|
{ key: 'fed_funds', extract: d => d.fred?.find(f => f.id === 'DFF')?.value, label: 'Fed Funds Rate', threshold: 1 },
|
|
{ key: '10y_yield', extract: d => d.fred?.find(f => f.id === 'DGS10')?.value, label: '10Y Yield', threshold: 3 },
|
|
{ key: 'usd_index', extract: d => d.fred?.find(f => f.id === 'DTWEXBGS')?.value, label: 'USD Index', threshold: 1 },
|
|
{ key: 'mortgage', extract: d => d.fred?.find(f => f.id === 'MORTGAGE30US')?.value, label: '30Y Mortgage', threshold: 2 },
|
|
];
|
|
|
|
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 || 0, label: 'News Items' },
|
|
{ key: 'sources_ok', extract: d => d.meta?.sourcesOk || 0, label: 'Sources OK' },
|
|
];
|
|
|
|
export function computeDelta(current, previous) {
|
|
if (!previous) return null;
|
|
|
|
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 pctChange = prev !== 0 ? ((curr - prev) / Math.abs(prev)) * 100 : 0;
|
|
|
|
if (Math.abs(pctChange) > m.threshold) {
|
|
const entry = {
|
|
key: m.key, label: m.label, from: prev, to: curr,
|
|
pctChange: parseFloat(pctChange.toFixed(2)),
|
|
direction: pctChange > 0 ? 'up' : 'down',
|
|
};
|
|
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
|
|
for (const m of COUNT_METRICS) {
|
|
const curr = m.extract(current);
|
|
const prev = m.extract(previous);
|
|
const diff = curr - prev;
|
|
|
|
if (Math.abs(diff) > 0) {
|
|
const entry = {
|
|
key: m.key, label: m.label, from: prev, to: curr,
|
|
change: diff, direction: diff > 0 ? 'up' : 'down',
|
|
};
|
|
if (diff > 0) signals.escalated.push(entry);
|
|
else signals.deescalated.push(entry);
|
|
} else {
|
|
signals.unchanged.push(m.key);
|
|
}
|
|
}
|
|
|
|
// New urgent posts (check by text content)
|
|
const prevUrgentTexts = new Set((previous.tg?.urgent || []).map(p => p.text?.substring(0, 60)));
|
|
for (const post of (current.tg?.urgent || [])) {
|
|
const key = post.text?.substring(0, 60);
|
|
if (key && !prevUrgentTexts.has(key)) {
|
|
signals.new.push({ key: 'tg_urgent', item: post, reason: 'New urgent OSINT post' });
|
|
criticalChanges++;
|
|
}
|
|
}
|
|
|
|
// Nuclear anomaly 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' });
|
|
criticalChanges += 5; // Critical
|
|
} else if (!currAnom && prevAnom) {
|
|
signals.deescalated.push({ key: 'nuke_anomaly', label: 'Nuclear Anomaly', direction: 'resolved' });
|
|
}
|
|
|
|
// Determine overall direction
|
|
let direction = 'mixed';
|
|
const riskUp = signals.escalated.filter(s =>
|
|
['vix', 'hy_spread', 'urgent_posts', 'conflict_events', 'thermal_total'].includes(s.key)
|
|
).length;
|
|
const riskDown = signals.deescalated.filter(s =>
|
|
['vix', 'hy_spread', 'urgent_posts', 'conflict_events', 'thermal_total'].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,
|
|
},
|
|
};
|
|
}
|