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 130 131 132 133 134 135                                                                          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)
  ]);
 
  // Build activity lookup by org code
  const activityMap: Record<string, number> = {};
  activity.forEach(a => {
    activityMap[a.org as string] = (a.document_count as number) || 0;
  });
 
  const nameToOrgCode = COMMITTEE_ORG_CODES;
 
  // Deduplicate committees by name, keeping the entry with the most data
  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;
    }
  });
 
  // Normalize committee metrics first so filtering and rendering use one consistent source.
  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 => {
      // Keep real committees only; skip generic node and inactive rows with no measured output.
      return (
        c.name !== 'Riksdagen' &&
        c.memberCount > 0 &&
        (c.productivityLevel !== 'INACTIVE' || c.documentsProcessed > 0)
      );
    })
    .sort((a, b) => b.documentsProcessed - a.documentsProcessed);
 
  // Build simple network graph from committees
  const nodes: NetworkNode[] = committees.map(c => ({
    id: c.id,
    name: c.name,
    size: c.influenceScore
  }));
 
  // Create edges between committees that share similar productivity levels
  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'
  };
}