euparliamentmonitor 0.8.4

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.
Files changed (276) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1005 -0
  3. package/SECURITY.md +151 -0
  4. package/package.json +131 -0
  5. package/scripts/constants/committee-indicator-map.d.ts +199 -0
  6. package/scripts/constants/committee-indicator-map.d.ts.map +1 -0
  7. package/scripts/constants/committee-indicator-map.js +1224 -0
  8. package/scripts/constants/committee-indicator-map.js.map +1 -0
  9. package/scripts/constants/config.d.ts +38 -0
  10. package/scripts/constants/config.d.ts.map +1 -0
  11. package/scripts/constants/config.js +66 -0
  12. package/scripts/constants/config.js.map +1 -0
  13. package/scripts/constants/language-articles.d.ts +84 -0
  14. package/scripts/constants/language-articles.d.ts.map +1 -0
  15. package/scripts/constants/language-articles.js +6771 -0
  16. package/scripts/constants/language-articles.js.map +1 -0
  17. package/scripts/constants/language-core.d.ts +38 -0
  18. package/scripts/constants/language-core.d.ts.map +1 -0
  19. package/scripts/constants/language-core.js +90 -0
  20. package/scripts/constants/language-core.js.map +1 -0
  21. package/scripts/constants/language-ui.d.ts +82 -0
  22. package/scripts/constants/language-ui.d.ts.map +1 -0
  23. package/scripts/constants/language-ui.js +889 -0
  24. package/scripts/constants/language-ui.js.map +1 -0
  25. package/scripts/constants/languages.d.ts +14 -0
  26. package/scripts/constants/languages.d.ts.map +1 -0
  27. package/scripts/constants/languages.js +15 -0
  28. package/scripts/constants/languages.js.map +1 -0
  29. package/scripts/generators/analysis-builders.d.ts +266 -0
  30. package/scripts/generators/analysis-builders.d.ts.map +1 -0
  31. package/scripts/generators/analysis-builders.js +2903 -0
  32. package/scripts/generators/analysis-builders.js.map +1 -0
  33. package/scripts/generators/breaking-content.d.ts +45 -0
  34. package/scripts/generators/breaking-content.d.ts.map +1 -0
  35. package/scripts/generators/breaking-content.js +530 -0
  36. package/scripts/generators/breaking-content.js.map +1 -0
  37. package/scripts/generators/committee-helpers.d.ts +54 -0
  38. package/scripts/generators/committee-helpers.d.ts.map +1 -0
  39. package/scripts/generators/committee-helpers.js +154 -0
  40. package/scripts/generators/committee-helpers.js.map +1 -0
  41. package/scripts/generators/dashboard-content.d.ts +95 -0
  42. package/scripts/generators/dashboard-content.d.ts.map +1 -0
  43. package/scripts/generators/dashboard-content.js +630 -0
  44. package/scripts/generators/dashboard-content.js.map +1 -0
  45. package/scripts/generators/deep-analysis-content.d.ts +23 -0
  46. package/scripts/generators/deep-analysis-content.d.ts.map +1 -0
  47. package/scripts/generators/deep-analysis-content.js +831 -0
  48. package/scripts/generators/deep-analysis-content.js.map +1 -0
  49. package/scripts/generators/mindmap-content.d.ts +55 -0
  50. package/scripts/generators/mindmap-content.d.ts.map +1 -0
  51. package/scripts/generators/mindmap-content.js +512 -0
  52. package/scripts/generators/mindmap-content.js.map +1 -0
  53. package/scripts/generators/motions-content.d.ts +50 -0
  54. package/scripts/generators/motions-content.d.ts.map +1 -0
  55. package/scripts/generators/motions-content.js +391 -0
  56. package/scripts/generators/motions-content.js.map +1 -0
  57. package/scripts/generators/news-enhanced.d.ts +14 -0
  58. package/scripts/generators/news-enhanced.d.ts.map +1 -0
  59. package/scripts/generators/news-enhanced.js +169 -0
  60. package/scripts/generators/news-enhanced.js.map +1 -0
  61. package/scripts/generators/news-indexes.d.ts +31 -0
  62. package/scripts/generators/news-indexes.d.ts.map +1 -0
  63. package/scripts/generators/news-indexes.js +410 -0
  64. package/scripts/generators/news-indexes.js.map +1 -0
  65. package/scripts/generators/pipeline/fetch-stage.d.ts +352 -0
  66. package/scripts/generators/pipeline/fetch-stage.d.ts.map +1 -0
  67. package/scripts/generators/pipeline/fetch-stage.js +1522 -0
  68. package/scripts/generators/pipeline/fetch-stage.js.map +1 -0
  69. package/scripts/generators/pipeline/generate-stage.d.ts +43 -0
  70. package/scripts/generators/pipeline/generate-stage.d.ts.map +1 -0
  71. package/scripts/generators/pipeline/generate-stage.js +204 -0
  72. package/scripts/generators/pipeline/generate-stage.js.map +1 -0
  73. package/scripts/generators/pipeline/output-stage.d.ts +48 -0
  74. package/scripts/generators/pipeline/output-stage.d.ts.map +1 -0
  75. package/scripts/generators/pipeline/output-stage.js +145 -0
  76. package/scripts/generators/pipeline/output-stage.js.map +1 -0
  77. package/scripts/generators/pipeline/transform-stage.d.ts +57 -0
  78. package/scripts/generators/pipeline/transform-stage.d.ts.map +1 -0
  79. package/scripts/generators/pipeline/transform-stage.js +111 -0
  80. package/scripts/generators/pipeline/transform-stage.js.map +1 -0
  81. package/scripts/generators/propositions-content.d.ts +29 -0
  82. package/scripts/generators/propositions-content.d.ts.map +1 -0
  83. package/scripts/generators/propositions-content.js +90 -0
  84. package/scripts/generators/propositions-content.js.map +1 -0
  85. package/scripts/generators/sankey-content.d.ts +45 -0
  86. package/scripts/generators/sankey-content.d.ts.map +1 -0
  87. package/scripts/generators/sankey-content.js +227 -0
  88. package/scripts/generators/sankey-content.js.map +1 -0
  89. package/scripts/generators/sitemap.d.ts +66 -0
  90. package/scripts/generators/sitemap.d.ts.map +1 -0
  91. package/scripts/generators/sitemap.js +562 -0
  92. package/scripts/generators/sitemap.js.map +1 -0
  93. package/scripts/generators/strategies/article-strategy.d.ts +146 -0
  94. package/scripts/generators/strategies/article-strategy.d.ts.map +1 -0
  95. package/scripts/generators/strategies/article-strategy.js +4 -0
  96. package/scripts/generators/strategies/article-strategy.js.map +1 -0
  97. package/scripts/generators/strategies/breaking-news-strategy.d.ts +64 -0
  98. package/scripts/generators/strategies/breaking-news-strategy.d.ts.map +1 -0
  99. package/scripts/generators/strategies/breaking-news-strategy.js +246 -0
  100. package/scripts/generators/strategies/breaking-news-strategy.js.map +1 -0
  101. package/scripts/generators/strategies/committee-reports-strategy.d.ts +93 -0
  102. package/scripts/generators/strategies/committee-reports-strategy.d.ts.map +1 -0
  103. package/scripts/generators/strategies/committee-reports-strategy.js +447 -0
  104. package/scripts/generators/strategies/committee-reports-strategy.js.map +1 -0
  105. package/scripts/generators/strategies/month-ahead-strategy.d.ts +60 -0
  106. package/scripts/generators/strategies/month-ahead-strategy.d.ts.map +1 -0
  107. package/scripts/generators/strategies/month-ahead-strategy.js +175 -0
  108. package/scripts/generators/strategies/month-ahead-strategy.js.map +1 -0
  109. package/scripts/generators/strategies/monthly-review-strategy.d.ts +66 -0
  110. package/scripts/generators/strategies/monthly-review-strategy.d.ts.map +1 -0
  111. package/scripts/generators/strategies/monthly-review-strategy.js +204 -0
  112. package/scripts/generators/strategies/monthly-review-strategy.js.map +1 -0
  113. package/scripts/generators/strategies/motions-strategy.d.ts +61 -0
  114. package/scripts/generators/strategies/motions-strategy.d.ts.map +1 -0
  115. package/scripts/generators/strategies/motions-strategy.js +215 -0
  116. package/scripts/generators/strategies/motions-strategy.js.map +1 -0
  117. package/scripts/generators/strategies/propositions-strategy.d.ts +60 -0
  118. package/scripts/generators/strategies/propositions-strategy.d.ts.map +1 -0
  119. package/scripts/generators/strategies/propositions-strategy.js +257 -0
  120. package/scripts/generators/strategies/propositions-strategy.js.map +1 -0
  121. package/scripts/generators/strategies/week-ahead-strategy.d.ts +57 -0
  122. package/scripts/generators/strategies/week-ahead-strategy.d.ts.map +1 -0
  123. package/scripts/generators/strategies/week-ahead-strategy.js +178 -0
  124. package/scripts/generators/strategies/week-ahead-strategy.js.map +1 -0
  125. package/scripts/generators/strategies/weekly-review-strategy.d.ts +63 -0
  126. package/scripts/generators/strategies/weekly-review-strategy.d.ts.map +1 -0
  127. package/scripts/generators/strategies/weekly-review-strategy.js +211 -0
  128. package/scripts/generators/strategies/weekly-review-strategy.js.map +1 -0
  129. package/scripts/generators/swot-content.d.ts +42 -0
  130. package/scripts/generators/swot-content.d.ts.map +1 -0
  131. package/scripts/generators/swot-content.js +366 -0
  132. package/scripts/generators/swot-content.js.map +1 -0
  133. package/scripts/generators/week-ahead-content.d.ts +103 -0
  134. package/scripts/generators/week-ahead-content.d.ts.map +1 -0
  135. package/scripts/generators/week-ahead-content.js +610 -0
  136. package/scripts/generators/week-ahead-content.js.map +1 -0
  137. package/scripts/index.d.ts +40 -0
  138. package/scripts/index.d.ts.map +1 -0
  139. package/scripts/index.js +53 -0
  140. package/scripts/index.js.map +1 -0
  141. package/scripts/mcp/ep-mcp-client.d.ts +471 -0
  142. package/scripts/mcp/ep-mcp-client.d.ts.map +1 -0
  143. package/scripts/mcp/ep-mcp-client.js +734 -0
  144. package/scripts/mcp/ep-mcp-client.js.map +1 -0
  145. package/scripts/mcp/mcp-connection.d.ts +264 -0
  146. package/scripts/mcp/mcp-connection.d.ts.map +1 -0
  147. package/scripts/mcp/mcp-connection.js +790 -0
  148. package/scripts/mcp/mcp-connection.js.map +1 -0
  149. package/scripts/mcp/mcp-health.d.ts +75 -0
  150. package/scripts/mcp/mcp-health.d.ts.map +1 -0
  151. package/scripts/mcp/mcp-health.js +78 -0
  152. package/scripts/mcp/mcp-health.js.map +1 -0
  153. package/scripts/mcp/mcp-retry.d.ts +94 -0
  154. package/scripts/mcp/mcp-retry.d.ts.map +1 -0
  155. package/scripts/mcp/mcp-retry.js +127 -0
  156. package/scripts/mcp/mcp-retry.js.map +1 -0
  157. package/scripts/mcp/wb-mcp-client.d.ts +38 -0
  158. package/scripts/mcp/wb-mcp-client.d.ts.map +1 -0
  159. package/scripts/mcp/wb-mcp-client.js +112 -0
  160. package/scripts/mcp/wb-mcp-client.js.map +1 -0
  161. package/scripts/templates/article-template.d.ts +9 -0
  162. package/scripts/templates/article-template.d.ts.map +1 -0
  163. package/scripts/templates/article-template.js +378 -0
  164. package/scripts/templates/article-template.js.map +1 -0
  165. package/scripts/templates/section-builders.d.ts +28 -0
  166. package/scripts/templates/section-builders.d.ts.map +1 -0
  167. package/scripts/templates/section-builders.js +142 -0
  168. package/scripts/templates/section-builders.js.map +1 -0
  169. package/scripts/types/analysis.d.ts +115 -0
  170. package/scripts/types/analysis.d.ts.map +1 -0
  171. package/scripts/types/analysis.js +4 -0
  172. package/scripts/types/analysis.js.map +1 -0
  173. package/scripts/types/common.d.ts +584 -0
  174. package/scripts/types/common.d.ts.map +1 -0
  175. package/scripts/types/common.js +96 -0
  176. package/scripts/types/common.js.map +1 -0
  177. package/scripts/types/generation.d.ts +104 -0
  178. package/scripts/types/generation.d.ts.map +1 -0
  179. package/scripts/types/generation.js +4 -0
  180. package/scripts/types/generation.js.map +1 -0
  181. package/scripts/types/index.d.ts +24 -0
  182. package/scripts/types/index.d.ts.map +1 -0
  183. package/scripts/types/index.js +16 -0
  184. package/scripts/types/index.js.map +1 -0
  185. package/scripts/types/intelligence.d.ts +129 -0
  186. package/scripts/types/intelligence.d.ts.map +1 -0
  187. package/scripts/types/intelligence.js +4 -0
  188. package/scripts/types/intelligence.js.map +1 -0
  189. package/scripts/types/mcp.d.ts +418 -0
  190. package/scripts/types/mcp.d.ts.map +1 -0
  191. package/scripts/types/mcp.js +4 -0
  192. package/scripts/types/mcp.js.map +1 -0
  193. package/scripts/types/parliament.d.ts +388 -0
  194. package/scripts/types/parliament.d.ts.map +1 -0
  195. package/scripts/types/parliament.js +4 -0
  196. package/scripts/types/parliament.js.map +1 -0
  197. package/scripts/types/quality.d.ts +114 -0
  198. package/scripts/types/quality.d.ts.map +1 -0
  199. package/scripts/types/quality.js +4 -0
  200. package/scripts/types/quality.js.map +1 -0
  201. package/scripts/types/stakeholder.d.ts +88 -0
  202. package/scripts/types/stakeholder.d.ts.map +1 -0
  203. package/scripts/types/stakeholder.js +16 -0
  204. package/scripts/types/stakeholder.js.map +1 -0
  205. package/scripts/types/visualization.d.ts +708 -0
  206. package/scripts/types/visualization.d.ts.map +1 -0
  207. package/scripts/types/visualization.js +4 -0
  208. package/scripts/types/visualization.js.map +1 -0
  209. package/scripts/types/world-bank.d.ts +85 -0
  210. package/scripts/types/world-bank.d.ts.map +1 -0
  211. package/scripts/types/world-bank.js +4 -0
  212. package/scripts/types/world-bank.js.map +1 -0
  213. package/scripts/utils/article-category.d.ts +18 -0
  214. package/scripts/utils/article-category.d.ts.map +1 -0
  215. package/scripts/utils/article-category.js +49 -0
  216. package/scripts/utils/article-category.js.map +1 -0
  217. package/scripts/utils/article-quality-scorer.d.ts +87 -0
  218. package/scripts/utils/article-quality-scorer.d.ts.map +1 -0
  219. package/scripts/utils/article-quality-scorer.js +1048 -0
  220. package/scripts/utils/article-quality-scorer.js.map +1 -0
  221. package/scripts/utils/content-metadata.d.ts +34 -0
  222. package/scripts/utils/content-metadata.d.ts.map +1 -0
  223. package/scripts/utils/content-metadata.js +249 -0
  224. package/scripts/utils/content-metadata.js.map +1 -0
  225. package/scripts/utils/content-validator.d.ts +94 -0
  226. package/scripts/utils/content-validator.d.ts.map +1 -0
  227. package/scripts/utils/content-validator.js +489 -0
  228. package/scripts/utils/content-validator.js.map +1 -0
  229. package/scripts/utils/copy-test-reports.d.ts +9 -0
  230. package/scripts/utils/copy-test-reports.d.ts.map +1 -0
  231. package/scripts/utils/copy-test-reports.js +508 -0
  232. package/scripts/utils/copy-test-reports.js.map +1 -0
  233. package/scripts/utils/file-utils.d.ts +144 -0
  234. package/scripts/utils/file-utils.d.ts.map +1 -0
  235. package/scripts/utils/file-utils.js +374 -0
  236. package/scripts/utils/file-utils.js.map +1 -0
  237. package/scripts/utils/fix-articles.d.ts +27 -0
  238. package/scripts/utils/fix-articles.d.ts.map +1 -0
  239. package/scripts/utils/fix-articles.js +510 -0
  240. package/scripts/utils/fix-articles.js.map +1 -0
  241. package/scripts/utils/generate-docs-index.d.ts +8 -0
  242. package/scripts/utils/generate-docs-index.d.ts.map +1 -0
  243. package/scripts/utils/generate-docs-index.js +275 -0
  244. package/scripts/utils/generate-docs-index.js.map +1 -0
  245. package/scripts/utils/html-sanitize.d.ts +18 -0
  246. package/scripts/utils/html-sanitize.d.ts.map +1 -0
  247. package/scripts/utils/html-sanitize.js +57 -0
  248. package/scripts/utils/html-sanitize.js.map +1 -0
  249. package/scripts/utils/intelligence-analysis.d.ts +173 -0
  250. package/scripts/utils/intelligence-analysis.d.ts.map +1 -0
  251. package/scripts/utils/intelligence-analysis.js +936 -0
  252. package/scripts/utils/intelligence-analysis.js.map +1 -0
  253. package/scripts/utils/intelligence-index.d.ts +126 -0
  254. package/scripts/utils/intelligence-index.d.ts.map +1 -0
  255. package/scripts/utils/intelligence-index.js +731 -0
  256. package/scripts/utils/intelligence-index.js.map +1 -0
  257. package/scripts/utils/metadata-utils.d.ts +14 -0
  258. package/scripts/utils/metadata-utils.d.ts.map +1 -0
  259. package/scripts/utils/metadata-utils.js +18 -0
  260. package/scripts/utils/metadata-utils.js.map +1 -0
  261. package/scripts/utils/news-metadata.d.ts +47 -0
  262. package/scripts/utils/news-metadata.d.ts.map +1 -0
  263. package/scripts/utils/news-metadata.js +259 -0
  264. package/scripts/utils/news-metadata.js.map +1 -0
  265. package/scripts/utils/validate-articles.d.ts +2 -0
  266. package/scripts/utils/validate-articles.d.ts.map +1 -0
  267. package/scripts/utils/validate-articles.js +284 -0
  268. package/scripts/utils/validate-articles.js.map +1 -0
  269. package/scripts/utils/validate-ep-api.d.ts +51 -0
  270. package/scripts/utils/validate-ep-api.d.ts.map +1 -0
  271. package/scripts/utils/validate-ep-api.js +160 -0
  272. package/scripts/utils/validate-ep-api.js.map +1 -0
  273. package/scripts/utils/world-bank-data.d.ts +84 -0
  274. package/scripts/utils/world-bank-data.d.ts.map +1 -0
  275. package/scripts/utils/world-bank-data.js +311 -0
  276. package/scripts/utils/world-bank-data.js.map +1 -0
@@ -0,0 +1,1522 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module Generators/Pipeline/FetchStage
5
+ * @description MCP data-fetching pipeline stage with circuit breaker protection.
6
+ *
7
+ * MCP-facing functions accept an explicit `client` argument instead of reading
8
+ * module-level state, making them straightforward to unit-test with a mock
9
+ * client. The {@link loadFeedDataFromFile} and {@link loadEPFeedDataFromFile}
10
+ * helpers introduce filesystem I/O to load pre-fetched feed JSON produced by
11
+ * agentic workflows.
12
+ *
13
+ * The {@link CircuitBreaker} (imported from `mcp/mcp-retry`) prevents cascading
14
+ * failures when the MCP server is degraded: after
15
+ * {@link CircuitBreakerOptions.failureThreshold} consecutive errors the circuit
16
+ * opens and subsequent calls short-circuit immediately.
17
+ */
18
+ import fs from 'fs';
19
+ import { getEPMCPClient } from '../../mcp/ep-mcp-client.js';
20
+ import { parsePlenarySessions, parseCommitteeMeetings, parseLegislativeDocuments, parseLegislativePipeline, parseParliamentaryQuestions, parseEPEvents, PLACEHOLDER_EVENTS, } from '../week-ahead-content.js';
21
+ import { applyCommitteeInfo, applyDocuments, applyEffectiveness, PLACEHOLDER_CHAIR, PLACEHOLDER_MEMBERS, } from '../committee-helpers.js';
22
+ import { getMotionsFallbackData } from '../motions-content.js';
23
+ import { escapeHTML } from '../../utils/file-utils.js';
24
+ // ─── Circuit Breaker (re-exported from mcp-retry bounded context) ────────────
25
+ export { CircuitBreaker, } from '../../mcp/mcp-retry.js';
26
+ import { CircuitBreaker } from '../../mcp/mcp-retry.js';
27
+ /** Module-level circuit breaker shared across all MCP fetch operations */
28
+ export const mcpCircuitBreaker = new CircuitBreaker();
29
+ // ─── Shared string constants ─────────────────────────────────────────────────
30
+ /** Log prefix for MCP fetch operations */
31
+ const MCP_FETCH_PREFIX = ' 📡';
32
+ /** Warning prefix for MCP failures */
33
+ const WARN_PREFIX = ' ⚠️';
34
+ /** Info prefix for fallback messages */
35
+ const INFO_PREFIX = ' ℹ️';
36
+ /**
37
+ * Execute a single MCP API call through the module-level circuit breaker.
38
+ * Short-circuits with `fallback` whenever the circuit breaker is not
39
+ * accepting requests (for example when OPEN, or in HALF_OPEN with no
40
+ * probe slots available).
41
+ * Records success or failure after each call, opening the circuit when
42
+ * {@link CircuitBreakerOptions.failureThreshold} consecutive failures occur.
43
+ *
44
+ * @param fn - Async factory that performs the MCP call
45
+ * @param fallback - Value returned when the circuit is not accepting requests
46
+ * @param context - Label used in warning messages
47
+ * @returns Result of `fn` or `fallback`
48
+ */
49
+ async function callMCP(fn, fallback, context) {
50
+ if (!mcpCircuitBreaker.canRequest()) {
51
+ console.warn(`${WARN_PREFIX} Circuit breaker not accepting requests (${mcpCircuitBreaker.getState()}) — skipping ${context}`);
52
+ return fallback;
53
+ }
54
+ try {
55
+ const result = await fn();
56
+ mcpCircuitBreaker.recordSuccess();
57
+ return result;
58
+ }
59
+ catch (error) {
60
+ mcpCircuitBreaker.recordFailure();
61
+ throw error;
62
+ }
63
+ }
64
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
65
+ /**
66
+ * Parse JSON text, returning `null` and logging a warning on parse failure.
67
+ *
68
+ * @param text - Raw JSON string
69
+ * @param context - Label used in the warning message
70
+ * @returns Parsed value or null
71
+ */
72
+ function parseJSON(text, context) {
73
+ try {
74
+ return JSON.parse(text);
75
+ }
76
+ catch {
77
+ console.warn(`${WARN_PREFIX} Failed to parse JSON for ${context}`);
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Normalize a feed-item date into canonical UTC `YYYY-MM-DD` form.
83
+ *
84
+ * @param value - Raw date string from MCP or a prefetched feed file
85
+ * @returns Canonical date string, or undefined when the value is missing/invalid
86
+ */
87
+ function normalizeFeedItemDate(value) {
88
+ const trimmed = value.trim();
89
+ if (trimmed === '')
90
+ return undefined;
91
+ const directDate = trimmed.slice(0, 10);
92
+ if (directDate.length === 10) {
93
+ const direct = new Date(`${directDate}T00:00:00Z`);
94
+ if (!Number.isNaN(direct.getTime())) {
95
+ return directDate;
96
+ }
97
+ }
98
+ const parsed = new Date(trimmed);
99
+ if (Number.isNaN(parsed.getTime()))
100
+ return undefined;
101
+ const parts = parsed.toISOString().split('T');
102
+ return parts[0];
103
+ }
104
+ /**
105
+ * Filter dated feed items to an inclusive UTC date window.
106
+ *
107
+ * Items without a parseable `date` are dropped when a window is supplied.
108
+ *
109
+ * @param items - Feed items to filter
110
+ * @param dateRange - Inclusive UTC window, or undefined to keep all items
111
+ * @param label - Human-readable label used in logs
112
+ * @returns Filtered array
113
+ */
114
+ function filterFeedItemsByDateRange(items, dateRange, label) {
115
+ if (!dateRange)
116
+ return [...items];
117
+ const filtered = items.filter((item) => {
118
+ const normalized = normalizeFeedItemDate(item.date);
119
+ return normalized !== undefined && normalized >= dateRange.start && normalized <= dateRange.end;
120
+ });
121
+ if (filtered.length !== items.length) {
122
+ console.log(`${INFO_PREFIX} Filtered ${label} to ${filtered.length}/${items.length} items within ` +
123
+ `${dateRange.start}..${dateRange.end}`);
124
+ }
125
+ return filtered;
126
+ }
127
+ /**
128
+ * Apply a date-range filter across all breaking-news feed arrays.
129
+ *
130
+ * @param feedData - Feed data to filter
131
+ * @param dateRange - Inclusive UTC window, or undefined to keep all items
132
+ * @returns Filtered feed payload
133
+ */
134
+ function filterBreakingNewsFeedDataByDateRange(feedData, dateRange) {
135
+ const filteredMEPUpdates = filterFeedItemsByDateRange(feedData.mepUpdates, dateRange, 'MEP updates');
136
+ return {
137
+ adoptedTexts: filterFeedItemsByDateRange(feedData.adoptedTexts, dateRange, 'adopted texts'),
138
+ events: filterFeedItemsByDateRange(feedData.events, dateRange, 'events'),
139
+ procedures: filterFeedItemsByDateRange(feedData.procedures, dateRange, 'procedures'),
140
+ mepUpdates: filteredMEPUpdates,
141
+ // When a date-range filter is applied the API-reported total covers the full
142
+ // feed window, not the filtered subset — clear it to avoid a misleading
143
+ // truncation note ("showing 10 of 525" on a single-day slice).
144
+ totalMEPUpdates: dateRange === undefined ? feedData.totalMEPUpdates : undefined,
145
+ };
146
+ }
147
+ /**
148
+ * Apply a date-range filter across all comprehensive EP feed arrays.
149
+ *
150
+ * @param feedData - Feed data to filter
151
+ * @param dateRange - Inclusive UTC window, or undefined to keep all items
152
+ * @returns Filtered feed payload
153
+ */
154
+ function filterEPFeedDataByDateRange(feedData, dateRange) {
155
+ return {
156
+ adoptedTexts: filterFeedItemsByDateRange(feedData.adoptedTexts, dateRange, 'adopted texts'),
157
+ events: filterFeedItemsByDateRange(feedData.events, dateRange, 'events'),
158
+ procedures: filterFeedItemsByDateRange(feedData.procedures, dateRange, 'procedures'),
159
+ mepUpdates: filterFeedItemsByDateRange(feedData.mepUpdates, dateRange, 'MEP updates'),
160
+ documents: filterFeedItemsByDateRange(feedData.documents, dateRange, 'documents'),
161
+ plenaryDocuments: filterFeedItemsByDateRange(feedData.plenaryDocuments, dateRange, 'plenary documents'),
162
+ committeeDocuments: filterFeedItemsByDateRange(feedData.committeeDocuments, dateRange, 'committee documents'),
163
+ plenarySessionDocuments: filterFeedItemsByDateRange(feedData.plenarySessionDocuments, dateRange, 'plenary session documents'),
164
+ externalDocuments: filterFeedItemsByDateRange(feedData.externalDocuments, dateRange, 'external documents'),
165
+ questions: filterFeedItemsByDateRange(feedData.questions, dateRange, 'questions'),
166
+ declarations: filterFeedItemsByDateRange(feedData.declarations, dateRange, 'declarations'),
167
+ corporateBodies: filterFeedItemsByDateRange(feedData.corporateBodies, dateRange, 'corporate bodies'),
168
+ };
169
+ }
170
+ /**
171
+ * Compute an inclusive UTC date window ending on `endDate`.
172
+ *
173
+ * @param endDate - Inclusive UTC end date in `YYYY-MM-DD` form
174
+ * @param lookbackDays - Number of calendar days to subtract for the start date
175
+ * @param context - Label used in error messages
176
+ * @returns Inclusive date range
177
+ */
178
+ export function computeRollingDateRange(endDate, lookbackDays, context) {
179
+ const startDate = new Date(`${endDate}T00:00:00Z`);
180
+ startDate.setUTCDate(startDate.getUTCDate() - lookbackDays);
181
+ const startDateParts = startDate.toISOString().split('T');
182
+ if (!startDateParts[0]) {
183
+ throw new Error(`Invalid date format generated for ${context}`);
184
+ }
185
+ return { start: startDateParts[0], end: endDate };
186
+ }
187
+ // ─── MCP client initialisation ───────────────────────────────────────────────
188
+ /**
189
+ * Attempt to connect to the European Parliament MCP server.
190
+ * Returns `null` (with a warning) if the connection fails or MCP is disabled.
191
+ *
192
+ * @param useMCP - Whether MCP should be used at all
193
+ * @returns Connected client or null
194
+ */
195
+ export async function initializeMCPClient(useMCP) {
196
+ if (!useMCP) {
197
+ console.log(`${INFO_PREFIX} MCP client disabled via USE_EP_MCP=false`);
198
+ return null;
199
+ }
200
+ try {
201
+ console.log('🔌 Attempting to connect to European Parliament MCP Server...');
202
+ const client = await getEPMCPClient();
203
+ console.log('✅ MCP client connected successfully');
204
+ return client;
205
+ }
206
+ catch (error) {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ console.warn('⚠️ Could not connect to MCP server:', message);
209
+ console.warn('⚠️ Falling back to placeholder content');
210
+ return null;
211
+ }
212
+ }
213
+ // ─── Pre-fetched feed data loading ───────────────────────────────────────────
214
+ /**
215
+ * Check whether a value is a non-null, non-array plain object.
216
+ *
217
+ * @param v - Value to check
218
+ * @returns True when v is a plain object
219
+ */
220
+ function isPlainObject(v) {
221
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
222
+ }
223
+ /**
224
+ * Sanitize an array of raw items into feed items with title-based required fields.
225
+ * Filters out non-objects and coerces `id`, `title`, `date` to strings.
226
+ *
227
+ * Uses `as unknown as T` because the spread preserves optional properties from
228
+ * the source JSON while the explicit field assignments guarantee the required
229
+ * base fields — TypeScript cannot infer this mixed provenance automatically.
230
+ *
231
+ * @param items - Raw array of unknown values from JSON
232
+ * @returns Sanitized array of typed feed items
233
+ */
234
+ function sanitizeTitleItems(items) {
235
+ return items
236
+ .filter(isPlainObject)
237
+ .filter((item) => (item['id'] !== undefined && item['id'] !== null) ||
238
+ (item['title'] !== undefined && item['title'] !== null))
239
+ .map((item) => ({
240
+ ...item,
241
+ id: String(item['id'] ?? ''),
242
+ title: String(item['title'] ?? ''),
243
+ date: String(item['date'] ?? ''),
244
+ }));
245
+ }
246
+ /**
247
+ * Sanitize an array of raw items into MEP feed items.
248
+ * Filters out non-objects and coerces `id`, `name`, `date` to strings.
249
+ *
250
+ * @param items - Raw array of unknown values from JSON
251
+ * @returns Sanitized array of MEP feed items
252
+ */
253
+ function sanitizeMEPItems(items) {
254
+ return items
255
+ .filter(isPlainObject)
256
+ .filter((item) => (item['id'] !== undefined && item['id'] !== null) ||
257
+ (item['name'] !== undefined && item['name'] !== null))
258
+ .map((item) => ({
259
+ ...item,
260
+ id: String(item['id'] ?? ''),
261
+ name: String(item['name'] ?? ''),
262
+ date: String(item['date'] ?? ''),
263
+ }));
264
+ }
265
+ /**
266
+ * Load pre-fetched feed data from a JSON file on disk.
267
+ *
268
+ * Agentic workflows fetch EP data via framework MCP tools but the TypeScript
269
+ * generator cannot access those tools directly. The workflow saves the MCP
270
+ * results to a JSON file and the generator reads them via this function,
271
+ * avoiding the need to manually construct article HTML.
272
+ *
273
+ * The file must contain a JSON object. The optional keys
274
+ * `adoptedTexts`, `events`, `procedures`, and `mepUpdates` are treated as
275
+ * arrays and default to empty arrays when missing (an empty object `{}` is valid).
276
+ *
277
+ * @param filePath - Absolute or relative path to the JSON file
278
+ * @param dateRange - Optional inclusive UTC window for filtering loaded items
279
+ * @returns Parsed {@link BreakingNewsFeedData}, or `undefined` on any error
280
+ */
281
+ export function loadFeedDataFromFile(filePath, dateRange) {
282
+ try {
283
+ if (!fs.existsSync(filePath)) {
284
+ console.warn(`${WARN_PREFIX} Feed data file not found: ${filePath}`);
285
+ return undefined;
286
+ }
287
+ const raw = fs.readFileSync(filePath, 'utf-8');
288
+ const parsed = JSON.parse(raw);
289
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
290
+ console.warn(`${WARN_PREFIX} Feed data file must contain a JSON object`);
291
+ return undefined;
292
+ }
293
+ const obj = parsed;
294
+ const adoptedTexts = sanitizeTitleItems(Array.isArray(obj['adoptedTexts']) ? obj['adoptedTexts'] : []);
295
+ const events = sanitizeTitleItems(Array.isArray(obj['events']) ? obj['events'] : []);
296
+ const procedures = sanitizeTitleItems(Array.isArray(obj['procedures']) ? obj['procedures'] : []);
297
+ const mepUpdates = sanitizeMEPItems(Array.isArray(obj['mepUpdates']) ? obj['mepUpdates'] : []);
298
+ const totalMEPUpdates = typeof obj['totalMEPUpdates'] === 'number' ? obj['totalMEPUpdates'] : undefined;
299
+ const filteredData = filterBreakingNewsFeedDataByDateRange({
300
+ adoptedTexts,
301
+ events,
302
+ procedures,
303
+ mepUpdates,
304
+ totalMEPUpdates,
305
+ }, dateRange);
306
+ console.log(`${INFO_PREFIX} Loaded feed data from file: ` +
307
+ `${filteredData.adoptedTexts.length} adopted texts, ${filteredData.events.length} events, ` +
308
+ `${filteredData.procedures.length} procedures, ${filteredData.mepUpdates.length} MEP updates`);
309
+ return filteredData;
310
+ }
311
+ catch (error) {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ console.warn(`${WARN_PREFIX} Failed to load feed data from file: ${message}`);
314
+ return undefined;
315
+ }
316
+ }
317
+ /**
318
+ * Load pre-fetched comprehensive EP feed data from a JSON file on disk.
319
+ *
320
+ * Agentic workflows fetch EP data via framework MCP tools but the TypeScript
321
+ * generator cannot access those tools directly. The workflow saves the MCP
322
+ * results to a JSON file and the generator reads them via this function,
323
+ * avoiding the need to manually construct article HTML.
324
+ *
325
+ * The file must contain a JSON object with EP feed data keys.
326
+ * Missing keys default to empty arrays.
327
+ *
328
+ * @param filePath - Absolute or relative path to the JSON file
329
+ * @param dateRange - Optional inclusive UTC window for filtering loaded items
330
+ * @returns Parsed {@link EPFeedData}, or `undefined` on any error
331
+ */
332
+ export function loadEPFeedDataFromFile(filePath, dateRange) {
333
+ try {
334
+ if (!fs.existsSync(filePath)) {
335
+ console.warn(`${WARN_PREFIX} EP feed data file not found: ${filePath}`);
336
+ return undefined;
337
+ }
338
+ const raw = fs.readFileSync(filePath, 'utf-8');
339
+ const parsed = JSON.parse(raw);
340
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
341
+ console.warn(`${WARN_PREFIX} EP feed data file must contain a JSON object`);
342
+ return undefined;
343
+ }
344
+ const obj = parsed;
345
+ const safeArray = (key) => Array.isArray(obj[key]) ? obj[key] : [];
346
+ const adoptedTexts = sanitizeTitleItems(safeArray('adoptedTexts'));
347
+ const events = sanitizeTitleItems(safeArray('events'));
348
+ const procedures = sanitizeTitleItems(safeArray('procedures'));
349
+ const mepUpdates = sanitizeMEPItems(safeArray('mepUpdates'));
350
+ const documents = sanitizeTitleItems(safeArray('documents'));
351
+ const plenaryDocuments = sanitizeTitleItems(safeArray('plenaryDocuments'));
352
+ const committeeDocuments = sanitizeTitleItems(safeArray('committeeDocuments'));
353
+ const plenarySessionDocuments = sanitizeTitleItems(safeArray('plenarySessionDocuments'));
354
+ const externalDocuments = sanitizeTitleItems(safeArray('externalDocuments'));
355
+ const questions = sanitizeTitleItems(safeArray('questions'));
356
+ const declarations = sanitizeTitleItems(safeArray('declarations'));
357
+ const corporateBodies = sanitizeTitleItems(safeArray('corporateBodies'));
358
+ const filteredData = filterEPFeedDataByDateRange({
359
+ adoptedTexts,
360
+ events,
361
+ procedures,
362
+ mepUpdates,
363
+ documents,
364
+ plenaryDocuments,
365
+ committeeDocuments,
366
+ plenarySessionDocuments,
367
+ externalDocuments,
368
+ questions,
369
+ declarations,
370
+ corporateBodies,
371
+ }, dateRange);
372
+ const totalItems = filteredData.adoptedTexts.length +
373
+ filteredData.events.length +
374
+ filteredData.procedures.length +
375
+ filteredData.mepUpdates.length +
376
+ filteredData.documents.length +
377
+ filteredData.plenaryDocuments.length +
378
+ filteredData.committeeDocuments.length +
379
+ filteredData.plenarySessionDocuments.length +
380
+ filteredData.externalDocuments.length +
381
+ filteredData.questions.length +
382
+ filteredData.declarations.length +
383
+ filteredData.corporateBodies.length;
384
+ console.log(`${INFO_PREFIX} Loaded EP feed data from file: ${totalItems} total items across 12 keys`);
385
+ return filteredData;
386
+ }
387
+ catch (error) {
388
+ const message = error instanceof Error ? error.message : String(error);
389
+ console.warn(`${WARN_PREFIX} Failed to load EP feed data from file: ${message}`);
390
+ return undefined;
391
+ }
392
+ }
393
+ // ─── Week-Ahead fetches ──────────────────────────────────────────────────────
394
+ /**
395
+ * Fetch aggregated week-ahead data from multiple MCP sources in parallel.
396
+ * Returns placeholder data when the client is unavailable.
397
+ *
398
+ * @param client - MCP client or null
399
+ * @param dateRange - Date range for the week-ahead period
400
+ * @returns Aggregated week-ahead data
401
+ */
402
+ export async function fetchWeekAheadData(client, dateRange) {
403
+ if (!client) {
404
+ console.log(`${INFO_PREFIX} MCP unavailable — using placeholder events`);
405
+ return {
406
+ events: PLACEHOLDER_EVENTS.map((e) => ({ ...e, date: dateRange.start })),
407
+ committees: [],
408
+ documents: [],
409
+ pipeline: [],
410
+ questions: [],
411
+ };
412
+ }
413
+ if (!mcpCircuitBreaker.canRequest()) {
414
+ console.warn(`${WARN_PREFIX} Circuit breaker not accepting requests (${mcpCircuitBreaker.getState()}) — using placeholder events`);
415
+ return {
416
+ events: PLACEHOLDER_EVENTS.map((e) => ({ ...e, date: dateRange.start })),
417
+ committees: [],
418
+ documents: [],
419
+ pipeline: [],
420
+ questions: [],
421
+ };
422
+ }
423
+ // Record whether we entered as a HALF_OPEN probe so any rejection triggers
424
+ // an immediate re-open (normal circuit-breaker probe semantics).
425
+ const wasHalfOpen = mcpCircuitBreaker.getState() === 'HALF_OPEN';
426
+ console.log(`${MCP_FETCH_PREFIX} Fetching week-ahead data from MCP (parallel)...`);
427
+ const [plenarySessions, committeeInfo, documents, pipeline, questions, epEvents] = await Promise.allSettled([
428
+ client.getPlenarySessions({ startDate: dateRange.start, endDate: dateRange.end, limit: 50 }),
429
+ client.getCommitteeInfo({ limit: 20 }),
430
+ client.searchDocuments({ query: 'parliament', limit: 20 }),
431
+ client.monitorLegislativePipeline({
432
+ dateFrom: dateRange.start,
433
+ dateTo: dateRange.end,
434
+ status: 'ACTIVE',
435
+ limit: 20,
436
+ }),
437
+ client.getParliamentaryQuestions({ startDate: dateRange.start, limit: 20 }),
438
+ client.getEvents({ dateFrom: dateRange.start, dateTo: dateRange.end, limit: 20 }),
439
+ ]);
440
+ const allFailed = [
441
+ plenarySessions,
442
+ committeeInfo,
443
+ documents,
444
+ pipeline,
445
+ questions,
446
+ epEvents,
447
+ ].every((r) => r.status === 'rejected');
448
+ const anyFailed = [plenarySessions, committeeInfo, documents, pipeline, questions, epEvents].some((r) => r.status === 'rejected');
449
+ // In HALF_OPEN any single rejection means the probe failed — re-open immediately.
450
+ if (allFailed || (wasHalfOpen && anyFailed)) {
451
+ mcpCircuitBreaker.recordFailure();
452
+ }
453
+ else {
454
+ mcpCircuitBreaker.recordSuccess();
455
+ }
456
+ const plenaryEvents = parsePlenarySessions(plenarySessions, dateRange.start);
457
+ const additionalEvents = parseEPEvents(epEvents, dateRange.start);
458
+ const events = [...plenaryEvents, ...additionalEvents];
459
+ return {
460
+ events: events.length > 0 ? events : [{ ...PLACEHOLDER_EVENTS[0], date: dateRange.start }],
461
+ committees: parseCommitteeMeetings(committeeInfo, dateRange.start),
462
+ documents: parseLegislativeDocuments(documents),
463
+ pipeline: parseLegislativePipeline(pipeline),
464
+ questions: parseParliamentaryQuestions(questions),
465
+ };
466
+ }
467
+ // ─── Breaking-News fetches ───────────────────────────────────────────────────
468
+ /**
469
+ * Fetch voting anomaly text from MCP, returning empty string on failure.
470
+ *
471
+ * @param client - MCP client or null
472
+ * @returns Raw anomaly data text
473
+ */
474
+ export async function fetchVotingAnomalies(client) {
475
+ if (!client)
476
+ return '';
477
+ try {
478
+ const result = await callMCP(() => client.callTool('detect_voting_anomalies', { sensitivityThreshold: 0.3 }), undefined, 'detect_voting_anomalies');
479
+ return result?.content?.[0]?.text ?? '';
480
+ }
481
+ catch (error) {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ console.warn(`${WARN_PREFIX} detect_voting_anomalies failed:`, message);
484
+ return '';
485
+ }
486
+ }
487
+ /**
488
+ * Fetch coalition dynamics analysis text from MCP.
489
+ *
490
+ * @param client - MCP client or null
491
+ * @returns Raw coalition dynamics text
492
+ */
493
+ export async function fetchCoalitionDynamics(client) {
494
+ if (!client)
495
+ return '';
496
+ try {
497
+ const result = await callMCP(() => client.callTool('analyze_coalition_dynamics', {}), undefined, 'analyze_coalition_dynamics');
498
+ return result?.content?.[0]?.text ?? '';
499
+ }
500
+ catch (error) {
501
+ const message = error instanceof Error ? error.message : String(error);
502
+ console.warn(`${WARN_PREFIX} analyze_coalition_dynamics failed:`, message);
503
+ return '';
504
+ }
505
+ }
506
+ /**
507
+ * Fetch voting statistics report text from MCP.
508
+ *
509
+ * @param client - MCP client or null
510
+ * @returns Raw voting report text
511
+ */
512
+ export async function fetchVotingReport(client) {
513
+ if (!client)
514
+ return '';
515
+ try {
516
+ const result = await callMCP(() => client.callTool('generate_report', { reportType: 'VOTING_STATISTICS' }), undefined, 'generate_report');
517
+ return result?.content?.[0]?.text ?? '';
518
+ }
519
+ catch (error) {
520
+ const message = error instanceof Error ? error.message : String(error);
521
+ console.warn(`${WARN_PREFIX} generate_report failed:`, message);
522
+ return '';
523
+ }
524
+ }
525
+ /**
526
+ * Fetch MEP influence assessment text from MCP.
527
+ * Short-circuits immediately when `mepId` is empty.
528
+ *
529
+ * @param client - MCP client or null
530
+ * @param mepId - MEP identifier; pass empty string to skip the call
531
+ * @returns Raw influence data text
532
+ */
533
+ export async function fetchMEPInfluence(client, mepId) {
534
+ if (!mepId || !client)
535
+ return '';
536
+ try {
537
+ const result = await callMCP(() => client.callTool('assess_mep_influence', { mepId, includeDetails: true }), undefined, 'assess_mep_influence');
538
+ return result?.content?.[0]?.text ?? '';
539
+ }
540
+ catch (error) {
541
+ const message = error instanceof Error ? error.message : String(error);
542
+ console.warn(`${WARN_PREFIX} assess_mep_influence failed:`, message);
543
+ return '';
544
+ }
545
+ }
546
+ // ─── Committee-Reports fetches ───────────────────────────────────────────────
547
+ /**
548
+ * Load pre-fetched committee data for a given abbreviation from a JSON file.
549
+ *
550
+ * The file must be a JSON object keyed by committee abbreviation, where each
551
+ * value conforms to {@link CommitteeData}. This allows agentic workflows to
552
+ * inject real EP committee data into the generator without a live MCP
553
+ * connection (same pattern as {@link loadEPFeedDataFromFile}).
554
+ *
555
+ * @param filePath - Path to the JSON file
556
+ * @param abbreviation - Committee code (e.g. `"ENVI"`)
557
+ * @returns Parsed {@link CommitteeData} for the committee, or `undefined`
558
+ */
559
+ export function loadCommitteeDataFromFile(filePath, abbreviation) {
560
+ try {
561
+ if (!fs.existsSync(filePath)) {
562
+ console.warn(`${WARN_PREFIX} Committee data file not found: ${filePath}`);
563
+ return undefined;
564
+ }
565
+ const raw = fs.readFileSync(filePath, 'utf-8');
566
+ const parsed = JSON.parse(raw);
567
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
568
+ console.warn(`${WARN_PREFIX} Committee data file must contain a JSON object`);
569
+ return undefined;
570
+ }
571
+ const obj = parsed;
572
+ const entry = obj[abbreviation];
573
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
574
+ return undefined;
575
+ }
576
+ const e = entry;
577
+ const docs = Array.isArray(e['documents'])
578
+ ? e['documents']
579
+ .filter((d) => typeof d === 'object' && d !== null && !Array.isArray(d))
580
+ .map((doc) => ({
581
+ title: typeof doc['title'] === 'string' ? doc['title'] : 'Document',
582
+ type: typeof doc['type'] === 'string' ? doc['type'] : 'Document',
583
+ date: typeof doc['date'] === 'string' ? doc['date'] : '',
584
+ }))
585
+ : [];
586
+ const result = {
587
+ name: typeof e['name'] === 'string' ? e['name'] : `${abbreviation} Committee`,
588
+ abbreviation,
589
+ chair: typeof e['chair'] === 'string' ? e['chair'] : 'N/A',
590
+ members: typeof e['members'] === 'number' && Number.isFinite(e['members']) ? e['members'] : 0,
591
+ documents: docs,
592
+ effectiveness: typeof e['effectiveness'] === 'string' ? e['effectiveness'] : null,
593
+ };
594
+ console.log(`${INFO_PREFIX} Loaded committee data from file: ${result.name} (${docs.length} documents)`);
595
+ return result;
596
+ }
597
+ catch (error) {
598
+ const message = error instanceof Error ? error.message : String(error);
599
+ console.warn(`${WARN_PREFIX} Failed to load committee data from file: ${message}`);
600
+ return undefined;
601
+ }
602
+ }
603
+ /**
604
+ * Try to load committee data from the `EP_COMMITTEE_DATA_FILE` env var.
605
+ * Returns the loaded {@link CommitteeData} when available, or `undefined` when
606
+ * the env var is unset or the file does not contain an entry for the given
607
+ * committee abbreviation. Logs a warning when the file exists but the entry
608
+ * is missing so callers can fall through to an MCP fetch.
609
+ *
610
+ * @param abbreviation - Committee code (e.g. `"ENVI"`)
611
+ * @returns Pre-fetched committee data or `undefined`
612
+ */
613
+ function tryLoadCommitteeDataFromEnv(abbreviation) {
614
+ const filePath = process.env['EP_COMMITTEE_DATA_FILE'];
615
+ if (!filePath)
616
+ return undefined;
617
+ const data = loadCommitteeDataFromFile(filePath, abbreviation);
618
+ if (!data && fs.existsSync(filePath)) {
619
+ console.warn(`${WARN_PREFIX} Committee data for ${abbreviation} not found in file — falling through to MCP fetch`);
620
+ }
621
+ return data;
622
+ }
623
+ // ─── EP v2 API direct fallback ──────────────────────────────────────────────
624
+ /** Base URL for the EP Open Data Portal v2 API */
625
+ const EP_API_V2_BASE = 'https://data.europarl.europa.eu/api/v2';
626
+ /** Timeout for direct EP API requests (ms) */
627
+ const EP_API_TIMEOUT_MS = 15_000;
628
+ /**
629
+ * Fetch committee info directly from the EP v2 API as a fallback when MCP
630
+ * returns placeholder data. Uses `GET /corporate-bodies/{abbreviation}` which
631
+ * is the canonical lookup for a committee by its code (e.g. `ENVI`).
632
+ *
633
+ * This function is intentionally conservative: it primarily populates `name`
634
+ * and `abbreviation`, and may populate `members` from `inverse_isVersionOf`
635
+ * when available. Placeholder status is broken by changing `members` from `0`
636
+ * (placeholder criteria is chair='N/A' AND members=0 AND docs=[]).
637
+ *
638
+ * @param abbreviation - Committee abbreviation (e.g. `"ENVI"`)
639
+ * @param data - Existing committee data to enrich
640
+ */
641
+ export async function fetchCommitteeInfoFromEPAPI(abbreviation, data) {
642
+ const url = `${EP_API_V2_BASE}/corporate-bodies/${encodeURIComponent(abbreviation)}?format=application%2Fld%2Bjson`;
643
+ try {
644
+ const controller = new AbortController();
645
+ const timer = setTimeout(() => controller.abort(), EP_API_TIMEOUT_MS);
646
+ let response;
647
+ try {
648
+ response = await fetch(url, {
649
+ signal: controller.signal,
650
+ headers: { Accept: 'application/ld+json' },
651
+ });
652
+ }
653
+ finally {
654
+ clearTimeout(timer);
655
+ }
656
+ if (!response.ok) {
657
+ console.warn(`${WARN_PREFIX} EP API direct lookup for ${abbreviation} returned ${String(response.status)}`);
658
+ return;
659
+ }
660
+ const body = (await response.json());
661
+ const items = body.data;
662
+ if (!Array.isArray(items) || items.length === 0)
663
+ return;
664
+ const item = items[0];
665
+ if (!item)
666
+ return;
667
+ // Extract name: prefer English prefLabel, then English altLabel, then label
668
+ const name = item.prefLabel?.['en'] ?? item.altLabel?.['en'] ?? item.label ?? undefined;
669
+ if (name && name.length > 0) {
670
+ data.name = name;
671
+ }
672
+ // Extract abbreviation: the label field holds the abbreviation code
673
+ const abbr = item.label;
674
+ if (abbr && abbr.length > 0 && !abbr.startsWith('org/')) {
675
+ data.abbreviation = abbr;
676
+ }
677
+ console.log(` ✅ EP API fallback: ${data.name} (${data.abbreviation})`);
678
+ }
679
+ catch (err) {
680
+ const message = err instanceof Error ? err.message : String(err);
681
+ console.warn(`${WARN_PREFIX} EP API direct fallback failed for ${abbreviation}:`, message);
682
+ }
683
+ }
684
+ /**
685
+ * Check whether a single committee data entry is still in placeholder state.
686
+ *
687
+ * @param data - Committee data to inspect
688
+ * @returns `true` when the entry matches all placeholder criteria
689
+ */
690
+ function isPlaceholderEntry(data) {
691
+ return (data.chair === PLACEHOLDER_CHAIR &&
692
+ data.members === PLACEHOLDER_MEMBERS &&
693
+ data.documents.length === 0);
694
+ }
695
+ /**
696
+ * Fetch committee data from three MCP sources for the given abbreviation.
697
+ * Each source failure is caught individually so partial data is still returned.
698
+ *
699
+ * When the environment variable `EP_COMMITTEE_DATA_FILE` is set, pre-fetched
700
+ * committee data is loaded from that JSON file instead of calling the MCP
701
+ * client. This enables agentic workflows to inject real EP data.
702
+ *
703
+ * When MCP returns placeholder data (chair=N/A, members=0, docs=[]),
704
+ * a direct call to the EP v2 API is attempted as a fallback to populate
705
+ * at least the committee name and abbreviation.
706
+ *
707
+ * @param client - MCP client or null
708
+ * @param abbreviation - Committee code (e.g. `"ENVI"`)
709
+ * @returns Populated committee data
710
+ */
711
+ export async function fetchCommitteeData(client, abbreviation) {
712
+ const defaultResult = {
713
+ name: `${abbreviation} Committee`,
714
+ abbreviation,
715
+ chair: 'N/A',
716
+ members: 0,
717
+ documents: [],
718
+ effectiveness: null,
719
+ };
720
+ // Check for pre-fetched committee data file (set by EP_COMMITTEE_DATA_FILE env var).
721
+ // This mirrors the EP_FEED_DATA_FILE pattern for fetchEPFeedData.
722
+ const fromFile = tryLoadCommitteeDataFromEnv(abbreviation);
723
+ if (fromFile)
724
+ return fromFile;
725
+ if (!client)
726
+ return defaultResult;
727
+ try {
728
+ console.log(`${MCP_FETCH_PREFIX} Fetching committee info for ${abbreviation}...`);
729
+ const committeeResult = await callMCP(() => client.getCommitteeInfo({ committeeId: abbreviation }), null, `getCommitteeInfo(${abbreviation})`);
730
+ if (committeeResult)
731
+ applyCommitteeInfo(committeeResult, defaultResult, abbreviation);
732
+ }
733
+ catch (err) {
734
+ const message = err instanceof Error ? err.message : String(err);
735
+ console.warn(`${WARN_PREFIX} getCommitteeInfo failed for ${abbreviation}:`, message);
736
+ }
737
+ try {
738
+ console.log(`${MCP_FETCH_PREFIX} Fetching documents for ${abbreviation}...`);
739
+ const docsResult = await callMCP(() => client.searchDocuments({ query: abbreviation, limit: 5 }), null, `searchDocuments(${abbreviation})`);
740
+ if (docsResult)
741
+ applyDocuments(docsResult, defaultResult);
742
+ }
743
+ catch (err) {
744
+ const message = err instanceof Error ? err.message : String(err);
745
+ console.warn(`${WARN_PREFIX} searchDocuments failed for ${abbreviation}:`, message);
746
+ }
747
+ try {
748
+ const effectivenessResult = await callMCP(() => client.analyzeLegislativeEffectiveness({
749
+ subjectType: 'COMMITTEE',
750
+ subjectId: abbreviation,
751
+ }), null, `analyzeLegislativeEffectiveness(${abbreviation})`);
752
+ if (effectivenessResult)
753
+ applyEffectiveness(effectivenessResult, defaultResult);
754
+ }
755
+ catch (err) {
756
+ const message = err instanceof Error ? err.message : String(err);
757
+ console.warn(`${WARN_PREFIX} analyzeLegislativeEffectiveness failed for ${abbreviation}:`, message);
758
+ }
759
+ // Fallback: when MCP left the committee in placeholder state, try the EP v2
760
+ // API directly. This provides resilience when the MCP server has issues
761
+ // parsing committee data (see European-Parliament-MCP-Server#233).
762
+ if (isPlaceholderEntry(defaultResult)) {
763
+ console.log(`${INFO_PREFIX} Committee ${abbreviation} still placeholder after MCP — trying EP v2 API directly...`);
764
+ await fetchCommitteeInfoFromEPAPI(abbreviation, defaultResult);
765
+ }
766
+ return defaultResult;
767
+ }
768
+ // ─── Motions fetches ─────────────────────────────────────────────────────────
769
+ /**
770
+ * Fetch recent voting records from MCP.
771
+ *
772
+ * @param client - MCP client or null
773
+ * @param dateFromStr - Start date (YYYY-MM-DD)
774
+ * @param dateStr - End date (YYYY-MM-DD)
775
+ * @returns Array of voting records
776
+ */
777
+ export async function fetchVotingRecords(client, dateFromStr, dateStr) {
778
+ if (!client)
779
+ return [];
780
+ try {
781
+ console.log(`${MCP_FETCH_PREFIX} Fetching voting records from MCP server...`);
782
+ const votingResult = (await callMCP(() => client.callTool('get_voting_records', {
783
+ dateFrom: dateFromStr,
784
+ dateTo: dateStr,
785
+ limit: 20,
786
+ }), undefined, 'get_voting_records'));
787
+ if (votingResult?.content?.[0]) {
788
+ const data = parseJSON(votingResult.content[0].text, 'voting records');
789
+ if (data?.records && data.records.length > 0) {
790
+ console.log(` ✅ Fetched ${data.records.length} voting records from MCP`);
791
+ return data.records.map((r) => ({
792
+ title: r.title ?? 'Parliamentary Vote',
793
+ date: r.date ?? dateStr,
794
+ result: r.result ?? 'Adopted',
795
+ votes: {
796
+ for: r.votes?.for ?? 0,
797
+ against: r.votes?.against ?? 0,
798
+ abstain: r.votes?.abstain ?? 0,
799
+ },
800
+ }));
801
+ }
802
+ }
803
+ }
804
+ catch (error) {
805
+ const message = error instanceof Error ? error.message : String(error);
806
+ console.warn(`${WARN_PREFIX} MCP voting records fetch failed:`, message);
807
+ }
808
+ return [];
809
+ }
810
+ /**
811
+ * Fetch voting patterns from MCP.
812
+ *
813
+ * @param client - MCP client or null
814
+ * @param dateFromStr - Start date
815
+ * @param dateStr - End date
816
+ * @returns Array of voting patterns
817
+ */
818
+ export async function fetchVotingPatterns(client, dateFromStr, dateStr) {
819
+ if (!client)
820
+ return [];
821
+ try {
822
+ console.log(`${MCP_FETCH_PREFIX} Fetching voting patterns from MCP server...`);
823
+ const patternsResult = (await callMCP(() => client.callTool('analyze_voting_patterns', {
824
+ dateFrom: dateFromStr,
825
+ dateTo: dateStr,
826
+ }), undefined, 'analyze_voting_patterns'));
827
+ if (patternsResult?.content?.[0]) {
828
+ const data = parseJSON(patternsResult.content[0].text, 'voting patterns');
829
+ if (data?.patterns && data.patterns.length > 0) {
830
+ console.log(` ✅ Fetched ${data.patterns.length} voting patterns from MCP`);
831
+ return data.patterns.map((p) => ({
832
+ group: p.group ?? 'Unknown Group',
833
+ cohesion: p.cohesion ?? 0,
834
+ participation: p.participation ?? 0,
835
+ }));
836
+ }
837
+ }
838
+ }
839
+ catch (error) {
840
+ const message = error instanceof Error ? error.message : String(error);
841
+ console.warn(`${WARN_PREFIX} MCP voting patterns fetch failed:`, message);
842
+ }
843
+ return [];
844
+ }
845
+ /**
846
+ * Fetch voting anomalies for a date range from MCP.
847
+ *
848
+ * @param client - MCP client or null
849
+ * @param dateFromStr - Start date
850
+ * @param dateStr - End date
851
+ * @returns Array of voting anomalies
852
+ */
853
+ export async function fetchMotionsAnomalies(client, dateFromStr, dateStr) {
854
+ if (!client)
855
+ return [];
856
+ try {
857
+ console.log(`${MCP_FETCH_PREFIX} Fetching voting anomalies from MCP server...`);
858
+ const anomaliesResult = (await callMCP(() => client.callTool('detect_voting_anomalies', {
859
+ dateFrom: dateFromStr,
860
+ dateTo: dateStr,
861
+ }), undefined, 'detect_voting_anomalies'));
862
+ if (anomaliesResult?.content?.[0]) {
863
+ const data = parseJSON(anomaliesResult.content[0].text, 'voting anomalies');
864
+ if (data?.anomalies && data.anomalies.length > 0) {
865
+ console.log(` ✅ Fetched ${data.anomalies.length} voting anomalies from MCP`);
866
+ return data.anomalies.map((a) => ({
867
+ type: a.type ?? 'Unusual Pattern',
868
+ description: a.description ?? 'No description available',
869
+ severity: a.severity ?? 'MEDIUM',
870
+ }));
871
+ }
872
+ }
873
+ }
874
+ catch (error) {
875
+ const message = error instanceof Error ? error.message : String(error);
876
+ console.warn(`${WARN_PREFIX} MCP voting anomalies fetch failed:`, message);
877
+ }
878
+ return [];
879
+ }
880
+ /**
881
+ * Fetch parliamentary questions from MCP for the given date range.
882
+ *
883
+ * @param client - MCP client or null
884
+ * @param dateFromStr - Start date
885
+ * @param dateStr - End date
886
+ * @returns Array of parliamentary questions
887
+ */
888
+ export async function fetchParliamentaryQuestionsForMotions(client, dateFromStr, dateStr) {
889
+ if (!client)
890
+ return [];
891
+ try {
892
+ console.log(`${MCP_FETCH_PREFIX} Fetching parliamentary questions from MCP server...`);
893
+ const questionsResult = await callMCP(() => client.getParliamentaryQuestions({
894
+ dateFrom: dateFromStr,
895
+ dateTo: dateStr,
896
+ limit: 10,
897
+ }), undefined, 'get_parliamentary_questions');
898
+ if (questionsResult?.content?.[0]) {
899
+ const data = parseJSON(questionsResult.content[0].text, 'parliamentary questions');
900
+ if (data?.questions && data.questions.length > 0) {
901
+ console.log(` ✅ Fetched ${data.questions.length} parliamentary questions from MCP`);
902
+ return data.questions.map((q) => ({
903
+ author: q.author ?? 'Unknown MEP',
904
+ topic: q.topic ?? q.subject ?? 'General inquiry',
905
+ date: q.date ?? dateStr,
906
+ status: q.status ?? 'PENDING',
907
+ }));
908
+ }
909
+ }
910
+ }
911
+ catch (error) {
912
+ const message = error instanceof Error ? error.message : String(error);
913
+ console.warn(`${WARN_PREFIX} MCP parliamentary questions fetch failed:`, message);
914
+ }
915
+ return [];
916
+ }
917
+ /**
918
+ * Fetch all motions data in parallel, applying fallback arrays for any
919
+ * section where MCP returned nothing.
920
+ *
921
+ * @param client - MCP client or null
922
+ * @param dateFromStr - Start date
923
+ * @param dateStr - End date
924
+ * @returns All motions data with fallbacks applied
925
+ */
926
+ export async function fetchMotionsData(client, dateFromStr, dateStr) {
927
+ const [votingRecordsResult, votingPatternsResult, anomaliesResult, questionsResult] = await Promise.allSettled([
928
+ fetchVotingRecords(client, dateFromStr, dateStr),
929
+ fetchVotingPatterns(client, dateFromStr, dateStr),
930
+ fetchMotionsAnomalies(client, dateFromStr, dateStr),
931
+ fetchParliamentaryQuestionsForMotions(client, dateFromStr, dateStr),
932
+ ]);
933
+ let votingRecords = votingRecordsResult.status === 'fulfilled' ? votingRecordsResult.value : [];
934
+ if (votingRecordsResult.status === 'rejected') {
935
+ console.warn(`${WARN_PREFIX} Failed to fetch voting records from MCP`);
936
+ }
937
+ let votingPatterns = votingPatternsResult.status === 'fulfilled' ? votingPatternsResult.value : [];
938
+ if (votingPatternsResult.status === 'rejected') {
939
+ console.warn(`${WARN_PREFIX} Failed to fetch voting patterns from MCP`);
940
+ }
941
+ let anomalies = anomaliesResult.status === 'fulfilled' ? anomaliesResult.value : [];
942
+ if (anomaliesResult.status === 'rejected') {
943
+ console.warn(`${WARN_PREFIX} Failed to fetch voting anomalies from MCP`);
944
+ }
945
+ let questions = questionsResult.status === 'fulfilled' ? questionsResult.value : [];
946
+ if (questionsResult.status === 'rejected') {
947
+ console.warn(`${WARN_PREFIX} Failed to fetch parliamentary questions from MCP`);
948
+ }
949
+ const fallback = getMotionsFallbackData(dateStr, dateFromStr);
950
+ if (votingRecords.length === 0) {
951
+ console.log(`${INFO_PREFIX} Using placeholder voting records`);
952
+ votingRecords = fallback.votingRecords;
953
+ }
954
+ if (votingPatterns.length === 0) {
955
+ console.log(`${INFO_PREFIX} Using placeholder voting patterns`);
956
+ votingPatterns = fallback.votingPatterns;
957
+ }
958
+ if (anomalies.length === 0) {
959
+ console.log(`${INFO_PREFIX} Using placeholder voting anomalies`);
960
+ anomalies = fallback.anomalies;
961
+ }
962
+ if (questions.length === 0) {
963
+ console.log(`${INFO_PREFIX} Using placeholder parliamentary questions`);
964
+ questions = fallback.questions;
965
+ }
966
+ return { votingRecords, votingPatterns, anomalies, questions };
967
+ }
968
+ // ─── Propositions fetches ─────────────────────────────────────────────────────
969
+ /**
970
+ * Fetch legislative proposals from MCP and build pre-sanitised HTML.
971
+ *
972
+ * @param client - MCP client or null
973
+ * @returns Proposals HTML and the first procedure ID found (if any)
974
+ */
975
+ export async function fetchProposalsFromMCP(client) {
976
+ if (!client)
977
+ return { html: '', firstProcedureId: '' };
978
+ const docsResult = await callMCP(() => client.searchDocuments({ keyword: 'legislative proposal', limit: 10 }), undefined, 'search_documents(proposals)');
979
+ if (!docsResult?.content?.[0])
980
+ return { html: '', firstProcedureId: '' };
981
+ const data = parseJSON(docsResult.content[0].text, 'proposals');
982
+ if (!data?.documents?.length)
983
+ return { html: '', firstProcedureId: '' };
984
+ console.log(` ✅ Fetched ${data.documents.length} proposals from MCP`);
985
+ const firstProcedureId = data.documents.find((d) => /\d{4}\/\d+\(.+\)/.test(d.id ?? ''))?.id ?? '';
986
+ const html = data.documents
987
+ .map((doc) => `
988
+ <div class="proposal-card">
989
+ <h3>${escapeHTML(doc.title ?? 'Legislative Proposal')}</h3>
990
+ <div class="proposal-meta">
991
+ ${doc.id ? `<span class="proposal-id">${escapeHTML(doc.id)}</span>` : ''}
992
+ ${doc.date ? `<span class="proposal-date">${escapeHTML(doc.date)}</span>` : ''}
993
+ ${doc.status ? `<span class="proposal-status">${escapeHTML(doc.status)}</span>` : ''}
994
+ </div>
995
+ ${doc.committee ? `<p class="proposal-committee">${escapeHTML(doc.committee)}</p>` : ''}
996
+ ${doc.rapporteur ? `<p class="proposal-rapporteur">${escapeHTML(doc.rapporteur)}</p>` : ''}
997
+ </div>`)
998
+ .join('');
999
+ return { html, firstProcedureId };
1000
+ }
1001
+ /**
1002
+ * Fetch active legislative pipeline data from MCP.
1003
+ *
1004
+ * @param client - MCP client or null
1005
+ * @returns Structured pipeline data or null when unavailable
1006
+ */
1007
+ export async function fetchPipelineFromMCP(client) {
1008
+ if (!client)
1009
+ return null;
1010
+ const pipelineResult = await callMCP(() => client.monitorLegislativePipeline({ status: 'ACTIVE', limit: 5 }), undefined, 'monitor_legislative_pipeline');
1011
+ if (!pipelineResult?.content?.[0])
1012
+ return null;
1013
+ const pipeData = parseJSON(pipelineResult.content[0].text, 'pipeline');
1014
+ if (!pipeData)
1015
+ return null;
1016
+ const healthScore = pipeData.pipelineHealthScore ?? 0;
1017
+ const throughput = pipeData.throughputRate ?? 0;
1018
+ const procRowsHtml = pipeData.procedures
1019
+ ?.map((proc) => `
1020
+ <div class="procedure-item">
1021
+ ${proc.id ? `<span class="procedure-id">${escapeHTML(proc.id)}</span>` : ''}
1022
+ ${proc.title ? `<span class="procedure-title">${escapeHTML(proc.title)}</span>` : ''}
1023
+ ${proc.stage ? `<span class="procedure-stage">${escapeHTML(proc.stage)}</span>` : ''}
1024
+ </div>`)
1025
+ .join('') ?? '';
1026
+ return { healthScore, throughput, procRowsHtml };
1027
+ }
1028
+ /**
1029
+ * Fetch a specific procedure's tracked-status HTML from MCP.
1030
+ * Returns empty string when `procedureId` is empty or MCP is unavailable.
1031
+ *
1032
+ * @param client - MCP client or null
1033
+ * @param procedureId - Procedure ID (e.g. `"2024/0001(COD)"`)
1034
+ * @returns HTML snippet for the procedure status section
1035
+ */
1036
+ export async function fetchProcedureStatusFromMCP(client, procedureId) {
1037
+ if (!procedureId || !client)
1038
+ return '';
1039
+ try {
1040
+ const result = await callMCP(() => client.trackLegislation(procedureId), undefined, `track_legislation(${procedureId})`);
1041
+ if (!result?.content?.[0])
1042
+ return '';
1043
+ const raw = result.content[0].text;
1044
+ return `<pre class="data-summary">${escapeHTML(raw.slice(0, 2000))}</pre>`;
1045
+ }
1046
+ catch (error) {
1047
+ const message = error instanceof Error ? error.message : String(error);
1048
+ console.warn(`${WARN_PREFIX} track_legislation failed:`, message);
1049
+ return '';
1050
+ }
1051
+ }
1052
+ // ─── EP Feed-based fetches (Breaking News) ──────────────────────────────────
1053
+ /**
1054
+ * Parse a feed result from MCP into a flat array of items.
1055
+ * EP API v2 feeds return items under the `data` key:
1056
+ * `{ data: [{ id, type, work_type, identifier, label }], "@context": [...] }`
1057
+ *
1058
+ * Also handles legacy shapes (`feed`, `entries`, `items`) and bare arrays.
1059
+ *
1060
+ * @param result - Raw MCP tool result
1061
+ * @returns Array of parsed feed entry objects (may be empty)
1062
+ */
1063
+ function parseFeedResult(result) {
1064
+ if (!result?.content?.[0]?.text)
1065
+ return [];
1066
+ const parsed = parseJSON(result.content[0].text, 'feed');
1067
+ if (!parsed)
1068
+ return [];
1069
+ // EP API v2 feeds use `data` key; also check legacy shapes
1070
+ const candidates = [
1071
+ parsed['data'],
1072
+ parsed['feed'],
1073
+ parsed['entries'],
1074
+ parsed['items'],
1075
+ parsed,
1076
+ ];
1077
+ for (const candidate of candidates) {
1078
+ if (Array.isArray(candidate))
1079
+ return candidate;
1080
+ }
1081
+ return [];
1082
+ }
1083
+ /**
1084
+ * Parse an EP API v2 feed response envelope in a single JSON parse, returning
1085
+ * both the array of feed items and the API-reported total count.
1086
+ * Avoids parsing the same JSON payload twice when both values are needed.
1087
+ *
1088
+ * @param result - Raw MCP tool result
1089
+ * @returns Object with `items` array and `total` count from the API
1090
+ */
1091
+ function parseFeedEnvelope(result) {
1092
+ if (!result?.content?.[0]?.text)
1093
+ return { items: [], total: 0 };
1094
+ const parsed = parseJSON(result.content[0].text, 'feed');
1095
+ if (!parsed || typeof parsed !== 'object')
1096
+ return { items: [], total: 0 };
1097
+ const envelope = parsed;
1098
+ const total = typeof envelope['total'] === 'number' ? envelope['total'] : 0;
1099
+ const candidates = [
1100
+ envelope['data'],
1101
+ envelope['feed'],
1102
+ envelope['entries'],
1103
+ envelope['items'],
1104
+ parsed,
1105
+ ];
1106
+ for (const candidate of candidates) {
1107
+ if (Array.isArray(candidate))
1108
+ return { items: candidate, total };
1109
+ }
1110
+ return { items: [], total };
1111
+ }
1112
+ /**
1113
+ * Map a raw EP API v2 feed item to a normalized feed item.
1114
+ * EP feeds return `{ id, type, work_type, identifier, label }` — we normalize
1115
+ * these into the domain feed item shape, using `label` as `title` when no title exists.
1116
+ *
1117
+ * @param item - Raw feed item record
1118
+ * @returns Common feed item fields
1119
+ */
1120
+ function mapFeedItemBase(item) {
1121
+ return {
1122
+ id: String(item['id'] ?? item['docId'] ?? ''),
1123
+ title: String(item['title'] ?? item['label'] ?? item['name'] ?? item['identifier'] ?? 'Untitled'),
1124
+ date: String(item['date'] ?? item['published'] ?? item['updated'] ?? ''),
1125
+ type: item['type']
1126
+ ? String(item['type'])
1127
+ : item['work_type']
1128
+ ? String(item['work_type'])
1129
+ : undefined,
1130
+ url: item['url'] ? String(item['url']) : undefined,
1131
+ identifier: item['identifier'] ? String(item['identifier']) : undefined,
1132
+ label: item['label'] ? String(item['label']) : undefined,
1133
+ };
1134
+ }
1135
+ /**
1136
+ * Fetch adopted texts feed from MCP.
1137
+ *
1138
+ * @param client - MCP client or null
1139
+ * @param timeframe - How far back to look (default: 'one-day')
1140
+ * @returns Array of adopted text feed items
1141
+ */
1142
+ export async function fetchAdoptedTextsFeed(client, timeframe = 'one-day') {
1143
+ if (!client)
1144
+ return [];
1145
+ try {
1146
+ console.log(`${MCP_FETCH_PREFIX} Fetching adopted texts feed (${timeframe})...`);
1147
+ const result = await callMCP(() => client.getAdoptedTextsFeed({ timeframe, limit: 20 }), undefined, 'get_adopted_texts_feed');
1148
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1149
+ }
1150
+ catch (error) {
1151
+ const message = error instanceof Error ? error.message : String(error);
1152
+ console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed:`, message);
1153
+ return [];
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Fetch events feed from MCP.
1158
+ *
1159
+ * @param client - MCP client or null
1160
+ * @param timeframe - How far back to look (default: 'one-day')
1161
+ * @returns Array of event feed items
1162
+ */
1163
+ export async function fetchEventsFeed(client, timeframe = 'one-day') {
1164
+ if (!client)
1165
+ return [];
1166
+ try {
1167
+ console.log(`${MCP_FETCH_PREFIX} Fetching events feed (${timeframe})...`);
1168
+ const result = await callMCP(() => client.getEventsFeed({ timeframe, limit: 20 }), undefined, 'get_events_feed');
1169
+ return parseFeedResult(result).map((item) => ({
1170
+ ...mapFeedItemBase(item),
1171
+ location: item['location'] ? String(item['location']) : undefined,
1172
+ }));
1173
+ }
1174
+ catch (error) {
1175
+ const message = error instanceof Error ? error.message : String(error);
1176
+ console.warn(`${WARN_PREFIX} get_events_feed failed:`, message);
1177
+ return [];
1178
+ }
1179
+ }
1180
+ /**
1181
+ * Fetch procedures feed from MCP.
1182
+ *
1183
+ * @param client - MCP client or null
1184
+ * @param timeframe - How far back to look (default: 'one-day')
1185
+ * @returns Array of procedure feed items
1186
+ */
1187
+ export async function fetchProceduresFeed(client, timeframe = 'one-day') {
1188
+ if (!client)
1189
+ return [];
1190
+ try {
1191
+ console.log(`${MCP_FETCH_PREFIX} Fetching procedures feed (${timeframe})...`);
1192
+ const result = await callMCP(() => client.getProceduresFeed({ timeframe, limit: 20 }), undefined, 'get_procedures_feed');
1193
+ return parseFeedResult(result).map((item) => ({
1194
+ ...mapFeedItemBase(item),
1195
+ stage: item['stage'] ? String(item['stage']) : undefined,
1196
+ }));
1197
+ }
1198
+ catch (error) {
1199
+ const message = error instanceof Error ? error.message : String(error);
1200
+ console.warn(`${WARN_PREFIX} get_procedures_feed failed:`, message);
1201
+ return [];
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Fetch MEPs feed from MCP.
1206
+ *
1207
+ * @param client - MCP client or null
1208
+ * @param timeframe - How far back to look (default: 'one-day')
1209
+ * @returns Array of MEP feed items
1210
+ */
1211
+ export async function fetchMEPsFeed(client, timeframe = 'one-day') {
1212
+ return (await fetchMEPsFeedWithTotal(client, timeframe)).items;
1213
+ }
1214
+ /**
1215
+ * Fetch MEPs feed from MCP, returning both items and the API's reported total count.
1216
+ * The `total` from the API response reflects all matching records in the feed,
1217
+ * which may exceed the `limit` parameter (currently capped at 100 per request).
1218
+ *
1219
+ * The limit is set to 100 (the EP API maximum) so the fetched sample is large
1220
+ * enough to populate a meaningful truncation note ("showing 10 of N") while
1221
+ * keeping each request bounded. When the feed contains more than 100 MEP
1222
+ * updates, the `total` field in the API response carries the true count.
1223
+ *
1224
+ * @param client - MCP client or null
1225
+ * @param timeframe - How far back to look (default: 'one-day')
1226
+ * @returns Object with `items` array and `total` count from the API
1227
+ */
1228
+ export async function fetchMEPsFeedWithTotal(client, timeframe = 'one-day') {
1229
+ if (!client)
1230
+ return { items: [], total: 0 };
1231
+ try {
1232
+ console.log(`${MCP_FETCH_PREFIX} Fetching MEPs feed (${timeframe})...`);
1233
+ const result = await callMCP(() => client.getMEPsFeed({ timeframe, limit: 100 }), undefined, 'get_meps_feed');
1234
+ const { items: rawItems, total } = parseFeedEnvelope(result);
1235
+ const items = rawItems.map((item) => ({
1236
+ id: String(item['id'] ?? item['mepId'] ?? ''),
1237
+ name: String(item['name'] ?? item['label'] ?? item['title'] ?? 'Unknown'),
1238
+ date: String(item['date'] ?? item['published'] ?? item['updated'] ?? ''),
1239
+ country: item['country'] ? String(item['country']) : undefined,
1240
+ group: item['group'] ? String(item['group']) : undefined,
1241
+ url: item['url'] ? String(item['url']) : undefined,
1242
+ identifier: item['identifier'] ? String(item['identifier']) : undefined,
1243
+ label: item['label'] ? String(item['label']) : undefined,
1244
+ }));
1245
+ return { items, total };
1246
+ }
1247
+ catch (error) {
1248
+ const message = error instanceof Error ? error.message : String(error);
1249
+ console.warn(`${WARN_PREFIX} get_meps_feed failed:`, message);
1250
+ return { items: [], total: 0 };
1251
+ }
1252
+ }
1253
+ /**
1254
+ * Fetch documents feed from MCP.
1255
+ *
1256
+ * @param client - MCP client or null
1257
+ * @param timeframe - How far back to look (default: 'one-day')
1258
+ * @returns Array of document feed items
1259
+ */
1260
+ export async function fetchDocumentsFeed(client, timeframe = 'one-day') {
1261
+ if (!client)
1262
+ return [];
1263
+ try {
1264
+ console.log(`${MCP_FETCH_PREFIX} Fetching documents feed (${timeframe})...`);
1265
+ const result = await callMCP(() => client.getDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_documents_feed');
1266
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1267
+ }
1268
+ catch (error) {
1269
+ const message = error instanceof Error ? error.message : String(error);
1270
+ console.warn(`${WARN_PREFIX} get_documents_feed failed:`, message);
1271
+ return [];
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Fetch plenary documents feed from MCP.
1276
+ *
1277
+ * @param client - MCP client or null
1278
+ * @param timeframe - How far back to look (default: 'one-day')
1279
+ * @returns Array of document feed items
1280
+ */
1281
+ export async function fetchPlenaryDocumentsFeed(client, timeframe = 'one-day') {
1282
+ if (!client)
1283
+ return [];
1284
+ try {
1285
+ console.log(`${MCP_FETCH_PREFIX} Fetching plenary documents feed (${timeframe})...`);
1286
+ const result = await callMCP(() => client.getPlenaryDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_plenary_documents_feed');
1287
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1288
+ }
1289
+ catch (error) {
1290
+ const message = error instanceof Error ? error.message : String(error);
1291
+ console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed:`, message);
1292
+ return [];
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Fetch committee documents feed from MCP.
1297
+ *
1298
+ * @param client - MCP client or null
1299
+ * @param timeframe - How far back to look (default: 'one-day')
1300
+ * @returns Array of document feed items
1301
+ */
1302
+ export async function fetchCommitteeDocumentsFeed(client, timeframe = 'one-day') {
1303
+ if (!client)
1304
+ return [];
1305
+ try {
1306
+ console.log(`${MCP_FETCH_PREFIX} Fetching committee documents feed (${timeframe})...`);
1307
+ const result = await callMCP(() => client.getCommitteeDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_committee_documents_feed');
1308
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1309
+ }
1310
+ catch (error) {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ console.warn(`${WARN_PREFIX} get_committee_documents_feed failed:`, message);
1313
+ return [];
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Fetch plenary session documents feed from MCP.
1318
+ *
1319
+ * @param client - MCP client or null
1320
+ * @param timeframe - How far back to look (default: 'one-day')
1321
+ * @returns Array of document feed items
1322
+ */
1323
+ export async function fetchPlenarySessionDocumentsFeed(client, timeframe = 'one-day') {
1324
+ if (!client)
1325
+ return [];
1326
+ try {
1327
+ console.log(`${MCP_FETCH_PREFIX} Fetching plenary session documents feed (${timeframe})...`);
1328
+ const result = await callMCP(() => client.getPlenarySessionDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_plenary_session_documents_feed');
1329
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1330
+ }
1331
+ catch (error) {
1332
+ const message = error instanceof Error ? error.message : String(error);
1333
+ console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed:`, message);
1334
+ return [];
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Fetch external documents feed from MCP.
1339
+ *
1340
+ * @param client - MCP client or null
1341
+ * @param timeframe - How far back to look (default: 'one-day')
1342
+ * @returns Array of document feed items
1343
+ */
1344
+ export async function fetchExternalDocumentsFeed(client, timeframe = 'one-day') {
1345
+ if (!client)
1346
+ return [];
1347
+ try {
1348
+ console.log(`${MCP_FETCH_PREFIX} Fetching external documents feed (${timeframe})...`);
1349
+ const result = await callMCP(() => client.getExternalDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_external_documents_feed');
1350
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1351
+ }
1352
+ catch (error) {
1353
+ const message = error instanceof Error ? error.message : String(error);
1354
+ console.warn(`${WARN_PREFIX} get_external_documents_feed failed:`, message);
1355
+ return [];
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Fetch parliamentary questions feed from MCP.
1360
+ *
1361
+ * @param client - MCP client or null
1362
+ * @param timeframe - How far back to look (default: 'one-day')
1363
+ * @returns Array of question feed items
1364
+ */
1365
+ export async function fetchQuestionsFeed(client, timeframe = 'one-day') {
1366
+ if (!client)
1367
+ return [];
1368
+ try {
1369
+ console.log(`${MCP_FETCH_PREFIX} Fetching parliamentary questions feed (${timeframe})...`);
1370
+ const result = await callMCP(() => client.getParliamentaryQuestionsFeed({ timeframe, limit: 20 }), undefined, 'get_parliamentary_questions_feed');
1371
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1372
+ }
1373
+ catch (error) {
1374
+ const message = error instanceof Error ? error.message : String(error);
1375
+ console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed:`, message);
1376
+ return [];
1377
+ }
1378
+ }
1379
+ /**
1380
+ * Fetch MEP declarations feed from MCP.
1381
+ *
1382
+ * @param client - MCP client or null
1383
+ * @param timeframe - How far back to look (default: 'one-day')
1384
+ * @returns Array of declaration feed items
1385
+ */
1386
+ export async function fetchDeclarationsFeed(client, timeframe = 'one-day') {
1387
+ if (!client)
1388
+ return [];
1389
+ try {
1390
+ console.log(`${MCP_FETCH_PREFIX} Fetching MEP declarations feed (${timeframe})...`);
1391
+ const result = await callMCP(() => client.getMEPDeclarationsFeed({ timeframe, limit: 20 }), undefined, 'get_mep_declarations_feed');
1392
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1393
+ }
1394
+ catch (error) {
1395
+ const message = error instanceof Error ? error.message : String(error);
1396
+ console.warn(`${WARN_PREFIX} get_mep_declarations_feed failed:`, message);
1397
+ return [];
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Fetch corporate bodies feed from MCP.
1402
+ *
1403
+ * @param client - MCP client or null
1404
+ * @param timeframe - How far back to look (default: 'one-day')
1405
+ * @returns Array of corporate body feed items
1406
+ */
1407
+ export async function fetchCorporateBodiesFeed(client, timeframe = 'one-day') {
1408
+ if (!client)
1409
+ return [];
1410
+ try {
1411
+ console.log(`${MCP_FETCH_PREFIX} Fetching corporate bodies feed (${timeframe})...`);
1412
+ const result = await callMCP(() => client.getCorporateBodiesFeed({ timeframe, limit: 20 }), undefined, 'get_corporate_bodies_feed');
1413
+ return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1414
+ }
1415
+ catch (error) {
1416
+ const message = error instanceof Error ? error.message : String(error);
1417
+ console.warn(`${WARN_PREFIX} get_corporate_bodies_feed failed:`, message);
1418
+ return [];
1419
+ }
1420
+ }
1421
+ /**
1422
+ * Fetch all EP feed data for breaking news articles.
1423
+ * Calls adopted texts, events, procedures, and MEPs feeds in parallel.
1424
+ * Returns `undefined` when client is null (MCP unavailable).
1425
+ *
1426
+ * @param client - MCP client or null
1427
+ * @param timeframe - How far back to look (default: 'one-day')
1428
+ * @returns Aggregated feed data for breaking news, or undefined when client is null
1429
+ */
1430
+ export async function fetchBreakingNewsFeedData(client, timeframe = 'one-day') {
1431
+ if (!client)
1432
+ return undefined;
1433
+ if (!mcpCircuitBreaker.canRequest()) {
1434
+ console.warn(`${WARN_PREFIX} Circuit breaker OPEN — treating as MCP unavailable for breaking news feeds`);
1435
+ return undefined;
1436
+ }
1437
+ const [adoptedTexts, events, procedures, mepFeedResult] = await Promise.all([
1438
+ fetchAdoptedTextsFeed(client, timeframe),
1439
+ fetchEventsFeed(client, timeframe),
1440
+ fetchProceduresFeed(client, timeframe),
1441
+ fetchMEPsFeedWithTotal(client, timeframe),
1442
+ ]);
1443
+ const { items: mepUpdates, total: totalMEPUpdates } = mepFeedResult;
1444
+ return {
1445
+ adoptedTexts,
1446
+ events,
1447
+ procedures,
1448
+ mepUpdates,
1449
+ totalMEPUpdates: totalMEPUpdates > 0 ? totalMEPUpdates : undefined,
1450
+ };
1451
+ }
1452
+ /**
1453
+ * Fetch comprehensive EP feed data from all 12 feed endpoints in parallel.
1454
+ * This is the primary data source for all article strategies.
1455
+ *
1456
+ * @param client - MCP client or null
1457
+ * @param timeframe - How far back to look (default: 'one-day')
1458
+ * @param dateRange - Optional inclusive UTC window for filtering feed items
1459
+ * @returns Full EPFeedData or undefined when client is null
1460
+ */
1461
+ export async function fetchEPFeedData(client, timeframe = 'one-day', dateRange) {
1462
+ // Check for pre-fetched feed data file (set by --feed-data CLI arg).
1463
+ // This allows agentic workflows to pass MCP data fetched via framework tools
1464
+ // into the generator without requiring a direct MCP connection.
1465
+ const feedDataFile = process.env['EP_FEED_DATA_FILE'];
1466
+ if (feedDataFile) {
1467
+ const fileData = loadEPFeedDataFromFile(feedDataFile, dateRange);
1468
+ if (fileData)
1469
+ return fileData;
1470
+ console.log(`${WARN_PREFIX} Pre-fetched EP feed data failed to load — falling through to MCP fetch`);
1471
+ }
1472
+ if (!client)
1473
+ return undefined;
1474
+ if (!mcpCircuitBreaker.canRequest()) {
1475
+ console.warn(`${WARN_PREFIX} Circuit breaker OPEN — treating as MCP unavailable for EP feeds`);
1476
+ return undefined;
1477
+ }
1478
+ console.log(`${MCP_FETCH_PREFIX} Fetching comprehensive EP feed data (${timeframe})...`);
1479
+ const [adoptedTexts, events, procedures, mepUpdates, documents, plenaryDocuments, committeeDocuments, plenarySessionDocuments, externalDocuments, questions, declarations, corporateBodies,] = await Promise.all([
1480
+ fetchAdoptedTextsFeed(client, timeframe),
1481
+ fetchEventsFeed(client, timeframe),
1482
+ fetchProceduresFeed(client, timeframe),
1483
+ fetchMEPsFeed(client, timeframe),
1484
+ fetchDocumentsFeed(client, timeframe),
1485
+ fetchPlenaryDocumentsFeed(client, timeframe),
1486
+ fetchCommitteeDocumentsFeed(client, timeframe),
1487
+ fetchPlenarySessionDocumentsFeed(client, timeframe),
1488
+ fetchExternalDocumentsFeed(client, timeframe),
1489
+ fetchQuestionsFeed(client, timeframe),
1490
+ fetchDeclarationsFeed(client, timeframe),
1491
+ fetchCorporateBodiesFeed(client, timeframe),
1492
+ ]);
1493
+ const filteredData = filterEPFeedDataByDateRange({
1494
+ adoptedTexts,
1495
+ events,
1496
+ procedures,
1497
+ mepUpdates,
1498
+ documents,
1499
+ plenaryDocuments,
1500
+ committeeDocuments,
1501
+ plenarySessionDocuments,
1502
+ externalDocuments,
1503
+ questions,
1504
+ declarations,
1505
+ corporateBodies,
1506
+ }, dateRange);
1507
+ const totalItems = filteredData.adoptedTexts.length +
1508
+ filteredData.events.length +
1509
+ filteredData.procedures.length +
1510
+ filteredData.mepUpdates.length +
1511
+ filteredData.documents.length +
1512
+ filteredData.plenaryDocuments.length +
1513
+ filteredData.committeeDocuments.length +
1514
+ filteredData.plenarySessionDocuments.length +
1515
+ filteredData.externalDocuments.length +
1516
+ filteredData.questions.length +
1517
+ filteredData.declarations.length +
1518
+ filteredData.corporateBodies.length;
1519
+ console.log(` ✅ Fetched ${totalItems} total feed items across 12 endpoints`);
1520
+ return filteredData;
1521
+ }
1522
+ //# sourceMappingURL=fetch-stage.js.map