feat: dedicated interpellations content generator with ministerial accountability analysis#1172
Conversation
…lity analysis 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 introduces a dedicated interpellations content generator so interpellation articles are rendered with accountability/debate framing instead of being routed through the motions generator.
Changes:
- Added
generateInterpellationsContent()and wiredinterpellationsrouting to use it instead ofgenerateMotionsContent(). - Extended
ContentLabelSetand populated new interpellations-specific labels across all 14 languages. - Updated barrel exports so the new generator is available throughout the data-transformers API.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/types/content.ts | Adds interpellations-specific label keys to the shared label interface. |
| scripts/data-transformers/index.ts | Routes interpellations article type to the dedicated generator and re-exports it. |
| scripts/data-transformers/content-generators/interpellations.ts | New interpellations generator with thematic grouping and accountability section. |
| scripts/data-transformers/content-generators/index.ts | Exports the new generator from the content-generators barrel. |
| scripts/data-transformers/content-generators.ts | Re-exports the new generator from the legacy barrel entrypoint. |
| scripts/data-transformers/constants/content-labels-part1.ts | Adds interpellations label strings/functions for en/sv/da/no/fi/de/fr. |
| scripts/data-transformers/constants/content-labels-part2.ts | Adds interpellations label strings/functions for es/nl/ar/he/ja/ko/zh. |
| const breakdownFn = L(lang, 'interpellationsBreakdown') as string | ((n: number) => string); | ||
| const breakdownText = typeof breakdownFn === 'function' | ||
| ? breakdownFn(interpellations.length) | ||
| : `${interpellations.length} new interpellations filed.`; | ||
| content += `<p class="article-lede">${escapeHtml(String(breakdownText))}</p>\n`; |
| // Fall back to looking for "till X statsråd" / "till statsminister" pattern in the title | ||
| const titleText = doc.titel || doc.title || ''; | ||
| const toMatch = titleText.match(/(?:till|to|an|à)\s+([^–—\-]{3,60}?)(?:\s*[-–—]|\s*$)/i); | ||
| if (toMatch?.[1]) { | ||
| return toMatch[1].trim(); | ||
| } |
|
|
||
| const docName = escapeHtml(doc.dokumentnamn || doc.dok_id || titleText); | ||
| const authorText = escapeHtml(doc.intressent_namn || doc.author || ''); | ||
| const partyText = escapeHtml(normalizePartyKey(doc.parti).toUpperCase()); |
| const strategyFn = L(lang, 'oppositionStrategyContext') as string | ((n: number) => string); | ||
| const strategyContext = typeof strategyFn === 'function' | ||
| ? strategyFn(partyCount) | ||
| : `Interpellations from ${partyCount} different parties demonstrate broad parliamentary scrutiny.`; | ||
| content += ` <p>${escapeHtml(String(strategyContext))}</p>\n`; |
| const policyImpFn = L(lang, 'policyImplicationsContext') as string | ((p: number, d: number) => string); | ||
| const policyImpText = typeof policyImpFn === 'function' | ||
| ? policyImpFn(interpellations.length, themeCount) | ||
| : `These ${interpellations.length} interpellations span ${themeCount} policy domain${themeCount !== 1 ? 's' : ''}, reflecting the breadth of parliamentary accountability demands.`; | ||
| content += ` <p>${escapeHtml(String(policyImpText))}</p>\n`; |
| content += `\n <h2>${L(lang, 'coalitionDynamics')}</h2>\n`; | ||
| content += ` <div class="context-box">\n <ul>\n`; | ||
| Object.entries(byParty).forEach(([party, partyIps]) => { | ||
| if (party !== 'other') { | ||
| const detailFn = L(lang, 'partyMotionsFiled') as string | ((party: string, n: number) => string); | ||
| const detail = typeof detailFn === 'function' | ||
| ? detailFn(party, partyIps.length) | ||
| : `${party}: ${partyIps.length} interpellations filed`; | ||
| content += ` <li>${escapeHtml(String(detail))}</li>\n`; |
| interpellationDefault: 'Parliamentary interpellation addressed to a minister.', | ||
| interpellationsBreakdown: (n: number): string => `Opposition MPs have filed ${n} interpellations demanding ministerial accountability. Interpellations are formal parliamentary questions that ministers must answer in chamber debate, making them a key tool of parliamentary scrutiny.`, | ||
| debateDynamics: 'Debate Dynamics & Accountability', | ||
| ministerAccountability: 'Minister Response', |
|
@copilot apply changes based on the comments in this thread |
- Tighten extractMinisterTarget() regex to require statsråd/minister terms - Skip party display when parti is missing/unknown (no "OTHER" shown) - Add interpellationsScrutinyContext label (replaces motion-specific oppositionStrategyContext) - Add interpellationsAccountabilityContext label (replaces proposition-specific policyImplicationsContext) - Add partyInterpellationsFiled label (replaces motion-specific partyMotionsFiled) - Change ministerAccountability label to "Directed to" across all 14 languages - Add comprehensive test suite (170 tests) Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
All 7 review comments addressed in
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Adds a dedicated content-generation path for the interpellations article type so interpellation articles render with interpellation-specific headings, labels, and accountability-focused analysis instead of reusing the motions generator.
Changes:
- Introduces
generateInterpellationsContent()with minister-target extraction, thematic grouping, accountability section, and deep analysis integration. - Routes
interpellationsarticle type to the new generator and exports it through existing barrels. - Extends
ContentLabelSetand label constants across all 14 languages, and adds Vitest coverage for labels/output expectations.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/interpellations.test.ts | Adds unit tests for interpellations labels and generator output constraints (no motions/propositions phrasing, minister extraction, party rendering). |
| scripts/types/content.ts | Adds required interpellations-specific label keys to ContentLabelSet. |
| scripts/data-transformers/index.ts | Routes interpellations to the dedicated generator and re-exports it. |
| scripts/data-transformers/content-generators/interpellations.ts | Implements the new interpellations content generator (entries, grouping, accountability + deep analysis). |
| scripts/data-transformers/content-generators/index.ts | Exposes generateInterpellationsContent from the content-generators barrel. |
| scripts/data-transformers/content-generators.ts | Re-exports generateInterpellationsContent through the higher-level barrel. |
| scripts/data-transformers/constants/content-labels-part1.ts | Adds interpellations label strings/functions for en/sv/da/no/fi/de/fr. |
| scripts/data-transformers/constants/content-labels-part2.ts | Adds interpellations label strings/functions for es/nl/ar/he/ja/ko/zh. |
| const docName = escapeHtml(doc.dokumentnamn || doc.dok_id || titleText); | ||
| const authorText = escapeHtml(doc.intressent_namn || doc.author || ''); | ||
| // Only show party when the raw value is present and not an "Unknown" sentinel | ||
| const rawParti = typeof doc.parti === 'string' ? doc.parti.trim() : ''; | ||
| const hasParty = rawParti !== '' && rawParti.toLowerCase() !== 'unknown'; | ||
| const partyText = hasParty ? escapeHtml(rawParti.toUpperCase()) : ''; |
| // Fall back to looking for "till [name] statsråd|statsminister|minister" pattern in the title | ||
| // Only match when followed by a minister-related term to avoid false positives (e.g. "till Gaza") | ||
| const titleText = doc.titel || doc.title || ''; | ||
| const ministerMatch = titleText.match( | ||
| /(?:till|to)\s+(.{3,60}?)\s*(?:statsråd|statsminister|minister)/i | ||
| ); | ||
| if (ministerMatch?.[1]) { | ||
| return ministerMatch[1].trim(); | ||
| } |
| */ | ||
| function extractMinisterTarget(doc: RawDocument): string { | ||
| // Some MCP responses include a mottagare (recipient) field | ||
| const mottagare = (doc as Record<string, unknown>)['mottagare']; |
|
@copilot apply changes based on the comments in this thread and TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request) |
…, fix docstring, restore CI patterns - Filter out "Unknown" sentinel values from author names (intressent_namn/author) so internal placeholders never leak to readers; added 3 tests. - Add `mottagare` as a typed optional field on RawDocument instead of casting through Record<string, unknown>. - Fix extractMinisterTarget() docstring to accurately describe checked fields (mottagare and title, not summary). - Restore missing cross-referencing examples and date calculation helpers in news-evening-analysis.md to fix 4 pre-existing CI test failures (Example 1/2/3 patterns, .filter(), millisecond constants). Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…Fallback inflection (Riksdagenin → Riksdagin) Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Fixed in
All 96 interpellations tests pass, TypeScript clean, CodeQL clean. |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Adds and validates a dedicated interpellations content path in the data-transformers pipeline, including interpellation-specific policy-significance fallback text and type cleanup, backed by expanded tests.
Changes:
- Added new interpellations-focused unit tests and expanded existing transformer tests for
ipanalysis behavior. - Updated policy significance generation with an
ip-specific fallback message (localized) and updated JSDoc for deep analysis implied doc types. - Cleaned up
RawDocumenttyping formottagareand re-exportedgenerateInterpellationsContentfrom the data-transformers barrel.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/interpellations.test.ts | Adds interpellations-specific generator/label/output assertions. |
| tests/data-transformers.test.ts | Adds ip-specific policy-analysis assertions to the main transformer test suite. |
| scripts/data-transformers/types.ts | Moves/defines mottagare on RawDocument and removes duplicate field. |
| scripts/data-transformers/policy-analysis.ts | Adds interpellation (ip) fallback significance text and updates deep-analysis JSDoc. |
| scripts/data-transformers/index.ts | Re-exports generateInterpellationsContent from the data-transformers barrel. |
| // Should contain multi-party strategy context heading | ||
| expect(result).toContain('Opposition Strategy'); | ||
| // Should contain interpellation-specific strategy context (not motions phrasing) | ||
| expect(result).toContain('parliamentary oversight'); |
| // Interpellation-specific fallback: references minister obligation, not committee review | ||
| const doktyp2 = doc.doktyp || doc.documentType || impliedDoktyp || ''; | ||
| if (doktyp2 === 'ip') { | ||
| const ipFallback: Record<string, string> = { | ||
| sv: 'Interpellationen debatteras i kammaren där ministern är skyldig att svara och förklara sina beslut.', | ||
| da: 'Interpellationen debatteres i Riksdagens kammare, hvor ministeren er forpligtet til at svare og stå til ansvar.', | ||
| no: 'Interpellasjonen debatteres i Riksdagens kammare, der ministeren er forpliktet til å svare og stå til ansvar.', | ||
| fi: 'Välikysymys käsitellään Riksdagin täysistunnossa, jossa ministerin on vastattava ja oltava tilivelvollinen.', | ||
| de: 'Die Interpellation wird in der Kammer debattiert, wobei der Minister verpflichtet ist, Rede und Antwort zu stehen.', | ||
| fr: "L'interpellation est débattue en séance plénière, où le ministre est tenu de répondre et de rendre des comptes.", | ||
| es: 'La interpelación se debate en el pleno, donde el ministro está obligado a responder y rendir cuentas.', | ||
| nl: 'De interpellatie wordt besproken in de Kamer, waar de minister verplicht is te antwoorden en verantwoording af te leggen.', | ||
| ar: 'تتم مناقشة الاستجواب في الجلسة العامة حيث يتعين على الوزير الرد وتحمل المسؤولية.', | ||
| he: 'האינטרפלציה נדונה במליאה, שם השר מחויב להשיב ולתת דין וחשבון.', | ||
| ja: '質問主意書は本会議で審議され、大臣は答弁と説明責任を果たす義務があります。', | ||
| ko: '대정부질문은 본회의에서 논의되며, 장관은 답변하고 책임을 져야 할 의무가 있습니다.', | ||
| zh: '质询在全体会议上进行辩论,部长有义务回应并承担责任。', | ||
| }; |
| // Interpellation-specific fallback: references minister obligation, not committee review | ||
| const doktyp2 = doc.doktyp || doc.documentType || impliedDoktyp || ''; | ||
| if (doktyp2 === 'ip') { | ||
| const ipFallback: Record<string, string> = { | ||
| sv: 'Interpellationen debatteras i kammaren där ministern är skyldig att svara och förklara sina beslut.', |
| /** | ||
| * Unit Tests for Interpellations Content Generator | ||
| * Validates: | ||
| * - Output does not contain motion/proposition-specific phrasing | ||
| * - Interpellations label keys exist for all 14 languages | ||
| * - Party rendering and coalition dynamics work correctly | ||
| * - Empty state renders appropriately | ||
| */ | ||
|
|
||
| import { describe, it, expect, afterEach, vi } from 'vitest'; | ||
| import { | ||
| generateArticleContent, | ||
| CONTENT_LABELS, | ||
| } from '../scripts/data-transformers.js'; | ||
| import type { Language } from '../scripts/types/language.js'; | ||
|
|
|
@copilot apply changes based on the comments in this thread |
…e tests 1. Hoist ipFallback → module-level IP_FALLBACK const (Readonly<Record>) to avoid per-call allocation in generatePolicySignificance(). 2. Update JSDoc to document 'ip' in impliedDoktyp param. 3. Replace hard-coded UI text in tests with CONTENT_LABELS references. 4. Delete redundant interpellations.test.ts; merge unique tests into interpellations-content-generator.test.ts (64 tests, no overlap). Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
All 4 review comments addressed in
All 3218 tests pass, TypeScript clean, CodeQL clean. |
🔍 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 finalizes first-class interpellations support in the data-transformers pipeline by adding dedicated generator plumbing and interpellation-specific policy significance fallbacks, backed by expanded regression tests.
Changes:
- Added/expanded tests to verify interpellation-specific rendering (CSS markers, lede) and interpellation policy-analysis fallbacks across languages.
- Cleaned up
RawDocumenttyping by removing the duplicatemottagarefield and keeping the canonical definition. - Added an interpellation-specific (
ip) fallback path ingeneratePolicySignificance()(with a hoisted localized fallback map) and re-exported the interpellations generator via thedata-transformersbarrel.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
tests/interpellations-content-generator.test.ts |
Adds regression tests for interpellation CSS markers, lede presence, and label/fallback smoke tests. |
tests/data-transformers.test.ts |
Adds integration-style assertions ensuring interpellations uses ip-specific analysis (incl. localized fallback expectations). |
scripts/data-transformers/types.ts |
Removes duplicated mottagare field and documents the remaining canonical property. |
scripts/data-transformers/policy-analysis.ts |
Introduces module-level IP_FALLBACK and adds 'ip' handling in generatePolicySignificance()/JSDoc. |
scripts/data-transformers/index.ts |
Re-exports generateInterpellationsContent from the data-transformers public API. |
| it('should include article-lede class with document count', () => { | ||
| const content = generateArticleContent({ interpellations: sampleInterpellations }, 'interpellations', 'en') as string; | ||
| expect(content).toContain('article-lede'); | ||
| expect(content).toContain('3'); |
|
@copilot apply changes based on the comments in this thread |
…ations' match Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Fixed in |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
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 |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
data-transformers/index.tsinterpellations.ts)policy-analysis.tsmottagareremoved)ipfallback localized for all 14 languagesipFallbackto module-levelIP_FALLBACKconst (avoid per-call allocation)generatePolicySignificance()to include'ip'CONTENT_LABELSreferencestoContain('3')assertion with specific'3 interpellations'matchOriginal prompt
This section details on the original issue you should resolve
<issue_title>Enhance Content Generators with AI-Powered Political Analysis Pipeline</issue_title>
<issue_description>## 📋 Issue Type
Feature / Enhancement
🎯 Objective
Transform all content generator modules (
scripts/data-transformers/content-generators/*.tsandscripts/news-types/*.ts) from template-based text producers into AI-powered political analysis generators that analyze document content, identify policy implications, assess political dynamics, and produce nuanced political journalism. Currently, generators likepropositions.ts,motions.ts,committee.tsproduce content by formatting document metadata into HTML — the "analysis" is template text, not genuine AI analysis of what the documents say.📊 Current State
scripts/data-transformers/content-generators/:propositions.ts,motions.ts,committee.ts— article type generators using template formattinggeneric.ts— fallback generator with basic document listingshared.ts— shared utilities (document type labels, etc.)swot-section.ts,stakeholder-swot-section.ts,dashboard-section.ts,mindmap-section.ts,sankey-section.ts,cia-overview-section.ts,economic-dashboard-section.ts— visualization generatorsnewsworthiness.ts— scoring functionmonth-ahead.ts,monthly-review.ts,week-ahead.ts— calendar-based generatorsscripts/news-types/: weekly-review (5 files), monthly-review, month-ahead, breaking-news, motions, propositions, committee-reports, week-aheadgenerateArticleContent()format document titles, dates, and committee names into HTML paragraphs — no analysis of document substancepolicy-analysis.tsmodule hasdetectPolicyDomains()(keyword-based) andassessConfidenceLevel()— basic pattern matching, not AI analysisgenerateDeepAnalysisSection()generates section headers and document lists, not analytical prose🚀 Desired State
enrichDocumentsWithContent()) to AI for substantive analysis🔧 Implementation Approach
Step 1: Create AI Content Analysis Module
Step 2: Upgrade Each Content Generator
For each generator in
content-generators/:Step 3: Create Dedicated Interpellations Generator
scripts/data-transformers/content-generators/interpellations.tsscripts/data-transformers/index.tsline 130Step 4: Upgrade Newsworthiness Scoring
scoreNewsworthiness()with AI-assessed scoring📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.