riksdagsmonitor 0.8.23 → 0.8.24

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 (546) hide show
  1. package/README.md +53 -1
  2. package/dist/lib/dashboards/party-dashboard.js +2 -1
  3. package/dist/lib/dashboards/party-dashboard.js.map +1 -1
  4. package/dist/lib/dashboards/risk-dashboard.js +2 -1
  5. package/dist/lib/dashboards/risk-dashboard.js.map +1 -1
  6. package/dist/lib/shared/index.d.ts +0 -1
  7. package/dist/lib/shared/index.d.ts.map +1 -1
  8. package/dist/lib/shared/index.js +3 -1
  9. package/dist/lib/shared/index.js.map +1 -1
  10. package/dist/lib/shared/register-globals.d.ts +1 -1
  11. package/dist/lib/shared/register-globals.d.ts.map +1 -1
  12. package/dist/lib/shared/register-globals.js +7 -1
  13. package/dist/lib/shared/register-globals.js.map +1 -1
  14. package/package.json +53 -21
  15. package/dist/scripts/analysis-reader.d.ts +0 -241
  16. package/dist/scripts/analysis-reader.d.ts.map +0 -1
  17. package/dist/scripts/analysis-reader.js +0 -531
  18. package/dist/scripts/analysis-reader.js.map +0 -1
  19. package/dist/scripts/article-quality-enhancer.d.ts +0 -148
  20. package/dist/scripts/article-quality-enhancer.d.ts.map +0 -1
  21. package/dist/scripts/article-quality-enhancer.js +0 -430
  22. package/dist/scripts/article-quality-enhancer.js.map +0 -1
  23. package/dist/scripts/article-template/constants.d.ts +0 -104
  24. package/dist/scripts/article-template/constants.d.ts.map +0 -1
  25. package/dist/scripts/article-template/constants.js +0 -225
  26. package/dist/scripts/article-template/constants.js.map +0 -1
  27. package/dist/scripts/article-template/helpers.d.ts +0 -83
  28. package/dist/scripts/article-template/helpers.d.ts.map +0 -1
  29. package/dist/scripts/article-template/helpers.js +0 -236
  30. package/dist/scripts/article-template/helpers.js.map +0 -1
  31. package/dist/scripts/article-template/index.d.ts +0 -39
  32. package/dist/scripts/article-template/index.d.ts.map +0 -1
  33. package/dist/scripts/article-template/index.js +0 -37
  34. package/dist/scripts/article-template/index.js.map +0 -1
  35. package/dist/scripts/article-template/registry.d.ts +0 -64
  36. package/dist/scripts/article-template/registry.d.ts.map +0 -1
  37. package/dist/scripts/article-template/registry.js +0 -405
  38. package/dist/scripts/article-template/registry.js.map +0 -1
  39. package/dist/scripts/article-template/template.d.ts +0 -18
  40. package/dist/scripts/article-template/template.d.ts.map +0 -1
  41. package/dist/scripts/article-template/template.js +0 -359
  42. package/dist/scripts/article-template/template.js.map +0 -1
  43. package/dist/scripts/article-template/types.d.ts +0 -90
  44. package/dist/scripts/article-template/types.d.ts.map +0 -1
  45. package/dist/scripts/article-template/types.js +0 -96
  46. package/dist/scripts/article-template/types.js.map +0 -1
  47. package/dist/scripts/article-template.d.ts +0 -18
  48. package/dist/scripts/article-template.d.ts.map +0 -1
  49. package/dist/scripts/article-template.js +0 -18
  50. package/dist/scripts/article-template.js.map +0 -1
  51. package/dist/scripts/catalog-downloaded-data.d.ts +0 -66
  52. package/dist/scripts/catalog-downloaded-data.d.ts.map +0 -1
  53. package/dist/scripts/catalog-downloaded-data.js +0 -239
  54. package/dist/scripts/catalog-downloaded-data.js.map +0 -1
  55. package/dist/scripts/check-cia-schema-updates.d.ts +0 -186
  56. package/dist/scripts/check-cia-schema-updates.d.ts.map +0 -1
  57. package/dist/scripts/check-cia-schema-updates.js +0 -363
  58. package/dist/scripts/check-cia-schema-updates.js.map +0 -1
  59. package/dist/scripts/data-transformers/calendar.d.ts +0 -16
  60. package/dist/scripts/data-transformers/calendar.d.ts.map +0 -1
  61. package/dist/scripts/data-transformers/calendar.js +0 -137
  62. package/dist/scripts/data-transformers/calendar.js.map +0 -1
  63. package/dist/scripts/data-transformers/constants/committee-names.d.ts +0 -15
  64. package/dist/scripts/data-transformers/constants/committee-names.d.ts.map +0 -1
  65. package/dist/scripts/data-transformers/constants/committee-names.js +0 -30
  66. package/dist/scripts/data-transformers/constants/committee-names.js.map +0 -1
  67. package/dist/scripts/data-transformers/constants/content-labels-part1.d.ts +0 -11
  68. package/dist/scripts/data-transformers/constants/content-labels-part1.d.ts.map +0 -1
  69. package/dist/scripts/data-transformers/constants/content-labels-part1.js +0 -900
  70. package/dist/scripts/data-transformers/constants/content-labels-part1.js.map +0 -1
  71. package/dist/scripts/data-transformers/constants/content-labels-part2.d.ts +0 -11
  72. package/dist/scripts/data-transformers/constants/content-labels-part2.d.ts.map +0 -1
  73. package/dist/scripts/data-transformers/constants/content-labels-part2.js +0 -900
  74. package/dist/scripts/data-transformers/constants/content-labels-part2.js.map +0 -1
  75. package/dist/scripts/data-transformers/constants/content-labels.d.ts +0 -13
  76. package/dist/scripts/data-transformers/constants/content-labels.d.ts.map +0 -1
  77. package/dist/scripts/data-transformers/constants/content-labels.js +0 -18
  78. package/dist/scripts/data-transformers/constants/content-labels.js.map +0 -1
  79. package/dist/scripts/data-transformers/constants/index.d.ts +0 -15
  80. package/dist/scripts/data-transformers/constants/index.d.ts.map +0 -1
  81. package/dist/scripts/data-transformers/constants/index.js +0 -15
  82. package/dist/scripts/data-transformers/constants/index.js.map +0 -1
  83. package/dist/scripts/data-transformers/constants/locale-map.d.ts +0 -13
  84. package/dist/scripts/data-transformers/constants/locale-map.d.ts.map +0 -1
  85. package/dist/scripts/data-transformers/constants/locale-map.js +0 -17
  86. package/dist/scripts/data-transformers/constants/locale-map.js.map +0 -1
  87. package/dist/scripts/data-transformers/constants.d.ts +0 -14
  88. package/dist/scripts/data-transformers/constants.d.ts.map +0 -1
  89. package/dist/scripts/data-transformers/constants.js +0 -14
  90. package/dist/scripts/data-transformers/constants.js.map +0 -1
  91. package/dist/scripts/data-transformers/content-generators/ai-mindmap-analyzer.d.ts +0 -64
  92. package/dist/scripts/data-transformers/content-generators/ai-mindmap-analyzer.d.ts.map +0 -1
  93. package/dist/scripts/data-transformers/content-generators/ai-mindmap-analyzer.js +0 -805
  94. package/dist/scripts/data-transformers/content-generators/ai-mindmap-analyzer.js.map +0 -1
  95. package/dist/scripts/data-transformers/content-generators/cia-overview-section.d.ts +0 -50
  96. package/dist/scripts/data-transformers/content-generators/cia-overview-section.d.ts.map +0 -1
  97. package/dist/scripts/data-transformers/content-generators/cia-overview-section.js +0 -310
  98. package/dist/scripts/data-transformers/content-generators/cia-overview-section.js.map +0 -1
  99. package/dist/scripts/data-transformers/content-generators/committee.d.ts +0 -12
  100. package/dist/scripts/data-transformers/content-generators/committee.d.ts.map +0 -1
  101. package/dist/scripts/data-transformers/content-generators/committee.js +0 -247
  102. package/dist/scripts/data-transformers/content-generators/committee.js.map +0 -1
  103. package/dist/scripts/data-transformers/content-generators/dashboard-section.d.ts +0 -124
  104. package/dist/scripts/data-transformers/content-generators/dashboard-section.d.ts.map +0 -1
  105. package/dist/scripts/data-transformers/content-generators/dashboard-section.js +0 -564
  106. package/dist/scripts/data-transformers/content-generators/dashboard-section.js.map +0 -1
  107. package/dist/scripts/data-transformers/content-generators/economic-dashboard-section.d.ts +0 -82
  108. package/dist/scripts/data-transformers/content-generators/economic-dashboard-section.d.ts.map +0 -1
  109. package/dist/scripts/data-transformers/content-generators/economic-dashboard-section.js +0 -321
  110. package/dist/scripts/data-transformers/content-generators/economic-dashboard-section.js.map +0 -1
  111. package/dist/scripts/data-transformers/content-generators/generic.d.ts +0 -12
  112. package/dist/scripts/data-transformers/content-generators/generic.d.ts.map +0 -1
  113. package/dist/scripts/data-transformers/content-generators/generic.js +0 -295
  114. package/dist/scripts/data-transformers/content-generators/generic.js.map +0 -1
  115. package/dist/scripts/data-transformers/content-generators/index.d.ts +0 -84
  116. package/dist/scripts/data-transformers/content-generators/index.d.ts.map +0 -1
  117. package/dist/scripts/data-transformers/content-generators/index.js +0 -47
  118. package/dist/scripts/data-transformers/content-generators/index.js.map +0 -1
  119. package/dist/scripts/data-transformers/content-generators/interpellations.d.ts +0 -12
  120. package/dist/scripts/data-transformers/content-generators/interpellations.d.ts.map +0 -1
  121. package/dist/scripts/data-transformers/content-generators/interpellations.js +0 -124
  122. package/dist/scripts/data-transformers/content-generators/interpellations.js.map +0 -1
  123. package/dist/scripts/data-transformers/content-generators/mindmap-section.d.ts +0 -137
  124. package/dist/scripts/data-transformers/content-generators/mindmap-section.d.ts.map +0 -1
  125. package/dist/scripts/data-transformers/content-generators/mindmap-section.js +0 -286
  126. package/dist/scripts/data-transformers/content-generators/mindmap-section.js.map +0 -1
  127. package/dist/scripts/data-transformers/content-generators/month-ahead.d.ts +0 -17
  128. package/dist/scripts/data-transformers/content-generators/month-ahead.d.ts.map +0 -1
  129. package/dist/scripts/data-transformers/content-generators/month-ahead.js +0 -212
  130. package/dist/scripts/data-transformers/content-generators/month-ahead.js.map +0 -1
  131. package/dist/scripts/data-transformers/content-generators/monthly-review.d.ts +0 -17
  132. package/dist/scripts/data-transformers/content-generators/monthly-review.d.ts.map +0 -1
  133. package/dist/scripts/data-transformers/content-generators/monthly-review.js +0 -173
  134. package/dist/scripts/data-transformers/content-generators/monthly-review.js.map +0 -1
  135. package/dist/scripts/data-transformers/content-generators/motions.d.ts +0 -12
  136. package/dist/scripts/data-transformers/content-generators/motions.d.ts.map +0 -1
  137. package/dist/scripts/data-transformers/content-generators/motions.js +0 -158
  138. package/dist/scripts/data-transformers/content-generators/motions.js.map +0 -1
  139. package/dist/scripts/data-transformers/content-generators/newsworthiness.d.ts +0 -62
  140. package/dist/scripts/data-transformers/content-generators/newsworthiness.d.ts.map +0 -1
  141. package/dist/scripts/data-transformers/content-generators/newsworthiness.js +0 -254
  142. package/dist/scripts/data-transformers/content-generators/newsworthiness.js.map +0 -1
  143. package/dist/scripts/data-transformers/content-generators/propositions.d.ts +0 -12
  144. package/dist/scripts/data-transformers/content-generators/propositions.d.ts.map +0 -1
  145. package/dist/scripts/data-transformers/content-generators/propositions.js +0 -263
  146. package/dist/scripts/data-transformers/content-generators/propositions.js.map +0 -1
  147. package/dist/scripts/data-transformers/content-generators/sankey-section.d.ts +0 -92
  148. package/dist/scripts/data-transformers/content-generators/sankey-section.d.ts.map +0 -1
  149. package/dist/scripts/data-transformers/content-generators/sankey-section.js +0 -257
  150. package/dist/scripts/data-transformers/content-generators/sankey-section.js.map +0 -1
  151. package/dist/scripts/data-transformers/content-generators/shared.d.ts +0 -139
  152. package/dist/scripts/data-transformers/content-generators/shared.d.ts.map +0 -1
  153. package/dist/scripts/data-transformers/content-generators/shared.js +0 -935
  154. package/dist/scripts/data-transformers/content-generators/shared.js.map +0 -1
  155. package/dist/scripts/data-transformers/content-generators/swot-section.d.ts +0 -50
  156. package/dist/scripts/data-transformers/content-generators/swot-section.d.ts.map +0 -1
  157. package/dist/scripts/data-transformers/content-generators/swot-section.js +0 -177
  158. package/dist/scripts/data-transformers/content-generators/swot-section.js.map +0 -1
  159. package/dist/scripts/data-transformers/content-generators/week-ahead.d.ts +0 -12
  160. package/dist/scripts/data-transformers/content-generators/week-ahead.d.ts.map +0 -1
  161. package/dist/scripts/data-transformers/content-generators/week-ahead.js +0 -332
  162. package/dist/scripts/data-transformers/content-generators/week-ahead.js.map +0 -1
  163. package/dist/scripts/data-transformers/content-generators.d.ts +0 -15
  164. package/dist/scripts/data-transformers/content-generators.d.ts.map +0 -1
  165. package/dist/scripts/data-transformers/content-generators.js +0 -14
  166. package/dist/scripts/data-transformers/content-generators.js.map +0 -1
  167. package/dist/scripts/data-transformers/document-analysis.d.ts +0 -58
  168. package/dist/scripts/data-transformers/document-analysis.d.ts.map +0 -1
  169. package/dist/scripts/data-transformers/document-analysis.js +0 -396
  170. package/dist/scripts/data-transformers/document-analysis.js.map +0 -1
  171. package/dist/scripts/data-transformers/helpers.d.ts +0 -144
  172. package/dist/scripts/data-transformers/helpers.d.ts.map +0 -1
  173. package/dist/scripts/data-transformers/helpers.js +0 -408
  174. package/dist/scripts/data-transformers/helpers.js.map +0 -1
  175. package/dist/scripts/data-transformers/index.d.ts +0 -45
  176. package/dist/scripts/data-transformers/index.d.ts.map +0 -1
  177. package/dist/scripts/data-transformers/index.js +0 -66
  178. package/dist/scripts/data-transformers/index.js.map +0 -1
  179. package/dist/scripts/data-transformers/metadata.d.ts +0 -42
  180. package/dist/scripts/data-transformers/metadata.d.ts.map +0 -1
  181. package/dist/scripts/data-transformers/metadata.js +0 -385
  182. package/dist/scripts/data-transformers/metadata.js.map +0 -1
  183. package/dist/scripts/data-transformers/policy-analysis.d.ts +0 -105
  184. package/dist/scripts/data-transformers/policy-analysis.d.ts.map +0 -1
  185. package/dist/scripts/data-transformers/policy-analysis.js +0 -686
  186. package/dist/scripts/data-transformers/policy-analysis.js.map +0 -1
  187. package/dist/scripts/data-transformers/risk-analysis.d.ts +0 -104
  188. package/dist/scripts/data-transformers/risk-analysis.d.ts.map +0 -1
  189. package/dist/scripts/data-transformers/risk-analysis.js +0 -279
  190. package/dist/scripts/data-transformers/risk-analysis.js.map +0 -1
  191. package/dist/scripts/data-transformers/types.d.ts +0 -260
  192. package/dist/scripts/data-transformers/types.d.ts.map +0 -1
  193. package/dist/scripts/data-transformers/types.js +0 -11
  194. package/dist/scripts/data-transformers/types.js.map +0 -1
  195. package/dist/scripts/data-transformers.d.ts +0 -23
  196. package/dist/scripts/data-transformers.d.ts.map +0 -1
  197. package/dist/scripts/data-transformers.js +0 -35
  198. package/dist/scripts/data-transformers.js.map +0 -1
  199. package/dist/scripts/deep-inspection/index.d.ts +0 -55
  200. package/dist/scripts/deep-inspection/index.d.ts.map +0 -1
  201. package/dist/scripts/deep-inspection/index.js +0 -66
  202. package/dist/scripts/deep-inspection/index.js.map +0 -1
  203. package/dist/scripts/detect-swedish-leakage.d.ts +0 -69
  204. package/dist/scripts/detect-swedish-leakage.d.ts.map +0 -1
  205. package/dist/scripts/detect-swedish-leakage.js +0 -417
  206. package/dist/scripts/detect-swedish-leakage.js.map +0 -1
  207. package/dist/scripts/editorial-framework.d.ts +0 -121
  208. package/dist/scripts/editorial-framework.d.ts.map +0 -1
  209. package/dist/scripts/editorial-framework.js +0 -364
  210. package/dist/scripts/editorial-framework.js.map +0 -1
  211. package/dist/scripts/editorial-pillars.d.ts +0 -43
  212. package/dist/scripts/editorial-pillars.d.ts.map +0 -1
  213. package/dist/scripts/editorial-pillars.js +0 -215
  214. package/dist/scripts/editorial-pillars.js.map +0 -1
  215. package/dist/scripts/extract-news-metadata.d.ts +0 -12
  216. package/dist/scripts/extract-news-metadata.d.ts.map +0 -1
  217. package/dist/scripts/extract-news-metadata.js +0 -107
  218. package/dist/scripts/extract-news-metadata.js.map +0 -1
  219. package/dist/scripts/extract-vocabulary.d.ts +0 -15
  220. package/dist/scripts/extract-vocabulary.d.ts.map +0 -1
  221. package/dist/scripts/extract-vocabulary.js +0 -255
  222. package/dist/scripts/extract-vocabulary.js.map +0 -1
  223. package/dist/scripts/fix-article-navigation.d.ts +0 -37
  224. package/dist/scripts/fix-article-navigation.d.ts.map +0 -1
  225. package/dist/scripts/fix-article-navigation.js +0 -198
  226. package/dist/scripts/fix-article-navigation.js.map +0 -1
  227. package/dist/scripts/fix-keywords-localization.d.ts +0 -18
  228. package/dist/scripts/fix-keywords-localization.d.ts.map +0 -1
  229. package/dist/scripts/fix-keywords-localization.js +0 -270
  230. package/dist/scripts/fix-keywords-localization.js.map +0 -1
  231. package/dist/scripts/fix-old-articles-branding.d.ts +0 -17
  232. package/dist/scripts/fix-old-articles-branding.d.ts.map +0 -1
  233. package/dist/scripts/fix-old-articles-branding.js +0 -229
  234. package/dist/scripts/fix-old-articles-branding.js.map +0 -1
  235. package/dist/scripts/generate-news-backport.d.ts +0 -16
  236. package/dist/scripts/generate-news-backport.d.ts.map +0 -1
  237. package/dist/scripts/generate-news-backport.js +0 -379
  238. package/dist/scripts/generate-news-backport.js.map +0 -1
  239. package/dist/scripts/generate-news-enhanced/ai-analysis-pipeline.d.ts +0 -141
  240. package/dist/scripts/generate-news-enhanced/ai-analysis-pipeline.d.ts.map +0 -1
  241. package/dist/scripts/generate-news-enhanced/ai-analysis-pipeline.js +0 -546
  242. package/dist/scripts/generate-news-enhanced/ai-analysis-pipeline.js.map +0 -1
  243. package/dist/scripts/generate-news-enhanced/analysis-cache.d.ts +0 -59
  244. package/dist/scripts/generate-news-enhanced/analysis-cache.d.ts.map +0 -1
  245. package/dist/scripts/generate-news-enhanced/analysis-cache.js +0 -116
  246. package/dist/scripts/generate-news-enhanced/analysis-cache.js.map +0 -1
  247. package/dist/scripts/generate-news-enhanced/analysis-labels.d.ts +0 -58
  248. package/dist/scripts/generate-news-enhanced/analysis-labels.d.ts.map +0 -1
  249. package/dist/scripts/generate-news-enhanced/analysis-labels.js +0 -144
  250. package/dist/scripts/generate-news-enhanced/analysis-labels.js.map +0 -1
  251. package/dist/scripts/generate-news-enhanced/config.d.ts +0 -58
  252. package/dist/scripts/generate-news-enhanced/config.d.ts.map +0 -1
  253. package/dist/scripts/generate-news-enhanced/config.js +0 -286
  254. package/dist/scripts/generate-news-enhanced/config.js.map +0 -1
  255. package/dist/scripts/generate-news-enhanced/generators.d.ts +0 -119
  256. package/dist/scripts/generate-news-enhanced/generators.d.ts.map +0 -1
  257. package/dist/scripts/generate-news-enhanced/generators.js +0 -2131
  258. package/dist/scripts/generate-news-enhanced/generators.js.map +0 -1
  259. package/dist/scripts/generate-news-enhanced/helpers.d.ts +0 -122
  260. package/dist/scripts/generate-news-enhanced/helpers.d.ts.map +0 -1
  261. package/dist/scripts/generate-news-enhanced/helpers.js +0 -468
  262. package/dist/scripts/generate-news-enhanced/helpers.js.map +0 -1
  263. package/dist/scripts/generate-news-enhanced/index.d.ts +0 -19
  264. package/dist/scripts/generate-news-enhanced/index.d.ts.map +0 -1
  265. package/dist/scripts/generate-news-enhanced/index.js +0 -221
  266. package/dist/scripts/generate-news-enhanced/index.js.map +0 -1
  267. package/dist/scripts/generate-news-enhanced/swot-analyzer.d.ts +0 -46
  268. package/dist/scripts/generate-news-enhanced/swot-analyzer.d.ts.map +0 -1
  269. package/dist/scripts/generate-news-enhanced/swot-analyzer.js +0 -1227
  270. package/dist/scripts/generate-news-enhanced/swot-analyzer.js.map +0 -1
  271. package/dist/scripts/generate-news-enhanced/types.d.ts +0 -34
  272. package/dist/scripts/generate-news-enhanced/types.d.ts.map +0 -1
  273. package/dist/scripts/generate-news-enhanced/types.js +0 -9
  274. package/dist/scripts/generate-news-enhanced/types.js.map +0 -1
  275. package/dist/scripts/generate-news-enhanced.d.ts +0 -25
  276. package/dist/scripts/generate-news-enhanced.d.ts.map +0 -1
  277. package/dist/scripts/generate-news-enhanced.js +0 -37
  278. package/dist/scripts/generate-news-enhanced.js.map +0 -1
  279. package/dist/scripts/generate-news-indexes/constants.d.ts +0 -20
  280. package/dist/scripts/generate-news-indexes/constants.d.ts.map +0 -1
  281. package/dist/scripts/generate-news-indexes/constants.js +0 -308
  282. package/dist/scripts/generate-news-indexes/constants.js.map +0 -1
  283. package/dist/scripts/generate-news-indexes/helpers.d.ts +0 -73
  284. package/dist/scripts/generate-news-indexes/helpers.d.ts.map +0 -1
  285. package/dist/scripts/generate-news-indexes/helpers.js +0 -352
  286. package/dist/scripts/generate-news-indexes/helpers.js.map +0 -1
  287. package/dist/scripts/generate-news-indexes/index.d.ts +0 -20
  288. package/dist/scripts/generate-news-indexes/index.d.ts.map +0 -1
  289. package/dist/scripts/generate-news-indexes/index.js +0 -75
  290. package/dist/scripts/generate-news-indexes/index.js.map +0 -1
  291. package/dist/scripts/generate-news-indexes/template.d.ts +0 -25
  292. package/dist/scripts/generate-news-indexes/template.d.ts.map +0 -1
  293. package/dist/scripts/generate-news-indexes/template.js +0 -644
  294. package/dist/scripts/generate-news-indexes/template.js.map +0 -1
  295. package/dist/scripts/generate-news-indexes/types.d.ts +0 -96
  296. package/dist/scripts/generate-news-indexes/types.d.ts.map +0 -1
  297. package/dist/scripts/generate-news-indexes/types.js +0 -9
  298. package/dist/scripts/generate-news-indexes/types.js.map +0 -1
  299. package/dist/scripts/generate-news-indexes.d.ts +0 -20
  300. package/dist/scripts/generate-news-indexes.d.ts.map +0 -1
  301. package/dist/scripts/generate-news-indexes.js +0 -19
  302. package/dist/scripts/generate-news-indexes.js.map +0 -1
  303. package/dist/scripts/generate-rss.d.ts +0 -48
  304. package/dist/scripts/generate-rss.d.ts.map +0 -1
  305. package/dist/scripts/generate-rss.js +0 -299
  306. package/dist/scripts/generate-rss.js.map +0 -1
  307. package/dist/scripts/generate-sitemap-html.d.ts +0 -79
  308. package/dist/scripts/generate-sitemap-html.d.ts.map +0 -1
  309. package/dist/scripts/generate-sitemap-html.js +0 -878
  310. package/dist/scripts/generate-sitemap-html.js.map +0 -1
  311. package/dist/scripts/generate-sitemap.d.ts +0 -23
  312. package/dist/scripts/generate-sitemap.d.ts.map +0 -1
  313. package/dist/scripts/generate-sitemap.js +0 -476
  314. package/dist/scripts/generate-sitemap.js.map +0 -1
  315. package/dist/scripts/generate-types-from-cia-schemas.d.ts +0 -176
  316. package/dist/scripts/generate-types-from-cia-schemas.d.ts.map +0 -1
  317. package/dist/scripts/generate-types-from-cia-schemas.js +0 -318
  318. package/dist/scripts/generate-types-from-cia-schemas.js.map +0 -1
  319. package/dist/scripts/government-role-validator.d.ts +0 -65
  320. package/dist/scripts/government-role-validator.d.ts.map +0 -1
  321. package/dist/scripts/government-role-validator.js +0 -217
  322. package/dist/scripts/government-role-validator.js.map +0 -1
  323. package/dist/scripts/html-utils.d.ts +0 -17
  324. package/dist/scripts/html-utils.d.ts.map +0 -1
  325. package/dist/scripts/html-utils.js +0 -29
  326. package/dist/scripts/html-utils.js.map +0 -1
  327. package/dist/scripts/load-cia-stats.d.ts +0 -89
  328. package/dist/scripts/load-cia-stats.d.ts.map +0 -1
  329. package/dist/scripts/load-cia-stats.js +0 -400
  330. package/dist/scripts/load-cia-stats.js.map +0 -1
  331. package/dist/scripts/mcp-client/client.d.ts +0 -73
  332. package/dist/scripts/mcp-client/client.d.ts.map +0 -1
  333. package/dist/scripts/mcp-client/client.js +0 -526
  334. package/dist/scripts/mcp-client/client.js.map +0 -1
  335. package/dist/scripts/mcp-client/document-types.d.ts +0 -23
  336. package/dist/scripts/mcp-client/document-types.d.ts.map +0 -1
  337. package/dist/scripts/mcp-client/document-types.js +0 -62
  338. package/dist/scripts/mcp-client/document-types.js.map +0 -1
  339. package/dist/scripts/mcp-client/index.d.ts +0 -34
  340. package/dist/scripts/mcp-client/index.d.ts.map +0 -1
  341. package/dist/scripts/mcp-client/index.js +0 -67
  342. package/dist/scripts/mcp-client/index.js.map +0 -1
  343. package/dist/scripts/mcp-client/transport.d.ts +0 -32
  344. package/dist/scripts/mcp-client/transport.d.ts.map +0 -1
  345. package/dist/scripts/mcp-client/transport.js +0 -102
  346. package/dist/scripts/mcp-client/transport.js.map +0 -1
  347. package/dist/scripts/mcp-client.d.ts +0 -18
  348. package/dist/scripts/mcp-client.d.ts.map +0 -1
  349. package/dist/scripts/mcp-client.js +0 -18
  350. package/dist/scripts/mcp-client.js.map +0 -1
  351. package/dist/scripts/mcp-query-cli.d.ts +0 -25
  352. package/dist/scripts/mcp-query-cli.d.ts.map +0 -1
  353. package/dist/scripts/mcp-query-cli.js +0 -66
  354. package/dist/scripts/mcp-query-cli.js.map +0 -1
  355. package/dist/scripts/news-types/breaking-news.d.ts +0 -178
  356. package/dist/scripts/news-types/breaking-news.d.ts.map +0 -1
  357. package/dist/scripts/news-types/breaking-news.js +0 -432
  358. package/dist/scripts/news-types/breaking-news.js.map +0 -1
  359. package/dist/scripts/news-types/committee-reports.d.ts +0 -212
  360. package/dist/scripts/news-types/committee-reports.d.ts.map +0 -1
  361. package/dist/scripts/news-types/committee-reports.js +0 -404
  362. package/dist/scripts/news-types/committee-reports.js.map +0 -1
  363. package/dist/scripts/news-types/month-ahead.d.ts +0 -71
  364. package/dist/scripts/news-types/month-ahead.d.ts.map +0 -1
  365. package/dist/scripts/news-types/month-ahead.js +0 -392
  366. package/dist/scripts/news-types/month-ahead.js.map +0 -1
  367. package/dist/scripts/news-types/monthly-review.d.ts +0 -61
  368. package/dist/scripts/news-types/monthly-review.d.ts.map +0 -1
  369. package/dist/scripts/news-types/monthly-review.js +0 -417
  370. package/dist/scripts/news-types/monthly-review.js.map +0 -1
  371. package/dist/scripts/news-types/motions.d.ts +0 -214
  372. package/dist/scripts/news-types/motions.d.ts.map +0 -1
  373. package/dist/scripts/news-types/motions.js +0 -447
  374. package/dist/scripts/news-types/motions.js.map +0 -1
  375. package/dist/scripts/news-types/propositions.d.ts +0 -204
  376. package/dist/scripts/news-types/propositions.d.ts.map +0 -1
  377. package/dist/scripts/news-types/propositions.js +0 -401
  378. package/dist/scripts/news-types/propositions.js.map +0 -1
  379. package/dist/scripts/news-types/week-ahead.d.ts +0 -231
  380. package/dist/scripts/news-types/week-ahead.d.ts.map +0 -1
  381. package/dist/scripts/news-types/week-ahead.js +0 -441
  382. package/dist/scripts/news-types/week-ahead.js.map +0 -1
  383. package/dist/scripts/news-types/weekly-review/analysis.d.ts +0 -47
  384. package/dist/scripts/news-types/weekly-review/analysis.d.ts.map +0 -1
  385. package/dist/scripts/news-types/weekly-review/analysis.js +0 -276
  386. package/dist/scripts/news-types/weekly-review/analysis.js.map +0 -1
  387. package/dist/scripts/news-types/weekly-review/data-loader.d.ts +0 -51
  388. package/dist/scripts/news-types/weekly-review/data-loader.d.ts.map +0 -1
  389. package/dist/scripts/news-types/weekly-review/data-loader.js +0 -298
  390. package/dist/scripts/news-types/weekly-review/data-loader.js.map +0 -1
  391. package/dist/scripts/news-types/weekly-review/generator.d.ts +0 -18
  392. package/dist/scripts/news-types/weekly-review/generator.d.ts.map +0 -1
  393. package/dist/scripts/news-types/weekly-review/generator.js +0 -284
  394. package/dist/scripts/news-types/weekly-review/generator.js.map +0 -1
  395. package/dist/scripts/news-types/weekly-review/index.d.ts +0 -20
  396. package/dist/scripts/news-types/weekly-review/index.d.ts.map +0 -1
  397. package/dist/scripts/news-types/weekly-review/index.js +0 -23
  398. package/dist/scripts/news-types/weekly-review/index.js.map +0 -1
  399. package/dist/scripts/news-types/weekly-review/types.d.ts +0 -72
  400. package/dist/scripts/news-types/weekly-review/types.d.ts.map +0 -1
  401. package/dist/scripts/news-types/weekly-review/types.js +0 -20
  402. package/dist/scripts/news-types/weekly-review/types.js.map +0 -1
  403. package/dist/scripts/news-types/weekly-review/validation.d.ts +0 -10
  404. package/dist/scripts/news-types/weekly-review/validation.d.ts.map +0 -1
  405. package/dist/scripts/news-types/weekly-review/validation.js +0 -95
  406. package/dist/scripts/news-types/weekly-review/validation.js.map +0 -1
  407. package/dist/scripts/news-types/weekly-review.d.ts +0 -15
  408. package/dist/scripts/news-types/weekly-review.d.ts.map +0 -1
  409. package/dist/scripts/news-types/weekly-review.js +0 -14
  410. package/dist/scripts/news-types/weekly-review.js.map +0 -1
  411. package/dist/scripts/party-variants.d.ts +0 -23
  412. package/dist/scripts/party-variants.d.ts.map +0 -1
  413. package/dist/scripts/party-variants.js +0 -50
  414. package/dist/scripts/party-variants.js.map +0 -1
  415. package/dist/scripts/pipeline/index.d.ts +0 -19
  416. package/dist/scripts/pipeline/index.d.ts.map +0 -1
  417. package/dist/scripts/pipeline/index.js +0 -17
  418. package/dist/scripts/pipeline/index.js.map +0 -1
  419. package/dist/scripts/pipeline/orchestrator.d.ts +0 -56
  420. package/dist/scripts/pipeline/orchestrator.d.ts.map +0 -1
  421. package/dist/scripts/pipeline/orchestrator.js +0 -147
  422. package/dist/scripts/pipeline/orchestrator.js.map +0 -1
  423. package/dist/scripts/pipeline/types.d.ts +0 -117
  424. package/dist/scripts/pipeline/types.d.ts.map +0 -1
  425. package/dist/scripts/pipeline/types.js +0 -13
  426. package/dist/scripts/pipeline/types.js.map +0 -1
  427. package/dist/scripts/pipeline/validation.d.ts +0 -66
  428. package/dist/scripts/pipeline/validation.d.ts.map +0 -1
  429. package/dist/scripts/pipeline/validation.js +0 -129
  430. package/dist/scripts/pipeline/validation.js.map +0 -1
  431. package/dist/scripts/populate-analysis-data.d.ts +0 -41
  432. package/dist/scripts/populate-analysis-data.d.ts.map +0 -1
  433. package/dist/scripts/populate-analysis-data.js +0 -255
  434. package/dist/scripts/populate-analysis-data.js.map +0 -1
  435. package/dist/scripts/pre-article-analysis/data-downloader.d.ts +0 -80
  436. package/dist/scripts/pre-article-analysis/data-downloader.d.ts.map +0 -1
  437. package/dist/scripts/pre-article-analysis/data-downloader.js +0 -230
  438. package/dist/scripts/pre-article-analysis/data-downloader.js.map +0 -1
  439. package/dist/scripts/pre-article-analysis/data-persistence.d.ts +0 -141
  440. package/dist/scripts/pre-article-analysis/data-persistence.d.ts.map +0 -1
  441. package/dist/scripts/pre-article-analysis/data-persistence.js +0 -345
  442. package/dist/scripts/pre-article-analysis/data-persistence.js.map +0 -1
  443. package/dist/scripts/pre-article-analysis/markdown-serializer.d.ts +0 -158
  444. package/dist/scripts/pre-article-analysis/markdown-serializer.d.ts.map +0 -1
  445. package/dist/scripts/pre-article-analysis/markdown-serializer.js +0 -648
  446. package/dist/scripts/pre-article-analysis/markdown-serializer.js.map +0 -1
  447. package/dist/scripts/pre-article-analysis/pdf-converter.d.ts +0 -58
  448. package/dist/scripts/pre-article-analysis/pdf-converter.d.ts.map +0 -1
  449. package/dist/scripts/pre-article-analysis/pdf-converter.js +0 -142
  450. package/dist/scripts/pre-article-analysis/pdf-converter.js.map +0 -1
  451. package/dist/scripts/pre-article-analysis.d.ts +0 -42
  452. package/dist/scripts/pre-article-analysis.d.ts.map +0 -1
  453. package/dist/scripts/pre-article-analysis.js +0 -642
  454. package/dist/scripts/pre-article-analysis.js.map +0 -1
  455. package/dist/scripts/scb-client.d.ts +0 -104
  456. package/dist/scripts/scb-client.d.ts.map +0 -1
  457. package/dist/scripts/scb-client.js +0 -286
  458. package/dist/scripts/scb-client.js.map +0 -1
  459. package/dist/scripts/scb-context.d.ts +0 -88
  460. package/dist/scripts/scb-context.d.ts.map +0 -1
  461. package/dist/scripts/scb-context.js +0 -307
  462. package/dist/scripts/scb-context.js.map +0 -1
  463. package/dist/scripts/shared/version.d.ts +0 -9
  464. package/dist/scripts/shared/version.d.ts.map +0 -1
  465. package/dist/scripts/shared/version.js +0 -28
  466. package/dist/scripts/shared/version.js.map +0 -1
  467. package/dist/scripts/statistical-claims-detector.d.ts +0 -119
  468. package/dist/scripts/statistical-claims-detector.d.ts.map +0 -1
  469. package/dist/scripts/statistical-claims-detector.js +0 -391
  470. package/dist/scripts/statistical-claims-detector.js.map +0 -1
  471. package/dist/scripts/sync-cia-schemas.d.ts +0 -52
  472. package/dist/scripts/sync-cia-schemas.d.ts.map +0 -1
  473. package/dist/scripts/sync-cia-schemas.js +0 -195
  474. package/dist/scripts/sync-cia-schemas.js.map +0 -1
  475. package/dist/scripts/translation-dictionary.d.ts +0 -45
  476. package/dist/scripts/translation-dictionary.d.ts.map +0 -1
  477. package/dist/scripts/translation-dictionary.js +0 -3642
  478. package/dist/scripts/translation-dictionary.js.map +0 -1
  479. package/dist/scripts/types/article.d.ts +0 -392
  480. package/dist/scripts/types/article.d.ts.map +0 -1
  481. package/dist/scripts/types/article.js +0 -6
  482. package/dist/scripts/types/article.js.map +0 -1
  483. package/dist/scripts/types/content.d.ts +0 -167
  484. package/dist/scripts/types/content.d.ts.map +0 -1
  485. package/dist/scripts/types/content.js +0 -6
  486. package/dist/scripts/types/content.js.map +0 -1
  487. package/dist/scripts/types/editorial.d.ts +0 -17
  488. package/dist/scripts/types/editorial.d.ts.map +0 -1
  489. package/dist/scripts/types/editorial.js +0 -6
  490. package/dist/scripts/types/editorial.js.map +0 -1
  491. package/dist/scripts/types/language.d.ts +0 -7
  492. package/dist/scripts/types/language.d.ts.map +0 -1
  493. package/dist/scripts/types/language.js +0 -6
  494. package/dist/scripts/types/language.js.map +0 -1
  495. package/dist/scripts/types/mcp.d.ts +0 -117
  496. package/dist/scripts/types/mcp.d.ts.map +0 -1
  497. package/dist/scripts/types/mcp.js +0 -6
  498. package/dist/scripts/types/mcp.js.map +0 -1
  499. package/dist/scripts/types/party.d.ts +0 -9
  500. package/dist/scripts/types/party.d.ts.map +0 -1
  501. package/dist/scripts/types/party.js +0 -6
  502. package/dist/scripts/types/party.js.map +0 -1
  503. package/dist/scripts/types/validation.d.ts +0 -136
  504. package/dist/scripts/types/validation.d.ts.map +0 -1
  505. package/dist/scripts/types/validation.js +0 -6
  506. package/dist/scripts/types/validation.js.map +0 -1
  507. package/dist/scripts/types/workflow.d.ts +0 -78
  508. package/dist/scripts/types/workflow.d.ts.map +0 -1
  509. package/dist/scripts/types/workflow.js +0 -6
  510. package/dist/scripts/types/workflow.js.map +0 -1
  511. package/dist/scripts/update-stats-from-cia.d.ts +0 -44
  512. package/dist/scripts/update-stats-from-cia.d.ts.map +0 -1
  513. package/dist/scripts/update-stats-from-cia.js +0 -310
  514. package/dist/scripts/update-stats-from-cia.js.map +0 -1
  515. package/dist/scripts/validate-against-cia-schemas.d.ts +0 -126
  516. package/dist/scripts/validate-against-cia-schemas.d.ts.map +0 -1
  517. package/dist/scripts/validate-against-cia-schemas.js +0 -299
  518. package/dist/scripts/validate-against-cia-schemas.js.map +0 -1
  519. package/dist/scripts/validate-cross-references.d.ts +0 -49
  520. package/dist/scripts/validate-cross-references.d.ts.map +0 -1
  521. package/dist/scripts/validate-cross-references.js +0 -183
  522. package/dist/scripts/validate-cross-references.js.map +0 -1
  523. package/dist/scripts/validate-file-ownership.d.ts +0 -68
  524. package/dist/scripts/validate-file-ownership.d.ts.map +0 -1
  525. package/dist/scripts/validate-file-ownership.js +0 -135
  526. package/dist/scripts/validate-file-ownership.js.map +0 -1
  527. package/dist/scripts/validate-news-translations.d.ts +0 -27
  528. package/dist/scripts/validate-news-translations.d.ts.map +0 -1
  529. package/dist/scripts/validate-news-translations.js +0 -258
  530. package/dist/scripts/validate-news-translations.js.map +0 -1
  531. package/dist/scripts/validate-translations.d.ts +0 -162
  532. package/dist/scripts/validate-translations.d.ts.map +0 -1
  533. package/dist/scripts/validate-translations.js +0 -378
  534. package/dist/scripts/validate-translations.js.map +0 -1
  535. package/dist/scripts/workflow-state-coordinator.d.ts +0 -354
  536. package/dist/scripts/workflow-state-coordinator.d.ts.map +0 -1
  537. package/dist/scripts/workflow-state-coordinator.js +0 -876
  538. package/dist/scripts/workflow-state-coordinator.js.map +0 -1
  539. package/dist/scripts/world-bank-client.d.ts +0 -122
  540. package/dist/scripts/world-bank-client.d.ts.map +0 -1
  541. package/dist/scripts/world-bank-client.js +0 -192
  542. package/dist/scripts/world-bank-client.js.map +0 -1
  543. package/dist/scripts/world-bank-context.d.ts +0 -88
  544. package/dist/scripts/world-bank-context.d.ts.map +0 -1
  545. package/dist/scripts/world-bank-context.js +0 -331
  546. package/dist/scripts/world-bank-context.js.map +0 -1
@@ -1,2131 +0,0 @@
1
- /**
2
- * @module generate-news-enhanced/generators
3
- * @description Article generator functions for week-ahead, committee reports,
4
- * propositions, and motions article types.
5
- *
6
- * @author Hack23 AB
7
- * @license Apache-2.0
8
- */
9
- import { transformCalendarToEventGrid, generateArticleContent, extractWatchPoints, generateMetadata, calculateReadTime, generateSources, filterFreshDocuments, } from '../data-transformers.js';
10
- import { generateStakeholderSwotSection, generateDashboardSection, generateEconomicDashboardSection, generateMindmapSection, generateSankeySection, buildAIMindmapAnalysis, buildMindmapOptionsFromAnalysis, } from '../data-transformers/index.js';
11
- import { generateDeepAnalysisSection, localizeDocType } from '../data-transformers/content-generators/index.js';
12
- import { generateDeepPolicyAnalysis, detectPolicyDomains } from '../data-transformers/policy-analysis.js';
13
- import { escapeHtml } from '../html-utils.js';
14
- import { generateArticleHTML } from '../article-template.js';
15
- import fs from 'node:fs';
16
- import path from 'node:path';
17
- import { languages, stats, getSharedClient, requireMcp, toISODate, documentIds, documentUrls, focusTopic, analysisDepth, analysisIterations, METADATA_DIR } from './config.js';
18
- import { getWeekAheadDateRange, formatDateForSlug, writeSingleArticle, generateDynamicTitle, getAnalysisEnrichment, } from './helpers.js';
19
- import { AIAnalysisPipeline } from './ai-analysis-pipeline.js';
20
- import { sharedAnalysisCache } from './analysis-cache.js';
21
- // ---------------------------------------------------------------------------
22
- // Stub functions replacing deleted analysis-generation modules.
23
- // Per ai-driven-analysis-guide.md Rule 2, scripts must NOT generate analysis.
24
- // The AI agent in agentic workflows now produces all political analysis.
25
- // These stubs return empty data so the HTML formatting pipeline still works.
26
- // ---------------------------------------------------------------------------
27
- /** Stub: analysis is now AI-driven in workflows, not script-generated */
28
- function buildAISwotStakeholders(_docs, _topic, _lang) {
29
- return [];
30
- }
31
- /** Stub: analysis is now AI-driven in workflows, not script-generated */
32
- function analyzeDashboardData(_docs, _topic, _lang) {
33
- return { charts: [], tables: [], summary: '' };
34
- }
35
- /** Stakeholder display names for mindmap/sankey labels */
36
- const AI_STAKEHOLDER_NAMES = {
37
- 'government-coalition': { en: 'Government Coalition', sv: 'Regeringskoalitionen' },
38
- 'opposition': { en: 'Opposition', sv: 'Oppositionen' },
39
- 'private-sector': { en: 'Private Sector', sv: 'Näringslivet' },
40
- };
41
- // ---------------------------------------------------------------------------
42
- // Shared article visualization builder
43
- // ---------------------------------------------------------------------------
44
- /**
45
- * Build SWOT, dashboard, and economic TemplateSections for standard article
46
- * types (not deep-inspection, which has its own richer builder).
47
- *
48
- * Produces 1–3 sections depending on available data:
49
- * - SWOT stakeholder analysis (always, when docs.length >= 2)
50
- * - Chart.js dashboard with document type breakdown (when docs.length >= 3)
51
- * - Economic dashboard (when policyDomains match World Bank indicators)
52
- *
53
- * Each section is safe to append to `generateArticleHTML({ sections })`.
54
- */
55
- export function buildArticleVisualizationSections(docs, topic, lang) {
56
- const sections = [];
57
- if (docs.length < 2)
58
- return sections;
59
- try {
60
- // ── 1. SWOT stakeholder analysis ──────────────────────────────────────
61
- const stakeholders = buildAISwotStakeholders(docs, topic ?? '', lang);
62
- if (stakeholders.length > 0) {
63
- const swotSection = generateStakeholderSwotSection({ stakeholders, lang });
64
- sections.push(swotSection);
65
- }
66
- }
67
- catch { /* graceful degradation */ }
68
- try {
69
- // ── 2. Chart.js dashboard (doc-type breakdown + AI analysis) ──────────
70
- if (docs.length >= 3) {
71
- const dashboardAnalysis = analyzeDashboardData(docs, topic ?? '', lang);
72
- if (dashboardAnalysis.charts.length > 0 || dashboardAnalysis.tables.length > 0) {
73
- const dashboardSection = generateDashboardSection({
74
- data: {
75
- title: 'Policy Analysis Dashboard',
76
- summary: dashboardAnalysis.summary,
77
- charts: dashboardAnalysis.charts,
78
- tables: dashboardAnalysis.tables,
79
- },
80
- lang,
81
- });
82
- sections.push(dashboardSection);
83
- }
84
- }
85
- }
86
- catch { /* graceful degradation */ }
87
- try {
88
- // ── 3. Economic dashboard (World Bank indicators for detected domains) ─
89
- const allDomains = new Set();
90
- for (const d of docs) {
91
- for (const dom of detectPolicyDomains(d, lang)) {
92
- allDomains.add(dom);
93
- }
94
- }
95
- if (allDomains.size > 0) {
96
- const econSection = generateEconomicDashboardSection({
97
- policyDomains: [...allDomains],
98
- lang,
99
- });
100
- if (econSection)
101
- sections.push(econSection);
102
- }
103
- }
104
- catch { /* graceful degradation */ }
105
- return sections;
106
- }
107
- // ---------------------------------------------------------------------------
108
- // Generator functions
109
- // ---------------------------------------------------------------------------
110
- /**
111
- * Generate Week Ahead article in specified languages
112
- */
113
- export async function generateWeekAhead() {
114
- console.log('📅 Generating Week Ahead article...');
115
- try {
116
- const client = await getSharedClient();
117
- const dateRange = getWeekAheadDateRange();
118
- console.log(` 📆 Date range: ${dateRange.start} to ${dateRange.end}`);
119
- // 1. Fetch calendar events from MCP
120
- console.log(' 🔄 Fetching calendar events from riksdag-regering-mcp...');
121
- const events = await client.fetchCalendarEvents(dateRange.start, dateRange.end);
122
- console.log(` 📊 Found ${events.length} events`);
123
- // 2. Fetch upcoming/recent documents
124
- const rawDocs = await client.searchDocuments({ from_date: dateRange.start, to_date: dateRange.end, limit: 30 })
125
- .catch((e) => { if (requireMcp)
126
- throw e; return []; });
127
- const documents = Array.isArray(rawDocs) ? rawDocs : [];
128
- console.log(` 📊 Found ${documents.length} upcoming documents`);
129
- // 3. Fetch parliamentary questions (fragor)
130
- console.log(' 🔄 Fetching parliamentary questions...');
131
- const rawQuestions = await client.fetchWrittenQuestions({ limit: 20 })
132
- .catch((e) => { if (requireMcp)
133
- throw e; return []; });
134
- const questions = Array.isArray(rawQuestions) ? rawQuestions : [];
135
- console.log(` 📊 Found ${questions.length} written questions`);
136
- // 4. Fetch interpellations (interpellationer)
137
- console.log(' 🔄 Fetching interpellations...');
138
- const rawInterpellations = await client.fetchInterpellations({ limit: 15 })
139
- .catch((e) => { if (requireMcp)
140
- throw e; return []; });
141
- const interpellations = Array.isArray(rawInterpellations) ? rawInterpellations : [];
142
- console.log(` 📊 Found ${interpellations.length} interpellations`);
143
- const today = new Date();
144
- const slug = `${formatDateForSlug(today)}-week-ahead`;
145
- // 5. Load pre-computed analysis enrichment (classification, risk, confidence)
146
- const enrichment = await getAnalysisEnrichment();
147
- // 6. Generate for each requested language
148
- for (const lang of languages) {
149
- console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
150
- // Transform data for this language
151
- // MCP returns unknown[] — cast to match data-transformers' expected shapes
152
- const eventGrid = transformCalendarToEventGrid(events, lang);
153
- const weekData = {
154
- events: events,
155
- documents,
156
- questions: questions,
157
- interpellations: interpellations,
158
- highlights: [],
159
- };
160
- const content = generateArticleContent(weekData, 'week-ahead', lang);
161
- const watchPoints = extractWatchPoints({ events: events, documents }, lang);
162
- const metadata = generateMetadata({ events: events, documents }, 'week-ahead', lang);
163
- const readTime = calculateReadTime(content);
164
- const sources = generateSources(['get_calendar_events', 'search_dokument', 'get_fragor', 'get_interpellationer']);
165
- // Language-specific titles
166
- const titles = {
167
- en: { title: `Week Ahead: ${dateRange.start} to ${dateRange.end}`, subtitle: `Parliamentary calendar, committee meetings, and chamber debates for the coming week` },
168
- sv: { title: `Vecka Framåt: ${dateRange.start} till ${dateRange.end}`, subtitle: `Riksdagens kalender, utskottsmöten och kammarens debatter för kommande vecka` },
169
- da: { title: `Ugen Fremover: ${dateRange.start} til ${dateRange.end}`, subtitle: `Parlamentarisk kalender, udvalgsmøder og debatter for den kommende uge` },
170
- no: { title: `Uke Fremover: ${dateRange.start} til ${dateRange.end}`, subtitle: `Parlamentarisk kalender, komitémøter og debatter for kommende uke` },
171
- fi: { title: `Tuleva Viikko: ${dateRange.start} - ${dateRange.end}`, subtitle: `Parlamentin kalenteri, valiokuntien kokoukset ja keskustelut tulevalle viikolle` },
172
- de: { title: `Woche Voraus: ${dateRange.start} bis ${dateRange.end}`, subtitle: `Parlamentarischer Kalender, Ausschusssitzungen und Debatten für die kommende Woche` },
173
- fr: { title: `Semaine à Venir: ${dateRange.start} au ${dateRange.end}`, subtitle: `Calendrier parlementaire, réunions de commission et débats pour la semaine à venir` },
174
- es: { title: `Semana Próxima: ${dateRange.start} a ${dateRange.end}`, subtitle: `Calendario parlamentario, reuniones de comisión y debates para la próxima semana` },
175
- nl: { title: `Week Vooruit: ${dateRange.start} tot ${dateRange.end}`, subtitle: `Parlementaire kalender, commissievergaderingen en debatten voor de komende week` },
176
- ar: { title: `الأسبوع القادم: ${dateRange.start} إلى ${dateRange.end}`, subtitle: `التقويم البرلماني واجتماعات اللجان والمناقشات للأسبوع المقبل` },
177
- he: { title: `השבוע הקרוב: ${dateRange.start} עד ${dateRange.end}`, subtitle: `לוח שנה פרלמנטרי, פגישות ועדה ודיונים לשבוע הקרוב` },
178
- ja: { title: `来週の展望: ${dateRange.start} から ${dateRange.end}`, subtitle: `来週の議会カレンダー、委員会会議、討論` },
179
- ko: { title: `다음 주 전망: ${dateRange.start}부터 ${dateRange.end}까지`, subtitle: `다음 주 의회 일정, 위원회 회의 및 토론` },
180
- zh: { title: `下周展望:${dateRange.start} 至 ${dateRange.end}`, subtitle: `下周议会日程、委员会会议和辩论` }
181
- };
182
- const langTitles = titles[lang] || titles.en;
183
- // Enrich English title/subtitle with content-based highlights
184
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, documents.length + events.length) : langTitles;
185
- // Build visualization sections (SWOT, dashboard, economic)
186
- const sections = buildArticleVisualizationSections(documents, null, lang);
187
- // Generate HTML for this language
188
- const html = generateArticleHTML({
189
- slug: `${slug}-${lang}.html`,
190
- title: enriched.title,
191
- subtitle: enriched.subtitle,
192
- date: toISODate(today),
193
- type: 'prospective',
194
- readTime,
195
- lang,
196
- content,
197
- events: eventGrid,
198
- watchPoints,
199
- sources,
200
- keywords: metadata.keywords,
201
- topics: metadata.topics,
202
- tags: metadata.tags,
203
- sections,
204
- ...(enrichment ?? {}),
205
- });
206
- await writeSingleArticle(html, slug, lang, 'week-ahead');
207
- console.log(` ✅ ${lang.toUpperCase()} version generated`);
208
- }
209
- console.log(' ✅ Week Ahead article generated successfully in all requested languages');
210
- return { success: true, files: languages.length, slug };
211
- }
212
- catch (error) {
213
- console.error('❌ Error generating Week Ahead:', error.message);
214
- console.error(' Stack:', error.stack);
215
- stats.errors++;
216
- return { success: false, error: error.message };
217
- }
218
- }
219
- /**
220
- * Generate Committee Reports article
221
- */
222
- export async function generateCommitteeReports() {
223
- console.log('📋 Generating Committee Reports article...');
224
- try {
225
- const client = await getSharedClient();
226
- console.log(' 🔄 Fetching committee reports from riksdag-regering-mcp...');
227
- let reports = filterFreshDocuments(await client.fetchCommitteeReports(10));
228
- console.log(` 📊 Found ${reports.length} committee reports`);
229
- if (reports.length === 0) {
230
- console.log(' ℹ️ No new committee reports found, skipping');
231
- return { success: true, files: 0 };
232
- }
233
- // Enrich documents with content and metadata
234
- console.log(' 🔍 Enriching documents with detailed content...');
235
- reports = await client.enrichDocumentsWithContent(reports, 3);
236
- const enrichedCount = reports.filter(r => r['contentFetched']).length;
237
- console.log(` ✅ Enriched ${enrichedCount}/${reports.length} reports with content`);
238
- const today = new Date();
239
- const slug = `${formatDateForSlug(today)}-committee-reports`;
240
- const enrichment = await getAnalysisEnrichment();
241
- for (const lang of languages) {
242
- console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
243
- const typedReports = reports;
244
- const content = generateArticleContent({ reports: typedReports }, 'committee-reports', lang);
245
- const watchPoints = extractWatchPoints({ reports: typedReports }, lang);
246
- const metadata = generateMetadata({ reports: typedReports }, 'committee-reports', lang);
247
- const readTime = calculateReadTime(content);
248
- const sources = generateSources(['get_betankanden', 'get_dokument_innehall']);
249
- const titles = {
250
- en: { title: `Committee Reports: Parliamentary Priorities This Week`, subtitle: `Analysis of ${reports.length} committee reports revealing Riksdag priorities for the current session` },
251
- sv: { title: `Utskottsbetänkanden: Riksdagens prioriteringar denna vecka`, subtitle: `Analys av ${reports.length} utskottsbetänkanden som avslöjar riksdagens prioriteringar` },
252
- da: { title: `Udvalgsbetænkninger: Parlamentets prioriteringer denne uge`, subtitle: `Analyse af ${reports.length} udvalgsbetænkninger` },
253
- no: { title: `Komitéinnstillinger: Stortingets prioriteringer denne uken`, subtitle: `Analyse av ${reports.length} komitéinnstillinger` },
254
- fi: { title: `Valiokunnan mietinnöt: Eduskunnan prioriteetit tällä viikolla`, subtitle: `Analyysi ${reports.length} valiokunnan mietinnöstä` },
255
- de: { title: `Ausschussberichte: Parlamentarische Prioritäten diese Woche`, subtitle: `Analyse von ${reports.length} Ausschussberichten` },
256
- fr: { title: `Rapports de commission: Priorités parlementaires cette semaine`, subtitle: `Analyse de ${reports.length} rapports de commission` },
257
- es: { title: `Informes de comisión: Prioridades parlamentarias esta semana`, subtitle: `Análisis de ${reports.length} informes de comisión` },
258
- nl: { title: `Commissierapporten: Parlementaire prioriteiten deze week`, subtitle: `Analyse van ${reports.length} commissierapporten` },
259
- ar: { title: `تقارير اللجان: أولويات البرلمان هذا الأسبوع`, subtitle: `تحليل ${reports.length} تقارير لجان` },
260
- he: { title: `דוחות ועדה: סדרי עדיפויות פרלמנטריים השבוע`, subtitle: `ניתוח ${reports.length} דוחות ועדה` },
261
- ja: { title: `委員会報告:今週の議会優先事項`, subtitle: `${reports.length}件の委員会報告の分析` },
262
- ko: { title: `위원회 보고서: 이번 주 의회 우선순위`, subtitle: `${reports.length}개 위원회 보고서 분석` },
263
- zh: { title: `委员会报告:本周议会优先事项`, subtitle: `${reports.length}份委员会报告分析` }
264
- };
265
- const langTitles = titles[lang] || titles.en;
266
- // Enrich English title/subtitle with content-based highlights
267
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, reports.length) : langTitles;
268
- // Build visualization sections (SWOT, dashboard, economic)
269
- const sections = buildArticleVisualizationSections(reports, null, lang);
270
- const html = generateArticleHTML({
271
- slug: `${slug}-${lang}.html`,
272
- title: enriched.title,
273
- subtitle: enriched.subtitle,
274
- date: toISODate(today),
275
- type: 'analysis',
276
- readTime,
277
- lang,
278
- content,
279
- watchPoints,
280
- sources,
281
- keywords: metadata.keywords,
282
- topics: metadata.topics,
283
- tags: metadata.tags,
284
- sections,
285
- ...(enrichment ?? {}),
286
- });
287
- await writeSingleArticle(html, slug, lang, 'committee-reports');
288
- }
289
- return { success: true, files: languages.length, slug };
290
- }
291
- catch (error) {
292
- console.error('❌ Error generating Committee Reports:', error.message);
293
- stats.errors++;
294
- return { success: false, error: error.message };
295
- }
296
- }
297
- /**
298
- * Generate Government Propositions article
299
- */
300
- export async function generatePropositions() {
301
- console.log('📜 Generating Government Propositions article...');
302
- try {
303
- const client = await getSharedClient();
304
- console.log(' 🔄 Fetching propositions from riksdag-regering-mcp...');
305
- let propositions = filterFreshDocuments(await client.fetchPropositions(10));
306
- console.log(` 📊 Found ${propositions.length} propositions`);
307
- if (propositions.length === 0) {
308
- console.log(' ℹ️ No new propositions found, skipping');
309
- return { success: true, files: 0 };
310
- }
311
- // Enrich documents with content and metadata
312
- console.log(' 🔍 Enriching documents with detailed content...');
313
- propositions = await client.enrichDocumentsWithContent(propositions, 3);
314
- const enrichedCount = propositions.filter(p => p['contentFetched']).length;
315
- console.log(` ✅ Enriched ${enrichedCount}/${propositions.length} propositions with content`);
316
- const today = new Date();
317
- const slug = `${formatDateForSlug(today)}-government-propositions`;
318
- const enrichment = await getAnalysisEnrichment();
319
- for (const lang of languages) {
320
- console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
321
- const typedPropositions = propositions;
322
- const content = generateArticleContent({ propositions: typedPropositions }, 'propositions', lang);
323
- const watchPoints = extractWatchPoints({ propositions: typedPropositions }, lang);
324
- const metadata = generateMetadata({ propositions: typedPropositions }, 'propositions', lang);
325
- const readTime = calculateReadTime(content);
326
- const sources = generateSources(['get_propositioner', 'get_dokument_innehall']);
327
- const titles = {
328
- en: { title: `Government Propositions: Policy Priorities This Week`, subtitle: `Analysis of ${propositions.length} government propositions shaping the legislative agenda` },
329
- sv: { title: `Regeringens propositioner: Veckans prioriteringar`, subtitle: `Analys av ${propositions.length} propositioner som formar den lagstiftande agendan` },
330
- da: { title: `Regeringsforslag: Politiske prioriteringer denne uge`, subtitle: `Analyse af ${propositions.length} regeringsforslag` },
331
- no: { title: `Regjeringens proposisjoner: Politiske prioriteringer denne uken`, subtitle: `Analyse av ${propositions.length} regjeringsproposisjoner` },
332
- fi: { title: `Hallituksen esitykset: Viikon poliittiset prioriteetit`, subtitle: `Analyysi ${propositions.length} hallituksen esityksestä` },
333
- de: { title: `Regierungsvorlagen: Politische Prioritäten diese Woche`, subtitle: `Analyse von ${propositions.length} Regierungsvorlagen` },
334
- fr: { title: `Propositions gouvernementales: Priorités politiques cette semaine`, subtitle: `Analyse de ${propositions.length} propositions gouvernementales` },
335
- es: { title: `Proposiciones gubernamentales: Prioridades políticas esta semana`, subtitle: `Análisis de ${propositions.length} proposiciones gubernamentales` },
336
- nl: { title: `Regeringsvoorstellen: Politieke prioriteiten deze week`, subtitle: `Analyse van ${propositions.length} regeringsvoorstellen` },
337
- ar: { title: `مقترحات الحكومة: الأولويات السياسية هذا الأسبوع`, subtitle: `تحليل ${propositions.length} مقترحات حكومية` },
338
- he: { title: `הצעות ממשלה: סדרי עדיפויות מדיניים השבוע`, subtitle: `ניתוח ${propositions.length} הצעות ממשלה` },
339
- ja: { title: `政府提案:今週の政策優先事項`, subtitle: `${propositions.length}件の政府提案の分析` },
340
- ko: { title: `정부 법안: 이번 주 정책 우선순위`, subtitle: `${propositions.length}개 정부 법안 분석` },
341
- zh: { title: `政府提案:本周政策优先事项`, subtitle: `${propositions.length}份政府提案分析` }
342
- };
343
- const langTitles = titles[lang] || titles.en;
344
- // Enrich English title/subtitle with content-based highlights
345
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, propositions.length) : langTitles;
346
- // Build visualization sections (SWOT, dashboard, economic)
347
- const sections = buildArticleVisualizationSections(propositions, null, lang);
348
- const html = generateArticleHTML({
349
- slug: `${slug}-${lang}.html`,
350
- title: enriched.title,
351
- subtitle: enriched.subtitle,
352
- date: toISODate(today),
353
- type: 'analysis',
354
- readTime,
355
- lang,
356
- content,
357
- watchPoints,
358
- sources,
359
- keywords: metadata.keywords,
360
- topics: metadata.topics,
361
- tags: metadata.tags,
362
- sections,
363
- ...(enrichment ?? {}),
364
- });
365
- await writeSingleArticle(html, slug, lang, 'propositions');
366
- }
367
- return { success: true, files: languages.length, slug };
368
- }
369
- catch (error) {
370
- console.error('❌ Error generating Propositions:', error.message);
371
- stats.errors++;
372
- return { success: false, error: error.message };
373
- }
374
- }
375
- /**
376
- * Generate Opposition Motions article
377
- */
378
- export async function generateMotions() {
379
- console.log('📝 Generating Opposition Motions article...');
380
- try {
381
- const client = await getSharedClient();
382
- console.log(' 🔄 Fetching motions from riksdag-regering-mcp...');
383
- let motions = filterFreshDocuments(await client.fetchMotions(10));
384
- console.log(` 📊 Found ${motions.length} motions`);
385
- if (motions.length === 0) {
386
- console.log(' ℹ️ No new motions found, skipping');
387
- return { success: true, files: 0 };
388
- }
389
- // Enrich documents with content and metadata
390
- console.log(' 🔍 Enriching documents with detailed content...');
391
- motions = await client.enrichDocumentsWithContent(motions, 3);
392
- const enrichedCount = motions.filter(m => m['contentFetched']).length;
393
- console.log(` ✅ Enriched ${enrichedCount}/${motions.length} motions with content`);
394
- const today = new Date();
395
- const slug = `${formatDateForSlug(today)}-opposition-motions`;
396
- const enrichment = await getAnalysisEnrichment();
397
- for (const lang of languages) {
398
- console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
399
- const typedMotions = motions;
400
- const content = generateArticleContent({ motions: typedMotions }, 'motions', lang);
401
- const watchPoints = extractWatchPoints({ motions: typedMotions }, lang);
402
- const metadata = generateMetadata({ motions: typedMotions }, 'motions', lang);
403
- const readTime = calculateReadTime(content);
404
- const sources = generateSources(['get_motioner', 'get_dokument_innehall']);
405
- const titles = {
406
- en: { title: `Opposition Motions: Battle Lines This Week`, subtitle: `Analysis of ${motions.length} opposition motions revealing parliamentary fault lines` },
407
- sv: { title: `Oppositionsmotioner: Veckans stridslinjer`, subtitle: `Analys av ${motions.length} oppositionsmotioner som avslöjar parlamentariska skiljelinjer` },
408
- da: { title: `Oppositionsforslag: Ugens kamppladser`, subtitle: `Analyse af ${motions.length} oppositionsforslag` },
409
- no: { title: `Opposisjonsforslag: Ukens kamplinjer`, subtitle: `Analyse av ${motions.length} opposisjonsforslag` },
410
- fi: { title: `Opposition aloitteet: Viikon taistelulinjat`, subtitle: `Analyysi ${motions.length} opposition aloitteesta` },
411
- de: { title: `Oppositionsanträge: Kampflinien dieser Woche`, subtitle: `Analyse von ${motions.length} Oppositionsanträgen` },
412
- fr: { title: `Motions d'opposition: Lignes de bataille cette semaine`, subtitle: `Analyse de ${motions.length} motions d'opposition` },
413
- es: { title: `Mociones de oposición: Líneas de batalla esta semana`, subtitle: `Análisis de ${motions.length} mociones de oposición` },
414
- nl: { title: `Oppositiemoties: Strijdlijnen deze week`, subtitle: `Analyse van ${motions.length} oppositiemoties` },
415
- ar: { title: `اقتراحات المعارضة: خطوط المعركة هذا الأسبوع`, subtitle: `تحليل ${motions.length} اقتراحات المعارضة` },
416
- he: { title: `הצעות אופוזיציה: קווי העימות השבוע`, subtitle: `ניתוח ${motions.length} הצעות אופוזיציה` },
417
- ja: { title: `野党動議:今週の対立構図`, subtitle: `${motions.length}件の野党動議の分析` },
418
- ko: { title: `야당 동의: 이번 주 대립 구도`, subtitle: `${motions.length}개 야당 동의 분석` },
419
- zh: { title: `反对党动议:本周对立格局`, subtitle: `${motions.length}份反对党动议分析` }
420
- };
421
- const langTitles = titles[lang] || titles.en;
422
- // Enrich English title/subtitle with content-based highlights
423
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, motions.length) : langTitles;
424
- // Build visualization sections (SWOT, dashboard, economic)
425
- const sections = buildArticleVisualizationSections(motions, null, lang);
426
- const html = generateArticleHTML({
427
- slug: `${slug}-${lang}.html`,
428
- title: enriched.title,
429
- subtitle: enriched.subtitle,
430
- date: toISODate(today),
431
- type: 'analysis',
432
- readTime,
433
- lang,
434
- content,
435
- watchPoints,
436
- sources,
437
- keywords: metadata.keywords,
438
- topics: metadata.topics,
439
- tags: metadata.tags,
440
- sections,
441
- ...(enrichment ?? {}),
442
- });
443
- await writeSingleArticle(html, slug, lang, 'motions');
444
- }
445
- return { success: true, files: languages.length, slug };
446
- }
447
- catch (error) {
448
- console.error('❌ Error generating Motions:', error.message);
449
- stats.errors++;
450
- return { success: false, error: error.message };
451
- }
452
- }
453
- /**
454
- * Generate Interpellation Debates article
455
- */
456
- export async function generateInterpellations() {
457
- console.log('🔔 Generating Interpellation Debates article...');
458
- try {
459
- const client = await getSharedClient();
460
- console.log(' 🔄 Fetching interpellations from riksdag-regering-mcp...');
461
- let interpellations = filterFreshDocuments(await client.fetchInterpellations({ limit: 15 }));
462
- console.log(` 📊 Found ${interpellations.length} interpellations`);
463
- if (interpellations.length === 0) {
464
- console.log(' ℹ️ No new interpellations found, skipping');
465
- return { success: true, files: 0 };
466
- }
467
- // Enrich documents with content and metadata
468
- console.log(' 🔍 Enriching documents with detailed content...');
469
- interpellations = await client.enrichDocumentsWithContent(interpellations, 3);
470
- const enrichedCount = interpellations.filter(m => m['contentFetched']).length;
471
- console.log(` ✅ Enriched ${enrichedCount}/${interpellations.length} interpellations with content`);
472
- const today = new Date();
473
- const slug = `${formatDateForSlug(today)}-interpellation-debates`;
474
- const enrichment = await getAnalysisEnrichment();
475
- for (const lang of languages) {
476
- console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
477
- const typedInterps = interpellations;
478
- const content = generateArticleContent({ interpellations: typedInterps }, 'interpellations', lang);
479
- const watchPoints = extractWatchPoints({ interpellations: typedInterps }, lang);
480
- const metadata = generateMetadata({ interpellations: typedInterps }, 'interpellations', lang);
481
- const readTime = calculateReadTime(content);
482
- const sources = generateSources(['get_interpellationer', 'get_dokument_innehall']);
483
- const titles = {
484
- en: { title: `Interpellation Debates: Holding Government to Account`, subtitle: `Analysis of ${interpellations.length} interpellation debates demanding ministerial accountability` },
485
- sv: { title: `Interpellationsdebatter: Regeringen ställs till svars`, subtitle: `Analys av ${interpellations.length} interpellationsdebatter som kräver ministersvar` },
486
- da: { title: `Interpellationsdebatter: Regeringen stilles til ansvar`, subtitle: `Analyse af ${interpellations.length} interpellationsdebatter` },
487
- no: { title: `Interpellasjonsdebatter: Regjeringen stilles til ansvar`, subtitle: `Analyse av ${interpellations.length} interpellasjonsdebatter` },
488
- fi: { title: `Välikysymyskeskustelut: Hallitus tilivelvollisena`, subtitle: `Analyysi ${interpellations.length} välikysymyskeskustelusta` },
489
- de: { title: `Interpellationsdebatten: Regierung in der Verantwortung`, subtitle: `Analyse von ${interpellations.length} Interpellationsdebatten` },
490
- fr: { title: `Débats d'interpellation: Le gouvernement sommé de répondre`, subtitle: `Analyse de ${interpellations.length} débats d'interpellation` },
491
- es: { title: `Debates de interpelación: El gobierno rinde cuentas`, subtitle: `Análisis de ${interpellations.length} debates de interpelación` },
492
- nl: { title: `Interpellatiedebatten: Regering ter verantwoording`, subtitle: `Analyse van ${interpellations.length} interpellatiedebatten` },
493
- ar: { title: `مناقشات الاستجواب: محاسبة الحكومة`, subtitle: `تحليل ${interpellations.length} مناقشات استجواب` },
494
- he: { title: `דיוני אינטרפלציה: הממשלה נדרשת לתת דין וחשבון`, subtitle: `ניתוח ${interpellations.length} דיוני אינטרפלציה` },
495
- ja: { title: `質問主意書討論:政府の説明責任を追及`, subtitle: `${interpellations.length}件の質問主意書討論の分析` },
496
- ko: { title: `대정부 질의 토론: 정부 책임 추궁`, subtitle: `${interpellations.length}건의 대정부 질의 토론 분석` },
497
- zh: { title: `质询辩论:追究政府责任`, subtitle: `${interpellations.length}场质询辩论分析` }
498
- };
499
- const langTitles = titles[lang] || titles.en;
500
- // Enrich English title/subtitle with content-based highlights
501
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, interpellations.length) : langTitles;
502
- // Build visualization sections (SWOT, dashboard, economic)
503
- const sections = buildArticleVisualizationSections(interpellations, null, lang);
504
- const html = generateArticleHTML({
505
- slug: `${slug}-${lang}.html`,
506
- title: enriched.title,
507
- subtitle: enriched.subtitle,
508
- date: toISODate(today),
509
- type: 'analysis',
510
- readTime,
511
- lang,
512
- content,
513
- watchPoints,
514
- sources,
515
- keywords: metadata.keywords,
516
- topics: metadata.topics,
517
- tags: metadata.tags,
518
- sections,
519
- ...(enrichment ?? {}),
520
- });
521
- await writeSingleArticle(html, slug, lang, 'interpellations');
522
- }
523
- return { success: true, files: languages.length, slug };
524
- }
525
- catch (error) {
526
- console.error('❌ Error generating Interpellations:', error.message);
527
- stats.errors++;
528
- return { success: false, error: error.message };
529
- }
530
- }
531
- /**
532
- * Extract a Riksdag document ID (dok_id) from a known URL pattern.
533
- * Supports:
534
- * - https://riksdagen.se/sv/dokument-och-lagar/dokument/{type}/{dok_id}/
535
- * - https://data.riksdagen.se/dokument/{dok_id}
536
- * - https://data.riksdagen.se/dokument/{dok_id}.json
537
- *
538
- * @returns The extracted dok_id, or null if the URL doesn't match a known pattern.
539
- */
540
- export function extractDocIdFromUrl(url) {
541
- try {
542
- const parsed = new URL(url);
543
- const hostname = parsed.hostname.toLowerCase();
544
- const segments = parsed.pathname.split('/').filter(Boolean);
545
- // https://riksdagen.se/sv/dokument-och-lagar/dokument/{type}/{dok_id}
546
- if (hostname === 'riksdagen.se' || hostname === 'www.riksdagen.se') {
547
- // Path: /sv/dokument-och-lagar/dokument/{type}/{dok_id}
548
- const dokIdx = segments.indexOf('dokument');
549
- if (dokIdx >= 0 && segments.length > dokIdx + 2) {
550
- return segments[dokIdx + 2];
551
- }
552
- }
553
- // https://data.riksdagen.se/dokument/{dok_id}[.json|.xml|.html]
554
- if (hostname === 'data.riksdagen.se') {
555
- const dokIdx = segments.indexOf('dokument');
556
- if (dokIdx >= 0 && segments.length > dokIdx + 1) {
557
- return segments[dokIdx + 1].replace(/\.(json|xml|html|pdf)$/i, ''); // strip known file extensions
558
- }
559
- }
560
- return null;
561
- }
562
- catch {
563
- return null;
564
- }
565
- }
566
- /**
567
- * Determine whether a URL points to a government (regeringen.se) resource
568
- * that can be fetched via the get_g0v_document_content MCP tool.
569
- */
570
- export function isGovernmentUrl(url) {
571
- try {
572
- const parsed = new URL(url);
573
- const hostname = parsed.hostname.toLowerCase();
574
- return hostname === 'regeringen.se' || hostname === 'www.regeringen.se';
575
- }
576
- catch {
577
- return false;
578
- }
579
- }
580
- /**
581
- * Determine whether a URL points to a GitHub repository resource
582
- * (github.com or raw.githubusercontent.com) that can be fetched as raw content.
583
- */
584
- export function isGitHubUrl(url) {
585
- try {
586
- const parsed = new URL(url);
587
- const hostname = parsed.hostname.toLowerCase();
588
- return hostname === 'github.com'
589
- || hostname === 'www.github.com'
590
- || hostname === 'raw.githubusercontent.com';
591
- }
592
- catch {
593
- return false;
594
- }
595
- }
596
- /**
597
- * Convert a GitHub blob/tree URL to a raw.githubusercontent.com URL.
598
- * Handles patterns like:
599
- * - https://github.com/{owner}/{repo}/blob/{branch}/{path}
600
- * - https://github.com/{owner}/{repo}/raw/{branch}/{path}
601
- * - https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} (returned as-is)
602
- *
603
- * @returns The raw URL, or null if the URL cannot be converted.
604
- */
605
- export function toGitHubRawUrl(url) {
606
- try {
607
- const parsed = new URL(url);
608
- const hostname = parsed.hostname.toLowerCase();
609
- // Already a raw URL — return as-is
610
- if (hostname === 'raw.githubusercontent.com') {
611
- return url;
612
- }
613
- if (hostname !== 'github.com' && hostname !== 'www.github.com') {
614
- return null;
615
- }
616
- // Path: /{owner}/{repo}/blob/{branch}/{...path}
617
- // or: /{owner}/{repo}/raw/{branch}/{...path}
618
- const segments = parsed.pathname.split('/').filter(Boolean);
619
- if (segments.length < 4)
620
- return null;
621
- const [owner, repo, refType, ...rest] = segments;
622
- if (refType !== 'blob' && refType !== 'raw')
623
- return null;
624
- // rest = [branch, ...pathParts]
625
- return `https://raw.githubusercontent.com/${owner}/${repo}/${rest.join('/')}`;
626
- }
627
- catch {
628
- return null;
629
- }
630
- }
631
- /**
632
- * Compute a short, deterministic hash suffix from a URL path string.
633
- * Used to generate collision-resistant `dok_id` values for documents
634
- * fetched from government or GitHub URLs.
635
- *
636
- * The hash is a simple DJB2-style left-shift-and-add over each character,
637
- * rendered in base-36. A leading `-` (from negative ints) is replaced with `n`.
638
- */
639
- export function hashPathSuffix(path) {
640
- return path
641
- .split('')
642
- .reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0)
643
- .toString(36)
644
- .replace(/^-/, 'n');
645
- }
646
- /**
647
- * Strip HTML tags from a user-supplied string to prevent XSS.
648
- * Uses a multi-pass loop to handle nested tag reconstruction attempts
649
- * (e.g. `<scr<script>ipt>`). Returns **plain text** — callers must
650
- * apply `escapeHtml()` at their render sites so escaping happens exactly once.
651
- */
652
- export function sanitizePlainText(text) {
653
- let cleaned = text;
654
- let prev;
655
- do {
656
- prev = cleaned;
657
- cleaned = cleaned.replace(/<[^>]*>/g, '');
658
- } while (cleaned !== prev);
659
- return cleaned;
660
- }
661
- // ---------------------------------------------------------------------------
662
- // Deep-Inspection content generator (topic-focused, comprehensive)
663
- // ---------------------------------------------------------------------------
664
- /** Cyberpunk-theme colour palette for deep-inspection dashboard charts. */
665
- const DEEP_CHART_PALETTE = [
666
- '#00d9ff', '#ff006e', '#ffbe0b', '#00ff88', '#ff8800', '#aa00ff',
667
- ];
668
- /** Per-language headings for sections of the deep-inspection article. */
669
- const DEEP_SECTION_LABELS = {
670
- documentIntelligence: {
671
- en: 'Document Intelligence Analysis',
672
- sv: 'Dokumentunderrättelseanalys',
673
- da: 'Dokumentefterretningsanalyse',
674
- no: 'Dokumentetterretningsanalyse',
675
- fi: 'Asiakirjatiedusteluanalyysi',
676
- de: 'Dokumentenintelligenz-Analyse',
677
- fr: 'Analyse renseignement documentaire',
678
- es: 'Análisis de inteligencia documental',
679
- nl: 'Documentintelligentie-analyse',
680
- ar: 'تحليل استخبارات الوثائق',
681
- he: 'ניתוח מודיעין מסמכים',
682
- ja: '文書インテリジェンス分析',
683
- ko: '문서 인텔리전스 분석',
684
- zh: '文件情报分析',
685
- },
686
- strategicImplications: {
687
- en: 'Strategic Implications',
688
- sv: 'Strategiska implikationer',
689
- da: 'Strategiske implikationer',
690
- no: 'Strategiske implikasjoner',
691
- fi: 'Strategiset vaikutukset',
692
- de: 'Strategische Implikationen',
693
- fr: 'Implications stratégiques',
694
- es: 'Implicaciones estratégicas',
695
- nl: 'Strategische implicaties',
696
- ar: 'الآثار الاستراتيجية',
697
- he: 'השלכות אסטרטגיות',
698
- ja: '戦略的示唆',
699
- ko: '전략적 시사점',
700
- zh: '战略影响',
701
- },
702
- keyTakeaways: {
703
- en: 'Key Takeaways',
704
- sv: 'Viktiga slutsatser',
705
- da: 'Vigtigste konklusioner',
706
- no: 'Viktigste konklusjoner',
707
- fi: 'Tärkeimmät johtopäätökset',
708
- de: 'Wesentliche Erkenntnisse',
709
- fr: 'Points clés',
710
- es: 'Conclusiones clave',
711
- nl: 'Belangrijkste bevindingen',
712
- ar: 'النقاط الرئيسية',
713
- he: 'נקודות מפתח',
714
- ja: '主なポイント',
715
- ko: '핵심 사항',
716
- zh: '关键要点',
717
- },
718
- topicContext: {
719
- en: 'Topic Context & Significance',
720
- sv: 'Ämneskontext och betydelse',
721
- da: 'Emnekontext og betydning',
722
- no: 'Emnekontext og betydning',
723
- fi: 'Aiheyhteyssä ja merkityksessä',
724
- de: 'Themenkontext und Bedeutung',
725
- fr: 'Contexte thématique et signification',
726
- es: 'Contexto temático y significación',
727
- nl: 'Onderwerpcontext en betekenis',
728
- ar: 'السياق الموضوعي والأهمية',
729
- he: 'הקשר נושאי ומשמעות',
730
- ja: 'トピックの文脈と重要性',
731
- ko: '주제 맥락 및 중요성',
732
- zh: '主题背景与意义',
733
- },
734
- documentsByType: {
735
- en: 'Documents by Type', sv: 'Dokument efter typ', da: 'Dokumenter efter type', no: 'Dokumenter etter type',
736
- fi: 'Asiakirjat tyypin mukaan', de: 'Dokumente nach Typ', fr: 'Documents par type', es: 'Documentos por tipo',
737
- nl: 'Documenten per type', ar: 'الوثائق حسب النوع', he: 'מסמכים לפי סוג',
738
- ja: '種類別文書', ko: '유형별 문서', zh: '按类型分类的文件',
739
- },
740
- documents: {
741
- en: 'Documents', sv: 'Dokument', da: 'Dokumenter', no: 'Dokumenter',
742
- fi: 'Asiakirjat', de: 'Dokumente', fr: 'Documents', es: 'Documentos',
743
- nl: 'Documenten', ar: 'وثائق', he: 'מסמכים',
744
- ja: '文書', ko: '문서', zh: '文件',
745
- },
746
- documentsAnalysed: {
747
- en: 'parliamentary documents analysed', sv: 'riksdagsdokument analyserade', da: 'parlamentsdokumenter analyseret', no: 'parlamentsdokumenter analysert',
748
- fi: 'asiakirjaa analysoitu', de: 'parlamentarische Dokumente analysiert', fr: 'documents parlementaires analysés', es: 'documentos parlamentarios analizados',
749
- nl: 'parlementaire documenten geanalyseerd', ar: 'وثيقة برلمانية تم تحليلها', he: 'מסמכים פרלמנטריים שנותחו',
750
- ja: '件の議会文書を分析', ko: '의회 문서 분석됨', zh: '份议会文件已分析',
751
- },
752
- documentAnalysed: {
753
- en: 'parliamentary document analysed', sv: 'riksdagsdokument analyserat', da: 'parlamentsdokument analyseret', no: 'parlamentsdokument analysert',
754
- fi: 'asiakirja analysoitu', de: 'parlamentarisches Dokument analysiert', fr: 'document parlementaire analysé', es: 'documento parlamentario analizado',
755
- nl: 'parlementair document geanalyseerd', ar: 'وثيقة برلمانية تم تحليلها', he: 'מסמך פרלמנטרי שנותח',
756
- ja: '件の議会文書を分析', ko: '의회 문서 분석됨', zh: '份议会文件已分析',
757
- },
758
- documentTypes: {
759
- en: 'Document Types', sv: 'Dokumenttyper', da: 'Dokumenttyper', no: 'Dokumenttyper',
760
- fi: 'Asiakirjatyypit', de: 'Dokumenttypen', fr: 'Types de documents', es: 'Tipos de documentos',
761
- nl: 'Documenttypen', ar: 'أنواع الوثائق', he: 'סוגי מסמכים',
762
- ja: '文書種類', ko: '문서 유형', zh: '文件类型',
763
- },
764
- policyDomains: {
765
- en: 'Policy Domains', sv: 'Politikområden', da: 'Politikområder', no: 'Politikkområder',
766
- fi: 'Politiikka-alueet', de: 'Politikbereiche', fr: 'Domaines politiques', es: 'Áreas de política',
767
- nl: 'Beleidsdomeinen', ar: 'مجالات السياسة', he: 'תחומי מדיניות',
768
- ja: '政策分野', ko: '정책 영역', zh: '政策领域',
769
- },
770
- stakeholders: {
771
- en: 'Stakeholders', sv: 'Intressenter', da: 'Interessenter', no: 'Interessenter',
772
- fi: 'Sidosryhmät', de: 'Stakeholder', fr: 'Parties prenantes', es: 'Partes interesadas',
773
- nl: 'Belanghebbenden', ar: 'أصحاب المصلحة', he: 'בעלי עניין',
774
- ja: 'ステークホルダー', ko: '이해관계자', zh: '利益相关者',
775
- },
776
- executiveSummary: {
777
- en: 'Executive Intelligence Summary',
778
- sv: 'Sammanfattning för beslutsfattare',
779
- da: 'Ledelsesinformation',
780
- no: 'Lederinformasjon',
781
- fi: 'Johdon yhteenveto',
782
- de: 'Führungszusammenfassung',
783
- fr: 'Résumé pour décideurs',
784
- es: 'Resumen ejecutivo de inteligencia',
785
- nl: 'Managementsamenvatting',
786
- ar: 'ملخص الاستخبارات التنفيذية',
787
- he: 'סיכום מודיעין מנהלים',
788
- ja: 'エグゼクティブ・インテリジェンス要約',
789
- ko: '경영진 인텔리전스 요약',
790
- zh: '执行情报摘要',
791
- },
792
- predictiveAssessment: {
793
- en: 'Predictive Assessment',
794
- sv: 'Prediktiv bedömning',
795
- da: 'Prædiktiv vurdering',
796
- no: 'Prediktiv vurdering',
797
- fi: 'Ennakoiva arviointi',
798
- de: 'Prädiktive Bewertung',
799
- fr: 'Évaluation prédictive',
800
- es: 'Evaluación predictiva',
801
- nl: 'Voorspellende beoordeling',
802
- ar: 'التقييم التنبؤي',
803
- he: 'הערכה חיזויית',
804
- ja: '予測評価',
805
- ko: '예측 평가',
806
- zh: '预测性评估',
807
- },
808
- historicalContext: {
809
- en: 'Historical Context & Precedents',
810
- sv: 'Historisk kontext och prejudikat',
811
- da: 'Historisk kontekst og præcedenser',
812
- no: 'Historisk kontekst og presedens',
813
- fi: 'Historiallinen konteksti ja ennakkotapaukset',
814
- de: 'Historischer Kontext und Präzedenzfälle',
815
- fr: 'Contexte historique et précédents',
816
- es: 'Contexto histórico y precedentes',
817
- nl: 'Historische context en precedenten',
818
- ar: 'السياق التاريخي والسوابق',
819
- he: 'הקשר היסטורי ותקדימים',
820
- ja: '歴史的背景と先例',
821
- ko: '역사적 맥락 및 선례',
822
- zh: '历史背景与先例',
823
- },
824
- methodology: {
825
- en: 'Methodology & Confidence',
826
- sv: 'Metodik och konfidensgrad',
827
- da: 'Metodologi og konfidens',
828
- no: 'Metodologi og konfidens',
829
- fi: 'Menetelmä ja luottamustaso',
830
- de: 'Methodik und Konfidenz',
831
- fr: 'Méthodologie et confiance',
832
- es: 'Metodología y confianza',
833
- nl: 'Methodologie en betrouwbaarheid',
834
- ar: 'المنهجية ودرجة الثقة',
835
- he: 'מתודולוגיה ורמת ביטחון',
836
- ja: '方法論と信頼度',
837
- ko: '방법론 및 신뢰도',
838
- zh: '方法论与置信度',
839
- },
840
- likelyOutcome: {
841
- en: 'Likely Outcome', sv: 'Troligt utfall', da: 'Sandsynligt udfald', no: 'Sannsynlig utfall',
842
- fi: 'Todennäköinen lopputulos', de: 'Wahrscheinliches Ergebnis', fr: 'Résultat probable', es: 'Resultado probable',
843
- nl: 'Waarschijnlijk resultaat', ar: 'النتيجة المحتملة', he: 'תוצאה סבירה',
844
- ja: '見込まれる結果', ko: '예상 결과', zh: '可能结果',
845
- },
846
- coalitionStability: {
847
- en: 'Coalition Stability Forecast', sv: 'Koalitionsstabilitetsprognos', da: 'Koalitionsstabilitetsprognose', no: 'Koalisjonstabilitetsprognose',
848
- fi: 'Koalition vakausennuste', de: 'Koalitionsstabilitätsprognose', fr: 'Prévision de stabilité de coalition', es: 'Pronóstico de estabilidad de coalición',
849
- nl: 'Coalitiesstabiliteitsprognose', ar: 'توقعات استقرار الائتلاف', he: 'תחזית יציבות קואליציה',
850
- ja: '連立安定性予測', ko: '연립 안정성 예측', zh: '联合稳定性预测',
851
- },
852
- riskScenarios: {
853
- en: 'Risk Scenarios', sv: 'Riskscenarier', da: 'Risikoscenarier', no: 'Risikoscenarier',
854
- fi: 'Riskiskenaariot', de: 'Risikoszenarien', fr: 'Scénarios de risque', es: 'Escenarios de riesgo',
855
- nl: "Risicoscenario's", ar: 'سيناريوهات المخاطر', he: 'תרחישי סיכון',
856
- ja: 'リスクシナリオ', ko: '위험 시나리오', zh: '风险情景',
857
- },
858
- parliamentaryAnalysis: {
859
- en: 'Parliamentary Analysis', sv: 'Riksdagsanalys', da: 'Parlamentarisk analyse', no: 'Parlamentarisk analyse',
860
- fi: 'Parlamentaarinen analyysi', de: 'Parlamentarische Analyse', fr: 'Analyse parlementaire', es: 'Análisis parlamentario',
861
- nl: 'Parlementaire analyse', ar: 'التحليل البرلماني', he: 'ניתוח פרלמנטרי',
862
- ja: '議会分析', ko: '의회 분석', zh: '议会分析',
863
- },
864
- govCommunications: {
865
- en: 'Gov. Communications', sv: 'Regeringsmeddelanden', da: 'Regeringsmeddelelser', no: 'Regjeringsmeldinger',
866
- fi: 'Hallituksen tiedonannot', de: 'Regierungsmitteilungen', fr: 'Communications gouvernementales', es: 'Comunicaciones del Gobierno',
867
- nl: 'Regeringsmededelingen', ar: 'بلاغات حكومية', he: 'הודעות ממשלתיות',
868
- ja: '政府通信', ko: '정부 통신', zh: '政府通报',
869
- },
870
- conceptualMap: {
871
- en: 'Conceptual map', sv: 'Konceptkarta', da: 'Konceptkort', no: 'Konseptkart',
872
- fi: 'Käsitekartta', de: 'Konzeptkarte', fr: 'Carte conceptuelle', es: 'Mapa conceptual',
873
- nl: 'Conceptmap', ar: 'خريطة مفاهيمية', he: 'מפת מושגים',
874
- ja: 'コンセプトマップ', ko: '개념 맵', zh: '概念图',
875
- },
876
- };
877
- function deepLabel(key, lang) {
878
- const map = DEEP_SECTION_LABELS[key];
879
- return (map?.[lang]) ?? (map?.en ?? key);
880
- }
881
- /** Checks whether a document represents an SFS (enacted statute). Delegates to {@link effectiveType}. */
882
- function isSfsDoc(d) {
883
- return effectiveType(d) === 'sfs';
884
- }
885
- function docTypeLabel(doktyp, lang, count) {
886
- return localizeDocType(doktyp, lang, count);
887
- }
888
- /**
889
- * Generate topic-focused, comprehensive deep-inspection article content.
890
- * All sections are explicitly oriented around `topic`. Uses enriched full-text
891
- * content from each document and the 5W deep-analysis framework.
892
- *
893
- * @param depth - Analysis depth (1–4). Higher depth adds more intelligence sections:
894
- * 1 = Topic Context + Document Intelligence + 5W Deep Analysis + Strategic Implications + Key Takeaways
895
- * 2 = depth 1 + Historical Context + Predictive Assessment
896
- * 3 = depth 2 + Executive Intelligence Summary + Methodology (3 iterations)
897
- * 4 = depth 3 + quality-review iteration in Methodology (4 iterations)
898
- * When an AIAnalysisResult is supplied, its strategic implications and key
899
- * takeaways replace the template-based versions for richer, context-aware output.
900
- */
901
- function generateDeepInspectionContent(docs, topic, lang, depth = 1, aiResult) {
902
- const esc = escapeHtml;
903
- let html = '';
904
- // ── 0. Executive Intelligence Summary (depth ≥ 3) ────────────────────────
905
- if (depth >= 3) {
906
- html += buildExecutiveSummary(docs, topic, lang);
907
- }
908
- // ── 1. Topic Context ───────────────────────────────────────────────────────
909
- const topicHeading = deepLabel('topicContext', lang);
910
- const topicCtxPara = buildTopicContextParagraph(docs, topic, lang);
911
- html += `\n<section class="deep-topic-context" aria-label="${esc(topicHeading)}">\n`;
912
- html += ` <h2>${esc(topicHeading)}</h2>\n`;
913
- html += ` ${topicCtxPara}\n`;
914
- html += `</section>\n`;
915
- // ── 2. Per-document deep intelligence entries ──────────────────────────────
916
- const docIntelHeading = deepLabel('documentIntelligence', lang);
917
- html += `\n<section class="document-intelligence-analysis" aria-label="${esc(docIntelHeading)}">\n`;
918
- html += ` <h2>${esc(docIntelHeading)}</h2>\n`;
919
- docs.forEach((doc, idx) => {
920
- html += buildDocumentEntry(doc, topic, lang, idx + 1, docs.length);
921
- });
922
- html += `</section>\n`;
923
- // ── 3. Cross-document 5W deep analysis ────────────────────────────────────
924
- const deepAnalysis = generateDeepAnalysisSection({
925
- documents: docs,
926
- lang,
927
- articleType: 'deep-inspection',
928
- whyContext: topic
929
- ? `This deep-inspection focuses exclusively on: ${topic}. All findings are evaluated in this context.`
930
- : undefined,
931
- });
932
- if (deepAnalysis)
933
- html += deepAnalysis;
934
- // ── 4. Strategic implications ──────────────────────────────────────────────
935
- const stratHeading = deepLabel('strategicImplications', lang);
936
- html += `\n<section class="strategic-implications" aria-label="${esc(stratHeading)}">\n`;
937
- html += ` <h2>${esc(stratHeading)}</h2>\n`;
938
- // Use AI-generated strategic implications when available (richer than template version).
939
- // Safe to inject directly: AIAnalysisPipeline.buildStrategicImplications() escapes all
940
- // dynamic values (topic, domain, signal) at construction time via escapeHtml(). The
941
- // fallback buildStrategicImplications() in generators.ts also escapes its inputs.
942
- const strategicImplHtml = aiResult?.strategicImplications
943
- ?? buildStrategicImplications(docs, topic, lang);
944
- html += ` ${strategicImplHtml}\n`;
945
- html += `</section>\n`;
946
- // ── 5. Historical Context (depth ≥ 2) ─────────────────────────────────────
947
- if (depth >= 2) {
948
- html += buildHistoricalContext(docs, topic, lang);
949
- }
950
- // ── 6. Predictive Assessment (depth ≥ 2) ──────────────────────────────────
951
- if (depth >= 2) {
952
- html += buildPredictiveAssessment(docs, topic, lang);
953
- }
954
- // ── 7. Key takeaways ───────────────────────────────────────────────────────
955
- const takeawayHeading = deepLabel('keyTakeaways', lang);
956
- html += `\n<section class="key-takeaways" aria-label="${esc(takeawayHeading)}">\n`;
957
- html += ` <h2>${esc(takeawayHeading)}</h2>\n`;
958
- if (aiResult?.keyTakeaways && aiResult.keyTakeaways.length > 0) {
959
- // Use AI-generated takeaways
960
- html += `<ul class="key-takeaways-list">\n`;
961
- aiResult.keyTakeaways.forEach(item => {
962
- html += ` <li>${esc(item)}</li>\n`;
963
- });
964
- html += `</ul>\n`;
965
- }
966
- else {
967
- html += buildKeyTakeaways(docs, topic, lang);
968
- }
969
- html += `</section>\n`;
970
- // ── 8. Methodology & Confidence (depth ≥ 3) ───────────────────────────────
971
- if (depth >= 3) {
972
- html += buildMethodologySection(docs, topic, lang, depth);
973
- }
974
- return html;
975
- }
976
- function mapReportDepthToPipelineDepth(depth) {
977
- if (depth <= 1)
978
- return 'quick';
979
- if (depth === 2)
980
- return 'standard';
981
- return 'deep';
982
- }
983
- function writeAnalysisMetadata(slug, metadata) {
984
- try {
985
- fs.mkdirSync(METADATA_DIR, { recursive: true });
986
- const filePath = path.join(METADATA_DIR, `ai-analysis-${slug}-${metadata.lang}.json`);
987
- fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), 'utf8');
988
- }
989
- catch (error) {
990
- console.warn(`⚠️ Failed to write analysis metadata for ${slug}/${metadata.lang}:`, error);
991
- }
992
- }
993
- /**
994
- * Test-only hooks for deep-inspection content generation.
995
- * Exported to support behavioral tests without source inspection.
996
- * @internal
997
- */
998
- export const __deepInspectionTestHooks = {
999
- generateDeepInspectionContent,
1000
- };
1001
- /** Build the topic context introductory paragraph. */
1002
- function buildTopicContextParagraph(docs, topic, lang) {
1003
- const esc = escapeHtml;
1004
- const docCount = docs.length;
1005
- const allDomains = new Set();
1006
- // When a focus topic is provided, suppress generic detected domains entirely — they can
1007
- // include tangential policy areas that bleed into "other areas" beyond the stated focus.
1008
- // The topic itself IS the scope; detected domains would only add noise.
1009
- if (!topic) {
1010
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDomains.add(dom)));
1011
- }
1012
- const domainList = [...allDomains].slice(0, 5).map(d => esc(d)).join(', ');
1013
- const templates = {
1014
- en: `This deep-inspection analyses ${docCount} targeted parliamentary document${docCount !== 1 ? 's' : ''}${topic ? ` with an exclusive focus on <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Policy domains covered: ${domainList}.` : ''} Each document has been individually reviewed for relevance, legislative significance, and strategic implications — all findings are evaluated through the lens of the stated focus.`,
1015
- sv: `Denna djupanalys granskar ${docCount} riktade riksdagsdokument${topic ? ` med exklusivt fokus på <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Policyområden: ${domainList}.` : ''} Varje dokument har granskats individuellt avseende relevans, lagstiftningssignifikans och strategiska implikationer — alla resultat utvärderas genom det angivna fokuset.`,
1016
- de: `Diese Tiefenanalyse untersucht ${docCount} gezielte Parlamentsdokument${docCount !== 1 ? 'e' : ''}${topic ? ` mit ausschließlichem Fokus auf <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Politikbereiche: ${domainList}.` : ''} Jedes Dokument wurde einzeln auf Relevanz, gesetzgeberische Bedeutung und strategische Implikationen geprüft.`,
1017
- fr: `Cette analyse approfondie examine ${docCount} document${docCount !== 1 ? 's' : ''} parlementaire${docCount !== 1 ? 's' : ''} ciblé${docCount !== 1 ? 's' : ''}${topic ? ` avec un focus exclusif sur <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Domaines politiques couverts: ${domainList}.` : ''} Chaque document a été examiné individuellement.`,
1018
- es: `Esta inspección profunda analiza ${docCount} documento${docCount !== 1 ? 's' : ''} parlamentario${docCount !== 1 ? 's' : ''} específico${docCount !== 1 ? 's' : ''}${topic ? ` con enfoque exclusivo en <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Áreas de política cubiertos: ${domainList}.` : ''}`,
1019
- da: `Denne dybdeanalyse undersøger ${docCount} målrettede parlamentariske dokument${docCount !== 1 ? 'er' : ''}${topic ? ` med eksklusivt fokus på <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Politikområder: ${domainList}.` : ''}`,
1020
- no: `Denne dybdeanalysen undersøker ${docCount} målrettede parlamentariske dokument${docCount !== 1 ? 'er' : ''}${topic ? ` med eksklusivt fokus på <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Politikkområder: ${domainList}.` : ''}`,
1021
- fi: `Tämä syväanalyysi tutkii ${docCount} kohdennettua parlamentaarista asiakirjaa${topic ? `, joissa on yksinomainen fokus aiheeseen <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Politiikka-alueet: ${domainList}.` : ''}`,
1022
- nl: `Deze diepteanalyse bestudeert ${docCount} gerichte parlementaire document${docCount !== 1 ? 'en' : ''}${topic ? ` met exclusieve focus op <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `Beleidsdomeinen: ${domainList}.` : ''}`,
1023
- ar: `يحلل هذا الفحص المعمق ${docCount} وثيقة برلمانية مستهدفة${topic ? ` مع التركيز الحصري على <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `مجالات السياسة المشمولة: ${domainList}.` : ''}`,
1024
- he: `ניתוח מעמיק זה בוחן ${docCount} מסמכים פרלמנטריים ממוקדים${topic ? ` עם מיקוד בלעדי על <strong>${esc(topic)}</strong>` : ''}. ${domainList ? `תחומי מדיניות: ${domainList}.` : ''}`,
1025
- ja: `この詳細分析は${docCount}件のターゲット議会文書を調査します${topic ? `、<strong>${esc(topic)}</strong>に専ら焦点を当てています` : ''}。${domainList ? `政策分野: ${domainList}。` : ''}`,
1026
- ko: `이 심층 분석은 ${docCount}건의 대상 의회 문서를 분석합니다${topic ? `, <strong>${esc(topic)}</strong>에 전적으로 집중합니다` : ''}. ${domainList ? `정책 분야: ${domainList}.` : ''}`,
1027
- zh: `本次深度分析对${docCount}份目标议会文件进行分析${topic ? `,专注于<strong>${esc(topic)}</strong>` : ''}。${domainList ? `涵盖政策领域:${domainList}。` : ''}`,
1028
- };
1029
- return `<p>${templates[lang] ?? templates.en}</p>`;
1030
- }
1031
- /** Build a comprehensive HTML entry for a single document — topic-focused. */
1032
- function buildDocumentEntry(doc, topic, lang, index, total) {
1033
- const esc = escapeHtml;
1034
- const title = doc.titel || doc.title || doc.dokumentnamn || doc.dok_id || '';
1035
- const doktyp = doc.doktyp || doc.documentType || '';
1036
- const date = doc.datum ? esc(doc.datum) : '';
1037
- const organ = doc.organ || doc.committee || '';
1038
- const typeLabel = doktyp ? docTypeLabel(doktyp, lang, 1) : '';
1039
- const domains = detectPolicyDomains(doc, lang);
1040
- let entry = `\n <article class="document-entry" data-index="${index}">\n`;
1041
- entry += ` <h3>${esc(title)}</h3>\n`;
1042
- // Document metadata line
1043
- const metaParts = [];
1044
- if (typeLabel)
1045
- metaParts.push(`<span class="doc-type">${esc(typeLabel)}</span>`);
1046
- if (doc.dok_id)
1047
- metaParts.push(`<code>${esc(doc.dok_id)}</code>`);
1048
- if (date)
1049
- metaParts.push(`<time datetime="${date}">${date}</time>`);
1050
- if (organ)
1051
- metaParts.push(`<span class="doc-organ">${esc(organ)}</span>`);
1052
- if (domains.length > 0 && !topic)
1053
- metaParts.push(`<em>${domains.map(d => esc(d)).join(', ')}</em>`);
1054
- if (metaParts.length > 0) {
1055
- entry += ` <p class="doc-meta">${metaParts.join(' · ')}</p>\n`;
1056
- }
1057
- // Topic relevance note when topic is provided
1058
- if (topic) {
1059
- const topicRelevanceTemplates = {
1060
- en: `Relevance to <strong>${esc(topic)}</strong>:`,
1061
- sv: `Relevans för <strong>${esc(topic)}</strong>:`,
1062
- de: `Relevanz für <strong>${esc(topic)}</strong>:`,
1063
- fr: `Pertinence pour <strong>${esc(topic)}</strong>:`,
1064
- es: `Relevancia para <strong>${esc(topic)}</strong>:`,
1065
- da: `Relevans for <strong>${esc(topic)}</strong>:`,
1066
- no: `Relevans for <strong>${esc(topic)}</strong>:`,
1067
- fi: `Relevanssi aiheeseen <strong>${esc(topic)}</strong>:`,
1068
- nl: `Relevantie voor <strong>${esc(topic)}</strong>:`,
1069
- ar: `الصلة بـ <strong>${esc(topic)}</strong>:`,
1070
- he: `הרלוונטיות ל<strong>${esc(topic)}</strong>:`,
1071
- ja: `<strong>${esc(topic)}</strong>への関連性:`,
1072
- ko: `<strong>${esc(topic)}</strong>에 대한 관련성:`,
1073
- zh: `与<strong>${esc(topic)}</strong>的关联:`,
1074
- };
1075
- entry += ` <p class="topic-relevance"><strong>${topicRelevanceTemplates[lang] ?? topicRelevanceTemplates.en}</strong></p>\n`;
1076
- }
1077
- // Deep policy analysis (uses full text if enriched, otherwise significance)
1078
- // Pass 600-char limit — deep inspection requires substantive per-document analysis.
1079
- const deepAnalysis = generateDeepPolicyAnalysis(doc, lang, doktyp || undefined, 600);
1080
- if (deepAnalysis) {
1081
- entry += ` <div class="doc-analysis">${deepAnalysis}</div>\n`;
1082
- }
1083
- // Summary/notis when no full text but summary is available
1084
- const summary = doc.summary || doc.notis || '';
1085
- if (summary && !doc.contentFetched) {
1086
- entry += ` <blockquote class="doc-summary">${esc(summary)}</blockquote>\n`;
1087
- }
1088
- if (index < total) {
1089
- entry += ` <hr class="doc-separator">\n`;
1090
- }
1091
- entry += ` </article>\n`;
1092
- return entry;
1093
- }
1094
- /** Build strategic implications paragraph tied to the topic and document patterns. */
1095
- function buildStrategicImplications(docs, topic, lang) {
1096
- const esc = escapeHtml;
1097
- const propCount = docs.filter(d => effectiveType(d) === 'prop').length;
1098
- const betCount = docs.filter(d => effectiveType(d) === 'bet').length;
1099
- const motCount = docs.filter(d => effectiveType(d) === 'mot').length;
1100
- const pressmCount = docs.filter(d => effectiveType(d) === 'pressm').length;
1101
- const extCount = docs.filter(d => effectiveType(d) === 'ext').length;
1102
- const enrichedCount = docs.filter(d => d.contentFetched).length;
1103
- const legislativeCount = propCount + betCount + motCount;
1104
- // Detect all policy domains across documents for richer context
1105
- const allDomains = new Set();
1106
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDomains.add(dom)));
1107
- const domainPhrase = allDomains.size > 0 ? [...allDomains].slice(0, 3).map(d => esc(d)).join(', ') : '';
1108
- // Choose a template style based on document composition
1109
- const isLegislativeFocused = legislativeCount > 0;
1110
- const isPressOrExternal = pressmCount + extCount > 0 && legislativeCount === 0;
1111
- let enText;
1112
- if (isPressOrExternal) {
1113
- // Non-legislative documents (press releases, external) — differentiate messaging
1114
- const typeDesc = pressmCount > 0 ? `${pressmCount} government press release${pressmCount !== 1 ? 's' : ''}` : `${extCount} external reference${extCount !== 1 ? 's' : ''}`;
1115
- const signalText = pressmCount > 0
1116
- ? 'Government press communications signal policy priorities and upcoming legislative action.'
1117
- : 'These external references illuminate the policy landscape and highlight areas of potential legislative interest.';
1118
- enText = `Based on analysis of ${docs.length} document${docs.length !== 1 ? 's' : ''} (${enrichedCount} enriched with full text)${topic ? ` specifically addressing <strong>${esc(topic)}</strong>` : ''}: This deep inspection examines ${typeDesc}${domainPhrase ? ` spanning ${domainPhrase}` : ''}. ${signalText} Stakeholders should track whether formal propositions or committee referrals follow, which would confirm the transition from policy signalling to legislative commitment.`;
1119
- }
1120
- else if (isLegislativeFocused) {
1121
- const signalText = propCount > betCount ? 'active government agenda-setting' : betCount > propCount ? 'strong parliamentary scrutiny' : 'balanced legislative activity';
1122
- enText = `Based on analysis of ${docs.length} parliamentary document${docs.length !== 1 ? 's' : ''} (${enrichedCount} enriched with full text)${topic ? ` specifically addressing <strong>${esc(topic)}</strong>` : ''}: The legislative pipeline shows ${propCount} government proposition${propCount !== 1 ? 's' : ''}, ${betCount} committee report${betCount !== 1 ? 's' : ''}, and ${motCount} opposition motion${motCount !== 1 ? 's' : ''}. This distribution signals ${signalText}${domainPhrase ? ` in ${domainPhrase}` : ' in this policy area'}. Stakeholders should monitor committee deliberations and chamber voting patterns as the most reliable indicators of policy trajectory.`;
1123
- }
1124
- else {
1125
- enText = `Based on analysis of ${docs.length} document${docs.length !== 1 ? 's' : ''} (${enrichedCount} enriched with full text)${topic ? ` specifically addressing <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? `, covering ${domainPhrase}` : ''}: This analysis provides a snapshot of current policy direction. Stakeholders should monitor subsequent legislative developments for concrete implementation signals.`;
1126
- }
1127
- const enrichedPhraseSv = `${enrichedCount} ${enrichedCount === 1 ? 'berikat' : 'berikade'} med fulltext`;
1128
- let svText;
1129
- if (isPressOrExternal) {
1130
- const typeDescSv = pressmCount > 0 ? `${pressmCount} pressmeddelande${pressmCount !== 1 ? 'n' : ''}` : `${extCount} extern${extCount !== 1 ? 'a' : ''} referens${extCount !== 1 ? 'er' : ''}`;
1131
- const signalTextSv = pressmCount > 0
1132
- ? 'Regeringens presskommunikation signalerar politiska prioriteringar och kommande lagstiftningsåtgärder.'
1133
- : 'Dessa externa referenser belyser det politiska landskapet och lyfter fram områden med potentiellt lagstiftningsintresse.';
1134
- svText = `Baserat på analys av ${docs.length} dokument (${enrichedPhraseSv})${topic ? ` med specifik inriktning på <strong>${esc(topic)}</strong>` : ''}: Denna djupanalys granskar ${typeDescSv}${domainPhrase ? ` inom ${domainPhrase}` : ''}. ${signalTextSv} Intressenter bör bevaka om formella propositioner eller utskottsremisser följer.`;
1135
- }
1136
- else if (isLegislativeFocused && legislativeCount > 0) {
1137
- svText = `Baserat på analys av ${docs.length} riksdagsdokument (${enrichedPhraseSv})${topic ? ` med specifik inriktning på <strong>${esc(topic)}</strong>` : ''}: Det lagstiftande flödet visar ${propCount} proposition${propCount !== 1 ? 'er' : ''}, ${betCount} betänkande${betCount !== 1 ? 'n' : ''} och ${motCount} motion${motCount !== 1 ? 'er' : ''}. Intressenter bör följa utskottens överläggningar och kammarens voteringsmönster.`;
1138
- }
1139
- else {
1140
- svText = `Baserat på analys av ${docs.length} dokument (${enrichedPhraseSv})${topic ? ` med specifik inriktning på <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` inom ${domainPhrase}` : ''}: Analysen ger en ögonblicksbild av den aktuella politiska inriktningen och dess betydelse för centrala intressenter.`;
1141
- }
1142
- const templates = {
1143
- en: enText,
1144
- sv: svText,
1145
- da: `Baseret på analyse af ${docs.length} dokument${docs.length !== 1 ? 'er' : ''} (${enrichedCount} beriget med fulde tekster)${topic ? ` specifikt om <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `Den lovgivningsmæssige aktivitet viser ${propCount} regeringsforslag, ${betCount} udvalgsrapport${betCount !== 1 ? 'er' : ''} og ${motCount} oppositionsforslag.` : 'Analysen giver et øjebliksbillede af den aktuelle politiske retning.'}`,
1146
- no: `Basert på analyse av ${docs.length} dokument${docs.length !== 1 ? 'er' : ''} (${enrichedCount} beriket med fulltekst)${topic ? ` spesifikt om <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `Lovgivningsaktiviteten viser ${propCount} regjeringsforslag, ${betCount} komitérapport${betCount !== 1 ? 'er' : ''} og ${motCount} opposisjonsforslag.` : 'Analysen gir et øyeblikksbilde av den aktuelle politiske retningen.'}`,
1147
- fi: `Perustuen ${docs.length} asiakirjan analyysiin (${enrichedCount} rikastettu koko tekstillä)${topic ? ` koskien erityisesti <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `Lainsäädäntötoiminta osoittaa ${propCount} hallituksen esitystä, ${betCount} valiokunnan mietintöä ja ${motCount} oppositioaloitetta.` : 'Analyysi tarjoaa tilannekuvan nykyisestä poliittisesta suunnasta.'}`,
1148
- de: `Basierend auf der Analyse von ${docs.length} Dokument${docs.length !== 1 ? 'en' : ''} (${enrichedCount} mit vollständigem Text angereichert)${topic ? ` speziell zu <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `Der Gesetzgebungsprozess zeigt ${propCount} Regierungsvorlage${propCount !== 1 ? 'n' : ''}, ${betCount} Ausschussbericht${betCount !== 1 ? 'e' : ''} und ${motCount} Oppositionsantrag${motCount !== 1 ? 'e' : ''}.` : 'Die Analyse bietet eine Momentaufnahme der aktuellen politischen Richtung.'}`,
1149
- fr: `Basé sur l'analyse de ${docs.length} document${docs.length !== 1 ? 's' : ''} (${enrichedCount} enrichi${enrichedCount !== 1 ? 's' : ''} avec le texte complet)${topic ? ` abordant spécifiquement <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `Le pipeline législatif montre ${propCount} proposition${propCount !== 1 ? 's' : ''} gouvernementale${propCount !== 1 ? 's' : ''}, ${betCount} rapport${betCount !== 1 ? 's' : ''} de commission et ${motCount} motion${motCount !== 1 ? 's' : ''} d'opposition.` : 'L\'analyse offre un aperçu de l\'orientation politique actuelle.'}`,
1150
- es: `Basado en el análisis de ${docs.length} documento${docs.length !== 1 ? 's' : ''} (${enrichedCount} enriquecido${enrichedCount !== 1 ? 's' : ''} con texto completo)${topic ? ` que abordan específicamente <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `La actividad legislativa muestra ${propCount} proposición${propCount !== 1 ? 'es' : ''} gubernamental${propCount !== 1 ? 'es' : ''}, ${betCount} informe${betCount !== 1 ? 's' : ''} de comité y ${motCount} moción${motCount !== 1 ? 'es' : ''} de oposición.` : 'El análisis proporciona una instantánea de la dirección política actual.'}`,
1151
- nl: `Gebaseerd op analyse van ${docs.length} document${docs.length !== 1 ? 'en' : ''} (${enrichedCount} verrijkt met volledige tekst)${topic ? ` specifiek over <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `De wetgevende activiteit toont ${propCount} regeringsvoorstel${propCount !== 1 ? 'len' : ''}, ${betCount} commissierapport${betCount !== 1 ? 'en' : ''} en ${motCount} oppositiemotie${motCount !== 1 ? 's' : ''}.` : 'De analyse biedt een momentopname van de huidige politieke richting.'}`,
1152
- ar: `استناداً إلى تحليل ${docs.length} وثيقة (${enrichedCount} مُعزَّزة بالنص الكامل)${topic ? ` تتناول تحديداً <strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `يُظهر النشاط التشريعي ${propCount} مقترحاً حكومياً و${betCount} تقرير${betCount !== 1 ? 'اً' : ''} للجنة و${motCount} اقتراح${motCount !== 1 ? 'اً' : ''} معارضاً.` : 'يوفر التحليل لقطة للاتجاه السياسي الحالي.'}`,
1153
- he: `בהתבסס על ניתוח ${docs.length} מסמכ${docs.length !== 1 ? 'ים' : ''} (${enrichedCount} עם טקסט מלא)${topic ? ` המתמקדים ב<strong>${esc(topic)}</strong>` : ''}: ${isLegislativeFocused ? `הפעילות החקיקתית מראה ${propCount} הצעת חוק ממשלתית, ${betCount} דוח ועדה ו-${motCount} הצעת אופוזיציה.` : 'הניתוח מספק תמונת מצב של הכיוון הפוליטי הנוכחי.'}`,
1154
- ja: `${docs.length}件の文書分析(${enrichedCount}件は全文で強化)${topic ? `、<strong>${esc(topic)}</strong>に特化` : ''}に基づく: ${isLegislativeFocused ? `立法活動は${propCount}件の政府提案、${betCount}件の委員会報告、${motCount}件の野党動議を示しています。` : '分析は現在の政策方向のスナップショットを提供しています。'}`,
1155
- ko: `${docs.length}개 문서 분석(${enrichedCount}개 전문 보강)${topic ? `, <strong>${esc(topic)}</strong>에 집중` : ''}에 기반: ${isLegislativeFocused ? `입법 활동은 ${propCount}개 정부 제안, ${betCount}개 위원회 보고서, ${motCount}개 야당 발의안을 보여줍니다.` : '분석은 현재 정책 방향의 스냅샷을 제공합니다.'}`,
1156
- zh: `基于对${docs.length}份文件(${enrichedCount}份含全文)的分析${topic ? `,专注于<strong>${esc(topic)}</strong>` : ''}:${isLegislativeFocused ? `立法活动显示${propCount}项政府提案、${betCount}份委员会报告和${motCount}项反对党动议。` : '该分析提供了当前政策方向的快照。'}`,
1157
- };
1158
- const text = templates[lang] ?? templates.en ?? '';
1159
- return `<p>${text}</p>`;
1160
- }
1161
- /** Build a bulleted key-takeaways list derived from document patterns and topic. */
1162
- function buildKeyTakeaways(docs, topic, lang) {
1163
- const esc = escapeHtml;
1164
- const items = [];
1165
- // Derive takeaways from document patterns
1166
- const propDocs = docs.filter(d => effectiveType(d) === 'prop');
1167
- const betDocs = docs.filter(d => effectiveType(d) === 'bet');
1168
- const motDocs = docs.filter(d => effectiveType(d) === 'mot');
1169
- const euDocs = docs.filter(d => effectiveType(d) === 'fpm' || effectiveType(d) === 'eu');
1170
- const sfsDocs = docs.filter(d => effectiveType(d) === 'sfs');
1171
- const pressmDocs = docs.filter(d => effectiveType(d) === 'pressm');
1172
- const topicPhrase = topic ? ` (${esc(topic)})` : '';
1173
- if (propDocs.length > 0) {
1174
- const titles = propDocs.slice(0, 2).map(d => esc(d.titel || d.title || d.dok_id || '')).join('; ');
1175
- items.push(lang === 'sv'
1176
- ? `<strong>${propDocs.length} proposition${propDocs.length !== 1 ? 'er' : ''}</strong>${topicPhrase} aktiv lagstiftning: ${titles}`
1177
- : `<strong>${propDocs.length} government proposition${propDocs.length !== 1 ? 's' : ''}</strong>${topicPhrase} in active legislation: ${titles}`);
1178
- }
1179
- if (sfsDocs.length > 0) {
1180
- items.push(lang === 'sv'
1181
- ? `<strong>${sfsDocs.length} antagen lag/förordning</strong>${topicPhrase} — rättsligt ramverk etablerat`
1182
- : `<strong>${sfsDocs.length} enacted law/statute</strong>${topicPhrase} — legal framework established`);
1183
- }
1184
- if (betDocs.length > 0) {
1185
- items.push(lang === 'sv'
1186
- ? `<strong>${betDocs.length} utskottsbetänkande${betDocs.length !== 1 ? 'n' : ''}</strong> visar parlamentarisk granskning av${topicPhrase}`
1187
- : `<strong>${betDocs.length} committee report${betDocs.length !== 1 ? 's' : ''}</strong> demonstrate parliamentary scrutiny of${topicPhrase}`);
1188
- }
1189
- if (motDocs.length > 0) {
1190
- items.push(lang === 'sv'
1191
- ? `<strong>${motDocs.length} oppositionsmotion${motDocs.length !== 1 ? 'er' : ''}</strong> utmanar${topicPhrase} inriktning`
1192
- : `<strong>${motDocs.length} opposition motion${motDocs.length !== 1 ? 's' : ''}</strong> challenge${motDocs.length === 1 ? 's' : ''} the${topicPhrase} direction`);
1193
- }
1194
- if (euDocs.length > 0) {
1195
- items.push(lang === 'sv'
1196
- ? `<strong>${euDocs.length} EU-faktapromemoria</strong> visar europeisk dimension av${topicPhrase}`
1197
- : `<strong>${euDocs.length} EU position paper${euDocs.length !== 1 ? 's' : ''}</strong> reveal the European dimension of${topicPhrase}`);
1198
- }
1199
- // Press release / government communication insights
1200
- if (pressmDocs.length > 0) {
1201
- items.push(lang === 'sv'
1202
- ? `<strong>${pressmDocs.length} pressmeddelande${pressmDocs.length !== 1 ? 'n' : ''} från regeringen</strong> signalerar kommande policyåtgärder${topicPhrase}`
1203
- : `<strong>${pressmDocs.length} government press release${pressmDocs.length !== 1 ? 's' : ''}</strong> signal${pressmDocs.length === 1 ? 's' : ''} upcoming policy action${topicPhrase}`);
1204
- }
1205
- // Content-derived insight: detected policy domains
1206
- const allDomains = new Set();
1207
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDomains.add(dom)));
1208
- if (allDomains.size > 0) {
1209
- const domainList = [...allDomains].slice(0, 4).map(d => esc(d)).join(', ');
1210
- items.push(lang === 'sv'
1211
- ? `<strong>Identifierade policyområden:</strong> ${domainList}`
1212
- : `<strong>Policy domains identified:</strong> ${domainList}`);
1213
- }
1214
- const enriched = docs.filter(d => d.contentFetched).length;
1215
- if (enriched > 0) {
1216
- items.push(lang === 'sv'
1217
- ? `<strong>${enriched} av ${docs.length} dokument</strong> ${enriched === 1 ? 'berikat' : 'berikade'} med fulltext för djupanalys`
1218
- : `<strong>${enriched} of ${docs.length} document${docs.length !== 1 ? 's' : ''}</strong> enriched with full text for deep analysis`);
1219
- }
1220
- if (items.length === 0) {
1221
- items.push(lang === 'sv'
1222
- ? `Djupanalys genomförd av ${docs.length} dokument${topicPhrase}`
1223
- : `Deep analysis conducted on ${docs.length} document${docs.length !== 1 ? 's' : ''}${topicPhrase}`);
1224
- }
1225
- return `<ul class="key-takeaways-list">\n${items.map(i => ` <li>${i}</li>`).join('\n')}\n</ul>\n`;
1226
- }
1227
- // ---------------------------------------------------------------------------
1228
- // Multi-iteration deep-inspection intelligence section builders
1229
- // ---------------------------------------------------------------------------
1230
- /**
1231
- * Build a concise Executive Intelligence Summary.
1232
- * Synthesises document composition, policy domains, and legislative posture
1233
- * into a briefing paragraph for decision-makers.
1234
- * Iteration 1 + Iteration 2 outcome: "what happened & why it matters".
1235
- */
1236
- function buildExecutiveSummary(docs, topic, lang) {
1237
- const esc = escapeHtml;
1238
- const propCount = docs.filter(d => effectiveType(d) === 'prop').length;
1239
- const betCount = docs.filter(d => effectiveType(d) === 'bet').length;
1240
- const motCount = docs.filter(d => effectiveType(d) === 'mot').length;
1241
- const sfsDocs = docs.filter(isSfsDoc);
1242
- const enriched = docs.filter(d => d.contentFetched).length;
1243
- const allDomains = new Set();
1244
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDomains.add(dom)));
1245
- const domainList = [...allDomains].slice(0, 4);
1246
- const domainPhrase = domainList.map(d => esc(d)).join(', ');
1247
- // Determine legislative posture — neutral when no props/motions/bets/SFS exist
1248
- const hasEnactedLaw = sfsDocs.length > 0;
1249
- const noLegSignal = propCount + motCount + betCount === 0 && !hasEnactedLaw;
1250
- const govLed = noLegSignal ? null : propCount > motCount;
1251
- const highScrutiny = betCount > 0;
1252
- const templates = {
1253
- en: (() => {
1254
- const enPosture = govLed === null ? 'non-legislative' : govLed ? 'government-led' : 'opposition-driven';
1255
- const enClauses = [];
1256
- if (propCount > 0)
1257
- enClauses.push(`${propCount} proposition${propCount !== 1 ? 's' : ''} advancing the executive agenda`);
1258
- if (betCount > 0)
1259
- enClauses.push(`${betCount} committee report${betCount !== 1 ? 's' : ''} providing parliamentary scrutiny`);
1260
- if (motCount > 0)
1261
- enClauses.push(`${motCount} opposition motion${motCount !== 1 ? 's' : ''} challenging the direction`);
1262
- const enClauseStr = enClauses.length > 0
1263
- ? `, with ${enClauses.length === 1 ? enClauses[0] : enClauses.slice(0, -1).join(', ') + ', and ' + enClauses[enClauses.length - 1]}`
1264
- : '';
1265
- return `This deep-inspection intelligence report analyses ${docs.length} parliamentary document${docs.length !== 1 ? 's' : ''}${topic ? ` on <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? `, spanning ${domainPhrase}` : ''}. Of these, ${enriched} ${enriched === 1 ? 'was' : 'were'} enriched with full text to enable substantive analysis. The legislative posture is ${enPosture}${enClauseStr}. ${hasEnactedLaw ? `${sfsDocs.length} statute${sfsDocs.length !== 1 ? 's' : ''} ${sfsDocs.length !== 1 ? 'have' : 'has'} already been enacted, establishing a legal baseline.` : highScrutiny ? 'Committee engagement indicates that the policy is under active parliamentary review, signalling that key decisions are imminent.' : 'The legislative pipeline remains at an early stage, requiring close monitoring for acceleration signals.'} ${domainPhrase ? `Policy domains engaged — ${domainPhrase} — reflect the cross-cutting nature of this initiative.` : 'The documents reflect focused policy engagement in this area.'} Decision-makers should prioritise tracking committee deliberations and chamber voting patterns as the most reliable forward indicators.`;
1266
- })(),
1267
- sv: (() => {
1268
- const svClauses = [];
1269
- if (propCount > 0)
1270
- svClauses.push(`${propCount} proposition${propCount !== 1 ? 'er' : ''}`);
1271
- if (betCount > 0)
1272
- svClauses.push(`${betCount} utskottsbetänkande${betCount !== 1 ? 'n' : ''} som ger parlamentarisk granskning`);
1273
- if (motCount > 0)
1274
- svClauses.push(`${motCount} opposition${motCount !== 1 ? 'smotioner' : 'smotion'} som ifrågasätter inriktningen`);
1275
- const svPosture = govLed === null ? 'icke-lagstiftningsmässigt' : govLed ? 'regeringsdrivet' : 'oppositionsdrivet';
1276
- const svClauseStr = svClauses.length > 0
1277
- ? (svClauses.length > 1
1278
- ? ' med ' + svClauses.slice(0, -1).join(', ') + ' och ' + svClauses[svClauses.length - 1]
1279
- : ' med ' + svClauses[0])
1280
- : '';
1281
- return `Denna djupanalys granskar ${docs.length} riksdagsdokument${topic ? ` rörande <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` inom ${domainPhrase}` : ''}. Av dessa berikades ${enriched} med fulltext. Det lagstiftande läget är ${svPosture}${svClauseStr}. ${hasEnactedLaw ? `${sfsDocs.length} lag${sfsDocs.length !== 1 ? 'ar' : ''} har redan antagits och fastställt ett rättsligt ramverk.` : highScrutiny ? 'Utskottsengagemanget visar att policyn är under aktiv parlamentarisk granskning.' : 'Lagstiftningspipelinen befinner sig i ett tidigt skede.'} Beslutsfattare bör prioritera att följa utskottens arbete och omröstningar i kammaren.`;
1282
- })(),
1283
- da: (() => {
1284
- const daClauses = [];
1285
- if (propCount > 0)
1286
- daClauses.push(`${propCount} forslag`);
1287
- if (betCount > 0)
1288
- daClauses.push(`${betCount} udvalgsrapport${betCount !== 1 ? 'er' : ''}`);
1289
- const daClauseStr = daClauses.length > 0 ? ` med ${daClauses.join(' og ')}` : '';
1290
- return `Denne dybdeanalyse undersøger ${docs.length} parlamentariske dokumenter${topic ? ` om <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` inden for ${domainPhrase}` : ''}. ${enriched} af disse er beriget med fulde tekster. Den lovgivningsmæssige holdning er ${govLed === null ? 'ikke-lovgivningsmæssig' : govLed ? 'regeringsdrevet' : 'oppositionsdrevet'}${daClauseStr}. Beslutningstagere bør følge udvalgsdrøftelser og afstemninger.`;
1291
- })(),
1292
- no: (() => {
1293
- const noClauses = [];
1294
- if (propCount > 0)
1295
- noClauses.push(`${propCount} forslag`);
1296
- if (betCount > 0)
1297
- noClauses.push(`${betCount} komitérapport${betCount !== 1 ? 'er' : ''}`);
1298
- const noClauseStr = noClauses.length > 0 ? ` med ${noClauses.join(' og ')}` : '';
1299
- return `Denne dybdeanalysen undersøker ${docs.length} parlamentariske dokumenter${topic ? ` om <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` innen ${domainPhrase}` : ''}. ${enriched} av disse er beriket med fulltekst. Den lovgivningsmessige posisjonen er ${govLed === null ? 'ikke-lovgivningsmessig' : govLed ? 'regjeringsledet' : 'opposisjonsdrevet'}${noClauseStr}. Beslutningstakere bør følge komitéforhandlinger og voteringsmønstre.`;
1300
- })(),
1301
- fi: (() => {
1302
- const fiClauses = [];
1303
- if (propCount > 0)
1304
- fiClauses.push(`${propCount} esitystä`);
1305
- if (betCount > 0)
1306
- fiClauses.push(`${betCount} valiokunnan mietintöä`);
1307
- const fiClauseStr = fiClauses.length > 0 ? ` — ${fiClauses.join(' ja ')}` : '';
1308
- return `Tämä syväanalyysi tutkii ${docs.length} parlamentaarista asiakirjaa${topic ? ` aiheesta <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` alueilla ${domainPhrase}` : ''}. Näistä ${enriched} rikastettiin koko tekstillä. Lainsäädäntöasenne on ${govLed === null ? 'ei-lainsäädännöllinen' : govLed ? 'hallitusvetoinen' : 'oppositiovetoinen'}${fiClauseStr}. Päätöksentekijöiden tulisi seurata valiokuntien harkintaa ja äänestyksiä.`;
1309
- })(),
1310
- de: `Dieser Tiefenanalysebericht untersucht ${docs.length} Parlamentsdokument${docs.length !== 1 ? 'e' : ''}${topic ? ` zu <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` in den Bereichen ${domainPhrase}` : ''}. Davon wurden ${enriched} mit vollständigem Text angereichert. Die gesetzgeberische Haltung ist ${govLed === null ? 'nicht-gesetzgeberisch' : govLed ? 'regierungsgeführt' : 'oppositionsgetrieben'}${propCount > 0 ? ` mit ${propCount} Regierungsvorlage${propCount !== 1 ? 'n' : ''}` : ''}${betCount > 0 ? `${propCount > 0 ? ' und' : ' mit'} ${betCount} Ausschussbericht${betCount !== 1 ? 'en' : ''}` : ''}. Entscheidungsträger sollten Ausschussberatungen und Abstimmungsmuster verfolgen.`,
1311
- fr: `Ce rapport d'analyse approfondie examine ${docs.length} document${docs.length !== 1 ? 's' : ''} parlementaire${docs.length !== 1 ? 's' : ''}${topic ? ` sur <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? `, couvrant ${domainPhrase}` : ''}. Parmi ceux-ci, ${enriched} ont été enrichis avec le texte complet. La posture législative est ${govLed === null ? 'non législative' : govLed ? 'gouvernementale' : "portée par l'opposition"}${propCount > 0 ? ` avec ${propCount} proposition${propCount !== 1 ? 's' : ''}` : ''}${betCount > 0 ? `${propCount > 0 ? ' et' : ' avec'} ${betCount} rapport${betCount !== 1 ? 's' : ''} de commission` : ''}. Les décideurs devraient suivre les délibérations des commissions et les votes.`,
1312
- es: `Este informe de análisis profundo examina ${docs.length} documento${docs.length !== 1 ? 's' : ''} parlamentario${docs.length !== 1 ? 's' : ''}${topic ? ` sobre <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? `, abarcando ${domainPhrase}` : ''}. De estos, ${enriched} fueron enriquecidos con texto completo. La postura legislativa es ${govLed === null ? 'no legislativa' : govLed ? 'liderada por el gobierno' : 'impulsada por la oposición'}${propCount > 0 ? ` con ${propCount} proposición${propCount !== 1 ? 'es' : ''}` : ''}${betCount > 0 ? `${propCount > 0 ? ' y' : ' con'} ${betCount} informe${betCount !== 1 ? 's' : ''} de comité` : ''}. Los tomadores de decisiones deben seguir las deliberaciones del comité y los patrones de votación.`,
1313
- nl: `Dit diepgaand analyserapport onderzoekt ${docs.length} parlementair${docs.length !== 1 ? 'e' : ''} document${docs.length !== 1 ? 'en' : ''}${topic ? ` over <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? `, gericht op ${domainPhrase}` : ''}. Hiervan werden ${enriched} verrijkt met volledige tekst. De wetgevende houding is ${govLed === null ? 'niet-wetgevend' : govLed ? 'regeringsgeleid' : 'oppositiegedreven'}${propCount > 0 ? ` met ${propCount} voorstel${propCount !== 1 ? 'len' : ''}` : ''}${betCount > 0 ? `${propCount > 0 ? ' en' : ' met'} ${betCount} commissierapport${betCount !== 1 ? 'en' : ''}` : ''}. Beslissers moeten commissiedeliberaties en stempatronen volgen.`,
1314
- ar: `يحلل تقرير التحليل المعمق هذا ${docs.length} وثيقة برلمانية${topic ? ` حول <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` في مجالات ${domainPhrase}` : ''}. منها ${enriched} مُعزَّزة بالنص الكامل. الموقف التشريعي ${govLed === null ? 'غير تشريعي' : govLed ? 'حكومي القيادة' : 'تقوده المعارضة'}${propCount > 0 ? ` مع ${propCount} مقترح${propCount !== 1 ? 'ات' : ''}` : ''}${betCount > 0 ? ` و${betCount} تقرير${betCount !== 1 ? 'ات' : ''} لجنة` : ''}. يجب على صانعي القرار متابعة مداولات اللجان وأنماط التصويت.`,
1315
- he: `דוח הניתוח המעמיק הזה בוחן ${docs.length} מסמך${docs.length !== 1 ? 'ים' : ''} פרלמנטר${docs.length !== 1 ? 'יים' : 'י'}${topic ? ` בנושא <strong>${esc(topic)}</strong>` : ''}${domainPhrase ? ` בתחומי ${domainPhrase}` : ''}. מתוכם ${enriched} הועשרו בטקסט מלא. העמדה החקיקתית ${govLed === null ? 'לא-חקיקתית' : govLed ? 'בהנהגת הממשלה' : 'בהנהגת האופוזיציה'}${propCount > 0 ? ` עם ${propCount} הצעת חוק` : ''}${betCount > 0 ? ` ו-${betCount} דוח ועדה` : ''}. מקבלי ההחלטות צריכים לעקוב אחר דיוני הוועדות ודפוסי ההצבעה.`,
1316
- ja: (() => {
1317
- const jaClauses = [];
1318
- if (propCount > 0)
1319
- jaClauses.push(`${propCount}件の提案`);
1320
- if (betCount > 0)
1321
- jaClauses.push(`${betCount}件の委員会報告`);
1322
- const jaClauseStr = jaClauses.length > 0 ? `で、${jaClauses.join('と')}があります` : 'です';
1323
- return `この詳細分析レポートは${docs.length}件の議会文書${topic ? `(<strong>${esc(topic)}</strong>に関する)` : ''}${domainPhrase ? `(${domainPhrase}分野)` : ''}を分析します。${enriched}件は全文で強化されています。立法スタンスは${govLed === null ? '非立法的' : govLed ? '政府主導' : '野党主導'}${jaClauseStr}。意思決定者は委員会審議と投票パターンを追跡する必要があります。`;
1324
- })(),
1325
- ko: (() => {
1326
- const koClauses = [];
1327
- if (propCount > 0)
1328
- koClauses.push(`${propCount}개 제안`);
1329
- if (betCount > 0)
1330
- koClauses.push(`${betCount}개 위원회 보고서`);
1331
- const koClauseStr = koClauses.length > 0 ? `이며, ${koClauses.join('과 ')}가 있습니다` : '입니다';
1332
- return `이 심층 분석 보고서는 ${docs.length}개의 의회 문서${topic ? `(<strong>${esc(topic)}</strong> 관련)` : ''}${domainPhrase ? `(${domainPhrase} 분야)` : ''}를 분석합니다. 이 중 ${enriched}개는 전문으로 보강되었습니다. 입법 태도는 ${govLed === null ? '비입법적' : govLed ? '정부 주도' : '야당 주도'}${koClauseStr}. 의사결정자는 위원회 심의와 투표 패턴을 추적해야 합니다.`;
1333
- })(),
1334
- zh: `本深度分析报告分析了${docs.length}份议会文件${topic ? `(关于<strong>${esc(topic)}</strong>)` : ''}${domainPhrase ? `(涵盖${domainPhrase})` : ''}。其中${enriched}份以全文强化。立法立场${govLed === null ? '为非立法性' : govLed ? '由政府主导' : '由反对党推动'}${propCount > 0 ? `,有${propCount}份提案` : ''}${betCount > 0 ? `${propCount > 0 ? '和' : ',有'}${betCount}份委员会报告` : ''}。决策者应追踪委员会审议和投票模式。`,
1335
- };
1336
- const heading = deepLabel('executiveSummary', lang);
1337
- const text = templates[lang] ?? templates.en ?? '';
1338
- return `\n<section class="executive-intelligence-summary" aria-label="${esc(heading)}">\n <h2>${esc(heading)}</h2>\n <p>${text}</p>\n</section>\n`;
1339
- }
1340
- /** Enrichment ratio threshold for HIGH confidence. */
1341
- const CONFIDENCE_HIGH_THRESHOLD = 0.7;
1342
- /** Enrichment ratio threshold for MEDIUM confidence. */
1343
- const CONFIDENCE_MEDIUM_THRESHOLD = 0.3;
1344
- /** Minimum document count for HIGH confidence. */
1345
- const CONFIDENCE_MIN_DOCS_HIGH = 3;
1346
- /**
1347
- * Derive a tri-state confidence level for the overall analysis based on
1348
- * document enrichment rate, document count, and SFS presence.
1349
- *
1350
- * @returns `'HIGH'` | `'MEDIUM'` | `'LOW'`
1351
- */
1352
- function deriveConfidence(docs) {
1353
- if (docs.length === 0)
1354
- return 'LOW';
1355
- const enriched = docs.filter(d => d.contentFetched).length;
1356
- const ratio = docs.length > 0 ? enriched / docs.length : 0;
1357
- const hasSfs = docs.some(isSfsDoc);
1358
- if (ratio >= CONFIDENCE_HIGH_THRESHOLD && docs.length >= CONFIDENCE_MIN_DOCS_HIGH)
1359
- return 'HIGH';
1360
- if (ratio >= CONFIDENCE_MEDIUM_THRESHOLD || hasSfs)
1361
- return 'MEDIUM';
1362
- return 'LOW';
1363
- }
1364
- /**
1365
- * Build a Predictive Assessment section with confidence percentages.
1366
- * Covers: likely legislative outcomes, coalition stability forecast, and
1367
- * risk scenarios (best / worst / most-likely).
1368
- * Iteration 3 output: "what happens next".
1369
- */
1370
- /** Base passage probability when legislative environment is favourable. */
1371
- const BASE_PASSAGE_PROBABILITY = 50;
1372
- /** Maximum passage probability cap for any single analysis. */
1373
- const MAX_PASSAGE_PROBABILITY = 90;
1374
- /** Minimum passage probability floor (avoids 0%). */
1375
- const MIN_PASSAGE_PROBABILITY = 20;
1376
- /** Confidence points added per committee report (bet) — signals parliamentary alignment. */
1377
- const COMMITTEE_REPORT_WEIGHT = 8;
1378
- /** Confidence points added per enacted statute (sfs) — confirms legal framework exists. */
1379
- const ENACTED_STATUTE_WEIGHT = 15;
1380
- /** Confidence points deducted per opposition motion (mot) — signals resistance. */
1381
- const OPPOSITION_MOTION_PENALTY = 5;
1382
- function buildPredictiveAssessment(docs, topic, lang) {
1383
- const esc = escapeHtml;
1384
- const propCount = docs.filter(d => effectiveType(d) === 'prop').length;
1385
- const betCount = docs.filter(d => effectiveType(d) === 'bet').length;
1386
- const motCount = docs.filter(d => effectiveType(d) === 'mot').length;
1387
- const sfsDocs = docs.filter(isSfsDoc);
1388
- const confidence = deriveConfidence(docs);
1389
- // Passage likelihood heuristic: if committee reports exceed motions → likely passage
1390
- const passageLikely = betCount > motCount || sfsDocs.length > 0;
1391
- const passagePct = passageLikely
1392
- ? Math.min(MAX_PASSAGE_PROBABILITY, BASE_PASSAGE_PROBABILITY + betCount * COMMITTEE_REPORT_WEIGHT + sfsDocs.length * ENACTED_STATUTE_WEIGHT)
1393
- : Math.max(MIN_PASSAGE_PROBABILITY, BASE_PASSAGE_PROBABILITY - motCount * OPPOSITION_MOTION_PENALTY);
1394
- const blockPct = 100 - passagePct;
1395
- const topicFallback = {
1396
- en: 'this area', sv: 'detta område', da: 'dette område', no: 'dette området',
1397
- fi: 'tämä alue', de: 'diesem Bereich', fr: 'ce domaine', es: 'esta área',
1398
- nl: 'dit gebied', ar: 'هذا المجال', he: 'תחום זה',
1399
- ja: 'この分野', ko: '이 분야', zh: '该领域',
1400
- };
1401
- const topicStr = topic ? esc(topic) : (topicFallback[lang] ?? topicFallback.en);
1402
- const headingPredictive = deepLabel('predictiveAssessment', lang);
1403
- const headingOutcome = deepLabel('likelyOutcome', lang);
1404
- const headingCoalition = deepLabel('coalitionStability', lang);
1405
- const headingRisk = deepLabel('riskScenarios', lang);
1406
- const sections = {
1407
- en: {
1408
- outcome: `Based on document composition analysis, the probability of legislative passage for <strong>${topicStr}</strong> is estimated at <strong>${passagePct}%</strong>, with a ${blockPct}% probability of delay or amendment. ${propCount > 0 ? `${propCount} active proposition${propCount !== 1 ? 's' : ''} indicate committed government intent.` : ''} ${betCount > 0 ? `${betCount} committee report${betCount !== 1 ? 's' : ''} confirm parliamentary engagement.` : ''} ${sfsDocs.length > 0 ? 'Enacted statutes confirm legal framework establishment.' : ''}`,
1409
- coalition: `Coalition stability assessment: ${betCount > motCount ? 'High — committee activity suggests governing coalition alignment.' : motCount > betCount ? 'Moderate — active opposition motions signal coalition stress points.' : 'Moderate — balanced legislative activity indicates ongoing negotiation.'} Monitor subsequent committee votes as the primary coalition stability indicator. Overall analysis confidence: <strong>${confidence}</strong>.`,
1410
- scenarios: `<ul class="risk-scenarios"><li><strong>Best case (${passagePct}% probability):</strong> ${topicStr} legislation passes with cross-party support, entering implementation phase.</li><li><strong>Most likely case:</strong> ${betCount > 0 ? 'Committee scrutiny leads to amendments before final vote, delaying implementation by 3–6 months.' : 'Legislation proceeds through normal parliamentary cycle with minor modifications.'}</li><li><strong>Worst case (${blockPct}% probability):</strong> ${motCount > propCount ? 'Opposition motions gain traction, forcing significant policy revisions or deferral to next session.' : 'External developments or coalition disagreements cause unexpected delay or withdrawal.'}</li></ul>`,
1411
- },
1412
- sv: {
1413
- outcome: `Baserat på dokumentsammansättningsanalys uppskattas sannolikheten för lagstiftningspassage för <strong>${topicStr}</strong> till <strong>${passagePct}%</strong>, med ${blockPct}% sannolikhet för fördröjning eller ändring. ${propCount > 0 ? `${propCount} aktiv${propCount !== 1 ? 'a' : ''} proposition${propCount !== 1 ? 'er' : ''} visar regeringens engagemang.` : ''} Analyskonfidens: <strong>${confidence}</strong>.`,
1414
- coalition: `Koalitionsstabilitetsbedömning: ${betCount > motCount ? 'Hög — utskottsaktivitet tyder på koalitionsanpassning.' : motCount > betCount ? 'Måttlig — aktiva oppositionsmotioner signalerar stressmoment.' : 'Måttlig — balanserad aktivitet indikerar pågående förhandlingar.'}`,
1415
- scenarios: `<ul class="risk-scenarios"><li><strong>Bästa scenariot (${passagePct}% sannolikhet):</strong> Lagstiftning antas med bred parlamentarisk konsensus.</li><li><strong>Troligaste scenariot:</strong> Utskottsgranskning leder till ändringar innan slutomröstning, med 3–6 månaders försenad implementering.</li><li><strong>Sämsta scenariot (${blockPct}% sannolikhet):</strong> ${motCount > propCount ? 'Oppositionsinitiativ tvingar till väsentliga policyrevisioner.' : 'Externa omständigheter orsakar oväntad försening.'}</li></ul>`,
1416
- },
1417
- de: {
1418
- outcome: `Basierend auf der Dokumentzusammensetzung wird die Wahrscheinlichkeit einer gesetzlichen Verabschiedung für <strong>${topicStr}</strong> auf <strong>${passagePct}%</strong> geschätzt. Analysekonfidens: <strong>${confidence}</strong>.`,
1419
- coalition: `Koalitionsstabilitätsbewertung: ${betCount > motCount ? 'Hoch — Ausschussaktivität deutet auf Koalitionsausrichtung hin.' : 'Mittel — laufende Verhandlungen erforderlich.'}`,
1420
- scenarios: `<ul class="risk-scenarios"><li><strong>Bestes Szenario (${passagePct}%):</strong> Gesetze werden mit breitem Konsens verabschiedet.</li><li><strong>Wahrscheinlichstes Szenario:</strong> Ausschussprüfung führt zu Änderungen vor der Endabstimmung.</li><li><strong>Schlimmstes Szenario (${blockPct}%):</strong> Unerwartete Verzögerungen aufgrund externer Faktoren.</li></ul>`,
1421
- },
1422
- fr: {
1423
- outcome: `Sur la base de l'analyse de la composition des documents, la probabilité de passage législatif pour <strong>${topicStr}</strong> est estimée à <strong>${passagePct}%</strong>. Confiance d'analyse : <strong>${confidence}</strong>.`,
1424
- coalition: `Évaluation de la stabilité de coalition : ${betCount > motCount ? 'Élevée — l\'activité des commissions suggère un alignement de la coalition.' : 'Modérée — négociations en cours nécessaires.'}`,
1425
- scenarios: `<ul class="risk-scenarios"><li><strong>Meilleur cas (${passagePct}%) :</strong> La législation est adoptée avec un large consensus.</li><li><strong>Cas le plus probable :</strong> L'examen en commission entraîne des amendements avant le vote final.</li><li><strong>Pire cas (${blockPct}%) :</strong> Des retards inattendus dus à des facteurs externes.</li></ul>`,
1426
- },
1427
- es: {
1428
- outcome: `Con base en el análisis de composición de documentos, la probabilidad de aprobación legislativa para <strong>${topicStr}</strong> se estima en <strong>${passagePct}%</strong>. Confianza del análisis: <strong>${confidence}</strong>.`,
1429
- coalition: `Evaluación de estabilidad de coalición: ${betCount > motCount ? 'Alta — la actividad del comité sugiere alineación de la coalición.' : 'Moderada — se requieren negociaciones en curso.'}`,
1430
- scenarios: `<ul class="risk-scenarios"><li><strong>Mejor caso (${passagePct}%):</strong> La legislación se aprueba con amplio consenso.</li><li><strong>Caso más probable:</strong> El escrutinio del comité lleva a enmiendas antes de la votación final.</li><li><strong>Peor caso (${blockPct}%):</strong> Retrasos inesperados debidos a factores externos.</li></ul>`,
1431
- },
1432
- da: {
1433
- outcome: `Baseret på dokumentsammensætningsanalyse anslås sandsynligheden for lovgivningsmæssig vedtagelse for <strong>${topicStr}</strong> til <strong>${passagePct}%</strong>. Analysekonfidensgrad: <strong>${confidence}</strong>.`,
1434
- coalition: `Koalitionsstabilitetsvurdering: ${betCount > motCount ? 'Høj — udvalgsaktivitet tyder på koalitionssammensætning.' : 'Moderat — igangværende forhandlinger nødvendige.'}`,
1435
- scenarios: `<ul class="risk-scenarios"><li><strong>Bedste tilfælde (${passagePct}%):</strong> Lovgivning vedtages med bred konsensus.</li><li><strong>Sandsynligste tilfælde:</strong> Udvalgsgennemgang fører til ændringer.</li><li><strong>Værste tilfælde (${blockPct}%):</strong> Uventede forsinkelser.</li></ul>`,
1436
- },
1437
- no: {
1438
- outcome: `Basert på dokumentsammensetningsanalyse anslås sannsynligheten for lovgivningsmessig vedtak for <strong>${topicStr}</strong> til <strong>${passagePct}%</strong>. Analysekonfidens: <strong>${confidence}</strong>.`,
1439
- coalition: `Koalisjonstabilitetsvurdering: ${betCount > motCount ? 'Høy — komitéaktivitet tyder på koalisjonssamstemmighet.' : 'Moderat — pågående forhandlinger nødvendig.'}`,
1440
- scenarios: `<ul class="risk-scenarios"><li><strong>Beste tilfelle (${passagePct}%):</strong> Lovgivning vedtas med bred konsensus.</li><li><strong>Mest sannsynlig:</strong> Komitégjennomgang fører til endringer.</li><li><strong>Verste tilfelle (${blockPct}%):</strong> Uventede forsinkelser.</li></ul>`,
1441
- },
1442
- fi: {
1443
- outcome: `Asiakirjakoostumuksen analyysin perusteella lainsäädännön läpimenon todennäköisyys aiheessa <strong>${topicStr}</strong> arvioidaan <strong>${passagePct}%</strong>:ksi. Analyysin luottamustaso: <strong>${confidence}</strong>.`,
1444
- coalition: `Koalition vakausarvio: ${betCount > motCount ? 'Korkea — valiokuntien aktiivisuus viittaa koalition yhdenmukaisuuteen.' : 'Kohtalainen — käynnissä olevia neuvotteluja tarvitaan.'}`,
1445
- scenarios: `<ul class="risk-scenarios"><li><strong>Paras tapaus (${passagePct}%):</strong> Lainsäädäntö hyväksytään laajalla konsensuksella.</li><li><strong>Todennäköisin:</strong> Valiokuntatarkastus johtaa muutoksiin.</li><li><strong>Pahin tapaus (${blockPct}%):</strong> Odottamattomia viivästyksiä.</li></ul>`,
1446
- },
1447
- nl: {
1448
- outcome: `Op basis van documentsamenstelling wordt de kans op wetgevende doorgang voor <strong>${topicStr}</strong> geschat op <strong>${passagePct}%</strong>. Analysebetrouwbaarheid: <strong>${confidence}</strong>.`,
1449
- coalition: `Coalitiesstabiliteitsbeoordeling: ${betCount > motCount ? 'Hoog — commissieactiviteit suggereert coalitie-afstemming.' : 'Matig — lopende onderhandelingen vereist.'}`,
1450
- scenarios: `<ul class="risk-scenarios"><li><strong>Beste geval (${passagePct}%):</strong> Wetgeving aangenomen met brede consensus.</li><li><strong>Meest waarschijnlijk:</strong> Commissieonderzoek leidt tot wijzigingen.</li><li><strong>Slechtste geval (${blockPct}%):</strong> Onverwachte vertragingen.</li></ul>`,
1451
- },
1452
- ar: {
1453
- outcome: `استناداً إلى تحليل تكوين الوثائق، تُقدَّر احتمالية المرور التشريعي لـ<strong>${topicStr}</strong> بـ<strong>${passagePct}%</strong>. ثقة التحليل: <strong>${confidence}</strong>.`,
1454
- coalition: `تقييم استقرار الائتلاف: ${betCount > motCount ? 'مرتفع — نشاط اللجان يشير إلى توافق الائتلاف.' : 'متوسط — مفاوضات جارية مطلوبة.'}`,
1455
- scenarios: `<ul class="risk-scenarios"><li><strong>أفضل الأحوال (${passagePct}%):</strong> تُقرّ التشريعات بتوافق واسع.</li><li><strong>الحالة الأكثر احتمالاً:</strong> تؤدي مراجعة اللجان إلى تعديلات.</li><li><strong>أسوأ الأحوال (${blockPct}%):</strong> تأخيرات غير متوقعة.</li></ul>`,
1456
- },
1457
- he: {
1458
- outcome: `בהתבסס על ניתוח הרכב מסמכים, הסבירות למעבר חקיקתי עבור <strong>${topicStr}</strong> מוערכת ב-<strong>${passagePct}%</strong>. רמת ביטחון הניתוח: <strong>${confidence}</strong>.`,
1459
- coalition: `הערכת יציבות קואליציה: ${betCount > motCount ? 'גבוהה — פעילות ועדות מצביעה על יישור הקואליציה.' : 'בינונית — נדרשים משא ומתן מתמשך.'}`,
1460
- scenarios: `<ul class="risk-scenarios"><li><strong>התרחיש הטוב ביותר (${passagePct}%):</strong> חקיקה עוברת עם הסכמה רחבה.</li><li><strong>התרחיש הסביר ביותר:</strong> בדיקת ועדה מובילה לתיקונים.</li><li><strong>התרחיש הגרוע ביותר (${blockPct}%):</strong> עיכובים בלתי צפויים.</li></ul>`,
1461
- },
1462
- ja: {
1463
- outcome: `文書構成分析に基づき、<strong>${topicStr}</strong>の立法可決確率は<strong>${passagePct}%</strong>と推定されます。分析信頼度:<strong>${confidence}</strong>。`,
1464
- coalition: `連立安定性評価:${betCount > motCount ? '高 — 委員会活動は連立整合を示唆。' : '中 — 継続的な交渉が必要。'}`,
1465
- scenarios: `<ul class="risk-scenarios"><li><strong>最良シナリオ(${passagePct}%):</strong>広範な合意で法案可決。</li><li><strong>最有力シナリオ:</strong>委員会審査による修正後に最終投票。</li><li><strong>最悪シナリオ(${blockPct}%):</strong>予期せぬ遅延が発生。</li></ul>`,
1466
- },
1467
- ko: {
1468
- outcome: `문서 구성 분석에 기반하여, <strong>${topicStr}</strong>의 입법 통과 확률은 <strong>${passagePct}%</strong>로 추정됩니다. 분석 신뢰도: <strong>${confidence}</strong>.`,
1469
- coalition: `연립 안정성 평가: ${betCount > motCount ? '높음 — 위원회 활동이 연립 조정을 시사.' : '보통 — 지속적인 협상 필요.'}`,
1470
- scenarios: `<ul class="risk-scenarios"><li><strong>최선의 경우 (${passagePct}%):</strong> 광범위한 합의로 법안 통과.</li><li><strong>가장 유력한 경우:</strong> 위원회 심사로 인한 수정 후 최종 투표.</li><li><strong>최악의 경우 (${blockPct}%):</strong> 예상치 못한 지연.</li></ul>`,
1471
- },
1472
- zh: {
1473
- outcome: `基于文件构成分析,<strong>${topicStr}</strong>的立法通过概率估计为<strong>${passagePct}%</strong>。分析置信度:<strong>${confidence}</strong>。`,
1474
- coalition: `联合稳定性评估:${betCount > motCount ? '高 — 委员会活动表明联合一致性。' : '中等 — 需要持续谈判。'}`,
1475
- scenarios: `<ul class="risk-scenarios"><li><strong>最佳情景(${passagePct}%):</strong>立法以广泛共识通过。</li><li><strong>最可能情景:</strong>委员会审查导致最终投票前进行修订。</li><li><strong>最坏情景(${blockPct}%):</strong>出现意外延误。</li></ul>`,
1476
- },
1477
- };
1478
- const s = sections[lang] ?? sections.en;
1479
- return [
1480
- `\n<section class="predictive-assessment" aria-label="${esc(headingPredictive)}">`,
1481
- ` <h2>${esc(headingPredictive)}</h2>`,
1482
- ` <h3>${esc(headingOutcome)}</h3>`,
1483
- ` <p>${s.outcome}</p>`,
1484
- ` <h3>${esc(headingCoalition)}</h3>`,
1485
- ` <p>${s.coalition}</p>`,
1486
- ` <h3>${esc(headingRisk)}</h3>`,
1487
- ` ${s.scenarios}`,
1488
- `</section>`,
1489
- ].join('\n') + '\n';
1490
- }
1491
- /**
1492
- * Build a Historical Context & Precedents section.
1493
- * Provides trend analysis, Nordic/EU benchmarking context, and precedent
1494
- * references based on document types and detected policy domains.
1495
- * Iteration 2 + Iteration 3 output: "why it matters historically".
1496
- */
1497
- function buildHistoricalContext(docs, topic, lang) {
1498
- const esc = escapeHtml;
1499
- const sfsDocs = docs.filter(isSfsDoc);
1500
- const propCount = docs.filter(d => effectiveType(d) === 'prop').length;
1501
- const allDomains = new Set();
1502
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDomains.add(dom)));
1503
- const domainList = [...allDomains].slice(0, 3).map(d => esc(d));
1504
- const hasEnacted = sfsDocs.length > 0;
1505
- const topicStr = topic ? esc(topic) : null;
1506
- const heading = deepLabel('historicalContext', lang);
1507
- const templates = {
1508
- en: `${topicStr ? `<strong>${topicStr}</strong> sits within` : 'This policy sits within'} a long tradition of Swedish parliamentary reform. ${hasEnacted ? `The presence of ${sfsDocs.length} enacted statute${sfsDocs.length !== 1 ? 's' : ''} indicates this area has established legal precedent.` : propCount > 0 ? `Active propositions suggest this policy cycle mirrors earlier reform waves, where government-initiated legislation progressed through committee scrutiny to enactment within 12–24 months.` : 'Early-stage documents suggest this represents a new policy initiative without direct statutory precedent.'} ${domainList.length > 0 ? `In the Nordic context, ${domainList.join(', ')} policy areas have historically benefited from cross-party consensus, with Sweden typically aligning with Danish and Norwegian approaches before adopting EU framework requirements.` : ''} International benchmarking indicates that comparable democracies — particularly Denmark, Norway, and Finland — have addressed similar policy challenges through incremental legislative packages rather than sweeping reform. Trend analysis across recent parliamentary sessions suggests that ${topicStr ? `${topicStr} legislation` : 'policy in this area'} is accelerating, driven by EU harmonisation requirements and coalition agreement commitments.`,
1509
- sv: `${topicStr ? `<strong>${topicStr}</strong> ingår i` : 'Denna policy ingår i'} en lång tradition av svensk parlamentarisk reform. ${hasEnacted ? `Förekomsten av ${sfsDocs.length} antagen lag/förordning visar att området har etablerat rättslig praxis.` : propCount > 0 ? 'Aktiva propositioner tyder på att denna policycykel speglar tidigare reformvågor.' : 'Tidiga dokument tyder på ett nytt policyinitiativ utan direkt lagstadgat prejudikat.'} ${domainList.length > 0 ? `I nordisk kontext har ${domainList.join(', ')} historiskt gynnats av partikonsensus, med Sverige som vanligtvis anpassar sig till danska och norska tillvägagångssätt.` : ''} Trendanalys indikerar att ${topicStr ? `${topicStr}-lagstiftning` : 'politiken på detta område'} accelererar, driven av EU-harmoniseringskrav och koalitionsöverenskommelser.`,
1510
- da: `${topicStr ? `<strong>${topicStr}</strong> er del af` : 'Denne politik er del af'} en lang tradition for svensk Riksdagsreform. ${domainList.length > 0 ? `I nordisk kontekst har ${domainList.join(', ')} historisk nydt gavn af tværpolitisk konsensus.` : ''} Trendanalyse viser, at politikken på dette område accelererer.`,
1511
- no: `${topicStr ? `<strong>${topicStr}</strong> er en del av` : 'Denne politikken er en del av'} en lang tradisjon for svensk riksdagsreform. ${domainList.length > 0 ? `I nordisk kontekst har ${domainList.join(', ')} historisk nytt godt av tverrpolitisk konsensus.` : ''} Trendanalyse indikerer at politikk på dette området akselererer.`,
1512
- fi: `${topicStr ? `<strong>${topicStr}</strong> on osa` : 'Tämä politiikka on osa'} pitkää Ruotsin valtiopäivien uudistusperinnettä. ${domainList.length > 0 ? `Pohjoismaisessa kontekstissa ${domainList.join(', ')} aloilla on historiallisesti hyöty puolueiden välisestä yhteisymmärryksestä.` : ''} Trendanalyysi osoittaa, että tämän alan politiikka kiihtyy.`,
1513
- de: `${topicStr ? `<strong>${topicStr}</strong> steht in` : 'Diese Politik steht in'} einer langen Tradition schwedischer parlamentarischer Reform. ${hasEnacted ? `Das Vorhandensein von ${sfsDocs.length} verabschiedeten Statuten zeigt, dass in diesem Bereich rechtliche Präzedenzfälle etabliert sind.` : ''} ${domainList.length > 0 ? `Im nordischen Kontext haben ${domainList.join(', ')}-Politikbereiche historisch von einem parteiübergreifenden Konsens profitiert.` : ''} Die Trendanalyse zeigt, dass sich ${topicStr ? `${topicStr}-Gesetzgebung` : 'die Politik in diesem Bereich'} beschleunigt.`,
1514
- fr: `${topicStr ? `<strong>${topicStr}</strong> s\u2019inscrit dans` : "Cette politique s\u2019inscrit dans"} une longue tradition de réforme parlementaire suédoise. ${hasEnacted ? `La présence de ${sfsDocs.length} statuts adoptés indique que ce domaine a établi des précédents juridiques.` : ''} ${domainList.length > 0 ? `Dans le contexte nordique, les domaines ${domainList.join(', ')} ont historiquement bénéficié d\u2019un consensus multipartite.` : ''} L\u2019analyse de tendances indique que ${topicStr ? `la législation sur ${topicStr}` : 'la politique dans ce domaine'} s\u2019accélère.`,
1515
- es: `${topicStr ? `<strong>${topicStr}</strong> se inscribe en` : 'Esta política se inscribe en'} una larga tradición de reforma parlamentaria sueca. ${hasEnacted ? `La presencia de ${sfsDocs.length} estatutos promulgados indica que esta área ha establecido precedentes legales.` : ''} ${domainList.length > 0 ? `En el contexto nórdico, las áreas de política ${domainList.join(', ')} históricamente se han beneficiado del consenso multipartidista.` : ''} El análisis de tendencias indica que ${topicStr ? `la legislación sobre ${topicStr}` : 'la política en esta área'} se está acelerando.`,
1516
- nl: `${topicStr ? `<strong>${topicStr}</strong> maakt deel uit van` : 'Dit beleid maakt deel uit van'} een lange traditie van Zweedse parlementaire hervorming. ${hasEnacted ? `De aanwezigheid van ${sfsDocs.length} ingevoerde wetgeving geeft aan dat er juridische precedenten zijn vastgesteld.` : ''} ${domainList.length > 0 ? `In de Noordse context hebben beleidsterreinen ${domainList.join(', ')} historisch geprofiteerd van partijoverstijgende consensus.` : ''} Trendanalyse geeft aan dat beleid op dit gebied versnelt.`,
1517
- ar: `${topicStr ? `<strong>${topicStr}</strong> يقع ضمن` : 'تقع هذه السياسة ضمن'} تقليد طويل من الإصلاح البرلماني السويدي. ${hasEnacted ? `وجود ${sfsDocs.length} قانون${sfsDocs.length !== 1 ? 'ين' : ''} مُعتمد يشير إلى وجود سوابق قانونية راسخة.` : ''} ${domainList.length > 0 ? `في السياق الإسكندنافي، استفادت مجالات ${domainList.join('، ')} تاريخياً من توافق متعدد الأحزاب.` : ''} يشير تحليل الاتجاهات إلى تسارع السياسات في هذا المجال.`,
1518
- he: `${topicStr ? `<strong>${topicStr}</strong> ממוקם ב` : 'מדיניות זו ממוקמת ב'}מסורת ארוכה של רפורמה פרלמנטרית שוודית. ${hasEnacted ? `נוכחות ${sfsDocs.length} חוקים שאושרו מצביעה על כך שנקבעו תקדימים משפטיים.` : ''} ${domainList.length > 0 ? `בהקשר הנורדי, תחומי ${domainList.join(', ')} נהנו היסטורית מקונצנזוס בין-מפלגתי.` : ''} ניתוח מגמות מצביע על האצת מדיניות בתחום זה.`,
1519
- ja: `${topicStr ? `<strong>${topicStr}</strong>は` : 'この政策は'}スウェーデン議会改革の長い伝統の中に位置します。${hasEnacted ? `${sfsDocs.length}件の制定された法律の存在は、この分野に法的先例があることを示しています。` : ''}${domainList.length > 0 ? `北欧の文脈では、${domainList.join('、')}分野は歴史的に超党派の合意から恩恵を受けてきました。` : ''}トレンド分析は、この分野の政策が加速していることを示しています。`,
1520
- ko: `${topicStr ? `<strong>${topicStr}</strong>는` : '이 정책은'} 스웨덴 의회 개혁의 오랜 전통 속에 있습니다. ${hasEnacted ? `${sfsDocs.length}개의 제정된 법률의 존재는 이 분야에 법적 선례가 있음을 나타냅니다.` : ''}${domainList.length > 0 ? `북유럽 맥락에서 ${domainList.join(', ')} 정책 영역은 역사적으로 초당적 합의에서 혜택을 받았습니다.` : ''} 추세 분석은 이 분야의 정책이 가속화되고 있음을 시사합니다.`,
1521
- zh: `${topicStr ? `<strong>${topicStr}</strong>处于` : '这一政策处于'}瑞典议会改革的悠久传统之中。${hasEnacted ? `${sfsDocs.length}项已颁布法规的存在表明该领域已建立法律先例。` : ''}${domainList.length > 0 ? `在北欧背景下,${domainList.join('、')}政策领域历史上受益于跨党派共识。` : ''}趋势分析表明该领域的政策正在加速。`,
1522
- };
1523
- const text = templates[lang] ?? templates.en ?? '';
1524
- return `\n<section class="historical-context" aria-label="${esc(heading)}">\n <h2>${esc(heading)}</h2>\n <p>${text}</p>\n</section>\n`;
1525
- }
1526
- /**
1527
- * Build a Methodology & Confidence section.
1528
- * Documents data sources, analysis methods, confidence scores, and known
1529
- * limitations — providing epistemic transparency for the intelligence report.
1530
- * Iteration 4 output: "is the analysis sound".
1531
- */
1532
- function buildMethodologySection(docs, topic, lang, depth) {
1533
- const clampedDepth = Math.max(1, Math.min(4, Math.round(depth)));
1534
- const esc = escapeHtml;
1535
- const enriched = docs.filter(d => d.contentFetched).length;
1536
- const confidence = deriveConfidence(docs);
1537
- const heading = deepLabel('methodology', lang);
1538
- const topicStr = topic ? esc(topic) : null;
1539
- const iterationLabels = {
1540
- en: ['Surface analysis (events and actors identified)', 'Deep analysis (motivations and strategic implications)', 'Predictive analysis (outcome forecasting and risk scenarios)', 'Quality review (bias check and completeness verification)'],
1541
- sv: ['Ytanalys (händelser och aktörer identifierade)', 'Djupanalys (motivationer och strategiska implikationer)', 'Prediktiv analys (prognoser och riskscenarier)', 'Kvalitetsgranskning (biaskontroll och fullständighetsverifiering)'],
1542
- da: ['Overfladeanalyse (hændelser og aktører identificeret)', 'Dybdeanalyse (motivationer og strategiske implikationer)', 'Prædiktiv analyse (prognoser og risikoscenarier)', 'Kvalitetsgennemgang (bias-tjek og fuldstændighedsverificering)'],
1543
- no: ['Overflateanalyse (hendelser og aktører identifisert)', 'Dybdeanalyse (motivasjoner og strategiske implikasjoner)', 'Prediktiv analyse (prognoser og risikoscenarier)', 'Kvalitetsgjennomgang (bias-sjekk og fullstendighetsverifisering)'],
1544
- fi: ['Pintaanalyysi (tapahtumat ja toimijat tunnistettu)', 'Syväanalyysi (motiivit ja strategiset vaikutukset)', 'Ennakoiva analyysi (ennusteet ja riskiskenaariot)', 'Laaduntarkistus (vinoutumien tarkistus ja kattavuuden varmennus)'],
1545
- de: ['Oberflächenanalyse (Ereignisse und Akteure identifiziert)', 'Tiefenanalyse (Motivationen und strategische Implikationen)', 'Prädiktive Analyse (Prognosen und Risikoszenarien)', 'Qualitätsprüfung (Bias-Prüfung und Vollständigkeitsverifikation)'],
1546
- fr: ['Analyse de surface (événements et acteurs identifiés)', 'Analyse approfondie (motivations et implications stratégiques)', 'Analyse prédictive (prévisions et scénarios de risque)', 'Revue qualité (vérification des biais et de l\'exhaustivité)'],
1547
- es: ['Análisis superficial (eventos y actores identificados)', 'Análisis profundo (motivaciones e implicaciones estratégicas)', 'Análisis predictivo (pronósticos y escenarios de riesgo)', 'Revisión de calidad (verificación de sesgos y exhaustividad)'],
1548
- nl: ['Oppervlakteanalyse (gebeurtenissen en actoren geïdentificeerd)', 'Diepteanalyse (motivaties en strategische implicaties)', 'Voorspellende analyse (prognoses en risicoscenario\'s)', 'Kwaliteitsreview (bias-controle en volledigheidsverificatie)'],
1549
- ar: ['تحليل سطحي (تحديد الأحداث والجهات الفاعلة)', 'تحليل معمق (الدوافع والتداعيات الاستراتيجية)', 'تحليل تنبؤي (توقعات وسيناريوهات المخاطر)', 'مراجعة الجودة (التحقق من التحيز والاكتمال)'],
1550
- he: ['ניתוח שטחי (זיהוי אירועים ושחקנים)', 'ניתוח עמוק (מניעים והשלכות אסטרטגיות)', 'ניתוח חיזויי (תחזיות ותרחישי סיכון)', 'ביקורת איכות (בדיקת הטיה ואימות שלמות)'],
1551
- ja: ['表面分析(出来事と関係者の特定)', '詳細分析(動機と戦略的示唆)', '予測分析(結果予測とリスクシナリオ)', '品質レビュー(バイアスチェックと網羅性検証)'],
1552
- ko: ['표면 분석 (사건 및 행위자 식별)', '심층 분석 (동기 및 전략적 시사점)', '예측 분석 (결과 예측 및 위험 시나리오)', '품질 검토 (편향 확인 및 완전성 검증)'],
1553
- zh: ['表面分析(事件和行为者识别)', '深度分析(动机和战略影响)', '预测分析(结果预测和风险情景)', '质量审查(偏差检查和完整性验证)'],
1554
- };
1555
- const labels = iterationLabels[lang] ?? iterationLabels.en;
1556
- const iterationItems = labels.slice(0, clampedDepth).map((label) => `<li>${esc(label)}</li>`).join('\n ');
1557
- const sourceLabels = {
1558
- en: 'Data Sources', sv: 'Datakällor', da: 'Datakilder', no: 'Datakilder',
1559
- fi: 'Tietolähteet', de: 'Datenquellen', fr: 'Sources de données', es: 'Fuentes de datos',
1560
- nl: 'Gegevensbronnen', ar: 'مصادر البيانات', he: 'מקורות נתונים',
1561
- ja: 'データソース', ko: '데이터 출처', zh: '数据来源',
1562
- };
1563
- const sourceDesc = {
1564
- en: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v proxy), and supplementary external sources (GitHub raw content, public government URLs) when available',
1565
- sv: 'Riksdagens MCP-API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v-proxy) samt kompletterande externa källor (GitHub-råinnehåll, offentliga myndighets-URL:er) vid tillgänglighet',
1566
- da: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v proxy) samt supplerende eksterne kilder ved tilgængelighed',
1567
- no: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v proxy) samt supplerende eksterne kilder ved tilgjengelighet',
1568
- fi: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v-välityspalvelin) sekä täydentävät ulkoiset lähteet saatavuuden mukaan',
1569
- de: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v-Proxy) sowie ergänzende externe Quellen bei Verfügbarkeit',
1570
- fr: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (proxy g0v) et sources externes complémentaires selon disponibilité',
1571
- es: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (proxy g0v) y fuentes externas complementarias según disponibilidad',
1572
- nl: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v proxy) en aanvullende externe bronnen indien beschikbaar',
1573
- ar: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall)، regeringen.se (وكيل g0v)، ومصادر خارجية تكميلية عند التوفر',
1574
- he: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (פרוקסי g0v), ומקורות חיצוניים משלימים בהתאם לזמינות',
1575
- ja: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall)、regeringen.se (g0v プロキシ)、および利用可能な場合は補足的な外部ソース',
1576
- ko: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall), regeringen.se (g0v 프록시) 및 이용 가능한 경우 보충 외부 소스',
1577
- zh: 'Riksdag MCP API (search_dokument, get_dokument, get_dokument_innehall)、regeringen.se (g0v代理) 以及可用时的补充外部来源',
1578
- };
1579
- const iterLabel = {
1580
- en: 'Analysis iterations completed', sv: 'Genomförda analysiterationer', da: 'Gennemførte analyseiterationer',
1581
- no: 'Gjennomførte analyseiterationer', fi: 'Suoritetut analyysikierrokset', de: 'Abgeschlossene Analyseiterationen',
1582
- fr: 'Itérations d\'analyse terminées', es: 'Iteraciones de análisis completadas', nl: 'Voltooide analyseiteraties',
1583
- ar: 'تكرارات التحليل المكتملة', he: 'איטרציות ניתוח שהושלמו', ja: '完了した分析反復', ko: '완료된 분석 반복', zh: '已完成的分析迭代',
1584
- };
1585
- const confLabel = {
1586
- en: 'Overall confidence score', sv: 'Övergripande konfidenspoäng', da: 'Samlet konfidensscore',
1587
- no: 'Samlet konfidensskår', fi: 'Kokonaisluottamuspistemäärä', de: 'Gesamtkonfidenzwert',
1588
- fr: 'Score de confiance global', es: 'Puntuación de confianza general', nl: 'Algehele betrouwbaarheidsscore',
1589
- ar: 'درجة الثقة الكلية', he: 'ציון ביטחון כולל', ja: '全体的な信頼スコア', ko: '전체 신뢰도 점수', zh: '整体置信度分数',
1590
- };
1591
- const enrichLabel = {
1592
- en: 'Documents enriched with full text', sv: 'Dokument berikade med fulltext', da: 'Dokumenter beriget med fulde tekster',
1593
- no: 'Dokumenter beriket med fulltekst', fi: 'Asiakirjat rikastettu koko tekstillä', de: 'Dokumente mit vollständigem Text angereichert',
1594
- fr: 'Documents enrichis avec le texte complet', es: 'Documentos enriquecidos con texto completo', nl: 'Documenten verrijkt met volledige tekst',
1595
- ar: 'وثائق معززة بالنص الكامل', he: 'מסמכים מועשרים בטקסט מלא', ja: '全文で強化された文書', ko: '전문으로 보강된 문서', zh: '以全文强化的文件',
1596
- };
1597
- const limitLabel = {
1598
- en: 'Known limitations', sv: 'Kända begränsningar', da: 'Kendte begrænsninger', no: 'Kjente begrensninger',
1599
- fi: 'Tunnetut rajoitukset', de: 'Bekannte Einschränkungen', fr: 'Limitations connues', es: 'Limitaciones conocidas',
1600
- nl: 'Bekende beperkingen', ar: 'القيود المعروفة', he: 'מגבלות ידועות', ja: '既知の制限事項', ko: '알려진 제한사항', zh: '已知限制',
1601
- };
1602
- const limitText = {
1603
- en: `Analysis based on publicly available parliamentary data only. ${enriched < docs.length ? `${docs.length - enriched} document${docs.length - enriched !== 1 ? 's' : ''} analysed without full text due to availability constraints.` : 'All documents enriched with full text.'} ${topicStr ? `Topic focus limited to: ${topicStr}.` : ''} Predictive assessments use heuristic models and should be treated as indicative, not definitive.`,
1604
- sv: `Analys baserad enbart på offentligt tillgängliga parlamentariska data. ${enriched < docs.length ? `${docs.length - enriched} dokument analyserade utan fulltext.` : 'Alla dokument berikade med fulltext.'} Prediktiva bedömningar är heuristiska och ska behandlas som vägledande.`,
1605
- da: `Analyse baseret på offentligt tilgængelige parlamentariske data. Prædiktive vurderinger er vejledende.`,
1606
- no: `Analyse basert på offentlig tilgjengelige parlamentariske data. Prediktive vurderinger er heuristiske.`,
1607
- fi: `Analyysi perustuu vain julkisesti saatavilla oleviin parlamentaarisiin tietoihin. Ennustavat arviot ovat heuristisia.`,
1608
- de: `Analyse basiert ausschließlich auf öffentlich zugänglichen parlamentarischen Daten. Prädiktive Bewertungen sind heuristisch.`,
1609
- fr: `Analyse basée uniquement sur des données parlementaires accessibles au public. Les évaluations prédictives sont heuristiques.`,
1610
- es: `Análisis basado únicamente en datos parlamentarios disponibles públicamente. Las evaluaciones predictivas son heurísticas.`,
1611
- nl: `Analyse gebaseerd op alleen publiek beschikbare parlementaire gegevens. Voorspellende beoordelingen zijn heuristisch.`,
1612
- ar: `التحليل مستند إلى البيانات البرلمانية المتاحة للعموم فقط. التقييمات التنبؤية هيوريستية.`,
1613
- he: `ניתוח מבוסס על נתונים פרלמנטריים זמינים לציבור בלבד. הערכות חיזויות הן היוריסטיות.`,
1614
- ja: `分析は公開されている議会データのみに基づいています。予測評価はヒューリスティックなものです。`,
1615
- ko: `분석은 공개적으로 이용 가능한 의회 데이터만을 기반으로 합니다. 예측 평가는 경험적입니다.`,
1616
- zh: `分析仅基于公开可用的议会数据。预测评估是启发式的。`,
1617
- };
1618
- return [
1619
- `\n<section class="methodology-confidence" aria-label="${esc(heading)}">`,
1620
- ` <h2>${esc(heading)}</h2>`,
1621
- ` <dl class="methodology-details">`,
1622
- ` <dt>${esc(sourceLabels[lang] ?? sourceLabels.en)}</dt>`,
1623
- ` <dd>${sourceDesc[lang] ?? sourceDesc.en}</dd>`,
1624
- ` <dt>${esc(iterLabel[lang] ?? iterLabel.en)}</dt>`,
1625
- ` <dd><ol class="iteration-list">\n ${iterationItems}\n </ol></dd>`,
1626
- ` <dt>${esc(confLabel[lang] ?? confLabel.en)}</dt>`,
1627
- ` <dd><strong>${confidence}</strong></dd>`,
1628
- ` <dt>${esc(enrichLabel[lang] ?? enrichLabel.en)}</dt>`,
1629
- ` <dd>${enriched} / ${docs.length}</dd>`,
1630
- ` <dt>${esc(limitLabel[lang] ?? limitLabel.en)}</dt>`,
1631
- ` <dd>${limitText[lang] ?? limitText.en}</dd>`,
1632
- ` </dl>`,
1633
- `</section>`,
1634
- ].join('\n') + '\n';
1635
- }
1636
- // ---------------------------------------------------------------------------
1637
- // Deep-Inspection TemplateSection builders (SWOT + Dashboard)
1638
- // ---------------------------------------------------------------------------
1639
- /**
1640
- * Compute the effective document type for a RawDocument.
1641
- * SFS-by-name docs (missing doktyp/documentType but dokumentnamn starting with "SFS")
1642
- * are normalised to 'sfs' so filters, typeCounts, and chart labels stay consistent.
1643
- */
1644
- function effectiveType(d) {
1645
- return (d.doktyp || d.documentType)
1646
- || ((d.dokumentnamn || '').startsWith('SFS') ? 'sfs' : 'other');
1647
- }
1648
- /**
1649
- * Build SWOT and dashboard TemplateSections for a deep-inspection article.
1650
- * Uses buildAISwotStakeholders() to derive 6 stakeholder perspectives
1651
- * from document metadata (types, titles, document IDs as evidence).
1652
- * Returns TemplateSection[] ready for generateArticleHTML.sections.
1653
- */
1654
- function buildDeepInspectionSections(docs, topic, lang, aiResult) {
1655
- if (docs.length === 0)
1656
- return [];
1657
- // Single-pass classification: bucket docs by effectiveType() to avoid N×filter passes.
1658
- // EU docs use both 'fpm' and 'eu' raw types; effectiveType() preserves the raw value,
1659
- // so we merge both into the euDocs bucket below.
1660
- const buckets = new Map();
1661
- for (const d of docs) {
1662
- const t = effectiveType(d);
1663
- let arr = buckets.get(t);
1664
- if (!arr) {
1665
- arr = [];
1666
- buckets.set(t, arr);
1667
- }
1668
- arr.push(d);
1669
- }
1670
- const propDocs = buckets.get('prop') ?? [];
1671
- const betDocs = buckets.get('bet') ?? [];
1672
- const motDocs = buckets.get('mot') ?? [];
1673
- const skrDocs = buckets.get('skr') ?? [];
1674
- const sfsDocs = buckets.get('sfs') ?? [];
1675
- const euDocs = [...(buckets.get('fpm') ?? []), ...(buckets.get('eu') ?? [])];
1676
- const pressmDocs = buckets.get('pressm') ?? [];
1677
- const extDocs = buckets.get('ext') ?? [];
1678
- // classifiedTypes must mirror every bucket key consumed above (including both EU keys)
1679
- const classifiedTypes = new Set(['prop', 'bet', 'mot', 'skr', 'sfs', 'fpm', 'eu', 'pressm', 'ext']);
1680
- const otherDocs = [...buckets.entries()]
1681
- .filter(([k]) => !classifiedTypes.has(k))
1682
- .flatMap(([, v]) => v);
1683
- // ── AI-driven 6-stakeholder SWOT ─────────────────────────────────────────
1684
- const stakeholders = buildAISwotStakeholders(docs, topic ?? '', lang);
1685
- const strategicContext = topic
1686
- ? `Analysis exclusively focused on: ${topic} — ${docs.length} parliamentary documents examined`
1687
- : `Multi-stakeholder analysis of ${docs.length} parliamentary documents`;
1688
- const swotSection = generateStakeholderSwotSection({ stakeholders, lang, strategicContext });
1689
- // ── Localised names for mindmap/sankey labels (single source from ai-swot-analyzer)
1690
- const govName = AI_STAKEHOLDER_NAMES['government-coalition'][lang] ?? AI_STAKEHOLDER_NAMES['government-coalition'].en;
1691
- const oppName = AI_STAKEHOLDER_NAMES['opposition'][lang] ?? AI_STAKEHOLDER_NAMES['opposition'].en;
1692
- const privateName = AI_STAKEHOLDER_NAMES['private-sector'][lang] ?? AI_STAKEHOLDER_NAMES['private-sector'].en;
1693
- // ── AI-analyzed multi-chart dashboard ─────────────────────────────────────
1694
- // Produces 3 chart types (radar, scatter, bar) with accessible data tables.
1695
- const dashboardAnalysis = analyzeDashboardData(docs, topic ?? '', lang);
1696
- // Also build the classic document-type distribution bar chart as chart #4
1697
- // so existing article consumers still see document counts.
1698
- const typeCounts = {};
1699
- docs.forEach(d => {
1700
- const t = effectiveType(d);
1701
- typeCounts[t] = (typeCounts[t] || 0) + 1;
1702
- });
1703
- const rawTypeKeys = Object.keys(typeCounts);
1704
- // Use localized display names for chart labels (e.g., "Press Release" not "pressm")
1705
- const chartLabels = rawTypeKeys.map(t => docTypeLabel(t, lang, typeCounts[t]));
1706
- const chartValues = rawTypeKeys.map(t => typeCounts[t]);
1707
- const docTypeChart = {
1708
- id: 'deep-inspection-doc-types',
1709
- type: 'bar',
1710
- title: deepLabel('documentsByType', lang),
1711
- labels: chartLabels,
1712
- datasets: [{
1713
- label: deepLabel('documents', lang),
1714
- data: chartValues,
1715
- backgroundColor: rawTypeKeys.map((_, i) => DEEP_CHART_PALETTE[i % DEEP_CHART_PALETTE.length]),
1716
- }],
1717
- };
1718
- const docTypeTable = {
1719
- caption: deepLabel('documentsByType', lang),
1720
- headers: [deepLabel('documentTypes', lang), deepLabel('documents', lang)],
1721
- rows: rawTypeKeys.map((t, i) => [docTypeLabel(t, lang, chartValues[i]), String(chartValues[i])]),
1722
- };
1723
- const dashboardSection = generateDashboardSection({
1724
- data: {
1725
- title: topic
1726
- ? `${deepLabel('documentIntelligence', lang)} — ${topic}`
1727
- : deepLabel('documentIntelligence', lang),
1728
- summary: dashboardAnalysis.summary,
1729
- charts: [...dashboardAnalysis.charts, docTypeChart],
1730
- tables: [...dashboardAnalysis.tables, docTypeTable],
1731
- },
1732
- lang,
1733
- });
1734
- // ── Mindmap: AI-driven conceptual map across 5 political dimensions ─────────
1735
- const allDetectedDomains = new Set();
1736
- docs.forEach(d => detectPolicyDomains(d, lang).forEach(dom => allDetectedDomains.add(dom)));
1737
- // Augment with AI-detected domains when available.
1738
- // emergingTrends format: "domain1, domain2, domain3 [CONFIDENCE]" — single suffix on whole list.
1739
- if (aiResult?.synthesis?.emergingTrends) {
1740
- aiResult.synthesis.emergingTrends
1741
- .split(',')
1742
- .map(s => s.split('[')[0]?.trim() ?? '')
1743
- .filter(Boolean)
1744
- .forEach(dom => allDetectedDomains.add(dom));
1745
- }
1746
- const detectedDomainList = [...allDetectedDomains].filter(Boolean).slice(0, 8);
1747
- // Pass precomputed domains to avoid iterating docs twice
1748
- const aiAnalysis = buildAIMindmapAnalysis(docs, topic, lang, detectedDomainList);
1749
- const mindmapSection = generateMindmapSection(buildMindmapOptionsFromAnalysis(aiAnalysis, lang, topic || deepLabel('parliamentaryAnalysis', lang), {
1750
- summary: topic
1751
- ? `${deepLabel('conceptualMap', lang)}: ${topic}`
1752
- : `${deepLabel('conceptualMap', lang)} — ${docs.length} ${deepLabel('documents', lang).toLowerCase()}`,
1753
- }));
1754
- // ── Sankey: party/doc-type flow → legislative outcome ─────────────────────
1755
- // The sankey uses three primary legislative actor groups as source nodes:
1756
- // - government: initiates propositions, laws, gov. communications, press releases,
1757
- // and EU position papers (fpm) — these originate from government ministries
1758
- // - opposition: initiates committee reports and motions
1759
- // - private sector / external actors: associated with external references
1760
- // and other document types
1761
- // Additional SWOT stakeholders (civil society, citizens, etc.) are
1762
- // analysis perspectives rather than document-originating actors.
1763
- const sankeyNodes = [
1764
- { id: 'gov', label: govName, color: 'cyan' },
1765
- { id: 'opp', label: oppName, color: 'magenta' },
1766
- { id: 'pvt', label: privateName, color: 'purple' },
1767
- ];
1768
- // Add document type nodes and target outcome nodes
1769
- const sankeyFlows = [];
1770
- if (propDocs.length > 0) {
1771
- sankeyNodes.push({ id: 'prop', label: 'Propositions', color: 'orange' });
1772
- sankeyFlows.push({ source: 'gov', target: 'prop', value: propDocs.length, label: `${propDocs.length}` });
1773
- }
1774
- if (betDocs.length > 0) {
1775
- sankeyNodes.push({ id: 'bet', label: 'Committee Reports', color: 'blue' });
1776
- sankeyFlows.push({ source: 'opp', target: 'bet', value: betDocs.length, label: `${betDocs.length}` });
1777
- }
1778
- if (motDocs.length > 0) {
1779
- sankeyNodes.push({ id: 'mot', label: 'Motions', color: 'yellow' });
1780
- sankeyFlows.push({ source: 'opp', target: 'mot', value: motDocs.length, label: `${motDocs.length}` });
1781
- }
1782
- if (sfsDocs.length > 0) {
1783
- sankeyNodes.push({ id: 'sfs', label: 'Laws (SFS)', color: 'green' });
1784
- sankeyFlows.push({ source: 'gov', target: 'sfs', value: sfsDocs.length, label: `${sfsDocs.length}` });
1785
- }
1786
- if (skrDocs.length > 0) {
1787
- sankeyNodes.push({ id: 'skr', label: deepLabel('govCommunications', lang), color: 'green' });
1788
- sankeyFlows.push({ source: 'gov', target: 'skr', value: skrDocs.length, label: `${skrDocs.length}` });
1789
- }
1790
- if (euDocs.length > 0) {
1791
- sankeyNodes.push({ id: 'eu', label: 'EU Positions', color: 'blue' });
1792
- sankeyFlows.push({ source: 'gov', target: 'eu', value: euDocs.length, label: `${euDocs.length}` });
1793
- }
1794
- if (pressmDocs.length > 0) {
1795
- sankeyNodes.push({ id: 'pressm', label: 'Press Releases', color: 'orange' });
1796
- sankeyFlows.push({ source: 'gov', target: 'pressm', value: pressmDocs.length, label: `${pressmDocs.length}` });
1797
- }
1798
- if (extDocs.length > 0) {
1799
- sankeyNodes.push({ id: 'ext', label: 'External / Reference', color: 'purple' });
1800
- sankeyFlows.push({ source: 'pvt', target: 'ext', value: extDocs.length, label: `${extDocs.length}` });
1801
- }
1802
- if (otherDocs.length > 0) {
1803
- sankeyNodes.push({ id: 'other', label: 'Other Docs', color: 'purple' });
1804
- sankeyFlows.push({ source: 'pvt', target: 'other', value: otherDocs.length, label: `${otherDocs.length}` });
1805
- }
1806
- // Only include Sankey when there is more than one non-trivial flow (otherwise uninformative)
1807
- const sankeySection = sankeyFlows.length >= 2
1808
- ? generateSankeySection({
1809
- nodes: sankeyNodes,
1810
- flows: sankeyFlows,
1811
- lang,
1812
- title: topic ? `Legislative Flow — ${topic}` : 'Legislative Flow',
1813
- summary: `Flow of ${docs.length} parliamentary documents from initiating actors to document types`,
1814
- })
1815
- : null;
1816
- // ── World Bank / Economic Dashboard ──────────────────────────────────────
1817
- const economicSection = detectedDomainList.length > 0
1818
- ? generateEconomicDashboardSection({ policyDomains: detectedDomainList, lang })
1819
- : null;
1820
- const additionalSections = [
1821
- ...(sankeySection ? [sankeySection] : []),
1822
- ...(economicSection ? [economicSection] : []),
1823
- mindmapSection,
1824
- ];
1825
- return [dashboardSection, swotSection, ...additionalSections];
1826
- }
1827
- /**
1828
- * Generate Deep-Inspection article targeting specific documents or policy topics.
1829
- * Uses documentIds, documentUrls, and focusTopic from CLI config to fetch
1830
- * targeted Riksdag documents and generate in-depth analysis articles.
1831
- */
1832
- export async function generateDeepInspection() {
1833
- console.log('🔍 Generating Deep-Inspection article...');
1834
- if (documentIds.length === 0 && documentUrls.length === 0 && !focusTopic) {
1835
- console.log(' ⚠️ No targeting parameters provided (--document-ids, --document-urls, or --focus-topic)');
1836
- console.log(' ℹ️ Deep-inspection requires at least one targeting parameter — skipping');
1837
- return { success: true, files: 0 };
1838
- }
1839
- console.log(` 📋 Document IDs: ${documentIds.length > 0 ? documentIds.join(', ') : '(none)'}`);
1840
- console.log(` 🔗 Document URLs: ${documentUrls.length > 0 ? documentUrls.join(', ') : '(none)'}`);
1841
- console.log(` 🎯 Focus topic: ${focusTopic || '(none)'}`);
1842
- console.log(` 🔬 Analysis depth: ${analysisDepth} (${['surface', 'predictive+historical', 'full with executive summary', 'full multi-iteration'][analysisDepth - 1]})`);
1843
- try {
1844
- const client = await getSharedClient();
1845
- // Resolve document IDs from URLs and collect government/GitHub URLs separately
1846
- const urlDerivedIds = [];
1847
- const governmentUrls = [];
1848
- const gitHubUrls = [];
1849
- for (const url of documentUrls) {
1850
- const docId = extractDocIdFromUrl(url);
1851
- if (docId) {
1852
- console.log(` 🔗 Resolved URL → dok_id: ${docId}`);
1853
- urlDerivedIds.push(docId);
1854
- }
1855
- else if (isGovernmentUrl(url)) {
1856
- console.log(` 🏛️ Government URL detected (will fetch via g0v): ${url}`);
1857
- governmentUrls.push(url);
1858
- }
1859
- else if (isGitHubUrl(url)) {
1860
- console.log(` 📦 GitHub URL detected (will fetch raw content): ${url}`);
1861
- gitHubUrls.push(url);
1862
- }
1863
- else {
1864
- console.warn(` ⚠️ Unsupported URL type (riksdagen.se, regeringen.se, github.com supported): ${url}`);
1865
- }
1866
- }
1867
- // Combine explicit IDs + URL-derived IDs (deduplicated)
1868
- const allDocIds = [...new Set([...documentIds, ...urlDerivedIds])];
1869
- // Fetch targeted documents by ID
1870
- const targetDocs = [];
1871
- for (const docId of allDocIds) {
1872
- try {
1873
- console.log(` 🔄 Fetching document ${docId}...`);
1874
- const doc = await client.request('get_dokument', { dok_id: docId });
1875
- if (doc)
1876
- targetDocs.push(doc);
1877
- }
1878
- catch (err) {
1879
- console.warn(` ⚠️ Could not fetch document ${docId}: ${err.message}`);
1880
- }
1881
- }
1882
- // Fetch government document content for regeringen.se URLs via g0v
1883
- for (const govUrl of governmentUrls) {
1884
- try {
1885
- console.log(` 🏛️ Fetching government document: ${govUrl}`);
1886
- const content = await client.fetchGovernmentDocumentContent(govUrl);
1887
- if (content) {
1888
- // Extract a human-readable title from the URL path's last segment.
1889
- // e.g. "/pressmeddelanden/2026/03/91-atgarder-ska-starka-..." → "91 atgarder ska starka ..."
1890
- const urlPath = new URL(govUrl).pathname;
1891
- const segments = urlPath.split('/').filter(Boolean);
1892
- const titleSlug = segments[segments.length - 1] ?? 'government-document';
1893
- const titleFromSlug = titleSlug.replace(/-/g, ' ');
1894
- // Use a URL-path-based hash suffix to avoid dok_id collisions between
1895
- // government documents that share the same first 30 chars of their slug.
1896
- const hashSuffix = hashPathSuffix(urlPath);
1897
- const govDoc = {
1898
- doktyp: 'pressm',
1899
- documentType: 'pressm',
1900
- titel: titleFromSlug,
1901
- title: titleFromSlug,
1902
- url: govUrl,
1903
- dok_id: `gov-${titleSlug.slice(0, 30)}-${hashSuffix}`,
1904
- fullText: content,
1905
- fullContent: content,
1906
- contentFetched: true,
1907
- summary: content.slice(0, 500),
1908
- datum: new Date().toISOString().split('T')[0],
1909
- };
1910
- targetDocs.push(govDoc);
1911
- console.log(` ✅ Government document fetched: ${titleFromSlug}`);
1912
- }
1913
- else {
1914
- console.warn(` ⚠️ No content returned for government URL: ${govUrl}`);
1915
- }
1916
- }
1917
- catch (err) {
1918
- console.warn(` ⚠️ Failed to fetch government document ${govUrl}: ${err.message}`);
1919
- }
1920
- }
1921
- // Fetch GitHub raw content for github.com URLs (e.g. strategy documents, reference docs)
1922
- for (const ghUrl of gitHubUrls) {
1923
- try {
1924
- const rawUrl = toGitHubRawUrl(ghUrl);
1925
- if (!rawUrl) {
1926
- console.warn(` ⚠️ Cannot convert GitHub URL to raw format: ${ghUrl}`);
1927
- continue;
1928
- }
1929
- console.log(` 📦 Fetching GitHub content: ${rawUrl}`);
1930
- const content = await client.fetchExternalUrlContent(rawUrl);
1931
- if (content) {
1932
- // Extract title from file path — e.g. "Information_Security_Strategy.md" → "Information Security Strategy"
1933
- const urlPath = new URL(ghUrl).pathname;
1934
- // After split('/').filter(Boolean), segments = ['owner', 'repo', 'blob', 'branch', ...pathParts]
1935
- const segments = urlPath.split('/').filter(Boolean);
1936
- const filename = segments[segments.length - 1] ?? 'external-document';
1937
- const titleFromFilename = filename
1938
- .replace(/\.(md|txt|rst|adoc|html)$/i, '')
1939
- .replace(/[-_]/g, ' ');
1940
- // Identify the repository context (owner/repo) for the title
1941
- const repoContext = segments.length >= 2 ? `${segments[0]}/${segments[1]}` : '';
1942
- const fullTitle = repoContext ? `${titleFromFilename} (${repoContext})` : titleFromFilename;
1943
- // Use full URL path hash to avoid dok_id collisions across repositories
1944
- const hashSuffix = hashPathSuffix(urlPath);
1945
- // Include repo context in dok_id for cross-repository uniqueness
1946
- const repoSlug = repoContext ? repoContext.replace('/', '-').slice(0, 20) : '';
1947
- const fileSlug = filename.slice(0, 30).replace(/\.(md|txt|rst|adoc|html)$/i, '');
1948
- const ghDoc = {
1949
- doktyp: 'ext',
1950
- documentType: 'ext',
1951
- titel: fullTitle,
1952
- title: fullTitle,
1953
- url: ghUrl,
1954
- dok_id: `gh-${repoSlug}-${fileSlug}-${hashSuffix}`,
1955
- fullText: content,
1956
- fullContent: content,
1957
- contentFetched: true,
1958
- summary: content.slice(0, 500),
1959
- datum: new Date().toISOString().split('T')[0],
1960
- };
1961
- targetDocs.push(ghDoc);
1962
- console.log(` ✅ GitHub document fetched: ${fullTitle}`);
1963
- }
1964
- else {
1965
- console.warn(` ⚠️ No content returned for GitHub URL: ${ghUrl}`);
1966
- }
1967
- }
1968
- catch (err) {
1969
- console.warn(` ⚠️ Failed to fetch GitHub document ${ghUrl}: ${err.message}`);
1970
- }
1971
- }
1972
- // Fetch documents by focus topic if no IDs resolved
1973
- if (targetDocs.length === 0 && focusTopic) {
1974
- console.log(` 🔄 Searching documents for topic: ${focusTopic}`);
1975
- const rawDocs = await client.searchDocuments({ titel: focusTopic, limit: 10 })
1976
- .catch((e) => { if (requireMcp)
1977
- throw e; return []; });
1978
- targetDocs.push(...(Array.isArray(rawDocs) ? rawDocs : []));
1979
- }
1980
- if (targetDocs.length === 0) {
1981
- console.log(' ℹ️ No target documents found for deep inspection — skipping');
1982
- return { success: true, files: 0 };
1983
- }
1984
- console.log(` 📊 Found ${targetDocs.length} target documents for deep inspection`);
1985
- // Enrich documents with content
1986
- console.log(' 🔍 Enriching documents with detailed content...');
1987
- const enriched = await client.enrichDocumentsWithContent(targetDocs, 3);
1988
- const enrichedDocs = enriched;
1989
- const enrichedCount = enrichedDocs.filter(d => d['contentFetched']).length;
1990
- console.log(` ✅ Enriched ${enrichedCount}/${enrichedDocs.length} documents with content`);
1991
- const today = new Date();
1992
- const sanitizeSlugSegment = (value) => value
1993
- .toLowerCase()
1994
- .replace(/[^a-z0-9\s-]/g, '')
1995
- .replace(/\s+/g, '-')
1996
- .slice(0, 40)
1997
- .replace(/^-+|-+$/g, '');
1998
- const focusSlug = focusTopic ? sanitizeSlugSegment(focusTopic) : '';
1999
- let topicSlug;
2000
- if (focusSlug) {
2001
- topicSlug = focusSlug;
2002
- }
2003
- else {
2004
- const primaryDocId = allDocIds[0]
2005
- ?? enrichedDocs[0]?.dok_id
2006
- ?? 'analysis';
2007
- const fallbackSlug = sanitizeSlugSegment(primaryDocId);
2008
- topicSlug = fallbackSlug || 'analysis';
2009
- }
2010
- const slug = `${formatDateForSlug(today)}-deep-inspection-${topicSlug}`;
2011
- const sanitizedTopicRaw = focusTopic ? sanitizePlainText(focusTopic) : '';
2012
- const sanitizedTopic = sanitizedTopicRaw.trim() || null;
2013
- const defaultTopicLabels = {
2014
- en: 'Policy Analysis',
2015
- sv: 'Policyanalys',
2016
- da: 'Politisk analyse',
2017
- no: 'Politisk analyse',
2018
- fi: 'Politiikka-analyysi',
2019
- de: 'Politikanalyse',
2020
- fr: 'Analyse politique',
2021
- es: 'Análisis político',
2022
- nl: 'Beleidsanalyse',
2023
- ar: 'تحليل السياسات',
2024
- he: 'ניתוח מדיניות',
2025
- ja: '政策分析',
2026
- ko: '정책 분석',
2027
- zh: '政策分析',
2028
- };
2029
- const titles = {
2030
- en: { title: `Deep Inspection: ${sanitizedTopic || defaultTopicLabels.en}`, subtitle: `In-depth analysis of ${enrichedDocs.length} parliamentary documents` },
2031
- sv: { title: `Djupanalys: ${sanitizedTopic || defaultTopicLabels.sv}`, subtitle: `Fördjupad analys av ${enrichedDocs.length} riksdagsdokument` },
2032
- da: { title: `Dybdeanalyse: ${sanitizedTopic || defaultTopicLabels.da}`, subtitle: `Dybdegående analyse af ${enrichedDocs.length} parlamentariske dokumenter` },
2033
- no: { title: `Dybdeanalyse: ${sanitizedTopic || defaultTopicLabels.no}`, subtitle: `Inngående analyse av ${enrichedDocs.length} parlamentariske dokumenter` },
2034
- fi: { title: `Syväanalyysi: ${sanitizedTopic || defaultTopicLabels.fi}`, subtitle: `Syvällinen analyysi ${enrichedDocs.length} parlamentaarisesta asiakirjasta` },
2035
- de: { title: `Tiefenanalyse: ${sanitizedTopic || defaultTopicLabels.de}`, subtitle: `Eingehende Analyse von ${enrichedDocs.length} parlamentarischen Dokumenten` },
2036
- fr: { title: `Analyse approfondie: ${sanitizedTopic || defaultTopicLabels.fr}`, subtitle: `Analyse en profondeur de ${enrichedDocs.length} documents parlementaires` },
2037
- es: { title: `Análisis en profundidad: ${sanitizedTopic || defaultTopicLabels.es}`, subtitle: `Análisis detallado de ${enrichedDocs.length} documentos parlamentarios` },
2038
- nl: { title: `Diepteanalyse: ${sanitizedTopic || defaultTopicLabels.nl}`, subtitle: `Diepgaande analyse van ${enrichedDocs.length} parlementaire documenten` },
2039
- ar: { title: `تحليل معمّق: ${sanitizedTopic || defaultTopicLabels.ar}`, subtitle: `تحليل متعمق لـ ${enrichedDocs.length} وثائق برلمانية` },
2040
- he: { title: `ניתוח מעמיק: ${sanitizedTopic || defaultTopicLabels.he}`, subtitle: `ניתוח מעמיק של ${enrichedDocs.length} מסמכים פרלמנטריים` },
2041
- ja: { title: `詳細分析:${sanitizedTopic || defaultTopicLabels.ja}`, subtitle: `${enrichedDocs.length}件の議会文書の詳細分析` },
2042
- ko: { title: `심층 분석: ${sanitizedTopic || defaultTopicLabels.ko}`, subtitle: `${enrichedDocs.length}개 의회 문서 심층 분석` },
2043
- zh: { title: `深度分析:${sanitizedTopic || defaultTopicLabels.zh}`, subtitle: `${enrichedDocs.length}份议会文件深度分析` },
2044
- };
2045
- const enrichment = await getAnalysisEnrichment();
2046
- for (const lang of languages) {
2047
- console.log(` 🌐 Generating ${lang.toUpperCase()} version (analysis-depth: ${analysisDepth})...`);
2048
- const pipelineDepth = mapReportDepthToPipelineDepth(analysisDepth);
2049
- // ── AI Analysis Pipeline (now handled by agentic workflows, not scripts) ──
2050
- // Per ai-driven-analysis-guide.md Rule 2, scripts must NOT generate analysis.
2051
- const analysis = {
2052
- iterationsCompleted: 0,
2053
- confidenceScore: 0,
2054
- documentCount: enrichedDocs.length,
2055
- enrichedCount: enrichedDocs.length,
2056
- completedAt: new Date().toISOString(),
2057
- };
2058
- const validation = { passed: true };
2059
- const iterationDurationsMs = [0];
2060
- console.log(` 🌐 Generating ${lang.toUpperCase()} version... (analysis delegated to AI workflow)`);
2061
- // Run multi-iteration AI analysis pipeline — cache result per language
2062
- const cacheKey = sharedAnalysisCache.generateKey(enrichedDocs, sanitizedTopic, analysisIterations, lang);
2063
- let aiResult = sharedAnalysisCache.get(cacheKey);
2064
- if (!aiResult) {
2065
- const pipeline = new AIAnalysisPipeline({ iterations: analysisIterations });
2066
- aiResult = pipeline.analyze(enrichedDocs, sanitizedTopic, lang);
2067
- sharedAnalysisCache.set(cacheKey, aiResult);
2068
- console.log(` 🤖 AI analysis: ${aiResult.iterations} iteration(s), analysis score ${aiResult.analysisScore}`);
2069
- }
2070
- // Write iteration metadata for audit trail
2071
- const iterationMetadata = {
2072
- articleSlug: slug,
2073
- lang,
2074
- depth: pipelineDepth,
2075
- iterationsCompleted: analysis.iterationsCompleted,
2076
- iterationDurationsMs,
2077
- confidenceScore: analysis.confidenceScore,
2078
- validationResult: validation,
2079
- documentCount: analysis.documentCount,
2080
- enrichedCount: analysis.enrichedCount,
2081
- focusTopic: sanitizedTopic ?? undefined,
2082
- completedAt: analysis.completedAt,
2083
- };
2084
- writeAnalysisMetadata(slug, iterationMetadata);
2085
- // Topic-focused deep-inspection content (uses AI strategic implications & takeaways when available)
2086
- const content = generateDeepInspectionContent(enrichedDocs, sanitizedTopic, lang, analysisDepth, aiResult);
2087
- // Metadata still derived from document data
2088
- const contentData = { documents: enrichedDocs };
2089
- const watchPoints = extractWatchPoints(contentData, lang);
2090
- const metadata = generateMetadata(contentData, 'deep-inspection', lang);
2091
- const readTime = calculateReadTime(content);
2092
- const sourceMethods = ['get_dokument', 'get_dokument_innehall', 'search_dokument'];
2093
- if (governmentUrls.length > 0)
2094
- sourceMethods.push('get_g0v_document_content');
2095
- if (gitHubUrls.length > 0)
2096
- sourceMethods.push('GitHub raw content');
2097
- const sources = generateSources(sourceMethods);
2098
- // SWOT + dashboard sections — AI-generated dynamic entries (context-aware, all 14 languages)
2099
- const sections = buildDeepInspectionSections(enrichedDocs, sanitizedTopic, lang, aiResult);
2100
- const langTitles = titles[lang] || titles.en;
2101
- // Enrich English title/subtitle with content-based highlights
2102
- const enriched = lang === 'en' ? generateDynamicTitle(langTitles.title, content, enrichedDocs.length) : langTitles;
2103
- const html = generateArticleHTML({
2104
- slug: `${slug}-${lang}.html`,
2105
- title: enriched.title,
2106
- subtitle: enriched.subtitle,
2107
- date: toISODate(today),
2108
- type: 'analysis',
2109
- readTime,
2110
- lang,
2111
- content,
2112
- watchPoints,
2113
- sources,
2114
- keywords: metadata.keywords,
2115
- topics: metadata.topics,
2116
- tags: metadata.tags,
2117
- sections,
2118
- ...(enrichment ?? {}),
2119
- });
2120
- await writeSingleArticle(html, slug, lang, 'deep-inspection');
2121
- }
2122
- console.log(' ✅ Deep-Inspection article generated successfully in all requested languages');
2123
- return { success: true, files: languages.length, slug };
2124
- }
2125
- catch (error) {
2126
- console.error('❌ Error generating Deep-Inspection:', error.message);
2127
- stats.errors++;
2128
- return { success: false, error: error.message };
2129
- }
2130
- }
2131
- //# sourceMappingURL=generators.js.map