euparliamentmonitor 0.9.1 → 0.9.3
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/README.md +2 -2
- package/package.json +5 -5
- package/scripts/aggregator/analysis-aggregator.d.ts +11 -0
- package/scripts/aggregator/analysis-aggregator.js +25 -6
- package/scripts/aggregator/article-generator.d.ts +18 -0
- package/scripts/aggregator/article-generator.js +72 -7
- package/scripts/aggregator/article-html.d.ts +44 -2
- package/scripts/aggregator/article-html.js +654 -12
- package/scripts/aggregator/article-metadata.d.ts +75 -0
- package/scripts/aggregator/article-metadata.js +482 -32
- package/scripts/aggregator/reader-intelligence-guide.d.ts +23 -2
- package/scripts/aggregator/reader-intelligence-guide.js +344 -9
- package/scripts/config/article-horizons.js +17 -2
- package/scripts/generators/news-indexes.js +3 -1
- package/scripts/mcp/ep-mcp-client.d.ts +6 -6
- package/scripts/mcp/ep-mcp-client.js +8 -8
- package/scripts/templates/section-builders.d.ts +2 -3
- package/scripts/templates/section-builders.js +15 -12
- package/scripts/types/mcp.d.ts +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ The published site is the audience-facing companion to this npm/TypeScript packa
|
|
|
136
136
|
|
|
137
137
|
**MCP Server Integration**: The project uses the
|
|
138
138
|
[European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server)
|
|
139
|
-
v1.3.
|
|
139
|
+
v1.3.2 for accessing real EU Parliament data via the Model Context Protocol.
|
|
140
140
|
|
|
141
141
|
- **MCP Server Status**: ✅ Fully operational — 60+ EP data tools available
|
|
142
142
|
(feeds, direct lookups, analytical tools, intelligence correlation)
|
|
@@ -432,7 +432,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
|
|
|
432
432
|
|
|
433
433
|
## 🔌 Data Sources
|
|
434
434
|
|
|
435
|
-
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.3.
|
|
435
|
+
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.3.2+, fully operational):
|
|
436
436
|
|
|
437
437
|
- 🗳️ Plenary sessions, voting records, roll-call votes
|
|
438
438
|
- 📜 Adopted texts, motions, resolutions, urgency files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"@playwright/test": "1.59.1",
|
|
147
147
|
"@types/d3": "7.4.3",
|
|
148
148
|
"@types/markdown-it": "^14.1.2",
|
|
149
|
-
"@types/node": "25.6.
|
|
149
|
+
"@types/node": "25.6.2",
|
|
150
150
|
"@types/papaparse": "5.5.2",
|
|
151
151
|
"@typescript-eslint/eslint-plugin": "8.59.2",
|
|
152
152
|
"@typescript-eslint/parser": "8.59.2",
|
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
"happy-dom": "20.9.0",
|
|
164
164
|
"htmlhint": "1.9.2",
|
|
165
165
|
"husky": "9.1.7",
|
|
166
|
-
"jscpd": "4.0
|
|
166
|
+
"jscpd": "4.1.0",
|
|
167
167
|
"knip": "^6.7.0",
|
|
168
|
-
"lint-staged": "17.0.
|
|
168
|
+
"lint-staged": "17.0.4",
|
|
169
169
|
"mermaid": "11.14.0",
|
|
170
170
|
"papaparse": "5.5.3",
|
|
171
171
|
"prettier": "3.8.3",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"node": ">=26"
|
|
180
180
|
},
|
|
181
181
|
"dependencies": {
|
|
182
|
-
"european-parliament-mcp-server": "1.3.
|
|
182
|
+
"european-parliament-mcp-server": "1.3.2",
|
|
183
183
|
"markdown-it": "^14.1.1",
|
|
184
184
|
"markdown-it-anchor": "^9.2.0",
|
|
185
185
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -115,6 +115,17 @@ export declare function renderProvenanceBlock(params: {
|
|
|
115
115
|
* @returns Markdown block with two subsections (methodologies, templates)
|
|
116
116
|
*/
|
|
117
117
|
export declare function renderTradecraftAppendix(files: readonly string[]): string;
|
|
118
|
+
/**
|
|
119
|
+
* Public re-export of the internal `humanize` helper so other aggregator
|
|
120
|
+
* modules (in particular `article-html.ts`) can derive the same display
|
|
121
|
+
* title from a file stem when no curated title is available. Keeping the
|
|
122
|
+
* single canonical implementation here avoids duplicate humanisation
|
|
123
|
+
* rules drifting across modules.
|
|
124
|
+
*
|
|
125
|
+
* @param stem - File stem (e.g. `electoral-cycle-methodology`)
|
|
126
|
+
* @returns Humanised title (e.g. `Electoral Cycle Methodology`)
|
|
127
|
+
*/
|
|
128
|
+
export declare function humanizeStem(stem: string): string;
|
|
118
129
|
/**
|
|
119
130
|
* Render the analysis-index appendix — a compact table of every included
|
|
120
131
|
* artifact and its section, plus a direct link to the manifest.
|
|
@@ -213,19 +213,25 @@ export function renderTradecraftAppendix(files) {
|
|
|
213
213
|
'This article is produced under the [Hack23 AB](https://hack23.com) intelligence tradecraft library. Every methodology and artifact template applied to this run is linked below.',
|
|
214
214
|
'',
|
|
215
215
|
];
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
// Order: Artifact templates first (concrete deliverables readers
|
|
217
|
+
// recognise from the article body), then Methodologies (the underlying
|
|
218
|
+
// tradecraft library). This matches the natural reader flow — the
|
|
219
|
+
// article is built from artifacts, and the methodologies explain how
|
|
220
|
+
// each artifact is produced — and pairs with the contextual, kind-
|
|
221
|
+
// aware "View …" CTAs rendered in `enhanceTradecraftCards`.
|
|
222
|
+
if (templates.length > 0) {
|
|
223
|
+
block.push('### Artifact templates');
|
|
218
224
|
block.push('');
|
|
219
|
-
for (const rel of
|
|
225
|
+
for (const rel of templates) {
|
|
220
226
|
const stem = rel.split('/').pop()?.replace(/\.md$/i, '') ?? rel;
|
|
221
227
|
block.push(`- [${humanize(stem)}](${githubBlobUrl(rel)})`);
|
|
222
228
|
}
|
|
223
229
|
block.push('');
|
|
224
230
|
}
|
|
225
|
-
if (
|
|
226
|
-
block.push('###
|
|
231
|
+
if (methods.length > 0) {
|
|
232
|
+
block.push('### Methodologies');
|
|
227
233
|
block.push('');
|
|
228
|
-
for (const rel of
|
|
234
|
+
for (const rel of methods) {
|
|
229
235
|
const stem = rel.split('/').pop()?.replace(/\.md$/i, '') ?? rel;
|
|
230
236
|
block.push(`- [${humanize(stem)}](${githubBlobUrl(rel)})`);
|
|
231
237
|
}
|
|
@@ -233,6 +239,19 @@ export function renderTradecraftAppendix(files) {
|
|
|
233
239
|
}
|
|
234
240
|
return block.join('\n');
|
|
235
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Public re-export of the internal `humanize` helper so other aggregator
|
|
244
|
+
* modules (in particular `article-html.ts`) can derive the same display
|
|
245
|
+
* title from a file stem when no curated title is available. Keeping the
|
|
246
|
+
* single canonical implementation here avoids duplicate humanisation
|
|
247
|
+
* rules drifting across modules.
|
|
248
|
+
*
|
|
249
|
+
* @param stem - File stem (e.g. `electoral-cycle-methodology`)
|
|
250
|
+
* @returns Humanised title (e.g. `Electoral Cycle Methodology`)
|
|
251
|
+
*/
|
|
252
|
+
export function humanizeStem(stem) {
|
|
253
|
+
return humanize(stem);
|
|
254
|
+
}
|
|
236
255
|
/**
|
|
237
256
|
* Render the analysis-index appendix — a compact table of every included
|
|
238
257
|
* artifact and its section, plus a direct link to the manifest.
|
|
@@ -100,6 +100,24 @@ export declare function sanitizeRunSuffix(runId: string): string;
|
|
|
100
100
|
* @returns Plain-text description, truncated to ≤300 characters
|
|
101
101
|
*/
|
|
102
102
|
export declare function extractDefaultDescription(markdown: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Insert the regenerated Reader Intelligence Guide HTML immediately after
|
|
105
|
+
* the Executive Brief section so the rendered article body matches the
|
|
106
|
+
* documented order (Executive Brief → Reader Intelligence Guide → Key
|
|
107
|
+
* Takeaways → deep sections). The Executive Brief section ends where the
|
|
108
|
+
* next H2 begins; we splice at that boundary. When the brief is absent
|
|
109
|
+
* (sparse runs) we fall back to prepending so the guide still appears
|
|
110
|
+
* at the top of the body.
|
|
111
|
+
*
|
|
112
|
+
* Implementation uses `indexOf` rather than a regex so the splice point
|
|
113
|
+
* is deterministic and immune to polynomial-regex backtracking on
|
|
114
|
+
* pathological input.
|
|
115
|
+
*
|
|
116
|
+
* @param bodyHtml - Rendered article body
|
|
117
|
+
* @param guideHtml - Reader Intelligence Guide HTML fragment
|
|
118
|
+
* @returns Body HTML with the guide spliced after the Executive Brief
|
|
119
|
+
*/
|
|
120
|
+
export declare function insertReaderGuideAfterExecutiveBrief(bodyHtml: string, guideHtml: string): string;
|
|
103
121
|
/**
|
|
104
122
|
* Run the full aggregate → render → write pipeline for one run.
|
|
105
123
|
*
|
|
@@ -29,7 +29,7 @@ import { resolveRunId as _resolveRunId } from './manifest/index.js';
|
|
|
29
29
|
import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
|
|
30
30
|
import { buildArticleMeta, serializeArticleMeta } from './article-meta.js';
|
|
31
31
|
import { renderMarkdown } from './markdown-renderer.js';
|
|
32
|
-
import { wrapArticleHtml, getArticleFilename, localizeArticleBody } from './article-html.js';
|
|
32
|
+
import { wrapArticleHtml, getArticleFilename, localizeArticleBody, enhanceTradecraftCards, enhanceAnalysisIndexCards, } from './article-html.js';
|
|
33
33
|
import { buildReaderIntelligenceGuideHtml, stripInlineReaderGuide, } from './reader-intelligence-guide.js';
|
|
34
34
|
import { ALL_LANGUAGES } from '../constants/language-core.js';
|
|
35
35
|
import { blobUrl } from './infra/github-urls.js';
|
|
@@ -349,16 +349,27 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
349
349
|
bodyHtml = bodyHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
350
350
|
const guideHtml = buildReaderIntelligenceGuideHtml(lang, aggregated.sectionToc, aggregated.includedArtifacts);
|
|
351
351
|
if (guideHtml) {
|
|
352
|
-
//
|
|
353
|
-
// the rendered
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
|
|
352
|
+
// Insert the guide IMMEDIATELY AFTER the Executive Brief section so
|
|
353
|
+
// the rendered HTML body order matches the documented article
|
|
354
|
+
// skeleton (Article-Generation.md §"Article skeleton"):
|
|
355
|
+
// 1. Executive Brief
|
|
356
|
+
// 2. Reader Intelligence Guide
|
|
357
|
+
// 3. Key Takeaways
|
|
358
|
+
// 4. … deep sections
|
|
359
|
+
// We splice at the start of the next H2 after the Executive Brief
|
|
360
|
+
// anchor; when the brief is missing (sparse runs) we fall back to
|
|
361
|
+
// prepending so the guide still appears at the top of the body.
|
|
362
|
+
bodyHtml = insertReaderGuideAfterExecutiveBrief(bodyHtml, guideHtml);
|
|
358
363
|
}
|
|
359
364
|
// Localize Tradecraft References, Analysis Index, and other appendix
|
|
360
365
|
// section headings and content into the target language.
|
|
361
366
|
bodyHtml = localizeArticleBody(bodyHtml, lang);
|
|
367
|
+
// Replace the plain Tradecraft References bullet lists and the
|
|
368
|
+
// Analysis Index table with `pi-card-grid` cards. Runs for every
|
|
369
|
+
// language (including English) so the "much nicer" rendering matches
|
|
370
|
+
// the political-intelligence.html visual vocabulary site-wide.
|
|
371
|
+
bodyHtml = enhanceTradecraftCards(bodyHtml, lang);
|
|
372
|
+
bodyHtml = enhanceAnalysisIndexCards(bodyHtml, lang);
|
|
362
373
|
// When a per-language translated source exists, prefer a summary derived
|
|
363
374
|
// from it so the `<meta description>` matches the visible prose. The
|
|
364
375
|
// editorial title still comes from the English resolver (per-language
|
|
@@ -385,6 +396,60 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
385
396
|
fs.writeFileSync(path.join(opts.outDir, filename), html, 'utf8');
|
|
386
397
|
return filename;
|
|
387
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Insert the regenerated Reader Intelligence Guide HTML immediately after
|
|
401
|
+
* the Executive Brief section so the rendered article body matches the
|
|
402
|
+
* documented order (Executive Brief → Reader Intelligence Guide → Key
|
|
403
|
+
* Takeaways → deep sections). The Executive Brief section ends where the
|
|
404
|
+
* next H2 begins; we splice at that boundary. When the brief is absent
|
|
405
|
+
* (sparse runs) we fall back to prepending so the guide still appears
|
|
406
|
+
* at the top of the body.
|
|
407
|
+
*
|
|
408
|
+
* Implementation uses `indexOf` rather than a regex so the splice point
|
|
409
|
+
* is deterministic and immune to polynomial-regex backtracking on
|
|
410
|
+
* pathological input.
|
|
411
|
+
*
|
|
412
|
+
* @param bodyHtml - Rendered article body
|
|
413
|
+
* @param guideHtml - Reader Intelligence Guide HTML fragment
|
|
414
|
+
* @returns Body HTML with the guide spliced after the Executive Brief
|
|
415
|
+
*/
|
|
416
|
+
export function insertReaderGuideAfterExecutiveBrief(bodyHtml, guideHtml) {
|
|
417
|
+
const execBriefAnchor = 'id="section-executive-brief"';
|
|
418
|
+
const briefIdx = bodyHtml.indexOf(execBriefAnchor);
|
|
419
|
+
if (briefIdx === -1) {
|
|
420
|
+
return guideHtml + '\n' + bodyHtml;
|
|
421
|
+
}
|
|
422
|
+
// Skip the Executive Brief opening tag itself, then walk forward to the
|
|
423
|
+
// next H2 — that's where the next section starts and where we want to
|
|
424
|
+
// splice the guide. `<h2 ` matches a tag with attributes; `<h2>` matches
|
|
425
|
+
// a bare tag (defensive).
|
|
426
|
+
const afterBrief = briefIdx + execBriefAnchor.length;
|
|
427
|
+
const nextH2Tagged = bodyHtml.indexOf('<h2 ', afterBrief);
|
|
428
|
+
const nextH2Bare = bodyHtml.indexOf('<h2>', afterBrief);
|
|
429
|
+
const nextH2 = pickEarliestIndex(nextH2Tagged, nextH2Bare);
|
|
430
|
+
if (nextH2 === -1) {
|
|
431
|
+
// Executive Brief is the only section — append the guide at the end.
|
|
432
|
+
return bodyHtml + '\n' + guideHtml;
|
|
433
|
+
}
|
|
434
|
+
return bodyHtml.slice(0, nextH2) + guideHtml + '\n' + bodyHtml.slice(nextH2);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Return the smaller of two `indexOf` results, treating `-1` as "not
|
|
438
|
+
* found" so the caller gets `-1` only when both probes failed. Extracted
|
|
439
|
+
* to keep {@link insertReaderGuideAfterExecutiveBrief} under the
|
|
440
|
+
* useless-assignment lint.
|
|
441
|
+
*
|
|
442
|
+
* @param a - First `indexOf` result
|
|
443
|
+
* @param b - Second `indexOf` result
|
|
444
|
+
* @returns Smaller non-negative index, or `-1` when both are `-1`
|
|
445
|
+
*/
|
|
446
|
+
function pickEarliestIndex(a, b) {
|
|
447
|
+
if (a === -1)
|
|
448
|
+
return b;
|
|
449
|
+
if (b === -1)
|
|
450
|
+
return a;
|
|
451
|
+
return Math.min(a, b);
|
|
452
|
+
}
|
|
388
453
|
/**
|
|
389
454
|
* Safely look up one language entry in a {@link ResolvedMetadata} map.
|
|
390
455
|
* The runtime shape is always complete (one entry per language), but the
|
|
@@ -73,8 +73,10 @@ export declare function buildArticleHreflangLinks(articleSlug: string): string;
|
|
|
73
73
|
* Build the article-level Table of Contents nav. Renders a labelled
|
|
74
74
|
* `<nav class="article-toc">` with one `<a>` per H2 section, keyed by the
|
|
75
75
|
* stable fragment ids produced by the aggregator. The containing `<aside>`
|
|
76
|
-
* is styled as a sticky sidebar on wide viewports and
|
|
77
|
-
* `<details>` disclosure on narrow viewports via
|
|
76
|
+
* is styled as a sticky, full-height sidebar on wide viewports and
|
|
77
|
+
* collapses into a `<details>` disclosure on narrow viewports via
|
|
78
|
+
* `styles.css`. Each entry is prefixed with a contextual emoji icon so
|
|
79
|
+
* readers can scan the navigation visually as well as textually.
|
|
78
80
|
*
|
|
79
81
|
* Returns an empty string when `entries` is empty so low-signal
|
|
80
82
|
* `ANALYSIS_ONLY` articles (few sections, no value in a TOC) stay compact.
|
|
@@ -94,6 +96,46 @@ export declare function buildArticleToc(entries: readonly ArticleTocEntry[], lan
|
|
|
94
96
|
* @returns HTML body with localized appendix sections
|
|
95
97
|
*/
|
|
96
98
|
export declare function localizeArticleBody(bodyHtml: string, lang: LanguageCode): string;
|
|
99
|
+
/**
|
|
100
|
+
* Replace the rendered Tradecraft References bullet lists with a
|
|
101
|
+
* `pi-card-grid` of richly described cards (icon, curated title,
|
|
102
|
+
* curated description, kind-aware CTA). The cards reuse the exact same
|
|
103
|
+
* class hooks as `political-intelligence.html`, so the site-wide CSS
|
|
104
|
+
* already styles them — no additional CSS is required.
|
|
105
|
+
*
|
|
106
|
+
* After the April-2026 reorder the rendered Markdown emits Artifact
|
|
107
|
+
* templates as the first sub-heading and Methodologies as the second,
|
|
108
|
+
* matching how readers encounter the run (artifacts first, methodology
|
|
109
|
+
* library second). The card upgrade follows the same order so the H3
|
|
110
|
+
* positions stay aligned with the kind-aware CTA labels.
|
|
111
|
+
*
|
|
112
|
+
* Falls back to the original Markdown-rendered list when the expected
|
|
113
|
+
* structure (H2 → intro paragraph → Artifact-templates sub-heading →
|
|
114
|
+
* `<ul>` → Methodologies sub-heading → `<ul>`) is missing, so partially
|
|
115
|
+
* stripped or unusual articles are not silently corrupted.
|
|
116
|
+
*
|
|
117
|
+
* @param bodyHtml - The (already-localised) article body HTML
|
|
118
|
+
* @param lang - Target language code for curated titles/descriptions
|
|
119
|
+
* @returns Body HTML with the tradecraft section upgraded to cards
|
|
120
|
+
*/
|
|
121
|
+
export declare function enhanceTradecraftCards(bodyHtml: string, lang: LanguageCode): string;
|
|
122
|
+
/**
|
|
123
|
+
* Replace the Analysis Index `<table>` with a `pi-card-grid` of cards,
|
|
124
|
+
* one per included artifact. Each card renders the artifact's curated
|
|
125
|
+
* localised title + description, the section it contributed to, the
|
|
126
|
+
* run-relative path as inline `<code>`, and a "View on GitHub" CTA.
|
|
127
|
+
*
|
|
128
|
+
* Strategy: parse the rendered table's `<tbody>` rows (each row carries
|
|
129
|
+
* `[sectionId, <a href="…">stem</a>, runRelPath]`) and re-render the
|
|
130
|
+
* region between the table's opening wrapper and `</table>` as a card
|
|
131
|
+
* grid. The wrapping `<div class="table-scroll">` is dropped because
|
|
132
|
+
* the card grid handles its own responsive layout via flex/grid.
|
|
133
|
+
*
|
|
134
|
+
* @param bodyHtml - Article body HTML
|
|
135
|
+
* @param lang - Target language code
|
|
136
|
+
* @returns Body HTML with the Analysis Index upgraded to a card grid
|
|
137
|
+
*/
|
|
138
|
+
export declare function enhanceAnalysisIndexCards(bodyHtml: string, lang: LanguageCode): string;
|
|
97
139
|
/**
|
|
98
140
|
* Render the full article HTML document with the shared chrome.
|
|
99
141
|
*
|