|
5 | 5 | * migration, EU, justice, labour, housing, transport, and trade domains |
6 | 6 | * using keyword matching against Swedish document titles. |
7 | 7 | * |
| 8 | + * Also provides confidence level assessment for intelligence analysis. |
| 9 | + * |
8 | 10 | * @author Hack23 AB |
9 | 11 | * @license Apache-2.0 |
10 | 12 | */ |
11 | 13 |
|
| 14 | +/** |
| 15 | + * Confidence level for intelligence assessments. |
| 16 | + * Reflects the quality and quantity of supporting evidence. |
| 17 | + */ |
| 18 | +export type ConfidenceLevel = 'HIGH' | 'MEDIUM' | 'LOW'; |
| 19 | + |
| 20 | +/** |
| 21 | + * Assess the confidence level of an intelligence analysis based on |
| 22 | + * the number of corroborating evidence items and the quality of sources. |
| 23 | + * |
| 24 | + * @param evidenceCount - Number of distinct evidence items supporting the assessment |
| 25 | + * @param sourceQuality - Quality score of sources (0-100, higher = better) |
| 26 | + * @returns Confidence level classification |
| 27 | + */ |
| 28 | +export function assessConfidenceLevel(evidenceCount: number, sourceQuality: number): ConfidenceLevel { |
| 29 | + const normalizedEvidence = Math.max(0, evidenceCount); |
| 30 | + const normalizedQuality = Math.max(0, Math.min(100, sourceQuality)); |
| 31 | + |
| 32 | + // HIGH: Multiple evidence items with good-quality sources |
| 33 | + if (normalizedEvidence >= 5 && normalizedQuality >= 70) return 'HIGH'; |
| 34 | + if (normalizedEvidence >= 3 && normalizedQuality >= 85) return 'HIGH'; |
| 35 | + |
| 36 | + // LOW: Very few evidence items or very poor source quality |
| 37 | + if (normalizedEvidence === 0) return 'LOW'; |
| 38 | + if (normalizedEvidence <= 1 && normalizedQuality < 50) return 'LOW'; |
| 39 | + if (normalizedQuality < 30) return 'LOW'; |
| 40 | + |
| 41 | + // MEDIUM: everything in between |
| 42 | + return 'MEDIUM'; |
| 43 | +} |
| 44 | + |
12 | 45 | import { escapeHtml } from '../html-utils.js'; |
13 | 46 | import type { Language } from '../types/language.js'; |
14 | 47 | import type { RawDocument } from './types.js'; |
@@ -86,6 +119,75 @@ export function detectPolicyDomains(doc: RawDocument, lang: Language | string = |
86 | 119 | return Array.from(set); |
87 | 120 | } |
88 | 121 |
|
| 122 | +/** |
| 123 | + * Dominant political narrative frames detected in document titles. |
| 124 | + * These represent recurring rhetorical frames used across parties. |
| 125 | + */ |
| 126 | +export type NarrativeFrame = |
| 127 | + | 'law-and-order' |
| 128 | + | 'welfare-state-defence' |
| 129 | + | 'fiscal-responsibility' |
| 130 | + | 'green-transition' |
| 131 | + | 'national-security' |
| 132 | + | 'integration-challenge' |
| 133 | + | 'eu-sovereignty' |
| 134 | + | 'workers-rights'; |
| 135 | + |
| 136 | +/** |
| 137 | + * Detect dominant narrative frames in a document title. |
| 138 | + * Narrative framing reveals which rhetorical strategies are being employed |
| 139 | + * regardless of the specific policy domain. |
| 140 | + * |
| 141 | + * @param doc - Document to analyse |
| 142 | + * @returns Array of detected narrative frames (deduplicated) |
| 143 | + */ |
| 144 | +export function detectNarrativeFrames(doc: RawDocument): NarrativeFrame[] { |
| 145 | + const title = (doc.titel || doc.title || '').toLowerCase(); |
| 146 | + const frames = new Set<NarrativeFrame>(); |
| 147 | + |
| 148 | + // Law-and-order: crime, punishment, police |
| 149 | + if (title.includes('brott') || title.includes('straff') || title.includes('polis') || |
| 150 | + title.includes('kriminal') || title.includes('gäng') || title.includes('säker')) |
| 151 | + frames.add('law-and-order'); |
| 152 | + |
| 153 | + // Welfare-state defence: healthcare, social services, welfare |
| 154 | + if (title.includes('välfärd') || title.includes('omsorg') || title.includes('social') || |
| 155 | + title.includes('pension') || title.includes('bidrag') || title.includes('trygghet')) |
| 156 | + frames.add('welfare-state-defence'); |
| 157 | + |
| 158 | + // Fiscal responsibility: budgets, debt, taxes |
| 159 | + if (title.includes('budget') || title.includes('skuld') || title.includes('bespar') || |
| 160 | + title.includes('effektiv') || title.includes('kostnad') || title.includes('överskott')) |
| 161 | + frames.add('fiscal-responsibility'); |
| 162 | + |
| 163 | + // Green transition: climate, environment, energy |
| 164 | + if (title.includes('klimat') || title.includes('hållbar') || title.includes('grön') || |
| 165 | + title.includes('utsläpp') || title.includes('förnybar') || title.includes('energiomstäl')) |
| 166 | + frames.add('green-transition'); |
| 167 | + |
| 168 | + // National security: defence, preparedness, NATO |
| 169 | + if (title.includes('försvar') || title.includes('nato') || title.includes('beredskap') || |
| 170 | + title.includes('totalförsvar') || title.includes('säkerhetsskydd')) |
| 171 | + frames.add('national-security'); |
| 172 | + |
| 173 | + // Integration challenge: migration, asylum, citizenship |
| 174 | + if (title.includes('integr') || title.includes('migration') || title.includes('invandring') || |
| 175 | + title.includes('asyl') || title.includes('utvisning')) |
| 176 | + frames.add('integration-challenge'); |
| 177 | + |
| 178 | + // EU sovereignty: EU, European, sovereignty |
| 179 | + if (/\beu\b/.test(title) || title.includes('europa') || title.includes('suveränitet') || |
| 180 | + title.includes('direktiv') || title.includes('förordning')) |
| 181 | + frames.add('eu-sovereignty'); |
| 182 | + |
| 183 | + // Workers' rights: labour, unions, wages |
| 184 | + if (title.includes('facklig') || title.includes('lön') || title.includes('arbetsrätt') || |
| 185 | + title.includes('kollektivavtal') || title.includes('strejk') || title.includes('anstäl')) |
| 186 | + frames.add('workers-rights'); |
| 187 | + |
| 188 | + return Array.from(frames); |
| 189 | +} |
| 190 | + |
89 | 191 | type _LangPair = { en: Record<string, string>; sv: Record<string, string> }; |
90 | 192 |
|
91 | 193 | /** Module-level constant — allocated once, shared across all calls. */ |
|
0 commit comments