Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8652d5d
Initial plan
Copilot Mar 13, 2026
aacd59e
feat: add dedicated interpellations content generator with accountabi…
Copilot Mar 13, 2026
a07ce1f
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 14, 2026
6ea42be
fix: address all 7 PR review comments for interpellations generator
Copilot Mar 14, 2026
2979929
fix: address review round 2 — filter "Unknown" author, type mottagare…
Copilot Mar 14, 2026
b0479c6
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 14, 2026
ffe1b62
fix: address review round 3 — compound minister regex, pillar transit…
Copilot Mar 14, 2026
39975b6
fix: address review round 4 — data.documents fallback with length che…
Copilot Mar 14, 2026
d7418cb
fix: address review round 5 — remove misleading 'ip' doktyp, expand m…
Copilot Mar 15, 2026
2ab8ceb
feat: add interpellation-specific 'ip' type key to getDomainSpecificA…
Copilot Mar 15, 2026
c2e0bf8
fix: simplify redundant Swedish phrasing in ip generic fallback
Copilot Mar 15, 2026
0b2b7d8
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 15, 2026
f2cec98
fix: Unicode-aware minister regex and Example 3 party/date field
Copilot Mar 15, 2026
7b5a079
fix: correct test description for försvarsministern Unicode test
Copilot Mar 15, 2026
cc9bded
fix: resolve merge conflicts with main branch - unify interpellations…
Copilot Mar 15, 2026
dc370a4
fix: resolve merge conflicts with main branch (6 files) — unify inter…
Copilot Mar 15, 2026
d053fd7
Merge branch 'main' into copilot/enhance-content-generators-pipeline …
Copilot Mar 15, 2026
1bac3e0
fix: resolve merge conflicts with main — adopt main's interpellations…
Copilot Mar 15, 2026
62f241d
Merge branch 'main' into copilot/enhance-content-generators-pipeline …
Copilot Mar 15, 2026
d5bdcba
Merge branch 'main' into copilot/enhance-content-generators-pipeline
Copilot Mar 15, 2026
19302d7
fix: remove duplicate mottagare type definition re-introduced by merge
Copilot Mar 15, 2026
d3a1763
test: improve interpellations tests with positive assertions for head…
Copilot Mar 15, 2026
43ac876
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 15, 2026
8baa862
fix: localize ip fallback text for all 14 languages in generatePolicy…
Copilot Mar 15, 2026
cf0efda
style: rename test variables deDe/frFr to deContent/frContent per cod…
Copilot Mar 15, 2026
767ec54
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 15, 2026
08ee3c1
fix: correct da/no/fi ipFallback to reference Swedish Riksdag, not ot…
Copilot Mar 15, 2026
ca4de6a
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 15, 2026
2f59a74
fix: remove duplicate interpellations JSDoc bullet and fix Finnish ip…
Copilot Mar 15, 2026
ad4ab83
refactor: hoist IP_FALLBACK to module-level, update JSDoc, consolidat…
Copilot Mar 15, 2026
eb1c5d4
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 16, 2026
5aa4b33
fix: replace weak toContain('3') assertion with specific '3 interpell…
Copilot Mar 16, 2026
2202173
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 16, 2026
4b63e85
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 16, 2026
2997ec7
Merge branch 'main' into copilot/enhance-content-generators-pipeline
pethers Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/data-transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export {
generateMindmapSection,
generateSankeySection,
generateCiaOverviewSection,
generateInterpellationsContent,
} from './content-generators.js';
export type {
SwotSectionOptions,
Expand Down
32 changes: 30 additions & 2 deletions scripts/data-transformers/policy-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,27 @@ export function getDomainSpecificAnalysis(primaryDomain: string, doktyp: string,
return entry.en['default'] ?? '';
}

/**
* Localised interpellation fallback text: references minister obligation
* to respond, not committee review. Hoisted to module-level to avoid
* per-call allocation inside generatePolicySignificance().
*/
const IP_FALLBACK: Readonly<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: '质询在全体会议上进行辩论,部长有义务回应并承担责任。',
};

/**
* Generate policy significance context for a document based on its metadata.
* Uses the localised policySignificanceTouches label plus a domain-specific
Expand All @@ -546,7 +567,7 @@ export function getDomainSpecificAnalysis(primaryDomain: string, doktyp: string,
* when no domain keyword matches but the document's organ field identifies a
* known Riksdag committee.
* @param impliedDoktyp - document type inferred from the calling context
* ('mot', 'bet', 'prop') when doc.doktyp / doc.documentType is absent.
* ('mot', 'bet', 'prop', 'ip') when doc.doktyp / doc.documentType is absent.
*/
export function generatePolicySignificance(doc: RawDocument, lang: Language | string, impliedDoktyp?: string): string {
const domains = detectPolicyDomains(doc, lang);
Expand Down Expand Up @@ -593,6 +614,13 @@ export function generatePolicySignificance(doc: RawDocument, lang: Language | st
}
}

// Interpellation-specific fallback: references minister obligation, not committee review
const doktyp2 = doc.doktyp || doc.documentType || impliedDoktyp || '';
if (doktyp2 === 'ip') {
return IP_FALLBACK[lang as string]
?? 'The interpellation is debated in the chamber, where the minister is obliged to respond and be held accountable.';
}
Comment on lines +617 to +622

// Generic significance when no domain detected and no known committee
const genericVal = L(lang, 'policySignificanceGeneric');
return typeof genericVal === 'string' ? genericVal : 'Requires committee review and chamber debate before a decision is reached.';
Expand All @@ -605,7 +633,7 @@ export function generatePolicySignificance(doc: RawDocument, lang: Language | st
* above in structured views and must not be duplicated here.
* Falls back to generatePolicySignificance when no enriched text is available.
* @param impliedDoktyp - document type inferred from the calling context
* ('mot', 'bet', 'prop') when doc.doktyp / doc.documentType is absent.
* ('mot', 'bet', 'prop', 'ip') when doc.doktyp / doc.documentType is absent.
*/
export function generateDeepPolicyAnalysis(doc: RawDocument, lang: Language | string, impliedDoktyp?: string, maxPassageChars = 300): string {
const effectiveDoktyp = doc.doktyp || doc.documentType || impliedDoktyp || '';
Expand Down
4 changes: 2 additions & 2 deletions scripts/data-transformers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface RawDocument {
intressent_namn?: string;
author?: string;
parti?: string;
/** Recipient minister / target for interpellations (mottagare field from the Riksdag API) */
mottagare?: string;
/** Publication date from the MCP API (ISO format YYYY-MM-DD) */
datum?: string;
/** Riksmöte (parliamentary session, e.g. "2025/26") */
Expand All @@ -55,8 +57,6 @@ export interface RawDocument {
fullContent?: string;
/** Whether this document was enriched with full content */
contentFetched?: boolean;
/** Target minister / recipient (used by interpellations) */
mottagare?: string;
/** Related speeches mentioning this document */
speeches?: Array<{ talare?: string; parti?: string; text?: string; anforande_nummer?: string }>;
}
Expand Down
48 changes: 48 additions & 0 deletions tests/data-transformers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,54 @@ describe('Data Transformers', () => {
// Should contain Swedish domain-specific text about NATO
expect(content).toMatch(/NATO|säkerhetsåtaganden|strategisk/i);
});

it('should use ip-specific analysis for interpellations — not default/proposal text', () => {
const content = generateArticleContent({
interpellations: [{ titel: 'Försvarspolitik', url: 'https://example.com/1', dok_id: 'IP1' }]
} as MockArticlePayload, 'interpellations', 'en') as string;

// ip-specific text references minister accountability / NATO scrutiny
expect(content).toMatch(/minister|accountability|scrutini/i);
// NOT the default proposition-focused text about "cross-party consensus-building"
expect(content).not.toContain('cross-party consensus-building mechanisms');
});

it('should use ip-specific Swedish analysis for interpellations in sv', () => {
const content = generateArticleContent({
interpellations: [{ titel: 'Klimatpolitik', url: 'https://example.com/1', dok_id: 'IP2' }]
} as MockArticlePayload, 'interpellations', 'sv') as string;

// ip-specific Swedish text references minister accountability
expect(content).toMatch(/ministern|statsråd|insyn|redovisning/i);
});

it('should use interpellation generic fallback when no domain detected', () => {
const content = generateArticleContent({
interpellations: [{ titel: 'Allmän fråga', url: 'https://example.com/1', dok_id: 'IP3' }]
} as MockArticlePayload, 'interpellations', 'en') as string;

// Generic interpellation text — references minister obligation, not committee review
expect(content).toMatch(/minister is obliged to respond|held accountable/i);
expect(content).not.toContain('Requires committee review');
});

it('should localize interpellation generic fallback for non-en/sv languages', () => {
const deContent = generateArticleContent({
interpellations: [{ titel: 'Allgemeine Frage', url: 'https://example.com/1', dok_id: 'IP4' }]
} as MockArticlePayload, 'interpellations', 'de') as string;

// German interpellation fallback references minister accountability
expect(deContent).toMatch(/Minister verpflichtet|Rede und Antwort/i);
expect(deContent).not.toContain('Requires committee review');

const frContent = generateArticleContent({
interpellations: [{ titel: 'Question générale', url: 'https://example.com/1', dok_id: 'IP5' }]
} as MockArticlePayload, 'interpellations', 'fr') as string;

// French interpellation fallback references minister accountability
expect(frContent).toMatch(/ministre est tenu|rendre des comptes/i);
expect(frContent).not.toContain('Requires committee review');
});
});

describe('Thematic grouping in motions content', () => {
Expand Down
49 changes: 49 additions & 0 deletions tests/interpellations-content-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,55 @@ describe('Interpellations Content Generator', () => {
}
});
});

describe('CSS class markers', () => {
it('should use interpellation-entry CSS class, not motion-entry', () => {
const content = generateInterpellationsContent({ interpellations: sampleInterpellations }, 'en');
expect(content).toContain('interpellation-entry');
expect(content).not.toContain('motion-entry');
});
});

describe('Analytical lede', () => {
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');
});
});

describe('Policy analysis ip fallback', () => {
it('should render ip-specific policy text, not committee review', () => {
const content = generateArticleContent({
interpellations: [{ titel: 'Interpellation om vård', parti: 'V', url: '#', dokumentnamn: 'IP1' }],
}, 'interpellations', 'en') as string;
expect(content).not.toContain('Requires committee review');
expect(content).toContain(CONTENT_LABELS.en.whyItMatters);
});
});

describe('Label function smoke tests', () => {
it('should produce interpellationStrategyContext with numeric argument', () => {
const fn = CONTENT_LABELS.en.interpellationStrategyContext;
expect(typeof fn).toBe('function');
const result = fn(4);
expect(result).toContain('4');
});

it('should have interpellationDefault for all 14 languages', () => {
for (const lang of LANGUAGES) {
const labels = CONTENT_LABELS[lang as Language];
expect(labels?.interpellationDefault, `interpellationDefault missing for ${lang}`).toBeTruthy();
}
});

it('should have readFullInterpellation for all 14 languages', () => {
for (const lang of LANGUAGES) {
const labels = CONTENT_LABELS[lang as Language];
expect(labels?.readFullInterpellation, `readFullInterpellation missing for ${lang}`).toBeTruthy();
}
});
});
});

describe('Shared Prompts Library', () => {
Expand Down
Loading