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.
Files changed (276) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1005 -0
  3. package/SECURITY.md +151 -0
  4. package/package.json +131 -0
  5. package/scripts/constants/committee-indicator-map.d.ts +199 -0
  6. package/scripts/constants/committee-indicator-map.d.ts.map +1 -0
  7. package/scripts/constants/committee-indicator-map.js +1224 -0
  8. package/scripts/constants/committee-indicator-map.js.map +1 -0
  9. package/scripts/constants/config.d.ts +38 -0
  10. package/scripts/constants/config.d.ts.map +1 -0
  11. package/scripts/constants/config.js +66 -0
  12. package/scripts/constants/config.js.map +1 -0
  13. package/scripts/constants/language-articles.d.ts +84 -0
  14. package/scripts/constants/language-articles.d.ts.map +1 -0
  15. package/scripts/constants/language-articles.js +6771 -0
  16. package/scripts/constants/language-articles.js.map +1 -0
  17. package/scripts/constants/language-core.d.ts +38 -0
  18. package/scripts/constants/language-core.d.ts.map +1 -0
  19. package/scripts/constants/language-core.js +90 -0
  20. package/scripts/constants/language-core.js.map +1 -0
  21. package/scripts/constants/language-ui.d.ts +82 -0
  22. package/scripts/constants/language-ui.d.ts.map +1 -0
  23. package/scripts/constants/language-ui.js +889 -0
  24. package/scripts/constants/language-ui.js.map +1 -0
  25. package/scripts/constants/languages.d.ts +14 -0
  26. package/scripts/constants/languages.d.ts.map +1 -0
  27. package/scripts/constants/languages.js +15 -0
  28. package/scripts/constants/languages.js.map +1 -0
  29. package/scripts/generators/analysis-builders.d.ts +266 -0
  30. package/scripts/generators/analysis-builders.d.ts.map +1 -0
  31. package/scripts/generators/analysis-builders.js +2903 -0
  32. package/scripts/generators/analysis-builders.js.map +1 -0
  33. package/scripts/generators/breaking-content.d.ts +45 -0
  34. package/scripts/generators/breaking-content.d.ts.map +1 -0
  35. package/scripts/generators/breaking-content.js +530 -0
  36. package/scripts/generators/breaking-content.js.map +1 -0
  37. package/scripts/generators/committee-helpers.d.ts +54 -0
  38. package/scripts/generators/committee-helpers.d.ts.map +1 -0
  39. package/scripts/generators/committee-helpers.js +154 -0
  40. package/scripts/generators/committee-helpers.js.map +1 -0
  41. package/scripts/generators/dashboard-content.d.ts +95 -0
  42. package/scripts/generators/dashboard-content.d.ts.map +1 -0
  43. package/scripts/generators/dashboard-content.js +630 -0
  44. package/scripts/generators/dashboard-content.js.map +1 -0
  45. package/scripts/generators/deep-analysis-content.d.ts +23 -0
  46. package/scripts/generators/deep-analysis-content.d.ts.map +1 -0
  47. package/scripts/generators/deep-analysis-content.js +831 -0
  48. package/scripts/generators/deep-analysis-content.js.map +1 -0
  49. package/scripts/generators/mindmap-content.d.ts +55 -0
  50. package/scripts/generators/mindmap-content.d.ts.map +1 -0
  51. package/scripts/generators/mindmap-content.js +512 -0
  52. package/scripts/generators/mindmap-content.js.map +1 -0
  53. package/scripts/generators/motions-content.d.ts +50 -0
  54. package/scripts/generators/motions-content.d.ts.map +1 -0
  55. package/scripts/generators/motions-content.js +391 -0
  56. package/scripts/generators/motions-content.js.map +1 -0
  57. package/scripts/generators/news-enhanced.d.ts +14 -0
  58. package/scripts/generators/news-enhanced.d.ts.map +1 -0
  59. package/scripts/generators/news-enhanced.js +169 -0
  60. package/scripts/generators/news-enhanced.js.map +1 -0
  61. package/scripts/generators/news-indexes.d.ts +31 -0
  62. package/scripts/generators/news-indexes.d.ts.map +1 -0
  63. package/scripts/generators/news-indexes.js +410 -0
  64. package/scripts/generators/news-indexes.js.map +1 -0
  65. package/scripts/generators/pipeline/fetch-stage.d.ts +352 -0
  66. package/scripts/generators/pipeline/fetch-stage.d.ts.map +1 -0
  67. package/scripts/generators/pipeline/fetch-stage.js +1522 -0
  68. package/scripts/generators/pipeline/fetch-stage.js.map +1 -0
  69. package/scripts/generators/pipeline/generate-stage.d.ts +43 -0
  70. package/scripts/generators/pipeline/generate-stage.d.ts.map +1 -0
  71. package/scripts/generators/pipeline/generate-stage.js +204 -0
  72. package/scripts/generators/pipeline/generate-stage.js.map +1 -0
  73. package/scripts/generators/pipeline/output-stage.d.ts +48 -0
  74. package/scripts/generators/pipeline/output-stage.d.ts.map +1 -0
  75. package/scripts/generators/pipeline/output-stage.js +145 -0
  76. package/scripts/generators/pipeline/output-stage.js.map +1 -0
  77. package/scripts/generators/pipeline/transform-stage.d.ts +57 -0
  78. package/scripts/generators/pipeline/transform-stage.d.ts.map +1 -0
  79. package/scripts/generators/pipeline/transform-stage.js +111 -0
  80. package/scripts/generators/pipeline/transform-stage.js.map +1 -0
  81. package/scripts/generators/propositions-content.d.ts +29 -0
  82. package/scripts/generators/propositions-content.d.ts.map +1 -0
  83. package/scripts/generators/propositions-content.js +90 -0
  84. package/scripts/generators/propositions-content.js.map +1 -0
  85. package/scripts/generators/sankey-content.d.ts +45 -0
  86. package/scripts/generators/sankey-content.d.ts.map +1 -0
  87. package/scripts/generators/sankey-content.js +227 -0
  88. package/scripts/generators/sankey-content.js.map +1 -0
  89. package/scripts/generators/sitemap.d.ts +66 -0
  90. package/scripts/generators/sitemap.d.ts.map +1 -0
  91. package/scripts/generators/sitemap.js +562 -0
  92. package/scripts/generators/sitemap.js.map +1 -0
  93. package/scripts/generators/strategies/article-strategy.d.ts +146 -0
  94. package/scripts/generators/strategies/article-strategy.d.ts.map +1 -0
  95. package/scripts/generators/strategies/article-strategy.js +4 -0
  96. package/scripts/generators/strategies/article-strategy.js.map +1 -0
  97. package/scripts/generators/strategies/breaking-news-strategy.d.ts +64 -0
  98. package/scripts/generators/strategies/breaking-news-strategy.d.ts.map +1 -0
  99. package/scripts/generators/strategies/breaking-news-strategy.js +246 -0
  100. package/scripts/generators/strategies/breaking-news-strategy.js.map +1 -0
  101. package/scripts/generators/strategies/committee-reports-strategy.d.ts +93 -0
  102. package/scripts/generators/strategies/committee-reports-strategy.d.ts.map +1 -0
  103. package/scripts/generators/strategies/committee-reports-strategy.js +447 -0
  104. package/scripts/generators/strategies/committee-reports-strategy.js.map +1 -0
  105. package/scripts/generators/strategies/month-ahead-strategy.d.ts +60 -0
  106. package/scripts/generators/strategies/month-ahead-strategy.d.ts.map +1 -0
  107. package/scripts/generators/strategies/month-ahead-strategy.js +175 -0
  108. package/scripts/generators/strategies/month-ahead-strategy.js.map +1 -0
  109. package/scripts/generators/strategies/monthly-review-strategy.d.ts +66 -0
  110. package/scripts/generators/strategies/monthly-review-strategy.d.ts.map +1 -0
  111. package/scripts/generators/strategies/monthly-review-strategy.js +204 -0
  112. package/scripts/generators/strategies/monthly-review-strategy.js.map +1 -0
  113. package/scripts/generators/strategies/motions-strategy.d.ts +61 -0
  114. package/scripts/generators/strategies/motions-strategy.d.ts.map +1 -0
  115. package/scripts/generators/strategies/motions-strategy.js +215 -0
  116. package/scripts/generators/strategies/motions-strategy.js.map +1 -0
  117. package/scripts/generators/strategies/propositions-strategy.d.ts +60 -0
  118. package/scripts/generators/strategies/propositions-strategy.d.ts.map +1 -0
  119. package/scripts/generators/strategies/propositions-strategy.js +257 -0
  120. package/scripts/generators/strategies/propositions-strategy.js.map +1 -0
  121. package/scripts/generators/strategies/week-ahead-strategy.d.ts +57 -0
  122. package/scripts/generators/strategies/week-ahead-strategy.d.ts.map +1 -0
  123. package/scripts/generators/strategies/week-ahead-strategy.js +178 -0
  124. package/scripts/generators/strategies/week-ahead-strategy.js.map +1 -0
  125. package/scripts/generators/strategies/weekly-review-strategy.d.ts +63 -0
  126. package/scripts/generators/strategies/weekly-review-strategy.d.ts.map +1 -0
  127. package/scripts/generators/strategies/weekly-review-strategy.js +211 -0
  128. package/scripts/generators/strategies/weekly-review-strategy.js.map +1 -0
  129. package/scripts/generators/swot-content.d.ts +42 -0
  130. package/scripts/generators/swot-content.d.ts.map +1 -0
  131. package/scripts/generators/swot-content.js +366 -0
  132. package/scripts/generators/swot-content.js.map +1 -0
  133. package/scripts/generators/week-ahead-content.d.ts +103 -0
  134. package/scripts/generators/week-ahead-content.d.ts.map +1 -0
  135. package/scripts/generators/week-ahead-content.js +610 -0
  136. package/scripts/generators/week-ahead-content.js.map +1 -0
  137. package/scripts/index.d.ts +40 -0
  138. package/scripts/index.d.ts.map +1 -0
  139. package/scripts/index.js +53 -0
  140. package/scripts/index.js.map +1 -0
  141. package/scripts/mcp/ep-mcp-client.d.ts +471 -0
  142. package/scripts/mcp/ep-mcp-client.d.ts.map +1 -0
  143. package/scripts/mcp/ep-mcp-client.js +734 -0
  144. package/scripts/mcp/ep-mcp-client.js.map +1 -0
  145. package/scripts/mcp/mcp-connection.d.ts +264 -0
  146. package/scripts/mcp/mcp-connection.d.ts.map +1 -0
  147. package/scripts/mcp/mcp-connection.js +790 -0
  148. package/scripts/mcp/mcp-connection.js.map +1 -0
  149. package/scripts/mcp/mcp-health.d.ts +75 -0
  150. package/scripts/mcp/mcp-health.d.ts.map +1 -0
  151. package/scripts/mcp/mcp-health.js +78 -0
  152. package/scripts/mcp/mcp-health.js.map +1 -0
  153. package/scripts/mcp/mcp-retry.d.ts +94 -0
  154. package/scripts/mcp/mcp-retry.d.ts.map +1 -0
  155. package/scripts/mcp/mcp-retry.js +127 -0
  156. package/scripts/mcp/mcp-retry.js.map +1 -0
  157. package/scripts/mcp/wb-mcp-client.d.ts +38 -0
  158. package/scripts/mcp/wb-mcp-client.d.ts.map +1 -0
  159. package/scripts/mcp/wb-mcp-client.js +112 -0
  160. package/scripts/mcp/wb-mcp-client.js.map +1 -0
  161. package/scripts/templates/article-template.d.ts +9 -0
  162. package/scripts/templates/article-template.d.ts.map +1 -0
  163. package/scripts/templates/article-template.js +378 -0
  164. package/scripts/templates/article-template.js.map +1 -0
  165. package/scripts/templates/section-builders.d.ts +28 -0
  166. package/scripts/templates/section-builders.d.ts.map +1 -0
  167. package/scripts/templates/section-builders.js +142 -0
  168. package/scripts/templates/section-builders.js.map +1 -0
  169. package/scripts/types/analysis.d.ts +115 -0
  170. package/scripts/types/analysis.d.ts.map +1 -0
  171. package/scripts/types/analysis.js +4 -0
  172. package/scripts/types/analysis.js.map +1 -0
  173. package/scripts/types/common.d.ts +584 -0
  174. package/scripts/types/common.d.ts.map +1 -0
  175. package/scripts/types/common.js +96 -0
  176. package/scripts/types/common.js.map +1 -0
  177. package/scripts/types/generation.d.ts +104 -0
  178. package/scripts/types/generation.d.ts.map +1 -0
  179. package/scripts/types/generation.js +4 -0
  180. package/scripts/types/generation.js.map +1 -0
  181. package/scripts/types/index.d.ts +24 -0
  182. package/scripts/types/index.d.ts.map +1 -0
  183. package/scripts/types/index.js +16 -0
  184. package/scripts/types/index.js.map +1 -0
  185. package/scripts/types/intelligence.d.ts +129 -0
  186. package/scripts/types/intelligence.d.ts.map +1 -0
  187. package/scripts/types/intelligence.js +4 -0
  188. package/scripts/types/intelligence.js.map +1 -0
  189. package/scripts/types/mcp.d.ts +418 -0
  190. package/scripts/types/mcp.d.ts.map +1 -0
  191. package/scripts/types/mcp.js +4 -0
  192. package/scripts/types/mcp.js.map +1 -0
  193. package/scripts/types/parliament.d.ts +388 -0
  194. package/scripts/types/parliament.d.ts.map +1 -0
  195. package/scripts/types/parliament.js +4 -0
  196. package/scripts/types/parliament.js.map +1 -0
  197. package/scripts/types/quality.d.ts +114 -0
  198. package/scripts/types/quality.d.ts.map +1 -0
  199. package/scripts/types/quality.js +4 -0
  200. package/scripts/types/quality.js.map +1 -0
  201. package/scripts/types/stakeholder.d.ts +88 -0
  202. package/scripts/types/stakeholder.d.ts.map +1 -0
  203. package/scripts/types/stakeholder.js +16 -0
  204. package/scripts/types/stakeholder.js.map +1 -0
  205. package/scripts/types/visualization.d.ts +708 -0
  206. package/scripts/types/visualization.d.ts.map +1 -0
  207. package/scripts/types/visualization.js +4 -0
  208. package/scripts/types/visualization.js.map +1 -0
  209. package/scripts/types/world-bank.d.ts +85 -0
  210. package/scripts/types/world-bank.d.ts.map +1 -0
  211. package/scripts/types/world-bank.js +4 -0
  212. package/scripts/types/world-bank.js.map +1 -0
  213. package/scripts/utils/article-category.d.ts +18 -0
  214. package/scripts/utils/article-category.d.ts.map +1 -0
  215. package/scripts/utils/article-category.js +49 -0
  216. package/scripts/utils/article-category.js.map +1 -0
  217. package/scripts/utils/article-quality-scorer.d.ts +87 -0
  218. package/scripts/utils/article-quality-scorer.d.ts.map +1 -0
  219. package/scripts/utils/article-quality-scorer.js +1048 -0
  220. package/scripts/utils/article-quality-scorer.js.map +1 -0
  221. package/scripts/utils/content-metadata.d.ts +34 -0
  222. package/scripts/utils/content-metadata.d.ts.map +1 -0
  223. package/scripts/utils/content-metadata.js +249 -0
  224. package/scripts/utils/content-metadata.js.map +1 -0
  225. package/scripts/utils/content-validator.d.ts +94 -0
  226. package/scripts/utils/content-validator.d.ts.map +1 -0
  227. package/scripts/utils/content-validator.js +489 -0
  228. package/scripts/utils/content-validator.js.map +1 -0
  229. package/scripts/utils/copy-test-reports.d.ts +9 -0
  230. package/scripts/utils/copy-test-reports.d.ts.map +1 -0
  231. package/scripts/utils/copy-test-reports.js +508 -0
  232. package/scripts/utils/copy-test-reports.js.map +1 -0
  233. package/scripts/utils/file-utils.d.ts +144 -0
  234. package/scripts/utils/file-utils.d.ts.map +1 -0
  235. package/scripts/utils/file-utils.js +374 -0
  236. package/scripts/utils/file-utils.js.map +1 -0
  237. package/scripts/utils/fix-articles.d.ts +27 -0
  238. package/scripts/utils/fix-articles.d.ts.map +1 -0
  239. package/scripts/utils/fix-articles.js +510 -0
  240. package/scripts/utils/fix-articles.js.map +1 -0
  241. package/scripts/utils/generate-docs-index.d.ts +8 -0
  242. package/scripts/utils/generate-docs-index.d.ts.map +1 -0
  243. package/scripts/utils/generate-docs-index.js +275 -0
  244. package/scripts/utils/generate-docs-index.js.map +1 -0
  245. package/scripts/utils/html-sanitize.d.ts +18 -0
  246. package/scripts/utils/html-sanitize.d.ts.map +1 -0
  247. package/scripts/utils/html-sanitize.js +57 -0
  248. package/scripts/utils/html-sanitize.js.map +1 -0
  249. package/scripts/utils/intelligence-analysis.d.ts +173 -0
  250. package/scripts/utils/intelligence-analysis.d.ts.map +1 -0
  251. package/scripts/utils/intelligence-analysis.js +936 -0
  252. package/scripts/utils/intelligence-analysis.js.map +1 -0
  253. package/scripts/utils/intelligence-index.d.ts +126 -0
  254. package/scripts/utils/intelligence-index.d.ts.map +1 -0
  255. package/scripts/utils/intelligence-index.js +731 -0
  256. package/scripts/utils/intelligence-index.js.map +1 -0
  257. package/scripts/utils/metadata-utils.d.ts +14 -0
  258. package/scripts/utils/metadata-utils.d.ts.map +1 -0
  259. package/scripts/utils/metadata-utils.js +18 -0
  260. package/scripts/utils/metadata-utils.js.map +1 -0
  261. package/scripts/utils/news-metadata.d.ts +47 -0
  262. package/scripts/utils/news-metadata.d.ts.map +1 -0
  263. package/scripts/utils/news-metadata.js +259 -0
  264. package/scripts/utils/news-metadata.js.map +1 -0
  265. package/scripts/utils/validate-articles.d.ts +2 -0
  266. package/scripts/utils/validate-articles.d.ts.map +1 -0
  267. package/scripts/utils/validate-articles.js +284 -0
  268. package/scripts/utils/validate-articles.js.map +1 -0
  269. package/scripts/utils/validate-ep-api.d.ts +51 -0
  270. package/scripts/utils/validate-ep-api.d.ts.map +1 -0
  271. package/scripts/utils/validate-ep-api.js +160 -0
  272. package/scripts/utils/validate-ep-api.js.map +1 -0
  273. package/scripts/utils/world-bank-data.d.ts +84 -0
  274. package/scripts/utils/world-bank-data.d.ts.map +1 -0
  275. package/scripts/utils/world-bank-data.js +311 -0
  276. 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