euparliamentmonitor 0.9.0 → 0.9.2

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.0",
3
+ "version": "0.9.2",
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",
@@ -165,7 +165,7 @@
165
165
  "husky": "9.1.7",
166
166
  "jscpd": "4.0.9",
167
167
  "knip": "^6.7.0",
168
- "lint-staged": "17.0.2",
168
+ "lint-staged": "17.0.3",
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",
@@ -550,30 +550,33 @@ export function aggregateAnalysisRun(options) {
550
550
  const analysisIndex = renderAnalysisIndex(includedArtifacts, manifestRelPath);
551
551
  const readerGuide = renderReaderIntelligenceGuide(emittedSections, includedArtifacts);
552
552
  // Deterministic 3–7 bullet "Key takeaways" block, harvested from the
553
- // synthesis-summary / intelligence-assessment artifacts. Placed
554
- // immediately after the Executive Brief so the reader gets the BLUF
555
- // followed by a digest of the strongest findings before being handed
556
- // off to the Reader Intelligence Guide and the deeper sections.
553
+ // synthesis-summary / intelligence-assessment artifacts. Sits between
554
+ // the Reader Intelligence Guide and the deep sections: the reader gets
555
+ // the BLUF (Executive Brief) a navigation map (Reader Guide) → a
556
+ // bullet digest of the strongest findings (Key Takeaways) the deep
557
+ // analysis. This is the order requested for reader UX so navigation
558
+ // is established before the reader commits to scanning takeaways.
557
559
  const keyTakeaways = buildKeyTakeaways({ runDir });
558
- // TOC ordering reflects the rendered document:
560
+ // TOC ordering must match the rendered Markdown body 1:1. Order:
559
561
  // Executive Brief (already first in emittedSections via appendSection) →
560
- // Key Takeaways (inserted right after the brief when present) →
561
- // Reader Intelligence Guide remaining sections audit appendices.
562
+ // Reader Intelligence Guide (inserted right after the brief when present) →
563
+ // Key Takeaways (inserted right after the guide when present) →
564
+ // remaining sections → audit appendices.
562
565
  let postBriefIdx = emittedSections.length > 0 &&
563
566
  emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
564
567
  ? 1
565
568
  : 0;
566
- if (keyTakeaways) {
569
+ if (readerGuide) {
567
570
  emittedSections.splice(postBriefIdx, 0, {
568
- id: KEY_TAKEAWAYS_SECTION_ID,
569
- title: KEY_TAKEAWAYS_SECTION_TITLE,
571
+ id: READER_GUIDE_SECTION_ID,
572
+ title: READER_GUIDE_SECTION_TITLE,
570
573
  });
571
574
  postBriefIdx += 1;
572
575
  }
573
- if (readerGuide) {
576
+ if (keyTakeaways) {
574
577
  emittedSections.splice(postBriefIdx, 0, {
575
- id: READER_GUIDE_SECTION_ID,
576
- title: READER_GUIDE_SECTION_TITLE,
578
+ id: KEY_TAKEAWAYS_SECTION_ID,
579
+ title: KEY_TAKEAWAYS_SECTION_TITLE,
577
580
  });
578
581
  }
579
582
  emittedSections.push({ id: TRADECRAFT_SECTION_ID, title: TRADECRAFT_SECTION_TITLE });
@@ -583,9 +586,8 @@ export function aggregateAnalysisRun(options) {
583
586
  '',
584
587
  ...execBriefMarkdown,
585
588
  '',
589
+ ...(readerGuide ? [readerGuide, ''] : []),
586
590
  ...(keyTakeaways ? [keyTakeaways, ''] : []),
587
- readerGuide,
588
- '',
589
591
  ...sectionMarkdown,
590
592
  '',
591
593
  provenance,
@@ -19,7 +19,11 @@ export interface CliOptions {
19
19
  * is earlier are skipped.
20
20
  */
21
21
  readonly since?: string;
22
- /** Languages to render (defaults to all 14). */
22
+ /**
23
+ * Languages to render. Defaults to all 14. The CLI always populates
24
+ * this with `[...ALL_LANGUAGES]` (the `--lang/--language` flags have
25
+ * been removed); programmatic callers (tests) can override it.
26
+ */
23
27
  readonly langs: readonly LanguageCode[];
24
28
  /** Output directory for HTML files (defaults to `news/`). */
25
29
  readonly outDir: string;
@@ -30,8 +34,11 @@ export interface CliOptions {
30
34
  /** Optional: override the auto-derived article description (single-run only). */
31
35
  readonly description?: string;
32
36
  /**
33
- * When true, only the source Markdown is written (no HTML) — useful for
34
- * upstream pipelines that translate first and then batch-render.
37
+ * When true, only the source Markdown is written (no HTML) — preserved
38
+ * for programmatic callers (tests) that need to skip HTML emission for
39
+ * speed. The CLI no longer exposes a flag for this and always sets
40
+ * `false`; every workflow-driven invocation emits HTML for all 14
41
+ * languages.
35
42
  */
36
43
  readonly markdownOnly: boolean;
37
44
  }
@@ -93,6 +100,24 @@ export declare function sanitizeRunSuffix(runId: string): string;
93
100
  * @returns Plain-text description, truncated to ≤300 characters
94
101
  */
95
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;
96
121
  /**
97
122
  * Run the full aggregate → render → write pipeline for one run.
98
123
  *
@@ -10,9 +10,14 @@
10
10
  *
11
11
  * Usage:
12
12
  * npm run generate-article -- --run analysis/daily/2026-01-15/breaking-run1
13
- * npm run generate-article -- --run ... --lang en --lang sv
14
13
  * npm run generate-article -- --run ... --out-dir news --title "Headline"
15
14
  *
15
+ * **Always-14-languages-always-HTML contract**: every CLI invocation
16
+ * renders every supported language to HTML. The legacy `--lang` /
17
+ * `--language` / `--markdown-only` flags have been removed. The
18
+ * programmatic `generateArticle()` API still accepts `langs` and
19
+ * `markdownOnly` for tests that need to scope a render for speed.
20
+ *
16
21
  * Designed to be idempotent: running again with no changes overwrites
17
22
  * identical files byte-for-byte.
18
23
  */
@@ -24,7 +29,7 @@ import { resolveRunId as _resolveRunId } from './manifest/index.js';
24
29
  import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
25
30
  import { buildArticleMeta, serializeArticleMeta } from './article-meta.js';
26
31
  import { renderMarkdown } from './markdown-renderer.js';
27
- import { wrapArticleHtml, getArticleFilename } from './article-html.js';
32
+ import { wrapArticleHtml, getArticleFilename, localizeArticleBody } from './article-html.js';
28
33
  import { buildReaderIntelligenceGuideHtml, stripInlineReaderGuide, } from './reader-intelligence-guide.js';
29
34
  import { ALL_LANGUAGES } from '../constants/language-core.js';
30
35
  import { blobUrl } from './infra/github-urls.js';
@@ -48,9 +53,6 @@ function applyFlagResult(acc, result) {
48
53
  case 'since':
49
54
  acc.since = result.value;
50
55
  return;
51
- case 'lang':
52
- acc.langs.push(result.value);
53
- return;
54
56
  case 'outDir':
55
57
  acc.outDir = result.value;
56
58
  return;
@@ -60,9 +62,6 @@ function applyFlagResult(acc, result) {
60
62
  case 'description':
61
63
  acc.description = result.value;
62
64
  return;
63
- case 'markdownOnly':
64
- acc.markdownOnly = true;
65
- return;
66
65
  default: {
67
66
  // Exhaustiveness guard — if a new FlagResult kind is added without a
68
67
  // matching case the compiler will surface the gap.
@@ -75,9 +74,7 @@ export function parseCliArgs(argv, repoRoot) {
75
74
  const acc = {
76
75
  runDir: null,
77
76
  all: false,
78
- langs: [],
79
77
  outDir: path.join(repoRoot, 'news'),
80
- markdownOnly: false,
81
78
  };
82
79
  for (let i = 0; i < argv.length; i++) {
83
80
  const arg = argv[i] ?? '';
@@ -105,10 +102,14 @@ export function parseCliArgs(argv, repoRoot) {
105
102
  const opts = {
106
103
  runDir: acc.runDir,
107
104
  all: acc.all,
108
- langs: acc.langs.length > 0 ? acc.langs : [...ALL_LANGUAGES],
105
+ // Always render every language the `--lang/--language` flags have
106
+ // been removed in the always-14-languages contract.
107
+ langs: [...ALL_LANGUAGES],
109
108
  outDir: acc.outDir,
110
109
  repoRoot,
111
- markdownOnly: acc.markdownOnly,
110
+ // Always emit HTML — the `--markdown-only` flag has been removed in
111
+ // the always-HTML contract.
112
+ markdownOnly: false,
112
113
  ...(acc.since !== undefined ? { since: acc.since } : {}),
113
114
  ...(acc.title !== undefined ? { title: acc.title } : {}),
114
115
  ...(acc.description !== undefined ? { description: acc.description } : {}),
@@ -117,7 +118,7 @@ export function parseCliArgs(argv, repoRoot) {
117
118
  }
118
119
  /**
119
120
  * Resolve one CLI flag to a {@link FlagResult}. Throws `Error` for any
120
- * unsupported flag or language code.
121
+ * unsupported flag.
121
122
  *
122
123
  * @param flag - Flag name (e.g. `--run`)
123
124
  * @param takeValue - Lazily returns the value argument for value-bearing flags
@@ -137,14 +138,6 @@ function applyCliFlag(flag, takeValue) {
137
138
  }
138
139
  return { kind: 'since', value };
139
140
  }
140
- case '--lang':
141
- case '--language': {
142
- const value = takeValue();
143
- if (!ALL_LANGUAGES.includes(value)) {
144
- throw new Error(`Unsupported language code: ${value}`);
145
- }
146
- return { kind: 'lang', value: value };
147
- }
148
141
  case '--out-dir':
149
142
  case '--output':
150
143
  return { kind: 'outDir', value: path.resolve(takeValue()) };
@@ -152,13 +145,19 @@ function applyCliFlag(flag, takeValue) {
152
145
  return { kind: 'title', value: takeValue() };
153
146
  case '--description':
154
147
  return { kind: 'description', value: takeValue() };
155
- case '--markdown-only':
156
- return { kind: 'markdownOnly' };
157
148
  case '--help':
158
149
  case '-h':
159
150
  printHelp();
160
151
  process.exit(0);
161
152
  // eslint-disable-next-line no-fallthrough
153
+ case '--lang':
154
+ case '--language':
155
+ case '--markdown-only':
156
+ // Removed in the always-14-languages-always-HTML contract — every
157
+ // article.md now always renders to all 14 supported languages and
158
+ // HTML emission cannot be skipped from the CLI.
159
+ throw new Error(`Flag ${flag} has been removed. The CLI always renders all 14 languages with HTML output. ` +
160
+ `See Article-Generation.md § "CLI contract" for the new always-on contract.`);
162
161
  default:
163
162
  throw new Error(`Unknown argument: ${flag}`);
164
163
  }
@@ -183,19 +182,22 @@ function printHelp() {
183
182
  ' generate-article --all [--since YYYY-MM-DD] [options]',
184
183
  '',
185
184
  'Aggregate analysis artifacts from an `analysis/daily/**/<run>` directory',
186
- 'into a canonical Markdown document and render it to HTML in all 14',
187
- 'languages. The `--all` form walks every run under `analysis/daily/`',
188
- 'and regenerates the full historic catalogue in one pass.',
185
+ 'into a canonical Markdown document and render it to HTML in **all 14',
186
+ 'supported languages** (en, sv, da, no, fi, de, fr, es, nl, ar, he, ja,',
187
+ 'ko, zh). The `--all` form walks every run under `analysis/daily/` and',
188
+ 'regenerates the full historic catalogue in one pass.',
189
+ '',
190
+ 'The 14-language HTML render is **always on** — there is no flag to',
191
+ 'scope a render to a single language or to skip HTML emission. Every',
192
+ 'article.md always produces 14 corresponding `<slug>-<lang>.html` files.',
189
193
  '',
190
194
  'Options:',
191
195
  ' --run <path> Analysis run directory (single-run mode)',
192
196
  ' --all Batch-regenerate every run under analysis/daily/',
193
197
  ' --since YYYY-MM-DD With --all: skip runs dated before this cut-off',
194
- ' --lang <code> Language to render (repeatable; default: all 14)',
195
198
  ' --out-dir <path> Output directory (default: news/)',
196
199
  ' --title <text> Override article title (single-run only)',
197
200
  ' --description <text> Override article meta description (single-run only)',
198
- ' --markdown-only Write only the source .md (skip HTML)',
199
201
  ' --help, -h Show this help',
200
202
  '',
201
203
  ].join('\n'));
@@ -347,13 +349,21 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
347
349
  bodyHtml = bodyHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, '');
348
350
  const guideHtml = buildReaderIntelligenceGuideHtml(lang, aggregated.sectionToc, aggregated.includedArtifacts);
349
351
  if (guideHtml) {
350
- // Prepend the guide to the body so it always appears at the top of
351
- // the rendered content, immediately after the chrome header. The
352
- // article chrome in wrapArticleHtml wraps the body in an <article>
353
- // with its own <header>/<h1>, so prepending here is deterministic
354
- // and avoids fragile in-body heading searches.
355
- 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);
356
363
  }
364
+ // Localize Tradecraft References, Analysis Index, and other appendix
365
+ // section headings and content into the target language.
366
+ bodyHtml = localizeArticleBody(bodyHtml, lang);
357
367
  // When a per-language translated source exists, prefer a summary derived
358
368
  // from it so the `<meta description>` matches the visible prose. The
359
369
  // editorial title still comes from the English resolver (per-language
@@ -380,6 +390,60 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
380
390
  fs.writeFileSync(path.join(opts.outDir, filename), html, 'utf8');
381
391
  return filename;
382
392
  }
393
+ /**
394
+ * Insert the regenerated Reader Intelligence Guide HTML immediately after
395
+ * the Executive Brief section so the rendered article body matches the
396
+ * documented order (Executive Brief → Reader Intelligence Guide → Key
397
+ * Takeaways → deep sections). The Executive Brief section ends where the
398
+ * next H2 begins; we splice at that boundary. When the brief is absent
399
+ * (sparse runs) we fall back to prepending so the guide still appears
400
+ * at the top of the body.
401
+ *
402
+ * Implementation uses `indexOf` rather than a regex so the splice point
403
+ * is deterministic and immune to polynomial-regex backtracking on
404
+ * pathological input.
405
+ *
406
+ * @param bodyHtml - Rendered article body
407
+ * @param guideHtml - Reader Intelligence Guide HTML fragment
408
+ * @returns Body HTML with the guide spliced after the Executive Brief
409
+ */
410
+ export function insertReaderGuideAfterExecutiveBrief(bodyHtml, guideHtml) {
411
+ const execBriefAnchor = 'id="section-executive-brief"';
412
+ const briefIdx = bodyHtml.indexOf(execBriefAnchor);
413
+ if (briefIdx === -1) {
414
+ return guideHtml + '\n' + bodyHtml;
415
+ }
416
+ // Skip the Executive Brief opening tag itself, then walk forward to the
417
+ // next H2 — that's where the next section starts and where we want to
418
+ // splice the guide. `<h2 ` matches a tag with attributes; `<h2>` matches
419
+ // a bare tag (defensive).
420
+ const afterBrief = briefIdx + execBriefAnchor.length;
421
+ const nextH2Tagged = bodyHtml.indexOf('<h2 ', afterBrief);
422
+ const nextH2Bare = bodyHtml.indexOf('<h2>', afterBrief);
423
+ const nextH2 = pickEarliestIndex(nextH2Tagged, nextH2Bare);
424
+ if (nextH2 === -1) {
425
+ // Executive Brief is the only section — append the guide at the end.
426
+ return bodyHtml + '\n' + guideHtml;
427
+ }
428
+ return bodyHtml.slice(0, nextH2) + guideHtml + '\n' + bodyHtml.slice(nextH2);
429
+ }
430
+ /**
431
+ * Return the smaller of two `indexOf` results, treating `-1` as "not
432
+ * found" so the caller gets `-1` only when both probes failed. Extracted
433
+ * to keep {@link insertReaderGuideAfterExecutiveBrief} under the
434
+ * useless-assignment lint.
435
+ *
436
+ * @param a - First `indexOf` result
437
+ * @param b - Second `indexOf` result
438
+ * @returns Smaller non-negative index, or `-1` when both are `-1`
439
+ */
440
+ function pickEarliestIndex(a, b) {
441
+ if (a === -1)
442
+ return b;
443
+ if (b === -1)
444
+ return a;
445
+ return Math.min(a, b);
446
+ }
383
447
  /**
384
448
  * Safely look up one language entry in a {@link ResolvedMetadata} map.
385
449
  * The runtime shape is always complete (one entry per language), but the
@@ -84,6 +84,16 @@ export declare function buildArticleHreflangLinks(articleSlug: string): string;
84
84
  * @returns HTML fragment for the sidebar, or `""` when no TOC is needed
85
85
  */
86
86
  export declare function buildArticleToc(entries: readonly ArticleTocEntry[], lang: LanguageCode): string;
87
+ /**
88
+ * Localize the Tradecraft References and Analysis Index sections in the
89
+ * rendered article body HTML. Replaces English headings, introductions,
90
+ * sub-headings, and table headers with translated equivalents.
91
+ *
92
+ * @param bodyHtml - The rendered HTML body (from Markdown)
93
+ * @param lang - Target language code
94
+ * @returns HTML body with localized appendix sections
95
+ */
96
+ export declare function localizeArticleBody(bodyHtml: string, lang: LanguageCode): string;
87
97
  /**
88
98
  * Render the full article HTML document with the shared chrome.
89
99
  *