euparliamentmonitor 0.8.19 → 0.8.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/package.json +7 -7
  2. package/scripts/constants/language-articles.d.ts +4 -0
  3. package/scripts/constants/language-articles.js +20 -0
  4. package/scripts/constants/language-ui.d.ts +8 -8
  5. package/scripts/constants/language-ui.js +64 -64
  6. package/scripts/constants/languages.d.ts +2 -2
  7. package/scripts/constants/languages.js +2 -2
  8. package/scripts/generators/news-enhanced.js +13 -3
  9. package/scripts/generators/pipeline/analysis-classification.d.ts +49 -0
  10. package/scripts/generators/pipeline/analysis-classification.js +333 -0
  11. package/scripts/generators/pipeline/analysis-existing.d.ts +67 -0
  12. package/scripts/generators/pipeline/analysis-existing.js +547 -0
  13. package/scripts/generators/pipeline/analysis-helpers.d.ts +140 -0
  14. package/scripts/generators/pipeline/analysis-helpers.js +266 -0
  15. package/scripts/generators/pipeline/analysis-risk.d.ts +49 -0
  16. package/scripts/generators/pipeline/analysis-risk.js +417 -0
  17. package/scripts/generators/pipeline/analysis-stage.d.ts +19 -39
  18. package/scripts/generators/pipeline/analysis-stage.js +219 -1704
  19. package/scripts/generators/pipeline/analysis-threats.d.ts +41 -0
  20. package/scripts/generators/pipeline/analysis-threats.js +142 -0
  21. package/scripts/generators/pipeline/fetch-stage.d.ts +25 -15
  22. package/scripts/generators/pipeline/fetch-stage.js +293 -117
  23. package/scripts/generators/strategies/article-strategy.d.ts +126 -7
  24. package/scripts/generators/strategies/article-strategy.js +491 -1
  25. package/scripts/generators/strategies/breaking-news-strategy.js +98 -8
  26. package/scripts/generators/strategies/committee-reports-strategy.js +23 -2
  27. package/scripts/generators/strategies/month-ahead-strategy.js +23 -2
  28. package/scripts/generators/strategies/monthly-review-strategy.js +13 -1
  29. package/scripts/generators/strategies/motions-strategy.js +15 -1
  30. package/scripts/generators/strategies/propositions-strategy.js +15 -1
  31. package/scripts/generators/strategies/week-ahead-strategy.js +19 -1
  32. package/scripts/generators/strategies/weekly-review-strategy.js +17 -1
  33. package/scripts/generators/synthesis-summary.d.ts +93 -0
  34. package/scripts/generators/synthesis-summary.js +364 -0
  35. package/scripts/index.d.ts +5 -2
  36. package/scripts/index.js +6 -1
  37. package/scripts/mcp/ep-mcp-client.d.ts +34 -1
  38. package/scripts/mcp/ep-mcp-client.js +110 -2
  39. package/scripts/mcp/mcp-connection.d.ts +3 -1
  40. package/scripts/mcp/mcp-connection.js +35 -4
  41. package/scripts/templates/article-template.js +24 -22
  42. package/scripts/templates/section-builders.js +2 -5
  43. package/scripts/types/index.d.ts +2 -1
  44. package/scripts/types/mcp.d.ts +7 -0
  45. package/scripts/types/political-classification.d.ts +1 -1
  46. package/scripts/types/quality.d.ts +9 -6
  47. package/scripts/types/significance.d.ts +130 -0
  48. package/scripts/types/significance.js +4 -0
  49. package/scripts/utils/article-quality-scorer.d.ts +13 -11
  50. package/scripts/utils/article-quality-scorer.js +36 -23
  51. package/scripts/utils/file-utils.d.ts +2 -2
  52. package/scripts/utils/file-utils.js +2 -2
  53. package/scripts/utils/html-sanitize.d.ts +10 -0
  54. package/scripts/utils/html-sanitize.js +32 -0
  55. package/scripts/utils/political-classification.d.ts +8 -7
  56. package/scripts/utils/political-classification.js +8 -7
  57. package/scripts/utils/political-risk-assessment.d.ts +1 -1
  58. package/scripts/utils/political-risk-assessment.js +1 -1
  59. package/scripts/utils/significance-scoring.d.ts +97 -0
  60. package/scripts/utils/significance-scoring.js +190 -0
@@ -0,0 +1,364 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module Generators/SynthesisSummary
5
+ * @description Aggregation engine that reads per-file analysis outputs and
6
+ * produces a synthesis summary — a single intelligence briefing consumed by
7
+ * article generators to determine narrative direction, headline selection,
8
+ * and publication priority.
9
+ *
10
+ * The synthesiser:
11
+ * 1. Scans the analysis date directory for markdown files
12
+ * 2. Extracts YAML frontmatter (method, confidence) from each
13
+ * 3. Counts SWOT mentions and risk-level keywords
14
+ * 4. Ranks findings by confidence and produces editorial recommendations
15
+ *
16
+ * @see analysis/templates/synthesis-summary.md
17
+ */
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { randomUUID } from 'crypto';
21
+ // ─── Constants ────────────────────────────────────────────────────────────────
22
+ /** Case-insensitive patterns for detecting SWOT mentions in analysis text */
23
+ const SWOT_PATTERNS = {
24
+ strengths: /\bstrength/giu,
25
+ weaknesses: /\bweakness/giu,
26
+ opportunities: /\bopportunit/giu,
27
+ threats: /\bthreat/giu,
28
+ };
29
+ /** Case-insensitive patterns for detecting risk-level mentions */
30
+ const RISK_PATTERNS = {
31
+ critical: /\bcritical\b/giu,
32
+ high: /\bhigh[- ]risk\b/giu,
33
+ medium: /\bmedium[- ]risk\b/giu,
34
+ low: /\blow[- ]risk\b/giu,
35
+ };
36
+ /** Confidence value ordering (higher = better) */
37
+ const CONFIDENCE_RANK = {
38
+ high: 3,
39
+ medium: 2,
40
+ low: 1,
41
+ };
42
+ /** Filename of the synthesis output itself — excluded from scanning to prevent self-contamination */
43
+ const SYNTHESIS_OUTPUT_FILENAME = 'synthesis-summary.md';
44
+ /** Subdirectory containing per-document analysis — excluded to prevent I/O bloat and skewed aggregation */
45
+ const DOCUMENTS_SUBDIR = 'documents';
46
+ // ─── Markdown sanitization ────────────────────────────────────────────────────
47
+ /**
48
+ * Sanitize untrusted text for safe use in a Markdown table cell.
49
+ *
50
+ * Escapes pipe characters, backslashes, and HTML entities, then normalizes
51
+ * whitespace to prevent table layout corruption.
52
+ *
53
+ * @param input - Untrusted cell text
54
+ * @returns Sanitized text safe for Markdown table cells
55
+ */
56
+ function sanitizeMdCell(input) {
57
+ return input
58
+ .replace(/\\/gu, '\\\\')
59
+ .replace(/\|/gu, '\\|')
60
+ .replace(/&/gu, '&')
61
+ .replace(/</gu, '&lt;')
62
+ .replace(/>/gu, '&gt;')
63
+ .replace(/[\r\n]+/gu, ' ')
64
+ .trim();
65
+ }
66
+ /**
67
+ * Parse YAML frontmatter from a markdown file's content.
68
+ *
69
+ * Extracts `method`, `confidence`, and `date` fields from the `---` delimited
70
+ * YAML block at the start of the file. Returns null when no valid frontmatter
71
+ * is found.
72
+ *
73
+ * @param content - Raw markdown content
74
+ * @returns Parsed frontmatter or null
75
+ */
76
+ export function parseFrontmatter(content) {
77
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
78
+ if (!match)
79
+ return null;
80
+ const yaml = match[1] ?? '';
81
+ const methodMatch = /^method:\s+(\S.*)$/mu.exec(yaml);
82
+ const confidenceMatch = /^confidence:\s+(\S.*)$/mu.exec(yaml);
83
+ const dateMatch = /^date:\s+(\S.*)$/mu.exec(yaml);
84
+ const method = methodMatch?.[1]?.trim() ?? 'unknown';
85
+ const rawConf = confidenceMatch?.[1]?.trim().toLowerCase() ?? 'low';
86
+ const confidence = rawConf === 'high' || rawConf === 'medium' ? rawConf : 'low';
87
+ const date = dateMatch?.[1]?.trim() ?? '';
88
+ return { method, confidence, date };
89
+ }
90
+ // ─── Text analysis ────────────────────────────────────────────────────────────
91
+ /**
92
+ * Count regex pattern matches in a body of text.
93
+ *
94
+ * @param text - Source text to scan
95
+ * @param pattern - RegExp with global flag
96
+ * @returns Number of matches
97
+ */
98
+ function countMatches(text, pattern) {
99
+ // Reset lastIndex for global regexps to avoid stale state
100
+ pattern.lastIndex = 0;
101
+ const matches = text.match(pattern);
102
+ return matches ? matches.length : 0;
103
+ }
104
+ /**
105
+ * Aggregate SWOT mention counts from a body of text.
106
+ *
107
+ * @param text - Combined analysis text
108
+ * @returns SWOT counts
109
+ */
110
+ export function aggregateSWOT(text) {
111
+ return {
112
+ strengths: countMatches(text, SWOT_PATTERNS.strengths),
113
+ weaknesses: countMatches(text, SWOT_PATTERNS.weaknesses),
114
+ opportunities: countMatches(text, SWOT_PATTERNS.opportunities),
115
+ threats: countMatches(text, SWOT_PATTERNS.threats),
116
+ };
117
+ }
118
+ /**
119
+ * Aggregate risk-level mention counts from a body of text.
120
+ *
121
+ * @param text - Combined analysis text
122
+ * @returns Risk level counts
123
+ */
124
+ export function aggregateRisks(text) {
125
+ return {
126
+ critical: countMatches(text, RISK_PATTERNS.critical),
127
+ high: countMatches(text, RISK_PATTERNS.high),
128
+ medium: countMatches(text, RISK_PATTERNS.medium),
129
+ low: countMatches(text, RISK_PATTERNS.low),
130
+ };
131
+ }
132
+ /**
133
+ * Extract the first non-empty non-frontmatter heading or paragraph as a
134
+ * one-line summary from a markdown file.
135
+ *
136
+ * @param content - Raw markdown content
137
+ * @returns One-line summary string
138
+ */
139
+ export function extractSummaryLine(content) {
140
+ // Strip frontmatter
141
+ const body = content.replace(/^---[\s\S]*?---\s*/u, '');
142
+ // Try first heading (# followed by at least one space and then non-space content)
143
+ const headingMatch = /^#+\s(\S.*)$/mu.exec(body);
144
+ if (headingMatch?.[1])
145
+ return headingMatch[1].trim();
146
+ // Fall back to first non-empty line
147
+ const lines = body.split('\n');
148
+ for (const line of lines) {
149
+ const trimmed = line.trim();
150
+ if (trimmed.length > 0 && !trimmed.startsWith('|') && !trimmed.startsWith('```')) {
151
+ return trimmed.slice(0, 200);
152
+ }
153
+ }
154
+ return 'No summary available';
155
+ }
156
+ // ─── Confidence aggregation ──────────────────────────────────────────────────
157
+ /**
158
+ * Determine the overall confidence level from a set of findings.
159
+ *
160
+ * Uses majority vote: whichever confidence level appears most often wins.
161
+ *
162
+ * @param findings - Findings with individual confidence levels
163
+ * @returns Aggregated confidence level
164
+ */
165
+ export function aggregateConfidence(findings) {
166
+ if (findings.length === 0)
167
+ return 'low';
168
+ const counts = { high: 0, medium: 0, low: 0 };
169
+ for (const f of findings) {
170
+ counts[f.confidence]++;
171
+ }
172
+ if (counts.high >= counts.medium && counts.high >= counts.low)
173
+ return 'high';
174
+ if (counts.medium >= counts.low)
175
+ return 'medium';
176
+ return 'low';
177
+ }
178
+ // ─── Directory scanning ──────────────────────────────────────────────────────
179
+ /**
180
+ * Recursively find all `.md` analysis files under a directory.
181
+ *
182
+ * Excludes:
183
+ * - The synthesis output file itself (prevents self-contamination on re-runs)
184
+ * - The `documents/` subdirectory (per-document analysis can bloat I/O and skew aggregation)
185
+ *
186
+ * @param dir - Absolute directory path
187
+ * @returns Array of absolute file paths
188
+ */
189
+ export function findMarkdownFiles(dir) {
190
+ const results = [];
191
+ if (!fs.existsSync(dir))
192
+ return results;
193
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
194
+ for (const entry of entries) {
195
+ const fullPath = path.join(dir, entry.name);
196
+ if (entry.isDirectory()) {
197
+ // Skip the documents/ subdirectory to avoid per-document analysis bloat
198
+ if (entry.name === DOCUMENTS_SUBDIR)
199
+ continue;
200
+ results.push(...findMarkdownFiles(fullPath));
201
+ }
202
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
203
+ // Skip the synthesis output itself to prevent self-contamination
204
+ if (entry.name === SYNTHESIS_OUTPUT_FILENAME)
205
+ continue;
206
+ results.push(fullPath);
207
+ }
208
+ }
209
+ return results;
210
+ }
211
+ // ─── Editorial recommendations ───────────────────────────────────────────────
212
+ /**
213
+ * Generate editorial recommendations based on aggregated analysis data.
214
+ *
215
+ * @param findings - Ranked findings
216
+ * @param swot - Aggregated SWOT counts
217
+ * @param risks - Risk level distribution
218
+ * @returns Array of recommendation strings
219
+ */
220
+ export function generateEditorialRecommendations(findings, swot, risks) {
221
+ const recommendations = [];
222
+ if (findings.length === 0) {
223
+ recommendations.push('No analysis files found — verify pipeline execution.');
224
+ return recommendations;
225
+ }
226
+ // High-confidence findings drive lead stories
227
+ const highConfCount = findings.filter((f) => f.confidence === 'high').length;
228
+ if (highConfCount > 0) {
229
+ recommendations.push(`${highConfCount} high-confidence finding(s) available for lead story selection.`);
230
+ }
231
+ // Risk-driven recommendations
232
+ if (risks.critical > 0) {
233
+ recommendations.push(`${risks.critical} critical-risk mention(s) detected — consider priority coverage.`);
234
+ }
235
+ // SWOT balance indicator
236
+ const totalSwot = swot.strengths + swot.weaknesses + swot.opportunities + swot.threats;
237
+ if (totalSwot > 0) {
238
+ const threatRatio = swot.threats / totalSwot;
239
+ if (threatRatio > 0.4) {
240
+ recommendations.push('Threat-heavy SWOT balance — narrative may benefit from opportunity framing.');
241
+ }
242
+ }
243
+ // Volume recommendation
244
+ if (findings.length >= 10) {
245
+ recommendations.push(`${findings.length} analysis files processed — consider multi-article output.`);
246
+ }
247
+ else if (findings.length <= 2) {
248
+ recommendations.push('Limited analysis coverage — consider consolidating into a single digest article.');
249
+ }
250
+ return recommendations;
251
+ }
252
+ // ─── Main synthesis ──────────────────────────────────────────────────────────
253
+ /**
254
+ * Build a synthesis summary from all analysis files in a date directory.
255
+ *
256
+ * Scans the directory recursively for `.md` analysis files, parses their
257
+ * frontmatter, extracts findings, aggregates SWOT and risk mentions, and
258
+ * produces a {@link SynthesisSummary} object.
259
+ *
260
+ * @param dateOutputDir - Absolute path to the date-scoped analysis directory
261
+ * @param date - ISO date string (YYYY-MM-DD)
262
+ * @returns Synthesis summary object
263
+ */
264
+ export function buildSynthesisSummary(dateOutputDir, date) {
265
+ const files = findMarkdownFiles(dateOutputDir);
266
+ const findings = [];
267
+ let combinedText = '';
268
+ for (const filePath of files) {
269
+ const content = fs.readFileSync(filePath, 'utf-8');
270
+ const frontmatter = parseFrontmatter(content);
271
+ combinedText += content + '\n';
272
+ findings.push({
273
+ method: frontmatter?.method ?? 'unknown',
274
+ file: path.basename(filePath),
275
+ confidence: frontmatter?.confidence ?? 'low',
276
+ summary: extractSummaryLine(content),
277
+ });
278
+ }
279
+ // Sort findings: high confidence first, then medium, then low.
280
+ // The heading in the output says "Top Findings by Confidence" to match.
281
+ findings.sort((a, b) => (CONFIDENCE_RANK[b.confidence] ?? 0) - (CONFIDENCE_RANK[a.confidence] ?? 0));
282
+ const swot = aggregateSWOT(combinedText);
283
+ const riskOverview = aggregateRisks(combinedText);
284
+ const overallConfidence = aggregateConfidence(findings);
285
+ const editorialRecommendations = generateEditorialRecommendations(findings, swot, riskOverview);
286
+ return {
287
+ synthesisId: `SYN-${date}-${randomUUID().slice(0, 8).toUpperCase()}`,
288
+ date,
289
+ documentsAnalyzed: files.length,
290
+ overallConfidence,
291
+ topFindings: findings.slice(0, 5),
292
+ swot,
293
+ riskOverview,
294
+ editorialRecommendations,
295
+ };
296
+ }
297
+ /**
298
+ * Generate a markdown report from a synthesis summary.
299
+ *
300
+ * Follows the template format defined in `analysis/templates/synthesis-summary.md`.
301
+ *
302
+ * @param summary - Computed synthesis summary
303
+ * @returns Markdown string
304
+ */
305
+ export function formatSynthesisMarkdown(summary) {
306
+ const findingsRows = summary.topFindings
307
+ .map((f, i) => `| ${i + 1} | ${sanitizeMdCell(f.file)} | ${sanitizeMdCell(f.method)} | ${f.confidence} | ${sanitizeMdCell(f.summary.slice(0, 80))} |`)
308
+ .join('\n');
309
+ return `---
310
+ method: synthesis-summary
311
+ date: ${summary.date}
312
+ confidence: ${summary.overallConfidence}
313
+ generated: ${new Date().toISOString()}
314
+ ---
315
+
316
+ # 🧩 Synthesis Summary — ${summary.date}
317
+
318
+ ## 📋 Synthesis Context
319
+
320
+ | Field | Value |
321
+ |-------|-------|
322
+ | **Synthesis ID** | \`${summary.synthesisId}\` |
323
+ | **Analysis Date** | \`${summary.date}\` |
324
+ | **Documents Analyzed** | ${summary.documentsAnalyzed} |
325
+ | **Overall Confidence** | ${summary.overallConfidence.toUpperCase()} |
326
+
327
+ ---
328
+
329
+ ## 🏆 Top Findings by Confidence
330
+
331
+ | Rank | File | Method | Confidence | Summary |
332
+ |:----:|------|--------|:----------:|---------|
333
+ ${findingsRows || '| — | — | — | — | — |'}
334
+
335
+ ---
336
+
337
+ ## 💪 Aggregated SWOT Summary
338
+
339
+ | Dimension | Count |
340
+ |-----------|:-----:|
341
+ | ✅ Strengths | ${summary.swot.strengths} |
342
+ | ⚠️ Weaknesses | ${summary.swot.weaknesses} |
343
+ | 🚀 Opportunities | ${summary.swot.opportunities} |
344
+ | 🔴 Threats | ${summary.swot.threats} |
345
+
346
+ ---
347
+
348
+ ## ⚖️ Risk Landscape Summary
349
+
350
+ | Level | Mentions |
351
+ |-------|:--------:|
352
+ | 🔴 Critical | ${summary.riskOverview.critical} |
353
+ | 🟠 High | ${summary.riskOverview.high} |
354
+ | 🟡 Medium | ${summary.riskOverview.medium} |
355
+ | 🟢 Low | ${summary.riskOverview.low} |
356
+
357
+ ---
358
+
359
+ ## 🎯 Editorial Recommendations
360
+
361
+ ${summary.editorialRecommendations.map((r) => `- ${r}`).join('\n')}
362
+ `;
363
+ }
364
+ //# sourceMappingURL=synthesis-summary.js.map
@@ -34,12 +34,14 @@ export { type ToolHealthEntry, type HealthSnapshot, MCPHealthMonitor } from './m
34
34
  export { scoreVotingAnomaly, analyzeCoalitionCohesion, scoreMEPInfluence, calculateLegislativeVelocity, rankBySignificance, buildIntelligenceSection, buildDefaultStakeholderPerspectives, scoreStakeholderInfluence, buildStakeholderOutcomeMatrix, rankStakeholdersByInfluence, computeVotingIntensity, detectCoalitionShifts, computePolarizationIndex, detectVotingTrends, computeCrossSessionCoalitionStability, rankMEPInfluenceByTopic, buildLegislativeVelocityReport, } from './utils/intelligence-analysis.js';
35
35
  export { createEmptyIndex, addArticleToIndex, buildIndexFromEntries, findRelatedArticles, generateCrossReferences, detectTrends, findOrCreateSeries, buildRelatedArticlesHTML, } from './utils/intelligence-index.js';
36
36
  export { assessAnalysisDepth, assessStakeholderCoverage, assessVisualizationQuality, calculateOverallScore, generateRecommendations, scoreArticleQuality, } from './utils/article-quality-scorer.js';
37
+ export { scoreSignificance, scoreBatch, clampScore, deriveDecision, formatScoreMarkdown, formatBatchMarkdown, WEIGHT_PARLIAMENTARY, WEIGHT_POLICY, WEIGHT_PUBLIC_INTEREST, WEIGHT_URGENCY, WEIGHT_INSTITUTIONAL, THRESHOLD_PUBLISH, THRESHOLD_HOLD, } from './utils/significance-scoring.js';
38
+ export { parseFrontmatter, aggregateSWOT, aggregateRisks, extractSummaryLine, aggregateConfidence, findMarkdownFiles, generateEditorialRecommendations, buildSynthesisSummary, formatSynthesisMarkdown, } from './generators/synthesis-summary.js';
37
39
  export { validateArticleContent, validateTranslationCompleteness, } from './utils/content-validator.js';
38
40
  export { enrichMetadataFromContent } from './utils/content-metadata.js';
39
41
  export { buildMetadataDatabase, writeMetadataDatabase, readMetadataDatabase, updateMetadataDatabase, updateIntelligenceIndex, } from './utils/news-metadata.js';
40
42
  export { pl, pl as pluralizeCount } from './utils/metadata-utils.js';
41
43
  export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree, analyzeLegislativeDisruption, generateThreatAssessmentMarkdown, ALL_THREAT_LANDSCAPE_DIMENSIONS, } from './utils/political-threat-assessment.js';
42
- export { stripScriptBlocks } from './utils/html-sanitize.js';
44
+ export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
43
45
  export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, type ArticleValidationResult, } from './utils/file-utils.js';
44
46
  export { detectCategory } from './utils/article-category.js';
45
47
  export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
@@ -58,7 +60,8 @@ export { mcpCircuitBreaker, computeRollingDateRange, initializeMCPClient, loadFe
58
60
  export { type ValidationResult, validateMCPResponse, normalizeISO8601Date, sanitizeText, isValidCountryCode, isValidLanguageCode, } from './generators/pipeline/transform-stage.js';
59
61
  export { type StrategyRegistry, createStrategyRegistry, generateArticleForStrategy, } from './generators/pipeline/generate-stage.js';
60
62
  export { type OutputOptions, writeArticleFile, writeSingleArticle, writeGenerationMetadata, } from './generators/pipeline/output-stage.js';
61
- export type { ArticleData, ArticleMetadata, ArticleStrategyBase, ArticleStrategy, } from './generators/strategies/article-strategy.js';
63
+ export type { ArticleData, ArticleMetadata, ArticleStrategyBase, ArticleStrategy, LoadedAnalysisContext, AnalysisFileContent, } from './generators/strategies/article-strategy.js';
64
+ export { loadAnalysisContext, extractAnalysisSummary, buildAnalysisInsightsSection, } from './generators/strategies/article-strategy.js';
62
65
  export { type BreakingNewsArticleData, BreakingNewsStrategy, breakingNewsStrategy, } from './generators/strategies/breaking-news-strategy.js';
63
66
  export { type CommitteeReportsArticleData, type CommitteeTheme, AFET_KEYWORDS, LIBE_KEYWORDS, AGRI_KEYWORDS, ENVI_KEYWORDS, ECON_KEYWORDS, categorizeAdoptedText, CommitteeReportsStrategy, committeeReportsStrategy, } from './generators/strategies/committee-reports-strategy.js';
64
67
  export { type MonthAheadArticleData, MonthAheadStrategy, monthAheadStrategy, } from './generators/strategies/month-ahead-strategy.js';
package/scripts/index.js CHANGED
@@ -41,6 +41,10 @@ export { scoreVotingAnomaly, analyzeCoalitionCohesion, scoreMEPInfluence, calcul
41
41
  export { createEmptyIndex, addArticleToIndex, buildIndexFromEntries, findRelatedArticles, generateCrossReferences, detectTrends, findOrCreateSeries, buildRelatedArticlesHTML, } from './utils/intelligence-index.js';
42
42
  // ─── Article Quality ─────────────────────────────────────────────────────────
43
43
  export { assessAnalysisDepth, assessStakeholderCoverage, assessVisualizationQuality, calculateOverallScore, generateRecommendations, scoreArticleQuality, } from './utils/article-quality-scorer.js';
44
+ // ─── Significance Scoring ────────────────────────────────────────────────────
45
+ export { scoreSignificance, scoreBatch, clampScore, deriveDecision, formatScoreMarkdown, formatBatchMarkdown, WEIGHT_PARLIAMENTARY, WEIGHT_POLICY, WEIGHT_PUBLIC_INTEREST, WEIGHT_URGENCY, WEIGHT_INSTITUTIONAL, THRESHOLD_PUBLISH, THRESHOLD_HOLD, } from './utils/significance-scoring.js';
46
+ // ─── Synthesis Summary ───────────────────────────────────────────────────────
47
+ export { parseFrontmatter, aggregateSWOT, aggregateRisks, extractSummaryLine, aggregateConfidence, findMarkdownFiles, generateEditorialRecommendations, buildSynthesisSummary, formatSynthesisMarkdown, } from './generators/synthesis-summary.js';
44
48
  // ─── Content Validation ──────────────────────────────────────────────────────
45
49
  export { validateArticleContent, validateTranslationCompleteness, } from './utils/content-validator.js';
46
50
  // ─── Content Metadata ────────────────────────────────────────────────────────
@@ -52,7 +56,7 @@ export { pl, pl as pluralizeCount } from './utils/metadata-utils.js';
52
56
  // ─── Political Threat Assessment ─────────────────────────────────────────────
53
57
  export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree, analyzeLegislativeDisruption, generateThreatAssessmentMarkdown, ALL_THREAT_LANDSCAPE_DIMENSIONS, } from './utils/political-threat-assessment.js';
54
58
  // ─── HTML Utilities ──────────────────────────────────────────────────────────
55
- export { stripScriptBlocks } from './utils/html-sanitize.js';
59
+ export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
56
60
  export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, } from './utils/file-utils.js';
57
61
  // ─── Article Category Detection ──────────────────────────────────────────────
58
62
  export { detectCategory } from './utils/article-category.js';
@@ -80,6 +84,7 @@ export { mcpCircuitBreaker, computeRollingDateRange, initializeMCPClient, loadFe
80
84
  export { validateMCPResponse, normalizeISO8601Date, sanitizeText, isValidCountryCode, isValidLanguageCode, } from './generators/pipeline/transform-stage.js';
81
85
  export { createStrategyRegistry, generateArticleForStrategy, } from './generators/pipeline/generate-stage.js';
82
86
  export { writeArticleFile, writeSingleArticle, writeGenerationMetadata, } from './generators/pipeline/output-stage.js';
87
+ export { loadAnalysisContext, extractAnalysisSummary, buildAnalysisInsightsSection, } from './generators/strategies/article-strategy.js';
83
88
  export { BreakingNewsStrategy, breakingNewsStrategy, } from './generators/strategies/breaking-news-strategy.js';
84
89
  export { AFET_KEYWORDS, LIBE_KEYWORDS, AGRI_KEYWORDS, ENVI_KEYWORDS, ECON_KEYWORDS, categorizeAdoptedText, CommitteeReportsStrategy, committeeReportsStrategy, } from './generators/strategies/committee-reports-strategy.js';
85
90
  export { MonthAheadStrategy, monthAheadStrategy, } from './generators/strategies/month-ahead-strategy.js';
@@ -4,12 +4,16 @@
4
4
  * built on top of the generic {@link MCPConnection} transport.
5
5
  */
6
6
  import { MCPConnection } from './mcp-connection.js';
7
- import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions } from '../types/index.js';
7
+ import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions, GetProcedureEventByIdOptions } from '../types/index.js';
8
8
  /**
9
9
  * MCP Client for European Parliament data access.
10
10
  * Extends {@link MCPConnection} with EP-specific tool wrapper methods.
11
11
  */
12
12
  export declare class EuropeanParliamentMCPClient extends MCPConnection {
13
+ /** Tracks tools that returned fallback data in the current session */
14
+ private readonly _failedTools;
15
+ /** Tracks tools that have been called (attempted) in the current session */
16
+ private readonly _calledTools;
13
17
  /**
14
18
  * Generic error-safe wrapper around {@link callToolWithRetry}.
15
19
  * Retries transient failures (timeouts, connection drops) with a bounded
@@ -28,6 +32,19 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
28
32
  * @returns Tool result or fallback
29
33
  */
30
34
  private safeCallTool;
35
+ /**
36
+ * Get a summary of tools that returned fallback data in the current session.
37
+ * Useful for diagnosing feed availability and data quality issues.
38
+ *
39
+ * @returns Map of tool name to error description
40
+ */
41
+ getFailedTools(): ReadonlyMap<string, string>;
42
+ /**
43
+ * Get a human-readable feed health summary for diagnostics.
44
+ *
45
+ * @returns Formatted summary of feed availability
46
+ */
47
+ getFeedHealthSummary(): string;
31
48
  /**
32
49
  * Get Members of European Parliament
33
50
  *
@@ -456,6 +473,22 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
456
473
  * @returns Controlled vocabularies feed data
457
474
  */
458
475
  getControlledVocabulariesFeed(options?: GetControlledVocabulariesFeedOptions): Promise<MCPToolResult>;
476
+ /**
477
+ * Get a specific event linked to a legislative procedure.
478
+ * Returns a single event for the specified procedure and event identifiers.
479
+ *
480
+ * @param options - Options including required processId and eventId
481
+ * @returns Procedure event data
482
+ */
483
+ getProcedureEventById(options: GetProcedureEventByIdOptions): Promise<MCPToolResult>;
484
+ /**
485
+ * Check server health and feed availability status.
486
+ * Returns server version, uptime, per-feed health status, and overall availability.
487
+ * Does not make upstream API calls — reports cached status from recent tool invocations.
488
+ *
489
+ * @returns Server health and feed availability data
490
+ */
491
+ getServerHealth(): Promise<MCPToolResult>;
459
492
  }
460
493
  /**
461
494
  * Get or create singleton MCP client instance
@@ -22,11 +22,19 @@ const ITEMS_FALLBACK = '{"items": []}';
22
22
  const INTELLIGENCE_FALLBACK = '{"analysis": null}';
23
23
  /** Fallback payload for precomputed statistics */
24
24
  const STATS_FALLBACK = '{"stats": null}';
25
+ /** Fallback payload for single procedure event lookup */
26
+ const PROCEDURE_EVENT_FALLBACK = '{"event": null}';
27
+ /** Fallback payload for server health status */
28
+ const SERVER_HEALTH_FALLBACK = '{"server": null, "feeds": []}';
25
29
  /**
26
30
  * MCP Client for European Parliament data access.
27
31
  * Extends {@link MCPConnection} with EP-specific tool wrapper methods.
28
32
  */
29
33
  export class EuropeanParliamentMCPClient extends MCPConnection {
34
+ /** Tracks tools that returned fallback data in the current session */
35
+ _failedTools = new Map();
36
+ /** Tracks tools that have been called (attempted) in the current session */
37
+ _calledTools = new Set();
30
38
  /**
31
39
  * Generic error-safe wrapper around {@link callToolWithRetry}.
32
40
  * Retries transient failures (timeouts, connection drops) with a bounded
@@ -45,16 +53,88 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
45
53
  * @returns Tool result or fallback
46
54
  */
47
55
  async safeCallTool(toolName, args, fallbackText) {
56
+ this._calledTools.add(toolName);
48
57
  try {
49
58
  const resolvedArgs = typeof args === 'function' ? args() : args;
50
- return await this.callToolWithRetry(toolName, resolvedArgs);
59
+ const result = await this.callToolWithRetry(toolName, resolvedArgs);
60
+ // Clear from failed tools on success
61
+ this._failedTools.delete(toolName);
62
+ return result;
51
63
  }
52
64
  catch (error) {
53
65
  const message = error instanceof Error ? error.message : String(error);
54
- console.warn(`${toolName} not available:`, message);
66
+ const lowerMsg = message.toLowerCase();
67
+ // Classify the error for better diagnostics.
68
+ // Check gateway 5xx first — a "504 Gateway Timeout" should be SERVER_ERROR,
69
+ // not TIMEOUT (which is reserved for client-side request timeouts).
70
+ const isGatewayServerError = lowerMsg.includes('gateway timeout') ||
71
+ lowerMsg.includes('gateway error 500') ||
72
+ lowerMsg.includes('gateway error 502') ||
73
+ lowerMsg.includes('gateway error 503') ||
74
+ lowerMsg.includes('gateway error 504');
75
+ const errorType = isGatewayServerError
76
+ ? 'SERVER_ERROR'
77
+ : lowerMsg.includes('404')
78
+ ? 'NOT_FOUND'
79
+ : lowerMsg.includes('timeout')
80
+ ? 'TIMEOUT'
81
+ : 'UNKNOWN';
82
+ this._failedTools.set(toolName, `${errorType}: ${message}`);
83
+ console.warn(`⚠️ ${toolName} failed [${errorType}]:`, message);
55
84
  return { content: [{ type: 'text', text: fallbackText }] };
56
85
  }
57
86
  }
87
+ /**
88
+ * Get a summary of tools that returned fallback data in the current session.
89
+ * Useful for diagnosing feed availability and data quality issues.
90
+ *
91
+ * @returns Map of tool name to error description
92
+ */
93
+ getFailedTools() {
94
+ return new Map(this._failedTools);
95
+ }
96
+ /**
97
+ * Get a human-readable feed health summary for diagnostics.
98
+ *
99
+ * @returns Formatted summary of feed availability
100
+ */
101
+ getFeedHealthSummary() {
102
+ const feedTools = [
103
+ 'get_meps_feed',
104
+ 'get_events_feed',
105
+ 'get_procedures_feed',
106
+ 'get_adopted_texts_feed',
107
+ 'get_mep_declarations_feed',
108
+ 'get_documents_feed',
109
+ 'get_plenary_documents_feed',
110
+ 'get_committee_documents_feed',
111
+ 'get_plenary_session_documents_feed',
112
+ 'get_external_documents_feed',
113
+ 'get_parliamentary_questions_feed',
114
+ 'get_corporate_bodies_feed',
115
+ 'get_controlled_vocabularies_feed',
116
+ ];
117
+ const lines = ['EP MCP Feed Health:'];
118
+ let operational = 0;
119
+ let unchecked = 0;
120
+ for (const tool of feedTools) {
121
+ const error = this._failedTools.get(tool);
122
+ if (error) {
123
+ lines.push(` ❌ ${tool}: ${error}`);
124
+ }
125
+ else if (this._calledTools.has(tool)) {
126
+ lines.push(` ✅ ${tool}`);
127
+ operational++;
128
+ }
129
+ else {
130
+ lines.push(` ⚪ ${tool} (not checked)`);
131
+ unchecked++;
132
+ }
133
+ }
134
+ const checked = feedTools.length - unchecked;
135
+ lines.push(` Summary: ${operational}/${checked} checked feeds operational${unchecked > 0 ? `, ${unchecked} unchecked` : ''}`);
136
+ return lines.join('\n');
137
+ }
58
138
  /**
59
139
  * Get Members of European Parliament
60
140
  *
@@ -707,6 +787,34 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
707
787
  async getControlledVocabulariesFeed(options = {}) {
708
788
  return this.safeCallTool('get_controlled_vocabularies_feed', options, EuropeanParliamentMCPClient.FEED_FALLBACK);
709
789
  }
790
+ /**
791
+ * Get a specific event linked to a legislative procedure.
792
+ * Returns a single event for the specified procedure and event identifiers.
793
+ *
794
+ * @param options - Options including required processId and eventId
795
+ * @returns Procedure event data
796
+ */
797
+ async getProcedureEventById(options) {
798
+ if (typeof options.processId !== 'string' || options.processId.trim().length === 0) {
799
+ console.warn('get_procedure_event_by_id called without valid processId (non-empty string required)');
800
+ return { content: [{ type: 'text', text: PROCEDURE_EVENT_FALLBACK }] };
801
+ }
802
+ if (typeof options.eventId !== 'string' || options.eventId.trim().length === 0) {
803
+ console.warn('get_procedure_event_by_id called without valid eventId (non-empty string required)');
804
+ return { content: [{ type: 'text', text: PROCEDURE_EVENT_FALLBACK }] };
805
+ }
806
+ return this.safeCallTool('get_procedure_event_by_id', { processId: options.processId.trim(), eventId: options.eventId.trim() }, PROCEDURE_EVENT_FALLBACK);
807
+ }
808
+ /**
809
+ * Check server health and feed availability status.
810
+ * Returns server version, uptime, per-feed health status, and overall availability.
811
+ * Does not make upstream API calls — reports cached status from recent tool invocations.
812
+ *
813
+ * @returns Server health and feed availability data
814
+ */
815
+ async getServerHealth() {
816
+ return this.safeCallTool('get_server_health', {}, SERVER_HEALTH_FALLBACK);
817
+ }
710
818
  }
711
819
  let clientInstance = null;
712
820
  /**
@@ -17,7 +17,8 @@ export declare class MCPRateLimitError extends Error {
17
17
  }
18
18
  /**
19
19
  * Returns true only for transient, retriable failures: request timeouts,
20
- * network-level connection-closed/reset errors, and "not connected" states.
20
+ * network-level connection-closed/reset errors, "not connected" states,
21
+ * and transient HTTP gateway errors (502, 503, 504).
21
22
  *
22
23
  * Uses an allow-list of known transient error patterns so that unknown or
23
24
  * server-level errors (e.g., tool runtime failures) are NOT retried:
@@ -25,6 +26,7 @@ export declare class MCPRateLimitError extends Error {
25
26
  * - connection closed / reset / refused — network-level transport failures
26
27
  * - not connected — local "not yet connected" guard error
27
28
  * - socket hang up — Node.js HTTP socket-level disconnection
29
+ * - gateway error 502/503/504 — transient upstream server errors
28
30
  *
29
31
  * Everything else (MCPSessionExpiredError, TypeError, rate-limit errors,
30
32
  * unknown errors) returns false so `callToolWithRetry` surfaces them immediately.