Skip to content

[WIP] Add SCB and Riksbank quantitative integration for economic analysis#2042

Merged
pethers merged 6 commits intomainfrom
copilot/add-scb-riksbank-integration
Apr 27, 2026
Merged

[WIP] Add SCB and Riksbank quantitative integration for economic analysis#2042
pethers merged 6 commits intomainfrom
copilot/add-scb-riksbank-integration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

  • Read issue [Methodology] SCB & Riksbank quantitative integration (CPI, AKU, fuel-price) for economic transmission analysis #2026 and identify required code changes
  • Inspect existing SCB, IMF, template, and test patterns
  • Run baseline relevant Vitest tests and script TypeScript build
  • Add SCB fetch CLI with curated KPI/AKU/HEK/fuel presets, provenance, persistence, and outage fallback
  • Add Riksbank fetch CLI with policy-rate/minutes/fuel-projection surfaces, provenance, persistence, and outage fallback
  • Update comparative-international and intelligence-assessment templates with standard quantitative snippets
  • Add Vitest coverage for new helpers, including SCB API outage path and provenance contract expectations
  • Update relevant skills/documentation references for SCB/Riksbank quantitative usage
  • First-round reviewer feedback: AKU typo, expanded template commands, PDF base64 + abort timeout + fail-soft outage payload
  • Second-round reviewer feedback: timeout covers body read in try/finally, Content-Length pre-check + capped streaming for PDF/text/JSON, manual redirect handling re-validating host allowlist, explicit --kind flag error, narrowed sourceUrl in persistence sidecar, safeCancel/safeReleaseLock helpers
  • Fix typedoc TS2339 (Property 'bytes' does not exist) by replacing the discriminated union with a uniform { exceeded; bytes; bytesRead } shape so narrowing is no longer required for .bytes access
  • Run targeted validation and final code/security review

@github-actions
Copy link
Copy Markdown
Contributor

🏷️ Automatic Labeling Summary

This PR has been automatically labeled based on the files changed and PR metadata.

Applied Labels: size-xs

Label Categories

  • 🗳️ Content: news, dashboard, visualization, intelligence
  • 💻 Technology: html-css, javascript, workflow, security
  • 📊 Data: cia-data, riksdag-data, data-pipeline, schema
  • 🌍 I18n: i18n, translation, rtl
  • 🔒 ISMS: isms, iso-27001, nist-csf, cis-controls
  • 🏗️ Infrastructure: ci-cd, deployment, performance, monitoring
  • 🔄 Quality: testing, accessibility, documentation, refactor
  • 🤖 AI: agent, skill, agentic-workflow

For more information, see .github/labeler.yml.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/3b652eec-4c5c-4fdd-90cf-3f98cc7ed90c

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
@github-actions github-actions Bot added documentation Documentation updates testing Test coverage refactor Code refactoring skill Skill configuration size-l Large change (250-1000 lines) labels Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers marked this pull request as ready for review April 27, 2026 08:21
Copilot AI review requested due to automatic review settings April 27, 2026 08:21
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Swedish ground-truth quantitative tooling (SCB + Riksbank) and template hooks to support more data-backed economic transmission analysis alongside the existing IMF-first canon.

Changes:

  • Introduces new CLIs for fetching SCB table payloads (with curated presets) and Riksbank web/JSON artifacts, including provenance fields and persistence hooks.
  • Extends the persistence layer to store Riksbank artifacts with sidecar metadata.
  • Updates analysis templates and skills docs to standardize when/how to incorporate SCB/Riksbank quantitative context, plus adds Vitest coverage for the new helpers.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/scb-fetch.test.ts Adds unit tests for SCB CLI parsing, presets, value-codes parsing, and outage/no-data behavior.
tests/riksbank-fetch.test.ts Adds unit tests for Riksbank CLI parsing, URL allowlisting, and HTML/JSON response handling.
tests/data-persistence.test.ts Adds coverage for persisting Riksbank artifacts and verifying sidecar metadata.
scripts/scb-fetch.ts New SCB CLI wrapper with curated presets, argument parsing, provenance payload construction, and persistence.
scripts/riksbank-fetch.ts New Riksbank fetch CLI with URL validation, provenance, and persistence support.
scripts/parliamentary-data/data-persistence.ts Adds riksbank to persistence document types and implements persistRiksbankData.
analysis/templates/intelligence-assessment.md Adds a standard SCB+Riksbank quantitative transmission checklist block.
analysis/templates/comparative-international.md Adds a Swedish ground-truth transmission layer table and usage guidance.
.github/skills/riksdag-regering-mcp/SKILL.md Documents pairing parliamentary evidence with SCB/Riksbank quantitative outputs.
.github/skills/data-science-for-intelligence/SKILL.md Updates skill guidance to include SCB/Riksbank transmission modeling constraints (IMF remains cross-country canon).

Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +115 to +141
const target = assertRiksbankFetchTarget(url);
const response = await fetch(target, {
headers: { Accept: 'application/json, text/html, application/pdf;q=0.8, text/plain;q=0.7' },
});
if (!response.ok) throw new Error(`Riksbank fetch error: ${response.status} ${response.statusText}`);
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
const retrievedAt = new Date().toISOString();
if (contentType.includes('json')) {
const json = await response.json() as unknown;
return {
provider: 'riksbank',
kind,
url: target.toString(),
contentType,
retrievedAt,
json,
economicProvenance: {
provider: 'riksbank',
dataflow: 'riksbank-web',
indicator: kind,
url: target.toString(),
retrieved_at: retrievedAt,
},
};
}
const text = await response.text();
return {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRiksbankPayload advertises PDF support in the Accept header, but the non-JSON branch always calls response.text(). If the endpoint returns application/pdf, this will decode binary as UTF-8 and read the entire PDF into memory before slicing, producing corrupted output and potentially large memory usage. Handle application/pdf explicitly (e.g., arrayBuffer() + size cap and persist as a .pdf/base64 field) and add an abort timeout similar to StatskontoretClient.fetchWithTimeout (scripts/statskontoret-client.ts:226).

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +116 to +120
const response = await fetch(target, {
headers: { Accept: 'application/json, text/html, application/pdf;q=0.8, text/plain;q=0.7' },
});
if (!response.ok) throw new Error(`Riksbank fetch error: ${response.status} ${response.statusText}`);
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Riksbank CLI currently fails hard on any network error or non-2xx response (fetchRiksbankPayload throws). The PR/issue description calls out an outage fallback to cached analysis/data/riksbank/ artifacts; to meet that, consider returning a fail-soft payload (e.g., status: "no-data" + warning + empty content) on fetch failures so downstream workflows/templates can degrade gracefully instead of exiting.

Copilot uses AI. Check for mistakes.
Comment thread scripts/scb-fetch.ts Outdated
label: 'Labour Force Survey (AKU)',
domain: 'labour',
defaultValueCodes: Object.freeze({ Tid: 'top(8)' }),
notes: 'Quarterly labour-market layer for AU unemployment and employment comparisons.',
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in preset notes: "AU unemployment" should be "AKU unemployment" to match the preset/key and the Swedish Labour Force Survey acronym.

Suggested change
notes: 'Quarterly labour-market layer for AU unemployment and employment comparisons.',
notes: 'Quarterly labour-market layer for AKU unemployment and employment comparisons.',

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +168
| Price shock / relief channel | `tsx scripts/scb-fetch.ts preset --preset cpi` + `--preset fuel-prices` | `scb` | — | Does the measure materially affect household cost pressure? |
| Labour-market sensitivity | `tsx scripts/scb-fetch.ts preset --preset aku` | `scb` | — | Does unemployment amplify or mute the political signal? |
| Household exposure | `tsx scripts/scb-fetch.ts preset --preset household-economy` | `scb` | — | Which voter segments face the strongest disposable-income effect? |
| Monetary-policy reaction function | `tsx scripts/riksbank-fetch.ts repo-rate-path` + `minutes` | `riksbank` | — | Does the Riksbank path support or contradict the claimed transmission? |
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command examples in the table are incomplete/ambiguous: tsx scripts/scb-fetch.ts preset --preset cpi cannot be combined with a bare --preset fuel-prices (it requires a second tsx scripts/scb-fetch.ts preset ... invocation), and the Riksbank row similarly shows + minutes without the full tsx scripts/riksbank-fetch.ts minutes command. Please spell out both commands explicitly so the template is copy/paste correct.

Suggested change
| Price shock / relief channel | `tsx scripts/scb-fetch.ts preset --preset cpi` + `--preset fuel-prices` | `scb` || Does the measure materially affect household cost pressure? |
| Labour-market sensitivity | `tsx scripts/scb-fetch.ts preset --preset aku` | `scb` || Does unemployment amplify or mute the political signal? |
| Household exposure | `tsx scripts/scb-fetch.ts preset --preset household-economy` | `scb` || Which voter segments face the strongest disposable-income effect? |
| Monetary-policy reaction function | `tsx scripts/riksbank-fetch.ts repo-rate-path` + `minutes` | `riksbank` || Does the Riksbank path support or contradict the claimed transmission? |
| Price shock / relief channel | `tsx scripts/scb-fetch.ts preset --preset cpi` + `tsx scripts/scb-fetch.ts preset --preset fuel-prices` | `scb` || Does the measure materially affect household cost pressure? |
| Labour-market sensitivity | `tsx scripts/scb-fetch.ts preset --preset aku` | `scb` || Does unemployment amplify or mute the political signal? |
| Household exposure | `tsx scripts/scb-fetch.ts preset --preset household-economy` | `scb` || Which voter segments face the strongest disposable-income effect? |
| Monetary-policy reaction function | `tsx scripts/riksbank-fetch.ts repo-rate-path` + `tsx scripts/riksbank-fetch.ts minutes` | `riksbank` || Does the Riksbank path support or contradict the claimed transmission? |

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Copy Markdown
Member

pethers commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

Applied all four reviewer comments in b927852:

  • Fixed AKU typo in scripts/scb-fetch.ts.
  • Spelled out the full tsx scripts/... commands in the intelligence-assessment template.
  • Added application/pdf handling in scripts/riksbank-fetch.ts with a 5 MB cap and base64 encoding.
  • Added an abort timeout and fail-soft status: "no-data" payload (instead of throwing) for network/non-2xx outages, plus expanded Vitest coverage for PDF/outage/HTTP-error paths.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Comment on lines +649 to +651
const sourceUrl = typeof response === 'object' && response !== null && 'url' in response
? String((response as { url?: unknown }).url)
: undefined;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceUrl is derived by stringifying any present url property; if url exists but is undefined/non-string, this will write a misleading value like "undefined" into the sidecar metadata. Consider narrowing this to typeof response.url === 'string' && response.url.length > 0 before persisting it.

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +175 to +179
`Riksbank fetch failed (${detail}); callers should fall back to cached analysis/data/riksbank/ artifacts.`,
);
}
clearTimeout(timeoutId);

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request timeout is cleared immediately after fetch() resolves, before the response body is consumed (json(), text(), arrayBuffer()). This means a slow/large body can hang indefinitely even though a timeout was configured. Keep the timeout active until after the body has been fully read (e.g., wrap the entire fetch+parse flow in a try/finally and clear the timer in finally).

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +212 to +216
if (contentType.includes('pdf')) {
const buffer = await response.arrayBuffer();
if (buffer.byteLength > PDF_MAX_BYTES) {
return buildOutagePayload(
kind,
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.arrayBuffer() reads the full PDF into memory before enforcing PDF_MAX_BYTES, so the size cap does not actually prevent large memory allocations. Consider checking Content-Length up front and/or streaming the body with an incremental byte counter and aborting once the cap is exceeded.

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +236 to +240
const text = await response.text();
const title = extractTitle(text);
return {
provider: 'riksbank',
kind,
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.text() reads the full response into memory and only then slices to TEXT_MAX_BYTES. If the endpoint returns unexpectedly large HTML, this can still cause high memory usage. Prefer enforcing a maximum via Content-Length and/or streaming with an early abort before accumulating the entire body.

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +164 to +168
response = await fetch(target, {
headers: { Accept: 'application/json, text/html, application/pdf;q=0.8, text/plain;q=0.7' },
signal: controller.signal,
});
} catch (error) {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The host allowlist is only enforced on the initial URL; fetch() will follow redirects by default, which can allow downloading content from a different host than the one validated. To keep the allowlist meaningful, consider using redirect: 'manual' (or validating response.url / Location on redirects) and rejecting cross-host redirects.

Copilot uses AI. Check for mistakes.
Comment thread scripts/riksbank-fetch.ts Outdated
Comment on lines +261 to +268
async function main(): Promise<void> {
const { command, flags, booleans } = parseRiksbankArgs(process.argv.slice(2));
if (command === 'help') {
process.stdout.write(HELP);
return;
}
const kind = command === 'fetch' ? parseRiksbankKind(flags.get('kind') ?? '') : command;
const url = flags.get('url') ?? DEFAULT_URLS[kind];
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using the fetch command, a missing --kind flag currently produces an "unknown Riksbank artifact kind" error (because '' is passed through). This is harder to diagnose than an explicit "missing required flag --kind" message. Consider adding a small requireRiksbankFlag() helper (similar to requireSCBFlag) and using it for --kind (and optionally --url).

Suggested change
async function main(): Promise<void> {
const { command, flags, booleans } = parseRiksbankArgs(process.argv.slice(2));
if (command === 'help') {
process.stdout.write(HELP);
return;
}
const kind = command === 'fetch' ? parseRiksbankKind(flags.get('kind') ?? '') : command;
const url = flags.get('url') ?? DEFAULT_URLS[kind];
function requireRiksbankFlag(
flags: ReadonlyMap<string, string>,
name: string,
): string {
const value = flags.get(name)?.trim();
if (!value) throw new Error(`missing required flag --${name}`);
return value;
}
async function main(): Promise<void> {
const { command, flags, booleans } = parseRiksbankArgs(process.argv.slice(2));
if (command === 'help') {
process.stdout.write(HELP);
return;
}
const kind =
command === 'fetch' ? parseRiksbankKind(requireRiksbankFlag(flags, 'kind')) : command;
const urlFlag = flags.get('url')?.trim();
const url = urlFlag && urlFlag.length > 0 ? urlFlag : DEFAULT_URLS[kind];

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Copy Markdown
Member

pethers commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

Applied all six reviewer comments in 3a58ef3:

  • data-persistence.ts: narrowed sourceUrl to typeof === 'string' && length > 0 so non-string url properties no longer leak "undefined" into sidecar metadata.
  • riksbank-fetch.ts: timeout now covers the entire fetch + body read via try/finally; PDF/text/JSON paths pre-check Content-Length and stream the body with an incremental byte counter that aborts once the cap is exceeded; redirects are followed manually with redirect: 'manual', re-validating the Location host against the allowlist on each hop (max 3); requireRiksbankFlag produces an explicit missing required flag --kind error; repeated cancel/release boilerplate extracted into safeCancel / safeReleaseLock helpers.
  • Vitest coverage extended for Content-Length pre-check, manual-redirect chaining, and off-allowlist redirect rejection (10/10 Riksbank tests pass; full suite 59/59).

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers
Copy link
Copy Markdown
Member

pethers commented Apr 27, 2026

@copilot analyse and improve TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request)
TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request)Failing after 49s

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot analyse and improve [TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request)](https://github.com/Hack23/riksdagsmonitor/actions/runs/24986596601/job/73161333306?p...

Root cause was the npm run typedoc step (not Vitest itself — Vitest never got to run because typedoc failed first). Typedoc's TypeScript checker did not narrow the discriminated union { exceeded: false; bytes } | { exceeded: true; bytesRead } after the early-return guard, producing TS2339: Property 'bytes' does not exist at four sites.

Fixed in 9d6938c by replacing the union with a uniform { exceeded: boolean; bytes: Uint8Array; bytesRead: number } shape — bytes is empty when exceeded is true, callers still guard on exceeded before consuming, and no narrowing is required. Verified locally: npm run typedoc now reports 0 errors, tsc --project tsconfig.scripts.json --noEmit clean, and 10/10 Riksbank Vitest specs pass.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers merged commit 686710b into main Apr 27, 2026
16 checks passed
@pethers pethers deleted the copilot/add-scb-riksbank-integration branch April 27, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation updates refactor Code refactoring size-l Large change (250-1000 lines) size-xs Extra small change (< 10 lines) skill Skill configuration testing Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Methodology] SCB & Riksbank quantitative integration (CPI, AKU, fuel-price) for economic transmission analysis

3 participants