All files / generators/strategies committee-reports-strategy.ts

88.46% Statements 23/26
62.5% Branches 5/8
88.88% Functions 8/9
88.46% Lines 23/26

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                                          8x     8x     8x     8x                                         5x     5x     3x                   5x       5x                             5x                                 11x   11x                                 1x 5x                 1x 5x     1x                     5x                     3x 3x 3x 3x                     8x  
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Generators/Strategies/CommitteeReportsStrategy
 * @description Article strategy for the Committee Reports article type.
 * Fetches data for each featured committee in parallel and renders an
 * effectiveness and activity overview article.
 */
 
import type { EuropeanParliamentMCPClient } from '../../mcp/ep-mcp-client.js';
import { ArticleCategory } from '../../types/index.js';
import type { LanguageCode, CommitteeData } from '../../types/index.js';
import { COMMITTEE_REPORTS_TITLES, getLocalizedString } from '../../constants/languages.js';
import { fetchCommitteeData } from '../pipeline/fetch-stage.js';
import { FEATURED_COMMITTEES } from '../committee-helpers.js';
import { escapeHTML } from '../../utils/file-utils.js';
import type { ArticleStrategy, ArticleData, ArticleMetadata } from './article-strategy.js';
import type { ArticleSource } from '../../types/index.js';
 
/** European Parliament home-page URL used as source reference */
const EP_SOURCE_URL = 'https://www.europarl.europa.eu';
 
/** European Parliament display name for source titles and article lede */
const EP_DISPLAY_NAME = 'European Parliament';
 
/** Keywords shared by all Committee Reports articles */
const COMMITTEE_REPORTS_KEYWORDS = ['committee', 'EU Parliament', 'legislation'] as const;
 
/** Source reference included in every committee reports article */
const COMMITTEE_REPORTS_SOURCES: readonly ArticleSource[] = [
  { title: EP_DISPLAY_NAME, url: EP_SOURCE_URL },
];
 
// ─── Data payload ─────────────────────────────────────────────────────────────
 
/** Data fetched and pre-processed by {@link CommitteeReportsStrategy} */
export interface CommitteeReportsArticleData extends ArticleData {
  /** Resolved data for each featured committee */
  readonly committeeDataList: readonly CommitteeData[];
}
 
// ─── HTML builders ────────────────────────────────────────────────────────────
 
/**
 * Build the HTML body for a committee reports article.
 *
 * @param committeeDataList - Pre-fetched committee data
 * @returns Article HTML body
 */
function buildCommitteeReportsHTML(committeeDataList: readonly CommitteeData[]): string {
  const committeeSections = committeeDataList
    .map((committee) => {
      const docItems =
        committee.documents.length > 0
          ? committee.documents
              .map(
                (doc) => `
                <li class="document-item">
                  <span class="document-type">${escapeHTML(doc.type)}</span>
                  <span class="document-title">${escapeHTML(doc.title)}</span>
                  ${doc.date ? `<span class="document-date">${escapeHTML(doc.date)}</span>` : ''}
                </li>`
              )
              .join('')
          : '<li>No recent documents available</li>';
 
      const effectivenessHtml = committee.effectiveness
        ? `<p class="effectiveness-score">${escapeHTML(committee.effectiveness)}</p>`
        : '';
 
      return `
      <section class="committee-card">
        <h3 class="committee-name">${escapeHTML(committee.name)} (${escapeHTML(committee.abbreviation)})</h3>
        <div class="committee-meta">
          <span class="committee-chair">Chair: ${escapeHTML(committee.chair)}</span>
          <span class="committee-members">Members: ${committee.members}</span>
        </div>
        <section class="recent-activity">
          <ul class="document-list">${docItems}</ul>
        </section>
        <section class="effectiveness-metrics">${effectivenessHtml}</section>
      </section>`;
    })
    .join('');
 
  return `
    <div class="article-content">
      <section class="committee-overview">
        <p class="lede">${escapeHTML(EP_DISPLAY_NAME)} committee activity and legislative effectiveness analysis.</p>
      </section>
      <section class="committee-reports">${committeeSections}</section>
    </div>`;
}
 
// ─── Strategy implementation ──────────────────────────────────────────────────
 
/**
 * Article strategy for {@link ArticleCategory.COMMITTEE_REPORTS}.
 * Fetches info, documents and effectiveness data for the featured committees
 * then renders an activity overview.
 */
export class CommitteeReportsStrategy implements ArticleStrategy<CommitteeReportsArticleData> {
  readonly type = ArticleCategory.COMMITTEE_REPORTS;
 
  readonly requiredMCPTools = [
    'get_committee_info',
    'search_documents',
    'analyze_legislative_effectiveness',
  ] as const;
 
  /**
   * Fetch committee data for all featured committees in parallel.
   *
   * @param client - MCP client or null
   * @param date - ISO 8601 publication date
   * @returns Populated committee reports data payload
   */
  async fetchData(
    client: EuropeanParliamentMCPClient | null,
    date: string
  ): Promise<CommitteeReportsArticleData> {
    const committeeDataRaw = await Promise.all(
      FEATURED_COMMITTEES.map((abbr) =>
        fetchCommitteeData(client, abbr).catch((error: unknown) => {
          const message = error instanceof Error ? error.message : String(error);
          console.error(`  ⚠️ Failed to fetch data for committee ${abbr}:`, message);
          return null;
        })
      )
    );
 
    const committeeDataList = committeeDataRaw.filter(
      (committee): committee is CommitteeData => committee !== null
    );
 
    return { date, committeeDataList };
  }
 
  /**
   * Build the committee reports HTML body.
   *
   * @param data - Committee reports data payload
   * @param _lang - Language code (unused — content is language-independent)
   * @returns Article HTML body
   */
  buildContent(data: CommitteeReportsArticleData, _lang: LanguageCode): string {
    return buildCommitteeReportsHTML(data.committeeDataList);
  }
 
  /**
   * Return language-specific metadata for the committee reports article.
   *
   * @param _data - Committee reports data payload (unused — metadata is data-independent)
   * @param lang - Target language code
   * @returns Localised metadata
   */
  getMetadata(_data: CommitteeReportsArticleData, lang: LanguageCode): ArticleMetadata {
    const committeeLabel = FEATURED_COMMITTEES.join(', ');
    const titleFn = getLocalizedString(COMMITTEE_REPORTS_TITLES, lang);
    const { title, subtitle } = titleFn(committeeLabel);
    return {
      title,
      subtitle,
      keywords: [...COMMITTEE_REPORTS_KEYWORDS],
      category: ArticleCategory.COMMITTEE_REPORTS,
      sources: COMMITTEE_REPORTS_SOURCES,
    };
  }
}
 
/** Singleton instance for use by the strategy registry */
export const committeeReportsStrategy = new CommitteeReportsStrategy();