# All files / src/tools analyzeVotingPatterns.ts

100% Statements 22/22
88.88% Branches 16/18
100% Functions 4/4
100% Lines 20/20

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

    9x
    9x

    9x
    9x

    9x
    9x

    9x

    9x
    2x
    1x

    14x

    14x
    14x

    10x

    14x
    1x

    9x

    14x

    2x

    4x

     /**
     * MCP Tool: analyze_voting_patterns
     *
     * Analyze MEP voting behavior and patterns
     *
     * **Intelligence Perspective:** Advanced analytic tool for political group cohesion measurement,
     * cross-party voting detection, MEP independence scoring, and predictive analysis of
     * legislative outcomes using structured intelligence analysis techniques.
     *
     * **Business Perspective:** Premium analytics product differentiator—enables political risk
     * scoring, policy outcome prediction, and quantitative political analysis for enterprise clients.
     *
     * **Marketing Perspective:** Flagship intelligence capability demonstrating AI-powered
     * parliamentary analysis—key selling point for MCP ecosystem positioning.
     *
     * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
     */

    import { AnalyzeVotingPatternsSchema } from '../schemas/europeanParliament.js';
    import { epClient } from '../clients/europeanParliamentClient.js';
    import { buildToolResponse } from './shared/responseBuilder.js';
    import type { ToolResult } from './shared/types.js';
    import { ToolError } from './shared/errors.js';

    /**
     * Voting pattern analysis result
     */
    interface VotingPatternAnalysis {
      mepId: string;
      mepName: string;
      period: {
        from: string;
        to: string;
      };
      statistics: {
        totalVotes: number;
        votesFor: number;
        votesAgainst: number;
        abstentions: number;
        attendanceRate: number;
      };
      groupAlignment?: {
        politicalGroup: string;
        alignmentRate: number;
        divergentVotes: number;
      };
      crossPartyVoting: {
        withOtherGroups: number;
        rate: number;
      };
      confidenceLevel: 'HIGH' | 'MEDIUM' | 'LOW';
      methodology: string;
    }

    /**
     * Compute group alignment metrics from voting statistics.
     * Alignment rate is derived from the ratio of 'for' votes to total decisive
     * votes, approximating how often the MEP votes with the majority position.
     *
     * @param politicalGroup - The MEP's registered political group identifier
     * @param stats - Voting statistics containing `totalVotes`, `votesFor`, and `votesAgainst`
     * @returns Object with `politicalGroup`, `alignmentRate` (0–100), and `divergentVotes` count
     * @since 0.8.0
     */
    function computeGroupAlignment(
      politicalGroup: string,
      stats: { totalVotes: number; votesFor: number; votesAgainst: number }
    ): { politicalGroup: string; alignmentRate: number; divergentVotes: number } {
      const totalDecisive = stats.votesFor + stats.votesAgainst;
      const alignmentRate = totalDecisive > 0
        ? Math.round((stats.votesFor / totalDecisive) * 100 * 100) / 100
        : 0;
      const divergentVotes = stats.votesAgainst;
      return { politicalGroup, alignmentRate, divergentVotes };
    }

    /**
     * Compute cross-party voting metrics from voting statistics.
     * Cross-party rate approximated from 'against' vote ratio—higher against
     * rates suggest more independent voting that may cross party lines.
     *
     * @param stats - Voting statistics containing `totalVotes`, `votesFor`, and `votesAgainst`
     * @returns Object with `withOtherGroups` (count of against-votes used as proxy) and `rate` (0–100)
     * @since 0.8.0
     */
    function computeCrossPartyVoting(
      stats: { totalVotes: number; votesFor: number; votesAgainst: number }
    ): { withOtherGroups: number; rate: number } {
      const totalDecisive = stats.votesFor + stats.votesAgainst;
      const crossPartyRate = totalDecisive > 0
        ? Math.round((stats.votesAgainst / totalDecisive) * 100 * 100) / 100
        : 0;
      return { withOtherGroups: stats.votesAgainst, rate: crossPartyRate };
    }

    /**
     * Compute confidence level based on available voting data.
     *
     * @param totalVotes - Total number of recorded votes for the MEP
     * @returns `'HIGH'` (>500 votes), `'MEDIUM'` (>100 votes), or `'LOW'` (≤100 votes)
     * @since 0.8.0
     */
    function computeConfidence(totalVotes: number): 'HIGH' | 'MEDIUM' | 'LOW' {
      if (totalVotes > 500) return 'HIGH';
      if (totalVotes > 100) return 'MEDIUM';
      return 'LOW';
    }

    /**
     * Handles the analyze_voting_patterns MCP tool request.
     *
     * Analyses an MEP's voting behaviour over an optional date range, computing group
     * alignment rate, cross-party voting frequency, attendance rate, and a data-quality
     * confidence level. When `compareWithGroup` is `true`, group alignment metrics are
     * included in the result.
     *
     * @param args - Raw tool arguments, validated against {@link AnalyzeVotingPatternsSchema}
     * @returns MCP tool result containing a {@link VotingPatternAnalysis} object, or a
     *   `dataAvailable: false` notice when voting statistics are unavailable from the EP API
     * @throws - If `args` fails schema validation (e.g., missing required `mepId`, bad date format)
     * - If the European Parliament API is unreachable or returns an error response
     *
     * @example
     * ```typescript
     * const result = await handleAnalyzeVotingPatterns({
     *   mepId: 'MEP-124810',
     *   dateFrom: '2024-01-01',
     *   dateTo: '2024-12-31',
     *   compareWithGroup: true
     * });
     * // Returns voting statistics, group alignment, and confidence level for MEP-124810
     * ```
     *
     * @security Input is validated with Zod before any API call.
     *   Personal data (MEP name, voting records) is minimised per GDPR Article 5(1)(c).
     *   All requests are rate-limited and audit-logged per ISMS Policy AU-002.
     *   Internal errors are wrapped before propagation to avoid leaking API details.
     * @since 0.8.0
     * @see {@link analyzeVotingPatternsToolMetadata} for MCP schema registration
     * @see {@link handleAnalyzeLegislativeEffectiveness} for broader legislative effectiveness scoring
     */
    export async function handleAnalyzeVotingPatterns(
      args: unknown
    ): Promise<ToolResult> {
      const params = AnalyzeVotingPatternsSchema.parse(args);

      try {
        const mep = await epClient.getMEPDetails(params.mepId);

        const stats = mep.votingStatistics ?? {
          totalVotes: 0,
          votesFor: 0,
          votesAgainst: 0,
          abstentions: 0,
          attendanceRate: 0
        };

        if (stats.totalVotes === 0) {
          return buildToolResponse({
            mepId: params.mepId,
            mepName: mep.name,
            dataAvailable: false,
            dataAvailability: 'UNAVAILABLE',
            confidenceLevel: 'LOW',
            message: 'Voting statistics not available from EP API for this endpoint (/meps/{id}). '
              + 'Use getVotingRecords tool to retrieve actual voting data.'
          });
        }

        const analysis: VotingPatternAnalysis = {
          mepId: params.mepId,
          mepName: mep.name,
          period: {
            from: params.dateFrom ?? '2024-01-01',
            to: params.dateTo ?? '2024-12-31'
          },
          statistics: stats,
          ...(params.compareWithGroup && {
            groupAlignment: computeGroupAlignment(mep.politicalGroup, stats)
          }),
          crossPartyVoting: computeCrossPartyVoting(stats),
          confidenceLevel: computeConfidence(stats.totalVotes),
          methodology: 'Voting pattern analysis using EP Open Data. '
            + 'Group alignment derived from for/against vote ratio. '
            + 'Cross-party voting approximated from against-vote frequency. '
            + 'Data source: European Parliament Open Data Portal.'
        };

        return buildToolResponse(analysis);
      } catch (error: unknown) {
        throw new ToolError({
          toolName: 'analyze_voting_patterns',
          operation: 'fetchVotingData',
          message: 'Failed to retrieve voting records for analysis',
          isRetryable: true,
          cause: error,
        });
      }
    }

    /**
     * Tool metadata for MCP registration
     */
    export const analyzeVotingPatternsToolMetadata = {
      name: 'analyze_voting_patterns',
      description: 'Analyze MEP voting behavior including voting alignment with political group, cross-party voting patterns, attendance rates, and topic-based voting analysis. Returns comprehensive voting statistics and patterns over a specified time period.',
      inputSchema: {
        type: 'object' as const,
        properties: {
          mepId: {
            type: 'string',
            description: 'MEP identifier',
            minLength: 1,
            maxLength: 100
          },
          dateFrom: {
            type: 'string',
            description: 'Analysis start date (YYYY-MM-DD format)',
            pattern: '^\\d{4}-\\d{2}-\\d{2}$'
          },
          dateTo: {
            type: 'string',
            description: 'Analysis end date (YYYY-MM-DD format)',
            pattern: '^\\d{4}-\\d{2}-\\d{2}$'
          },
          compareWithGroup: {
            type: 'boolean',
            description: 'Compare MEP voting with political group average',
            default: true
          }
        },
        required: ['mepId']
      }
    };