riksdagsmonitor 0.8.15 → 0.8.17
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/dist/lib/dashboards/coalition-dashboard.d.ts.map +1 -1
- package/dist/lib/dashboards/coalition-dashboard.js +25 -9
- package/dist/lib/dashboards/coalition-dashboard.js.map +1 -1
- package/dist/lib/dashboards/coalition-loader.d.ts.map +1 -1
- package/dist/lib/dashboards/coalition-loader.js +68 -32
- package/dist/lib/dashboards/coalition-loader.js.map +1 -1
- package/dist/lib/shared/chart-factory.d.ts +9 -2
- package/dist/lib/shared/chart-factory.d.ts.map +1 -1
- package/dist/lib/shared/chart-factory.js +9 -2
- package/dist/lib/shared/chart-factory.js.map +1 -1
- package/dist/lib/shared/theme.d.ts +1 -18
- package/dist/lib/shared/theme.d.ts.map +1 -1
- package/dist/lib/shared/theme.js +1 -18
- package/dist/lib/shared/theme.js.map +1 -1
- package/dist/scripts/analysis-reader.d.ts +2 -1
- package/dist/scripts/analysis-reader.d.ts.map +1 -1
- package/dist/scripts/analysis-reader.js.map +1 -1
- package/dist/scripts/data-transformers/content-generators/index.d.ts +32 -4
- package/dist/scripts/data-transformers/content-generators/index.d.ts.map +1 -1
- package/dist/scripts/data-transformers/content-generators/index.js +4 -2
- package/dist/scripts/data-transformers/content-generators/index.js.map +1 -1
- package/dist/scripts/data-transformers/content-generators/shared.d.ts +44 -2
- package/dist/scripts/data-transformers/content-generators/shared.d.ts.map +1 -1
- package/dist/scripts/data-transformers/content-generators/shared.js +9 -3
- package/dist/scripts/data-transformers/content-generators/shared.js.map +1 -1
- package/dist/scripts/generate-news-enhanced/config.d.ts +5 -4
- package/dist/scripts/generate-news-enhanced/config.d.ts.map +1 -1
- package/dist/scripts/generate-news-enhanced/config.js +3 -3
- package/dist/scripts/generate-news-enhanced/config.js.map +1 -1
- package/dist/scripts/generate-news-enhanced/generators.d.ts.map +1 -1
- package/dist/scripts/generate-news-enhanced/generators.js +38 -15
- package/dist/scripts/generate-news-enhanced/generators.js.map +1 -1
- package/dist/scripts/generate-news-enhanced/helpers.d.ts +1 -2
- package/dist/scripts/generate-news-enhanced/helpers.d.ts.map +1 -1
- package/dist/scripts/generate-news-enhanced/helpers.js +31 -1
- package/dist/scripts/generate-news-enhanced/helpers.js.map +1 -1
- package/dist/scripts/generate-news-enhanced/swot-analyzer.d.ts +12 -1
- package/dist/scripts/generate-news-enhanced/swot-analyzer.d.ts.map +1 -1
- package/dist/scripts/generate-news-enhanced/swot-analyzer.js +48 -0
- package/dist/scripts/generate-news-enhanced/swot-analyzer.js.map +1 -1
- package/dist/scripts/news-types/breaking-news.d.ts.map +1 -1
- package/dist/scripts/news-types/breaking-news.js +5 -1
- package/dist/scripts/news-types/breaking-news.js.map +1 -1
- package/dist/scripts/pre-article-analysis/markdown-serializer.d.ts +64 -1
- package/dist/scripts/pre-article-analysis/markdown-serializer.d.ts.map +1 -1
- package/dist/scripts/pre-article-analysis/markdown-serializer.js.map +1 -1
- package/dist/scripts/pre-article-analysis.d.ts.map +1 -1
- package/dist/scripts/pre-article-analysis.js +20 -7
- package/dist/scripts/pre-article-analysis.js.map +1 -1
- package/dist/scripts/types/article.d.ts +33 -3
- package/dist/scripts/types/article.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/scripts/ai-analysis/coalition-detector.d.ts +0 -28
- package/dist/scripts/ai-analysis/coalition-detector.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/coalition-detector.js +0 -242
- package/dist/scripts/ai-analysis/coalition-detector.js.map +0 -1
- package/dist/scripts/ai-analysis/dashboard-analyzer.d.ts +0 -73
- package/dist/scripts/ai-analysis/dashboard-analyzer.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/dashboard-analyzer.js +0 -511
- package/dist/scripts/ai-analysis/dashboard-analyzer.js.map +0 -1
- package/dist/scripts/ai-analysis/document-analyzer.d.ts +0 -196
- package/dist/scripts/ai-analysis/document-analyzer.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/document-analyzer.js +0 -1001
- package/dist/scripts/ai-analysis/document-analyzer.js.map +0 -1
- package/dist/scripts/ai-analysis/domains/index.d.ts +0 -15
- package/dist/scripts/ai-analysis/domains/index.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/domains/index.js +0 -494
- package/dist/scripts/ai-analysis/domains/index.js.map +0 -1
- package/dist/scripts/ai-analysis/helpers.d.ts +0 -43
- package/dist/scripts/ai-analysis/helpers.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/helpers.js +0 -79
- package/dist/scripts/ai-analysis/helpers.js.map +0 -1
- package/dist/scripts/ai-analysis/index.d.ts +0 -15
- package/dist/scripts/ai-analysis/index.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/index.js +0 -14
- package/dist/scripts/ai-analysis/index.js.map +0 -1
- package/dist/scripts/ai-analysis/pipeline.d.ts +0 -41
- package/dist/scripts/ai-analysis/pipeline.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/pipeline.js +0 -208
- package/dist/scripts/ai-analysis/pipeline.js.map +0 -1
- package/dist/scripts/ai-analysis/political-significance.d.ts +0 -63
- package/dist/scripts/ai-analysis/political-significance.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/political-significance.js +0 -227
- package/dist/scripts/ai-analysis/political-significance.js.map +0 -1
- package/dist/scripts/ai-analysis/quality-assessor.d.ts +0 -104
- package/dist/scripts/ai-analysis/quality-assessor.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/quality-assessor.js +0 -554
- package/dist/scripts/ai-analysis/quality-assessor.js.map +0 -1
- package/dist/scripts/ai-analysis/swot/index.d.ts +0 -56
- package/dist/scripts/ai-analysis/swot/index.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/swot/index.js +0 -360
- package/dist/scripts/ai-analysis/swot/index.js.map +0 -1
- package/dist/scripts/ai-analysis/swot/placeholders.d.ts +0 -30
- package/dist/scripts/ai-analysis/swot/placeholders.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/swot/placeholders.js +0 -381
- package/dist/scripts/ai-analysis/swot/placeholders.js.map +0 -1
- package/dist/scripts/ai-analysis/types.d.ts +0 -333
- package/dist/scripts/ai-analysis/types.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/types.js +0 -19
- package/dist/scripts/ai-analysis/types.js.map +0 -1
- package/dist/scripts/ai-analysis/visualisation/index.d.ts +0 -15
- package/dist/scripts/ai-analysis/visualisation/index.d.ts.map +0 -1
- package/dist/scripts/ai-analysis/visualisation/index.js +0 -179
- package/dist/scripts/ai-analysis/visualisation/index.js.map +0 -1
- package/dist/scripts/analysis-framework/cross-reference.d.ts +0 -35
- package/dist/scripts/analysis-framework/cross-reference.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/cross-reference.js +0 -290
- package/dist/scripts/analysis-framework/cross-reference.js.map +0 -1
- package/dist/scripts/analysis-framework/index.d.ts +0 -79
- package/dist/scripts/analysis-framework/index.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/index.js +0 -133
- package/dist/scripts/analysis-framework/index.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/citizen.d.ts +0 -29
- package/dist/scripts/analysis-framework/lenses/citizen.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/citizen.js +0 -220
- package/dist/scripts/analysis-framework/lenses/citizen.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/economic.d.ts +0 -29
- package/dist/scripts/analysis-framework/lenses/economic.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/economic.js +0 -227
- package/dist/scripts/analysis-framework/lenses/economic.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/government.d.ts +0 -32
- package/dist/scripts/analysis-framework/lenses/government.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/government.js +0 -265
- package/dist/scripts/analysis-framework/lenses/government.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/international.d.ts +0 -29
- package/dist/scripts/analysis-framework/lenses/international.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/international.js +0 -248
- package/dist/scripts/analysis-framework/lenses/international.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/media.d.ts +0 -29
- package/dist/scripts/analysis-framework/lenses/media.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/media.js +0 -211
- package/dist/scripts/analysis-framework/lenses/media.js.map +0 -1
- package/dist/scripts/analysis-framework/lenses/opposition.d.ts +0 -29
- package/dist/scripts/analysis-framework/lenses/opposition.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/lenses/opposition.js +0 -234
- package/dist/scripts/analysis-framework/lenses/opposition.js.map +0 -1
- package/dist/scripts/analysis-framework/methodology-types.d.ts +0 -309
- package/dist/scripts/analysis-framework/methodology-types.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/methodology-types.js +0 -38
- package/dist/scripts/analysis-framework/methodology-types.js.map +0 -1
- package/dist/scripts/analysis-framework/political-classification.d.ts +0 -58
- package/dist/scripts/analysis-framework/political-classification.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/political-classification.js +0 -414
- package/dist/scripts/analysis-framework/political-classification.js.map +0 -1
- package/dist/scripts/analysis-framework/political-risk-assessment.d.ts +0 -74
- package/dist/scripts/analysis-framework/political-risk-assessment.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/political-risk-assessment.js +0 -656
- package/dist/scripts/analysis-framework/political-risk-assessment.js.map +0 -1
- package/dist/scripts/analysis-framework/political-threat-analysis.d.ts +0 -65
- package/dist/scripts/analysis-framework/political-threat-analysis.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/political-threat-analysis.js +0 -553
- package/dist/scripts/analysis-framework/political-threat-analysis.js.map +0 -1
- package/dist/scripts/analysis-framework/significance-scorer.d.ts +0 -62
- package/dist/scripts/analysis-framework/significance-scorer.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/significance-scorer.js +0 -196
- package/dist/scripts/analysis-framework/significance-scorer.js.map +0 -1
- package/dist/scripts/analysis-framework/types.d.ts +0 -138
- package/dist/scripts/analysis-framework/types.d.ts.map +0 -1
- package/dist/scripts/analysis-framework/types.js +0 -12
- package/dist/scripts/analysis-framework/types.js.map +0 -1
- package/dist/scripts/data-transformers/content-generators/ai-swot-analyzer.d.ts +0 -89
- package/dist/scripts/data-transformers/content-generators/ai-swot-analyzer.d.ts.map +0 -1
- package/dist/scripts/data-transformers/content-generators/ai-swot-analyzer.js +0 -667
- package/dist/scripts/data-transformers/content-generators/ai-swot-analyzer.js.map +0 -1
- package/dist/scripts/data-transformers/content-generators/stakeholder-swot-section.d.ts +0 -83
- package/dist/scripts/data-transformers/content-generators/stakeholder-swot-section.d.ts.map +0 -1
- package/dist/scripts/data-transformers/content-generators/stakeholder-swot-section.js +0 -219
- package/dist/scripts/data-transformers/content-generators/stakeholder-swot-section.js.map +0 -1
|
@@ -1,1001 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module ai-analysis/document-analyzer
|
|
3
|
-
* @description AI-powered comprehensive document analysis framework for
|
|
4
|
-
* parliamentary documents, government propositions, and policy papers.
|
|
5
|
-
*
|
|
6
|
-
* Provides multi-stakeholder impact assessment, PESTLE analysis, coalition
|
|
7
|
-
* dynamics, historical context, implementation feasibility, risk assessment,
|
|
8
|
-
* and confidence scoring through a multi-iteration analysis protocol.
|
|
9
|
-
*
|
|
10
|
-
* The framework is the shared analytical backbone consumed by all content
|
|
11
|
-
* generators and agentic workflows for consistent, high-quality political
|
|
12
|
-
* intelligence.
|
|
13
|
-
*
|
|
14
|
-
* @author Hack23 AB
|
|
15
|
-
* @license Apache-2.0
|
|
16
|
-
*/
|
|
17
|
-
import { createHash } from 'node:crypto';
|
|
18
|
-
import { detectPolicyDomains, assessConfidenceLevel, DOMAIN_NAME_TO_KEY, } from '../data-transformers/policy-analysis.js';
|
|
19
|
-
import { calculateInfluenceScore } from '../data-transformers/document-analysis.js';
|
|
20
|
-
import { calculateCoalitionRiskIndex } from '../data-transformers/risk-analysis.js';
|
|
21
|
-
import { extractKeyPassage, generateEnhancedSummary, normalizePartyKey, } from '../data-transformers/helpers.js';
|
|
22
|
-
import { escapeHtml } from '../html-utils.js';
|
|
23
|
-
import { getCurrentRiksmote } from '../news-types/motions.js';
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Analysis cache
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
const _analysisCache = new Map();
|
|
28
|
-
/**
|
|
29
|
-
* Maximum number of entries retained in `_analysisCache`.
|
|
30
|
-
* When the limit is reached the oldest entry (first inserted — Map iteration
|
|
31
|
-
* order) is evicted. This prevents unbounded memory growth in long-running
|
|
32
|
-
* processes or large batch runs.
|
|
33
|
-
*/
|
|
34
|
-
export const MAX_CACHE_SIZE = 500;
|
|
35
|
-
/** Minimum relevance score assigned to any detected policy domain. */
|
|
36
|
-
const MIN_DOMAIN_RELEVANCE = 30;
|
|
37
|
-
/** Maximum (baseline) relevance score for the first detected domain. */
|
|
38
|
-
const MAX_DOMAIN_RELEVANCE = 100;
|
|
39
|
-
/** Per-rank decay applied to successive domain relevance scores. */
|
|
40
|
-
const DOMAIN_RELEVANCE_DECAY = 15;
|
|
41
|
-
/** Clear the in-process analysis cache (useful for testing). */
|
|
42
|
-
export function clearAnalysisCache() {
|
|
43
|
-
_analysisCache.clear();
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* WeakMap cache for content fingerprints, storing both the SHA-256 digest
|
|
47
|
-
* and a lightweight guard derived from content field lengths.
|
|
48
|
-
*
|
|
49
|
-
* On cache hit the guard is re-checked against the current field lengths.
|
|
50
|
-
* If any content field was mutated in place (e.g. `fullText` added after
|
|
51
|
-
* an initial metadata-only analysis) the guard will mismatch and the
|
|
52
|
-
* fingerprint is recomputed. This prevents stale fingerprints when
|
|
53
|
-
* documents are enriched by mutation (the production enrichment path).
|
|
54
|
-
*/
|
|
55
|
-
const _fingerprintCache = new WeakMap();
|
|
56
|
-
/**
|
|
57
|
-
* Lightweight signature of content **and metadata** field lengths/values for
|
|
58
|
-
* mutation detection. Covers both the content fields hashed by
|
|
59
|
-
* `contentFingerprint()` and the metadata fields that affect analysis output
|
|
60
|
-
* (stakeholder selection, executive summary, PESTLE, etc.).
|
|
61
|
-
*/
|
|
62
|
-
function contentGuard(doc) {
|
|
63
|
-
return [
|
|
64
|
-
// Content fields
|
|
65
|
-
doc.fullText?.length ?? -1,
|
|
66
|
-
doc.summary?.length ?? -1,
|
|
67
|
-
doc.fullContent?.length ?? -1,
|
|
68
|
-
doc.notis?.length ?? -1,
|
|
69
|
-
// Metadata fields that affect analysis output
|
|
70
|
-
doc.titel?.length ?? -1,
|
|
71
|
-
doc.title?.length ?? -1,
|
|
72
|
-
doc.rubrik?.length ?? -1,
|
|
73
|
-
doc.doktyp ?? '',
|
|
74
|
-
doc.parti ?? '',
|
|
75
|
-
doc.datum ?? '',
|
|
76
|
-
doc.rm ?? '',
|
|
77
|
-
doc.organ ?? '',
|
|
78
|
-
doc.mottagare ?? '',
|
|
79
|
-
].join('|');
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Return a truncated SHA-256 hex digest of the document's content **and
|
|
83
|
-
* metadata** fields.
|
|
84
|
-
*
|
|
85
|
-
* Used in cache keys so that documents enriched with `fullText`/`fullContent`
|
|
86
|
-
* after an initial metadata-only fetch — or whose metadata changes between
|
|
87
|
-
* calls (e.g. corrected `doktyp`, updated `parti`) — produce a different
|
|
88
|
-
* fingerprint and therefore receive a fresh analysis instead of a stale one.
|
|
89
|
-
*
|
|
90
|
-
* A real hash (vs. simple length sums) ensures that different content of the
|
|
91
|
-
* same combined length cannot collide.
|
|
92
|
-
*
|
|
93
|
-
* Results are cached in a WeakMap keyed by object identity **plus** a
|
|
94
|
-
* lightweight content/metadata guard. When a document object is mutated in
|
|
95
|
-
* place (e.g. `doc.fullText = "..."` added during enrichment, or metadata
|
|
96
|
-
* corrected) the guard changes, triggering a re-hash so that subsequent
|
|
97
|
-
* `analyzeDocument()` calls receive a fresh analysis instead of a stale one.
|
|
98
|
-
*/
|
|
99
|
-
function contentFingerprint(doc) {
|
|
100
|
-
const guard = contentGuard(doc);
|
|
101
|
-
const cached = _fingerprintCache.get(doc);
|
|
102
|
-
if (cached !== undefined && cached.guard === guard)
|
|
103
|
-
return cached.fp;
|
|
104
|
-
const payload = [
|
|
105
|
-
// Content fields
|
|
106
|
-
doc.fullText ?? '',
|
|
107
|
-
doc.summary ?? '',
|
|
108
|
-
doc.fullContent ?? '',
|
|
109
|
-
doc.notis ?? '',
|
|
110
|
-
// Metadata fields that affect analysis output
|
|
111
|
-
doc.titel ?? '',
|
|
112
|
-
doc.title ?? '',
|
|
113
|
-
doc.rubrik ?? '',
|
|
114
|
-
doc.doktyp ?? '',
|
|
115
|
-
doc.parti ?? '',
|
|
116
|
-
doc.datum ?? '',
|
|
117
|
-
doc.rm ?? '',
|
|
118
|
-
doc.organ ?? '',
|
|
119
|
-
doc.mottagare ?? '',
|
|
120
|
-
].join('\x00');
|
|
121
|
-
const fp = createHash('sha256').update(payload).digest('hex').slice(0, 32);
|
|
122
|
-
_fingerprintCache.set(doc, { fp, guard });
|
|
123
|
-
return fp;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Return a base identity string for a document (no lang/context suffix).
|
|
127
|
-
*
|
|
128
|
-
* This is the human-recognizable, stable document identity used for
|
|
129
|
-
* `documentId` in results and Map keys in `analyzeDocuments()`.
|
|
130
|
-
*/
|
|
131
|
-
function documentBaseKey(doc, fp) {
|
|
132
|
-
if (doc.dok_id)
|
|
133
|
-
return `dok:${doc.dok_id}`;
|
|
134
|
-
if (doc.url)
|
|
135
|
-
return `url:${doc.url}`;
|
|
136
|
-
const title = doc.titel ?? doc.title ?? 'unknown';
|
|
137
|
-
const date = doc.datum ?? '';
|
|
138
|
-
return `title:${title}-${date}-${fp ?? contentFingerprint(doc)}`;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Return a short stable discriminator string for a CIAContext.
|
|
142
|
-
*
|
|
143
|
-
* Different CIA contexts (e.g. different coalition-stability scores or
|
|
144
|
-
* overall motion denial rates) produce a different discriminator so that
|
|
145
|
-
* cached analyses are not reused across meaningfully different intelligence
|
|
146
|
-
* inputs. The discriminator is a truncated SHA-256 hex digest (8 chars)
|
|
147
|
-
* of the key numeric fields.
|
|
148
|
-
*/
|
|
149
|
-
function ciaDiscriminator(ctx) {
|
|
150
|
-
const payload = [
|
|
151
|
-
ctx.coalitionStability?.stabilityScore ?? 0,
|
|
152
|
-
ctx.coalitionStability?.defectionProbability ?? 0,
|
|
153
|
-
ctx.coalitionStability?.majorityMargin ?? 0,
|
|
154
|
-
ctx.overallMotionDenialRate ?? 0,
|
|
155
|
-
].join('|');
|
|
156
|
-
return createHash('sha256').update(payload).digest('hex').slice(0, 8);
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Return the cache key for a document.
|
|
160
|
-
*
|
|
161
|
-
* The key includes the document identity *plus* a content fingerprint,
|
|
162
|
-
* `lang`, and a CIA-context discriminator so that:
|
|
163
|
-
* - analyses with different languages or CIA context get separate slots
|
|
164
|
-
* - different CIA contexts (e.g. different coalition stability or denial
|
|
165
|
-
* rates) produce separate cache slots instead of returning stale results
|
|
166
|
-
* - enriched documents (with `fullText`/`fullContent` added after initial
|
|
167
|
-
* metadata-only fetch) are not served stale pre-enrichment results
|
|
168
|
-
*
|
|
169
|
-
* The fingerprint is computed once and reused in both the base key (for
|
|
170
|
-
* title-only fallback) and the cache key suffix to avoid redundant SHA-256
|
|
171
|
-
* work.
|
|
172
|
-
*
|
|
173
|
-
* @returns a tuple of [cacheKey, fingerprint] so callers can reuse the fp
|
|
174
|
-
*/
|
|
175
|
-
function cacheKeyAndFp(doc, lang = 'en', ciaContext) {
|
|
176
|
-
const fp = contentFingerprint(doc);
|
|
177
|
-
const ciaDisc = ciaContext ? ciaDiscriminator(ciaContext) : '0';
|
|
178
|
-
return [`${documentBaseKey(doc, fp)}|${fp}|${lang}|cia:${ciaDisc}`, fp];
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Derive a stable, collision-resistant document identifier.
|
|
182
|
-
* Unlike `cacheKeyAndFp()`, this is independent of language and CIA context —
|
|
183
|
-
* it identifies the document itself, not a particular analysis of it.
|
|
184
|
-
*
|
|
185
|
-
* @param doc - Raw document
|
|
186
|
-
* @param fp - Precomputed content fingerprint (avoids redundant SHA-256)
|
|
187
|
-
*/
|
|
188
|
-
function stableDocumentId(doc, fp) {
|
|
189
|
-
return documentBaseKey(doc, fp);
|
|
190
|
-
}
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Stakeholder display names (14 languages)
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
const STAKEHOLDER_NAMES = {
|
|
195
|
-
'government-coalition': {
|
|
196
|
-
en: 'Government Coalition', sv: 'Regeringskoalitionen', da: 'Regeringskoalitionen',
|
|
197
|
-
no: 'Regjeringskoalisjonen', fi: 'Hallituskoalitio', de: 'Regierungskoalition',
|
|
198
|
-
fr: 'Coalition gouvernementale', es: 'Coalición de gobierno', nl: 'Regeringscoalitie',
|
|
199
|
-
ar: 'الائتلاف الحكومي', he: 'קואליציה ממשלתית', ja: '与党連立', ko: '연립정부', zh: '执政联盟',
|
|
200
|
-
},
|
|
201
|
-
'opposition-parties': {
|
|
202
|
-
en: 'Opposition Parties', sv: 'Oppositionspartier', da: 'Oppositionspartier',
|
|
203
|
-
no: 'Opposisjonspartier', fi: 'Oppositiopuolueet', de: 'Oppositionsparteien',
|
|
204
|
-
fr: "Partis d'opposition", es: 'Partidos de oposición', nl: 'Oppositiepartijen',
|
|
205
|
-
ar: 'أحزاب المعارضة', he: 'מפלגות האופוזיציה', ja: '野党', ko: '야당', zh: '反对党',
|
|
206
|
-
},
|
|
207
|
-
'state-agencies': {
|
|
208
|
-
en: 'State Agencies', sv: 'Statliga myndigheter', da: 'Statslige myndigheder',
|
|
209
|
-
no: 'Statlige etater', fi: 'Valtion virastot', de: 'Staatliche Behörden',
|
|
210
|
-
fr: 'Agences de l\'État', es: 'Agencias estatales', nl: 'Overheidsinstanties',
|
|
211
|
-
ar: 'الوكالات الحكومية', he: 'סוכנויות המדינה', ja: '国家機関', ko: '국가 기관', zh: '政府机构',
|
|
212
|
-
},
|
|
213
|
-
'municipalities-regions': {
|
|
214
|
-
en: 'Municipalities & Regions', sv: 'Kommuner och regioner', da: 'Kommuner og regioner',
|
|
215
|
-
no: 'Kommuner og regioner', fi: 'Kunnat ja alueet', de: 'Gemeinden und Regionen',
|
|
216
|
-
fr: 'Municipalités et régions', es: 'Municipios y regiones', nl: 'Gemeenten en regio\'s',
|
|
217
|
-
ar: 'البلديات والمناطق', he: 'עיריות ואזורים', ja: '市町村・地域', ko: '지방자치단체·지역', zh: '市镇与地区',
|
|
218
|
-
},
|
|
219
|
-
'private-sector': {
|
|
220
|
-
en: 'Private Sector', sv: 'Näringsliv', da: 'Erhvervsliv', no: 'Næringsliv',
|
|
221
|
-
fi: 'Yksityinen sektori', de: 'Privatwirtschaft', fr: 'Secteur privé', es: 'Sector privado',
|
|
222
|
-
nl: 'Bedrijfsleven', ar: 'القطاع الخاص', he: 'המגזר הפרטי', ja: '民間企業', ko: '민간 부문', zh: '私营部门',
|
|
223
|
-
},
|
|
224
|
-
'labor-market': {
|
|
225
|
-
en: 'Labour Market (Unions)', sv: 'Arbetsmarknad (fackförbund)', da: 'Arbejdsmarked (fagforeninger)',
|
|
226
|
-
no: 'Arbeidsmarked (fagforeninger)', fi: 'Työmarkkinat (ammattiliitot)', de: 'Arbeitsmarkt (Gewerkschaften)',
|
|
227
|
-
fr: 'Marché du travail (syndicats)', es: 'Mercado laboral (sindicatos)', nl: 'Arbeidsmarkt (vakbonden)',
|
|
228
|
-
ar: 'سوق العمل (النقابات)', he: 'שוק העבודה (איגודי עובדים)', ja: '労働市場(労働組合)', ko: '노동 시장(노조)', zh: '劳动力市场(工会)',
|
|
229
|
-
},
|
|
230
|
-
'civil-society': {
|
|
231
|
-
en: 'Civil Society', sv: 'Civilsamhälle', da: 'Civilsamfund', no: 'Sivilsamfunn',
|
|
232
|
-
fi: 'Kansalaisyhteiskunta', de: 'Zivilgesellschaft', fr: 'Société civile', es: 'Sociedad civil',
|
|
233
|
-
nl: 'Maatschappelijk middenveld', ar: 'المجتمع المدني', he: 'חברה אזרחית', ja: '市民社会', ko: '시민 사회', zh: '公民社会',
|
|
234
|
-
},
|
|
235
|
-
'international-eu': {
|
|
236
|
-
en: 'International / EU', sv: 'Internationellt / EU', da: 'Internationalt / EU',
|
|
237
|
-
no: 'Internasjonalt / EU', fi: 'Kansainvälinen / EU', de: 'International / EU',
|
|
238
|
-
fr: 'International / UE', es: 'Internacional / UE', nl: 'Internationaal / EU',
|
|
239
|
-
ar: 'دولي / الاتحاد الأوروبي', he: 'בינלאומי / האיחוד האירופי', ja: '国際・EU', ko: '국제 / EU', zh: '国际/欧盟',
|
|
240
|
-
},
|
|
241
|
-
'media-press': {
|
|
242
|
-
en: 'Media & Press', sv: 'Medier och press', da: 'Medier og presse', no: 'Medier og presse',
|
|
243
|
-
fi: 'Media ja lehdistö', de: 'Medien und Presse', fr: 'Médias et presse', es: 'Medios y prensa',
|
|
244
|
-
nl: 'Media en pers', ar: 'الإعلام والصحافة', he: 'תקשורת ועיתונות', ja: 'メディア・報道', ko: '미디어·언론', zh: '媒体与新闻',
|
|
245
|
-
},
|
|
246
|
-
'academia-research': {
|
|
247
|
-
en: 'Academia & Research', sv: 'Akademi och forskning', da: 'Akademi og forskning',
|
|
248
|
-
no: 'Akademia og forskning', fi: 'Akateeminen maailma ja tutkimus', de: 'Wissenschaft und Forschung',
|
|
249
|
-
fr: 'Monde académique et recherche', es: 'Academia e investigación', nl: 'Academische wereld en onderzoek',
|
|
250
|
-
ar: 'الأوساط الأكاديمية والبحث', he: 'אקדמיה ומחקר', ja: '学術・研究', ko: '학계·연구', zh: '学术界与研究',
|
|
251
|
-
},
|
|
252
|
-
'citizens-voters': {
|
|
253
|
-
en: 'Citizens & Voters', sv: 'Medborgare och väljare', da: 'Borgere og vælgere',
|
|
254
|
-
no: 'Borgere og velgere', fi: 'Kansalaiset ja äänestäjät', de: 'Bürger und Wähler',
|
|
255
|
-
fr: 'Citoyens et électeurs', es: 'Ciudadanos y votantes', nl: 'Burgers en kiezers',
|
|
256
|
-
ar: 'المواطنون والناخبون', he: 'אזרחים ובוחרים', ja: '市民・有権者', ko: '시민·유권자', zh: '公民与选民',
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
/** Resolve localised display name for a stakeholder group. */
|
|
260
|
-
function stakeholderName(group, lang) {
|
|
261
|
-
return STAKEHOLDER_NAMES[group]?.[lang]
|
|
262
|
-
?? STAKEHOLDER_NAMES[group]?.en
|
|
263
|
-
?? group;
|
|
264
|
-
}
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// Domain key ↔ localised name helpers (reuses canonical mapping from
|
|
267
|
-
// policy-analysis.ts via DOMAIN_NAME_TO_KEY — no duplication)
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
/**
|
|
270
|
-
* Check whether a domain key (e.g. 'healthcare') is present in the array
|
|
271
|
-
* returned by `detectPolicyDomains()`. Since `detectPolicyDomains` returns
|
|
272
|
-
* localised display names, this helper uses the canonical reverse mapping
|
|
273
|
-
* (`DOMAIN_NAME_TO_KEY`) to convert each localised name back to its key
|
|
274
|
-
* before comparison — so it works correctly regardless of language.
|
|
275
|
-
*/
|
|
276
|
-
function hasDomain(domains, key) {
|
|
277
|
-
return domains.some(d => {
|
|
278
|
-
const canonicalKey = DOMAIN_NAME_TO_KEY[d] ?? DOMAIN_NAME_TO_KEY[d.toLowerCase()];
|
|
279
|
-
if (canonicalKey === key)
|
|
280
|
-
return true;
|
|
281
|
-
// Fallback: substring match for unknown/un-mapped entries
|
|
282
|
-
return d.toLowerCase().includes(key.toLowerCase());
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Derive canonical domain keys from a localised domain name array.
|
|
287
|
-
* Returns `[canonicalKey, localisedDisplayName]` tuples.
|
|
288
|
-
*/
|
|
289
|
-
function toDomainKeyPairs(domains) {
|
|
290
|
-
return domains.map(d => ({
|
|
291
|
-
key: DOMAIN_NAME_TO_KEY[d] ?? DOMAIN_NAME_TO_KEY[d.toLowerCase()] ?? d,
|
|
292
|
-
name: d,
|
|
293
|
-
}));
|
|
294
|
-
}
|
|
295
|
-
/** Document-content-based relevance signals for each stakeholder group. */
|
|
296
|
-
const STAKEHOLDER_SIGNALS = {
|
|
297
|
-
'government-coalition': ['proposition', 'regering', 'budget', 'statsminister', 'minister'],
|
|
298
|
-
'opposition-parties': ['motion', 'opposition', 'socialdemokrat', 'vänsterpartiet', 'centerpartiet', 'miljöpartiet'],
|
|
299
|
-
'state-agencies': ['myndighet', 'länsstyrelse', 'domstol', 'polis', 'skatteverket', 'folkhälsomyndighet'],
|
|
300
|
-
'municipalities-regions': ['kommuner', 'regioner', 'landsting', 'primärkommunal'],
|
|
301
|
-
'private-sector': ['näringsliv', 'företag', 'arbetsgivare', 'industry', 'handel', 'marknad'],
|
|
302
|
-
'labor-market': ['facket', 'fackförbund', 'arbetsrätt', 'arbetsmarknad', 'lo', 'tco', 'saco'],
|
|
303
|
-
'civil-society': ['civilsamhälle', 'ngo', 'ideell', 'förening', 'frivillig'],
|
|
304
|
-
'international-eu': ['europa', 'nato', 'nordisk', 'internationell', 'eu', 'fn'],
|
|
305
|
-
'media-press': ['offentlighet', 'tryckfrihet', 'medier', 'yttrandefrihet', 'journalistik'],
|
|
306
|
-
'academia-research': ['forskning', 'universitet', 'vetenskap', 'kunskapsunderlag'],
|
|
307
|
-
'citizens-voters': ['medborgare', 'allmänhet', 'val', 'röst'],
|
|
308
|
-
};
|
|
309
|
-
/** Max length for a signal to be considered "short" and require word-boundary matching. */
|
|
310
|
-
const SHORT_SIGNAL_MAX_LEN = 4;
|
|
311
|
-
/** Pre-compiled word-boundary regexes for short signals (≤ SHORT_SIGNAL_MAX_LEN chars). */
|
|
312
|
-
const shortSignalRegexCache = new Map();
|
|
313
|
-
/** Escape regex metacharacters in a string. */
|
|
314
|
-
function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
315
|
-
/**
|
|
316
|
-
* Check whether `text` contains a signal term, using word-boundary regex for
|
|
317
|
-
* short tokens (≤ {@link SHORT_SIGNAL_MAX_LEN} chars) to avoid substring false
|
|
318
|
-
* positives (e.g. "lo" matching "lokalt", "skr" matching "skrivelse").
|
|
319
|
-
*/
|
|
320
|
-
function matchesSignal(text, signal) {
|
|
321
|
-
if (signal.length > SHORT_SIGNAL_MAX_LEN)
|
|
322
|
-
return text.includes(signal);
|
|
323
|
-
let re = shortSignalRegexCache.get(signal);
|
|
324
|
-
if (!re) {
|
|
325
|
-
re = new RegExp(`\\b${escapeRegExp(signal)}\\b`, 'i');
|
|
326
|
-
shortSignalRegexCache.set(signal, re);
|
|
327
|
-
}
|
|
328
|
-
return re.test(text);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Determine which stakeholder groups are relevant for a document.
|
|
332
|
-
* Government coalition, opposition parties, and citizens/voters are always
|
|
333
|
-
* included; other groups are added based on document-content signals.
|
|
334
|
-
*/
|
|
335
|
-
export function selectRelevantStakeholders(doc) {
|
|
336
|
-
const alwaysIncluded = ['government-coalition', 'opposition-parties', 'citizens-voters'];
|
|
337
|
-
const text = [
|
|
338
|
-
doc.titel, doc.title, doc.rubrik, doc.summary, doc.notis, doc.fullText, doc.fullContent,
|
|
339
|
-
].filter(Boolean).join(' ').toLowerCase();
|
|
340
|
-
const optional = [
|
|
341
|
-
'state-agencies', 'municipalities-regions', 'private-sector',
|
|
342
|
-
'labor-market', 'civil-society', 'international-eu',
|
|
343
|
-
'media-press', 'academia-research',
|
|
344
|
-
];
|
|
345
|
-
const relevant = optional.filter(group => {
|
|
346
|
-
const signals = STAKEHOLDER_SIGNALS[group] ?? [];
|
|
347
|
-
return signals.some(s => matchesSignal(text, s));
|
|
348
|
-
});
|
|
349
|
-
// Deduplicate, preserving order: always-included first then optional relevants
|
|
350
|
-
const seen = new Set();
|
|
351
|
-
const result = [];
|
|
352
|
-
for (const g of [...alwaysIncluded, ...relevant]) {
|
|
353
|
-
if (!seen.has(g)) {
|
|
354
|
-
seen.add(g);
|
|
355
|
-
result.push(g);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return result;
|
|
359
|
-
}
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
|
-
// SWOT builder
|
|
362
|
-
// ---------------------------------------------------------------------------
|
|
363
|
-
/** Build a SWOT data structure for a stakeholder given document metadata. */
|
|
364
|
-
function buildStakeholderSwot(group, doc, lang, ciaContext, ctx) {
|
|
365
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
366
|
-
const isGovernment = docType === 'prop';
|
|
367
|
-
const isMotion = docType === 'mot';
|
|
368
|
-
const party = normalizePartyKey(doc.parti);
|
|
369
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
370
|
-
// Government coalition SWOT
|
|
371
|
-
if (group === 'government-coalition') {
|
|
372
|
-
const stabilityScore = ciaContext?.coalitionStability?.stabilityScore ?? 70;
|
|
373
|
-
return {
|
|
374
|
-
subject: stakeholderName(group, lang),
|
|
375
|
-
strengths: [
|
|
376
|
-
{ text: isGovernment ? 'Advances governing agenda through this proposition' : 'May leverage document in policy debate', impact: 'high' },
|
|
377
|
-
{ text: `Coalition stability score: ${stabilityScore}/100`, impact: stabilityScore >= 60 ? 'high' : 'medium' },
|
|
378
|
-
],
|
|
379
|
-
weaknesses: [
|
|
380
|
-
{ text: isMotion ? 'Opposition motion challenges policy direction' : 'Requires parliamentary majority support', impact: 'medium' },
|
|
381
|
-
{ text: 'Implementation burden may draw public criticism', impact: 'low' },
|
|
382
|
-
],
|
|
383
|
-
opportunities: [
|
|
384
|
-
{ text: 'Can shape public narrative around document priorities', impact: 'high' },
|
|
385
|
-
{ text: domains.length > 0 ? `Policy win across ${domains.slice(0, 2).join(', ')} domains` : 'Broad policy opportunity', impact: 'medium' },
|
|
386
|
-
],
|
|
387
|
-
threats: [
|
|
388
|
-
{ text: 'Opposition scrutiny and counter-motions', impact: 'medium' },
|
|
389
|
-
{ text: 'Media and public accountability expectations', impact: 'low' },
|
|
390
|
-
],
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
// Opposition SWOT
|
|
394
|
-
if (group === 'opposition-parties') {
|
|
395
|
-
const partyLabel = party && party !== 'other' ? `Party ${party.toUpperCase()}` : 'The opposition';
|
|
396
|
-
return {
|
|
397
|
-
subject: stakeholderName(group, lang),
|
|
398
|
-
strengths: [
|
|
399
|
-
{ text: isMotion ? `${partyLabel} actively engaged through motions` : 'Can hold government accountable through debate', impact: 'high' },
|
|
400
|
-
{ text: 'Democratic scrutiny role provides legitimacy', impact: 'medium' },
|
|
401
|
-
],
|
|
402
|
-
weaknesses: [
|
|
403
|
-
{ text: 'Limited formal power to block government propositions', impact: 'high' },
|
|
404
|
-
{ text: 'Internal coalition divisions may weaken unified response', impact: 'medium' },
|
|
405
|
-
],
|
|
406
|
-
opportunities: [
|
|
407
|
-
{ text: 'Policy failures in implementation create electoral openings', impact: 'high' },
|
|
408
|
-
{ text: 'Can propose alternatives that resonate with voters', impact: 'medium' },
|
|
409
|
-
],
|
|
410
|
-
threats: [
|
|
411
|
-
{ text: 'Government controls parliamentary calendar and agenda', impact: 'medium' },
|
|
412
|
-
{ text: 'Cross-party votes could undermine opposition unity', impact: 'low' },
|
|
413
|
-
],
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
// Generic stakeholder SWOT — domain-aware
|
|
417
|
-
const hasHighInfluence = (ctx?.influenceScore ?? calculateInfluenceScore(doc)) > 60;
|
|
418
|
-
return {
|
|
419
|
-
subject: stakeholderName(group, lang),
|
|
420
|
-
strengths: [
|
|
421
|
-
{ text: 'Established institutional capacity to respond', impact: 'medium' },
|
|
422
|
-
{ text: 'Domain expertise in relevant policy areas', impact: 'medium' },
|
|
423
|
-
],
|
|
424
|
-
weaknesses: [
|
|
425
|
-
{ text: hasHighInfluence ? 'High-influence document may impose significant obligations' : 'Moderate implementation requirements', impact: hasHighInfluence ? 'high' : 'medium' },
|
|
426
|
-
],
|
|
427
|
-
opportunities: [
|
|
428
|
-
{ text: 'Influence implementation guidelines and secondary legislation', impact: 'medium' },
|
|
429
|
-
],
|
|
430
|
-
threats: [
|
|
431
|
-
{ text: 'Resource constraints may limit effective engagement', impact: 'medium' },
|
|
432
|
-
{ text: 'Timeline pressures during legislative implementation', impact: 'low' },
|
|
433
|
-
],
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
// Stakeholder impact builder
|
|
438
|
-
// ---------------------------------------------------------------------------
|
|
439
|
-
/** Derive direct-impact direction from document type and stakeholder role. */
|
|
440
|
-
function deriveImpactDirection(group, doc) {
|
|
441
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
442
|
-
if (docType === 'prop') {
|
|
443
|
-
if (group === 'government-coalition')
|
|
444
|
-
return 'positive';
|
|
445
|
-
if (group === 'opposition-parties')
|
|
446
|
-
return 'mixed';
|
|
447
|
-
}
|
|
448
|
-
if (docType === 'mot') {
|
|
449
|
-
if (group === 'government-coalition')
|
|
450
|
-
return 'mixed';
|
|
451
|
-
if (group === 'opposition-parties')
|
|
452
|
-
return 'positive';
|
|
453
|
-
}
|
|
454
|
-
return 'neutral';
|
|
455
|
-
}
|
|
456
|
-
/** Build complete stakeholder impact for one group. */
|
|
457
|
-
function buildStakeholderImpact(group, doc, lang, ciaContext, ctx) {
|
|
458
|
-
const direction = deriveImpactDirection(group, doc);
|
|
459
|
-
const influenceScore = ctx?.influenceScore ?? calculateInfluenceScore(doc);
|
|
460
|
-
const magnitude = influenceScore >= 65 ? 'significant' : influenceScore >= 35 ? 'moderate' : 'minor';
|
|
461
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
462
|
-
const domainStr = domains.slice(0, 2).join(', ') || 'general policy';
|
|
463
|
-
const displayName = stakeholderName(group, lang);
|
|
464
|
-
const summaryMap = {
|
|
465
|
-
positive: `${displayName} stands to benefit from this document's provisions.`,
|
|
466
|
-
negative: `${displayName} faces constraints or burdens from this document.`,
|
|
467
|
-
neutral: `${displayName} experiences limited direct impact.`,
|
|
468
|
-
mixed: `${displayName} faces both opportunities and challenges.`,
|
|
469
|
-
};
|
|
470
|
-
const indirectEffects = [];
|
|
471
|
-
if (group === 'citizens-voters') {
|
|
472
|
-
indirectEffects.push(`Policy changes in ${domainStr} may affect daily life.`);
|
|
473
|
-
}
|
|
474
|
-
if (group === 'state-agencies') {
|
|
475
|
-
indirectEffects.push('New reporting or enforcement obligations may arise.');
|
|
476
|
-
}
|
|
477
|
-
if (group === 'municipalities-regions') {
|
|
478
|
-
indirectEffects.push('Local implementation burden requires resource planning.');
|
|
479
|
-
}
|
|
480
|
-
const burden = group === 'state-agencies' || group === 'municipalities-regions'
|
|
481
|
-
? magnitude === 'significant' ? 'high' : 'medium'
|
|
482
|
-
: 'low';
|
|
483
|
-
const politicalImplications = group === 'government-coalition'
|
|
484
|
-
? 'Aligns with governing-coalition policy priorities and provides legislative track record.'
|
|
485
|
-
: group === 'opposition-parties'
|
|
486
|
-
? 'Provides opportunity to differentiate policy positions ahead of elections.'
|
|
487
|
-
: `Stakeholder engagement may shape implementation and secondary legislation in ${domainStr}.`;
|
|
488
|
-
const confidence = assessConfidenceLevel(domains.length + ((doc.fullText || doc.fullContent) ? 3 : 0), influenceScore);
|
|
489
|
-
return {
|
|
490
|
-
stakeholder: group,
|
|
491
|
-
displayName,
|
|
492
|
-
directImpact: { direction, magnitude, summary: summaryMap[direction] },
|
|
493
|
-
indirectEffects,
|
|
494
|
-
implementationBurden: burden,
|
|
495
|
-
politicalImplications,
|
|
496
|
-
swot: buildStakeholderSwot(group, doc, lang, ciaContext, ctx),
|
|
497
|
-
confidence,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
// ---------------------------------------------------------------------------
|
|
501
|
-
// PESTLE builder
|
|
502
|
-
// ---------------------------------------------------------------------------
|
|
503
|
-
/** Build PESTLE analysis dimensions from a document. */
|
|
504
|
-
export function buildPestleAnalysis(doc, lang, ctx) {
|
|
505
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
506
|
-
const title = doc.titel ?? doc.title ?? doc.rubrik ?? '';
|
|
507
|
-
const titleLower = title.toLowerCase();
|
|
508
|
-
// Reuse precomputed domains when available; otherwise detect with the
|
|
509
|
-
// caller's language. `hasDomain()` uses `DOMAIN_NAME_TO_KEY` which is
|
|
510
|
-
// language-agnostic (covers all 14 languages), so domain-trigger checks
|
|
511
|
-
// work reliably regardless of which language the domains were detected in.
|
|
512
|
-
const triggerDomains = ctx?.domains ?? detectPolicyDomains(doc, lang ?? 'en');
|
|
513
|
-
const political = [
|
|
514
|
-
docType === 'prop'
|
|
515
|
-
? 'Government-sponsored legislation — signals executive policy priority.'
|
|
516
|
-
: docType === 'mot'
|
|
517
|
-
? 'Opposition-initiated motion — reflects parliamentary accountability mechanism.'
|
|
518
|
-
: 'Parliamentary document shapes political agenda.',
|
|
519
|
-
];
|
|
520
|
-
if (hasDomain(triggerDomains, 'defence'))
|
|
521
|
-
political.push('Intersects with national security and defence strategy.');
|
|
522
|
-
if (hasDomain(triggerDomains, 'eu-foreign'))
|
|
523
|
-
political.push('EU/international obligations may constrain domestic policy space.');
|
|
524
|
-
const economic = [];
|
|
525
|
-
if (hasDomain(triggerDomains, 'fiscal'))
|
|
526
|
-
economic.push('Direct fiscal implications for state budget allocation.');
|
|
527
|
-
if (hasDomain(triggerDomains, 'labour'))
|
|
528
|
-
economic.push('Employment and wage effects across affected sectors.');
|
|
529
|
-
if (hasDomain(triggerDomains, 'trade'))
|
|
530
|
-
economic.push('Trade competitiveness and export/import dynamics affected.');
|
|
531
|
-
if (economic.length === 0)
|
|
532
|
-
economic.push('Indirect economic effects possible through regulatory changes.');
|
|
533
|
-
const social = [];
|
|
534
|
-
if (hasDomain(triggerDomains, 'healthcare'))
|
|
535
|
-
social.push('Healthcare access and quality of life implications.');
|
|
536
|
-
if (hasDomain(triggerDomains, 'education'))
|
|
537
|
-
social.push('Educational outcomes and social mobility effects.');
|
|
538
|
-
if (hasDomain(triggerDomains, 'migration'))
|
|
539
|
-
social.push('Migration flows and social cohesion dimensions.');
|
|
540
|
-
if (hasDomain(triggerDomains, 'housing'))
|
|
541
|
-
social.push('Housing availability and affordability impacts.');
|
|
542
|
-
if (social.length === 0)
|
|
543
|
-
social.push('Social equity and public service delivery effects possible.');
|
|
544
|
-
const technological = [
|
|
545
|
-
titleLower.includes('digital') || titleLower.includes('cyber') || /\b(?:IT|ICT)\b/i.test(title) || titleLower.includes('it-system')
|
|
546
|
-
? 'Digital infrastructure or technology governance dimensions present.'
|
|
547
|
-
: 'Technology adoption for implementation may be required.',
|
|
548
|
-
];
|
|
549
|
-
const legal = [
|
|
550
|
-
docType === 'prop' ? 'Proposed as primary legislation — will require Riksdag vote.' : 'May require amendments to existing statutes.',
|
|
551
|
-
];
|
|
552
|
-
if (hasDomain(triggerDomains, 'justice'))
|
|
553
|
-
legal.push('Criminal justice or rule-of-law provisions included.');
|
|
554
|
-
if (hasDomain(triggerDomains, 'eu-foreign'))
|
|
555
|
-
legal.push('EU Directive transposition obligations may apply.');
|
|
556
|
-
const environmental = [];
|
|
557
|
-
if (hasDomain(triggerDomains, 'environment')) {
|
|
558
|
-
environmental.push('Direct environmental and climate policy implications.');
|
|
559
|
-
environmental.push('May interact with EU Green Deal commitments.');
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
environmental.push('Indirect environmental effects through implementation activities.');
|
|
563
|
-
}
|
|
564
|
-
return { political, economic, social, technological, legal, environmental };
|
|
565
|
-
}
|
|
566
|
-
// ---------------------------------------------------------------------------
|
|
567
|
-
// Coalition dynamics builder
|
|
568
|
-
// ---------------------------------------------------------------------------
|
|
569
|
-
/** Derive coalition dynamics from document and CIA context. */
|
|
570
|
-
export function buildCoalitionDynamics(doc, ciaContext) {
|
|
571
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
572
|
-
const riskIndex = ciaContext ? calculateCoalitionRiskIndex(ciaContext) : null;
|
|
573
|
-
const riskLevel = riskIndex?.level ?? 'MEDIUM';
|
|
574
|
-
const governmentImpact = docType === 'prop' ? 'positive' : docType === 'mot' ? 'negative' : 'neutral';
|
|
575
|
-
const oppositionResponse = docType === 'prop'
|
|
576
|
-
? 'Opposition likely to scrutinise and file counter-motions. Committee stage will surface disagreements.'
|
|
577
|
-
: docType === 'mot'
|
|
578
|
-
? 'Opposition motion unlikely to pass given government majority, but raises public debate.'
|
|
579
|
-
: 'Debate and committee deliberation will follow standard parliamentary procedure.';
|
|
580
|
-
const stabilityEffect = riskLevel === 'CRITICAL' ? 'destabilising'
|
|
581
|
-
: riskLevel === 'HIGH' ? 'destabilising'
|
|
582
|
-
: docType === 'prop' ? 'stabilising'
|
|
583
|
-
: 'neutral';
|
|
584
|
-
const summary = riskIndex
|
|
585
|
-
? `Coalition risk: ${riskIndex.level} (score ${riskIndex.score}/100). ${riskIndex.summary}`
|
|
586
|
-
: 'No CIA coalition data available; assessment based on document type and parliamentary context.';
|
|
587
|
-
return {
|
|
588
|
-
governmentImpact,
|
|
589
|
-
oppositionResponse,
|
|
590
|
-
crossPartyPotential: riskLevel === 'HIGH' || riskLevel === 'CRITICAL',
|
|
591
|
-
stabilityEffect,
|
|
592
|
-
summary,
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
// ---------------------------------------------------------------------------
|
|
596
|
-
// Historical context builder
|
|
597
|
-
// ---------------------------------------------------------------------------
|
|
598
|
-
/** Build historical and legislative context (structural inference). */
|
|
599
|
-
export function buildHistoricalContext(doc, precomputedDomains) {
|
|
600
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
601
|
-
const domains = precomputedDomains ?? detectPolicyDomains(doc);
|
|
602
|
-
const precedents = [];
|
|
603
|
-
if (hasDomain(domains, 'fiscal'))
|
|
604
|
-
precedents.push('Swedish fiscal consolidation policies dating to the 1990s banking crisis reforms.');
|
|
605
|
-
if (hasDomain(domains, 'healthcare'))
|
|
606
|
-
precedents.push('Reforms following the 1992 Dagmar reform and 2010 Patient Safety Act.');
|
|
607
|
-
if (hasDomain(domains, 'defence'))
|
|
608
|
-
precedents.push('Defence spending trajectory since Sweden\'s 2022 NATO application.');
|
|
609
|
-
if (hasDomain(domains, 'migration'))
|
|
610
|
-
precedents.push('Migration policy tightening following 2015–16 refugee influx.');
|
|
611
|
-
if (precedents.length === 0)
|
|
612
|
-
precedents.push('Part of Sweden\'s continuous parliamentary legislative programme.');
|
|
613
|
-
const relatedLegislation = [];
|
|
614
|
-
if (docType === 'mot')
|
|
615
|
-
relatedLegislation.push('Responds to or complements government propositions currently in committee.');
|
|
616
|
-
if (docType === 'prop')
|
|
617
|
-
relatedLegislation.push('Will replace or amend existing statutes upon parliamentary approval.');
|
|
618
|
-
relatedLegislation.push('Related committee reports (betänkanden) expected in subsequent session.');
|
|
619
|
-
const riksmote = deriveRiksmote(doc);
|
|
620
|
-
const policyEvolution = domains.length > 0
|
|
621
|
-
? `This document advances policy in ${domains.slice(0, 3).join(', ')} — domains with active legislative activity in the ${riksmote} parliamentary session.`
|
|
622
|
-
: 'Part of the ongoing parliamentary work programme for the current session.';
|
|
623
|
-
return { precedents, relatedLegislation, policyEvolution };
|
|
624
|
-
}
|
|
625
|
-
// ---------------------------------------------------------------------------
|
|
626
|
-
// Implementation assessment builder
|
|
627
|
-
// ---------------------------------------------------------------------------
|
|
628
|
-
/** Build implementation feasibility assessment from document metadata. */
|
|
629
|
-
export function buildImplementationAssessment(doc, ctx) {
|
|
630
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
631
|
-
const influenceScore = ctx?.influenceScore ?? calculateInfluenceScore(doc);
|
|
632
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
633
|
-
const feasibility = docType === 'prop' && influenceScore < 45 ? 'high'
|
|
634
|
-
: docType === 'prop' ? 'medium' // influenceScore >= 45 — moderate or high complexity
|
|
635
|
-
: docType === 'mot' ? 'low'
|
|
636
|
-
: 'medium';
|
|
637
|
-
const timeline = docType === 'prop' ? '6–18 months from parliamentary approval to full implementation.'
|
|
638
|
-
: docType === 'mot' ? 'If adopted, 12–24 months given typical legislative cycle.'
|
|
639
|
-
: '12–18 months for regulatory follow-up and agency guidance.';
|
|
640
|
-
const keyObstacles = [];
|
|
641
|
-
if (hasDomain(domains, 'fiscal'))
|
|
642
|
-
keyObstacles.push('Budgetary appropriation required from Finance Committee.');
|
|
643
|
-
if (hasDomain(domains, 'healthcare') || hasDomain(domains, 'education')) {
|
|
644
|
-
keyObstacles.push('Regional coordination with municipalities and county councils required.');
|
|
645
|
-
}
|
|
646
|
-
if (influenceScore >= 70)
|
|
647
|
-
keyObstacles.push('High-complexity document may require secondary legislation.');
|
|
648
|
-
if (keyObstacles.length === 0)
|
|
649
|
-
keyObstacles.push('Standard parliamentary and regulatory approval process.');
|
|
650
|
-
const resourceRequirements = influenceScore >= 65
|
|
651
|
-
? 'Significant administrative and financial resources required across affected agencies.'
|
|
652
|
-
: 'Moderate resource requirements; implementable within existing agency budgets.';
|
|
653
|
-
const agencies = [];
|
|
654
|
-
if (hasDomain(domains, 'fiscal'))
|
|
655
|
-
agencies.push('Finansdepartementet', 'Skatteverket');
|
|
656
|
-
if (hasDomain(domains, 'healthcare'))
|
|
657
|
-
agencies.push('Socialstyrelsen', 'Folkhälsomyndigheten');
|
|
658
|
-
if (hasDomain(domains, 'justice'))
|
|
659
|
-
agencies.push('Justitiedepartementet', 'Polismyndigheten');
|
|
660
|
-
if (hasDomain(domains, 'education'))
|
|
661
|
-
agencies.push('Skolverket', 'Universitetskanslersämbetet');
|
|
662
|
-
if (agencies.length === 0)
|
|
663
|
-
agencies.push('Responsible line ministry and affected state agencies.');
|
|
664
|
-
return {
|
|
665
|
-
feasibility,
|
|
666
|
-
estimatedTimeline: timeline,
|
|
667
|
-
keyObstacles,
|
|
668
|
-
resourceRequirements,
|
|
669
|
-
agenciesInvolved: agencies,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
// ---------------------------------------------------------------------------
|
|
673
|
-
// Risk assessment builder
|
|
674
|
-
// ---------------------------------------------------------------------------
|
|
675
|
-
/** Build risk assessment list for a document. */
|
|
676
|
-
export function buildRiskAssessment(doc, ciaContext, ctx) {
|
|
677
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
678
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
679
|
-
const riskIndex = ciaContext ? calculateCoalitionRiskIndex(ciaContext) : null;
|
|
680
|
-
const risks = [];
|
|
681
|
-
// Political risks
|
|
682
|
-
const politicalSeverity = riskIndex?.level === 'CRITICAL' || riskIndex?.level === 'HIGH' ? 'high' : 'medium';
|
|
683
|
-
risks.push({
|
|
684
|
-
type: 'political',
|
|
685
|
-
severity: politicalSeverity,
|
|
686
|
-
description: docType === 'prop'
|
|
687
|
-
? 'Risk of parliamentary defeat if coalition unity falters.'
|
|
688
|
-
: docType === 'mot'
|
|
689
|
-
? 'Limited likelihood of parliamentary success given prevailing government majority dynamics.'
|
|
690
|
-
: 'Political risk depends on committee handling and cross-party alignment.',
|
|
691
|
-
mitigationOptions: ['Cross-party dialogue', 'Committee amendments to broaden support', 'Public consultation to build legitimacy'],
|
|
692
|
-
});
|
|
693
|
-
// Implementation risks
|
|
694
|
-
const influenceScore = ctx?.influenceScore ?? calculateInfluenceScore(doc);
|
|
695
|
-
if (influenceScore >= 50) {
|
|
696
|
-
risks.push({
|
|
697
|
-
type: 'implementation',
|
|
698
|
-
severity: influenceScore >= 70 ? 'high' : 'medium',
|
|
699
|
-
description: 'Complex implementation may lead to delays, cost overruns, or divergent regional outcomes.',
|
|
700
|
-
mitigationOptions: ['Pilot programmes in selected municipalities', 'Clear agency mandate and funding certainty', 'Parliamentary follow-up reporting requirements'],
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
// Public acceptance risks
|
|
704
|
-
if (hasDomain(domains, 'migration') || hasDomain(domains, 'healthcare') || hasDomain(domains, 'fiscal')) {
|
|
705
|
-
risks.push({
|
|
706
|
-
type: 'public-acceptance',
|
|
707
|
-
severity: 'medium',
|
|
708
|
-
description: `Public sensitivity in ${domains.slice(0, 2).join(' and ')} may generate media scrutiny and voter backlash.`,
|
|
709
|
-
mitigationOptions: ['Transparent communication strategy', 'Phased rollout to allow adaptation', 'Stakeholder consultation before implementation'],
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
// Legal risks
|
|
713
|
-
if (hasDomain(domains, 'eu-foreign')) {
|
|
714
|
-
risks.push({
|
|
715
|
-
type: 'legal',
|
|
716
|
-
severity: 'medium',
|
|
717
|
-
description: 'EU law compliance must be verified; risk of infringement proceedings if directive requirements unmet.',
|
|
718
|
-
mitigationOptions: ['Legal review by Lagrådet', 'EU notification procedures', 'Parliamentary Committee for EU Affairs scrutiny'],
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
// Financial risks
|
|
722
|
-
if (hasDomain(domains, 'fiscal') || influenceScore >= 65) {
|
|
723
|
-
risks.push({
|
|
724
|
-
type: 'financial',
|
|
725
|
-
severity: 'medium',
|
|
726
|
-
description: 'Budgetary implications require Finance Committee review and multi-year spending plan adjustment.',
|
|
727
|
-
mitigationOptions: ['Detailed cost-benefit analysis', 'Phased appropriations across budget years', 'Performance-based funding conditionality'],
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
return risks;
|
|
731
|
-
}
|
|
732
|
-
// ---------------------------------------------------------------------------
|
|
733
|
-
// Executive summary builder
|
|
734
|
-
// ---------------------------------------------------------------------------
|
|
735
|
-
/**
|
|
736
|
-
* Map raw Riksdag doktyp codes (`'prop'`, `'mot'`, `'bet'`, `'skr'`) to the
|
|
737
|
-
* normalized type strings (`'proposition'`, `'motion'`, `'interpellation'`, `'report'`, …) that
|
|
738
|
-
* `generateEnhancedSummary()` in `helpers.ts` expects for content branching.
|
|
739
|
-
* Without this, the helper silently falls through to a generic default text.
|
|
740
|
-
*
|
|
741
|
-
* Note: 'skr' (government communication / skrivelse) maps to 'report' because
|
|
742
|
-
* `generateEnhancedSummary()` only has branches for 'report'|'proposition'|'motion'|'interpellation'.
|
|
743
|
-
* Government communications are informational reports by nature, so 'report' is the
|
|
744
|
-
* closest semantic match.
|
|
745
|
-
*/
|
|
746
|
-
function normalizeDocType(doktyp) {
|
|
747
|
-
switch (doktyp) {
|
|
748
|
-
case 'prop': return 'proposition';
|
|
749
|
-
case 'mot': return 'motion';
|
|
750
|
-
case 'ip': return 'interpellation';
|
|
751
|
-
case 'bet': return 'report';
|
|
752
|
-
case 'skr': return 'report'; // Government communications treated as reports
|
|
753
|
-
default: return doktyp;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
/** Pattern for valid riksmöte strings: YYYY/YY */
|
|
757
|
-
const RIKSMOTE_PATTERN = /^\d{4}\/\d{2}$/;
|
|
758
|
-
/** Pattern for YYYY-MM-DD date strings. */
|
|
759
|
-
const DATUM_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
760
|
-
/**
|
|
761
|
-
* Derive the riksmöte (parliamentary session) string from a document.
|
|
762
|
-
* Prefers `doc.rm` when present and valid; otherwise derives from `doc.datum`
|
|
763
|
-
* via `getCurrentRiksmote()`. Falls back to the current session if neither is
|
|
764
|
-
* available.
|
|
765
|
-
*
|
|
766
|
-
* The datum is parsed via regex and reconstructed at local noon to avoid
|
|
767
|
-
* timezone-sensitive date shifts that can occur with `new Date('YYYY-MM-DD')`
|
|
768
|
-
* (which is parsed as UTC midnight, shifting to the previous day in UTC+
|
|
769
|
-
* timezones).
|
|
770
|
-
*/
|
|
771
|
-
function deriveRiksmote(doc) {
|
|
772
|
-
if (doc.rm && RIKSMOTE_PATTERN.test(doc.rm))
|
|
773
|
-
return doc.rm;
|
|
774
|
-
if (doc.datum) {
|
|
775
|
-
const m = DATUM_PATTERN.exec(doc.datum);
|
|
776
|
-
if (m) {
|
|
777
|
-
const [, year, month, day] = m;
|
|
778
|
-
// Construct at local noon to avoid cross-day timezone shifts
|
|
779
|
-
const d = new Date(Number(year), Number(month) - 1, Number(day), 12, 0, 0);
|
|
780
|
-
return getCurrentRiksmote(d);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
return getCurrentRiksmote();
|
|
784
|
-
}
|
|
785
|
-
/** Generate a structured executive summary for a document. */
|
|
786
|
-
export function generateExecutiveSummary(doc, lang, ctx) {
|
|
787
|
-
const title = doc.titel ?? doc.title ?? doc.rubrik ?? 'Document';
|
|
788
|
-
const docType = doc.doktyp ?? doc.documentType ?? '';
|
|
789
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc, lang);
|
|
790
|
-
const influenceScore = ctx?.influenceScore ?? calculateInfluenceScore(doc);
|
|
791
|
-
const party = normalizePartyKey(doc.parti);
|
|
792
|
-
// Paragraph 1: What the document is and who authored it
|
|
793
|
-
const typeLabel = docType === 'prop' ? 'government proposition'
|
|
794
|
-
: docType === 'mot' ? 'parliamentary motion'
|
|
795
|
-
: docType === 'ip' ? 'interpellation'
|
|
796
|
-
: docType === 'bet' ? 'committee report'
|
|
797
|
-
: docType === 'skr' ? 'government communication'
|
|
798
|
-
: 'parliamentary document';
|
|
799
|
-
const authorPart = party && party !== 'other' ? ` filed by ${escapeHtml(party.toUpperCase())}` : '';
|
|
800
|
-
const effectiveText = doc.fullText ?? doc.fullContent ?? '';
|
|
801
|
-
const passage = effectiveText ? extractKeyPassage(effectiveText, 200) : '';
|
|
802
|
-
const contentSentence = passage ? ` Key provision: "${escapeHtml(passage)}"` : '';
|
|
803
|
-
const para1 = `This ${typeLabel}${authorPart} — ${escapeHtml(title)} — is a parliamentary document with an influence score of ${influenceScore}/100.${contentSentence}`;
|
|
804
|
-
// Paragraph 2: Policy significance across domains
|
|
805
|
-
// Escape the enhanced summary to prevent XSS when rendered as HTML
|
|
806
|
-
const domainStr = domains.length > 0 ? domains.slice(0, 3).join(', ') : 'general policy';
|
|
807
|
-
const normalizedType = normalizeDocType(docType);
|
|
808
|
-
const enhancedSummary = escapeHtml(generateEnhancedSummary(doc, normalizedType, lang));
|
|
809
|
-
const riksmote = deriveRiksmote(doc);
|
|
810
|
-
const para2 = `The document intersects ${domainStr} policy domains, placing it within the broader legislative agenda of the ${riksmote} parliamentary session. ${enhancedSummary}`;
|
|
811
|
-
// Paragraph 3: Strategic significance
|
|
812
|
-
const para3 = docType === 'prop'
|
|
813
|
-
? 'As a government proposition, this document represents executive policy intent and will be scrutinised by the relevant parliamentary committee before a plenary vote.'
|
|
814
|
-
: docType === 'mot'
|
|
815
|
-
? 'As an opposition motion, this document fulfils the parliamentary oversight function, pressing the government on policy gaps and alternative directions.'
|
|
816
|
-
: docType === 'ip'
|
|
817
|
-
? 'As an interpellation, this document formally questions a minister on a specific policy matter, requiring a public response and parliamentary debate.'
|
|
818
|
-
: docType === 'bet'
|
|
819
|
-
? `As a committee report, this document presents the committee's recommendation to the Riksdag after deliberating on one or more government proposals or motions.`
|
|
820
|
-
: docType === 'skr'
|
|
821
|
-
? 'As a government communication, this document conveys the government\'s position or report on a matter to the Riksdag, informing parliamentary oversight and public accountability.'
|
|
822
|
-
: 'This document contributes to the parliamentary record and informs committee deliberations and future legislative activity.';
|
|
823
|
-
return `${para1}\n\n${para2}\n\n${para3}`;
|
|
824
|
-
}
|
|
825
|
-
// ---------------------------------------------------------------------------
|
|
826
|
-
// Multi-iteration analysis protocol
|
|
827
|
-
// ---------------------------------------------------------------------------
|
|
828
|
-
/** Build the four-iteration analysis protocol record. */
|
|
829
|
-
function buildIterations(doc, stakeholders, ctx) {
|
|
830
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
831
|
-
const influence = ctx?.influenceScore ?? calculateInfluenceScore(doc);
|
|
832
|
-
return [
|
|
833
|
-
{
|
|
834
|
-
iteration: 1,
|
|
835
|
-
label: 'generation',
|
|
836
|
-
summary: 'Initial analysis from document metadata, type, and available content.',
|
|
837
|
-
refinements: [
|
|
838
|
-
`Identified ${domains.length} policy domain(s): ${domains.join(', ') || 'general'}.`,
|
|
839
|
-
`Influence score calculated: ${influence}/100.`,
|
|
840
|
-
],
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
iteration: 2,
|
|
844
|
-
label: 'deepening',
|
|
845
|
-
summary: 'Cross-referenced document context, challenged initial assumptions, deepened domain analysis.',
|
|
846
|
-
refinements: [
|
|
847
|
-
'Verified stakeholder relevance signals against document text.',
|
|
848
|
-
'PESTLE dimensions expanded from domain-specific triggers.',
|
|
849
|
-
hasDomain(domains, 'eu-foreign') ? 'EU law compliance risk identified and flagged.' : 'No EU law compliance issues identified.',
|
|
850
|
-
],
|
|
851
|
-
},
|
|
852
|
-
{
|
|
853
|
-
iteration: 3,
|
|
854
|
-
label: 'stakeholder-review',
|
|
855
|
-
summary: `Reviewed representation across ${stakeholders.length} stakeholder groups; ensured balance.`,
|
|
856
|
-
refinements: [
|
|
857
|
-
`All ${stakeholders.length} relevant stakeholders given SWOT assessment.`,
|
|
858
|
-
'Implementation burden differentiated by institutional capacity.',
|
|
859
|
-
'Opposition and government perspectives balanced.',
|
|
860
|
-
],
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
iteration: 4,
|
|
864
|
-
label: 'synthesis',
|
|
865
|
-
summary: 'Synthesised all analytical dimensions into unified intelligence picture.',
|
|
866
|
-
refinements: [
|
|
867
|
-
'Executive summary integrates document content, domain analysis, and significance.',
|
|
868
|
-
'Confidence scores reflect evidence quality for each dimension.',
|
|
869
|
-
'Risk assessment cross-linked with coalition dynamics.',
|
|
870
|
-
],
|
|
871
|
-
},
|
|
872
|
-
];
|
|
873
|
-
}
|
|
874
|
-
// ---------------------------------------------------------------------------
|
|
875
|
-
// Confidence scores
|
|
876
|
-
// ---------------------------------------------------------------------------
|
|
877
|
-
/** Build per-dimension confidence scores for the analysis. */
|
|
878
|
-
function buildConfidenceScores(doc, ciaContext, ctx) {
|
|
879
|
-
const scores = new Map();
|
|
880
|
-
const domains = ctx?.domains ?? detectPolicyDomains(doc);
|
|
881
|
-
const hasFullText = !!(doc.fullText || doc.fullContent);
|
|
882
|
-
const hasCIA = !!ciaContext;
|
|
883
|
-
scores.set('executiveSummary', assessConfidenceLevel(domains.length + (hasFullText ? 2 : 0), 70));
|
|
884
|
-
scores.set('stakeholderImpacts', assessConfidenceLevel(domains.length + 2, hasCIA ? 80 : 60));
|
|
885
|
-
scores.set('pestleDimensions', assessConfidenceLevel(domains.length, 65));
|
|
886
|
-
scores.set('policyDomains', assessConfidenceLevel(domains.length, hasFullText ? 85 : 65));
|
|
887
|
-
scores.set('coalitionDynamics', assessConfidenceLevel(hasCIA ? 5 : 2, hasCIA ? 85 : 55));
|
|
888
|
-
scores.set('historicalContext', assessConfidenceLevel(2, 60));
|
|
889
|
-
scores.set('implementationAssessment', assessConfidenceLevel(domains.length + 1, 65));
|
|
890
|
-
scores.set('riskAssessment', assessConfidenceLevel(domains.length + (hasCIA ? 3 : 1), hasCIA ? 75 : 60));
|
|
891
|
-
return scores;
|
|
892
|
-
}
|
|
893
|
-
// ---------------------------------------------------------------------------
|
|
894
|
-
// Public API
|
|
895
|
-
// ---------------------------------------------------------------------------
|
|
896
|
-
/**
|
|
897
|
-
* Analyse a parliamentary document through the comprehensive multi-iteration
|
|
898
|
-
* framework. Results are cached by document ID to avoid redundant analysis
|
|
899
|
-
* when the same document appears in multiple articles.
|
|
900
|
-
*
|
|
901
|
-
* @param doc - Raw document from the MCP server
|
|
902
|
-
* @param lang - Target language for localised display names
|
|
903
|
-
* @param ciaContext - Optional CIA intelligence context for enriched analysis
|
|
904
|
-
* @param forceRefresh - Skip cache and recompute (default false)
|
|
905
|
-
* @returns Full DocumentAnalysis result
|
|
906
|
-
*/
|
|
907
|
-
export function analyzeDocument(doc, lang = 'en', ciaContext, forceRefresh = false) {
|
|
908
|
-
const [key, fp] = cacheKeyAndFp(doc, typeof lang === 'string' ? lang : 'en', ciaContext);
|
|
909
|
-
if (!forceRefresh && _analysisCache.has(key)) {
|
|
910
|
-
return _analysisCache.get(key);
|
|
911
|
-
}
|
|
912
|
-
const relevantStakeholders = selectRelevantStakeholders(doc);
|
|
913
|
-
// Precompute expensive values once to avoid 7–11× redundant
|
|
914
|
-
// detectPolicyDomains() and calculateInfluenceScore() calls in
|
|
915
|
-
// downstream builders (stakeholder impacts, risk, implementation, etc.)
|
|
916
|
-
const rawDomains = detectPolicyDomains(doc, lang);
|
|
917
|
-
const influenceScore = calculateInfluenceScore(doc);
|
|
918
|
-
const ctx = { domains: rawDomains, influenceScore };
|
|
919
|
-
const stakeholderImpacts = relevantStakeholders.map(group => buildStakeholderImpact(group, doc, lang, ciaContext, ctx));
|
|
920
|
-
const pestleDimensions = buildPestleAnalysis(doc, lang, ctx);
|
|
921
|
-
const policyDomains = toDomainKeyPairs(rawDomains).map(({ key: k, name: n }, i) => ({
|
|
922
|
-
key: k,
|
|
923
|
-
name: n,
|
|
924
|
-
relevanceScore: Math.max(MIN_DOMAIN_RELEVANCE, MAX_DOMAIN_RELEVANCE - i * DOMAIN_RELEVANCE_DECAY),
|
|
925
|
-
}));
|
|
926
|
-
// Iteration 2: coalition + context
|
|
927
|
-
const coalitionDynamics = buildCoalitionDynamics(doc, ciaContext);
|
|
928
|
-
const historicalContext = buildHistoricalContext(doc, rawDomains);
|
|
929
|
-
// Iteration 3: implementation + risk
|
|
930
|
-
const implementationAssessment = buildImplementationAssessment(doc, ctx);
|
|
931
|
-
const riskAssessment = buildRiskAssessment(doc, ciaContext, ctx);
|
|
932
|
-
// Iteration 4: synthesis
|
|
933
|
-
const executiveSummary = generateExecutiveSummary(doc, lang, ctx);
|
|
934
|
-
const confidenceScores = buildConfidenceScores(doc, ciaContext, ctx);
|
|
935
|
-
const iterations = buildIterations(doc, relevantStakeholders, ctx);
|
|
936
|
-
const analysis = {
|
|
937
|
-
documentId: stableDocumentId(doc, fp),
|
|
938
|
-
documentTitle: doc.titel ?? doc.title ?? doc.rubrik ?? 'Unknown Document',
|
|
939
|
-
executiveSummary,
|
|
940
|
-
stakeholderImpacts,
|
|
941
|
-
pestleDimensions,
|
|
942
|
-
policyDomains,
|
|
943
|
-
coalitionDynamics,
|
|
944
|
-
historicalContext,
|
|
945
|
-
implementationAssessment,
|
|
946
|
-
riskAssessment,
|
|
947
|
-
confidenceScores,
|
|
948
|
-
iterations,
|
|
949
|
-
influenceScore,
|
|
950
|
-
analyzedAt: new Date().toISOString(),
|
|
951
|
-
};
|
|
952
|
-
_analysisCache.set(key, analysis);
|
|
953
|
-
// Evict oldest entry when cache exceeds size limit.
|
|
954
|
-
// Map iteration order is insertion order, so the first key is the oldest.
|
|
955
|
-
if (_analysisCache.size > MAX_CACHE_SIZE) {
|
|
956
|
-
const oldest = _analysisCache.keys().next().value;
|
|
957
|
-
if (oldest !== undefined)
|
|
958
|
-
_analysisCache.delete(oldest);
|
|
959
|
-
}
|
|
960
|
-
return analysis;
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* Analyse multiple documents in batch, returning a map from document ID to
|
|
964
|
-
* analysis result. Uses caching so documents appearing multiple times are
|
|
965
|
-
* only analysed once.
|
|
966
|
-
*
|
|
967
|
-
* When the input array contains multiple versions of the same document
|
|
968
|
-
* (e.g. a metadata-only record and a content-enriched record sharing the
|
|
969
|
-
* same `dok_id`), the batch is de-duplicated up-front using a "prefer
|
|
970
|
-
* enriched" strategy: among entries that share the same stable document
|
|
971
|
-
* base key, the version with `fullText` or `fullContent` wins. This
|
|
972
|
-
* prevents order-dependent overwrites and ensures the richest available
|
|
973
|
-
* version is analysed.
|
|
974
|
-
*/
|
|
975
|
-
export function analyzeDocuments(docs, lang = 'en', ciaContext) {
|
|
976
|
-
// De-duplicate: prefer enriched versions when multiple entries share
|
|
977
|
-
// the same stable base key (dok_id or url).
|
|
978
|
-
const deduped = new Map();
|
|
979
|
-
for (const doc of docs) {
|
|
980
|
-
const baseId = documentBaseKey(doc);
|
|
981
|
-
const existing = deduped.get(baseId);
|
|
982
|
-
if (!existing) {
|
|
983
|
-
deduped.set(baseId, doc);
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
// Prefer whichever has richer content fields
|
|
987
|
-
const existingHasContent = !!(existing.fullText || existing.fullContent);
|
|
988
|
-
const docHasContent = !!(doc.fullText || doc.fullContent);
|
|
989
|
-
if (docHasContent && !existingHasContent) {
|
|
990
|
-
deduped.set(baseId, doc);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
const results = new Map();
|
|
995
|
-
for (const doc of deduped.values()) {
|
|
996
|
-
const analysis = analyzeDocument(doc, lang, ciaContext);
|
|
997
|
-
results.set(analysis.documentId, analysis);
|
|
998
|
-
}
|
|
999
|
-
return results;
|
|
1000
|
-
}
|
|
1001
|
-
//# sourceMappingURL=document-analyzer.js.map
|