diff --git a/docs/api/SupplyChainService.openapi.json b/docs/api/SupplyChainService.openapi.json index 0581c7d01..846ad8443 100644 --- a/docs/api/SupplyChainService.openapi.json +++ b/docs/api/SupplyChainService.openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"BypassCorridorOption":{"description":"BypassCorridorOption is a single enriched bypass corridor for the Route Explorer UI.\n Includes coordinate endpoints so the client can call MapContainer.setBypassRoutes\n directly without any client-side geometry lookup.","properties":{"addedCostMultiplier":{"format":"double","type":"number"},"addedTransitDays":{"format":"int32","type":"integer"},"fromPort":{"$ref":"#/components/schemas/GeoPoint"},"id":{"type":"string"},"name":{"type":"string"},"status":{"description":"Status of a bypass corridor for UI labeling. \"active\" means usable today;\n \"proposed\" means documented but not yet built/operational; \"unavailable\"\n means blockaded or otherwise blocked from use.","enum":["CORRIDOR_STATUS_UNSPECIFIED","CORRIDOR_STATUS_ACTIVE","CORRIDOR_STATUS_PROPOSED","CORRIDOR_STATUS_UNAVAILABLE"],"type":"string"},"toPort":{"$ref":"#/components/schemas/GeoPoint"},"type":{"type":"string"},"warRiskTier":{"type":"string"}},"type":"object"},"BypassOption":{"properties":{"activationThreshold":{"type":"string"},"addedCostMultiplier":{"format":"double","type":"number"},"addedTransitDays":{"format":"int32","type":"integer"},"bypassWarRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"},"capacityConstraintTonnage":{"format":"int64","type":"string"},"id":{"type":"string"},"liveScore":{"format":"double","type":"number"},"name":{"type":"string"},"notes":{"type":"string"},"suitableCargoTypes":{"items":{"type":"string"},"type":"array"},"type":{"type":"string"},"waypointChokepointIds":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ChokepointExposureEntry":{"description":"ChokepointExposureEntry holds per-chokepoint exposure data for a country.","properties":{"chokepointId":{"description":"Canonical chokepoint ID from the chokepoint registry.","type":"string"},"chokepointName":{"description":"Human-readable chokepoint name.","type":"string"},"coastSide":{"description":"Which ocean/basin side the country's ports face (atlantic, pacific, indian, med, multi, landlocked).","type":"string"},"exposureScore":{"description":"Exposure score 0–100; higher = more dependent on this chokepoint.","format":"double","type":"number"},"shockSupported":{"description":"Whether the shock model is supported for this chokepoint + hs2 combination.","type":"boolean"}},"type":"object"},"ChokepointExposureSummary":{"properties":{"chokepointId":{"type":"string"},"chokepointName":{"type":"string"},"exposurePct":{"format":"int32","type":"integer"}},"type":"object"},"ChokepointInfo":{"properties":{"activeWarnings":{"format":"int32","type":"integer"},"affectedRoutes":{"items":{"type":"string"},"type":"array"},"aisDisruptions":{"format":"int32","type":"integer"},"congestionLevel":{"type":"string"},"description":{"type":"string"},"directionalDwt":{"items":{"$ref":"#/components/schemas/DirectionalDwt"},"type":"array"},"directions":{"items":{"type":"string"},"type":"array"},"disruptionScore":{"format":"int32","type":"integer"},"flowEstimate":{"$ref":"#/components/schemas/FlowEstimate"},"id":{"type":"string"},"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"},"name":{"type":"string"},"status":{"type":"string"},"transitSummary":{"$ref":"#/components/schemas/TransitSummary"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"CountryProduct":{"properties":{"description":{"type":"string"},"hs4":{"type":"string"},"topExporters":{"items":{"$ref":"#/components/schemas/ProductExporter"},"type":"array"},"totalValue":{"format":"double","type":"number"},"year":{"format":"int32","type":"integer"}},"type":"object"},"CriticalMineral":{"properties":{"globalProduction":{"format":"double","type":"number"},"hhi":{"format":"double","type":"number"},"mineral":{"type":"string"},"riskRating":{"type":"string"},"topProducers":{"items":{"$ref":"#/components/schemas/MineralProducer"},"type":"array"},"unit":{"type":"string"}},"type":"object"},"DirectionalDwt":{"properties":{"direction":{"type":"string"},"dwtThousandTonnes":{"format":"double","type":"number"},"wowChangePct":{"format":"double","type":"number"}},"type":"object"},"EnergyDisruptionEntry":{"properties":{"assetId":{"description":"Maps to a pipeline or storage-facility id seeded elsewhere.","type":"string"},"assetType":{"description":"One of: \"pipeline\" | \"storage\"","type":"string"},"capacityOfflineBcmYr":{"description":"Headline-offline capacity (contextual — 0 when not applicable).","format":"double","type":"number"},"capacityOfflineMbd":{"format":"double","type":"number"},"causeChain":{"items":{"description":"Contributing causes, primary-first.","type":"string"},"type":"array"},"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"endAt":{"description":"Empty string when event is still ongoing.","type":"string"},"eventType":{"description":"One of: \"sabotage\" | \"sanction\" | \"maintenance\" | \"mechanical\" |\n \"weather\" | \"commercial\" | \"war\" | \"other\"","type":"string"},"id":{"type":"string"},"lastEvidenceUpdate":{"type":"string"},"shortDescription":{"type":"string"},"sources":{"items":{"$ref":"#/components/schemas/EnergyDisruptionSource"},"type":"array"},"startAt":{"type":"string"}},"type":"object"},"EnergyDisruptionSource":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"sourceType":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"FlowEstimate":{"properties":{"baselineMbd":{"format":"double","type":"number"},"currentMbd":{"format":"double","type":"number"},"disrupted":{"type":"boolean"},"flowRatio":{"format":"double","type":"number"},"hazardAlertLevel":{"type":"string"},"hazardAlertName":{"type":"string"},"source":{"type":"string"}},"type":"object"},"FuelShortageEntry":{"properties":{"causeChain":{"items":{"description":"Contributing root causes, ordered primary-first. Subset of:\n \"upstream_refinery\" | \"logistics\" | \"policy\" | \"chokepoint\" |\n \"sanction\" | \"war\" | \"import_cut\"","type":"string"},"type":"array"},"country":{"type":"string"},"evidence":{"$ref":"#/components/schemas/FuelShortageEvidence"},"firstSeen":{"type":"string"},"id":{"type":"string"},"impactTypes":{"items":{"description":"Observable consumer-facing impacts. Subset of:\n \"stations_closed\" | \"rationing\" | \"flights_cancelled\" | \"import_cut\" | \"price_spike\"","type":"string"},"type":"array"},"lastConfirmed":{"type":"string"},"product":{"description":"One of: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\"","type":"string"},"resolvedAt":{"description":"Empty string when not yet resolved.","type":"string"},"severity":{"description":"One of: \"confirmed\" | \"watch\" (classifier output — not a client-side derivation)","type":"string"},"shortDescription":{"type":"string"}},"type":"object"},"FuelShortageEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"evidenceSources":{"items":{"$ref":"#/components/schemas/FuelShortageEvidenceSource"},"type":"array"},"firstRegulatorConfirmation":{"description":"ISO date of the first regulator confirmation, if any. Empty when\n severity is \"watch\" on press-only signal.","type":"string"},"lastEvidenceUpdate":{"type":"string"}},"type":"object"},"FuelShortageEvidenceSource":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"sourceType":{"description":"One of: \"regulator\" | \"operator\" | \"press\" | \"ais-relay\" | \"satellite\"","type":"string"},"title":{"type":"string"},"url":{"type":"string"}},"type":"object"},"GeoPoint":{"description":"GeoPoint is a [longitude, latitude] pair.","properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"GetBypassOptionsRequest":{"properties":{"cargoType":{"description":"container | tanker | bulk | roro (default: \"container\")","type":"string"},"chokepointId":{"type":"string"},"closurePct":{"description":"0-100, percent of capacity blocked (default: 100)","format":"int32","type":"integer"}},"required":["chokepointId"],"type":"object"},"GetBypassOptionsResponse":{"properties":{"cargoType":{"type":"string"},"chokepointId":{"type":"string"},"closurePct":{"format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"options":{"items":{"$ref":"#/components/schemas/BypassOption"},"type":"array"},"primaryChokepointWarRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetChokepointHistoryRequest":{"description":"GetChokepointHistory returns the transit-count history for a single\n chokepoint. Loaded lazily on card expand so the main chokepoint-status\n response can stay compact (no 180-day history per chokepoint).","properties":{"chokepointId":{"type":"string"}},"required":["chokepointId"],"type":"object"},"GetChokepointHistoryResponse":{"properties":{"chokepointId":{"type":"string"},"fetchedAt":{"format":"int64","type":"string"},"history":{"items":{"$ref":"#/components/schemas/TransitDayCount"},"type":"array"}},"type":"object"},"GetChokepointStatusRequest":{"type":"object"},"GetChokepointStatusResponse":{"properties":{"chokepoints":{"items":{"$ref":"#/components/schemas/ChokepointInfo"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetCountryChokepointIndexRequest":{"description":"GetCountryChokepointIndexRequest specifies the country and optional HS2 chapter.","properties":{"hs2":{"description":"HS2 chapter (2-digit string). Defaults to \"27\" (energy/mineral fuels) when absent.","type":"string"},"iso2":{"description":"ISO 3166-1 alpha-2 country code (uppercase).","pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2"],"type":"object"},"GetCountryChokepointIndexResponse":{"description":"GetCountryChokepointIndexResponse returns exposure scores for all relevant chokepoints.","properties":{"exposures":{"items":{"$ref":"#/components/schemas/ChokepointExposureEntry"},"type":"array"},"fetchedAt":{"description":"ISO timestamp of when this data was last seeded.","type":"string"},"hs2":{"description":"HS2 chapter used for the computation.","type":"string"},"iso2":{"description":"ISO 3166-1 alpha-2 country code echoed from the request.","type":"string"},"primaryChokepointId":{"description":"Canonical ID of the chokepoint with the highest exposure score.","type":"string"},"vulnerabilityIndex":{"description":"Composite vulnerability index 0–100 (weighted sum of top-3 exposures).","format":"double","type":"number"}},"type":"object"},"GetCountryCostShockRequest":{"properties":{"chokepointId":{"type":"string"},"hs2":{"description":"HS2 chapter (default: \"27\")","type":"string"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","chokepointId"],"type":"object"},"GetCountryCostShockResponse":{"properties":{"chokepointId":{"type":"string"},"coverageDays":{"description":"Energy stockpile coverage in days (IEA data, HS 27 only; 0 for non-energy sectors or net exporters)","format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"hasEnergyModel":{"description":"Whether supply_deficit_pct and coverage_days are modelled (true) or unavailable (false)","type":"boolean"},"hs2":{"type":"string"},"iso2":{"type":"string"},"supplyDeficitPct":{"description":"Average refined-product supply deficit % under full closure (Gasoline/Diesel/Jet fuel/LPG average; HS 27 only)","format":"double","type":"number"},"unavailableReason":{"description":"Null/unavailable explanation for non-energy sectors","type":"string"},"warRiskPremiumBps":{"description":"War risk insurance premium in basis points for this chokepoint","format":"int32","type":"integer"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetCountryProductsRequest":{"properties":{"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2"],"type":"object"},"GetCountryProductsResponse":{"properties":{"fetchedAt":{"description":"ISO timestamp from the seeded payload (empty when no data is cached).","type":"string"},"iso2":{"type":"string"},"products":{"items":{"$ref":"#/components/schemas/CountryProduct"},"type":"array"}},"type":"object"},"GetCriticalMineralsRequest":{"type":"object"},"GetCriticalMineralsResponse":{"properties":{"fetchedAt":{"type":"string"},"minerals":{"items":{"$ref":"#/components/schemas/CriticalMineral"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetFuelShortageDetailRequest":{"description":"GetFuelShortageDetail returns a single shortage with full evidence\n bundle + citation timeline. Loaded lazily on drawer open.","properties":{"shortageId":{"type":"string"}},"required":["shortageId"],"type":"object"},"GetFuelShortageDetailResponse":{"properties":{"fetchedAt":{"type":"string"},"shortage":{"$ref":"#/components/schemas/FuelShortageEntry"},"unavailable":{"type":"boolean"}},"type":"object"},"GetMultiSectorCostShockRequest":{"properties":{"chokepointId":{"type":"string"},"closureDays":{"description":"Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.","format":"int32","type":"integer"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","chokepointId"],"type":"object"},"GetMultiSectorCostShockResponse":{"properties":{"chokepointId":{"type":"string"},"closureDays":{"description":"Server-clamped closure-window duration in days (1-365).","format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"iso2":{"type":"string"},"sectors":{"items":{"$ref":"#/components/schemas/MultiSectorCostShock"},"type":"array"},"totalAddedCost":{"description":"Sum of total_cost_shock across all sectors.","format":"double","type":"number"},"unavailableReason":{"description":"Populated when no seeded import data is available for the country.","type":"string"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetPipelineDetailRequest":{"description":"GetPipelineDetail returns a single pipeline with its full evidence bundle.\n Evidence surface here is richer than ListPipelinesResponse — the list view\n is designed for the map layer's compact shape; detail is designed for the\n click-through drawer.","properties":{"pipelineId":{"type":"string"}},"required":["pipelineId"],"type":"object"},"GetPipelineDetailResponse":{"properties":{"fetchedAt":{"type":"string"},"pipeline":{"$ref":"#/components/schemas/PipelineEntry"},"revisions":{"items":{"$ref":"#/components/schemas/PipelineRevisionEntry"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"GetRouteExplorerLaneRequest":{"properties":{"cargoType":{"description":"One of: container, tanker, bulk, roro","type":"string"},"fromIso2":{"pattern":"^[A-Z]{2}$","type":"string"},"hs2":{"description":"HS2 chapter code, e.g. \"27\", \"85\"","type":"string"},"toIso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["fromIso2","toIso2","hs2","cargoType"],"type":"object"},"GetRouteExplorerLaneResponse":{"properties":{"bypassOptions":{"items":{"$ref":"#/components/schemas/BypassCorridorOption"},"type":"array"},"cargoType":{"type":"string"},"chokepointExposures":{"items":{"$ref":"#/components/schemas/ChokepointExposureSummary"},"type":"array"},"disruptionScore":{"format":"double","type":"number"},"estFreightUsdPerTeuRange":{"$ref":"#/components/schemas/NumberRange"},"estTransitDaysRange":{"$ref":"#/components/schemas/NumberRange"},"fetchedAt":{"type":"string"},"fromIso2":{"type":"string"},"hs2":{"type":"string"},"noModeledLane":{"description":"True when the wrapper fell back to the origin's first route (no shared route\n between origin and destination clusters). Signals \"no modeled lane\" to the UI.","type":"boolean"},"primaryRouteGeometry":{"items":{"$ref":"#/components/schemas/GeoPoint"},"type":"array"},"primaryRouteId":{"description":"Primary trade route ID from TRADE_ROUTES config. Empty when no modeled lane.","type":"string"},"toIso2":{"type":"string"},"warRiskTier":{"type":"string"}},"type":"object"},"GetRouteImpactRequest":{"properties":{"fromIso2":{"pattern":"^[A-Z]{2}$","type":"string"},"hs2":{"type":"string"},"toIso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["fromIso2","toIso2","hs2"],"type":"object"},"GetRouteImpactResponse":{"properties":{"comtradeSource":{"type":"string"},"dependencyFlags":{"items":{"description":"DependencyFlag classifies how a country+sector dependency can fail.","enum":["DEPENDENCY_FLAG_UNSPECIFIED","DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL","DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL","DEPENDENCY_FLAG_COMPOUND_RISK","DEPENDENCY_FLAG_DIVERSIFIABLE"],"type":"string"},"type":"array"},"fetchedAt":{"type":"string"},"hs2InSeededUniverse":{"type":"boolean"},"laneValueUsd":{"format":"double","type":"number"},"primaryExporterIso2":{"type":"string"},"primaryExporterShare":{"format":"double","type":"number"},"resilienceScore":{"format":"double","type":"number"},"topStrategicProducts":{"items":{"$ref":"#/components/schemas/StrategicProduct"},"type":"array"}},"type":"object"},"GetSectorDependencyRequest":{"properties":{"hs2":{"description":"HS2 chapter code, e.g. \"27\" (mineral fuels), \"85\" (electronics)","type":"string"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","hs2"],"type":"object"},"GetSectorDependencyResponse":{"properties":{"fetchedAt":{"type":"string"},"flags":{"items":{"description":"DependencyFlag classifies how a country+sector dependency can fail.","enum":["DEPENDENCY_FLAG_UNSPECIFIED","DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL","DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL","DEPENDENCY_FLAG_COMPOUND_RISK","DEPENDENCY_FLAG_DIVERSIFIABLE"],"type":"string"},"type":"array"},"hasViableBypass":{"description":"Whether at least one viable bypass corridor exists for the primary chokepoint.","type":"boolean"},"hs2":{"type":"string"},"hs2Label":{"description":"Human-readable HS2 chapter name.","type":"string"},"iso2":{"type":"string"},"primaryChokepointExposure":{"description":"Exposure score for the primary chokepoint (0–100).","format":"double","type":"number"},"primaryChokepointId":{"description":"Chokepoint ID with the highest exposure score for this country+sector.","type":"string"},"primaryExporterIso2":{"description":"ISO2 of the country supplying the largest share of this sector's imports.","type":"string"},"primaryExporterShare":{"description":"Share of imports from the primary exporter (0–1). 0 = no Comtrade data available.","format":"double","type":"number"}},"type":"object"},"GetShippingRatesRequest":{"type":"object"},"GetShippingRatesResponse":{"properties":{"fetchedAt":{"type":"string"},"indices":{"items":{"$ref":"#/components/schemas/ShippingIndex"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetShippingStressRequest":{"type":"object"},"GetShippingStressResponse":{"properties":{"carriers":{"items":{"$ref":"#/components/schemas/ShippingStressCarrier"},"type":"array"},"fetchedAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"stressLevel":{"description":"\"low\" | \"moderate\" | \"elevated\" | \"critical\".","type":"string"},"stressScore":{"description":"Composite stress score 0–100 (higher = more disruption).","format":"double","type":"number"},"upstreamUnavailable":{"description":"Set to true when upstream data source is unavailable and cached data is stale.","type":"boolean"}},"type":"object"},"GetStorageFacilityDetailRequest":{"description":"GetStorageFacilityDetail returns a single facility with its full evidence\n bundle + revision log. Revisions land in Week 3 alongside the disruption\n event log (Week 3 milestone — empty array in v1).","properties":{"facilityId":{"type":"string"}},"required":["facilityId"],"type":"object"},"GetStorageFacilityDetailResponse":{"properties":{"facility":{"$ref":"#/components/schemas/StorageFacilityEntry"},"fetchedAt":{"type":"string"},"revisions":{"items":{"$ref":"#/components/schemas/StorageFacilityRevisionEntry"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"LatLon":{"properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"ListEnergyDisruptionsRequest":{"description":"ListEnergyDisruptions returns the energy disruption event log — state\n transitions for pipelines and storage facilities (sabotage, sanction,\n maintenance, mechanical, weather, commercial, war). Each event ties\n back to an assetId seeded by the pipeline or storage registry, so the\n panel drawer can render an asset-scoped timeline without a second RPC.\n\n Seeded from a curated JSON file (scripts/data/energy-disruptions.json).\n An automated state-transition classifier was scoped but not shipped —\n the log is curated-only today.\n\n See docs/methodology/disruptions.mdx.","properties":{"assetId":{"description":"Filter to one asset. Omit for all. When set, also narrows to the\n matching asset_type if provided.","type":"string"},"assetType":{"description":"Filter to one asset type. Accepts: \"pipeline\" | \"storage\".","type":"string"},"ongoingOnly":{"description":"If true, only return events with endAt empty (still ongoing).","type":"boolean"}},"type":"object"},"ListEnergyDisruptionsResponse":{"properties":{"classifierVersion":{"type":"string"},"events":{"items":{"$ref":"#/components/schemas/EnergyDisruptionEntry"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListFuelShortagesRequest":{"description":"ListFuelShortages returns the global fuel-shortage alert registry.\n Seeded from a curated JSON file (scripts/data/fuel-shortages.json).\n An LLM classifier extension was scoped but not shipped — the registry\n is curated-only today. Severity (\"confirmed\" or \"watch\") is a row\n field authored at curation time; the evidence is shipped alongside\n so agents and humans can audit the grounds for a severity label.\n\n See docs/methodology/shortages.mdx for the evidence-threshold spec.","properties":{"country":{"description":"Filter to one ISO 3166-1 alpha-2 country. Omit for global.","type":"string"},"product":{"description":"Filter to one product. Accepts: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\".\n Omit for all products.","type":"string"},"severity":{"description":"Filter to one severity. Accepts: \"confirmed\" | \"watch\". Omit for both.","type":"string"}},"type":"object"},"ListFuelShortagesResponse":{"properties":{"classifierVersion":{"type":"string"},"fetchedAt":{"type":"string"},"shortages":{"items":{"$ref":"#/components/schemas/FuelShortageEntry"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListPipelinesRequest":{"description":"ListPipelines returns the full oil and/or gas pipeline registry with\n evidence-based status per asset. Registry is curated (see\n docs/methodology/pipelines.mdx) and refreshed weekly by\n scripts/seed-pipelines-{gas,oil}.mjs. Typical consumer: PipelineStatusPanel\n rendering the DeckGL PathLayer.\n\n The public badge is DERIVED from the evidence bundle server-side at\n read time — callers MUST use `public_badge` for display, not invent\n their own derivation from `evidence` fields. This keeps the atlas and\n MCP clients consistent and versioned together.","properties":{"commodityType":{"description":"Filter to one commodity. Omit (or pass empty) to receive both.","type":"string"}},"type":"object"},"ListPipelinesResponse":{"properties":{"classifierVersion":{"type":"string"},"fetchedAt":{"type":"string"},"pipelines":{"items":{"$ref":"#/components/schemas/PipelineEntry"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListStorageFacilitiesRequest":{"description":"ListStorageFacilities returns the curated strategic storage registry\n (underground gas storage, strategic petroleum reserves, LNG terminals,\n crude tank farms) with evidence-based status per asset. Registry is\n curated (see docs/methodology/storage.mdx) and refreshed weekly by\n scripts/seed-storage-facilities.mjs. Typical consumer:\n StorageFacilityMapPanel rendering the DeckGL ScatterplotLayer.\n\n Like pipelines, the public badge is DERIVED from the evidence bundle\n server-side at read time. Callers MUST use `public_badge` for display\n rather than inventing their own derivation from `evidence` fields, so\n the atlas and MCP clients stay consistent and versioned together.","properties":{"facilityType":{"description":"Filter to one facility type. Accepts:\n \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"\n Omit (or pass empty) to receive all types.","type":"string"}},"type":"object"},"ListStorageFacilitiesResponse":{"properties":{"classifierVersion":{"type":"string"},"facilities":{"items":{"$ref":"#/components/schemas/StorageFacilityEntry"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"MineralProducer":{"properties":{"country":{"type":"string"},"countryCode":{"type":"string"},"productionTonnes":{"format":"double","type":"number"},"sharePct":{"format":"double","type":"number"}},"type":"object"},"MultiSectorCostShock":{"properties":{"addedTransitDays":{"description":"Bypass-corridor transit penalty (informational).","format":"int32","type":"integer"},"closureDays":{"description":"Echoes the clamped closure duration used for total_cost_shock (1-365).","format":"int32","type":"integer"},"freightAddedPctPerTon":{"description":"Bypass-corridor freight uplift fraction (0.10 == +10% per ton).","format":"double","type":"number"},"hs2":{"description":"HS2 chapter code (e.g. \"27\" mineral fuels, \"85\" electronics).","type":"string"},"hs2Label":{"description":"Friendly chapter label (e.g. \"Energy\", \"Electronics\").","type":"string"},"importValueAnnual":{"description":"Total annual import value (USD) for this sector.","format":"double","type":"number"},"totalCostShock":{"description":"Cost for the requested closure_days window.","format":"double","type":"number"},"totalCostShock30Days":{"format":"double","type":"number"},"totalCostShock90Days":{"format":"double","type":"number"},"totalCostShockPerDay":{"format":"double","type":"number"},"warRiskPremiumBps":{"description":"War-risk insurance premium (basis points) sourced from the chokepoint tier.","format":"int32","type":"integer"}},"type":"object"},"NumberRange":{"description":"Inclusive integer range for transit days / freight USD estimates.","properties":{"max":{"format":"int32","type":"integer"},"min":{"format":"int32","type":"integer"}},"type":"object"},"OperatorStatement":{"properties":{"date":{"type":"string"},"text":{"type":"string"},"url":{"type":"string"}},"type":"object"},"PipelineEntry":{"properties":{"capacityBcmYr":{"format":"double","type":"number"},"capacityMbd":{"format":"double","type":"number"},"commodityType":{"description":"Either \"gas\" or \"oil\".","type":"string"},"endPoint":{"$ref":"#/components/schemas/LatLon"},"evidence":{"$ref":"#/components/schemas/PipelineEvidence"},"fromCountry":{"type":"string"},"id":{"type":"string"},"inService":{"format":"int32","type":"integer"},"lengthKm":{"format":"int32","type":"integer"},"name":{"type":"string"},"operator":{"type":"string"},"publicBadge":{"description":"Server-derived public badge. One of:\n \"flowing\" | \"reduced\" | \"offline\" | \"disputed\"","type":"string"},"startPoint":{"$ref":"#/components/schemas/LatLon"},"toCountry":{"type":"string"},"transitCountries":{"items":{"type":"string"},"type":"array"},"waypoints":{"items":{"$ref":"#/components/schemas/LatLon"},"type":"array"}},"type":"object"},"PipelineEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"commercialState":{"description":"One of: \"under_contract\" | \"expired\" | \"suspended\" | \"unknown\"","type":"string"},"lastEvidenceUpdate":{"type":"string"},"operatorStatement":{"$ref":"#/components/schemas/OperatorStatement"},"physicalState":{"description":"One of: \"flowing\" | \"reduced\" | \"offline\" | \"unknown\"","type":"string"},"physicalStateSource":{"description":"One of: \"operator\" | \"regulator\" | \"press\" | \"satellite\" | \"ais-relay\"","type":"string"},"sanctionRefs":{"items":{"$ref":"#/components/schemas/SanctionRef"},"type":"array"}},"type":"object"},"PipelineRevisionEntry":{"properties":{"classifierVersion":{"type":"string"},"date":{"type":"string"},"fieldChanged":{"type":"string"},"newValue":{"type":"string"},"previousValue":{"type":"string"},"sourcesUsed":{"items":{"type":"string"},"type":"array"},"trigger":{"description":"One of: \"classifier\" | \"source\" | \"decay\" | \"override\"","type":"string"}},"type":"object"},"ProductExporter":{"properties":{"partnerCode":{"format":"int32","type":"integer"},"partnerIso2":{"type":"string"},"share":{"format":"double","type":"number"},"value":{"format":"double","type":"number"}},"type":"object"},"SanctionRef":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"listId":{"type":"string"},"url":{"type":"string"}},"type":"object"},"ShippingIndex":{"properties":{"changePct":{"format":"double","type":"number"},"currentValue":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/ShippingRatePoint"},"type":"array"},"indexId":{"type":"string"},"name":{"type":"string"},"previousValue":{"format":"double","type":"number"},"spikeAlert":{"type":"boolean"},"unit":{"type":"string"}},"type":"object"},"ShippingRatePoint":{"properties":{"date":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ShippingStressCarrier":{"description":"ShippingStressCarrier represents market stress data for a carrier or shipping index.","properties":{"carrierType":{"description":"Carrier type: \"etf\" | \"carrier\" | \"index\".","type":"string"},"changePct":{"description":"Percentage change from previous close.","format":"double","type":"number"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"30-day price sparkline.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker or identifier (e.g., \"BDRY\", \"ZIM\").","type":"string"}},"type":"object"},"StorageEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"commercialState":{"description":"One of: \"under_contract\" | \"expired\" | \"suspended\" | \"unknown\"","type":"string"},"fillDisclosed":{"description":"Whether working-gas / stock-level fill is publicly disclosed for this\n asset. LNG export terminals, for instance, tend NOT to disclose; UGS\n sites in Europe are required to disclose via GIE AGSI+.","type":"boolean"},"fillSource":{"description":"Source of the disclosed fill series (\"GIE AGSI+\", \"EIA SPR weekly\n stock report\", etc.). Empty when fill_disclosed=false.","type":"string"},"lastEvidenceUpdate":{"type":"string"},"operatorStatement":{"$ref":"#/components/schemas/StorageOperatorStatement"},"physicalState":{"description":"One of: \"operational\" | \"reduced\" | \"offline\" | \"under_construction\" | \"unknown\"","type":"string"},"physicalStateSource":{"description":"One of: \"operator\" | \"regulator\" | \"press\" | \"satellite\" | \"ais-relay\"","type":"string"},"sanctionRefs":{"items":{"$ref":"#/components/schemas/StorageSanctionRef"},"type":"array"}},"type":"object"},"StorageFacilityEntry":{"properties":{"capacityMb":{"format":"double","type":"number"},"capacityMtpa":{"format":"double","type":"number"},"capacityTwh":{"description":"Working capacity in the facility's native unit (see working_capacity_unit).\n Exactly ONE of these is populated per facility, chosen by facility_type:\n ugs → capacity_twh\n spr, crude_tank_farm → capacity_mb\n lng_export, lng_import → capacity_mtpa","format":"double","type":"number"},"country":{"type":"string"},"evidence":{"$ref":"#/components/schemas/StorageEvidence"},"facilityType":{"description":"One of: \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"","type":"string"},"id":{"type":"string"},"inService":{"format":"int32","type":"integer"},"location":{"$ref":"#/components/schemas/StorageLatLon"},"name":{"type":"string"},"operator":{"type":"string"},"publicBadge":{"description":"Server-derived public badge. One of:\n \"operational\" | \"reduced\" | \"offline\" | \"disputed\"","type":"string"},"workingCapacityUnit":{"description":"One of: \"TWh\" | \"Mb\" | \"Mtpa\"","type":"string"}},"type":"object"},"StorageFacilityRevisionEntry":{"properties":{"classifierVersion":{"type":"string"},"date":{"type":"string"},"fieldChanged":{"type":"string"},"newValue":{"type":"string"},"previousValue":{"type":"string"},"sourcesUsed":{"items":{"type":"string"},"type":"array"},"trigger":{"description":"One of: \"classifier\" | \"source\" | \"decay\" | \"override\"","type":"string"}},"type":"object"},"StorageLatLon":{"properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"StorageOperatorStatement":{"properties":{"date":{"type":"string"},"text":{"type":"string"},"url":{"type":"string"}},"type":"object"},"StorageSanctionRef":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"listId":{"type":"string"},"url":{"type":"string"}},"type":"object"},"StrategicProduct":{"properties":{"hs4":{"type":"string"},"label":{"type":"string"},"primaryChokepointId":{"type":"string"},"topExporterIso2":{"type":"string"},"topExporterShare":{"format":"double","type":"number"},"totalValueUsd":{"format":"double","type":"number"}},"type":"object"},"TransitDayCount":{"properties":{"capContainer":{"format":"double","type":"number"},"capDryBulk":{"format":"double","type":"number"},"capGeneralCargo":{"format":"double","type":"number"},"capRoro":{"format":"double","type":"number"},"capTanker":{"format":"double","type":"number"},"cargo":{"format":"int32","type":"integer"},"container":{"format":"int32","type":"integer"},"date":{"type":"string"},"dryBulk":{"format":"int32","type":"integer"},"generalCargo":{"format":"int32","type":"integer"},"other":{"format":"int32","type":"integer"},"roro":{"format":"int32","type":"integer"},"tanker":{"format":"int32","type":"integer"},"total":{"format":"int32","type":"integer"}},"type":"object"},"TransitSummary":{"properties":{"dataAvailable":{"description":"False when the upstream portwatch/relay source did not return data for\n this chokepoint in the current cycle — the summary fields are zero-state\n fill, not a genuine \"zero traffic\" reading. Client should render a\n \"transit data unavailable\" indicator and skip stat/chart rendering.","type":"boolean"},"disruptionPct":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/TransitDayCount"},"type":"array"},"incidentCount7d":{"format":"int32","type":"integer"},"riskLevel":{"type":"string"},"riskReportAction":{"type":"string"},"riskSummary":{"type":"string"},"todayCargo":{"format":"int32","type":"integer"},"todayOther":{"format":"int32","type":"integer"},"todayTanker":{"format":"int32","type":"integer"},"todayTotal":{"format":"int32","type":"integer"},"wowChangePct":{"format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"SupplyChainService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/supply-chain/v1/get-bypass-options":{"get":{"description":"GetBypassOptions returns ranked bypass corridors for a chokepoint. PRO-gated.","operationId":"GetBypassOptions","parameters":[{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"container | tanker | bulk | roro (default: \"container\")","in":"query","name":"cargoType","required":false,"schema":{"type":"string"}},{"description":"0-100, percent of capacity blocked (default: 100)","in":"query","name":"closurePct","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBypassOptionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBypassOptions","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-chokepoint-history":{"get":{"description":"GetChokepointHistory returns transit-count history for a single chokepoint,\n loaded lazily on card expand. Keeps the status RPC compact (no 180-day\n history per chokepoint on every call).","operationId":"GetChokepointHistory","parameters":[{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetChokepointHistoryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetChokepointHistory","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-chokepoint-status":{"get":{"operationId":"GetChokepointStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetChokepointStatusResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetChokepointStatus","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-chokepoint-index":{"get":{"description":"GetCountryChokepointIndex returns per-chokepoint exposure scores for a country. PRO-gated.","operationId":"GetCountryChokepointIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (uppercase).","in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter (2-digit string). Defaults to \"27\" (energy/mineral fuels) when absent.","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryChokepointIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryChokepointIndex","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-cost-shock":{"get":{"description":"GetCountryCostShock returns cost shock and war risk data for a country+chokepoint. PRO-gated.","operationId":"GetCountryCostShock","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter (default: \"27\")","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryCostShockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryCostShock","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-products":{"get":{"description":"GetCountryProducts returns the seeded bilateral-HS4 import basket for a country. PRO-gated.","operationId":"GetCountryProducts","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryProductsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryProducts","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-critical-minerals":{"get":{"operationId":"GetCriticalMinerals","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCriticalMineralsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCriticalMinerals","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-fuel-shortage-detail":{"get":{"description":"GetFuelShortageDetail returns a single shortage with full evidence\n bundle and citation timeline. Loaded lazily on drawer open.","operationId":"GetFuelShortageDetail","parameters":[{"in":"query","name":"shortageId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFuelShortageDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFuelShortageDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-multi-sector-cost-shock":{"get":{"description":"GetMultiSectorCostShock returns per-sector cost-shock estimates for a\n country+chokepoint+closure-window. PRO-gated.","operationId":"GetMultiSectorCostShock","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.","in":"query","name":"closureDays","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMultiSectorCostShockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetMultiSectorCostShock","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-pipeline-detail":{"get":{"description":"GetPipelineDetail returns a single pipeline with full evidence bundle\n + auto-revision-log entries. Loaded lazily on drawer open.","operationId":"GetPipelineDetail","parameters":[{"in":"query","name":"pipelineId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetPipelineDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetPipelineDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-route-explorer-lane":{"get":{"description":"GetRouteExplorerLane returns the primary maritime route, chokepoint exposures,\n bypass options with geometry, war risk, and static transit/freight estimates for\n a country pair + HS2 + cargo type. PRO-gated. Wraps the route-intelligence vendor\n endpoint's compute with browser-callable auth and adds fields needed by the\n Route Explorer UI.","operationId":"GetRouteExplorerLane","parameters":[{"in":"query","name":"fromIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"toIso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter code, e.g. \"27\", \"85\"","in":"query","name":"hs2","required":false,"schema":{"type":"string"}},{"description":"One of: container, tanker, bulk, roro","in":"query","name":"cargoType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetRouteExplorerLaneResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetRouteExplorerLane","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-route-impact":{"get":{"operationId":"GetRouteImpact","parameters":[{"in":"query","name":"fromIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"toIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetRouteImpactResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetRouteImpact","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-sector-dependency":{"get":{"description":"GetSectorDependency returns dependency flags and risk profile for a country+HS2 sector. PRO-gated.","operationId":"GetSectorDependency","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter code, e.g. \"27\" (mineral fuels), \"85\" (electronics)","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorDependencyResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetSectorDependency","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-shipping-rates":{"get":{"operationId":"GetShippingRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetShippingRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetShippingRates","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-shipping-stress":{"get":{"description":"GetShippingStress returns carrier market data and a composite stress index.","operationId":"GetShippingStress","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetShippingStressResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetShippingStress","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-storage-facility-detail":{"get":{"description":"GetStorageFacilityDetail returns a single facility with full evidence\n bundle + revision log. Loaded lazily on drawer open.","operationId":"GetStorageFacilityDetail","parameters":[{"in":"query","name":"facilityId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStorageFacilityDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetStorageFacilityDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-energy-disruptions":{"get":{"description":"ListEnergyDisruptions returns the disruption event log for pipelines\n and storage facilities. Supports per-asset or per-asset-type filtering\n so panel drawers can fetch a scoped timeline without pulling the\n full registry.","operationId":"ListEnergyDisruptions","parameters":[{"description":"Filter to one asset. Omit for all. When set, also narrows to the\n matching asset_type if provided.","in":"query","name":"assetId","required":false,"schema":{"type":"string"}},{"description":"Filter to one asset type. Accepts: \"pipeline\" | \"storage\".","in":"query","name":"assetType","required":false,"schema":{"type":"string"}},{"description":"If true, only return events with endAt empty (still ongoing).","in":"query","name":"ongoingOnly","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEnergyDisruptionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEnergyDisruptions","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-fuel-shortages":{"get":{"description":"ListFuelShortages returns the global fuel-shortage alert registry.\n Curated-only: severity (\"confirmed\" | \"watch\") is a row field authored\n at curation time, not a client-side derivation. Free tier.","operationId":"ListFuelShortages","parameters":[{"description":"Filter to one ISO 3166-1 alpha-2 country. Omit for global.","in":"query","name":"country","required":false,"schema":{"type":"string"}},{"description":"Filter to one product. Accepts: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\".\n Omit for all products.","in":"query","name":"product","required":false,"schema":{"type":"string"}},{"description":"Filter to one severity. Accepts: \"confirmed\" | \"watch\". Omit for both.","in":"query","name":"severity","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFuelShortagesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListFuelShortages","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-pipelines":{"get":{"description":"ListPipelines returns the curated oil \u0026 gas pipeline registry for the\n Energy Atlas PathLayer. Public badges are DERIVED from evidence bundles\n server-side and versioned (classifier_version). Free-tier; see\n docs/methodology/pipelines.mdx for data + classifier spec.","operationId":"ListPipelines","parameters":[{"description":"Filter to one commodity. Omit (or pass empty) to receive both.","in":"query","name":"commodityType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListPipelinesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListPipelines","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-storage-facilities":{"get":{"description":"ListStorageFacilities returns the curated strategic storage registry\n (UGS + SPR + LNG + crude tank farms) for the Energy Atlas DeckGL\n ScatterplotLayer. Public badges are DERIVED from evidence bundles\n server-side and versioned (classifier_version). Free-tier; see\n docs/methodology/storage.mdx.","operationId":"ListStorageFacilities","parameters":[{"description":"Filter to one facility type. Accepts:\n \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"\n Omit (or pass empty) to receive all types.","in":"query","name":"facilityType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStorageFacilitiesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStorageFacilities","tags":["SupplyChainService"]}}}} \ No newline at end of file +{"components":{"schemas":{"BypassCorridorOption":{"description":"BypassCorridorOption is a single enriched bypass corridor for the Route Explorer UI.\n Includes coordinate endpoints so the client can call MapContainer.setBypassRoutes\n directly without any client-side geometry lookup.","properties":{"addedCostMultiplier":{"format":"double","type":"number"},"addedTransitDays":{"format":"int32","type":"integer"},"fromPort":{"$ref":"#/components/schemas/GeoPoint"},"id":{"type":"string"},"name":{"type":"string"},"status":{"description":"Status of a bypass corridor for UI labeling. \"active\" means usable today;\n \"proposed\" means documented but not yet built/operational; \"unavailable\"\n means blockaded or otherwise blocked from use.","enum":["CORRIDOR_STATUS_UNSPECIFIED","CORRIDOR_STATUS_ACTIVE","CORRIDOR_STATUS_PROPOSED","CORRIDOR_STATUS_UNAVAILABLE"],"type":"string"},"toPort":{"$ref":"#/components/schemas/GeoPoint"},"type":{"type":"string"},"warRiskTier":{"type":"string"}},"type":"object"},"BypassOption":{"properties":{"activationThreshold":{"type":"string"},"addedCostMultiplier":{"format":"double","type":"number"},"addedTransitDays":{"format":"int32","type":"integer"},"bypassWarRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"},"capacityConstraintTonnage":{"format":"int64","type":"string"},"id":{"type":"string"},"liveScore":{"format":"double","type":"number"},"name":{"type":"string"},"notes":{"type":"string"},"suitableCargoTypes":{"items":{"type":"string"},"type":"array"},"type":{"type":"string"},"waypointChokepointIds":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ChokepointExposureEntry":{"description":"ChokepointExposureEntry holds per-chokepoint exposure data for a country.","properties":{"chokepointId":{"description":"Canonical chokepoint ID from the chokepoint registry.","type":"string"},"chokepointName":{"description":"Human-readable chokepoint name.","type":"string"},"coastSide":{"description":"Which ocean/basin side the country's ports face (atlantic, pacific, indian, med, multi, landlocked).","type":"string"},"exposureScore":{"description":"Exposure score 0–100; higher = more dependent on this chokepoint.","format":"double","type":"number"},"shockSupported":{"description":"Whether the shock model is supported for this chokepoint + hs2 combination.","type":"boolean"}},"type":"object"},"ChokepointExposureSummary":{"properties":{"chokepointId":{"type":"string"},"chokepointName":{"type":"string"},"exposurePct":{"format":"int32","type":"integer"}},"type":"object"},"ChokepointInfo":{"properties":{"activeWarnings":{"format":"int32","type":"integer"},"affectedRoutes":{"items":{"type":"string"},"type":"array"},"aisDisruptions":{"format":"int32","type":"integer"},"congestionLevel":{"type":"string"},"description":{"type":"string"},"directionalDwt":{"items":{"$ref":"#/components/schemas/DirectionalDwt"},"type":"array"},"directions":{"items":{"type":"string"},"type":"array"},"disruptionScore":{"format":"int32","type":"integer"},"flowEstimate":{"$ref":"#/components/schemas/FlowEstimate"},"id":{"type":"string"},"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"},"name":{"type":"string"},"status":{"type":"string"},"transitSummary":{"$ref":"#/components/schemas/TransitSummary"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"CountryProduct":{"properties":{"description":{"type":"string"},"hs4":{"type":"string"},"topExporters":{"items":{"$ref":"#/components/schemas/ProductExporter"},"type":"array"},"totalValue":{"format":"double","type":"number"},"year":{"format":"int32","type":"integer"}},"type":"object"},"CriticalMineral":{"properties":{"globalProduction":{"format":"double","type":"number"},"hhi":{"format":"double","type":"number"},"mineral":{"type":"string"},"riskRating":{"type":"string"},"topProducers":{"items":{"$ref":"#/components/schemas/MineralProducer"},"type":"array"},"unit":{"type":"string"}},"type":"object"},"DirectionalDwt":{"properties":{"direction":{"type":"string"},"dwtThousandTonnes":{"format":"double","type":"number"},"wowChangePct":{"format":"double","type":"number"}},"type":"object"},"EnergyDisruptionEntry":{"properties":{"assetId":{"description":"Maps to a pipeline or storage-facility id seeded elsewhere.","type":"string"},"assetType":{"description":"One of: \"pipeline\" | \"storage\"","type":"string"},"capacityOfflineBcmYr":{"description":"Headline-offline capacity (contextual — 0 when not applicable).","format":"double","type":"number"},"capacityOfflineMbd":{"format":"double","type":"number"},"causeChain":{"items":{"description":"Contributing causes, primary-first.","type":"string"},"type":"array"},"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"countries":{"items":{"description":"Countries touched by the referenced asset (pipeline: fromCountry +\n toCountry + transitCountries; storage: country). Denormalised at seed\n time so CountryDeepDivePanel can filter events without a second RPC.\n Always non-empty in well-formed payloads; empty only if the upstream\n asset was removed without an event update. Per plan §R/#5 decision B.","type":"string"},"type":"array"},"endAt":{"description":"Empty string when event is still ongoing.","type":"string"},"eventType":{"description":"One of: \"sabotage\" | \"sanction\" | \"maintenance\" | \"mechanical\" |\n \"weather\" | \"commercial\" | \"war\" | \"other\"","type":"string"},"id":{"type":"string"},"lastEvidenceUpdate":{"type":"string"},"shortDescription":{"type":"string"},"sources":{"items":{"$ref":"#/components/schemas/EnergyDisruptionSource"},"type":"array"},"startAt":{"type":"string"}},"type":"object"},"EnergyDisruptionSource":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"sourceType":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"FlowEstimate":{"properties":{"baselineMbd":{"format":"double","type":"number"},"currentMbd":{"format":"double","type":"number"},"disrupted":{"type":"boolean"},"flowRatio":{"format":"double","type":"number"},"hazardAlertLevel":{"type":"string"},"hazardAlertName":{"type":"string"},"source":{"type":"string"}},"type":"object"},"FuelShortageEntry":{"properties":{"causeChain":{"items":{"description":"Contributing root causes, ordered primary-first. Subset of:\n \"upstream_refinery\" | \"logistics\" | \"policy\" | \"chokepoint\" |\n \"sanction\" | \"war\" | \"import_cut\"","type":"string"},"type":"array"},"country":{"type":"string"},"evidence":{"$ref":"#/components/schemas/FuelShortageEvidence"},"firstSeen":{"type":"string"},"id":{"type":"string"},"impactTypes":{"items":{"description":"Observable consumer-facing impacts. Subset of:\n \"stations_closed\" | \"rationing\" | \"flights_cancelled\" | \"import_cut\" | \"price_spike\"","type":"string"},"type":"array"},"lastConfirmed":{"type":"string"},"product":{"description":"One of: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\"","type":"string"},"resolvedAt":{"description":"Empty string when not yet resolved.","type":"string"},"severity":{"description":"One of: \"confirmed\" | \"watch\" (classifier output — not a client-side derivation)","type":"string"},"shortDescription":{"type":"string"}},"type":"object"},"FuelShortageEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"evidenceSources":{"items":{"$ref":"#/components/schemas/FuelShortageEvidenceSource"},"type":"array"},"firstRegulatorConfirmation":{"description":"ISO date of the first regulator confirmation, if any. Empty when\n severity is \"watch\" on press-only signal.","type":"string"},"lastEvidenceUpdate":{"type":"string"}},"type":"object"},"FuelShortageEvidenceSource":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"sourceType":{"description":"One of: \"regulator\" | \"operator\" | \"press\" | \"ais-relay\" | \"satellite\"","type":"string"},"title":{"type":"string"},"url":{"type":"string"}},"type":"object"},"GeoPoint":{"description":"GeoPoint is a [longitude, latitude] pair.","properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"GetBypassOptionsRequest":{"properties":{"cargoType":{"description":"container | tanker | bulk | roro (default: \"container\")","type":"string"},"chokepointId":{"type":"string"},"closurePct":{"description":"0-100, percent of capacity blocked (default: 100)","format":"int32","type":"integer"}},"required":["chokepointId"],"type":"object"},"GetBypassOptionsResponse":{"properties":{"cargoType":{"type":"string"},"chokepointId":{"type":"string"},"closurePct":{"format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"options":{"items":{"$ref":"#/components/schemas/BypassOption"},"type":"array"},"primaryChokepointWarRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetChokepointHistoryRequest":{"description":"GetChokepointHistory returns the transit-count history for a single\n chokepoint. Loaded lazily on card expand so the main chokepoint-status\n response can stay compact (no 180-day history per chokepoint).","properties":{"chokepointId":{"type":"string"}},"required":["chokepointId"],"type":"object"},"GetChokepointHistoryResponse":{"properties":{"chokepointId":{"type":"string"},"fetchedAt":{"format":"int64","type":"string"},"history":{"items":{"$ref":"#/components/schemas/TransitDayCount"},"type":"array"}},"type":"object"},"GetChokepointStatusRequest":{"type":"object"},"GetChokepointStatusResponse":{"properties":{"chokepoints":{"items":{"$ref":"#/components/schemas/ChokepointInfo"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetCountryChokepointIndexRequest":{"description":"GetCountryChokepointIndexRequest specifies the country and optional HS2 chapter.","properties":{"hs2":{"description":"HS2 chapter (2-digit string). Defaults to \"27\" (energy/mineral fuels) when absent.","type":"string"},"iso2":{"description":"ISO 3166-1 alpha-2 country code (uppercase).","pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2"],"type":"object"},"GetCountryChokepointIndexResponse":{"description":"GetCountryChokepointIndexResponse returns exposure scores for all relevant chokepoints.","properties":{"exposures":{"items":{"$ref":"#/components/schemas/ChokepointExposureEntry"},"type":"array"},"fetchedAt":{"description":"ISO timestamp of when this data was last seeded.","type":"string"},"hs2":{"description":"HS2 chapter used for the computation.","type":"string"},"iso2":{"description":"ISO 3166-1 alpha-2 country code echoed from the request.","type":"string"},"primaryChokepointId":{"description":"Canonical ID of the chokepoint with the highest exposure score.","type":"string"},"vulnerabilityIndex":{"description":"Composite vulnerability index 0–100 (weighted sum of top-3 exposures).","format":"double","type":"number"}},"type":"object"},"GetCountryCostShockRequest":{"properties":{"chokepointId":{"type":"string"},"hs2":{"description":"HS2 chapter (default: \"27\")","type":"string"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","chokepointId"],"type":"object"},"GetCountryCostShockResponse":{"properties":{"chokepointId":{"type":"string"},"coverageDays":{"description":"Energy stockpile coverage in days (IEA data, HS 27 only; 0 for non-energy sectors or net exporters)","format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"hasEnergyModel":{"description":"Whether supply_deficit_pct and coverage_days are modelled (true) or unavailable (false)","type":"boolean"},"hs2":{"type":"string"},"iso2":{"type":"string"},"supplyDeficitPct":{"description":"Average refined-product supply deficit % under full closure (Gasoline/Diesel/Jet fuel/LPG average; HS 27 only)","format":"double","type":"number"},"unavailableReason":{"description":"Null/unavailable explanation for non-energy sectors","type":"string"},"warRiskPremiumBps":{"description":"War risk insurance premium in basis points for this chokepoint","format":"int32","type":"integer"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetCountryProductsRequest":{"properties":{"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2"],"type":"object"},"GetCountryProductsResponse":{"properties":{"fetchedAt":{"description":"ISO timestamp from the seeded payload (empty when no data is cached).","type":"string"},"iso2":{"type":"string"},"products":{"items":{"$ref":"#/components/schemas/CountryProduct"},"type":"array"}},"type":"object"},"GetCriticalMineralsRequest":{"type":"object"},"GetCriticalMineralsResponse":{"properties":{"fetchedAt":{"type":"string"},"minerals":{"items":{"$ref":"#/components/schemas/CriticalMineral"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetFuelShortageDetailRequest":{"description":"GetFuelShortageDetail returns a single shortage with full evidence\n bundle + citation timeline. Loaded lazily on drawer open.","properties":{"shortageId":{"type":"string"}},"required":["shortageId"],"type":"object"},"GetFuelShortageDetailResponse":{"properties":{"fetchedAt":{"type":"string"},"shortage":{"$ref":"#/components/schemas/FuelShortageEntry"},"unavailable":{"type":"boolean"}},"type":"object"},"GetMultiSectorCostShockRequest":{"properties":{"chokepointId":{"type":"string"},"closureDays":{"description":"Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.","format":"int32","type":"integer"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","chokepointId"],"type":"object"},"GetMultiSectorCostShockResponse":{"properties":{"chokepointId":{"type":"string"},"closureDays":{"description":"Server-clamped closure-window duration in days (1-365).","format":"int32","type":"integer"},"fetchedAt":{"type":"string"},"iso2":{"type":"string"},"sectors":{"items":{"$ref":"#/components/schemas/MultiSectorCostShock"},"type":"array"},"totalAddedCost":{"description":"Sum of total_cost_shock across all sectors.","format":"double","type":"number"},"unavailableReason":{"description":"Populated when no seeded import data is available for the country.","type":"string"},"warRiskTier":{"description":"*\n War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.\n This is a FREE field (no PRO gate) — it exposes the existing server-internal\n threatLevel from ChokepointConfig, making it available to clients for badges\n and bypass corridor scoring.","enum":["WAR_RISK_TIER_UNSPECIFIED","WAR_RISK_TIER_NORMAL","WAR_RISK_TIER_ELEVATED","WAR_RISK_TIER_HIGH","WAR_RISK_TIER_CRITICAL","WAR_RISK_TIER_WAR_ZONE"],"type":"string"}},"type":"object"},"GetPipelineDetailRequest":{"description":"GetPipelineDetail returns a single pipeline with its full evidence bundle.\n Evidence surface here is richer than ListPipelinesResponse — the list view\n is designed for the map layer's compact shape; detail is designed for the\n click-through drawer.","properties":{"pipelineId":{"type":"string"}},"required":["pipelineId"],"type":"object"},"GetPipelineDetailResponse":{"properties":{"fetchedAt":{"type":"string"},"pipeline":{"$ref":"#/components/schemas/PipelineEntry"},"revisions":{"items":{"$ref":"#/components/schemas/PipelineRevisionEntry"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"GetRouteExplorerLaneRequest":{"properties":{"cargoType":{"description":"One of: container, tanker, bulk, roro","type":"string"},"fromIso2":{"pattern":"^[A-Z]{2}$","type":"string"},"hs2":{"description":"HS2 chapter code, e.g. \"27\", \"85\"","type":"string"},"toIso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["fromIso2","toIso2","hs2","cargoType"],"type":"object"},"GetRouteExplorerLaneResponse":{"properties":{"bypassOptions":{"items":{"$ref":"#/components/schemas/BypassCorridorOption"},"type":"array"},"cargoType":{"type":"string"},"chokepointExposures":{"items":{"$ref":"#/components/schemas/ChokepointExposureSummary"},"type":"array"},"disruptionScore":{"format":"double","type":"number"},"estFreightUsdPerTeuRange":{"$ref":"#/components/schemas/NumberRange"},"estTransitDaysRange":{"$ref":"#/components/schemas/NumberRange"},"fetchedAt":{"type":"string"},"fromIso2":{"type":"string"},"hs2":{"type":"string"},"noModeledLane":{"description":"True when the wrapper fell back to the origin's first route (no shared route\n between origin and destination clusters). Signals \"no modeled lane\" to the UI.","type":"boolean"},"primaryRouteGeometry":{"items":{"$ref":"#/components/schemas/GeoPoint"},"type":"array"},"primaryRouteId":{"description":"Primary trade route ID from TRADE_ROUTES config. Empty when no modeled lane.","type":"string"},"toIso2":{"type":"string"},"warRiskTier":{"type":"string"}},"type":"object"},"GetRouteImpactRequest":{"properties":{"fromIso2":{"pattern":"^[A-Z]{2}$","type":"string"},"hs2":{"type":"string"},"toIso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["fromIso2","toIso2","hs2"],"type":"object"},"GetRouteImpactResponse":{"properties":{"comtradeSource":{"type":"string"},"dependencyFlags":{"items":{"description":"DependencyFlag classifies how a country+sector dependency can fail.","enum":["DEPENDENCY_FLAG_UNSPECIFIED","DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL","DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL","DEPENDENCY_FLAG_COMPOUND_RISK","DEPENDENCY_FLAG_DIVERSIFIABLE"],"type":"string"},"type":"array"},"fetchedAt":{"type":"string"},"hs2InSeededUniverse":{"type":"boolean"},"laneValueUsd":{"format":"double","type":"number"},"primaryExporterIso2":{"type":"string"},"primaryExporterShare":{"format":"double","type":"number"},"resilienceScore":{"format":"double","type":"number"},"topStrategicProducts":{"items":{"$ref":"#/components/schemas/StrategicProduct"},"type":"array"}},"type":"object"},"GetSectorDependencyRequest":{"properties":{"hs2":{"description":"HS2 chapter code, e.g. \"27\" (mineral fuels), \"85\" (electronics)","type":"string"},"iso2":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["iso2","hs2"],"type":"object"},"GetSectorDependencyResponse":{"properties":{"fetchedAt":{"type":"string"},"flags":{"items":{"description":"DependencyFlag classifies how a country+sector dependency can fail.","enum":["DEPENDENCY_FLAG_UNSPECIFIED","DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL","DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL","DEPENDENCY_FLAG_COMPOUND_RISK","DEPENDENCY_FLAG_DIVERSIFIABLE"],"type":"string"},"type":"array"},"hasViableBypass":{"description":"Whether at least one viable bypass corridor exists for the primary chokepoint.","type":"boolean"},"hs2":{"type":"string"},"hs2Label":{"description":"Human-readable HS2 chapter name.","type":"string"},"iso2":{"type":"string"},"primaryChokepointExposure":{"description":"Exposure score for the primary chokepoint (0–100).","format":"double","type":"number"},"primaryChokepointId":{"description":"Chokepoint ID with the highest exposure score for this country+sector.","type":"string"},"primaryExporterIso2":{"description":"ISO2 of the country supplying the largest share of this sector's imports.","type":"string"},"primaryExporterShare":{"description":"Share of imports from the primary exporter (0–1). 0 = no Comtrade data available.","format":"double","type":"number"}},"type":"object"},"GetShippingRatesRequest":{"type":"object"},"GetShippingRatesResponse":{"properties":{"fetchedAt":{"type":"string"},"indices":{"items":{"$ref":"#/components/schemas/ShippingIndex"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetShippingStressRequest":{"type":"object"},"GetShippingStressResponse":{"properties":{"carriers":{"items":{"$ref":"#/components/schemas/ShippingStressCarrier"},"type":"array"},"fetchedAt":{"description":"Warning: Values \u003e 2^53 may lose precision in JavaScript","format":"int64","type":"integer"},"stressLevel":{"description":"\"low\" | \"moderate\" | \"elevated\" | \"critical\".","type":"string"},"stressScore":{"description":"Composite stress score 0–100 (higher = more disruption).","format":"double","type":"number"},"upstreamUnavailable":{"description":"Set to true when upstream data source is unavailable and cached data is stale.","type":"boolean"}},"type":"object"},"GetStorageFacilityDetailRequest":{"description":"GetStorageFacilityDetail returns a single facility with its full evidence\n bundle + revision log. Revisions land in Week 3 alongside the disruption\n event log (Week 3 milestone — empty array in v1).","properties":{"facilityId":{"type":"string"}},"required":["facilityId"],"type":"object"},"GetStorageFacilityDetailResponse":{"properties":{"facility":{"$ref":"#/components/schemas/StorageFacilityEntry"},"fetchedAt":{"type":"string"},"revisions":{"items":{"$ref":"#/components/schemas/StorageFacilityRevisionEntry"},"type":"array"},"unavailable":{"type":"boolean"}},"type":"object"},"LatLon":{"properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"ListEnergyDisruptionsRequest":{"description":"ListEnergyDisruptions returns the energy disruption event log — state\n transitions for pipelines and storage facilities (sabotage, sanction,\n maintenance, mechanical, weather, commercial, war). Each event ties\n back to an assetId seeded by the pipeline or storage registry, so the\n panel drawer can render an asset-scoped timeline without a second RPC.\n\n Seeded from a curated JSON file (scripts/data/energy-disruptions.json).\n An automated state-transition classifier was scoped but not shipped —\n the log is curated-only today.\n\n See docs/methodology/disruptions.mdx.","properties":{"assetId":{"description":"Filter to one asset. Omit for all. When set, also narrows to the\n matching asset_type if provided.","type":"string"},"assetType":{"description":"Filter to one asset type. Accepts: \"pipeline\" | \"storage\".","type":"string"},"ongoingOnly":{"description":"If true, only return events with endAt empty (still ongoing).","type":"boolean"}},"type":"object"},"ListEnergyDisruptionsResponse":{"properties":{"classifierVersion":{"type":"string"},"events":{"items":{"$ref":"#/components/schemas/EnergyDisruptionEntry"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListFuelShortagesRequest":{"description":"ListFuelShortages returns the global fuel-shortage alert registry.\n Seeded from a curated JSON file (scripts/data/fuel-shortages.json).\n An LLM classifier extension was scoped but not shipped — the registry\n is curated-only today. Severity (\"confirmed\" or \"watch\") is a row\n field authored at curation time; the evidence is shipped alongside\n so agents and humans can audit the grounds for a severity label.\n\n See docs/methodology/shortages.mdx for the evidence-threshold spec.","properties":{"country":{"description":"Filter to one ISO 3166-1 alpha-2 country. Omit for global.","type":"string"},"product":{"description":"Filter to one product. Accepts: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\".\n Omit for all products.","type":"string"},"severity":{"description":"Filter to one severity. Accepts: \"confirmed\" | \"watch\". Omit for both.","type":"string"}},"type":"object"},"ListFuelShortagesResponse":{"properties":{"classifierVersion":{"type":"string"},"fetchedAt":{"type":"string"},"shortages":{"items":{"$ref":"#/components/schemas/FuelShortageEntry"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListPipelinesRequest":{"description":"ListPipelines returns the full oil and/or gas pipeline registry with\n evidence-based status per asset. Registry is curated (see\n docs/methodology/pipelines.mdx) and refreshed weekly by\n scripts/seed-pipelines-{gas,oil}.mjs. Typical consumer: PipelineStatusPanel\n rendering the DeckGL PathLayer.\n\n The public badge is DERIVED from the evidence bundle server-side at\n read time — callers MUST use `public_badge` for display, not invent\n their own derivation from `evidence` fields. This keeps the atlas and\n MCP clients consistent and versioned together.","properties":{"commodityType":{"description":"Filter to one commodity. Omit (or pass empty) to receive both.","type":"string"}},"type":"object"},"ListPipelinesResponse":{"properties":{"classifierVersion":{"type":"string"},"fetchedAt":{"type":"string"},"pipelines":{"items":{"$ref":"#/components/schemas/PipelineEntry"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"ListStorageFacilitiesRequest":{"description":"ListStorageFacilities returns the curated strategic storage registry\n (underground gas storage, strategic petroleum reserves, LNG terminals,\n crude tank farms) with evidence-based status per asset. Registry is\n curated (see docs/methodology/storage.mdx) and refreshed weekly by\n scripts/seed-storage-facilities.mjs. Typical consumer:\n StorageFacilityMapPanel rendering the DeckGL ScatterplotLayer.\n\n Like pipelines, the public badge is DERIVED from the evidence bundle\n server-side at read time. Callers MUST use `public_badge` for display\n rather than inventing their own derivation from `evidence` fields, so\n the atlas and MCP clients stay consistent and versioned together.","properties":{"facilityType":{"description":"Filter to one facility type. Accepts:\n \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"\n Omit (or pass empty) to receive all types.","type":"string"}},"type":"object"},"ListStorageFacilitiesResponse":{"properties":{"classifierVersion":{"type":"string"},"facilities":{"items":{"$ref":"#/components/schemas/StorageFacilityEntry"},"type":"array"},"fetchedAt":{"type":"string"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"MineralProducer":{"properties":{"country":{"type":"string"},"countryCode":{"type":"string"},"productionTonnes":{"format":"double","type":"number"},"sharePct":{"format":"double","type":"number"}},"type":"object"},"MultiSectorCostShock":{"properties":{"addedTransitDays":{"description":"Bypass-corridor transit penalty (informational).","format":"int32","type":"integer"},"closureDays":{"description":"Echoes the clamped closure duration used for total_cost_shock (1-365).","format":"int32","type":"integer"},"freightAddedPctPerTon":{"description":"Bypass-corridor freight uplift fraction (0.10 == +10% per ton).","format":"double","type":"number"},"hs2":{"description":"HS2 chapter code (e.g. \"27\" mineral fuels, \"85\" electronics).","type":"string"},"hs2Label":{"description":"Friendly chapter label (e.g. \"Energy\", \"Electronics\").","type":"string"},"importValueAnnual":{"description":"Total annual import value (USD) for this sector.","format":"double","type":"number"},"totalCostShock":{"description":"Cost for the requested closure_days window.","format":"double","type":"number"},"totalCostShock30Days":{"format":"double","type":"number"},"totalCostShock90Days":{"format":"double","type":"number"},"totalCostShockPerDay":{"format":"double","type":"number"},"warRiskPremiumBps":{"description":"War-risk insurance premium (basis points) sourced from the chokepoint tier.","format":"int32","type":"integer"}},"type":"object"},"NumberRange":{"description":"Inclusive integer range for transit days / freight USD estimates.","properties":{"max":{"format":"int32","type":"integer"},"min":{"format":"int32","type":"integer"}},"type":"object"},"OperatorStatement":{"properties":{"date":{"type":"string"},"text":{"type":"string"},"url":{"type":"string"}},"type":"object"},"PipelineEntry":{"properties":{"capacityBcmYr":{"format":"double","type":"number"},"capacityMbd":{"format":"double","type":"number"},"commodityType":{"description":"Either \"gas\" or \"oil\".","type":"string"},"endPoint":{"$ref":"#/components/schemas/LatLon"},"evidence":{"$ref":"#/components/schemas/PipelineEvidence"},"fromCountry":{"type":"string"},"id":{"type":"string"},"inService":{"format":"int32","type":"integer"},"lengthKm":{"format":"int32","type":"integer"},"name":{"type":"string"},"operator":{"type":"string"},"publicBadge":{"description":"Server-derived public badge. One of:\n \"flowing\" | \"reduced\" | \"offline\" | \"disputed\"","type":"string"},"startPoint":{"$ref":"#/components/schemas/LatLon"},"toCountry":{"type":"string"},"transitCountries":{"items":{"type":"string"},"type":"array"},"waypoints":{"items":{"$ref":"#/components/schemas/LatLon"},"type":"array"}},"type":"object"},"PipelineEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"commercialState":{"description":"One of: \"under_contract\" | \"expired\" | \"suspended\" | \"unknown\"","type":"string"},"lastEvidenceUpdate":{"type":"string"},"operatorStatement":{"$ref":"#/components/schemas/OperatorStatement"},"physicalState":{"description":"One of: \"flowing\" | \"reduced\" | \"offline\" | \"unknown\"","type":"string"},"physicalStateSource":{"description":"One of: \"operator\" | \"regulator\" | \"press\" | \"satellite\" | \"ais-relay\"","type":"string"},"sanctionRefs":{"items":{"$ref":"#/components/schemas/SanctionRef"},"type":"array"}},"type":"object"},"PipelineRevisionEntry":{"properties":{"classifierVersion":{"type":"string"},"date":{"type":"string"},"fieldChanged":{"type":"string"},"newValue":{"type":"string"},"previousValue":{"type":"string"},"sourcesUsed":{"items":{"type":"string"},"type":"array"},"trigger":{"description":"One of: \"classifier\" | \"source\" | \"decay\" | \"override\"","type":"string"}},"type":"object"},"ProductExporter":{"properties":{"partnerCode":{"format":"int32","type":"integer"},"partnerIso2":{"type":"string"},"share":{"format":"double","type":"number"},"value":{"format":"double","type":"number"}},"type":"object"},"SanctionRef":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"listId":{"type":"string"},"url":{"type":"string"}},"type":"object"},"ShippingIndex":{"properties":{"changePct":{"format":"double","type":"number"},"currentValue":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/ShippingRatePoint"},"type":"array"},"indexId":{"type":"string"},"name":{"type":"string"},"previousValue":{"format":"double","type":"number"},"spikeAlert":{"type":"boolean"},"unit":{"type":"string"}},"type":"object"},"ShippingRatePoint":{"properties":{"date":{"type":"string"},"value":{"format":"double","type":"number"}},"type":"object"},"ShippingStressCarrier":{"description":"ShippingStressCarrier represents market stress data for a carrier or shipping index.","properties":{"carrierType":{"description":"Carrier type: \"etf\" | \"carrier\" | \"index\".","type":"string"},"changePct":{"description":"Percentage change from previous close.","format":"double","type":"number"},"name":{"description":"Human-readable name.","type":"string"},"price":{"description":"Current price.","format":"double","type":"number"},"sparkline":{"items":{"description":"30-day price sparkline.","format":"double","type":"number"},"type":"array"},"symbol":{"description":"Ticker or identifier (e.g., \"BDRY\", \"ZIM\").","type":"string"}},"type":"object"},"StorageEvidence":{"properties":{"classifierConfidence":{"format":"double","type":"number"},"classifierVersion":{"type":"string"},"commercialState":{"description":"One of: \"under_contract\" | \"expired\" | \"suspended\" | \"unknown\"","type":"string"},"fillDisclosed":{"description":"Whether working-gas / stock-level fill is publicly disclosed for this\n asset. LNG export terminals, for instance, tend NOT to disclose; UGS\n sites in Europe are required to disclose via GIE AGSI+.","type":"boolean"},"fillSource":{"description":"Source of the disclosed fill series (\"GIE AGSI+\", \"EIA SPR weekly\n stock report\", etc.). Empty when fill_disclosed=false.","type":"string"},"lastEvidenceUpdate":{"type":"string"},"operatorStatement":{"$ref":"#/components/schemas/StorageOperatorStatement"},"physicalState":{"description":"One of: \"operational\" | \"reduced\" | \"offline\" | \"under_construction\" | \"unknown\"","type":"string"},"physicalStateSource":{"description":"One of: \"operator\" | \"regulator\" | \"press\" | \"satellite\" | \"ais-relay\"","type":"string"},"sanctionRefs":{"items":{"$ref":"#/components/schemas/StorageSanctionRef"},"type":"array"}},"type":"object"},"StorageFacilityEntry":{"properties":{"capacityMb":{"format":"double","type":"number"},"capacityMtpa":{"format":"double","type":"number"},"capacityTwh":{"description":"Working capacity in the facility's native unit (see working_capacity_unit).\n Exactly ONE of these is populated per facility, chosen by facility_type:\n ugs → capacity_twh\n spr, crude_tank_farm → capacity_mb\n lng_export, lng_import → capacity_mtpa","format":"double","type":"number"},"country":{"type":"string"},"evidence":{"$ref":"#/components/schemas/StorageEvidence"},"facilityType":{"description":"One of: \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"","type":"string"},"id":{"type":"string"},"inService":{"format":"int32","type":"integer"},"location":{"$ref":"#/components/schemas/StorageLatLon"},"name":{"type":"string"},"operator":{"type":"string"},"publicBadge":{"description":"Server-derived public badge. One of:\n \"operational\" | \"reduced\" | \"offline\" | \"disputed\"","type":"string"},"workingCapacityUnit":{"description":"One of: \"TWh\" | \"Mb\" | \"Mtpa\"","type":"string"}},"type":"object"},"StorageFacilityRevisionEntry":{"properties":{"classifierVersion":{"type":"string"},"date":{"type":"string"},"fieldChanged":{"type":"string"},"newValue":{"type":"string"},"previousValue":{"type":"string"},"sourcesUsed":{"items":{"type":"string"},"type":"array"},"trigger":{"description":"One of: \"classifier\" | \"source\" | \"decay\" | \"override\"","type":"string"}},"type":"object"},"StorageLatLon":{"properties":{"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"}},"type":"object"},"StorageOperatorStatement":{"properties":{"date":{"type":"string"},"text":{"type":"string"},"url":{"type":"string"}},"type":"object"},"StorageSanctionRef":{"properties":{"authority":{"type":"string"},"date":{"type":"string"},"listId":{"type":"string"},"url":{"type":"string"}},"type":"object"},"StrategicProduct":{"properties":{"hs4":{"type":"string"},"label":{"type":"string"},"primaryChokepointId":{"type":"string"},"topExporterIso2":{"type":"string"},"topExporterShare":{"format":"double","type":"number"},"totalValueUsd":{"format":"double","type":"number"}},"type":"object"},"TransitDayCount":{"properties":{"capContainer":{"format":"double","type":"number"},"capDryBulk":{"format":"double","type":"number"},"capGeneralCargo":{"format":"double","type":"number"},"capRoro":{"format":"double","type":"number"},"capTanker":{"format":"double","type":"number"},"cargo":{"format":"int32","type":"integer"},"container":{"format":"int32","type":"integer"},"date":{"type":"string"},"dryBulk":{"format":"int32","type":"integer"},"generalCargo":{"format":"int32","type":"integer"},"other":{"format":"int32","type":"integer"},"roro":{"format":"int32","type":"integer"},"tanker":{"format":"int32","type":"integer"},"total":{"format":"int32","type":"integer"}},"type":"object"},"TransitSummary":{"properties":{"dataAvailable":{"description":"False when the upstream portwatch/relay source did not return data for\n this chokepoint in the current cycle — the summary fields are zero-state\n fill, not a genuine \"zero traffic\" reading. Client should render a\n \"transit data unavailable\" indicator and skip stat/chart rendering.","type":"boolean"},"disruptionPct":{"format":"double","type":"number"},"history":{"items":{"$ref":"#/components/schemas/TransitDayCount"},"type":"array"},"incidentCount7d":{"format":"int32","type":"integer"},"riskLevel":{"type":"string"},"riskReportAction":{"type":"string"},"riskSummary":{"type":"string"},"todayCargo":{"format":"int32","type":"integer"},"todayOther":{"format":"int32","type":"integer"},"todayTanker":{"format":"int32","type":"integer"},"todayTotal":{"format":"int32","type":"integer"},"wowChangePct":{"format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"SupplyChainService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/supply-chain/v1/get-bypass-options":{"get":{"description":"GetBypassOptions returns ranked bypass corridors for a chokepoint. PRO-gated.","operationId":"GetBypassOptions","parameters":[{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"container | tanker | bulk | roro (default: \"container\")","in":"query","name":"cargoType","required":false,"schema":{"type":"string"}},{"description":"0-100, percent of capacity blocked (default: 100)","in":"query","name":"closurePct","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetBypassOptionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetBypassOptions","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-chokepoint-history":{"get":{"description":"GetChokepointHistory returns transit-count history for a single chokepoint,\n loaded lazily on card expand. Keeps the status RPC compact (no 180-day\n history per chokepoint on every call).","operationId":"GetChokepointHistory","parameters":[{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetChokepointHistoryResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetChokepointHistory","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-chokepoint-status":{"get":{"operationId":"GetChokepointStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetChokepointStatusResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetChokepointStatus","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-chokepoint-index":{"get":{"description":"GetCountryChokepointIndex returns per-chokepoint exposure scores for a country. PRO-gated.","operationId":"GetCountryChokepointIndex","parameters":[{"description":"ISO 3166-1 alpha-2 country code (uppercase).","in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter (2-digit string). Defaults to \"27\" (energy/mineral fuels) when absent.","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryChokepointIndexResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryChokepointIndex","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-cost-shock":{"get":{"description":"GetCountryCostShock returns cost shock and war risk data for a country+chokepoint. PRO-gated.","operationId":"GetCountryCostShock","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter (default: \"27\")","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryCostShockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryCostShock","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-country-products":{"get":{"description":"GetCountryProducts returns the seeded bilateral-HS4 import basket for a country. PRO-gated.","operationId":"GetCountryProducts","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCountryProductsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCountryProducts","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-critical-minerals":{"get":{"operationId":"GetCriticalMinerals","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCriticalMineralsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCriticalMinerals","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-fuel-shortage-detail":{"get":{"description":"GetFuelShortageDetail returns a single shortage with full evidence\n bundle and citation timeline. Loaded lazily on drawer open.","operationId":"GetFuelShortageDetail","parameters":[{"in":"query","name":"shortageId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetFuelShortageDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetFuelShortageDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-multi-sector-cost-shock":{"get":{"description":"GetMultiSectorCostShock returns per-sector cost-shock estimates for a\n country+chokepoint+closure-window. PRO-gated.","operationId":"GetMultiSectorCostShock","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"chokepointId","required":false,"schema":{"type":"string"}},{"description":"Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.","in":"query","name":"closureDays","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetMultiSectorCostShockResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetMultiSectorCostShock","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-pipeline-detail":{"get":{"description":"GetPipelineDetail returns a single pipeline with full evidence bundle\n + auto-revision-log entries. Loaded lazily on drawer open.","operationId":"GetPipelineDetail","parameters":[{"in":"query","name":"pipelineId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetPipelineDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetPipelineDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-route-explorer-lane":{"get":{"description":"GetRouteExplorerLane returns the primary maritime route, chokepoint exposures,\n bypass options with geometry, war risk, and static transit/freight estimates for\n a country pair + HS2 + cargo type. PRO-gated. Wraps the route-intelligence vendor\n endpoint's compute with browser-callable auth and adds fields needed by the\n Route Explorer UI.","operationId":"GetRouteExplorerLane","parameters":[{"in":"query","name":"fromIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"toIso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter code, e.g. \"27\", \"85\"","in":"query","name":"hs2","required":false,"schema":{"type":"string"}},{"description":"One of: container, tanker, bulk, roro","in":"query","name":"cargoType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetRouteExplorerLaneResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetRouteExplorerLane","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-route-impact":{"get":{"operationId":"GetRouteImpact","parameters":[{"in":"query","name":"fromIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"toIso2","required":false,"schema":{"type":"string"}},{"in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetRouteImpactResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetRouteImpact","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-sector-dependency":{"get":{"description":"GetSectorDependency returns dependency flags and risk profile for a country+HS2 sector. PRO-gated.","operationId":"GetSectorDependency","parameters":[{"in":"query","name":"iso2","required":false,"schema":{"type":"string"}},{"description":"HS2 chapter code, e.g. \"27\" (mineral fuels), \"85\" (electronics)","in":"query","name":"hs2","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSectorDependencyResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetSectorDependency","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-shipping-rates":{"get":{"operationId":"GetShippingRates","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetShippingRatesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetShippingRates","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-shipping-stress":{"get":{"description":"GetShippingStress returns carrier market data and a composite stress index.","operationId":"GetShippingStress","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetShippingStressResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetShippingStress","tags":["SupplyChainService"]}},"/api/supply-chain/v1/get-storage-facility-detail":{"get":{"description":"GetStorageFacilityDetail returns a single facility with full evidence\n bundle + revision log. Loaded lazily on drawer open.","operationId":"GetStorageFacilityDetail","parameters":[{"in":"query","name":"facilityId","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetStorageFacilityDetailResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetStorageFacilityDetail","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-energy-disruptions":{"get":{"description":"ListEnergyDisruptions returns the disruption event log for pipelines\n and storage facilities. Supports per-asset or per-asset-type filtering\n so panel drawers can fetch a scoped timeline without pulling the\n full registry.","operationId":"ListEnergyDisruptions","parameters":[{"description":"Filter to one asset. Omit for all. When set, also narrows to the\n matching asset_type if provided.","in":"query","name":"assetId","required":false,"schema":{"type":"string"}},{"description":"Filter to one asset type. Accepts: \"pipeline\" | \"storage\".","in":"query","name":"assetType","required":false,"schema":{"type":"string"}},{"description":"If true, only return events with endAt empty (still ongoing).","in":"query","name":"ongoingOnly","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEnergyDisruptionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListEnergyDisruptions","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-fuel-shortages":{"get":{"description":"ListFuelShortages returns the global fuel-shortage alert registry.\n Curated-only: severity (\"confirmed\" | \"watch\") is a row field authored\n at curation time, not a client-side derivation. Free tier.","operationId":"ListFuelShortages","parameters":[{"description":"Filter to one ISO 3166-1 alpha-2 country. Omit for global.","in":"query","name":"country","required":false,"schema":{"type":"string"}},{"description":"Filter to one product. Accepts: \"petrol\" | \"diesel\" | \"jet\" | \"heating_oil\".\n Omit for all products.","in":"query","name":"product","required":false,"schema":{"type":"string"}},{"description":"Filter to one severity. Accepts: \"confirmed\" | \"watch\". Omit for both.","in":"query","name":"severity","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFuelShortagesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListFuelShortages","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-pipelines":{"get":{"description":"ListPipelines returns the curated oil \u0026 gas pipeline registry for the\n Energy Atlas PathLayer. Public badges are DERIVED from evidence bundles\n server-side and versioned (classifier_version). Free-tier; see\n docs/methodology/pipelines.mdx for data + classifier spec.","operationId":"ListPipelines","parameters":[{"description":"Filter to one commodity. Omit (or pass empty) to receive both.","in":"query","name":"commodityType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListPipelinesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListPipelines","tags":["SupplyChainService"]}},"/api/supply-chain/v1/list-storage-facilities":{"get":{"description":"ListStorageFacilities returns the curated strategic storage registry\n (UGS + SPR + LNG + crude tank farms) for the Energy Atlas DeckGL\n ScatterplotLayer. Public badges are DERIVED from evidence bundles\n server-side and versioned (classifier_version). Free-tier; see\n docs/methodology/storage.mdx.","operationId":"ListStorageFacilities","parameters":[{"description":"Filter to one facility type. Accepts:\n \"ugs\" | \"spr\" | \"lng_export\" | \"lng_import\" | \"crude_tank_farm\"\n Omit (or pass empty) to receive all types.","in":"query","name":"facilityType","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStorageFacilitiesResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListStorageFacilities","tags":["SupplyChainService"]}}}} \ No newline at end of file diff --git a/docs/api/SupplyChainService.openapi.yaml b/docs/api/SupplyChainService.openapi.yaml index 79aa5b844..6b4bbcb39 100644 --- a/docs/api/SupplyChainService.openapi.yaml +++ b/docs/api/SupplyChainService.openapi.yaml @@ -2319,6 +2319,16 @@ components: format: double lastEvidenceUpdate: type: string + countries: + type: array + items: + type: string + description: |- + Countries touched by the referenced asset (pipeline: fromCountry + + toCountry + transitCountries; storage: country). Denormalised at seed + time so CountryDeepDivePanel can filter events without a second RPC. + Always non-empty in well-formed payloads; empty only if the upstream + asset was removed without an event update. Per plan §R/#5 decision B. EnergyDisruptionSource: type: object properties: diff --git a/docs/api/worldmonitor.openapi.yaml b/docs/api/worldmonitor.openapi.yaml index a99dd4bec..85cbbeb8b 100644 --- a/docs/api/worldmonitor.openapi.yaml +++ b/docs/api/worldmonitor.openapi.yaml @@ -20518,6 +20518,16 @@ components: format: double lastEvidenceUpdate: type: string + countries: + type: array + items: + type: string + description: |- + Countries touched by the referenced asset (pipeline: fromCountry + + toCountry + transitCountries; storage: country). Denormalised at seed + time so CountryDeepDivePanel can filter events without a second RPC. + Always non-empty in well-formed payloads; empty only if the upstream + asset was removed without an event update. Per plan §R/#5 decision B. worldmonitor_supply_chain_v1_EnergyDisruptionSource: type: object properties: diff --git a/proto/worldmonitor/supply_chain/v1/list_energy_disruptions.proto b/proto/worldmonitor/supply_chain/v1/list_energy_disruptions.proto index ac8c61f85..3a5650c0f 100644 --- a/proto/worldmonitor/supply_chain/v1/list_energy_disruptions.proto +++ b/proto/worldmonitor/supply_chain/v1/list_energy_disruptions.proto @@ -54,6 +54,12 @@ message EnergyDisruptionEntry { string classifier_version = 12; double classifier_confidence = 13; // 0..1 string last_evidence_update = 14; // ISO8601 + // Countries touched by the referenced asset (pipeline: fromCountry + + // toCountry + transitCountries; storage: country). Denormalised at seed + // time so CountryDeepDivePanel can filter events without a second RPC. + // Always non-empty in well-formed payloads; empty only if the upstream + // asset was removed without an event update. Per plan §R/#5 decision B. + repeated string countries = 15; } message EnergyDisruptionSource { diff --git a/scripts/_energy-disruption-registry.mjs b/scripts/_energy-disruption-registry.mjs index bf1038402..a7de912dc 100644 --- a/scripts/_energy-disruption-registry.mjs +++ b/scripts/_energy-disruption-registry.mjs @@ -38,6 +38,83 @@ function loadRegistry() { return JSON.parse(raw); } +/** + * Load the pipeline + storage registries so `buildPayload` can join each + * disruption event to its referenced asset and compute the `countries[]` + * denorm field (plan §R/#5 decision B). + * + * Pipelines contribute fromCountry, toCountry, and transitCountries[]. + * Storage facilities contribute their single country code. Duplicates are + * deduped and sorted so the seed output is stable across runs — unstable + * ordering would churn the seeded payload bytes on every cron tick and + * defeat envelope diffing. + * + * @returns {{ + * pipelines: Record, + * storage: Record, + * }} + */ +function loadAssetRegistries() { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const gas = JSON.parse(readFileSync(resolve(__dirname, 'data', 'pipelines-gas.json'), 'utf-8')); + const oil = JSON.parse(readFileSync(resolve(__dirname, 'data', 'pipelines-oil.json'), 'utf-8')); + const storageRaw = JSON.parse(readFileSync(resolve(__dirname, 'data', 'storage-facilities.json'), 'utf-8')); + + // Merge with explicit collision detection. A spread like + // { ...gas.pipelines, ...oil.pipelines } would silently let an oil + // entry overwrite a gas entry if a curator ever added a pipeline + // under the same id to both files — `deriveCountriesForEvent` would + // then return data for whichever side won the spread regardless of + // which commodity the disruption actually references, and the + // collision would surface as mysterious wrong-country filter + // results with no test or validator flagging it. Codex P2 on + // PR #3377. Throw loudly so the next cron tick fails validation + // and health alarms fire. + /** @type {Record} */ + const pipelines = {}; + for (const [id, p] of Object.entries(gas.pipelines ?? {})) pipelines[id] = p; + for (const [id, p] of Object.entries(oil.pipelines ?? {})) { + if (pipelines[id]) { + throw new Error( + `Duplicate pipeline id "${id}" present in both pipelines-gas.json ` + + `and pipelines-oil.json — an event referencing this id would resolve ` + + `ambiguously. Rename one of them before re-running the seeder.`, + ); + } + pipelines[id] = p; + } + + return { pipelines, storage: storageRaw.facilities ?? {} }; +} + +/** + * Compute the denormalised country set for a single event. + * + * @param {{ assetId: string; assetType: string }} event + * @param {ReturnType} registries + * @returns {string[]} ISO2 codes, deduped + alpha-sorted. Empty array when + * the referenced asset cannot be resolved — callers (seeder) should + * treat empty as a hard validation failure so stale references surface + * loudly on the next cron tick rather than silently corrupt the filter. + */ +function deriveCountriesForEvent(event, registries) { + const out = new Set(); + if (event.assetType === 'pipeline') { + const p = registries.pipelines[event.assetId]; + if (p) { + if (typeof p.fromCountry === 'string') out.add(p.fromCountry); + if (typeof p.toCountry === 'string') out.add(p.toCountry); + if (Array.isArray(p.transitCountries)) { + for (const c of p.transitCountries) if (typeof c === 'string') out.add(c); + } + } + } else if (event.assetType === 'storage') { + const s = registries.storage[event.assetId]; + if (s && typeof s.country === 'string') out.add(s.country); + } + return Array.from(out).sort(); +} + /** * @param {unknown} data * @returns {boolean} @@ -83,6 +160,16 @@ export function validateRegistry(data) { const end = Date.parse(e.endAt); if (end < start) return false; } + // countries[] is the denorm introduced in plan §R/#5 (decision B). Every + // event must resolve to ≥1 country code from its referenced asset. An + // empty array here means the upstream asset was removed or the assetId + // is misspelled — both are hard errors the cron should surface by + // failing validation (emptyDataIsFailure upstream preserves seed-meta + // staleness so health alarms fire). + if (!Array.isArray(e.countries) || e.countries.length === 0) return false; + for (const c of e.countries) { + if (typeof c !== 'string' || !/^[A-Z]{2}$/.test(c)) return false; + } } return true; } @@ -94,7 +181,22 @@ function isIsoDate(v) { export function buildPayload() { const registry = loadRegistry(); - return { ...registry, updatedAt: new Date().toISOString() }; + const assets = loadAssetRegistries(); + + // Denormalise countries[] on every event so CountryDeepDivePanel can + // filter by country without an asset-registry round trip. If an event's + // assetId cannot be resolved we leave countries[] empty — validateRegistry + // rejects that shape, which fails the seed (emptyDataIsFailure: true) + // and keeps seed-meta stale until the curator fixes the orphaned id. + const rawEvents = /** @type {Record} */ (registry.events ?? {}); + const events = Object.fromEntries( + Object.entries(rawEvents).map(([id, event]) => [ + id, + { ...event, countries: deriveCountriesForEvent(event, assets) }, + ]), + ); + + return { ...registry, events, updatedAt: new Date().toISOString() }; } /** diff --git a/scripts/data/energy-disruptions.json b/scripts/data/energy-disruptions.json index e1550dbe4..3dac49f62 100644 --- a/scripts/data/energy-disruptions.json +++ b/scripts/data/energy-disruptions.json @@ -63,7 +63,7 @@ }, "cpc-force-majeure-2022": { "id": "cpc-force-majeure-2022", - "assetId": "cpc-pipeline", + "assetId": "cpc", "assetType": "pipeline", "eventType": "mechanical", "startAt": "2022-03-22T00:00:00Z", @@ -275,7 +275,7 @@ "classifierVersion": "v1", "classifierConfidence": 0.96, "lastEvidenceUpdate": "2026-04-22T00:00:00Z" }, "pdvsa-designation-2019": { - "id": "pdvsa-designation-2019", "assetId": "ve-petrol-2026-q1", "assetType": "pipeline", + "id": "pdvsa-designation-2019", "assetId": "venezuela-anzoategui-puerto-la-cruz", "assetType": "pipeline", "eventType": "sanction", "startAt": "2019-01-28T00:00:00Z", "endAt": null, "capacityOfflineBcmYr": 0, "capacityOfflineMbd": 0.5, "causeChain": ["sanction"], diff --git a/server/worldmonitor/supply-chain/v1/list-energy-disruptions.ts b/server/worldmonitor/supply-chain/v1/list-energy-disruptions.ts index ae3509943..38a80734e 100644 --- a/server/worldmonitor/supply-chain/v1/list-energy-disruptions.ts +++ b/server/worldmonitor/supply-chain/v1/list-energy-disruptions.ts @@ -59,6 +59,13 @@ export function projectDisruption(raw: unknown): EnergyDisruptionEntry | null { classifierVersion: coerceString(r.classifierVersion, 'v1'), classifierConfidence: coerceNumber(r.classifierConfidence), lastEvidenceUpdate: coerceString(r.lastEvidenceUpdate), + // Seed-denormalised countries[] (plan §R/#5 decision B). The registry + // seeder joins each event's assetId against the pipeline/storage + // registries and emits the touched ISO2 set. Legacy rows written + // before the denorm shipped can still exist in Redis transiently; we + // surface an empty array there so the field is always present on the + // wire but consumers can detect pre-denorm data by checking length. + countries: coerceStringArray(r.countries), }; } diff --git a/src/components/CountryDeepDivePanel.ts b/src/components/CountryDeepDivePanel.ts index f8e8e2144..639403891 100644 --- a/src/components/CountryDeepDivePanel.ts +++ b/src/components/CountryDeepDivePanel.ts @@ -74,6 +74,19 @@ const SEVERITY_ORDER: Record = { info: 0, }; +// Clamp long disruption shortDescriptions when rendered in the compact +// CountryDeepDive Atlas row. Some registry entries (OFAC designations, +// multi-clause sanctions summaries) run 100–200 chars; without a clamp +// they overflow the row. 80 chars is a balance between scannability and +// information density; full detail stays accessible by clicking through +// to the asset drawer. +const DISRUPTION_LABEL_MAX_LEN = 80; +function truncateDisruptionLabel(eventType: string, shortDescription: string): string { + const base = `${eventType} — ${shortDescription}`; + if (base.length <= DISRUPTION_LABEL_MAX_LEN) return base; + return base.slice(0, DISRUPTION_LABEL_MAX_LEN - 1) + '…'; +} + export class CountryDeepDivePanel implements CountryBriefPanel { private panel: HTMLElement; private content: HTMLElement; @@ -1255,6 +1268,80 @@ export class CountryDeepDivePanel implements CountryBriefPanel { ); } }).catch(() => {}); + + // Disruptions filter (plan §R/#5 decision B). The seeded registry carries + // denormalised `countries[]` on every event, populated from the referenced + // pipeline or storage facility. We fetch the full list once (no asset + // filter) and narrow client-side; the bootstrap payload already contains + // the registry so this is usually cache-hot. If the RPC round-trip returns + // nothing, we silently skip — CountryDeepDive is not the primary + // disruption surface (EnergyDisruptionsPanel is), so an empty row is + // preferable to a spurious error. + this.loadDisruptionsForCountry(iso2); + } + + private async loadDisruptionsForCountry(iso2: string): Promise { + try { + const { SupplyChainServiceClient } = await import( + '@/generated/client/worldmonitor/supply_chain/v1/service_client' + ); + const { getRpcBaseUrl } = await import('@/services/rpc-client'); + // Thread the panel's `signal` into the fetch shim so a country + // switch or panel close cancels the in-flight request, not just + // discards the result via the `this.currentCode !== iso2` guard + // below. Codex P2 on PR #3377. + const abortSignal = this.signal; + const client = new SupplyChainServiceClient(getRpcBaseUrl(), { + fetch: (input, init) => globalThis.fetch(input, { ...(init ?? {}), signal: abortSignal }), + }); + const res = await client.listEnergyDisruptions({ + assetId: '', + assetType: '', + ongoingOnly: false, + }); + if (!res || !Array.isArray(res.events) || this.currentCode !== iso2) return; + const events = res.events.filter(e => + Array.isArray(e.countries) && e.countries.includes(iso2), + ); + if (events.length === 0) return; + const ongoing = events.filter(e => !e.endAt).length; + const summary = ongoing > 0 + ? `${ongoing} ongoing · ${events.length - ongoing} resolved` + : `${events.length} resolved`; + this.appendAtlasRow( + `Energy disruptions in ${iso2}`, + summary, + events.map(e => ({ + id: e.id, + // Clamp long descriptions (some registry entries run 100-200 + // chars, e.g. OFAC designation paragraphs) so the row layout + // stays compact. 80-char limit + ellipsis. Codex P2 on PR #3377. + label: truncateDisruptionLabel(e.eventType, e.shortDescription), + // Event type mirrors the existing asset-detail events (pipeline / + // storage) because disruptions reference the underlying asset; the + // panel-layout listener routes to the matching asset panel. + event: e.assetType === 'storage' + ? 'energy:open-storage-facility-detail' + : 'energy:open-pipeline-detail', + // Emit ONLY the {pipelineId, facilityId} the drawers consume today + // (see PipelineStatusPanel + StorageFacilityMapPanel + // openDetailHandler). Previously this detail included a + // `highlightEventId` that no receiver read — Codex P2 flagged the + // misleading API surface. Clicking a row jumps to the asset + // drawer; the user sees the full per-asset timeline and locates + // the event visually. Re-add `highlightEventId` here and in + // EnergyDisruptionsPanel's dispatchOpenAsset only when the + // drawer panels ship matching consumer code. + detail: e.assetType === 'storage' + ? { facilityId: e.assetId } + : { pipelineId: e.assetId }, + })), + ); + } catch { + // Silent — disruptions row is supplementary; failures elsewhere + // surface via the dedicated EnergyDisruptionsPanel. Abort errors + // from signal cancellation are also swallowed here intentionally. + } } private appendAtlasRow( diff --git a/src/generated/client/worldmonitor/supply_chain/v1/service_client.ts b/src/generated/client/worldmonitor/supply_chain/v1/service_client.ts index 81bcd350f..5df2b0821 100644 --- a/src/generated/client/worldmonitor/supply_chain/v1/service_client.ts +++ b/src/generated/client/worldmonitor/supply_chain/v1/service_client.ts @@ -621,6 +621,7 @@ export interface EnergyDisruptionEntry { classifierVersion: string; classifierConfidence: number; lastEvidenceUpdate: string; + countries: string[]; } export interface EnergyDisruptionSource { diff --git a/src/generated/server/worldmonitor/supply_chain/v1/service_server.ts b/src/generated/server/worldmonitor/supply_chain/v1/service_server.ts index f0ae00db7..52f5fb6f1 100644 --- a/src/generated/server/worldmonitor/supply_chain/v1/service_server.ts +++ b/src/generated/server/worldmonitor/supply_chain/v1/service_server.ts @@ -621,6 +621,7 @@ export interface EnergyDisruptionEntry { classifierVersion: string; classifierConfidence: number; lastEvidenceUpdate: string; + countries: string[]; } export interface EnergyDisruptionSource { diff --git a/tests/energy-disruptions-registry.test.mts b/tests/energy-disruptions-registry.test.mts index 7fa3a190d..1c2fa8a72 100644 --- a/tests/energy-disruptions-registry.test.mts +++ b/tests/energy-disruptions-registry.test.mts @@ -7,13 +7,20 @@ import { fileURLToPath } from 'node:url'; import { validateRegistry, recordCount, + buildPayload, ENERGY_DISRUPTIONS_CANONICAL_KEY, MAX_STALE_MIN, } from '../scripts/_energy-disruption-registry.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const raw = readFileSync(resolve(__dirname, '../scripts/data/energy-disruptions.json'), 'utf-8'); -const registry = JSON.parse(raw) as { events: Record }; +const rawRegistry = JSON.parse(raw) as { events: Record }; +// validateRegistry checks the buildPayload output (the denormalised shape +// the seeder actually writes to Redis), not the raw JSON on disk. Since +// plan §R/#5 decision B, buildPayload attaches countries[] per event; the +// raw file intentionally omits that field so a curator can edit events +// without manually computing affected countries. +const registry = buildPayload() as { events: Record }; describe('energy-disruptions registry — schema', () => { test('registry passes validateRegistry', () => { @@ -94,6 +101,37 @@ describe('energy-disruptions registry — evidence', () => { }); }); +describe('energy-disruptions registry — countries[] denorm (§R #5 B)', () => { + test('every event in buildPayload output has non-empty countries[]', () => { + for (const e of Object.values(registry.events)) { + assert.ok( + Array.isArray(e.countries) && e.countries.length > 0, + `${e.id}: empty countries[] — assetId may be orphaned`, + ); + } + }); + + test('every country code is ISO-3166-1 alpha-2 uppercase', () => { + for (const e of Object.values(registry.events)) { + for (const c of e.countries) { + assert.ok(/^[A-Z]{2}$/.test(c), `${e.id}: bad country code ${c}`); + } + } + }); + + test('raw JSON on disk does NOT carry countries[] (source of truth is the join)', () => { + for (const e of Object.values(rawRegistry.events)) { + assert.equal(e.countries, undefined, `${e.id}: raw JSON should not pre-compute countries[]`); + } + }); + + test('nord-stream-1-sabotage-2022 resolves to [DE, RU]', () => { + const nord = registry.events['nord-stream-1-sabotage-2022']; + assert.ok(nord, 'nord-stream-1-sabotage-2022 missing from registry'); + assert.deepEqual(nord.countries, ['DE', 'RU']); + }); +}); + describe('energy-disruptions registry — validateRegistry rejects bad input', () => { test('rejects empty object', () => { assert.equal(validateRegistry({}), false);