Conversation
…ions to watch, and interpellation spotlight 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 week-ahead articles by implementing deep cross-referencing between calendar events and parliamentary documents/questions. Previously, week-ahead articles displayed calendar events, documents, questions, and interpellations as disconnected lists. Now, high-priority calendar events are enriched with inline "Policy Context" boxes showing up to 3 related documents/questions per event, questions are relabeled as "Questions to Watch," and interpellations gain a "Interpellation Spotlight" section with minister attribution extracted from summaries.
Changes:
- Added 4 new helper functions (
extractKeywords,findRelatedDocuments,findRelatedQuestions,extractMinister) to enable keyword-based cross-referencing between events, documents, and questions - Replaced generic section labels ("Parliamentary Questions to Ministers" → "Questions to Watch", "Interpellations Pending" → "Interpellation Spotlight") with more engaging titles in all 14 languages
- Generated inline "Policy Context" boxes for high-priority events showing related documents and questions with proper HTML escaping and URL sanitization
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| scripts/news-types/week-ahead.ts | Removed TODO comments and updated documentation to reflect that all 5 MCP tools are now actively used with cross-referencing implementation |
| scripts/data-transformers/content-generators.ts | Added cross-referencing logic with 4 helper functions, inline Policy Context rendering, updated section labels for Questions/Interpellations in 14 languages, and minister extraction from interpellation summaries |
| tests/news-types/week-ahead.test.ts | Added 6 new tests in "Enhanced Cross-Referencing" suite covering Policy Context rendering, multilingual section labels (EN/SV), and minister name extraction |
| function findRelatedDocuments(event: RawCalendarEvent, documents: RawDocument[]): RawDocument[] { | ||
| const rec = event as Record<string, string>; | ||
| const eventOrgan = rec['organ'] ?? ''; | ||
| const keywords = extractKeywords(rec['rubrik'] ?? rec['titel'] ?? rec['title'] ?? ''); |
There was a problem hiding this comment.
The function accesses the organ property via type casting to Record<string, string>, but RawCalendarEvent doesn't define an organ field in scripts/data-transformers/types.ts (lines 12-24). This creates a type safety gap.
Consider either:
- Adding
organ?: string;to theRawCalendarEventinterface if the MCP server actually returns this field - Or documenting that this field may not exist in production data, which would cause the organ-matching logic to silently fail
The test mocks provide this field, but if production data doesn't include it, the cross-referencing feature will only work via keyword matching, not organ matching.
|
|
||
| /** Extract meaningful keywords from text for cross-reference matching (min 2 chars, captures EU, KU, etc.) */ | ||
| function extractKeywords(text: string): string[] { | ||
| return text.toLowerCase().split(/\s+/).filter(w => w.length >= 2); |
There was a problem hiding this comment.
The extractKeywords function filters words by minimum length of 2 characters, which correctly captures acronyms like 'EU', 'KU', and 'AI'. However, it splits on whitespace only and doesn't handle common Swedish compound word patterns or punctuation-separated terms.
For example, if an event title is "Budget-diskussion EU-riktlinjer" (Budget discussion EU guidelines), splitting on \s+ will produce "Budget-diskussion" and "EU-riktlinjer" as single keywords, potentially missing matches against documents that use "Budget" or "EU" alone.
Consider splitting on [\s\-,]+ to also break on hyphens and commas, or using a more sophisticated tokenization approach for better cross-referencing accuracy.
| return text.toLowerCase().split(/\s+/).filter(w => w.length >= 2); | |
| return text.toLowerCase().split(/[\s,–-]+/u).filter(w => w.length >= 2); |
| /** Extract targeted minister name from interpellation summary "till MINISTER" header line */ | ||
| function extractMinister(summary: string): string { | ||
| const m = summary.match(/\btill\s+([^\n]+)/i); | ||
| return m ? m[1].trim() : ''; |
There was a problem hiding this comment.
The regex pattern /\btill\s+([^\n]+)/i captures everything after "till " until the first newline. This assumes the minister name doesn't contain newlines, but the captured group could include trailing content if the format is "till MINISTER\nExtra text".
For robustness, consider trimming or more specifically matching common minister title patterns. Swedish minister names typically follow patterns like "Statsminister NAME" or "Minister för AREA NAME". A more specific pattern could avoid capturing unwanted trailing text.
| return m ? m[1].trim() : ''; | |
| if (!m) return ''; | |
| const raw = m[1].trim(); | |
| // Remove common trailing topic clauses (e.g. "om X", "angående Y") and punctuation | |
| const lowerRaw = raw.toLowerCase(); | |
| const stopPhrases = [' om ', ' angående ', ' rörande ', ' beträffande ']; | |
| let end = raw.length; | |
| for (const phrase of stopPhrases) { | |
| const idx = lowerRaw.indexOf(phrase); | |
| if (idx !== -1 && idx < end) { | |
| end = idx; | |
| } | |
| } | |
| // Also cut at common terminating punctuation if it comes earlier | |
| const punctIdx = raw.search(/[?:;.,]/); | |
| if (punctIdx !== -1 && punctIdx < end) { | |
| end = punctIdx; | |
| } | |
| return raw.slice(0, end).trim(); |
tests/news-types/week-ahead.test.ts
Outdated
| const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); | ||
| expect(result.success).toBe(true); | ||
| const article = result.articles[0]!; |
There was a problem hiding this comment.
The tests use result.articles[0]! (non-null assertion) to access the first article. While the test expects result.success to be true, if the article generation silently produces zero articles, this would cause a test crash rather than a clear failure message.
Consider using optional chaining result.articles[0]?.html or explicit length checks with better error messages to make test failures more debuggable when articles aren't generated as expected.
| expect(article.html).toContain('minister-target'); | ||
| expect(article.html).toContain('Statsminister Ulf Kristersson'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The test verifies that minister names are extracted and displayed, but doesn't test edge cases where:
- The summary has no "till" line (extractMinister returns empty string)
- The summary has malformed minister lines
- Multiple "till" patterns appear
Consider adding test cases for these edge cases to ensure the feature degrades gracefully when interpellation summaries don't follow the expected format.
| it('should handle interpellation summary without minister line gracefully', async () => { | |
| mockClientInstance.fetchInterpellations.mockResolvedValue([{ | |
| titel: 'Question about transport policy', | |
| dok_id: 'H901ip4', | |
| parti: 'M', | |
| summary: 'Interpellation 2025/26:4\nav Alex Example\nDetta är en fråga om transportpolitiken utan specifik ministerrad.', | |
| }]); | |
| const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); | |
| expect(result.success).toBe(true); | |
| const article = result.articles[0]!; | |
| // When no minister can be extracted, component should degrade gracefully and not render a minister-target | |
| expect(article.html).not.toContain('minister-target'); | |
| }); | |
| it('should handle malformed minister line without breaking rendering', async () => { | |
| mockClientInstance.fetchInterpellations.mockResolvedValue([{ | |
| titel: 'Question about education policy', | |
| dok_id: 'H901ip5', | |
| parti: 'L', | |
| // "till" appears but no valid minister name follows | |
| summary: 'Interpellation 2025/26:5\nav Chris Example\ntill \nDetta är en fråga om utbildningspolitiken.', | |
| }]); | |
| const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); | |
| expect(result.success).toBe(true); | |
| const article = result.articles[0]!; | |
| // Malformed minister line should not cause a crash or render an empty minister-target | |
| expect(article.html).not.toContain('minister-target'); | |
| }); | |
| it('should handle multiple "till" patterns in summary without failing', async () => { | |
| mockClientInstance.fetchInterpellations.mockResolvedValue([{ | |
| titel: 'Question about climate and finance policy', | |
| dok_id: 'H901ip6', | |
| parti: 'C', | |
| summary: 'Interpellation 2025/26:6\nav Pat Example\ntill Klimatminister Romina Pourmokhtari\noch till Finansminister Elisabeth Svantesson\nDetta är en fråga om klimat- och finanspolitiken.', | |
| }]); | |
| const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); | |
| expect(result.success).toBe(true); | |
| const article = result.articles[0]!; | |
| // The spotlight section should still render; parsing ambiguities must not break generation | |
| expect(article.html).toContain('Interpellation Spotlight'); | |
| }); |
| it('should show Policy Context box when event organ matches document organ', async () => { | ||
| // Provide a high-priority event (contains 'EU' to pass isHighPriority) with organ matching the doc | ||
| mockClientInstance.fetchCalendarEvents.mockResolvedValue([{ | ||
| id: '1', title: 'EU budget vote', date: '2026-02-16', type: 'chamber', organ: 'Kammaren', | ||
| }]); | ||
| mockClientInstance.searchDocuments.mockResolvedValue([{ | ||
| titel: 'Budget Proposition 2026', | ||
| dok_id: 'H901prop1', | ||
| doktyp: 'prop', | ||
| organ: 'Kammaren', | ||
| }]); | ||
|
|
||
| const result = await weekAheadModule.generateWeekAhead({ languages: ['en'] }); | ||
| expect(result.success).toBe(true); | ||
| const article = result.articles[0]!; | ||
| // 'EU budget vote' event has organ 'Kammaren', matching the document's organ | ||
| expect(article.html).toContain('Policy Context'); | ||
| expect(article.html).toContain('policy-context-box'); | ||
| }); |
There was a problem hiding this comment.
The test only verifies that keyword-overlap matching works when an event title contains "budget" and a question title also contains "budget". However, it doesn't test:
- The organ-matching code path (which has a type safety issue)
- Cases where no documents/questions match any events
- Cases where more than 3 matches exist (to verify the slice(0,3) limit works)
- Events that are high-priority but have no related content
Consider adding test cases to exercise all code paths in the cross-referencing logic.
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
… robustness, and edge-case tests Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all changes from the review thread in commit
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
extractKeywordshelper (min-length 2 for EU/KU/AI acronyms, DRY shared by both matchers)findRelatedDocuments,findRelatedQuestions,extractMinisterincontent-generators.tsminister-targetCSS classweek-ahead.ts(lines 50-53), replaced with implementation noteorgan?: stringtoRawCalendarEventinterface (type safety — no more cast toRecord<string, string>)extractKeywordsnow splits on[\s,–-]+(hyphens, commas, en-dashes) so hyphenated terms like "EU-riktlinjer" are tokenized to "eu" + "riktlinjer"extractMinisteruses[^\S\n]+(non-newline whitespace) to prevent crossing into the next line; strips trailing "om/angående/rörande/beträffande" clauses and punctuationresult.articles[0]!replaced with explicit length checks + descriptivethrow new Error()Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.