Skip to content

feat: surface IMF fiscal indicators on country Economic Indicators card#3668

Merged
koala73 merged 4 commits into
mainfrom
feat/imf-fiscal-economic-indicators
May 12, 2026
Merged

feat: surface IMF fiscal indicators on country Economic Indicators card#3668
koala73 merged 4 commits into
mainfrom
feat/imf-fiscal-economic-indicators

Conversation

@koala73

@koala73 koala73 commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds Public Spending / Gov Revenue / Primary Balance rows to the Economic Indicators card on CountryDeepDivePanel
  • Sourced from already-seeded IMF GGX_NGDP / GGR_NGDP / GGXONLB_NGDP fields in economic:imf:macro:v2 — no new ingestion
  • Pure UI additive change; zero impact on resilience scoring or any data pipeline

Why

These series are pulled monthly by seed-imf-macro.mjs but no UI surface consumed them — they were orphan data in Redis. Plan #1 of a two-part fiscal-resilience initiative; Plan #2 (debtSustainabilityGap resilience indicator) is drafted in plans/add-debt-sustainability-gap-indicator.md and will ship as a separate PR.

Trend semantics

  • Public Spending / Gov Revenue: flat trend — level is descriptive context, not directional. Nordics sit at 50%+ public spending and score top-tier on stability; a "high = bad" arrow would be misleading.
  • Primary Balance: directional with IMF DSA noise-floor thresholds — > +1% → up, < -3% → down, else flat. A 0.3% surplus showing a green arrow would be noise.

Test plan

  • npx tsc --noEmit clean
  • node --import tsx --test tests/imf-country-data.test.mts → 12/12 pass (was 7; added 5 new tests + updated 1 label-list assertion to expect Gov Revenue row)
  • npx biome lint src/services/imf-country-data.ts tests/imf-country-data.test.mts clean
  • Manual: render CountryDeepDivePanel for France in the dev app, confirm three new rows appear with correctly formatted values (57.2% GDP, XX.X% GDP, ±X.X% GDP) alongside the existing GDP Growth / CPI Inflation / Unemployment / GDP-per-Capita rows

Post-Deploy Monitoring & Validation

No additional operational monitoring required. Purely additive rows on the existing Economic Indicators card; reads existing seeded economic:imf:macro:v2 fields with null-skip guards. No new network calls, no new error paths, no new failure modes, no resilience-score impact.

Rollback: revert the PR — Economic Indicators card returns to its previous 4 rows; no data cleanup, no cache invalidation needed.

Observable behavior: yes — three new rows visible on CountryDeepDivePanel's Economic Indicators card for countries with IMF WEO coverage (≈190 countries). Demo evidence skipped for this PR; rows are testable locally with any IMF-WEO-covered country.

…dicators card

Adds three orphan-data rows to buildImfEconomicIndicators sourced from
already-seeded IMF GGX_NGDP / GGR_NGDP / GGXONLB_NGDP series:

- Public Spending (govExpenditurePct, flat trend — level is descriptive,
  Nordics at 50%+ are stable)
- Gov Revenue (govRevenuePct, flat trend)
- Primary Balance (primaryBalancePct, directional trend with IMF DSA
  noise-floor thresholds: > +1 → up, < -3 → down)

Pattern mirrors the existing GDP Growth / Inflation / Unemployment /
GDP-per-Capita rows in the same function. Renders on CountryDeepDivePanel
only. Zero impact on resilience scoring or data pipeline.

Resolves the "ingest-but-not-render" gap on fiscal series we already
pay to fetch every month via seed-imf-macro.mjs.
@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview May 12, 2026 6:18am

Request Review

@greptile-apps

greptile-apps Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Surfaces three previously-orphaned IMF WEO fiscal fields — Public Spending, Gov Revenue, and Primary Balance — as new rows on the CountryDeepDivePanel Economic Indicators card, reading already-seeded economic:imf:macro:v2 data with no pipeline changes.

  • src/services/imf-country-data.ts: Appends three out.push(…) blocks to buildImfEconomicIndicators; spending and revenue are intentionally flat (level-descriptive, not directional), while Primary Balance uses IMF DSA noise-floor thresholds (> +1 → up, < -3 → down, else flat) with a signed ±X.X% GDP format.
  • tests/imf-country-data.test.mts: Adds five targeted unit tests covering the new rows, flat-trend semantics, signed formatting, and the surplus/deficit directional thresholds; one existing label-list assertion updated to expect the Gov Revenue row.

Confidence Score: 4/5

Safe to merge — purely additive UI rows reading already-seeded data with correct null/NaN guards and well-reasoned trend thresholds.

The implementation is straightforward and the null/finite guards are applied consistently across all three new fields. The only gaps are missing boundary tests at primaryBalance === -3.0 and primaryBalance === 1.0, and the existing NaN-guard test not exercising the new fields — both are minor test coverage holes with no impact on production behavior.

tests/imf-country-data.test.mts — boundary and NaN-guard coverage for the new Primary Balance thresholds.

Important Files Changed

Filename Overview
src/services/imf-country-data.ts Adds Public Spending, Gov Revenue, and Primary Balance rows to buildImfEconomicIndicators; null/NaN guards are correct, trend thresholds match documented IMF DSA semantics, and the signed-value formatter handles zero correctly.
tests/imf-country-data.test.mts Five new focused tests added for the three fiscal rows; exact boundary values at -3.0 and +1.0 for Primary Balance are not covered, and the non-finite guard test is not exercised for the new fields.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildImfEconomicIndicators\nbundle: ImfCountryBundle] --> B{bundle.growth?\n.realGdpGrowthPct}
    B -- not null & finite --> C[Real GDP Growth row\ntrend: >0.5 up / <-0.5 down / flat]
    B -- null/NaN --> D{bundle.macro?\n.inflationPct}
    C --> D
    D -- not null & finite --> E[CPI Inflation row\ntrend: >5 down / <1 flat / else up]
    D -- null/NaN --> F{bundle.labor?\n.unemploymentPct}
    E --> F
    F -- not null & finite --> G[Unemployment row\ntrend: >10 down / <5 up / flat]
    F -- null/NaN --> H{bundle.growth?\n.gdpPerCapitaUsd}
    G --> H
    H -- not null & finite --> I[GDP / Capita row\ntrend: flat]
    H -- null/NaN --> J{bundle.macro?\n.govExpenditurePct}
    I --> J
    J -- not null & finite --> K[Public Spending row\ntrend: flat - NEW]
    J -- null/NaN --> L{bundle.macro?\n.govRevenuePct}
    K --> L
    L -- not null & finite --> M[Gov Revenue row\ntrend: flat - NEW]
    L -- null/NaN --> N{bundle.macro?\n.primaryBalancePct}
    M --> N
    N -- not null & finite --> O{primaryBalance\nthreshold check}
    N -- null/NaN --> P[return out]
    O -- greater than +1 --> Q[trend: up - NEW]
    O -- less than -3 --> R[trend: down - NEW]
    O -- else --> S[trend: flat - NEW]
    Q --> P
    R --> P
    S --> P
Loading

Comments Outside Diff (1)

  1. tests/imf-country-data.test.mts, line 174-183 (link)

    P2 NaN-guard test does not cover the three new fields

    The skips rows whose values are null or non-finite test only exercises inflationPct: NaN, leaving govExpenditurePct, govRevenuePct, and primaryBalancePct untested with non-finite values. While the Number.isFinite guard in the implementation is correct, the test will not catch a future regression that accidentally drops the guard on one of the new fields.

Reviews (1): Last reviewed commit: "feat(country-deep-dive): surface IMF fis..." | Re-trigger Greptile

Comment on lines +161 to +172
it('marks severe primary deficit (<-3) with a downward trend', () => {
const rows = buildImfEconomicIndicators(bundle({
macro: {
inflationPct: null, currentAccountPct: null, govRevenuePct: null,
cpiIndex: null, cpiEopPct: null, govExpenditurePct: null, primaryBalancePct: -4.0,
year: 2024,
},
}));
const pb = rows.find(r => r.label === 'Primary Balance')!;
assert.equal(pb.value, '-4.0% GDP');
assert.equal(pb.trend, 'down');
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Boundary value at exactly -3.0 is untested

The primaryBalance < -3 ? 'down' : 'flat' threshold means -3.0 exactly renders as flat, yet there is no test for it. The PR covers -1.0 (flat) and -4.0 (down) but skips the exact boundary, so a future refactor changing < to <= would go undetected. Adding a case for primaryBalancePct: -3.0 expecting flat would pin this boundary. Similarly, primaryBalancePct: 1.0 (upper boundary, should be flat) has no coverage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b44e78601 — added boundary tests for primaryBalancePct: 1.0 and primaryBalancePct: -3.0 (both expected flat), plus extended the non-finite guard test to put NaN/Infinity into all 7 IMF fields (govExpenditurePct, govRevenuePct, primaryBalancePct, realGdpGrowthPct, gdpPerCapitaUsd, unemploymentPct, currentAccountPct) and assert 0 rows. Tests: 13 → 15.

…es 6-row slice

Caller's Economic Indicators card caps at 6 rows
(src/app/country-intel.ts:1288). Previous push order emitted Primary
Balance as the 7th IMF row (after the 3 flat context rows), guaranteeing
that the most informative directional fiscal signal was the first to be
sliced off — exactly the opposite of intent.

New order, mirroring "directional first, flat context last":

  Real GDP Growth  →  CPI Inflation  →  Unemployment  →  Primary Balance
  →  GDP / Capita  →  Public Spending  →  Gov Revenue

For a country with no stock/score rows, Primary Balance now lands at
slot 4 and survives the slice cleanly. For full-coverage countries
(stock + momentum + score + macro), the 6-row cap still drops some IMF
rows, but the directional macro signals win priority over flat context.

Adds an explicit ordering test that asserts the full 7-row sequence.
Existing tests unaffected — they use `.find()` to locate rows and the
one label-list assertion happens to not exercise primaryBalance.
…icIndicators

External reviewer caught that two cap sites gate visibility of the
Economic Indicators card, not just the country-intel.ts one I cited in
the prior commit:

  - src/app/country-intel.ts:1288 — caps the combined producer output
  - src/components/CountryDeepDivePanel.ts:2240 — re-caps after the
    asynchronous Stock Index prepend in updateStock

For full-coverage countries (stock + score + 7 IMF rows), the two caps
combine to drop 3-4 IMF rows. The reorder (prior commit) ensures the
4 directional rows — growth / inflation / unemployment / primary
balance — win priority over the 3 flat context rows. Behavior is
correct; bumping either cap is a follow-up if more rows are desired.

Comment-only change — no behavioral or test impact.
…ge gaps

Two pure test additions addressing PR #3668 Greptile review findings.
No production code changed.

1. Boundary tests at the exact threshold values for Primary Balance:
   - primaryBalancePct: 1.0   → trend: flat (pins `> 1` inclusivity)
   - primaryBalancePct: -3.0  → trend: flat (pins `< -3` inclusivity)
   A future refactor flipping `>` to `>=` (or `<` to `<=`) now fails
   tests immediately.

2. Extended the non-finite guard test to cover all 7 IMF fields, not
   just inflationPct. Puts NaN/Infinity into govExpenditurePct,
   govRevenuePct, primaryBalancePct, realGdpGrowthPct, gdpPerCapitaUsd,
   unemploymentPct, and currentAccountPct, asserting all rows skip.
   Catches a future regression where the Number.isFinite guard is
   accidentally dropped on one of the new fiscal fields.

Tests: 13 → 15 (all pass). tsc clean, biome lint clean.
@koala73
koala73 merged commit 631b4fa into main May 12, 2026
11 checks passed
@koala73
koala73 deleted the feat/imf-fiscal-economic-indicators branch May 12, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant