euparliamentmonitor 0.8.51 → 0.8.52

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.2.15 for accessing real EU Parliament data via the Model Context Protocol.
139
+ v1.2.18 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)
@@ -426,7 +426,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
426
426
 
427
427
  ## 🔌 Data Sources
428
428
 
429
- **Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.15+, fully operational):
429
+ **Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.18+, fully operational):
430
430
 
431
431
  - 🗳️ Plenary sessions, voting records, roll-call votes
432
432
  - 📜 Adopted texts, motions, resolutions, urgency files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.51",
3
+ "version": "0.8.52",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -48,7 +48,8 @@
48
48
  "registry": "https://registry.npmjs.org/"
49
49
  },
50
50
  "scripts": {
51
- "prebuild": "node scripts/generators/news-indexes.js && node scripts/generators/sitemap.js",
51
+ "prebuild": "node scripts/generators/build-info.js && node scripts/generators/news-indexes.js && node scripts/generators/sitemap.js",
52
+ "generate-build-info": "node scripts/generators/build-info.js",
52
53
  "build": "tsc",
53
54
  "build:check": "tsc --noEmit",
54
55
  "build:check-tests": "tsc --project tsconfig.test.json --noEmit",
@@ -168,7 +169,7 @@
168
169
  "node": ">=25"
169
170
  },
170
171
  "dependencies": {
171
- "european-parliament-mcp-server": "1.2.15",
172
+ "european-parliament-mcp-server": "1.2.18",
172
173
  "markdown-it": "^14.1.1",
173
174
  "markdown-it-anchor": "^9.2.0",
174
175
  "markdown-it-attrs": "^4.3.1",
@@ -19,7 +19,8 @@
19
19
  * browser and CloudFront caches automatically.
20
20
  */
21
21
  import { BASE_URL, MERMAID_VERSION } from '../constants/config.js';
22
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
22
+ import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
23
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
23
24
  import { escapeHTML } from '../utils/file-utils.js';
24
25
  import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
25
26
  /**
@@ -189,6 +190,10 @@ ${hreflangLinks}
189
190
  <link rel="manifest" href="../site.webmanifest">
190
191
  <meta name="theme-color" content="#003399">
191
192
  <link rel="stylesheet" href="../styles.css">
193
+ <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, safeLang))}">
194
+ <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, safeLang))}">
195
+ <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, safeLang))}">
196
+ ${buildHeadFreshnessTags('../')}
192
197
  <script type="application/ld+json">${jsonLdString}</script>
193
198
  <script type="module" src="../js/mermaid-init.js?v=${MERMAID_VERSION}" defer></script>
194
199
  <script src="../js/article-runtime.js" defer></script>
@@ -3,24 +3,33 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  /**
6
- * Prior-run diff helper for the re-run merge rule.
6
+ * Prior-run diff helper for the re-run improve/extend rule.
7
7
  *
8
8
  * Reads `manifest.json.history[]` from a same-day analysis folder and
9
- * classifies every artifact as **at-floor** (carry-forward) or **below-floor**
10
- * (rewrite). The result — a `priorRunDiff` plan — is written to stdout as
11
- * JSON and can be consumed by Stage A of the analysis workflow.
9
+ * classifies every artifact as **at-floor** (must-extend / carry-forward) or
10
+ * **below-floor** (rewrite). The result — a `priorRunDiff` plan with
11
+ * `mode: "improve-and-extend"` is written to stdout as JSON and is
12
+ * consumed by Stage B of the analysis workflow.
12
13
  *
13
- * Controlled by the `ENABLE_PRIOR_RUN_MERGE` environment variable:
14
- * - `ENABLE_PRIOR_RUN_MERGE=true` → normal operation (produce plan)
15
- * - unset / any other value → short-circuit: emit plan with
16
- * `enabled: false` and empty arrays
14
+ * **Re-run semantics (never no-op).** Entries listed under `carryForward[]`
15
+ * are **NOT** skipped on re-runs they are must-extend targets. Stage B
16
+ * MUST raise their depth: each prior artifact's `priorLines` becomes the new
17
+ * floor and the agent must add ≥1 new section, ≥3 new evidence citations, or
18
+ * ≥1 new chart, ending at `lines >= max(floor, priorLines + 20)`. Entries in
19
+ * `rewrite[]` are still written from scratch to the catalog floor.
20
+ *
21
+ * Always-on. The `ENABLE_PRIOR_RUN_MERGE` environment variable is no longer
22
+ * read — the helper runs unconditionally so re-runs cannot accidentally
23
+ * regress to the legacy "skip-write" behaviour. The `buildPriorRunDiff(..,
24
+ * enabled)` parameter is kept for back-compat with unit tests but the CLI
25
+ * always passes `true`.
17
26
  *
18
27
  * Invocation:
19
28
  * node scripts/aggregator/prior-run-diff.js <runDir>
20
29
  * npm run prior-run-diff -- analysis/daily/2026-04-26/week-in-review
21
30
  *
22
31
  * Exit codes:
23
- * 0 — plan emitted successfully (or feature disabled)
32
+ * 0 — plan emitted successfully
24
33
  * 1 — runDir missing or invalid
25
34
  * 2 — bad CLI usage
26
35
  *
@@ -28,6 +37,7 @@
28
37
  * ```json
29
38
  * {
30
39
  * "enabled": true,
40
+ * "mode": "improve-and-extend",
31
41
  * "runDir": "analysis/daily/2026-04-26/week-in-review",
32
42
  * "articleType": "week-in-review",
33
43
  * "priorRunId": "week-in-review-run-1714128000",
@@ -35,8 +45,10 @@
35
45
  * {
36
46
  * "relativePath": "intelligence/synthesis-summary.md",
37
47
  * "lines": 250,
48
+ * "priorLines": 250,
38
49
  * "floor": 180,
39
- * "source": "carry-forward-from:week-in-review-run-1714128000"
50
+ * "extendFloor": 270,
51
+ * "source": "extend-from-prior:week-in-review-run-1714128000"
40
52
  * }
41
53
  * ],
42
54
  * "rewrite": [
@@ -50,10 +62,13 @@
50
62
  * }
51
63
  * ```
52
64
  *
53
- * The `source` value on each carry-forward entry follows the schema:
54
- * `"carry-forward-from:<runId>"`
55
- * which Stage B writes into `manifest.json.artifactSources` (additive,
56
- * backward-compatible with the existing schema).
65
+ * - `priorLines` exposes the prior-run line count so Stage B knows the lower
66
+ * bound it must beat.
67
+ * - `extendFloor` = `max(floor, priorLines + 20)` — the minimum line count
68
+ * the new pass MUST reach for this artifact.
69
+ * - The `source` value follows the schema `"extend-from-prior:<runId>"`,
70
+ * which Stage B writes into `manifest.json.artifactSources` (additive,
71
+ * back-compat with prior `"carry-forward-from:<runId>"` consumers).
57
72
  */
58
73
 
59
74
  import fs from 'node:fs';
@@ -63,6 +78,7 @@ import { fileURLToPath } from 'node:url';
63
78
 
64
79
  const ROOT = process.cwd();
65
80
  const DEFAULT_MIN_LINES = 30;
81
+ const EXTEND_DELTA_LINES = 20;
66
82
 
67
83
  // Artifacts that must contain at least one Mermaid fenced block.
68
84
  // Mirrors the directory-based heuristic in validate-analysis-completeness.js.
@@ -86,9 +102,10 @@ function usage(code = 2) {
86
102
  '',
87
103
  ' <runDir> Path to analysis/daily/<date>/<slug>/',
88
104
  '',
89
- 'Environment:',
90
- ' ENABLE_PRIOR_RUN_MERGE=true Enable the carry-forward classifier',
91
- ' (default: disabled emits empty plan)',
105
+ 'Always-on. The helper unconditionally classifies prior-run artifacts as',
106
+ 'must-extend (carryForward[]) or below-floor rewrite (rewrite[]) so re-runs',
107
+ 'can never accidentally no-op. The legacy ENABLE_PRIOR_RUN_MERGE env flag',
108
+ 'is no longer read.',
92
109
  '',
93
110
  'Example:',
94
111
  ' npm run prior-run-diff -- analysis/daily/2026-04-26/week-in-review',
@@ -190,9 +207,14 @@ export function classifyArtifact(runDir, relativePath, floor, mermaidRequiredLis
190
207
  /**
191
208
  * Build the `priorRunDiff` plan for a same-day analysis folder.
192
209
  *
210
+ * Mode is always **improve-and-extend**: `carryForward[]` entries are
211
+ * must-extend targets (their `priorLines` and `extendFloor` exposed), not
212
+ * skip-write targets. The `enabled` parameter is preserved for back-compat
213
+ * with the legacy unit-test signature; the CLI always passes `true`.
214
+ *
193
215
  * @param {string} runDir - Absolute path to the run folder.
194
216
  * @param {object|null} thresholdsJson - Parsed reference-quality-thresholds.json.
195
- * @param {boolean} enabled - Whether the feature is enabled.
217
+ * @param {boolean} enabled - Whether the feature is enabled (CLI: always true).
196
218
  * @returns {object} The diff plan (serialisable to JSON).
197
219
  */
198
220
  export function buildPriorRunDiff(runDir, thresholdsJson, enabled) {
@@ -210,6 +232,7 @@ export function buildPriorRunDiff(runDir, thresholdsJson, enabled) {
210
232
  if (!enabled) {
211
233
  return {
212
234
  enabled: false,
235
+ mode: 'improve-and-extend',
213
236
  runDir: relRunDir,
214
237
  articleType,
215
238
  priorRunId: null,
@@ -222,6 +245,7 @@ export function buildPriorRunDiff(runDir, thresholdsJson, enabled) {
222
245
  if (history.length === 0) {
223
246
  return {
224
247
  enabled: true,
248
+ mode: 'improve-and-extend',
225
249
  runDir: relRunDir,
226
250
  articleType,
227
251
  priorRunId: null,
@@ -248,11 +272,14 @@ export function buildPriorRunDiff(runDir, thresholdsJson, enabled) {
248
272
  const floor = Math.max(DEFAULT_MIN_LINES, perArtifactFloors[relativePath] ?? 0);
249
273
  const result = classifyArtifact(runDir, relativePath, floor, mermaidRequiredList);
250
274
  if (result.atFloor) {
275
+ const extendFloor = Math.max(floor, result.lines + EXTEND_DELTA_LINES);
251
276
  carryForward.push({
252
277
  relativePath,
253
278
  lines: result.lines,
279
+ priorLines: result.lines,
254
280
  floor: result.floor,
255
- source: `carry-forward-from:${priorRunId}`,
281
+ extendFloor,
282
+ source: `extend-from-prior:${priorRunId}`,
256
283
  });
257
284
  } else {
258
285
  rewrite.push({
@@ -266,6 +293,7 @@ export function buildPriorRunDiff(runDir, thresholdsJson, enabled) {
266
293
 
267
294
  return {
268
295
  enabled: true,
296
+ mode: 'improve-and-extend',
269
297
  runDir: relRunDir,
270
298
  articleType,
271
299
  priorRunId,
@@ -338,7 +366,11 @@ function main() {
338
366
  process.exit(1);
339
367
  }
340
368
 
341
- const enabled = process.env['ENABLE_PRIOR_RUN_MERGE'] === 'true';
369
+ // Re-run improve/extend rule is always-on. The legacy ENABLE_PRIOR_RUN_MERGE
370
+ // env flag is no longer read — re-runs cannot accidentally regress to the
371
+ // pre-2026-05 skip-write behaviour. See .github/prompts/02-analysis-protocol.md
372
+ // §"Re-run improve/extend rule".
373
+ const enabled = true;
342
374
  const thresholdsJson = loadThresholds(opts.thresholdsPath);
343
375
  const plan = buildPriorRunDiff(runDir, thresholdsJson, enabled);
344
376
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Build the shared freshness/PWA `<head>` block.
3
+ *
4
+ * @param pathPrefix - Asset path prefix (`''` for root pages, `'../'`
5
+ * for `news/` pages).
6
+ * @returns Multi-line HTML string. Caller is responsible for placing it
7
+ * inside `<head>…</head>`. The result is already HTML-escaped.
8
+ */
9
+ export declare function buildHeadFreshnessTags(pathPrefix: string): string;
10
+ //# sourceMappingURL=build-info-meta.d.ts.map
@@ -0,0 +1,45 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module Constants/BuildInfoMeta
5
+ * @description Shared helper that emits the `<head>` freshness tags every
6
+ * generator must include so the PWA layer (`js/pwa-register.js`) can:
7
+ *
8
+ * - Read the embedded build commit SHA + timestamp from `<meta>` tags.
9
+ * - Load the same-origin service-worker registration script.
10
+ * - Tell browsers + intermediate proxies to revalidate every navigation
11
+ * (`Cache-Control: no-cache`, `Pragma: no-cache`).
12
+ *
13
+ * Every value is HTML-escaped — `BUILD_ID`/`BUILD_TIME` are tightly
14
+ * formatted (40-char hex / ISO 8601) but defence-in-depth is cheap.
15
+ *
16
+ * The output is a multi-line string; callers should drop it into the
17
+ * `<head>` block alongside other `<meta>` tags. CSP stays `script-src
18
+ * 'self'` because the only emitted `<script>` references a same-origin
19
+ * file with a `defer` attribute.
20
+ */
21
+ import { BUILD_ID, BUILD_TIME } from './config.js';
22
+ import { escapeHTML } from '../utils/file-utils.js';
23
+ /**
24
+ * Build the shared freshness/PWA `<head>` block.
25
+ *
26
+ * @param pathPrefix - Asset path prefix (`''` for root pages, `'../'`
27
+ * for `news/` pages).
28
+ * @returns Multi-line HTML string. Caller is responsible for placing it
29
+ * inside `<head>…</head>`. The result is already HTML-escaped.
30
+ */
31
+ export function buildHeadFreshnessTags(pathPrefix) {
32
+ const safeBuildId = escapeHTML(BUILD_ID);
33
+ const safeBuildTime = escapeHTML(BUILD_TIME);
34
+ // Path prefix is built from controlled string literals (`''` or `'../'`),
35
+ // but escape it anyway to keep the helper safe under future callers.
36
+ const safePrefix = escapeHTML(pathPrefix);
37
+ return [
38
+ ` <meta name="build-id" content="${safeBuildId}">`,
39
+ ` <meta name="build-time" content="${safeBuildTime}">`,
40
+ ` <meta http-equiv="Cache-Control" content="no-cache">`,
41
+ ` <meta http-equiv="Pragma" content="no-cache">`,
42
+ ` <script src="${safePrefix}js/pwa-register.js" defer></script>`,
43
+ ].join('\n');
44
+ }
45
+ //# sourceMappingURL=build-info-meta.js.map
@@ -64,4 +64,24 @@ export declare const THEME_TOGGLE_SCRIPT_CONTENT = "\n (function(){\n var do
64
64
  * Detects system theme on first click when no explicit preference is saved.
65
65
  */
66
66
  export declare const THEME_TOGGLE_SCRIPT = "\n <script>\n (function(){\n var docEl=document.documentElement;\n var t=localStorage.getItem('ep-theme');\n var storedTheme=t==='light'?'light':t==='dark'?'dark':null;\n if(storedTheme){\n docEl.setAttribute('data-theme',storedTheme);\n }else if(t){\n localStorage.removeItem('ep-theme');\n }\n var btn=document.querySelector('.theme-toggle');\n if(!btn)return;\n btn.addEventListener('click',function(){\n var cur=docEl.getAttribute('data-theme');\n if(!cur){\n cur=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';\n }\n var next=cur==='dark'?'light':'dark';\n docEl.setAttribute('data-theme',next);\n localStorage.setItem('ep-theme',next);\n });\n })();\n </script>";
67
+ /**
68
+ * Full git commit SHA (40 chars) for the running build. Resolved via env
69
+ * (`BUILD_ID`), then `git rev-parse HEAD`, then a deterministic placeholder
70
+ * (`'0'.repeat(40)`). Never empty, never throws.
71
+ */
72
+ export declare const BUILD_ID: string;
73
+ /** First 7 chars of {@link BUILD_ID} — the conventional short SHA. */
74
+ export declare const BUILD_SHORT: string;
75
+ /**
76
+ * ISO 8601 timestamp for when this build was produced. Precedence:
77
+ * 1. `process.env.BUILD_TIME` (CI sets this in the workflow)
78
+ * 2. `new Date().toISOString()` fallback
79
+ */
80
+ export declare const BUILD_TIME: string;
81
+ /**
82
+ * Optional release tag (e.g. `v0.8.51`). Empty string when no tag was
83
+ * supplied via `process.env.RELEASE_TAG`. Surfaced in `build-info.json`
84
+ * for clients that want a human-readable label.
85
+ */
86
+ export declare const RELEASE_TAG: string;
67
87
  //# sourceMappingURL=config.d.ts.map
@@ -4,6 +4,7 @@
4
4
  * @module Constants/Config
5
5
  * @description Shared configuration constants
6
6
  */
7
+ import { execSync } from 'child_process';
7
8
  import fs from 'fs';
8
9
  import path from 'path';
9
10
  import { fileURLToPath } from 'url';
@@ -137,4 +138,60 @@ export const THEME_TOGGLE_SCRIPT_CONTENT = `
137
138
  */
138
139
  export const THEME_TOGGLE_SCRIPT = `
139
140
  <script>${THEME_TOGGLE_SCRIPT_CONTENT}</script>`;
141
+ /**
142
+ * Resolve the current build commit SHA. Precedence:
143
+ * 1. `process.env.BUILD_ID` (CI sets this from `${{ github.sha }}`)
144
+ * 2. `git rev-parse HEAD` (works in dev clones / local builds)
145
+ * 3. `'0'.repeat(40)` (deterministic, never throws)
146
+ *
147
+ * Always returns a 40-char lowercase hex string. Never throws — generator
148
+ * scripts must be safe to run on machines without git installed.
149
+ *
150
+ * @returns 40-char lowercase hex commit SHA, or `'0'.repeat(40)` placeholder.
151
+ */
152
+ function resolveBuildId() {
153
+ const fromEnv = (process.env.BUILD_ID ?? '').trim();
154
+ if (/^[0-9a-f]{40}$/i.test(fromEnv)) {
155
+ return fromEnv.toLowerCase();
156
+ }
157
+ try {
158
+ const fromGit = execSync('git rev-parse HEAD', {
159
+ encoding: 'utf-8',
160
+ stdio: ['ignore', 'pipe', 'ignore'],
161
+ cwd: PROJECT_ROOT,
162
+ }).trim();
163
+ if (/^[0-9a-f]{40}$/i.test(fromGit)) {
164
+ return fromGit.toLowerCase();
165
+ }
166
+ }
167
+ catch {
168
+ /* git unavailable or not a repo — fall through to placeholder */
169
+ }
170
+ return '0'.repeat(40);
171
+ }
172
+ /**
173
+ * Full git commit SHA (40 chars) for the running build. Resolved via env
174
+ * (`BUILD_ID`), then `git rev-parse HEAD`, then a deterministic placeholder
175
+ * (`'0'.repeat(40)`). Never empty, never throws.
176
+ */
177
+ export const BUILD_ID = resolveBuildId();
178
+ /** First 7 chars of {@link BUILD_ID} — the conventional short SHA. */
179
+ export const BUILD_SHORT = BUILD_ID.slice(0, 7);
180
+ /**
181
+ * ISO 8601 timestamp for when this build was produced. Precedence:
182
+ * 1. `process.env.BUILD_TIME` (CI sets this in the workflow)
183
+ * 2. `new Date().toISOString()` fallback
184
+ */
185
+ export const BUILD_TIME = (() => {
186
+ const fromEnv = (process.env.BUILD_TIME ?? '').trim();
187
+ if (fromEnv)
188
+ return fromEnv;
189
+ return new Date().toISOString();
190
+ })();
191
+ /**
192
+ * Optional release tag (e.g. `v0.8.51`). Empty string when no tag was
193
+ * supplied via `process.env.RELEASE_TAG`. Surfaced in `build-info.json`
194
+ * for clients that want a human-readable label.
195
+ */
196
+ export const RELEASE_TAG = (process.env.RELEASE_TAG ?? '').trim();
140
197
  //# sourceMappingURL=config.js.map
@@ -193,4 +193,22 @@ export declare const FOOTER_REPORT_ISSUES_LABELS: LanguageMap;
193
193
  export declare const FOOTER_ARTICLES_AVAILABLE_LABELS: LanguageMap;
194
194
  /** Localized "Political Intelligence" link label used in footer Quick Links section */
195
195
  export declare const FOOTER_POLITICAL_INTELLIGENCE_LABELS: LanguageMap;
196
+ /** "Install app" CTA label (PWA install hint). */
197
+ export declare const INSTALL_APP_LABELS: LanguageMap;
198
+ /** Toast message shown when a newer build of the site is detected. */
199
+ export declare const UPDATE_AVAILABLE_LABELS: LanguageMap;
200
+ /** "Refresh" CTA on the update toast — reloads the page. */
201
+ export declare const UPDATE_REFRESH_CTA_LABELS: LanguageMap;
202
+ /** "Dismiss" aria-label on the update toast close button. */
203
+ export declare const UPDATE_DISMISS_LABELS: LanguageMap;
204
+ /** Offline page title. */
205
+ export declare const OFFLINE_TITLE_LABELS: LanguageMap;
206
+ /** Offline page body explaining the situation. */
207
+ export declare const OFFLINE_BODY_LABELS: LanguageMap;
208
+ /** "Try again" button on the offline page. */
209
+ export declare const OFFLINE_RETRY_LABELS: LanguageMap;
210
+ /** "Build" label for the build-id link in the footer. */
211
+ export declare const BUILD_INFO_COMMIT_LABELS: LanguageMap;
212
+ /** "Deployed" label preceding the build timestamp in the footer. */
213
+ export declare const BUILD_INFO_DEPLOYED_LABELS: LanguageMap;
196
214
  //# sourceMappingURL=language-ui.d.ts.map
@@ -1866,4 +1866,158 @@ export const FOOTER_POLITICAL_INTELLIGENCE_LABELS = {
1866
1866
  ko: '정치 정보',
1867
1867
  zh: '政治情报',
1868
1868
  };
1869
+ /* ─── PWA + freshness UI labels (Phase 3) ────────────────────────── */
1870
+ /** "Install app" CTA label (PWA install hint). */
1871
+ export const INSTALL_APP_LABELS = {
1872
+ en: 'Install app',
1873
+ sv: 'Installera app',
1874
+ da: 'Installer app',
1875
+ no: 'Installer app',
1876
+ fi: 'Asenna sovellus',
1877
+ de: 'App installieren',
1878
+ fr: "Installer l'application",
1879
+ es: 'Instalar aplicación',
1880
+ nl: 'App installeren',
1881
+ ar: 'تثبيت التطبيق',
1882
+ he: 'התקן את האפליקציה',
1883
+ ja: 'アプリをインストール',
1884
+ ko: '앱 설치',
1885
+ zh: '安装应用',
1886
+ };
1887
+ /** Toast message shown when a newer build of the site is detected. */
1888
+ export const UPDATE_AVAILABLE_LABELS = {
1889
+ en: 'Updated content available',
1890
+ sv: 'Uppdaterat innehåll tillgängligt',
1891
+ da: 'Opdateret indhold tilgængeligt',
1892
+ no: 'Oppdatert innhold tilgjengelig',
1893
+ fi: 'Päivitetty sisältö saatavilla',
1894
+ de: 'Aktualisierte Inhalte verfügbar',
1895
+ fr: 'Contenu mis à jour disponible',
1896
+ es: 'Contenido actualizado disponible',
1897
+ nl: 'Bijgewerkte inhoud beschikbaar',
1898
+ ar: 'يتوفر محتوى محدث',
1899
+ he: 'תוכן מעודכן זמין',
1900
+ ja: '最新のコンテンツが利用可能です',
1901
+ ko: '업데이트된 콘텐츠가 있습니다',
1902
+ zh: '有可用的更新内容',
1903
+ };
1904
+ /** "Refresh" CTA on the update toast — reloads the page. */
1905
+ export const UPDATE_REFRESH_CTA_LABELS = {
1906
+ en: 'Refresh',
1907
+ sv: 'Uppdatera',
1908
+ da: 'Opdater',
1909
+ no: 'Oppdater',
1910
+ fi: 'Päivitä',
1911
+ de: 'Aktualisieren',
1912
+ fr: 'Actualiser',
1913
+ es: 'Actualizar',
1914
+ nl: 'Vernieuwen',
1915
+ ar: 'تحديث',
1916
+ he: 'רענן',
1917
+ ja: '更新',
1918
+ ko: '새로고침',
1919
+ zh: '刷新',
1920
+ };
1921
+ /** "Dismiss" aria-label on the update toast close button. */
1922
+ export const UPDATE_DISMISS_LABELS = {
1923
+ en: 'Dismiss',
1924
+ sv: 'Stäng',
1925
+ da: 'Luk',
1926
+ no: 'Lukk',
1927
+ fi: 'Sulje',
1928
+ de: 'Schließen',
1929
+ fr: 'Fermer',
1930
+ es: 'Cerrar',
1931
+ nl: 'Sluiten',
1932
+ ar: 'إغلاق',
1933
+ he: 'סגור',
1934
+ ja: '閉じる',
1935
+ ko: '닫기',
1936
+ zh: '关闭',
1937
+ };
1938
+ /** Offline page title. */
1939
+ export const OFFLINE_TITLE_LABELS = {
1940
+ en: "You're offline",
1941
+ sv: 'Du är offline',
1942
+ da: 'Du er offline',
1943
+ no: 'Du er frakoblet',
1944
+ fi: 'Olet offline-tilassa',
1945
+ de: 'Sie sind offline',
1946
+ fr: 'Vous êtes hors ligne',
1947
+ es: 'Estás sin conexión',
1948
+ nl: 'Je bent offline',
1949
+ ar: 'أنت غير متصل',
1950
+ he: 'אתה במצב לא מקוון',
1951
+ ja: 'オフラインです',
1952
+ ko: '오프라인 상태입니다',
1953
+ zh: '您当前处于离线状态',
1954
+ };
1955
+ /** Offline page body explaining the situation. */
1956
+ export const OFFLINE_BODY_LABELS = {
1957
+ en: "EU Parliament Monitor is unavailable while you're offline. Reconnect to load the latest political intelligence.",
1958
+ sv: 'EU Parliament Monitor är otillgänglig medan du är offline. Anslut igen för att läsa den senaste politiska underrättelsen.',
1959
+ da: 'EU Parliament Monitor er utilgængelig, mens du er offline. Opret forbindelse igen for at hente den nyeste politiske efterretning.',
1960
+ no: 'EU Parliament Monitor er utilgjengelig mens du er frakoblet. Koble til på nytt for å laste inn den nyeste politiske etterretningen.',
1961
+ fi: 'EU Parliament Monitor ei ole käytettävissä, kun olet offline-tilassa. Yhdistä uudelleen ladataksesi uusimman poliittisen tiedustelun.',
1962
+ de: 'EU Parliament Monitor ist offline nicht verfügbar. Stellen Sie die Verbindung wieder her, um die neuesten politischen Informationen zu laden.',
1963
+ fr: 'EU Parliament Monitor est indisponible hors ligne. Reconnectez-vous pour charger les dernières informations politiques.',
1964
+ es: 'EU Parliament Monitor no está disponible sin conexión. Vuelve a conectarte para cargar la última inteligencia política.',
1965
+ nl: 'EU Parliament Monitor is offline niet beschikbaar. Maak opnieuw verbinding om de laatste politieke intelligentie te laden.',
1966
+ ar: 'مراقب البرلمان الأوروبي غير متوفر أثناء عدم الاتصال. أعد الاتصال لتحميل أحدث المعلومات السياسية.',
1967
+ he: 'EU Parliament Monitor אינו זמין במצב לא מקוון. התחבר מחדש כדי לטעון את המודיעין הפוליטי העדכני ביותר.',
1968
+ ja: 'オフライン中は EU Parliament Monitor を利用できません。再接続して最新の政治情報を読み込んでください。',
1969
+ ko: '오프라인 상태에서는 EU Parliament Monitor를 이용할 수 없습니다. 다시 연결하여 최신 정치 정보를 불러오세요.',
1970
+ zh: '您处于离线状态,无法使用 EU Parliament Monitor。重新连接以加载最新的政治情报。',
1971
+ };
1972
+ /** "Try again" button on the offline page. */
1973
+ export const OFFLINE_RETRY_LABELS = {
1974
+ en: 'Try again',
1975
+ sv: 'Försök igen',
1976
+ da: 'Prøv igen',
1977
+ no: 'Prøv igjen',
1978
+ fi: 'Yritä uudelleen',
1979
+ de: 'Erneut versuchen',
1980
+ fr: 'Réessayer',
1981
+ es: 'Intentar de nuevo',
1982
+ nl: 'Opnieuw proberen',
1983
+ ar: 'حاول مرة أخرى',
1984
+ he: 'נסה שוב',
1985
+ ja: '再試行',
1986
+ ko: '다시 시도',
1987
+ zh: '重试',
1988
+ };
1989
+ /** "Build" label for the build-id link in the footer. */
1990
+ export const BUILD_INFO_COMMIT_LABELS = {
1991
+ en: 'Build',
1992
+ sv: 'Bygg',
1993
+ da: 'Build',
1994
+ no: 'Build',
1995
+ fi: 'Build',
1996
+ de: 'Build',
1997
+ fr: 'Build',
1998
+ es: 'Build',
1999
+ nl: 'Build',
2000
+ ar: 'البناء',
2001
+ he: 'בנייה',
2002
+ ja: 'ビルド',
2003
+ ko: '빌드',
2004
+ zh: '构建',
2005
+ };
2006
+ /** "Deployed" label preceding the build timestamp in the footer. */
2007
+ export const BUILD_INFO_DEPLOYED_LABELS = {
2008
+ en: 'Deployed',
2009
+ sv: 'Driftsatt',
2010
+ da: 'Implementeret',
2011
+ no: 'Distribuert',
2012
+ fi: 'Julkaistu',
2013
+ de: 'Veröffentlicht',
2014
+ fr: 'Déployé',
2015
+ es: 'Desplegado',
2016
+ nl: 'Geïmplementeerd',
2017
+ ar: 'تم النشر',
2018
+ he: 'נפרס',
2019
+ ja: 'デプロイ済み',
2020
+ ko: '배포됨',
2021
+ zh: '已部署',
2022
+ };
1869
2023
  //# sourceMappingURL=language-ui.js.map
@@ -8,7 +8,7 @@
8
8
  * - **language-articles** — Article-type title generators and body-text strings
9
9
  */
10
10
  export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './language-core.js';
11
- export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, } from './language-ui.js';
11
+ export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, } from './language-ui.js';
12
12
  export type { AISection, RelationshipLabels, RelatedAnalysisStrings } from './language-ui.js';
13
13
  export { WEEK_AHEAD_TITLES, MONTH_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, PROPOSITIONS_TITLES, PROPOSITIONS_STRINGS, EDITORIAL_STRINGS, MOTIONS_STRINGS, WEEK_AHEAD_STRINGS, WEEK_AHEAD_STAKEHOLDER_STRINGS, BREAKING_STRINGS, DEEP_ANALYSIS_STRINGS, COMMITTEE_ANALYSIS_CONTENT_STRINGS, SWOT_STRINGS, DASHBOARD_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, LOCALIZED_KEYWORDS, MONTH_IN_REVIEW_STRINGS, ANALYSIS_QUALITY_LABELS, ANALYSIS_INSIGHTS_HEADING, } from './language-articles.js';
14
14
  //# sourceMappingURL=languages.d.ts.map
@@ -10,6 +10,6 @@
10
10
  * - **language-articles** — Article-type title generators and body-text strings
11
11
  */
12
12
  export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './language-core.js';
13
- export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, } from './language-ui.js';
13
+ export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, } from './language-ui.js';
14
14
  export { WEEK_AHEAD_TITLES, MONTH_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, PROPOSITIONS_TITLES, PROPOSITIONS_STRINGS, EDITORIAL_STRINGS, MOTIONS_STRINGS, WEEK_AHEAD_STRINGS, WEEK_AHEAD_STAKEHOLDER_STRINGS, BREAKING_STRINGS, DEEP_ANALYSIS_STRINGS, COMMITTEE_ANALYSIS_CONTENT_STRINGS, SWOT_STRINGS, DASHBOARD_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, LOCALIZED_KEYWORDS, MONTH_IN_REVIEW_STRINGS, ANALYSIS_QUALITY_LABELS, ANALYSIS_INSIGHTS_HEADING, } from './language-articles.js';
15
15
  //# sourceMappingURL=languages.js.map
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ /**
6
+ * Generate `build-info.json` (and the runtime `sw.js` from `sw.js.template`)
7
+ * at the project root. Called by `npm run prebuild` BEFORE the TypeScript
8
+ * compile step, so it stays in pure JS (no tsc dependency).
9
+ *
10
+ * The file is published at `/build-info.json` and polled by
11
+ * `js/pwa-register.js` to detect deploys. Schema:
12
+ *
13
+ * {
14
+ * "buildId": "40-char-lowercase-hex",
15
+ * "buildShort": "1234567",
16
+ * "buildTime": "2026-04-30T12:34:56.000Z",
17
+ * "appVersion": "0.8.51",
18
+ * "releaseTag": "" // optional, empty when no tag
19
+ * }
20
+ *
21
+ * Build metadata is resolved by `scripts/constants/config.js` so the
22
+ * generator, TypeScript templates, and runtime metadata share one source of
23
+ * truth for precedence, validation, and fallbacks.
24
+ *
25
+ * Idempotent: rerunning produces a byte-identical file when env + repo
26
+ * state are unchanged (ignoring BUILD_TIME drift).
27
+ */
28
+
29
+ import fs from 'fs';
30
+ import path from 'path';
31
+ import { fileURLToPath } from 'url';
32
+ import {
33
+ APP_VERSION,
34
+ BUILD_ID,
35
+ BUILD_SHORT,
36
+ BUILD_TIME,
37
+ RELEASE_TAG,
38
+ } from '../constants/config.js';
39
+
40
+ const __filename = fileURLToPath(import.meta.url);
41
+ const __dirname = path.dirname(__filename);
42
+ const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
43
+
44
+ function main() {
45
+ const payload = {
46
+ buildId: BUILD_ID,
47
+ buildShort: BUILD_SHORT,
48
+ buildTime: BUILD_TIME,
49
+ appVersion: APP_VERSION,
50
+ releaseTag: RELEASE_TAG,
51
+ };
52
+
53
+ const outPath = path.join(PROJECT_ROOT, 'build-info.json');
54
+ fs.writeFileSync(outPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
55
+ console.log(`✅ Wrote build-info.json (buildShort=${BUILD_SHORT}, appVersion=${APP_VERSION})`);
56
+
57
+ // Render the service-worker from its template — substitutes the build id
58
+ // into `CACHE_VERSION` so old caches are evicted on every deploy.
59
+ const tplPath = path.join(PROJECT_ROOT, 'sw.js.template');
60
+ const swPath = path.join(PROJECT_ROOT, 'sw.js');
61
+ if (fs.existsSync(tplPath)) {
62
+ const tpl = fs.readFileSync(tplPath, 'utf-8');
63
+ const rendered = tpl
64
+ .replace(/__BUILD_ID__/g, BUILD_ID)
65
+ .replace(/__BUILD_SHORT__/g, BUILD_SHORT);
66
+ fs.writeFileSync(swPath, rendered, 'utf-8');
67
+ console.log(`✅ Rendered sw.js from template (CACHE_VERSION=${BUILD_SHORT})`);
68
+ } else {
69
+ console.warn(`⚠️ sw.js.template not found at ${tplPath} — skipping sw.js render`);
70
+ }
71
+ }
72
+
73
+ main();
@@ -10,7 +10,8 @@
10
10
  import path, { resolve } from 'path';
11
11
  import { pathToFileURL } from 'url';
12
12
  import { PROJECT_ROOT, APP_VERSION, NEWS_DIR } from '../constants/config.js';
13
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, AI_SECTION_CONTENT, FILTER_LABELS, ARTICLE_TYPE_LABELS, HEADER_SUBTITLE_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
13
+ import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
14
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, AI_SECTION_CONTENT, FILTER_LABELS, ARTICLE_TYPE_LABELS, HEADER_SUBTITLE_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
14
15
  import { buildSiteFooter, buildSiteHeader } from '../templates/section-builders.js';
15
16
  import { getNewsArticles, groupArticlesByLanguage, formatSlug, parseArticleFilename, extractArticleMeta, escapeHTML, atomicWrite, } from '../utils/file-utils.js';
16
17
  import { writeMetadataDatabase } from '../utils/news-metadata.js';
@@ -197,6 +198,10 @@ export function generateIndexHTML(lang, articles, metaMap = new Map()) {
197
198
  <meta name="theme-color" content="#003399">
198
199
  <link rel="alternate" type="application/rss+xml" title="EU Parliament Monitor RSS" href="rss.xml">
199
200
  <link rel="stylesheet" href="styles.css">
201
+ <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
202
+ <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
203
+ <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
204
+ ${buildHeadFreshnessTags('')}
200
205
  </head>
201
206
  <body>
202
207
  <a href="#main" class="skip-link">${skipLinkText}</a>
@@ -13,7 +13,8 @@
13
13
  * tradecraft behind every published article.
14
14
  */
15
15
  import { BASE_URL, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
16
- import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, PAGE_TITLES, SKIP_LINK_TEXTS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
16
+ import { buildHeadFreshnessTags } from '../../constants/build-info-meta.js';
17
+ import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, PAGE_TITLES, SKIP_LINK_TEXTS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
17
18
  import { FOOTER_SITEMAP_LABELS } from '../../constants/language-ui.js';
18
19
  import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../../templates/section-builders.js';
19
20
  import { escapeHTML } from '../../utils/file-utils.js';
@@ -366,6 +367,10 @@ ${hreflangLinks}
366
367
  <link rel="manifest" href="site.webmanifest">
367
368
  <meta name="theme-color" content="#003399">
368
369
  <link rel="stylesheet" href="styles.css">
370
+ <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
371
+ <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
372
+ <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
373
+ ${buildHeadFreshnessTags('')}
369
374
  <script type="application/ld+json">${jsonLdString}</script>
370
375
  </head>
371
376
  <body>
@@ -20,7 +20,8 @@
20
20
  * golden snapshots taken from `npm run prebuild`).
21
21
  */
22
22
  import { BASE_URL, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
23
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SKIP_LINK_TEXTS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
23
+ import { buildHeadFreshnessTags } from '../../constants/build-info-meta.js';
24
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SKIP_LINK_TEXTS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
24
25
  import { escapeHTML } from '../../utils/file-utils.js';
25
26
  import { detectCategory } from '../../utils/article-category.js';
26
27
  import { ARTICLE_TYPE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, } from '../../constants/language-ui.js';
@@ -245,6 +246,10 @@ ${hreflangLinks}
245
246
  <link rel="manifest" href="site.webmanifest">
246
247
  <meta name="theme-color" content="#003399">
247
248
  <link rel="stylesheet" href="styles.css">
249
+ <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
250
+ <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
251
+ <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
252
+ ${buildHeadFreshnessTags('')}
248
253
  <script type="application/ld+json">${jsonLdString}</script>
249
254
  </head>
250
255
  <body>
@@ -7,7 +7,7 @@ import { MCPConnection } from './mcp-connection.js';
7
7
  import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions, GetProcedureEventByIdOptions, GetFreshProceduresOptions } from '../types/index.js';
8
8
  /**
9
9
  * Canonical list of tools exposed by the European Parliament MCP gateway
10
- * (`european-parliament-mcp-server@1.2.15`). The news workflows, prompt
10
+ * (`european-parliament-mcp-server@1.2.18`). The news workflows, prompt
11
11
  * library (`.github/prompts/07-mcp-reference.md`), and the integration test
12
12
  * suite all reference this list so a regression that adds/removes a tool
13
13
  * fails a single drift guard
@@ -22,7 +22,7 @@ export declare const EP_MCP_TOOLS: readonly string[];
22
22
  * covering the two shapes historically emitted by the EP MCP server.
23
23
  *
24
24
  * 1. **Uniform envelope** (all feeds as of
25
- * `european-parliament-mcp-server@1.2.15`) —
25
+ * `european-parliament-mcp-server@1.2.18`) —
26
26
  * `{status:"unavailable", items:[], generatedAt:"..."}` established by
27
27
  * Hack23/European-Parliament-MCP-Server#301 and extended to
28
28
  * `get_events_feed`/`get_procedures_feed` by
@@ -168,9 +168,9 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
168
168
  *
169
169
  * @remarks
170
170
  * This repository is currently documented/configured against
171
- * `european-parliament-mcp-server@1.2.15`.
171
+ * `european-parliament-mcp-server@1.2.18`.
172
172
  *
173
- * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.15 server):** the upstream server
173
+ * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.18 server):** the upstream server
174
174
  * applies a server-side post-filter on `dateFrom`/`dateTo` before serialisation, because the
175
175
  * EP Open Data Portal `/meetings` endpoint silently ignores its `date-from`/`date-to` query
176
176
  * parameters (Defect #5). Under this contract:
@@ -179,7 +179,7 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
179
179
  * - Per-window session counts are reproducible because the EP-side regression is masked by
180
180
  * the upstream post-filter.
181
181
  *
182
- * No local post-filter is applied here. The repository is pinned to v1.2.15, so the
182
+ * No local post-filter is applied here. The repository is pinned to v1.2.18, so the
183
183
  * date-filter guarantees above apply; consumers running against an older server image
184
184
  * (pre-v1.2.14) must not assume them.
185
185
  */
@@ -10,7 +10,7 @@ import { ProcedureSeenCache } from './procedure-seen-cache.js';
10
10
  import { recordPendingDocument, markDocumentResolved, getPendingDocumentsForReprobe, escalateExpiredDocuments, getPendingDocumentsSummary, } from './pending-documents.js';
11
11
  /**
12
12
  * Canonical list of tools exposed by the European Parliament MCP gateway
13
- * (`european-parliament-mcp-server@1.2.15`). The news workflows, prompt
13
+ * (`european-parliament-mcp-server@1.2.18`). The news workflows, prompt
14
14
  * library (`.github/prompts/07-mcp-reference.md`), and the integration test
15
15
  * suite all reference this list so a regression that adds/removes a tool
16
16
  * fails a single drift guard
@@ -114,7 +114,7 @@ const CONTENT_NOT_YET_AVAILABLE_SUBSTRING = 'document indexed but content not ye
114
114
  /**
115
115
  * Classify an error message into a diagnostic error category.
116
116
  *
117
- * Maps EP MCP Server v1.2.15 structured error codes and generic HTTP/network
117
+ * Maps EP MCP Server v1.2.18 structured error codes and generic HTTP/network
118
118
  * errors into one of six broad categories used for logging and retry decisions:
119
119
  *
120
120
  * Returned categories (priority order):
@@ -130,7 +130,7 @@ const CONTENT_NOT_YET_AVAILABLE_SUBSTRING = 'document indexed but content not ye
130
130
  */
131
131
  function classifyToolError(message) {
132
132
  const lowerMsg = message.toLowerCase();
133
- // EP MCP Server v1.2.15 structured error codes (matched case-insensitively)
133
+ // EP MCP Server v1.2.18 structured error codes (matched case-insensitively)
134
134
  if (lowerMsg.includes('internal_error')) {
135
135
  return 'INTERNAL_ERROR';
136
136
  }
@@ -189,7 +189,7 @@ function _parseResultPayload(result) {
189
189
  * covering the two shapes historically emitted by the EP MCP server.
190
190
  *
191
191
  * 1. **Uniform envelope** (all feeds as of
192
- * `european-parliament-mcp-server@1.2.15`) —
192
+ * `european-parliament-mcp-server@1.2.18`) —
193
193
  * `{status:"unavailable", items:[], generatedAt:"..."}` established by
194
194
  * Hack23/European-Parliament-MCP-Server#301 and extended to
195
195
  * `get_events_feed`/`get_procedures_feed` by
@@ -548,9 +548,9 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
548
548
  *
549
549
  * @remarks
550
550
  * This repository is currently documented/configured against
551
- * `european-parliament-mcp-server@1.2.15`.
551
+ * `european-parliament-mcp-server@1.2.18`.
552
552
  *
553
- * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.15 server):** the upstream server
553
+ * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.18 server):** the upstream server
554
554
  * applies a server-side post-filter on `dateFrom`/`dateTo` before serialisation, because the
555
555
  * EP Open Data Portal `/meetings` endpoint silently ignores its `date-from`/`date-to` query
556
556
  * parameters (Defect #5). Under this contract:
@@ -559,7 +559,7 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
559
559
  * - Per-window session counts are reproducible because the EP-side regression is masked by
560
560
  * the upstream post-filter.
561
561
  *
562
- * No local post-filter is applied here. The repository is pinned to v1.2.15, so the
562
+ * No local post-filter is applied here. The repository is pinned to v1.2.18, so the
563
563
  * date-filter guarantees above apply; consumers running against an older server image
564
564
  * (pre-v1.2.14) must not assume them.
565
565
  */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module Templates/Icons
3
+ * @description Inline SVG icon set used by the shared site header / footer
4
+ * to upgrade visual consistency across browsers (emoji rendering varies
5
+ * wildly between Windows, macOS, Android and Linux).
6
+ *
7
+ * Every icon is a 24×24 outline glyph that inherits `currentColor`. SVGs
8
+ * are emitted with `aria-hidden="true"` and `focusable="false"` so they
9
+ * stay decorative — call sites should keep their existing emoji span
10
+ * (also `aria-hidden`) so the visual diff is small while progressively
11
+ * enhancing the chrome.
12
+ *
13
+ * Returning `<span aria-hidden="true">` (empty) for unknown names keeps
14
+ * call sites safe even if a future caller passes a typo'd name.
15
+ */
16
+ /** Identifiers for the icons exported from this module. */
17
+ export type IconName = 'sponsor' | 'security' | 'rss' | 'github' | 'sitemap' | 'pi' | 'install' | 'refresh' | 'close';
18
+ export interface IconOptions {
19
+ /** Pixel size for both width and height (default 18). */
20
+ readonly size?: number;
21
+ }
22
+ /**
23
+ * Render an inline SVG icon.
24
+ *
25
+ * @param name - One of the {@link IconName} values.
26
+ * @param opts - Optional overrides (currently size only).
27
+ * @returns SVG markup string, or an empty `<span aria-hidden>` for unknown names.
28
+ */
29
+ export declare function icon(name: IconName, opts?: IconOptions): string;
30
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1,32 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ const PATHS = {
4
+ // Heart-in-circle for "Sponsor".
5
+ sponsor: '<path d="M12 21s-7-4.5-7-10a4 4 0 0 1 7-2.7A4 4 0 0 1 19 11c0 5.5-7 10-7 10Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>',
6
+ // Shield + checkmark.
7
+ security: '<path d="M12 3 4.5 6v6a8 8 0 0 0 7.5 8 8 8 0 0 0 7.5-8V6L12 3Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 12 2 2 4-4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
8
+ rss: '<path d="M5 5a14 14 0 0 1 14 14M5 11a8 8 0 0 1 8 8M6 17.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>',
9
+ github: '<path d="M12 3a9 9 0 0 0-2.85 17.54c.45.08.62-.2.62-.43v-1.5c-2.5.55-3.04-1.06-3.04-1.06-.4-1.05-1-1.32-1-1.32-.83-.57.06-.55.06-.55.92.07 1.4.94 1.4.94.81 1.4 2.13 1 2.65.76.08-.6.32-1 .58-1.23-2-.23-4.1-1-4.1-4.4 0-.97.34-1.77.9-2.4-.1-.23-.4-1.13.08-2.36 0 0 .73-.23 2.4.92a8.3 8.3 0 0 1 4.36 0c1.66-1.15 2.4-.92 2.4-.92.48 1.23.18 2.13.08 2.36.56.63.9 1.43.9 2.4 0 3.4-2.1 4.16-4.1 4.39.32.28.62.84.62 1.7v2.52c0 .24.17.52.62.43A9 9 0 0 0 12 3Z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>',
10
+ sitemap: '<path d="M12 4v4M12 12v4M6 16v4M18 16v4M6 16h12M9 8h6v4H9V8ZM4 18h4v3H4v-3ZM16 18h4v3h-4v-3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>',
11
+ // Compass for "Political Intelligence".
12
+ pi: '<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="m9 15 2-6 6-2-2 6-6 2Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>',
13
+ install: '<path d="M12 3v12m0 0-4-4m4 4 4-4M5 19h14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
14
+ refresh: '<path d="M21 12a9 9 0 1 1-3.5-7M21 4v5h-5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
15
+ close: '<path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>',
16
+ };
17
+ /**
18
+ * Render an inline SVG icon.
19
+ *
20
+ * @param name - One of the {@link IconName} values.
21
+ * @param opts - Optional overrides (currently size only).
22
+ * @returns SVG markup string, or an empty `<span aria-hidden>` for unknown names.
23
+ */
24
+ export function icon(name, opts = {}) {
25
+ const path = PATHS[name];
26
+ if (!path) {
27
+ return '<span class="icon icon-inline" aria-hidden="true"></span>';
28
+ }
29
+ const size = typeof opts.size === 'number' && opts.size > 0 ? opts.size : 18;
30
+ return `<svg class="icon icon-inline" width="${size}" height="${size}" viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false">${path}</svg>`;
31
+ }
32
+ //# sourceMappingURL=icons.js.map
@@ -7,8 +7,9 @@
7
7
  * timeline sections, comparison tables, and key figures bars.
8
8
  */
9
9
  import { escapeHTML } from '../utils/file-utils.js';
10
- import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, TOC_ARIA_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, } from '../constants/languages.js';
11
- import { APP_VERSION, createThemeToggleButton } from '../constants/config.js';
10
+ import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, TOC_ARIA_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, } from '../constants/languages.js';
11
+ import { APP_VERSION, BUILD_ID, BUILD_SHORT, BUILD_TIME, createThemeToggleButton, } from '../constants/config.js';
12
+ import { icon } from './icons.js';
12
13
  import { stripScriptBlocks, stripHtmlTags } from '../utils/html-sanitize.js';
13
14
  /**
14
15
  * Count occurrences of a regex pattern in a string.
@@ -281,9 +282,9 @@ export function buildSiteHeader(options) {
281
282
  </span>
282
283
  </a>
283
284
  <div class="site-header__actions">
284
- <a class="site-header__cta site-header__cta--sponsor" href="https://github.com/sponsors/Hack23">💖 Sponsor Hack23</a>
285
+ <a class="site-header__cta site-header__cta--sponsor" href="https://github.com/sponsors/Hack23">${icon('sponsor')}<span class="emoji" aria-hidden="true">💖</span> Sponsor Hack23</a>
285
286
  <a class="site-header__cta" href="https://www.hack23.com">Become a sponsor</a>
286
- <a class="site-header__cta site-header__cta--security" href="https://github.com/Hack23/euparliamentmonitor/blob/main/SECURITY.md">🔐 Commitment to Transparency and Security</a>
287
+ <a class="site-header__cta site-header__cta--security" href="https://github.com/Hack23/euparliamentmonitor/blob/main/SECURITY.md">${icon('security')}<span class="emoji" aria-hidden="true">🔐</span> Commitment to Transparency and Security</a>
287
288
  ${createThemeToggleButton(themeToggleLabel)}
288
289
  </div>
289
290
  <nav class="site-header__langs" role="navigation" aria-label="Language selection">
@@ -366,6 +367,17 @@ export function buildSiteFooter(options) {
366
367
  ? `\n <p class="footer-stats">${escapeHTML(getLocalizedString(FOOTER_ARTICLES_AVAILABLE_LABELS, lang).replace('{count}', String(articleCount)))}</p>`
367
368
  : '';
368
369
  const langGrid = buildFooterLangGrid(lang, pathPrefix);
370
+ const buildLabel = escapeHTML(getLocalizedString(BUILD_INFO_COMMIT_LABELS, lang));
371
+ const deployedLabel = escapeHTML(getLocalizedString(BUILD_INFO_DEPLOYED_LABELS, lang));
372
+ const safeBuildId = escapeHTML(BUILD_ID);
373
+ const safeBuildShort = escapeHTML(BUILD_SHORT);
374
+ const safeBuildTime = escapeHTML(BUILD_TIME);
375
+ const buildLine = `v${escapeHTML(APP_VERSION)} · ` +
376
+ `<a href="https://github.com/Hack23/euparliamentmonitor/commit/${safeBuildId}" ` +
377
+ `class="footer-build" title="${buildLabel} ${safeBuildShort}" rel="noopener">` +
378
+ `<code>${safeBuildShort}</code></a> · ` +
379
+ `<span class="footer-build-deployed">${deployedLabel}</span> ` +
380
+ `<time class="footer-build-time" datetime="${safeBuildTime}" data-relative-time>${safeBuildTime}</time>`;
369
381
  return `<footer class="site-footer" role="contentinfo">
370
382
  <div class="footer-content">
371
383
  <div class="footer-section">
@@ -378,17 +390,17 @@ export function buildSiteFooter(options) {
378
390
  <ul>
379
391
  <li><a href="${homeHref}">${homeLabel}</a></li>
380
392
  <li><a href="${homeHref}#main">News</a></li>
381
- <li><a href="${analysisDocsHref}">📊 Analysis & Reports</a></li>
393
+ <li><a href="${analysisDocsHref}">📊 Analysis &amp; Reports</a></li>
382
394
  <li><a href="${pathPrefix}docs/index.html">Dashboard</a></li>
383
- <li><a href="${politicalIntelligenceHref}">🧠 ${politicalIntelligenceLabel}</a></li>
384
- <li><a href="${sitemapHref}">🗺️ ${sitemapLabel}</a></li>
395
+ <li><a href="${politicalIntelligenceHref}">${icon('pi')}<span class="emoji" aria-hidden="true">🧠</span> ${politicalIntelligenceLabel}</a></li>
396
+ <li><a href="${sitemapHref}">${icon('sitemap')}<span class="emoji" aria-hidden="true">🗺️</span> ${sitemapLabel}</a></li>
385
397
  <li><a href="${apiDocsHref}">📚 API Documentation (TypeDoc)</a></li>
386
- <li><a href="${pathPrefix}rss.xml">${rssLabel}</a></li>
398
+ <li><a href="${pathPrefix}rss.xml">${icon('rss')}${rssLabel}</a></li>
387
399
  <li><a href="https://hack23.com/euparliamentmonitor.html">EU Parliament Monitor by Hack23</a></li>
388
400
  <li><a href="https://hack23.com/euparliamentmonitor-features.html">EU Parliament Monitor Features</a></li>
389
401
  <li><a href="https://hack23.com/cia-features.html">CIA Platform</a></li>
390
402
  <li><a href="https://www.riksdagen.se/">Sveriges Riksdag</a></li>
391
- <li><a href="https://github.com/Hack23/euparliamentmonitor">${githubLabel}</a></li>
403
+ <li><a href="https://github.com/Hack23/euparliamentmonitor">${icon('github')}${githubLabel}</a></li>
392
404
  <li><a href="https://github.com/Hack23/euparliamentmonitor/issues">${reportIssuesLabel}</a></li>
393
405
  <li><a href="https://github.com/Hack23/euparliamentmonitor/blob/main/LICENSE">${licenseLabel}</a></li>
394
406
  <li><a href="https://www.europarl.europa.eu/">${europarlLabel}</a></li>
@@ -426,7 +438,7 @@ export function buildSiteFooter(options) {
426
438
  </div>
427
439
  </div>
428
440
  <div class="footer-bottom">
429
- <p>&copy; 2008-${year} <a href="https://hack23.com">Hack23 AB</a> (Org.nr 5595347807) | Gothenburg, Sweden | v${escapeHTML(APP_VERSION)}</p>
441
+ <p>&copy; 2008-${year} <a href="https://hack23.com">Hack23 AB</a> (Org.nr 5595347807) | Gothenburg, Sweden | ${buildLine}</p>
430
442
  <p class="footer-disclaimer"><span aria-hidden="true">⚠️</span> ${disclaimerText} <a href="https://github.com/Hack23/euparliamentmonitor/issues">${reportIssuesLabel}</a>.</p>
431
443
  </div>
432
444
  </footer>`;
@@ -803,6 +803,51 @@ function main() {
803
803
  const forwardRegistryResult = validateForwardStatementsRegistryCoverage(runDir, articleType);
804
804
  mergeForwardRegistryResult(results, forwardRegistryResult);
805
805
 
806
+ // ── Re-run improve/extend enforcement ────────────────────────────────────
807
+ // Detect whether this is a re-run of an existing same-day analysis by
808
+ // checking manifest.history[]. When prior runs exist the agent MUST extend
809
+ // every artifact (rewriteCount must equal total artifact count, and each
810
+ // carry-forward artifact must reach its extendFloor). These are hard-RED
811
+ // violations, not warnings. See `.github/prompts/02-analysis-protocol.md`
812
+ // §"Re-run improve/extend rule".
813
+ const isRerun =
814
+ Array.isArray(manifest.history) && manifest.history.length > 0;
815
+
816
+ // Load prior-run-diff.json if present (produced unconditionally by
817
+ // `npm run prior-run-diff` in Stage A). Use a safeReadJson wrapper so a
818
+ // corrupt file degrades gracefully.
819
+ let priorRunDiff = null;
820
+ const priorRunDiffPath = path.join(runDir, 'runs', 'prior-run-diff.json');
821
+ if (fs.existsSync(priorRunDiffPath)) {
822
+ const raw = safeReadJson(priorRunDiffPath);
823
+ if (!raw.__error) priorRunDiff = raw;
824
+ }
825
+
826
+ // Build a quick lookup: relativePath → extendFloor, used below.
827
+ const extendFloorByPath = new Map();
828
+ if (priorRunDiff?.carryForward && Array.isArray(priorRunDiff.carryForward)) {
829
+ for (const entry of priorRunDiff.carryForward) {
830
+ if (entry.relativePath && typeof entry.extendFloor === 'number') {
831
+ extendFloorByPath.set(entry.relativePath, entry.extendFloor);
832
+ }
833
+ }
834
+ }
835
+
836
+ // On re-runs, check every carry-forward artifact's new line count against
837
+ // its extendFloor. Failures are hard-RED violations injected directly into
838
+ // the per-artifact result (not warnings) because a skip-write on a
839
+ // carry-forward target defeats the never-no-op contract.
840
+ if (isRerun && extendFloorByPath.size > 0) {
841
+ for (const r of results) {
842
+ const extendFloor = extendFloorByPath.get(r.relativePath);
843
+ if (extendFloor === undefined) continue;
844
+ if (!r.exists) continue; // already flagged as missing
845
+ if (r.lines < extendFloor) {
846
+ r.issues.push(`extend:below-extendFloor(${r.lines}<${extendFloor})`);
847
+ }
848
+ }
849
+ }
850
+
806
851
  // Orphans are reported as warnings (not blocking) — they may be valid extras.
807
852
  const summary = summarize(results);
808
853
  const offending = results.filter((r) => r.issues.length > 0);
@@ -825,13 +870,15 @@ function main() {
825
870
  );
826
871
  }
827
872
 
828
- const green = offending.length === 0;
829
-
873
+ // ── pass2 rewriteCount enforcement ───────────────────────────────────────
830
874
  // Pass-2-skipped heuristic: warn when manifest.pass2 is absent, malformed,
831
875
  // or `rewriteCount === 0` AND at least one artifact sits at exactly its
832
- // line floor. This is the script-side enforcement of the B1/B2 split
833
- // defined in `.github/prompts/02-analysis-protocol.md` §3. A malformed
834
- // pass2 block (non-numeric, non-finite, negative, non-integer
876
+ // line floor. On a re-run, `rewriteCount === 0` is a hard-RED violation
877
+ // (not a warning) because every artifact must be extended — a zero count
878
+ // means Stage B was a no-op. See `.github/prompts/02-analysis-protocol.md`
879
+ // §"Re-run improve/extend rule".
880
+ //
881
+ // A malformed pass2 block (non-numeric, non-finite, negative, non-integer
835
882
  // rewriteCount, or missing/non-string startedAt/endedAt timestamps) is
836
883
  // treated like an absent block so the enforcement can't be bypassed by
837
884
  // typos or malformed values.
@@ -876,7 +923,19 @@ function main() {
876
923
  );
877
924
  }
878
925
 
879
- if (pass2Absent || pass2Invalid || pass2ZeroRewrites) {
926
+ // On re-runs, a zero-rewrite pass2 is a hard-RED gate violation because
927
+ // every artifact must be extended. On first runs, it remains a warning.
928
+ let rerunZeroRewritesRed = false;
929
+ if (isRerun && pass2ZeroRewrites) {
930
+ rerunZeroRewritesRed = true;
931
+ process.stderr.write(
932
+ `RED rerun-no-op: manifest.pass2.rewriteCount === 0 on a re-run ` +
933
+ `(history[] non-empty) — Stage B must extend every artifact. ` +
934
+ `See .github/prompts/02-analysis-protocol.md §"Re-run improve/extend rule".\n`,
935
+ );
936
+ }
937
+
938
+ if (!rerunZeroRewritesRed && (pass2Absent || pass2Invalid || pass2ZeroRewrites)) {
880
939
  const atFloor = results.filter(
881
940
  (r) => r.exists && r.lines > 0 && r.lines === r.minLines,
882
941
  );
@@ -892,6 +951,8 @@ function main() {
892
951
  }
893
952
  }
894
953
 
954
+ const green = offending.length === 0 && !rerunZeroRewritesRed;
955
+
895
956
  const gateLine = green
896
957
  ? `STAGE_C_GATE: GREEN articleType=${articleType} artifacts=${results.length} lines=${summary.totalLines}`
897
958
  : `STAGE_C_GATE: RED articleType=${articleType} missing=${summary.missing} short=${summary.short} placeholders=${summary.placeholders} mermaid_missing=${summary.mermaidMissing} other=${summary.other}`;
@@ -905,6 +966,7 @@ function main() {
905
966
  articleType,
906
967
  runDir: path.relative(ROOT, runDir) || runDir,
907
968
  artifacts: results.length,
969
+ isRerun,
908
970
  summary,
909
971
  results,
910
972
  orphans,