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.
- package/LICENSE +201 -0
- package/README.md +1005 -0
- package/SECURITY.md +151 -0
- package/package.json +131 -0
- package/scripts/constants/committee-indicator-map.d.ts +199 -0
- package/scripts/constants/committee-indicator-map.d.ts.map +1 -0
- package/scripts/constants/committee-indicator-map.js +1224 -0
- package/scripts/constants/committee-indicator-map.js.map +1 -0
- package/scripts/constants/config.d.ts +38 -0
- package/scripts/constants/config.d.ts.map +1 -0
- package/scripts/constants/config.js +66 -0
- package/scripts/constants/config.js.map +1 -0
- package/scripts/constants/language-articles.d.ts +84 -0
- package/scripts/constants/language-articles.d.ts.map +1 -0
- package/scripts/constants/language-articles.js +6771 -0
- package/scripts/constants/language-articles.js.map +1 -0
- package/scripts/constants/language-core.d.ts +38 -0
- package/scripts/constants/language-core.d.ts.map +1 -0
- package/scripts/constants/language-core.js +90 -0
- package/scripts/constants/language-core.js.map +1 -0
- package/scripts/constants/language-ui.d.ts +82 -0
- package/scripts/constants/language-ui.d.ts.map +1 -0
- package/scripts/constants/language-ui.js +889 -0
- package/scripts/constants/language-ui.js.map +1 -0
- package/scripts/constants/languages.d.ts +14 -0
- package/scripts/constants/languages.d.ts.map +1 -0
- package/scripts/constants/languages.js +15 -0
- package/scripts/constants/languages.js.map +1 -0
- package/scripts/generators/analysis-builders.d.ts +266 -0
- package/scripts/generators/analysis-builders.d.ts.map +1 -0
- package/scripts/generators/analysis-builders.js +2903 -0
- package/scripts/generators/analysis-builders.js.map +1 -0
- package/scripts/generators/breaking-content.d.ts +45 -0
- package/scripts/generators/breaking-content.d.ts.map +1 -0
- package/scripts/generators/breaking-content.js +530 -0
- package/scripts/generators/breaking-content.js.map +1 -0
- package/scripts/generators/committee-helpers.d.ts +54 -0
- package/scripts/generators/committee-helpers.d.ts.map +1 -0
- package/scripts/generators/committee-helpers.js +154 -0
- package/scripts/generators/committee-helpers.js.map +1 -0
- package/scripts/generators/dashboard-content.d.ts +95 -0
- package/scripts/generators/dashboard-content.d.ts.map +1 -0
- package/scripts/generators/dashboard-content.js +630 -0
- package/scripts/generators/dashboard-content.js.map +1 -0
- package/scripts/generators/deep-analysis-content.d.ts +23 -0
- package/scripts/generators/deep-analysis-content.d.ts.map +1 -0
- package/scripts/generators/deep-analysis-content.js +831 -0
- package/scripts/generators/deep-analysis-content.js.map +1 -0
- package/scripts/generators/mindmap-content.d.ts +55 -0
- package/scripts/generators/mindmap-content.d.ts.map +1 -0
- package/scripts/generators/mindmap-content.js +512 -0
- package/scripts/generators/mindmap-content.js.map +1 -0
- package/scripts/generators/motions-content.d.ts +50 -0
- package/scripts/generators/motions-content.d.ts.map +1 -0
- package/scripts/generators/motions-content.js +391 -0
- package/scripts/generators/motions-content.js.map +1 -0
- package/scripts/generators/news-enhanced.d.ts +14 -0
- package/scripts/generators/news-enhanced.d.ts.map +1 -0
- package/scripts/generators/news-enhanced.js +169 -0
- package/scripts/generators/news-enhanced.js.map +1 -0
- package/scripts/generators/news-indexes.d.ts +31 -0
- package/scripts/generators/news-indexes.d.ts.map +1 -0
- package/scripts/generators/news-indexes.js +410 -0
- package/scripts/generators/news-indexes.js.map +1 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts +352 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/fetch-stage.js +1522 -0
- package/scripts/generators/pipeline/fetch-stage.js.map +1 -0
- package/scripts/generators/pipeline/generate-stage.d.ts +43 -0
- package/scripts/generators/pipeline/generate-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/generate-stage.js +204 -0
- package/scripts/generators/pipeline/generate-stage.js.map +1 -0
- package/scripts/generators/pipeline/output-stage.d.ts +48 -0
- package/scripts/generators/pipeline/output-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/output-stage.js +145 -0
- package/scripts/generators/pipeline/output-stage.js.map +1 -0
- package/scripts/generators/pipeline/transform-stage.d.ts +57 -0
- package/scripts/generators/pipeline/transform-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/transform-stage.js +111 -0
- package/scripts/generators/pipeline/transform-stage.js.map +1 -0
- package/scripts/generators/propositions-content.d.ts +29 -0
- package/scripts/generators/propositions-content.d.ts.map +1 -0
- package/scripts/generators/propositions-content.js +90 -0
- package/scripts/generators/propositions-content.js.map +1 -0
- package/scripts/generators/sankey-content.d.ts +45 -0
- package/scripts/generators/sankey-content.d.ts.map +1 -0
- package/scripts/generators/sankey-content.js +227 -0
- package/scripts/generators/sankey-content.js.map +1 -0
- package/scripts/generators/sitemap.d.ts +66 -0
- package/scripts/generators/sitemap.d.ts.map +1 -0
- package/scripts/generators/sitemap.js +562 -0
- package/scripts/generators/sitemap.js.map +1 -0
- package/scripts/generators/strategies/article-strategy.d.ts +146 -0
- package/scripts/generators/strategies/article-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/article-strategy.js +4 -0
- package/scripts/generators/strategies/article-strategy.js.map +1 -0
- package/scripts/generators/strategies/breaking-news-strategy.d.ts +64 -0
- package/scripts/generators/strategies/breaking-news-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/breaking-news-strategy.js +246 -0
- package/scripts/generators/strategies/breaking-news-strategy.js.map +1 -0
- package/scripts/generators/strategies/committee-reports-strategy.d.ts +93 -0
- package/scripts/generators/strategies/committee-reports-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/committee-reports-strategy.js +447 -0
- package/scripts/generators/strategies/committee-reports-strategy.js.map +1 -0
- package/scripts/generators/strategies/month-ahead-strategy.d.ts +60 -0
- package/scripts/generators/strategies/month-ahead-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/month-ahead-strategy.js +175 -0
- package/scripts/generators/strategies/month-ahead-strategy.js.map +1 -0
- package/scripts/generators/strategies/monthly-review-strategy.d.ts +66 -0
- package/scripts/generators/strategies/monthly-review-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/monthly-review-strategy.js +204 -0
- package/scripts/generators/strategies/monthly-review-strategy.js.map +1 -0
- package/scripts/generators/strategies/motions-strategy.d.ts +61 -0
- package/scripts/generators/strategies/motions-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/motions-strategy.js +215 -0
- package/scripts/generators/strategies/motions-strategy.js.map +1 -0
- package/scripts/generators/strategies/propositions-strategy.d.ts +60 -0
- package/scripts/generators/strategies/propositions-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/propositions-strategy.js +257 -0
- package/scripts/generators/strategies/propositions-strategy.js.map +1 -0
- package/scripts/generators/strategies/week-ahead-strategy.d.ts +57 -0
- package/scripts/generators/strategies/week-ahead-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/week-ahead-strategy.js +178 -0
- package/scripts/generators/strategies/week-ahead-strategy.js.map +1 -0
- package/scripts/generators/strategies/weekly-review-strategy.d.ts +63 -0
- package/scripts/generators/strategies/weekly-review-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/weekly-review-strategy.js +211 -0
- package/scripts/generators/strategies/weekly-review-strategy.js.map +1 -0
- package/scripts/generators/swot-content.d.ts +42 -0
- package/scripts/generators/swot-content.d.ts.map +1 -0
- package/scripts/generators/swot-content.js +366 -0
- package/scripts/generators/swot-content.js.map +1 -0
- package/scripts/generators/week-ahead-content.d.ts +103 -0
- package/scripts/generators/week-ahead-content.d.ts.map +1 -0
- package/scripts/generators/week-ahead-content.js +610 -0
- package/scripts/generators/week-ahead-content.js.map +1 -0
- package/scripts/index.d.ts +40 -0
- package/scripts/index.d.ts.map +1 -0
- package/scripts/index.js +53 -0
- package/scripts/index.js.map +1 -0
- package/scripts/mcp/ep-mcp-client.d.ts +471 -0
- package/scripts/mcp/ep-mcp-client.d.ts.map +1 -0
- package/scripts/mcp/ep-mcp-client.js +734 -0
- package/scripts/mcp/ep-mcp-client.js.map +1 -0
- package/scripts/mcp/mcp-connection.d.ts +264 -0
- package/scripts/mcp/mcp-connection.d.ts.map +1 -0
- package/scripts/mcp/mcp-connection.js +790 -0
- package/scripts/mcp/mcp-connection.js.map +1 -0
- package/scripts/mcp/mcp-health.d.ts +75 -0
- package/scripts/mcp/mcp-health.d.ts.map +1 -0
- package/scripts/mcp/mcp-health.js +78 -0
- package/scripts/mcp/mcp-health.js.map +1 -0
- package/scripts/mcp/mcp-retry.d.ts +94 -0
- package/scripts/mcp/mcp-retry.d.ts.map +1 -0
- package/scripts/mcp/mcp-retry.js +127 -0
- package/scripts/mcp/mcp-retry.js.map +1 -0
- package/scripts/mcp/wb-mcp-client.d.ts +38 -0
- package/scripts/mcp/wb-mcp-client.d.ts.map +1 -0
- package/scripts/mcp/wb-mcp-client.js +112 -0
- package/scripts/mcp/wb-mcp-client.js.map +1 -0
- package/scripts/templates/article-template.d.ts +9 -0
- package/scripts/templates/article-template.d.ts.map +1 -0
- package/scripts/templates/article-template.js +378 -0
- package/scripts/templates/article-template.js.map +1 -0
- package/scripts/templates/section-builders.d.ts +28 -0
- package/scripts/templates/section-builders.d.ts.map +1 -0
- package/scripts/templates/section-builders.js +142 -0
- package/scripts/templates/section-builders.js.map +1 -0
- package/scripts/types/analysis.d.ts +115 -0
- package/scripts/types/analysis.d.ts.map +1 -0
- package/scripts/types/analysis.js +4 -0
- package/scripts/types/analysis.js.map +1 -0
- package/scripts/types/common.d.ts +584 -0
- package/scripts/types/common.d.ts.map +1 -0
- package/scripts/types/common.js +96 -0
- package/scripts/types/common.js.map +1 -0
- package/scripts/types/generation.d.ts +104 -0
- package/scripts/types/generation.d.ts.map +1 -0
- package/scripts/types/generation.js +4 -0
- package/scripts/types/generation.js.map +1 -0
- package/scripts/types/index.d.ts +24 -0
- package/scripts/types/index.d.ts.map +1 -0
- package/scripts/types/index.js +16 -0
- package/scripts/types/index.js.map +1 -0
- package/scripts/types/intelligence.d.ts +129 -0
- package/scripts/types/intelligence.d.ts.map +1 -0
- package/scripts/types/intelligence.js +4 -0
- package/scripts/types/intelligence.js.map +1 -0
- package/scripts/types/mcp.d.ts +418 -0
- package/scripts/types/mcp.d.ts.map +1 -0
- package/scripts/types/mcp.js +4 -0
- package/scripts/types/mcp.js.map +1 -0
- package/scripts/types/parliament.d.ts +388 -0
- package/scripts/types/parliament.d.ts.map +1 -0
- package/scripts/types/parliament.js +4 -0
- package/scripts/types/parliament.js.map +1 -0
- package/scripts/types/quality.d.ts +114 -0
- package/scripts/types/quality.d.ts.map +1 -0
- package/scripts/types/quality.js +4 -0
- package/scripts/types/quality.js.map +1 -0
- package/scripts/types/stakeholder.d.ts +88 -0
- package/scripts/types/stakeholder.d.ts.map +1 -0
- package/scripts/types/stakeholder.js +16 -0
- package/scripts/types/stakeholder.js.map +1 -0
- package/scripts/types/visualization.d.ts +708 -0
- package/scripts/types/visualization.d.ts.map +1 -0
- package/scripts/types/visualization.js +4 -0
- package/scripts/types/visualization.js.map +1 -0
- package/scripts/types/world-bank.d.ts +85 -0
- package/scripts/types/world-bank.d.ts.map +1 -0
- package/scripts/types/world-bank.js +4 -0
- package/scripts/types/world-bank.js.map +1 -0
- package/scripts/utils/article-category.d.ts +18 -0
- package/scripts/utils/article-category.d.ts.map +1 -0
- package/scripts/utils/article-category.js +49 -0
- package/scripts/utils/article-category.js.map +1 -0
- package/scripts/utils/article-quality-scorer.d.ts +87 -0
- package/scripts/utils/article-quality-scorer.d.ts.map +1 -0
- package/scripts/utils/article-quality-scorer.js +1048 -0
- package/scripts/utils/article-quality-scorer.js.map +1 -0
- package/scripts/utils/content-metadata.d.ts +34 -0
- package/scripts/utils/content-metadata.d.ts.map +1 -0
- package/scripts/utils/content-metadata.js +249 -0
- package/scripts/utils/content-metadata.js.map +1 -0
- package/scripts/utils/content-validator.d.ts +94 -0
- package/scripts/utils/content-validator.d.ts.map +1 -0
- package/scripts/utils/content-validator.js +489 -0
- package/scripts/utils/content-validator.js.map +1 -0
- package/scripts/utils/copy-test-reports.d.ts +9 -0
- package/scripts/utils/copy-test-reports.d.ts.map +1 -0
- package/scripts/utils/copy-test-reports.js +508 -0
- package/scripts/utils/copy-test-reports.js.map +1 -0
- package/scripts/utils/file-utils.d.ts +144 -0
- package/scripts/utils/file-utils.d.ts.map +1 -0
- package/scripts/utils/file-utils.js +374 -0
- package/scripts/utils/file-utils.js.map +1 -0
- package/scripts/utils/fix-articles.d.ts +27 -0
- package/scripts/utils/fix-articles.d.ts.map +1 -0
- package/scripts/utils/fix-articles.js +510 -0
- package/scripts/utils/fix-articles.js.map +1 -0
- package/scripts/utils/generate-docs-index.d.ts +8 -0
- package/scripts/utils/generate-docs-index.d.ts.map +1 -0
- package/scripts/utils/generate-docs-index.js +275 -0
- package/scripts/utils/generate-docs-index.js.map +1 -0
- package/scripts/utils/html-sanitize.d.ts +18 -0
- package/scripts/utils/html-sanitize.d.ts.map +1 -0
- package/scripts/utils/html-sanitize.js +57 -0
- package/scripts/utils/html-sanitize.js.map +1 -0
- package/scripts/utils/intelligence-analysis.d.ts +173 -0
- package/scripts/utils/intelligence-analysis.d.ts.map +1 -0
- package/scripts/utils/intelligence-analysis.js +936 -0
- package/scripts/utils/intelligence-analysis.js.map +1 -0
- package/scripts/utils/intelligence-index.d.ts +126 -0
- package/scripts/utils/intelligence-index.d.ts.map +1 -0
- package/scripts/utils/intelligence-index.js +731 -0
- package/scripts/utils/intelligence-index.js.map +1 -0
- package/scripts/utils/metadata-utils.d.ts +14 -0
- package/scripts/utils/metadata-utils.d.ts.map +1 -0
- package/scripts/utils/metadata-utils.js +18 -0
- package/scripts/utils/metadata-utils.js.map +1 -0
- package/scripts/utils/news-metadata.d.ts +47 -0
- package/scripts/utils/news-metadata.d.ts.map +1 -0
- package/scripts/utils/news-metadata.js +259 -0
- package/scripts/utils/news-metadata.js.map +1 -0
- package/scripts/utils/validate-articles.d.ts +2 -0
- package/scripts/utils/validate-articles.d.ts.map +1 -0
- package/scripts/utils/validate-articles.js +284 -0
- package/scripts/utils/validate-articles.js.map +1 -0
- package/scripts/utils/validate-ep-api.d.ts +51 -0
- package/scripts/utils/validate-ep-api.d.ts.map +1 -0
- package/scripts/utils/validate-ep-api.js +160 -0
- package/scripts/utils/validate-ep-api.js.map +1 -0
- package/scripts/utils/world-bank-data.d.ts +84 -0
- package/scripts/utils/world-bank-data.d.ts.map +1 -0
- package/scripts/utils/world-bank-data.js +311 -0
- 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
|