From d486c16e093e0f94744b8950bc83a13e1c1f52cf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 21:13:35 +0000
Subject: [PATCH 1/8] feat: enhance month-ahead with strategic forecasting,
pipeline tracking, and trend analysis
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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>
---
.../data-transformers/content-generators.ts | 205 ++++++++++++++++++
scripts/data-transformers/index.ts | 3 +-
scripts/news-types/month-ahead.ts | 68 +++++-
tests/news-types/month-ahead.test.ts | 59 +++++
4 files changed, 329 insertions(+), 6 deletions(-)
diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts
index 690926df4..880eda5ef 100644
--- a/scripts/data-transformers/content-generators.ts
+++ b/scripts/data-transformers/content-generators.ts
@@ -1061,3 +1061,208 @@ export function generateGenericContent(data: ArticleContentData, lang: Language
return content;
}
+
+/**
+ * Generate Month-Ahead article content with strategic legislative forecasting.
+ * Extends week-ahead calendar coverage with committee pipeline tracking,
+ * propositions in pipeline, and motion trend analysis.
+ */
+export function generateMonthAheadContent(data: ArticleContentData, lang: Language | string): string {
+ // Base calendar/event content (handles events + upcoming documents)
+ let content = generateWeekAheadContent(data as WeekAheadData, lang);
+
+ const propositions = data.propositions ?? [];
+ const reports = data.reports ?? [];
+ const motions = data.motions ?? [];
+
+ // ── Strategic Legislative Outlook ────────────────────────────────────────
+ if (propositions.length > 0) {
+ const outlookLabel = lang === 'sv' ? 'Strategisk lagstiftningsutsikt'
+ : lang === 'de' ? 'Strategischer Gesetzgebungsausblick'
+ : lang === 'fr' ? 'Perspectives législatives stratégiques'
+ : lang === 'es' ? 'Perspectiva legislativa estratégica'
+ : lang === 'da' ? 'Strategisk lovgivningsmæssigt udsyn'
+ : lang === 'no' ? 'Strategisk lovgivningsmessig utsikt'
+ : lang === 'fi' ? 'Strateginen lainsäädäntönäkymä'
+ : lang === 'nl' ? 'Strategisch wetgevingsoverzicht'
+ : lang === 'ar' ? 'التوقعات التشريعية الاستراتيجية'
+ : lang === 'he' ? 'תחזית חקיקתית אסטרטגית'
+ : lang === 'ja' ? '戦略的立法見通し'
+ : lang === 'ko' ? '전략적 입법 전망'
+ : lang === 'zh' ? '战略立法展望'
+ : 'Strategic Legislative Outlook';
+ content += `\n
${outlookLabel}
\n`;
+
+ // Lede: how many propositions are in the pipeline
+ const propLedeTemplates: Record string> = {
+ sv: n => `${n} propositioner befinner sig i den lagstiftande processen denna månad.`,
+ da: n => `${n} lovforslag befinder sig i den lovgivningsmæssige proces denne måned.`,
+ no: n => `${n} proposisjoner er i den lovgivningsmessige prosessen denne måneden.`,
+ fi: n => `${n} hallituksen esitystä on lainsäädäntöprosessissa tässä kuussa.`,
+ de: n => `${n} Regierungsvorlagen befinden sich diesen Monat im Gesetzgebungsprozess.`,
+ fr: n => `${n} propositions gouvernementales sont en cours de traitement législatif ce mois-ci.`,
+ es: n => `${n} proposiciones gubernamentales se encuentran en el proceso legislativo este mes.`,
+ nl: n => `${n} regeringsvoorstellen bevinden zich deze maand in het wetgevingsproces.`,
+ ar: n => `${n} مقترحات حكومية في المسار التشريعي هذا الشهر.`,
+ he: n => `${n} הצעות ממשלה נמצאות בתהליך החקיקתי החודש.`,
+ ja: n => `今月は${n}件の政府提案が立法プロセスにあります。`,
+ ko: n => `이달 ${n}건의 정부 법안이 입법 프로세스에 있습니다.`,
+ zh: n => `本月${n}项政府提案处于立法审议过程中。`,
+ };
+ const propLedeTpl = propLedeTemplates[lang as string];
+ const propLede = propLedeTpl
+ ? propLedeTpl(propositions.length)
+ : `${propositions.length} government propositions are in the legislative pipeline this month.`;
+ content += ` ${escapeHtml(propLede)}
\n`;
+
+ propositions.slice(0, 8).forEach(prop => { // 8 propositions: readable summary without overwhelming
+ const rec = prop as Record;
+ const titleText = rec['titel'] || rec['title'] || rec['doktyp'] || 'Proposition';
+ const escapedTitle = escapeHtml(titleText);
+ const titleHtml = (rec['titel'] && !rec['title']) ? svSpan(escapedTitle, lang) : escapedTitle;
+ const significance = generatePolicySignificance(prop, lang);
+ const dokId = rec['dok_id'] ?? rec['id'] ?? '';
+ const urlBase = 'https://riksdagen.se/sv/dokument-och-lagar/dokument/';
+ const safeUrl = dokId ? sanitizeUrl(`${urlBase}${encodeURIComponent(dokId)}/`) : '';
+ content += ` \n`;
+ content += `
\n`;
+ if (significance) {
+ content += `
${escapeHtml(significance)}
\n`;
+ }
+ content += `
\n`;
+ });
+ }
+
+ // ── Committee Pipeline ────────────────────────────────────────────────────
+ if (reports.length > 0) {
+ const pipelineLabel = lang === 'sv' ? 'Utskottspipeline'
+ : lang === 'de' ? 'Ausschusspipeline'
+ : lang === 'fr' ? 'Pipeline des commissions'
+ : lang === 'es' ? 'Proceso en comité'
+ : lang === 'da' ? 'Udvalgspipeline'
+ : lang === 'no' ? 'Komitépipeline'
+ : lang === 'fi' ? 'Valiokuntaputkisto'
+ : lang === 'nl' ? 'Commissiepijplijn'
+ : lang === 'ar' ? 'مسار اللجان'
+ : lang === 'he' ? 'צינור הוועדות'
+ : lang === 'ja' ? '委員会パイプライン'
+ : lang === 'ko' ? '위원회 파이프라인'
+ : lang === 'zh' ? '委员会审议流程'
+ : 'Committee Pipeline';
+ content += `\n ${pipelineLabel}
\n`;
+
+ // Group reports by committee
+ const byCommittee: Record = {};
+ reports.forEach(r => {
+ const key = (r as Record).organ || 'unknown';
+ if (!byCommittee[key]) byCommittee[key] = [];
+ byCommittee[key].push(r);
+ });
+
+ Object.entries(byCommittee).slice(0, 5).forEach(([committeeCode, committeeReports]) => { // up to 5 committees
+ const committeeName = getCommitteeName(committeeCode, lang);
+ content += ` ${escapeHtml(committeeName)}
\n`;
+ committeeReports.slice(0, 3).forEach(report => { // 3 reports per committee keeps the section scannable
+ const rec = report as Record;
+ const titleText = rec['titel'] || rec['title'] || 'Report';
+ const escapedTitle = escapeHtml(titleText);
+ const titleHtml = (rec['titel'] && !rec['title']) ? svSpan(escapedTitle, lang) : escapedTitle;
+ const dokId = rec['dok_id'] ?? '';
+ const urlBase = 'https://riksdagen.se/sv/dokument-och-lagar/dokument/';
+ const safeUrl = dokId ? sanitizeUrl(`${urlBase}${encodeURIComponent(dokId)}/`) : '';
+ content += ` \n`;
+ content += `
\n`;
+ content += `
\n`;
+ });
+ });
+ }
+
+ // ── Policy Trends ─────────────────────────────────────────────────────────
+ if (motions.length > 0) {
+ const trendsLabel = lang === 'sv' ? 'Politiska trender'
+ : lang === 'de' ? 'Politische Trends'
+ : lang === 'fr' ? 'Tendances politiques'
+ : lang === 'es' ? 'Tendencias políticas'
+ : lang === 'da' ? 'Politiske tendenser'
+ : lang === 'no' ? 'Politiske trender'
+ : lang === 'fi' ? 'Poliittiset trendit'
+ : lang === 'nl' ? 'Politieke trends'
+ : lang === 'ar' ? 'الاتجاهات السياسية'
+ : lang === 'he' ? 'מגמות מדיניות'
+ : lang === 'ja' ? '政策トレンド'
+ : lang === 'ko' ? '정책 트렌드'
+ : lang === 'zh' ? '政策趋势'
+ : 'Policy Trends';
+ content += `\n ${trendsLabel}
\n`;
+
+ // Identify active policy domains from motions
+ const allTrendDomains = new Set();
+ motions.forEach(m => detectPolicyDomains(m, lang).forEach(d => allTrendDomains.add(d)));
+
+ // Party activity breakdown
+ const byPartyTrend: Record = {};
+ motions.forEach(m => {
+ const party = normalizePartyKey((m as Record).parti);
+ byPartyTrend[party] = (byPartyTrend[party] || 0) + 1;
+ });
+
+ if (allTrendDomains.size > 0) {
+ const domainList = Array.from(allTrendDomains).slice(0, 5).join(', '); // top 5 domains for readability
+ const domainIntroTemplates: Record string> = {
+ sv: (d, n) => `${n} motioner identifierar aktiva policydomäner: ${d}.`,
+ da: (d, n) => `${n} motioner identificerer aktive politikdomæner: ${d}.`,
+ no: (d, n) => `${n} forslag identifiserer aktive politikkdomener: ${d}.`,
+ fi: (d, n) => `${n} aloitetta tunnistaa aktiiviset politiikka-alueet: ${d}.`,
+ de: (d, n) => `${n} Anträge identifizieren aktive Politikbereiche: ${d}.`,
+ fr: (d, n) => `${n} motions identifient des domaines politiques actifs: ${d}.`,
+ es: (d, n) => `${n} mociones identifican áreas de política activas: ${d}.`,
+ nl: (d, n) => `${n} moties identificeren actieve beleidsgebieden: ${d}.`,
+ ar: (d, n) => `${n} اقتراحات تُحدد مجالات السياسة النشطة: ${d}.`,
+ he: (d, n) => `${n} הצעות מזהות תחומי מדיניות פעילים: ${d}.`,
+ ja: (d, n) => `${n}件の動議が活発な政策領域を特定しています: ${d}。`,
+ ko: (d, n) => `${n}건의 동의가 활발한 정책 분야를 식별합니다: ${d}.`,
+ zh: (d, n) => `${n}项动议确定了活跃的政策领域:${d}。`,
+ };
+ const domTpl = domainIntroTemplates[lang as string];
+ const domainIntro = domTpl
+ ? domTpl(escapeHtml(domainList), motions.length)
+ : `${motions.length} motions identify active policy domains: ${escapeHtml(domainList)}.`;
+ content += ` ${domainIntro}
\n`;
+ }
+
+ // Top parties by motion volume
+ const topParties = Object.entries(byPartyTrend)
+ .filter(([k]) => k !== 'unknown')
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 4); // 4 parties: covers the typical Swedish governing+major-opposition parties
+
+ if (topParties.length > 0) {
+ content += ` \n`;
+ topParties.forEach(([party, count]) => {
+ const partyMotionTemplates: Record string> = {
+ sv: (p, n) => `${p}: ${n} motioner inlämnade`,
+ da: (p, n) => `${p}: ${n} motioner indgivet`,
+ no: (p, n) => `${p}: ${n} forslag innsendt`,
+ fi: (p, n) => `${p}: ${n} aloitetta jätetty`,
+ de: (p, n) => `${p}: ${n} Anträge eingereicht`,
+ fr: (p, n) => `${p}: ${n} motions déposées`,
+ es: (p, n) => `${p}: ${n} mociones presentadas`,
+ nl: (p, n) => `${p}: ${n} moties ingediend`,
+ ar: (p, n) => `${p}: ${n} اقتراحات مقدمة`,
+ he: (p, n) => `${p}: ${n} הצעות הוגשו`,
+ ja: (p, n) => `${p}: ${n}件の動議を提出`,
+ ko: (p, n) => `${p}: ${n}건의 동의 제출`,
+ zh: (p, n) => `${p}: ${n}项动议提交`,
+ };
+ const partyTpl = partyMotionTemplates[lang as string];
+ const partyEntry = partyTpl
+ ? partyTpl(escapeHtml(party), count)
+ : `${escapeHtml(party)}: ${count} motions submitted`;
+ content += ` - ${partyEntry}
\n`;
+ });
+ content += `
\n`;
+ }
+ }
+
+ return content;
+}
diff --git a/scripts/data-transformers/index.ts b/scripts/data-transformers/index.ts
index dcba80e8a..98e010e34 100644
--- a/scripts/data-transformers/index.ts
+++ b/scripts/data-transformers/index.ts
@@ -65,6 +65,7 @@ import {
generatePropositionsContent,
generateMotionsContent,
generateGenericContent,
+ generateMonthAheadContent,
} from './content-generators.js';
/**
@@ -85,7 +86,7 @@ export function generateArticleContent(
case 'week-ahead':
return generateWeekAheadContent(data as WeekAheadData, lang);
case 'month-ahead':
- return generateWeekAheadContent(data as WeekAheadData, lang);
+ return generateMonthAheadContent(data, lang);
case 'committee-reports':
return generateCommitteeContent(data, lang);
case 'propositions':
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index 5e5bb25c1..4b0188555 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -47,6 +47,9 @@ import type { ArticleCategory, GeneratedArticle, GenerationResult, MCPCallRecord
export const REQUIRED_TOOLS: readonly string[] = [
'get_calendar_events',
'search_dokument', // conditional: used only when calendar is empty
+ 'get_betankanden',
+ 'get_propositioner',
+ 'get_motioner',
];
export interface TitleSet {
@@ -59,6 +62,7 @@ export interface MonthAheadValidationResult {
hasMinimumSources: boolean;
hasForwardLookingTone: boolean;
hasStrategicContext: boolean;
+ hasLegislativePipeline: boolean;
passed: boolean;
}
@@ -102,6 +106,14 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
const fromStr = formatDateForSlug(today);
const toStr = formatDateForSlug(endDate);
+ // 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)}`;
+
console.log(` 🔄 Fetching calendar events ${fromStr} → ${toStr}...`);
const events = await client.fetchCalendarEvents(fromStr, toStr) as RawDocument[];
mcpCalls.push({ tool: 'get_calendar_events', result: events });
@@ -138,6 +150,29 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
}
}
+ // ── Fetch strategic legislative pipeline data ──────────────────────────
+ console.log(' 🔄 Fetching legislative pipeline (betankanden, propositioner, motioner)...');
+ const [committeeReports, propositionDocs, motionDocs] = await Promise.all([
+ Promise.resolve()
+ .then(() => client.fetchCommitteeReports(20, currentRiksmote) as Promise)
+ .catch((err: unknown) => { console.error('Failed to fetch committee reports:', err); return [] as unknown[]; }),
+ Promise.resolve()
+ .then(() => client.fetchPropositions(15, currentRiksmote) as Promise)
+ .catch((err: unknown) => { console.error('Failed to fetch propositions:', err); return [] as unknown[]; }),
+ Promise.resolve()
+ .then(() => client.fetchMotions(50, currentRiksmote) as Promise)
+ .catch((err: unknown) => { console.error('Failed to fetch motions:', err); return [] as unknown[]; }),
+ ]);
+
+ mcpCalls.push({ tool: 'get_betankanden', result: committeeReports });
+ mcpCalls.push({ tool: 'get_propositioner', result: propositionDocs });
+ mcpCalls.push({ tool: 'get_motioner', result: motionDocs });
+
+ console.log(
+ ` 📊 Pipeline: ${committeeReports.length} reports, ` +
+ `${propositionDocs.length} propositions, ${motionDocs.length} motions`
+ );
+
const slug = `${formatDateForSlug(today)}-month-ahead`;
const articles: GeneratedArticle[] = [];
@@ -145,16 +180,27 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
console.log(` 🌐 Generating ${lang.toUpperCase()} version...`);
const dataForContent = events.length > 0
- ? { events }
- : { events: [], documents };
+ ? {
+ events,
+ reports: committeeReports as RawDocument[],
+ propositions: propositionDocs as RawDocument[],
+ motions: motionDocs as RawDocument[],
+ }
+ : {
+ events: [],
+ documents,
+ reports: committeeReports as RawDocument[],
+ propositions: propositionDocs as RawDocument[],
+ motions: motionDocs as RawDocument[],
+ };
const content: string = generateArticleContent(dataForContent, 'month-ahead', lang);
const watchPoints = extractWatchPoints(dataForContent, lang);
const metadata = generateMetadata(dataForContent, 'month-ahead', lang);
const readTime: string = calculateReadTime(content);
const usedTools = events.length > 0
- ? ['get_calendar_events']
- : ['get_calendar_events', 'search_dokument'];
+ ? ['get_calendar_events', 'get_betankanden', 'get_propositioner', 'get_motioner']
+ : ['get_calendar_events', 'search_dokument', 'get_betankanden', 'get_propositioner', 'get_motioner'];
const sources: string[] = generateSources(usedTools);
const itemCount = events.length > 0 ? events.length : documents.length;
@@ -199,7 +245,9 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
event: events.length > 0
? `${events.length} events over ${daysAhead} days`
: `${documents.length} upcoming documents`,
- sources: events.length > 0 ? ['calendar_events'] : ['calendar_events', 'search_dokument'],
+ sources: events.length > 0
+ ? ['calendar_events', 'get_betankanden', 'get_propositioner', 'get_motioner']
+ : ['calendar_events', 'search_dokument', 'get_betankanden', 'get_propositioner', 'get_motioner'],
},
};
} catch (error: unknown) {
@@ -286,12 +334,14 @@ export function validateMonthAhead(article: ArticleInput): MonthAheadValidationR
const hasMinimumSources = countSources(article) >= 3;
const hasForwardLookingTone = checkForwardLookingTone(article);
const hasStrategicContext = checkStrategicContext(article);
+ const hasLegislativePipeline = checkLegislativePipeline(article);
return {
hasCalendarEvents,
hasMinimumSources,
hasForwardLookingTone,
hasStrategicContext,
+ hasLegislativePipeline,
passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext
};
}
@@ -323,3 +373,11 @@ function checkStrategicContext(article: ArticleInput): boolean {
(article.content as string).toLowerCase().includes(keyword)
);
}
+
+function checkLegislativePipeline(article: ArticleInput): boolean {
+ if (!article || !article.content) return false;
+ const pipelineKeywords = ['pipeline', 'committee', 'proposition', 'motion', 'report', 'betank'];
+ return pipelineKeywords.some(keyword =>
+ (article.content as string).toLowerCase().includes(keyword)
+ );
+}
diff --git a/tests/news-types/month-ahead.test.ts b/tests/news-types/month-ahead.test.ts
index 6c2f95d55..0b2df6154 100644
--- a/tests/news-types/month-ahead.test.ts
+++ b/tests/news-types/month-ahead.test.ts
@@ -24,6 +24,9 @@ interface CalendarEvent {
interface MockMCPClientShape {
fetchCalendarEvents: Mock<(from: string, tom: string) => Promise>;
searchDocuments: Mock<(params: Record) => Promise>;
+ fetchCommitteeReports: Mock<(limit: number, rm: string) => Promise>;
+ fetchPropositions: Mock<(limit: number, rm: string) => Promise>;
+ fetchMotions: Mock<(limit: number, rm: string) => Promise>;
}
/** Validation input */
@@ -73,6 +76,9 @@ const { mockClientInstance, mockCalendarEvents, MockMCPClient } = vi.hoisted(()
const mockClientInstance: MockMCPClientShape = {
fetchCalendarEvents: vi.fn().mockResolvedValue(mockCalendarEvents) as MockMCPClientShape['fetchCalendarEvents'],
searchDocuments: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchDocuments'],
+ fetchCommitteeReports: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchCommitteeReports'],
+ fetchPropositions: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchPropositions'],
+ fetchMotions: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchMotions'],
};
function MockMCPClient(): MockMCPClientShape {
@@ -97,6 +103,9 @@ describe('Month-Ahead Article Generation', () => {
vi.clearAllMocks();
mockClientInstance.fetchCalendarEvents.mockResolvedValue(mockCalendarEvents);
mockClientInstance.searchDocuments.mockResolvedValue([]);
+ mockClientInstance.fetchCommitteeReports.mockResolvedValue([]);
+ mockClientInstance.fetchPropositions.mockResolvedValue([]);
+ mockClientInstance.fetchMotions.mockResolvedValue([]);
});
afterEach(() => {
@@ -117,6 +126,18 @@ describe('Month-Ahead Article Generation', () => {
it('should require search_dokument tool for document fallback', () => {
expect(monthAheadModule.REQUIRED_TOOLS).toContain('search_dokument');
});
+
+ it('should require get_betankanden tool for committee pipeline', () => {
+ expect(monthAheadModule.REQUIRED_TOOLS).toContain('get_betankanden');
+ });
+
+ it('should require get_propositioner tool for strategic outlook', () => {
+ expect(monthAheadModule.REQUIRED_TOOLS).toContain('get_propositioner');
+ });
+
+ it('should require get_motioner tool for trend analysis', () => {
+ expect(monthAheadModule.REQUIRED_TOOLS).toContain('get_motioner');
+ });
});
describe('Data Collection', () => {
@@ -129,6 +150,33 @@ describe('Month-Ahead Article Generation', () => {
expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_calendar_events')).toBe(true);
});
+ 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);
+ });
+
+ it('should fetch propositions from MCP', async () => {
+ const result = await monthAheadModule.generateMonthAhead({
+ languages: ['en']
+ });
+
+ expect(mockClientInstance.fetchPropositions).toHaveBeenCalled();
+ expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_propositioner')).toBe(true);
+ });
+
+ it('should fetch motions from MCP', async () => {
+ const result = await monthAheadModule.generateMonthAhead({
+ languages: ['en']
+ });
+
+ expect(mockClientInstance.fetchMotions).toHaveBeenCalled();
+ expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_motioner')).toBe(true);
+ });
+
it('should handle empty calendar events', async () => {
mockClientInstance.fetchCalendarEvents.mockResolvedValue([]);
@@ -177,6 +225,17 @@ describe('Month-Ahead Article Generation', () => {
expect(validation.hasCalendarEvents).toBe(true);
expect(validation.hasForwardLookingTone).toBe(true);
});
+
+ 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);
+ });
});
describe('Multi-Language', () => {
From a7eb61d65441bf5082a277942d1f573a8778d83a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 06:42:57 +0000
Subject: [PATCH 2/8] fix: address PR review comments on month-ahead
enhancements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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>
---
scripts/data-transformers/content-generators.ts | 7 ++++---
scripts/news-types/month-ahead.ts | 5 ++++-
tests/news-types/month-ahead.test.ts | 6 +++---
3 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts
index 880eda5ef..f7bcaa9a3 100644
--- a/scripts/data-transformers/content-generators.ts
+++ b/scripts/data-transformers/content-generators.ts
@@ -1224,9 +1224,10 @@ export function generateMonthAheadContent(data: ArticleContentData, lang: Langua
zh: (d, n) => `${n}项动议确定了活跃的政策领域:${d}。`,
};
const domTpl = domainIntroTemplates[lang as string];
- 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);
content += ` ${domainIntro}
\n`;
}
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index 4b0188555..f89deec6e 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -108,6 +108,9 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
// 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").
+ // Note: month-ahead uses dynamic calculation unlike weekly-review/monthly-review which hardcode
+ // '2025/26'. This is intentional: month-ahead must remain accurate across parliamentary years
+ // without requiring a code change at each new session boundary.
const year = today.getFullYear();
const month = today.getMonth() + 1; // 1-12
const currentRiksmote = month >= 9
@@ -342,7 +345,7 @@ export function validateMonthAhead(article: ArticleInput): MonthAheadValidationR
hasForwardLookingTone,
hasStrategicContext,
hasLegislativePipeline,
- passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext
+ passed: hasCalendarEvents && hasMinimumSources && hasForwardLookingTone && hasStrategicContext && hasLegislativePipeline
};
}
diff --git a/tests/news-types/month-ahead.test.ts b/tests/news-types/month-ahead.test.ts
index 0b2df6154..52e0a6102 100644
--- a/tests/news-types/month-ahead.test.ts
+++ b/tests/news-types/month-ahead.test.ts
@@ -24,9 +24,9 @@ interface CalendarEvent {
interface MockMCPClientShape {
fetchCalendarEvents: Mock<(from: string, tom: string) => Promise>;
searchDocuments: Mock<(params: Record) => Promise>;
- fetchCommitteeReports: Mock<(limit: number, rm: string) => Promise>;
- fetchPropositions: Mock<(limit: number, rm: string) => Promise>;
- fetchMotions: Mock<(limit: number, rm: string) => Promise>;
+ fetchCommitteeReports: Mock<(limit: number, rm: string | null) => Promise>;
+ fetchPropositions: Mock<(limit: number, rm: string | null) => Promise>;
+ fetchMotions: Mock<(limit: number, rm: string | null) => Promise>;
}
/** Validation input */
From d14f2e0038e4f12e40a0a817618b93f1ce674338 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 07:20:15 +0000
Subject: [PATCH 3/8] fix: address second-round PR review comments on
month-ahead
- 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>
---
.../data-transformers/content-generators.ts | 5 +--
scripts/news-types/month-ahead.ts | 34 ++++++++++++++++---
scripts/validate-cross-references.ts | 6 ++++
tests/news-types/month-ahead.test.ts | 1 +
4 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts
index db232dfb7..dde5d5a72 100644
--- a/scripts/data-transformers/content-generators.ts
+++ b/scripts/data-transformers/content-generators.ts
@@ -1230,7 +1230,8 @@ export function generateMonthAheadContent(data: ArticleContentData, lang: Langua
// Group reports by committee
const byCommittee: Record = {};
reports.forEach(r => {
- const key = (r as Record).organ || 'unknown';
+ const rec2 = r as Record;
+ const key = rec2.organ ?? rec2.committee ?? 'unknown';
if (!byCommittee[key]) byCommittee[key] = [];
byCommittee[key].push(r);
});
@@ -1309,7 +1310,7 @@ export function generateMonthAheadContent(data: ArticleContentData, lang: Langua
// Top parties by motion volume
const topParties = Object.entries(byPartyTrend)
- .filter(([k]) => k !== 'unknown')
+ .filter(([k]) => k !== 'unknown' && k !== 'other')
.sort((a, b) => b[1] - a[1])
.slice(0, 4); // 4 parties: covers the typical Swedish governing+major-opposition parties
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index f89deec6e..a6902c78a 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -379,8 +379,34 @@ function checkStrategicContext(article: ArticleInput): boolean {
function checkLegislativePipeline(article: ArticleInput): boolean {
if (!article || !article.content) return false;
- const pipelineKeywords = ['pipeline', 'committee', 'proposition', 'motion', 'report', 'betank'];
- return pipelineKeywords.some(keyword =>
- (article.content as string).toLowerCase().includes(keyword)
- );
+ // Language-aware keyword set covering English plus all 13 localized terms used in generated content
+ // (avoids false negatives for non-English articles; uses short stems to cover inflected forms).
+ const pipelineKeywords = [
+ // English
+ 'pipeline', 'committee', 'proposition', 'motion', 'report',
+ // Swedish (with/without diacritics)
+ 'betänk', 'betank', 'utskott',
+ // Danish / Norwegian
+ 'komité', 'komite', 'kommitté', 'kommission', 'komisjon',
+ // German
+ 'ausschuss', 'vorlage', 'bericht',
+ // French
+ 'commission', 'rapport',
+ // Spanish
+ 'comité', 'comite', 'informe', 'dictamen', 'propuesta',
+ // Finnish
+ 'valiokunta',
+ // Japanese
+ '委員会',
+ // Korean
+ '위원회',
+ // Chinese
+ '委员会',
+ // Arabic
+ 'لجنة',
+ // Hebrew
+ 'ועדה',
+ ];
+ const content = (article.content as string).toLowerCase();
+ return pipelineKeywords.some(keyword => content.includes(keyword));
}
diff --git a/scripts/validate-cross-references.ts b/scripts/validate-cross-references.ts
index 0ebf61596..c45cc5868 100644
--- a/scripts/validate-cross-references.ts
+++ b/scripts/validate-cross-references.ts
@@ -44,6 +44,12 @@ export const REQUIRED_TOOLS_PER_TYPE: RequiredToolsMap = {
'analyze_g0v_by_department',
'search_anforanden',
],
+ 'month-ahead': [
+ 'get_calendar_events',
+ 'get_betankanden',
+ 'get_propositioner',
+ 'get_motioner',
+ ],
breaking: [
'search_voteringar',
'get_voting_group',
diff --git a/tests/news-types/month-ahead.test.ts b/tests/news-types/month-ahead.test.ts
index 52e0a6102..457f75920 100644
--- a/tests/news-types/month-ahead.test.ts
+++ b/tests/news-types/month-ahead.test.ts
@@ -42,6 +42,7 @@ interface MonthAheadValidationResult {
hasMinimumSources: boolean;
hasForwardLookingTone: boolean;
hasStrategicContext: boolean;
+ hasLegislativePipeline: boolean;
passed: boolean;
}
From 2e46fe9e2b33804864bf0e21a7788091f7f85857 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:23 +0000
Subject: [PATCH 4/8] fix: address third-round PR review comments on
month-ahead
- Use console.warn (not console.error) for non-fatal pipeline fetch failures
- Replace broad generic keywords with specific structural section markers in checkLegislativePipeline
- Type fetchCalendarEvents result as RawCalendarEvent[] instead of RawDocument[]
- Update validation test to use structural section heading as content marker
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
---
scripts/news-types/month-ahead.ts | 75 +++++++++++++++++++---------
tests/news-types/month-ahead.test.ts | 2 +-
2 files changed, 53 insertions(+), 24 deletions(-)
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index a6902c78a..4583b1325 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -32,7 +32,8 @@ import {
generateMetadata,
calculateReadTime,
generateSources,
- type RawDocument
+ type RawDocument,
+ type RawCalendarEvent
} from '../data-transformers.js';
import { generateArticleHTML } from '../article-template.js';
import type { Language } from '../types/language.js';
@@ -118,7 +119,7 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
: `${year - 1}/${String(year).slice(-2)}`;
console.log(` 🔄 Fetching calendar events ${fromStr} → ${toStr}...`);
- const events = await client.fetchCalendarEvents(fromStr, toStr) as RawDocument[];
+ const events = await client.fetchCalendarEvents(fromStr, toStr) as RawCalendarEvent[];
mcpCalls.push({ tool: 'get_calendar_events', result: events });
console.log(` 📊 Found ${events.length} calendar events`);
@@ -158,13 +159,13 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
const [committeeReports, propositionDocs, motionDocs] = await Promise.all([
Promise.resolve()
.then(() => client.fetchCommitteeReports(20, currentRiksmote) as Promise)
- .catch((err: unknown) => { console.error('Failed to fetch committee reports:', err); return [] as unknown[]; }),
+ .catch((err: unknown) => { console.warn('Non-fatal: failed to fetch committee reports, continuing with empty list:', err); return [] as unknown[]; }),
Promise.resolve()
.then(() => client.fetchPropositions(15, currentRiksmote) as Promise)
- .catch((err: unknown) => { console.error('Failed to fetch propositions:', err); return [] as unknown[]; }),
+ .catch((err: unknown) => { console.warn('Non-fatal: failed to fetch propositions, continuing with empty list:', err); return [] as unknown[]; }),
Promise.resolve()
.then(() => client.fetchMotions(50, currentRiksmote) as Promise)
- .catch((err: unknown) => { console.error('Failed to fetch motions:', err); return [] as unknown[]; }),
+ .catch((err: unknown) => { console.warn('Non-fatal: failed to fetch motions, continuing with empty list:', err); return [] as unknown[]; }),
]);
mcpCalls.push({ tool: 'get_betankanden', result: committeeReports });
@@ -379,34 +380,62 @@ function checkStrategicContext(article: ArticleInput): boolean {
function checkLegislativePipeline(article: ArticleInput): boolean {
if (!article || !article.content) return false;
- // Language-aware keyword set covering English plus all 13 localized terms used in generated content
- // (avoids false negatives for non-English articles; uses short stems to cover inflected forms).
- const pipelineKeywords = [
+ // Match on specific section headings inserted by generateMonthAheadContent —
+ // these only appear when the pipeline sections were actually generated, avoiding
+ // false positives from generic words that can appear in ordinary calendar text.
+ const pipelineSectionMarkers = [
// English
- 'pipeline', 'committee', 'proposition', 'motion', 'report',
- // Swedish (with/without diacritics)
- 'betänk', 'betank', 'utskott',
- // Danish / Norwegian
- 'komité', 'komite', 'kommitté', 'kommission', 'komisjon',
+ 'strategic legislative outlook',
+ 'committee pipeline',
+ 'policy trends',
+ // Swedish
+ 'strategisk lagstiftningsutsikt',
+ 'utskottspipeline',
+ 'politiska trender',
// German
- 'ausschuss', 'vorlage', 'bericht',
+ 'strategischer gesetzgebungsausblick',
+ 'ausschusspipeline',
+ 'politische trends',
// French
- 'commission', 'rapport',
+ 'perspectives législatives stratégiques',
+ 'pipeline des commissions',
+ 'tendances politiques',
// Spanish
- 'comité', 'comite', 'informe', 'dictamen', 'propuesta',
+ 'perspectiva legislativa estratégica',
+ 'proceso en comité',
+ 'tendencias políticas',
+ // Danish
+ 'strategisk lovgivningsmæssigt udsyn',
+ 'udvalgspipeline',
+ 'politiske tendenser',
+ // Norwegian
+ 'strategisk lovgivningsmessig utsikt',
+ 'komitépipeline',
+ 'politiske trender',
// Finnish
- 'valiokunta',
+ 'strateginen lainsäädäntönäkymä',
+ 'valiokuntaputkisto',
+ 'poliittiset trendit',
+ // Dutch
+ 'strategisch wetgevingsoverzicht',
+ 'commissiepijplijn',
+ 'politieke trends',
// Japanese
- '委員会',
+ '戦略的立法見通し',
+ '委員会パイプライン',
// Korean
- '위원회',
+ '전략적 입법 전망',
+ '위원회 파이프라인',
// Chinese
- '委员会',
+ '战略立法展望',
+ '委员会审议流程',
// Arabic
- 'لجنة',
+ 'التوقعات التشريعية الاستراتيجية',
+ 'مسار اللجان',
// Hebrew
- 'ועדה',
+ 'תחזית חקיקתית אסטרטגית',
+ 'צינור הוועדות',
];
const content = (article.content as string).toLowerCase();
- return pipelineKeywords.some(keyword => content.includes(keyword));
+ return pipelineSectionMarkers.some(marker => content.includes(marker));
}
diff --git a/tests/news-types/month-ahead.test.ts b/tests/news-types/month-ahead.test.ts
index 457f75920..796e2b4eb 100644
--- a/tests/news-types/month-ahead.test.ts
+++ b/tests/news-types/month-ahead.test.ts
@@ -229,7 +229,7 @@ describe('Month-Ahead Article Generation', () => {
it('should include hasLegislativePipeline in validation result', () => {
const article: ArticleInput = {
- content: 'Committee pipeline report proposition motion scheduled for next month.',
+ content: 'Strategic Legislative Outlook
Committee pipeline report proposition motion scheduled for next month.
',
sources: ['source1', 'source2', 'source3']
};
From 0224ef1a0c163bca22c0d4faff1f579a85515202 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 10:59:26 +0000
Subject: [PATCH 5/8] fix: add missing Policy Trends h2 markers for
JA/KO/ZH/AR/HE in checkLegislativePipeline
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
---
scripts/news-types/month-ahead.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index 4583b1325..202e9bb04 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -423,18 +423,23 @@ function checkLegislativePipeline(article: ArticleInput): boolean {
// Japanese
'戦略的立法見通し',
'委員会パイプライン',
+ '政策トレンド',
// Korean
'전략적 입법 전망',
'위원회 파이프라인',
+ '정책 트렌드',
// Chinese
'战略立法展望',
'委员会审议流程',
+ '政策趋势',
// Arabic
'التوقعات التشريعية الاستراتيجية',
'مسار اللجان',
+ 'الاتجاهات السياسية',
// Hebrew
'תחזית חקיקתית אסטרטגית',
'צינור הוועדות',
+ 'מגמות מדיניות',
];
const content = (article.content as string).toLowerCase();
return pipelineSectionMarkers.some(marker => content.includes(marker));
From a24e1a60535f852099768968a7b720825dd9499f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 11:26:11 +0000
Subject: [PATCH 6/8] =?UTF-8?q?fix:=20align=20riksm=C3=B6te=20month=20comp?=
=?UTF-8?q?arison=20to=200-based=20pattern=20matching=20motions.ts?=
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/month-ahead.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index 202e9bb04..d04b21764 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -113,8 +113,8 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
// '2025/26'. This is intentional: month-ahead must remain accurate across parliamentary years
// without requiring a code change at each new session boundary.
const year = today.getFullYear();
- const month = today.getMonth() + 1; // 1-12
- const currentRiksmote = month >= 9
+ 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)}`;
From 258aa6cf61d9b476eefd118cf4b09bbff1d3d33d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 12:01:38 +0000
Subject: [PATCH 7/8] Fix early-return blocking pipeline fetches; use
noun-style crossReferences.sources
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
---
scripts/news-types/month-ahead.ts | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index d04b21764..f2a00ca03 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -148,13 +148,12 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
console.log(` 📊 Found ${documents.length} recent pipeline documents`);
}
- if (documents.length === 0) {
- console.log(' ℹ️ No documents found, skipping');
- return { success: true, files: 0, mcpCalls };
- }
}
- // ── Fetch strategic legislative pipeline data ──────────────────────────
+ // ── Fetch strategic legislative pipeline data (always, before early-return) ─
+ // These fetches run unconditionally so that betankanden/propositioner/motioner
+ // data can drive the Strategic Outlook / Pipeline / Trends sections even when
+ // calendar events and document fallbacks both return empty results.
console.log(' 🔄 Fetching legislative pipeline (betankanden, propositioner, motioner)...');
const [committeeReports, propositionDocs, motionDocs] = await Promise.all([
Promise.resolve()
@@ -177,6 +176,13 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
`${propositionDocs.length} propositions, ${motionDocs.length} motions`
);
+ // Only skip generation when all data sources (calendar, docs, and pipeline) are empty
+ if (events.length === 0 && documents.length === 0 &&
+ committeeReports.length === 0 && propositionDocs.length === 0 && motionDocs.length === 0) {
+ console.log(' ℹ️ No data from any source, skipping');
+ return { success: true, files: 0, mcpCalls };
+ }
+
const slug = `${formatDateForSlug(today)}-month-ahead`;
const articles: GeneratedArticle[] = [];
@@ -250,8 +256,8 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
? `${events.length} events over ${daysAhead} days`
: `${documents.length} upcoming documents`,
sources: events.length > 0
- ? ['calendar_events', 'get_betankanden', 'get_propositioner', 'get_motioner']
- : ['calendar_events', 'search_dokument', 'get_betankanden', 'get_propositioner', 'get_motioner'],
+ ? ['calendar_events', 'betankanden', 'propositioner', 'motioner']
+ : ['calendar_events', 'search_dokument', 'betankanden', 'propositioner', 'motioner'],
},
};
} catch (error: unknown) {
From 041437751dbaf4297069ee772d968dfbc2b9cc66 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Feb 2026 13:14:23 +0000
Subject: [PATCH 8/8] =?UTF-8?q?Fix=20riksm=C3=B6te=20to=20use=20endDate=20?=
=?UTF-8?q?for=20session=20boundary=20crossing;=20add=20pipeline-only=20te?=
=?UTF-8?q?st?=
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/month-ahead.ts | 12 +++++++-----
tests/news-types/month-ahead.test.ts | 18 ++++++++++++++++++
2 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/scripts/news-types/month-ahead.ts b/scripts/news-types/month-ahead.ts
index f2a00ca03..3f16b48a8 100644
--- a/scripts/news-types/month-ahead.ts
+++ b/scripts/news-types/month-ahead.ts
@@ -112,11 +112,13 @@ export async function generateMonthAhead(options: GenerationOptions = {}): Promi
// Note: month-ahead uses dynamic calculation unlike weekly-review/monthly-review which hardcode
// '2025/26'. This is intentional: month-ahead must remain accurate across parliamentary years
// without requiring a code change at each new session boundary.
- 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)}`;
+ // Use endDate (not today) as the session reference so that a late-August run covering the
+ // Sep-1 boundary fetches the new session's pipeline data, not the outgoing session's.
+ 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)}`;
console.log(` 🔄 Fetching calendar events ${fromStr} → ${toStr}...`);
const events = await client.fetchCalendarEvents(fromStr, toStr) as RawCalendarEvent[];
diff --git a/tests/news-types/month-ahead.test.ts b/tests/news-types/month-ahead.test.ts
index 796e2b4eb..6676a0914 100644
--- a/tests/news-types/month-ahead.test.ts
+++ b/tests/news-types/month-ahead.test.ts
@@ -188,6 +188,24 @@ describe('Month-Ahead Article Generation', () => {
expect(result.success).toBe(true);
expect(result.files).toBe(0);
});
+
+ it('should generate article when calendar is empty but pipeline data is present', async () => {
+ mockClientInstance.fetchCalendarEvents.mockResolvedValue([]);
+ mockClientInstance.searchDocuments.mockResolvedValue([]);
+ mockClientInstance.fetchPropositions.mockResolvedValue([
+ { titel: 'Proposition on climate policy', organ: 'MN', parti: 'MP' },
+ { titel: 'Tax reform bill', organ: 'FiU', parti: 'M' }
+ ]);
+
+ const result = await monthAheadModule.generateMonthAhead({
+ languages: ['en']
+ });
+
+ expect(result.success).toBe(true);
+ expect(result.files).toBeGreaterThan(0);
+ const enArticle = result.articles.find((a: GeneratedArticle) => a.lang === 'en');
+ expect(enArticle!.html).toContain('Strategic Legislative Outlook');
+ });
});
describe('Article Structure', () => {