All files / src/generators/pipeline analysis-threats.ts

95.23% Statements 40/42
64.44% Branches 29/45
100% Functions 10/10
100% Lines 40/40

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                                                                                                7x 7x 7x                           5x 5x 5x             5x       3x         5x                                                       5x 5x 5x   5x 5x 6x 6x 6x 6x 6x 6x   12x 12x 12x 12x 12x       5x                                                 5x 5x 5x   5x 5x 6x 6x 6x 6x 6x 6x 6x         5x                                   7x            
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Generators/Pipeline/AnalysisThreats
 * @description Threat assessment analysis method builders for the analysis pipeline.
 *
 * Contains markdown builders for the **Threat Assessment** analysis method group:
 * - `political-threat-landscape` — overall political threat assessment
 * - `actor-threat-profiling` — individual actor threat profiles
 * - `consequence-trees` — action-consequence chain analysis
 * - `legislative-disruption` — legislative process disruption identification
 */
 
import {
  assessPoliticalThreats,
  buildActorThreatProfiles,
  buildConsequenceTree,
  analyzeLegislativeDisruption,
  generateThreatAssessmentMarkdown,
} from '../../utils/political-threat-assessment.js';
import {
  sanitizeCell,
  safeArr,
  toThreatInput,
  buildMarkdownHeader,
  EMPTY_TABLE_ROW_6,
} from './analysis-helpers.js';
import type { MarkdownBuilder } from './analysis-helpers.js';
import type { AnalysisMethod } from './analysis-stage.js';
 
// ─── Per-method markdown builders ────────────────────────────────────────────
 
/**
 * Build markdown for the political threat landscape assessment.
 *
 * Uses the pipeline `date` parameter to ensure the assessment date in the
 * generated markdown matches the `analysis/{date}/` folder, overriding
 * the `new Date()` timestamp that `assessPoliticalThreats()` stamps internally.
 *
 * @param fetchedData - Raw fetched EP data
 * @param date - Analysis date (used to override assessment date for consistency)
 * @returns Markdown content string
 */
export function buildThreatLandscapeMarkdown(
  fetchedData: Record<string, unknown>,
  date: string
): string {
  const input = toThreatInput(fetchedData);
  const assessment = assessPoliticalThreats(input);
  return generateThreatAssessmentMarkdown({ ...assessment, date });
}
 
/**
 * Build markdown for actor threat profiling.
 *
 * @param fetchedData - Raw fetched EP data
 * @param date - Analysis date
 * @returns Markdown content string
 */
export function buildActorThreatProfilingMarkdown(
  fetchedData: Record<string, unknown>,
  date: string
): string {
  const input = toThreatInput(fetchedData);
  const profiles = buildActorThreatProfiles(input);
  const header = buildMarkdownHeader(
    'actor-threat-profiling',
    date,
    profiles.length > 0 ? 'medium' : 'low'
  );
 
  const profileRows =
    profiles.length > 0
      ? profiles
          .map(
            (p) =>
              `| ${sanitizeCell(p.actor)} | ${sanitizeCell(p.actorType)} | ${sanitizeCell(p.capability)} | ${sanitizeCell(p.motivation)} | ${sanitizeCell(p.opportunity)} | ${sanitizeCell(p.overallThreatLevel)} |`
          )
          .join('\n')
      : EMPTY_TABLE_ROW_6;
 
  return (
    header +
    `# Actor Threat Profiles
 
## Overview
Individual threat profiles for ${profiles.length} political actors.
 
## Actor Threat Matrix
| Actor | Type | Capability | Motivation | Opportunity | Threat Level |
|-------|------|------------|------------|-------------|--------------|
${profileRows}
 
## Date: ${date}
`
  );
}
 
/**
 * Build markdown for consequence tree analysis.
 *
 * @param fetchedData - Raw fetched EP data
 * @param date - Analysis date
 * @returns Markdown content string
 */
export function buildConsequenceTreesMarkdown(
  fetchedData: Record<string, unknown>,
  date: string
): string {
  const input = toThreatInput(fetchedData);
  const procedures = safeArr(fetchedData, 'procedures');
  const header = buildMarkdownHeader('consequence-trees', date, 'medium');
 
  const trees: string[] = [];
  for (const raw of procedures.slice(0, 5)) {
    const proc = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : null;
    const rawTitle = proc ? String(proc['title'] ?? '') : '';
    Iif (!rawTitle) continue;
    const title = sanitizeCell(rawTitle);
    const tree = buildConsequenceTree(rawTitle, input);
    trees.push(
      `### ${title}\n` +
        `- **Immediate**: ${tree.immediateConsequences.map((c) => sanitizeCell(c.description)).join('; ') || 'No immediate consequences identified'}\n` +
        `- **Secondary**: ${tree.secondaryEffects.map((c) => sanitizeCell(c.description)).join('; ') || 'No secondary effects identified'}\n` +
        `- **Long-term**: ${tree.longTermImplications.map((c) => sanitizeCell(c.description)).join('; ') || 'No long-term implications identified'}\n` +
        `- **Mitigating factors**: ${tree.mitigatingFactors.map((f) => sanitizeCell(f)).join(', ') || '—'}\n` +
        `- **Amplifying factors**: ${tree.amplifyingFactors.map((f) => sanitizeCell(f)).join(', ') || '—'}`
    );
  }
 
  return (
    header +
    `# Consequence Tree Analysis
 
## Overview
Structured analysis of action-consequence chains for ${Math.min(procedures.length, 5)} legislative procedures.
 
${trees.length > 0 ? trees.join('\n\n') : '## No procedures available for consequence analysis'}
 
## Date: ${date}
`
  );
}
 
/**
 * Build markdown for legislative disruption analysis.
 *
 * @param fetchedData - Raw fetched EP data
 * @param date - Analysis date
 * @returns Markdown content string
 */
export function buildLegislativeDisruptionMarkdown(
  fetchedData: Record<string, unknown>,
  date: string
): string {
  const input = toThreatInput(fetchedData);
  const procedures = safeArr(fetchedData, 'procedures');
  const header = buildMarkdownHeader('legislative-disruption', date, 'medium');
 
  const disruptions: string[] = [];
  for (const raw of procedures.slice(0, 5)) {
    const proc = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : null;
    const id = proc ? String(proc['procedureId'] ?? proc['id'] ?? '') : '';
    const title = proc ? String(proc['title'] ?? '') : '';
    Iif (!id || !title) continue;
    const analysis = analyzeLegislativeDisruption(id, input);
    const disruptionCount = analysis.disruptionPoints.length;
    disruptions.push(
      `| ${sanitizeCell(id)} | ${sanitizeCell(title.slice(0, 50))} | ${sanitizeCell(analysis.currentStage)} | ${sanitizeCell(analysis.resilience)} | ${disruptionCount} |`
    );
  }
 
  return (
    header +
    `# Legislative Disruption Analysis
 
## Overview
Identification of factors disrupting the normal legislative process.
 
## Disruption Assessment
| Procedure ID | Title | Stage | Resilience | Disruption Points |
|-------------|-------|-------|-----------|-------------------|
${disruptions.length > 0 ? disruptions.join('\n') : '| — | — | — | — | — |'}
 
## Date: ${date}
`
  );
}
 
/** All threat assessment method builders keyed by their AnalysisMethod identifier */
export const THREAT_BUILDERS: Readonly<Partial<Record<AnalysisMethod, MarkdownBuilder>>> = {
  'political-threat-landscape': buildThreatLandscapeMarkdown,
  'actor-threat-profiling': buildActorThreatProfilingMarkdown,
  'consequence-trees': buildConsequenceTreesMarkdown,
  'legislative-disruption': buildLegislativeDisruptionMarkdown,
};