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 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.0 for accessing real EU Parliament data via the Model Context Protocol.
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.0+, fully operational):
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.1",
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.0",
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.9",
166
+ "jscpd": "4.1.0",
167
167
  "knip": "^6.7.0",
168
- "lint-staged": "17.0.2",
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.0",
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
- if (methods.length > 0) {
217
- block.push('### Methodologies');
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 methods) {
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 (templates.length > 0) {
226
- block.push('### Artifact templates');
231
+ if (methods.length > 0) {
232
+ block.push('### Methodologies');
227
233
  block.push('');
228
- for (const rel of templates) {
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
- // Prepend the guide to the body so it always appears at the top of
353
- // the rendered content, immediately after the chrome header. The
354
- // article chrome in wrapArticleHtml wraps the body in an <article>
355
- // with its own <header>/<h1>, so prepending here is deterministic
356
- // and avoids fragile in-body heading searches.
357
- bodyHtml = guideHtml + '\n' + bodyHtml;
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 collapses into a
77
- * `<details>` disclosure on narrow viewports via `styles.css`.
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
  *