Files
worldmonitor/scripts/seed-chokepoint-baselines.mjs
Elie Habib a09f49ff9c feat(supply-chain): energy flow estimates per chokepoint (mb/d card row) (#2780)
* feat(supply-chain): energy flow estimates per chokepoint (mb/d card row)

- Add FlowEstimate proto message + ChokepointInfo field 15; regenerate stubs
- Add baselineId mapping to _chokepoint-ids.ts (7 of 13 chokepoints)
- Add relayId to seed-chokepoint-baselines.mjs CHOKEPOINTS entries
- New seed-chokepoint-flows.mjs: reads portwatch + baselines, computes
  7d tanker avg vs 90d baseline, outputs flow_ratio and current_mbd;
  prefers DWT (capTanker) when available; flags disruption if last 3 days
  each below 0.85 threshold; writes energy:chokepoint-flows:v1 (TTL 3d)
- get-chokepoint-status.ts: parallel-reads flows key, attaches flowEstimate
- SupplyChainPanel: compact card gains mb/d row (red <85%, amber <95%)
- 19 new unit tests for flow computation and seeder contract

* fix(chokepoint-flows): base useDwt on 90d baseline window, not recent 7 days

Zero recent capTanker is the disruption signal, not a reason to fall back
to vessel counts. Switching metrics during peak disruption caused the seeder
to report a higher (less accurate) flow estimate exactly when oil-flow
collapse is most acute. useDwt is now locked to whether the baseline window
has DWT data -- stable across disruption events.

Adds regression test covering DWT-collapse scenario.

* fix(chokepoint-flows): require majority DWT coverage in baseline before activating DWT mode

capBaselineSum > 0 would activate DWT on a single non-zero day during
partial data roll-out, pulling down the baseline average via zero-filled
gaps. Now requires >= ceil(prev90.length / 2) days with DWT data.
ArcGIS data is all-or-nothing per chokepoint in practice, so this
guard catches edge cases without affecting normal operation.
2026-04-07 12:43:54 +04:00

46 lines
1.8 KiB
JavaScript

#!/usr/bin/env node
import { loadEnvFile, runSeed } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
export const CANONICAL_KEY = 'energy:chokepoint-baselines:v1';
export const CHOKEPOINT_TTL_SECONDS = 34_560_000;
export const CHOKEPOINTS = [
{ id: 'hormuz', relayId: 'hormuz_strait', name: 'Strait of Hormuz', mbd: 21.0, lat: 26.6, lon: 56.3 },
{ id: 'malacca', relayId: 'malacca_strait', name: 'Strait of Malacca', mbd: 17.2, lat: 1.3, lon: 103.8 },
{ id: 'suez', relayId: 'suez', name: 'Suez Canal / SUMED', mbd: 7.6, lat: 30.7, lon: 32.3 },
{ id: 'babelm', relayId: 'bab_el_mandeb', name: 'Bab el-Mandeb', mbd: 6.2, lat: 12.6, lon: 43.4 },
{ id: 'danish', relayId: 'dover_strait', name: 'Danish Straits', mbd: 3.0, lat: 57.5, lon: 10.5 },
{ id: 'turkish', relayId: 'bosphorus', name: 'Turkish Straits', mbd: 2.9, lat: 41.1, lon: 29.0 },
{ id: 'panama', relayId: 'panama', name: 'Panama Canal', mbd: 0.9, lat: 9.1, lon: -79.7 },
];
export function buildPayload() {
return {
source: 'EIA World Oil Transit Chokepoints',
referenceYear: 2023,
updatedAt: new Date().toISOString(),
chokepoints: CHOKEPOINTS,
};
}
export function validateFn(data) {
return Array.isArray(data?.chokepoints) && data.chokepoints.length === 7;
}
const isMain = process.argv[1]?.endsWith('seed-chokepoint-baselines.mjs');
if (isMain) {
runSeed('energy', 'chokepoint-baselines', CANONICAL_KEY, buildPayload, {
validateFn,
ttlSeconds: CHOKEPOINT_TTL_SECONDS,
sourceVersion: 'eia-chokepoint-baselines-v1',
recordCount: (data) => data?.chokepoints?.length || 0,
}).catch((err) => {
const cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : '';
console.error('FATAL:', (err.message || err) + cause);
process.exit(1);
});
}