All files / src/browser/cia/loaders committees.ts

96.96% Statements 32/33
72.22% Branches 26/36
100% Functions 7/7
100% Lines 29/29

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                                                                          1x         1x 1x 2x     1x   1x 1x 4x 4x 4x 4x     4x       1x   4x 4x 4x 4x 4x 4x 4x                                   4x           1x   2x           1x 1x 2x 1x       1x                   1x                    
/**
 * @module CIA/Loaders/Committees
 * @category Intelligence Platform - Data Acquisition & Pipeline Management
 *
 * @description
 * Builds the committee network analysis from CIA CSV exports.
 * Filters out INACTIVE committees with no measured output, deduplicates by
 * name, and produces both the committee table and a derived productivity
 * similarity network graph.
 *
 * @author Hack23 AB - Data Pipeline Engineering
 * @license Apache-2.0
 * @since 2026
 */
 
import type {
  CSVRow,
  CommitteeEntry,
  CommitteeNetwork,
  NetworkEdge,
  NetworkNode
} from '../types.js';
import type { LoadCSV } from '../csv-utils.js';
import {
  COMMITTEE_DOCS_PER_MEETING_ESTIMATE,
  COMMITTEE_ORG_CODES,
  CSV_SOURCES
} from '../sources.js';
 
/**
 * Build `CommitteeNetwork` analysis from CSV sources.
 * Replaces the legacy `committee-network.json` static export.
 *
 * @param loadCSV - CSV loader closure
 * @returns Committee table plus a derived productivity-similarity network graph
 */
export async function loadCommitteeNetwork(loadCSV: LoadCSV): Promise<CommitteeNetwork> {
  const [productivity, activity] = await Promise.all([
    loadCSV(CSV_SOURCES.committeeProductivity.local),
    loadCSV(CSV_SOURCES.committeeActivity.local)
  ]);
 
  const activityMap: Record<string, number> = {};
  activity.forEach(a => {
    activityMap[a.org as string] = (a.document_count as number) || 0;
  });
 
  const nameToOrgCode = COMMITTEE_ORG_CODES;
 
  const bestByName: Record<string, CSVRow> = {};
  productivity.forEach(c => {
    const name = c.committee_name as string;
    Iif (!name) return;
    const existing = bestByName[name];
    Eif (!existing || (c.total_documents as number) > (existing.total_documents as number) ||
        ((c.total_documents as number) === (existing.total_documents as number) &&
         (c.total_members as number) > (existing.total_members as number))) {
      bestByName[name] = c;
    }
  });
 
  const committees: CommitteeEntry[] = Object.values(bestByName)
    .map(c => {
      const name = c.committee_name as string;
      const code = nameToOrgCode[name] || name.substring(0, 3).toUpperCase();
      const totalDocuments = (c.total_documents as number) || 0;
      const activityDocs = activityMap[code] || 0;
      const documentsProcessed = Math.max(totalDocuments, activityDocs);
      const productivityLevel = (c.productivity_level as string) || '';
      return {
        id: code,
        name,
        memberCount: (c.total_members as number) || 0,
        influenceScore: c.docs_per_member
          ? Math.round((c.docs_per_member as number) * 100)
          : 0,
        documentsProcessed,
        productivityLevel,
        meetingsPerYear:
          documentsProcessed > 0
            ? Math.round(documentsProcessed / COMMITTEE_DOCS_PER_MEETING_ESTIMATE)
            : 0,
        keyIssues: [productivityLevel || 'N/A'],
        _source: 'csv'
      };
    })
    .filter(c => {
      return (
        c.name !== 'Riksdagen' &&
        c.memberCount > 0 &&
        (c.productivityLevel !== 'INACTIVE' || c.documentsProcessed > 0)
      );
    })
    .sort((a, b) => b.documentsProcessed - a.documentsProcessed);
 
  const nodes: NetworkNode[] = committees.map(c => ({
    id: c.id,
    name: c.name,
    size: c.influenceScore
  }));
 
  const edges: NetworkEdge[] = [];
  for (let i = 0; i < committees.length; i++) {
    for (let j = i + 1; j < committees.length && edges.length < 10; j++) {
      Eif (
        committees[i].productivityLevel === committees[j].productivityLevel &&
        committees[i].productivityLevel !== 'INACTIVE'
      ) {
        edges.push({
          source: committees[i].id,
          target: committees[j].id,
          weight: Math.min(committees[i].documentsProcessed, committees[j].documentsProcessed),
          type: 'productivity_similarity'
        });
      }
    }
  }
 
  return {
    title: 'Committee Network Analysis',
    description: 'Committee data from CIA committee productivity view',
    lastUpdated: new Date().toISOString(),
    committees,
    networkGraph: { nodes, edges },
    crossCommitteeMPs: [],
    _source: 'csv'
  };
}