euparliamentmonitor 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/scripts/aggregator/analysis-aggregator.js +48 -0
- package/scripts/aggregator/article-html.js +4 -11
- package/scripts/aggregator/article-metadata.js +69 -6
- package/scripts/aggregator/artifact-order.js +28 -5
- package/scripts/aggregator/reader-guide-constants.js +13 -1
- package/scripts/aggregator/reader-intelligence-guide.js +105 -0
- package/scripts/constants/config.d.ts +6 -1
- package/scripts/constants/config.js +18 -1
- package/scripts/copy-vendor.js +164 -41
- package/scripts/generate-responsive-images.js +106 -0
- package/scripts/generators/build-info.js +12 -4
- package/scripts/generators/news-indexes.d.ts +12 -0
- package/scripts/generators/news-indexes.js +101 -18
- package/scripts/generators/political-intelligence/html.js +3 -12
- package/scripts/generators/sitemap/html.js +3 -12
- package/scripts/generators/sitemap/rss.js +1 -0
- package/scripts/generators/sitemap/xml.js +1 -0
- package/scripts/generators/sitemap.js +115 -52
- package/scripts/mcp/ep-open-data-client.d.ts +1 -1
- package/scripts/mcp/imf-mcp-client.d.ts +1 -1
- package/scripts/minify-assets.js +253 -0
- package/scripts/normalize-legacy-articles.js +238 -0
- package/scripts/optimize-css.js +80 -0
- package/scripts/templates/section-builders.d.ts +36 -0
- package/scripts/templates/section-builders.js +84 -9
- package/scripts/utils/file-utils.d.ts +18 -1
- package/scripts/utils/file-utils.js +36 -4
- package/scripts/utils/news-metadata.d.ts +7 -1
- package/scripts/utils/news-metadata.js +36 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -70,6 +70,9 @@
|
|
|
70
70
|
"generate-article:all": "node scripts/aggregator/article-generator.js --all",
|
|
71
71
|
"generate-news-indexes": "node scripts/generators/news-indexes.js",
|
|
72
72
|
"generate-sitemap": "node scripts/generators/sitemap.js",
|
|
73
|
+
"image:generate": "node scripts/generate-responsive-images.js",
|
|
74
|
+
"optimize-css": "node scripts/optimize-css.js",
|
|
75
|
+
"minify-assets": "node scripts/minify-assets.js",
|
|
73
76
|
"validate-ep-api": "npx tsx src/utils/validate-ep-api.ts",
|
|
74
77
|
"lint:prompts": "node scripts/lint-prompts.js",
|
|
75
78
|
"htmlhint": "sh -c 'htmlhint *.html; set -- news/*.html; if [ -e \"$1\" ]; then htmlhint \"$@\"; else echo \"No news/*.html files to lint\"; fi'",
|
|
@@ -154,6 +157,7 @@
|
|
|
154
157
|
"@vitest/ui": "4.1.6",
|
|
155
158
|
"chart.js": "4.5.1",
|
|
156
159
|
"chartjs-plugin-annotation": "3.1.0",
|
|
160
|
+
"clean-css": "^5.3.3",
|
|
157
161
|
"d3": "7.9.0",
|
|
158
162
|
"eslint": "10.3.0",
|
|
159
163
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -161,6 +165,7 @@
|
|
|
161
165
|
"eslint-plugin-security": "4.0.0",
|
|
162
166
|
"eslint-plugin-sonarjs": "4.0.3",
|
|
163
167
|
"happy-dom": "20.9.0",
|
|
168
|
+
"html-minifier-terser": "^7.2.0",
|
|
164
169
|
"htmlhint": "1.9.2",
|
|
165
170
|
"husky": "9.1.7",
|
|
166
171
|
"jscpd": "4.1.1",
|
|
@@ -169,6 +174,9 @@
|
|
|
169
174
|
"mermaid": "11.15.0",
|
|
170
175
|
"papaparse": "5.5.3",
|
|
171
176
|
"prettier": "3.8.3",
|
|
177
|
+
"purgecss": "8.0.0",
|
|
178
|
+
"sharp": "^0.34.5",
|
|
179
|
+
"terser": "^5.47.1",
|
|
172
180
|
"ts-api-utils": "2.5.0",
|
|
173
181
|
"tsx": "4.21.0",
|
|
174
182
|
"typedoc": "0.28.19",
|
|
@@ -284,6 +284,10 @@ const READER_GUIDE_EN = {
|
|
|
284
284
|
need: 'Significance scoring',
|
|
285
285
|
value: 'why this story outranks or trails other same-day European Parliament signals',
|
|
286
286
|
},
|
|
287
|
+
'section-actors-forces': {
|
|
288
|
+
need: 'Actors and forces',
|
|
289
|
+
value: 'who is driving the story, what political forces line up behind them, and which institutional levers they can pull',
|
|
290
|
+
},
|
|
287
291
|
'section-coalitions-voting': {
|
|
288
292
|
need: 'Coalitions and voting',
|
|
289
293
|
value: 'political group alignment, voting evidence, and coalition pressure points',
|
|
@@ -304,6 +308,50 @@ const READER_GUIDE_EN = {
|
|
|
304
308
|
need: 'Risk assessment',
|
|
305
309
|
value: 'policy, institutional, coalition, communications, and implementation risk register',
|
|
306
310
|
},
|
|
311
|
+
'section-threat': {
|
|
312
|
+
need: 'Threat landscape',
|
|
313
|
+
value: 'hostile actors, attack vectors, consequence trees, and legislative-disruption pathways',
|
|
314
|
+
},
|
|
315
|
+
'section-forward-projection': {
|
|
316
|
+
need: 'What to watch',
|
|
317
|
+
value: 'dated trigger events, calendar dependencies, and legislative-pipeline forecasts',
|
|
318
|
+
},
|
|
319
|
+
'section-electoral-arc': {
|
|
320
|
+
need: 'Electoral arc and mandate',
|
|
321
|
+
value: 'where the story sits in the EP term, mandate fulfilment, seat projection, and presidency-trio context',
|
|
322
|
+
},
|
|
323
|
+
'section-pestle-context': {
|
|
324
|
+
need: 'PESTLE and structural context',
|
|
325
|
+
value: 'political, economic, social, technological, legal, and environmental forces plus the historical baseline',
|
|
326
|
+
},
|
|
327
|
+
'section-continuity': {
|
|
328
|
+
need: 'Cross-run continuity',
|
|
329
|
+
value: 'what changed since prior sessions and how confidence shifted between runs',
|
|
330
|
+
},
|
|
331
|
+
'section-deep-analysis': {
|
|
332
|
+
need: 'Deep analysis',
|
|
333
|
+
value: 'long-form Economist-style explanation for readers who want the full argument',
|
|
334
|
+
},
|
|
335
|
+
'section-documents': {
|
|
336
|
+
need: 'Document trail',
|
|
337
|
+
value: 'the document index and per-file analysis behind the public judgement',
|
|
338
|
+
},
|
|
339
|
+
'section-extended-intel': {
|
|
340
|
+
need: 'Extended intelligence',
|
|
341
|
+
value: "devil's-advocate critique, comparative parallels, historical precedents, and media framing",
|
|
342
|
+
},
|
|
343
|
+
'section-mcp-reliability': {
|
|
344
|
+
need: 'MCP data reliability',
|
|
345
|
+
value: 'which feeds were healthy, which were degraded, and how data limits bound conclusions',
|
|
346
|
+
},
|
|
347
|
+
'section-quality-reflection': {
|
|
348
|
+
need: 'Analytical quality and reflection',
|
|
349
|
+
value: 'self-assessment scores, methodology audit, structured analytic techniques, and known limitations',
|
|
350
|
+
},
|
|
351
|
+
'section-supplementary-intelligence': {
|
|
352
|
+
need: 'Supplementary intelligence',
|
|
353
|
+
value: 'additional markdown discovered in the run that has not yet been assigned to a canonical section',
|
|
354
|
+
},
|
|
307
355
|
};
|
|
308
356
|
/**
|
|
309
357
|
* Render the generated reader-intelligence guide that appears before the
|
|
@@ -23,7 +23,7 @@ import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
|
|
|
23
23
|
import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, ARTICLE_TYPE_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, ARTICLE_TYPE_ICONS, FOOTER_SITEMAP_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
|
|
24
24
|
import { ArticleCategory } from '../types/index.js';
|
|
25
25
|
import { escapeHTML } from '../utils/file-utils.js';
|
|
26
|
-
import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
|
|
26
|
+
import { buildResponsiveIconLinks, buildResponsiveSocialImageMeta, buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
|
|
27
27
|
import { READER_GUIDE_SECTION_ID } from './reader-guide-constants.js';
|
|
28
28
|
import { READER_GUIDE_TITLE_LABELS, getReaderGuideSectionIcon, } from './reader-intelligence-guide.js';
|
|
29
29
|
import { TRADECRAFT_SECTION_ID, MANIFEST_SECTION_ID, SUPPLEMENTARY_SECTION_ID, } from './artifact-order.js';
|
|
@@ -888,7 +888,7 @@ export function wrapArticleHtml(options) {
|
|
|
888
888
|
dateModified: options.date,
|
|
889
889
|
inLanguage: safeLang,
|
|
890
890
|
url: canonicalUrl,
|
|
891
|
-
image: `${BASE_URL}/images/og-image.jpg`,
|
|
891
|
+
image: `${BASE_URL}/images/og-image-1200.jpg`,
|
|
892
892
|
author: { '@type': 'Organization', name: PUBLISHER_NAME, url: 'https://hack23.com' },
|
|
893
893
|
publisher: {
|
|
894
894
|
'@type': 'Organization',
|
|
@@ -969,18 +969,11 @@ ${hreflangLinks}
|
|
|
969
969
|
<meta property="og:url" content="${canonicalUrl}">
|
|
970
970
|
<meta property="og:site_name" content="EU Parliament Monitor">
|
|
971
971
|
<meta property="og:locale" content="${safeLang}">
|
|
972
|
-
|
|
973
|
-
<meta property="og:image:alt" content="${escapeHTML(options.title)} — EU Parliament Monitor">
|
|
974
|
-
<meta property="og:image:width" content="1200">
|
|
975
|
-
<meta property="og:image:height" content="630">
|
|
972
|
+
${buildResponsiveSocialImageMeta(`${options.title} — EU Parliament Monitor`)}
|
|
976
973
|
<meta name="twitter:card" content="summary_large_image">
|
|
977
974
|
<meta name="twitter:title" content="${escapeHTML(options.title)}">
|
|
978
975
|
<meta name="twitter:description" content="${escapeHTML(options.description)}">
|
|
979
|
-
|
|
980
|
-
<link rel="icon" type="image/x-icon" href="../favicon.ico">
|
|
981
|
-
<link rel="icon" type="image/png" sizes="32x32" href="../images/favicon-32x32.png">
|
|
982
|
-
<link rel="icon" type="image/png" sizes="16x16" href="../images/favicon-16x16.png">
|
|
983
|
-
<link rel="apple-touch-icon" sizes="180x180" href="../images/apple-touch-icon.png">
|
|
976
|
+
${buildResponsiveIconLinks('../')}
|
|
984
977
|
<link rel="manifest" href="../site.webmanifest">
|
|
985
978
|
<meta name="color-scheme" content="light dark">
|
|
986
979
|
<meta name="theme-color" content="#003399" media="(prefers-color-scheme: light)">
|
|
@@ -455,6 +455,56 @@ export function stripInlineMarkdown(raw) {
|
|
|
455
455
|
.replace(/\s+/g, ' ')
|
|
456
456
|
.trim();
|
|
457
457
|
}
|
|
458
|
+
/** Connector / determiner words that read as broken copy when they are
|
|
459
|
+
* the final token before a truncation ellipsis. */
|
|
460
|
+
const TRAILING_STOP_WORDS = new Set([
|
|
461
|
+
'the',
|
|
462
|
+
'a',
|
|
463
|
+
'an',
|
|
464
|
+
'of',
|
|
465
|
+
'to',
|
|
466
|
+
'for',
|
|
467
|
+
'in',
|
|
468
|
+
'on',
|
|
469
|
+
'at',
|
|
470
|
+
'by',
|
|
471
|
+
'and',
|
|
472
|
+
'or',
|
|
473
|
+
'with',
|
|
474
|
+
'from',
|
|
475
|
+
]);
|
|
476
|
+
/** Trailing characters we always strip before appending our own ellipsis,
|
|
477
|
+
* so we never emit double-ellipsis or stray punctuation. */
|
|
478
|
+
const TRAILING_PUNCT = /[.,;:—\-…\s]/u;
|
|
479
|
+
/**
|
|
480
|
+
* Repeatedly strip trailing stop-words (separated by a single space) and
|
|
481
|
+
* trailing punctuation (including any pre-existing ellipsis). Implemented
|
|
482
|
+
* imperatively to avoid super-linear regex backtracking on the
|
|
483
|
+
* `(?:\s+stop-word)+$` pattern flagged by `security/detect-unsafe-regex`.
|
|
484
|
+
*
|
|
485
|
+
* @param input - Pre-clipped string to clean up
|
|
486
|
+
* @returns Cleaned string with no trailing stop-words or punctuation
|
|
487
|
+
*/
|
|
488
|
+
function stripTrailingStopWordsAndPunctuation(input) {
|
|
489
|
+
let result = input;
|
|
490
|
+
let changed = true;
|
|
491
|
+
while (changed) {
|
|
492
|
+
changed = false;
|
|
493
|
+
while (result.length > 0 && TRAILING_PUNCT.test(result.charAt(result.length - 1))) {
|
|
494
|
+
result = result.slice(0, -1);
|
|
495
|
+
changed = true;
|
|
496
|
+
}
|
|
497
|
+
const lastSpace = result.lastIndexOf(' ');
|
|
498
|
+
if (lastSpace >= 0) {
|
|
499
|
+
const tail = result.slice(lastSpace + 1).toLowerCase();
|
|
500
|
+
if (TRAILING_STOP_WORDS.has(tail)) {
|
|
501
|
+
result = result.slice(0, lastSpace);
|
|
502
|
+
changed = true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
458
508
|
/**
|
|
459
509
|
* Clamp a string to `DESCRIPTION_MAX_LENGTH` characters, appending
|
|
460
510
|
* an ellipsis when truncation actually happens. Does not break words if
|
|
@@ -467,10 +517,22 @@ export function stripInlineMarkdown(raw) {
|
|
|
467
517
|
export function truncateDescription(text) {
|
|
468
518
|
if (text.length <= DESCRIPTION_MAX_LENGTH)
|
|
469
519
|
return text;
|
|
470
|
-
const cut = text.slice(0, DESCRIPTION_MAX_LENGTH -
|
|
520
|
+
const cut = text.slice(0, DESCRIPTION_MAX_LENGTH - 1);
|
|
521
|
+
// Prefer the last full sentence terminator within the cut so we don't
|
|
522
|
+
// end on a dangling determiner ("…year. The"). Period/!/? followed by
|
|
523
|
+
// a space marks a clean boundary. Only honour the boundary when it
|
|
524
|
+
// sits past the soft minimum so we keep enough body text to be useful.
|
|
525
|
+
const sentenceEnd = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '));
|
|
526
|
+
if (sentenceEnd >= DESCRIPTION_MIN_LENGTH) {
|
|
527
|
+
return cut.slice(0, sentenceEnd + 1).replace(/\s+$/, '');
|
|
528
|
+
}
|
|
471
529
|
const lastSpace = cut.lastIndexOf(' ');
|
|
472
|
-
|
|
473
|
-
|
|
530
|
+
let safe = lastSpace > DESCRIPTION_MAX_LENGTH - 60 ? cut.slice(0, lastSpace) : cut;
|
|
531
|
+
// Drop dangling stop-words and trailing punctuation/ellipsis so we
|
|
532
|
+
// never emit broken copy ("…year. The" → "…year.") or double-ellipsis
|
|
533
|
+
// ("The……") when the upstream input already carried an ellipsis.
|
|
534
|
+
safe = stripTrailingStopWordsAndPunctuation(safe);
|
|
535
|
+
return `${safe}…`;
|
|
474
536
|
}
|
|
475
537
|
/**
|
|
476
538
|
* Clamp a title to `TITLE_MAX_LENGTH` characters in the same
|
|
@@ -482,10 +544,11 @@ export function truncateDescription(text) {
|
|
|
482
544
|
export function truncateTitle(text) {
|
|
483
545
|
if (text.length <= TITLE_MAX_LENGTH)
|
|
484
546
|
return text;
|
|
485
|
-
const cut = text.slice(0, TITLE_MAX_LENGTH -
|
|
547
|
+
const cut = text.slice(0, TITLE_MAX_LENGTH - 1);
|
|
486
548
|
const lastSpace = cut.lastIndexOf(' ');
|
|
487
|
-
|
|
488
|
-
|
|
549
|
+
let safe = lastSpace > TITLE_MAX_LENGTH - 40 ? cut.slice(0, lastSpace) : cut;
|
|
550
|
+
safe = stripTrailingStopWordsAndPunctuation(safe);
|
|
551
|
+
return `${safe}…`;
|
|
489
552
|
}
|
|
490
553
|
/**
|
|
491
554
|
* Return the first Markdown H1 (`# …`) in the supplied text, stripped of
|
|
@@ -29,7 +29,7 @@ export const ARTIFACT_SECTIONS = [
|
|
|
29
29
|
{
|
|
30
30
|
id: 'synthesis',
|
|
31
31
|
title: 'Synthesis Summary',
|
|
32
|
-
artifacts: ['intelligence/synthesis-summary.md'],
|
|
32
|
+
artifacts: ['intelligence/synthesis-summary.md', 'synthesis.md'],
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
id: 'significance',
|
|
@@ -40,6 +40,7 @@ export const ARTIFACT_SECTIONS = [
|
|
|
40
40
|
'classification/priority-matrix.md',
|
|
41
41
|
'classification/issue-classification.md',
|
|
42
42
|
'intelligence/significance-scoring.md',
|
|
43
|
+
'significance-assessment.md',
|
|
43
44
|
],
|
|
44
45
|
},
|
|
45
46
|
{
|
|
@@ -50,6 +51,9 @@ export const ARTIFACT_SECTIONS = [
|
|
|
50
51
|
'classification/forces-analysis.md',
|
|
51
52
|
'classification/impact-matrix.md',
|
|
52
53
|
'classification/stakeholder-classification.md',
|
|
54
|
+
'actor-mapping.md',
|
|
55
|
+
'political-forces.md',
|
|
56
|
+
'impact-assessment.md',
|
|
53
57
|
// Catch-all for any other classification/*.md not consumed above
|
|
54
58
|
// (keeps non-canonical artifact names out of the Supplementary bucket
|
|
55
59
|
// and inside their journalist-correct section).
|
|
@@ -68,7 +72,11 @@ export const ARTIFACT_SECTIONS = [
|
|
|
68
72
|
{
|
|
69
73
|
id: 'stakeholder-map',
|
|
70
74
|
title: 'Stakeholder Map',
|
|
71
|
-
artifacts: [
|
|
75
|
+
artifacts: [
|
|
76
|
+
'intelligence/stakeholder-map.md',
|
|
77
|
+
'existing/stakeholder-impact.md',
|
|
78
|
+
'stakeholder-perspectives.md',
|
|
79
|
+
],
|
|
72
80
|
},
|
|
73
81
|
{
|
|
74
82
|
id: 'economic-context',
|
|
@@ -87,6 +95,8 @@ export const ARTIFACT_SECTIONS = [
|
|
|
87
95
|
'risk-scoring/legislative-risk.md',
|
|
88
96
|
'risk-scoring/economic-risk.md',
|
|
89
97
|
'risk-scoring/institutional-risk.md',
|
|
98
|
+
'risk-matrix.md',
|
|
99
|
+
'quantitative-swot.md',
|
|
90
100
|
// Catch-all for any other risk-scoring/*.md (e.g. naming variants) so
|
|
91
101
|
// they render under Risk Assessment instead of Supplementary.
|
|
92
102
|
'risk-scoring/',
|
|
@@ -104,7 +114,11 @@ export const ARTIFACT_SECTIONS = [
|
|
|
104
114
|
{
|
|
105
115
|
id: 'scenarios',
|
|
106
116
|
title: 'Scenarios & Wildcards',
|
|
107
|
-
artifacts: [
|
|
117
|
+
artifacts: [
|
|
118
|
+
'intelligence/scenario-forecast.md',
|
|
119
|
+
'intelligence/wildcards-blackswans.md',
|
|
120
|
+
'scenario-forecast.md',
|
|
121
|
+
],
|
|
108
122
|
},
|
|
109
123
|
{
|
|
110
124
|
id: 'forward-projection',
|
|
@@ -113,6 +127,9 @@ export const ARTIFACT_SECTIONS = [
|
|
|
113
127
|
'intelligence/forward-projection.md',
|
|
114
128
|
'intelligence/legislative-pipeline-forecast.md',
|
|
115
129
|
'intelligence/parliamentary-calendar-projection.md',
|
|
130
|
+
'forward/forward-projection.md',
|
|
131
|
+
'forward/legislative-pipeline-forecast.md',
|
|
132
|
+
'forward/parliamentary-calendar-projection.md',
|
|
116
133
|
'extended/forward-indicators.md',
|
|
117
134
|
],
|
|
118
135
|
},
|
|
@@ -130,7 +147,11 @@ export const ARTIFACT_SECTIONS = [
|
|
|
130
147
|
{
|
|
131
148
|
id: 'pestle-context',
|
|
132
149
|
title: 'PESTLE & Context',
|
|
133
|
-
artifacts: [
|
|
150
|
+
artifacts: [
|
|
151
|
+
'intelligence/pestle-analysis.md',
|
|
152
|
+
'intelligence/historical-baseline.md',
|
|
153
|
+
'pestle-analysis.md',
|
|
154
|
+
],
|
|
134
155
|
},
|
|
135
156
|
{
|
|
136
157
|
id: 'continuity',
|
|
@@ -162,7 +183,7 @@ export const ARTIFACT_SECTIONS = [
|
|
|
162
183
|
{
|
|
163
184
|
id: 'extended-intel',
|
|
164
185
|
title: 'Extended Intelligence',
|
|
165
|
-
artifacts: ['extended/'],
|
|
186
|
+
artifacts: ['extended/', 'media-framing.md'],
|
|
166
187
|
},
|
|
167
188
|
{
|
|
168
189
|
id: 'mcp-reliability',
|
|
@@ -177,6 +198,8 @@ export const ARTIFACT_SECTIONS = [
|
|
|
177
198
|
'intelligence/reference-analysis-quality.md',
|
|
178
199
|
'intelligence/workflow-audit.md',
|
|
179
200
|
'intelligence/methodology-reflection.md',
|
|
201
|
+
'article-index.md',
|
|
202
|
+
'methodology-reflection.md',
|
|
180
203
|
],
|
|
181
204
|
},
|
|
182
205
|
];
|
|
@@ -14,10 +14,22 @@ export const READER_GUIDE_SECTION_IDS = [
|
|
|
14
14
|
'section-executive-brief',
|
|
15
15
|
'section-synthesis',
|
|
16
16
|
'section-significance',
|
|
17
|
+
'section-actors-forces',
|
|
17
18
|
'section-coalitions-voting',
|
|
18
19
|
'section-stakeholder-map',
|
|
19
20
|
'section-economic-context',
|
|
20
|
-
'section-scenarios',
|
|
21
21
|
'section-risk',
|
|
22
|
+
'section-threat',
|
|
23
|
+
'section-scenarios',
|
|
24
|
+
'section-forward-projection',
|
|
25
|
+
'section-electoral-arc',
|
|
26
|
+
'section-pestle-context',
|
|
27
|
+
'section-continuity',
|
|
28
|
+
'section-deep-analysis',
|
|
29
|
+
'section-documents',
|
|
30
|
+
'section-extended-intel',
|
|
31
|
+
'section-mcp-reliability',
|
|
32
|
+
'section-quality-reflection',
|
|
33
|
+
'section-supplementary-intelligence',
|
|
22
34
|
];
|
|
23
35
|
//# sourceMappingURL=reader-guide-constants.js.map
|
|
@@ -567,6 +567,74 @@ const READER_GUIDE_ROWS = {
|
|
|
567
567
|
zh: '本次运行如何与先前会话关联、变化了什么以及置信度在运行之间如何变化',
|
|
568
568
|
},
|
|
569
569
|
},
|
|
570
|
+
'section-deep-analysis': {
|
|
571
|
+
need: {
|
|
572
|
+
en: 'Deep analysis',
|
|
573
|
+
sv: 'Djupanalys',
|
|
574
|
+
da: 'Dybdegående analyse',
|
|
575
|
+
no: 'Dybdeanalyse',
|
|
576
|
+
fi: 'Syväanalyysi',
|
|
577
|
+
de: 'Tiefenanalyse',
|
|
578
|
+
fr: 'Analyse approfondie',
|
|
579
|
+
es: 'Análisis profundo',
|
|
580
|
+
nl: 'Diepteanalyse',
|
|
581
|
+
ar: 'تحليل معمق',
|
|
582
|
+
he: 'ניתוח עומק',
|
|
583
|
+
ja: '詳細分析',
|
|
584
|
+
ko: '심층 분석',
|
|
585
|
+
zh: '深度分析',
|
|
586
|
+
},
|
|
587
|
+
value: {
|
|
588
|
+
en: 'long-form Economist-style explanation for readers who want the full argument',
|
|
589
|
+
sv: 'lång Economist-liknande förklaring för läsare som vill ha hela argumentet',
|
|
590
|
+
da: 'lang Economist-lignende forklaring for læsere der ønsker hele argumentet',
|
|
591
|
+
no: 'lang Economist-lignende forklaring for lesere som ønsker hele argumentet',
|
|
592
|
+
fi: 'pitkä Economist-tyylinen selitys lukijoille, jotka haluavat koko perustelun',
|
|
593
|
+
de: 'lange, Economist-artige Erklärung für Leser, die das ganze Argument wollen',
|
|
594
|
+
fr: "explication longue de style Economist pour les lecteurs qui veulent l'argument complet",
|
|
595
|
+
es: 'explicación extensa de estilo Economist para lectores que quieren el argumento completo',
|
|
596
|
+
nl: 'lange uitleg in Economist-stijl voor lezers die het volledige argument willen',
|
|
597
|
+
ar: 'شرح مطول بأسلوب إيكونوميست للقراء الذين يريدون الحجة كاملة',
|
|
598
|
+
he: 'הסבר ארוך בסגנון האקונומיסט לקוראים שרוצים את הטיעון המלא',
|
|
599
|
+
ja: '全体の論旨を求める読者向けのエコノミスト風長文解説',
|
|
600
|
+
ko: '전체 논지를 원하는 독자를 위한 이코노미스트식 장문 설명',
|
|
601
|
+
zh: '为希望了解完整论证的读者提供的《经济学人》式长篇解释',
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
'section-documents': {
|
|
605
|
+
need: {
|
|
606
|
+
en: 'Document trail',
|
|
607
|
+
sv: 'Dokumentspår',
|
|
608
|
+
da: 'Dokumentspor',
|
|
609
|
+
no: 'Dokumentspor',
|
|
610
|
+
fi: 'Asiakirjapolku',
|
|
611
|
+
de: 'Dokumentenspur',
|
|
612
|
+
fr: 'Piste documentaire',
|
|
613
|
+
es: 'Rastro documental',
|
|
614
|
+
nl: 'Documentspoor',
|
|
615
|
+
ar: 'مسار الوثائق',
|
|
616
|
+
he: 'מסלול מסמכים',
|
|
617
|
+
ja: '文書トレイル',
|
|
618
|
+
ko: '문서 추적',
|
|
619
|
+
zh: '文件线索',
|
|
620
|
+
},
|
|
621
|
+
value: {
|
|
622
|
+
en: 'the document index and per-file analysis behind the public judgement',
|
|
623
|
+
sv: 'dokumentindexet och analysen per fil bakom den offentliga bedömningen',
|
|
624
|
+
da: 'dokumentindekset og analyse pr. fil bag den offentlige vurdering',
|
|
625
|
+
no: 'dokumentindeksen og analyse per fil bak den offentlige vurderingen',
|
|
626
|
+
fi: 'asiakirjahakemisto ja tiedostokohtainen analyysi julkisen arvion taustalla',
|
|
627
|
+
de: 'Dokumentenindex und Einzeldateianalyse hinter der öffentlichen Bewertung',
|
|
628
|
+
fr: "l'index des documents et l'analyse fichier par fichier derrière le jugement public",
|
|
629
|
+
es: 'el índice documental y el análisis por archivo detrás del juicio público',
|
|
630
|
+
nl: 'de documentenindex en analyse per bestand achter het publieke oordeel',
|
|
631
|
+
ar: 'فهرس الوثائق والتحليل لكل ملف خلف الحكم العام',
|
|
632
|
+
he: 'אינדקס המסמכים וניתוח לפי קובץ שמאחורי השיפוט הציבורי',
|
|
633
|
+
ja: '公開判断の背後にある文書索引とファイル別分析',
|
|
634
|
+
ko: '공개 판단 뒤에 있는 문서 색인과 파일별 분석',
|
|
635
|
+
zh: '公共判断背后的文件索引和逐文件分析',
|
|
636
|
+
},
|
|
637
|
+
},
|
|
570
638
|
'section-extended-intel': {
|
|
571
639
|
need: {
|
|
572
640
|
en: 'Extended intelligence',
|
|
@@ -669,6 +737,40 @@ const READER_GUIDE_ROWS = {
|
|
|
669
737
|
zh: '自我评估分数、方法论审计、使用的结构化分析技术和已知限制',
|
|
670
738
|
},
|
|
671
739
|
},
|
|
740
|
+
'section-supplementary-intelligence': {
|
|
741
|
+
need: {
|
|
742
|
+
en: 'Supplementary intelligence',
|
|
743
|
+
sv: 'Kompletterande underrättelse',
|
|
744
|
+
da: 'Supplerende efterretning',
|
|
745
|
+
no: 'Supplerende etterretning',
|
|
746
|
+
fi: 'Täydentävä tiedustelu',
|
|
747
|
+
de: 'Ergänzende Aufklärung',
|
|
748
|
+
fr: 'Renseignement supplémentaire',
|
|
749
|
+
es: 'Inteligencia suplementaria',
|
|
750
|
+
nl: 'Aanvullende inlichtingen',
|
|
751
|
+
ar: 'استخبارات تكميلية',
|
|
752
|
+
he: 'מודיעין משלים',
|
|
753
|
+
ja: '補足インテリジェンス',
|
|
754
|
+
ko: '보충 인텔리전스',
|
|
755
|
+
zh: '补充情报',
|
|
756
|
+
},
|
|
757
|
+
value: {
|
|
758
|
+
en: 'additional markdown discovered in the run that has not yet been assigned to a canonical section',
|
|
759
|
+
sv: 'ytterligare markdown som hittats i körningen och ännu inte tilldelats en kanonisk sektion',
|
|
760
|
+
da: 'yderligere markdown fundet i kørslen som endnu ikke er tildelt en kanonisk sektion',
|
|
761
|
+
no: 'ytterligere markdown funnet i kjøringen som ennå ikke er tilordnet en kanonisk seksjon',
|
|
762
|
+
fi: 'ajossa löydetty lisämarkdown, jota ei vielä ole liitetty kanoniseen osioon',
|
|
763
|
+
de: 'zusätzliches Markdown aus dem Lauf, das noch keinem kanonischen Abschnitt zugeordnet ist',
|
|
764
|
+
fr: "markdown supplémentaire découvert dans l'exécution et pas encore affecté à une section canonique",
|
|
765
|
+
es: 'markdown adicional descubierto en la ejecución que aún no se ha asignado a una sección canónica',
|
|
766
|
+
nl: 'extra markdown gevonden in de run dat nog niet aan een canonieke sectie is toegewezen',
|
|
767
|
+
ar: 'ملفات ماركداون إضافية اكتُشفت في التشغيل ولم تُسند بعد إلى قسم معياري',
|
|
768
|
+
he: 'מרקדאון נוסף שהתגלה בהרצה ועדיין לא שובץ למדור קנוני',
|
|
769
|
+
ja: '実行内で見つかったがまだ正規セクションに割り当てられていない追加Markdown',
|
|
770
|
+
ko: '실행에서 발견되었지만 아직 표준 섹션에 할당되지 않은 추가 마크다운',
|
|
771
|
+
zh: '运行中发现但尚未分配到规范章节的附加Markdown',
|
|
772
|
+
},
|
|
773
|
+
},
|
|
672
774
|
};
|
|
673
775
|
/* ─── Section icons ─────────────────────────────────────────────── */
|
|
674
776
|
/** Visual icons for each reader guide section to improve scannability. */
|
|
@@ -687,9 +789,12 @@ const SECTION_ICONS = {
|
|
|
687
789
|
'section-electoral-arc': '🗳️',
|
|
688
790
|
'section-pestle-context': '🌍',
|
|
689
791
|
'section-continuity': '🔁',
|
|
792
|
+
'section-deep-analysis': '🔬',
|
|
793
|
+
'section-documents': '📄',
|
|
690
794
|
'section-extended-intel': '🧠',
|
|
691
795
|
'section-mcp-reliability': '📡',
|
|
692
796
|
'section-quality-reflection': '🪞',
|
|
797
|
+
'section-supplementary-intelligence': '📎',
|
|
693
798
|
};
|
|
694
799
|
/**
|
|
695
800
|
* Look up the visual icon for a known article section.
|
|
@@ -82,7 +82,12 @@ export declare const BUILD_SHORT: string;
|
|
|
82
82
|
/**
|
|
83
83
|
* ISO 8601 timestamp for when this build was produced. Precedence:
|
|
84
84
|
* 1. `process.env.BUILD_TIME` (CI sets this in the workflow)
|
|
85
|
-
* 2.
|
|
85
|
+
* 2. Commit timestamp of {@link BUILD_ID} (`git log -1 --format=%cI`) —
|
|
86
|
+
* deterministic for a given commit, so workflow_dispatch re-runs of the
|
|
87
|
+
* same SHA produce a byte-identical `build-info.json` and `sw.js` and
|
|
88
|
+
* `aws s3 sync` correctly skips them as unchanged.
|
|
89
|
+
* 3. `new Date().toISOString()` fallback (only hits when both env and git
|
|
90
|
+
* are unavailable, e.g. tarball builds outside a git checkout).
|
|
86
91
|
*/
|
|
87
92
|
export declare const BUILD_TIME: string;
|
|
88
93
|
/**
|
|
@@ -207,12 +207,29 @@ export const BUILD_SHORT = BUILD_ID.slice(0, 7);
|
|
|
207
207
|
/**
|
|
208
208
|
* ISO 8601 timestamp for when this build was produced. Precedence:
|
|
209
209
|
* 1. `process.env.BUILD_TIME` (CI sets this in the workflow)
|
|
210
|
-
* 2.
|
|
210
|
+
* 2. Commit timestamp of {@link BUILD_ID} (`git log -1 --format=%cI`) —
|
|
211
|
+
* deterministic for a given commit, so workflow_dispatch re-runs of the
|
|
212
|
+
* same SHA produce a byte-identical `build-info.json` and `sw.js` and
|
|
213
|
+
* `aws s3 sync` correctly skips them as unchanged.
|
|
214
|
+
* 3. `new Date().toISOString()` fallback (only hits when both env and git
|
|
215
|
+
* are unavailable, e.g. tarball builds outside a git checkout).
|
|
211
216
|
*/
|
|
212
217
|
export const BUILD_TIME = (() => {
|
|
213
218
|
const fromEnv = (process.env.BUILD_TIME ?? '').trim();
|
|
214
219
|
if (fromEnv)
|
|
215
220
|
return fromEnv;
|
|
221
|
+
try {
|
|
222
|
+
const fromGit = execSync('git log -1 --format=%cI', {
|
|
223
|
+
encoding: 'utf-8',
|
|
224
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
225
|
+
cwd: PROJECT_ROOT,
|
|
226
|
+
}).trim();
|
|
227
|
+
if (fromGit)
|
|
228
|
+
return fromGit;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* git unavailable or not a repo — fall through to wall-clock fallback */
|
|
232
|
+
}
|
|
216
233
|
return new Date().toISOString();
|
|
217
234
|
})();
|
|
218
235
|
/**
|