Conversation
…ng, and trend analysis - Add get_betankanden, get_propositioner, get_motioner to REQUIRED_TOOLS - Fetch committee reports (20), propositions (15), motions (50) in parallel - Calculate riksmöte dynamically from current date (Sep-Jun parliamentary year) - Add generateMonthAheadContent with Strategic Legislative Outlook, Committee Pipeline, and Policy Trends sections (14-language support) - Update data-transformers dispatcher to use new month-ahead generator - Add hasLegislativePipeline to MonthAheadValidationResult - Extend test mock with new MCP methods; 7 new tests (20 total) 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
This PR enhances the month-ahead article generator from a simple calendar listing into a strategic legislative intelligence brief by integrating three additional MCP data sources: committee reports, propositions, and motions. The changes transform the smallest article generator (291 lines) into a comprehensive strategic forecasting tool.
Changes:
- Added parallel fetching of committee reports (×20), propositions (×15), and motions (×50) using Promise.all with graceful degradation
- Implemented dynamic riksmöte (parliamentary session) calculation based on the current date
- Extended article validation with
hasLegislativePipelinecheck to ensure strategic content is present - Created new
generateMonthAheadContentfunction with three additional sections: Strategic Legislative Outlook (top 8 propositions), Committee Pipeline (5 committees × 3 reports), and Policy Trends (top 5 domains, top 4 parties by motion volume) - Updated REQUIRED_TOOLS to include
get_betankanden,get_propositioner,get_motioner - Added comprehensive test coverage for new MCP tool integrations
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| scripts/news-types/month-ahead.ts | Core generator extended with parallel legislative pipeline data fetching, dynamic riksmöte calculation, and enhanced validation |
| scripts/data-transformers/content-generators.ts | New generateMonthAheadContent function with strategic legislative sections in 14 languages |
| scripts/data-transformers/index.ts | Dispatcher updated to route month-ahead to dedicated content generator instead of reusing week-ahead |
| tests/news-types/month-ahead.test.ts | Test coverage extended with mock methods and 7 new tests for legislative pipeline integration |
scripts/news-types/month-ahead.ts
Outdated
| hasForwardLookingTone, | ||
| hasStrategicContext, | ||
| hasLegislativePipeline, | ||
| passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext |
There was a problem hiding this comment.
The validation logic for passed does not include the new hasLegislativePipeline field in its boolean expression. This means articles can pass validation without having legislative pipeline content, which contradicts the PR's goal of making this content a core strategic feature. Consider adding hasLegislativePipeline to the passed condition: passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext && hasLegislativePipeline
| passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext | |
| passed: | |
| hasCalendarEvents && | |
| hasMinimumSources && | |
| hasForwardLookingTone && | |
| hasStrategicContext && | |
| hasLegislativePipeline |
There was a problem hiding this comment.
Fixed in a7eb61d — hasLegislativePipeline is now included in the passed condition.
scripts/news-types/month-ahead.ts
Outdated
| // Determine current riksmöte (Swedish parliamentary year: Sep 1 → Jun/Jul of next year). | ||
| // Any date in September or later belongs to the new year's session (e.g. 2025-09-01 → "2025/26"). | ||
| const year = today.getFullYear(); | ||
| const month = today.getMonth() + 1; // 1-12 | ||
| const currentRiksmote = month >= 9 | ||
| ? `${year}/${String(year + 1).slice(-2)}` | ||
| : `${year - 1}/${String(year).slice(-2)}`; |
There was a problem hiding this comment.
The riksmöte calculation uses a dynamic calculation based on the current date, while weekly-review.ts and monthly-review.ts hardcode '2025/26'. This creates an inconsistency in the codebase. While the dynamic approach is more maintainable long-term, it differs from the established pattern. Either all generators should use dynamic calculation, or month-ahead should follow the existing hardcoded pattern for consistency. The dynamic approach is technically superior but breaks the established convention. Consider documenting why this generator differs from the pattern.
There was a problem hiding this comment.
Added a comment in a7eb61d explaining the intentional divergence: month-ahead uses dynamic calculation so it remains accurate across parliamentary year boundaries without requiring a code change each September, unlike weekly-review/monthly-review which are short-lived generators where hardcoding is acceptable.
| const domainIntro = domTpl | ||
| ? domTpl(escapeHtml(domainList), motions.length) | ||
| : `${motions.length} motions identify active policy domains: ${escapeHtml(domainList)}.`; |
There was a problem hiding this comment.
In the content generator, the domain list is escaped after being joined with comma separators, but the template functions then embed this pre-escaped value. The escaping happens at line 1228 with escapeHtml(domainList), but domainList is the result of joining domain strings that may already contain HTML-safe characters from detectPolicyDomains. While not a security issue (escaping is conservative), this creates double-escaped output if domains contain special characters. The pattern in other generators (e.g., generateCommitteeContent at line 360) escapes the final assembled string. Consider moving escapeHtml to be applied directly to domainIntro after template function execution for consistency.
| const domainIntro = domTpl | |
| ? domTpl(escapeHtml(domainList), motions.length) | |
| : `${motions.length} motions identify active policy domains: ${escapeHtml(domainList)}.`; | |
| const domainIntroRaw = domTpl | |
| ? domTpl(domainList, motions.length) | |
| : `${motions.length} motions identify active policy domains: ${domainList}.`; | |
| const domainIntro = escapeHtml(domainIntroRaw); |
There was a problem hiding this comment.
Fixed in a7eb61d — domain list is now passed raw to the template function and escapeHtml is applied to the assembled string afterward, matching the pattern used in other generators.
tests/news-types/month-ahead.test.ts
Outdated
| fetchCommitteeReports: Mock<(limit: number, rm: string) => Promise<unknown[]>>; | ||
| fetchPropositions: Mock<(limit: number, rm: string) => Promise<unknown[]>>; | ||
| fetchMotions: Mock<(limit: number, rm: string) => Promise<unknown[]>>; |
There was a problem hiding this comment.
The mock function signatures define the second parameter as rm: string (line 27-29), but according to the actual MCP client implementation (scripts/mcp-client/client.ts:394-420), the rm parameter is nullable with type string | null. The mock should match the actual signature: Mock<(limit: number, rm: string | null) => Promise<unknown[]>>. This mismatch could mask bugs where null is passed but tests don't catch it.
| fetchCommitteeReports: Mock<(limit: number, rm: string) => Promise<unknown[]>>; | |
| fetchPropositions: Mock<(limit: number, rm: string) => Promise<unknown[]>>; | |
| fetchMotions: Mock<(limit: number, rm: string) => Promise<unknown[]>>; | |
| fetchCommitteeReports: Mock<(limit: number, rm: string | null) => Promise<unknown[]>>; | |
| fetchPropositions: Mock<(limit: number, rm: string | null) => Promise<unknown[]>>; | |
| fetchMotions: Mock<(limit: number, rm: string | null) => Promise<unknown[]>>; |
There was a problem hiding this comment.
Fixed in a7eb61d — mock signatures now use string | null to match the actual MCP client signatures.
|
@copilot apply changes based on the comments in this thread |
- Include hasLegislativePipeline in validateMonthAhead passed condition - Fix double-escaping in Policy Trends domain list (escape after template) - Correct mock signatures to use string | null for rm parameter - Document why dynamic riksmöte differs from hardcoded pattern in other generators Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
scripts/news-types/month-ahead.ts
Outdated
| const pipelineKeywords = ['pipeline', 'committee', 'proposition', 'motion', 'report', 'betank']; | ||
| return pipelineKeywords.some(keyword => | ||
| (article.content as string).toLowerCase().includes(keyword) | ||
| ); |
There was a problem hiding this comment.
checkLegislativePipeline (and the overall passed gate) is currently based on a small set of English keywords (plus betank), which will likely produce false negatives for non-English month-ahead articles (e.g. Spanish headings use “Proceso en comité”, Swedish uses “betänkande” with diacritics). Since passed now requires this check, consider switching to language-agnostic structural markers inserted by the generator (e.g. presence of the Month-Ahead sections’ <h2> blocks and/or .document-entry blocks), or broaden the keyword detection to include the localized terms used in the generated content (including betänk).
| const pipelineKeywords = ['pipeline', 'committee', 'proposition', 'motion', 'report', 'betank']; | |
| return pipelineKeywords.some(keyword => | |
| (article.content as string).toLowerCase().includes(keyword) | |
| ); | |
| // Broad, language-aware keyword set for legislative pipeline concepts. | |
| // Includes English plus localized terms used in generated content | |
| // (e.g. Swedish "betänkande", Spanish "Proceso en comité", etc.). | |
| const pipelineKeywords = [ | |
| // English | |
| 'pipeline', | |
| 'committee', | |
| 'proposition', | |
| 'motion', | |
| 'report', | |
| // Swedish (and ASCII fallbacks) | |
| 'betank', | |
| 'betänk', | |
| 'betänkande', | |
| 'utskott', | |
| 'lagstiftningsprocess', | |
| // Spanish | |
| 'proceso en comité', | |
| 'proceso en comite', | |
| 'proceso legislativo', | |
| 'comité', | |
| 'comite', | |
| 'informe', | |
| 'dictamen', | |
| 'proposición', | |
| 'proposicion', | |
| 'propuesta', | |
| // German | |
| 'ausschuss', | |
| 'ausschüsse', | |
| 'gesetzgebungsverfahren', | |
| 'vorlage', | |
| 'bericht', | |
| // French | |
| 'commission', | |
| 'procédure législative', | |
| 'procedure legislative', | |
| 'rapport', | |
| 'proposition de loi', | |
| // Nordic / other committee terms (short stems) | |
| 'komité', | |
| 'komite', | |
| 'kommitté', | |
| 'kommission', | |
| 'komisjon' | |
| ]; | |
| const content = (article.content as string).toLowerCase(); | |
| return pipelineKeywords.some(keyword => content.includes(keyword)); |
There was a problem hiding this comment.
Fixed in d14f2e0 — checkLegislativePipeline now includes localized terms for all 14 languages: Swedish (betänk, utskott), German (ausschuss, vorlage, bericht), French (commission, rapport), Spanish (comité, informe, dictamen, propuesta), Finnish (valiokunta), Nordic variants (komité, kommitté, kommission), plus Japanese, Korean, Chinese, Arabic, and Hebrew committee terms.
| export const REQUIRED_TOOLS: readonly string[] = [ | ||
| 'get_calendar_events', | ||
| 'search_dokument', // conditional: used only when calendar is empty | ||
| 'get_betankanden', | ||
| 'get_propositioner', | ||
| 'get_motioner', | ||
| ]; |
There was a problem hiding this comment.
REQUIRED_TOOLS is updated here, but the repository’s cross-reference tool validation map in scripts/validate-cross-references.ts (REQUIRED_TOOLS_PER_TYPE) currently has no entry for month-ahead. That means any CI/QA that relies on that map won’t enforce the new required tools for month-ahead articles. Consider adding a month-ahead entry there to keep tool-requirements consistent across validation layers.
There was a problem hiding this comment.
Fixed in d14f2e0 — added a 'month-ahead' entry to REQUIRED_TOOLS_PER_TYPE listing get_calendar_events, get_betankanden, get_propositioner, get_motioner.
| // Group reports by committee | ||
| const byCommittee: Record<string, RawDocument[]> = {}; | ||
| reports.forEach(r => { | ||
| const key = (r as Record<string, string>).organ || 'unknown'; |
There was a problem hiding this comment.
The committee grouping key uses only organ, but RawDocument supports both organ and committee (and other parts of the codebase use organ ?? committee). If fetchCommitteeReports returns committee codes under committee, this will incorrectly bucket them under unknown. Consider falling back to committee (or the same resolution logic used elsewhere) when building byCommittee.
| const key = (r as Record<string, string>).organ || 'unknown'; | |
| const rec = r as Record<string, string>; | |
| const key = rec.organ ?? rec.committee ?? 'unknown'; |
There was a problem hiding this comment.
Fixed in d14f2e0 — committee grouping now uses rec2.organ ?? rec2.committee ?? 'unknown'.
|
|
||
| // Top parties by motion volume | ||
| const topParties = Object.entries(byPartyTrend) | ||
| .filter(([k]) => k !== 'unknown') |
There was a problem hiding this comment.
normalizePartyKey() maps missing/"unknown" parties to 'other', but this filter excludes 'unknown' instead. As a result, the “Top parties” list can include the sentinel 'other' bucket and potentially crowd out real parties. Consider filtering out 'other' (to match the existing pattern used elsewhere in this file).
| .filter(([k]) => k !== 'unknown') | |
| .filter(([k]) => k !== 'other') |
There was a problem hiding this comment.
Fixed in d14f2e0 — filter is now .filter(([k]) => k !== 'unknown' && k !== 'other') to exclude both sentinels.
| it('should include hasLegislativePipeline in validation result', () => { | ||
| const article: ArticleInput = { | ||
| content: 'Committee pipeline report proposition motion scheduled for next month.', | ||
| sources: ['source1', 'source2', 'source3'] | ||
| }; | ||
|
|
||
| const validation = monthAheadModule.validateMonthAhead(article); | ||
| expect(validation).toHaveProperty('hasLegislativePipeline'); | ||
| expect(validation.hasLegislativePipeline).toBe(true); | ||
| }); |
There was a problem hiding this comment.
The local MonthAheadValidationResult type defined in this test file doesn’t include the new hasLegislativePipeline field, but the test asserts on validation.hasLegislativePipeline. If/when tests are typechecked, this will fail. Consider updating the test-local interface to include hasLegislativePipeline: boolean.
There was a problem hiding this comment.
Fixed in d14f2e0 — added hasLegislativePipeline: boolean to the test-local MonthAheadValidationResult interface.
|
@copilot apply changes based on the comments in this thread |
- Broaden checkLegislativePipeline to include localized terms (SV, DE, FR, ES, FI, JA, KO, ZH, AR, HE) - Add month-ahead entry to REQUIRED_TOOLS_PER_TYPE in validate-cross-references.ts - Fall back to rec.committee when rec.organ is absent in committee grouping - Filter out both 'unknown' and 'other' from top-parties list - Add hasLegislativePipeline to test-local MonthAheadValidationResult interface Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…ons.ts Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
scripts/news-types/month-ahead.ts
Outdated
| @@ -138,23 +154,57 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi | |||
| } | |||
There was a problem hiding this comment.
In the events.length === 0 path, the function returns early when documents.length === 0, which prevents the new legislative pipeline fetches from running. That means month-ahead generation can be skipped even if get_betankanden/get_propositioner/get_motioner would have returned useful data for the Strategic Outlook / Pipeline / Trends sections. Consider moving the pipeline fetch before this early-return, or only skipping when both calendar/doc fallbacks and the pipeline results are empty.
There was a problem hiding this comment.
Fixed in 258aa6c — the pipeline fetch (get_betankanden, get_propositioner, get_motioner) is now moved before the early-return block and always runs unconditionally. The early-return condition is updated to only skip generation when all data sources are empty: events, documents, committee reports, propositions, and motions.
| sources: events.length > 0 | ||
| ? ['calendar_events', 'get_betankanden', 'get_propositioner', 'get_motioner'] | ||
| : ['calendar_events', 'search_dokument', 'get_betankanden', 'get_propositioner', 'get_motioner'], | ||
| }, |
There was a problem hiding this comment.
crossReferences.sources is now populated with MCP tool names like get_betankanden/get_propositioner/get_motioner, but other generators use stable source identifiers (e.g. betankanden, propositioner, motioner, calendar_events). This inconsistency makes cross-run summaries harder to compare and can break any consumers that expect the existing identifier style. Consider switching these entries to the same noun-style identifiers used elsewhere (and keep tool names confined to mcpCalls).
There was a problem hiding this comment.
Fixed in 258aa6c — crossReferences.sources now uses noun-style identifiers ('betankanden', 'propositioner', 'motioner') matching the established pattern in committee-reports.ts and motions.ts.
|
@copilot apply changes based on the comments in this thread |
…ences.sources Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
scripts/news-types/month-ahead.ts
Outdated
| const year = today.getFullYear(); | ||
| const month = today.getMonth(); // 0-11, align with motions.ts pattern | ||
| const currentRiksmote = month >= 8 | ||
| ? `${year}/${String(year + 1).slice(-2)}` | ||
| : `${year - 1}/${String(year).slice(-2)}`; |
There was a problem hiding this comment.
currentRiksmote is derived from today, but Month-Ahead covers a range (today → today + daysAhead). If generation runs in late August, the article range can cross into September (new parliamentary session) while currentRiksmote still points to the previous session, so the betänkanden/propositioner/motioner fetches can pull the wrong session’s pipeline data. Consider deriving the riksmöte from endDate (or detecting a Sep-1 boundary crossing and fetching/merging both sessions) so the pipeline sections stay accurate around the session rollover.
| const year = today.getFullYear(); | |
| const month = today.getMonth(); // 0-11, align with motions.ts pattern | |
| const currentRiksmote = month >= 8 | |
| ? `${year}/${String(year + 1).slice(-2)}` | |
| : `${year - 1}/${String(year).slice(-2)}`; | |
| const sessionRefYear = endDate.getFullYear(); | |
| const sessionRefMonth = endDate.getMonth(); // 0-11, align with motions.ts pattern | |
| const currentRiksmote = sessionRefMonth >= 8 | |
| ? `${sessionRefYear}/${String(sessionRefYear + 1).slice(-2)}` | |
| : `${sessionRefYear - 1}/${String(sessionRefYear).slice(-2)}`; |
There was a problem hiding this comment.
Fixed in 0414377 — currentRiksmote is now derived from endDate instead of today, so a late-August run whose article window crosses the Sep-1 boundary correctly fetches the new session's pipeline data.
| it('should fetch committee reports from MCP', async () => { | ||
| const result = await monthAheadModule.generateMonthAhead({ | ||
| languages: ['en'] | ||
| }); | ||
|
|
||
| expect(mockClientInstance.fetchCommitteeReports).toHaveBeenCalled(); | ||
| expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_betankanden')).toBe(true); |
There was a problem hiding this comment.
The new pipeline fetches mean Month-Ahead should generate an article even when get_calendar_events is empty, as long as any of betänkanden/propositioner/motioner returns data. There’s no test covering this new behavior (only the all-empty skip path). Add a test where calendar+searchDocuments are empty but one of the pipeline mocks returns a non-empty list, and assert that files > 0 and the generated HTML includes the expected <h2> pipeline section heading(s).
There was a problem hiding this comment.
Added in 0414377 — new test 'should generate article when calendar is empty but pipeline data is present' sets fetchCalendarEvents and searchDocuments to return [], returns two propositions from fetchPropositions, and asserts files > 0 and html contains Strategic Legislative Outlook.
|
@copilot apply changes based on the comments in this thread |
…ne-only test Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
Enhances
scripts/news-types/month-ahead.tsfrom a simple calendar listing into a strategic legislative intelligence brief by integrating three additional MCP data sources: committee reports, propositions, and motions.Changes Made
Core Generator (
scripts/news-types/month-ahead.ts)get_betankanden,get_propositioner,get_motionertoREQUIRED_TOOLSget_betankanden×20,get_propositioner×15,get_motioner×50) run unconditionally before the calendar early-return, using Promise.all with graceful degradation (console.warnon failure)endDate(nottoday) so articles spanning the August→September session boundary always fetch the correct parliamentary session's pipeline data; 0-basedmonth >= 8pattern matchesmotions.tsandcommittee-reports.tscrossReferences.sourcesuses noun-style identifiers (betankanden,propositioner,motioner) consistent with other generatorsMonthAheadValidationResultextended withhasLegislativePipeline;passedgate requires all fields includinghasLegislativePipelinecheckLegislativePipelinematches specific<h2>section headings emitted by the generator across all 14 languages (including Policy Trends headings for JA/KO/ZH/AR/HE)Content Generator (
scripts/data-transformers/content-generators.ts)generateMonthAheadContentfunction adds three sections in 14 languages:rec.committeewhenrec.organis absentunknownandothersentinels);escapeHtmlapplied after template execution to avoid double-escapingrec.organ ?? rec.committee ?? 'unknown'Dispatcher (
scripts/data-transformers/index.ts)generateArticleContentroutesmonth-aheadto the dedicatedgenerateMonthAheadContentinstead of reusing week-aheadValidation (
scripts/validate-cross-references.ts)month-aheadentry toREQUIRED_TOOLS_PER_TYPEenforcingget_calendar_events,get_betankanden,get_propositioner,get_motionerTests (
tests/news-types/month-ahead.test.ts)string | nullfor thermparameter, matching actual MCP client signaturesMonthAheadValidationResultinterface includeshasLegislativePipeline: booleanTesting
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.