mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(seeds): expand IMF WEO coverage — growth, labor, external themes (#3027) Adds three new SDMX-3.0 seeders alongside the existing imf-macro seeder to surface 15+ additional WEO indicators across ~210 countries at zero incremental API cost. Bundled into seed-bundle-imf-extended.mjs on the same monthly Railway cron cadence. Seeders + Redis keys: - seed-imf-growth.mjs → economic:imf:growth:v1 NGDP_RPCH, NGDPDPC, NGDP_R, PPPPC, PPPGDP, NID_NGDP, NGSD_NGDP - seed-imf-labor.mjs → economic:imf:labor:v1 LUR (unemployment), LP (population) - seed-imf-external.mjs → economic:imf:external:v1 BX, BM, BCA, TM_RPCH, TX_RPCH (+ derived trade balance) - seed-imf-macro.mjs extended with PCPI, PCPIEPCH, GGX_NGDP, GGXONLB_NGDP All four seeders share the 35-day TTL (monthly WEO release) and ~210 country coverage via the same imfSdmxFetchIndicator helper. Wiring: - api/bootstrap.js, api/health.js, server/_shared/cache-keys.ts — register new keys, mark them slow-tier, add SEED_META freshness thresholds matching the imfMacro entry (70d = 2× monthly cadence) - server/worldmonitor/resilience/v1/_dimension-freshness.ts — override entries for the dash-vs-colon seed-meta key shape - _indicator-registry.ts — add LUR as a 4th macroFiscal sub-metric (enrichment tier, weight 0.15); rebalance govRevenuePct (0.5→0.4) and currentAccountPct (0.3→0.25) so weights still sum to 1.0 - _dimension-scorers.ts — read economic:imf:labor:v1 in scoreMacroFiscal, normalize LUR with goalposts 3% (best) → 25% (worst); null-tolerant so weightedBlend redistributes when labor data is unavailable - api/mcp.ts — new get_country_macro tool bundling all four IMF keys with a single freshness check; describes per-country fields including growth/inflation/labor/BOP for LLM-driven country screening - src/services/imf-country-data.ts — bootstrap-cached client + pure buildImfEconomicIndicators helper - src/app/country-intel.ts — async-fetch the IMF bundle on country selection and merge real GDP growth, CPI inflation, unemployment, and GDP/capita rows into the Economic Indicators card; bumps card cap from 3 → 6 rows to fit live signals + IMF context Tests: - tests/seed-imf-extended.test.mjs — 13 unit tests across the three new seeders' pure helpers (canonical keys, ISO3→ISO2 mapping, aggregate filtering, derived savings-investment gap & trade balance, validate thresholds) - tests/imf-country-data.test.mts — 6 tests for the panel rendering helper, including stagflation flag and high-unemployment trend - tests/resilience-dimension-scorers.test.mts — new LUR sub-metric test (tight vs slack labor); existing scoreMacroFiscal coverage assertions updated for the new 4-metric weight split - tests/helpers/resilience-fixtures.mts — labor fixture for NO/US/YE so the existing macroFiscal ordering test still resolves the LUR weight - tests/bootstrap.test.mjs — register imfGrowth/imfLabor/imfExternal as pending consumers (matching imfMacro) - tests/mcp.test.mjs — bump tools/list count 28 → 29 https://claude.ai/code/session_018enRzZuRqaMudKsLD5RLZv * fix(resilience): update macroFiscal goldens for LUR weight rebalance Recompute pinned fixture values after adding labor-unemployment as 4th macroFiscal sub-metric (weight rebalance in _indicator-registry). Also align seed-imf-external tradeBalance to a single reference year to avoid mixing ex/im values from different WEO vintages. * fix(seeds): tighten IMF coverage gates to reject partial snapshots IMF WEO growth/labor/external indicators report ~210 countries for healthy runs. Previous thresholds (150/100/150) let a bad IMF run overwrite a good snapshot with dozens of missing countries and still pass validation. Raise all three to >=190, matching the pattern of sibling seeders and leaving a ~20-country margin for indicators with slightly narrower reporting. Labor validator unions LUR + population (LP), so healthy coverage tracks LP (~210), not LUR (~100) — the old 100 threshold was based on a misread of the union logic. * fix(seed-health): register imf-growth/labor/external seed-meta keys Missing SEED_DOMAINS entries meant the 3 new IMF WEO seeders (growth, labor, external) had no /api/seed-health visibility. intervalMin=50400 matches health.js maxStaleMin/2 (100800/2) — same monthly WEO cadence as imf-macro. --------- Co-authored-by: Claude <noreply@anthropic.com>
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
|
|
import { buildImfEconomicIndicators, type ImfCountryBundle } from '../src/services/imf-country-data.ts';
|
|
|
|
function bundle(overrides: Partial<ImfCountryBundle> = {}): ImfCountryBundle {
|
|
return {
|
|
macro: null,
|
|
growth: null,
|
|
labor: null,
|
|
external: null,
|
|
fetchedAt: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('buildImfEconomicIndicators (panel rendering)', () => {
|
|
it('returns no rows when no IMF data is present', () => {
|
|
assert.deepEqual(buildImfEconomicIndicators(bundle()), []);
|
|
});
|
|
|
|
it('renders real GDP growth + inflation + unemployment + GDP/capita rows', () => {
|
|
const rows = buildImfEconomicIndicators(bundle({
|
|
macro: {
|
|
inflationPct: 3.4, currentAccountPct: -2.1, govRevenuePct: 30,
|
|
cpiIndex: null, cpiEopPct: null, govExpenditurePct: null, primaryBalancePct: null,
|
|
year: 2025,
|
|
},
|
|
growth: {
|
|
realGdpGrowthPct: 2.7, gdpPerCapitaUsd: 78500, realGdp: null,
|
|
gdpPerCapitaPpp: null, gdpPpp: null, investmentPct: null, savingsPct: null,
|
|
savingsInvestmentGap: null, year: 2025,
|
|
},
|
|
labor: {
|
|
unemploymentPct: 4.2, populationMillions: 333.3, year: 2025,
|
|
},
|
|
}));
|
|
assert.deepEqual(rows.map(r => r.label), [
|
|
'Real GDP Growth', 'CPI Inflation', 'Unemployment', 'GDP / Capita',
|
|
]);
|
|
assert.equal(rows[0].value, '+2.7%');
|
|
assert.equal(rows[0].trend, 'up');
|
|
assert.equal(rows[1].value, '+3.4%');
|
|
assert.equal(rows[1].trend, 'up'); // 3.4% inflation: warning but not crisis
|
|
assert.equal(rows[2].value, '4.2%');
|
|
assert.equal(rows[2].trend, 'up'); // <5% unemployment is good
|
|
assert.equal(rows[3].value, '$78.5k');
|
|
for (const row of rows) assert.equal(row.source, 'IMF WEO');
|
|
});
|
|
|
|
it('flags stagflation: rising inflation + contracting growth', () => {
|
|
const rows = buildImfEconomicIndicators(bundle({
|
|
macro: {
|
|
inflationPct: 12, currentAccountPct: null, govRevenuePct: null,
|
|
cpiIndex: null, cpiEopPct: null, govExpenditurePct: null, primaryBalancePct: null,
|
|
year: 2025,
|
|
},
|
|
growth: {
|
|
realGdpGrowthPct: -1.4, gdpPerCapitaUsd: null, realGdp: null,
|
|
gdpPerCapitaPpp: null, gdpPpp: null, investmentPct: null, savingsPct: null,
|
|
savingsInvestmentGap: null, year: 2025,
|
|
},
|
|
}));
|
|
const growth = rows.find(r => r.label === 'Real GDP Growth')!;
|
|
const infl = rows.find(r => r.label === 'CPI Inflation')!;
|
|
assert.equal(growth.value, '-1.4%');
|
|
assert.equal(growth.trend, 'down');
|
|
assert.equal(infl.value, '+12.0%');
|
|
assert.equal(infl.trend, 'down'); // >5% inflation flagged downward
|
|
});
|
|
|
|
it('marks high unemployment with a downward trend', () => {
|
|
const rows = buildImfEconomicIndicators(bundle({
|
|
labor: { unemploymentPct: 22.5, populationMillions: null, year: 2025 },
|
|
}));
|
|
const lur = rows.find(r => r.label === 'Unemployment')!;
|
|
assert.equal(lur.value, '22.5%');
|
|
assert.equal(lur.trend, 'down');
|
|
});
|
|
|
|
it('formats sub-$1k GDP/capita with the dollar prefix', () => {
|
|
const rows = buildImfEconomicIndicators(bundle({
|
|
growth: {
|
|
realGdpGrowthPct: null, gdpPerCapitaUsd: 850, realGdp: null,
|
|
gdpPerCapitaPpp: null, gdpPpp: null, investmentPct: null, savingsPct: null,
|
|
savingsInvestmentGap: null, year: 2025,
|
|
},
|
|
}));
|
|
const gdp = rows.find(r => r.label === 'GDP / Capita')!;
|
|
assert.equal(gdp.value, '$850');
|
|
});
|
|
|
|
it('skips rows whose values are null or non-finite', () => {
|
|
const rows = buildImfEconomicIndicators(bundle({
|
|
macro: {
|
|
inflationPct: NaN, currentAccountPct: null, govRevenuePct: null,
|
|
cpiIndex: null, cpiEopPct: null, govExpenditurePct: null, primaryBalancePct: null,
|
|
year: 2025,
|
|
},
|
|
}));
|
|
assert.equal(rows.length, 0);
|
|
});
|
|
});
|