euparliamentmonitor 0.8.46 → 0.8.48

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
@@ -163,7 +163,7 @@ The published platform at **[euparliamentmonitor.com](https://euparliamentmonito
163
163
 
164
164
  **MCP Server Integration**: The project uses the
165
165
  [European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server)
166
- v1.2.13 for accessing real EU Parliament data via the Model Context Protocol.
166
+ v1.2.15 for accessing real EU Parliament data via the Model Context Protocol.
167
167
 
168
168
  - **MCP Server Status**: ✅ Fully operational — 60+ EP data tools available
169
169
  (feeds, direct lookups, analytical tools, intelligence correlation)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.46",
3
+ "version": "0.8.48",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -62,6 +62,7 @@
62
62
  "build:check-tests": "tsc --project tsconfig.test.json --noEmit",
63
63
  "copy-vendor": "node scripts/copy-vendor.js",
64
64
  "validate-analysis": "node scripts/validate-analysis-completeness.js",
65
+ "prior-run-diff": "node scripts/aggregator/prior-run-diff.js",
65
66
  "generate-article": "node scripts/aggregator/article-generator.js",
66
67
  "generate-article:all": "node scripts/aggregator/article-generator.js --all",
67
68
  "generate-news-indexes": "node scripts/generators/news-indexes.js",
@@ -158,7 +159,7 @@
158
159
  "husky": "9.1.7",
159
160
  "jscpd": "4.0.9",
160
161
  "lint-staged": "16.4.0",
161
- "mermaid": "^11.14.0",
162
+ "mermaid": "11.14.0",
162
163
  "papaparse": "5.5.3",
163
164
  "prettier": "3.8.3",
164
165
  "ts-api-utils": "2.5.0",
@@ -171,7 +172,7 @@
171
172
  "node": ">=25"
172
173
  },
173
174
  "dependencies": {
174
- "european-parliament-mcp-server": "1.2.14",
175
+ "european-parliament-mcp-server": "1.2.15",
175
176
  "markdown-it": "^14.1.1",
176
177
  "markdown-it-anchor": "^9.2.0",
177
178
  "markdown-it-attrs": "^4.3.1",
@@ -2,6 +2,17 @@ import { type ArtifactSection } from './artifact-order.js';
2
2
  /** Raw manifest shape as committed by the analysis pipeline. */
3
3
  export interface AnalysisManifest {
4
4
  readonly articleType: string;
5
+ /**
6
+ * Legacy plural variant emitted by some pre-aggregator-pipeline workflows.
7
+ * Used as a fallback when `articleType` is absent so historic runs with
8
+ * `articleTypes: ["<slug>"]` can still be aggregated.
9
+ */
10
+ readonly articleTypes?: readonly string[];
11
+ /**
12
+ * Legacy field emitted by older breaking-run manifests. Used as the last
13
+ * fallback when neither `articleType` nor `articleTypes` is present.
14
+ */
15
+ readonly runType?: string;
5
16
  readonly runId?: string;
6
17
  readonly date?: string;
7
18
  readonly analysisDir?: string;
@@ -167,6 +178,20 @@ export declare function renderAnalysisIndex(included: readonly IncludedArtifact[
167
178
  * @returns Markdown block containing the guide table
168
179
  */
169
180
  export declare function renderReaderIntelligenceGuide(sections: readonly TocSection[], included: readonly IncludedArtifact[]): string;
181
+ /**
182
+ * Resolve the article-type slug from a manifest, tolerating legacy schemas.
183
+ *
184
+ * Resolution order (highest precedence first):
185
+ * 1. `articleType` — canonical singular field
186
+ * 2. `articleTypes[0]` — pre-aggregator-pipeline plural array
187
+ * 3. `runType` — legacy field on older breaking-run manifests
188
+ *
189
+ * Falls back to `'unknown'` when none of the above is a non-empty string.
190
+ *
191
+ * @param manifest - Parsed manifest (any of the supported schemas)
192
+ * @returns Article-type slug usable as a filename component
193
+ */
194
+ export declare function resolveArticleTypeFromManifest(manifest: AnalysisManifest): string;
170
195
  /**
171
196
  * Read, clean, and concatenate every artifact declared by the run's manifest
172
197
  * (with discovery fallback when manifest.files is missing), returning a
@@ -471,6 +471,32 @@ function appendSection(runDir, runDirRelPath, sectionId, sectionTitle, paths, se
471
471
  }
472
472
  sectionMarkdown.push('');
473
473
  }
474
+ /**
475
+ * Resolve the article-type slug from a manifest, tolerating legacy schemas.
476
+ *
477
+ * Resolution order (highest precedence first):
478
+ * 1. `articleType` — canonical singular field
479
+ * 2. `articleTypes[0]` — pre-aggregator-pipeline plural array
480
+ * 3. `runType` — legacy field on older breaking-run manifests
481
+ *
482
+ * Falls back to `'unknown'` when none of the above is a non-empty string.
483
+ *
484
+ * @param manifest - Parsed manifest (any of the supported schemas)
485
+ * @returns Article-type slug usable as a filename component
486
+ */
487
+ export function resolveArticleTypeFromManifest(manifest) {
488
+ if (typeof manifest.articleType === 'string' && manifest.articleType) {
489
+ return manifest.articleType;
490
+ }
491
+ const first = manifest.articleTypes?.[0];
492
+ if (typeof first === 'string' && first) {
493
+ return first;
494
+ }
495
+ if (typeof manifest.runType === 'string' && manifest.runType) {
496
+ return manifest.runType;
497
+ }
498
+ return 'unknown';
499
+ }
474
500
  /**
475
501
  * Read, clean, and concatenate every artifact declared by the run's manifest
476
502
  * (with discovery fallback when manifest.files is missing), returning a
@@ -516,7 +542,7 @@ export function aggregateAnalysisRun(options) {
516
542
  consumed.add(p);
517
543
  }
518
544
  const tradecraftFiles = options.tradecraftFiles ?? discoverTradecraftFiles(repoRoot);
519
- const articleType = manifest.articleType ?? 'unknown';
545
+ const articleType = resolveArticleTypeFromManifest(manifest);
520
546
  const date = manifest.date ?? guessDateFromRunDir(runDirRelPath);
521
547
  const runId = manifest.runId ?? path.basename(runDir);
522
548
  const gateResult = latestGateResult(manifest);
@@ -19,7 +19,7 @@
19
19
  import fs from 'fs';
20
20
  import path from 'path';
21
21
  import { pathToFileURL } from 'url';
22
- import { aggregateAnalysisRun } from './analysis-aggregator.js';
22
+ import { aggregateAnalysisRun, resolveArticleTypeFromManifest, } from './analysis-aggregator.js';
23
23
  import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
24
24
  import { renderMarkdown } from './markdown-renderer.js';
25
25
  import { wrapArticleHtml, getArticleFilename } from './article-html.js';
@@ -528,14 +528,21 @@ function readRunCandidate(runDir, manifestPath) {
528
528
  catch {
529
529
  return null;
530
530
  }
531
- const articleType = typeof parsed.articleType === 'string' ? parsed.articleType : '';
531
+ // Resolve via the same precedence used by the aggregator (articleType
532
+ // articleTypes[0] → runType) so legacy-schema manifests are picked up by
533
+ // batch mode rather than silently skipped.
534
+ const articleType = resolveArticleTypeFromManifest(parsed);
532
535
  if (!articleType || articleType === 'unknown')
533
536
  return null;
534
537
  const dateFromManifest = typeof parsed.date === 'string' ? parsed.date : '';
535
538
  const date = /^\d{4}-\d{2}-\d{2}$/.test(dateFromManifest)
536
539
  ? dateFromManifest
537
540
  : dateFromRunPath(runDir);
538
- const runId = typeof parsed.runId === 'string' && parsed.runId ? parsed.runId : path.basename(runDir);
541
+ const runId = typeof parsed.runId === 'string' && parsed.runId
542
+ ? parsed.runId
543
+ : typeof parsed.runId === 'number'
544
+ ? String(parsed.runId)
545
+ : path.basename(runDir);
539
546
  return { runDir, articleType, date, runId };
540
547
  }
541
548
  /**
@@ -609,8 +616,9 @@ function readManifestMetadata(runDir) {
609
616
  try {
610
617
  const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
611
618
  const manifest = {};
612
- if (typeof parsed.articleType === 'string') {
613
- Object.assign(manifest, { articleType: parsed.articleType });
619
+ const resolvedType = resolveArticleTypeFromManifest(parsed);
620
+ if (resolvedType && resolvedType !== 'unknown') {
621
+ Object.assign(manifest, { articleType: resolvedType });
614
622
  }
615
623
  if (typeof parsed.date === 'string') {
616
624
  Object.assign(manifest, { date: parsed.date });
@@ -10,11 +10,15 @@
10
10
  * consistent with the rest of the site.
11
11
  *
12
12
  * The output is a complete HTML5 document. No inline `<script>` is emitted
13
- * in the body. Mermaid is loaded from the vendored ESM bundle via
14
- * `<script type="module" src="js/vendor/mermaid.esm.min.mjs">` so CSP
15
- * stays `script-src 'self'`.
13
+ * in the body. Mermaid is loaded from the same-origin vendored ESM bundle
14
+ * (copied to `js/vendor/mermaid/` by `scripts/copy-vendor.js`) via
15
+ * `<script type="module" src="../js/mermaid-init.js?v=<MERMAID_VERSION>" defer>`
16
+ * so CSP stays `script-src 'self'`. The `?v=` query parameter is sourced
17
+ * from `devDependencies.mermaid` in `package.json` (a fixed pin like
18
+ * `11.14.0`); regenerating articles after a Mermaid bump invalidates
19
+ * browser and CloudFront caches automatically.
16
20
  */
17
- import { BASE_URL, createThemeToggleButton, THEME_TOGGLE_SCRIPT } from '../constants/config.js';
21
+ import { BASE_URL, createThemeToggleButton, MERMAID_VERSION, THEME_TOGGLE_SCRIPT, } from '../constants/config.js';
18
22
  import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, TOC_ARIA_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
19
23
  import { escapeHTML } from '../utils/file-utils.js';
20
24
  import { buildSiteFooter } from '../templates/section-builders.js';
@@ -181,7 +185,7 @@ ${hreflangLinks}
181
185
  <meta name="theme-color" content="#003399">
182
186
  <link rel="stylesheet" href="../styles.css">
183
187
  <script type="application/ld+json">${jsonLdString}</script>
184
- <script type="module" src="../js/mermaid-init.js" defer></script>
188
+ <script type="module" src="../js/mermaid-init.js?v=${MERMAID_VERSION}" defer></script>
185
189
  </head>
186
190
  <body>
187
191
  <a href="#main" class="skip-link">${escapeHTML(skipLinkText)}</a>
@@ -165,6 +165,23 @@ export declare function deriveWeekRange(date: string): {
165
165
  readonly start: string;
166
166
  readonly end: string;
167
167
  };
168
+ /**
169
+ * Return the D-36 → D-8 reporting window for the `week-in-review`
170
+ * article type. EP roll-call voting data is published with a 2–6 week
171
+ * lag, so using the most-recent 7 days structurally produces a
172
+ * vote-empty dataset. Shifting 8 days back and widening to 28 days
173
+ * (start = D-36, end = D-8) ensures the window always contains at
174
+ * least one full EP plenary week with published roll-call data
175
+ * (ADR-006). Direction is consistent with the workflow's
176
+ * `DATE_FROM` (start = D-36) → `DATE_TO` (end = D-8) variables.
177
+ *
178
+ * @param date - ISO article date string (`YYYY-MM-DD`) — typically TODAY
179
+ * @returns `{ start: D-36, end: D-8 }` both as `YYYY-MM-DD` ISO strings
180
+ */
181
+ export declare function deriveReportingWindowForWeekInReview(date: string): {
182
+ readonly start: string;
183
+ readonly end: string;
184
+ };
168
185
  /**
169
186
  * Return a human-friendly month label for an ISO date — English month
170
187
  * name + four-digit year (e.g. `April 2026`). The non-English template
@@ -447,7 +447,12 @@ function safeReaddir(dir) {
447
447
  */
448
448
  export function buildTemplateFallback(articleType, date, committee) {
449
449
  const map = Object.create(null);
450
- const weekRange = deriveWeekRange(date);
450
+ // week-in-review uses the D-36→D-8 reporting window (ADR-006) so that
451
+ // EP roll-call voting data — published 2–6 weeks after the sitting —
452
+ // is always available in the analysis window.
453
+ const weekRange = articleType === 'week-in-review'
454
+ ? deriveReportingWindowForWeekInReview(date)
455
+ : deriveWeekRange(date);
451
456
  const monthLabel = deriveMonthLabel(date);
452
457
  const committeeLabel = committee && committee.trim().length > 0 ? committee : 'Main Committees';
453
458
  for (const lang of ALL_LANGUAGES) {
@@ -503,6 +508,8 @@ function templateForType(lang, articleType, inputs) {
503
508
  };
504
509
  }
505
510
  }
511
+ /** Milliseconds in one UTC day — used by date-window derivation helpers. */
512
+ const MS_PER_DAY = 86_400_000;
506
513
  /**
507
514
  * Parse an ISO date and return the `[start, end]` week range as ISO
508
515
  * strings. Week starts on Monday and ends on the following Sunday.
@@ -518,10 +525,32 @@ export function deriveWeekRange(date) {
518
525
  const day = parsed.getUTCDay();
519
526
  // Shift so Monday = 0, Sunday = 6.
520
527
  const shift = (day + 6) % 7;
521
- const startMs = parsed.getTime() - shift * 86_400_000;
522
- const endMs = startMs + 6 * 86_400_000;
528
+ const startMs = parsed.getTime() - shift * MS_PER_DAY;
529
+ const endMs = startMs + 6 * MS_PER_DAY;
523
530
  return { start: formatIsoDate(new Date(startMs)), end: formatIsoDate(new Date(endMs)) };
524
531
  }
532
+ /**
533
+ * Return the D-36 → D-8 reporting window for the `week-in-review`
534
+ * article type. EP roll-call voting data is published with a 2–6 week
535
+ * lag, so using the most-recent 7 days structurally produces a
536
+ * vote-empty dataset. Shifting 8 days back and widening to 28 days
537
+ * (start = D-36, end = D-8) ensures the window always contains at
538
+ * least one full EP plenary week with published roll-call data
539
+ * (ADR-006). Direction is consistent with the workflow's
540
+ * `DATE_FROM` (start = D-36) → `DATE_TO` (end = D-8) variables.
541
+ *
542
+ * @param date - ISO article date string (`YYYY-MM-DD`) — typically TODAY
543
+ * @returns `{ start: D-36, end: D-8 }` both as `YYYY-MM-DD` ISO strings
544
+ */
545
+ export function deriveReportingWindowForWeekInReview(date) {
546
+ const parsed = parseIsoDate(date);
547
+ if (!parsed)
548
+ return { start: date, end: date };
549
+ return {
550
+ start: formatIsoDate(new Date(parsed.getTime() - 36 * MS_PER_DAY)),
551
+ end: formatIsoDate(new Date(parsed.getTime() - 8 * MS_PER_DAY)),
552
+ };
553
+ }
525
554
  /**
526
555
  * Return a human-friendly month label for an ISO date — English month
527
556
  * name + four-digit year (e.g. `April 2026`). The non-English template