| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { |
| type EntityIndex, |
| findEntitiesInText, |
| } from './entity-extraction-core.js'; |
| import { |
| GEO_CONVERGENCE_WINDOW_MS, |
| type GeoEventInput, |
| type GeoPlaceDatasets, |
| } from './analysis-geo-convergence'; |
| import type { |
| CountrySignalCluster, |
| FocalClusterInput, |
| GeoSignal, |
| SignalSummary, |
| SignalType, |
| } from './analysis-focal-points'; |
| import type { CableInput, WaterwayInput } from './analysis-infrastructure-cascade'; |
| import type { |
| MilitaryFlightInput, |
| TheaterActivity, |
| TheaterPostureSummary, |
| } from './analysis-military-surge'; |
| import { CONFLICT_ZONES, INTEL_HOTSPOTS, STRATEGIC_WATERWAYS } from './geo-data'; |
| import { |
| asArray, |
| asRecord, |
| finiteNumber, |
| nestedLocation, |
| nonEmptyString, |
| usableCoord, |
| } from './analysis-adapter-guards'; |
|
|
| |
| |
| |
|
|
|
|
|
|
| |
| function arrayField(payload: unknown, field: string): unknown[] { |
| const record = asRecord(payload); |
| return record ? asArray(record[field]) : []; |
| } |
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| export interface GeoAdapterOptions { |
| |
| now?: number; |
| |
| windowMs?: number; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function toGeoEvents( |
| records: unknown[], |
| readCoord: (record: Record<string, unknown>) => { lat: number | null; lon: number | null }, |
| readTime: (record: Record<string, unknown>) => number | null, |
| fallbackTime: number | null, |
| options: GeoAdapterOptions, |
| ): GeoEventInput[] { |
| const now = options.now ?? Date.now(); |
| const windowMs = options.windowMs ?? GEO_CONVERGENCE_WINDOW_MS; |
| const cutoff = now - windowMs; |
| const events: GeoEventInput[] = []; |
|
|
| for (const raw of records) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const { lat, lon } = readCoord(record); |
| if (!usableCoord(lat, lon) || lon === null) continue; |
| |
| |
| |
| const time = readTime(record) ?? fallbackTime ?? now; |
| if (time < cutoff) continue; |
| events.push({ lat, lon, time }); |
| } |
|
|
| return events; |
| } |
|
|
|
|
| |
| export function unrestEventsToGeoEvents(payload: unknown, options: GeoAdapterOptions = {}): GeoEventInput[] { |
| return toGeoEvents( |
| arrayField(payload, 'events'), |
| nestedLocation, |
| (record) => finiteNumber(record.occurredAt), |
| finiteNumber(asRecord(payload)?.fetchedAt), |
| options, |
| ); |
| } |
|
|
| |
| export function militaryFlightsToGeoEvents(payload: unknown, options: GeoAdapterOptions = {}): GeoEventInput[] { |
| return toGeoEvents( |
| arrayField(payload, 'flights'), |
| (record) => ({ lat: finiteNumber(record.lat), lon: finiteNumber(record.lon) }), |
| (record) => finiteNumber(record.lastSeenMs), |
| finiteNumber(asRecord(payload)?.fetchedAt), |
| options, |
| ); |
| } |
|
|
| |
| export function earthquakesToGeoEvents(payload: unknown, options: GeoAdapterOptions = {}): GeoEventInput[] { |
| return toGeoEvents( |
| arrayField(payload, 'earthquakes'), |
| nestedLocation, |
| (record) => finiteNumber(record.occurredAt), |
| finiteNumber(asRecord(payload)?.fetchedAt), |
| options, |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function usniVesselsToGeoEvents(payload: unknown, options: GeoAdapterOptions = {}): GeoEventInput[] { |
| return toGeoEvents( |
| arrayField(payload, 'vessels'), |
| (record) => ({ lat: finiteNumber(record.regionLat), lon: finiteNumber(record.regionLon) }), |
| () => null, |
| finiteNumber(asRecord(payload)?.timestamp), |
| options, |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export const MCP_GEO_PLACES: GeoPlaceDatasets = { |
| conflictZones: CONFLICT_ZONES.map((zone) => ({ name: zone.name, center: zone.center })), |
| waterways: STRATEGIC_WATERWAYS.map((waterway) => ({ |
| name: waterway.name, |
| lat: waterway.lat, |
| lon: waterway.lon, |
| })), |
| hotspots: INTEL_HOTSPOTS.map((hotspot) => ({ |
| name: hotspot.name, |
| lat: hotspot.lat, |
| lon: hotspot.lon, |
| })), |
| }; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function insightsToFocalClusters(payload: unknown): FocalClusterInput[] { |
| const clusters: FocalClusterInput[] = []; |
|
|
| for (const [index, raw] of arrayField(payload, 'topStories').entries()) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const primaryTitle = nonEmptyString(record.primaryTitle); |
| |
| |
| if (!primaryTitle) continue; |
|
|
| const memberTitles = asArray(record.memberTitles) |
| .map((title) => nonEmptyString(title)) |
| .filter(Boolean) |
| .map((title) => ({ title })); |
|
|
| clusters.push({ |
| id: `insights-${index}`, |
| primaryTitle, |
| primaryLink: nonEmptyString(record.primaryLink), |
| allItems: memberTitles.length > 0 ? memberTitles : [{ title: primaryTitle }], |
| }); |
| } |
|
|
| return clusters; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const CROSS_SOURCE_TO_FOCAL_SIGNAL: Record<string, SignalType> = { |
| CROSS_SOURCE_SIGNAL_TYPE_MILITARY_FLIGHT_SURGE: 'military_flight', |
| CROSS_SOURCE_SIGNAL_TYPE_UNREST_SURGE: 'protest', |
| CROSS_SOURCE_SIGNAL_TYPE_INFRASTRUCTURE_OUTAGE: 'internet_outage', |
| CROSS_SOURCE_SIGNAL_TYPE_SHIPPING_DISRUPTION: 'ais_disruption', |
| CROSS_SOURCE_SIGNAL_TYPE_THERMAL_SPIKE: 'satellite_fire', |
| CROSS_SOURCE_SIGNAL_TYPE_RADIATION_ANOMALY: 'radiation_anomaly', |
| CROSS_SOURCE_SIGNAL_TYPE_SANCTIONS_SURGE: 'sanctions_pressure', |
| CROSS_SOURCE_SIGNAL_TYPE_OREF_ALERT_CLUSTER: 'active_strike', |
| }; |
|
|
| const CROSS_SOURCE_SEVERITY: Record<string, GeoSignal['severity']> = { |
| CROSS_SOURCE_SIGNAL_SEVERITY_LOW: 'low', |
| CROSS_SOURCE_SIGNAL_SEVERITY_MEDIUM: 'medium', |
| CROSS_SOURCE_SIGNAL_SEVERITY_HIGH: 'high', |
| |
| CROSS_SOURCE_SIGNAL_SEVERITY_CRITICAL: 'high', |
| }; |
|
|
| export interface CrossSourceSignalMapping { |
| summary: SignalSummary; |
| |
| signalsTotal: number; |
| |
| signalsMapped: number; |
| |
| signalsUnmapped: number; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function crossSourceSignalsToSignalSummary( |
| payload: unknown, |
| index: EntityIndex, |
| ): CrossSourceSignalMapping { |
| const raw = arrayField(payload, 'signals'); |
| const byCountry = new Map<string, CountrySignalCluster>(); |
| let signalsMapped = 0; |
|
|
| for (const item of raw) { |
| const record = asRecord(item); |
| if (!record) continue; |
|
|
| const focalType = CROSS_SOURCE_TO_FOCAL_SIGNAL[nonEmptyString(record.type)]; |
| if (!focalType) continue; |
|
|
| const severity = CROSS_SOURCE_SEVERITY[nonEmptyString(record.severity)] ?? 'low'; |
| const text = `${nonEmptyString(record.summary)} ${nonEmptyString(record.theater)}`.trim(); |
| const countries = [ |
| ...new Set( |
| findEntitiesInText(text, index) |
| .filter((match) => index.byId.get(match.entityId)?.type === 'country') |
| .map((match) => match.entityId), |
| ), |
| ]; |
| if (countries.length === 0) continue; |
|
|
| signalsMapped += 1; |
| for (const country of countries) { |
| let cluster = byCountry.get(country); |
| if (!cluster) { |
| cluster = { |
| country, |
| signals: [], |
| signalTypes: new Set<SignalType>(), |
| totalCount: 0, |
| highSeverityCount: 0, |
| }; |
| byCountry.set(country, cluster); |
| } |
| cluster.signals.push({ type: focalType, severity }); |
| cluster.signalTypes.add(focalType); |
| cluster.totalCount += 1; |
| if (severity === 'high') cluster.highSeverityCount += 1; |
| } |
| } |
|
|
| const topCountries = [...byCountry.values()].sort( |
| (a, b) => b.highSeverityCount - a.highSeverityCount || b.totalCount - a.totalCount, |
| ); |
|
|
| return { |
| summary: { topCountries }, |
| signalsTotal: raw.length, |
| signalsMapped, |
| signalsUnmapped: raw.length - signalsMapped, |
| }; |
| } |
|
|
| |
| |
| |
| |
| export function riskScoresToCiiLookup(payload: unknown): (countryCode: string) => number | null { |
| const scores = new Map<string, number>(); |
|
|
| for (const raw of arrayField(payload, 'ciiScores')) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const code = nonEmptyString(record.region).toUpperCase(); |
| const score = finiteNumber(record.combinedScore); |
| if (code && score !== null) scores.set(code, score); |
| } |
|
|
| return (countryCode: string) => scores.get(String(countryCode ?? '').toUpperCase()) ?? null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function filterFocalPointsByCountry<T extends { entityId: string }>( |
| points: T[], |
| countryCode: string, |
| index: EntityIndex, |
| ): T[] { |
| const code = nonEmptyString(countryCode).toUpperCase(); |
| if (!code) return points; |
|
|
| const relatedToCountry = new Set( |
| (index.byId.get(code)?.related ?? []).map((related) => related.toUpperCase()), |
| ); |
|
|
| return points.filter((point) => { |
| const entityId = point.entityId.toUpperCase(); |
| if (entityId === code) return true; |
| if (relatedToCountry.has(entityId)) return true; |
| const entity = index.byId.get(point.entityId); |
| return Boolean(entity?.related?.some((related) => related.toUpperCase() === code)); |
| }); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function submarineCablesToCableInputs(payload: unknown): CableInput[] { |
| const cables: CableInput[] = []; |
|
|
| for (const raw of arrayField(payload, 'cables')) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const id = nonEmptyString(record.id); |
| const name = nonEmptyString(record.name); |
| if (!id || !name) continue; |
|
|
| const countriesServed: NonNullable<CableInput['countriesServed']> = []; |
| for (const entry of asArray(record.countriesServed)) { |
| const served = asRecord(entry); |
| const country = nonEmptyString(served?.country); |
| if (!country) continue; |
| countriesServed.push({ |
| country, |
| capacityShare: finiteNumber(served?.capacityShare) ?? 0, |
| isRedundant: served?.isRedundant === true, |
| }); |
| } |
|
|
| const landingPoints: NonNullable<CableInput['landingPoints']> = []; |
| for (const entry of asArray(record.landingPoints)) { |
| const point = asRecord(entry); |
| const country = nonEmptyString(point?.country); |
| if (!country) continue; |
| landingPoints.push({ |
| country, |
| countryName: nonEmptyString(point?.countryName) || undefined, |
| city: nonEmptyString(point?.city) || undefined, |
| lat: finiteNumber(point?.lat) ?? undefined, |
| lon: finiteNumber(point?.lon) ?? undefined, |
| }); |
| } |
|
|
| const cable: CableInput = { id, name, countriesServed, landingPoints }; |
| const rfsYear = finiteNumber(record.rfsYear); |
| if (rfsYear !== null) cable.rfsYear = rfsYear; |
| const owners = asArray(record.owners).map((owner) => nonEmptyString(owner)).filter(Boolean); |
| if (owners.length > 0) cable.owners = owners; |
|
|
| cables.push(cable); |
| } |
|
|
| return cables; |
| } |
|
|
| |
| export const MCP_CASCADE_WATERWAYS: WaterwayInput[] = STRATEGIC_WATERWAYS.map((waterway) => ({ |
| id: waterway.id, |
| name: waterway.name, |
| lat: waterway.lat, |
| lon: waterway.lon, |
| description: waterway.description, |
| })); |
|
|
| |
| |
| |
|
|
| |
| export function militaryFlightsToSurgeInputs(payload: unknown): MilitaryFlightInput[] { |
| const flights: MilitaryFlightInput[] = []; |
|
|
| for (const raw of arrayField(payload, 'flights')) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const lat = finiteNumber(record.lat); |
| const lon = finiteNumber(record.lon); |
| if (!usableCoord(lat, lon) || lon === null) continue; |
|
|
| const flight: MilitaryFlightInput = { |
| id: nonEmptyString(record.id) || nonEmptyString(record.hexCode) || `flight-${flights.length}`, |
| callsign: nonEmptyString(record.callsign), |
| |
| |
| |
| aircraftType: nonEmptyString(record.aircraftType) || 'unknown', |
| operator: nonEmptyString(record.operator) || 'unknown', |
| lat, |
| lon, |
| }; |
| const aircraftModel = nonEmptyString(record.aircraftModel); |
| if (aircraftModel) flight.aircraftModel = aircraftModel; |
|
|
| flights.push(flight); |
| } |
|
|
| return flights; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function theaterPostureVesselCounts(payload: unknown): Map<string, number> { |
| const counts = new Map<string, number>(); |
|
|
| for (const raw of arrayField(payload, 'theaters')) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const theaterId = nonEmptyString(record.theater) || nonEmptyString(record.theaterId); |
| const vessels = finiteNumber(record.trackedVessels); |
| if (!theaterId || vessels === null) continue; |
| counts.set(theaterId, vessels); |
| } |
|
|
| return counts; |
| } |
|
|
| |
| export function applyVesselCountsToPostures( |
| postures: TheaterPostureSummary[], |
| counts: ReadonlyMap<string, number>, |
| ): void { |
| for (const posture of postures) { |
| const vessels = counts.get(posture.theaterId); |
| if (vessels === undefined) continue; |
| |
| |
| posture.totalVessels = vessels; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function surgeHistoryToActivityHistory(payload: unknown): Map<string, TheaterActivity[]> { |
| const history = new Map<string, TheaterActivity[]>(); |
|
|
| const runs = arrayField(payload, 'history') |
| .map((raw) => asRecord(raw)) |
| .filter((run): run is Record<string, unknown> => run !== null) |
| .map((run) => ({ run, timestamp: finiteNumber(run.assessedAt) })) |
| .filter((entry): entry is { run: Record<string, unknown>; timestamp: number } => entry.timestamp !== null) |
| |
| |
| .sort((a, b) => a.timestamp - b.timestamp); |
|
|
| for (const { run, timestamp } of runs) { |
| for (const raw of asArray(run.theaters)) { |
| const record = asRecord(raw); |
| if (!record) continue; |
| const theaterId = nonEmptyString(record.theaterId); |
| if (!theaterId) continue; |
|
|
| const entries = history.get(theaterId) ?? []; |
| entries.push({ |
| theaterId, |
| timestamp, |
| transportCount: finiteNumber(record.transport) ?? 0, |
| fighterCount: finiteNumber(record.fighters) ?? 0, |
| reconCount: finiteNumber(record.reconnaissance) ?? 0, |
| totalMilitary: finiteNumber(record.totalFlights) ?? 0, |
| |
| |
| flightIds: [], |
| }); |
| history.set(theaterId, entries); |
| } |
| } |
|
|
| return history; |
| } |
|
|