mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
feat(resilience): add tradeToGdpPct to resilience static bundle (#2794)
* feat(seed): add tradeToGdpPct to resilience static bundle from WB NE.TRD.GNFS.ZS Trade as % of GDP is needed for Phase 2 exposure-weighting of shipping stress. Small open economies (Singapore ~300%, Belgium ~170%) will feel shipping disruption more than large autarkies (US ~25%). * fix(seed): bump static source version to v2 for tradeToGdp backfill The static seeder skips re-runs within the same seed year if a snapshot exists. Without a version bump, the new tradeToGdpPct field would never backfill for countries already seeded in 2026. Also added sourceVersion check to shouldSkipSeedYear() so future version bumps automatically force a re-seed without needing to clear Redis.
This commit is contained in:
@@ -29,13 +29,13 @@ export const RESILIENCE_STATIC_INDEX_KEY = 'resilience:static:index:v1';
|
||||
export const RESILIENCE_STATIC_META_KEY = 'seed-meta:resilience:static';
|
||||
export const RESILIENCE_STATIC_PREFIX = 'resilience:static:';
|
||||
export const RESILIENCE_STATIC_TTL_SECONDS = 400 * 24 * 60 * 60;
|
||||
export const RESILIENCE_STATIC_SOURCE_VERSION = 'resilience-static-v1';
|
||||
export const RESILIENCE_STATIC_SOURCE_VERSION = 'resilience-static-v2';
|
||||
export const RESILIENCE_STATIC_WINDOW_CRON = '0 */4 1-3 10 *';
|
||||
|
||||
const LOCK_DOMAIN = 'resilience:static';
|
||||
const LOCK_TTL_MS = 2 * 60 * 60 * 1000;
|
||||
const TOTAL_DATASET_SLOTS = 8;
|
||||
const COUNTRY_DATASET_FIELDS = ['wgi', 'infrastructure', 'gpi', 'rsf', 'who', 'fao', 'aquastat', 'iea'];
|
||||
const TOTAL_DATASET_SLOTS = 9;
|
||||
const COUNTRY_DATASET_FIELDS = ['wgi', 'infrastructure', 'gpi', 'rsf', 'who', 'fao', 'aquastat', 'iea', 'tradeToGdp'];
|
||||
const WGI_INDICATORS = ['VA.EST', 'PV.EST', 'GE.EST', 'RQ.EST', 'RL.EST', 'CC.EST'];
|
||||
const INFRASTRUCTURE_INDICATORS = ['EG.ELC.ACCS.ZS', 'IS.ROD.PAVE.ZS', 'EG.USE.ELEC.KH.PC'];
|
||||
const WHO_INDICATORS = {
|
||||
@@ -63,6 +63,7 @@ export function shouldSkipSeedYear(meta, seedYear = nowSeedYear()) {
|
||||
return Boolean(
|
||||
meta
|
||||
&& meta.status === 'ok'
|
||||
&& meta.sourceVersion === RESILIENCE_STATIC_SOURCE_VERSION
|
||||
&& Number(meta.seedYear) === seedYear
|
||||
&& Number.isFinite(Number(meta.recordCount))
|
||||
&& Number(meta.recordCount) > 0,
|
||||
@@ -640,6 +641,27 @@ async function fetchAquastatDataset() {
|
||||
return buildAquastatWbMap(latest);
|
||||
}
|
||||
|
||||
const WB_TRADE_TO_GDP_INDICATOR = 'NE.TRD.GNFS.ZS';
|
||||
|
||||
export function buildTradeToGdpMap(latestByCountry) {
|
||||
const byCountry = new Map();
|
||||
for (const [iso2, entry] of latestByCountry.entries()) {
|
||||
byCountry.set(iso2, {
|
||||
source: 'worldbank',
|
||||
tradeToGdpPct: entry.value,
|
||||
year: entry.year,
|
||||
});
|
||||
}
|
||||
if (byCountry.size === 0) throw new Error('World Bank trade-to-GDP returned no usable rows');
|
||||
return byCountry;
|
||||
}
|
||||
|
||||
async function fetchTradeToGdpDataset() {
|
||||
const rows = await fetchWorldBankIndicatorRows(WB_TRADE_TO_GDP_INDICATOR, { mrv: '12' });
|
||||
const latest = selectLatestWorldBankByCountry(rows);
|
||||
return buildTradeToGdpMap(latest);
|
||||
}
|
||||
|
||||
export function finalizeCountryPayloads(datasetMaps, seedYear = nowSeedYear(), seededAt = new Date().toISOString()) {
|
||||
const merged = new Map();
|
||||
|
||||
@@ -770,6 +792,7 @@ async function fetchAllDatasetMaps() {
|
||||
{ key: 'fao', fetcher: fetchFsinDataset },
|
||||
{ key: 'aquastat', fetcher: fetchAquastatDataset },
|
||||
{ key: 'iea', fetcher: fetchEnergyDependencyDataset },
|
||||
{ key: 'tradeToGdp', fetcher: fetchTradeToGdpDataset },
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(adapters.map((adapter) => adapter.fetcher()));
|
||||
|
||||
@@ -31,6 +31,7 @@ export const RESILIENCE_FIXTURES: FixtureMap = {
|
||||
fao: { peopleInCrisis: 10, phase: 'IPC Phase 1', year: 2025 },
|
||||
aquastat: { indicator: 'Renewable water availability', value: 4000, year: 2024 },
|
||||
iea: { energyImportDependency: { value: 15, year: 2024, source: 'IEA' } },
|
||||
tradeToGdp: { source: 'worldbank', tradeToGdpPct: 70, year: 2023 },
|
||||
},
|
||||
'resilience:static:US': {
|
||||
wgi: {
|
||||
@@ -62,6 +63,7 @@ export const RESILIENCE_FIXTURES: FixtureMap = {
|
||||
fao: { peopleInCrisis: 5000, phase: 'IPC Phase 2', year: 2025 },
|
||||
aquastat: { indicator: 'Renewable water availability', value: 1500, year: 2024 },
|
||||
iea: { energyImportDependency: { value: 25, year: 2024, source: 'IEA' } },
|
||||
tradeToGdp: { source: 'worldbank', tradeToGdpPct: 25, year: 2023 },
|
||||
},
|
||||
'resilience:static:YE': {
|
||||
wgi: {
|
||||
@@ -93,6 +95,7 @@ export const RESILIENCE_FIXTURES: FixtureMap = {
|
||||
fao: { peopleInCrisis: 2_000_000, phase: 'IPC Phase 4', year: 2025 },
|
||||
aquastat: { indicator: 'Water stress', value: 85, year: 2024 },
|
||||
iea: { energyImportDependency: { value: 95, year: 2024, source: 'IEA' } },
|
||||
tradeToGdp: { source: 'worldbank', tradeToGdpPct: 30, year: 2023 },
|
||||
},
|
||||
'energy:mix:v1:NO': {
|
||||
iso2: 'NO',
|
||||
@@ -333,6 +336,7 @@ export const RESILIENCE_FIXTURES: FixtureMap = {
|
||||
fao: { peopleInCrisis: 1_500_000, phase: 'IPC Phase 3', year: 2025 },
|
||||
aquastat: { indicator: 'Water stress', value: 72, year: 2024 },
|
||||
iea: null, // Eurostat is EU-only — Lebanon absent → energy import dependency unknown
|
||||
tradeToGdp: { source: 'worldbank', tradeToGdpPct: 95, year: 2023 },
|
||||
},
|
||||
'energy:mix:v1:LB': {
|
||||
iso2: 'LB',
|
||||
@@ -354,7 +358,7 @@ export const RESILIENCE_FIXTURES: FixtureMap = {
|
||||
failedDatasets: [],
|
||||
seedYear: 2025,
|
||||
seededAt: '2026-04-03T00:00:00.000Z',
|
||||
sourceVersion: 'resilience-static-v1',
|
||||
sourceVersion: 'resilience-static-v2',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ export function buildReleaseGateFixtures(): ReleaseGateFixtureMap {
|
||||
failedDatasets: [],
|
||||
seedYear: 2025,
|
||||
seededAt: '2026-04-04T00:00:00.000Z',
|
||||
sourceVersion: 'resilience-static-v1',
|
||||
sourceVersion: 'resilience-static-v2',
|
||||
},
|
||||
'supply_chain:shipping_stress:v1': { stressScore: 18 },
|
||||
'supply_chain:transit-summaries:v1': {
|
||||
|
||||
@@ -7,8 +7,10 @@ import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
RESILIENCE_STATIC_INDEX_KEY,
|
||||
RESILIENCE_STATIC_META_KEY,
|
||||
RESILIENCE_STATIC_SOURCE_VERSION,
|
||||
buildFailureRefreshKeys,
|
||||
buildManifest,
|
||||
buildTradeToGdpMap,
|
||||
countryRedisKey,
|
||||
createCountryResolvers,
|
||||
finalizeCountryPayloads,
|
||||
@@ -210,6 +212,28 @@ describe('resilience static seed CSV parsers', () => {
|
||||
assert.ok(result.get('DE')?.indicator?.includes('stress'), 'indicator must include "stress" to route correctly in scoreAquastatValue()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTradeToGdpMap', () => {
|
||||
it('produces { source, tradeToGdpPct, year } shape for known countries', () => {
|
||||
const input = new Map([
|
||||
['NO', { value: 70.5, year: 2023 }],
|
||||
['US', { value: 25.3, year: 2023 }],
|
||||
['SG', { value: 318.2, year: 2023 }],
|
||||
]);
|
||||
const result = buildTradeToGdpMap(input);
|
||||
assert.equal(result.size, 3);
|
||||
const no = result.get('NO');
|
||||
assert.ok(no != null);
|
||||
assert.equal(no.source, 'worldbank');
|
||||
assert.equal(no.tradeToGdpPct, 70.5);
|
||||
assert.equal(no.year, 2023);
|
||||
assert.equal(result.get('SG')?.tradeToGdpPct, 318.2);
|
||||
});
|
||||
|
||||
it('throws when input map is empty', () => {
|
||||
assert.throws(() => buildTradeToGdpMap(new Map()), /no usable rows/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resilience static seed parsers', () => {
|
||||
@@ -290,6 +314,9 @@ describe('resilience static seed payload assembly', () => {
|
||||
iea: new Map([
|
||||
['NO', { source: 'eurostat-nrg_ind_id', energyImportDependency: { value: -13.3, year: 2024, source: 'eurostat' } }],
|
||||
]),
|
||||
tradeToGdp: new Map([
|
||||
['NO', { source: 'worldbank', tradeToGdpPct: 70.5, year: 2023 }],
|
||||
]),
|
||||
}, 2026, '2026-04-03T12:00:00.000Z');
|
||||
|
||||
assert.deepEqual([...payloads.keys()].sort(), ['NO', 'US', 'YE']);
|
||||
@@ -303,7 +330,8 @@ describe('resilience static seed payload assembly', () => {
|
||||
fao: null,
|
||||
aquastat: null,
|
||||
iea: { source: 'eurostat-nrg_ind_id', energyImportDependency: { value: -13.3, year: 2024, source: 'eurostat' } },
|
||||
coverage: { availableDatasets: 3, totalDatasets: 8, ratio: 0.375 },
|
||||
tradeToGdp: { source: 'worldbank', tradeToGdpPct: 70.5, year: 2023 },
|
||||
coverage: { availableDatasets: 4, totalDatasets: 9, ratio: 0.444 },
|
||||
seedYear: 2026,
|
||||
seededAt: '2026-04-03T12:00:00.000Z',
|
||||
});
|
||||
@@ -326,7 +354,7 @@ describe('resilience static seed payload assembly', () => {
|
||||
failedDatasets: ['aquastat', 'gpi'],
|
||||
seedYear: 2026,
|
||||
seededAt: '2026-04-03T12:00:00.000Z',
|
||||
sourceVersion: 'resilience-static-v1',
|
||||
sourceVersion: RESILIENCE_STATIC_SOURCE_VERSION,
|
||||
});
|
||||
|
||||
assert.deepEqual(buildFailureRefreshKeys(manifest), [
|
||||
@@ -338,10 +366,13 @@ describe('resilience static seed payload assembly', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips reruns only after a successful snapshot for the same seed year', () => {
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2026, recordCount: 150 }, 2026), true);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'error', seedYear: 2026, recordCount: 150 }, 2026), false);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2025, recordCount: 150 }, 2026), false);
|
||||
it('skips reruns only after a successful snapshot for the same seed year and source version', () => {
|
||||
const v = RESILIENCE_STATIC_SOURCE_VERSION;
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2026, recordCount: 150, sourceVersion: v }, 2026), true);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'error', seedYear: 2026, recordCount: 150, sourceVersion: v }, 2026), false);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2025, recordCount: 150, sourceVersion: v }, 2026), false);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2026, recordCount: 150, sourceVersion: 'resilience-static-v1' }, 2026), false);
|
||||
assert.equal(shouldSkipSeedYear({ status: 'ok', seedYear: 2026, recordCount: 150 }, 2026), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,6 +389,7 @@ describe('recoverFailedDatasets', () => {
|
||||
wgi: new Map([['YE', { source: 'worldbank-wgi' }]]),
|
||||
infrastructure: new Map(), gpi: new Map(), rsf: new Map(),
|
||||
who: new Map(), fao: faoOverride, aquastat: new Map(), iea: new Map(),
|
||||
tradeToGdp: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user