Single source of truth for live IMF / SCB economic data, Chart.js visualisations, and AI commentary in every news article. Consumed by
scripts/validate-economic-context.tsand referenced (by link) from everynews-*.mdagentic workflow.
Schema v3.1 (2026-05-10) — IMF is the economic-data source for every economic claim. Every economic indicator (GDP, inflation, unemployment, fiscal aggregates, debt, BoP, trade flows, commodity prices, FX, interest rates) carries
provider: "imf"and an IMF database tag (WEO,FM,IFS,BOP,GFS_COFOG,DOTS,PCPS,MFS_IR,ER). SCB supplies Swedish-specific ground truth (AKU labour-force survey, KPIF, regional and high-frequency monthly tables). World Bank supplies non-economic context: governance (WGI,source=75), environment, social/education residue, defence historicals, and crime/justice. Vintage discipline: economic data older than 6 months carries an explicit annotation.v3.1 (2026-05-10) — IMF Data SDMX API now requires the
Ocp-Apim-Subscription-KeyAzure APIM header on every/data/...request to the SDMX 3.0 surface (api.imf.org/external/sdmx/3.0) — the only IMF SDMX surface this repo targets. All 14 news workflows forwardsecrets.IMF_SDMX_SUBSCRIPTION_KEY(primary, required) to thenews-prewarmcomposite action;IMF_SDMX_SUBSCRIPTION_KEY_SECONDARYis the optional rotation key (stored only, not consumed by code). The Datamapper transport (WEO + FM) remains unauthenticated and is unaffected. Seeanalysis/imf/agentic-integration.md§"Pre-warm gate" for forwarding mechanics and the rotation playbook.v3.2 (2026-05-10) — SDMX 2.1 transport removed from the contract. The client (
scripts/imf-client.ts::sdmxFetch) auto-rewrites the human-readable comma-form path (/data/AGENCY,FLOW,VERSION/key) into the SDMX 3.0 slash-form (/data/dataflow/AGENCY/FLOW/VERSION/key) before sending — so docs, CLI examples, andweoSdmxPath()keep the comma form for readability while the wire request always targets SDMX 3.0.
The April 17 2026 committee-reports article shipped a bullet-list
"Economic Context" placeholder because the rendering pipeline had no
supplied dataPoints and the bullet-only fallback was emitted. This
contract closes the gap so that:
- Every
<section id="economic-dashboard">renders real Chart.js canvases backed by live IMF / SCB values (with World Bank residue for non-economic context only). - Every article carries a 2–4 sentence AI commentary paragraph that cites concrete numbers.
- A deterministic quality gate fails the PR if any of data, chart, or commentary is missing.
Every agentic workflow MUST write:
analysis/daily/YYYY-MM-DD/{analysisSubfolder}/economic-data.json
where {analysisSubfolder} maps from the kebab article-type slug via
scripts/analysis-references.ts → ARTICLE_TYPE_TO_ANALYSIS_SUBFOLDER:
| Article type slug | analysisSubfolder |
|---|---|
committee-reports |
committeeReports |
propositions |
propositions |
motions |
motions |
interpellations |
interpellations |
evening-analysis |
evening-analysis |
realtime-monitor |
realtime-monitor |
breaking |
breaking |
week-ahead |
week-ahead |
month-ahead |
month-ahead |
weekly-review |
weekly-review |
monthly-review |
monthly-review |
deep-inspection |
deep-inspection |
Schema: analysis/schemas/economic-data.schema.json.
Shape (v2.0 — additive over v1):
Hard rules:
dataPointsMUST be non-empty unlessskip: true(and the article type is on the allow-list).commentaryMUST cite 2–3 concrete numeric values that appear indataPoints(the validator enforces only a word count; the human review + multi-dim quality score enforces the citation).- At least one of
source.imf/source.scbMUST be non-empty for every economic-context article.source.worldBankis the appropriate field for governance / environment / social / defence-historicals / crime data points. - Every
dataPointwhose policy domain is in{macro, fiscal, monetary, external, trade, inflation, commodity, exchangeRate}carriesprovider: "imf"(orprovider: "scb"for Swedish-specific ground truth). The validator confirms each row in those domains uses an IMF or SCB provider. - When any
dataPoint.projection === true, it MUST carry aprojectionVintagetag (e.g."WEO-2026-04","FM-2026-04"). Projection values MAY only be cited in article commentary for the look-ahead allow-list:week-ahead,month-ahead,weekly-review,monthly-review. Daily types (committee-reports,propositions,motions,interpellations,evening-analysis) MAY include projection values indataPointsfor context but MUST NOT quote them as definitive forward-looking claims in commentary. - File MUST validate against
analysis/schemas/economic-data.schema.json.
Step 2.6 of every news-*.md workflow MUST perform these MCP calls
before writing economic-data.json. Provider precedence is:
- IMF — every economic claim. Macro (GDP, inflation, unemployment), fiscal (debt, deficit, revenue, expenditure, COFOG functional decomposition), monetary, external sector, trade, commodity, FX. WEO projections extend to T+5 and MUST be used for look-ahead article types.
- SCB — Swedish-specific ground truth that IMF does not carry (household consumption patterns, regional data, high-frequency monthly tables, AKU labour-force survey).
- World Bank — non-economic residue only: governance (WGI:
CC.EST, RL.EST, VA.EST, GE.EST, RQ.EST, PV.EST), environment (CO2,
forest area, renewables, water), defence historicals
(
MS.MIL.XPND.GD.ZS,MS.MIL.TOTL.P1), and long-horizon social/education indicators IMF does not cover.
view analysis/economic-indicators-inventory.json
Each indicator entry carries a provider field (imf | worldBank |
scb), plus IMF-specific fields (imfDatabase, imfIndicatorCode,
imfDimensionFilters, projectionHorizon) when the primary provider
is IMF. Select every indicator whose committees / policyAreas
match the day's source documents. The non-economic residue inventory
at analysis/worldbank/indicators-inventory.json covers governance /
environment / social / defence historicals only.
Via the repository's pure-TypeScript client scripts/imf-client.ts,
exposed to agentic workflows through the thin scripts/imf-fetch.ts
CLI. No Python MCP / uvx runtime is involved; all IMF traffic goes
directly to data.imf.org, api.imf.org, and www.imf.org on the
firewall allowlist.
# 1. WEO time series for one country (default 10 years; --persist caches
# the JSON under analysis/data/imf/{indicator}/{country}.json with
# projectionVintage provenance).
tsx scripts/imf-fetch.ts weo \
--country SWE --indicator NGDP_RPCH --years 15 --persist
# 2. Compare the latest WEO value across Sweden + Nordic peers in one call.
tsx scripts/imf-fetch.ts compare \
--indicator GGXWDG_NGDP \
--countries SWE,DNK,NOR,FIN,DEU --persist
# 3. SDMX 3.0 passthrough for IFS / BOP / FM / GFS / DOTS.
tsx scripts/imf-fetch.ts sdmx \
--path "/data/IMF.STA,CPI,5.0.0/SWE.CPI._T.IX.M?startPeriod=2024-01" \
--indicator _T.IX --country SWE --persist
# 4. Inspect the built-in WEO + FM indicator catalog (no network call).
tsx scripts/imf-fetch.ts list-indicatorsRate-limit discipline (IMF ~10 req / 5 s): prefer the compare
subcommand (one batched call across countries), insert a 1 s sleep
between separate imf-fetch.ts invocations, and rely on the client's
built-in 3× retry with exponential back-off (1 s → 2 s → 4 s) for 429 /
5xx. Pre-warm 1 request at workflow start.
Scope — World Bank supplies non-economic context: governance (WGI
source=75), environment (CO₂, forest area, renewables, water), social/education residue (population, life expectancy, school enrolment), defence historicals (MS.MIL.XPND.GD.ZS,MS.MIL.TOTL.P1), and crime/justice (VC.IHR.PSRC.P5). Usescripts/imf-fetch.tsfor every economic claim; usescripts/scb-client.tsfor Swedish-specific ground truth.
# Governance / WGI (source=75)
get-economic-data(countryCode="SE", indicator="CC.EST", years=5) # Control of Corruption
get-economic-data(countryCode="SE", indicator="RL.EST", years=5) # Rule of Law
get-economic-data(countryCode="SE", indicator="VA.EST", years=5) # Voice & Accountability
# Environment
get-economic-data(countryCode="SE", indicator="EN.ATM.CO2E.PC", years=10) # CO2 emissions per capita
get-economic-data(countryCode="SE", indicator="EG.FEC.RNEW.ZS", years=10) # Renewables share
get-economic-data(countryCode="SE", indicator="AG.LND.FRST.ZS", years=10) # Forest cover
# Defence historicals (use instead of COFOG 02 when 5+ year trend needed)
get-economic-data(countryCode="SE", indicator="MS.MIL.XPND.GD.ZS", years=15)
# Education participation
get-education-data(countryCode="SE", indicator="SCHOOL_ENROLLMENT", years=10)
# Social / demographic context (WB preferred for long-horizon > 15 years)
get-social-data(countryCode="SE", indicator="POPULATION", years=10)
# CRITICAL: language MUST be "sv" or "en". NEVER "no" — SCB returns
# HTTP 400 "Unsupported language" for "no".
search_tables(query="<committee-topic>", language="en")
# Then fetch specific tables from the committee→TAB mapping
# (scripts/scb-context.ts):
# FiU → TAB1291 AU → TAB5765 JuU → TAB1172
# MJU → TAB5404 SoU → health tables UbU → education tables
query_table(table_id="<TAB>", value_codes={"Tid": "top(10)", ...})
| Committee | Primary provider(s) | MUST query (provider:code) |
|---|---|---|
| FiU | IMF + WB | imf:WEO:NGDP_RPCH, imf:WEO:PCPIPCH, imf:WEO:NGDPDPC, imf:WEO:GGXWDG_NGDP |
| SkU | IMF + WB | imf:WEO:GGR_NGDP, imf:WEO:GGX_NGDP, imf:WEO:GGXCNL_NGDP, imf:FM:GGXONLB_NGDP |
| AU | IMF + WB | imf:WEO:LUR, wb:SL.UEM.1524.ZS, wb:SL.TLF.CACT.ZS |
| NU / UU | IMF | imf:WEO:BCA_NGDPD, imf:WEO:TX_RPCH |
| SoU | WB | wb:SH.XPD.CHEX.GD.ZS, wb:SH.MED.PHYS.ZS, wb:SH.MED.BEDS.ZS, wb:SP.DYN.LE00.IN |
| UbU | WB | wb:SE.XPD.TOTL.GD.ZS, wb:SE.ADT.LITR.ZS, wb:SE.PRM.ENRR |
| FöU | WB | wb:MS.MIL.XPND.GD.ZS, wb:MS.MIL.XPND.CD |
| MJU | WB | wb:EN.ATM.CO2E.PC, wb:EG.FEC.RNEW.ZS, wb:AG.LND.FRST.ZS |
| KU | WB | wb:CC.EST, wb:RL.EST, wb:VA.EST (WGI, source=75) |
| JuU | WB | wb:VC.IHR.PSRC.P5, wb:CC.EST |
Article types: week-ahead, month-ahead, weekly-review,
monthly-review. In addition to economic-data.json, produce a
coalition-flow.json with nodes[]/flows[] consumed by
generateSankeySection. Minimum 2 non-trivial flows.
scripts/validate-economic-context.ts reads this matrix. Changes here
must be mirrored in the validator's COVERAGE_MATRIX.
| Article type | Min Chart.js canvases | Min commentary words | D3 required | May skip |
|---|---|---|---|---|
| committee-reports | 2 | 60 | No | No |
| propositions | 2 | 60 | No | No |
| motions | 1 | 40 | No | No |
| interpellations | 1 | 40 | No | No |
| evening-analysis | 1 | 40 | No | No |
| realtime-monitor | 1 | 30 | No | Yes |
| breaking | 1 | 30 | No | Yes |
| week-ahead | 2 | 80 | Optional | No |
| month-ahead | 3 | 100 | Recommended | No |
| weekly-review | 3 | 150 | Required | No |
| monthly-review | 4 | 200 | Required | No |
| deep-inspection | 1 | 40 | No | Yes |
| article-generator | 1 | 40 | No | Yes |
"May skip" = economic-data.json may be written with skip: true and
skipReason when the article is a pure-process piece (e.g. "Riksdag
voting schedule changed"). The validator enforces the allow-list;
workflows MUST NOT use skip as a shortcut to avoid fetching data.
The aggregate+render pipeline (scripts/aggregate-analysis.ts →
scripts/render-articles.ts → scripts/render-lib/) handles all
client-side wiring. Whenever the rendered HTML contains a Mermaid
fenced block or a Chart.js / D3 canvas, the renderer auto-injects the
appropriate runtime via scripts/render-lib/chrome.ts:
<script type="module" src="../js/lib/mermaid-init.mjs"></script>— emitted whenever any<pre class="mermaid">survivesrehype-sanitize<script src="../js/lib/chart.umd.4.4.1.js"></script>— Chart.js 4<script src="../js/lib/chartjs-plugin-annotation.3.0.1.min.js">— added when any config uses"annotations"/"annotation"<script src="../js/chart-init.js"></script>— scans[data-chart-config]canvases on DOMContentLoaded and callsnew Chart(ctx, cfg)for each<script src="../js/lib/d3.7.9.0.min.js"></script>— added when any section usesdata-d3-sankey=
Consequences for the AI agent writing analysis artifacts:
- DO drop ```mermaid fenced blocks straight into your
.mdartifacts under `analysis/daily/$DATE/$SUB/`. The renderer's allow-list lets `` pass through `rehype-sanitize`; `mermaid-init.mjs` handles render in the browser.
- DO include
<canvas data-chart-config="…">markup inside fenced HTML blocks for richer dashboards. - DO NOT hand-roll inline
<script src="/js/lib/chart.umd.*.js">blocks — the renderer adds runtimes once, in the right order; duplicates can double-render the same canvas. - DO NOT reference
chart-init.jsormermaid-init.mjsmanually; the renderer includes them when (and only when) the corresponding markup is present.
All five vendor libraries (chart.umd.4.4.1.js, the annotation plugin,
d3.7.9.0.min.js, papaparse.5.5.3.min.js,
chartjs-adapter-date-fns.3.0.0.bundle.min.js) ship from
js/lib/ → dist/js/lib/ → S3 via .github/workflows/deploy-s3.yml
step "Copy JS libraries to build output" — no deployment change is
required per article.
# In every news-*.md, immediately after Step 2.5 pre-article analysis:
ANALYSIS_DIR="analysis/daily/$ARTICLE_DATE/$ANALYSIS_SUBFOLDER"
mkdir -p "$ANALYSIS_DIR"
# Agent runs the MCP calls above, accumulates dataPoints, drafts
# commentary (Step 3d), and writes:
cat > "$ANALYSIS_DIR/economic-data.json" <<EOF
{
"version": "1.0",
"articleType": "committee-reports",
"date": "$ARTICLE_DATE",
"policyDomains": [...],
"dataPoints": [...],
"commentary": "...",
"source": { "worldBank": [...], "scb": [...] }
}
EOFCache raw MCP responses by provider:
- IMF:
analysis/data/imf/$(date +%Y)/$indicator-$country.json - SCB:
analysis/data/scb/$(date +%Y)/$table.json - World Bank (non-economic residue only):
analysis/data/worldbank/$(date +%Y)/$indicator-$country.json
Reuse across article types in the same daily run (rate-limit mitigation).
After writing economic-data.json, re-open it and replace commentary
with a 2–4 sentence paragraph that:
- cites 2–3 concrete numeric values from
dataPoints, - ties the numbers to the day's political developments,
- is written in plain English (translated to other languages by the existing translation post-process — never hand-write translations).
Banned phrasings (all detected by multi-dim quality score):
- "The political landscape remains fluid…"
- "Touches on X policy…"
- "Analysis of N documents…"
- Pure definitions of indicators (e.g. "GDP is the total output of…").
- Un-sourced forecasts — phrases like "Sweden will…" / "The economy is
expected to…" / "growth is forecast to…" / "analysts expect…" without a
cited IMF WEO or Fiscal Monitor value from
dataPointswhereprojection: true. Use the explicit form "IMF projects Sweden's debt/GDP at 32.4 % in 2027 (WEO Apr-2026, GGXWDG_NGDP)" instead.
Every economic claim cites the IMF dataflow + indicator code. Use:
WEO:NGDP_RPCHfor real GDP growth ·WEO:NGDPDfor nominal GDP ·WEO:NGDPDPCfor GDP per capita.WEO:PCPIPCHfor annual CPI inflation ·CPI:_T.IXfor monthly CPI · SCB KPIF for Swedish-specific high-frequency inflation.WEO:LURfor the annual unemployment rate · SCB AKU for the Swedish-specific labour-force survey.FM:GGXWDG_NGDP(orWEO:GGXWDG_NGDP) for gross public debt ·WEO:GGXCNL_NGDP/FM:GGXCNLB_NGDPfor fiscal balance ·FM:GGXONLB_NGDPfor primary balance ·FM:GGSB_NPGDPfor the cyclically-adjusted balance.WEO:GGR_NGDPfor revenue / GDP ·WEO:GGX_NGDPfor expenditure / GDP ·GFS_COFOG:GF02_T / GF07_T / GF09_T / GF10_Tfor COFOG functional decomposition.WEO:BCA_NGDPDfor current account ·BOP:*for BoP detail ·WEO:TX_RPCH/WEO:TM_RPCHfor export/import volume growth ·IMTS:XG_FOB_USD,IMTS:MG_CIF_USDfor bilateral trade flows.MFS_IR:MMRT_RT_PT_A_PTfor the Riksbank policy rate ·ER:USD_XDC.PA_RT/ER:EUR_XDC.PA_RTfor exchange rates ·PCPS:POILBRE/PCPS:PALLFNFfor commodity prices.
World Bank citations remain authoritative for non-economic context:
WGI governance (*.EST), environment (EN.*, EG.*, AG.LND.FRST.ZS),
defence historicals (MS.MIL.*), social/education residue (SP.*,
SH.*, SE.*), and crime/justice (VC.IHR.PSRC.P5).
From schema v2 onwards, dataPoint.projection === true values may be
cited in commentary only for these article types:
week-ahead,month-ahead— explicit forward-looking narrativeweekly-review,monthly-review— retrospective + 6-12 month outlook
Any projection citation MUST include the projectionVintage tag
(e.g. (WEO-2026-04)) to make stale vintages visible to the audit.
A projection citation whose projectionVintage is more than 6 months
older than the article's publication date is flagged as a stale
vintage in methodology-reflection.md with a [STALE-VINTAGE] tag:
- Article date 2026-04-24 · vintage
WEO-2026-04→ OK (same cycle). - Article date 2026-04-24 · vintage
WEO-2025-10→ OK (< 6 months). - Article date 2026-04-24 · vintage
WEO-2025-04→ [STALE-VINTAGE] — re-fetch.
On each IMF flagship release (April / October), update DEFAULT_WEO_VINTAGE
in scripts/imf-client.ts, vintageDiscipline.current in
analysis/imf/indicators-inventory.json, and the banner in
analysis/imf/README.md in the same PR.
scripts/validate-economic-context.ts is invoked from
scripts/validate-news-generation.sh (Check 21) and runs
non-interactively in CI. It fails non-zero when any article
commits violations:
class="economic-dashboard-placeholder"in HTML.- Fewer than
minChartsdata-chart-config=canvases. - Missing / empty / malformed
economic-data.json. dataPointsempty (without a validskip: true).- Commentary below
minCommentaryWords. - Missing footer attribution
Data by IMF / SCB(orData by IMF / SCB / World Bankwhen non-economic WB data is also cited in the article).
The validator only enforces the contract on articles published on or
after CONTRACT_EFFECTIVE_DATE (2026-04-18, the first full day the
v1.0 contract + loader + validator were authoritative). Articles dated
earlier are skipped — they legitimately predate the schema and cannot
be retroactively populated with real World Bank / SCB data without
re-running MCP queries against a historical snapshot.
The cut-off also prevents the daily audit
(.github/workflows/economic-context-audit.yml) from re-surfacing the
same pre-contract articles every day for the 7-day lookback window
after a rollout. To bump the cut-off, update
CONTRACT_EFFECTIVE_DATE in scripts/validate-economic-context.ts and
mirror the change in the version history below.
| Risk | Mitigation |
|---|---|
| MCP cold-start latency on Render.com | 6× pre-warm in mcp-setup.sh + 3× retry inside Step 2.6 |
| World Bank rate-limiting | Cache raw responses under analysis/data/worldbank/$year/ for re-use across article types in the same day |
SCB no-language 400 |
language param MUST be "sv" or "en". Workflows lint against language: "no" calls. |
| Placeholder silently shipping | generateEconomicDashboardSection({ dataPoints: [] }) now returns null; validator enforces on HTML and JSON sides. |
- 3.0 (2026-04-28) — IMF is the economic-data source for every
economic claim.
INDICATOR_IDS(scripts/world-bank-client.ts) andanalysis/worldbank/indicators-inventory.jsonv4.0 catalogue the World Bank's non-economic scope: governance, environment, social, defence historicals, demographics, health, education, innovation, infrastructure, inequality, gender, energy use. Footer attribution isData by IMF / SCBfor economic-context articles andData by IMF / SCB / World Bankwhen an article also cites governance, environment, or social World Bank data. - 2.1 (2026-04-24) — IMF promoted to primary across all economic domains. Vintage-staleness rule (6-month threshold) added. Step 3 reframed as governance / environment / social residue.
- 2.0 (2026-04-20) — Add IMF as a first-class primary source via
the repository's pure-TypeScript
scripts/imf-client.ts+scripts/imf-fetch.tsCLI (Datamapper JSON for WEO, SDMX 3.0 passthrough for IFS / BOP / FM / GFS / DOTS). No Python MCP /uvx— IMF access is under the same npm / SBOM governance asworld-bank-client.tsandscb-client.ts. Additive schema changes:source.imf: string[]dataPoints[].provider(imf|worldBank|scb)dataPoints[].projection(boolean)dataPoints[].projectionVintage(string; required whenprojection: true) Validator accepts IMF in the footer attribution. Projections are allowed in commentary for look-ahead article types (week-ahead,month-ahead,weekly-review,monthly-review) only.
- 1.0.1 (2026-04-18) — Added
CONTRACT_EFFECTIVE_DATE = 2026-04-18exemption to the validator so the daily audit stops re-reporting pre-contract articles for 7 days after every rollout. No change to the schema or enforcement for new articles. - 1.0 (2026-04-17) — Initial contract following the April 17 committee-reports placeholder incident.
{ "version": "2.0", "articleType": "committee-reports", "date": "2026-04-20", "policyDomains": ["fiscal policy", "labor market"], "dataPoints": [ // IMF (WEO projections permitted for look-ahead article types) { "countryCode": "SWE", "countryName": "Sweden", "indicatorId": "NGDP_RPCH", "date": "2025", "value": 1.9, "provider": "imf", "projection": false }, { "countryCode": "SWE", "countryName": "Sweden", "indicatorId": "GGXWDG_NGDP", "date": "2027", "value": 32.4, "provider": "imf", "projection": true, "projectionVintage": "WEO-2026-04" }, // World Bank (governance / environment / social residue) { "countryCode": "SWE", "countryName": "Sweden", "indicatorId": "CC.EST", "date": "2023", "value": 2.1, "provider": "worldBank", "projection": false }, // SCB (Swedish primary source) { "countryCode": "SWE", "countryName": "Sweden", "indicatorId": "TAB1291", "date": "2025-02", "value": 405.2, "provider": "scb", "projection": false } ], "commentary": "IMF projects Sweden's general government gross debt at 32.4 % of GDP in 2027 (WEO Apr-2026, GGXWDG_NGDP), giving SkU-2025/26:12 fiscal headroom absent in 2022 (38 %). Control-of-corruption remains strong at 2.1 (WGI 2023).", "source": { "worldBank": ["CC.EST"], "scb": ["TAB1291"], "imf": ["WEO:NGDP_RPCH", "WEO:GGXWDG_NGDP"] } }