euparliamentmonitor 0.8.49 → 0.8.51
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/package.json +5 -9
- package/scripts/aggregator/analysis-aggregator.d.ts +4 -26
- package/scripts/aggregator/analysis-aggregator.js +2 -2
- package/scripts/aggregator/article-generator.d.ts +2 -2
- package/scripts/aggregator/article-generator.js +1 -1
- package/scripts/aggregator/article-metadata.d.ts +4 -4
- package/scripts/aggregator/article-metadata.js +2 -2
- package/scripts/aggregator/cli/parse.d.ts +1 -1
- package/scripts/aggregator/cli/parse.js +2 -2
- package/scripts/aggregator/infra/github-urls.d.ts +1 -1
- package/scripts/aggregator/infra/github-urls.js +1 -1
- package/scripts/aggregator/manifest/resolver.d.ts +2 -2
- package/scripts/aggregator/manifest/resolver.js +2 -2
- package/scripts/aggregator/manifest/types.d.ts +10 -8
- package/scripts/aggregator/runs/discover.d.ts +1 -1
- package/scripts/aggregator/runs/discover.js +1 -1
- package/scripts/backport-article-seo.js +9 -9
- package/scripts/constants/analysis-constants.d.ts +1 -1
- package/scripts/constants/analysis-constants.js +1 -1
- package/scripts/generators/political-intelligence-descriptions.d.ts +5 -3
- package/scripts/generators/political-intelligence-descriptions.js +2 -2
- package/scripts/generators/sitemap/html.js +1 -1
- package/scripts/generators/sitemap/rss.js +2 -0
- package/scripts/generators/sitemap/xml.js +3 -1
- package/scripts/lint-prompts.js +19 -0
- package/scripts/mcp/ep-mcp-client.d.ts +1 -1
- package/scripts/mcp/ep-mcp-client.js +4 -4
- package/scripts/mcp/imf-mcp-client.d.ts +1 -1
- package/scripts/mcp/mcp-connection.js +1 -1
- package/scripts/types/imf.d.ts +1 -1
- package/scripts/types/parliament.d.ts +1 -1
- package/scripts/types/world-bank.d.ts +1 -1
- package/scripts/utils/file-utils.d.ts +2 -2
- package/scripts/utils/file-utils.js +2 -2
- package/scripts/validate-analysis-completeness.js +89 -0
- package/scripts/index.old.js +0 -125
- package/scripts/utils/migrate-legacy-articles.js +0 -225
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -33,14 +33,6 @@
|
|
|
33
33
|
"./generators/*": {
|
|
34
34
|
"import": "./scripts/generators/*.js",
|
|
35
35
|
"types": "./scripts/generators/*.d.ts"
|
|
36
|
-
},
|
|
37
|
-
"./generators/pipeline/*": {
|
|
38
|
-
"import": "./scripts/generators/pipeline/*.js",
|
|
39
|
-
"types": "./scripts/generators/pipeline/*.d.ts"
|
|
40
|
-
},
|
|
41
|
-
"./generators/strategies/*": {
|
|
42
|
-
"import": "./scripts/generators/strategies/*.js",
|
|
43
|
-
"types": "./scripts/generators/strategies/*.d.ts"
|
|
44
36
|
}
|
|
45
37
|
},
|
|
46
38
|
"files": [
|
|
@@ -84,6 +76,9 @@
|
|
|
84
76
|
"test:e2e:report": "playwright show-report",
|
|
85
77
|
"lint": "eslint src/",
|
|
86
78
|
"lint:fix": "eslint src/ --fix",
|
|
79
|
+
"knip": "knip",
|
|
80
|
+
"knip:production": "knip --production",
|
|
81
|
+
"knip:fix": "knip --fix",
|
|
87
82
|
"lint:report": "eslint src/ --format json --output-file builds/test-results/eslint-report.json",
|
|
88
83
|
"lint:report:html": "eslint src/ --format html --output-file builds/test-results/eslint-report.html",
|
|
89
84
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -158,6 +153,7 @@
|
|
|
158
153
|
"htmlhint": "1.9.2",
|
|
159
154
|
"husky": "9.1.7",
|
|
160
155
|
"jscpd": "4.0.9",
|
|
156
|
+
"knip": "^6.7.0",
|
|
161
157
|
"lint-staged": "16.4.0",
|
|
162
158
|
"mermaid": "11.14.0",
|
|
163
159
|
"papaparse": "5.5.3",
|
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { type ArtifactSection } from './artifact-order.js';
|
|
2
|
-
import { type Manifest, type ManifestFiles
|
|
3
|
-
/**
|
|
4
|
-
* Raw manifest shape as committed by the analysis pipeline.
|
|
5
|
-
*
|
|
6
|
-
* @deprecated Use {@link Manifest} from `aggregator/manifest/index.js`.
|
|
7
|
-
* This alias is preserved for back-compat with the existing test suite
|
|
8
|
-
* and external curators that import `AnalysisManifest` from this module.
|
|
9
|
-
*/
|
|
10
|
-
export type AnalysisManifest = Manifest;
|
|
11
|
-
/**
|
|
12
|
-
* `manifest.files` can be nested category → paths or flat path → description.
|
|
13
|
-
*
|
|
14
|
-
* @deprecated Use {@link _ManifestFiles} (`ManifestFiles`) from
|
|
15
|
-
* `aggregator/manifest/index.js`.
|
|
16
|
-
*/
|
|
17
|
-
export type ManifestFiles = _ManifestFiles;
|
|
18
|
-
/**
|
|
19
|
-
* One entry in `manifest.history[]`; only fields we read are typed.
|
|
20
|
-
*
|
|
21
|
-
* @deprecated Use {@link _ManifestHistoryEntry} (`ManifestHistoryEntry`) from
|
|
22
|
-
* `aggregator/manifest/index.js`.
|
|
23
|
-
*/
|
|
24
|
-
export type ManifestHistoryEntry = _ManifestHistoryEntry;
|
|
2
|
+
import { type Manifest, type ManifestFiles } from './manifest/index.js';
|
|
25
3
|
/** Result of {@link aggregateAnalysisRun}. */
|
|
26
4
|
export interface AggregatedRun {
|
|
27
5
|
/** Final Markdown document (provenance + sections + appendices). */
|
|
@@ -100,7 +78,7 @@ export declare function flattenManifestFiles(files: ManifestFiles | undefined):
|
|
|
100
78
|
* @param manifest - Parsed manifest object
|
|
101
79
|
* @returns The latest non-PENDING gate result, or `"PENDING"` when none found
|
|
102
80
|
*/
|
|
103
|
-
export declare function latestGateResult(manifest:
|
|
81
|
+
export declare function latestGateResult(manifest: Manifest): string;
|
|
104
82
|
/**
|
|
105
83
|
* Expand an `artifacts` entry from {@link ArtifactSection} into a list of
|
|
106
84
|
* concrete artifact paths. Exact paths are kept as-is; directory prefixes
|
|
@@ -174,7 +152,7 @@ export declare function renderAnalysisIndex(included: readonly IncludedArtifact[
|
|
|
174
152
|
*/
|
|
175
153
|
export declare function renderReaderIntelligenceGuide(sections: readonly TocSection[], included: readonly IncludedArtifact[]): string;
|
|
176
154
|
/**
|
|
177
|
-
* Resolve the article-type slug from a manifest, tolerating
|
|
155
|
+
* Resolve the article-type slug from a manifest, tolerating historic schemas.
|
|
178
156
|
*
|
|
179
157
|
* Thin re-export of {@link _resolveArticleType} from
|
|
180
158
|
* `aggregator/manifest/index.js`. Resolution order: `articleType` →
|
|
@@ -183,7 +161,7 @@ export declare function renderReaderIntelligenceGuide(sections: readonly TocSect
|
|
|
183
161
|
* @param manifest - Parsed manifest (any of the supported schemas)
|
|
184
162
|
* @returns Article-type slug usable as a filename component
|
|
185
163
|
*/
|
|
186
|
-
export declare function resolveArticleTypeFromManifest(manifest:
|
|
164
|
+
export declare function resolveArticleTypeFromManifest(manifest: Manifest): string;
|
|
187
165
|
/**
|
|
188
166
|
* Read, clean, and concatenate every artifact declared by the run's manifest
|
|
189
167
|
* (with discovery fallback when manifest.files is missing), returning a
|
|
@@ -141,7 +141,7 @@ function collectRunArtifacts(runDir) {
|
|
|
141
141
|
const full = path.join(dir, entry.name);
|
|
142
142
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
143
143
|
if (entry.isDirectory()) {
|
|
144
|
-
// Skip raw payloads,
|
|
144
|
+
// Skip raw payloads, prior-run snapshots, and Pass-1 work-in-progress
|
|
145
145
|
// snapshots so they are not rendered as supplementary artifacts.
|
|
146
146
|
if (entry.name === 'data' || entry.name === 'runs' || entry.name === 'pass1')
|
|
147
147
|
continue;
|
|
@@ -448,7 +448,7 @@ function appendSection(runDir, runDirRelPath, sectionId, sectionTitle, paths, se
|
|
|
448
448
|
sectionMarkdown.push('');
|
|
449
449
|
}
|
|
450
450
|
/**
|
|
451
|
-
* Resolve the article-type slug from a manifest, tolerating
|
|
451
|
+
* Resolve the article-type slug from a manifest, tolerating historic schemas.
|
|
452
452
|
*
|
|
453
453
|
* Thin re-export of {@link _resolveArticleType} from
|
|
454
454
|
* `aggregator/manifest/index.js`. Resolution order: `articleType` →
|
|
@@ -102,11 +102,11 @@ export declare function extractDefaultDescription(markdown: string): string;
|
|
|
102
102
|
* @returns Summary of the generated artefacts ({@link GenerateResult})
|
|
103
103
|
*/
|
|
104
104
|
export declare function generateArticle(opts: CliOptions, runSuffix?: string, articleCountOverride?: number): GenerateResult;
|
|
105
|
-
/** Candidate run discovered under `analysis/daily/`. */
|
|
106
105
|
/**
|
|
107
106
|
* One run discovered by {@link discoverAnalysisRuns}.
|
|
108
107
|
*
|
|
109
|
-
*
|
|
108
|
+
* Thin re-export of {@link _DiscoveredRun} from `aggregator/runs/index.js`,
|
|
109
|
+
* preserved here as the public type for `article-generator` consumers.
|
|
110
110
|
*/
|
|
111
111
|
export type DiscoveredRun = _DiscoveredRun;
|
|
112
112
|
/**
|
|
@@ -261,7 +261,7 @@ const FALLBACK_DESCRIPTION = 'EU Parliament intelligence summary derived from co
|
|
|
261
261
|
*/
|
|
262
262
|
export function extractDefaultDescription(markdown) {
|
|
263
263
|
// Suppress unused warning: keep `shouldSkipDescriptionLine` for any
|
|
264
|
-
//
|
|
264
|
+
// historic consumer importing it transitively.
|
|
265
265
|
void shouldSkipDescriptionLine;
|
|
266
266
|
const strong = extractStrongProseLine(markdown);
|
|
267
267
|
return strong.length > 0 ? strong : FALLBACK_DESCRIPTION;
|
|
@@ -8,9 +8,9 @@ export interface ResolvedMetadataEntry {
|
|
|
8
8
|
export type ResolvedMetadata = LanguageMap<ResolvedMetadataEntry>;
|
|
9
9
|
/**
|
|
10
10
|
* Raw manifest subset consumed by the resolver. Deliberately narrower
|
|
11
|
-
* than the full {@link
|
|
12
|
-
* usable for backport (which only has the manifest in
|
|
13
|
-
* callers that don't need the full typed structure.
|
|
11
|
+
* than the full {@link import('./manifest/types.js').Manifest} shape so
|
|
12
|
+
* the resolver stays usable for backport (which only has the manifest in
|
|
13
|
+
* text form) and for callers that don't need the full typed structure.
|
|
14
14
|
*/
|
|
15
15
|
export interface MetadataManifest {
|
|
16
16
|
readonly articleType?: string;
|
|
@@ -40,7 +40,7 @@ export interface ResolveMetadataOptions {
|
|
|
40
40
|
readonly date: string;
|
|
41
41
|
/** Aggregated Markdown document body (after provenance/header). */
|
|
42
42
|
readonly markdown: string;
|
|
43
|
-
/** Parsed analysis manifest (may be empty for
|
|
43
|
+
/** Parsed analysis manifest (may be empty for historic/backport callers). */
|
|
44
44
|
readonly manifest?: MetadataManifest;
|
|
45
45
|
/**
|
|
46
46
|
* Absolute path to the analysis run directory so the resolver can
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 3. **Aggregated-markdown H1** — the first `# …` heading in the aggregator
|
|
25
25
|
* output, accepted under the same non-generic rule. In practice this
|
|
26
26
|
* tier rarely fires because the aggregator itself writes the generic
|
|
27
|
-
* default, but it covers hand-edited or
|
|
27
|
+
* default, but it covers hand-edited or historic aggregates.
|
|
28
28
|
* 4. **First strong prose paragraph** — the first line of the aggregated
|
|
29
29
|
* Markdown that survives {@link shouldSkipDescriptionLine}. Used for
|
|
30
30
|
* `description`; also used for `title` as a last editorial-content
|
|
@@ -313,7 +313,7 @@ export function isGenericHeading(heading, articleType, date) {
|
|
|
313
313
|
`${human} ${date}`,
|
|
314
314
|
];
|
|
315
315
|
// Also accept the collision-suffix pattern (e.g. `Breaking Breaking — …`)
|
|
316
|
-
// and the auto-generated "EU Parliament <Type> — <date>"
|
|
316
|
+
// and the auto-generated "EU Parliament <Type> — <date>" historic form.
|
|
317
317
|
const humanRedundant = `${human} ${human}`;
|
|
318
318
|
for (const p of patterns) {
|
|
319
319
|
if (normalized === p)
|
|
@@ -39,7 +39,7 @@ export declare const HELP_TEXT: string;
|
|
|
39
39
|
* - `{kind:'options', value}` — argv parsed cleanly; `value` is ready to
|
|
40
40
|
* pass to `generateArticle` / `generateAllArticles`.
|
|
41
41
|
*
|
|
42
|
-
* Compared to the
|
|
42
|
+
* Compared to the original `parseCliArgs` in `article-generator.ts` (which
|
|
43
43
|
* throws and calls `process.exit` on `--help`), this entry point keeps
|
|
44
44
|
* tests self-contained.
|
|
45
45
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @module Aggregator/Cli/Parse
|
|
5
5
|
* @description Pure CLI parser that returns a discriminated union instead
|
|
6
|
-
* of calling `process.exit` mid-parse. The
|
|
6
|
+
* of calling `process.exit` mid-parse. The original `parseCliArgs` entry
|
|
7
7
|
* point in `article-generator.ts` is preserved for backward compatibility
|
|
8
8
|
* with existing callers and tests; new callers and unit tests should
|
|
9
9
|
* prefer {@link parseCliArgsSafe} so the `--help` and error branches are
|
|
@@ -202,7 +202,7 @@ function processArgvToken(argv, index, acc) {
|
|
|
202
202
|
* - `{kind:'options', value}` — argv parsed cleanly; `value` is ready to
|
|
203
203
|
* pass to `generateArticle` / `generateAllArticles`.
|
|
204
204
|
*
|
|
205
|
-
* Compared to the
|
|
205
|
+
* Compared to the original `parseCliArgs` in `article-generator.ts` (which
|
|
206
206
|
* throws and calls `process.exit` on `--help`), this entry point keeps
|
|
207
207
|
* tests self-contained.
|
|
208
208
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `githubRawUrl`) and `article-generator.ts` (which embedded the same slug
|
|
8
8
|
* literally inside an `isBasedOn` template string).
|
|
9
9
|
*
|
|
10
|
-
* Every consumer should import from here; the
|
|
10
|
+
* Every consumer should import from here; the original entry points in
|
|
11
11
|
* `clean-artifact.ts` are preserved as thin re-export shims for back-compat.
|
|
12
12
|
*/
|
|
13
13
|
/** Hack23 repo slug used when building blob/raw/tree URLs. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `githubRawUrl`) and `article-generator.ts` (which embedded the same slug
|
|
10
10
|
* literally inside an `isBasedOn` template string).
|
|
11
11
|
*
|
|
12
|
-
* Every consumer should import from here; the
|
|
12
|
+
* Every consumer should import from here; the original entry points in
|
|
13
13
|
* `clean-artifact.ts` are preserved as thin re-export shims for back-compat.
|
|
14
14
|
*/
|
|
15
15
|
/** Hack23 repo slug used when building blob/raw/tree URLs. */
|
|
@@ -10,12 +10,12 @@ import type { Manifest, ManifestFiles } from './types.js';
|
|
|
10
10
|
/** Sentinel used when no schema variant supplies a usable article type. */
|
|
11
11
|
export declare const UNKNOWN_ARTICLE_TYPE = "unknown";
|
|
12
12
|
/**
|
|
13
|
-
* Resolve the article-type slug from a manifest, tolerating
|
|
13
|
+
* Resolve the article-type slug from a manifest, tolerating historic schemas.
|
|
14
14
|
*
|
|
15
15
|
* Resolution order (highest precedence first):
|
|
16
16
|
* 1. `articleType` — canonical singular field
|
|
17
17
|
* 2. `articleTypes[0]` — pre-aggregator-pipeline plural array
|
|
18
|
-
* 3. `runType` —
|
|
18
|
+
* 3. `runType` — historic field on older breaking-run manifests
|
|
19
19
|
*
|
|
20
20
|
* Falls back to `'unknown'` when none of the above is a non-empty string.
|
|
21
21
|
*
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
/** Sentinel used when no schema variant supplies a usable article type. */
|
|
4
4
|
export const UNKNOWN_ARTICLE_TYPE = 'unknown';
|
|
5
5
|
/**
|
|
6
|
-
* Resolve the article-type slug from a manifest, tolerating
|
|
6
|
+
* Resolve the article-type slug from a manifest, tolerating historic schemas.
|
|
7
7
|
*
|
|
8
8
|
* Resolution order (highest precedence first):
|
|
9
9
|
* 1. `articleType` — canonical singular field
|
|
10
10
|
* 2. `articleTypes[0]` — pre-aggregator-pipeline plural array
|
|
11
|
-
* 3. `runType` —
|
|
11
|
+
* 3. `runType` — historic field on older breaking-run manifests
|
|
12
12
|
*
|
|
13
13
|
* Falls back to `'unknown'` when none of the above is a non-empty string.
|
|
14
14
|
*
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @module Aggregator/Manifest/Types
|
|
3
3
|
* @description Canonical manifest schema for analysis runs and the narrower
|
|
4
4
|
* projection consumed by the editorial-metadata resolver. Centralises every
|
|
5
|
-
* historic schema variant (canonical `articleType`,
|
|
6
|
-
* `articleTypes[]`,
|
|
5
|
+
* historic schema variant (canonical `articleType`, plural
|
|
6
|
+
* `articleTypes[]`, original `runType`) into one type that downstream
|
|
7
7
|
* modules can read against.
|
|
8
8
|
*/
|
|
9
9
|
import type { LanguageCode } from '../../types/index.js';
|
|
@@ -28,20 +28,22 @@ export type ManifestMetadataOverride = string | Partial<Record<LanguageCode, str
|
|
|
28
28
|
/**
|
|
29
29
|
* Raw manifest shape as committed by the analysis pipeline. Matches every
|
|
30
30
|
* schema variant the pipeline has ever emitted; readers consult
|
|
31
|
-
* {@link resolveArticleType} rather than `articleType` directly so
|
|
31
|
+
* {@link resolveArticleType} rather than `articleType` directly so historic
|
|
32
32
|
* runs stay readable.
|
|
33
33
|
*/
|
|
34
34
|
export interface Manifest {
|
|
35
35
|
/** Canonical singular form (current pipeline). */
|
|
36
36
|
readonly articleType?: string;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
* When present, `articleTypes[0]` is treated as the
|
|
38
|
+
* Plural form emitted by some pre-aggregator-pipeline workflows (historic
|
|
39
|
+
* schema variant). When present, `articleTypes[0]` is treated as the
|
|
40
|
+
* article type.
|
|
40
41
|
*/
|
|
41
42
|
readonly articleTypes?: readonly string[];
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
-
* fallback when neither `articleType` nor `articleTypes`
|
|
44
|
+
* Original field on older breaking-run manifests (historic schema variant).
|
|
45
|
+
* Used as the last fallback when neither `articleType` nor `articleTypes`
|
|
46
|
+
* is present.
|
|
45
47
|
*/
|
|
46
48
|
readonly runType?: string;
|
|
47
49
|
/** Stable run identifier; falls back to the run-dir basename. */
|
|
@@ -65,7 +67,7 @@ export interface Manifest {
|
|
|
65
67
|
* Narrower manifest projection consumed by {@link resolveArticleMetadata}
|
|
66
68
|
* in `aggregator/article-metadata.ts`. The metadata resolver only needs a
|
|
67
69
|
* subset; keeping this projection separate means string-only callers
|
|
68
|
-
* (backport,
|
|
70
|
+
* (backport, historic curators) don't have to construct a full {@link Manifest}.
|
|
69
71
|
*/
|
|
70
72
|
export interface MetadataManifest {
|
|
71
73
|
readonly articleType?: string;
|
|
@@ -34,7 +34,7 @@ export declare function readRunCandidate(runDir: string): DiscoveredRun | null;
|
|
|
34
34
|
* The walk stops descending into a directory the moment it sees a
|
|
35
35
|
* `manifest.json`, so nested artifact subdirectories never get reported
|
|
36
36
|
* as separate runs. Results are sorted by date ascending then by path
|
|
37
|
-
* lexically — the same order used by the
|
|
37
|
+
* lexically — the same order used by the previous in-line implementation in
|
|
38
38
|
* `article-generator.ts`.
|
|
39
39
|
*
|
|
40
40
|
* @param repoRoot - Absolute repository root
|
|
@@ -52,7 +52,7 @@ export function readRunCandidate(runDir) {
|
|
|
52
52
|
* The walk stops descending into a directory the moment it sees a
|
|
53
53
|
* `manifest.json`, so nested artifact subdirectories never get reported
|
|
54
54
|
* as separate runs. Results are sorted by date ascending then by path
|
|
55
|
-
* lexically — the same order used by the
|
|
55
|
+
* lexically — the same order used by the previous in-line implementation in
|
|
56
56
|
* `article-generator.ts`.
|
|
57
57
|
*
|
|
58
58
|
* @param repoRoot - Absolute repository root
|
|
@@ -388,7 +388,7 @@ function extractBodyFirstProse(articleHtml) {
|
|
|
388
388
|
* 1. If a manifest.json for the run exists (aggregator cohort), use the
|
|
389
389
|
* full {@link resolveArticleMetadata} pipeline — this picks up manifest
|
|
390
390
|
* overrides and artefact H1s.
|
|
391
|
-
* 2. Otherwise (
|
|
391
|
+
* 2. Otherwise (historic cohort), derive from the rendered body:
|
|
392
392
|
* - Title = non-generic `<h1>` from the body, else first sentence of
|
|
393
393
|
* the first strong prose paragraph.
|
|
394
394
|
* - Description = first strong prose paragraph (full, not the same
|
|
@@ -412,7 +412,7 @@ function deriveMetadataForFile(file, html) {
|
|
|
412
412
|
// fallback for `committee-reports` renders realistic abbreviations
|
|
413
413
|
// (`ENVI, ECON, AFET, LIBE, AGRI`) instead of the placeholder
|
|
414
414
|
// `Main Committees`. This keeps the localized template consistent
|
|
415
|
-
// with the
|
|
415
|
+
// with the historic format even when the manifest is missing.
|
|
416
416
|
const committee = extractCommitteeCodes(bodyH1) || extractCommitteeCodes(bodyProse);
|
|
417
417
|
|
|
418
418
|
const resolved = resolveArticleMetadata({
|
|
@@ -425,7 +425,7 @@ function deriveMetadataForFile(file, html) {
|
|
|
425
425
|
|
|
426
426
|
if (file.lang !== 'en') {
|
|
427
427
|
// NON-ENGLISH files: The article body may be in a different language
|
|
428
|
-
// than the file claims to be —
|
|
428
|
+
// than the file claims to be — historic files have localized H1/chrome
|
|
429
429
|
// but English body prose; aggregator PR#1404 files have English H1
|
|
430
430
|
// AND English body in every language variant. We accept body content
|
|
431
431
|
// only when it is plausibly in the file's language, and fall back to
|
|
@@ -658,9 +658,9 @@ function buildSyntheticMarkdown(h1, prose) {
|
|
|
658
658
|
|
|
659
659
|
/**
|
|
660
660
|
* Choose the final title text. Prefers a non-generic body H1. When the
|
|
661
|
-
* H1 is generic (e.g.
|
|
661
|
+
* H1 is generic (e.g. historic "Legislative Procedures: European Parliament
|
|
662
662
|
* Monitor"), falls back to the first sentence of body prose — this is
|
|
663
|
-
* the single biggest SEO win for
|
|
663
|
+
* the single biggest SEO win for historic files.
|
|
664
664
|
*
|
|
665
665
|
* @param {string} bodyH1 - First H1 from the body
|
|
666
666
|
* @param {string} bodyProse - First strong prose paragraph
|
|
@@ -681,7 +681,7 @@ function chooseTitle(bodyH1, bodyProse, templateTitle, file) {
|
|
|
681
681
|
|
|
682
682
|
/**
|
|
683
683
|
* Extend {@link isGenericHeading} with a few extra patterns specific to
|
|
684
|
-
*
|
|
684
|
+
* historic-era titles (pre-aggregator pipeline) so those files get
|
|
685
685
|
* replaced during backport. Also catches the pure `<Title-Case-Phrase>
|
|
686
686
|
* — <ISO-date>` form that the default aggregator title emits when the
|
|
687
687
|
* articleType slug has a run suffix (e.g. `breaking-190`) that
|
|
@@ -695,7 +695,7 @@ function chooseTitle(bodyH1, bodyProse, templateTitle, file) {
|
|
|
695
695
|
function isGenericBodyH1(h1, articleType, date) {
|
|
696
696
|
if (isGenericHeading(h1, articleType, date)) return true;
|
|
697
697
|
const normalized = h1.trim();
|
|
698
|
-
const
|
|
698
|
+
const historicTemplates = [
|
|
699
699
|
'Legislative Procedures: European Parliament Monitor',
|
|
700
700
|
'EU Parliament Committee Activity Report',
|
|
701
701
|
'EU Parliament Breaking',
|
|
@@ -703,7 +703,7 @@ function isGenericBodyH1(h1, articleType, date) {
|
|
|
703
703
|
'Plenary Votes & Resolutions',
|
|
704
704
|
'Plenary Votes and Resolutions',
|
|
705
705
|
];
|
|
706
|
-
for (const t of
|
|
706
|
+
for (const t of historicTemplates) {
|
|
707
707
|
if (normalized === t || normalized.startsWith(`${t} `) || normalized.startsWith(`${t}:`)) {
|
|
708
708
|
return true;
|
|
709
709
|
}
|
|
@@ -814,7 +814,7 @@ function rewriteHtml(html, metadata) {
|
|
|
814
814
|
/**
|
|
815
815
|
* Replace `<meta name="<name>" content="…">` in-place. When absent the
|
|
816
816
|
* document is returned unchanged — we never inject new tags during
|
|
817
|
-
* backport so
|
|
817
|
+
* backport so historic files retain their original meta-tag order.
|
|
818
818
|
*
|
|
819
819
|
* The `content` match is quote-aware: the content of a double-quoted
|
|
820
820
|
* attribute value may contain apostrophes (e.g. `Parliament's`), so the
|
|
@@ -27,7 +27,7 @@ export declare const AI_MARKER = "[AI_ANALYSIS_REQUIRED]";
|
|
|
27
27
|
*
|
|
28
28
|
* Recognises three marker formats:
|
|
29
29
|
* - `[AI_ANALYSIS_REQUIRED]` — the current standard marker (v3.0+)
|
|
30
|
-
* - `[REQUIRED]` —
|
|
30
|
+
* - `[REQUIRED]` — historic marker used in template stubs before v3.0
|
|
31
31
|
* - `[?]` — shorthand used in some early methodology templates
|
|
32
32
|
*
|
|
33
33
|
* @param text - Text to test
|
|
@@ -29,7 +29,7 @@ export const AI_MARKER = '[AI_ANALYSIS_REQUIRED]';
|
|
|
29
29
|
*
|
|
30
30
|
* Recognises three marker formats:
|
|
31
31
|
* - `[AI_ANALYSIS_REQUIRED]` — the current standard marker (v3.0+)
|
|
32
|
-
* - `[REQUIRED]` —
|
|
32
|
+
* - `[REQUIRED]` — historic marker used in template stubs before v3.0
|
|
33
33
|
* - `[?]` — shorthand used in some early methodology templates
|
|
34
34
|
*
|
|
35
35
|
* @param text - Text to test
|
|
@@ -25,8 +25,10 @@ import type { LanguageCode } from '../types/index.js';
|
|
|
25
25
|
/** Per-language text overlay keyed by 2-letter language code. */
|
|
26
26
|
export type TextI18n = Partial<Record<LanguageCode, string>>;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Back-compat alias for {@link TextI18n}. Preserved so downstream
|
|
29
|
+
* TypeScript consumers that import this name from
|
|
30
|
+
* `euparliamentmonitor/generators/political-intelligence-descriptions`
|
|
31
|
+
* keep compiling. Prefer `TextI18n` for new code.
|
|
30
32
|
*/
|
|
31
33
|
export type DescriptionI18n = TextI18n;
|
|
32
34
|
/** One curated entry for a methodology / template / reference file. */
|
|
@@ -122,7 +124,7 @@ export declare function hasCuratedTitle(relPath: string): boolean;
|
|
|
122
124
|
* this is where all 14-language localization is maintained)
|
|
123
125
|
* 2. Curated English title from {@link CURATED_TITLES} (`.en` overlay)
|
|
124
126
|
* 3. Per-entry `titleI18n[lang]` on a `CURATED_DESCRIPTIONS` entry
|
|
125
|
-
* (
|
|
127
|
+
* (historic path; retained so future entries can colocate title + desc)
|
|
126
128
|
* 4. Per-entry `title` on a `CURATED_DESCRIPTIONS` entry
|
|
127
129
|
* 5. `fallback` — the H1-extracted title from the source Markdown
|
|
128
130
|
*
|
|
@@ -2047,7 +2047,7 @@ export function hasCuratedTitle(relPath) {
|
|
|
2047
2047
|
* this is where all 14-language localization is maintained)
|
|
2048
2048
|
* 2. Curated English title from {@link CURATED_TITLES} (`.en` overlay)
|
|
2049
2049
|
* 3. Per-entry `titleI18n[lang]` on a `CURATED_DESCRIPTIONS` entry
|
|
2050
|
-
* (
|
|
2050
|
+
* (historic path; retained so future entries can colocate title + desc)
|
|
2051
2051
|
* 4. Per-entry `title` on a `CURATED_DESCRIPTIONS` entry
|
|
2052
2052
|
* 5. `fallback` — the H1-extracted title from the source Markdown
|
|
2053
2053
|
*
|
|
@@ -2073,7 +2073,7 @@ export function getCuratedTitle(relPath, lang, fallback) {
|
|
|
2073
2073
|
if (titleEntry.en)
|
|
2074
2074
|
return titleEntry.en;
|
|
2075
2075
|
}
|
|
2076
|
-
// 3 + 4:
|
|
2076
|
+
// 3 + 4: historic colocated title on CURATED_DESCRIPTIONS entry
|
|
2077
2077
|
// eslint-disable-next-line security/detect-object-injection
|
|
2078
2078
|
const descEntry = CURATED_DESCRIPTIONS[key];
|
|
2079
2079
|
if (descEntry) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* future renderer (Atom-feed metadata, OG-card builder, etc.) without
|
|
14
14
|
* having to import `sitemap.ts` and pull in the entire CLI surface.
|
|
15
15
|
*
|
|
16
|
-
* Output is byte-identical to the
|
|
16
|
+
* Output is byte-identical to the previous in-line implementation that
|
|
17
17
|
* lived in `sitemap.ts` between Apr-2026 and the bounded-context
|
|
18
18
|
* refactor — verified by the regression test in
|
|
19
19
|
* `test/unit/sitemap-byte-equality.test.js` (compares against the
|
|
@@ -37,6 +37,7 @@ export function generateRssFeed(articleInfos, buildDate = new Date().toUTCString
|
|
|
37
37
|
<dc:language>${escapeXML(item.lang)}</dc:language>
|
|
38
38
|
</item>`)
|
|
39
39
|
.join('\n');
|
|
40
|
+
// REUSE-IgnoreStart
|
|
40
41
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
41
42
|
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
|
|
42
43
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
@@ -51,5 +52,6 @@ export function generateRssFeed(articleInfos, buildDate = new Date().toUTCString
|
|
|
51
52
|
${items}
|
|
52
53
|
</channel>
|
|
53
54
|
</rss>`;
|
|
55
|
+
// REUSE-IgnoreEnd
|
|
54
56
|
}
|
|
55
57
|
//# sourceMappingURL=rss.js.map
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* pure (no HTML chrome dependencies), and so future XML output formats
|
|
15
15
|
* (news-sitemap, video-sitemap) can reuse the same URL builders.
|
|
16
16
|
*
|
|
17
|
-
* Output is byte-identical to the
|
|
17
|
+
* Output is byte-identical to the previous in-line implementation that
|
|
18
18
|
* lived in `sitemap.ts`, verified by the byte-equality regression test.
|
|
19
19
|
*/
|
|
20
20
|
import fs from 'fs';
|
|
@@ -86,12 +86,14 @@ export function generateSitemap(articles, docsFiles = []) {
|
|
|
86
86
|
...buildArticleUrls(articles),
|
|
87
87
|
...buildDocsUrls(docsFiles, today),
|
|
88
88
|
];
|
|
89
|
+
// REUSE-IgnoreStart
|
|
89
90
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
90
91
|
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
|
|
91
92
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
92
93
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
93
94
|
${urls.map(renderSitemapUrl).join('\n')}
|
|
94
95
|
</urlset>`;
|
|
96
|
+
// REUSE-IgnoreEnd
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Build the absolute URL for a language-specific index page.
|
package/scripts/lint-prompts.js
CHANGED
|
@@ -96,6 +96,25 @@ const FORBIDDEN_PHRASES = [
|
|
|
96
96
|
/\bnews-<type>-analysis\.md\b/i,
|
|
97
97
|
/\bnews-<type>-article\.md\b/i,
|
|
98
98
|
/\bgenerate-news\b(?!-indexes\b)/i,
|
|
99
|
+
// IMF-primary editorial policy: IMF is the SOLE authoritative source
|
|
100
|
+
// for every economic / fiscal / monetary / trade / FDI / exchange-rate /
|
|
101
|
+
// banking-soundness claim. World Bank is for non-economic domains.
|
|
102
|
+
// Workflow prompts must use IMF for economic context. We catch the
|
|
103
|
+
// forbidden listings:
|
|
104
|
+
//
|
|
105
|
+
// - "World Bank **or** IMF" (bold or plain)
|
|
106
|
+
// - "IMF **or** World Bank" (bold or plain)
|
|
107
|
+
// - "WB or IMF" / "IMF or WB"
|
|
108
|
+
// - "WB/IMF" / "IMF/WB" inside an economic context phrase
|
|
109
|
+
//
|
|
110
|
+
// See .github/skills/imf-data-integration.md and
|
|
111
|
+
// analysis/methodologies/imf-indicator-mapping.md §8.
|
|
112
|
+
/\bWorld\s+Bank\s+(?:\*\*)?or(?:\*\*)?\s+IMF\b/i,
|
|
113
|
+
/\bIMF\s+(?:\*\*)?or(?:\*\*)?\s+World\s+Bank\b/i,
|
|
114
|
+
/\bWB\s+or\s+IMF\b/i,
|
|
115
|
+
/\bIMF\s+or\s+WB\b/i,
|
|
116
|
+
/economic\s+context[^.\n]{0,40}\bWB\s*\/\s*IMF\b/i,
|
|
117
|
+
/economic\s+context[^.\n]{0,40}\bIMF\s*\/\s*WB\b/i,
|
|
99
118
|
// Note: the AI_MARKER / [AI_ANALYSIS_REQUIRED] / FALLBACK_TEMPLATE_PATTERNS
|
|
100
119
|
// string tokens are NOT banned — workflow prompts legitimately instruct the
|
|
101
120
|
// agent "no [AI_ANALYSIS_REQUIRED] markers may remain in committed
|
|
@@ -27,7 +27,7 @@ export declare const EP_MCP_TOOLS: readonly string[];
|
|
|
27
27
|
* Hack23/European-Parliament-MCP-Server#301 and extended to
|
|
28
28
|
* `get_events_feed`/`get_procedures_feed` by
|
|
29
29
|
* Hack23/European-Parliament-MCP-Server#380 (which closed #378).
|
|
30
|
-
* 2. **
|
|
30
|
+
* 2. **Pre-v1.2.13 raw upstream 404 shape** (historically emitted pre-v1.2.13 by
|
|
31
31
|
* `get_events_feed` / `get_procedures_feed`, fixed upstream in PR #380) —
|
|
32
32
|
* `{"@id":"https://data.europarl.europa.eu/eli/dl/...","error":"404 N..."}`.
|
|
33
33
|
* Retained purely as defense-in-depth for older pinned server versions or
|
|
@@ -194,7 +194,7 @@ function _parseResultPayload(result) {
|
|
|
194
194
|
* Hack23/European-Parliament-MCP-Server#301 and extended to
|
|
195
195
|
* `get_events_feed`/`get_procedures_feed` by
|
|
196
196
|
* Hack23/European-Parliament-MCP-Server#380 (which closed #378).
|
|
197
|
-
* 2. **
|
|
197
|
+
* 2. **Pre-v1.2.13 raw upstream 404 shape** (historically emitted pre-v1.2.13 by
|
|
198
198
|
* `get_events_feed` / `get_procedures_feed`, fixed upstream in PR #380) —
|
|
199
199
|
* `{"@id":"https://data.europarl.europa.eu/eli/dl/...","error":"404 N..."}`.
|
|
200
200
|
* Retained purely as defense-in-depth for older pinned server versions or
|
|
@@ -214,7 +214,7 @@ export function isFeedUnavailable(result) {
|
|
|
214
214
|
// Shape 1 — uniform {status:"unavailable"} envelope (#301 / #380).
|
|
215
215
|
if (envelope['status'] === 'unavailable')
|
|
216
216
|
return true;
|
|
217
|
-
// Shape 2 —
|
|
217
|
+
// Shape 2 — pre-v1.2.13 raw upstream 404 leak (historically pre-v1.2.13, #378).
|
|
218
218
|
const error = envelope['error'];
|
|
219
219
|
const idField = envelope['@id'];
|
|
220
220
|
if (typeof error === 'string' &&
|
|
@@ -404,7 +404,7 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
|
|
|
404
404
|
return this._recordToolFailure(toolName, result.content?.[0]?.text ?? '', fallbackText);
|
|
405
405
|
}
|
|
406
406
|
// Detect the unavailable-feed envelope — uniform `{status:"unavailable"}`
|
|
407
|
-
// (all feeds as of v1.2.13, #301/#380) as well as the
|
|
407
|
+
// (all feeds as of v1.2.13, #301/#380) as well as the pre-v1.2.13 raw upstream
|
|
408
408
|
// 404 shape `{"@id":..., "error":"404 ..."}` that pre-v1.2.13
|
|
409
409
|
// get_events_feed / get_procedures_feed emitted
|
|
410
410
|
// (Hack23/European-Parliament-MCP-Server#378, closed by PR #380). The
|
|
@@ -1269,7 +1269,7 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
|
|
|
1269
1269
|
this._slowFeedWarnings.delete('get_events_feed');
|
|
1270
1270
|
return this._recordToolFailure('get_events_feed', result.content?.[0]?.text ?? '', EuropeanParliamentMCPClient.FEED_FALLBACK);
|
|
1271
1271
|
}
|
|
1272
|
-
// Detect unavailable-feed envelope (uniform {status:"unavailable"} or
|
|
1272
|
+
// Detect unavailable-feed envelope (uniform {status:"unavailable"} or pre-v1.2.13 404)
|
|
1273
1273
|
if (isFeedUnavailable(result)) {
|
|
1274
1274
|
this._slowFeedWarnings.delete('get_events_feed');
|
|
1275
1275
|
return this._recordToolFailure('get_events_feed', `UPSTREAM_404: ${result.content?.[0]?.text?.slice(0, 200) ?? 'feed unavailable'}`, EuropeanParliamentMCPClient.FEED_FALLBACK);
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* `https://dataservices.imf.org/REST/SDMX_3.0`).
|
|
45
45
|
* - `IMF_API_TIMEOUT_MS` — per-request timeout (default `30000`).
|
|
46
46
|
*
|
|
47
|
-
*
|
|
47
|
+
* Historic env vars (`IMF_MCP_GATEWAY_URL`, `IMF_MCP_GATEWAY_API_KEY`,
|
|
48
48
|
* `IMF_MCP_SERVER_PATH`) are no longer consulted — no gateway is needed
|
|
49
49
|
* because the IMF SDMX 3.0 API is an unauthenticated public endpoint.
|
|
50
50
|
*/
|
|
@@ -114,7 +114,7 @@ export function isRetriableError(error) {
|
|
|
114
114
|
const msg = error.message?.toLowerCase() ?? '';
|
|
115
115
|
// Never retry rate-limit errors — callers must honour the Retry-After delay.
|
|
116
116
|
// `instanceof MCPRateLimitError` is the primary guard for typed errors;
|
|
117
|
-
// the string prefix fallback handles any
|
|
117
|
+
// the string prefix fallback handles any untyped plain Error with a rate-limit message.
|
|
118
118
|
if (error instanceof MCPRateLimitError || msg.startsWith(RATE_LIMIT_MSG.toLowerCase())) {
|
|
119
119
|
return false;
|
|
120
120
|
}
|
package/scripts/types/imf.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type { MCPClientOptions } from './mcp.js';
|
|
|
46
46
|
* - `timeoutMs` — per-request timeout in milliseconds.
|
|
47
47
|
* - `fetchImpl` — optional `fetch` injection for tests.
|
|
48
48
|
*
|
|
49
|
-
* The inherited
|
|
49
|
+
* The inherited historic MCP transport fields (`serverPath`, `gatewayUrl`,
|
|
50
50
|
* `gatewayApiKey`, `maxConnectionAttempts`, `connectionRetryDelay`) are
|
|
51
51
|
* accepted for backwards compatibility but **ignored** by the native
|
|
52
52
|
* client — they date from the earlier `c-cf/imf-data-mcp` proxy
|
|
@@ -346,7 +346,7 @@ export interface CorporateBodyFeedItem {
|
|
|
346
346
|
identifier?: string | undefined;
|
|
347
347
|
label?: string | undefined;
|
|
348
348
|
}
|
|
349
|
-
/** Aggregated feed data for breaking news articles (
|
|
349
|
+
/** Aggregated feed data for breaking news articles (back-compat shape) */
|
|
350
350
|
export interface BreakingNewsFeedData {
|
|
351
351
|
adoptedTexts: readonly AdoptedTextFeedItem[];
|
|
352
352
|
events: readonly EventFeedItem[];
|
|
@@ -129,7 +129,7 @@ export interface PolicyRelevantIndicators {
|
|
|
129
129
|
*
|
|
130
130
|
* **Note:** The codebase also has a `WorldBankMCPClient.getIndicatorForCountry()`
|
|
131
131
|
* wrapper (in `src/mcp/wb-mcp-client.ts`) that calls the `get_indicator_for_country`
|
|
132
|
-
* tool — this is a
|
|
132
|
+
* tool — this is a back-compat convenience method not listed in this type union since it
|
|
133
133
|
* is not part of the standard worldbank-mcp tool surface.
|
|
134
134
|
*/
|
|
135
135
|
export type WBMCPToolName = 'get-economic-data' | 'get-social-data' | 'get-health-data' | 'get-education-data' | 'get-country-info' | 'get-countries' | 'search-indicators';
|
|
@@ -198,7 +198,7 @@ export declare function checkArticleExists(slug: string, lang: string, newsDir?:
|
|
|
198
198
|
*
|
|
199
199
|
* Title resolution order:
|
|
200
200
|
* 1. `<head><title>` value with the trailing ` — EU Parliament Monitor`
|
|
201
|
-
* (or
|
|
201
|
+
* (or historic ` | EU Parliament Monitor`) site-suffix stripped.
|
|
202
202
|
* This is where the editorial-highlights resolver + SEO backport
|
|
203
203
|
* script write their output, so using it as the primary source
|
|
204
204
|
* surfaces the strongest headline on index cards and sitemaps.
|
|
@@ -243,7 +243,7 @@ export interface ArticleValidationResult {
|
|
|
243
243
|
* Validate that generated article HTML includes all required structural elements.
|
|
244
244
|
*
|
|
245
245
|
* This is the primary validation gate — articles must be generated correctly
|
|
246
|
-
* by the template. The fix-articles script is only a fallback for
|
|
246
|
+
* by the template. The fix-articles script is only a fallback for historic articles.
|
|
247
247
|
*
|
|
248
248
|
* @param html - Complete HTML string of the article
|
|
249
249
|
* @returns Validation result with errors list (empty if valid)
|
|
@@ -508,7 +508,7 @@ function decodeHtmlEntities(str) {
|
|
|
508
508
|
*
|
|
509
509
|
* Title resolution order:
|
|
510
510
|
* 1. `<head><title>` value with the trailing ` — EU Parliament Monitor`
|
|
511
|
-
* (or
|
|
511
|
+
* (or historic ` | EU Parliament Monitor`) site-suffix stripped.
|
|
512
512
|
* This is where the editorial-highlights resolver + SEO backport
|
|
513
513
|
* script write their output, so using it as the primary source
|
|
514
514
|
* surfaces the strongest headline on index cards and sitemaps.
|
|
@@ -609,7 +609,7 @@ const REQUIRED_ARTICLE_ELEMENTS = [
|
|
|
609
609
|
* Validate that generated article HTML includes all required structural elements.
|
|
610
610
|
*
|
|
611
611
|
* This is the primary validation gate — articles must be generated correctly
|
|
612
|
-
* by the template. The fix-articles script is only a fallback for
|
|
612
|
+
* by the template. The fix-articles script is only a fallback for historic articles.
|
|
613
613
|
*
|
|
614
614
|
* @param html - Complete HTML string of the article
|
|
615
615
|
* @returns Validation result with errors list (empty if valid)
|
|
@@ -96,6 +96,35 @@ const IMF_SOURCE_FIELD_RE =
|
|
|
96
96
|
const IMF_FIGURE_CLAIM_RE =
|
|
97
97
|
/\bIMF\b[\s\S]{0,160}\b\d+(?:\.\d+)?\s*(?:%|pp|percentage points|GDP|EUR|USD|billion|trillion|million)/i;
|
|
98
98
|
|
|
99
|
+
// IMF-primary editorial policy: IMF is the sole authoritative source for
|
|
100
|
+
// economic / fiscal / monetary / trade / FDI / exchange-rate /
|
|
101
|
+
// banking-soundness claims inside economic-context.md. World Bank is
|
|
102
|
+
// used for non-economic domains. Two complementary detectors:
|
|
103
|
+
//
|
|
104
|
+
// 1. WB economic indicator codes — surface the offending SDMX-style
|
|
105
|
+
// identifier when an economic-context artifact still cites raw WB
|
|
106
|
+
// economic series (NY.GDP.*, FP.CPI.*, SL.UEM.*, GC.DOD.*, NE.EXP.*,
|
|
107
|
+
// NE.TRD.*, BX.KLT.*, NY.GNP.*, GC.TAX.*, NE.CON.GOVT.*).
|
|
108
|
+
// 2. WB economic prose claim — match "World Bank" within 120 chars of an
|
|
109
|
+
// economic noun (GDP, inflation, unemployment, fiscal balance, debt,
|
|
110
|
+
// trade, FDI, exchange rate). The window is intentionally narrow so
|
|
111
|
+
// a sentence like "World Bank WGI governance index" (legitimate
|
|
112
|
+
// non-economic domain) does not trigger.
|
|
113
|
+
//
|
|
114
|
+
// Both detectors deliberately exclude the narrative "Retired from WB
|
|
115
|
+
// (now IMF-primary…)" and "legacy WB economic codes (… retained for
|
|
116
|
+
// backward compatibility but MUST NOT…)" wording that appears in the
|
|
117
|
+
// methodology files themselves — those files are not validated as run
|
|
118
|
+
// artifacts. The detectors only run against `intelligence/economic-
|
|
119
|
+
// context.md` and only when the artifact is also flagged as making
|
|
120
|
+
// numeric IMF claims (i.e. the artifact actually carries economic
|
|
121
|
+
// content), see callers in `evaluateArtifact`.
|
|
122
|
+
const WB_ECONOMIC_INDICATOR_CODE_RE =
|
|
123
|
+
/\b(NY\.GDP\.[A-Z0-9.]+|NY\.GNP\.[A-Z0-9.]+|FP\.CPI\.[A-Z0-9.]+|SL\.UEM\.[A-Z0-9.]+|GC\.DOD\.[A-Z0-9.]+|GC\.TAX\.[A-Z0-9.]+|NE\.EXP\.[A-Z0-9.]+|NE\.IMP\.[A-Z0-9.]+|NE\.TRD\.[A-Z0-9.]+|NE\.CON\.GOVT\.[A-Z0-9.]+|BX\.KLT\.[A-Z0-9.]+|BN\.KLT\.[A-Z0-9.]+|FR\.INR\.[A-Z0-9.]+)\b/;
|
|
124
|
+
|
|
125
|
+
const WB_ECONOMIC_CLAIM_RE =
|
|
126
|
+
/\bWorld\s+Bank\b[\s\S]{0,120}\b(?:GDP(?:\s+growth|\s+per\s+capita)?|inflation|CPI|unemployment(?:\s+rate)?|fiscal\s+balance|primary\s+balance|government\s+debt|public\s+debt|current\s+account|trade(?:\s+balance)?|FDI|foreign\s+direct\s+investment|exchange\s+rate|REER|policy\s+rate|reserve\s+assets|capital\s+adequacy|NPL\s+ratio)\b/i;
|
|
127
|
+
|
|
99
128
|
// Bypass placeholder scan only on template-instruction blocks themselves —
|
|
100
129
|
// NOT on every artifact that happens to link to a methodology document.
|
|
101
130
|
// Matching `methodology` here would suppress placeholder detection for any
|
|
@@ -285,6 +314,48 @@ function claimsImfFigures(content) {
|
|
|
285
314
|
return IMF_FIGURE_CLAIM_RE.test(content);
|
|
286
315
|
}
|
|
287
316
|
|
|
317
|
+
/**
|
|
318
|
+
* IMF-primary editorial policy.
|
|
319
|
+
*
|
|
320
|
+
* Detect WB economic-policy violations inside `intelligence/economic-
|
|
321
|
+
* context.md`:
|
|
322
|
+
*
|
|
323
|
+
* - Any WB economic indicator code (NY.GDP.*, FP.CPI.*, SL.UEM.*,
|
|
324
|
+
* GC.DOD.*, NE.EXP.*, NE.TRD.*, BX.KLT.*, NY.GNP.*, GC.TAX.*,
|
|
325
|
+
* NE.CON.GOVT.*, FR.INR.*) — these belong in IMF SDMX form (NGDP,
|
|
326
|
+
* PCPIPCH, LUR, GGXWDG_NGDP, BCA_NGDPD, …).
|
|
327
|
+
* - Any "World Bank … <economic noun>" prose claim within 120 chars
|
|
328
|
+
* (GDP, inflation, unemployment, fiscal balance, debt, trade, FDI,
|
|
329
|
+
* exchange rate, policy rate, banking soundness).
|
|
330
|
+
*
|
|
331
|
+
* The detector deliberately runs only on `intelligence/economic-
|
|
332
|
+
* context.md`. Other artifacts may legitimately reference WB for non-
|
|
333
|
+
* economic context (governance WGI, demographics, social, environment,
|
|
334
|
+
* defence-spending, agriculture, innovation, education, health) and
|
|
335
|
+
* the WB methodology files themselves describe legacy codes for
|
|
336
|
+
* backward-compatibility — neither path is validated here.
|
|
337
|
+
*
|
|
338
|
+
* Returns `{ codes, prose }` arrays of offending excerpts. Empty arrays
|
|
339
|
+
* mean clean.
|
|
340
|
+
*/
|
|
341
|
+
function detectWorldBankEconomicViolations(content) {
|
|
342
|
+
// Use matchAll() so callers get every offending excerpt, not just the
|
|
343
|
+
// first hit — an artifact that cites several WB economic series
|
|
344
|
+
// ("NY.GDP.MKTP.KD.ZG and FP.CPI.TOTL.ZG and SL.UEM.TOTL.ZS") must
|
|
345
|
+
// surface all three to the editor in a single Stage-C pass.
|
|
346
|
+
const codes = [];
|
|
347
|
+
const codeRe = new RegExp(WB_ECONOMIC_INDICATOR_CODE_RE.source, 'gi');
|
|
348
|
+
for (const m of content.matchAll(codeRe)) {
|
|
349
|
+
codes.push(m[1]);
|
|
350
|
+
}
|
|
351
|
+
const prose = [];
|
|
352
|
+
const proseRe = new RegExp(WB_ECONOMIC_CLAIM_RE.source, 'gi');
|
|
353
|
+
for (const m of content.matchAll(proseRe)) {
|
|
354
|
+
prose.push(m[0].replace(/\s+/g, ' ').trim().slice(0, 100));
|
|
355
|
+
}
|
|
356
|
+
return { codes, prose };
|
|
357
|
+
}
|
|
358
|
+
|
|
288
359
|
// Stage C IMF evidence gate. The probe always writes a summary JSON even
|
|
289
360
|
// when `available:false`, so a generic "any .json file" check is insufficient
|
|
290
361
|
// — it would let a failed probe satisfy the gate. Require:
|
|
@@ -476,6 +547,24 @@ function validateArtifact({
|
|
|
476
547
|
result.issues.push('imf-cache:missing');
|
|
477
548
|
}
|
|
478
549
|
}
|
|
550
|
+
// IMF-primary editorial policy: economic-context.md must not
|
|
551
|
+
// cite World Bank for economic claims regardless of whether IMF prose
|
|
552
|
+
// is also present. Run on every economic-context artifact (not gated
|
|
553
|
+
// on claimsImfFigures) so an article that drops IMF entirely and
|
|
554
|
+
// tries to satisfy economic context with WB alone is caught.
|
|
555
|
+
if (isEconomicContextArtifact(relativePath)) {
|
|
556
|
+
const { codes: wbCodes, prose: wbProse } =
|
|
557
|
+
detectWorldBankEconomicViolations(content);
|
|
558
|
+
// Surface every offending code (de-duplicated to keep the issue
|
|
559
|
+
// list concise when the same series is cited many times in one
|
|
560
|
+
// artifact). Stage-C editors get the full picture in one pass.
|
|
561
|
+
for (const code of [...new Set(wbCodes)]) {
|
|
562
|
+
result.issues.push(`economic-context:wb-economic-code:${code}`);
|
|
563
|
+
}
|
|
564
|
+
if (wbProse.length > 0) {
|
|
565
|
+
result.issues.push('economic-context:wb-economic-claim');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
479
568
|
|
|
480
569
|
return result;
|
|
481
570
|
}
|
package/scripts/index.old.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* @module euparliamentmonitor
|
|
5
|
-
* @description European Parliament Intelligence Platform — npm package entry point.
|
|
6
|
-
*
|
|
7
|
-
* This barrel re-exports the full library surface of the EU Parliament
|
|
8
|
-
* Monitor so that other projects can consume types, MCP clients, analysis
|
|
9
|
-
* utilities, templates, generators, strategies, and language constants
|
|
10
|
-
* without duplicating code.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* import {
|
|
15
|
-
* EuropeanParliamentMCPClient,
|
|
16
|
-
* getEPMCPClient,
|
|
17
|
-
* ALL_LANGUAGES,
|
|
18
|
-
* scoreVotingAnomaly,
|
|
19
|
-
* generateArticleHTML,
|
|
20
|
-
* BreakingNewsStrategy,
|
|
21
|
-
* buildBreakingNewsContent,
|
|
22
|
-
* } from 'euparliamentmonitor';
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @see {@link https://github.com/Hack23/euparliamentmonitor | GitHub Repository}
|
|
26
|
-
* @see {@link https://github.com/Hack23/ISMS-PUBLIC/blob/main/Secure_Development_Policy.md | Secure Development Policy}
|
|
27
|
-
* @see {@link https://github.com/Hack23/euparliamentmonitor/blob/main/ARCHITECTURE.md | Architecture}
|
|
28
|
-
* @see {@link https://github.com/Hack23/euparliamentmonitor/blob/main/SECURITY_ARCHITECTURE.md | Security Architecture}
|
|
29
|
-
*
|
|
30
|
-
* **Dead-code removal (v0.8.27+):** The following previously-exported symbols
|
|
31
|
-
* were removed as unused dead code: `buildSankeySection`,
|
|
32
|
-
* `sankey-content`, `buildMindmapSection`,
|
|
33
|
-
* `buildMultiDimensionalSwotSection`, `build*MultiDimensionalSwot`
|
|
34
|
-
* builders, `MULTI_DIMENSIONAL_SWOT_STRINGS`, and related visualization
|
|
35
|
-
* types. Use `buildIntelligenceMindmapSection`, `buildSwotSection`, and
|
|
36
|
-
* domain-specific builders instead.
|
|
37
|
-
*/
|
|
38
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
39
|
-
export * from './types/index.js';
|
|
40
|
-
// ─── MCP Clients ─────────────────────────────────────────────────────────────
|
|
41
|
-
export { MCPConnection, MCPSessionExpiredError, MCPRateLimitError, isRetriableError, formatRetryAfter, parseSSEResponse, } from './mcp/mcp-connection.js';
|
|
42
|
-
export { EuropeanParliamentMCPClient, getEPMCPClient, closeEPMCPClient, } from './mcp/ep-mcp-client.js';
|
|
43
|
-
export { WorldBankMCPClient, getWBMCPClient, closeWBMCPClient } from './mcp/wb-mcp-client.js';
|
|
44
|
-
export { IMFMCPClient, IMF_MCP_TOOLS, getIMFMCPClient, closeIMFMCPClient, } from './mcp/imf-mcp-client.js';
|
|
45
|
-
export { CircuitBreaker, withRetry, } from './mcp/mcp-retry.js';
|
|
46
|
-
export { MCPHealthMonitor } from './mcp/mcp-health.js';
|
|
47
|
-
// ─── Intelligence Analysis ───────────────────────────────────────────────────
|
|
48
|
-
export { scoreVotingAnomaly, analyzeCoalitionCohesion, scoreMEPInfluence, calculateLegislativeVelocity, rankBySignificance, buildIntelligenceSection, buildDefaultStakeholderPerspectives, scoreStakeholderInfluence, buildStakeholderOutcomeMatrix, rankStakeholdersByInfluence, computeVotingIntensity, detectCoalitionShifts, computePolarizationIndex, detectVotingTrends, computeCrossSessionCoalitionStability, rankMEPInfluenceByTopic, buildLegislativeVelocityReport, } from './utils/intelligence-analysis.js';
|
|
49
|
-
// ─── Intelligence Index ──────────────────────────────────────────────────────
|
|
50
|
-
export { createEmptyIndex, addArticleToIndex, buildIndexFromEntries, findRelatedArticles, generateCrossReferences, detectTrends, findOrCreateSeries, buildRelatedArticlesHTML, } from './utils/intelligence-index.js';
|
|
51
|
-
// ─── Article Quality ─────────────────────────────────────────────────────────
|
|
52
|
-
export { assessAnalysisDepth, assessStakeholderCoverage, assessVisualizationQuality, calculateOverallScore, generateRecommendations, scoreArticleQuality, } from './utils/article-quality-scorer.js';
|
|
53
|
-
// ─── Significance Scoring ────────────────────────────────────────────────────
|
|
54
|
-
export { scoreSignificance, scoreBatch, clampScore, deriveDecision, formatScoreMarkdown, formatBatchMarkdown, WEIGHT_PARLIAMENTARY, WEIGHT_POLICY, WEIGHT_PUBLIC_INTEREST, WEIGHT_URGENCY, WEIGHT_INSTITUTIONAL, THRESHOLD_PUBLISH, THRESHOLD_HOLD, } from './utils/significance-scoring.js';
|
|
55
|
-
// ─── Synthesis Summary ───────────────────────────────────────────────────────
|
|
56
|
-
export { parseFrontmatter, aggregateSWOT, aggregateRisks, extractSummaryLine, aggregateConfidence, findMarkdownFiles, generateEditorialRecommendations, buildSynthesisSummary, formatSynthesisMarkdown, } from './generators/synthesis-summary.js';
|
|
57
|
-
// ─── Content Validation ──────────────────────────────────────────────────────
|
|
58
|
-
export { validateArticleContent, validateTranslationCompleteness, } from './utils/content-validator.js';
|
|
59
|
-
// ─── Economic-context evidence helpers (Wave 1 dual-source) ──────────────────
|
|
60
|
-
export { WORLD_BANK_STRONG_FINGERPRINTS, WORLD_BANK_INDICATOR_CODES, WORLD_BANK_FINGERPRINTS, hasWorldBankEvidence, articlePolicyHasWorldBank, IMF_STRONG_FINGERPRINTS, IMF_INDICATOR_CODES, hasIMFEvidence, articlePolicyHasEconomicContext, } from './utils/content-validator.js';
|
|
61
|
-
// ─── Content Metadata ────────────────────────────────────────────────────────
|
|
62
|
-
export { enrichMetadataFromContent } from './utils/content-metadata.js';
|
|
63
|
-
// ─── News Metadata ───────────────────────────────────────────────────────────
|
|
64
|
-
export { buildMetadataDatabase, writeMetadataDatabase, readMetadataDatabase, updateMetadataDatabase, updateIntelligenceIndex, } from './utils/news-metadata.js';
|
|
65
|
-
// ─── Metadata Utilities ──────────────────────────────────────────────────────
|
|
66
|
-
export { pl, pl as pluralizeCount } from './utils/metadata-utils.js';
|
|
67
|
-
// ─── Political Threat Assessment ─────────────────────────────────────────────
|
|
68
|
-
export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree, analyzeLegislativeDisruption, generateThreatAssessmentMarkdown, ALL_THREAT_LANDSCAPE_DIMENSIONS, } from './utils/political-threat-assessment.js';
|
|
69
|
-
// ─── HTML Utilities ──────────────────────────────────────────────────────────
|
|
70
|
-
export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
|
|
71
|
-
export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, } from './utils/file-utils.js';
|
|
72
|
-
// ─── Article Category Detection ──────────────────────────────────────────────
|
|
73
|
-
export { detectCategory } from './utils/article-category.js';
|
|
74
|
-
// ─── World Bank Data Utilities ───────────────────────────────────────────────
|
|
75
|
-
export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, COMPARISON_COUNTRIES, WB_AGGREGATE_LABELS, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, isMCPSupportedWBCountryCode, buildEconomicContextHTML, } from './utils/world-bank-data.js';
|
|
76
|
-
// ─── IMF Data Utilities ──────────────────────────────────────────────────────
|
|
77
|
-
export { IMF_EU_COUNTRY_CODES, IMF_COUNTRY_CODE_OVERRIDES, IMF_EURO_AREA_CODE, IMF_AGGREGATE_LABELS, IMF_POLICY_INDICATORS, IMF_INDICATOR_SDMX_CODES, getIMFCountryCode, isIMFEUMemberState, parseSDMXJSON, getMostRecentObservation, getForecastPoints, formatIMFValue, buildIMFEconomicContext, buildIMFEconomicContextHTML, } from './utils/imf-data.js';
|
|
78
|
-
// ─── Templates ───────────────────────────────────────────────────────────────
|
|
79
|
-
export { generateArticleHTML } from './templates/article-template.js';
|
|
80
|
-
export { computeArticleQualityScore, buildTableOfContents, buildQualityScoreBadge, } from './templates/section-builders.js';
|
|
81
|
-
// ─── Constants & Languages ───────────────────────────────────────────────────
|
|
82
|
-
export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './constants/language-core.js';
|
|
83
|
-
export { WB_INDICATORS, COMMITTEE_INDICATOR_MAP, CATEGORY_INDICATOR_MAP, getCommitteeIndicators, getCommitteePrimaryIndicators, getCategoryIndicators, getIndicatorIdsForCommittees, getAllCategoryIndicatorIds, } from './constants/committee-indicator-map.js';
|
|
84
|
-
// ─── Configuration Constants ─────────────────────────────────────────────────
|
|
85
|
-
export { PROJECT_ROOT, NEWS_DIR, METADATA_DIR, BASE_URL, ARTICLE_FILENAME_PATTERN, WORDS_PER_MINUTE, VALID_ARTICLE_CATEGORIES, ARTICLE_TYPE_WEEK_AHEAD, ARTICLE_TYPE_BREAKING, ARTICLE_TYPE_COMMITTEE_REPORTS, ARTICLE_TYPE_PROPOSITIONS, ARTICLE_TYPE_MOTIONS, ARTICLE_TYPE_MONTH_AHEAD, ARTICLE_TYPE_WEEK_IN_REVIEW, ARTICLE_TYPE_MONTH_IN_REVIEW, ARG_SEPARATOR, APP_VERSION, createThemeToggleButton, THEME_TOGGLE_SCRIPT_CONTENT, THEME_TOGGLE_SCRIPT, } from './constants/config.js';
|
|
86
|
-
// ─── Analysis Constants ──────────────────────────────────────────────────────
|
|
87
|
-
export { AI_MARKER } from './constants/analysis-constants.js';
|
|
88
|
-
// ─── Language UI Strings ─────────────────────────────────────────────────────
|
|
89
|
-
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, 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, TOC_ARIA_LABELS, RELATED_ANALYSIS_LABELS, } from './constants/language-ui.js';
|
|
90
|
-
// ─── Language Article Strings ────────────────────────────────────────────────
|
|
91
|
-
export { LOCALIZED_KEYWORDS, 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, DEEP_ANALYSIS_STRINGS, MOTIONS_STRINGS, WEEK_AHEAD_STRINGS, WEEK_AHEAD_STAKEHOLDER_STRINGS, BREAKING_STRINGS, COMMITTEE_ANALYSIS_CONTENT_STRINGS, SWOT_STRINGS, DASHBOARD_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, MONTH_IN_REVIEW_STRINGS, ANALYSIS_QUALITY_LABELS, } from './constants/language-articles.js';
|
|
92
|
-
// ─── Political Risk Assessment ───────────────────────────────────────────────
|
|
93
|
-
export { calculatePoliticalRiskScore, assessPoliticalCapitalAtRisk, buildQuantitativeSWOT, assessLegislativeVelocityRisk, runAgentRiskAssessment, generateRiskAssessmentMarkdown, generatePoliticalRiskSummary, createScoredSWOTItem, createScoredOpportunityOrThreat, createRiskDriver, } from './utils/political-risk-assessment.js';
|
|
94
|
-
// ─── Analysis Pipeline Stages ─────────────────────────────────────────────────
|
|
95
|
-
export { ALL_ANALYSIS_METHODS, VALID_ANALYSIS_METHODS, runAnalysisStage, } from './generators/pipeline/analysis-stage.js';
|
|
96
|
-
export { mcpCircuitBreaker, computeRollingDateRange, initializeMCPClient, loadFeedDataFromFile, loadEPFeedDataFromFile, fetchWeekAheadData, fetchVotingAnomalies, fetchCoalitionDynamics, fetchVotingReport, fetchMEPInfluence, loadCommitteeDataFromFile, fetchCommitteeInfoFromEPAPI, fetchCommitteeData, fetchVotingRecords, fetchVotingPatterns, fetchMotionsAnomalies, fetchParliamentaryQuestionsForMotions, fetchMotionsData, fetchProposalsFromMCP, fetchPipelineFromMCP, fetchProcedureStatusFromMCP, fetchAdoptedTextsFeed, fetchEventsFeed, fetchProceduresFeed, fetchMEPsFeed, fetchMEPsFeedWithTotal, fetchDocumentsFeed, fetchPlenaryDocumentsFeed, fetchCommitteeDocumentsFeed, fetchPlenarySessionDocumentsFeed, fetchExternalDocumentsFeed, fetchQuestionsFeed, fetchDeclarationsFeed, fetchCorporateBodiesFeed, fetchBreakingNewsFeedData, fetchEPFeedData, } from './generators/pipeline/fetch-stage.js';
|
|
97
|
-
export { validateMCPResponse, normalizeISO8601Date, sanitizeText, isValidCountryCode, isValidLanguageCode, } from './generators/pipeline/transform-stage.js';
|
|
98
|
-
export { createStrategyRegistry, generateArticleForStrategy, } from './generators/pipeline/generate-stage.js';
|
|
99
|
-
export { writeArticleFile, writeSingleArticle, writeGenerationMetadata, } from './generators/pipeline/output-stage.js';
|
|
100
|
-
export { loadAnalysisContext, extractAnalysisSummary, buildAnalysisInsightsSection, } from './generators/strategies/article-strategy.js';
|
|
101
|
-
export { BreakingNewsStrategy, breakingNewsStrategy, } from './generators/strategies/breaking-news-strategy.js';
|
|
102
|
-
export { AFET_KEYWORDS, LIBE_KEYWORDS, AGRI_KEYWORDS, ENVI_KEYWORDS, ECON_KEYWORDS, categorizeAdoptedText, CommitteeReportsStrategy, committeeReportsStrategy, } from './generators/strategies/committee-reports-strategy.js';
|
|
103
|
-
export { MonthAheadStrategy, monthAheadStrategy, } from './generators/strategies/month-ahead-strategy.js';
|
|
104
|
-
export { MonthlyReviewStrategy, monthlyReviewStrategy, } from './generators/strategies/monthly-review-strategy.js';
|
|
105
|
-
export { MotionsStrategy, motionsStrategy, } from './generators/strategies/motions-strategy.js';
|
|
106
|
-
export { PropositionsStrategy, propositionsStrategy, } from './generators/strategies/propositions-strategy.js';
|
|
107
|
-
export { WeekAheadStrategy, weekAheadStrategy, } from './generators/strategies/week-ahead-strategy.js';
|
|
108
|
-
export { WeeklyReviewStrategy, weeklyReviewStrategy, } from './generators/strategies/weekly-review-strategy.js';
|
|
109
|
-
// ─── Content Generators ──────────────────────────────────────────────────────
|
|
110
|
-
export { buildVotingAnalysis, buildProspectiveAnalysis, buildBreakingAnalysis, buildPropositionsAnalysis, buildCommitteeAnalysis, buildVotingSwot, buildProspectiveSwot, buildBreakingSwot, buildPropositionsSwot, buildCommitteeSwot, buildVotingDashboard, buildProspectiveDashboard, buildBreakingDashboard, buildPropositionsDashboard, buildCommitteeDashboard, buildVotingMindmap, buildProspectiveMindmap, buildBreakingMindmap, buildPropositionsMindmap, buildCommitteeMindmap, } from './generators/analysis-builders.js';
|
|
111
|
-
export { SIGNIFICANCE_THRESHOLD, scoreBreakingNewsSignificance, buildBreakingNewsContent, } from './generators/breaking-content.js';
|
|
112
|
-
export { FEATURED_COMMITTEES, PLACEHOLDER_CHAIR, PLACEHOLDER_MEMBERS, applyCommitteeInfo, applyDocuments, isPlaceholderCommitteeData, applyEffectiveness, } from './generators/committee-helpers.js';
|
|
113
|
-
export { buildCoalitionPanel, buildPipelinePanel, buildTrendPanel, buildStakeholderScorecardPanel, buildDashboardSection, dashboardHasCharts, buildEconomicContextPanel, } from './generators/dashboard-content.js';
|
|
114
|
-
export { buildDeepAnalysisSection } from './generators/deep-analysis-content.js';
|
|
115
|
-
export { buildIntelligenceMindmapSection } from './generators/mindmap-content.js';
|
|
116
|
-
export { PLACEHOLDER_MARKER, getMotionsFallbackData, generateMotionsContent, buildPoliticalAlignmentSection, buildAdoptedTextsSection, } from './generators/motions-content.js';
|
|
117
|
-
export { buildSwotSection } from './generators/swot-content.js';
|
|
118
|
-
export { PLACEHOLDER_EVENTS, parsePlenarySessions, parseEPEvents, parseCommitteeMeetings, parseLegislativeDocuments, parseLegislativePipeline, parseParliamentaryQuestions, computeWeekPoliticalTemperature, buildStakeholderImpactMatrix, buildWeekAheadContent, buildKeywords, buildWhatToWatchSection, } from './generators/week-ahead-content.js';
|
|
119
|
-
export { buildPropositionsContent } from './generators/propositions-content.js';
|
|
120
|
-
// ─── Index & Sitemap Generators ──────────────────────────────────────────────
|
|
121
|
-
export { getIndexFilename, generateIndexHTML } from './generators/news-indexes.js';
|
|
122
|
-
export { collectDocsHtmlFiles, generateSitemap, getSitemapFilename, generateSitemapHTML, generateRssFeed, } from './generators/sitemap.js';
|
|
123
|
-
// ─── Political Intelligence Classification ────────────────────────────────────
|
|
124
|
-
export { FRAMEWORK_VERSION, assessPoliticalSignificance, buildImpactMatrix, classifyPoliticalActors, analyzePoliticalForces, initializeAnalysisDirectory, serializeFrontmatter, writeAnalysisFile, writeAnalysisManifest, compareSignificance, maxSignificance, } from './utils/political-classification.js';
|
|
125
|
-
//# sourceMappingURL=index.old.js.map
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* One-shot migration script to normalize legacy news/*.html files to the
|
|
6
|
-
* current canonical header/footer/script layout.
|
|
7
|
-
*
|
|
8
|
-
* Applied operations per file (idempotent — safe to re-run):
|
|
9
|
-
* 1. Upgrade the `<header class="site-header">` block to the current markup
|
|
10
|
-
* (header-logo.png/webp at 72×48, theme-toggle, in-header language
|
|
11
|
-
* switcher `<nav class="site-header__langs">`).
|
|
12
|
-
* 2. Remove any standalone legacy `<nav class="language-switcher">` block
|
|
13
|
-
* that sits outside the header.
|
|
14
|
-
* 3. Replace the two inline `<script>` blocks (reading-progress + theme
|
|
15
|
-
* toggle) with a single `<script src="../js/article-runtime.js" defer>`.
|
|
16
|
-
* 4. Replace the `<footer class="site-footer">` with a freshly rendered
|
|
17
|
-
* fully-localized footer built via `buildSiteFooter` — so Quick Links,
|
|
18
|
-
* Built by, contact, disclaimer etc. are all in the correct language
|
|
19
|
-
* for all 14 languages.
|
|
20
|
-
*
|
|
21
|
-
* Preserved as-is:
|
|
22
|
-
* - All JSON-LD schema scripts (`<script type="application/ld+json">`).
|
|
23
|
-
* - The `<article>` body and all analysis sections.
|
|
24
|
-
* - Chart.js / D3 vendor + init scripts when already present.
|
|
25
|
-
* - Hreflang link alternates, canonical link, meta tags.
|
|
26
|
-
*
|
|
27
|
-
* Usage: `node scripts/utils/migrate-legacy-articles.js [--dry-run]`
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { promises as fs } from 'node:fs';
|
|
31
|
-
import path from 'node:path';
|
|
32
|
-
import { fileURLToPath } from 'node:url';
|
|
33
|
-
|
|
34
|
-
import { buildSiteFooter } from '../templates/section-builders.js';
|
|
35
|
-
import {
|
|
36
|
-
ALL_LANGUAGES,
|
|
37
|
-
LANGUAGE_FLAGS,
|
|
38
|
-
LANGUAGE_NAMES,
|
|
39
|
-
THEME_TOGGLE_LABELS,
|
|
40
|
-
HEADER_SUBTITLE_LABELS,
|
|
41
|
-
getLocalizedString,
|
|
42
|
-
} from '../constants/languages.js';
|
|
43
|
-
import { createThemeToggleButton } from '../constants/config.js';
|
|
44
|
-
|
|
45
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
46
|
-
const __dirname = path.dirname(__filename);
|
|
47
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
48
|
-
const NEWS_DIR = path.join(REPO_ROOT, 'news');
|
|
49
|
-
|
|
50
|
-
/** Simple HTML-entity encode for attribute values. */
|
|
51
|
-
function escapeHTML(s) {
|
|
52
|
-
return String(s)
|
|
53
|
-
.replace(/&/g, '&')
|
|
54
|
-
.replace(/</g, '<')
|
|
55
|
-
.replace(/>/g, '>')
|
|
56
|
-
.replace(/"/g, '"')
|
|
57
|
-
.replace(/'/g, ''');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Parse `YYYY-MM-DD-slug-[runNN-]LL.html` → { date, slug, lang }. */
|
|
61
|
-
function parseFilename(filename) {
|
|
62
|
-
const m = filename.match(/^(\d{4}-\d{2}-\d{2})-([a-z0-9-]+?)(?:-run\d+)?-([a-z]{2})\.html$/);
|
|
63
|
-
if (!m) return null;
|
|
64
|
-
const [, date, slug, lang] = m;
|
|
65
|
-
if (!ALL_LANGUAGES.includes(lang)) return null;
|
|
66
|
-
return { date, slug, lang };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Build the in-header language switcher anchor list (no wrapping <nav>). */
|
|
70
|
-
function buildLangSwitcherAnchors(date, slug, currentLang, availableLanguages) {
|
|
71
|
-
const langs = availableLanguages.length > 0 ? availableLanguages : ALL_LANGUAGES;
|
|
72
|
-
return langs
|
|
73
|
-
.map((code) => {
|
|
74
|
-
const flag = getLocalizedString(LANGUAGE_FLAGS, code);
|
|
75
|
-
const name = getLocalizedString(LANGUAGE_NAMES, code);
|
|
76
|
-
const active = code === currentLang ? ' active' : '';
|
|
77
|
-
const href = `${date}-${slug}-${code}.html`;
|
|
78
|
-
return `<a href="${escapeHTML(href)}" class="lang-link${active}" hreflang="${code}" lang="${code}" title="${escapeHTML(name)}">${flag} ${code.toUpperCase()}</a>`;
|
|
79
|
-
})
|
|
80
|
-
.join('\n ');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Build the canonical `<header class="site-header">…</header>` block. */
|
|
84
|
-
function buildCanonicalHeader(date, slug, lang, availableLanguages) {
|
|
85
|
-
const themeLabel = escapeHTML(getLocalizedString(THEME_TOGGLE_LABELS, lang));
|
|
86
|
-
const subtitle = escapeHTML(getLocalizedString(HEADER_SUBTITLE_LABELS, lang));
|
|
87
|
-
const langs = buildLangSwitcherAnchors(date, slug, lang, availableLanguages);
|
|
88
|
-
const indexHref = lang === 'en' ? '../index.html' : `../index-${lang}.html`;
|
|
89
|
-
return `<header class="site-header" role="banner">
|
|
90
|
-
<div class="site-header__inner">
|
|
91
|
-
<a href="${escapeHTML(indexHref)}" class="site-header__brand" aria-label="EU Parliament Monitor">
|
|
92
|
-
<picture class="site-header__logo-picture">
|
|
93
|
-
<source srcset="../images/header-logo.webp" type="image/webp">
|
|
94
|
-
<img class="site-header__logo site-header__logo--header" src="../images/header-logo.png" alt="" width="72" height="48" aria-hidden="true">
|
|
95
|
-
</picture>
|
|
96
|
-
<span>
|
|
97
|
-
<span class="site-header__title">EU Parliament Monitor</span>
|
|
98
|
-
<span class="site-header__subtitle">${subtitle}</span>
|
|
99
|
-
</span>
|
|
100
|
-
</a>
|
|
101
|
-
${createThemeToggleButton(themeLabel)}
|
|
102
|
-
<nav class="site-header__langs" role="navigation" aria-label="Language selection">
|
|
103
|
-
${langs}
|
|
104
|
-
</nav>
|
|
105
|
-
</div>
|
|
106
|
-
</header>`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Detect which languages a given {date}-{slug} article has. */
|
|
110
|
-
function discoverAvailableLanguages(allFilenames, date, slug) {
|
|
111
|
-
const set = new Set();
|
|
112
|
-
for (const f of allFilenames) {
|
|
113
|
-
// Match both suffixed (-runNN-LL) and plain (-LL) patterns
|
|
114
|
-
const m = f.match(
|
|
115
|
-
new RegExp(`^${date}-${slug.replace(/[-.]/g, '\\$&')}(?:-run\\d+)?-([a-z]{2})\\.html$`)
|
|
116
|
-
);
|
|
117
|
-
if (m && ALL_LANGUAGES.includes(m[1])) set.add(m[1]);
|
|
118
|
-
}
|
|
119
|
-
return ALL_LANGUAGES.filter((l) => set.has(l));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Apply in-place migrations to one HTML document string.
|
|
124
|
-
* Returns the new content (same string if no changes).
|
|
125
|
-
*/
|
|
126
|
-
function migrateDocument(html, date, slug, lang, availableLanguages) {
|
|
127
|
-
let out = html;
|
|
128
|
-
|
|
129
|
-
// ── 1. Replace the `<header class="site-header">…</header>` block
|
|
130
|
-
// with the canonical version (handles both legacy and "modern" headers)
|
|
131
|
-
const canonicalHeader = buildCanonicalHeader(date, slug, lang, availableLanguages);
|
|
132
|
-
out = out.replace(
|
|
133
|
-
/<header class="site-header"[\s\S]*?<\/header>/,
|
|
134
|
-
() => canonicalHeader
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// ── 2. Remove any standalone legacy `<nav class="language-switcher">…</nav>`
|
|
138
|
-
// (legacy files put lang-switcher nav OUTSIDE the header; modern
|
|
139
|
-
// files put it INSIDE the header as site-header__langs — we already
|
|
140
|
-
// injected that in step 1, so any remaining standalone nav is redundant).
|
|
141
|
-
out = out.replace(
|
|
142
|
-
/\s*<nav class="language-switcher"[\s\S]*?<\/nav>\s*/g,
|
|
143
|
-
'\n\n '
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
// ── 3. Replace inline `<script>…</script>` blocks that handle
|
|
147
|
-
// reading-progress or theme-toggle with nothing. The external
|
|
148
|
-
// `<script src="../js/article-runtime.js" defer>` is injected if
|
|
149
|
-
// not already present. JSON-LD scripts (type="application/ld+json")
|
|
150
|
-
// are left untouched.
|
|
151
|
-
out = out.replace(/<script(?![^>]*\bsrc=)(?![^>]*\btype="application\/ld\+json")[^>]*>[\s\S]*?<\/script>\s*/g,
|
|
152
|
-
(block) => {
|
|
153
|
-
// Preserve non-theme/non-reading-progress inline scripts (very rare but
|
|
154
|
-
// possible for chart-init/d3-init fallbacks)
|
|
155
|
-
if (/reading-progress/.test(block) || /ep-theme/.test(block) || /theme-toggle/.test(block)) {
|
|
156
|
-
return '';
|
|
157
|
-
}
|
|
158
|
-
return block;
|
|
159
|
-
}
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Ensure article-runtime.js is referenced exactly once before </body>.
|
|
163
|
-
if (!/<script\s[^>]*src="\.\.\/js\/article-runtime\.js"/i.test(out)) {
|
|
164
|
-
out = out.replace(
|
|
165
|
-
/<\/body>/i,
|
|
166
|
-
' <script src="../js/article-runtime.js" defer></script>\n</body>'
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ── 4. Replace the <footer class="site-footer">…</footer> block with a
|
|
171
|
-
// freshly-rendered, fully-localized footer.
|
|
172
|
-
const newFooter = buildSiteFooter({ lang, pathPrefix: '../' });
|
|
173
|
-
out = out.replace(/<footer class="site-footer"[\s\S]*?<\/footer>/, () => newFooter);
|
|
174
|
-
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function main() {
|
|
179
|
-
const dryRun = process.argv.includes('--dry-run');
|
|
180
|
-
const filesArg = process.argv.find((a) => a.startsWith('--files='));
|
|
181
|
-
const allFiles = (await fs.readdir(NEWS_DIR)).filter((f) => f.endsWith('.html'));
|
|
182
|
-
const files = filesArg
|
|
183
|
-
? filesArg.slice('--files='.length).split(',').filter((f) => f.endsWith('.html'))
|
|
184
|
-
: allFiles;
|
|
185
|
-
|
|
186
|
-
let changed = 0;
|
|
187
|
-
let skipped = 0;
|
|
188
|
-
let errors = 0;
|
|
189
|
-
|
|
190
|
-
for (const f of files) {
|
|
191
|
-
const parsed = parseFilename(f);
|
|
192
|
-
if (!parsed) {
|
|
193
|
-
skipped++;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const { date, slug, lang } = parsed;
|
|
197
|
-
const available = discoverAvailableLanguages(allFiles, date, slug);
|
|
198
|
-
|
|
199
|
-
const p = path.join(NEWS_DIR, f);
|
|
200
|
-
try {
|
|
201
|
-
const before = await fs.readFile(p, 'utf8');
|
|
202
|
-
const after = migrateDocument(before, date, slug, lang, available);
|
|
203
|
-
if (after !== before) {
|
|
204
|
-
if (!dryRun) await fs.writeFile(p, after, 'utf8');
|
|
205
|
-
changed++;
|
|
206
|
-
} else {
|
|
207
|
-
skipped++;
|
|
208
|
-
}
|
|
209
|
-
} catch (err) {
|
|
210
|
-
console.error(`✗ ${f}: ${err.message}`);
|
|
211
|
-
errors++;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
console.log(
|
|
216
|
-
`${dryRun ? '[dry-run] ' : ''}migrated: ${changed}, unchanged: ${skipped}, errors: ${errors}, total: ${files.length}`
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
if (errors > 0) process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
main().catch((err) => {
|
|
223
|
-
console.error(err);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
});
|