[WIP] Add SCB and Riksbank quantitative integration for economic analysis#2042
[WIP] Add SCB and Riksbank quantitative integration for economic analysis#2042
Conversation
🏷️ Automatic Labeling SummaryThis PR has been automatically labeled based on the files changed and PR metadata. Applied Labels: size-xs Label Categories
For more information, see |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/3b652eec-4c5c-4fdd-90cf-3f98cc7ed90c Co-authored-by: pethers <1726836+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 |
There was a problem hiding this comment.
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). |
| 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 { |
There was a problem hiding this comment.
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).
| 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'; |
There was a problem hiding this comment.
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.
| label: 'Labour Force Survey (AKU)', | ||
| domain: 'labour', | ||
| defaultValueCodes: Object.freeze({ Tid: 'top(8)' }), | ||
| notes: 'Quarterly labour-market layer for AU unemployment and employment comparisons.', |
There was a problem hiding this comment.
Typo in preset notes: "AU unemployment" should be "AKU unemployment" to match the preset/key and the Swedish Labour Force Survey acronym.
| notes: 'Quarterly labour-market layer for AU unemployment and employment comparisons.', | |
| notes: 'Quarterly labour-market layer for AKU unemployment and employment comparisons.', |
| | 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? | |
There was a problem hiding this comment.
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.
| | 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 apply changes based on the comments in this thread |
…timeout/outage Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/9a198a77-7c64-4245-a198-3fbe79ef71a5 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all four reviewer comments in b927852:
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| const sourceUrl = typeof response === 'object' && response !== null && 'url' in response | ||
| ? String((response as { url?: unknown }).url) | ||
| : undefined; |
There was a problem hiding this comment.
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.
| `Riksbank fetch failed (${detail}); callers should fall back to cached analysis/data/riksbank/ artifacts.`, | ||
| ); | ||
| } | ||
| clearTimeout(timeoutId); | ||
|
|
There was a problem hiding this comment.
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).
| if (contentType.includes('pdf')) { | ||
| const buffer = await response.arrayBuffer(); | ||
| if (buffer.byteLength > PDF_MAX_BYTES) { | ||
| return buildOutagePayload( | ||
| kind, |
There was a problem hiding this comment.
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.
| const text = await response.text(); | ||
| const title = extractTitle(text); | ||
| return { | ||
| provider: 'riksbank', | ||
| kind, |
There was a problem hiding this comment.
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.
| 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) { |
There was a problem hiding this comment.
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.
| 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]; |
There was a problem hiding this comment.
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).
| 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 apply changes based on the comments in this thread |
…rceUrl Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/d07d61b5-6010-4413-b8ac-3867415b50b4 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all six reviewer comments in 3a58ef3:
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
|
@copilot analyse and improve TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request) |
Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/1f2cad03-23d5-4764-b572-386ed5014ef1 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Root cause was the Fixed in 9d6938c by replacing the union with a uniform |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
--kindflag error, narrowed sourceUrl in persistence sidecar,safeCancel/safeReleaseLockhelpersProperty 'bytes' does not exist) by replacing the discriminated union with a uniform{ exceeded; bytes; bytesRead }shape so narrowing is no longer required for.bytesaccess