euparliamentmonitor 0.8.25 → 0.8.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -162,7 +162,7 @@
162
162
  "prettier": "3.8.2",
163
163
  "ts-api-utils": "2.5.0",
164
164
  "tsx": "4.21.0",
165
- "typedoc": "0.28.18",
165
+ "typedoc": "0.28.19",
166
166
  "typescript": "6.0.2",
167
167
  "vitest": "4.1.4"
168
168
  },
@@ -170,7 +170,7 @@
170
170
  "node": ">=25"
171
171
  },
172
172
  "dependencies": {
173
- "european-parliament-mcp-server": "1.2.2"
173
+ "european-parliament-mcp-server": "1.2.4"
174
174
  },
175
175
  "optionalDependencies": {
176
176
  "worldbank-mcp": "1.0.1"
@@ -10,12 +10,31 @@
10
10
  *
11
11
  * Used by news generation strategies to automatically select the right
12
12
  * economic indicators when enriching articles with World Bank data.
13
+ *
14
+ * ## ⚠️ For AI Agents / Agentic Workflows
15
+ *
16
+ * The 34 indicators in {@link WB_INDICATORS} are a **curated subset** mapped
17
+ * to EP committee mandates. The full World Bank catalog has **thousands** of
18
+ * indicators. For dynamic indicator discovery:
19
+ *
20
+ * 1. Use `search-indicators` MCP tool to find indicators by keyword on demand
21
+ * 2. Read `analysis/worldbank/indicator-catalog.md` for the full **200+ indicator**
22
+ * reference organized by 10 EP policy domains
23
+ * 3. Use `analysis/worldbank/use-cases.md` to decide when indicators add editorial value
24
+ * 4. Use `analysis/worldbank/chart-integration-guide.md` for visualization templates
25
+ *
26
+ * **Never assume the indicators below are exhaustive.** Always consider whether
27
+ * `search-indicators` would reveal a more specific indicator for the policy topic.
13
28
  */
14
29
  import { ArticleCategory, AnalysisPerspective } from '../types/common.js';
15
30
  /**
16
31
  * Extended set of World Bank indicator IDs relevant to EU Parliament policy areas.
17
32
  * Superset of {@link PolicyRelevantIndicators} — includes domain-specific indicators
18
33
  * that map to individual committee mandates.
34
+ *
35
+ * ⚠️ **AI Agents**: This is a **curated subset of 34 indicators**. The World Bank
36
+ * has thousands more. Use `search-indicators` MCP tool for on-demand discovery.
37
+ * Full reference: `analysis/worldbank/indicator-catalog.md` (200+ indicators).
19
38
  */
20
39
  export declare const WB_INDICATORS: {
21
40
  /** GDP (current US$) */
@@ -12,6 +12,21 @@
12
12
  *
13
13
  * Used by news generation strategies to automatically select the right
14
14
  * economic indicators when enriching articles with World Bank data.
15
+ *
16
+ * ## ⚠️ For AI Agents / Agentic Workflows
17
+ *
18
+ * The 34 indicators in {@link WB_INDICATORS} are a **curated subset** mapped
19
+ * to EP committee mandates. The full World Bank catalog has **thousands** of
20
+ * indicators. For dynamic indicator discovery:
21
+ *
22
+ * 1. Use `search-indicators` MCP tool to find indicators by keyword on demand
23
+ * 2. Read `analysis/worldbank/indicator-catalog.md` for the full **200+ indicator**
24
+ * reference organized by 10 EP policy domains
25
+ * 3. Use `analysis/worldbank/use-cases.md` to decide when indicators add editorial value
26
+ * 4. Use `analysis/worldbank/chart-integration-guide.md` for visualization templates
27
+ *
28
+ * **Never assume the indicators below are exhaustive.** Always consider whether
29
+ * `search-indicators` would reveal a more specific indicator for the policy topic.
15
30
  */
16
31
  import { ArticleCategory, AnalysisPerspective } from '../types/common.js';
17
32
  // ─── Priority Constants ──────────────────────────────────────────────────────
@@ -61,6 +76,10 @@ const N = {
61
76
  * Extended set of World Bank indicator IDs relevant to EU Parliament policy areas.
62
77
  * Superset of {@link PolicyRelevantIndicators} — includes domain-specific indicators
63
78
  * that map to individual committee mandates.
79
+ *
80
+ * ⚠️ **AI Agents**: This is a **curated subset of 34 indicators**. The World Bank
81
+ * has thousands more. Use `search-indicators` MCP tool for on-demand discovery.
82
+ * Full reference: `analysis/worldbank/indicator-catalog.md` (200+ indicators).
64
83
  */
65
84
  export const WB_INDICATORS = {
66
85
  // ── Macro-economic ──
@@ -1072,15 +1072,75 @@ const TIMEFRAME_FALLBACK_CHAIN = new Map([
1072
1072
  function getWiderTimeframe(current) {
1073
1073
  return TIMEFRAME_FALLBACK_CHAIN.get(current);
1074
1074
  }
1075
+ /**
1076
+ * Error thrown when the EP MCP server returns a timeout envelope instead of data.
1077
+ * This is distinct from network-level timeouts — the MCP server responds successfully
1078
+ * but reports that the upstream EP API did not respond in time.
1079
+ */
1080
+ class UpstreamTimeoutError extends Error {
1081
+ constructor(toolName) {
1082
+ super(`EP MCP upstream timeout for ${toolName} — data may be incomplete`);
1083
+ this.name = 'UpstreamTimeoutError';
1084
+ }
1085
+ }
1086
+ /**
1087
+ * Check whether a parsed MCP response envelope indicates an upstream timeout
1088
+ * and throw {@link UpstreamTimeoutError} if so. The EP MCP server returns
1089
+ * `{ timedOut: true, status: "timeout" }` when the upstream EP API did not
1090
+ * respond within the configured timeout window. Throwing ensures callers
1091
+ * stop timeframe-widening retry loops instead of treating the empty `data: []`
1092
+ * as "no data".
1093
+ *
1094
+ * @param value - Parsed response value (may be an object envelope or a bare array)
1095
+ * @throws {UpstreamTimeoutError} when the response indicates an upstream timeout
1096
+ */
1097
+ function checkUpstreamTimeout(value) {
1098
+ if (typeof value !== 'object' || value === null)
1099
+ return;
1100
+ const envelope = value;
1101
+ if (envelope['timedOut'] === true || envelope['status'] === 'timeout') {
1102
+ const toolName = envelope['toolName'] ? String(envelope['toolName']) : 'unknown';
1103
+ console.warn(`${WARN_PREFIX} EP MCP upstream timeout for ${toolName} — data may be incomplete. ` +
1104
+ 'Consider using year-based endpoints as fallback.');
1105
+ throw new UpstreamTimeoutError(toolName);
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Handle errors from feed-fetching functions with timeframe-widening logic.
1110
+ * Encapsulates the common catch-block pattern: upstream timeouts return `undefined`
1111
+ * (caller should stop retrying), errors whose message contains '404' or 'timed out'
1112
+ * widen to the next timeframe, and other errors log and return `undefined`.
1113
+ *
1114
+ * @param error - Caught error
1115
+ * @param tf - Current feed timeframe
1116
+ * @param toolName - Feed tool name for log messages
1117
+ * @returns A wider {@link FeedTimeframe} to retry, or `undefined` to stop
1118
+ */
1119
+ function handleFeedFetchError(error, tf, toolName) {
1120
+ if (error instanceof UpstreamTimeoutError)
1121
+ return undefined;
1122
+ const message = error instanceof Error ? error.message : String(error);
1123
+ const wider = getWiderTimeframe(tf);
1124
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1125
+ console.warn(`${WARN_PREFIX} ${toolName} failed (${tf}): ${message} — retrying with ${wider}`);
1126
+ return wider;
1127
+ }
1128
+ console.warn(`${WARN_PREFIX} ${toolName} failed:`, message);
1129
+ return undefined;
1130
+ }
1075
1131
  /**
1076
1132
  * Parse a feed result from MCP into a flat array of items.
1077
1133
  * EP API v2 feeds return items under the `data` key:
1078
1134
  * `{ data: [{ id, type, work_type, identifier, label }], "@context": [...] }`
1079
1135
  *
1080
1136
  * Also handles legacy shapes (`feed`, `entries`, `items`) and bare arrays.
1137
+ * Throws {@link UpstreamTimeoutError} when the MCP server reports an upstream
1138
+ * timeout (`{ timedOut: true, status: "timeout" }`), so callers can stop
1139
+ * timeframe-widening loops instead of treating the empty `data: []` as "no data".
1081
1140
  *
1082
1141
  * @param result - Raw MCP tool result
1083
1142
  * @returns Array of parsed feed entry objects (may be empty)
1143
+ * @throws {UpstreamTimeoutError} when the response indicates an upstream timeout
1084
1144
  */
1085
1145
  function parseFeedResult(result) {
1086
1146
  if (!result?.content?.[0]?.text)
@@ -1088,12 +1148,14 @@ function parseFeedResult(result) {
1088
1148
  const parsed = parseJSON(result.content[0].text, 'feed');
1089
1149
  if (!parsed)
1090
1150
  return [];
1151
+ checkUpstreamTimeout(parsed);
1152
+ const envelope = parsed;
1091
1153
  // EP API v2 feeds use `data` key; also check legacy shapes
1092
1154
  const candidates = [
1093
- parsed['data'],
1094
- parsed['feed'],
1095
- parsed['entries'],
1096
- parsed['items'],
1155
+ envelope['data'],
1156
+ envelope['feed'],
1157
+ envelope['entries'],
1158
+ envelope['items'],
1097
1159
  parsed,
1098
1160
  ];
1099
1161
  for (const candidate of candidates) {
@@ -1106,9 +1168,13 @@ function parseFeedResult(result) {
1106
1168
  * Parse an EP API v2 feed response envelope in a single JSON parse, returning
1107
1169
  * both the array of feed items and the API-reported total count.
1108
1170
  * Avoids parsing the same JSON payload twice when both values are needed.
1171
+ * Throws {@link UpstreamTimeoutError} when the MCP server reports an upstream
1172
+ * timeout (`{ timedOut: true, status: "timeout" }`), so callers can stop
1173
+ * timeframe-widening loops instead of treating the empty `data: []` as "no data".
1109
1174
  *
1110
1175
  * @param result - Raw MCP tool result
1111
1176
  * @returns Object with `items` array and `total` count from the API
1177
+ * @throws {UpstreamTimeoutError} when the response indicates an upstream timeout
1112
1178
  */
1113
1179
  function parseFeedEnvelope(result) {
1114
1180
  if (!result?.content?.[0]?.text)
@@ -1116,6 +1182,7 @@ function parseFeedEnvelope(result) {
1116
1182
  const parsed = parseJSON(result.content[0].text, 'feed');
1117
1183
  if (!parsed || typeof parsed !== 'object')
1118
1184
  return { items: [], total: 0 };
1185
+ checkUpstreamTimeout(parsed);
1119
1186
  const envelope = parsed;
1120
1187
  const total = typeof envelope['total'] === 'number' ? envelope['total'] : 0;
1121
1188
  const candidates = [
@@ -1178,14 +1245,11 @@ export async function fetchAdoptedTextsFeed(client, timeframe = 'one-week') {
1178
1245
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1179
1246
  }
1180
1247
  catch (error) {
1181
- const message = error instanceof Error ? error.message : String(error);
1182
- const wider = getWiderTimeframe(tf);
1183
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1184
- console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1248
+ const wider = handleFeedFetchError(error, tf, 'get_adopted_texts_feed');
1249
+ if (wider) {
1185
1250
  currentTimeframe = wider;
1186
1251
  }
1187
1252
  else {
1188
- console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed:`, message);
1189
1253
  return [];
1190
1254
  }
1191
1255
  }
@@ -1220,14 +1284,11 @@ export async function fetchEventsFeed(client, timeframe = 'one-week') {
1220
1284
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1221
1285
  }
1222
1286
  catch (error) {
1223
- const message = error instanceof Error ? error.message : String(error);
1224
- const wider = getWiderTimeframe(tf);
1225
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1226
- console.warn(`${WARN_PREFIX} get_events_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1287
+ const wider = handleFeedFetchError(error, tf, 'get_events_feed');
1288
+ if (wider) {
1227
1289
  currentTimeframe = wider;
1228
1290
  }
1229
1291
  else {
1230
- console.warn(`${WARN_PREFIX} get_events_feed failed:`, message);
1231
1292
  return [];
1232
1293
  }
1233
1294
  }
@@ -1261,14 +1322,11 @@ export async function fetchProceduresFeed(client, timeframe = 'one-week') {
1261
1322
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1262
1323
  }
1263
1324
  catch (error) {
1264
- const message = error instanceof Error ? error.message : String(error);
1265
- const wider = getWiderTimeframe(tf);
1266
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1267
- console.warn(`${WARN_PREFIX} get_procedures_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1325
+ const wider = handleFeedFetchError(error, tf, 'get_procedures_feed');
1326
+ if (wider) {
1268
1327
  currentTimeframe = wider;
1269
1328
  }
1270
1329
  else {
1271
- console.warn(`${WARN_PREFIX} get_procedures_feed failed:`, message);
1272
1330
  return [];
1273
1331
  }
1274
1332
  }
@@ -1319,6 +1377,8 @@ export async function fetchMEPsFeedWithTotal(client, timeframe = 'one-week') {
1319
1377
  return { items, total };
1320
1378
  }
1321
1379
  catch (error) {
1380
+ if (error instanceof UpstreamTimeoutError)
1381
+ return { items: [], total: 0 };
1322
1382
  const message = error instanceof Error ? error.message : String(error);
1323
1383
  console.warn(`${WARN_PREFIX} get_meps_feed failed:`, message);
1324
1384
  return { items: [], total: 0 };
@@ -1348,14 +1408,11 @@ export async function fetchDocumentsFeed(client, timeframe = 'one-week') {
1348
1408
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1349
1409
  }
1350
1410
  catch (error) {
1351
- const message = error instanceof Error ? error.message : String(error);
1352
- const wider = getWiderTimeframe(tf);
1353
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1354
- console.warn(`${WARN_PREFIX} get_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1411
+ const wider = handleFeedFetchError(error, tf, 'get_documents_feed');
1412
+ if (wider) {
1355
1413
  currentTimeframe = wider;
1356
1414
  }
1357
1415
  else {
1358
- console.warn(`${WARN_PREFIX} get_documents_feed failed:`, message);
1359
1416
  return [];
1360
1417
  }
1361
1418
  }
@@ -1386,14 +1443,11 @@ export async function fetchPlenaryDocumentsFeed(client, timeframe = 'one-week')
1386
1443
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1387
1444
  }
1388
1445
  catch (error) {
1389
- const message = error instanceof Error ? error.message : String(error);
1390
- const wider = getWiderTimeframe(tf);
1391
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1392
- console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1446
+ const wider = handleFeedFetchError(error, tf, 'get_plenary_documents_feed');
1447
+ if (wider) {
1393
1448
  currentTimeframe = wider;
1394
1449
  }
1395
1450
  else {
1396
- console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed:`, message);
1397
1451
  return [];
1398
1452
  }
1399
1453
  }
@@ -1424,14 +1478,11 @@ export async function fetchCommitteeDocumentsFeed(client, timeframe = 'one-week'
1424
1478
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1425
1479
  }
1426
1480
  catch (error) {
1427
- const message = error instanceof Error ? error.message : String(error);
1428
- const wider = getWiderTimeframe(tf);
1429
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1430
- console.warn(`${WARN_PREFIX} get_committee_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1481
+ const wider = handleFeedFetchError(error, tf, 'get_committee_documents_feed');
1482
+ if (wider) {
1431
1483
  currentTimeframe = wider;
1432
1484
  }
1433
1485
  else {
1434
- console.warn(`${WARN_PREFIX} get_committee_documents_feed failed:`, message);
1435
1486
  return [];
1436
1487
  }
1437
1488
  }
@@ -1462,14 +1513,11 @@ export async function fetchPlenarySessionDocumentsFeed(client, timeframe = 'one-
1462
1513
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1463
1514
  }
1464
1515
  catch (error) {
1465
- const message = error instanceof Error ? error.message : String(error);
1466
- const wider = getWiderTimeframe(tf);
1467
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1468
- console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1516
+ const wider = handleFeedFetchError(error, tf, 'get_plenary_session_documents_feed');
1517
+ if (wider) {
1469
1518
  currentTimeframe = wider;
1470
1519
  }
1471
1520
  else {
1472
- console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed:`, message);
1473
1521
  return [];
1474
1522
  }
1475
1523
  }
@@ -1500,14 +1548,11 @@ export async function fetchExternalDocumentsFeed(client, timeframe = 'one-week')
1500
1548
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1501
1549
  }
1502
1550
  catch (error) {
1503
- const message = error instanceof Error ? error.message : String(error);
1504
- const wider = getWiderTimeframe(tf);
1505
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1506
- console.warn(`${WARN_PREFIX} get_external_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1551
+ const wider = handleFeedFetchError(error, tf, 'get_external_documents_feed');
1552
+ if (wider) {
1507
1553
  currentTimeframe = wider;
1508
1554
  }
1509
1555
  else {
1510
- console.warn(`${WARN_PREFIX} get_external_documents_feed failed:`, message);
1511
1556
  return [];
1512
1557
  }
1513
1558
  }
@@ -1538,14 +1583,11 @@ export async function fetchQuestionsFeed(client, timeframe = 'one-week') {
1538
1583
  currentTimeframe = getWiderTimeframe(currentTimeframe);
1539
1584
  }
1540
1585
  catch (error) {
1541
- const message = error instanceof Error ? error.message : String(error);
1542
- const wider = getWiderTimeframe(tf);
1543
- if (wider && (message.includes('404') || message.includes('timed out'))) {
1544
- console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1586
+ const wider = handleFeedFetchError(error, tf, 'get_parliamentary_questions_feed');
1587
+ if (wider) {
1545
1588
  currentTimeframe = wider;
1546
1589
  }
1547
1590
  else {
1548
- console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed:`, message);
1549
1591
  return [];
1550
1592
  }
1551
1593
  }
@@ -1568,6 +1610,8 @@ export async function fetchDeclarationsFeed(client, timeframe = 'one-week') {
1568
1610
  return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1569
1611
  }
1570
1612
  catch (error) {
1613
+ if (error instanceof UpstreamTimeoutError)
1614
+ return [];
1571
1615
  const message = error instanceof Error ? error.message : String(error);
1572
1616
  console.warn(`${WARN_PREFIX} get_mep_declarations_feed failed:`, message);
1573
1617
  return [];
@@ -1589,6 +1633,8 @@ export async function fetchCorporateBodiesFeed(client, timeframe = 'one-week') {
1589
1633
  return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1590
1634
  }
1591
1635
  catch (error) {
1636
+ if (error instanceof UpstreamTimeoutError)
1637
+ return [];
1592
1638
  const message = error instanceof Error ? error.message : String(error);
1593
1639
  console.warn(`${WARN_PREFIX} get_corporate_bodies_feed failed:`, message);
1594
1640
  return [];
@@ -44,7 +44,7 @@ export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree,
44
44
  export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
45
45
  export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, type ArticleValidationResult, } from './utils/file-utils.js';
46
46
  export { detectCategory } from './utils/article-category.js';
47
- export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
47
+ export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, COMPARISON_COUNTRIES, WB_AGGREGATE_LABELS, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
48
48
  export { generateArticleHTML } from './templates/article-template.js';
49
49
  export { computeArticleQualityScore, buildTableOfContents, buildQualityScoreBadge, } from './templates/section-builders.js';
50
50
  export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './constants/language-core.js';
package/scripts/index.js CHANGED
@@ -61,7 +61,7 @@ export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafe
61
61
  // ─── Article Category Detection ──────────────────────────────────────────────
62
62
  export { detectCategory } from './utils/article-category.js';
63
63
  // ─── World Bank Data Utilities ───────────────────────────────────────────────
64
- export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
64
+ export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, COMPARISON_COUNTRIES, WB_AGGREGATE_LABELS, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
65
65
  // ─── Templates ───────────────────────────────────────────────────────────────
66
66
  export { generateArticleHTML } from './templates/article-template.js';
67
67
  export { computeArticleQualityScore, buildTableOfContents, buildQualityScoreBadge, } from './templates/section-builders.js';
@@ -29,7 +29,7 @@ const SERVER_HEALTH_FALLBACK = '{"server": null, "feeds": []}';
29
29
  /**
30
30
  * Classify an error message into a diagnostic error category.
31
31
  *
32
- * Maps EP MCP Server v1.2.2 structured error codes and generic HTTP/network
32
+ * Maps EP MCP Server v1.2.4 structured error codes and generic HTTP/network
33
33
  * errors into one of six broad categories used for logging and retry decisions:
34
34
  *
35
35
  * Returned categories (priority order):
@@ -45,7 +45,7 @@ const SERVER_HEALTH_FALLBACK = '{"server": null, "feeds": []}';
45
45
  */
46
46
  function classifyToolError(message) {
47
47
  const lowerMsg = message.toLowerCase();
48
- // EP MCP Server v1.2.2 structured error codes (matched case-insensitively)
48
+ // EP MCP Server v1.2.4 structured error codes (matched case-insensitively)
49
49
  if (lowerMsg.includes('internal_error')) {
50
50
  return 'INTERNAL_ERROR';
51
51
  }
@@ -69,17 +69,82 @@ export interface PolicyRelevantIndicators {
69
69
  gdp: string;
70
70
  /** GDP growth (annual %) — NY.GDP.MKTP.KD.ZG */
71
71
  gdpGrowth: string;
72
+ /** GDP per capita (current US$) — NY.GDP.PCAP.CD */
73
+ gdpPerCapita: string;
74
+ /** GNI per capita, Atlas method (current US$) — NY.GNP.PCAP.CD */
75
+ gniPerCapita: string;
72
76
  /** Inflation, consumer prices (annual %) — FP.CPI.TOTL.ZG */
73
77
  inflation: string;
74
78
  /** Unemployment, total (% of total labor force) — SL.UEM.TOTL.ZS */
75
79
  unemployment: string;
80
+ /** Exports of goods and services (% of GDP) — NE.EXP.GNFS.ZS */
81
+ exportsGdp: string;
82
+ /** Foreign direct investment, net inflows (BoP, current US$) — BN.KLT.DINV.CD */
83
+ fdiNet: string;
76
84
  /** Trade (% of GDP) — NE.TRD.GNFS.ZS */
77
85
  trade: string;
78
- /** CO2 emissions (metric tons per capita) — EN.ATM.CO2E.PC */
79
- co2Emissions: string;
86
+ /** Tax revenue (% of GDP) — GC.TAX.TOTL.GD.ZS */
87
+ taxRevenue: string;
88
+ /** General government final consumption expenditure (% of GDP) — NE.CON.GOVT.ZS */
89
+ govExpenditure: string;
90
+ /** Military expenditure (% of GDP) — MS.MIL.XPND.GD.ZS */
91
+ militaryExpenditure: string;
80
92
  /** Population, total — SP.POP.TOTL */
81
93
  population: string;
94
+ /** Life expectancy at birth, total (years) — SP.DYN.LE00.IN */
95
+ lifeExpectancy: string;
96
+ /** Birth rate, crude (per 1,000 people) — SP.DYN.CBRT.IN */
97
+ birthRate: string;
98
+ /** Death rate, crude (per 1,000 people) — SP.DYN.CDRT.IN */
99
+ deathRate: string;
100
+ /** Individuals using the Internet (% of population) — IT.NET.USER.ZS */
101
+ internetUsers: string;
102
+ /** Current health expenditure (% of GDP) — SH.XPD.CHEX.GD.ZS */
103
+ healthExpenditure: string;
104
+ /** Physicians (per 1,000 people) — SH.MED.PHYS.ZS */
105
+ physicians: string;
106
+ /** Hospital beds (per 1,000 people) — SH.MED.BEDS.ZS */
107
+ hospitalBeds: string;
108
+ /** Government expenditure on education, total (% of GDP) — SE.XPD.TOTL.GD.ZS */
109
+ educationExpenditure: string;
110
+ /** CO2 emissions (metric tons per capita) — EN.ATM.CO2E.PC */
111
+ co2Emissions: string;
112
+ /** Renewable energy consumption (% of total) — EG.FEC.RNEW.ZS */
113
+ renewableEnergy: string;
82
114
  /** Research and development expenditure (% of GDP) — GB.XPD.RSDV.GD.ZS */
83
115
  rdExpenditure: string;
116
+ /** High-technology exports (% of manufactured exports) — TX.VAL.TECH.MF.ZS */
117
+ hightechExports: string;
118
+ }
119
+ /**
120
+ * World Bank MCP tool names available for data fetching.
121
+ * Maps to the tool functions on the worldbank-mcp server.
122
+ *
123
+ * These type literals use **kebab-case** (e.g., `get-economic-data`) to match
124
+ * the underlying tool registration names in worldbank-mcp@1.0.0
125
+ * (`server.tool('get-economic-data', ...)`).
126
+ * When called via the MCP gateway prefix in this repository's workflows, use
127
+ * the corresponding **snake_case** form, for example
128
+ * `world_bank___get_economic_data(...)`.
129
+ *
130
+ * **Note:** The codebase also has a `WorldBankMCPClient.getIndicatorForCountry()`
131
+ * wrapper (in `src/mcp/wb-mcp-client.ts`) that calls the `get_indicator_for_country`
132
+ * tool — this is a legacy convenience method not listed in this type union since it
133
+ * is not part of the standard worldbank-mcp tool surface.
134
+ */
135
+ export type WBMCPToolName = 'get-economic-data' | 'get-social-data' | 'get-health-data' | 'get-education-data' | 'get-country-info' | 'get-countries' | 'search-indicators';
136
+ /**
137
+ * World Bank MCP tool indicator key mapping.
138
+ * Maps each tool to the indicator key literals it accepts.
139
+ */
140
+ export interface WBToolIndicatorKeys {
141
+ /** Economic indicators accepted by `get-economic-data` */
142
+ 'get-economic-data': 'GDP' | 'GDP_GROWTH' | 'GDP_PER_CAPITA' | 'GNI' | 'GNI_PER_CAPITA' | 'EXPORTS_GDP' | 'FDI_NET' | 'INFLATION' | 'UNEMPLOYMENT';
143
+ /** Social indicators accepted by `get-social-data` */
144
+ 'get-social-data': 'POPULATION' | 'LIFE_EXPECTANCY' | 'BIRTH_RATE' | 'DEATH_RATE' | 'INTERNET_USERS';
145
+ /** Health indicators accepted by `get-health-data` */
146
+ 'get-health-data': 'HEALTH_EXPENDITURE' | 'PHYSICIANS' | 'HOSPITAL_BEDS' | 'IMMUNIZATION' | 'HIV_PREVALENCE' | 'MALNUTRITION' | 'TUBERCULOSIS';
147
+ /** Education indicators accepted by `get-education-data` */
148
+ 'get-education-data': 'LITERACY_RATE' | 'SCHOOL_ENROLLMENT' | 'SCHOOL_COMPLETION' | 'TEACHERS_PRIMARY' | 'EDUCATION_EXPENDITURE';
84
149
  }
85
150
  //# sourceMappingURL=world-bank.d.ts.map
@@ -9,6 +9,22 @@
9
9
  * Functions in this module are designed to be stateless and avoid observable
10
10
  * side effects, with the exception of explicitly recording metadata such as
11
11
  * data timestamps in returned objects.
12
+ *
13
+ * ## ⚠️ For AI Agents / Agentic Workflows
14
+ *
15
+ * The constants below ({@link POLICY_INDICATORS}, {@link EU_COUNTRY_CODES},
16
+ * {@link COMPARISON_COUNTRIES}) are a **convenience subset** used by TypeScript
17
+ * code for formatting and parsing. They do **NOT** represent the full World Bank
18
+ * indicator inventory.
19
+ *
20
+ * **For indicator selection in articles and analysis:**
21
+ * 1. Read `analysis/worldbank/indicator-catalog.md` — **200+ indicators** by EP policy domain
22
+ * 2. Use `search-indicators` MCP tool to **discover indicators on demand** by keyword
23
+ * 3. Read `analysis/worldbank/eu-country-mapping.md` for country codes + comparison groups
24
+ * 4. Read `analysis/worldbank/chart-integration-guide.md` for Chart.js + Mermaid templates
25
+ *
26
+ * The World Bank MCP server has thousands of indicators beyond the ones listed
27
+ * here. Use `search-indicators` to find the best match for any policy topic.
12
28
  */
13
29
  import type { EconomicContext, PolicyRelevantIndicators, WorldBankIndicator } from '../types/world-bank.js';
14
30
  /**
@@ -18,9 +34,25 @@ import type { EconomicContext, PolicyRelevantIndicators, WorldBankIndicator } fr
18
34
  export declare const EU_COUNTRY_CODES: Readonly<Record<string, string>>;
19
35
  /** EU aggregate code in World Bank (European Union) */
20
36
  export declare const EU_AGGREGATE_CODE = "EUU";
37
+ /**
38
+ * Comparison country codes for benchmarking EU performance against global peers.
39
+ * Organized by geopolitical relevance to EU Parliament policy analysis.
40
+ */
41
+ export declare const COMPARISON_COUNTRIES: Readonly<Record<string, string>>;
42
+ /**
43
+ * Aggregate/region codes useful for EU benchmarking.
44
+ * Keys are World Bank group codes; values are human-readable labels.
45
+ */
46
+ export declare const WB_AGGREGATE_LABELS: Readonly<Record<string, string>>;
21
47
  /**
22
48
  * World Bank indicator IDs relevant to EU Parliament policy analysis.
23
- * These indicators map to common policy areas discussed in EP legislation.
49
+ *
50
+ * ⚠️ **AI Agents**: This is a convenience subset of 25 core indicators used by
51
+ * TypeScript formatting code. The World Bank has **thousands** of indicators.
52
+ * For article/analysis generation:
53
+ * - Read `analysis/worldbank/indicator-catalog.md` for the full **200+ indicator** reference
54
+ * - Use `search-indicators` MCP tool to discover indicators on demand by keyword
55
+ * - See `analysis/worldbank/use-cases.md` for when each indicator type adds value
24
56
  */
25
57
  export declare const POLICY_INDICATORS: PolicyRelevantIndicators;
26
58
  /**
@@ -37,19 +37,96 @@ export const EU_COUNTRY_CODES = {
37
37
  };
38
38
  /** EU aggregate code in World Bank (European Union) */
39
39
  export const EU_AGGREGATE_CODE = 'EUU';
40
+ /**
41
+ * Comparison country codes for benchmarking EU performance against global peers.
42
+ * Organized by geopolitical relevance to EU Parliament policy analysis.
43
+ */
44
+ export const COMPARISON_COUNTRIES = {
45
+ // ── G7 Non-EU ──
46
+ US: 'USA', // United States
47
+ GB: 'GBR', // United Kingdom (post-Brexit benchmark)
48
+ JP: 'JPN', // Japan
49
+ CA: 'CAN', // Canada
50
+ // ── BRICS ──
51
+ CN: 'CHN', // China
52
+ IN: 'IND', // India
53
+ BR: 'BRA', // Brazil
54
+ RU: 'RUS', // Russia
55
+ ZA: 'ZAF', // South Africa
56
+ // ── EU Candidate States ──
57
+ UA: 'UKR', // Ukraine
58
+ TR: 'TUR', // Türkiye
59
+ RS: 'SRB', // Serbia
60
+ ME: 'MNE', // Montenegro
61
+ AL: 'ALB', // Albania
62
+ MK: 'MKD', // North Macedonia
63
+ MD: 'MDA', // Moldova
64
+ BA: 'BIH', // Bosnia & Herzegovina
65
+ GE: 'GEO', // Georgia
66
+ // ── Key Trade Partners ──
67
+ KR: 'KOR', // South Korea
68
+ AU: 'AUS', // Australia
69
+ NO: 'NOR', // Norway (EEA)
70
+ CH: 'CHE', // Switzerland (EFTA)
71
+ IL: 'ISR', // Israel
72
+ };
73
+ /**
74
+ * Aggregate/region codes useful for EU benchmarking.
75
+ * Keys are World Bank group codes; values are human-readable labels.
76
+ */
77
+ export const WB_AGGREGATE_LABELS = {
78
+ EUU: 'European Union',
79
+ EMU: 'Euro area',
80
+ OED: 'OECD members',
81
+ WLD: 'World',
82
+ ECS: 'Europe & Central Asia',
83
+ NAC: 'North America',
84
+ EAS: 'East Asia & Pacific',
85
+ SSF: 'Sub-Saharan Africa',
86
+ };
40
87
  /**
41
88
  * World Bank indicator IDs relevant to EU Parliament policy analysis.
42
- * These indicators map to common policy areas discussed in EP legislation.
89
+ *
90
+ * ⚠️ **AI Agents**: This is a convenience subset of 25 core indicators used by
91
+ * TypeScript formatting code. The World Bank has **thousands** of indicators.
92
+ * For article/analysis generation:
93
+ * - Read `analysis/worldbank/indicator-catalog.md` for the full **200+ indicator** reference
94
+ * - Use `search-indicators` MCP tool to discover indicators on demand by keyword
95
+ * - See `analysis/worldbank/use-cases.md` for when each indicator type adds value
43
96
  */
44
97
  export const POLICY_INDICATORS = {
98
+ // Macro-economic (get-economic-data)
45
99
  gdp: 'NY.GDP.MKTP.CD',
46
100
  gdpGrowth: 'NY.GDP.MKTP.KD.ZG',
101
+ gdpPerCapita: 'NY.GDP.PCAP.CD',
102
+ gniPerCapita: 'NY.GNP.PCAP.CD',
47
103
  inflation: 'FP.CPI.TOTL.ZG',
48
104
  unemployment: 'SL.UEM.TOTL.ZS',
105
+ exportsGdp: 'NE.EXP.GNFS.ZS',
106
+ fdiNet: 'BN.KLT.DINV.CD',
107
+ // Trade & fiscal
49
108
  trade: 'NE.TRD.GNFS.ZS',
50
- co2Emissions: 'EN.ATM.CO2E.PC',
109
+ taxRevenue: 'GC.TAX.TOTL.GD.ZS',
110
+ govExpenditure: 'NE.CON.GOVT.ZS',
111
+ militaryExpenditure: 'MS.MIL.XPND.GD.ZS',
112
+ // Social (get-social-data)
51
113
  population: 'SP.POP.TOTL',
114
+ lifeExpectancy: 'SP.DYN.LE00.IN',
115
+ birthRate: 'SP.DYN.CBRT.IN',
116
+ deathRate: 'SP.DYN.CDRT.IN',
117
+ internetUsers: 'IT.NET.USER.ZS',
118
+ // Health (get-health-data)
119
+ healthExpenditure: 'SH.XPD.CHEX.GD.ZS',
120
+ physicians: 'SH.MED.PHYS.ZS',
121
+ hospitalBeds: 'SH.MED.BEDS.ZS',
122
+ // Education (get-education-data)
123
+ educationExpenditure: 'SE.XPD.TOTL.GD.ZS',
124
+ // Environment & energy
125
+ co2Emissions: 'EN.ATM.CO2E.PC',
126
+ renewableEnergy: 'EG.FEC.RNEW.ZS',
127
+ // Research & innovation
52
128
  rdExpenditure: 'GB.XPD.RSDV.GD.ZS',
129
+ hightechExports: 'TX.VAL.TECH.MF.ZS',
53
130
  };
54
131
  // ─── CSV Parsing ─────────────────────────────────────────────────────────────
55
132
  /** Known CSV header aliases for each World Bank field */
@@ -194,7 +271,16 @@ export function formatIndicatorValue(value, indicatorId) {
194
271
  indicatorId === POLICY_INDICATORS.inflation ||
195
272
  indicatorId === POLICY_INDICATORS.unemployment ||
196
273
  indicatorId === POLICY_INDICATORS.trade ||
197
- indicatorId === POLICY_INDICATORS.rdExpenditure) {
274
+ indicatorId === POLICY_INDICATORS.taxRevenue ||
275
+ indicatorId === POLICY_INDICATORS.govExpenditure ||
276
+ indicatorId === POLICY_INDICATORS.militaryExpenditure ||
277
+ indicatorId === POLICY_INDICATORS.exportsGdp ||
278
+ indicatorId === POLICY_INDICATORS.healthExpenditure ||
279
+ indicatorId === POLICY_INDICATORS.educationExpenditure ||
280
+ indicatorId === POLICY_INDICATORS.internetUsers ||
281
+ indicatorId === POLICY_INDICATORS.renewableEnergy ||
282
+ indicatorId === POLICY_INDICATORS.rdExpenditure ||
283
+ indicatorId === POLICY_INDICATORS.hightechExports) {
198
284
  return `${value.toFixed(1)}%`;
199
285
  }
200
286
  // CO2 emissions - metric tons per capita
@@ -232,12 +318,29 @@ export function buildEconomicContext(countryCode, countryName, indicatorData) {
232
318
  const indicatorNames = {
233
319
  [POLICY_INDICATORS.gdp]: 'GDP',
234
320
  [POLICY_INDICATORS.gdpGrowth]: 'GDP Growth',
321
+ [POLICY_INDICATORS.gdpPerCapita]: 'GDP per Capita',
322
+ [POLICY_INDICATORS.gniPerCapita]: 'GNI per Capita',
235
323
  [POLICY_INDICATORS.inflation]: 'Inflation',
236
324
  [POLICY_INDICATORS.unemployment]: 'Unemployment',
325
+ [POLICY_INDICATORS.exportsGdp]: 'Exports (% of GDP)',
326
+ [POLICY_INDICATORS.fdiNet]: 'FDI Net Inflows',
237
327
  [POLICY_INDICATORS.trade]: 'Trade (% of GDP)',
238
- [POLICY_INDICATORS.co2Emissions]: 'CO₂ Emissions',
328
+ [POLICY_INDICATORS.taxRevenue]: 'Tax Revenue (% of GDP)',
329
+ [POLICY_INDICATORS.govExpenditure]: 'Gov. Expenditure (% of GDP)',
330
+ [POLICY_INDICATORS.militaryExpenditure]: 'Military Expenditure (% of GDP)',
239
331
  [POLICY_INDICATORS.population]: 'Population',
240
- [POLICY_INDICATORS.rdExpenditure]: 'R&D Expenditure',
332
+ [POLICY_INDICATORS.lifeExpectancy]: 'Life Expectancy',
333
+ [POLICY_INDICATORS.birthRate]: 'Birth Rate',
334
+ [POLICY_INDICATORS.deathRate]: 'Death Rate',
335
+ [POLICY_INDICATORS.internetUsers]: 'Internet Users (%)',
336
+ [POLICY_INDICATORS.healthExpenditure]: 'Health Expenditure (% of GDP)',
337
+ [POLICY_INDICATORS.physicians]: 'Physicians (per 1,000)',
338
+ [POLICY_INDICATORS.hospitalBeds]: 'Hospital Beds (per 1,000)',
339
+ [POLICY_INDICATORS.educationExpenditure]: 'Education Expenditure (% of GDP)',
340
+ [POLICY_INDICATORS.co2Emissions]: 'CO₂ Emissions',
341
+ [POLICY_INDICATORS.renewableEnergy]: 'Renewable Energy (%)',
342
+ [POLICY_INDICATORS.rdExpenditure]: 'R&D Expenditure (% of GDP)',
343
+ [POLICY_INDICATORS.hightechExports]: 'High-Tech Exports (%)',
241
344
  };
242
345
  for (const [indicatorId, dataPoints] of indicatorData) {
243
346
  const recent = getMostRecentValue(dataPoints);