euparliamentmonitor 0.8.58 → 0.9.0

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 (67) hide show
  1. package/README.md +4 -4
  2. package/package.json +15 -5
  3. package/scripts/aggregator/article-meta.d.ts +1 -1
  4. package/scripts/aggregator/article-metadata.js +0 -1
  5. package/scripts/aggregator/artifacts/index.d.ts +11 -0
  6. package/scripts/aggregator/artifacts/index.js +5 -0
  7. package/scripts/aggregator/artifacts/types.d.ts +46 -0
  8. package/scripts/aggregator/artifacts/types.js +4 -0
  9. package/scripts/aggregator/clean-artifact.js +2 -2
  10. package/scripts/aggregator/content/index.d.ts +14 -0
  11. package/scripts/aggregator/content/index.js +7 -0
  12. package/scripts/aggregator/content/types.d.ts +42 -0
  13. package/scripts/aggregator/content/types.js +4 -0
  14. package/scripts/aggregator/markdown/index.d.ts +9 -0
  15. package/scripts/aggregator/markdown/index.js +4 -0
  16. package/scripts/aggregator/markdown-renderer.js +2 -2
  17. package/scripts/aggregator/metadata/index.d.ts +12 -0
  18. package/scripts/aggregator/metadata/index.js +5 -0
  19. package/scripts/aggregator/metadata/types.d.ts +28 -0
  20. package/scripts/aggregator/metadata/types.js +4 -0
  21. package/scripts/config/article-horizons.js +8 -8
  22. package/scripts/constants/committee-indicator-map.js +0 -1
  23. package/scripts/constants/language-core.js +0 -1
  24. package/scripts/generators/political-intelligence/copy.js +0 -1
  25. package/scripts/generators/political-intelligence-descriptions.js +22 -32
  26. package/scripts/generators/shared/html-escape.d.ts +41 -0
  27. package/scripts/generators/shared/html-escape.js +108 -0
  28. package/scripts/generators/shared/index.d.ts +16 -0
  29. package/scripts/generators/shared/index.js +7 -0
  30. package/scripts/generators/shared/template-helpers.d.ts +67 -0
  31. package/scripts/generators/shared/template-helpers.js +109 -0
  32. package/scripts/generators/shared/types.d.ts +97 -0
  33. package/scripts/generators/shared/types.js +4 -0
  34. package/scripts/mcp/ep-mcp-client.d.ts +19 -6
  35. package/scripts/mcp/ep-mcp-client.js +23 -7
  36. package/scripts/mcp/fetch-proxy-server.d.ts +91 -0
  37. package/scripts/mcp/fetch-proxy-server.js +249 -0
  38. package/scripts/mcp/html-lang-patcher.d.ts +48 -0
  39. package/scripts/mcp/html-lang-patcher.js +138 -0
  40. package/scripts/mcp/imf-mcp-client.d.ts +5 -4
  41. package/scripts/mcp/imf-mcp-client.js +13 -5
  42. package/scripts/mcp/mcp-config-reader.d.ts +61 -0
  43. package/scripts/mcp/mcp-config-reader.js +143 -0
  44. package/scripts/types/index.d.ts +1 -1
  45. package/scripts/types/mcp.d.ts +7 -0
  46. package/scripts/utils/intelligence-index.js +1 -8
  47. package/scripts/validate-analysis-completeness.js +47 -2
  48. package/scripts/workflows/completeness-gate/constants.d.ts +89 -0
  49. package/scripts/workflows/completeness-gate/constants.js +115 -0
  50. package/scripts/workflows/completeness-gate/index.d.ts +10 -0
  51. package/scripts/workflows/completeness-gate/index.js +5 -0
  52. package/scripts/workflows/completeness-gate/types.d.ts +104 -0
  53. package/scripts/workflows/completeness-gate/types.js +4 -0
  54. package/scripts/workflows/completeness-gate/validators.d.ts +117 -0
  55. package/scripts/workflows/completeness-gate/validators.js +212 -0
  56. package/scripts/workflows/index.d.ts +21 -0
  57. package/scripts/workflows/index.js +8 -0
  58. package/scripts/workflows/infrastructure/index.d.ts +7 -0
  59. package/scripts/workflows/infrastructure/index.js +4 -0
  60. package/scripts/workflows/infrastructure/shell-safety.d.ts +62 -0
  61. package/scripts/workflows/infrastructure/shell-safety.js +106 -0
  62. package/scripts/workflows/safe-outputs/index.d.ts +7 -0
  63. package/scripts/workflows/safe-outputs/index.js +4 -0
  64. package/scripts/workflows/safe-outputs/types.d.ts +63 -0
  65. package/scripts/workflows/safe-outputs/types.js +39 -0
  66. package/scripts/workflows/types.d.ts +110 -0
  67. package/scripts/workflows/types.js +14 -0
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.2.21 for accessing real EU Parliament data via the Model Context Protocol.
139
+ v1.3.0 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.2.21+, fully operational):
435
+ **Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.3.0+, fully operational):
436
436
 
437
437
  - 🗳️ Plenary sessions, voting records, roll-call votes
438
438
  - 📜 Adopted texts, motions, resolutions, urgency files
@@ -492,8 +492,8 @@ EU Parliament Monitor implements **security-by-design** under the [Hack23 ISMS](
492
492
 
493
493
  ### Requirements
494
494
 
495
- - **Node.js** 25 or higher
496
- - **npm** 10 or higher (ships with Node.js 25)
495
+ - **Node.js** 26 or higher
496
+ - **npm** 10 or higher (ships with Node.js 26)
497
497
  - **Git**
498
498
 
499
499
  ### From source
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.58",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -33,6 +33,14 @@
33
33
  "./generators/*": {
34
34
  "import": "./scripts/generators/*.js",
35
35
  "types": "./scripts/generators/*.d.ts"
36
+ },
37
+ "./workflows": {
38
+ "import": "./scripts/workflows/index.js",
39
+ "types": "./scripts/workflows/index.d.ts"
40
+ },
41
+ "./workflows/*": {
42
+ "import": "./scripts/workflows/*.js",
43
+ "types": "./scripts/workflows/*.d.ts"
36
44
  }
37
45
  },
38
46
  "files": [
@@ -157,7 +165,7 @@
157
165
  "husky": "9.1.7",
158
166
  "jscpd": "4.0.9",
159
167
  "knip": "^6.7.0",
160
- "lint-staged": "16.4.0",
168
+ "lint-staged": "17.0.2",
161
169
  "mermaid": "11.14.0",
162
170
  "papaparse": "5.5.3",
163
171
  "prettier": "3.8.3",
@@ -168,10 +176,10 @@
168
176
  "vitest": "4.1.5"
169
177
  },
170
178
  "engines": {
171
- "node": ">=25"
179
+ "node": ">=26"
172
180
  },
173
181
  "dependencies": {
174
- "european-parliament-mcp-server": "1.2.21",
182
+ "european-parliament-mcp-server": "1.3.0",
175
183
  "markdown-it": "^14.1.1",
176
184
  "markdown-it-anchor": "^9.2.0",
177
185
  "markdown-it-attrs": "^4.3.1",
@@ -183,6 +191,8 @@
183
191
  },
184
192
  "overrides": {
185
193
  "flatted": ">=3.4.2",
186
- "path-to-regexp": ">=8.4.0"
194
+ "path-to-regexp": ">=8.4.0",
195
+ "ip-address": ">=10.1.1",
196
+ "uuid": ">=11.1.1"
187
197
  }
188
198
  }
@@ -23,7 +23,7 @@ export interface ArticleMeta {
23
23
  readonly keyDates: readonly string[];
24
24
  /** Key actors / political groups identified by the artifacts. */
25
25
  readonly keyActors: readonly string[];
26
- /** Optional IMF / WorldBank macro hook surfaced as a sidebar callout. */
26
+ /** Optional IMF economic-context macro hook surfaced as a sidebar callout. Empty string when absent. */
27
27
  readonly macroContext: string;
28
28
  /**
29
29
  * Run-relative paths of every artifact whose content fed into this meta
@@ -324,7 +324,6 @@ export function isGenericHeading(heading, articleType, date) {
324
324
  return true;
325
325
  }
326
326
  // The bare `${human} — <anything>` with nothing extra is also generic.
327
- // eslint-disable-next-line security/detect-non-literal-regexp -- `human` derives from a sanitised slug via escapeRegex
328
327
  const trailingDateOnly = new RegExp(`^${escapeRegex(human)}\\s*[—–-]\\s*[\\d-]+$`, 'u');
329
328
  if (trailingDateOnly.test(normalized)) {
330
329
  return true;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module Aggregator/Artifacts
3
+ * @description Public re-exports for the artifacts bounded context.
4
+ * Provides artifact ordering, sanitization, and type definitions.
5
+ */
6
+ export type { ArtifactContent, CleanedArtifactWithPath, ResolvedSection } from './types.js';
7
+ export type { ArtifactSection } from '../artifact-order.js';
8
+ export { ARTIFACT_SECTIONS, MANIFEST_SECTION_ID, MANIFEST_SECTION_TITLE, SUPPLEMENTARY_SECTION_ID, SUPPLEMENTARY_SECTION_TITLE, TRADECRAFT_SECTION_ID, TRADECRAFT_SECTION_TITLE, } from '../artifact-order.js';
9
+ export type { CleanArtifactOptions, CleanArtifactResult } from '../clean-artifact.js';
10
+ export { cleanArtifact, dedupMermaid, demoteHeadings, githubBlobUrl, githubRawUrl, resolveLink, rewriteLinks, stripArtifactMetadataPreamble, stripBanners, stripFrontMatter, stripSpdxTags, } from '../clean-artifact.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { ARTIFACT_SECTIONS, MANIFEST_SECTION_ID, MANIFEST_SECTION_TITLE, SUPPLEMENTARY_SECTION_ID, SUPPLEMENTARY_SECTION_TITLE, TRADECRAFT_SECTION_ID, TRADECRAFT_SECTION_TITLE, } from '../artifact-order.js';
4
+ export { cleanArtifact, dedupMermaid, demoteHeadings, githubBlobUrl, githubRawUrl, resolveLink, rewriteLinks, stripArtifactMetadataPreamble, stripBanners, stripFrontMatter, stripSpdxTags, } from '../clean-artifact.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @module Aggregator/Artifacts/Types
3
+ * @description Strict type definitions for the artifacts bounded context.
4
+ * Defines the shape of artifact content, section mappings, and sanitization
5
+ * options used throughout the aggregator pipeline.
6
+ */
7
+ import type { CleanArtifactResult } from '../clean-artifact.js';
8
+ /**
9
+ * Represents raw artifact content read from an analysis run directory.
10
+ * Carries the original Markdown body along with its provenance metadata.
11
+ */
12
+ export interface ArtifactContent {
13
+ /** Run-relative path of the artifact (e.g. `intelligence/synthesis-summary.md`). */
14
+ readonly path: string;
15
+ /** Raw Markdown body as read from disk. */
16
+ readonly body: string;
17
+ /** Byte length of the raw content (before cleaning). */
18
+ readonly byteLength: number;
19
+ }
20
+ /**
21
+ * A {@link CleanArtifactResult} extended with provenance (the run-relative
22
+ * path of the source artifact). Use this type when you need to track which
23
+ * artifact produced a given cleaned output — e.g. for telemetry or
24
+ * source-mapping in `article-meta.json`.
25
+ *
26
+ * `CleanArtifactResult` is the raw return of `cleanArtifact()`;
27
+ * `CleanedArtifactWithPath` adds the origin path for pipeline bookkeeping.
28
+ */
29
+ export interface CleanedArtifactWithPath extends CleanArtifactResult {
30
+ /** Run-relative path of the source artifact. */
31
+ readonly path: string;
32
+ }
33
+ /**
34
+ * Represents a fully resolved section in the aggregated article, mapping
35
+ * from the canonical section definition to the actual artifact paths found
36
+ * in the run directory.
37
+ */
38
+ export interface ResolvedSection {
39
+ /** Stable section id used for HTML anchors. */
40
+ readonly id: string;
41
+ /** English section title. */
42
+ readonly title: string;
43
+ /** Ordered list of resolved artifact paths present in this section. */
44
+ readonly resolvedArtifacts: readonly string[];
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -194,7 +194,7 @@ export function stripSpdxTags(md) {
194
194
  */
195
195
  function advanceFenceState(line, inFence, fenceMarker) {
196
196
  const fenceMatch = /^(\s*)(```+|~~~+)(.*)$/.exec(line);
197
- if (!fenceMatch || !fenceMatch[2])
197
+ if (!fenceMatch?.[2])
198
198
  return { inFence, fenceMarker, matched: false };
199
199
  const marker = fenceMatch[2];
200
200
  if (!inFence) {
@@ -224,7 +224,7 @@ function processHeadingLine(lines, index) {
224
224
  return { output: null, consumed: 2, h1Removed: true };
225
225
  }
226
226
  const atx = /^(\s*)(#{1,6})(\s.*)$/.exec(line);
227
- if (!atx || !atx[2]) {
227
+ if (!atx?.[2]) {
228
228
  return { output: line, consumed: 1, h1Removed: false };
229
229
  }
230
230
  const level = atx[2].length;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @module Aggregator/Content
3
+ * @description Public re-exports for the content extraction bounded context.
4
+ * Provides lead extraction, key takeaways synthesis, and intelligence guide
5
+ * generation.
6
+ */
7
+ export type { ExtractedLead, SynthesisedTakeaway, KeyTakeawaysResult } from './types.js';
8
+ export { extractExecutiveLead, extractLeadParagraph, trimToLeadSentence, MAX_LEAD_CHARS, } from '../lead-extractor.js';
9
+ export type { Takeaway, BuildKeyTakeawaysOptions } from '../key-takeaways.js';
10
+ export { buildKeyTakeaways, MIN_TAKEAWAYS, MAX_TAKEAWAYS, KEY_TAKEAWAYS_SECTION_ID, KEY_TAKEAWAYS_SECTION_TITLE, } from '../key-takeaways.js';
11
+ export { buildReaderIntelligenceGuideHtml } from '../reader-intelligence-guide.js';
12
+ export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from '../reader-guide-constants.js';
13
+ export type { TocSection, IncludedArtifact } from '../reader-guide-constants.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { extractExecutiveLead, extractLeadParagraph, trimToLeadSentence, MAX_LEAD_CHARS, } from '../lead-extractor.js';
4
+ export { buildKeyTakeaways, MIN_TAKEAWAYS, MAX_TAKEAWAYS, KEY_TAKEAWAYS_SECTION_ID, KEY_TAKEAWAYS_SECTION_TITLE, } from '../key-takeaways.js';
5
+ export { buildReaderIntelligenceGuideHtml } from '../reader-intelligence-guide.js';
6
+ export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from '../reader-guide-constants.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module Aggregator/Content/Types
3
+ * @description Strict type definitions for the content extraction bounded
4
+ * context. Covers lead paragraph extraction, key takeaways synthesis,
5
+ * and section-to-artifact mapping.
6
+ */
7
+ /**
8
+ * Extracted lead paragraph — the journalistic "nut graf" from the article.
9
+ */
10
+ export interface ExtractedLead {
11
+ /** The extracted lead text (plain, no Markdown). */
12
+ readonly text: string;
13
+ /** Run-relative source artifact the lead was extracted from. */
14
+ readonly source: string;
15
+ /** Character length of the extracted lead. */
16
+ readonly length: number;
17
+ }
18
+ /**
19
+ * One synthesised key takeaway bullet ready for rendering.
20
+ */
21
+ export interface SynthesisedTakeaway {
22
+ /** Bullet body in Markdown (trimmed; no leading `- `). */
23
+ readonly body: string;
24
+ /** Run-relative path of the source artifact. */
25
+ readonly source: string;
26
+ }
27
+ /**
28
+ * Structured representation of key-takeaways synthesis output.
29
+ *
30
+ * **Note:** The current `buildKeyTakeaways` function returns a plain `string`
31
+ * (the rendered Markdown block). This interface is a forward-looking contract
32
+ * for future structured consumers that need both the raw takeaway items and
33
+ * the rendered output. Use {@link Takeaway} (from `key-takeaways.ts`) for
34
+ * per-item typing when processing the raw bullets.
35
+ */
36
+ export interface KeyTakeawaysResult {
37
+ /** Ordered list of synthesised takeaways (3–7 items). */
38
+ readonly takeaways: readonly SynthesisedTakeaway[];
39
+ /** Rendered Markdown block (empty string when below minimum threshold). */
40
+ readonly markdown: string;
41
+ }
42
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module Aggregator/Markdown
3
+ * @description Public re-exports for the Markdown rendering bounded context.
4
+ * Provides markdown-it based HTML rendering with plugin support and
5
+ * TOC generation.
6
+ */
7
+ export type { RenderOptions, RenderedMarkdown, TocEntry } from '../markdown-renderer.js';
8
+ export { buildMarkdownIt, renderMarkdown } from '../markdown-renderer.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { buildMarkdownIt, renderMarkdown } from '../markdown-renderer.js';
4
+ //# sourceMappingURL=index.js.map
@@ -185,14 +185,14 @@ function harvestToc(tokens) {
185
185
  const out = [];
186
186
  for (let i = 0; i < tokens.length; i++) {
187
187
  const token = tokens[i];
188
- if (!token || token.type !== 'heading_open')
188
+ if (token?.type !== 'heading_open')
189
189
  continue;
190
190
  const level = Number.parseInt(token.tag.slice(1), 10);
191
191
  if (!Number.isFinite(level) || level < 2 || level > 6)
192
192
  continue;
193
193
  const slug = typeof token.attrGet === 'function' ? token.attrGet('id') : null;
194
194
  const inline = tokens[i + 1];
195
- if (!inline || inline.type !== 'inline')
195
+ if (inline?.type !== 'inline')
196
196
  continue;
197
197
  const text = (inline.content ?? '').trim();
198
198
  out.push({ level, slug: slug ?? slugify(text), text });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module Aggregator/Metadata
3
+ * @description Public re-exports for the metadata bounded context.
4
+ * Provides article metadata building, SEO metadata resolution,
5
+ * and structured data contracts.
6
+ */
7
+ export type { PerLanguageMetadata, ArticleMetaSidecar } from './types.js';
8
+ export type { ArticleMeta, BuildArticleMetaOptions } from '../article-meta.js';
9
+ export { buildArticleMeta } from '../article-meta.js';
10
+ export type { ResolvedMetadataEntry, ResolvedMetadata } from '../article-metadata.js';
11
+ export { resolveArticleMetadata, stripInlineMarkdown } from '../article-metadata.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { buildArticleMeta } from '../article-meta.js';
4
+ export { resolveArticleMetadata, stripInlineMarkdown } from '../article-metadata.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module Aggregator/Metadata/Types
3
+ * @description Strict type definitions for the metadata bounded context.
4
+ * Covers article metadata, SEO metadata, and structured data contracts.
5
+ */
6
+ import type { LanguageCode } from '../../types/index.js';
7
+ /**
8
+ * Per-language resolved metadata entry containing the title and
9
+ * description that will be rendered into HTML meta tags and
10
+ * structured data (JSON-LD).
11
+ */
12
+ export interface PerLanguageMetadata {
13
+ /** Language code this entry is for. */
14
+ readonly lang: LanguageCode;
15
+ /** Resolved title for the article in this language. */
16
+ readonly title: string;
17
+ /** Resolved description for the article in this language. */
18
+ readonly description: string;
19
+ }
20
+ /**
21
+ * Complete article metadata sidecar shape (`article-meta.json`).
22
+ * This is a type alias for the canonical {@link ArticleMeta} interface
23
+ * defined in `article-meta.ts`. Use this name when referring to the
24
+ * sidecar contract in downstream consumers (HTML, RSS, sitemap, news
25
+ * indexes).
26
+ */
27
+ export type { ArticleMeta as ArticleMetaSidecar } from '../article-meta.js';
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -157,7 +157,7 @@ export const ARTICLE_HORIZONS = {
157
157
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.WEEK_AHEAD],
158
158
  timePeriod: CATEGORY_TIME_PERIOD[ArticleCategory.WEEK_AHEAD],
159
159
  dataWindow: { direction: 'forward', days: 7, anchor: 'today' },
160
- cadence: { cron: '0 6 * * 0', description: 'Weekly — Sunday 06:00 UTC' },
160
+ cadence: { cron: '0 7 * * 5', description: 'Weekly — Friday 07:00 UTC' },
161
161
  primaryFeeds: [...STANDARD_FEEDS],
162
162
  mandatoryArtifacts: [...PROSPECTIVE_MANDATORY, A_FORWARD_PROJECTION],
163
163
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -171,7 +171,7 @@ export const ARTICLE_HORIZONS = {
171
171
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.MONTH_AHEAD],
172
172
  timePeriod: CATEGORY_TIME_PERIOD[ArticleCategory.MONTH_AHEAD],
173
173
  dataWindow: { direction: 'forward', days: 30, anchor: 'today' },
174
- cadence: { cron: '0 6 1 * *', description: 'Monthly — 1st @ 06:00 UTC' },
174
+ cadence: { cron: '0 8 1 * *', description: 'Monthly — 1st @ 08:00 UTC' },
175
175
  primaryFeeds: [...STANDARD_FEEDS],
176
176
  mandatoryArtifacts: [...PROSPECTIVE_MANDATORY, A_FORWARD_PROJECTION],
177
177
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -222,7 +222,7 @@ export const ARTICLE_HORIZONS = {
222
222
  timePeriod: CATEGORY_TIME_PERIOD[ArticleCategory.WEEK_IN_REVIEW],
223
223
  // ADR-006: D-8 → D-36 reporting window for roll-call publication delay.
224
224
  dataWindow: { direction: 'backward', days: 28, anchor: 'today' },
225
- cadence: { cron: '0 6 * * 6', description: 'Weekly — Saturday 06:00 UTC' },
225
+ cadence: { cron: '0 9 * * 6', description: 'Weekly — Saturday 09:00 UTC' },
226
226
  primaryFeeds: [...STANDARD_FEEDS, 'get_voting_records'],
227
227
  mandatoryArtifacts: [...RETROSPECTIVE_MANDATORY],
228
228
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -236,7 +236,7 @@ export const ARTICLE_HORIZONS = {
236
236
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.MONTH_IN_REVIEW],
237
237
  timePeriod: CATEGORY_TIME_PERIOD[ArticleCategory.MONTH_IN_REVIEW],
238
238
  dataWindow: { direction: 'backward', days: 30, anchor: 'today' },
239
- cadence: { cron: '0 6 5 * *', description: 'Monthly — 5th @ 06:00 UTC' },
239
+ cadence: { cron: '0 10 28 * *', description: 'Monthly — 28th @ 10:00 UTC' },
240
240
  primaryFeeds: [...STANDARD_FEEDS, 'get_voting_records'],
241
241
  mandatoryArtifacts: [...RETROSPECTIVE_MANDATORY],
242
242
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -328,7 +328,7 @@ export const ARTICLE_HORIZONS = {
328
328
  slug: 'breaking',
329
329
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.BREAKING_NEWS],
330
330
  dataWindow: { direction: 'point', anchor: 'today' },
331
- cadence: { cron: '0 */4 * * *', description: 'Every 4 hours' },
331
+ cadence: { cron: '0 */6 * * *', description: 'Every 6 hours' },
332
332
  primaryFeeds: [...STANDARD_FEEDS],
333
333
  mandatoryArtifacts: [
334
334
  A_SIGNIFICANCE,
@@ -357,7 +357,7 @@ export const ARTICLE_HORIZONS = {
357
357
  slug: 'committee-reports',
358
358
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.COMMITTEE_REPORTS],
359
359
  dataWindow: { direction: 'backward', days: 30, anchor: 'today' },
360
- cadence: { cron: '0 7 * * 1', description: 'WeeklyMonday 07:00 UTC' },
360
+ cadence: { cron: '0 4 * * 1-5', description: 'WeekdaysMon–Fri 04:00 UTC' },
361
361
  primaryFeeds: [...STANDARD_FEEDS, 'get_committee_documents'],
362
362
  mandatoryArtifacts: [...RETROSPECTIVE_MANDATORY],
363
363
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -370,7 +370,7 @@ export const ARTICLE_HORIZONS = {
370
370
  slug: 'motions',
371
371
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.MOTIONS],
372
372
  dataWindow: { direction: 'backward', days: 30, anchor: 'today' },
373
- cadence: { cron: '0 7 * * 2', description: 'WeeklyTuesday 07:00 UTC' },
373
+ cadence: { cron: '0 6 * * 1-5', description: 'WeekdaysMon–Fri 06:00 UTC' },
374
374
  primaryFeeds: [...STANDARD_FEEDS, 'get_voting_records'],
375
375
  mandatoryArtifacts: [...RETROSPECTIVE_MANDATORY],
376
376
  optionalArtifacts: [A_EXEC_BRIEF],
@@ -383,7 +383,7 @@ export const ARTICLE_HORIZONS = {
383
383
  slug: 'propositions',
384
384
  perspective: CATEGORY_PERSPECTIVE[ArticleCategory.PROPOSITIONS],
385
385
  dataWindow: { direction: 'forward', days: 90, anchor: 'today' },
386
- cadence: { cron: '0 7 * * 3', description: 'WeeklyWednesday 07:00 UTC' },
386
+ cadence: { cron: '0 5 * * 1-5', description: 'WeekdaysMon–Fri 05:00 UTC' },
387
387
  primaryFeeds: [...STANDARD_FEEDS, 'get_procedures'],
388
388
  mandatoryArtifacts: [...PROSPECTIVE_MANDATORY],
389
389
  optionalArtifacts: [A_PIPELINE_FORECAST, A_EXEC_BRIEF],
@@ -1347,7 +1347,6 @@ export function getCategoryIndicators(category) {
1347
1347
  if (!Object.hasOwn(CATEGORY_INDICATOR_MAP, category)) {
1348
1348
  return getCategoryIndicators(ArticleCategory.BREAKING_NEWS);
1349
1349
  }
1350
- // eslint-disable-next-line security/detect-object-injection -- key validated via Object.hasOwn
1351
1350
  return CATEGORY_INDICATOR_MAP[category];
1352
1351
  }
1353
1352
  /**
@@ -66,7 +66,6 @@ export const LANGUAGE_NAMES = {
66
66
  */
67
67
  export function getLocalizedString(map, lang) {
68
68
  const code = lang;
69
- // eslint-disable-next-line security/detect-object-injection -- key validated via Object.hasOwn
70
69
  return Object.hasOwn(map, code) ? (map[code] ?? map.en) : map.en;
71
70
  }
72
71
  /**
@@ -442,7 +442,6 @@ export const PI_COPY = (() => {
442
442
  * @returns Fully-populated {@link PICopy}
443
443
  */
444
444
  export function getPICopy(lang) {
445
- // eslint-disable-next-line security/detect-object-injection
446
445
  const overrides = PI_COPY[lang] ?? {};
447
446
  return { ...DEFAULT_COPY, ...overrides };
448
447
  }
@@ -73,6 +73,9 @@ export const CURATED_DESCRIPTIONS = {
73
73
  zh: '欧盟范围选举分析方法论 — 预测、欧洲议会 361 席阈值及成员国层面的联盟数学,以及选民分群框架。',
74
74
  },
75
75
  },
76
+ 'analysis/methodologies/analytical-supplementary-methodology.md': {
77
+ description: 'Optional deep-dive methodology — PESTLE, Wildcards, SWOT scoring, and Media Framing v2.0.',
78
+ },
76
79
  'analysis/methodologies/imf-indicator-mapping.md': {
77
80
  description: 'Canonical mapping of IMF WEO, Fiscal Monitor, IFS, BOP, ER and PCPS indicators to European Parliament Monitor article types — the primary source for economic, monetary, fiscal, trade and FDI context.',
78
81
  i18n: {
@@ -313,7 +316,7 @@ export const CURATED_DESCRIPTIONS = {
313
316
  description: 'MCP reliability audit — endpoint health and uptime report for every European Parliament MCP tool invocation during a workflow run.',
314
317
  },
315
318
  'analysis/templates/media-framing-analysis.md': {
316
- description: 'Media framing analysismaps how narratives spread across outlets and languages, comparing national-media framings of EP events.',
319
+ description: 'Media framing & influence-operations DISARM TTPs, CIB detection, narrative-laundering, counter-resilience across EU-27.',
317
320
  },
318
321
  'analysis/templates/methodology-reflection.md': {
319
322
  description: 'Methodology reflection template — the final Step 10.5 artifact capturing lessons learned, protocol gaps and continuous-improvement notes for each run.',
@@ -1951,7 +1954,6 @@ function kindWord(relPath, lang) {
1951
1954
  * @returns The resolved string (never empty for well-formed records)
1952
1955
  */
1953
1956
  function getFromRecord(record, lang) {
1954
- // eslint-disable-next-line security/detect-object-injection
1955
1957
  return record[lang] ?? record.en;
1956
1958
  }
1957
1959
  /**
@@ -1968,7 +1970,6 @@ function getFromRecord(record, lang) {
1968
1970
  * @returns Fully localized description sentence
1969
1971
  */
1970
1972
  function buildGenericFallback(relPath, lang, title) {
1971
- // eslint-disable-next-line security/detect-object-injection
1972
1973
  const template = GENERIC_FALLBACK_I18N[lang] ?? GENERIC_FALLBACK_I18N.en;
1973
1974
  const kind = kindWord(relPath, lang);
1974
1975
  return template.replace('{title}', title).replace('{kind}', kind);
@@ -1996,10 +1997,14 @@ function buildGenericFallback(relPath, lang, title) {
1996
1997
  export function getCuratedDescription(relPath, lang, fallback = '') {
1997
1998
  // Normalise path separators so Windows callers don't silently miss entries.
1998
1999
  const key = relPath.replace(/\\/g, '/');
1999
- // eslint-disable-next-line security/detect-object-injection
2000
+ // Guard against prototype-chain lookups for keys like __proto__ or constructor.
2001
+ if (!Object.prototype.hasOwnProperty.call(CURATED_DESCRIPTIONS, key)) {
2002
+ // No curated entry — build a localized fallback from the file's title.
2003
+ const localizedTitle = getCuratedTitle(key, lang, fallback || stripEmojiAndPunct(key));
2004
+ return buildGenericFallback(key, lang, localizedTitle);
2005
+ }
2000
2006
  const entry = CURATED_DESCRIPTIONS[key];
2001
2007
  if (entry) {
2002
- // eslint-disable-next-line security/detect-object-injection
2003
2008
  const localized = entry.i18n?.[lang];
2004
2009
  if (localized)
2005
2010
  return localized;
@@ -2023,7 +2028,6 @@ export function getCuratedDescription(relPath, lang, fallback = '') {
2023
2028
  * @returns `true` when the curated table contains the file
2024
2029
  */
2025
2030
  export function hasCuratedDescription(relPath) {
2026
- // eslint-disable-next-line security/detect-object-injection
2027
2031
  return Object.prototype.hasOwnProperty.call(CURATED_DESCRIPTIONS, relPath.replace(/\\/g, '/'));
2028
2032
  }
2029
2033
  /**
@@ -2035,7 +2039,6 @@ export function hasCuratedDescription(relPath) {
2035
2039
  * @returns `true` when {@link CURATED_TITLES} contains the file
2036
2040
  */
2037
2041
  export function hasCuratedTitle(relPath) {
2038
- // eslint-disable-next-line security/detect-object-injection
2039
2042
  return Object.prototype.hasOwnProperty.call(CURATED_TITLES, relPath.replace(/\\/g, '/'));
2040
2043
  }
2041
2044
  /**
@@ -2062,27 +2065,20 @@ export function hasCuratedTitle(relPath) {
2062
2065
  */
2063
2066
  export function getCuratedTitle(relPath, lang, fallback) {
2064
2067
  const key = relPath.replace(/\\/g, '/');
2065
- // 1 + 2: curated title overlay
2066
- // eslint-disable-next-line security/detect-object-injection
2067
- const titleEntry = CURATED_TITLES[key];
2068
- if (titleEntry) {
2069
- // eslint-disable-next-line security/detect-object-injection
2070
- const localized = titleEntry[lang];
2071
- if (localized)
2072
- return localized;
2073
- if (titleEntry.en)
2074
- return titleEntry.en;
2068
+ // 1 + 2: curated title overlay — guard against prototype-chain lookups
2069
+ if (Object.prototype.hasOwnProperty.call(CURATED_TITLES, key)) {
2070
+ const titleEntry = CURATED_TITLES[key];
2071
+ if (titleEntry) {
2072
+ // Prefer localized, then English overlay
2073
+ return titleEntry[lang] ?? titleEntry.en ?? fallback;
2074
+ }
2075
2075
  }
2076
2076
  // 3 + 4: historic colocated title on CURATED_DESCRIPTIONS entry
2077
- // eslint-disable-next-line security/detect-object-injection
2078
- const descEntry = CURATED_DESCRIPTIONS[key];
2079
- if (descEntry) {
2080
- // eslint-disable-next-line security/detect-object-injection
2081
- const localized = descEntry.titleI18n?.[lang];
2082
- if (localized)
2083
- return localized;
2084
- if (descEntry.title)
2085
- return descEntry.title;
2077
+ if (Object.prototype.hasOwnProperty.call(CURATED_DESCRIPTIONS, key)) {
2078
+ const descEntry = CURATED_DESCRIPTIONS[key];
2079
+ if (descEntry) {
2080
+ return descEntry.titleI18n?.[lang] ?? descEntry.title ?? fallback;
2081
+ }
2086
2082
  }
2087
2083
  return fallback;
2088
2084
  }
@@ -2673,7 +2669,6 @@ export function parseRunSlug(slug) {
2673
2669
  const sorted = [...RUN_TYPE_SLUGS].sort((a, b) => b.length - a.length);
2674
2670
  for (const prefix of sorted) {
2675
2671
  if (lower === prefix || lower.startsWith(`${prefix}-`) || lower.startsWith(`${prefix}_`)) {
2676
- // eslint-disable-next-line security/detect-object-injection
2677
2672
  const canonical = RUN_TYPE_ALIASES[prefix];
2678
2673
  const tail = slug.slice(prefix.length).replace(/^[-_]+/, '');
2679
2674
  return { type: canonical, runId: tail };
@@ -2696,9 +2691,7 @@ export function parseRunSlug(slug) {
2696
2691
  export function getRunTypeInfo(slug, lang) {
2697
2692
  const { type, runId } = parseRunSlug(slug);
2698
2693
  if (type) {
2699
- // eslint-disable-next-line security/detect-object-injection
2700
2694
  const titleRecord = RUN_TYPE_TITLES[type];
2701
- // eslint-disable-next-line security/detect-object-injection
2702
2695
  const descRecord = RUN_TYPE_DESCRIPTIONS[type];
2703
2696
  const title = titleRecord ? getFromRecord(titleRecord, lang) : stripEmojiAndPunct(slug);
2704
2697
  const description = descRecord ? getFromRecord(descRecord, lang) : '';
@@ -2797,7 +2790,6 @@ function canonicalizeArtifactStem(stem) {
2797
2790
  'ai-voting-patterns': 'voting-patterns',
2798
2791
  };
2799
2792
  if (Object.prototype.hasOwnProperty.call(SYNONYMS, s)) {
2800
- // eslint-disable-next-line security/detect-object-injection
2801
2793
  const synonym = SYNONYMS[s];
2802
2794
  if (typeof synonym === 'string')
2803
2795
  return synonym;
@@ -3135,7 +3127,6 @@ export function getArtifactInfo(shortPath, lang) {
3135
3127
  // and we still guard with `hasOwn` to block any prototype-key surprise.
3136
3128
  const feed = parseFeedPrefix(rawStem);
3137
3129
  if (feed && Object.prototype.hasOwnProperty.call(FEED_PREFIX_LABELS, feed.feed)) {
3138
- // eslint-disable-next-line security/detect-object-injection
3139
3130
  const entry = FEED_PREFIX_LABELS[feed.feed];
3140
3131
  if (entry) {
3141
3132
  return {
@@ -3152,7 +3143,6 @@ export function getArtifactInfo(shortPath, lang) {
3152
3143
  // (e.g. a hypothetical `__proto__.md` file).
3153
3144
  const stemLower = stem.toLowerCase();
3154
3145
  if (Object.prototype.hasOwnProperty.call(ORPHAN_ARTIFACT_INFO, stemLower)) {
3155
- // eslint-disable-next-line security/detect-object-injection
3156
3146
  const orphan = ORPHAN_ARTIFACT_INFO[stemLower];
3157
3147
  if (orphan) {
3158
3148
  return {