All files / scripts/analysis-framework/lenses government.ts

93.97% Statements 78/83
79.78% Branches 75/94
100% Functions 13/13
100% Lines 70/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313                                                      1x             1x               1x           1x                                   75x               162x 2052x               75x 75x 75x 75x     75x 6x     6x   5x 3x       75x     75x   69x 31x       6x     6x     6x   1x               75x 75x 75x   75x       75x       75x       75x               75x 75x 75x     75x 69x                 75x 73x                 75x 39x                 75x 39x               75x               75x   75x 41x         41x             75x           75x               75x 75x   75x           75x 173x             75x               75x   75x 75x 75x 75x   75x                                               75x 75x 41x 80x   80x 41x     75x 75x   75x                          
/**
 * @module analysis-framework/lenses/government
 * @description Government perspective lens for parliamentary document analysis.
 *
 * Evaluates every document from the government's vantage point:
 * - Policy execution feasibility
 * - Budget and resource implications
 * - Coalition agreement alignment
 * - International commitment compliance
 *
 * The lens produces a `PerspectiveAnalysis` with SWOT contributions,
 * dashboard metrics, and mindmap nodes ready for downstream generators.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import type { RawDocument, CIAContext } from '../../data-transformers/types.js';
import type { Language } from '../../types/language.js';
import type { PerspectiveAnalysis, ImpactLevel, Sentiment, SwotContribution, DashboardMetric, MindmapNode } from '../types.js';
import { detectPolicyDomains } from '../../data-transformers/policy-analysis.js';
 
// ---------------------------------------------------------------------------
// Internal keyword banks
// ---------------------------------------------------------------------------
 
/** Keywords that indicate a document involves coalition-critical decisions */
const COALITION_CRITICAL_KEYWORDS: readonly string[] = [
  'tidöavtal', 'tidöpartner', 'samarbetspartier', 'januariavtal',
  'samarbetspartierna', 'sd-stöd', 'budgetram', 'budgetproposition',
  'statsbudget', 'ramproposition', 'takpolitik',
];
 
/** Keywords signalling budget pressure or fiscal constraint */
const FISCAL_PRESSURE_KEYWORDS: readonly string[] = [
  'kostnadsökning', 'budgetbelastning', 'finansiering', 'finansiellt utrymme',
  'anslagsökning', 'anslagnedskärning', 'nettoutgifter', 'statsskuld',
  'lånebehov', 'underskott', 'surplus', 'deficit', 'fiscal gap',
  'savings requirement', 'besparing',
];
 
/** High-influence committee codes for the government lens */
const GOV_HIGH_INFLUENCE_COMMITTEES = new Set(['FiU', 'KU', 'FöU', 'UU', 'JuU']);
 
// ---------------------------------------------------------------------------
// Localised lens labels
// ---------------------------------------------------------------------------
 
const LENS_LABELS: Readonly<Record<string, { lensName: string; stakeholder: string }>> = {
  en: { lensName: 'Government Perspective', stakeholder: 'Government' },
  sv: { lensName: 'Regeringsperspektiv', stakeholder: 'Regeringen' },
  da: { lensName: 'Regeringsperspektiv', stakeholder: 'Regeringen' },
  no: { lensName: 'Regjeringsperspektiv', stakeholder: 'Regjeringen' },
  fi: { lensName: 'Hallituksen näkökulma', stakeholder: 'Hallitus' },
  de: { lensName: 'Regierungsperspektive', stakeholder: 'Regierung' },
  fr: { lensName: 'Perspective gouvernementale', stakeholder: 'Gouvernement' },
  es: { lensName: 'Perspectiva gubernamental', stakeholder: 'Gobierno' },
  nl: { lensName: 'Regeringsperspectief', stakeholder: 'Regering' },
  ar: { lensName: 'منظور الحكومة', stakeholder: 'الحكومة' },
  he: { lensName: 'פרספקטיבת הממשלה', stakeholder: 'הממשלה' },
  ja: { lensName: '政府の視点', stakeholder: '政府' },
  ko: { lensName: '정부 관점', stakeholder: '정부' },
  zh: { lensName: '政府视角', stakeholder: '政府' },
};
 
function label(lang: Language | string): { lensName: string; stakeholder: string } {
  return LENS_LABELS[lang] ?? LENS_LABELS['en'];
}
 
// ---------------------------------------------------------------------------
// Helper utilities
// ---------------------------------------------------------------------------
 
function containsAny(text: string, keywords: readonly string[]): boolean {
  const lower = text.toLowerCase();
  return keywords.some(kw => lower.includes(kw.toLowerCase()));
}
 
// ---------------------------------------------------------------------------
// Impact / Sentiment calculation
// ---------------------------------------------------------------------------
 
function computeImpact(doc: RawDocument, cia: CIAContext | undefined): ImpactLevel {
  const docType = doc.doktyp || doc.documentType || '';
  const committee = doc.organ || doc.committee || '';
  const isHighCommittee = GOV_HIGH_INFLUENCE_COMMITTEES.has(committee);
  const isHighDocType = ['prop', 'bet', 'skr'].includes(docType);
 
  // Propositions and committee reports on strategic committees are inherently high-impact
  if (isHighDocType && isHighCommittee) return 'high';
  Iif (docType === 'prop') return 'high';
 
  // Thin majority amplifies impact for all government-sponsored documents
  if (cia && cia.coalitionStability.majorityMargin <= 3) return 'high';
 
  if (isHighDocType || isHighCommittee) return 'medium';
  return 'low';
}
 
function computeSentiment(doc: RawDocument, cia: CIAContext | undefined): Sentiment {
  const allText = [doc.titel, doc.title, doc.summary, doc.notis].filter(Boolean).join(' ');
 
  // Government-sponsored propositions are generally positive for the government
  if (doc.doktyp === 'prop') {
    // Unless they arrive during a coalition instability period
    if (cia && cia.coalitionStability.stabilityScore < 40) return 'neutral';
    return 'positive';
  }
 
  // Fiscal pressure signals negative impact on government agenda
  Iif (containsAny(allText, FISCAL_PRESSURE_KEYWORDS)) return 'negative';
 
  // Coalition-critical documents are neutral (could go either way)
  Iif (containsAny(allText, COALITION_CRITICAL_KEYWORDS)) return 'neutral';
 
  // Default neutral for motions
  if (doc.doktyp === 'mot') return 'neutral';
 
  return 'neutral';
}
 
// ---------------------------------------------------------------------------
// Summary generation
// ---------------------------------------------------------------------------
 
function buildSummary(doc: RawDocument, cia: CIAContext | undefined, _lang: Language | string, domains: string[]): string {
  const title = doc.titel || doc.title || 'Untitled';
  const docType = doc.doktyp || doc.documentType || 'document';
  const domainPhrase = domains.length > 0 ? ` touching ${domains.slice(0, 2).join(' and ')}` : '';
 
  const stabilityNote = cia && cia.coalitionStability.stabilityScore < 50
    ? ` Current coalition instability (stability score: ${cia.coalitionStability.stabilityScore}) increases execution risk.`
    : '';
 
  const coalitionNote = containsAny(title, COALITION_CRITICAL_KEYWORDS)
    ? ' Coalition agreement alignment must be verified before advancement.'
    : '';
 
  const fiscalNote = containsAny(title, FISCAL_PRESSURE_KEYWORDS)
    ? ' Fiscal feasibility assessment required; budget headroom may be constrained.'
    : '';
 
  return `From the government perspective, this ${docType}${domainPhrase} requires assessment of policy execution capacity and resource allocation.${stabilityNote}${coalitionNote}${fiscalNote}`.trim();
}
 
// ---------------------------------------------------------------------------
// SWOT contributions
// ---------------------------------------------------------------------------
 
function buildSwotContributions(doc: RawDocument, cia: CIAContext | undefined, lang: Language | string, domains: string[]): SwotContribution[] {
  const { stakeholder } = label(lang);
  const contributions: SwotContribution[] = [];
  const docType = doc.doktyp || doc.documentType || '';
 
  // Strength: government-initiated propositions reinforce policy mandate
  if (docType === 'prop') {
    contributions.push({
      quadrant: 'strength',
      forStakeholder: stakeholder,
      text: 'Government-initiated legislation reinforces policy mandate and demonstrates programme delivery.',
      impact: 'high',
    });
  }
 
  // Opportunity: policy domain expansion
  if (domains.length > 0) {
    contributions.push({
      quadrant: 'opportunity',
      forStakeholder: stakeholder,
      text: `Policy advancement opportunity in ${domains.slice(0, 2).join(' and ')} domain(s), aligned with coalition programme.`,
      impact: 'medium',
    });
  }
 
  // Weakness: coalition instability
  if (cia && cia.coalitionStability.stabilityScore < 50) {
    contributions.push({
      quadrant: 'weakness',
      forStakeholder: stakeholder,
      text: `Coalition fragility (stability score ${cia.coalitionStability.stabilityScore}) risks legislative delays or defeats.`,
      impact: 'high',
    });
  }
 
  // Threat: thin majority
  if (cia && cia.coalitionStability.majorityMargin <= 3) {
    contributions.push({
      quadrant: 'threat',
      forStakeholder: stakeholder,
      text: `Razor-thin majority (${cia.coalitionStability.majorityMargin} seat(s)) creates defeat risk on contested votes.`,
      impact: 'high',
    });
  }
 
  return contributions;
}
 
// ---------------------------------------------------------------------------
// Dashboard metrics
// ---------------------------------------------------------------------------
 
function buildDashboardMetrics(_doc: RawDocument, cia: CIAContext | undefined, domains: string[]): DashboardMetric[] {
  const metrics: DashboardMetric[] = [];
 
  if (cia) {
    metrics.push({
      metricName: 'Coalition Stability',
      value: cia.coalitionStability.stabilityScore,
      unit: 'score',
    });
    metrics.push({
      metricName: 'Majority Margin',
      value: cia.coalitionStability.majorityMargin,
      unit: 'seats',
    });
  }
 
  metrics.push({
    metricName: 'Policy Domains Affected',
    value: domains.length,
    unit: 'domains',
  });
 
  return metrics;
}
 
// ---------------------------------------------------------------------------
// Mindmap nodes
// ---------------------------------------------------------------------------
 
function buildMindmapNodes(doc: RawDocument, _lang: Language | string, domains: string[]): MindmapNode[] {
  const nodes: MindmapNode[] = [];
  const docType = doc.doktyp || doc.documentType || '';
 
  nodes.push({
    branch: 'Legislative Pipeline',
    item: docType === 'prop' ? 'Government Bill' : docType === 'bet' ? 'Committee Report' : 'Parliamentary Document',
    weight: docType === 'prop' ? 'critical' : 'significant',
  });
 
  for (const domain of domains.slice(0, 3)) {
    nodes.push({
      branch: 'Policy Domains',
      item: domain,
      weight: 'significant',
    });
  }
 
  return nodes;
}
 
// ---------------------------------------------------------------------------
// Confidence scoring
// ---------------------------------------------------------------------------
 
function computeConfidence(doc: RawDocument, cia: CIAContext | undefined): number {
  let score = 40; // Base
 
  Iif (doc.fullText || doc.fullContent) score += 20;
  if (doc.summary || doc.notis) score += 10;
  if (cia) score += 20;
  Iif (doc.speeches && doc.speeches.length > 0) score += 10;
 
  return Math.min(100, score);
}
 
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
 
/**
 * Apply the Government analysis lens to a single parliamentary document.
 *
 * Evaluates policy execution feasibility, budget implications, coalition
 * alignment, and international commitment compliance.
 *
 * @param doc  - The document to analyse
 * @param cia  - Optional CIA context for coalition stability data
 * @param lang - Target language for all generated text
 * @returns    A `PerspectiveAnalysis` for the government lens
 */
export function analyzeGovernmentPerspective(
  doc: RawDocument,
  cia: CIAContext | undefined,
  lang: Language | string,
  precomputedDomains?: string[],
): PerspectiveAnalysis {
  const keyActors: string[] = ['Prime Minister', 'Cabinet'];
  if (cia) {
    const govParties = cia.partyPerformance
      .filter(p => p.metrics.seats > 0)
      .slice(0, 3)
      .map(p => p.partyName);
    keyActors.push(...govParties);
  }
 
  const domains = precomputedDomains ?? detectPolicyDomains(doc, 'en');
  const relatedPolicies = [...domains, 'Coalition Programme 2022-2026'].filter(Boolean);
 
  return {
    lens: 'government',
    summary: buildSummary(doc, cia, lang, domains),
    impact: computeImpact(doc, cia),
    sentiment: computeSentiment(doc, cia),
    keyActors: [...new Set(keyActors)].slice(0, 5),
    relatedPolicies: relatedPolicies.slice(0, 5),
    swotContribution: buildSwotContributions(doc, cia, lang, domains),
    dashboardMetrics: buildDashboardMetrics(doc, cia, domains),
    mindmapNodes: buildMindmapNodes(doc, lang, domains),
    confidence: computeConfidence(doc, cia),
  };
}