From 0e2aebc3de1f0cb0fed195df642b63da5ab8b2a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:12:41 +0000 Subject: [PATCH 1/4] feat: add search_voteringar, search_anforanden, get_propositioner to committee-reports Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .../data-transformers/content-generators.ts | 107 ++++++++++++++++++ scripts/data-transformers/types.ts | 4 + scripts/news-types/committee-reports.ts | 43 ++++--- tests/news-types/committee-reports.test.ts | 61 +++++++++- 4 files changed, 195 insertions(+), 20 deletions(-) diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 690926df..8907e00f 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -463,6 +463,113 @@ export function generateCommitteeContent(data: ArticleContentData, lang: Languag content += ` \n \n`; + // ── Optional: Voting Results section ───────────────────────────────────── + const votes = (data.votes ?? []) as unknown[]; + if (votes.length > 0) { + const votingSectionHeaders: Record = { + sv: 'Röstningsresultat', da: 'Afstemningsresultater', no: 'Voteringsresultater', + fi: 'Äänestystulokset', de: 'Abstimmungsergebnisse', fr: 'Résultats du vote', + es: 'Resultados de la votación', nl: 'Stemresultaten', ar: 'نتائج التصويت', + he: 'תוצאות ההצבעה', ja: '投票結果', ko: '투표 결과', zh: '投票结果', + }; + const votingCountTemplates: Record string> = { + sv: (n) => `${n} röstningsprotokoll visar hur partierna röstade i utskottsbeslut denna period.`, + da: (n) => `${n} afstemningsprotokoller viser, hvordan partierne stemte om udvalgets beslutninger.`, + no: (n) => `${n} voteringsprotokoll viser hvordan partiene stemte i komitévedtak.`, + fi: (n) => `${n} äänestysrekisteriä osoittaa, miten puolueet äänestivät valiokunnan päätöksistä.`, + de: (n) => `${n} Abstimmungsrekorde zeigen, wie die Parteien über Ausschussbeschlüsse abstimmten.`, + fr: (n) => `${n} procès-verbaux de vote montrent comment les partis ont voté sur les décisions de commission.`, + es: (n) => `${n} registros de votación muestran cómo votaron los partidos en las decisiones de la comisión.`, + nl: (n) => `${n} stemregisters tonen hoe partijen stemden over commissiebeslissingen.`, + ar: (n) => `${n} سجلات التصويت تظهر كيف صوتت الأحزاب على قرارات اللجنة.`, + he: (n) => `${n} פרוטוקולי הצבעה מציגים כיצד הצביעו המפלגות על החלטות הוועדה.`, + ja: (n) => `${n}件の投票記録が、委員会決定に対する各党の投票方法を示しています。`, + ko: (n) => `${n}건의 투표 기록이 위원회 결정에 대한 각 정당의 투표 방식을 보여줍니다.`, + zh: (n) => `${n}条投票记录显示各党派对委员会决定的投票情况。`, + }; + const votingHeader = votingSectionHeaders[lang as string] ?? 'Voting Results'; + const votingCountFn = votingCountTemplates[lang as string]; + const votingCountText = votingCountFn + ? votingCountFn(votes.length) + : `${votes.length} voting records show how parties voted on committee decisions this period.`; + content += `\n

${escapeHtml(votingHeader)}

\n`; + content += `

${escapeHtml(votingCountText)}

\n`; + } + + // ── Optional: Committee Debate section ─────────────────────────────────── + const speeches = (data.speeches ?? []) as unknown[]; + if (speeches.length > 0) { + const debateSectionHeaders: Record = { + sv: 'Utskottsdebatt', da: 'Udvalgets debat', no: 'Komitédebatt', + fi: 'Valiokunnan keskustelu', de: 'Ausschussdebatte', fr: 'Débat en commission', + es: 'Debate en comisión', nl: 'Commissiedebat', ar: 'نقاش اللجنة', + he: 'דיון בוועדה', ja: '委員会討論', ko: '위원회 토론', zh: '委员会讨论', + }; + const debateCountTemplates: Record string> = { + sv: (n) => `${n} anföranden i kammaren belyser de viktigaste argumenten och partipositionerna i dessa frågor.`, + da: (n) => `${n} parlamentariske taler belyser nøgleargumenter og partipositioner.`, + no: (n) => `${n} parlamentariske innlegg belyser nøkkelargumenter og partiposisjoner.`, + fi: (n) => `${n} parlamentaarista puheenvuoroa valaisee keskeisiä argumentteja ja puolueiden kantoja.`, + de: (n) => `${n} parlamentarische Reden beleuchten Hauptargumente und Parteipositionen.`, + fr: (n) => `${n} discours parlementaires éclairent les arguments clés et les positions des partis.`, + es: (n) => `${n} discursos parlamentarios iluminan los principales argumentos y posiciones de los partidos.`, + nl: (n) => `${n} parlementaire toespraken belichten de belangrijkste argumenten en partijposities.`, + ar: (n) => `${n} خطاب برلماني يسلط الضوء على الحجج الرئيسية ومواقف الأحزاب.`, + he: (n) => `${n} נאומים פרלמנטריים מאירים טיעונים מרכזיים ועמדות מפלגות.`, + ja: (n) => `${n}件の議会演説が主要な論点と各党の立場を明らかにしています。`, + ko: (n) => `${n}건의 의회 연설이 주요 논점과 각 정당의 입장을 보여줍니다.`, + zh: (n) => `${n}篇议会演讲揭示了主要论点和各党派立场。`, + }; + const debateHeader = debateSectionHeaders[lang as string] ?? 'Committee Debate'; + const debateCountFn = debateCountTemplates[lang as string]; + const debateCountText = debateCountFn + ? debateCountFn(speeches.length) + : `${speeches.length} parliamentary speeches highlight key arguments and party positions on these issues.`; + content += `\n

${escapeHtml(debateHeader)}

\n`; + content += `

${escapeHtml(debateCountText)}

\n`; + } + + // ── Optional: Government Bill Linkage section ───────────────────────────── + const propositions = (data.propositions ?? []) as RawDocument[]; + if (propositions.length > 0) { + const billSectionHeaders: Record = { + sv: 'Koppling till regeringspropositioner', da: 'Tilknytning til regeringsforslag', + no: 'Tilknytning til regjeringsproposisjoner', fi: 'Yhteys hallituksen esityksiin', + de: 'Verknüpfung mit Regierungsvorlagen', fr: 'Lien avec les projets de loi gouvernementaux', + es: 'Vinculación con proyectos de ley gubernamentales', nl: 'Koppeling aan regeringsvoorstellen', + ar: 'الصلة بمشاريع قوانين الحكومة', he: 'קישור להצעות חוק ממשלתיות', + ja: '政府法案との連携', ko: '정부 법안과의 연계', zh: '与政府法案的关联', + }; + const billCountTemplates: Record string> = { + sv: (n) => `${n} regeringspropositioner är kopplade till dessa betänkanden och visar lagstiftningskedjan.`, + da: (n) => `${n} regeringsforslag er knyttet til disse betænkninger og viser den lovgivningsmæssige kæde.`, + no: (n) => `${n} regjeringsproposisjoner er knyttet til disse innstillingene og viser den legislative kjeden.`, + fi: (n) => `${n} hallituksen esitystä liittyy näihin mietintöihin ja osoittaa lainsäädäntöketjun.`, + de: (n) => `${n} Regierungsvorlagen sind mit diesen Berichten verknüpft und zeigen die Gesetzgebungskette.`, + fr: (n) => `${n} projets de loi gouvernementaux sont liés à ces rapports, montrant la chaîne législative.`, + es: (n) => `${n} proyectos de ley gubernamentales están vinculados a estos informes, mostrando la cadena legislativa.`, + nl: (n) => `${n} regeringsvoorstellen zijn gekoppeld aan deze rapporten en tonen de wetgevingsketen.`, + ar: (n) => `${n} مشاريع قوانين حكومية مرتبطة بهذه التقارير، مما يُظهر السلسلة التشريعية.`, + he: (n) => `${n} הצעות חוק ממשלתיות קשורות לדוחות אלה, ומציגות את השרשרת החקיקתית.`, + ja: (n) => `${n}件の政府法案がこれらの報告書に関連しており、立法プロセスの連鎖を示しています。`, + ko: (n) => `${n}건의 정부 법안이 이 보고서들과 연계되어 입법 과정의 연결고리를 보여줍니다.`, + zh: (n) => `${n}项政府法案与这些报告相关,展示了立法链条。`, + }; + const billHeader = billSectionHeaders[lang as string] ?? 'Government Bill Linkage'; + const billCountFn = billCountTemplates[lang as string]; + const billCountText = billCountFn + ? billCountFn(propositions.length) + : `${propositions.length} government propositions are linked to these reports, tracing the full legislative chain.`; + content += `\n

${escapeHtml(billHeader)}

\n`; + content += `

${escapeHtml(billCountText)}

\n`; + propositions.slice(0, 3).forEach(prop => { // display up to 3 linked propositions + const propTitle = escapeHtml(prop.titel || prop.title || prop.dokumentnamn || ''); + if (propTitle) { + content += `

→ ${propTitle}

\n`; + } + }); + } + return content; } diff --git a/scripts/data-transformers/types.ts b/scripts/data-transformers/types.ts index 3193ec50..1dc69836 100644 --- a/scripts/data-transformers/types.ts +++ b/scripts/data-transformers/types.ts @@ -91,4 +91,8 @@ export interface ArticleContentData { context?: string; /** CIA intelligence context for enriched analysis */ ciaContext?: CIAContext; + /** Voting records for cross-referencing committee decisions */ + votes?: unknown[]; + /** Parliamentary speeches for committee debate context */ + speeches?: unknown[]; } diff --git a/scripts/news-types/committee-reports.ts b/scripts/news-types/committee-reports.ts index cb6516b7..0350419a 100644 --- a/scripts/news-types/committee-reports.ts +++ b/scripts/news-types/committee-reports.ts @@ -183,17 +183,12 @@ import type { ArticleCategory, GeneratedArticle, GenerationResult, MCPCallRecord /** * Required MCP tools for committee-reports articles - * - * REQUIRED_TOOLS UPDATE (2026-02-14): - * Initially set to 4 tools ['get_betankanden', 'search_voteringar', 'search_anforanden', 'get_propositioner'] - * to match tests/validation expectations. However, this caused runtime validation failures - * since the implementation only calls get_betankanden (line 66). - * - * Reverted to actual implementation (1 tool) to prevent validation failures. - * When additional tools are implemented in generateCommitteeReports(), add them back here. */ export const REQUIRED_TOOLS: readonly string[] = [ - 'get_betankanden' + 'get_betankanden', + 'search_voteringar', + 'search_anforanden', + 'get_propositioner' ]; export interface TitleSet { @@ -255,8 +250,24 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): return { success: true, files: 0, mcpCalls }; } - // Cross-reference with votes and debates (optional enhancement) - // Future: Add voteringar, anforanden, propositioner queries here + // Step 2: Enrich with voting patterns, speeches, and propositions (non-fatal) + console.log(' 🔄 Fetching voting patterns, speeches, and propositions...'); + const currentRm = '2025/26'; + const [votes, speeches, propositions] = await Promise.all([ + Promise.resolve() + .then(() => client.fetchVotingRecords({ rm: currentRm, limit: 20 }) as Promise) + .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch voting records:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), + Promise.resolve() + .then(() => client.searchSpeeches({ rm: currentRm, limit: 15 }) as Promise) + .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch speeches:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), + Promise.resolve() + .then(() => client.fetchPropositions(20) as Promise) + .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch propositions:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), + ]); + mcpCalls.push({ tool: 'search_voteringar', result: votes }); + mcpCalls.push({ tool: 'search_anforanden', result: speeches }); + mcpCalls.push({ tool: 'get_propositioner', result: propositions }); + console.log(` 🗳 Found ${votes.length} voting records, ${speeches.length} speeches, ${(propositions as unknown[]).length} propositions`); const today = new Date(); const slug = `${formatDateForSlug(today)}-committee-reports`; @@ -265,11 +276,15 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): for (const lang of languages) { console.log(` 🌐 Generating ${lang.toUpperCase()} version...`); - const content: string = generateArticleContent({ reports }, 'committee-reports', lang); + const content: string = generateArticleContent( + { reports, votes, speeches, propositions: propositions as RawDocument[] }, + 'committee-reports', + lang + ); const watchPoints = extractWatchPoints({ reports }, lang); const metadata = generateMetadata({ reports }, 'committee-reports', lang); const readTime: string = calculateReadTime(content); - const sources: string[] = generateSources(['get_betankanden']); + const sources: string[] = generateSources(['get_betankanden', 'search_voteringar', 'search_anforanden', 'get_propositioner']); const titles: TitleSet = getTitles(lang, reports.length, reports); @@ -310,7 +325,7 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): mcpCalls, crossReferences: { event: `${reports.length} reports`, - sources: ['betankanden'] + sources: ['betankanden', 'voteringar', 'anforanden', 'propositioner'] } }; diff --git a/tests/news-types/committee-reports.test.ts b/tests/news-types/committee-reports.test.ts index cf26472f..22d0226c 100644 --- a/tests/news-types/committee-reports.test.ts +++ b/tests/news-types/committee-reports.test.ts @@ -25,6 +25,9 @@ interface CommitteeReport { /** Mock MCP client shape */ interface MockMCPClientShape { fetchCommitteeReports: Mock<(limit: number) => Promise>; + fetchVotingRecords: Mock<(filters: object) => Promise>; + searchSpeeches: Mock<(params: object) => Promise>; + fetchPropositions: Mock<(limit: number) => Promise>; } /** Validation input */ @@ -75,7 +78,10 @@ const { mockClientInstance, mockCommitteeReports, MockMCPClient } = vi.hoisted(( ]; const mockClientInstance: MockMCPClientShape = { - fetchCommitteeReports: vi.fn().mockResolvedValue(mockCommitteeReports) as MockMCPClientShape['fetchCommitteeReports'] + fetchCommitteeReports: vi.fn().mockResolvedValue(mockCommitteeReports) as MockMCPClientShape['fetchCommitteeReports'], + fetchVotingRecords: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchVotingRecords'], + searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches'], + fetchPropositions: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchPropositions'], }; function MockMCPClient(): MockMCPClientShape { @@ -99,6 +105,9 @@ describe('Committee Reports Article Generation', () => { beforeEach(() => { vi.clearAllMocks(); mockClientInstance.fetchCommitteeReports.mockResolvedValue(mockCommitteeReports); + mockClientInstance.fetchVotingRecords.mockResolvedValue([]); + mockClientInstance.searchSpeeches.mockResolvedValue([]); + mockClientInstance.fetchPropositions.mockResolvedValue([]); }); afterEach(() => { @@ -116,11 +125,13 @@ describe('Committee Reports Article Generation', () => { expect(committeeReportsModule.REQUIRED_TOOLS).toContain('get_betankanden'); }); - it('should only list tools actually used by generateCommitteeReports', () => { - // REQUIRED_TOOLS must match tools actually called in the implementation. - // Future cross-referencing tools (voteringar, anforanden, propositioner) - // should be added here when implemented in generateCommitteeReports(). - expect(committeeReportsModule.REQUIRED_TOOLS).toEqual(['get_betankanden']); + it('should require all four MCP tools', () => { + expect(committeeReportsModule.REQUIRED_TOOLS).toEqual([ + 'get_betankanden', + 'search_voteringar', + 'search_anforanden', + 'get_propositioner' + ]); }); }); @@ -134,6 +145,33 @@ describe('Committee Reports Article Generation', () => { expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_betankanden')).toBe(true); }); + it('should call search_voteringar for voting patterns', async () => { + const result = await committeeReportsModule.generateCommitteeReports({ + languages: ['en'] + }); + + expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled(); + expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'search_voteringar')).toBe(true); + }); + + it('should call search_anforanden for debate context', async () => { + const result = await committeeReportsModule.generateCommitteeReports({ + languages: ['en'] + }); + + expect(mockClientInstance.searchSpeeches).toHaveBeenCalled(); + expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'search_anforanden')).toBe(true); + }); + + it('should call get_propositioner for bill linkage', async () => { + const result = await committeeReportsModule.generateCommitteeReports({ + languages: ['en'] + }); + + expect(mockClientInstance.fetchPropositions).toHaveBeenCalled(); + expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_propositioner')).toBe(true); + }); + it('should fetch specified number of reports', async () => { await committeeReportsModule.generateCommitteeReports({ languages: ['en'], @@ -250,6 +288,17 @@ describe('Committee Reports Article Generation', () => { expect(result.crossReferences.sources).toContain('betankanden'); }); + + it('should track all four data sources', async () => { + const result = await committeeReportsModule.generateCommitteeReports({ + languages: ['en'] + }); + + expect(result.crossReferences.sources).toContain('betankanden'); + expect(result.crossReferences.sources).toContain('voteringar'); + expect(result.crossReferences.sources).toContain('anforanden'); + expect(result.crossReferences.sources).toContain('propositioner'); + }); }); describe('Validation Functions', () => { From ab8bff61e9bea455e1f577c17bb444feb8981d15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 06:45:30 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20derive=20riksm=C3=B6te=20dynamically?= =?UTF-8?q?,=20pass=20rm=20to=20fetchPropositions,=20add=20enrichment=20se?= =?UTF-8?q?ction=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/news-types/committee-reports.ts | 9 ++- tests/data-transformers.test.ts | 75 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/scripts/news-types/committee-reports.ts b/scripts/news-types/committee-reports.ts index 0350419a..0f0c22d6 100644 --- a/scripts/news-types/committee-reports.ts +++ b/scripts/news-types/committee-reports.ts @@ -252,7 +252,12 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): // Step 2: Enrich with voting patterns, speeches, and propositions (non-fatal) console.log(' 🔄 Fetching voting patterns, speeches, and propositions...'); - const currentRm = '2025/26'; + // Derive riksmöte from the first report's rm field, or calculate from current date. + // Parliamentary year starts in September; e.g. Sep 2025–Aug 2026 → '2025/26'. + const firstReportRm = (reports[0] as Record)?.['rm'] as string | undefined; + const now = new Date(); + const startYear = now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1; + const currentRm = firstReportRm ?? `${startYear}/${String(startYear + 1).slice(-2)}`; const [votes, speeches, propositions] = await Promise.all([ Promise.resolve() .then(() => client.fetchVotingRecords({ rm: currentRm, limit: 20 }) as Promise) @@ -261,7 +266,7 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): .then(() => client.searchSpeeches({ rm: currentRm, limit: 15 }) as Promise) .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch speeches:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), Promise.resolve() - .then(() => client.fetchPropositions(20) as Promise) + .then(() => client.fetchPropositions(20, currentRm) as Promise) .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch propositions:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), ]); mcpCalls.push({ tool: 'search_voteringar', result: votes }); diff --git a/tests/data-transformers.test.ts b/tests/data-transformers.test.ts index 2ab29ac9..4657c385 100644 --- a/tests/data-transformers.test.ts +++ b/tests/data-transformers.test.ts @@ -79,6 +79,8 @@ interface MockArticlePayload { dok_id?: string; organ?: string; }>; + votes?: unknown[]; + speeches?: unknown[]; } describe('Data Transformers', () => { @@ -378,6 +380,79 @@ describe('Data Transformers', () => { expect(content).toContain('No committee reports'); }); + it('should render Voting Results section when votes are provided', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], votes: [{}] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).toContain('Voting Results'); + }); + + it('should omit Voting Results section when votes array is empty', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], votes: [] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).not.toContain('Voting Results'); + }); + + it('should render Committee Debate section when speeches are provided', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], speeches: [{}] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).toContain('Committee Debate'); + }); + + it('should omit Committee Debate section when speeches array is empty', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], speeches: [] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).not.toContain('Committee Debate'); + }); + + it('should render Government Bill Linkage section when propositions are provided', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], propositions: [{ titel: 'Prop 2025/26:1' }] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).toContain('Government Bill Linkage'); + expect(content).toContain('Prop 2025/26:1'); + }); + + it('should omit Government Bill Linkage section when propositions array is empty', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test Report', url: '#', organ: 'FiU' }], propositions: [] } as MockArticlePayload, + 'committee-reports', + 'en' + ) as string; + expect(content).not.toContain('Government Bill Linkage'); + }); + + it('should render Swedish Röstningsresultat heading for sv language when votes provided', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test', url: '#', organ: 'FiU' }], votes: [{}] } as MockArticlePayload, + 'committee-reports', + 'sv' + ) as string; + expect(content).toContain('Röstningsresultat'); + }); + + it('should render Swedish Utskottsdebatt heading for sv language when speeches provided', () => { + const content = generateArticleContent( + { reports: [{ titel: 'Test', url: '#', organ: 'FiU' }], speeches: [{}] } as MockArticlePayload, + 'committee-reports', + 'sv' + ) as string; + expect(content).toContain('Utskottsdebatt'); + }); + it('should handle propositions with data', () => { const content = generateArticleContent({ propositions: [{ titel: 'Test Prop', url: '#', dokumentnamn: 'Prop 2025/26:1' }] From ffd42c9787c52da3ab530e315ce705c5fce8981b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:18:37 +0000 Subject: [PATCH 3/4] fix(tests): correct fetchPropositions mock signature and assert rm param in enrichment calls Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- tests/news-types/committee-reports.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/news-types/committee-reports.test.ts b/tests/news-types/committee-reports.test.ts index 22d0226c..db95b6a1 100644 --- a/tests/news-types/committee-reports.test.ts +++ b/tests/news-types/committee-reports.test.ts @@ -27,7 +27,7 @@ interface MockMCPClientShape { fetchCommitteeReports: Mock<(limit: number) => Promise>; fetchVotingRecords: Mock<(filters: object) => Promise>; searchSpeeches: Mock<(params: object) => Promise>; - fetchPropositions: Mock<(limit: number) => Promise>; + fetchPropositions: Mock<(limit?: number, rm?: string | null) => Promise>; } /** Validation input */ @@ -150,7 +150,9 @@ describe('Committee Reports Article Generation', () => { languages: ['en'] }); - expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled(); + expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalledWith( + expect.objectContaining({ rm: '2024/25', limit: 20 }) + ); expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'search_voteringar')).toBe(true); }); @@ -159,7 +161,9 @@ describe('Committee Reports Article Generation', () => { languages: ['en'] }); - expect(mockClientInstance.searchSpeeches).toHaveBeenCalled(); + expect(mockClientInstance.searchSpeeches).toHaveBeenCalledWith( + expect.objectContaining({ rm: '2024/25', limit: 15 }) + ); expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'search_anforanden')).toBe(true); }); @@ -168,7 +172,7 @@ describe('Committee Reports Article Generation', () => { languages: ['en'] }); - expect(mockClientInstance.fetchPropositions).toHaveBeenCalled(); + expect(mockClientInstance.fetchPropositions).toHaveBeenCalledWith(20, '2024/25'); expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_propositioner')).toBe(true); }); From d39d17557865f92872f259e60730de211e52e1af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:21:32 +0000 Subject: [PATCH 4/4] fix: validate rm format, normalize propositions, guard prop loop, extend generateSources Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/data-transformers/content-generators.ts | 3 ++- scripts/data-transformers/metadata.ts | 6 ++++++ scripts/news-types/committee-reports.ts | 12 ++++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 0bd3f8eb..cc5357c1 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -658,7 +658,8 @@ export function generateCommitteeContent(data: ArticleContentData, lang: Languag content += `\n

${escapeHtml(billHeader)}

\n`; content += `

${escapeHtml(billCountText)}

\n`; propositions.slice(0, 3).forEach(prop => { // display up to 3 linked propositions - const propTitle = escapeHtml(prop.titel || prop.title || prop.dokumentnamn || ''); + if (typeof prop !== 'object' || prop === null) return; + const propTitle = escapeHtml((prop as RawDocument).titel || (prop as RawDocument).title || (prop as RawDocument).dokumentnamn || ''); if (propTitle) { content += `

→ ${propTitle}

\n`; } diff --git a/scripts/data-transformers/metadata.ts b/scripts/data-transformers/metadata.ts index 655d23b0..e23d07d9 100644 --- a/scripts/data-transformers/metadata.ts +++ b/scripts/data-transformers/metadata.ts @@ -340,6 +340,12 @@ export function generateSources(tools: string[] = []): string[] { if (tools.includes('get_dokument_innehall')) { sources.push('Riksdagen Document Content'); } + if (tools.includes('search_voteringar')) { + sources.push('Riksdagen Voting Records'); + } + if (tools.includes('search_anforanden')) { + sources.push('Riksdagen Speeches'); + } return sources; } diff --git a/scripts/news-types/committee-reports.ts b/scripts/news-types/committee-reports.ts index 0f0c22d6..e4b6c93d 100644 --- a/scripts/news-types/committee-reports.ts +++ b/scripts/news-types/committee-reports.ts @@ -254,7 +254,10 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): console.log(' 🔄 Fetching voting patterns, speeches, and propositions...'); // Derive riksmöte from the first report's rm field, or calculate from current date. // Parliamentary year starts in September; e.g. Sep 2025–Aug 2026 → '2025/26'. - const firstReportRm = (reports[0] as Record)?.['rm'] as string | undefined; + const firstReportRaw = (reports[0] as Record)?.['rm']; + const rmPattern = /^\d{4}\/\d{2}$/; + const firstReportRm: string | undefined = + typeof firstReportRaw === 'string' && rmPattern.test(firstReportRaw) ? firstReportRaw : undefined; const now = new Date(); const startYear = now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1; const currentRm = firstReportRm ?? `${startYear}/${String(startYear + 1).slice(-2)}`; @@ -274,6 +277,11 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): mcpCalls.push({ tool: 'get_propositioner', result: propositions }); console.log(` 🗳 Found ${votes.length} voting records, ${speeches.length} speeches, ${(propositions as unknown[]).length} propositions`); + // Normalize propositions: keep only plain objects to avoid runtime errors in content generator + const safePropositions = propositions.filter( + (p): p is RawDocument => typeof p === 'object' && p !== null + ); + const today = new Date(); const slug = `${formatDateForSlug(today)}-committee-reports`; const articles: GeneratedArticle[] = []; @@ -282,7 +290,7 @@ export async function generateCommitteeReports(options: GenerationOptions = {}): console.log(` 🌐 Generating ${lang.toUpperCase()} version...`); const content: string = generateArticleContent( - { reports, votes, speeches, propositions: propositions as RawDocument[] }, + { reports, votes, speeches, propositions: safePropositions }, 'committee-reports', lang );