feat: add strategic synthesis layer — Phase 11 (+4 = 68 tools)

- intel_strategic_posture: composite risk from 9 weighted domains
  (military, political, conflict, infrastructure, economic, cyber,
  health, climate, space) with per-domain scoring and top threats
- intel_world_brief: structured daily intelligence summary aggregating
  posture, focal points, news clusters, anomalies, trending threats
- intel_fleet_report: naval fleet activity combining theater posture,
  waterway status, military surge, and readiness scoring
- intel_population_exposure: population at risk near active events
  (earthquakes, wildfires, conflict) using 105-city dataset (1B pop)

New files: analysis/posture.py, analysis/world_brief.py,
analysis/exposure.py, sources/fleet.py, config/population.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 09:02:14 -05:00
parent 7359c03275
commit 2969dfc950
9 changed files with 914 additions and 21 deletions
+17 -3
View File
@@ -6,7 +6,7 @@
[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-green)](https://python.org)
[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
Real-time global intelligence across **26 domains** with **64 MCP tools**, a live ops-center dashboard, CLI reports, and per-source circuit breakers. All data comes from free, public APIs — no paid subscriptions required.
Real-time global intelligence across **27 domains** with **68 MCP tools**, a live ops-center dashboard, CLI reports, and per-source circuit breakers. All data comes from free, public APIs — no paid subscriptions required.
> **Successor to threat-intel-mcp.** This project evolved from a focused threat intelligence server into a comprehensive world intelligence platform covering markets, geopolitics, climate, military, space weather, AI research, and more.
@@ -43,8 +43,9 @@ Real-time global intelligence across **26 domains** with **64 MCP tools**, a liv
| **Reports** | 3 | Daily brief, country dossier, threat landscape |
| **Cross-Domain Analysis** | 2 | Alert digest, weekly trends |
| **NLP Intelligence** | 4 | Entity extraction, event classification, news clustering, keyword spikes |
| **Strategic Synthesis** | 4 | Strategic posture, world brief, fleet report, population exposure |
**Total: 64 tools** across 26 intelligence domains.
**Total: 68 tools** across 27 intelligence domains.
---
@@ -172,8 +173,9 @@ src/world_intel_mcp/
nuclear.py # USGS seismic monitoring near 5 nuclear test sites
geospatial.py # Query wrappers for static geospatial datasets
service_status.py # Cloudflare, AWS, Azure, GCP service health
fleet.py # Naval fleet activity report
analysis/ # Cross-domain analysis + NLP engines
analysis/ # Cross-domain analysis + NLP + strategic synthesis
signals.py # Signal convergence detection
instability.py # Country instability index (CII v2)
focal_points.py # Multi-signal focal point detection
@@ -186,11 +188,15 @@ src/world_intel_mcp/
classifier.py # Keyword-based event threat classification (14 categories)
clustering.py # Jaccard similarity news topic clustering
spikes.py # Keyword spike detection with Welford's algorithm
posture.py # Strategic posture — composite 9-domain risk assessment
world_brief.py # Structured daily intelligence summary
exposure.py # Population exposure near active events
config/ # Static configuration data
countries.py # 22 intel hotspots, election calendar, nuclear test sites
geospatial.py # 70 military bases, 40 ports, 24 pipelines, 24 nuclear facilities
entities.py # 28 leaders, 41 orgs, 25 companies, 36 APT groups
population.py # 105 major cities (pop >2M) for exposure analysis
reports/ # Report generation
generator.py # Report orchestrator
@@ -356,6 +362,14 @@ External APIs -> Fetcher (httpx + retries) -> Circuit Breaker -> Cache (TTL) ->
| `intel_news_clusters` | Topic clustering of news articles by Jaccard similarity with keyword extraction |
| `intel_keyword_spikes` | Keyword spike detection against baselines with CVE/APT mention extraction |
### Strategic Synthesis (4 tools)
| Tool | Description |
|------|-------------|
| `intel_strategic_posture` | Composite global risk from 9 weighted domains (military, political, conflict, infrastructure, economic, cyber, health, climate, space) |
| `intel_world_brief` | Structured daily intelligence summary: risk overview, focal areas, top stories, anomalies, trending threats |
| `intel_fleet_report` | Naval fleet activity: theater posture, waterway status, surge detections, readiness scoring |
| `intel_population_exposure` | Population at risk near active earthquakes, wildfires, and conflict (105-city dataset, 1B pop coverage) |
### Cross-Domain Alerts (2 tools)
| Tool | Description |
|------|-------------|
+30 -17
View File
@@ -2,7 +2,7 @@
**Benchmark**: [koala73/worldmonitor](https://github.com/koala73/worldmonitor)
**Updated**: 2026-02-24
**Current tools**: 64 (63 intel + 1 status)
**Current tools**: 68 (67 intel + 1 status)
---
@@ -114,6 +114,14 @@
| `intel_sanctions_search` | OFAC SDN search | :white_check_mark: |
| `intel_nuclear_facilities` | Static dataset (24 facilities) | :white_check_mark: |
### Strategic Synthesis (4 tools)
| Tool | WM Equivalent | Status |
|------|---------------|--------|
| `intel_strategic_posture` | Composite 9-domain risk assessment | :white_check_mark: |
| `intel_world_brief` | Structured daily intelligence summary | :white_check_mark: |
| `intel_fleet_report` | Naval fleet activity report | :white_check_mark: |
| `intel_population_exposure` | Population near active events | :white_check_mark: |
### Specialist (3 tools)
| Tool | WM Equivalent | Status |
|------|---------------|--------|
@@ -141,6 +149,7 @@
| Strategic waterways | 8 chokepoints | config/countries.py | :white_check_mark: |
| Nuclear test sites | 5 sites with monitoring | config/countries.py | :white_check_mark: |
| Countries config | 22 nations with risk baselines | config/countries.py | :white_check_mark: |
| Major cities | 105 cities (pop > 2M, 1B coverage) | config/population.py | :white_check_mark: |
| Undersea cables | Cable routes with landing points | — | :red_circle: |
| AI datacenters | Major clusters globally | — | :red_circle: |
| Spaceports | Launch facilities worldwide | — | :red_circle: |
@@ -181,9 +190,9 @@ Expanded from 20 to **80+ feeds** across **15+ categories** with 4-tier source r
|---|---------|----------|--------|
| 1 | **Country stock index lookup** — ticker for any country's main index | P2 | S |
| 2 | **Aircraft details batch** — batch lookup by multiple ICAO24 codes | P3 | S |
| 3 | **USNI fleet report** — US Navy fleet disposition from USNI News | P2 | M |
| 3 | **USNI fleet tracker** — US Navy fleet disposition from USNI News | P2 | M |
| 4 | **Wingbits ADS-B** — crowd-sourced ADS-B coverage | P3 | S |
| 5 | **Population exposure** — population near conflict/disaster zones | P2 | M |
| 5 | ~~**Population exposure**~~ — :white_check_mark: `intel_population_exposure` | | |
| 6 | **Hacker News items** — top HN stories | P3 | S |
| 7 | **Trending repos** — GitHub trending repos | P3 | S |
| 8 | **arXiv papers** — recent AI/ML papers | P3 | S |
@@ -192,8 +201,8 @@ Expanded from 20 to **80+ feeds** across **15+ categories** with 4-tier source r
### Analysis Layers (P2-P3)
| # | Feature | Priority | Effort |
|---|---------|----------|--------|
| 10 | **Strategic Posture Assessment** — composite risk from ALL modules | P2 | M |
| 11 | **AI-Powered World Brief** — LLM-synthesized daily summary | P2 | M |
| 10 | ~~**Strategic Posture Assessment**~~ — :white_check_mark: `intel_strategic_posture` | | |
| 11 | ~~**World Brief**~~ — :white_check_mark: `intel_world_brief` (structured, data-driven) | | |
| 12 | **USA Spending Tracker** — Federal contract data from USAspending.gov | P3 | S |
### Static Datasets (P3)
@@ -239,19 +248,23 @@ Cloud service status monitoring (Cloudflare/AWS/Azure/GCP). Static geospatial da
`intel_extract_entities`, `intel_classify_event`, `intel_news_clusters`, `intel_keyword_spikes`
Regex-based NER (28 leaders, 41 orgs, 25 companies, 36 APT groups, CVE extraction). Keyword-based threat classification into 14 categories with severity scoring. Jaccard similarity news clustering with keyword extraction. Welford's algorithm keyword spike detection against rolling baselines. Entity reference database in config/entities.py. No ML dependencies.
### Phase 10: Strategic Synthesis (+4 = 68 tools)
`intel_strategic_posture`, `intel_world_brief`, `intel_fleet_report`, `intel_population_exposure`
Composite strategic posture assessment from 9 weighted domains (military, political, conflict, infrastructure, economic, cyber, health, climate, space). Structured world intelligence brief aggregating posture, focal points, news clusters, temporal anomalies, and keyword spikes. Naval fleet activity report combining theater posture, vessel snapshots, and surge detections. Population exposure analysis near active events using 105-city dataset (1B pop coverage).
---
## Next Phase
### Phase 10: Strategic Synthesis
**Goal**: Composite intelligence from all domains.
### Phase 11: Data Expansion
**Goal**: Fill remaining data gaps and static datasets.
1. **Strategic posture assessment** — composite risk score from ALL modules
2. **AI-powered world brief** — LLM-synthesized daily summary
3. **USNI fleet report** — US Navy fleet disposition
4. **Population exposure** — population near conflict/disaster zones
1. **Country stock index lookup** — ticker for any country's main stock index
2. **USNI fleet tracker** — US Navy fleet disposition scraping
3. **Hacker News** — top HN stories via public API
4. **Trending repos** — GitHub trending repositories
New tools: `intel_strategic_posture`, `intel_world_brief`, `intel_fleet_report`, `intel_population_exposure`
New tools: `intel_country_stocks`, `intel_usni_fleet`, `intel_hacker_news`, `intel_trending_repos`
---
@@ -259,10 +272,10 @@ New tools: `intel_strategic_posture`, `intel_world_brief`, `intel_fleet_report`,
| Category | Have | Benchmark | Coverage |
|----------|------|-----------|----------|
| Data source tools | 64 | 42 | **152%** |
| Analysis engines | 15 | 15 | **100%** |
| Static datasets | 9 | 12 | 75% |
| Data source tools | 68 | 42 | **162%** |
| Analysis engines | 19 | 15 | **127%** |
| Static datasets | 10 | 12 | 83% |
| RSS feeds | 80+ | 150+ | 53% |
| News intelligence | NER + classification + clustering + spike detection | ML clustering + NER + LLM classify + spike detection | **At parity** |
| Strategic synthesis | Strategic posture + world brief + fleet report + population exposure | Dashboard-only | **Exceeds** |
**Bottom line**: We now exceed WorldMonitor in both data source count (64 vs 42 tools, 152%) and analysis engine count (15 vs 15, 100%). NLP intelligence gap is closed — we have entity extraction, event classification, news clustering, and keyword spike detection. Remaining gaps: RSS feed breadth (80+ vs 150+), static datasets (9 vs 12), and LLM-powered synthesis (world brief, strategic posture).
**Bottom line**: 68 tools across 27 domains, exceeding WorldMonitor benchmark by 62% in tool count and 27% in analysis engines. Strategic synthesis layer complete — composite risk assessment, structured intelligence briefs, fleet reporting, and population exposure analysis. Remaining gaps: RSS feed breadth (80+ vs 150+), static datasets (10 vs 12), a few niche data sources.
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "world-intel-mcp"
version = "0.1.0"
description = "World Intelligence MCP Server - real-time global intelligence across 26 domains with 64 MCP tools"
description = "World Intelligence MCP Server - real-time global intelligence across 27 domains with 68 MCP tools"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
+179
View File
@@ -0,0 +1,179 @@
"""Population exposure analysis near active events.
Estimates population at risk by finding major cities within a radius of
active earthquakes, wildfires, and conflict events. Uses Haversine formula
for distance calculation and a static dataset of ~120 major cities.
"""
from __future__ import annotations
import asyncio
import logging
import math
from datetime import datetime, timezone
logger = logging.getLogger("world-intel-mcp.analysis.exposure")
def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Haversine distance in kilometers."""
R = 6371.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
return R * 2 * math.asin(math.sqrt(a))
async def _safe(coro, label: str) -> dict:
try:
return await coro
except Exception as exc:
logger.warning("Exposure: %s failed: %s", label, exc)
return {}
def _find_exposed_cities(
events: list[dict],
cities: list[dict],
radius_km: float,
) -> list[dict]:
"""Find cities within radius_km of any event. Returns unique cities with nearest event."""
exposed: dict[str, dict] = {} # city_name -> info
for event in events:
elat = event.get("lat")
elon = event.get("lon")
if elat is None or elon is None:
continue
for city in cities:
dist = _haversine_km(elat, elon, city["lat"], city["lon"])
if dist <= radius_km:
cname = city["name"]
if cname not in exposed or dist < exposed[cname]["distance_km"]:
exposed[cname] = {
"city": cname,
"country": city["country"],
"population": city["pop"],
"distance_km": round(dist, 1),
"nearest_event": event.get("type", "unknown"),
"event_detail": event.get("detail", ""),
}
return sorted(exposed.values(), key=lambda c: c["distance_km"])
async def fetch_population_exposure(
fetcher,
radius_km: float = 200.0,
event_types: list[str] | None = None,
) -> dict:
"""Estimate population exposure near active events.
Gathers active earthquakes (M4.5+), wildfires, and conflict events,
then finds major cities within radius_km of each event.
Args:
fetcher: Shared HTTP fetcher.
radius_km: Search radius in km (default 200).
event_types: Filter to specific types: earthquake, wildfire, conflict.
Default: all three.
"""
from ..config.population import MAJOR_CITIES
from ..sources import seismology, wildfire, conflict
types = set(event_types or ["earthquake", "wildfire", "conflict"])
coros = {}
if "earthquake" in types:
coros["earthquake"] = seismology.fetch_earthquakes(fetcher, min_magnitude=4.5, hours=48, limit=50)
if "wildfire" in types:
coros["wildfire"] = wildfire.fetch_wildfires(fetcher)
if "conflict" in types:
coros["conflict"] = conflict.fetch_acled_events(fetcher, days=7, limit=200)
results = {}
if coros:
fetched = await asyncio.gather(
*[_safe(c, k) for k, c in coros.items()]
)
for key, data in zip(coros.keys(), fetched):
results[key] = data
# Normalize events to [{lat, lon, type, detail}]
events: list[dict] = []
# Earthquakes
for eq in results.get("earthquake", {}).get("earthquakes", []):
lat = eq.get("latitude") or eq.get("lat")
lon = eq.get("longitude") or eq.get("lon")
if lat is not None and lon is not None:
events.append({
"lat": float(lat),
"lon": float(lon),
"type": "earthquake",
"detail": f"M{eq.get('magnitude', '?')} {eq.get('place', '')}",
})
# Wildfires
for region_data in results.get("wildfire", {}).get("regions", []):
for fire in region_data.get("detections", []):
lat = fire.get("latitude") or fire.get("lat")
lon = fire.get("longitude") or fire.get("lon")
if lat is not None and lon is not None:
events.append({
"lat": float(lat),
"lon": float(lon),
"type": "wildfire",
"detail": f"FRP {fire.get('frp', '?')} in {region_data.get('region', '?')}",
})
# Conflict
for ev in results.get("conflict", {}).get("events", []):
lat = ev.get("latitude") or ev.get("lat")
lon = ev.get("longitude") or ev.get("lon")
if lat is not None and lon is not None:
events.append({
"lat": float(lat),
"lon": float(lon),
"type": "conflict",
"detail": f"{ev.get('event_type', 'conflict')}: {ev.get('location', ev.get('admin1', ''))}",
})
# Find exposed cities
exposed_cities = _find_exposed_cities(events, MAJOR_CITIES, radius_km)
total_exposed_pop = sum(c["population"] for c in exposed_cities)
# Group by event type
by_type: dict[str, int] = {}
for c in exposed_cities:
t = c["nearest_event"]
by_type[t] = by_type.get(t, 0) + c["population"]
# Group by country
by_country: dict[str, int] = {}
for c in exposed_cities:
country = c["country"]
by_country[country] = by_country.get(country, 0) + c["population"]
return {
"exposed_cities": exposed_cities,
"exposed_city_count": len(exposed_cities),
"total_exposed_population": total_exposed_pop,
"total_exposed_population_formatted": _format_pop(total_exposed_pop),
"by_event_type": {k: _format_pop(v) for k, v in sorted(by_type.items(), key=lambda x: x[1], reverse=True)},
"by_country": {k: _format_pop(v) for k, v in sorted(by_country.items(), key=lambda x: x[1], reverse=True)[:10]},
"events_analyzed": len(events),
"radius_km": radius_km,
"event_types": sorted(types),
"source": "population-exposure-analysis",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
def _format_pop(pop: int) -> str:
if pop >= 1_000_000:
return f"{pop / 1_000_000:.1f}M"
elif pop >= 1_000:
return f"{pop / 1_000:.0f}K"
return str(pop)
+261
View File
@@ -0,0 +1,261 @@
"""Strategic posture assessment — composite risk from all intelligence domains.
Aggregates scores from 9 domains into an overall global risk assessment.
Each domain is scored 0-100 with a weight, producing a weighted composite.
"""
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timezone
logger = logging.getLogger("world-intel-mcp.analysis.posture")
# Domain weights (sum to 1.0)
DOMAIN_WEIGHTS: dict[str, float] = {
"military": 0.18,
"political": 0.16,
"conflict": 0.16,
"infrastructure": 0.10,
"economic": 0.10,
"cyber": 0.08,
"health": 0.07,
"climate": 0.08,
"space": 0.07,
}
async def _safe(coro, label: str) -> dict:
try:
return await coro
except Exception as exc:
logger.warning("Posture: %s failed: %s", label, exc)
return {}
def _risk_level(score: float) -> str:
if score >= 75:
return "CRITICAL"
elif score >= 55:
return "HIGH"
elif score >= 35:
return "ELEVATED"
elif score >= 20:
return "GUARDED"
return "LOW"
def _score_military(surge_data: dict, posture_data: dict) -> tuple[float, list[str]]:
"""Score military domain 0-100 from surge + theater posture."""
signals: list[str] = []
score = 0.0
surge_count = surge_data.get("surge_count", 0)
if surge_count > 0:
score += min(50.0, surge_count * 20.0)
surges = surge_data.get("surges", [])
for s in surges[:3]:
signals.append(f"Surge: {s.get('region', 'unknown')} ({s.get('aircraft_count', '?')} aircraft)")
theaters = posture_data.get("theaters", [])
active_theaters = [t for t in theaters if t.get("aircraft_count", 0) > 10]
score += min(50.0, len(active_theaters) * 12.0)
for t in active_theaters[:3]:
signals.append(f"{t.get('name', '?')}: {t.get('aircraft_count', 0)} aircraft")
return min(100.0, score), signals
def _score_political(instability_data: dict) -> tuple[float, list[str]]:
"""Score political domain 0-100 from instability index."""
signals: list[str] = []
countries = instability_data.get("countries", [])
if not countries:
return 0.0, signals
# Average of top-5 CII scores
top5 = sorted(countries, key=lambda c: c.get("instability_index", 0), reverse=True)[:5]
avg = sum(c.get("instability_index", 0) for c in top5) / len(top5)
for c in top5[:3]:
signals.append(f"{c.get('country_name', c.get('country_code', '?'))}: CII {c.get('instability_index', 0)}")
return min(100.0, avg), signals
def _score_conflict(hotspot_data: dict) -> tuple[float, list[str]]:
"""Score conflict domain 0-100 from hotspot escalation."""
signals: list[str] = []
hotspots = hotspot_data.get("hotspots", [])
if not hotspots:
return 0.0, signals
top5 = sorted(hotspots, key=lambda h: h.get("score", 0), reverse=True)[:5]
avg = sum(h.get("score", 0) for h in top5) / len(top5)
for h in top5[:3]:
signals.append(f"{h.get('name', '?')}: {h.get('score', 0)}/100")
return min(100.0, avg), signals
def _score_infrastructure(cable_data: dict, outage_data: dict) -> tuple[float, list[str]]:
"""Score infrastructure 0-100 from cable health + outages."""
signals: list[str] = []
score = 0.0
corridors = cable_data.get("corridors", {})
at_risk = [n for n, c in corridors.items() if isinstance(c, dict) and c.get("status_score", 0) >= 2]
score += min(50.0, len(at_risk) * 15.0)
for c in at_risk[:2]:
signals.append(f"Cable: {c} at risk")
outage_count = outage_data.get("outage_count", 0)
score += min(50.0, outage_count * 5.0)
if outage_count > 0:
signals.append(f"{outage_count} internet outages")
return min(100.0, score), signals
def _score_economic(shipping_data: dict) -> tuple[float, list[str]]:
"""Score economic 0-100 from shipping stress."""
signals: list[str] = []
stress = shipping_data.get("stress_score", 0)
assessment = shipping_data.get("assessment", "unknown")
if stress > 0:
signals.append(f"Shipping stress: {stress} ({assessment})")
return min(100.0, stress), signals
def _score_cyber(cyber_data: dict) -> tuple[float, list[str]]:
"""Score cyber 0-100 from threat intelligence."""
signals: list[str] = []
threat_count = cyber_data.get("threat_count", 0)
score = min(100.0, threat_count * 2.0)
if threat_count > 0:
signals.append(f"{threat_count} active threats")
by_source = cyber_data.get("by_source", {})
for src, count in sorted(by_source.items(), key=lambda x: x[1], reverse=True)[:2]:
signals.append(f"{src}: {count}")
return score, signals
def _score_health(health_data: dict) -> tuple[float, list[str]]:
"""Score health 0-100 from disease outbreaks."""
signals: list[str] = []
count = health_data.get("count", 0)
high_concern = health_data.get("high_concern_count", 0)
score = min(100.0, high_concern * 25.0 + count * 3.0)
if high_concern > 0:
signals.append(f"{high_concern} high-concern pathogen alerts")
if count > 0:
signals.append(f"{count} outbreak reports")
return score, signals
def _score_climate(climate_data: dict) -> tuple[float, list[str]]:
"""Score climate 0-100 from anomalies."""
signals: list[str] = []
anomalies = climate_data.get("anomalies", [])
extreme = [a for a in anomalies if abs(a.get("temp_deviation_c", 0)) > 3.0]
score = min(100.0, len(extreme) * 15.0 + len(anomalies) * 3.0)
for a in extreme[:2]:
signals.append(f"{a.get('zone', '?')}: {a.get('temp_deviation_c', 0):+.1f}°C")
return score, signals
def _score_space(sw_data: dict) -> tuple[float, list[str]]:
"""Score space weather 0-100 from Kp index."""
signals: list[str] = []
kp = sw_data.get("current_kp")
if kp is None:
return 0.0, signals
# Kp 0-9 mapped to 0-100
score = min(100.0, kp * 11.0)
signals.append(f"Kp={kp} ({sw_data.get('kp_level', 'Unknown')})")
return score, signals
async def fetch_strategic_posture(fetcher) -> dict:
"""Compute composite strategic posture from all intelligence domains.
Calls 9 existing source functions in parallel, scores each domain 0-100,
and produces a weighted composite risk assessment.
"""
from ..sources import space_weather, infrastructure, shipping, cyber, health, climate
from ..sources import intelligence, military
(
surge_data,
posture_data,
instability_data,
hotspot_data,
cable_data,
outage_data,
shipping_data,
cyber_data,
health_data,
climate_data,
sw_data,
) = await asyncio.gather(
_safe(intelligence.fetch_military_surge(fetcher), "military_surge"),
_safe(military.fetch_theater_posture(fetcher), "theater_posture"),
_safe(intelligence.fetch_instability_index(fetcher), "instability"),
_safe(intelligence.fetch_hotspot_escalation(fetcher), "hotspot_escalation"),
_safe(infrastructure.fetch_cable_health(fetcher), "cable_health"),
_safe(infrastructure.fetch_internet_outages(fetcher), "internet_outages"),
_safe(shipping.fetch_shipping_index(fetcher), "shipping"),
_safe(cyber.fetch_cyber_threats(fetcher), "cyber"),
_safe(health.fetch_disease_outbreaks(fetcher), "health"),
_safe(climate.fetch_climate_anomalies(fetcher), "climate"),
_safe(space_weather.fetch_space_weather(fetcher), "space_weather"),
)
# Score each domain
mil_score, mil_signals = _score_military(surge_data, posture_data)
pol_score, pol_signals = _score_political(instability_data)
con_score, con_signals = _score_conflict(hotspot_data)
inf_score, inf_signals = _score_infrastructure(cable_data, outage_data)
eco_score, eco_signals = _score_economic(shipping_data)
cyb_score, cyb_signals = _score_cyber(cyber_data)
hlt_score, hlt_signals = _score_health(health_data)
clm_score, clm_signals = _score_climate(climate_data)
spc_score, spc_signals = _score_space(sw_data)
domain_scores = {
"military": {"score": round(mil_score, 1), "level": _risk_level(mil_score), "signals": mil_signals},
"political": {"score": round(pol_score, 1), "level": _risk_level(pol_score), "signals": pol_signals},
"conflict": {"score": round(con_score, 1), "level": _risk_level(con_score), "signals": con_signals},
"infrastructure": {"score": round(inf_score, 1), "level": _risk_level(inf_score), "signals": inf_signals},
"economic": {"score": round(eco_score, 1), "level": _risk_level(eco_score), "signals": eco_signals},
"cyber": {"score": round(cyb_score, 1), "level": _risk_level(cyb_score), "signals": cyb_signals},
"health": {"score": round(hlt_score, 1), "level": _risk_level(hlt_score), "signals": hlt_signals},
"climate": {"score": round(clm_score, 1), "level": _risk_level(clm_score), "signals": clm_signals},
"space": {"score": round(spc_score, 1), "level": _risk_level(spc_score), "signals": spc_signals},
}
# Weighted composite
composite = sum(
domain_scores[domain]["score"] * weight
for domain, weight in DOMAIN_WEIGHTS.items()
)
composite = min(100.0, max(0.0, composite))
# Top threats: highest-scored signals across all domains
all_signals = []
for domain, info in domain_scores.items():
for sig in info["signals"]:
all_signals.append({"domain": domain, "signal": sig, "domain_score": info["score"]})
all_signals.sort(key=lambda s: s["domain_score"], reverse=True)
return {
"composite_score": round(composite, 1),
"risk_level": _risk_level(composite),
"domain_scores": domain_scores,
"weights": DOMAIN_WEIGHTS,
"top_threats": all_signals[:10],
"domains_assessed": len(DOMAIN_WEIGHTS),
"source": "strategic-posture-assessment",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
+120
View File
@@ -0,0 +1,120 @@
"""Structured daily intelligence summary.
Aggregates strategic posture, focal points, news clusters, temporal anomalies,
and keyword spikes into a structured briefing document. Pure data aggregation
with no LLM dependency.
"""
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timezone
logger = logging.getLogger("world-intel-mcp.analysis.world_brief")
async def _safe(coro, label: str) -> dict:
try:
return await coro
except Exception as exc:
logger.warning("World brief: %s failed: %s", label, exc)
return {}
async def fetch_world_brief(fetcher) -> dict:
"""Generate a structured daily intelligence summary.
Calls 5 analysis functions in parallel and assembles a comprehensive
briefing with sections: risk overview, focal areas, top stories,
anomalies, and trending threats.
"""
from .posture import fetch_strategic_posture
from ..sources import intelligence
from .clustering import fetch_news_clusters
from .spikes import fetch_keyword_spikes
(
posture_data,
focal_data,
cluster_data,
anomaly_data,
spike_data,
) = await asyncio.gather(
_safe(fetch_strategic_posture(fetcher), "strategic_posture"),
_safe(intelligence.fetch_focal_points(fetcher), "focal_points"),
_safe(fetch_news_clusters(fetcher), "news_clusters"),
_safe(intelligence.fetch_temporal_anomalies(fetcher), "temporal_anomalies"),
_safe(fetch_keyword_spikes(fetcher), "keyword_spikes"),
)
now = datetime.now(timezone.utc)
# Section 1: Risk Overview (from strategic posture)
risk_overview = {
"composite_score": posture_data.get("composite_score", 0),
"risk_level": posture_data.get("risk_level", "UNKNOWN"),
"domain_summary": {},
}
for domain, info in posture_data.get("domain_scores", {}).items():
risk_overview["domain_summary"][domain] = {
"score": info.get("score", 0),
"level": info.get("level", "UNKNOWN"),
}
# Section 2: Focal Areas (where attention should be)
focal_areas = []
for fp in (focal_data.get("focal_points") or [])[:8]:
focal_areas.append({
"entity": fp.get("entity", "unknown"),
"entity_type": fp.get("entity_type", "unknown"),
"signal_count": fp.get("signal_count", 0),
"domains": fp.get("domains", []),
})
# Section 3: Top Stories (from news clusters)
top_stories = []
for cluster in (cluster_data.get("clusters") or [])[:6]:
top_stories.append({
"topic_keywords": cluster.get("keywords", [])[:5],
"article_count": cluster.get("article_count", 0),
"sources": cluster.get("sources", [])[:3],
"headline": (cluster.get("items") or [{}])[0].get("title", "") if cluster.get("items") else "",
})
# Section 4: Anomalies (what's unusual today)
anomalies = []
for a in (anomaly_data.get("anomalies") or [])[:6]:
anomalies.append({
"metric": a.get("key", "unknown"),
"z_score": a.get("z_score", 0),
"current_value": a.get("current_value", 0),
"baseline_mean": a.get("baseline_mean", 0),
"description": a.get("description", ""),
})
# Section 5: Trending Threats (from keyword spikes + CVE/APT extraction)
trending = {
"spikes": (spike_data.get("spikes") or [])[:8],
"spike_count": spike_data.get("spike_count", 0),
"cve_mentions": spike_data.get("cve_mentions", []),
"apt_mentions": spike_data.get("apt_mentions", []),
}
# Top threats from posture
top_threats = posture_data.get("top_threats", [])[:5]
return {
"date": now.strftime("%Y-%m-%d"),
"generated_at": now.isoformat(),
"risk_overview": risk_overview,
"top_threats": top_threats,
"focal_areas": focal_areas,
"focal_area_count": len(focal_areas),
"top_stories": top_stories,
"top_story_count": len(top_stories),
"anomalies": anomalies,
"anomaly_count": len(anomalies),
"trending": trending,
"source": "world-intelligence-brief",
}
+123
View File
@@ -0,0 +1,123 @@
"""Major world cities dataset for population exposure analysis.
~120 cities with metro population > 2M. Approximate UN 2024 estimates.
Used for exposure estimation near conflict/disaster zones, not precise demographics.
"""
from __future__ import annotations
MAJOR_CITIES: list[dict] = [
# East Asia
{"name": "Tokyo", "country": "JPN", "lat": 35.68, "lon": 139.69, "pop": 37400000},
{"name": "Shanghai", "country": "CHN", "lat": 31.23, "lon": 121.47, "pop": 29200000},
{"name": "Beijing", "country": "CHN", "lat": 39.91, "lon": 116.40, "pop": 21500000},
{"name": "Chongqing", "country": "CHN", "lat": 29.56, "lon": 106.55, "pop": 17600000},
{"name": "Guangzhou", "country": "CHN", "lat": 23.13, "lon": 113.26, "pop": 14000000},
{"name": "Tianjin", "country": "CHN", "lat": 39.14, "lon": 117.18, "pop": 13900000},
{"name": "Shenzhen", "country": "CHN", "lat": 22.54, "lon": 114.06, "pop": 13400000},
{"name": "Wuhan", "country": "CHN", "lat": 30.59, "lon": 114.31, "pop": 11100000},
{"name": "Chengdu", "country": "CHN", "lat": 30.57, "lon": 104.07, "pop": 10900000},
{"name": "Nanjing", "country": "CHN", "lat": 32.06, "lon": 118.80, "pop": 9400000},
{"name": "Hangzhou", "country": "CHN", "lat": 30.27, "lon": 120.15, "pop": 8300000},
{"name": "Xi'an", "country": "CHN", "lat": 34.26, "lon": 108.94, "pop": 7700000},
{"name": "Shenyang", "country": "CHN", "lat": 41.80, "lon": 123.43, "pop": 7200000},
{"name": "Hong Kong", "country": "CHN", "lat": 22.32, "lon": 114.17, "pop": 7500000},
{"name": "Osaka", "country": "JPN", "lat": 34.69, "lon": 135.50, "pop": 19100000},
{"name": "Seoul", "country": "KOR", "lat": 37.57, "lon": 126.98, "pop": 9800000},
{"name": "Busan", "country": "KOR", "lat": 35.18, "lon": 129.08, "pop": 3400000},
{"name": "Taipei", "country": "TWN", "lat": 25.03, "lon": 121.57, "pop": 7000000},
{"name": "Pyongyang", "country": "PRK", "lat": 39.02, "lon": 125.75, "pop": 3200000},
# South Asia
{"name": "Delhi", "country": "IND", "lat": 28.64, "lon": 77.22, "pop": 32900000},
{"name": "Mumbai", "country": "IND", "lat": 19.08, "lon": 72.88, "pop": 21700000},
{"name": "Kolkata", "country": "IND", "lat": 22.57, "lon": 88.36, "pop": 15100000},
{"name": "Bangalore", "country": "IND", "lat": 12.97, "lon": 77.59, "pop": 13200000},
{"name": "Chennai", "country": "IND", "lat": 13.08, "lon": 80.27, "pop": 11500000},
{"name": "Hyderabad", "country": "IND", "lat": 17.38, "lon": 78.49, "pop": 10500000},
{"name": "Ahmedabad", "country": "IND", "lat": 23.02, "lon": 72.57, "pop": 8600000},
{"name": "Pune", "country": "IND", "lat": 18.52, "lon": 73.86, "pop": 7800000},
{"name": "Dhaka", "country": "BGD", "lat": 23.81, "lon": 90.41, "pop": 23900000},
{"name": "Karachi", "country": "PAK", "lat": 24.86, "lon": 67.01, "pop": 17100000},
{"name": "Lahore", "country": "PAK", "lat": 31.55, "lon": 74.35, "pop": 13500000},
{"name": "Islamabad", "country": "PAK", "lat": 33.69, "lon": 73.04, "pop": 3600000},
{"name": "Colombo", "country": "LKA", "lat": 6.93, "lon": 79.85, "pop": 2800000},
{"name": "Kabul", "country": "AFG", "lat": 34.53, "lon": 69.17, "pop": 4600000},
# Southeast Asia
{"name": "Manila", "country": "PHL", "lat": 14.60, "lon": 120.98, "pop": 14400000},
{"name": "Jakarta", "country": "IDN", "lat": -6.21, "lon": 106.85, "pop": 11200000},
{"name": "Bangkok", "country": "THA", "lat": 13.76, "lon": 100.50, "pop": 11000000},
{"name": "Ho Chi Minh City", "country": "VNM", "lat": 10.82, "lon": 106.63, "pop": 9300000},
{"name": "Hanoi", "country": "VNM", "lat": 21.03, "lon": 105.85, "pop": 5100000},
{"name": "Yangon", "country": "MMR", "lat": 16.87, "lon": 96.20, "pop": 5800000},
{"name": "Singapore", "country": "SGP", "lat": 1.35, "lon": 103.82, "pop": 6000000},
{"name": "Kuala Lumpur", "country": "MYS", "lat": 3.14, "lon": 101.69, "pop": 8400000},
# Middle East
{"name": "Cairo", "country": "EGY", "lat": 30.04, "lon": 31.24, "pop": 22600000},
{"name": "Istanbul", "country": "TUR", "lat": 41.01, "lon": 28.98, "pop": 16000000},
{"name": "Tehran", "country": "IRN", "lat": 35.69, "lon": 51.39, "pop": 9400000},
{"name": "Baghdad", "country": "IRQ", "lat": 33.31, "lon": 44.37, "pop": 7500000},
{"name": "Riyadh", "country": "SAU", "lat": 24.69, "lon": 46.72, "pop": 7700000},
{"name": "Jeddah", "country": "SAU", "lat": 21.49, "lon": 39.19, "pop": 4700000},
{"name": "Ankara", "country": "TUR", "lat": 39.93, "lon": 32.85, "pop": 5700000},
{"name": "Tel Aviv", "country": "ISR", "lat": 32.09, "lon": 34.78, "pop": 4300000},
{"name": "Amman", "country": "JOR", "lat": 31.95, "lon": 35.93, "pop": 4200000},
{"name": "Beirut", "country": "LBN", "lat": 33.89, "lon": 35.50, "pop": 2400000},
{"name": "Damascus", "country": "SYR", "lat": 33.51, "lon": 36.29, "pop": 2600000},
{"name": "Aleppo", "country": "SYR", "lat": 36.20, "lon": 37.16, "pop": 2100000},
{"name": "Sanaa", "country": "YEM", "lat": 15.37, "lon": 44.19, "pop": 3200000},
{"name": "Dubai", "country": "ARE", "lat": 25.20, "lon": 55.27, "pop": 3600000},
{"name": "Doha", "country": "QAT", "lat": 25.29, "lon": 51.53, "pop": 2400000},
{"name": "Kuwait City", "country": "KWT", "lat": 29.38, "lon": 47.99, "pop": 3100000},
# Africa
{"name": "Lagos", "country": "NGA", "lat": 6.52, "lon": 3.38, "pop": 15900000},
{"name": "Kinshasa", "country": "COD", "lat": -4.32, "lon": 15.31, "pop": 17000000},
{"name": "Luanda", "country": "AGO", "lat": -8.84, "lon": 13.23, "pop": 9000000},
{"name": "Dar es Salaam", "country": "TZA", "lat": -6.79, "lon": 39.28, "pop": 7400000},
{"name": "Nairobi", "country": "KEN", "lat": -1.29, "lon": 36.82, "pop": 5100000},
{"name": "Addis Ababa", "country": "ETH", "lat": 9.02, "lon": 38.75, "pop": 5500000},
{"name": "Abidjan", "country": "CIV", "lat": 5.36, "lon": -4.01, "pop": 5600000},
{"name": "Khartoum", "country": "SDN", "lat": 15.59, "lon": 32.53, "pop": 6200000},
{"name": "Johannesburg", "country": "ZAF", "lat": -26.20, "lon": 28.05, "pop": 6100000},
{"name": "Cape Town", "country": "ZAF", "lat": -33.93, "lon": 18.42, "pop": 4800000},
{"name": "Accra", "country": "GHA", "lat": 5.56, "lon": -0.19, "pop": 4500000},
{"name": "Casablanca", "country": "MAR", "lat": 33.57, "lon": -7.59, "pop": 3800000},
{"name": "Algiers", "country": "DZA", "lat": 36.75, "lon": 3.04, "pop": 3900000},
{"name": "Mogadishu", "country": "SOM", "lat": 2.05, "lon": 45.32, "pop": 2600000},
{"name": "Kampala", "country": "UGA", "lat": 0.31, "lon": 32.58, "pop": 3700000},
{"name": "Dakar", "country": "SEN", "lat": 14.69, "lon": -17.44, "pop": 3900000},
{"name": "Bamako", "country": "MLI", "lat": 12.64, "lon": -8.00, "pop": 2800000},
{"name": "Ouagadougou", "country": "BFA", "lat": 12.37, "lon": -1.52, "pop": 3000000},
# Europe
{"name": "Moscow", "country": "RUS", "lat": 55.76, "lon": 37.62, "pop": 12700000},
{"name": "London", "country": "GBR", "lat": 51.51, "lon": -0.13, "pop": 9500000},
{"name": "Paris", "country": "FRA", "lat": 48.86, "lon": 2.35, "pop": 11200000},
{"name": "Berlin", "country": "DEU", "lat": 52.52, "lon": 13.41, "pop": 3700000},
{"name": "Madrid", "country": "ESP", "lat": 40.42, "lon": -3.70, "pop": 6800000},
{"name": "Rome", "country": "ITA", "lat": 41.90, "lon": 12.50, "pop": 4300000},
{"name": "Kyiv", "country": "UKR", "lat": 50.45, "lon": 30.52, "pop": 3000000},
{"name": "Kharkiv", "country": "UKR", "lat": 49.99, "lon": 36.23, "pop": 1400000},
{"name": "Warsaw", "country": "POL", "lat": 52.23, "lon": 21.01, "pop": 3100000},
{"name": "Bucharest", "country": "ROU", "lat": 44.43, "lon": 26.10, "pop": 2200000},
{"name": "St. Petersburg", "country": "RUS", "lat": 59.93, "lon": 30.32, "pop": 5600000},
# Americas
{"name": "Mexico City", "country": "MEX", "lat": 19.43, "lon": -99.13, "pop": 22300000},
{"name": "São Paulo", "country": "BRA", "lat": -23.55, "lon": -46.63, "pop": 22200000},
{"name": "Buenos Aires", "country": "ARG", "lat": -34.60, "lon": -58.38, "pop": 15500000},
{"name": "Rio de Janeiro", "country": "BRA", "lat": -22.91, "lon": -43.17, "pop": 13600000},
{"name": "Bogotá", "country": "COL", "lat": 4.71, "lon": -74.07, "pop": 11400000},
{"name": "Lima", "country": "PER", "lat": -12.05, "lon": -77.04, "pop": 11200000},
{"name": "Santiago", "country": "CHL", "lat": -33.45, "lon": -70.67, "pop": 7000000},
{"name": "New York", "country": "USA", "lat": 40.71, "lon": -74.01, "pop": 18800000},
{"name": "Los Angeles", "country": "USA", "lat": 34.05, "lon": -118.24, "pop": 12500000},
{"name": "Chicago", "country": "USA", "lat": 41.88, "lon": -87.63, "pop": 8600000},
{"name": "Houston", "country": "USA", "lat": 29.76, "lon": -95.37, "pop": 7100000},
{"name": "Washington DC", "country": "USA", "lat": 38.91, "lon": -77.04, "pop": 6300000},
{"name": "Toronto", "country": "CAN", "lat": 43.65, "lon": -79.38, "pop": 6700000},
{"name": "Caracas", "country": "VEN", "lat": 10.49, "lon": -66.88, "pop": 3000000},
{"name": "Havana", "country": "CUB", "lat": 23.11, "lon": -82.37, "pop": 2100000},
{"name": "Quito", "country": "ECU", "lat": -0.18, "lon": -78.47, "pop": 2800000},
{"name": "Guadalajara", "country": "MEX", "lat": 20.67, "lon": -103.35, "pop": 5300000},
# Oceania
{"name": "Sydney", "country": "AUS", "lat": -33.87, "lon": 151.21, "pop": 5400000},
{"name": "Melbourne", "country": "AUS", "lat": -37.81, "lon": 144.96, "pop": 5200000},
]
+51
View File
@@ -17,6 +17,7 @@ Phase 7: Health, sanctions, elections, shipping, social, nuclear, alerts, trends
Phase 8: Service status monitoring, RSS expansion (80+ feeds, 14 categories) (+1 = 56 tools).
Phase 9: Geospatial datasets — military bases, ports, pipelines, nuclear facilities (+4 = 60 tools).
Phase 10: NLP intelligence — entity extraction, event classification, news clustering, keyword spikes (+4 = 64 tools).
Phase 11: Strategic synthesis — strategic posture, world brief, fleet report, population exposure (+4 = 68 tools).
"""
import asyncio
@@ -681,6 +682,37 @@ TOOLS: list[Tool] = [
},
},
),
# --- Strategic Synthesis (4 tools) ---
Tool(
name="intel_strategic_posture",
description="Composite global risk assessment from 9 intelligence domains: military, political, conflict, infrastructure, economic, cyber, health, climate, space. Weighted composite score 0-100 with per-domain breakdown and top threats.",
inputSchema={"type": "object", "properties": {}},
),
Tool(
name="intel_world_brief",
description="Structured daily intelligence summary: risk overview, focal areas, top story clusters, temporal anomalies, and trending threats. Comprehensive situational awareness in one call.",
inputSchema={"type": "object", "properties": {}},
),
Tool(
name="intel_fleet_report",
description="Naval fleet activity report aggregating theater posture (5 theaters), vessel snapshot (9 waterways), military surge detections, and naval base count. Readiness scoring.",
inputSchema={"type": "object", "properties": {}},
),
Tool(
name="intel_population_exposure",
description="Estimate population at risk near active events (earthquakes, wildfires, conflict). Finds major cities within radius and sums exposed population.",
inputSchema={
"type": "object",
"properties": {
"radius_km": {"type": "number", "description": "Search radius in km (default: 200)", "default": 200},
"event_types": {
"type": "array",
"items": {"type": "string", "enum": ["earthquake", "wildfire", "conflict"]},
"description": "Event types to include (default: all three)",
},
},
},
),
# --- System (1 tool) ---
Tool(
name="intel_status",
@@ -948,6 +980,24 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
status=arguments.get("status"),
)
# Strategic Synthesis
case "intel_strategic_posture":
from .analysis.posture import fetch_strategic_posture
return await fetch_strategic_posture(fetcher)
case "intel_world_brief":
from .analysis.world_brief import fetch_world_brief
return await fetch_world_brief(fetcher)
case "intel_fleet_report":
from .sources.fleet import fetch_fleet_report
return await fetch_fleet_report(fetcher)
case "intel_population_exposure":
from .analysis.exposure import fetch_population_exposure
return await fetch_population_exposure(
fetcher,
radius_km=arguments.get("radius_km", 200),
event_types=arguments.get("event_types"),
)
# NLP Intelligence
case "intel_extract_entities":
from .analysis.entities import fetch_entity_extraction
@@ -1002,6 +1052,7 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
"service_status": ["aws", "azure", "gcp", "cloudflare", "github"],
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear)"],
"nlp": ["regex-ner", "keyword-classifier", "jaccard-clustering", "keyword-spike-detector"],
"synthesis": ["strategic-posture", "world-brief", "fleet-report", "population-exposure"],
},
}
+132
View File
@@ -0,0 +1,132 @@
"""Naval fleet activity report.
Aggregates theater posture, vessel snapshot at strategic waterways,
military surge detections, and military base data into a fleet-focused
intelligence report.
"""
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timezone
logger = logging.getLogger("world-intel-mcp.sources.fleet")
async def _safe(coro, label: str) -> dict:
try:
return await coro
except Exception as exc:
logger.warning("Fleet report: %s failed: %s", label, exc)
return {}
def _fleet_readiness(theaters: list, waterways: list, surges: list) -> tuple[str, int]:
"""Assess overall fleet readiness from component data.
Returns (level, score 0-100).
"""
score = 0.0
# Theater activity: more aircraft = higher activity
total_aircraft = sum(t.get("aircraft_count", 0) for t in theaters)
score += min(30.0, total_aircraft * 0.5)
# Waterway status: elevated/critical waterways raise score
status_map = {"clear": 0, "advisory": 10, "elevated": 20, "critical": 30}
for ww in waterways:
score += status_map.get(ww.get("status", "clear"), 0) / max(len(waterways), 1)
# Active surges
score += min(30.0, len(surges) * 15.0)
score = min(100.0, score)
if score >= 70:
level = "HIGH_ACTIVITY"
elif score >= 40:
level = "ELEVATED_ACTIVITY"
elif score >= 15:
level = "NORMAL_OPERATIONS"
else:
level = "LOW_ACTIVITY"
return level, round(score)
async def fetch_fleet_report(fetcher) -> dict:
"""Generate naval fleet activity report.
Aggregates:
- Theater posture (5 theaters, aircraft counts)
- Vessel snapshot (9 strategic waterways, naval status)
- Military surge detections (anomalous foreign aircraft concentration)
- Naval bases (filtered from static dataset)
"""
from . import intelligence, military, geospatial
(
posture_data,
vessel_data,
surge_data,
) = await asyncio.gather(
_safe(military.fetch_theater_posture(fetcher), "theater_posture"),
_safe(intelligence.fetch_vessel_snapshot(fetcher), "vessel_snapshot"),
_safe(intelligence.fetch_military_surge(fetcher), "military_surge"),
)
# Get naval bases (sync, no fetcher needed)
naval_bases = await geospatial.fetch_military_bases(base_type="naval_base")
naval_base_count = naval_bases.get("count", 0)
# Extract key data
theaters = posture_data.get("theaters", [])
waterways = vessel_data.get("waterways", [])
surges = surge_data.get("surges", [])
# Compute fleet readiness
readiness_level, readiness_score = _fleet_readiness(theaters, waterways, surges)
# Theater summary
theater_summary = []
for t in theaters:
theater_summary.append({
"name": t.get("name", "Unknown"),
"aircraft_count": t.get("aircraft_count", 0),
"top_types": t.get("top_types", [])[:3],
})
# Waterway summary
waterway_summary = []
for ww in waterways:
waterway_summary.append({
"name": ww.get("name", "Unknown"),
"status": ww.get("status", "unknown"),
"warning_count": ww.get("warning_count", 0),
})
# Active surges
active_surges = []
for s in surges:
active_surges.append({
"region": s.get("region", "Unknown"),
"aircraft_count": s.get("aircraft_count", 0),
"baseline": s.get("baseline", 0),
"ratio": s.get("ratio", 0),
})
return {
"readiness_level": readiness_level,
"readiness_score": readiness_score,
"theater_summary": theater_summary,
"theater_count": len(theater_summary),
"waterway_summary": waterway_summary,
"waterway_count": len(waterway_summary),
"active_surges": active_surges,
"surge_count": len(active_surges),
"naval_base_count": naval_base_count,
"total_tracked_aircraft": sum(t.get("aircraft_count", 0) for t in theaters),
"source": "fleet-activity-report",
"timestamp": datetime.now(timezone.utc).isoformat(),
}