Conversation
…committee-reports Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Implements the previously-missing MCP enrichment for committee report articles so they include voting records, debate/speech context, and proposition linkage, and updates the data pipeline/types/tests accordingly.
Changes:
- Restores
REQUIRED_TOOLSfor committee reports to the full 4-tool set and records MCP calls for all tools. - Fetches votes, speeches, and propositions in parallel and passes them into article content generation.
- Extends
ArticleContentDataand committee content generation with optional enrichment sections; updates and expands unit tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
scripts/news-types/committee-reports.ts |
Restores 4-tool requirement and adds parallel enrichment (votes/speeches/propositions) into committee report generation. |
scripts/data-transformers/types.ts |
Extends ArticleContentData with optional votes and speeches fields. |
scripts/data-transformers/content-generators.ts |
Adds optional per-language sections for voting results, debate context, and proposition linkage in committee reports content. |
tests/news-types/committee-reports.test.ts |
Updates mocks and assertions to validate the expanded tool usage and cross-reference tracking. |
| // 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<unknown[]>) | ||
| .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<unknown[]>) | ||
| .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<unknown[]>) | ||
| .catch((err: unknown) => { console.error(' ⚠️ Failed to fetch propositions:', (err as Error)?.message ?? String(err)); return [] as unknown[]; }), |
There was a problem hiding this comment.
currentRm is hard-coded to '2025/26', while the report fetch (fetchCommitteeReports(limit)) is not scoped to the same rm. This can produce enrichment data (votes/speeches/propositions) from a different parliamentary session than the reports, and will silently become stale when the session changes. Consider deriving rm from the fetched report data (if available), or passing the same rm consistently into all three enrichment calls (and fetchCommitteeReports) / making it an option.
There was a problem hiding this comment.
Fixed in ab8bff6. currentRm is now derived from the first fetched report's rm field when available, falling back to a date-based calculation (month ≥ 8/September → current year, else previous year) matching the parliamentary calendar. This ensures all three enrichment calls use the same session as the reports themselves.
| .then(() => client.searchSpeeches({ rm: currentRm, limit: 15 }) as Promise<unknown[]>) | ||
| .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<unknown[]>) |
There was a problem hiding this comment.
fetchPropositions supports an rm parameter (limit, rm), but the call here omits it even though currentRm is defined and used for votes/speeches. This makes proposition enrichment potentially inconsistent with the rest of the enrichment set; pass currentRm (or remove currentRm entirely and rely on server defaults consistently).
| .then(() => client.fetchPropositions(20) as Promise<unknown[]>) | |
| .then(() => client.fetchPropositions(20, currentRm) as Promise<unknown[]>) |
There was a problem hiding this comment.
Fixed in ab8bff6 — currentRm is now passed as the second argument to fetchPropositions(20, currentRm), keeping all three enrichment calls consistent.
| // ── Optional: Voting Results section ───────────────────────────────────── | ||
| const votes = (data.votes ?? []) as unknown[]; | ||
| if (votes.length > 0) { | ||
| const votingSectionHeaders: Record<string, string> = { | ||
| 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, (n: number) => 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 <h2>${escapeHtml(votingHeader)}</h2>\n`; | ||
| content += ` <p>${escapeHtml(votingCountText)}</p>\n`; | ||
| } | ||
|
|
||
| // ── Optional: Committee Debate section ─────────────────────────────────── | ||
| const speeches = (data.speeches ?? []) as unknown[]; | ||
| if (speeches.length > 0) { | ||
| const debateSectionHeaders: Record<string, string> = { | ||
| 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, (n: number) => 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 <h2>${escapeHtml(debateHeader)}</h2>\n`; | ||
| content += ` <p>${escapeHtml(debateCountText)}</p>\n`; | ||
| } | ||
|
|
There was a problem hiding this comment.
New optional sections (Voting Results / Committee Debate / Government Bill Linkage) are rendered based on votes, speeches, and propositions, but there are no unit tests asserting that these sections appear when non-empty and are omitted when empty. Add focused tests (e.g., in tests/data-transformers.test.ts) that pass in minimal non-empty arrays and assert the expected headings/text are present/absent.
There was a problem hiding this comment.
Added in ab8bff6. Eight new tests in tests/data-transformers.test.ts assert that each optional section (Voting Results, Committee Debate, Government Bill Linkage) is rendered when its respective array is non-empty and omitted when empty, plus Swedish heading variants for sv locale.
|
@copilot apply changes based on the comments in this thread |
…nrichment section tests Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Done in ab8bff6. All three review comments addressed: dynamic riksmöte derivation, |
| fetchCommitteeReports: Mock<(limit: number) => Promise<CommitteeReport[]>>; | ||
| fetchVotingRecords: Mock<(filters: object) => Promise<unknown[]>>; | ||
| searchSpeeches: Mock<(params: object) => Promise<unknown[]>>; | ||
| fetchPropositions: Mock<(limit: number) => Promise<unknown[]>>; |
There was a problem hiding this comment.
The mock signature for fetchPropositions is incomplete. The actual method signature is fetchPropositions(limit = 10, rm: string | null = null), which takes two parameters. The mock should reflect this to accurately test that the rm parameter is being passed correctly.
| fetchPropositions: Mock<(limit: number) => Promise<unknown[]>>; | |
| fetchPropositions: Mock<(limit?: number, rm?: string | null) => Promise<unknown[]>>; |
There was a problem hiding this comment.
Fixed in ffd42c9 — mock signature updated to Mock<(limit?: number, rm?: string | null) => Promise<unknown[]>>.
| 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); | ||
| }); |
There was a problem hiding this comment.
While the tests verify that enrichment methods are called, they don't verify that the rm parameter is correctly passed to fetchVotingRecords, searchSpeeches, and fetchPropositions. Consider adding assertions like expect(mockClientInstance.fetchPropositions).toHaveBeenCalledWith(20, '2024/25') to verify the riksmöte session parameter is consistently passed to all enrichment calls.
There was a problem hiding this comment.
Fixed in ffd42c9. Assertions now verify the rm parameter is passed consistently to all three enrichment calls:
fetchVotingRecords—expect.objectContaining({ rm: '2024/25', limit: 20 })searchSpeeches—expect.objectContaining({ rm: '2024/25', limit: 15 })fetchPropositions—toHaveBeenCalledWith(20, '2024/25')
The '2024/25' value is derived from the mock report data's rm field, matching the actual runtime behaviour.
|
@copilot apply changes based on the comments in this thread |
…ram in enrichment calls Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Done in ffd42c9 — both review comments addressed. |
Signed-off-by: James Pether Sörling <pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| 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<string, unknown>)?.['rm'] as string | undefined; |
There was a problem hiding this comment.
firstReportRm is read via a forced cast (as string | undefined). Since rm is not part of the RawDocument type here, this can silently pass a non-string (or an invalid session format) into the enrichment calls. Prefer extracting rm as unknown and validating typeof rm === 'string' (and optionally a simple YYYY/YY format check) before using it; otherwise fall back to the date-derived value.
| const firstReportRm = (reports[0] as Record<string, unknown>)?.['rm'] as string | undefined; | |
| const firstReportRaw = (reports[0] as Record<string, unknown>)?.['rm']; | |
| let firstReportRm: string | undefined; | |
| if (typeof firstReportRaw === 'string') { | |
| // Basic riksmöte format validation: 'YYYY/YY', e.g. '2025/26' | |
| const rmPattern = /^\d{4}\/\d{2}$/; | |
| if (rmPattern.test(firstReportRaw)) { | |
| firstReportRm = firstReportRaw; | |
| } | |
| } |
There was a problem hiding this comment.
Fixed in d39d175. firstReportRm is now validated as a string matching /^\d{4}\/\d{2}$/ before use; invalid or non-string values fall through to the date-based calculation.
| const content: string = generateArticleContent( | ||
| { reports, votes, speeches, propositions: propositions as RawDocument[] }, | ||
| 'committee-reports', |
There was a problem hiding this comment.
propositions is unknown[] from fetchPropositions, but it’s passed into generateArticleContent as propositions: propositions as RawDocument[]. If the MCP response ever contains non-object entries, downstream rendering (which reads prop.titel/title/dokumentnamn) will throw at runtime. Consider filtering/normalizing propositions to plain objects (or to RawDocument-shaped records) before passing it into the content generator.
There was a problem hiding this comment.
Fixed in d39d175. propositions is now filtered to only plain objects (safePropositions) before being passed to the content generator, eliminating the unsafe cast.
| propositions.slice(0, 3).forEach(prop => { // display up to 3 linked propositions | ||
| const propTitle = escapeHtml(prop.titel || prop.title || prop.dokumentnamn || ''); | ||
| if (propTitle) { | ||
| content += ` <p>→ ${propTitle}</p>\n`; | ||
| } | ||
| }); |
There was a problem hiding this comment.
propositions is treated as RawDocument[], but the loop accesses prop.titel/prop.title/prop.dokumentnamn directly. If any entry is not a non-null object, this will throw. Consider guarding inside the loop (e.g., skip non-object entries) or pre-filtering propositions to only objects with string fields before rendering.
There was a problem hiding this comment.
Fixed in d39d175. The loop now skips entries where typeof prop !== 'object' || prop === null before accessing title fields.
| 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']); |
There was a problem hiding this comment.
generateSources() currently only adds specific source labels for a subset of tool names (e.g. get_betankanden, get_propositioner, search_dokument), and does not handle search_voteringar or search_anforanden. Passing those tool names here is therefore misleading (no additional sources will be listed). Either extend generateSources to map these tools to appropriate source labels, or omit them from this call to avoid implying they’re reflected in the sources list.
| const sources: string[] = generateSources(['get_betankanden', 'search_voteringar', 'search_anforanden', 'get_propositioner']); | |
| const sources: string[] = generateSources(['get_betankanden', 'get_propositioner']); |
There was a problem hiding this comment.
Fixed in d39d175. Extended generateSources in metadata.ts to map search_voteringar → 'Riksdagen Voting Records' and search_anforanden → 'Riksdagen Speeches', so both tools now produce meaningful source labels.
Signed-off-by: James Pether Sörling <pethers@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…end generateSources Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Done in d39d175 — all four review comments addressed. |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
REQUIRED_TOOLSincommittee-reports.tsto include all 4 toolssearch_voteringar,search_anforanden,get_propositionerMCP calls with graceful degradationgenerateArticleContentand update sourcesvotesandspeechesfields toArticleContentDataintypes.tsgenerateCommitteeContentto include Voting Results, Committee Debate, Bill Linkage sections (all 14 languages)currentRmdynamically from report data or date (not hard-coded)currentRmtofetchPropositionsfor consistencydata-transformers.test.tsverifying optional sections appear/are omitted based on datafetchPropositionsmock signature to(limit?: number, rm?: string | null)rmparameter is correctly passed to all three enrichment calls (fetchVotingRecords,searchSpeeches,fetchPropositions)firstReportRmas string matchingYYYY/YYregex before using (not just cast)propositionsto plain objects before passing to content generator (safePropositions)content-generators.tsgenerateSourcesinmetadata.tsto mapsearch_voteringar→ 'Riksdagen Voting Records' andsearch_anforanden→ 'Riksdagen Speeches'Original prompt
This section details on the original issue you should resolve
<issue_title>Improve Committee Reports: Add voting patterns, debate context, and proposition linkage</issue_title>
<issue_description>## 📋 Issue Type
Enhancement — Implement missing MCP tools for committee reports: voting patterns, debate context, and proposition linkage
🎯 Objective
Implement the three TODO MCP tools in
scripts/news-types/committee-reports.ts:search_voteringar(committee voting patterns),search_anforanden(committee member statements and debate), andget_propositioner(linkage to originating government proposals). These tools will transform committee report articles from basic report listings into comprehensive legislative intelligence.📊 Current State
scripts/news-types/committee-reports.ts(425 lines)get_betankanden✅ (only tool)search_voteringar❌,search_anforanden❌,get_propositioner❌get_betankanden) — reduced from original 4 to avoid validation failures🚀 Desired State
🔧 Implementation Approach
1. Add
search_voteringarintegration (~80 lines)2. Add
search_anforandenintegration (~70 lines)3. Add
get_propositionerintegration (~50 lines)4. Update REQUIRED_TOOLS
Restore full 4-tool list:
get_betankanden,search_voteringar,search_anforanden,get_propositioner5. Enhanced article templates
🤖 Recommended Agent
code-quality-engineer— TypeScript implementation matching existing MCP integration patterns✅ Acceptance Criteria
search_voteringarintegrated for committee voting pattern analysissearch_anforandenintegrated for committee debate contextget_propositionerintegrated for government bill linkage📚 References
scripts/news-types/committee-reports.ts:78-81(TODO comments)scripts/news-types/committee-reports.ts:187-196(REQUIRED_TOOLS update comment)search_voteringar,search_anforanden,get_propositionerscripts/data-transformers/content-generators.ts💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.