euparliamentmonitor 0.8.4
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/LICENSE +201 -0
- package/README.md +1005 -0
- package/SECURITY.md +151 -0
- package/package.json +131 -0
- package/scripts/constants/committee-indicator-map.d.ts +199 -0
- package/scripts/constants/committee-indicator-map.d.ts.map +1 -0
- package/scripts/constants/committee-indicator-map.js +1224 -0
- package/scripts/constants/committee-indicator-map.js.map +1 -0
- package/scripts/constants/config.d.ts +38 -0
- package/scripts/constants/config.d.ts.map +1 -0
- package/scripts/constants/config.js +66 -0
- package/scripts/constants/config.js.map +1 -0
- package/scripts/constants/language-articles.d.ts +84 -0
- package/scripts/constants/language-articles.d.ts.map +1 -0
- package/scripts/constants/language-articles.js +6771 -0
- package/scripts/constants/language-articles.js.map +1 -0
- package/scripts/constants/language-core.d.ts +38 -0
- package/scripts/constants/language-core.d.ts.map +1 -0
- package/scripts/constants/language-core.js +90 -0
- package/scripts/constants/language-core.js.map +1 -0
- package/scripts/constants/language-ui.d.ts +82 -0
- package/scripts/constants/language-ui.d.ts.map +1 -0
- package/scripts/constants/language-ui.js +889 -0
- package/scripts/constants/language-ui.js.map +1 -0
- package/scripts/constants/languages.d.ts +14 -0
- package/scripts/constants/languages.d.ts.map +1 -0
- package/scripts/constants/languages.js +15 -0
- package/scripts/constants/languages.js.map +1 -0
- package/scripts/generators/analysis-builders.d.ts +266 -0
- package/scripts/generators/analysis-builders.d.ts.map +1 -0
- package/scripts/generators/analysis-builders.js +2903 -0
- package/scripts/generators/analysis-builders.js.map +1 -0
- package/scripts/generators/breaking-content.d.ts +45 -0
- package/scripts/generators/breaking-content.d.ts.map +1 -0
- package/scripts/generators/breaking-content.js +530 -0
- package/scripts/generators/breaking-content.js.map +1 -0
- package/scripts/generators/committee-helpers.d.ts +54 -0
- package/scripts/generators/committee-helpers.d.ts.map +1 -0
- package/scripts/generators/committee-helpers.js +154 -0
- package/scripts/generators/committee-helpers.js.map +1 -0
- package/scripts/generators/dashboard-content.d.ts +95 -0
- package/scripts/generators/dashboard-content.d.ts.map +1 -0
- package/scripts/generators/dashboard-content.js +630 -0
- package/scripts/generators/dashboard-content.js.map +1 -0
- package/scripts/generators/deep-analysis-content.d.ts +23 -0
- package/scripts/generators/deep-analysis-content.d.ts.map +1 -0
- package/scripts/generators/deep-analysis-content.js +831 -0
- package/scripts/generators/deep-analysis-content.js.map +1 -0
- package/scripts/generators/mindmap-content.d.ts +55 -0
- package/scripts/generators/mindmap-content.d.ts.map +1 -0
- package/scripts/generators/mindmap-content.js +512 -0
- package/scripts/generators/mindmap-content.js.map +1 -0
- package/scripts/generators/motions-content.d.ts +50 -0
- package/scripts/generators/motions-content.d.ts.map +1 -0
- package/scripts/generators/motions-content.js +391 -0
- package/scripts/generators/motions-content.js.map +1 -0
- package/scripts/generators/news-enhanced.d.ts +14 -0
- package/scripts/generators/news-enhanced.d.ts.map +1 -0
- package/scripts/generators/news-enhanced.js +169 -0
- package/scripts/generators/news-enhanced.js.map +1 -0
- package/scripts/generators/news-indexes.d.ts +31 -0
- package/scripts/generators/news-indexes.d.ts.map +1 -0
- package/scripts/generators/news-indexes.js +410 -0
- package/scripts/generators/news-indexes.js.map +1 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts +352 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/fetch-stage.js +1522 -0
- package/scripts/generators/pipeline/fetch-stage.js.map +1 -0
- package/scripts/generators/pipeline/generate-stage.d.ts +43 -0
- package/scripts/generators/pipeline/generate-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/generate-stage.js +204 -0
- package/scripts/generators/pipeline/generate-stage.js.map +1 -0
- package/scripts/generators/pipeline/output-stage.d.ts +48 -0
- package/scripts/generators/pipeline/output-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/output-stage.js +145 -0
- package/scripts/generators/pipeline/output-stage.js.map +1 -0
- package/scripts/generators/pipeline/transform-stage.d.ts +57 -0
- package/scripts/generators/pipeline/transform-stage.d.ts.map +1 -0
- package/scripts/generators/pipeline/transform-stage.js +111 -0
- package/scripts/generators/pipeline/transform-stage.js.map +1 -0
- package/scripts/generators/propositions-content.d.ts +29 -0
- package/scripts/generators/propositions-content.d.ts.map +1 -0
- package/scripts/generators/propositions-content.js +90 -0
- package/scripts/generators/propositions-content.js.map +1 -0
- package/scripts/generators/sankey-content.d.ts +45 -0
- package/scripts/generators/sankey-content.d.ts.map +1 -0
- package/scripts/generators/sankey-content.js +227 -0
- package/scripts/generators/sankey-content.js.map +1 -0
- package/scripts/generators/sitemap.d.ts +66 -0
- package/scripts/generators/sitemap.d.ts.map +1 -0
- package/scripts/generators/sitemap.js +562 -0
- package/scripts/generators/sitemap.js.map +1 -0
- package/scripts/generators/strategies/article-strategy.d.ts +146 -0
- package/scripts/generators/strategies/article-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/article-strategy.js +4 -0
- package/scripts/generators/strategies/article-strategy.js.map +1 -0
- package/scripts/generators/strategies/breaking-news-strategy.d.ts +64 -0
- package/scripts/generators/strategies/breaking-news-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/breaking-news-strategy.js +246 -0
- package/scripts/generators/strategies/breaking-news-strategy.js.map +1 -0
- package/scripts/generators/strategies/committee-reports-strategy.d.ts +93 -0
- package/scripts/generators/strategies/committee-reports-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/committee-reports-strategy.js +447 -0
- package/scripts/generators/strategies/committee-reports-strategy.js.map +1 -0
- package/scripts/generators/strategies/month-ahead-strategy.d.ts +60 -0
- package/scripts/generators/strategies/month-ahead-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/month-ahead-strategy.js +175 -0
- package/scripts/generators/strategies/month-ahead-strategy.js.map +1 -0
- package/scripts/generators/strategies/monthly-review-strategy.d.ts +66 -0
- package/scripts/generators/strategies/monthly-review-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/monthly-review-strategy.js +204 -0
- package/scripts/generators/strategies/monthly-review-strategy.js.map +1 -0
- package/scripts/generators/strategies/motions-strategy.d.ts +61 -0
- package/scripts/generators/strategies/motions-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/motions-strategy.js +215 -0
- package/scripts/generators/strategies/motions-strategy.js.map +1 -0
- package/scripts/generators/strategies/propositions-strategy.d.ts +60 -0
- package/scripts/generators/strategies/propositions-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/propositions-strategy.js +257 -0
- package/scripts/generators/strategies/propositions-strategy.js.map +1 -0
- package/scripts/generators/strategies/week-ahead-strategy.d.ts +57 -0
- package/scripts/generators/strategies/week-ahead-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/week-ahead-strategy.js +178 -0
- package/scripts/generators/strategies/week-ahead-strategy.js.map +1 -0
- package/scripts/generators/strategies/weekly-review-strategy.d.ts +63 -0
- package/scripts/generators/strategies/weekly-review-strategy.d.ts.map +1 -0
- package/scripts/generators/strategies/weekly-review-strategy.js +211 -0
- package/scripts/generators/strategies/weekly-review-strategy.js.map +1 -0
- package/scripts/generators/swot-content.d.ts +42 -0
- package/scripts/generators/swot-content.d.ts.map +1 -0
- package/scripts/generators/swot-content.js +366 -0
- package/scripts/generators/swot-content.js.map +1 -0
- package/scripts/generators/week-ahead-content.d.ts +103 -0
- package/scripts/generators/week-ahead-content.d.ts.map +1 -0
- package/scripts/generators/week-ahead-content.js +610 -0
- package/scripts/generators/week-ahead-content.js.map +1 -0
- package/scripts/index.d.ts +40 -0
- package/scripts/index.d.ts.map +1 -0
- package/scripts/index.js +53 -0
- package/scripts/index.js.map +1 -0
- package/scripts/mcp/ep-mcp-client.d.ts +471 -0
- package/scripts/mcp/ep-mcp-client.d.ts.map +1 -0
- package/scripts/mcp/ep-mcp-client.js +734 -0
- package/scripts/mcp/ep-mcp-client.js.map +1 -0
- package/scripts/mcp/mcp-connection.d.ts +264 -0
- package/scripts/mcp/mcp-connection.d.ts.map +1 -0
- package/scripts/mcp/mcp-connection.js +790 -0
- package/scripts/mcp/mcp-connection.js.map +1 -0
- package/scripts/mcp/mcp-health.d.ts +75 -0
- package/scripts/mcp/mcp-health.d.ts.map +1 -0
- package/scripts/mcp/mcp-health.js +78 -0
- package/scripts/mcp/mcp-health.js.map +1 -0
- package/scripts/mcp/mcp-retry.d.ts +94 -0
- package/scripts/mcp/mcp-retry.d.ts.map +1 -0
- package/scripts/mcp/mcp-retry.js +127 -0
- package/scripts/mcp/mcp-retry.js.map +1 -0
- package/scripts/mcp/wb-mcp-client.d.ts +38 -0
- package/scripts/mcp/wb-mcp-client.d.ts.map +1 -0
- package/scripts/mcp/wb-mcp-client.js +112 -0
- package/scripts/mcp/wb-mcp-client.js.map +1 -0
- package/scripts/templates/article-template.d.ts +9 -0
- package/scripts/templates/article-template.d.ts.map +1 -0
- package/scripts/templates/article-template.js +378 -0
- package/scripts/templates/article-template.js.map +1 -0
- package/scripts/templates/section-builders.d.ts +28 -0
- package/scripts/templates/section-builders.d.ts.map +1 -0
- package/scripts/templates/section-builders.js +142 -0
- package/scripts/templates/section-builders.js.map +1 -0
- package/scripts/types/analysis.d.ts +115 -0
- package/scripts/types/analysis.d.ts.map +1 -0
- package/scripts/types/analysis.js +4 -0
- package/scripts/types/analysis.js.map +1 -0
- package/scripts/types/common.d.ts +584 -0
- package/scripts/types/common.d.ts.map +1 -0
- package/scripts/types/common.js +96 -0
- package/scripts/types/common.js.map +1 -0
- package/scripts/types/generation.d.ts +104 -0
- package/scripts/types/generation.d.ts.map +1 -0
- package/scripts/types/generation.js +4 -0
- package/scripts/types/generation.js.map +1 -0
- package/scripts/types/index.d.ts +24 -0
- package/scripts/types/index.d.ts.map +1 -0
- package/scripts/types/index.js +16 -0
- package/scripts/types/index.js.map +1 -0
- package/scripts/types/intelligence.d.ts +129 -0
- package/scripts/types/intelligence.d.ts.map +1 -0
- package/scripts/types/intelligence.js +4 -0
- package/scripts/types/intelligence.js.map +1 -0
- package/scripts/types/mcp.d.ts +418 -0
- package/scripts/types/mcp.d.ts.map +1 -0
- package/scripts/types/mcp.js +4 -0
- package/scripts/types/mcp.js.map +1 -0
- package/scripts/types/parliament.d.ts +388 -0
- package/scripts/types/parliament.d.ts.map +1 -0
- package/scripts/types/parliament.js +4 -0
- package/scripts/types/parliament.js.map +1 -0
- package/scripts/types/quality.d.ts +114 -0
- package/scripts/types/quality.d.ts.map +1 -0
- package/scripts/types/quality.js +4 -0
- package/scripts/types/quality.js.map +1 -0
- package/scripts/types/stakeholder.d.ts +88 -0
- package/scripts/types/stakeholder.d.ts.map +1 -0
- package/scripts/types/stakeholder.js +16 -0
- package/scripts/types/stakeholder.js.map +1 -0
- package/scripts/types/visualization.d.ts +708 -0
- package/scripts/types/visualization.d.ts.map +1 -0
- package/scripts/types/visualization.js +4 -0
- package/scripts/types/visualization.js.map +1 -0
- package/scripts/types/world-bank.d.ts +85 -0
- package/scripts/types/world-bank.d.ts.map +1 -0
- package/scripts/types/world-bank.js +4 -0
- package/scripts/types/world-bank.js.map +1 -0
- package/scripts/utils/article-category.d.ts +18 -0
- package/scripts/utils/article-category.d.ts.map +1 -0
- package/scripts/utils/article-category.js +49 -0
- package/scripts/utils/article-category.js.map +1 -0
- package/scripts/utils/article-quality-scorer.d.ts +87 -0
- package/scripts/utils/article-quality-scorer.d.ts.map +1 -0
- package/scripts/utils/article-quality-scorer.js +1048 -0
- package/scripts/utils/article-quality-scorer.js.map +1 -0
- package/scripts/utils/content-metadata.d.ts +34 -0
- package/scripts/utils/content-metadata.d.ts.map +1 -0
- package/scripts/utils/content-metadata.js +249 -0
- package/scripts/utils/content-metadata.js.map +1 -0
- package/scripts/utils/content-validator.d.ts +94 -0
- package/scripts/utils/content-validator.d.ts.map +1 -0
- package/scripts/utils/content-validator.js +489 -0
- package/scripts/utils/content-validator.js.map +1 -0
- package/scripts/utils/copy-test-reports.d.ts +9 -0
- package/scripts/utils/copy-test-reports.d.ts.map +1 -0
- package/scripts/utils/copy-test-reports.js +508 -0
- package/scripts/utils/copy-test-reports.js.map +1 -0
- package/scripts/utils/file-utils.d.ts +144 -0
- package/scripts/utils/file-utils.d.ts.map +1 -0
- package/scripts/utils/file-utils.js +374 -0
- package/scripts/utils/file-utils.js.map +1 -0
- package/scripts/utils/fix-articles.d.ts +27 -0
- package/scripts/utils/fix-articles.d.ts.map +1 -0
- package/scripts/utils/fix-articles.js +510 -0
- package/scripts/utils/fix-articles.js.map +1 -0
- package/scripts/utils/generate-docs-index.d.ts +8 -0
- package/scripts/utils/generate-docs-index.d.ts.map +1 -0
- package/scripts/utils/generate-docs-index.js +275 -0
- package/scripts/utils/generate-docs-index.js.map +1 -0
- package/scripts/utils/html-sanitize.d.ts +18 -0
- package/scripts/utils/html-sanitize.d.ts.map +1 -0
- package/scripts/utils/html-sanitize.js +57 -0
- package/scripts/utils/html-sanitize.js.map +1 -0
- package/scripts/utils/intelligence-analysis.d.ts +173 -0
- package/scripts/utils/intelligence-analysis.d.ts.map +1 -0
- package/scripts/utils/intelligence-analysis.js +936 -0
- package/scripts/utils/intelligence-analysis.js.map +1 -0
- package/scripts/utils/intelligence-index.d.ts +126 -0
- package/scripts/utils/intelligence-index.d.ts.map +1 -0
- package/scripts/utils/intelligence-index.js +731 -0
- package/scripts/utils/intelligence-index.js.map +1 -0
- package/scripts/utils/metadata-utils.d.ts +14 -0
- package/scripts/utils/metadata-utils.d.ts.map +1 -0
- package/scripts/utils/metadata-utils.js +18 -0
- package/scripts/utils/metadata-utils.js.map +1 -0
- package/scripts/utils/news-metadata.d.ts +47 -0
- package/scripts/utils/news-metadata.d.ts.map +1 -0
- package/scripts/utils/news-metadata.js +259 -0
- package/scripts/utils/news-metadata.js.map +1 -0
- package/scripts/utils/validate-articles.d.ts +2 -0
- package/scripts/utils/validate-articles.d.ts.map +1 -0
- package/scripts/utils/validate-articles.js +284 -0
- package/scripts/utils/validate-articles.js.map +1 -0
- package/scripts/utils/validate-ep-api.d.ts +51 -0
- package/scripts/utils/validate-ep-api.d.ts.map +1 -0
- package/scripts/utils/validate-ep-api.js +160 -0
- package/scripts/utils/validate-ep-api.js.map +1 -0
- package/scripts/utils/world-bank-data.d.ts +84 -0
- package/scripts/utils/world-bank-data.d.ts.map +1 -0
- package/scripts/utils/world-bank-data.js +311 -0
- package/scripts/utils/world-bank-data.js.map +1 -0
|
@@ -0,0 +1,2903 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { getLocalizedString, COMMITTEE_ANALYSIS_CONTENT_STRINGS, BREAKING_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, } from '../constants/languages.js';
|
|
4
|
+
import { isPlaceholderCommitteeData } from './committee-helpers.js';
|
|
5
|
+
import { PLACEHOLDER_MARKER } from './motions-content.js';
|
|
6
|
+
import { buildDefaultStakeholderPerspectives, buildStakeholderOutcomeMatrix, computeVotingIntensity, computePolarizationIndex, } from '../utils/intelligence-analysis.js';
|
|
7
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Derive stakeholder outcomes from voting records.
|
|
10
|
+
* Groups that win votes are "winners"; groups on the losing side are "losers".
|
|
11
|
+
*
|
|
12
|
+
* @param records - Voting records
|
|
13
|
+
* @param patterns - Voting pattern data
|
|
14
|
+
* @returns Stakeholder outcome assessments
|
|
15
|
+
*/
|
|
16
|
+
function deriveStakeholderOutcomesFromVoting(records, patterns) {
|
|
17
|
+
const outcomes = [];
|
|
18
|
+
// High-cohesion groups that vote with majority are winners
|
|
19
|
+
for (const pattern of patterns) {
|
|
20
|
+
if (pattern.cohesion > 0.8 && pattern.participation > 0.7) {
|
|
21
|
+
outcomes.push({
|
|
22
|
+
actor: pattern.group,
|
|
23
|
+
outcome: 'winner',
|
|
24
|
+
reason: `High cohesion (${(pattern.cohesion * 100).toFixed(0)}%) with strong participation — disciplined bloc that shapes outcomes`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (pattern.cohesion < 0.5) {
|
|
28
|
+
outcomes.push({
|
|
29
|
+
actor: pattern.group,
|
|
30
|
+
outcome: 'loser',
|
|
31
|
+
reason: `Low cohesion (${(pattern.cohesion * 100).toFixed(0)}%) — internal divisions weaken bargaining power`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Adopted motions → the proposing side wins
|
|
36
|
+
for (const record of records.slice(0, 3)) {
|
|
37
|
+
if (record.result?.toLowerCase().includes('adopt')) {
|
|
38
|
+
outcomes.push({
|
|
39
|
+
actor: 'Majority coalition',
|
|
40
|
+
outcome: 'winner',
|
|
41
|
+
reason: `"${record.title}" adopted (${record.votes.for} for vs ${record.votes.against} against)`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return outcomes;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derive action→consequence chains from voting records and anomalies.
|
|
49
|
+
*
|
|
50
|
+
* @param records - Voting records
|
|
51
|
+
* @param anomalies - Detected anomalies
|
|
52
|
+
* @returns Action-consequence pairs
|
|
53
|
+
*/
|
|
54
|
+
function deriveConsequencesFromVoting(records, anomalies) {
|
|
55
|
+
const consequences = [];
|
|
56
|
+
for (const record of records.slice(0, 3)) {
|
|
57
|
+
if (record.result === PLACEHOLDER_MARKER)
|
|
58
|
+
continue;
|
|
59
|
+
consequences.push({
|
|
60
|
+
action: `Vote on "${record.title}"`,
|
|
61
|
+
consequence: `Result: ${record.result} (${record.votes.for}+ / ${record.votes.against}− / ${record.votes.abstain} abstain)`,
|
|
62
|
+
severity: record.votes.for > record.votes.against * 2 ? 'high' : 'medium',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (const anomaly of anomalies.slice(0, 2)) {
|
|
66
|
+
if (/placeholder/i.test(anomaly.type))
|
|
67
|
+
continue;
|
|
68
|
+
consequences.push({
|
|
69
|
+
action: `${anomaly.type} detected`,
|
|
70
|
+
consequence: anomaly.description,
|
|
71
|
+
severity: anomaly.severity?.toLowerCase() === 'high' ? 'high' : 'medium',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return consequences;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Derive political mistakes from anomalies — defections signal miscalculations.
|
|
78
|
+
*
|
|
79
|
+
* @param anomalies - Detected voting anomalies
|
|
80
|
+
* @returns Political mistake assessments
|
|
81
|
+
*/
|
|
82
|
+
function deriveMistakesFromAnomalies(anomalies) {
|
|
83
|
+
return anomalies
|
|
84
|
+
.filter((a) => a.type?.toLowerCase().includes('defect') || a.severity?.toUpperCase() === 'HIGH')
|
|
85
|
+
.slice(0, 3)
|
|
86
|
+
.map((a) => ({
|
|
87
|
+
actor: 'Political group leadership',
|
|
88
|
+
description: `${a.type}: ${a.description}`,
|
|
89
|
+
alternative: 'Stronger whip coordination or earlier compromise negotiation could have maintained party discipline',
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
// ─── Stakeholder perspective builders ────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Build multi-stakeholder perspectives for a voting analysis.
|
|
95
|
+
* Derives per-group importance scores based on adopted/rejected counts and
|
|
96
|
+
* cohesion anomalies.
|
|
97
|
+
*
|
|
98
|
+
* @param adoptedCount - Number of adopted texts
|
|
99
|
+
* @param anomalies - Detected voting anomalies
|
|
100
|
+
* @param topic - Primary topic string for context
|
|
101
|
+
* @returns Array of stakeholder perspectives
|
|
102
|
+
*/
|
|
103
|
+
function buildVotingStakeholderPerspectives(adoptedCount, anomalies, topic) {
|
|
104
|
+
const hasHighAnomalies = anomalies.some((a) => a.severity?.toUpperCase() === 'HIGH');
|
|
105
|
+
return buildDefaultStakeholderPerspectives(topic, {
|
|
106
|
+
political_groups: hasHighAnomalies ? 0.9 : adoptedCount > 0 ? 0.8 : 0.5,
|
|
107
|
+
civil_society: adoptedCount > 0 ? 0.6 : 0.4,
|
|
108
|
+
industry: adoptedCount > 0 ? 0.7 : 0.4,
|
|
109
|
+
national_govts: 0.7,
|
|
110
|
+
citizens: adoptedCount > 0 ? 0.6 : 0.3,
|
|
111
|
+
eu_institutions: 0.8,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build multi-stakeholder perspectives for a prospective (week/month-ahead) analysis.
|
|
116
|
+
*
|
|
117
|
+
* @param eventCount - Number of scheduled events
|
|
118
|
+
* @param bottleneckCount - Number of bottlenecked procedures
|
|
119
|
+
* @param topic - Primary topic string for context
|
|
120
|
+
* @returns Array of stakeholder perspectives
|
|
121
|
+
*/
|
|
122
|
+
function buildProspectiveStakeholderPerspectives(eventCount, bottleneckCount, topic) {
|
|
123
|
+
return buildDefaultStakeholderPerspectives(topic, {
|
|
124
|
+
political_groups: eventCount > 5 ? 0.8 : 0.6,
|
|
125
|
+
civil_society: 0.5,
|
|
126
|
+
industry: bottleneckCount > 0 ? 0.3 : 0.6,
|
|
127
|
+
national_govts: 0.7,
|
|
128
|
+
citizens: 0.5,
|
|
129
|
+
eu_institutions: 0.8,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build multi-stakeholder perspectives for a breaking news analysis.
|
|
134
|
+
*
|
|
135
|
+
* @param adoptedCount - Number of adopted texts in the feed
|
|
136
|
+
* @param topic - Primary topic string for context
|
|
137
|
+
* @returns Array of stakeholder perspectives
|
|
138
|
+
*/
|
|
139
|
+
function buildBreakingStakeholderPerspectives(adoptedCount, topic) {
|
|
140
|
+
return buildDefaultStakeholderPerspectives(topic, {
|
|
141
|
+
political_groups: 0.9,
|
|
142
|
+
civil_society: adoptedCount > 0 ? 0.6 : 0.4,
|
|
143
|
+
industry: adoptedCount > 0 ? 0.7 : 0.4,
|
|
144
|
+
national_govts: 0.7,
|
|
145
|
+
citizens: adoptedCount > 0 ? 0.6 : 0.3,
|
|
146
|
+
eu_institutions: 0.9,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build multi-stakeholder perspectives for a propositions pipeline analysis.
|
|
151
|
+
*
|
|
152
|
+
* @param healthScore - Pipeline health score (0-1)
|
|
153
|
+
* @param topic - Primary topic string for context
|
|
154
|
+
* @returns Array of stakeholder perspectives
|
|
155
|
+
*/
|
|
156
|
+
function buildPropositionsStakeholderPerspectives(healthScore, topic) {
|
|
157
|
+
return buildDefaultStakeholderPerspectives(topic, {
|
|
158
|
+
political_groups: 0.7,
|
|
159
|
+
civil_society: healthScore < 0.5 ? 0.3 : 0.5,
|
|
160
|
+
industry: healthScore < 0.5 ? 0.3 : 0.6,
|
|
161
|
+
national_govts: healthScore < 0.5 ? 0.3 : 0.6,
|
|
162
|
+
citizens: healthScore < 0.5 ? 0.2 : 0.5,
|
|
163
|
+
eu_institutions: 0.8,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build multi-stakeholder perspectives for a committee reports analysis.
|
|
168
|
+
*
|
|
169
|
+
* @param activePct - Percentage of committees with documents (0-100)
|
|
170
|
+
* @param totalDocs - Total document count
|
|
171
|
+
* @param topic - Primary topic string for context
|
|
172
|
+
* @returns Array of stakeholder perspectives
|
|
173
|
+
*/
|
|
174
|
+
function buildCommitteeStakeholderPerspectives(activePct, totalDocs, topic) {
|
|
175
|
+
return buildDefaultStakeholderPerspectives(topic, {
|
|
176
|
+
political_groups: activePct > 70 ? 0.8 : 0.5,
|
|
177
|
+
civil_society: totalDocs > 5 ? 0.6 : 0.4,
|
|
178
|
+
industry: totalDocs > 5 ? 0.7 : 0.4,
|
|
179
|
+
national_govts: activePct > 70 ? 0.7 : 0.4,
|
|
180
|
+
citizens: totalDocs > 5 ? 0.5 : 0.3,
|
|
181
|
+
eu_institutions: 0.8,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build the stakeholder outcome matrix for a list of key actions.
|
|
186
|
+
* Used by all 5 analysis builders to populate the outcome matrix.
|
|
187
|
+
*
|
|
188
|
+
* @param actions - Array of (action, scores) pairs to include in the matrix
|
|
189
|
+
* @returns Stakeholder outcome matrix rows
|
|
190
|
+
*/
|
|
191
|
+
function buildOutcomeMatrix(actions) {
|
|
192
|
+
return actions.map(({ action, scores, confidence }) => buildStakeholderOutcomeMatrix(action, scores, confidence));
|
|
193
|
+
}
|
|
194
|
+
// ─── Voting analysis text helpers ─────────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Build the "what" summary for a voting analysis, including intensity metrics.
|
|
197
|
+
*
|
|
198
|
+
* @param dateFrom - Period start
|
|
199
|
+
* @param dateTo - Period end
|
|
200
|
+
* @param recordCount - Real voting record count
|
|
201
|
+
* @param adoptedCount - Adopted count
|
|
202
|
+
* @param rejectedCount - Rejected count
|
|
203
|
+
* @param anomalyCount - Anomaly count
|
|
204
|
+
* @param patternCount - Pattern count
|
|
205
|
+
* @param questionCount - Question count
|
|
206
|
+
* @param intensity - Voting intensity metrics (may be null)
|
|
207
|
+
* @param polarization - Polarization index (may be null)
|
|
208
|
+
* @returns Summary text
|
|
209
|
+
*/
|
|
210
|
+
function buildVotingWhatText(dateFrom, dateTo, recordCount, adoptedCount, rejectedCount, anomalyCount, patternCount, questionCount, intensity, polarization) {
|
|
211
|
+
if (recordCount === 0 && patternCount === 0 && questionCount === 0) {
|
|
212
|
+
return `Parliamentary activity from ${dateFrom} to ${dateTo}. Detailed roll-call data unavailable for this period.`;
|
|
213
|
+
}
|
|
214
|
+
const base = `${recordCount} votes recorded between ${dateFrom} and ${dateTo}: ${adoptedCount} adopted, ${rejectedCount} rejected. ${anomalyCount} voting anomalies detected across ${patternCount} political groups. ${questionCount} parliamentary questions filed.`;
|
|
215
|
+
if (!intensity || recordCount === 0)
|
|
216
|
+
return base;
|
|
217
|
+
return `${base} Voting intensity: ${intensity.closeVoteCount} close ${intensity.closeVoteCount === 1 ? 'vote' : 'votes'}, ${intensity.decisiveVoteCount} decisive ${intensity.decisiveVoteCount === 1 ? 'vote' : 'votes'}. Polarization index: ${polarization?.assessment ?? 'N/A'}.`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build the "why" text for a voting analysis, including polarization insights.
|
|
221
|
+
*
|
|
222
|
+
* @param patterns - Real voting patterns
|
|
223
|
+
* @param polarization - Polarization index (may be null)
|
|
224
|
+
* @returns Why text
|
|
225
|
+
*/
|
|
226
|
+
function buildVotingWhyText(patterns, polarization) {
|
|
227
|
+
if (polarization && patterns.length > 0) {
|
|
228
|
+
const fragmented = polarization.fragmentedGroups.length > 0
|
|
229
|
+
? `Fragmented groups (${polarization.fragmentedGroups.join(', ')}) weaken opposition capacity.`
|
|
230
|
+
: 'No critically fragmented groups detected.';
|
|
231
|
+
return `Polarization assessment: ${polarization.assessment} (index ${polarization.overallIndex}). ${polarization.highCohesionGroups.length} ${polarization.highCohesionGroups.length === 1 ? 'group' : 'groups'} above 80% cohesion can form blocking minorities. ${fragmented} Effective number of voting blocs: ${polarization.effectiveBlocs.toFixed(1)}.`;
|
|
232
|
+
}
|
|
233
|
+
if (patterns.length > 0) {
|
|
234
|
+
const highCount = patterns.filter((p) => p.cohesion > 0.8).length;
|
|
235
|
+
return `Voting behaviour reveals the balance of power: groups with high cohesion (${highCount} groups above 80%) can form blocking minorities or drive legislation. Anomalies signal shifting alliances and emerging fault lines that may reshape future coalition dynamics.`;
|
|
236
|
+
}
|
|
237
|
+
return 'Voting patterns in this period reflect ongoing legislative negotiations and inter-institutional bargaining positions.';
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build the political impact text for a voting analysis.
|
|
241
|
+
*
|
|
242
|
+
* @param recordCount - Real record count
|
|
243
|
+
* @param adoptedCount - Adopted count
|
|
244
|
+
* @param anomalyCount - Anomaly count
|
|
245
|
+
* @param intensity - Voting intensity metrics (may be null)
|
|
246
|
+
* @returns Political impact text
|
|
247
|
+
*/
|
|
248
|
+
function buildVotingPoliticalImpact(recordCount, adoptedCount, anomalyCount, intensity) {
|
|
249
|
+
if (recordCount === 0) {
|
|
250
|
+
return 'Legislative outcomes in this period will shape EU policy priorities and inter-institutional dynamics.';
|
|
251
|
+
}
|
|
252
|
+
const base = `${adoptedCount} adopted texts will shape EU policy. ${anomalyCount} anomalies suggest internal disagreements that may affect future negotiations.`;
|
|
253
|
+
if (!intensity)
|
|
254
|
+
return base;
|
|
255
|
+
const marginPct = (intensity.averageMargin * 100).toFixed(0);
|
|
256
|
+
const marginInsight = intensity.averageMargin > 0.3
|
|
257
|
+
? 'decisive outcomes signal clear political direction'
|
|
258
|
+
: 'narrow margins suggest fragile coalitions';
|
|
259
|
+
return `${base} Average margin: ${marginPct}% — ${marginInsight}.`;
|
|
260
|
+
}
|
|
261
|
+
// ─── Strategy-specific builders ──────────────────────────────────────────────
|
|
262
|
+
/**
|
|
263
|
+
* Build deep analysis for voting-based articles (motions, weekly/monthly review).
|
|
264
|
+
*
|
|
265
|
+
* @param dateFrom - Period start date
|
|
266
|
+
* @param dateTo - Period end date
|
|
267
|
+
* @param records - Voting records
|
|
268
|
+
* @param patterns - Voting patterns
|
|
269
|
+
* @param anomalies - Anomalies detected
|
|
270
|
+
* @param questions - Parliamentary questions
|
|
271
|
+
* @returns Deep analysis object
|
|
272
|
+
*/
|
|
273
|
+
export function buildVotingAnalysis(dateFrom, dateTo, records, patterns, anomalies, questions) {
|
|
274
|
+
const realRecords = records.filter((r) => r.result !== PLACEHOLDER_MARKER);
|
|
275
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
276
|
+
const realAnomalies = anomalies.filter((a) => !/placeholder/i.test(a.type));
|
|
277
|
+
const realQuestions = questions.filter((q) => q.status !== PLACEHOLDER_MARKER);
|
|
278
|
+
const adoptedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('adopt')).length;
|
|
279
|
+
const rejectedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('reject')).length;
|
|
280
|
+
const topTopics = realRecords.slice(0, 3).map((r) => r.title);
|
|
281
|
+
// ── Advanced political intelligence ────────────────────────────────────────
|
|
282
|
+
const intensity = computeVotingIntensity(realRecords);
|
|
283
|
+
const polarization = computePolarizationIndex(realPatterns);
|
|
284
|
+
return {
|
|
285
|
+
what: buildVotingWhatText(dateFrom, dateTo, realRecords.length, adoptedCount, rejectedCount, realAnomalies.length, realPatterns.length, realQuestions.length, intensity, polarization),
|
|
286
|
+
who: [
|
|
287
|
+
...realPatterns.map((p) => `${p.group} — cohesion: ${(p.cohesion * 100).toFixed(0)}%, participation: ${(p.participation * 100).toFixed(0)}%`),
|
|
288
|
+
...realQuestions.slice(0, 3).map((q) => `${q.author} — question on "${q.topic}"`),
|
|
289
|
+
],
|
|
290
|
+
when: [
|
|
291
|
+
`Period: ${dateFrom} to ${dateTo}`,
|
|
292
|
+
...realRecords.slice(0, 3).map((r) => `${r.date}: Vote on "${r.title}" — ${r.result}`),
|
|
293
|
+
],
|
|
294
|
+
why: buildVotingWhyText(realPatterns, polarization),
|
|
295
|
+
stakeholderOutcomes: deriveStakeholderOutcomesFromVoting(realRecords, realPatterns),
|
|
296
|
+
impactAssessment: {
|
|
297
|
+
political: buildVotingPoliticalImpact(realRecords.length, adoptedCount, realAnomalies.length, intensity),
|
|
298
|
+
economic: topTopics.length > 0
|
|
299
|
+
? `Legislation on ${topTopics.join(', ')} may affect regulatory environments, compliance costs, and market conditions across member states.`
|
|
300
|
+
: 'The legislative outcomes in this period carry potential economic implications for EU businesses and citizens.',
|
|
301
|
+
social: realQuestions.length > 0
|
|
302
|
+
? `Parliamentary questions on ${realQuestions
|
|
303
|
+
.slice(0, 2)
|
|
304
|
+
.map((q) => q.topic)
|
|
305
|
+
.join(' and ')} highlight citizen concerns that MEPs are bringing to the legislative agenda.`
|
|
306
|
+
: 'Parliamentary questions in this period reflect citizens\u2019 concerns and MEPs\u2019 oversight role.',
|
|
307
|
+
legal: realRecords.length > 0
|
|
308
|
+
? `${adoptedCount} adopted texts enter the EU legal framework. Rejected proposals (${rejectedCount}) may return in amended form, creating legal uncertainty in affected policy areas.`
|
|
309
|
+
: 'Adopted texts from this period will enter the EU legal framework, while any rejected proposals may be reintroduced in amended form.',
|
|
310
|
+
geopolitical: 'Voting patterns reflect evolving EU positions on international affairs, trade relationships, and global governance commitments.',
|
|
311
|
+
},
|
|
312
|
+
actionConsequences: deriveConsequencesFromVoting(realRecords, realAnomalies),
|
|
313
|
+
mistakes: deriveMistakesFromAnomalies(realAnomalies),
|
|
314
|
+
outlook: realAnomalies.length > 0
|
|
315
|
+
? `Watch for coalition realignment: ${realAnomalies.length} anomalies detected.${polarization && polarization.fragmentedGroups.length > 0 ? ` Fragmented groups (${polarization.fragmentedGroups.join(', ')}) may seek new alliance partners.` : ' Groups with declining cohesion may seek new alliance partners.'} Upcoming committee votes will test whether these shifts are temporary or structural.`
|
|
316
|
+
: 'The legislative trajectory suggests continued consensus-building with potential pressure points in the weeks ahead.',
|
|
317
|
+
stakeholderPerspectives: buildVotingStakeholderPerspectives(adoptedCount, realAnomalies, topTopics[0] ?? `voting period ${dateFrom}–${dateTo}`),
|
|
318
|
+
stakeholderOutcomeMatrix: buildOutcomeMatrix([
|
|
319
|
+
{
|
|
320
|
+
action: `Voting outcomes ${dateFrom}–${dateTo}`,
|
|
321
|
+
scores: {
|
|
322
|
+
political_groups: realAnomalies.length > 0 ? 0.8 : 0.6,
|
|
323
|
+
civil_society: adoptedCount > 0 ? 0.6 : 0.4,
|
|
324
|
+
industry: adoptedCount > 0 ? 0.7 : 0.4,
|
|
325
|
+
national_govts: 0.7,
|
|
326
|
+
citizens: adoptedCount > 0 ? 0.6 : 0.3,
|
|
327
|
+
eu_institutions: 0.8,
|
|
328
|
+
},
|
|
329
|
+
confidence: realRecords.length > 0 ? 'high' : 'low',
|
|
330
|
+
},
|
|
331
|
+
]),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Build deep analysis for week-ahead/month-ahead articles.
|
|
336
|
+
*
|
|
337
|
+
* @param weekData - Aggregated week/month data
|
|
338
|
+
* @param dateRange - Date range for the preview period
|
|
339
|
+
* @param label - "week" or "month"
|
|
340
|
+
* @returns Deep analysis object
|
|
341
|
+
*/
|
|
342
|
+
export function buildProspectiveAnalysis(weekData, dateRange, label) {
|
|
343
|
+
const eventCount = weekData.events.length;
|
|
344
|
+
const committeeCount = weekData.committees.length;
|
|
345
|
+
const docCount = weekData.documents.length;
|
|
346
|
+
const pipelineCount = weekData.pipeline.length;
|
|
347
|
+
const questionCount = weekData.questions.length;
|
|
348
|
+
const bottleneckProcedures = weekData.pipeline.filter((p) => p.bottleneck === true);
|
|
349
|
+
return {
|
|
350
|
+
what: `European Parliament ${label} ahead (${dateRange.start} to ${dateRange.end}): ${eventCount} plenary events, ${committeeCount} committee meetings, ${docCount} legislative documents, ${pipelineCount} pipeline procedures, ${questionCount} parliamentary questions scheduled.`,
|
|
351
|
+
who: [
|
|
352
|
+
...weekData.events.slice(0, 3).map((e) => `${e.type}: ${e.title}`),
|
|
353
|
+
...weekData.committees
|
|
354
|
+
.slice(0, 3)
|
|
355
|
+
.map((c) => `${c.committeeName ?? c.committee} — ${c.agenda?.length ?? 0} agenda items`),
|
|
356
|
+
],
|
|
357
|
+
when: [
|
|
358
|
+
`Period: ${dateRange.start} to ${dateRange.end}`,
|
|
359
|
+
...weekData.events.slice(0, 4).map((e) => `${e.date}: ${e.title}`),
|
|
360
|
+
],
|
|
361
|
+
why: bottleneckProcedures.length > 0
|
|
362
|
+
? `${bottleneckProcedures.length} legislative procedures face bottleneck risks. These delays affect the EU's ability to respond to pressing policy challenges and may create downstream scheduling conflicts.`
|
|
363
|
+
: `With ${eventCount} events and ${pipelineCount} active procedures, this ${label} represents a significant workload. Scheduling density increases the risk of compressed debate time and last-minute amendments.`,
|
|
364
|
+
stakeholderOutcomes: [
|
|
365
|
+
...(bottleneckProcedures.length > 0
|
|
366
|
+
? [
|
|
367
|
+
{
|
|
368
|
+
actor: 'Legislative pipeline',
|
|
369
|
+
outcome: 'loser',
|
|
370
|
+
reason: `${bottleneckProcedures.length} procedures bottlenecked — delays impact legislative throughput`,
|
|
371
|
+
},
|
|
372
|
+
]
|
|
373
|
+
: []),
|
|
374
|
+
...(weekData.committees.length > 3
|
|
375
|
+
? [
|
|
376
|
+
{
|
|
377
|
+
actor: 'Committee system',
|
|
378
|
+
outcome: 'neutral',
|
|
379
|
+
reason: `${committeeCount} committees active — heavy workload demands efficient agenda management`,
|
|
380
|
+
},
|
|
381
|
+
]
|
|
382
|
+
: []),
|
|
383
|
+
],
|
|
384
|
+
impactAssessment: {
|
|
385
|
+
political: `${eventCount} plenary events will test political group discipline and coalition stability. Watch for amendment battles in key legislative files.`,
|
|
386
|
+
economic: docCount > 0
|
|
387
|
+
? `${docCount} legislative documents under consideration may reshape market regulations, trade policies, or fiscal rules.`
|
|
388
|
+
: 'No major economic legislation currently flagged, though committee discussions may surface new regulatory proposals.',
|
|
389
|
+
social: questionCount > 0
|
|
390
|
+
? `${questionCount} parliamentary questions signal MEP engagement with citizen concerns on policy implementation and accountability.`
|
|
391
|
+
: 'Social impact depends on the outcomes of plenary debates and committee decisions.',
|
|
392
|
+
legal: pipelineCount > 0
|
|
393
|
+
? `${pipelineCount} pipeline procedures advancing through legislative stages — each vote creates binding legal obligations for member states.`
|
|
394
|
+
: 'The legal landscape awaits legislative outcomes from scheduled proceedings.',
|
|
395
|
+
geopolitical: 'External affairs debates and foreign policy questions may signal evolving EU positioning on global matters.',
|
|
396
|
+
},
|
|
397
|
+
actionConsequences: [
|
|
398
|
+
...bottleneckProcedures.slice(0, 2).map((p) => ({
|
|
399
|
+
action: `"${p.title}" in ${p.stage ?? 'committee'} stage`,
|
|
400
|
+
consequence: 'Bottleneck risk may cause delay or force procedural shortcuts',
|
|
401
|
+
severity: 'high',
|
|
402
|
+
})),
|
|
403
|
+
...weekData.events.slice(0, 2).map((e) => ({
|
|
404
|
+
action: `${e.type} on "${e.title}"`,
|
|
405
|
+
consequence: e.description || 'Outcome will shape legislative direction',
|
|
406
|
+
severity: 'medium',
|
|
407
|
+
})),
|
|
408
|
+
],
|
|
409
|
+
mistakes: bottleneckProcedures.slice(0, 2).map((p) => ({
|
|
410
|
+
actor: 'Legislative coordinators',
|
|
411
|
+
description: `"${p.title}" has reached bottleneck status at ${p.stage ?? 'committee'} stage`,
|
|
412
|
+
alternative: 'Earlier trilogue engagement or simplified procedure could have prevented delay',
|
|
413
|
+
})),
|
|
414
|
+
outlook: `The coming ${label} will test Parliament's capacity to manage ${eventCount} events and ${pipelineCount} active files simultaneously. Key decisions on ${weekData.events[0]?.title ?? 'pending matters'} may set the tone for the legislative session.`,
|
|
415
|
+
stakeholderPerspectives: buildProspectiveStakeholderPerspectives(eventCount, bottleneckProcedures.length, weekData.events[0]?.title ?? `${label} ahead`),
|
|
416
|
+
stakeholderOutcomeMatrix: buildOutcomeMatrix([
|
|
417
|
+
{
|
|
418
|
+
action: `${label}-ahead schedule (${dateRange.start}–${dateRange.end})`,
|
|
419
|
+
scores: {
|
|
420
|
+
political_groups: eventCount > 5 ? 0.8 : 0.6,
|
|
421
|
+
civil_society: 0.5,
|
|
422
|
+
industry: bottleneckProcedures.length > 0 ? 0.3 : 0.6,
|
|
423
|
+
national_govts: 0.7,
|
|
424
|
+
citizens: questionCount > 0 ? 0.6 : 0.4,
|
|
425
|
+
eu_institutions: 0.8,
|
|
426
|
+
},
|
|
427
|
+
confidence: eventCount > 0 ? 'medium' : 'low',
|
|
428
|
+
},
|
|
429
|
+
]),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Build deep analysis for breaking news articles.
|
|
434
|
+
*
|
|
435
|
+
* @param date - Publication date
|
|
436
|
+
* @param feedData - EP feed data
|
|
437
|
+
* @param anomalyRaw - Raw anomaly text
|
|
438
|
+
* @param coalitionRaw - Raw coalition text
|
|
439
|
+
* @param lang - Target display language (default: 'en')
|
|
440
|
+
* @returns Deep analysis object
|
|
441
|
+
*/
|
|
442
|
+
export function buildBreakingAnalysis(date, feedData, anomalyRaw, coalitionRaw, lang = 'en') {
|
|
443
|
+
const adoptedCount = feedData?.adoptedTexts.length ?? 0;
|
|
444
|
+
const eventCount = feedData?.events.length ?? 0;
|
|
445
|
+
const procCount = feedData?.procedures.length ?? 0;
|
|
446
|
+
const mepCount = feedData?.mepUpdates.length ?? 0;
|
|
447
|
+
const s = getLocalizedString(BREAKING_STRINGS, lang);
|
|
448
|
+
return {
|
|
449
|
+
what: s.breakingWhatFn(date, adoptedCount, eventCount, procCount, mepCount),
|
|
450
|
+
who: [
|
|
451
|
+
...(feedData?.adoptedTexts
|
|
452
|
+
.slice(0, 3)
|
|
453
|
+
.map((t) => `${s.breakingAdoptedPrefix} ${t.title}${t.date ? ` (${t.date})` : ''}`) ?? []),
|
|
454
|
+
...(feedData?.mepUpdates
|
|
455
|
+
.slice(0, 2)
|
|
456
|
+
.map((m) => `${s.breakingMEPPrefix} ${m.name}${m.date ? ` (${m.date})` : ''}`) ?? []),
|
|
457
|
+
],
|
|
458
|
+
when: [
|
|
459
|
+
`${date}`,
|
|
460
|
+
...(feedData?.events.slice(0, 3).map((e) => `${e.title}${e.date ? ` (${e.date})` : ''}`) ??
|
|
461
|
+
[]),
|
|
462
|
+
],
|
|
463
|
+
why: anomalyRaw ? s.breakingWhyAnomalies : s.breakingWhyNormal,
|
|
464
|
+
stakeholderOutcomes: [
|
|
465
|
+
...(adoptedCount > 0
|
|
466
|
+
? [
|
|
467
|
+
{
|
|
468
|
+
actor: s.breakingWinnerActor,
|
|
469
|
+
outcome: 'winner',
|
|
470
|
+
reason: s.breakingWinnerReasonFn(adoptedCount),
|
|
471
|
+
},
|
|
472
|
+
]
|
|
473
|
+
: []),
|
|
474
|
+
...(coalitionRaw
|
|
475
|
+
? [
|
|
476
|
+
{
|
|
477
|
+
actor: s.breakingNeutralActor,
|
|
478
|
+
outcome: 'neutral',
|
|
479
|
+
reason: s.breakingNeutralReason,
|
|
480
|
+
},
|
|
481
|
+
]
|
|
482
|
+
: []),
|
|
483
|
+
],
|
|
484
|
+
impactAssessment: {
|
|
485
|
+
political: anomalyRaw
|
|
486
|
+
? s.breakingImpactPoliticalAnomalies
|
|
487
|
+
: s.breakingImpactPoliticalNormalFn(adoptedCount),
|
|
488
|
+
economic: s.breakingImpactEconomic,
|
|
489
|
+
social: s.breakingImpactSocial,
|
|
490
|
+
legal: s.breakingImpactLegalFn(adoptedCount),
|
|
491
|
+
geopolitical: coalitionRaw
|
|
492
|
+
? s.breakingImpactGeopoliticalCoalition
|
|
493
|
+
: s.breakingImpactGeopoliticalNormal,
|
|
494
|
+
},
|
|
495
|
+
actionConsequences: [
|
|
496
|
+
...(feedData?.adoptedTexts.slice(0, 2).map((t) => ({
|
|
497
|
+
action: `${s.breakingAdoptedPrefix} "${t.title}"${t.date ? ` (${t.date})` : ''}`,
|
|
498
|
+
consequence: s.breakingLegalObligationsConsequence,
|
|
499
|
+
severity: 'high',
|
|
500
|
+
})) ?? []),
|
|
501
|
+
...(feedData?.procedures.slice(0, 2).map((p) => ({
|
|
502
|
+
action: `${p.title}${p.date ? ` (${p.date})` : ''}`,
|
|
503
|
+
consequence: s.breakingProcedureConsequence,
|
|
504
|
+
severity: 'medium',
|
|
505
|
+
})) ?? []),
|
|
506
|
+
],
|
|
507
|
+
mistakes: anomalyRaw
|
|
508
|
+
? [
|
|
509
|
+
{
|
|
510
|
+
actor: s.breakingMistakeActor,
|
|
511
|
+
description: s.breakingMistakeDescription,
|
|
512
|
+
alternative: s.breakingMistakeAlternative,
|
|
513
|
+
},
|
|
514
|
+
]
|
|
515
|
+
: [],
|
|
516
|
+
outlook: adoptedCount > 0 ? s.breakingOutlookActiveFn(date) : s.breakingOutlookTransitionalFn(date),
|
|
517
|
+
stakeholderPerspectives: buildBreakingStakeholderPerspectives(adoptedCount, feedData?.adoptedTexts[0]?.title ?? `EP activity ${date}`),
|
|
518
|
+
stakeholderOutcomeMatrix: buildOutcomeMatrix([
|
|
519
|
+
{
|
|
520
|
+
action: `EP breaking news ${date}`,
|
|
521
|
+
scores: {
|
|
522
|
+
political_groups: 0.9,
|
|
523
|
+
civil_society: adoptedCount > 0 ? 0.6 : 0.4,
|
|
524
|
+
industry: adoptedCount > 0 ? 0.7 : 0.4,
|
|
525
|
+
national_govts: 0.7,
|
|
526
|
+
citizens: adoptedCount > 0 ? 0.6 : 0.3,
|
|
527
|
+
eu_institutions: 0.9,
|
|
528
|
+
},
|
|
529
|
+
confidence: adoptedCount > 0 ? 'high' : 'medium',
|
|
530
|
+
},
|
|
531
|
+
]),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Classify pipeline health status.
|
|
536
|
+
*
|
|
537
|
+
* @param score - Health score between 0 and 1
|
|
538
|
+
* @returns Human-readable health label
|
|
539
|
+
*/
|
|
540
|
+
function pipelineHealthLabel(score) {
|
|
541
|
+
if (score > 0.7)
|
|
542
|
+
return 'strong';
|
|
543
|
+
if (score > 0.4)
|
|
544
|
+
return 'moderate';
|
|
545
|
+
return 'weak';
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Build the "why" explanation for propositions based on pipeline health.
|
|
549
|
+
*
|
|
550
|
+
* @param healthScore - 0-1 score
|
|
551
|
+
* @param throughput - Throughput rate
|
|
552
|
+
* @returns Explanation string
|
|
553
|
+
*/
|
|
554
|
+
function buildPropositionsWhy(healthScore, throughput) {
|
|
555
|
+
const pct = (healthScore * 100).toFixed(0);
|
|
556
|
+
if (healthScore < 0.5) {
|
|
557
|
+
return `Pipeline health at ${pct}% signals legislative congestion. Low throughput (${throughput}) suggests inter-institutional negotiations are stalling, with knock-on effects for the legislative cycle.`;
|
|
558
|
+
}
|
|
559
|
+
const quality = healthScore > 0.7 ? 'healthy' : 'moderate';
|
|
560
|
+
return `Pipeline health at ${pct}% with throughput ${throughput} indicates ${quality} legislative progress. The co-decision process is functioning within normal parameters.`;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Localized names for the EP Conference of Presidents across supported languages.
|
|
564
|
+
* Used to translate the actor name in the propositions deep-analysis mistake card.
|
|
565
|
+
*/
|
|
566
|
+
const CONFERENCE_OF_PRESIDENTS_EN = 'Conference of Presidents';
|
|
567
|
+
const CONFERENCE_OF_PRESIDENTS = {
|
|
568
|
+
en: CONFERENCE_OF_PRESIDENTS_EN,
|
|
569
|
+
sv: 'Presidentkonferensen',
|
|
570
|
+
da: 'Formandskabskonferencen',
|
|
571
|
+
no: 'Presidentkonferansen',
|
|
572
|
+
fi: 'Puheenjohtajakonferenssi',
|
|
573
|
+
de: 'Konferenz der Präsidenten',
|
|
574
|
+
fr: 'Conférence des présidents',
|
|
575
|
+
es: 'Conferencia de Presidentes',
|
|
576
|
+
nl: 'Conferentie van voorzitters',
|
|
577
|
+
ar: 'مؤتمر الرؤساء',
|
|
578
|
+
he: 'ועידת הנשיאים',
|
|
579
|
+
ja: '議長会議',
|
|
580
|
+
ko: '의장단 회의',
|
|
581
|
+
zh: '主席团会议',
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* Get the localized Conference of Presidents name.
|
|
585
|
+
*
|
|
586
|
+
* @param lang - Target language code
|
|
587
|
+
* @returns Localized name or English fallback
|
|
588
|
+
*/
|
|
589
|
+
function getConferenceOfPresidents(lang) {
|
|
590
|
+
if (!Object.hasOwn(CONFERENCE_OF_PRESIDENTS, lang))
|
|
591
|
+
return CONFERENCE_OF_PRESIDENTS_EN;
|
|
592
|
+
// eslint-disable-next-line security/detect-object-injection -- key validated via Object.hasOwn
|
|
593
|
+
return CONFERENCE_OF_PRESIDENTS[lang] ?? CONFERENCE_OF_PRESIDENTS_EN;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Build the action-consequence pairs for propositions analysis.
|
|
597
|
+
*
|
|
598
|
+
* @param pct - Pipeline health percentage as string
|
|
599
|
+
* @param healthScore - Pipeline health score (0-1)
|
|
600
|
+
* @param throughput - Throughput rate
|
|
601
|
+
* @returns Action-consequence pairs
|
|
602
|
+
*/
|
|
603
|
+
function buildPropositionsConsequences(pct, healthScore, throughput) {
|
|
604
|
+
const healthConsequence = healthScore < 0.5
|
|
605
|
+
? 'Risk of legislative session overrun; may force prioritisation and file abandonment'
|
|
606
|
+
: 'Sustainable pace; Parliament can accommodate new files without delay';
|
|
607
|
+
const healthSeverity = healthScore < 0.3 ? 'critical' : healthScore < 0.5 ? 'high' : 'medium';
|
|
608
|
+
const throughputConsequence = throughput < 5
|
|
609
|
+
? 'Slow processing reduces legislative output and postpones policy implementation'
|
|
610
|
+
: 'Healthy throughput enables timely delivery of policy commitments';
|
|
611
|
+
return [
|
|
612
|
+
{
|
|
613
|
+
action: `Pipeline health at ${pct}%`,
|
|
614
|
+
consequence: healthConsequence,
|
|
615
|
+
severity: healthSeverity,
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
action: `Throughput rate at ${throughput}`,
|
|
619
|
+
consequence: throughputConsequence,
|
|
620
|
+
severity: throughput < 5 ? 'high' : 'low',
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Build the impact assessment for propositions analysis.
|
|
626
|
+
*
|
|
627
|
+
* @param healthScore - Pipeline health score (0-1)
|
|
628
|
+
* @param throughput - Throughput rate
|
|
629
|
+
* @returns Impact assessment object
|
|
630
|
+
*/
|
|
631
|
+
function buildPropositionsImpact(healthScore, throughput) {
|
|
632
|
+
const politicalTail = healthScore < 0.5
|
|
633
|
+
? 'Current congestion benefits status-quo defenders.'
|
|
634
|
+
: 'Current pace favours reform-oriented groups.';
|
|
635
|
+
const legalText = throughput > 0
|
|
636
|
+
? `${throughput} procedures at various stages create a complex legal landscape. Overlapping implementation timelines may strain member state transposition capacity.`
|
|
637
|
+
: 'Legislative procedures at various stages create a complex legal landscape. Overlapping implementation timelines may strain member state transposition capacity.';
|
|
638
|
+
return {
|
|
639
|
+
political: `Legislative throughput affects each political group's ability to deliver on manifesto commitments. ${politicalTail}`,
|
|
640
|
+
economic: 'Pending legislation on digital markets, sustainability reporting, and fiscal governance carries significant economic implications for EU businesses.',
|
|
641
|
+
social: 'Citizens await legislative outcomes on healthcare, education, and social protection proposals currently in the pipeline.',
|
|
642
|
+
legal: legalText,
|
|
643
|
+
geopolitical: 'Trade, foreign aid, and sanctions-related proposals in the pipeline affect EU positioning in international negotiations.',
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Build the primary stakeholder outcome for propositions analysis.
|
|
648
|
+
*
|
|
649
|
+
* @param healthScore - Pipeline health score (0-1)
|
|
650
|
+
* @param pct - Pipeline health percentage as string
|
|
651
|
+
* @returns Single stakeholder outcome
|
|
652
|
+
*/
|
|
653
|
+
function buildPropositionsStakeholderOutcome(healthScore, pct) {
|
|
654
|
+
if (healthScore > 0.7) {
|
|
655
|
+
return {
|
|
656
|
+
actor: 'Parliament presidency',
|
|
657
|
+
outcome: 'winner',
|
|
658
|
+
reason: `High pipeline health (${pct}%) demonstrates effective legislative management`,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
actor: 'Pending legislation sponsors',
|
|
663
|
+
outcome: 'loser',
|
|
664
|
+
reason: `Low pipeline health (${pct}%) means delays and potential session carry-overs`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Build deep analysis for propositions articles.
|
|
669
|
+
*
|
|
670
|
+
* @param proposalsHtml - Proposals HTML (used to detect content presence)
|
|
671
|
+
* @param pipelineData - Pipeline metrics
|
|
672
|
+
* @param date - Publication date
|
|
673
|
+
* @param lang - Target display language (default: 'en')
|
|
674
|
+
* @param adoptedTextsHtml - Adopted texts HTML (also used to detect content presence)
|
|
675
|
+
* @returns Deep analysis object
|
|
676
|
+
*/
|
|
677
|
+
export function buildPropositionsAnalysis(proposalsHtml, pipelineData, date, lang = 'en', adoptedTextsHtml = '') {
|
|
678
|
+
const hasProposals = proposalsHtml.length > 0 || adoptedTextsHtml.length > 0;
|
|
679
|
+
const healthScore = pipelineData?.healthScore ?? 0;
|
|
680
|
+
const throughput = pipelineData?.throughput ?? 0;
|
|
681
|
+
const pct = (healthScore * 100).toFixed(0);
|
|
682
|
+
return {
|
|
683
|
+
what: `Legislative pipeline assessment as of ${date}: Health score ${pct}%, throughput rate ${throughput}. ${hasProposals ? 'Active proposals under consideration.' : 'No new proposals detected in this period.'}`,
|
|
684
|
+
who: [
|
|
685
|
+
'European Commission (proposal originator)',
|
|
686
|
+
'Rapporteurs (responsible for steering through committee)',
|
|
687
|
+
'Shadow rapporteurs (political group negotiators)',
|
|
688
|
+
'Council of the EU (co-legislator)',
|
|
689
|
+
],
|
|
690
|
+
when: [`Assessment date: ${date}`, 'Pipeline health reflects cumulative legislative progress'],
|
|
691
|
+
why: buildPropositionsWhy(healthScore, throughput),
|
|
692
|
+
stakeholderOutcomes: [buildPropositionsStakeholderOutcome(healthScore, pct)],
|
|
693
|
+
impactAssessment: buildPropositionsImpact(healthScore, throughput),
|
|
694
|
+
actionConsequences: buildPropositionsConsequences(pct, healthScore, throughput),
|
|
695
|
+
mistakes: healthScore < 0.5
|
|
696
|
+
? [
|
|
697
|
+
{
|
|
698
|
+
actor: getConferenceOfPresidents(lang),
|
|
699
|
+
description: `Pipeline health dropped to ${pct}% — legislative agenda may be overloaded`,
|
|
700
|
+
alternative: 'Prioritise flagship files and defer low-priority proposals to maintain pipeline flow',
|
|
701
|
+
},
|
|
702
|
+
]
|
|
703
|
+
: [],
|
|
704
|
+
outlook: `The legislative pipeline's ${pipelineHealthLabel(healthScore)} health will determine whether current proposals reach plenary before session breaks. Key trilogues and committee votes in the coming weeks will be decisive.`,
|
|
705
|
+
stakeholderPerspectives: buildPropositionsStakeholderPerspectives(healthScore, `legislative pipeline as of ${date}`),
|
|
706
|
+
stakeholderOutcomeMatrix: buildOutcomeMatrix([
|
|
707
|
+
{
|
|
708
|
+
action: `Pipeline health at ${pct}% (throughput ${throughput})`,
|
|
709
|
+
scores: {
|
|
710
|
+
political_groups: 0.7,
|
|
711
|
+
civil_society: healthScore < 0.5 ? 0.3 : 0.5,
|
|
712
|
+
industry: healthScore < 0.5 ? 0.3 : 0.6,
|
|
713
|
+
national_govts: healthScore < 0.5 ? 0.3 : 0.6,
|
|
714
|
+
citizens: healthScore < 0.5 ? 0.2 : 0.5,
|
|
715
|
+
eu_institutions: 0.8,
|
|
716
|
+
},
|
|
717
|
+
confidence: pipelineData !== null ? 'high' : 'low',
|
|
718
|
+
},
|
|
719
|
+
]),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Build deep analysis for committee reports articles.
|
|
724
|
+
*
|
|
725
|
+
* @param committees - Committee data list
|
|
726
|
+
* @param date - Publication date
|
|
727
|
+
* @param lang - Target language code for localized content
|
|
728
|
+
* @returns Deep analysis object, or `null` when all committee data is placeholder
|
|
729
|
+
*/
|
|
730
|
+
export function buildCommitteeAnalysis(committees, date, lang = 'en') {
|
|
731
|
+
if (isPlaceholderCommitteeData(committees))
|
|
732
|
+
return null;
|
|
733
|
+
const totalDocs = committees.reduce((sum, c) => sum + c.documents.length, 0);
|
|
734
|
+
const activeCommittees = committees.filter((c) => c.documents.length > 0);
|
|
735
|
+
const s = getLocalizedString(COMMITTEE_ANALYSIS_CONTENT_STRINGS, lang);
|
|
736
|
+
const pct = ((activeCommittees.length / Math.max(committees.length, 1)) * 100).toFixed(0);
|
|
737
|
+
const descriptor = activeCommittees.length === 0
|
|
738
|
+
? s.productivityLow
|
|
739
|
+
: committees.length > 0 && activeCommittees.length >= committees.length * 0.7
|
|
740
|
+
? s.productivityRobust
|
|
741
|
+
: s.productivityModerate;
|
|
742
|
+
return {
|
|
743
|
+
what: totalDocs === 0
|
|
744
|
+
? s.whatNoData.replace('{date}', date).replace('{total}', String(committees.length))
|
|
745
|
+
: s.what
|
|
746
|
+
.replace('{date}', date)
|
|
747
|
+
.replace('{total}', String(committees.length))
|
|
748
|
+
.replace('{docs}', String(totalDocs))
|
|
749
|
+
.replace('{active}', String(activeCommittees.length)),
|
|
750
|
+
who: committees.map((c) => `${c.name} (${c.abbreviation}) — ${s.chairLabel} ${c.chair}, ${c.members} ${s.membersLabel}`),
|
|
751
|
+
when: [
|
|
752
|
+
`${s.reportDateLabel} ${date}`,
|
|
753
|
+
...committees
|
|
754
|
+
.slice(0, 3)
|
|
755
|
+
.flatMap((c) => c.documents
|
|
756
|
+
.slice(0, 1)
|
|
757
|
+
.map((d) => `${c.abbreviation}: ${d.title}${d.date ? ` (${d.date})` : ''}`)),
|
|
758
|
+
],
|
|
759
|
+
why: s.why.replace('{pct}', pct).replace('{descriptor}', descriptor),
|
|
760
|
+
stakeholderOutcomes: committees.slice(0, 4).map((c) => ({
|
|
761
|
+
actor: `${c.name} (${c.abbreviation})`,
|
|
762
|
+
outcome: (c.documents.length > 2
|
|
763
|
+
? 'winner'
|
|
764
|
+
: c.documents.length > 0
|
|
765
|
+
? 'neutral'
|
|
766
|
+
: 'loser'),
|
|
767
|
+
reason: c.documents.length > 2
|
|
768
|
+
? s.stakeholderHighlyProductive.replace('{n}', String(c.documents.length))
|
|
769
|
+
: c.documents.length > 0
|
|
770
|
+
? s.stakeholderModerateActivity.replace('{n}', String(c.documents.length))
|
|
771
|
+
: s.stakeholderNoDocs,
|
|
772
|
+
})),
|
|
773
|
+
impactAssessment: {
|
|
774
|
+
political: activeCommittees.length === 0
|
|
775
|
+
? s.impactPoliticalNone
|
|
776
|
+
: s.impactPolitical
|
|
777
|
+
.replace('{active}', String(activeCommittees.length))
|
|
778
|
+
.replace('{total}', String(committees.length)),
|
|
779
|
+
economic: s.impactEconomic,
|
|
780
|
+
social: s.impactSocial,
|
|
781
|
+
legal: s.impactLegal.replace('{docs}', String(totalDocs)),
|
|
782
|
+
geopolitical: s.impactGeopolitical,
|
|
783
|
+
},
|
|
784
|
+
actionConsequences: activeCommittees.slice(0, 3).map((c) => ({
|
|
785
|
+
action: s.actionProcessed
|
|
786
|
+
.replace('{abbr}', c.abbreviation)
|
|
787
|
+
.replace('{n}', String(c.documents.length)),
|
|
788
|
+
consequence: s.actionConsequence,
|
|
789
|
+
severity: (c.documents.length > 3 ? 'high' : 'medium'),
|
|
790
|
+
})),
|
|
791
|
+
mistakes: committees
|
|
792
|
+
.filter((c) => c.documents.length === 0)
|
|
793
|
+
.slice(0, 2)
|
|
794
|
+
.map((c) => ({
|
|
795
|
+
actor: `${c.name} (${c.abbreviation})`,
|
|
796
|
+
description: s.mistakeDescription,
|
|
797
|
+
alternative: s.mistakeAlternative,
|
|
798
|
+
})),
|
|
799
|
+
outlook: committees.length > 0 && activeCommittees.length >= committees.length * 0.7
|
|
800
|
+
? s.outlookGood
|
|
801
|
+
.replace('{n}', String(activeCommittees.length))
|
|
802
|
+
.replace('{total}', String(committees.length))
|
|
803
|
+
: s.outlookConcern,
|
|
804
|
+
stakeholderPerspectives: buildCommitteeStakeholderPerspectives(Number(pct), totalDocs, committees[0]?.name ?? 'EP committees'),
|
|
805
|
+
stakeholderOutcomeMatrix: buildOutcomeMatrix([
|
|
806
|
+
{
|
|
807
|
+
action: `Committee activity as of ${date} (${activeCommittees.length}/${committees.length} active)`,
|
|
808
|
+
scores: {
|
|
809
|
+
political_groups: Number(pct) > 70 ? 0.8 : 0.5,
|
|
810
|
+
civil_society: totalDocs > 5 ? 0.6 : 0.4,
|
|
811
|
+
industry: totalDocs > 5 ? 0.7 : 0.4,
|
|
812
|
+
national_govts: Number(pct) > 70 ? 0.7 : 0.4,
|
|
813
|
+
citizens: totalDocs > 5 ? 0.5 : 0.3,
|
|
814
|
+
eu_institutions: 0.8,
|
|
815
|
+
},
|
|
816
|
+
confidence: committees.length > 0 ? 'high' : 'low',
|
|
817
|
+
},
|
|
818
|
+
]),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
// ─── SWOT builders ───────────────────────────────────────────────────────────
|
|
822
|
+
/**
|
|
823
|
+
* Build SWOT analysis for voting-based articles (motions, weekly/monthly review).
|
|
824
|
+
*
|
|
825
|
+
* @param records - Voting records
|
|
826
|
+
* @param patterns - Voting patterns
|
|
827
|
+
* @param anomalies - Detected anomalies
|
|
828
|
+
* @param lang - Target language code
|
|
829
|
+
* @returns SWOT analysis data
|
|
830
|
+
*/
|
|
831
|
+
export function buildVotingSwot(records, patterns, anomalies, lang = 'en') {
|
|
832
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
833
|
+
const realRecords = records.filter((r) => r.result !== PLACEHOLDER_MARKER);
|
|
834
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
835
|
+
const realAnomalies = anomalies.filter((a) => !/placeholder/i.test(a.type));
|
|
836
|
+
const adoptedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('adopt')).length;
|
|
837
|
+
const highCohesionGroups = realPatterns.filter((p) => p.cohesion > 0.8);
|
|
838
|
+
const lowCohesionGroups = realPatterns.filter((p) => p.cohesion < 0.5);
|
|
839
|
+
const highSeverityAnomalies = realAnomalies.filter((a) => a.severity?.toUpperCase() === 'HIGH');
|
|
840
|
+
return {
|
|
841
|
+
strengths: [
|
|
842
|
+
...(highCohesionGroups.length > 0
|
|
843
|
+
? [
|
|
844
|
+
{
|
|
845
|
+
text: s.votingHighCohesion(highCohesionGroups.length),
|
|
846
|
+
severity: 'high',
|
|
847
|
+
},
|
|
848
|
+
]
|
|
849
|
+
: []),
|
|
850
|
+
...(adoptedCount > 0
|
|
851
|
+
? [
|
|
852
|
+
{
|
|
853
|
+
text: s.votingAdopted(adoptedCount),
|
|
854
|
+
severity: 'medium',
|
|
855
|
+
},
|
|
856
|
+
]
|
|
857
|
+
: []),
|
|
858
|
+
...(realRecords.length > 0
|
|
859
|
+
? [
|
|
860
|
+
{
|
|
861
|
+
text: s.votingActiveVotes(realRecords.length),
|
|
862
|
+
severity: 'medium',
|
|
863
|
+
},
|
|
864
|
+
]
|
|
865
|
+
: []),
|
|
866
|
+
],
|
|
867
|
+
weaknesses: [
|
|
868
|
+
...(lowCohesionGroups.length > 0
|
|
869
|
+
? [
|
|
870
|
+
{
|
|
871
|
+
text: s.votingLowCohesion(lowCohesionGroups.length),
|
|
872
|
+
severity: 'high',
|
|
873
|
+
},
|
|
874
|
+
]
|
|
875
|
+
: []),
|
|
876
|
+
...(realAnomalies.length > 0
|
|
877
|
+
? [
|
|
878
|
+
{
|
|
879
|
+
text: s.votingAnomalies(realAnomalies.length),
|
|
880
|
+
severity: 'medium',
|
|
881
|
+
},
|
|
882
|
+
]
|
|
883
|
+
: []),
|
|
884
|
+
],
|
|
885
|
+
opportunities: [
|
|
886
|
+
{
|
|
887
|
+
text: s.votingCrossParty,
|
|
888
|
+
severity: 'medium',
|
|
889
|
+
},
|
|
890
|
+
...(realPatterns.length > 0
|
|
891
|
+
? [
|
|
892
|
+
{
|
|
893
|
+
text: s.votingDiverseGroups(realPatterns.length),
|
|
894
|
+
severity: 'medium',
|
|
895
|
+
},
|
|
896
|
+
]
|
|
897
|
+
: []),
|
|
898
|
+
],
|
|
899
|
+
threats: [
|
|
900
|
+
...(highSeverityAnomalies.length > 0
|
|
901
|
+
? [
|
|
902
|
+
{
|
|
903
|
+
text: s.votingHighSeverity(highSeverityAnomalies.length),
|
|
904
|
+
severity: 'high',
|
|
905
|
+
},
|
|
906
|
+
]
|
|
907
|
+
: []),
|
|
908
|
+
{
|
|
909
|
+
text: s.votingShiftingAlliances,
|
|
910
|
+
severity: 'medium',
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Build SWOT analysis for week-ahead / month-ahead articles.
|
|
917
|
+
*
|
|
918
|
+
* @param weekData - Aggregated week/month data
|
|
919
|
+
* @param _label - "week" or "month" (reserved for future localisation)
|
|
920
|
+
* @param lang - Target language code
|
|
921
|
+
* @returns SWOT analysis data
|
|
922
|
+
*/
|
|
923
|
+
export function buildProspectiveSwot(weekData, _label, lang = 'en') {
|
|
924
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
925
|
+
const bottleneckCount = weekData.pipeline.filter((p) => p.bottleneck === true).length;
|
|
926
|
+
return {
|
|
927
|
+
strengths: [
|
|
928
|
+
...(weekData.events.length > 0
|
|
929
|
+
? [
|
|
930
|
+
{
|
|
931
|
+
text: s.prospectiveEvents(weekData.events.length),
|
|
932
|
+
severity: 'high',
|
|
933
|
+
},
|
|
934
|
+
]
|
|
935
|
+
: []),
|
|
936
|
+
...(weekData.committees.length > 0
|
|
937
|
+
? [
|
|
938
|
+
{
|
|
939
|
+
text: s.prospectiveCommittees(weekData.committees.length),
|
|
940
|
+
severity: 'medium',
|
|
941
|
+
},
|
|
942
|
+
]
|
|
943
|
+
: []),
|
|
944
|
+
],
|
|
945
|
+
weaknesses: [
|
|
946
|
+
...(bottleneckCount > 0
|
|
947
|
+
? [
|
|
948
|
+
{
|
|
949
|
+
text: s.prospectiveBottlenecks(bottleneckCount),
|
|
950
|
+
severity: 'high',
|
|
951
|
+
},
|
|
952
|
+
]
|
|
953
|
+
: []),
|
|
954
|
+
...(weekData.events.length > 5
|
|
955
|
+
? [
|
|
956
|
+
{
|
|
957
|
+
text: s.prospectiveHighDensity(weekData.events.length),
|
|
958
|
+
severity: 'medium',
|
|
959
|
+
},
|
|
960
|
+
]
|
|
961
|
+
: []),
|
|
962
|
+
],
|
|
963
|
+
opportunities: [
|
|
964
|
+
...(weekData.documents.length > 0
|
|
965
|
+
? [
|
|
966
|
+
{
|
|
967
|
+
text: s.prospectiveDocuments(weekData.documents.length),
|
|
968
|
+
severity: 'medium',
|
|
969
|
+
},
|
|
970
|
+
]
|
|
971
|
+
: []),
|
|
972
|
+
...(weekData.questions.length > 0
|
|
973
|
+
? [
|
|
974
|
+
{
|
|
975
|
+
text: s.prospectiveQuestions(weekData.questions.length),
|
|
976
|
+
severity: 'medium',
|
|
977
|
+
},
|
|
978
|
+
]
|
|
979
|
+
: []),
|
|
980
|
+
],
|
|
981
|
+
threats: [
|
|
982
|
+
...(bottleneckCount > 0
|
|
983
|
+
? [
|
|
984
|
+
{
|
|
985
|
+
text: s.prospectiveBottleneckRisk,
|
|
986
|
+
severity: 'high',
|
|
987
|
+
},
|
|
988
|
+
]
|
|
989
|
+
: []),
|
|
990
|
+
{
|
|
991
|
+
text: s.prospectiveSchedulingRisk,
|
|
992
|
+
severity: 'medium',
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Build SWOT analysis for breaking news articles.
|
|
999
|
+
*
|
|
1000
|
+
* @param feedData - EP feed data
|
|
1001
|
+
* @param anomalyRaw - Raw anomaly text
|
|
1002
|
+
* @param coalitionRaw - Raw coalition text
|
|
1003
|
+
* @param lang - Target language code
|
|
1004
|
+
* @returns SWOT analysis data
|
|
1005
|
+
*/
|
|
1006
|
+
export function buildBreakingSwot(feedData, anomalyRaw, coalitionRaw, lang = 'en') {
|
|
1007
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
1008
|
+
const adoptedCount = feedData?.adoptedTexts.length ?? 0;
|
|
1009
|
+
const eventCount = feedData?.events.length ?? 0;
|
|
1010
|
+
const procCount = feedData?.procedures.length ?? 0;
|
|
1011
|
+
return {
|
|
1012
|
+
strengths: [
|
|
1013
|
+
...(adoptedCount > 0
|
|
1014
|
+
? [
|
|
1015
|
+
{
|
|
1016
|
+
text: s.breakingAdopted(adoptedCount),
|
|
1017
|
+
severity: 'high',
|
|
1018
|
+
},
|
|
1019
|
+
]
|
|
1020
|
+
: []),
|
|
1021
|
+
...(eventCount > 0
|
|
1022
|
+
? [
|
|
1023
|
+
{
|
|
1024
|
+
text: s.breakingEvents(eventCount),
|
|
1025
|
+
severity: 'medium',
|
|
1026
|
+
},
|
|
1027
|
+
]
|
|
1028
|
+
: []),
|
|
1029
|
+
],
|
|
1030
|
+
weaknesses: [
|
|
1031
|
+
...(anomalyRaw
|
|
1032
|
+
? [
|
|
1033
|
+
{
|
|
1034
|
+
text: s.breakingAnomalyWeakness,
|
|
1035
|
+
severity: 'high',
|
|
1036
|
+
},
|
|
1037
|
+
]
|
|
1038
|
+
: []),
|
|
1039
|
+
...(procCount === 0
|
|
1040
|
+
? [
|
|
1041
|
+
{
|
|
1042
|
+
text: s.breakingNoProcedures,
|
|
1043
|
+
severity: 'medium',
|
|
1044
|
+
},
|
|
1045
|
+
]
|
|
1046
|
+
: []),
|
|
1047
|
+
],
|
|
1048
|
+
opportunities: [
|
|
1049
|
+
...(procCount > 0
|
|
1050
|
+
? [
|
|
1051
|
+
{
|
|
1052
|
+
text: s.breakingProceduresActive(procCount),
|
|
1053
|
+
severity: 'medium',
|
|
1054
|
+
},
|
|
1055
|
+
]
|
|
1056
|
+
: []),
|
|
1057
|
+
...(coalitionRaw
|
|
1058
|
+
? [
|
|
1059
|
+
{
|
|
1060
|
+
text: s.breakingCoalitionOpportunity,
|
|
1061
|
+
severity: 'medium',
|
|
1062
|
+
},
|
|
1063
|
+
]
|
|
1064
|
+
: []),
|
|
1065
|
+
],
|
|
1066
|
+
threats: [
|
|
1067
|
+
...(anomalyRaw
|
|
1068
|
+
? [
|
|
1069
|
+
{
|
|
1070
|
+
text: s.breakingAnomalyThreat,
|
|
1071
|
+
severity: 'high',
|
|
1072
|
+
},
|
|
1073
|
+
]
|
|
1074
|
+
: []),
|
|
1075
|
+
{
|
|
1076
|
+
text: s.breakingRapidEvents,
|
|
1077
|
+
severity: 'medium',
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Build SWOT analysis for propositions articles.
|
|
1084
|
+
*
|
|
1085
|
+
* @param pipelineData - Pipeline metrics
|
|
1086
|
+
* @param lang - Target language code
|
|
1087
|
+
* @returns SWOT analysis data
|
|
1088
|
+
*/
|
|
1089
|
+
export function buildPropositionsSwot(pipelineData, lang = 'en') {
|
|
1090
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
1091
|
+
const healthScore = pipelineData?.healthScore ?? 0;
|
|
1092
|
+
const throughput = pipelineData?.throughput ?? 0;
|
|
1093
|
+
const pct = (healthScore * 100).toFixed(0);
|
|
1094
|
+
return {
|
|
1095
|
+
strengths: [
|
|
1096
|
+
...(healthScore > 0.7
|
|
1097
|
+
? [
|
|
1098
|
+
{
|
|
1099
|
+
text: s.propositionsHealthStrong(pct),
|
|
1100
|
+
severity: 'high',
|
|
1101
|
+
},
|
|
1102
|
+
]
|
|
1103
|
+
: []),
|
|
1104
|
+
...(throughput >= 5
|
|
1105
|
+
? [
|
|
1106
|
+
{
|
|
1107
|
+
text: s.propositionsThroughputGood(throughput),
|
|
1108
|
+
severity: 'medium',
|
|
1109
|
+
},
|
|
1110
|
+
]
|
|
1111
|
+
: []),
|
|
1112
|
+
],
|
|
1113
|
+
weaknesses: [
|
|
1114
|
+
...(healthScore < 0.5
|
|
1115
|
+
? [
|
|
1116
|
+
{
|
|
1117
|
+
text: s.propositionsHealthWeak(pct),
|
|
1118
|
+
severity: 'high',
|
|
1119
|
+
},
|
|
1120
|
+
]
|
|
1121
|
+
: []),
|
|
1122
|
+
...(throughput < 5
|
|
1123
|
+
? [
|
|
1124
|
+
{
|
|
1125
|
+
text: s.propositionsThroughputLow(throughput),
|
|
1126
|
+
severity: 'medium',
|
|
1127
|
+
},
|
|
1128
|
+
]
|
|
1129
|
+
: []),
|
|
1130
|
+
],
|
|
1131
|
+
opportunities: [
|
|
1132
|
+
{
|
|
1133
|
+
text: s.propositionsPrioritisation,
|
|
1134
|
+
severity: 'medium',
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
text: s.propositionsTrilogueAcceleration,
|
|
1138
|
+
severity: 'medium',
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
threats: [
|
|
1142
|
+
...(healthScore < 0.3
|
|
1143
|
+
? [
|
|
1144
|
+
{
|
|
1145
|
+
text: s.propositionsCriticalCongestion,
|
|
1146
|
+
severity: 'high',
|
|
1147
|
+
},
|
|
1148
|
+
]
|
|
1149
|
+
: []),
|
|
1150
|
+
{
|
|
1151
|
+
text: s.propositionsOverlapping,
|
|
1152
|
+
severity: 'medium',
|
|
1153
|
+
},
|
|
1154
|
+
],
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Build SWOT analysis for committee reports articles.
|
|
1159
|
+
*
|
|
1160
|
+
* @param committees - Committee data list
|
|
1161
|
+
* @param lang - Target language code
|
|
1162
|
+
* @returns SWOT analysis data, or `null` when all committee data is placeholder
|
|
1163
|
+
*/
|
|
1164
|
+
export function buildCommitteeSwot(committees, lang = 'en') {
|
|
1165
|
+
if (isPlaceholderCommitteeData(committees))
|
|
1166
|
+
return null;
|
|
1167
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
1168
|
+
const activeCommittees = committees.filter((c) => c.documents.length > 0);
|
|
1169
|
+
const totalDocs = committees.reduce((sum, c) => sum + c.documents.length, 0);
|
|
1170
|
+
const inactiveCount = committees.length - activeCommittees.length;
|
|
1171
|
+
return {
|
|
1172
|
+
strengths: [
|
|
1173
|
+
...(activeCommittees.length > 0
|
|
1174
|
+
? [
|
|
1175
|
+
{
|
|
1176
|
+
text: s.committeeActive(activeCommittees.length, committees.length),
|
|
1177
|
+
severity: activeCommittees.length >= committees.length * 0.7
|
|
1178
|
+
? 'high'
|
|
1179
|
+
: 'medium',
|
|
1180
|
+
},
|
|
1181
|
+
]
|
|
1182
|
+
: []),
|
|
1183
|
+
...(totalDocs > 0
|
|
1184
|
+
? [
|
|
1185
|
+
{
|
|
1186
|
+
text: s.committeeDocuments(totalDocs),
|
|
1187
|
+
severity: 'medium',
|
|
1188
|
+
},
|
|
1189
|
+
]
|
|
1190
|
+
: []),
|
|
1191
|
+
],
|
|
1192
|
+
weaknesses: [
|
|
1193
|
+
...(inactiveCount > 0
|
|
1194
|
+
? [
|
|
1195
|
+
{
|
|
1196
|
+
text: s.committeeInactive(inactiveCount),
|
|
1197
|
+
severity: inactiveCount > committees.length * 0.3 ? 'high' : 'medium',
|
|
1198
|
+
},
|
|
1199
|
+
]
|
|
1200
|
+
: []),
|
|
1201
|
+
],
|
|
1202
|
+
opportunities: [
|
|
1203
|
+
{
|
|
1204
|
+
text: s.committeeCrossCollaboration,
|
|
1205
|
+
severity: 'medium',
|
|
1206
|
+
},
|
|
1207
|
+
...(committees.length > 0
|
|
1208
|
+
? [
|
|
1209
|
+
{
|
|
1210
|
+
text: s.committeeHearings,
|
|
1211
|
+
severity: 'medium',
|
|
1212
|
+
},
|
|
1213
|
+
]
|
|
1214
|
+
: []),
|
|
1215
|
+
],
|
|
1216
|
+
threats: [
|
|
1217
|
+
...(inactiveCount > committees.length * 0.3
|
|
1218
|
+
? [
|
|
1219
|
+
{
|
|
1220
|
+
text: s.committeeLowActivity,
|
|
1221
|
+
severity: 'high',
|
|
1222
|
+
},
|
|
1223
|
+
]
|
|
1224
|
+
: []),
|
|
1225
|
+
{
|
|
1226
|
+
text: s.committeeCompetingPriorities,
|
|
1227
|
+
severity: 'medium',
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
// ─── Dashboard builders ──────────────────────────────────────────────────────
|
|
1233
|
+
// ─── Political intelligence data builders ─────────────────────────────────────
|
|
1234
|
+
/**
|
|
1235
|
+
* Build coalition metrics from voting patterns data.
|
|
1236
|
+
* Derives alignment scores and shift indicators for the coalition radar chart.
|
|
1237
|
+
*
|
|
1238
|
+
* @param patterns - Voting pattern data
|
|
1239
|
+
* @returns Coalition metrics object or null if no real patterns
|
|
1240
|
+
*/
|
|
1241
|
+
function buildCoalitionMetricsFromPatterns(patterns) {
|
|
1242
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
1243
|
+
if (realPatterns.length === 0)
|
|
1244
|
+
return null;
|
|
1245
|
+
const avgCohesion = realPatterns.reduce((sum, p) => sum + p.cohesion, 0) / realPatterns.length;
|
|
1246
|
+
const alignmentScore = Math.round(avgCohesion * 100);
|
|
1247
|
+
// Detect shift from cohesion spread
|
|
1248
|
+
const maxCohesion = Math.max(...realPatterns.map((p) => p.cohesion));
|
|
1249
|
+
const minCohesion = Math.min(...realPatterns.map((p) => p.cohesion));
|
|
1250
|
+
const spread = maxCohesion - minCohesion;
|
|
1251
|
+
const shiftIndicator = spread > 0.3 ? 'weakening' : avgCohesion > 0.7 ? 'strengthening' : 'stable';
|
|
1252
|
+
return {
|
|
1253
|
+
alignmentScore,
|
|
1254
|
+
votingBlocs: realPatterns.slice(0, 6).map((p) => ({
|
|
1255
|
+
group: p.group,
|
|
1256
|
+
alignmentScore: Math.round(p.cohesion * 100),
|
|
1257
|
+
})),
|
|
1258
|
+
shiftIndicator,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Build legislative pipeline data from WeekAheadData.
|
|
1263
|
+
*
|
|
1264
|
+
* @param weekData - Aggregated week/month data
|
|
1265
|
+
* @returns Legislative pipeline object
|
|
1266
|
+
*/
|
|
1267
|
+
function buildPipelineFromWeekData(weekData) {
|
|
1268
|
+
const bottlenecked = weekData.pipeline.filter((p) => p.bottleneck === true).length;
|
|
1269
|
+
const total = weekData.pipeline.length;
|
|
1270
|
+
const onTrack = total - bottlenecked;
|
|
1271
|
+
const healthScore = total > 0 ? Math.round((onTrack / total) * 100) : 100;
|
|
1272
|
+
return {
|
|
1273
|
+
healthScore,
|
|
1274
|
+
onTrack,
|
|
1275
|
+
delayed: bottlenecked,
|
|
1276
|
+
blocked: 0,
|
|
1277
|
+
fastTracked: 0,
|
|
1278
|
+
total,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Build legislative pipeline data from PipelineData.
|
|
1283
|
+
*
|
|
1284
|
+
* @param pipelineData - Pipeline metrics or null
|
|
1285
|
+
* @returns Legislative pipeline object
|
|
1286
|
+
*/
|
|
1287
|
+
function buildPipelineFromPipelineData(pipelineData) {
|
|
1288
|
+
if (!pipelineData)
|
|
1289
|
+
return null;
|
|
1290
|
+
const healthScore = Math.round(pipelineData.healthScore * 100);
|
|
1291
|
+
const total = pipelineData.throughput;
|
|
1292
|
+
if (total === 0)
|
|
1293
|
+
return null;
|
|
1294
|
+
const onTrack = Math.round(total * pipelineData.healthScore);
|
|
1295
|
+
const remaining = total - onTrack;
|
|
1296
|
+
const blocked = Math.round(remaining * 0.3);
|
|
1297
|
+
const delayed = remaining - blocked;
|
|
1298
|
+
return {
|
|
1299
|
+
healthScore,
|
|
1300
|
+
onTrack,
|
|
1301
|
+
delayed,
|
|
1302
|
+
blocked,
|
|
1303
|
+
fastTracked: 0,
|
|
1304
|
+
total,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Build trend analytics from feed data counts using the last 4 items as periods.
|
|
1309
|
+
*
|
|
1310
|
+
* @param counts - Array of activity counts per period
|
|
1311
|
+
* @param period - Trend period label
|
|
1312
|
+
* @returns Trend analytics object or null if no data
|
|
1313
|
+
*/
|
|
1314
|
+
function buildTrendFromCounts(counts, period) {
|
|
1315
|
+
if (counts.length === 0 || counts.every((c) => c === 0))
|
|
1316
|
+
return null;
|
|
1317
|
+
const periodLabels = counts.map((_, i) => {
|
|
1318
|
+
if (period === 'weekly')
|
|
1319
|
+
return `W${i + 1}`;
|
|
1320
|
+
if (period === 'monthly')
|
|
1321
|
+
return `M${i + 1}`;
|
|
1322
|
+
return `Q${i + 1}`;
|
|
1323
|
+
});
|
|
1324
|
+
const metrics = counts.map((value, i) => ({ period: periodLabels[i] ?? `${i + 1}`, value }));
|
|
1325
|
+
const last = counts.at(-1) ?? 0;
|
|
1326
|
+
const prev = counts.at(-2) ?? last;
|
|
1327
|
+
const change = prev > 0 ? ((last - prev) / prev) * 100 : 0;
|
|
1328
|
+
const direction = change > 5 ? 'improving' : change < -5 ? 'declining' : 'stable';
|
|
1329
|
+
return {
|
|
1330
|
+
period,
|
|
1331
|
+
metrics,
|
|
1332
|
+
direction,
|
|
1333
|
+
weekOverWeekChange: period === 'weekly' ? Math.round(change * 10) / 10 : undefined,
|
|
1334
|
+
monthOverMonthChange: period === 'monthly' ? Math.round(change * 10) / 10 : undefined,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Build stakeholder metrics from voting patterns.
|
|
1339
|
+
*
|
|
1340
|
+
* @param patterns - Voting patterns
|
|
1341
|
+
* @param anomalyCount - Number of anomalies
|
|
1342
|
+
* @returns Stakeholder metric array
|
|
1343
|
+
*/
|
|
1344
|
+
function buildStakeholderMetricsFromVoting(patterns, anomalyCount) {
|
|
1345
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
1346
|
+
const metrics = realPatterns.slice(0, 4).map((p) => ({
|
|
1347
|
+
stakeholder: p.group,
|
|
1348
|
+
impactScore: Math.round(p.cohesion * 100),
|
|
1349
|
+
impactDirection: (p.cohesion > 0.7 ? 'positive' : p.cohesion < 0.4 ? 'negative' : 'neutral'),
|
|
1350
|
+
}));
|
|
1351
|
+
if (anomalyCount > 0) {
|
|
1352
|
+
metrics.push({
|
|
1353
|
+
stakeholder: 'Coalition stability',
|
|
1354
|
+
impactScore: Math.max(0, 100 - anomalyCount * 15),
|
|
1355
|
+
impactDirection: anomalyCount > 3 ? 'negative' : 'neutral',
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
return metrics;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Build stakeholder metrics for legislative pipeline actors.
|
|
1362
|
+
*
|
|
1363
|
+
* @param pipeline - Legislative pipeline data
|
|
1364
|
+
* @returns Stakeholder metric array
|
|
1365
|
+
*/
|
|
1366
|
+
function buildStakeholderMetricsFromPipeline(pipeline) {
|
|
1367
|
+
if (!pipeline || pipeline.total === 0)
|
|
1368
|
+
return [];
|
|
1369
|
+
return [
|
|
1370
|
+
{
|
|
1371
|
+
stakeholder: 'Legislators',
|
|
1372
|
+
impactScore: pipeline.healthScore,
|
|
1373
|
+
impactDirection: pipeline.healthScore > 70 ? 'positive' : pipeline.healthScore < 40 ? 'negative' : 'neutral',
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
stakeholder: 'Pending proposals',
|
|
1377
|
+
impactScore: pipeline.total > 0 ? Math.round((pipeline.blocked / pipeline.total) * 100) : 0,
|
|
1378
|
+
impactDirection: pipeline.blocked > 0 ? 'negative' : 'neutral',
|
|
1379
|
+
description: pipeline.blocked > 0
|
|
1380
|
+
? `${pipeline.blocked} blocked procedure${pipeline.blocked > 1 ? 's' : ''}`
|
|
1381
|
+
: undefined,
|
|
1382
|
+
},
|
|
1383
|
+
];
|
|
1384
|
+
}
|
|
1385
|
+
// ─── Dashboard builders ──────────────────────────────────────────────────────
|
|
1386
|
+
/** EP blue transparent color used for chart backgrounds */
|
|
1387
|
+
const EP_BLUE_TRANSPARENT = 'rgba(0,51,153,0.1)';
|
|
1388
|
+
/** EP blue border color used for chart lines */
|
|
1389
|
+
const EP_BLUE_BORDER = '#003399';
|
|
1390
|
+
/**
|
|
1391
|
+
* Build the coalition alignment panel for a voting dashboard.
|
|
1392
|
+
*
|
|
1393
|
+
* @param d - Localized dashboard strings
|
|
1394
|
+
* @param coalition - Coalition metrics
|
|
1395
|
+
* @returns Panel object or null
|
|
1396
|
+
*/
|
|
1397
|
+
function buildVotingCoalitionPanel(d, coalition) {
|
|
1398
|
+
if (!coalition)
|
|
1399
|
+
return null;
|
|
1400
|
+
const shiftLabel = coalition.shiftIndicator === 'strengthening'
|
|
1401
|
+
? d.coalitionStrengthening
|
|
1402
|
+
: coalition.shiftIndicator === 'weakening'
|
|
1403
|
+
? d.coalitionWeakening
|
|
1404
|
+
: d.coalitionStable;
|
|
1405
|
+
const shiftTrend = coalition.shiftIndicator === 'strengthening'
|
|
1406
|
+
? 'up'
|
|
1407
|
+
: coalition.shiftIndicator === 'weakening'
|
|
1408
|
+
? 'down'
|
|
1409
|
+
: 'stable';
|
|
1410
|
+
return {
|
|
1411
|
+
title: d.coalitionAlignment,
|
|
1412
|
+
metrics: [
|
|
1413
|
+
{ label: d.alignmentScore, value: `${coalition.alignmentScore}%`, trend: shiftTrend },
|
|
1414
|
+
{ label: d.coalitionShift, value: shiftLabel },
|
|
1415
|
+
],
|
|
1416
|
+
chart: {
|
|
1417
|
+
type: 'radar',
|
|
1418
|
+
title: d.coalitionRadarChart,
|
|
1419
|
+
data: {
|
|
1420
|
+
labels: coalition.votingBlocs.map((b) => b.group),
|
|
1421
|
+
datasets: [
|
|
1422
|
+
{
|
|
1423
|
+
label: d.alignmentScore,
|
|
1424
|
+
data: coalition.votingBlocs.map((b) => b.alignmentScore),
|
|
1425
|
+
backgroundColor: EP_BLUE_TRANSPARENT,
|
|
1426
|
+
borderColor: EP_BLUE_BORDER,
|
|
1427
|
+
},
|
|
1428
|
+
],
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Build the trend panel for a voting dashboard.
|
|
1435
|
+
*
|
|
1436
|
+
* @param d - Localized dashboard strings
|
|
1437
|
+
* @param realRecords - Filtered real voting records
|
|
1438
|
+
* @param adoptedCount - Number of adopted votes
|
|
1439
|
+
* @param rejectedCount - Number of rejected votes
|
|
1440
|
+
* @returns Panel object or null
|
|
1441
|
+
*/
|
|
1442
|
+
function buildVotingTrendPanel(d, realRecords, adoptedCount, rejectedCount) {
|
|
1443
|
+
if (realRecords.length < 2)
|
|
1444
|
+
return null;
|
|
1445
|
+
return {
|
|
1446
|
+
title: d.trendAnalysis,
|
|
1447
|
+
metrics: [
|
|
1448
|
+
{
|
|
1449
|
+
label: d.adopted,
|
|
1450
|
+
value: String(adoptedCount),
|
|
1451
|
+
trend: (adoptedCount > rejectedCount ? 'up' : 'stable'),
|
|
1452
|
+
},
|
|
1453
|
+
],
|
|
1454
|
+
chart: {
|
|
1455
|
+
type: 'line',
|
|
1456
|
+
title: d.activityTrendChart,
|
|
1457
|
+
data: {
|
|
1458
|
+
labels: realRecords.slice(0, 6).map((r) => r.date ?? ''),
|
|
1459
|
+
datasets: [
|
|
1460
|
+
{
|
|
1461
|
+
label: d.adopted,
|
|
1462
|
+
data: realRecords
|
|
1463
|
+
.slice(0, 6)
|
|
1464
|
+
.map((r) => (r.result?.toLowerCase().includes('adopt') ? 1 : 0)),
|
|
1465
|
+
borderColor: '#28a745',
|
|
1466
|
+
backgroundColor: 'rgba(40,167,69,0.1)',
|
|
1467
|
+
},
|
|
1468
|
+
],
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Build the stakeholder panel for a voting dashboard.
|
|
1475
|
+
*
|
|
1476
|
+
* @param d - Localized dashboard strings
|
|
1477
|
+
* @param patterns - Voting patterns
|
|
1478
|
+
* @param anomalyCount - Number of voting anomalies
|
|
1479
|
+
* @returns Panel object or null
|
|
1480
|
+
*/
|
|
1481
|
+
function buildVotingStakeholderPanel(d, patterns, anomalyCount) {
|
|
1482
|
+
const stakeholderMetrics = buildStakeholderMetricsFromVoting(patterns, anomalyCount);
|
|
1483
|
+
return buildStakeholderPanel(d, stakeholderMetrics);
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Build dashboard for voting-based articles (motions, weekly/monthly review).
|
|
1487
|
+
* Includes a coalition alignment radar chart and stakeholder impact scorecard.
|
|
1488
|
+
*
|
|
1489
|
+
* @param records - Voting records
|
|
1490
|
+
* @param patterns - Voting patterns
|
|
1491
|
+
* @param anomalies - Detected anomalies
|
|
1492
|
+
* @param lang - Target language code
|
|
1493
|
+
* @returns Dashboard configuration with coalition and stakeholder intelligence
|
|
1494
|
+
*/
|
|
1495
|
+
export function buildVotingDashboard(records, patterns, anomalies, lang = 'en') {
|
|
1496
|
+
const d = getLocalizedString(DASHBOARD_BUILDER_STRINGS, lang);
|
|
1497
|
+
const realRecords = records.filter((r) => r.result !== PLACEHOLDER_MARKER);
|
|
1498
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
1499
|
+
const realAnomalies = anomalies.filter((a) => !/placeholder/i.test(a.type));
|
|
1500
|
+
const adoptedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('adopt')).length;
|
|
1501
|
+
const rejectedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('reject')).length;
|
|
1502
|
+
const overviewPanel = {
|
|
1503
|
+
title: d.votingOverview,
|
|
1504
|
+
metrics: [
|
|
1505
|
+
{ label: d.totalVotes, value: String(realRecords.length), trend: 'stable' },
|
|
1506
|
+
{
|
|
1507
|
+
label: d.adopted,
|
|
1508
|
+
value: String(adoptedCount),
|
|
1509
|
+
trend: adoptedCount > 0 ? 'up' : 'stable',
|
|
1510
|
+
},
|
|
1511
|
+
{ label: d.rejected, value: String(rejectedCount) },
|
|
1512
|
+
{ label: d.anomalies, value: String(realAnomalies.length) },
|
|
1513
|
+
],
|
|
1514
|
+
};
|
|
1515
|
+
const cohesionPanel = realPatterns.length > 0
|
|
1516
|
+
? {
|
|
1517
|
+
title: d.politicalGroupCohesion,
|
|
1518
|
+
metrics: realPatterns.slice(0, 4).map((p) => ({
|
|
1519
|
+
label: p.group,
|
|
1520
|
+
value: `${(p.cohesion * 100).toFixed(0)}%`,
|
|
1521
|
+
trend: (p.cohesion > 0.8 ? 'up' : p.cohesion < 0.5 ? 'down' : 'stable'),
|
|
1522
|
+
})),
|
|
1523
|
+
chart: {
|
|
1524
|
+
type: 'bar',
|
|
1525
|
+
title: d.groupCohesionRates,
|
|
1526
|
+
data: {
|
|
1527
|
+
labels: realPatterns.slice(0, 6).map((p) => p.group),
|
|
1528
|
+
datasets: [
|
|
1529
|
+
{
|
|
1530
|
+
label: d.cohesionPct,
|
|
1531
|
+
data: realPatterns.slice(0, 6).map((p) => Math.round(p.cohesion * 100)),
|
|
1532
|
+
},
|
|
1533
|
+
],
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
}
|
|
1537
|
+
: null;
|
|
1538
|
+
const coalition = buildCoalitionMetricsFromPatterns(realPatterns);
|
|
1539
|
+
const coalitionPanel = buildVotingCoalitionPanel(d, coalition);
|
|
1540
|
+
const trendPanel = buildVotingTrendPanel(d, realRecords, adoptedCount, rejectedCount);
|
|
1541
|
+
const stakeholderPanel = buildVotingStakeholderPanel(d, realPatterns, realAnomalies.length);
|
|
1542
|
+
const panels = [
|
|
1543
|
+
overviewPanel,
|
|
1544
|
+
...(cohesionPanel ? [cohesionPanel] : []),
|
|
1545
|
+
...(coalitionPanel ? [coalitionPanel] : []),
|
|
1546
|
+
...(trendPanel ? [trendPanel] : []),
|
|
1547
|
+
...(stakeholderPanel ? [stakeholderPanel] : []),
|
|
1548
|
+
];
|
|
1549
|
+
return { panels };
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Resolve a direction label from trend direction.
|
|
1553
|
+
*
|
|
1554
|
+
* @param d - Localized strings
|
|
1555
|
+
* @param direction - Trend direction
|
|
1556
|
+
* @returns Localized direction label
|
|
1557
|
+
*/
|
|
1558
|
+
function resolveTrendDirectionLabel(d, direction) {
|
|
1559
|
+
if (direction === 'improving')
|
|
1560
|
+
return d.trendImproving;
|
|
1561
|
+
if (direction === 'declining')
|
|
1562
|
+
return d.trendDeclining;
|
|
1563
|
+
return d.trendStableLabel;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Build a generic trend panel from a trend object.
|
|
1567
|
+
*
|
|
1568
|
+
* @param d - Localized strings
|
|
1569
|
+
* @param trend - Trend analytics
|
|
1570
|
+
* @param labels - Labels for x-axis
|
|
1571
|
+
* @param datasetLabel - Label for the dataset
|
|
1572
|
+
* @returns Panel object or null
|
|
1573
|
+
*/
|
|
1574
|
+
function buildGenericTrendPanel(d, trend, labels, datasetLabel) {
|
|
1575
|
+
if (!trend)
|
|
1576
|
+
return null;
|
|
1577
|
+
return {
|
|
1578
|
+
title: d.trendAnalysis,
|
|
1579
|
+
metrics: [
|
|
1580
|
+
{
|
|
1581
|
+
label: d.trendAnalysis,
|
|
1582
|
+
value: resolveTrendDirectionLabel(d, trend.direction),
|
|
1583
|
+
},
|
|
1584
|
+
],
|
|
1585
|
+
chart: {
|
|
1586
|
+
type: 'line',
|
|
1587
|
+
title: d.activityTrendChart,
|
|
1588
|
+
data: {
|
|
1589
|
+
labels,
|
|
1590
|
+
datasets: [
|
|
1591
|
+
{
|
|
1592
|
+
label: datasetLabel,
|
|
1593
|
+
data: trend.metrics.map((m) => m.value),
|
|
1594
|
+
borderColor: EP_BLUE_BORDER,
|
|
1595
|
+
backgroundColor: EP_BLUE_TRANSPARENT,
|
|
1596
|
+
},
|
|
1597
|
+
],
|
|
1598
|
+
},
|
|
1599
|
+
},
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Build dashboard for week-ahead / month-ahead articles.
|
|
1604
|
+
* Includes pipeline status bars and trend analytics panels.
|
|
1605
|
+
*
|
|
1606
|
+
* @param weekData - Aggregated week/month data
|
|
1607
|
+
* @param _label - "week" or "month" (reserved for future localisation)
|
|
1608
|
+
* @param lang - Target language code
|
|
1609
|
+
* @returns Dashboard configuration with pipeline and trend intelligence
|
|
1610
|
+
*/
|
|
1611
|
+
export function buildProspectiveDashboard(weekData, _label, lang = 'en') {
|
|
1612
|
+
const d = getLocalizedString(DASHBOARD_BUILDER_STRINGS, lang);
|
|
1613
|
+
const bottleneckCount = weekData.pipeline.filter((p) => p.bottleneck === true).length;
|
|
1614
|
+
const scheduledPanel = {
|
|
1615
|
+
title: d.scheduledActivity,
|
|
1616
|
+
metrics: [
|
|
1617
|
+
{ label: d.plenaryEvents, value: String(weekData.events.length) },
|
|
1618
|
+
{ label: d.committeeMeetings, value: String(weekData.committees.length) },
|
|
1619
|
+
{ label: d.documents, value: String(weekData.documents.length) },
|
|
1620
|
+
{
|
|
1621
|
+
label: d.pipelineProcedures,
|
|
1622
|
+
value: String(weekData.pipeline.length),
|
|
1623
|
+
trend: bottleneckCount > 0 ? 'down' : 'stable',
|
|
1624
|
+
},
|
|
1625
|
+
],
|
|
1626
|
+
};
|
|
1627
|
+
const questionsPanel = {
|
|
1628
|
+
title: d.parliamentaryQuestions,
|
|
1629
|
+
metrics: [
|
|
1630
|
+
{ label: d.questionsFiled, value: String(weekData.questions.length) },
|
|
1631
|
+
{
|
|
1632
|
+
label: d.bottleneckProcedures,
|
|
1633
|
+
value: String(bottleneckCount),
|
|
1634
|
+
trend: bottleneckCount > 0 ? 'down' : 'up',
|
|
1635
|
+
},
|
|
1636
|
+
],
|
|
1637
|
+
};
|
|
1638
|
+
// Pipeline status panel
|
|
1639
|
+
const pipeline = buildPipelineFromWeekData(weekData);
|
|
1640
|
+
const pipelinePanel = pipeline.total > 0
|
|
1641
|
+
? {
|
|
1642
|
+
title: d.pipelineStatus,
|
|
1643
|
+
metrics: [
|
|
1644
|
+
{
|
|
1645
|
+
label: d.onTrack,
|
|
1646
|
+
value: String(pipeline.onTrack),
|
|
1647
|
+
trend: pipeline.onTrack > pipeline.delayed ? 'up' : 'stable',
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
label: d.delayed,
|
|
1651
|
+
value: String(pipeline.delayed),
|
|
1652
|
+
trend: pipeline.delayed > 0 ? 'down' : 'stable',
|
|
1653
|
+
},
|
|
1654
|
+
{ label: d.healthScore, value: `${pipeline.healthScore}%` },
|
|
1655
|
+
],
|
|
1656
|
+
chart: {
|
|
1657
|
+
type: 'bar',
|
|
1658
|
+
title: d.pipelineStatusChart,
|
|
1659
|
+
data: {
|
|
1660
|
+
labels: [d.onTrack, d.delayed],
|
|
1661
|
+
datasets: [
|
|
1662
|
+
{
|
|
1663
|
+
label: d.procedures,
|
|
1664
|
+
data: [pipeline.onTrack, pipeline.delayed],
|
|
1665
|
+
backgroundColor: ['#28a745', '#ffc107'],
|
|
1666
|
+
},
|
|
1667
|
+
],
|
|
1668
|
+
},
|
|
1669
|
+
},
|
|
1670
|
+
}
|
|
1671
|
+
: null;
|
|
1672
|
+
// Trend analytics from activity counts
|
|
1673
|
+
const activityCounts = [
|
|
1674
|
+
weekData.events.length,
|
|
1675
|
+
weekData.committees.length,
|
|
1676
|
+
weekData.documents.length,
|
|
1677
|
+
weekData.questions.length,
|
|
1678
|
+
].filter((c) => c > 0);
|
|
1679
|
+
const trend = activityCounts.length >= 2 ? buildTrendFromCounts(activityCounts, 'weekly') : null;
|
|
1680
|
+
const trendPanel = buildGenericTrendPanel(d, trend, [d.plenaryEvents, d.committeeMeetings, d.documents, d.questionsFiled], d.scheduledActivity);
|
|
1681
|
+
const panels = [
|
|
1682
|
+
scheduledPanel,
|
|
1683
|
+
questionsPanel,
|
|
1684
|
+
...(pipelinePanel ? [pipelinePanel] : []),
|
|
1685
|
+
...(trendPanel ? [trendPanel] : []),
|
|
1686
|
+
];
|
|
1687
|
+
return { panels };
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Build dashboard for breaking news articles.
|
|
1691
|
+
* Includes activity trend sparklines for cross-article analysis.
|
|
1692
|
+
*
|
|
1693
|
+
* @param feedData - EP feed data
|
|
1694
|
+
* @param lang - Target language code
|
|
1695
|
+
* @returns Dashboard configuration with trend intelligence
|
|
1696
|
+
*/
|
|
1697
|
+
export function buildBreakingDashboard(feedData, lang = 'en') {
|
|
1698
|
+
const d = getLocalizedString(DASHBOARD_BUILDER_STRINGS, lang);
|
|
1699
|
+
const adoptedCount = feedData?.adoptedTexts.length ?? 0;
|
|
1700
|
+
const eventCount = feedData?.events.length ?? 0;
|
|
1701
|
+
const procCount = feedData?.procedures.length ?? 0;
|
|
1702
|
+
const mepCount = feedData?.mepUpdates.length ?? 0;
|
|
1703
|
+
const totalItems = adoptedCount + eventCount + procCount + mepCount;
|
|
1704
|
+
const feedPanel = {
|
|
1705
|
+
title: d.feedActivity,
|
|
1706
|
+
metrics: [
|
|
1707
|
+
{
|
|
1708
|
+
label: d.adoptedTexts,
|
|
1709
|
+
value: String(adoptedCount),
|
|
1710
|
+
trend: adoptedCount > 0 ? 'up' : 'stable',
|
|
1711
|
+
},
|
|
1712
|
+
{ label: d.events, value: String(eventCount) },
|
|
1713
|
+
{ label: d.procedures, value: String(procCount) },
|
|
1714
|
+
{ label: d.mepUpdates, value: String(mepCount) },
|
|
1715
|
+
],
|
|
1716
|
+
};
|
|
1717
|
+
const summaryPanel = {
|
|
1718
|
+
title: d.activitySummary,
|
|
1719
|
+
metrics: [{ label: d.totalItems, value: String(totalItems) }],
|
|
1720
|
+
...(totalItems > 0
|
|
1721
|
+
? {
|
|
1722
|
+
chart: {
|
|
1723
|
+
type: 'doughnut',
|
|
1724
|
+
title: d.feedBreakdown,
|
|
1725
|
+
data: {
|
|
1726
|
+
labels: [d.adoptedTexts, d.events, d.procedures, d.mepUpdates],
|
|
1727
|
+
datasets: [
|
|
1728
|
+
{
|
|
1729
|
+
label: d.items,
|
|
1730
|
+
data: [adoptedCount, eventCount, procCount, mepCount],
|
|
1731
|
+
},
|
|
1732
|
+
],
|
|
1733
|
+
},
|
|
1734
|
+
},
|
|
1735
|
+
}
|
|
1736
|
+
: {}),
|
|
1737
|
+
};
|
|
1738
|
+
// Trend analytics from feed counts
|
|
1739
|
+
const feedCounts = [adoptedCount, eventCount, procCount, mepCount];
|
|
1740
|
+
const trend = buildTrendFromCounts(feedCounts, 'weekly');
|
|
1741
|
+
const trendPanel = buildGenericTrendPanel(d, trend, [d.adoptedTexts, d.events, d.procedures, d.mepUpdates], d.feedActivity);
|
|
1742
|
+
const panels = [feedPanel, summaryPanel, ...(trendPanel ? [trendPanel] : [])];
|
|
1743
|
+
return { panels };
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Build a stakeholder panel from stakeholder metric array.
|
|
1747
|
+
*
|
|
1748
|
+
* @param d - Localized strings
|
|
1749
|
+
* @param stakeholderMetrics - Stakeholder metric data
|
|
1750
|
+
* @returns Panel object or null
|
|
1751
|
+
*/
|
|
1752
|
+
function buildStakeholderPanel(d, stakeholderMetrics) {
|
|
1753
|
+
if (stakeholderMetrics.length === 0)
|
|
1754
|
+
return null;
|
|
1755
|
+
return {
|
|
1756
|
+
title: d.stakeholderImpact,
|
|
1757
|
+
metrics: stakeholderMetrics.map((s) => ({
|
|
1758
|
+
label: s.stakeholder,
|
|
1759
|
+
value: `${s.impactScore}/100`,
|
|
1760
|
+
trend: (s.impactDirection === 'positive'
|
|
1761
|
+
? 'up'
|
|
1762
|
+
: s.impactDirection === 'negative'
|
|
1763
|
+
? 'down'
|
|
1764
|
+
: 'stable'),
|
|
1765
|
+
})),
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Build a pipeline status breakdown panel for propositions dashboard.
|
|
1770
|
+
*
|
|
1771
|
+
* @param d - Localized strings
|
|
1772
|
+
* @param pipeline - Legislative pipeline data
|
|
1773
|
+
* @returns Panel object or null
|
|
1774
|
+
*/
|
|
1775
|
+
function buildPropositionsPipelinePanel(d, pipeline) {
|
|
1776
|
+
if (!pipeline)
|
|
1777
|
+
return null;
|
|
1778
|
+
return {
|
|
1779
|
+
title: d.pipelineStatus,
|
|
1780
|
+
metrics: [
|
|
1781
|
+
{
|
|
1782
|
+
label: d.onTrack,
|
|
1783
|
+
value: String(pipeline.onTrack),
|
|
1784
|
+
trend: (pipeline.onTrack > 0 ? 'up' : 'stable'),
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
label: d.delayed,
|
|
1788
|
+
value: String(pipeline.delayed),
|
|
1789
|
+
trend: (pipeline.delayed > 0 ? 'down' : 'stable'),
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
label: d.blocked,
|
|
1793
|
+
value: String(pipeline.blocked),
|
|
1794
|
+
trend: (pipeline.blocked > 0 ? 'down' : 'stable'),
|
|
1795
|
+
},
|
|
1796
|
+
],
|
|
1797
|
+
chart: {
|
|
1798
|
+
type: 'bar',
|
|
1799
|
+
title: d.pipelineStatusChart,
|
|
1800
|
+
data: {
|
|
1801
|
+
labels: [d.onTrack, d.delayed, d.blocked],
|
|
1802
|
+
datasets: [
|
|
1803
|
+
{
|
|
1804
|
+
label: d.procedures,
|
|
1805
|
+
data: [pipeline.onTrack, pipeline.delayed, pipeline.blocked],
|
|
1806
|
+
backgroundColor: ['#28a745', '#ffc107', '#dc3545'],
|
|
1807
|
+
},
|
|
1808
|
+
],
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Resolve the pipeline strength label from a health score.
|
|
1815
|
+
*
|
|
1816
|
+
* @param d - Localized strings
|
|
1817
|
+
* @param healthScore - Health score 0-1
|
|
1818
|
+
* @returns Localized pipeline strength label
|
|
1819
|
+
*/
|
|
1820
|
+
function resolvePipelineStrengthLabel(d, healthScore) {
|
|
1821
|
+
if (healthScore > 0.7)
|
|
1822
|
+
return d.pipelineStrong;
|
|
1823
|
+
if (healthScore > 0.4)
|
|
1824
|
+
return d.pipelineModerate;
|
|
1825
|
+
return d.pipelineWeak;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Build dashboard for propositions articles.
|
|
1829
|
+
* Includes color-coded pipeline status chart and stakeholder scorecard.
|
|
1830
|
+
*
|
|
1831
|
+
* @param pipelineData - Pipeline metrics
|
|
1832
|
+
* @param lang - Target language code
|
|
1833
|
+
* @returns Dashboard configuration with pipeline intelligence panels
|
|
1834
|
+
*/
|
|
1835
|
+
export function buildPropositionsDashboard(pipelineData, lang = 'en') {
|
|
1836
|
+
const d = getLocalizedString(DASHBOARD_BUILDER_STRINGS, lang);
|
|
1837
|
+
const healthScore = pipelineData?.healthScore ?? 0;
|
|
1838
|
+
const throughput = pipelineData?.throughput ?? 0;
|
|
1839
|
+
const pct = (healthScore * 100).toFixed(0);
|
|
1840
|
+
const healthPanel = {
|
|
1841
|
+
title: d.pipelineHealth,
|
|
1842
|
+
metrics: [
|
|
1843
|
+
{
|
|
1844
|
+
label: d.healthScore,
|
|
1845
|
+
value: `${pct}%`,
|
|
1846
|
+
trend: (healthScore > 0.7 ? 'up' : healthScore < 0.5 ? 'down' : 'stable'),
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
label: d.throughput,
|
|
1850
|
+
value: String(throughput),
|
|
1851
|
+
trend: throughput >= 5 ? 'up' : 'down',
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
label: d.status,
|
|
1855
|
+
value: resolvePipelineStrengthLabel(d, healthScore),
|
|
1856
|
+
},
|
|
1857
|
+
],
|
|
1858
|
+
};
|
|
1859
|
+
// Pipeline status breakdown panel
|
|
1860
|
+
const pipeline = buildPipelineFromPipelineData(pipelineData);
|
|
1861
|
+
const pipelinePanel = buildPropositionsPipelinePanel(d, pipeline);
|
|
1862
|
+
// Stakeholder impact scorecard for pipeline actors
|
|
1863
|
+
const stakeholderMetrics = buildStakeholderMetricsFromPipeline(pipeline);
|
|
1864
|
+
const stakeholderPanel = buildStakeholderPanel(d, stakeholderMetrics);
|
|
1865
|
+
const panels = [
|
|
1866
|
+
healthPanel,
|
|
1867
|
+
...(pipelinePanel ? [pipelinePanel] : []),
|
|
1868
|
+
...(stakeholderPanel ? [stakeholderPanel] : []),
|
|
1869
|
+
];
|
|
1870
|
+
return { panels };
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Build dashboard for committee reports articles.
|
|
1874
|
+
* Includes document trend analytics alongside committee activity metrics.
|
|
1875
|
+
*
|
|
1876
|
+
* @param committees - Committee data list
|
|
1877
|
+
* @param lang - Target language code
|
|
1878
|
+
* @returns Dashboard configuration, or `null` when all committee data is placeholder
|
|
1879
|
+
*/
|
|
1880
|
+
export function buildCommitteeDashboard(committees, lang = 'en') {
|
|
1881
|
+
if (isPlaceholderCommitteeData(committees))
|
|
1882
|
+
return null;
|
|
1883
|
+
const d = getLocalizedString(DASHBOARD_BUILDER_STRINGS, lang);
|
|
1884
|
+
const activeCommittees = committees.filter((c) => c.documents.length > 0);
|
|
1885
|
+
const totalDocs = committees.reduce((sum, c) => sum + c.documents.length, 0);
|
|
1886
|
+
const activePct = committees.length > 0 ? ((activeCommittees.length / committees.length) * 100).toFixed(0) : '0';
|
|
1887
|
+
const overviewPanel = {
|
|
1888
|
+
title: d.committeeOverview,
|
|
1889
|
+
metrics: [
|
|
1890
|
+
{ label: d.totalCommittees, value: String(committees.length) },
|
|
1891
|
+
{
|
|
1892
|
+
label: d.activeCommittees,
|
|
1893
|
+
value: String(activeCommittees.length),
|
|
1894
|
+
trend: activeCommittees.length >= committees.length * 0.7 ? 'up' : 'down',
|
|
1895
|
+
},
|
|
1896
|
+
{ label: d.activityRate, value: `${activePct}%` },
|
|
1897
|
+
{ label: d.documentsProduced, value: String(totalDocs) },
|
|
1898
|
+
],
|
|
1899
|
+
};
|
|
1900
|
+
const chartPanel = committees.length > 0
|
|
1901
|
+
? (() => {
|
|
1902
|
+
const topCommittees = [...committees]
|
|
1903
|
+
.sort((a, b) => b.documents.length - a.documents.length)
|
|
1904
|
+
.slice(0, 6);
|
|
1905
|
+
return {
|
|
1906
|
+
title: d.documentOutputByCommittee,
|
|
1907
|
+
chart: {
|
|
1908
|
+
type: 'bar',
|
|
1909
|
+
title: d.documentsPerCommittee,
|
|
1910
|
+
data: {
|
|
1911
|
+
labels: topCommittees.map((c) => c.abbreviation),
|
|
1912
|
+
datasets: [
|
|
1913
|
+
{
|
|
1914
|
+
label: d.documents,
|
|
1915
|
+
data: topCommittees.map((c) => c.documents.length),
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
1918
|
+
},
|
|
1919
|
+
},
|
|
1920
|
+
};
|
|
1921
|
+
})()
|
|
1922
|
+
: null;
|
|
1923
|
+
// Trend analytics from committee document counts
|
|
1924
|
+
const docCounts = committees.slice(0, 6).map((c) => c.documents.length);
|
|
1925
|
+
const trend = docCounts.length >= 2 ? buildTrendFromCounts(docCounts, 'monthly') : null;
|
|
1926
|
+
const committeeLabels = committees.slice(0, 6).map((c) => c.abbreviation);
|
|
1927
|
+
const trendPanel = buildGenericTrendPanel(d, trend, committeeLabels, d.documentsProduced);
|
|
1928
|
+
const panels = [
|
|
1929
|
+
overviewPanel,
|
|
1930
|
+
...(chartPanel ? [chartPanel] : []),
|
|
1931
|
+
...(trendPanel ? [trendPanel] : []),
|
|
1932
|
+
];
|
|
1933
|
+
return { panels };
|
|
1934
|
+
}
|
|
1935
|
+
// ─── Intelligence Mindmap Builders ───────────────────────────────────────────
|
|
1936
|
+
/** Reusable stakeholder group name for civil society actors. */
|
|
1937
|
+
const CIVIL_SOCIETY = 'Civil Society';
|
|
1938
|
+
/**
|
|
1939
|
+
* Build intelligence mindmap for voting analysis articles.
|
|
1940
|
+
*
|
|
1941
|
+
* Constructs a policy domain intelligence map with political group nodes
|
|
1942
|
+
* as the primary domain layer, voting pattern sub-topics, and anomaly actors.
|
|
1943
|
+
*
|
|
1944
|
+
* @param records - Voting records for the period
|
|
1945
|
+
* @param patterns - Political group voting pattern data
|
|
1946
|
+
* @param anomalies - Detected voting anomalies
|
|
1947
|
+
* @param _lang - Reserved for future localisation (default: 'en')
|
|
1948
|
+
* @returns Intelligence mindmap data, or null when all data is placeholder
|
|
1949
|
+
*/
|
|
1950
|
+
export function buildVotingMindmap(records, patterns, anomalies, _lang = 'en') {
|
|
1951
|
+
void _lang;
|
|
1952
|
+
const realRecords = records.filter((r) => r.result !== PLACEHOLDER_MARKER);
|
|
1953
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
1954
|
+
const realAnomalies = anomalies.filter((a) => !/placeholder/i.test(a.type));
|
|
1955
|
+
if (realRecords.length === 0 && realPatterns.length === 0)
|
|
1956
|
+
return null;
|
|
1957
|
+
if (realPatterns.length === 0)
|
|
1958
|
+
return null;
|
|
1959
|
+
const domainNodes = realPatterns.slice(0, 8).map((p, i) => {
|
|
1960
|
+
const cohesion = p.cohesion ?? 0;
|
|
1961
|
+
const children = realRecords
|
|
1962
|
+
.filter((r) => r.result !== PLACEHOLDER_MARKER)
|
|
1963
|
+
.slice(0, 3)
|
|
1964
|
+
.map((r, ri) => ({
|
|
1965
|
+
id: `record-${i}-${ri}`,
|
|
1966
|
+
label: r.title.slice(0, 50),
|
|
1967
|
+
category: 'action',
|
|
1968
|
+
influence: r.votes.for / Math.max(1, r.votes.for + r.votes.against + r.votes.abstain),
|
|
1969
|
+
color: r.result?.toLowerCase().includes('adopt') ? 'green' : 'red',
|
|
1970
|
+
children: [],
|
|
1971
|
+
metadata: { documentRef: r.title.slice(0, 30) },
|
|
1972
|
+
}));
|
|
1973
|
+
return {
|
|
1974
|
+
id: `group-${i}`,
|
|
1975
|
+
label: p.group,
|
|
1976
|
+
category: 'policy_domain',
|
|
1977
|
+
influence: cohesion,
|
|
1978
|
+
color: cohesion > 0.8 ? 'green' : cohesion > 0.5 ? 'cyan' : 'red',
|
|
1979
|
+
children,
|
|
1980
|
+
metadata: { politicalGroup: p.group },
|
|
1981
|
+
};
|
|
1982
|
+
});
|
|
1983
|
+
const actorNetwork = [
|
|
1984
|
+
...realPatterns.slice(0, 6).map((p, i) => ({
|
|
1985
|
+
id: `actor-group-${i}`,
|
|
1986
|
+
name: p.group,
|
|
1987
|
+
type: 'group',
|
|
1988
|
+
influence: p.cohesion ?? 0,
|
|
1989
|
+
connections: realAnomalies
|
|
1990
|
+
.filter((a) => a.type && !a.type.includes('placeholder'))
|
|
1991
|
+
.slice(0, 2)
|
|
1992
|
+
.map((_, ai) => `anomaly-${ai}`),
|
|
1993
|
+
})),
|
|
1994
|
+
...realAnomalies.slice(0, 3).map((a, i) => ({
|
|
1995
|
+
id: `anomaly-${i}`,
|
|
1996
|
+
name: a.type,
|
|
1997
|
+
type: 'external',
|
|
1998
|
+
influence: a.severity?.toUpperCase() === 'HIGH' ? 0.9 : 0.5,
|
|
1999
|
+
connections: [],
|
|
2000
|
+
})),
|
|
2001
|
+
];
|
|
2002
|
+
const anomalyActorCount = Math.min(realAnomalies.length, 3);
|
|
2003
|
+
const connections = realAnomalies.slice(0, anomalyActorCount).map((a, i) => ({
|
|
2004
|
+
from: `anomaly-${i}`,
|
|
2005
|
+
to: `group-${i % Math.max(1, domainNodes.length)}`,
|
|
2006
|
+
strength: a.severity?.toUpperCase() === 'HIGH' ? 'strong' : 'moderate',
|
|
2007
|
+
type: 'political',
|
|
2008
|
+
evidence: a.type,
|
|
2009
|
+
}));
|
|
2010
|
+
const adoptedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('adopt')).length;
|
|
2011
|
+
return {
|
|
2012
|
+
centralTopic: 'Voting Intelligence Analysis',
|
|
2013
|
+
layers: [{ depth: 1, nodes: domainNodes }],
|
|
2014
|
+
connections,
|
|
2015
|
+
actorNetwork,
|
|
2016
|
+
stakeholderGroups: ['Political Groups', CIVIL_SOCIETY, 'Member States'],
|
|
2017
|
+
summary: `Analysing ${realRecords.length} votes across ${realPatterns.length} political groups. ${adoptedCount} measures adopted.`,
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Build intelligence mindmap for week-ahead / month-ahead (prospective) articles.
|
|
2022
|
+
*
|
|
2023
|
+
* Maps scheduled parliamentary activities by policy domain with committee nodes
|
|
2024
|
+
* and pipeline bottleneck indicators.
|
|
2025
|
+
*
|
|
2026
|
+
* @param weekData - Aggregated week/month-ahead data
|
|
2027
|
+
* @param _lang - Reserved for future localisation (default: 'en')
|
|
2028
|
+
* @returns Intelligence mindmap data
|
|
2029
|
+
*/
|
|
2030
|
+
export function buildProspectiveMindmap(weekData, _lang = 'en') {
|
|
2031
|
+
void _lang;
|
|
2032
|
+
const policyDomains = [
|
|
2033
|
+
{ id: 'envi', label: 'Environment & Climate', color: 'green' },
|
|
2034
|
+
{ id: 'econ', label: 'Economy & Finance', color: 'cyan' },
|
|
2035
|
+
{ id: 'afet', label: 'Foreign Affairs', color: 'blue' },
|
|
2036
|
+
{ id: 'libe', label: 'Civil Liberties', color: 'purple' },
|
|
2037
|
+
{ id: 'agri', label: 'Agriculture', color: 'yellow' },
|
|
2038
|
+
];
|
|
2039
|
+
const events = weekData.events ?? [];
|
|
2040
|
+
const pipeline = weekData.pipeline ?? [];
|
|
2041
|
+
const pipelineSlice = pipeline.slice(0, 4);
|
|
2042
|
+
const bottleneckCount = pipelineSlice.filter((p) => p.bottleneck === true).length;
|
|
2043
|
+
const domainNodes = policyDomains.map((domain, i) => {
|
|
2044
|
+
const relatedEvents = events.slice(i * 2, i * 2 + 2);
|
|
2045
|
+
const children = relatedEvents.map((ev, ei) => ({
|
|
2046
|
+
id: `event-${i}-${ei}`,
|
|
2047
|
+
label: ev.title ? ev.title.slice(0, 50) : 'Scheduled event',
|
|
2048
|
+
category: 'action',
|
|
2049
|
+
influence: 0.6,
|
|
2050
|
+
color: 'orange',
|
|
2051
|
+
children: [],
|
|
2052
|
+
}));
|
|
2053
|
+
return {
|
|
2054
|
+
id: domain.id,
|
|
2055
|
+
label: domain.label,
|
|
2056
|
+
category: 'policy_domain',
|
|
2057
|
+
influence: 0.5 + (relatedEvents.length > 0 ? 0.3 : 0),
|
|
2058
|
+
color: domain.color,
|
|
2059
|
+
children,
|
|
2060
|
+
};
|
|
2061
|
+
});
|
|
2062
|
+
// Build pipeline actor nodes preserving original indices as stable IDs
|
|
2063
|
+
const actorNetwork = [
|
|
2064
|
+
{
|
|
2065
|
+
id: 'ep-plenary',
|
|
2066
|
+
name: 'Plenary Session',
|
|
2067
|
+
type: 'committee',
|
|
2068
|
+
influence: 0.95,
|
|
2069
|
+
connections: policyDomains.map((d) => d.id),
|
|
2070
|
+
},
|
|
2071
|
+
...pipelineSlice.map((p, i) => ({
|
|
2072
|
+
id: `pipeline-${i}`,
|
|
2073
|
+
name: p.title ? p.title.slice(0, 40) : 'Legislative procedure',
|
|
2074
|
+
type: 'external',
|
|
2075
|
+
influence: p.bottleneck === true ? 0.85 : 0.5,
|
|
2076
|
+
connections: [],
|
|
2077
|
+
})),
|
|
2078
|
+
];
|
|
2079
|
+
// Filter bottlenecks from the same slice, keeping original index for stable IDs
|
|
2080
|
+
const connections = pipelineSlice
|
|
2081
|
+
.map((p, origIdx) => ({ p, origIdx }))
|
|
2082
|
+
.filter(({ p }) => p.bottleneck === true)
|
|
2083
|
+
.slice(0, 3)
|
|
2084
|
+
.map(({ p, origIdx }, i) => ({
|
|
2085
|
+
from: policyDomains[i % policyDomains.length]?.id ?? 'envi',
|
|
2086
|
+
to: `pipeline-${origIdx}`,
|
|
2087
|
+
strength: 'strong',
|
|
2088
|
+
type: 'legislative',
|
|
2089
|
+
evidence: p.title ? p.title.slice(0, 60) : 'Legislative bottleneck',
|
|
2090
|
+
}));
|
|
2091
|
+
return {
|
|
2092
|
+
centralTopic: 'Week Ahead: Parliamentary Priorities',
|
|
2093
|
+
layers: [{ depth: 1, nodes: domainNodes }],
|
|
2094
|
+
connections,
|
|
2095
|
+
actorNetwork,
|
|
2096
|
+
stakeholderGroups: ['Parliament', 'Council', 'Commission', CIVIL_SOCIETY],
|
|
2097
|
+
summary: `${events.length} events scheduled. ${bottleneckCount} legislative bottlenecks identified.`,
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Build intelligence mindmap for breaking news articles.
|
|
2102
|
+
*
|
|
2103
|
+
* Maps EP feed categories (adopted texts, events, procedures, MEP updates)
|
|
2104
|
+
* as policy domain nodes with recent activity sub-nodes.
|
|
2105
|
+
*
|
|
2106
|
+
* @param feedData - Breaking news EP feed data
|
|
2107
|
+
* @param _lang - Reserved for future localisation (default: 'en')
|
|
2108
|
+
* @returns Intelligence mindmap data
|
|
2109
|
+
*/
|
|
2110
|
+
export function buildBreakingMindmap(feedData, _lang = 'en') {
|
|
2111
|
+
void _lang;
|
|
2112
|
+
const adoptedTexts = feedData?.adoptedTexts ?? [];
|
|
2113
|
+
const events = feedData?.events ?? [];
|
|
2114
|
+
const procedures = feedData?.procedures ?? [];
|
|
2115
|
+
const mepUpdates = feedData?.mepUpdates ?? [];
|
|
2116
|
+
const domainNodes = [
|
|
2117
|
+
{
|
|
2118
|
+
id: 'adopted',
|
|
2119
|
+
label: 'Adopted Texts',
|
|
2120
|
+
category: 'policy_domain',
|
|
2121
|
+
influence: Math.min(1, adoptedTexts.length / 5),
|
|
2122
|
+
color: 'green',
|
|
2123
|
+
children: adoptedTexts.slice(0, 3).map((t, i) => ({
|
|
2124
|
+
id: `adopted-${i}`,
|
|
2125
|
+
label: t.title ? t.title.slice(0, 50) : 'Adopted measure',
|
|
2126
|
+
category: 'outcome',
|
|
2127
|
+
influence: 0.7,
|
|
2128
|
+
color: 'green',
|
|
2129
|
+
children: [],
|
|
2130
|
+
metadata: { documentRef: t.title?.slice(0, 30) },
|
|
2131
|
+
})),
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
id: 'events',
|
|
2135
|
+
label: 'Parliamentary Events',
|
|
2136
|
+
category: 'policy_domain',
|
|
2137
|
+
influence: Math.min(1, events.length / 5),
|
|
2138
|
+
color: 'blue',
|
|
2139
|
+
children: events.slice(0, 3).map((ev, i) => ({
|
|
2140
|
+
id: `event-${i}`,
|
|
2141
|
+
label: ev.title ? ev.title.slice(0, 50) : 'Parliamentary event',
|
|
2142
|
+
category: 'action',
|
|
2143
|
+
influence: 0.6,
|
|
2144
|
+
color: 'blue',
|
|
2145
|
+
children: [],
|
|
2146
|
+
})),
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
id: 'procedures',
|
|
2150
|
+
label: 'Active Procedures',
|
|
2151
|
+
category: 'policy_domain',
|
|
2152
|
+
influence: Math.min(1, procedures.length / 5),
|
|
2153
|
+
color: 'orange',
|
|
2154
|
+
children: procedures.slice(0, 3).map((p, i) => ({
|
|
2155
|
+
id: `procedure-${i}`,
|
|
2156
|
+
label: p.title ? p.title.slice(0, 50) : 'Legislative procedure',
|
|
2157
|
+
category: 'action',
|
|
2158
|
+
influence: 0.65,
|
|
2159
|
+
color: 'orange',
|
|
2160
|
+
children: [],
|
|
2161
|
+
})),
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
id: 'meps',
|
|
2165
|
+
label: 'MEP Updates',
|
|
2166
|
+
category: 'policy_domain',
|
|
2167
|
+
influence: Math.min(1, mepUpdates.length / 5),
|
|
2168
|
+
color: 'purple',
|
|
2169
|
+
children: mepUpdates.slice(0, 2).map((m, i) => ({
|
|
2170
|
+
id: `mep-${i}`,
|
|
2171
|
+
label: m.name ? m.name.slice(0, 50) : 'MEP activity',
|
|
2172
|
+
category: 'actor',
|
|
2173
|
+
influence: 0.55,
|
|
2174
|
+
color: 'purple',
|
|
2175
|
+
children: [],
|
|
2176
|
+
})),
|
|
2177
|
+
},
|
|
2178
|
+
].filter((n) => n.influence > 0 || n.children.length > 0);
|
|
2179
|
+
if (domainNodes.length === 0) {
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
const actorNetwork = [
|
|
2183
|
+
{
|
|
2184
|
+
id: 'ep-parliament',
|
|
2185
|
+
name: 'European Parliament',
|
|
2186
|
+
type: 'committee',
|
|
2187
|
+
influence: 1.0,
|
|
2188
|
+
connections: domainNodes.map((n) => n.id),
|
|
2189
|
+
},
|
|
2190
|
+
...mepUpdates.slice(0, 3).map((m, i) => ({
|
|
2191
|
+
id: `mep-actor-${i}`,
|
|
2192
|
+
name: m.name ? m.name.slice(0, 40) : 'MEP',
|
|
2193
|
+
type: 'mep',
|
|
2194
|
+
influence: 0.6,
|
|
2195
|
+
connections: ['meps'],
|
|
2196
|
+
})),
|
|
2197
|
+
];
|
|
2198
|
+
const connections = [
|
|
2199
|
+
...(adoptedTexts.length > 0 && procedures.length > 0
|
|
2200
|
+
? [
|
|
2201
|
+
{
|
|
2202
|
+
from: 'adopted',
|
|
2203
|
+
to: 'procedures',
|
|
2204
|
+
strength: 'strong',
|
|
2205
|
+
type: 'legislative',
|
|
2206
|
+
evidence: 'Adopted texts conclude active legislative procedures',
|
|
2207
|
+
},
|
|
2208
|
+
]
|
|
2209
|
+
: []),
|
|
2210
|
+
...(events.length > 0 && procedures.length > 0
|
|
2211
|
+
? [
|
|
2212
|
+
{
|
|
2213
|
+
from: 'events',
|
|
2214
|
+
to: 'procedures',
|
|
2215
|
+
strength: 'moderate',
|
|
2216
|
+
type: 'procedural',
|
|
2217
|
+
evidence: 'Parliamentary events drive procedure progression',
|
|
2218
|
+
},
|
|
2219
|
+
]
|
|
2220
|
+
: []),
|
|
2221
|
+
];
|
|
2222
|
+
const totalItems = adoptedTexts.length + events.length + procedures.length + mepUpdates.length;
|
|
2223
|
+
return {
|
|
2224
|
+
centralTopic: 'Breaking News Intelligence',
|
|
2225
|
+
layers: [{ depth: 1, nodes: domainNodes }],
|
|
2226
|
+
connections,
|
|
2227
|
+
actorNetwork,
|
|
2228
|
+
stakeholderGroups: ['Parliament', 'Commission', 'Council', 'Public'],
|
|
2229
|
+
summary: `${totalItems} feed items detected across ${domainNodes.length} activity categories.`,
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Build intelligence mindmap for propositions / legislative pipeline articles.
|
|
2234
|
+
*
|
|
2235
|
+
* Maps the legislative pipeline stages as policy domain nodes with procedure
|
|
2236
|
+
* health and throughput indicators.
|
|
2237
|
+
*
|
|
2238
|
+
* @param pipelineData - Legislative pipeline metrics (null when unavailable)
|
|
2239
|
+
* @param _lang - Reserved for future localisation (default: 'en')
|
|
2240
|
+
* @returns Intelligence mindmap data
|
|
2241
|
+
*/
|
|
2242
|
+
export function buildPropositionsMindmap(pipelineData, _lang = 'en') {
|
|
2243
|
+
void _lang;
|
|
2244
|
+
const healthScore = pipelineData?.healthScore ?? 0;
|
|
2245
|
+
const throughput = pipelineData?.throughput ?? 0;
|
|
2246
|
+
const healthPct = (healthScore * 100).toFixed(0);
|
|
2247
|
+
const pipelineStages = [
|
|
2248
|
+
{
|
|
2249
|
+
id: 'proposal',
|
|
2250
|
+
label: 'Commission Proposals',
|
|
2251
|
+
category: 'policy_domain',
|
|
2252
|
+
influence: 0.9,
|
|
2253
|
+
color: 'cyan',
|
|
2254
|
+
children: [
|
|
2255
|
+
{
|
|
2256
|
+
id: 'proposal-review',
|
|
2257
|
+
label: 'Initial Committee Review',
|
|
2258
|
+
category: 'action',
|
|
2259
|
+
influence: 0.7,
|
|
2260
|
+
color: 'cyan',
|
|
2261
|
+
children: [],
|
|
2262
|
+
metadata: { committee: 'Lead Committee' },
|
|
2263
|
+
},
|
|
2264
|
+
],
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
id: 'committee',
|
|
2268
|
+
label: 'Committee Stage',
|
|
2269
|
+
category: 'policy_domain',
|
|
2270
|
+
influence: 0.85,
|
|
2271
|
+
color: 'green',
|
|
2272
|
+
children: [
|
|
2273
|
+
{
|
|
2274
|
+
id: 'rapporteur',
|
|
2275
|
+
label: 'Rapporteur Report',
|
|
2276
|
+
category: 'action',
|
|
2277
|
+
influence: 0.8,
|
|
2278
|
+
color: 'green',
|
|
2279
|
+
children: [],
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
id: 'amendments',
|
|
2283
|
+
label: 'Amendments',
|
|
2284
|
+
category: 'action',
|
|
2285
|
+
influence: 0.75,
|
|
2286
|
+
color: 'yellow',
|
|
2287
|
+
children: [],
|
|
2288
|
+
},
|
|
2289
|
+
],
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
id: 'plenary',
|
|
2293
|
+
label: 'Plenary Vote',
|
|
2294
|
+
category: 'policy_domain',
|
|
2295
|
+
influence: healthScore,
|
|
2296
|
+
color: healthScore > 0.7 ? 'green' : healthScore > 0.4 ? 'yellow' : 'red',
|
|
2297
|
+
children: [
|
|
2298
|
+
{
|
|
2299
|
+
id: 'plenary-debate',
|
|
2300
|
+
label: 'Debate',
|
|
2301
|
+
category: 'action',
|
|
2302
|
+
influence: 0.7,
|
|
2303
|
+
color: 'blue',
|
|
2304
|
+
children: [],
|
|
2305
|
+
},
|
|
2306
|
+
],
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
id: 'trilogue',
|
|
2310
|
+
label: 'Inter-institutional Trilogue',
|
|
2311
|
+
category: 'policy_domain',
|
|
2312
|
+
influence: 0.8,
|
|
2313
|
+
color: 'orange',
|
|
2314
|
+
children: [
|
|
2315
|
+
{
|
|
2316
|
+
id: 'council-position',
|
|
2317
|
+
label: 'Council Position',
|
|
2318
|
+
category: 'actor',
|
|
2319
|
+
influence: 0.85,
|
|
2320
|
+
color: 'orange',
|
|
2321
|
+
children: [],
|
|
2322
|
+
metadata: { committee: 'Council of the EU' },
|
|
2323
|
+
},
|
|
2324
|
+
],
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
id: 'adoption',
|
|
2328
|
+
label: 'Final Adoption',
|
|
2329
|
+
category: 'outcome',
|
|
2330
|
+
influence: healthScore > 0.5 ? 0.9 : 0.4,
|
|
2331
|
+
color: healthScore > 0.5 ? 'green' : 'red',
|
|
2332
|
+
children: [],
|
|
2333
|
+
},
|
|
2334
|
+
];
|
|
2335
|
+
const actorNetwork = [
|
|
2336
|
+
{
|
|
2337
|
+
id: 'commission',
|
|
2338
|
+
name: 'European Commission',
|
|
2339
|
+
type: 'external',
|
|
2340
|
+
influence: 0.9,
|
|
2341
|
+
connections: ['proposal'],
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
id: 'parliament',
|
|
2345
|
+
name: 'European Parliament',
|
|
2346
|
+
type: 'committee',
|
|
2347
|
+
influence: 0.95,
|
|
2348
|
+
connections: ['committee', 'plenary'],
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
id: 'council',
|
|
2352
|
+
name: 'Council of the EU',
|
|
2353
|
+
type: 'external',
|
|
2354
|
+
influence: 0.9,
|
|
2355
|
+
connections: ['trilogue', 'adoption'],
|
|
2356
|
+
},
|
|
2357
|
+
];
|
|
2358
|
+
const connections = [
|
|
2359
|
+
{
|
|
2360
|
+
from: 'proposal',
|
|
2361
|
+
to: 'committee',
|
|
2362
|
+
strength: 'strong',
|
|
2363
|
+
type: 'legislative',
|
|
2364
|
+
evidence: 'Formal referral to committee',
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
from: 'committee',
|
|
2368
|
+
to: 'plenary',
|
|
2369
|
+
strength: 'strong',
|
|
2370
|
+
type: 'procedural',
|
|
2371
|
+
evidence: 'Committee report referred to plenary',
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
from: 'plenary',
|
|
2375
|
+
to: 'trilogue',
|
|
2376
|
+
strength: throughput > 5 ? 'strong' : 'moderate',
|
|
2377
|
+
type: 'legislative',
|
|
2378
|
+
evidence: 'Parliament position triggers inter-institutional negotiations',
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
from: 'trilogue',
|
|
2382
|
+
to: 'adoption',
|
|
2383
|
+
strength: healthScore > 0.6 ? 'strong' : 'weak',
|
|
2384
|
+
type: 'legislative',
|
|
2385
|
+
evidence: `Pipeline health: ${healthPct}%`,
|
|
2386
|
+
},
|
|
2387
|
+
];
|
|
2388
|
+
return {
|
|
2389
|
+
centralTopic: 'Legislative Pipeline Intelligence',
|
|
2390
|
+
layers: [{ depth: 1, nodes: pipelineStages }],
|
|
2391
|
+
connections,
|
|
2392
|
+
actorNetwork,
|
|
2393
|
+
stakeholderGroups: ['Commission', 'Parliament', 'Council', 'Businesses', CIVIL_SOCIETY],
|
|
2394
|
+
summary: `Pipeline health: ${healthPct}%. Throughput rate: ${throughput}. ${throughput > 5 ? 'Strong legislative momentum.' : 'Moderate legislative pace.'}`,
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Build intelligence mindmap for committee reports articles.
|
|
2399
|
+
*
|
|
2400
|
+
* Maps committee activity as policy domain nodes with document output
|
|
2401
|
+
* and inter-committee relationship indicators.
|
|
2402
|
+
*
|
|
2403
|
+
* @param committees - Committee data list
|
|
2404
|
+
* @param _lang - Reserved for future localisation (default: 'en')
|
|
2405
|
+
* @returns Intelligence mindmap data, or null when all data is placeholder
|
|
2406
|
+
*/
|
|
2407
|
+
export function buildCommitteeMindmap(committees, _lang = 'en') {
|
|
2408
|
+
void _lang;
|
|
2409
|
+
if (isPlaceholderCommitteeData(committees))
|
|
2410
|
+
return null;
|
|
2411
|
+
const activeCommittees = committees.filter((c) => c.documents.length > 0);
|
|
2412
|
+
if (activeCommittees.length === 0)
|
|
2413
|
+
return null;
|
|
2414
|
+
const domainNodes = activeCommittees.slice(0, 8).map((c, i) => {
|
|
2415
|
+
const influence = Math.min(1, c.documents.length / 10);
|
|
2416
|
+
const children = c.documents.slice(0, 3).map((doc, di) => ({
|
|
2417
|
+
id: `doc-${i}-${di}`,
|
|
2418
|
+
label: doc.title ? doc.title.slice(0, 50) : 'Committee document',
|
|
2419
|
+
category: 'action',
|
|
2420
|
+
influence: 0.6,
|
|
2421
|
+
color: 'blue',
|
|
2422
|
+
children: [],
|
|
2423
|
+
metadata: { committee: c.abbreviation, documentRef: doc.title?.slice(0, 30) },
|
|
2424
|
+
}));
|
|
2425
|
+
const colors = [
|
|
2426
|
+
'green',
|
|
2427
|
+
'cyan',
|
|
2428
|
+
'blue',
|
|
2429
|
+
'purple',
|
|
2430
|
+
'orange',
|
|
2431
|
+
'yellow',
|
|
2432
|
+
'magenta',
|
|
2433
|
+
'red',
|
|
2434
|
+
];
|
|
2435
|
+
return {
|
|
2436
|
+
id: `committee-${c.abbreviation}`,
|
|
2437
|
+
label: c.abbreviation,
|
|
2438
|
+
category: 'policy_domain',
|
|
2439
|
+
influence,
|
|
2440
|
+
color: colors[i % colors.length] ?? 'cyan',
|
|
2441
|
+
children,
|
|
2442
|
+
metadata: { committee: c.abbreviation },
|
|
2443
|
+
};
|
|
2444
|
+
});
|
|
2445
|
+
const actorNetwork = activeCommittees.slice(0, 6).map((c, i) => ({
|
|
2446
|
+
id: `committee-actor-${i}`,
|
|
2447
|
+
name: c.abbreviation,
|
|
2448
|
+
type: 'committee',
|
|
2449
|
+
influence: Math.min(1, c.documents.length / 10),
|
|
2450
|
+
connections: domainNodes
|
|
2451
|
+
.filter((n) => n.id !== `committee-${c.abbreviation}`)
|
|
2452
|
+
.slice(0, 2)
|
|
2453
|
+
.map((n) => n.id),
|
|
2454
|
+
}));
|
|
2455
|
+
const connections = activeCommittees.slice(0, 3).flatMap((c, i) => activeCommittees.slice(i + 1, i + 2).map((c2) => ({
|
|
2456
|
+
from: `committee-${c.abbreviation}`,
|
|
2457
|
+
to: `committee-${c2.abbreviation}`,
|
|
2458
|
+
strength: 'moderate',
|
|
2459
|
+
type: 'thematic',
|
|
2460
|
+
evidence: `Inter-committee collaboration between ${c.abbreviation} and ${c2.abbreviation}`,
|
|
2461
|
+
})));
|
|
2462
|
+
const totalDocs = committees.reduce((sum, c) => sum + c.documents.length, 0);
|
|
2463
|
+
return {
|
|
2464
|
+
centralTopic: 'Committee Intelligence Network',
|
|
2465
|
+
layers: [{ depth: 1, nodes: domainNodes }],
|
|
2466
|
+
connections,
|
|
2467
|
+
actorNetwork,
|
|
2468
|
+
stakeholderGroups: ['MEPs', 'Political Groups', 'Secretariat', 'External Experts'],
|
|
2469
|
+
summary: `${activeCommittees.length} active committees producing ${totalDocs} documents.`,
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
// ─── Multi-dimensional SWOT builders ────────────────────────────────────────
|
|
2473
|
+
/**
|
|
2474
|
+
* Build a dimension object from sets of pre-computed SWOT items.
|
|
2475
|
+
*
|
|
2476
|
+
* @param name - Dimension name
|
|
2477
|
+
* @param strengths - Strength items for this dimension
|
|
2478
|
+
* @param weaknesses - Weakness items for this dimension
|
|
2479
|
+
* @param opportunities - Opportunity items for this dimension
|
|
2480
|
+
* @param threats - Threat items for this dimension
|
|
2481
|
+
* @returns Typed SwotDimension
|
|
2482
|
+
*/
|
|
2483
|
+
function makeDimension(name, strengths, weaknesses, opportunities, threats) {
|
|
2484
|
+
return { name, strengths, weaknesses, opportunities, threats };
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Build stakeholder views for voting multi-dimensional SWOT.
|
|
2488
|
+
*
|
|
2489
|
+
* @param adoptedCount - Number of adopted votes
|
|
2490
|
+
* @param realAnomalies - Non-placeholder anomalies
|
|
2491
|
+
* @param highSeverity - High-severity anomalies
|
|
2492
|
+
* @param highCohesion - High-cohesion patterns
|
|
2493
|
+
* @param lowCohesion - Low-cohesion patterns
|
|
2494
|
+
* @param realPatterns - Non-placeholder patterns
|
|
2495
|
+
* @param s - Localized SWOT builder strings
|
|
2496
|
+
* @returns Stakeholder views map
|
|
2497
|
+
*/
|
|
2498
|
+
function buildVotingMDStakeholders(adoptedCount, realAnomalies, highSeverity, highCohesion, lowCohesion, realPatterns, s) {
|
|
2499
|
+
return {
|
|
2500
|
+
citizen: {
|
|
2501
|
+
strengths: adoptedCount > 0
|
|
2502
|
+
? [{ text: s.votingAdopted(adoptedCount), severity: 'medium' }]
|
|
2503
|
+
: [],
|
|
2504
|
+
weaknesses: realAnomalies.length > 0
|
|
2505
|
+
? [{ text: s.votingAnomalies(realAnomalies.length), severity: 'medium' }]
|
|
2506
|
+
: [],
|
|
2507
|
+
opportunities: [{ text: s.votingCrossParty, severity: 'medium' }],
|
|
2508
|
+
threats: highSeverity.length > 0
|
|
2509
|
+
? [{ text: s.votingHighSeverity(highSeverity.length), severity: 'high' }]
|
|
2510
|
+
: [],
|
|
2511
|
+
},
|
|
2512
|
+
mep: {
|
|
2513
|
+
strengths: highCohesion.length > 0
|
|
2514
|
+
? [{ text: s.votingHighCohesion(highCohesion.length), severity: 'high' }]
|
|
2515
|
+
: [],
|
|
2516
|
+
weaknesses: lowCohesion.length > 0
|
|
2517
|
+
? [{ text: s.votingLowCohesion(lowCohesion.length), severity: 'high' }]
|
|
2518
|
+
: [],
|
|
2519
|
+
opportunities: realPatterns.length > 0
|
|
2520
|
+
? [{ text: s.votingDiverseGroups(realPatterns.length), severity: 'medium' }]
|
|
2521
|
+
: [],
|
|
2522
|
+
threats: [{ text: s.votingShiftingAlliances, severity: 'medium' }],
|
|
2523
|
+
},
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Build stakeholder views for breaking multi-dimensional SWOT.
|
|
2528
|
+
*
|
|
2529
|
+
* @param adoptedCount - Number of adopted texts
|
|
2530
|
+
* @param anomalyRaw - Raw anomaly text
|
|
2531
|
+
* @param procCount - Number of active procedures
|
|
2532
|
+
* @param eventCount - Number of events
|
|
2533
|
+
* @param coalitionRaw - Raw coalition text
|
|
2534
|
+
* @param s - Localized SWOT builder strings
|
|
2535
|
+
* @returns Stakeholder views map
|
|
2536
|
+
*/
|
|
2537
|
+
function buildBreakingMDStakeholders(adoptedCount, anomalyRaw, procCount, eventCount, coalitionRaw, s) {
|
|
2538
|
+
return {
|
|
2539
|
+
citizen: {
|
|
2540
|
+
strengths: adoptedCount > 0
|
|
2541
|
+
? [{ text: s.breakingAdopted(adoptedCount), severity: 'medium' }]
|
|
2542
|
+
: [],
|
|
2543
|
+
weaknesses: anomalyRaw
|
|
2544
|
+
? [{ text: s.breakingAnomalyWeakness, severity: 'high' }]
|
|
2545
|
+
: [],
|
|
2546
|
+
opportunities: procCount > 0
|
|
2547
|
+
? [{ text: s.breakingProceduresActive(procCount), severity: 'medium' }]
|
|
2548
|
+
: [],
|
|
2549
|
+
threats: anomalyRaw ? [{ text: s.breakingAnomalyThreat, severity: 'high' }] : [],
|
|
2550
|
+
},
|
|
2551
|
+
media: {
|
|
2552
|
+
strengths: eventCount > 0 ? [{ text: s.breakingEvents(eventCount), severity: 'high' }] : [],
|
|
2553
|
+
weaknesses: [],
|
|
2554
|
+
opportunities: coalitionRaw
|
|
2555
|
+
? [{ text: s.breakingCoalitionOpportunity, severity: 'medium' }]
|
|
2556
|
+
: [],
|
|
2557
|
+
threats: [{ text: s.breakingRapidEvents, severity: 'medium' }],
|
|
2558
|
+
},
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Build stakeholder views for committee multi-dimensional SWOT.
|
|
2563
|
+
*
|
|
2564
|
+
* @param active - Active committees
|
|
2565
|
+
* @param committees - All committees
|
|
2566
|
+
* @param totalDocs - Total document count
|
|
2567
|
+
* @param inactiveCount - Number of inactive committees
|
|
2568
|
+
* @param s - Localized SWOT builder strings
|
|
2569
|
+
* @returns Stakeholder views map
|
|
2570
|
+
*/
|
|
2571
|
+
function buildCommitteeMDStakeholders(active, committees, totalDocs, inactiveCount, s) {
|
|
2572
|
+
return {
|
|
2573
|
+
mep: {
|
|
2574
|
+
strengths: active.length > 0
|
|
2575
|
+
? [
|
|
2576
|
+
{
|
|
2577
|
+
text: s.committeeActive(active.length, committees.length),
|
|
2578
|
+
severity: 'high',
|
|
2579
|
+
},
|
|
2580
|
+
]
|
|
2581
|
+
: [],
|
|
2582
|
+
weaknesses: inactiveCount > 0
|
|
2583
|
+
? [{ text: s.committeeInactive(inactiveCount), severity: 'medium' }]
|
|
2584
|
+
: [],
|
|
2585
|
+
opportunities: [{ text: s.committeeHearings, severity: 'medium' }],
|
|
2586
|
+
threats: [{ text: s.committeeCompetingPriorities, severity: 'medium' }],
|
|
2587
|
+
},
|
|
2588
|
+
ngo: {
|
|
2589
|
+
strengths: totalDocs > 0
|
|
2590
|
+
? [{ text: s.committeeDocuments(totalDocs), severity: 'medium' }]
|
|
2591
|
+
: [],
|
|
2592
|
+
weaknesses: inactiveCount > committees.length * 0.3
|
|
2593
|
+
? [{ text: s.committeeLowActivity, severity: 'high' }]
|
|
2594
|
+
: [],
|
|
2595
|
+
opportunities: [{ text: s.committeeCrossCollaboration, severity: 'medium' }],
|
|
2596
|
+
threats: [],
|
|
2597
|
+
},
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Build multi-dimensional SWOT analysis for voting-based articles.
|
|
2602
|
+
*
|
|
2603
|
+
* Produces dimension-specific breakdowns (political, economic, social,
|
|
2604
|
+
* legal, geopolitical), temporal assessments, and stakeholder views
|
|
2605
|
+
* derived from voting records, patterns, and anomaly data.
|
|
2606
|
+
*
|
|
2607
|
+
* @param records - Voting records
|
|
2608
|
+
* @param patterns - Voting patterns
|
|
2609
|
+
* @param anomalies - Detected anomalies
|
|
2610
|
+
* @param lang - Target language code
|
|
2611
|
+
* @returns Multi-dimensional SWOT data
|
|
2612
|
+
*/
|
|
2613
|
+
export function buildVotingMultiDimensionalSwot(records, patterns, anomalies, lang = 'en') {
|
|
2614
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
2615
|
+
const base = buildVotingSwot(records, patterns, anomalies, lang);
|
|
2616
|
+
const realRecords = records.filter((r) => r.result !== PLACEHOLDER_MARKER);
|
|
2617
|
+
const realPatterns = patterns.filter((p) => !/placeholder/i.test(p.group));
|
|
2618
|
+
const realAnomalies = anomalies.filter((a) => !/placeholder/i.test(a.type));
|
|
2619
|
+
const adoptedCount = realRecords.filter((r) => r.result?.toLowerCase().includes('adopt')).length;
|
|
2620
|
+
const highCohesion = realPatterns.filter((p) => p.cohesion > 0.8);
|
|
2621
|
+
const lowCohesion = realPatterns.filter((p) => p.cohesion < 0.5);
|
|
2622
|
+
const highSeverity = realAnomalies.filter((a) => a.severity?.toUpperCase() === 'HIGH');
|
|
2623
|
+
const political = makeDimension('political', highCohesion.length > 0
|
|
2624
|
+
? [{ text: s.votingHighCohesion(highCohesion.length), severity: 'high' }]
|
|
2625
|
+
: [], lowCohesion.length > 0
|
|
2626
|
+
? [{ text: s.votingLowCohesion(lowCohesion.length), severity: 'high' }]
|
|
2627
|
+
: [], [{ text: s.votingCrossParty, severity: 'medium' }], highSeverity.length > 0
|
|
2628
|
+
? [{ text: s.votingHighSeverity(highSeverity.length), severity: 'high' }]
|
|
2629
|
+
: []);
|
|
2630
|
+
const economic = makeDimension('economic', adoptedCount > 0 ? [{ text: s.votingAdopted(adoptedCount), severity: 'medium' }] : [], [], realPatterns.length > 0
|
|
2631
|
+
? [{ text: s.votingDiverseGroups(realPatterns.length), severity: 'medium' }]
|
|
2632
|
+
: [], [{ text: s.votingShiftingAlliances, severity: 'medium' }]);
|
|
2633
|
+
const social = makeDimension('social', realRecords.length > 0
|
|
2634
|
+
? [{ text: s.votingActiveVotes(realRecords.length), severity: 'medium' }]
|
|
2635
|
+
: [], realAnomalies.length > 0
|
|
2636
|
+
? [{ text: s.votingAnomalies(realAnomalies.length), severity: 'medium' }]
|
|
2637
|
+
: [], [], []);
|
|
2638
|
+
const legal = makeDimension('legal', adoptedCount > 0 ? [{ text: s.votingAdopted(adoptedCount), severity: 'medium' }] : [], [], [], highSeverity.length > 0
|
|
2639
|
+
? [{ text: s.votingHighSeverity(highSeverity.length), severity: 'high' }]
|
|
2640
|
+
: []);
|
|
2641
|
+
const geopolitical = makeDimension('geopolitical', [], lowCohesion.length > 0
|
|
2642
|
+
? [{ text: s.votingLowCohesion(lowCohesion.length), severity: 'medium' }]
|
|
2643
|
+
: [], highCohesion.length > 0
|
|
2644
|
+
? [{ text: s.votingHighCohesion(highCohesion.length), severity: 'medium' }]
|
|
2645
|
+
: [], [{ text: s.votingShiftingAlliances, severity: 'medium' }]);
|
|
2646
|
+
const temporal = {
|
|
2647
|
+
shortTerm: base,
|
|
2648
|
+
mediumTerm: {
|
|
2649
|
+
strengths: base.strengths.filter((i) => i.severity === 'high'),
|
|
2650
|
+
weaknesses: base.weaknesses.filter((i) => i.severity === 'high'),
|
|
2651
|
+
opportunities: base.opportunities,
|
|
2652
|
+
threats: base.threats.filter((i) => i.severity === 'high'),
|
|
2653
|
+
},
|
|
2654
|
+
};
|
|
2655
|
+
const stakeholderViews = buildVotingMDStakeholders(adoptedCount, realAnomalies, highSeverity, highCohesion, lowCohesion, realPatterns, s);
|
|
2656
|
+
return {
|
|
2657
|
+
title: base.title,
|
|
2658
|
+
dimensions: [political, economic, social, legal, geopolitical],
|
|
2659
|
+
temporal,
|
|
2660
|
+
stakeholderViews,
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Build multi-dimensional SWOT analysis for prospective (week/month-ahead) articles.
|
|
2665
|
+
*
|
|
2666
|
+
* @param weekData - Aggregated week/month data
|
|
2667
|
+
* @param _label - "week" or "month" (reserved for future localisation)
|
|
2668
|
+
* @param lang - Target language code
|
|
2669
|
+
* @returns Multi-dimensional SWOT data
|
|
2670
|
+
*/
|
|
2671
|
+
export function buildProspectiveMultiDimensionalSwot(weekData, _label, lang = 'en') {
|
|
2672
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
2673
|
+
const base = buildProspectiveSwot(weekData, _label, lang);
|
|
2674
|
+
const bottlenecks = weekData.pipeline.filter((p) => p.bottleneck === true).length;
|
|
2675
|
+
const political = makeDimension('political', weekData.events.length > 0
|
|
2676
|
+
? [{ text: s.prospectiveEvents(weekData.events.length), severity: 'high' }]
|
|
2677
|
+
: [], bottlenecks > 0
|
|
2678
|
+
? [{ text: s.prospectiveBottlenecks(bottlenecks), severity: 'high' }]
|
|
2679
|
+
: [], [], bottlenecks > 0 ? [{ text: s.prospectiveBottleneckRisk, severity: 'high' }] : []);
|
|
2680
|
+
const economic = makeDimension('economic', [], weekData.events.length > 5
|
|
2681
|
+
? [{ text: s.prospectiveHighDensity(weekData.events.length), severity: 'medium' }]
|
|
2682
|
+
: [], weekData.documents.length > 0
|
|
2683
|
+
? [{ text: s.prospectiveDocuments(weekData.documents.length), severity: 'medium' }]
|
|
2684
|
+
: [], [{ text: s.prospectiveSchedulingRisk, severity: 'medium' }]);
|
|
2685
|
+
const social = makeDimension('social', weekData.committees.length > 0
|
|
2686
|
+
? [{ text: s.prospectiveCommittees(weekData.committees.length), severity: 'medium' }]
|
|
2687
|
+
: [], [], weekData.questions.length > 0
|
|
2688
|
+
? [{ text: s.prospectiveQuestions(weekData.questions.length), severity: 'medium' }]
|
|
2689
|
+
: [], []);
|
|
2690
|
+
const legal = makeDimension('legal', [], bottlenecks > 0
|
|
2691
|
+
? [{ text: s.prospectiveBottlenecks(bottlenecks), severity: 'high' }]
|
|
2692
|
+
: [], weekData.documents.length > 0
|
|
2693
|
+
? [{ text: s.prospectiveDocuments(weekData.documents.length), severity: 'medium' }]
|
|
2694
|
+
: [], bottlenecks > 0 ? [{ text: s.prospectiveBottleneckRisk, severity: 'high' }] : []);
|
|
2695
|
+
const geopolitical = makeDimension('geopolitical', weekData.events.length > 0
|
|
2696
|
+
? [{ text: s.prospectiveEvents(weekData.events.length), severity: 'medium' }]
|
|
2697
|
+
: [], [], [], [{ text: s.prospectiveSchedulingRisk, severity: 'medium' }]);
|
|
2698
|
+
const temporal = {
|
|
2699
|
+
shortTerm: base,
|
|
2700
|
+
mediumTerm: {
|
|
2701
|
+
strengths: base.strengths,
|
|
2702
|
+
weaknesses: base.weaknesses.filter((i) => i.severity === 'high'),
|
|
2703
|
+
opportunities: base.opportunities,
|
|
2704
|
+
threats: base.threats.filter((i) => i.severity === 'high'),
|
|
2705
|
+
},
|
|
2706
|
+
};
|
|
2707
|
+
return {
|
|
2708
|
+
dimensions: [political, economic, social, legal, geopolitical],
|
|
2709
|
+
temporal,
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Compute weakness and opportunity items for breaking news based on procedure count.
|
|
2714
|
+
* Returns a weakness when no procedures exist, or an opportunity when they do.
|
|
2715
|
+
*
|
|
2716
|
+
* @param procCount - Number of active procedures
|
|
2717
|
+
* @param s - Localized SWOT builder strings
|
|
2718
|
+
* @returns Tuple of weakness items and opportunity items
|
|
2719
|
+
*/
|
|
2720
|
+
function getBreakingProcedureItems(procCount, s) {
|
|
2721
|
+
if (procCount === 0) {
|
|
2722
|
+
return [[{ text: s.breakingNoProcedures, severity: 'medium' }], []];
|
|
2723
|
+
}
|
|
2724
|
+
return [[], [{ text: s.breakingProceduresActive(procCount), severity: 'medium' }]];
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Build the 5 SWOT dimensions for breaking news multi-dimensional SWOT.
|
|
2728
|
+
*
|
|
2729
|
+
* @param adoptedCount - Number of adopted texts
|
|
2730
|
+
* @param anomalyRaw - Raw anomaly text
|
|
2731
|
+
* @param coalitionRaw - Raw coalition text
|
|
2732
|
+
* @param procCount - Number of active procedures
|
|
2733
|
+
* @param eventCount - Number of events
|
|
2734
|
+
* @param s - Localized SWOT builder strings
|
|
2735
|
+
* @returns Array of 5 SwotDimension objects
|
|
2736
|
+
*/
|
|
2737
|
+
function buildBreakingMDDimensions(adoptedCount, anomalyRaw, coalitionRaw, procCount, eventCount, s) {
|
|
2738
|
+
const [procWeakness, procOpportunity] = getBreakingProcedureItems(procCount, s);
|
|
2739
|
+
const political = makeDimension('political', adoptedCount > 0 ? [{ text: s.breakingAdopted(adoptedCount), severity: 'high' }] : [], anomalyRaw ? [{ text: s.breakingAnomalyWeakness, severity: 'high' }] : [], coalitionRaw ? [{ text: s.breakingCoalitionOpportunity, severity: 'medium' }] : [], anomalyRaw ? [{ text: s.breakingAnomalyThreat, severity: 'high' }] : []);
|
|
2740
|
+
const economic = makeDimension('economic', adoptedCount > 0
|
|
2741
|
+
? [{ text: s.breakingAdopted(adoptedCount), severity: 'medium' }]
|
|
2742
|
+
: [], procWeakness, procOpportunity, [{ text: s.breakingRapidEvents, severity: 'medium' }]);
|
|
2743
|
+
const social = makeDimension('social', eventCount > 0 ? [{ text: s.breakingEvents(eventCount), severity: 'medium' }] : [], [], procOpportunity, [{ text: s.breakingRapidEvents, severity: 'medium' }]);
|
|
2744
|
+
const legal = makeDimension('legal', adoptedCount > 0 ? [{ text: s.breakingAdopted(adoptedCount), severity: 'high' }] : [], procWeakness, procOpportunity, anomalyRaw ? [{ text: s.breakingAnomalyThreat, severity: 'high' }] : []);
|
|
2745
|
+
const geopolitical = makeDimension('geopolitical', eventCount > 0 ? [{ text: s.breakingEvents(eventCount), severity: 'medium' }] : [], [], coalitionRaw ? [{ text: s.breakingCoalitionOpportunity, severity: 'medium' }] : [], anomalyRaw ? [{ text: s.breakingAnomalyThreat, severity: 'medium' }] : []);
|
|
2746
|
+
return [political, economic, social, legal, geopolitical];
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Build multi-dimensional SWOT analysis for breaking news articles.
|
|
2750
|
+
*
|
|
2751
|
+
* @param feedData - EP feed data
|
|
2752
|
+
* @param anomalyRaw - Raw anomaly text
|
|
2753
|
+
* @param coalitionRaw - Raw coalition text
|
|
2754
|
+
* @param lang - Target language code
|
|
2755
|
+
* @returns Multi-dimensional SWOT data
|
|
2756
|
+
*/
|
|
2757
|
+
export function buildBreakingMultiDimensionalSwot(feedData, anomalyRaw, coalitionRaw, lang = 'en') {
|
|
2758
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
2759
|
+
const base = buildBreakingSwot(feedData, anomalyRaw, coalitionRaw, lang);
|
|
2760
|
+
const adoptedCount = feedData?.adoptedTexts.length ?? 0;
|
|
2761
|
+
const eventCount = feedData?.events.length ?? 0;
|
|
2762
|
+
const procCount = feedData?.procedures.length ?? 0;
|
|
2763
|
+
const dimensions = buildBreakingMDDimensions(adoptedCount, anomalyRaw, coalitionRaw, procCount, eventCount, s);
|
|
2764
|
+
const temporal = {
|
|
2765
|
+
shortTerm: base,
|
|
2766
|
+
mediumTerm: {
|
|
2767
|
+
strengths: base.strengths.filter((i) => i.severity === 'high'),
|
|
2768
|
+
weaknesses: base.weaknesses,
|
|
2769
|
+
opportunities: base.opportunities,
|
|
2770
|
+
threats: base.threats.filter((i) => i.severity === 'high'),
|
|
2771
|
+
},
|
|
2772
|
+
};
|
|
2773
|
+
const stakeholderViews = buildBreakingMDStakeholders(adoptedCount, anomalyRaw, procCount, eventCount, coalitionRaw, s);
|
|
2774
|
+
return {
|
|
2775
|
+
dimensions,
|
|
2776
|
+
temporal,
|
|
2777
|
+
stakeholderViews,
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Build multi-dimensional SWOT analysis for propositions articles.
|
|
2782
|
+
*
|
|
2783
|
+
* @param pipelineData - Pipeline metrics
|
|
2784
|
+
* @param lang - Target language code
|
|
2785
|
+
* @returns Multi-dimensional SWOT data
|
|
2786
|
+
*/
|
|
2787
|
+
export function buildPropositionsMultiDimensionalSwot(pipelineData, lang = 'en') {
|
|
2788
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
2789
|
+
const base = buildPropositionsSwot(pipelineData, lang);
|
|
2790
|
+
const healthScore = pipelineData?.healthScore ?? 0;
|
|
2791
|
+
const throughput = pipelineData?.throughput ?? 0;
|
|
2792
|
+
const pct = (healthScore * 100).toFixed(0);
|
|
2793
|
+
const political = makeDimension('political', healthScore > 0.7 ? [{ text: s.propositionsHealthStrong(pct), severity: 'high' }] : [], healthScore < 0.5 ? [{ text: s.propositionsHealthWeak(pct), severity: 'high' }] : [], [{ text: s.propositionsPrioritisation, severity: 'medium' }], healthScore < 0.3 ? [{ text: s.propositionsCriticalCongestion, severity: 'high' }] : []);
|
|
2794
|
+
const economic = makeDimension('economic', throughput >= 5
|
|
2795
|
+
? [{ text: s.propositionsThroughputGood(throughput), severity: 'medium' }]
|
|
2796
|
+
: [], throughput < 5
|
|
2797
|
+
? [{ text: s.propositionsThroughputLow(throughput), severity: 'medium' }]
|
|
2798
|
+
: [], [{ text: s.propositionsTrilogueAcceleration, severity: 'medium' }], [{ text: s.propositionsOverlapping, severity: 'medium' }]);
|
|
2799
|
+
const social = makeDimension('social', [], [], [{ text: s.propositionsPrioritisation, severity: 'medium' }], healthScore < 0.3 ? [{ text: s.propositionsCriticalCongestion, severity: 'high' }] : []);
|
|
2800
|
+
const legal = makeDimension('legal', healthScore > 0.7 ? [{ text: s.propositionsHealthStrong(pct), severity: 'high' }] : [], healthScore < 0.5 ? [{ text: s.propositionsHealthWeak(pct), severity: 'high' }] : [], [{ text: s.propositionsTrilogueAcceleration, severity: 'medium' }], [{ text: s.propositionsOverlapping, severity: 'medium' }]);
|
|
2801
|
+
const geopolitical = makeDimension('geopolitical', throughput >= 5
|
|
2802
|
+
? [{ text: s.propositionsThroughputGood(throughput), severity: 'medium' }]
|
|
2803
|
+
: [], [], [], healthScore < 0.3
|
|
2804
|
+
? [{ text: s.propositionsCriticalCongestion, severity: 'high' }]
|
|
2805
|
+
: [{ text: s.propositionsOverlapping, severity: 'medium' }]);
|
|
2806
|
+
const temporal = {
|
|
2807
|
+
shortTerm: base,
|
|
2808
|
+
mediumTerm: {
|
|
2809
|
+
strengths: base.strengths,
|
|
2810
|
+
weaknesses: base.weaknesses.filter((i) => i.severity === 'high'),
|
|
2811
|
+
opportunities: base.opportunities,
|
|
2812
|
+
threats: base.threats.filter((i) => i.severity === 'high'),
|
|
2813
|
+
},
|
|
2814
|
+
};
|
|
2815
|
+
const stakeholderViews = {
|
|
2816
|
+
industry: {
|
|
2817
|
+
strengths: throughput >= 5
|
|
2818
|
+
? [{ text: s.propositionsThroughputGood(throughput), severity: 'medium' }]
|
|
2819
|
+
: [],
|
|
2820
|
+
weaknesses: throughput < 5
|
|
2821
|
+
? [{ text: s.propositionsThroughputLow(throughput), severity: 'medium' }]
|
|
2822
|
+
: [],
|
|
2823
|
+
opportunities: [{ text: s.propositionsTrilogueAcceleration, severity: 'medium' }],
|
|
2824
|
+
threats: [{ text: s.propositionsOverlapping, severity: 'medium' }],
|
|
2825
|
+
},
|
|
2826
|
+
government: {
|
|
2827
|
+
strengths: healthScore > 0.7
|
|
2828
|
+
? [{ text: s.propositionsHealthStrong(pct), severity: 'high' }]
|
|
2829
|
+
: [],
|
|
2830
|
+
weaknesses: healthScore < 0.5
|
|
2831
|
+
? [{ text: s.propositionsHealthWeak(pct), severity: 'high' }]
|
|
2832
|
+
: [],
|
|
2833
|
+
opportunities: [{ text: s.propositionsPrioritisation, severity: 'medium' }],
|
|
2834
|
+
threats: healthScore < 0.3
|
|
2835
|
+
? [{ text: s.propositionsCriticalCongestion, severity: 'high' }]
|
|
2836
|
+
: [],
|
|
2837
|
+
},
|
|
2838
|
+
};
|
|
2839
|
+
return {
|
|
2840
|
+
dimensions: [political, economic, social, legal, geopolitical],
|
|
2841
|
+
temporal,
|
|
2842
|
+
stakeholderViews,
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Build multi-dimensional SWOT analysis for committee reports articles.
|
|
2847
|
+
*
|
|
2848
|
+
* @param committees - Committee data list
|
|
2849
|
+
* @param lang - Target language code
|
|
2850
|
+
* @returns Multi-dimensional SWOT data, or `null` when all committee data is placeholder
|
|
2851
|
+
*/
|
|
2852
|
+
export function buildCommitteeMultiDimensionalSwot(committees, lang = 'en') {
|
|
2853
|
+
if (isPlaceholderCommitteeData(committees))
|
|
2854
|
+
return null;
|
|
2855
|
+
const s = getLocalizedString(SWOT_BUILDER_STRINGS, lang);
|
|
2856
|
+
const base = buildCommitteeSwot(committees, lang);
|
|
2857
|
+
if (!base)
|
|
2858
|
+
return null;
|
|
2859
|
+
const active = committees.filter((c) => c.documents.length > 0);
|
|
2860
|
+
const totalDocs = committees.reduce((sum, c) => sum + c.documents.length, 0);
|
|
2861
|
+
const inactiveCount = committees.length - active.length;
|
|
2862
|
+
const highActivity = active.length >= committees.length * 0.7;
|
|
2863
|
+
const political = makeDimension('political', active.length > 0
|
|
2864
|
+
? [
|
|
2865
|
+
{
|
|
2866
|
+
text: s.committeeActive(active.length, committees.length),
|
|
2867
|
+
severity: highActivity ? 'high' : 'medium',
|
|
2868
|
+
},
|
|
2869
|
+
]
|
|
2870
|
+
: [], inactiveCount > committees.length * 0.3
|
|
2871
|
+
? [{ text: s.committeeInactive(inactiveCount), severity: 'high' }]
|
|
2872
|
+
: [], [{ text: s.committeeCrossCollaboration, severity: 'medium' }], inactiveCount > committees.length * 0.3
|
|
2873
|
+
? [{ text: s.committeeLowActivity, severity: 'high' }]
|
|
2874
|
+
: [{ text: s.committeeCompetingPriorities, severity: 'medium' }]);
|
|
2875
|
+
const economic = makeDimension('economic', totalDocs > 0 ? [{ text: s.committeeDocuments(totalDocs), severity: 'medium' }] : [], inactiveCount > 0
|
|
2876
|
+
? [{ text: s.committeeInactive(inactiveCount), severity: 'medium' }]
|
|
2877
|
+
: [], committees.length > 0 ? [{ text: s.committeeHearings, severity: 'medium' }] : [], [{ text: s.committeeCompetingPriorities, severity: 'medium' }]);
|
|
2878
|
+
const social = makeDimension('social', active.length > 0
|
|
2879
|
+
? [{ text: s.committeeActive(active.length, committees.length), severity: 'medium' }]
|
|
2880
|
+
: [], [], [{ text: s.committeeCrossCollaboration, severity: 'medium' }], []);
|
|
2881
|
+
const legal = makeDimension('legal', totalDocs > 0 ? [{ text: s.committeeDocuments(totalDocs), severity: 'high' }] : [], inactiveCount > committees.length * 0.3
|
|
2882
|
+
? [{ text: s.committeeInactive(inactiveCount), severity: 'high' }]
|
|
2883
|
+
: [], committees.length > 0 ? [{ text: s.committeeHearings, severity: 'medium' }] : [], inactiveCount > committees.length * 0.3
|
|
2884
|
+
? [{ text: s.committeeLowActivity, severity: 'high' }]
|
|
2885
|
+
: []);
|
|
2886
|
+
const geopolitical = makeDimension('geopolitical', [], [], [{ text: s.committeeCrossCollaboration, severity: 'medium' }], [{ text: s.committeeCompetingPriorities, severity: 'medium' }]);
|
|
2887
|
+
const temporal = {
|
|
2888
|
+
shortTerm: base,
|
|
2889
|
+
mediumTerm: {
|
|
2890
|
+
strengths: base.strengths.filter((i) => i.severity === 'high'),
|
|
2891
|
+
weaknesses: base.weaknesses,
|
|
2892
|
+
opportunities: base.opportunities,
|
|
2893
|
+
threats: base.threats,
|
|
2894
|
+
},
|
|
2895
|
+
};
|
|
2896
|
+
const stakeholderViews = buildCommitteeMDStakeholders(active, committees, totalDocs, inactiveCount, s);
|
|
2897
|
+
return {
|
|
2898
|
+
dimensions: [political, economic, social, legal, geopolitical],
|
|
2899
|
+
temporal,
|
|
2900
|
+
stakeholderViews,
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
//# sourceMappingURL=analysis-builders.js.map
|