Files
worldmonitor/docs/api/SupplyChainService.openapi.yaml
Elie Habib 84ee2beb3e feat(energy): Energy Atlas end-to-end — pipelines + storage + shortages + disruptions + country drill-down (#3294)
* feat(energy): pipeline registries (gas + oil) — evidence-based schema

Day 6 of the Energy Atlas Release 1 plan (Week 2). First curated asset
registry for the atlas — the real gap vs GEF.

## Curated data (critical assets only, not global completeness)

scripts/data/pipelines-gas.json — 12 critical gas lines:
  Nord Stream 1/2 (offline; Swedish EEZ sabotage 2022; EU sanctions refs),
  TurkStream, Yamal–Europe (offline; Polish counter-sanctions),
  Brotherhood/Soyuz (offline; Ukraine transit expired 2024-12-31),
  Power of Siberia, Dolphin, Medgaz, TAP, TANAP,
  Central Asia–China, Langeled.

scripts/data/pipelines-oil.json — 12 critical oil lines:
  Druzhba North/South (N offline per EU 2022/879; S under landlocked
  derogation), CPC, ESPO (+ price-cap sanction ref), BTC, TAPS,
  Habshan–Fujairah (Hormuz bypass), Keystone, Kirkuk–Ceyhan (offline
  since 2023 ICC ruling), Baku–Supsa, Trans-Mountain (TMX expansion
  May 2024), ESPO spur to Daqing.

Scope note: 75+ each is Week 2b work via GEM bulk import. Today's cut
is curated from first-hand operator disclosures + regulator filings so
I can stand behind every evidence field.

## Evidence-based schema (not conclusion labels)

Per docs/methodology/pipelines.mdx: no bare `sanctions_blocked` field.
Every pipeline carries an evidence bundle with `physicalState`,
`physicalStateSource`, `operatorStatement`, `commercialState`,
`sanctionRefs[]`, `lastEvidenceUpdate`, `classifierVersion`,
`classifierConfidence`. The public badge (`flowing|reduced|offline|
disputed`) is derived server-side from this bundle at read time.

## Seeder

scripts/seed-pipelines.mjs — single process publishes BOTH keys
(energy:pipelines:{gas,oil}:v1) via two runSeed() calls. Tiny datasets
(<20KB each) so co-location is cheap and guarantees classifierVersion
consistency.

Conventions followed (worldmonitor-bootstrap-registration skill):
- TTL 21d = 3× weekly cadence (gold-standard per
  feedback_seeder_gold_standard.md)
- maxStaleMin 20_160 = 2× cadence (health-maxstalemin-write-cadence skill)
- sourceVersion + schemaVersion + recordCount + declareRecords wired
  (seed-contract-foundation)
- Zero-case explicitly NOT allowed — MIN_PIPELINES_PER_REGISTRY=8 floor

## Health registration (dual, per feedback_two_health_endpoints_must_match)

- api/health.js: BOOTSTRAP_KEYS adds pipelinesGas + pipelinesOil;
  SEED_META adds both with maxStaleMin=20_160.
- api/seed-health.js: mirror entries with intervalMin=10_080 (maxStaleMin/2).

## Bundle registration

scripts/seed-bundle-energy-sources.mjs adds a single Pipelines entry
(not two) because seed-pipelines.mjs publishes both keys in one run —
listing oil separately would double-execute. Monitoring of the oil key
staleness happens in api/health.js instead.

## Tests (tests/pipelines-registry.test.mts)

17 passing node:test assertions covering:
- Schema validation (both registries pass validateRegistry)
- Identity resolution (no id collisions, id matches object key)
- Country ISO2 normalization (from/to/transit all match /^[A-Z]{2}$/)
- Endpoint geometry within Earth bounds
- Evidence rigor: non-flowing badges require at least one supporting
  evidence source (operator statement / sanctionRefs / ais-relay /
  satellite / press)
- ClassifierConfidence in 0..1
- Commodity/capacity pairing (gas uses capacityBcmYr, oil uses
  capacityMbd — mixing = test fail)
- validateRegistry rejects: empty object, null, no-evidence fixtures,
  below-floor counts

Typecheck clean (both tsconfig.json and tsconfig.api.json).

Next: Day 7 will add list-pipelines / get-pipeline-detail RPCs in
supply-chain/v1. Day 8 ships PipelineStatusPanel with DeckGL PathLayer
consuming the registry.

* fix(energy): split seed-pipelines.mjs into two entry points — runSeed hard-exits

High finding from PR review. scripts/seed-pipelines.mjs called runSeed()
twice in one process and awaited Promise.all. But runSeed() in
scripts/_seed-utils.mjs hard-exits via process.exit on ~9 terminal paths
(lines 816, 820, 839, 888, 917, 989, plus fetch-retry 946, fatal 859,
skipped-lock 81). The first runSeed to reach any terminal path exits the
entire node process, so the second runSeed's resolve never fires — only
one of energy:pipelines:{gas,oil}:v1 would ever be written.

Since the bundle scheduled seed-pipelines.mjs exactly once, and both
api/health.js and api/seed-health.js expect both keys populated, the
other registry would stay permanently EMPTY/STALE after deploy.

Fix: split into two entry-point scripts around a shared utility.

- scripts/_pipeline-registry.mjs (NEW, was seed-pipelines.mjs) — shared
  helpers ONLY. Exports GAS_CANONICAL_KEY, OIL_CANONICAL_KEY,
  PIPELINES_TTL_SECONDS, MAX_STALE_MIN, buildGasPayload, buildOilPayload,
  validateRegistry, recordCount, declareRecords. Underscore prefix marks
  it as non-entry-point (matches _seed-utils.mjs / _seed-envelope-source.mjs
  convention).
- scripts/seed-pipelines-gas.mjs (NEW) — imports from the shared module,
  single runSeed('energy','pipelines-gas',…) call.
- scripts/seed-pipelines-oil.mjs (NEW) — same shape, oil.
- scripts/seed-bundle-energy-sources.mjs — register BOTH seeders (not one).
- scripts/seed-pipelines.mjs — deleted.
- tests/pipelines-registry.test.mts — update import path to the shared
  module. All 17 tests still pass.

Typecheck clean (both configs). Tests pass. No other consumers import
from the deleted script.

* fix(energy): complete pipeline bootstrap registration per 4-file checklist

High finding from PR review. My earlier PR description claimed
worldmonitor-bootstrap-registration was complete, but I only touched two
of the four registries (api/health.js + api/seed-health.js). The bootstrap
hydration payload itself (api/bootstrap.js) and the shared cache-keys
registry (server/_shared/cache-keys.ts) still had no entry for either
pipeline key, so any consumer that reads bootstrap data would see
pipelinesGas/pipelinesOil as missing on first load.

Files updated this commit:

- api/bootstrap.js — KEYS map + SLOW_KEYS set both gain pipelinesGas +
  pipelinesOil. Placed next to sprPolicies (same curated-registry cadence
  and tier). Slow tier is correct: weekly cron, not needed on first paint.
- server/_shared/cache-keys.ts — PIPELINES_GAS_KEY + PIPELINES_OIL_KEY
  exported constants (matches SPR_POLICIES_KEY pattern), BOOTSTRAP_KEYS map
  entries, and BOOTSTRAP_TIERS entries (both 'slow').

Not touched (intentional):
- server/gateway.ts — pipeline data is free-tier per the Energy Atlas
  plan; no PREMIUM_RPC_PATHS entry required. Energy Atlas monetization
  hooks (scenario runner, MCP tools, subscriptions) are Release 2.

Full 4-file checklist now complete:
   server/_shared/cache-keys.ts (this commit)
   api/bootstrap.js          (this commit)
   api/health.js             (earlier in PR)
   api/seed-health.js        (earlier in PR — dual-registry rule)

Typecheck clean (both configs).

* feat(energy): ListPipelines + GetPipelineDetail RPCs with evidence-derived badges

Day 7 of the Energy Atlas Release 1 plan (Week 2). Exposes the pipeline
registries (shipped in Day 6) via two supply-chain RPCs and ships the
evidence-to-badge derivation server-side.

## Proto

proto/worldmonitor/supply_chain/v1/list_pipelines.proto — new:
- ListPipelinesRequest { commodity_type?: 'gas' | 'oil' }
- ListPipelinesResponse { pipelines[], fetched_at, classifier_version, upstream_unavailable }
- GetPipelineDetailRequest { pipeline_id (required, query-param) }
- GetPipelineDetailResponse { pipeline?, revisions[], fetched_at, unavailable }
- PipelineEntry — wire shape mirroring scripts/data/pipelines-{gas,oil}.json
  + a server-derived public_badge field
- PipelineEvidence, OperatorStatement, SanctionRef, LatLon, PipelineRevisionEntry

service.proto adds both rpc methods with HTTP_METHOD_GET + path bindings:
  /api/supply-chain/v1/list-pipelines
  /api/supply-chain/v1/get-pipeline-detail

`make generate` regenerated src/generated/{client,server}/… + docs/api/
OpenAPI json/yaml.

## Evidence-derivation

server/worldmonitor/supply-chain/v1/_pipeline-evidence.ts — new.
derivePublicBadge(evidence) → 'flowing' | 'reduced' | 'offline' | 'disputed'
is deterministic + versioned (DERIVER_VERSION='badge-deriver-v1').

Rules (first match wins):
1. offline + sanctionRef OR expired/suspended commercial → offline
2. offline + operator statement → offline
3. offline + only press/ais/satellite → disputed (single-source negative claim)
4. reduced → reduced
5. flowing → flowing
6. unknown / malformed → disputed

Staleness guard: non-flowing badges on >14d-old evidence demote to
disputed. Flowing is the optimistic default — stale "still flowing" is
safer than stale "offline". Matches seed-pipelines-{gas,oil}.mjs maxStaleMin.

Tests (tests/pipeline-evidence-derivation.test.mts) — 15 passing cases
covering happy paths, disputed fallbacks, staleness guard, versioning.

## Handlers

server/worldmonitor/supply-chain/v1/list-pipelines.ts
- Reads energy:pipelines:{gas,oil}:v1 via getCachedJson.
- projectPipeline() narrows the Upstash `unknown` into PipelineEntry
  shape + calls derivePublicBadge.
- Honors commodity_type filter (skip the opposite registry's Redis read
  when the client pre-filters).
- Returns upstream_unavailable=true when BOTH registries miss.

server/worldmonitor/supply-chain/v1/get-pipeline-detail.ts
- Scans both registries by id (ids are globally unique per
  tests/pipelines-registry.test.mts).
- Empty revisions[] for now; auto-revision log wires up in Week 3.

handler.ts registers both into supplyChainHandler.

## Gateway

server/gateway.ts adds 'static' cache-tier for both new RPC paths
(registry is slow-moving; 'static' matches the other read-mostly
supply-chain endpoints).

## Consumer wiring

Not in this commit — PipelineStatusPanel (Day 8) is what will call
listPipelines/getPipelineDetail via the generated client. pipelinesGas
+ pipelinesOil stay in PENDING_CONSUMERS until Day 8.

Typecheck clean (both configs). 15 new tests + 17 registry tests all pass.

* feat(energy): PipelineStatusPanel — evidence-backed status table + drawer

Day 8 of the Energy Atlas Release 1 plan. First consumer of the Day 6–7
registries + RPCs.

## What this PR adds

- src/components/PipelineStatusPanel.ts — new panel (id=pipeline-status).
  * Bootstrap-hydrates from pipelinesGas + pipelinesOil for instant first
    paint; falls through to listPipelines() RPC if bootstrap misses.
    Background re-fetch runs on every render so a classifier-version bump
    between bootstrap stamp and first view produces a visible update.
  * Table rows sorted non-flowing-first (offline / reduced / disputed
    before flowing) — what an atlas reader cares about.
  * Click-to-expand drawer calls getPipelineDetail() lazily — operator
    statements, sanction refs (with clickable source URLs), commercial
    state, classifier version + confidence %, capacity + route metadata.
  * publicBadge color-chip palette matches the methodology doc.
  * Attribution footer with GEM (CC-BY 4.0) credit + classifier version.

- src/components/index.ts — barrel export.
- src/app/panel-layout.ts — import + createPanel('pipeline-status', …).
- src/config/panels.ts — ENERGY_PANELS adds 'pipeline-status' at priority 1.

## PENDING_CONSUMERS cleanup

tests/bootstrap.test.mjs — removes 'pipelinesGas' + 'pipelinesOil' from
the allowlist. The invariant "every bootstrap key has a getHydratedData
consumer" now enforces real wiring for these keys: the panel literally
calls getHydratedData('pipelinesGas') and getHydratedData('pipelinesOil').
Future regressions that remove the consumer will fail pre-push.

## Consumer contract verified

- 67 tests pass including bootstrap.test.mjs consumer coverage check.
- Typecheck clean.
- No DeckGL PathLayer in this commit — existing 'pipelines-layer' has a
  separate data source, so modifying DeckGLMap.ts to overlay evidence-
  derived badges on the map is a follow-up commit to avoid clobbering.

## Out of scope for Day 8 (next steps on same PR)

- DeckGL PathLayer integration (color pipelines on the main map by
  publicBadge, click-to-open this drawer) — Day 8b commit.
- Storage facility registry + StorageFacilityMapPanel — Days 9-10.

* fix(energy): PipelineStatusPanel bootstrap path — client-side badge derivation

High finding from PR review. The Day-8 panel crashed on first paint
whenever bootstrap hydration succeeded, because:

- Bootstrap hydrates raw scripts/data/pipelines-{gas,oil}.json verbatim.
- That JSON does NOT include publicBadge — that field is only added by
  the server handler's projectPipeline() in list-pipelines.ts.
- PipelineStatusPanel passed raw entries into badgeChip(), which called
  badgeLabel(undefined).charAt(0) → TypeError.

The background RPC refresh that would have repaired the data never ran
because the panel threw before reaching it. So the exact bootstrap path
newly wired in commit 6b01fa537 was broken for the new panel.

Fix: move the evidence→badge deriver to src/shared/pipeline-evidence.ts
so the client panel and the server handler run the identical function on
identical inputs. Panel projects raw bootstrap JSON through the shared
deriver client-side, producing the same publicBadge the RPC would have
returned. No UI flicker on hydration because pre- and post-RPC badges
match exactly (same function, same input).

## Changes

- src/shared/pipeline-evidence.ts (NEW) — pure deriver with duck-typed
  PipelineEvidenceInput (no generated-type dependency, so both client
  and server assign their proto-typed evidence bundles by structural
  subtyping). Exports derivePipelinePublicBadge + version + type.
- server/worldmonitor/supply-chain/v1/_pipeline-evidence.ts — now a thin
  re-export of the shared module under its older name so in-handler
  imports keep working without a sweep.
- src/components/PipelineStatusPanel.ts:
  * Imports derivePipelinePublicBadge from @/shared/pipeline-evidence.
  * NEW projectRawPipeline() defensively coerces every field from
    unknown → PipelineEntry shape, mirroring the server projection.
  * buildBootstrapResponse now routes every raw entry through the
    projection before returning, so the wire-format PipelineEntry[] the
    renderer receives always has publicBadge populated.
  * badgeChip() gained a null-guard fallback to 'disputed' — belt +
    braces so even if a future caller passes an undefined, the UI
    renders safely instead of throwing.
  * BootstrapRegistry renamed RawBootstrapRegistry with a comment
    explaining why the seeder ships raw JSON (not wire format).

## Regression tests

tests/pipeline-panel-bootstrap.test.mts (NEW) — 6 tests that exercise
the bootstrap-first-paint path end-to-end:

- Every gas + oil curated entry produces a valid badge.
- Raw entries never ship with pre-computed publicBadge (contract guard
  on the seed data format).
- Deriver never throws on undefined/null/{} evidence (was the crash).
- Nord Stream 1 regression check (offline + paperwork → offline).
- Druzhba-South staleness behavior (reduced when fresh, disputed after
  60 days without update).

38/38 tests now pass (17 registry + 15 deriver + 6 new bootstrap-path).
Typecheck clean on both configs.

## Invariant preserved

The server handler and the panel render identical badges because:
1. Same pure function (imported from the same module).
2. Same deterministic rules, same staleness window.
3. Same bootstrap data read by both paths (Redis → either bootstrap
   payload or RPC response).

No UI flicker on hydration.

* fix(energy): three PR-review P2s on PipelineStatusPanel + aggregators

## P2-1 — sanitizeUrl on external evidence links (XSS hardening)

Sanction-ref URLs and operator-statement URLs were interpolated with
escapeHtml only. HTML-escaping blocks tag injection but NOT javascript:
or data: URL schemes, so a bad URL in the seeded registry would execute
in-app when a reader clicked the evidence link. Every other panel in
the codebase (NewsPanel, GdeltIntelPanel, GeoHubsPanel, AirlineIntelPanel,
MonitorPanel) uses sanitizeUrl for this exact reason.

Fix: import sanitizeUrl from @/utils/sanitize and route both hrefs
through it. sanitizeUrl() drops non-http(s) schemes + returns '' on
invalid URLs. The renderer now suppresses the <a> entirely when
sanitize rejects — the date label still renders as plain text instead
of becoming an executable link.

## P2-2 — loadDetail catch path missing stale-response guard

The success path at loadDetail() checked `this.selectedId !== pipelineId`
to suppress stale responses when the user has clicked another pipeline
mid-flight. The catch path at line 219 had no such guard: if the user
clicked A, then B, and A's request failed before B resolved, A's error
handler cleared detailLoading and detail, showing "Pipeline detail
unavailable" for B's drawer even though B was still loading.

Fix: mirror the same `if (this.selectedId !== pipelineId) return` guard
in the catch path. The newer request now owns the drawer state
regardless of which path (success OR failure) the older one took.

## P2-3 — always-gas-preference aggregator for classifierVersion + fetchedAt

Three call sites (list-pipelines.ts handler, get-pipeline-detail.ts
handler, PipelineStatusPanel bootstrap projection) computed aggregate
classifier version and fetchedAt by `gas?.x || oil?.x || fallback`.
That was defensible when a single seed-pipelines.mjs wrote both keys
atomically (fix commit 29b4ac78f split this into two separate Railway
cron entry points). Now gas + oil cron independently, so mixed-version
(gas=v1, oil=v2 during classifier rollout) and mixed-timestamp (oil
refreshed 6h after gas) windows are the EXPECTED state, not the
exceptional one. The comment in list-pipelines.ts even said "pick the
newest classifier version" but the code didn't actually compare.

Fix: add two shared helpers in src/shared/pipeline-evidence.ts —

- pickNewerClassifierVersion(a,b) — parses /^v(\\d+)$/ and returns the
  higher-numbered version; falls back to lexicographic for non-v-
  prefixed values; handles single-missing inputs.
- pickNewerIsoTimestamp(a,b) — Date.parse()-compares and returns the
  later ISO; handles missing / malformed inputs gracefully.

Both server RPCs and the panel bootstrap projection now call these
helpers identically, so clients are told the truth about version +
freshness during partial rollouts.

## Tests

Extended tests/pipeline-evidence-derivation.test.mts with 8 new
assertions covering both pickers:

- Higher v-number wins regardless of order (v1 vs v2 → v2 both ways)
- Single-missing falls back to the one present
- Missing + missing → default 'v1' for version / '' for ts
- Non-v-numbered values fall back to lexicographic
- Explicit regression: "gas=v1 + oil=v2 during rollout" returns v2
- Explicit regression: "oil fresher than gas" returns the oil timestamp

38 → 46 tests. All pass. Typecheck clean on both configs.

* feat(energy): DeckGL PathLayer colored by evidence-derived badge + map↔panel link

Day 8b of the Energy Atlas plan. Pipelines now render on the main
DeckGL map of the energy variant colored by their derived publicBadge,
and clicking a pipeline on the map opens the same evidence drawer the
panel row-click opens.

## Why this commit

Day 8 shipped the PipelineStatusPanel as a table + drawer view.
Reviewer flag notwithstanding (fixed in 149d33ec3 + db52965cd), a
table-only pipeline view is a weak product compared to the map-centric
atlas it's meant to rival. The map-layer differentiation is the whole
point of the feature.

## What this adds

src/components/DeckGLMap.ts:
- New createEnergyPipelinesLayer() — reads hydrated pipeline registries
  via getHydratedData, projects raw JSON through the shared deriver
  (src/shared/pipeline-evidence.ts), renders a DeckGL PathLayer colored
  by publicBadge:
    flowing  → green (46,204,113)
    reduced  → amber (243,156,18)
    offline  → red   (231,76,60)
    disputed → purple (155,89,182)
  Offline + disputed get thicker strokes (3px vs 2px) for at-a-glance
  surfacing of disrupted assets. Geometry comes from raw startPoint +
  waypoints[] + endPoint per asset (straight line when no waypoints).
- Branching at line ~1498: SITE_VARIANT === 'energy' routes to the
  new method; other variants keep the static PIPELINES config (colored
  by oil/gas type). Existing commodity/finance/full map layers are
  untouched — no cross-variant leakage.
- onClick handler emits `energy:open-pipeline-detail` as a window
  CustomEvent with { pipelineId }. Loose coupling: the map doesn't
  import the panel, the panel doesn't import the map.
- Fallback: if bootstrap hasn't hydrated yet, createEnergyPipelinesLayer
  falls back to the static createPipelinesLayer() so the pipelines
  toggle always shows *something*.

src/components/PipelineStatusPanel.ts:
- Constructor registers a window event listener for
  'energy:open-pipeline-detail' → calls this.loadDetail(pipelineId) →
  drawer opens on the clicked asset. Map click and row click converge
  on the same drawer, same evidence view.
- destroy() removes the listener to prevent ghost handlers after panel
  unmount.

## Guarantees

- Bootstrap parity: the DeckGL layer calls the SAME derivePipelinePublicBadge
  as the panel and the server handler, so the map color, the table row
  chip, and the RPC response all agree on the badge. No flicker, no
  drift, no confused user.
- Variant isolation: only SITE_VARIANT === 'energy' triggers the new
  path. Commodity / finance / full map layers untouched.
- No cross-component import: the panel doesn't reference the map class
  and vice versa. The event contract is the only coupling — testable,
  swappable, tauri-safe (guarded with `typeof window !== 'undefined'`).

Typecheck clean. PR #3294 now has 8 commits.

Follow-up backlog:
- Add waypoints[] to the curated pipelines-{gas,oil}.json so the map
  draws real routes instead of straight lines (cosmetic; does not
  affect correctness).
- Tooltip case in the picking tooltip registry (line ~3748) so hover
  shows "Nord Stream 1 · OFFLINE" before click.

* fix(energy): three PR-review findings on Day 8b DeckGL integration

## P1 — getHydratedData single-use race between map + panel

src/services/bootstrap.ts:34 — `if (val !== undefined) hydrationCache.delete(key);`
The helper drains its slot on first read. Day 8 (PipelineStatusPanel) and
Day 8b (createEnergyPipelinesLayer) BOTH call getHydratedData('pipelinesGas')
and getHydratedData('pipelinesOil') — whoever renders first drains the cache
and forces the loser onto its fallback path (panel → RPC, map → static
PIPELINES layer). The commit's "shared bootstrap-backed data" guarantee
did not actually hold.

Fix: new src/shared/pipeline-registry-store.ts that reads once and memoizes.
Both consumers read through getCachedPipelineRegistries() — same data, same
reference, unlimited re-reads. When the panel's background RPC fetch lands,
it calls setCachedPipelineRegistries() to back-propagate fresh data into
the store so the map's next re-render sees the newer classifierVersion +
fetchedAt too (no map/panel drift during classifier rollouts).

Test-only injection hook (__setBootstrapReaderForTests) makes the drain-once
semantics observable without a real bootstrap payload.

## P2 — pipelines-layer tooltip regresses to blank label on energy variant

src/components/DeckGLMap.ts:3748 (pipelines-layer tooltip case) still assumed
the static-config shape (obj.type). The new energy layer emits objects with
commodityType + badge fields, so the tooltip's type-ternary fell through to
the generic fallback — hover rendered " pipeline" (empty leading commodity)
instead of "Nord Stream 1 · OFFLINE".

Fix: differentiate by presence of obj.badge (only the energy layer sets it).
On the energy variant, tooltip now reads name + commodity + badge. Static-
config variants (commodity / finance / full) keep their existing format
unchanged.

## P2 — createEnergyPipelinesLayer dropped highlightedAssets behavior

The static createPipelinesLayer() reads this.highlightedAssets.pipeline and
threads it into getColor / getWidth with an updateTrigger on the signature.
Any caller using flashAssets('pipeline', [...]) or highlightAssets([...])
gets a visible red-outline flash on the matching paths. My Day 8b energy
layer ignored the set entirely — those APIs silently no-op'd on the energy
variant.

Fix: createEnergyPipelinesLayer() now reads the same highlight set, applies
HIGHLIGHT_COLOR + wider stroke to matching IDs, and wires
updateTriggers: { getColor: sig, getWidth: sig } so DeckGL actually
recomputes when the set changes.

Also removed the unnecessary layerCache.set() in the energy path: the
store can update via RPC back-propagation, and a cache keyed only on
highlight-signature would serve stale data. With ~25 critical-asset
pipelines, rebuild per render is trivial.

## Tests

tests/pipeline-registry-store.test.mts (NEW) — 5 tests covering the
drain-once read-many invariant: multiple consumers get cached data
without re-draining, RPC back-propagation updates the source, partial
updates preserve the other commodity, and pure RPC-first (no bootstrap)
works without invoking the reader.

All 51 PR tests pass. Typecheck clean on both configs.

* feat(energy): Day 9 — storage facility registry (UGS + SPR + LNG + crude hubs)

Ships 21 critical strategic storage facilities as a curated registry, same
evidence-bundle pattern as the pipeline registries in Day 7/8:

- scripts/data/storage-facilities.json — 4 UGS + 4 SPR + 6 LNG export +
  3 LNG import + 4 crude tank farms. Each carries physicalState +
  sanctionRefs + classifierVersion/Confidence + fillDisclosed/fillSource.
- scripts/_storage-facility-registry.mjs — shared helpers (validator,
  builder, canonical key, MAX_STALE_MIN). Validator enforces facility-type
  × capacity-unit pairing (ugs→TWh, spr/tank-farm→Mb, LNG→Mtpa) and the
  non-operational badge ⇒ evidence invariant.
- scripts/seed-storage-facilities.mjs — single runSeed entry (only one
  key, so no split-seeder dance needed).
- Registered in the 4-file bootstrap checklist: cache-keys.ts
  (STORAGE_FACILITIES_KEY + BOOTSTRAP_CACHE_KEYS + BOOTSTRAP_TIERS),
  api/bootstrap.js (KEYS + SLOW_KEYS), api/health.js (BOOTSTRAP_KEYS +
  SEED_META, 14d threshold = 2× weekly cron), api/seed-health.js (mirror).
- tests/bootstrap.test.mjs PENDING_CONSUMERS adds storageFacilities —
  Day 10 StorageFacilityMapPanel will remove it.
- tests/storage-facilities-registry.test.mts — 20 tests covering schema,
  identity, geometry, type×capacity pairing, evidence contract, and
  negative-input validator rejection.

Registry fields are slow-moving; badge derivation happens at read-time
server-side once the RPC handler lands in Day 10 (panel + deckGL
ScatterplotLayer). Seeded data is live in Redis from this commit so the
Day 10 PR only adds display surfaces.

Tests: 56 pass (36 prior + 20 new). Typecheck + typecheck:api clean.

* feat(energy): Day 10 — storage atlas (ListStorageFacilities RPC + DeckGL ScatterplotLayer + panel)

End-to-end wiring for the strategic storage registry seeded in Day 9. Same
pattern as the pipeline shipping path (Days 7+8+8b): proto → handler →
shared evidence deriver → panel → DeckGL map layer, with a shared
read-once store keeping map + panel aligned.

Proto + generated code:
- list_storage_facilities.proto: ListStorageFacilities +
  GetStorageFacilityDetail messages with StorageFacilityEntry,
  StorageEvidence, StorageSanctionRef, StorageOperatorStatement,
  StorageLatLon, StorageFacilityRevisionEntry.
- service.proto wires both RPCs under /api/supply-chain/v1.
- make generate → regenerated client + server stubs + OpenAPI.

Server handlers:
- src/shared/storage-evidence.ts: shared pure deriver. Duck-typed input
  interface avoids generated-type deps; identical rules to the pipeline
  deriver (sanction/commercial paperwork vs external-signal-only offline,
  14d staleness window, version pin).
- _storage-evidence.ts: thin re-export for server handler import ergonomics.
- list-storage-facilities.ts: reads STORAGE_FACILITIES_KEY from Upstash,
  projects raw → wire format, attaches derived publicBadge, filters by
  optional facilityType query arg.
- get-storage-facility-detail.ts: single-asset lookup for drawer.
- handler.ts registers both new methods.
- gateway.ts: both routes → 'static' cache tier (registry is near-static).

Panel + map:
- src/shared/storage-facility-registry-store.ts: drain-once memo mirroring
  pipeline-registry-store. Both panel and DeckGL layer read through this
  so the single-use getHydratedData drain doesn't race between consumers.
  RPC back-propagation via setCachedStorageFacilityRegistry() keeps map ↔
  panel on the same classifierVersion during rollouts.
- StorageFacilityMapPanel.ts: table + evidence drawer. Bootstrap hot path
  projects raw registry through same deriver as server so first-paint
  badge matches post-RPC badge (no flicker). sanitizeUrl + stale-response
  guards (success + catch paths) carried over from PipelineStatusPanel.
- DeckGLMap.ts createEnergyStorageLayer(): ScatterplotLayer keyed on
  badge color; log-scale radius (6km–26km) keeps Rehden visible next to
  Ras Laffan. Click dispatches 'energy:open-storage-facility-detail' —
  panel listens and opens its drawer (loose coupling, no direct refs).
- Tooltip branch on storage-facilities-layer shows facility type, country,
  capacity unit, and badge.
- Added 'storageFacilities' optional field to MapLayers type (optional so
  existing variant literals across commodity/finance/tech/happy/full/etc.
  don't need touching). Wired into LAYER_REGISTRY + VARIANT_LAYER_ORDER.energy
  + ENERGY_MAP_LAYERS + ENERGY_MOBILE_MAP_LAYERS. Panel entry added to
  ENERGY_PANELS + panel-layout createPanel. PENDING_CONSUMERS entry from
  Day 9 removed — panel + map layer are now real consumers.

Tests:
- storage-evidence-derivation.test.mts (17 tests): covers every curated
  facility yields a valid badge, null/malformed input never throws,
  offline sanction/commercial/operator rules, external-signal-only offline
  → disputed, staleness demotion.
- storage-facility-registry-store.test.mts (4 tests): drain-once, no-data
  drain, RPC update, pure-RPC-first path.

All 6,426 unit tests pass. Typecheck + typecheck:api clean. Pre-existing
src-tauri/sidecar/ test failure is unrelated (no diff touches src-tauri/).

* feat(energy): Day 11 — fuel-shortage registry schema + seed + RPC (classifier post-launch)

Ships v1 of the global fuel-shortage alert registry. Severity is the
CLASSIFIER OUTPUT (confirmed/watch), not a client derivation — we ship
the evidence alongside so readers can audit the grounds. v1 is seeded
from curated JSON; post-launch the proactive-intelligence classifier
(Day 12 work) extends the same key directly.

Data:
- scripts/data/fuel-shortages.json — 15 known active shortages
  (PK, LK, NG×2, CU, VE, LB, ZW, AR, IR, BO, KE, PA, EG, BY)
  spanning petrol/diesel/jet across confirmed + watch tiers. Each entry
  carries evidenceSources[] (regulator/operator/press), firstSeen,
  lastConfirmed, resolvedAt, impactTypes[], causeChain[], classifier
  version + confidence. Confirmed severity enforces authoritative
  evidence at schema level.

Seeder:
- scripts/_fuel-shortage-registry.mjs — shared validator (enforces
  iso2 country, enum products/severities/impacts/causes, authoritative
  evidence for confirmed). MIN_SHORTAGES=10.
- scripts/seed-fuel-shortages.mjs — single runSeed entry.
- Registered in seed-bundle-energy-sources.mjs at DAY cadence (shortages
  move faster than registry assets).

Bootstrap 4-file registration:
- cache-keys.ts: FUEL_SHORTAGES_KEY + BOOTSTRAP_CACHE_KEYS + BOOTSTRAP_TIERS.
- api/bootstrap.js: KEYS + SLOW_KEYS.
- api/health.js: BOOTSTRAP_KEYS + SEED_META (2880min = 2× daily cron).
- api/seed-health.js: mirrors intervalMin=1440.

Proto + RPC:
- list_fuel_shortages.proto: ListFuelShortages (country/product/severity
  query facets) + GetFuelShortageDetail messages with FuelShortageEntry,
  FuelShortageEvidence, FuelShortageEvidenceSource.
- service.proto wires both new RPCs under /api/supply-chain/v1.
- list-fuel-shortages.ts handler projects raw → wire format, supports
  server-side country/product/severity filtering.
- get-fuel-shortage-detail.ts single-shortage lookup.
- handler.ts registers both. gateway.ts: 'medium' cache-tier (daily
  classifier updates warrant moderate freshness).

Shared evidence helper:
- src/shared/shortage-evidence.ts: deriveShortageEvidenceQuality maps
  (confidence + authoritative-source count + freshness) → 'strong' |
  'moderate' | 'thin' for client-side sort/trust indicators. Does NOT
  change severity — classifier owns that decision.
- countEvidenceSources buckets sources for the drawer's "n regulator /
  m press" line.

Tests:
- tests/fuel-shortages-registry.test.mts (19 tests): schema, identity,
  enum coverage, evidence contract (confirmed → authoritative source),
  validateRegistry negative cases.
- tests/shortage-evidence.test.mts (10 tests): quality deriver edge
  cases, source bucketing.
- tests/bootstrap.test.mjs PENDING_CONSUMERS adds fuelShortages —
  FuelShortagePanel arrives Day 12 which will remove the entry.

Typecheck + typecheck:api clean. 64 tests pass.

* feat(energy): Day 12 — FuelShortagePanel + DeckGL shortage pins

End-to-end wiring of the fuel-shortage registry shipped in Day 11: panel
on the Energy variant page, ScatterplotLayer pins on the DeckGL map,
both reading through a shared single-drain store so they don't race on
the bootstrap cache.

Panel:
- src/components/FuelShortagePanel.ts — table sorted by severity (confirmed
  first) then evidence quality (strong → thin) then most-recent lastConfirmed.
  Drawer shows short description, first-seen / last-confirmed / resolved,
  impact types, cause chain, classifier version/confidence, and a typed
  evidence-source list with regulator/operator/press chips. sanitizeUrl on
  every href so classifier-ingested URLs can't render as javascript:. Same
  stale-response guards on success + catch paths as the other detail drawers.
- Consumes deriveShortageEvidenceQuality for client-side trust indicator
  (three-dot ●●● / ●●○ / ●○○), NOT for severity — severity is classifier
  output.
- Registered in ENERGY_PANELS + panel-layout.ts + components barrel.

Shared store:
- src/shared/fuel-shortage-registry-store.ts — same drain-once memoize
  pattern as pipeline- and storage-facility-registry-store. Both the
  panel and the DeckGL shortage-pins layer read through it.

DeckGL layer:
- DeckGLMap.createEnergyShortagePinsLayer: ScatterplotLayer placing one
  pin per active shortage at the country centroid (via getCountryCentroid
  from services/country-geometry). Stacking offset (~0.8° lon) when
  multiple shortages share a country so Nigeria's petrol + diesel don't
  render as a single dot. Confirmed pins 55km radius; watch 38km. Click
  dispatches 'energy:open-fuel-shortage-detail' — panel listens.
- Tooltip branch on fuel-shortages-layer: country · product · short
  description · severity.
- Layer registered in LAYER_REGISTRY, VARIANT_LAYER_ORDER.energy,
  ENERGY_MAP_LAYERS, ENERGY_MOBILE_MAP_LAYERS. MapLayers.fuelShortages
  is optional on the type so other variants' literals remain valid.

Tests:
- tests/fuel-shortage-registry-store.test.mts (4 tests): drain-once,
  no-data, RPC back-prop, pure-RPC-first path.
- tests/bootstrap.test.mjs — fuelShortages removed from PENDING_CONSUMERS.

Typecheck + typecheck:api clean. 39 tests pass (plus full suite in pre-push).

* feat(energy): Day 13 — energy disruption event log + asset timeline drawer

Ships the energy:disruptions:v1 registry that threads together pipelines
and storage facilities: state transitions (sabotage, sanction, maintenance,
mechanical, weather, commercial, war) keyed by assetId so any asset's
drawer can render its history without a second registry lookup.

Data + seeder:
- scripts/data/energy-disruptions.json — 12 curated events spanning
  Nord Stream 1/2 sabotage, Druzhba sanctions, CPC force majeure,
  TurkStream maintenance, Yamal halt, Rehden trusteeship, Arctic LNG 2
  sanction, ESPO drone strikes, BTC fire (historical), Sabine Pass
  Hurricane Beryl, Power of Siberia ramp. Each event links back to a
  seeded asset.
- scripts/_energy-disruption-registry.mjs — validator enforces valid
  assetType/eventType/cause enums, http(s) sources, startAt ≤ endAt,
  MIN_EVENTS=8.
- scripts/seed-energy-disruptions.mjs — runSeed entry (weekly cron).
- Bundle entry at 7×DAY cadence.

Bootstrap 4-file registration (cache-keys.ts + bootstrap.js + health.js +
seed-health.js) — energyDisruptions in PENDING_CONSUMERS because panel
drawers fetch lazily via RPC on drawer-open rather than hydrating from
bootstrap directly.

Proto + handler:
- list_energy_disruptions.proto: ListEnergyDisruptions with
  assetId / assetType / ongoingOnly query facets. Returns events sorted
  newest-first.
- list-energy-disruptions.ts projects raw → wire format, supports all
  three query facets.
- Registered in handler.ts. gateway.ts: 'medium' cache tier.

Shared timeline helper:
- src/shared/disruption-timeline.ts — pure formatters (formatEventWindow,
  formatCapacityOffline, statusForEvent). No generated-type deps so
  PipelineStatusPanel + StorageFacilityMapPanel import the same helpers
  and render the timeline identically.

Panel integration:
- PipelineStatusPanel.loadDetail now fetches getPipelineDetail +
  listEnergyDisruptions({assetId, assetType:'pipeline'}) in parallel.
  Drawer gains "Disruption timeline (N)" section with event type, date
  window, capacity offline, cause chain, and short description per entry.
- StorageFacilityMapPanel gets identical treatment with assetType='storage'.
- Both reset detailEvents on closeDetail and on fresh click (stale-response
  safety).

Tests:
- tests/energy-disruptions-registry.test.mts (17 tests): schema, identity,
  enum coverage, evidence, negative inputs.
- tests/bootstrap.test.mjs — energyDisruptions added to PENDING_CONSUMERS.

Typecheck + typecheck:api clean. 51 tests pass locally (plus full suite
in pre-push).

* feat(energy): Day 14 — country drill-down Atlas exposure section

Extends CountryDeepDivePanel's existing "Energy Profile" card with a
mini Atlas-exposure section that surfaces per-country exposure to the
new registries we shipped in Days 7-13.

For each country:
- Pipelines touching this country (from, to, or transit) — clickable
  rows that dispatch 'energy:open-pipeline-detail' so the PipelineStatusPanel
  drawer opens on the energy variant; no-op on other variants.
- Storage facilities in this country — same loose-coupling pattern
  with 'energy:open-storage-facility-detail'.
- Active fuel shortages in this country — severity breakdown line
  (N confirmed · M watch) plus clickable rows emitting
  'energy:open-fuel-shortage-detail'.

Silent absence: sections render only when the country has matching
assets/events, so countries with no pipeline, storage, or shortage
touchpoints see the existing energy-profile card unchanged.

Lazy stores: reads go through the same shared drain-once stores
(getCachedPipelineRegistries, getCachedStorageFacilityRegistry,
getCachedFuelShortageRegistry) so CountryDeepDivePanel does NOT race
with Atlas panels over the single-drain bootstrap cache. Dynamic
import() keeps the three stores out of the panel's static import graph
so non-energy variants can tree-shake them.

Typecheck clean. No schema changes; purely additive UI read from
already-shipped registries.

* docs(energy): methodology page for energy disruption event log

Fills the /docs/methodology/disruptions URL referenced by
list_energy_disruptions.proto, scripts/_energy-disruption-registry.mjs,
and the panel attribution footers. Explains scope (state transitions
not daily noise), data shape, what counts as a disruption, classifier
evolution path, RPC contract, and ties into the sibling pipeline +
storage + shortage methodology pages.

No code change; pure docs completion for Week 4 launch polish.

* fix(energy): upstreamUnavailable only fires when Redis returned nothing

Two handlers (list-storage-facilities + list-pipelines) conflated "empty
filter result on a healthy registry" with "upstream unavailable". A
caller who queried one facilityType/commodityType and legitimately got
zero matches was told the upstream was down — which may push clients to
error-state rendering or suppress caching instead of showing a valid
empty list.

list-storage-facilities.ts — upstreamUnavailable now only fires when
`raw` is null (Redis miss). Zero filtered rows on a healthy registry
returns upstreamUnavailable: false + empty array. Matches the sibling
list-fuel-shortages handler and the wire contract in
list_storage_facilities.proto.

list-pipelines.ts — same bug, subtler shape. Now checks "requested at
least one side AND received nothing" rather than "zero rows after
collection". A filter that legitimately matches no gas/oil pipelines on
a healthy registry now returns upstreamUnavailable: false.

list-energy-disruptions.ts and list-fuel-shortages.ts already had the
correct shape (only flag unavailable when raw is missing) — left as-is.

Typecheck + typecheck:api clean. No tests added: the existing registry
schema tests cover the projection/filter helpers, and the handler-level
gating change is documented in code comments for future audits.

* fix(energy): three Greptile findings on PR #3294

Two P1 filter bugs (resolved shortages rendered as active) and one P2
contract inconsistency on the disruptions handler.

P1: DeckGLMap createEnergyShortagePinsLayer rendered every shortage in
the registry as an active crisis pin — including entries where the
classifier has written resolvedAt to mark the crisis over. Added a
filter so only entries with a null/empty resolvedAt become map pins.
Curated v1 data has resolvedAt=null everywhere so no visible change
today, but the moment the classifier starts writing resolutions
post-launch, resolved shortages would have appeared as ongoing.

P1: CountryDeepDivePanel renderAtlasExposure had the same bug in the
country drill-down — "N confirmed · M watch" counts included resolved
entries, inflating the active-crisis line per country. Same one-line
filter fix.

P2: list-energy-disruptions.ts gated upstreamUnavailable on
`!raw?.events` — a partial write (top-level object present but `events`
property missing) fired the "upstream down" flag, inconsistent with
the sibling handlers (list-pipelines, list-storage-facilities,
list-fuel-shortages) that only fire on `!raw`. Rewrote to match:
`!raw` → upstreamUnavailable, empty events → normal empty list. This
also aligns with the contract documented on the upstream-unavailable-
vs-empty-filter skill extracted from the earlier P2 review.

Typecheck + typecheck:api clean. All three fixes are one-liner filter
or gate changes; no test additions needed (registry tests still pass
with v1 data since resolvedAt is null throughout).
2026-04-23 07:34:07 +04:00

2335 lines
91 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: SupplyChainService API
version: 1.0.0
paths:
/api/supply-chain/v1/get-shipping-rates:
get:
tags:
- SupplyChainService
summary: GetShippingRates
operationId: GetShippingRates
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetShippingRatesResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-chokepoint-status:
get:
tags:
- SupplyChainService
summary: GetChokepointStatus
operationId: GetChokepointStatus
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetChokepointStatusResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-chokepoint-history:
get:
tags:
- SupplyChainService
summary: GetChokepointHistory
description: |-
GetChokepointHistory returns transit-count history for a single chokepoint,
loaded lazily on card expand. Keeps the status RPC compact (no 180-day
history per chokepoint on every call).
operationId: GetChokepointHistory
parameters:
- name: chokepointId
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetChokepointHistoryResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-critical-minerals:
get:
tags:
- SupplyChainService
summary: GetCriticalMinerals
operationId: GetCriticalMinerals
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetCriticalMineralsResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-shipping-stress:
get:
tags:
- SupplyChainService
summary: GetShippingStress
description: GetShippingStress returns carrier market data and a composite stress index.
operationId: GetShippingStress
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetShippingStressResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-country-chokepoint-index:
get:
tags:
- SupplyChainService
summary: GetCountryChokepointIndex
description: GetCountryChokepointIndex returns per-chokepoint exposure scores for a country. PRO-gated.
operationId: GetCountryChokepointIndex
parameters:
- name: iso2
in: query
description: ISO 3166-1 alpha-2 country code (uppercase).
required: false
schema:
type: string
- name: hs2
in: query
description: HS2 chapter (2-digit string). Defaults to "27" (energy/mineral fuels) when absent.
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetCountryChokepointIndexResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-bypass-options:
get:
tags:
- SupplyChainService
summary: GetBypassOptions
description: GetBypassOptions returns ranked bypass corridors for a chokepoint. PRO-gated.
operationId: GetBypassOptions
parameters:
- name: chokepointId
in: query
required: false
schema:
type: string
- name: cargoType
in: query
description: 'container | tanker | bulk | roro (default: "container")'
required: false
schema:
type: string
- name: closurePct
in: query
description: '0-100, percent of capacity blocked (default: 100)'
required: false
schema:
type: integer
format: int32
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetBypassOptionsResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-country-cost-shock:
get:
tags:
- SupplyChainService
summary: GetCountryCostShock
description: GetCountryCostShock returns cost shock and war risk data for a country+chokepoint. PRO-gated.
operationId: GetCountryCostShock
parameters:
- name: iso2
in: query
required: false
schema:
type: string
- name: chokepointId
in: query
required: false
schema:
type: string
- name: hs2
in: query
description: 'HS2 chapter (default: "27")'
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetCountryCostShockResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-country-products:
get:
tags:
- SupplyChainService
summary: GetCountryProducts
description: GetCountryProducts returns the seeded bilateral-HS4 import basket for a country. PRO-gated.
operationId: GetCountryProducts
parameters:
- name: iso2
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetCountryProductsResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-multi-sector-cost-shock:
get:
tags:
- SupplyChainService
summary: GetMultiSectorCostShock
description: |-
GetMultiSectorCostShock returns per-sector cost-shock estimates for a
country+chokepoint+closure-window. PRO-gated.
operationId: GetMultiSectorCostShock
parameters:
- name: iso2
in: query
required: false
schema:
type: string
- name: chokepointId
in: query
required: false
schema:
type: string
- name: closureDays
in: query
description: Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.
required: false
schema:
type: integer
format: int32
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetMultiSectorCostShockResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-sector-dependency:
get:
tags:
- SupplyChainService
summary: GetSectorDependency
description: GetSectorDependency returns dependency flags and risk profile for a country+HS2 sector. PRO-gated.
operationId: GetSectorDependency
parameters:
- name: iso2
in: query
required: false
schema:
type: string
- name: hs2
in: query
description: HS2 chapter code, e.g. "27" (mineral fuels), "85" (electronics)
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetSectorDependencyResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-route-explorer-lane:
get:
tags:
- SupplyChainService
summary: GetRouteExplorerLane
description: |-
GetRouteExplorerLane returns the primary maritime route, chokepoint exposures,
bypass options with geometry, war risk, and static transit/freight estimates for
a country pair + HS2 + cargo type. PRO-gated. Wraps the route-intelligence vendor
endpoint's compute with browser-callable auth and adds fields needed by the
Route Explorer UI.
operationId: GetRouteExplorerLane
parameters:
- name: fromIso2
in: query
required: false
schema:
type: string
- name: toIso2
in: query
required: false
schema:
type: string
- name: hs2
in: query
description: HS2 chapter code, e.g. "27", "85"
required: false
schema:
type: string
- name: cargoType
in: query
description: 'One of: container, tanker, bulk, roro'
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetRouteExplorerLaneResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-route-impact:
get:
tags:
- SupplyChainService
summary: GetRouteImpact
operationId: GetRouteImpact
parameters:
- name: fromIso2
in: query
required: false
schema:
type: string
- name: toIso2
in: query
required: false
schema:
type: string
- name: hs2
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetRouteImpactResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/list-pipelines:
get:
tags:
- SupplyChainService
summary: ListPipelines
description: |-
ListPipelines returns the curated oil & gas pipeline registry for the
Energy Atlas PathLayer. Public badges are DERIVED from evidence bundles
server-side and versioned (classifier_version). Free-tier; see
docs/methodology/pipelines.mdx for data + classifier spec.
operationId: ListPipelines
parameters:
- name: commodityType
in: query
description: Filter to one commodity. Omit (or pass empty) to receive both.
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListPipelinesResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-pipeline-detail:
get:
tags:
- SupplyChainService
summary: GetPipelineDetail
description: |-
GetPipelineDetail returns a single pipeline with full evidence bundle
+ auto-revision-log entries. Loaded lazily on drawer open.
operationId: GetPipelineDetail
parameters:
- name: pipelineId
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetPipelineDetailResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/list-storage-facilities:
get:
tags:
- SupplyChainService
summary: ListStorageFacilities
description: |-
ListStorageFacilities returns the curated strategic storage registry
(UGS + SPR + LNG + crude tank farms) for the Energy Atlas DeckGL
ScatterplotLayer. Public badges are DERIVED from evidence bundles
server-side and versioned (classifier_version). Free-tier; see
docs/methodology/storage.mdx.
operationId: ListStorageFacilities
parameters:
- name: facilityType
in: query
description: |-
Filter to one facility type. Accepts:
"ugs" | "spr" | "lng_export" | "lng_import" | "crude_tank_farm"
Omit (or pass empty) to receive all types.
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListStorageFacilitiesResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-storage-facility-detail:
get:
tags:
- SupplyChainService
summary: GetStorageFacilityDetail
description: |-
GetStorageFacilityDetail returns a single facility with full evidence
bundle + revision log. Loaded lazily on drawer open.
operationId: GetStorageFacilityDetail
parameters:
- name: facilityId
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetStorageFacilityDetailResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/list-fuel-shortages:
get:
tags:
- SupplyChainService
summary: ListFuelShortages
description: |-
ListFuelShortages returns the global fuel-shortage alert registry.
Severity is CLASSIFIER OUTPUT not a client derivation. Curated v1;
classifier-extended post-launch. Free tier.
operationId: ListFuelShortages
parameters:
- name: country
in: query
description: Filter to one ISO 3166-1 alpha-2 country. Omit for global.
required: false
schema:
type: string
- name: product
in: query
description: |-
Filter to one product. Accepts: "petrol" | "diesel" | "jet" | "heating_oil".
Omit for all products.
required: false
schema:
type: string
- name: severity
in: query
description: 'Filter to one severity. Accepts: "confirmed" | "watch". Omit for both.'
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListFuelShortagesResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/get-fuel-shortage-detail:
get:
tags:
- SupplyChainService
summary: GetFuelShortageDetail
description: |-
GetFuelShortageDetail returns a single shortage with full evidence
bundle and citation timeline. Loaded lazily on drawer open.
operationId: GetFuelShortageDetail
parameters:
- name: shortageId
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetFuelShortageDetailResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/supply-chain/v1/list-energy-disruptions:
get:
tags:
- SupplyChainService
summary: ListEnergyDisruptions
description: |-
ListEnergyDisruptions returns the disruption event log for pipelines
and storage facilities. Supports per-asset or per-asset-type filtering
so panel drawers can fetch a scoped timeline without pulling the
full registry.
operationId: ListEnergyDisruptions
parameters:
- name: assetId
in: query
description: |-
Filter to one asset. Omit for all. When set, also narrows to the
matching asset_type if provided.
required: false
schema:
type: string
- name: assetType
in: query
description: 'Filter to one asset type. Accepts: "pipeline" | "storage".'
required: false
schema:
type: string
- name: ongoingOnly
in: query
description: If true, only return events with endAt empty (still ongoing).
required: false
schema:
type: boolean
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListEnergyDisruptionsResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
properties:
message:
type: string
description: Error message (e.g., 'user not found', 'database connection failed')
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
FieldViolation:
type: object
properties:
field:
type: string
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')
description:
type: string
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
required:
- field
- description
description: FieldViolation describes a single validation error for a specific field.
ValidationError:
type: object
properties:
violations:
type: array
items:
$ref: '#/components/schemas/FieldViolation'
description: List of validation violations
required:
- violations
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
GetShippingRatesRequest:
type: object
GetShippingRatesResponse:
type: object
properties:
indices:
type: array
items:
$ref: '#/components/schemas/ShippingIndex'
fetchedAt:
type: string
upstreamUnavailable:
type: boolean
ShippingIndex:
type: object
properties:
indexId:
type: string
name:
type: string
currentValue:
type: number
format: double
previousValue:
type: number
format: double
changePct:
type: number
format: double
unit:
type: string
history:
type: array
items:
$ref: '#/components/schemas/ShippingRatePoint'
spikeAlert:
type: boolean
ShippingRatePoint:
type: object
properties:
date:
type: string
value:
type: number
format: double
GetChokepointStatusRequest:
type: object
GetChokepointStatusResponse:
type: object
properties:
chokepoints:
type: array
items:
$ref: '#/components/schemas/ChokepointInfo'
fetchedAt:
type: string
upstreamUnavailable:
type: boolean
ChokepointInfo:
type: object
properties:
id:
type: string
name:
type: string
lat:
type: number
format: double
lon:
type: number
format: double
disruptionScore:
type: integer
format: int32
status:
type: string
activeWarnings:
type: integer
format: int32
congestionLevel:
type: string
affectedRoutes:
type: array
items:
type: string
description:
type: string
aisDisruptions:
type: integer
format: int32
directions:
type: array
items:
type: string
directionalDwt:
type: array
items:
$ref: '#/components/schemas/DirectionalDwt'
transitSummary:
$ref: '#/components/schemas/TransitSummary'
flowEstimate:
$ref: '#/components/schemas/FlowEstimate'
warRiskTier:
type: string
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
description: |-
*
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
This is a FREE field (no PRO gate) — it exposes the existing server-internal
threatLevel from ChokepointConfig, making it available to clients for badges
and bypass corridor scoring.
DirectionalDwt:
type: object
properties:
direction:
type: string
dwtThousandTonnes:
type: number
format: double
wowChangePct:
type: number
format: double
TransitSummary:
type: object
properties:
todayTotal:
type: integer
format: int32
todayTanker:
type: integer
format: int32
todayCargo:
type: integer
format: int32
todayOther:
type: integer
format: int32
wowChangePct:
type: number
format: double
history:
type: array
items:
$ref: '#/components/schemas/TransitDayCount'
riskLevel:
type: string
incidentCount7d:
type: integer
format: int32
disruptionPct:
type: number
format: double
riskSummary:
type: string
riskReportAction:
type: string
dataAvailable:
type: boolean
description: |-
False when the upstream portwatch/relay source did not return data for
this chokepoint in the current cycle — the summary fields are zero-state
fill, not a genuine "zero traffic" reading. Client should render a
"transit data unavailable" indicator and skip stat/chart rendering.
TransitDayCount:
type: object
properties:
date:
type: string
tanker:
type: integer
format: int32
cargo:
type: integer
format: int32
other:
type: integer
format: int32
total:
type: integer
format: int32
container:
type: integer
format: int32
dryBulk:
type: integer
format: int32
generalCargo:
type: integer
format: int32
roro:
type: integer
format: int32
capContainer:
type: number
format: double
capDryBulk:
type: number
format: double
capGeneralCargo:
type: number
format: double
capRoro:
type: number
format: double
capTanker:
type: number
format: double
FlowEstimate:
type: object
properties:
currentMbd:
type: number
format: double
baselineMbd:
type: number
format: double
flowRatio:
type: number
format: double
disrupted:
type: boolean
source:
type: string
hazardAlertLevel:
type: string
hazardAlertName:
type: string
GetChokepointHistoryRequest:
type: object
properties:
chokepointId:
type: string
required:
- chokepointId
description: |-
GetChokepointHistory returns the transit-count history for a single
chokepoint. Loaded lazily on card expand so the main chokepoint-status
response can stay compact (no 180-day history per chokepoint).
GetChokepointHistoryResponse:
type: object
properties:
chokepointId:
type: string
history:
type: array
items:
$ref: '#/components/schemas/TransitDayCount'
fetchedAt:
type: string
format: int64
GetCriticalMineralsRequest:
type: object
GetCriticalMineralsResponse:
type: object
properties:
minerals:
type: array
items:
$ref: '#/components/schemas/CriticalMineral'
fetchedAt:
type: string
upstreamUnavailable:
type: boolean
CriticalMineral:
type: object
properties:
mineral:
type: string
topProducers:
type: array
items:
$ref: '#/components/schemas/MineralProducer'
hhi:
type: number
format: double
riskRating:
type: string
globalProduction:
type: number
format: double
unit:
type: string
MineralProducer:
type: object
properties:
country:
type: string
countryCode:
type: string
productionTonnes:
type: number
format: double
sharePct:
type: number
format: double
GetShippingStressRequest:
type: object
GetShippingStressResponse:
type: object
properties:
carriers:
type: array
items:
$ref: '#/components/schemas/ShippingStressCarrier'
stressScore:
type: number
format: double
description: Composite stress score 0100 (higher = more disruption).
stressLevel:
type: string
description: '"low" | "moderate" | "elevated" | "critical".'
fetchedAt:
type: integer
format: int64
description: 'Warning: Values > 2^53 may lose precision in JavaScript'
upstreamUnavailable:
type: boolean
description: Set to true when upstream data source is unavailable and cached data is stale.
ShippingStressCarrier:
type: object
properties:
symbol:
type: string
description: Ticker or identifier (e.g., "BDRY", "ZIM").
name:
type: string
description: Human-readable name.
price:
type: number
format: double
description: Current price.
changePct:
type: number
format: double
description: Percentage change from previous close.
carrierType:
type: string
description: 'Carrier type: "etf" | "carrier" | "index".'
sparkline:
type: array
items:
type: number
format: double
description: 30-day price sparkline.
description: ShippingStressCarrier represents market stress data for a carrier or shipping index.
GetCountryChokepointIndexRequest:
type: object
properties:
iso2:
type: string
pattern: ^[A-Z]{2}$
description: ISO 3166-1 alpha-2 country code (uppercase).
hs2:
type: string
description: HS2 chapter (2-digit string). Defaults to "27" (energy/mineral fuels) when absent.
required:
- iso2
description: GetCountryChokepointIndexRequest specifies the country and optional HS2 chapter.
GetCountryChokepointIndexResponse:
type: object
properties:
iso2:
type: string
description: ISO 3166-1 alpha-2 country code echoed from the request.
hs2:
type: string
description: HS2 chapter used for the computation.
exposures:
type: array
items:
$ref: '#/components/schemas/ChokepointExposureEntry'
primaryChokepointId:
type: string
description: Canonical ID of the chokepoint with the highest exposure score.
vulnerabilityIndex:
type: number
format: double
description: Composite vulnerability index 0100 (weighted sum of top-3 exposures).
fetchedAt:
type: string
description: ISO timestamp of when this data was last seeded.
description: GetCountryChokepointIndexResponse returns exposure scores for all relevant chokepoints.
ChokepointExposureEntry:
type: object
properties:
chokepointId:
type: string
description: Canonical chokepoint ID from the chokepoint registry.
chokepointName:
type: string
description: Human-readable chokepoint name.
exposureScore:
type: number
format: double
description: Exposure score 0100; higher = more dependent on this chokepoint.
coastSide:
type: string
description: Which ocean/basin side the country's ports face (atlantic, pacific, indian, med, multi, landlocked).
shockSupported:
type: boolean
description: Whether the shock model is supported for this chokepoint + hs2 combination.
description: ChokepointExposureEntry holds per-chokepoint exposure data for a country.
GetBypassOptionsRequest:
type: object
properties:
chokepointId:
type: string
cargoType:
type: string
description: 'container | tanker | bulk | roro (default: "container")'
closurePct:
type: integer
format: int32
description: '0-100, percent of capacity blocked (default: 100)'
required:
- chokepointId
GetBypassOptionsResponse:
type: object
properties:
chokepointId:
type: string
cargoType:
type: string
closurePct:
type: integer
format: int32
options:
type: array
items:
$ref: '#/components/schemas/BypassOption'
fetchedAt:
type: string
primaryChokepointWarRiskTier:
type: string
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
description: |-
*
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
This is a FREE field (no PRO gate) — it exposes the existing server-internal
threatLevel from ChokepointConfig, making it available to clients for badges
and bypass corridor scoring.
BypassOption:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
addedTransitDays:
type: integer
format: int32
addedCostMultiplier:
type: number
format: double
capacityConstraintTonnage:
type: string
format: int64
suitableCargoTypes:
type: array
items:
type: string
activationThreshold:
type: string
waypointChokepointIds:
type: array
items:
type: string
liveScore:
type: number
format: double
bypassWarRiskTier:
type: string
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
description: |-
*
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
This is a FREE field (no PRO gate) — it exposes the existing server-internal
threatLevel from ChokepointConfig, making it available to clients for badges
and bypass corridor scoring.
notes:
type: string
GetCountryCostShockRequest:
type: object
properties:
iso2:
type: string
pattern: ^[A-Z]{2}$
chokepointId:
type: string
hs2:
type: string
description: 'HS2 chapter (default: "27")'
required:
- iso2
- chokepointId
GetCountryCostShockResponse:
type: object
properties:
iso2:
type: string
chokepointId:
type: string
hs2:
type: string
supplyDeficitPct:
type: number
format: double
description: Average refined-product supply deficit % under full closure (Gasoline/Diesel/Jet fuel/LPG average; HS 27 only)
coverageDays:
type: integer
format: int32
description: Energy stockpile coverage in days (IEA data, HS 27 only; 0 for non-energy sectors or net exporters)
warRiskPremiumBps:
type: integer
format: int32
description: War risk insurance premium in basis points for this chokepoint
warRiskTier:
type: string
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
description: |-
*
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
This is a FREE field (no PRO gate) — it exposes the existing server-internal
threatLevel from ChokepointConfig, making it available to clients for badges
and bypass corridor scoring.
hasEnergyModel:
type: boolean
description: Whether supply_deficit_pct and coverage_days are modelled (true) or unavailable (false)
unavailableReason:
type: string
description: Null/unavailable explanation for non-energy sectors
fetchedAt:
type: string
GetCountryProductsRequest:
type: object
properties:
iso2:
type: string
pattern: ^[A-Z]{2}$
required:
- iso2
GetCountryProductsResponse:
type: object
properties:
iso2:
type: string
products:
type: array
items:
$ref: '#/components/schemas/CountryProduct'
fetchedAt:
type: string
description: ISO timestamp from the seeded payload (empty when no data is cached).
CountryProduct:
type: object
properties:
hs4:
type: string
description:
type: string
totalValue:
type: number
format: double
topExporters:
type: array
items:
$ref: '#/components/schemas/ProductExporter'
year:
type: integer
format: int32
ProductExporter:
type: object
properties:
partnerCode:
type: integer
format: int32
partnerIso2:
type: string
value:
type: number
format: double
share:
type: number
format: double
GetMultiSectorCostShockRequest:
type: object
properties:
iso2:
type: string
pattern: ^[A-Z]{2}$
chokepointId:
type: string
closureDays:
type: integer
format: int32
description: Closure-window duration in days. Server clamps to [1, 365]. Defaults to 30.
required:
- iso2
- chokepointId
GetMultiSectorCostShockResponse:
type: object
properties:
iso2:
type: string
chokepointId:
type: string
closureDays:
type: integer
format: int32
description: Server-clamped closure-window duration in days (1-365).
warRiskTier:
type: string
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
description: |-
*
War risk tier derived from Lloyd's JWC Listed Areas + OSINT threat classification.
This is a FREE field (no PRO gate) — it exposes the existing server-internal
threatLevel from ChokepointConfig, making it available to clients for badges
and bypass corridor scoring.
sectors:
type: array
items:
$ref: '#/components/schemas/MultiSectorCostShock'
totalAddedCost:
type: number
format: double
description: Sum of total_cost_shock across all sectors.
fetchedAt:
type: string
unavailableReason:
type: string
description: Populated when no seeded import data is available for the country.
MultiSectorCostShock:
type: object
properties:
hs2:
type: string
description: HS2 chapter code (e.g. "27" mineral fuels, "85" electronics).
hs2Label:
type: string
description: Friendly chapter label (e.g. "Energy", "Electronics").
importValueAnnual:
type: number
format: double
description: Total annual import value (USD) for this sector.
freightAddedPctPerTon:
type: number
format: double
description: Bypass-corridor freight uplift fraction (0.10 == +10% per ton).
warRiskPremiumBps:
type: integer
format: int32
description: War-risk insurance premium (basis points) sourced from the chokepoint tier.
addedTransitDays:
type: integer
format: int32
description: Bypass-corridor transit penalty (informational).
totalCostShockPerDay:
type: number
format: double
totalCostShock30Days:
type: number
format: double
totalCostShock90Days:
type: number
format: double
totalCostShock:
type: number
format: double
description: Cost for the requested closure_days window.
closureDays:
type: integer
format: int32
description: Echoes the clamped closure duration used for total_cost_shock (1-365).
GetSectorDependencyRequest:
type: object
properties:
iso2:
type: string
pattern: ^[A-Z]{2}$
hs2:
type: string
description: HS2 chapter code, e.g. "27" (mineral fuels), "85" (electronics)
required:
- iso2
- hs2
GetSectorDependencyResponse:
type: object
properties:
iso2:
type: string
hs2:
type: string
hs2Label:
type: string
description: Human-readable HS2 chapter name.
flags:
type: array
items:
type: string
enum:
- DEPENDENCY_FLAG_UNSPECIFIED
- DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL
- DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL
- DEPENDENCY_FLAG_COMPOUND_RISK
- DEPENDENCY_FLAG_DIVERSIFIABLE
description: DependencyFlag classifies how a country+sector dependency can fail.
primaryExporterIso2:
type: string
description: ISO2 of the country supplying the largest share of this sector's imports.
primaryExporterShare:
type: number
format: double
description: Share of imports from the primary exporter (01). 0 = no Comtrade data available.
primaryChokepointId:
type: string
description: Chokepoint ID with the highest exposure score for this country+sector.
primaryChokepointExposure:
type: number
format: double
description: Exposure score for the primary chokepoint (0100).
hasViableBypass:
type: boolean
description: Whether at least one viable bypass corridor exists for the primary chokepoint.
fetchedAt:
type: string
GetRouteExplorerLaneRequest:
type: object
properties:
fromIso2:
type: string
pattern: ^[A-Z]{2}$
toIso2:
type: string
pattern: ^[A-Z]{2}$
hs2:
type: string
description: HS2 chapter code, e.g. "27", "85"
cargoType:
type: string
description: 'One of: container, tanker, bulk, roro'
required:
- fromIso2
- toIso2
- hs2
- cargoType
GetRouteExplorerLaneResponse:
type: object
properties:
fromIso2:
type: string
toIso2:
type: string
hs2:
type: string
cargoType:
type: string
primaryRouteId:
type: string
description: Primary trade route ID from TRADE_ROUTES config. Empty when no modeled lane.
primaryRouteGeometry:
type: array
items:
$ref: '#/components/schemas/GeoPoint'
chokepointExposures:
type: array
items:
$ref: '#/components/schemas/ChokepointExposureSummary'
bypassOptions:
type: array
items:
$ref: '#/components/schemas/BypassCorridorOption'
warRiskTier:
type: string
disruptionScore:
type: number
format: double
estTransitDaysRange:
$ref: '#/components/schemas/NumberRange'
estFreightUsdPerTeuRange:
$ref: '#/components/schemas/NumberRange'
noModeledLane:
type: boolean
description: |-
True when the wrapper fell back to the origin's first route (no shared route
between origin and destination clusters). Signals "no modeled lane" to the UI.
fetchedAt:
type: string
GeoPoint:
type: object
properties:
lon:
type: number
format: double
lat:
type: number
format: double
description: GeoPoint is a [longitude, latitude] pair.
ChokepointExposureSummary:
type: object
properties:
chokepointId:
type: string
chokepointName:
type: string
exposurePct:
type: integer
format: int32
BypassCorridorOption:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
addedTransitDays:
type: integer
format: int32
addedCostMultiplier:
type: number
format: double
warRiskTier:
type: string
status:
type: string
enum:
- CORRIDOR_STATUS_UNSPECIFIED
- CORRIDOR_STATUS_ACTIVE
- CORRIDOR_STATUS_PROPOSED
- CORRIDOR_STATUS_UNAVAILABLE
description: |-
Status of a bypass corridor for UI labeling. "active" means usable today;
"proposed" means documented but not yet built/operational; "unavailable"
means blockaded or otherwise blocked from use.
fromPort:
$ref: '#/components/schemas/GeoPoint'
toPort:
$ref: '#/components/schemas/GeoPoint'
description: |-
BypassCorridorOption is a single enriched bypass corridor for the Route Explorer UI.
Includes coordinate endpoints so the client can call MapContainer.setBypassRoutes
directly without any client-side geometry lookup.
NumberRange:
type: object
properties:
min:
type: integer
format: int32
max:
type: integer
format: int32
description: Inclusive integer range for transit days / freight USD estimates.
GetRouteImpactRequest:
type: object
properties:
fromIso2:
type: string
pattern: ^[A-Z]{2}$
toIso2:
type: string
pattern: ^[A-Z]{2}$
hs2:
type: string
required:
- fromIso2
- toIso2
- hs2
GetRouteImpactResponse:
type: object
properties:
laneValueUsd:
type: number
format: double
primaryExporterIso2:
type: string
primaryExporterShare:
type: number
format: double
topStrategicProducts:
type: array
items:
$ref: '#/components/schemas/StrategicProduct'
resilienceScore:
type: number
format: double
dependencyFlags:
type: array
items:
type: string
enum:
- DEPENDENCY_FLAG_UNSPECIFIED
- DEPENDENCY_FLAG_SINGLE_SOURCE_CRITICAL
- DEPENDENCY_FLAG_SINGLE_CORRIDOR_CRITICAL
- DEPENDENCY_FLAG_COMPOUND_RISK
- DEPENDENCY_FLAG_DIVERSIFIABLE
description: DependencyFlag classifies how a country+sector dependency can fail.
hs2InSeededUniverse:
type: boolean
comtradeSource:
type: string
fetchedAt:
type: string
StrategicProduct:
type: object
properties:
hs4:
type: string
label:
type: string
totalValueUsd:
type: number
format: double
topExporterIso2:
type: string
topExporterShare:
type: number
format: double
primaryChokepointId:
type: string
ListPipelinesRequest:
type: object
properties:
commodityType:
type: string
description: Filter to one commodity. Omit (or pass empty) to receive both.
description: |-
ListPipelines returns the full oil and/or gas pipeline registry with
evidence-based status per asset. Registry is curated (see
docs/methodology/pipelines.mdx) and refreshed weekly by
scripts/seed-pipelines-{gas,oil}.mjs. Typical consumer: PipelineStatusPanel
rendering the DeckGL PathLayer.
The public badge is DERIVED from the evidence bundle server-side at
read time — callers MUST use `public_badge` for display, not invent
their own derivation from `evidence` fields. This keeps the atlas and
MCP clients consistent and versioned together.
ListPipelinesResponse:
type: object
properties:
pipelines:
type: array
items:
$ref: '#/components/schemas/PipelineEntry'
fetchedAt:
type: string
classifierVersion:
type: string
upstreamUnavailable:
type: boolean
PipelineEntry:
type: object
properties:
id:
type: string
name:
type: string
operator:
type: string
commodityType:
type: string
description: Either "gas" or "oil".
fromCountry:
type: string
toCountry:
type: string
transitCountries:
type: array
items:
type: string
capacityBcmYr:
type: number
format: double
capacityMbd:
type: number
format: double
lengthKm:
type: integer
format: int32
inService:
type: integer
format: int32
startPoint:
$ref: '#/components/schemas/LatLon'
endPoint:
$ref: '#/components/schemas/LatLon'
waypoints:
type: array
items:
$ref: '#/components/schemas/LatLon'
evidence:
$ref: '#/components/schemas/PipelineEvidence'
publicBadge:
type: string
description: |-
Server-derived public badge. One of:
"flowing" | "reduced" | "offline" | "disputed"
LatLon:
type: object
properties:
lat:
type: number
format: double
lon:
type: number
format: double
PipelineEvidence:
type: object
properties:
physicalState:
type: string
description: 'One of: "flowing" | "reduced" | "offline" | "unknown"'
physicalStateSource:
type: string
description: 'One of: "operator" | "regulator" | "press" | "satellite" | "ais-relay"'
operatorStatement:
$ref: '#/components/schemas/OperatorStatement'
commercialState:
type: string
description: 'One of: "under_contract" | "expired" | "suspended" | "unknown"'
sanctionRefs:
type: array
items:
$ref: '#/components/schemas/SanctionRef'
lastEvidenceUpdate:
type: string
classifierVersion:
type: string
classifierConfidence:
type: number
format: double
OperatorStatement:
type: object
properties:
text:
type: string
url:
type: string
date:
type: string
SanctionRef:
type: object
properties:
authority:
type: string
listId:
type: string
date:
type: string
url:
type: string
GetPipelineDetailRequest:
type: object
properties:
pipelineId:
type: string
required:
- pipelineId
description: |-
GetPipelineDetail returns a single pipeline with its full evidence bundle.
Evidence surface here is richer than ListPipelinesResponse — the list view
is designed for the map layer's compact shape; detail is designed for the
click-through drawer.
GetPipelineDetailResponse:
type: object
properties:
pipeline:
$ref: '#/components/schemas/PipelineEntry'
revisions:
type: array
items:
$ref: '#/components/schemas/PipelineRevisionEntry'
fetchedAt:
type: string
unavailable:
type: boolean
PipelineRevisionEntry:
type: object
properties:
date:
type: string
fieldChanged:
type: string
previousValue:
type: string
newValue:
type: string
trigger:
type: string
description: 'One of: "classifier" | "source" | "decay" | "override"'
sourcesUsed:
type: array
items:
type: string
classifierVersion:
type: string
ListStorageFacilitiesRequest:
type: object
properties:
facilityType:
type: string
description: |-
Filter to one facility type. Accepts:
"ugs" | "spr" | "lng_export" | "lng_import" | "crude_tank_farm"
Omit (or pass empty) to receive all types.
description: |-
ListStorageFacilities returns the curated strategic storage registry
(underground gas storage, strategic petroleum reserves, LNG terminals,
crude tank farms) with evidence-based status per asset. Registry is
curated (see docs/methodology/storage.mdx) and refreshed weekly by
scripts/seed-storage-facilities.mjs. Typical consumer:
StorageFacilityMapPanel rendering the DeckGL ScatterplotLayer.
Like pipelines, the public badge is DERIVED from the evidence bundle
server-side at read time. Callers MUST use `public_badge` for display
rather than inventing their own derivation from `evidence` fields, so
the atlas and MCP clients stay consistent and versioned together.
ListStorageFacilitiesResponse:
type: object
properties:
facilities:
type: array
items:
$ref: '#/components/schemas/StorageFacilityEntry'
fetchedAt:
type: string
classifierVersion:
type: string
upstreamUnavailable:
type: boolean
StorageFacilityEntry:
type: object
properties:
id:
type: string
name:
type: string
operator:
type: string
facilityType:
type: string
description: 'One of: "ugs" | "spr" | "lng_export" | "lng_import" | "crude_tank_farm"'
country:
type: string
location:
$ref: '#/components/schemas/StorageLatLon'
capacityTwh:
type: number
format: double
description: |-
Working capacity in the facility's native unit (see working_capacity_unit).
Exactly ONE of these is populated per facility, chosen by facility_type:
ugs → capacity_twh
spr, crude_tank_farm → capacity_mb
lng_export, lng_import → capacity_mtpa
capacityMb:
type: number
format: double
capacityMtpa:
type: number
format: double
workingCapacityUnit:
type: string
description: 'One of: "TWh" | "Mb" | "Mtpa"'
inService:
type: integer
format: int32
evidence:
$ref: '#/components/schemas/StorageEvidence'
publicBadge:
type: string
description: |-
Server-derived public badge. One of:
"operational" | "reduced" | "offline" | "disputed"
StorageLatLon:
type: object
properties:
lat:
type: number
format: double
lon:
type: number
format: double
StorageEvidence:
type: object
properties:
physicalState:
type: string
description: 'One of: "operational" | "reduced" | "offline" | "under_construction" | "unknown"'
physicalStateSource:
type: string
description: 'One of: "operator" | "regulator" | "press" | "satellite" | "ais-relay"'
operatorStatement:
$ref: '#/components/schemas/StorageOperatorStatement'
commercialState:
type: string
description: 'One of: "under_contract" | "expired" | "suspended" | "unknown"'
sanctionRefs:
type: array
items:
$ref: '#/components/schemas/StorageSanctionRef'
fillDisclosed:
type: boolean
description: |-
Whether working-gas / stock-level fill is publicly disclosed for this
asset. LNG export terminals, for instance, tend NOT to disclose; UGS
sites in Europe are required to disclose via GIE AGSI+.
fillSource:
type: string
description: |-
Source of the disclosed fill series ("GIE AGSI+", "EIA SPR weekly
stock report", etc.). Empty when fill_disclosed=false.
lastEvidenceUpdate:
type: string
classifierVersion:
type: string
classifierConfidence:
type: number
format: double
StorageOperatorStatement:
type: object
properties:
text:
type: string
url:
type: string
date:
type: string
StorageSanctionRef:
type: object
properties:
authority:
type: string
listId:
type: string
date:
type: string
url:
type: string
GetStorageFacilityDetailRequest:
type: object
properties:
facilityId:
type: string
required:
- facilityId
description: |-
GetStorageFacilityDetail returns a single facility with its full evidence
bundle + revision log. Revisions land in Week 3 alongside the disruption
event log (Week 3 milestone — empty array in v1).
GetStorageFacilityDetailResponse:
type: object
properties:
facility:
$ref: '#/components/schemas/StorageFacilityEntry'
revisions:
type: array
items:
$ref: '#/components/schemas/StorageFacilityRevisionEntry'
fetchedAt:
type: string
unavailable:
type: boolean
StorageFacilityRevisionEntry:
type: object
properties:
date:
type: string
fieldChanged:
type: string
previousValue:
type: string
newValue:
type: string
trigger:
type: string
description: 'One of: "classifier" | "source" | "decay" | "override"'
sourcesUsed:
type: array
items:
type: string
classifierVersion:
type: string
ListFuelShortagesRequest:
type: object
properties:
country:
type: string
description: Filter to one ISO 3166-1 alpha-2 country. Omit for global.
product:
type: string
description: |-
Filter to one product. Accepts: "petrol" | "diesel" | "jet" | "heating_oil".
Omit for all products.
severity:
type: string
description: 'Filter to one severity. Accepts: "confirmed" | "watch". Omit for both.'
description: |-
ListFuelShortages returns the global fuel-shortage alert registry.
v1 is seeded from a curated JSON file (scripts/data/fuel-shortages.json);
post-launch it's extended automatically by the proactive-intelligence
classifier. Severity is the CLASSIFIER OUTPUT ("confirmed" or "watch"),
not a client-side derivation — we ship the evidence alongside so agents
and humans can audit the grounds for a severity label.
See docs/methodology/shortages.mdx for the evidence-threshold spec.
ListFuelShortagesResponse:
type: object
properties:
shortages:
type: array
items:
$ref: '#/components/schemas/FuelShortageEntry'
fetchedAt:
type: string
classifierVersion:
type: string
upstreamUnavailable:
type: boolean
FuelShortageEntry:
type: object
properties:
id:
type: string
country:
type: string
product:
type: string
description: 'One of: "petrol" | "diesel" | "jet" | "heating_oil"'
severity:
type: string
description: 'One of: "confirmed" | "watch" (classifier output — not a client-side derivation)'
firstSeen:
type: string
lastConfirmed:
type: string
resolvedAt:
type: string
description: Empty string when not yet resolved.
impactTypes:
type: array
items:
type: string
description: |-
Observable consumer-facing impacts. Subset of:
"stations_closed" | "rationing" | "flights_cancelled" | "import_cut" | "price_spike"
causeChain:
type: array
items:
type: string
description: |-
Contributing root causes, ordered primary-first. Subset of:
"upstream_refinery" | "logistics" | "policy" | "chokepoint" |
"sanction" | "war" | "import_cut"
shortDescription:
type: string
evidence:
$ref: '#/components/schemas/FuelShortageEvidence'
FuelShortageEvidence:
type: object
properties:
evidenceSources:
type: array
items:
$ref: '#/components/schemas/FuelShortageEvidenceSource'
firstRegulatorConfirmation:
type: string
description: |-
ISO date of the first regulator confirmation, if any. Empty when
severity is "watch" on press-only signal.
classifierVersion:
type: string
classifierConfidence:
type: number
format: double
lastEvidenceUpdate:
type: string
FuelShortageEvidenceSource:
type: object
properties:
authority:
type: string
title:
type: string
url:
type: string
date:
type: string
sourceType:
type: string
description: 'One of: "regulator" | "operator" | "press" | "ais-relay" | "satellite"'
GetFuelShortageDetailRequest:
type: object
properties:
shortageId:
type: string
required:
- shortageId
description: |-
GetFuelShortageDetail returns a single shortage with full evidence
bundle + citation timeline. Loaded lazily on drawer open.
GetFuelShortageDetailResponse:
type: object
properties:
shortage:
$ref: '#/components/schemas/FuelShortageEntry'
fetchedAt:
type: string
unavailable:
type: boolean
ListEnergyDisruptionsRequest:
type: object
properties:
assetId:
type: string
description: |-
Filter to one asset. Omit for all. When set, also narrows to the
matching asset_type if provided.
assetType:
type: string
description: 'Filter to one asset type. Accepts: "pipeline" | "storage".'
ongoingOnly:
type: boolean
description: If true, only return events with endAt empty (still ongoing).
description: |-
ListEnergyDisruptions returns the energy disruption event log — state
transitions for pipelines and storage facilities (sabotage, sanction,
maintenance, mechanical, weather, commercial, war). Each event ties
back to an assetId seeded by the pipeline or storage registry, so the
panel drawer can render an asset-scoped timeline without a second RPC.
v1 is seeded from a curated JSON file (scripts/data/energy-disruptions.json);
post-launch the classifier in proactive-intelligence.mjs extends this
key with state transitions auto-derived from evidence-field changes.
See docs/methodology/disruptions.mdx.
ListEnergyDisruptionsResponse:
type: object
properties:
events:
type: array
items:
$ref: '#/components/schemas/EnergyDisruptionEntry'
fetchedAt:
type: string
classifierVersion:
type: string
upstreamUnavailable:
type: boolean
EnergyDisruptionEntry:
type: object
properties:
id:
type: string
assetId:
type: string
description: Maps to a pipeline or storage-facility id seeded elsewhere.
assetType:
type: string
description: 'One of: "pipeline" | "storage"'
eventType:
type: string
description: |-
One of: "sabotage" | "sanction" | "maintenance" | "mechanical" |
"weather" | "commercial" | "war" | "other"
startAt:
type: string
endAt:
type: string
description: Empty string when event is still ongoing.
capacityOfflineBcmYr:
type: number
format: double
description: Headline-offline capacity (contextual — 0 when not applicable).
capacityOfflineMbd:
type: number
format: double
causeChain:
type: array
items:
type: string
description: Contributing causes, primary-first.
shortDescription:
type: string
sources:
type: array
items:
$ref: '#/components/schemas/EnergyDisruptionSource'
classifierVersion:
type: string
classifierConfidence:
type: number
format: double
lastEvidenceUpdate:
type: string
EnergyDisruptionSource:
type: object
properties:
authority:
type: string
title:
type: string
url:
type: string
date:
type: string
sourceType:
type: string