From public Swedish parliamentary evidence to auditable, multilingual political-intelligence articles
Agentic workflows · 23-artifact analysis contract · deterministic Markdown aggregation · sanitized HTML rendering · S3/CloudFront deployment
📋 Document Owner: CEO | 📅 Last Updated: 2026-04-25 (UTC)
🏢 Owner: Hack23 AB (Org.nr 559534-7807) | 🏷️ Classification: Public
Primary example: analysis/daily/2026-04-24/interpellations/article.md → news/2026-04-24-interpellations-en.html / news/2026-04-24-interpellations-sv.html
- 🎯 Executive Summary
- 💼 Purpose, Function, Business Value and Political-Analysis Object
- 🧭 End-to-End Generation Map
- 🤖 Agentic Workflow Architecture
- 📥 Data Collection and Evidence Foundation
- 🧠 Analysis Methodologies and Templates
- 🚦 Analysis Gate
- 📝 How
article.mdIs Generated - 🌐 How
article.mdBecomes HTML - 🎨 UI/UX, Mermaid, D3 and Chart.js Support
- 🌍 Language Switchers and Translation Model
- 🚀 Build and S3 Deployment
- 🛡️ Security, Privacy and ISMS Controls
- ✅ Operational Checklist
- 🚀 Future Improvements Roadmap
- 🔗 Source File Index
Riksdagsmonitor articles are not hand-written HTML pages. They are deterministic projections of a deeper political-intelligence product:
- Agentic workflows in
.github/workflows/news-*.mdrun on schedules or manual dispatch. - The workflow imports bounded prompt modules from
.github/prompts/. - The AI agent collects public Riksdag/Regering data through the
riksdag-regeringMCP server, Swedish statistics through SCB, agency-capacity and public-management evidence from Statskontoret, supplementary governance/environment/social/education indicators through World Bank, and economic context through the repository IMF TypeScript client. - The agent produces a stable set of 23 core analysis artifacts plus per-document files under
analysis/daily/$ARTICLE_DATE/$SUBFOLDER/. - The single blocking gate in
.github/prompts/05-analysis-gate.mdmust pass before any article is generated. scripts/aggregate-analysis.tsturns the analysis folder into one canonicalarticle.md.scripts/render-articles.tssanitizes Markdown and wraps it in shared article chrome to createnews/$DATE-$SUBFOLDER-$LANG.html.- Vite builds the static site and
.github/workflows/deploy-s3.ymlpublishesdist/to S3 + CloudFront.
The result is a transparent political-intelligence article where every claim remains traceable to source artifacts, every source artifact remains traceable to public evidence, and the HTML page carries machine-readable provenance through JSON-LD NewsArticle.isBasedOn.
Current publication principle: the AI writes analysis artifacts, not final HTML. Scripts own aggregation, sanitization, chrome, SEO, language alternates, source footers and deployment behavior. This separation is what makes the system auditable.
The article-generation pipeline exists to turn Swedish public parliamentary events into rigorous, auditable, citizen-facing intelligence. It supports the Riksdagsmonitor mission from README.md: systematic transparency over Swedish Riksdag activity, coalition dynamics, voting patterns, and public accountability.
| Layer | Function | Primary files |
|---|---|---|
| Collection | Fetch public parliamentary, government, statistical and economic evidence | .github/prompts/03-data-download.md, scripts/download-parliamentary-data.ts, scripts/imf-fetch.ts |
| Analysis | Produce structured OSINT/INTOP assessments with evidence, uncertainty and color-coded Mermaid | analysis/methodologies/, analysis/templates/, .github/prompts/04-analysis-pipeline.md |
| Gate | Enforce artifact presence, evidence quality, Mermaid coverage and Pass-2 improvement | .github/prompts/05-analysis-gate.md |
| Aggregation | Convert the folder of analysis artifacts into canonical article.md |
scripts/aggregate-analysis.ts, scripts/render-lib/aggregator.ts |
| Rendering | Sanitize Markdown and build complete article HTML with SEO, language switcher and source footer | scripts/render-articles.ts, scripts/render-lib/markdown.ts, article.ts, chrome.ts |
| Publishing | Build static assets and deploy with correct MIME types, cache headers and CloudFront invalidation | package.json, vite.config.js, .github/workflows/deploy-s3.yml, scripts/deploy-s3.sh |
| Value area | How article generation contributes |
|---|---|
| Trust enhancement | Every article is backed by visible source files and primary-source links. |
| Competitive advantage | Riksdagsmonitor combines official Swedish political data with structured intelligence techniques (DIW, ACH, SWOT, risk, threat, stakeholder, scenario and forward-indicator analysis). |
| Operational excellence | Deterministic scripts (aggregate-analysis.ts, render-articles.ts) make publication repeatable, testable and auditable. |
| Reputational protection | AI-generated political text is gated by evidence standards, source diversity, neutral language and human-reviewable PRs. |
| Democratic accountability | Citizens can inspect the same source artifacts used to produce the public article. |
An article is the dissemination layer of an analysis object. The primary analytical object is the folder:
analysis/daily/$ARTICLE_DATE/$SUBFOLDER/
For the example run:
analysis/daily/2026-04-24/interpellations/
This folder contains the full political-intelligence object:
- 23 mandatory core artifacts.
- Per-document analysis files under
documents/. - Optional supplementary files.
- The generated canonical
article.md. - Supporting JSON chart/economic/provenance files when applicable.
The article is therefore a rendered view of the intelligence object, not the source of record.
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#1565C0","primaryTextColor":"#ffffff","primaryBorderColor":"#0A3F7F","lineColor":"#90CAF9","secondaryColor":"#2E7D32","secondaryTextColor":"#ffffff","tertiaryColor":"#FF9800","tertiaryTextColor":"#000000","mainBkg":"#1565C0","secondBkg":"#2E7D32","tertiaryBkg":"#FF9800","noteBkgColor":"#FFC107","noteTextColor":"#000000","errorBkgColor":"#D32F2F","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart TB
A["Trigger<br/>news-*.md schedule or workflow_dispatch"] --> B["🧰 Runtime setup<br/>Node 25 · npm ci · MCP pre-warm"]
B --> C["📥 Public evidence download<br/>Riksdag/Regering · SCB · IMF · WB residue"]
C --> D["🧠 Analysis Pass 1<br/>23 artifacts + per-document files"]
D --> E["🔁 Analysis Pass 2<br/>read back and improve every section"]
E --> F{"🚦 05 Analysis Gate<br/>evidence · Mermaid · Pass-2 · structure"}
F -- fail --> E
F -- pass --> G["📝 aggregate-analysis.ts<br/>analysis folder → article.md"]
G --> H["🌐 render-articles.ts<br/>article.md → news/*-{en,sv}.html"]
H --> I["🌍 news-translate.md<br/>12 additional languages"]
H --> J["📦 Vite build<br/>prebuild aggregates/renders/indexes/rss/sitemap"]
I --> J
J --> K["🚀 deploy-s3.yml<br/>S3 upload + CloudFront invalidation"]
style A fill:#1565C0,color:#ffffff
style C fill:#2E7D32,color:#ffffff
style D fill:#7B1FA2,color:#ffffff
style E fill:#4CAF50,color:#ffffff
style F fill:#D32F2F,color:#ffffff
style G fill:#FF9800,color:#000000
style H fill:#FF9800,color:#000000
style I fill:#00897B,color:#ffffff
style K fill:#0A66C2,color:#ffffff
The news workflows are Markdown-based GitHub Agentic Workflows. The interpellation example is:
It declares:
| Concern | Current configuration |
|---|---|
| Name | News: Interpellation Debates |
| Schedule | Daily around 07:00 on weekdays |
| Manual inputs | article_date, force_generation, languages, analysis_depth |
| Runtime | Node.js 25 |
| Engine | Copilot with claude-opus-4.7 |
| Permissions | Read-only content/issues/PR/actions/discussions/security-events for AI job |
| MCP gateway | Enabled |
| Safe outputs | One PR max, labels agentic-news, analysis-data, one translation dispatch max |
| Core output | analysis/daily/$ARTICLE_DATE/interpellations/article.md and news/$ARTICLE_DATE-interpellations-{en,sv}.html |
Riksdagsmonitor produces articles across three families — single-type (daily coverage), tier-c-aggregation (multi-source synthesis), and long-horizon-forecast (forward-looking analysis at increasing time-windows). The single source of truth for all registered types is analysis/article-types.json, with the corresponding schema defined in schemas/article-types.schema.json. CI parity is enforced by tests/article-types.test.ts and the check:docs script; full Ajv-based JSON Schema validation is planned follow-up.
| id | family | horizonDays | tierCMultiplier | articleWordFloor | electionCycleAnchor | cronExpression |
|---|---|---|---|---|---|---|
| propositions | single-type | 0 | 1 | 1000 | current | 0 5 * * 1-5 |
| motions | single-type | 0 | 1 | 1000 | current | 0 6 * * 1-5 |
| committee-reports | single-type | 0 | 1 | 1000 | current | 0 4 * * 1-5 |
| interpellations | single-type | 0 | 1 | 1000 | current | 0 7 * * 1-5 |
| realtime-monitor | tier-c-aggregation | 0 | 0.8 | 1500 | current | 0 10,14 * * 1-5 |
| evening-analysis | tier-c-aggregation | 0 | 1 | 1500 | current | 0 18 * * 1-5 |
| week-ahead | long-horizon-forecast | 7 | 1.2 | 1500 | current | 0 7 * * 5 |
| month-ahead | long-horizon-forecast | 30 | 1.5 | 1500 | current | 0 8 1 * * |
| quarter-ahead | long-horizon-forecast | 90 | 1.7 | 2000 | current | 0 9 1,15 * * |
| year-ahead | long-horizon-forecast | 365 | 2 | 2500 | current | 0 9 5 1,7 * |
| election-cycle | long-horizon-forecast | 1460 | 2.5 | 3500 | both | dispatch-only |
| weekly-review | tier-c-aggregation | 0 | 1.2 | 1500 | current | 0 9 * * 6 |
| monthly-review | tier-c-aggregation | 0 | 1.5 | 1500 | current | 0 10 28 * * |
Long-horizon workflows additionally import ext/long-horizon-forecasting.md (horizon stratification, scenario-tree depth, counterfactual mandate, IMF projection-year stamps, PESTLE blocking thresholds, cross-horizon citation). The election-cycle workflow further imports ext/cycle-rollover.md, which is active only within ± 30 days of a Swedish election anchor (the next being 2026-09-13).
Every content workflow imports the bounded-context prompt library:
| Import | Responsibility |
|---|---|
00-base-contract.md |
Role, ethics, GDPR/ISMS, AI-FIRST, session and PR boundaries |
01-bash-and-shell-safety.md |
Safe shell patterns and command discipline |
02-mcp-access.md |
MCP inventory and health gates |
03-data-download.md |
Data download and manifest rules |
04-analysis-pipeline.md |
23-artifact production and Pass 1/Pass 2 methodology |
05-analysis-gate.md |
Single blocking gate before article generation |
06-article-generation.md |
Aggregate + render contract |
07-commit-and-pr.md |
Stage, commit and exactly one PR |
The interpellation workflow documents a compressed single-run budget:
| Window | Phase |
|---|---|
| 0–2 min | MCP pre-warm and network diagnostics |
| 2–5 min | Download data and catalogue source documents |
| 5–15 min | Analysis Pass 1, all 23 artifacts plus per-document files |
| 15–21 min | Analysis Pass 2, read back and improve |
| 21–22 min | Analysis gate |
| 22–24 min | Aggregate article.md and render EN/SV HTML |
| 24–28 min | Stage, commit and create exactly one PR |
This budget exists because the safeoutputs MCP session may expire after approximately 30–35 minutes of idle time. The workflow explicitly prefers scope compression over skipping Pass 2.
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#0A66C2","primaryTextColor":"#ffffff","primaryBorderColor":"#003B73","lineColor":"#00D9FF","secondaryColor":"#1A1E3D","tertiaryColor":"#FFBE0B","tertiaryTextColor":"#000000","errorBkgColor":"#D32F2F","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart LR
I["Untrusted public inputs<br/>Riksdag docs · speeches · webpages"] --> S["System prompt + modules<br/>ethical and evidence rules"]
S --> R["Read-only AI job<br/>limited GitHub permissions"]
R --> T["Allowed tools<br/>MCP · bash · GitHub read"]
T --> O["Structured outputs<br/>analysis artifacts + safe PR request"]
O --> V["Gate + review surface<br/>analysis gate · PR diff · CI"]
V --> P["Published static site<br/>S3/CloudFront · GitHub Pages DR"]
style I fill:#D32F2F,color:#ffffff
style S fill:#1565C0,color:#ffffff
style R fill:#2E7D32,color:#ffffff
style T fill:#7B1FA2,color:#ffffff
style O fill:#FF9800,color:#000000
style V fill:#FFC107,color:#000000
style P fill:#0A66C2,color:#ffffff
| Provider | Use in article generation | Interface |
|---|---|---|
| Riksdag/Regering MCP | MPs, documents, speeches, votes, interpellations, propositions, motions, committee reports, government documents | .github/copilot-mcp.json + workflow mcp-servers |
| SCB | Swedish-specific statistics and demographic context | @jarib/pxweb-mcp@2.0.0 |
| IMF | Primary economic/fiscal/monetary/external-sector/trade context, with WEO/FM projection vintage discipline and economic-data.json provenance |
tsx scripts/imf-fetch.ts + scripts/imf-client.ts |
| Statskontoret | Swedish agency governance, administrative capacity, implementation feasibility, regulatory burden and public-sector efficiency evidence | Public web pages / reports (www.statskontoret.se) |
| World Bank | Non-economic residue only: governance, environment, social/education, defence historicals, crime | worldbank-mcp@1.0.1 |
| GitHub | PR creation and repository metadata | GitHub MCP / safe outputs |
The authoritative IMF-first / World-Bank-residue split is defined in .github/aw/ECONOMIC_DATA_CONTRACT.md. In short: macroeconomic, fiscal, monetary, external-sector and trade claims are IMF-first; World Bank is reserved for governance, environment and other non-economic residue that IMF does not publish.
Statskontoret is not an MCP server. It is a public-source enrichment layer for agency capacity and implementation feasibility. Workflow allowlists include www.statskontoret.se / statskontoret.se; source use is recorded in data-download-manifest.md and cited in the affected analysis artifacts.
Every analytical claim must tie to at least one of:
- A real
dok_idsuch asHD10447. - A named MP, minister, party, committee or actor.
- Vote counts or voting records.
- A primary-source URL from
riksdagen.se,regeringen.se,scb.se,statskontoret.se, IMF or World Bank non-economic endpoints.
The sample interpellation article demonstrates this standard:
| Claim type | Example in 2026-04-24/interpellations/article.md |
|---|---|
dok_id |
HD10447 links to https://data.riksdagen.se/dokument/HD10447.html |
| Named actors | Patrik Lundqvist (S), Ebba Busch (KD), Elisabeth Svantesson (M) |
| Count | 12 of 16 interpellations in the HD10428–HD10447 window were S-filed |
| Forward trigger | Ministerial answer window 2026-05-07 |
| Confidence | MEDIUM / HIGH / LOW-MEDIUM labels and Admiralty A2 markers |
Article generation is governed by:
analysis/methodologies/artifact-catalog.mdanalysis/methodologies/per-artifact-methodologies.mdanalysis/methodologies/ai-driven-analysis-guide.mdanalysis/methodologies/political-style-guide.mdanalysis/methodologies/osint-tradecraft-standards.md- Supporting methods for classification, SWOT, risk, threat, synthesis, structural metadata, strategic extensions, electoral/domain analysis and per-document analysis.
The pipeline requires at least two complete iterations:
| Pass | Required work | Quality effect |
|---|---|---|
| Pass 1 — Create | Produce all 23 core artifacts and all per-document files. | Establishes coverage and first analytical structure. |
| Snapshot | Save Pass-1 drafts under pass1/ for gate evidence. |
Provides proof that Pass 2 changed the analysis. |
| Pass 2 — Improve | Read every Pass-1 file back completely and strengthen evidence, diagrams, uncertainty, stakeholders and forward indicators. | Converts shallow first drafts into publication-quality intelligence. |
The aggregator deliberately strips trailing Pass 2 process sections from public articles, so Pass-2 improvements must be integrated into the actual analytical sections.
Every content workflow produces the same 23 artifacts under analysis/daily/$ARTICLE_DATE/$SUBFOLDER/.
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#7B1FA2","primaryTextColor":"#ffffff","primaryBorderColor":"#4A148C","lineColor":"#FFBE0B","secondaryColor":"#1565C0","secondaryTextColor":"#ffffff","tertiaryColor":"#2E7D32","tertiaryTextColor":"#ffffff","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart TB
subgraph A["📘 Family A — Core Synthesis (9)"]
A1[README.md]
A2[executive-brief.md]
A3[synthesis-summary.md]
A4[significance-scoring.md]
A5[classification-results.md]
A6[swot-analysis.md]
A7[risk-assessment.md]
A8[threat-analysis.md]
A9[stakeholder-perspectives.md]
end
subgraph B["📗 Family B — Structural Metadata (2)"]
B1[data-download-manifest.md]
B2[cross-reference-map.md]
end
subgraph C["📙 Family C — Strategic Extensions (5)"]
C1[scenario-analysis.md]
C2[comparative-international.md]
C3[devils-advocate.md]
C4[intelligence-assessment.md]
C5[methodology-reflection.md]
end
subgraph D["📕 Family D — Electoral & Domain Lenses (7)"]
D1[election-2026-analysis.md]
D2[voter-segmentation.md]
D3[coalition-mathematics.md]
D4[historical-parallels.md]
D5[media-framing-analysis.md]
D6[implementation-feasibility.md]
D7[forward-indicators.md]
end
subgraph E["📒 Family E — Per document"]
E1[documents/{dok_id}-analysis.md]
end
A --> G["🚦 Analysis Gate"]
B --> G
C --> G
D --> G
E --> G
style A fill:#7B1FA2,color:#ffffff
style B fill:#1565C0,color:#ffffff
style C fill:#FF9800,color:#000000
style D fill:#2E7D32,color:#ffffff
style E fill:#00897B,color:#ffffff
style G fill:#D32F2F,color:#ffffff
The mapping below is exhaustive and ordered to match scripts/render-lib/aggregator/order.ts:AGGREGATION_ORDER so the table reads top-to-bottom in the same sequence the aggregator splices the artifacts into article.md. The order follows a journalist-optimal narrative arc — Phase A (lead/BLUF) → Phase B (per-document evidence injected after significance-scoring.md) → Phase C (actors & political arithmetic) → Phase D (forward trajectory) → Phase E (risk & threat) → Phase F (context & narrative environment, with media framing late) → Phase G (devil's-advocate critique) → Phase H (audit appendix). Every production template under analysis/templates/ appears in the table; supplementary / operational templates (analysis-index, session-baseline, cross-run-diff, cross-session-intelligence, mcp-reliability-audit, reference-analysis-quality, workflow-audit) are listed at the end because they support the analysis run rather than the article body.
| # | Aggregator slot | Template / artifact | Role in eventual article |
|---|---|---|---|
| 1 | Lead | executive-brief.md |
Supplies article title, meta description, BLUF, supported decisions, top forward trigger, lead visual; first BLUF paragraph becomes the <meta description>. |
| 2 | Synthesis | synthesis-summary.md |
Sets lead story, DIW ranking, narrative frame and article metadata suggestions; carries ≥ 1 colour-coded Mermaid diagram. |
| 3 | Key Judgments | intelligence-assessment.md |
Provides ≥ 3 Key Judgments + PIRs + confidence labels (ICD 203). |
| 4 | Ranking | significance-scoring.md |
DIW scoring + sensitivity analysis with evidence-tagged rows and ranked Mermaid bar chart. |
| — | Per-document | per-file-political-intelligence.md → documents/{dok_id}-analysis.md |
Phase B primary-source evidence: one subsection per primary-source document, injected immediately after significance-scoring.md so readers meet the actual motions / propositions / committee reports BEFORE any interpretive lens. Required to cite the dok_id. |
| 5 | Stakeholders | stakeholder-impact.md → stakeholder-perspectives.md |
Power × interest × position lens, named actors and influence network. Note: the template instructs saving as stakeholder-impact.md but the aggregator + gate require the canonical output filename stakeholder-perspectives.md. |
| 6 | Coalitions | coalition-mathematics.md |
Sainte-Laguë seat table + coalition graphs. Phase C — who can pass it. |
| 7 | Voters | voter-segmentation.md |
SCB segment cuts + segment Mermaid map. Phase C — whose interests are at stake. |
| 8 | Forward look | forward-indicators.md |
≥ 10 dated watch items across 4 horizons (Mermaid Gantt). |
| 9 | Futures | scenario-analysis.md |
≥ 3 scenarios with priors, posteriors, indicators and falsifiers. |
| 10 | Election | election-2026-analysis.md / election-cycle-analysis.md / election-2026-implications.md |
Seat deltas, campaign implications; alias-deduplicated by FILENAME_ALIASES (all three map to the same aggregator slot). |
| 11 | Cycle | cycle-trajectory.md |
24th artifact for news-election-cycle only — multi-year trajectory bands T+1y → T+5y with colour-coded Mermaid. |
| 12 | Calendar | parliamentary-season.md |
Riksmöte phase ribbon, committee schedule, Lagrådet referrals, watchlist heat-map (long-horizon workflows). |
| 13 | Risk | risk-assessment.md |
Top 5 risks with L × I + cascading + Mermaid heat-map. |
| 14 | Strategic | swot-analysis.md |
Evidence-bound SWOT quadrant + TOWS moves (Mermaid quadrantChart). |
| 15 | Quant SWOT | quantitative-swot.md |
Scored SWOT ranking with composite Mermaid bar (year-ahead + cycle blocking). |
| 16 | Threat | threat-analysis.md |
Political Threat Taxonomy + attack-tree Mermaid diagrams. |
| 17 | STRIDE | political-stride-assessment.md |
STRIDE + MITRE ATT&CK mapping (cycle blocking). |
| 18 | Black swans | wildcards-blackswans.md |
Wildcards + 3-order cascades (year-ahead + cycle blocking). |
| 19 | PESTLE | pestle-analysis.md |
6-dimension scan (year-ahead + cycle blocking, supplementary elsewhere). |
| 20 | History | historical-parallels.md |
≥ 2 historical episodes with confidence and divergence. |
| 21 | Comparative | comparative-international.md |
≥ 2 peer-country rows via WB / IMF / SCB. |
| 22 | Feasibility | implementation-feasibility.md |
Actor-capacity + Statskontoret evidence + timeline. |
| 23 | Narrative contestation | media-framing-analysis.md |
DISARM TTPs · CIB ABCDE · narrative-laundering chain · Outlet Bias Audit · L1–L5 counter-resilience ladder. Phase F — placed late so readers form their own view of substance before being shown how the story is being framed. |
| 24 | Devil's Advocate | devils-advocate.md |
≥ 3 ACH hypotheses, KAC, Red Team. |
| 25 | Classification | political-classification.md → classification-results.md |
Priority tiers, retention, 7-dimension classification. |
| 26 | Cross-refs | cross-reference-map.md |
Continuity contracts + sibling folders. |
| 27 | PIR roll-forward | horizon-pir-rollforward.md |
PIR genealogy graph for long-horizon runs (supplementary). |
| 28 | Methodology | methodology-reflection.md |
ICD 203 audit, ≥ 10 SATs, DIW reconciliation, PIR retirement log, Pass-2 audit log. |
| 29 | Manifest | data-download-manifest.md |
Collection transparency / source inventory (appendix). |
| op | Run navigator | analysis-index.md |
Read-me-first run index (aggregated alphabetically after core slots). |
| op | Same-type baseline | session-baseline.md |
30-day baseline for pattern recognition (aggregated alphabetically after core slots). |
| op | Day-over-day | cross-run-diff.md |
Same-type delta; gate-required when ANALYSIS_RUN_COUNT ≥ 2 (aggregated alphabetically after core slots). |
| op | Cross-session | cross-session-intelligence.md |
Week / month / quarter aggregation across Riksdag sessions (aggregated alphabetically after core slots). |
| op | MCP health | mcp-reliability-audit.md |
MCP tool reliability snapshot (aggregated alphabetically after core slots). |
| op | Threshold audit | reference-analysis-quality.md |
Threshold audit against reference-quality-thresholds.json (aggregated alphabetically after core slots). |
| op | Workflow audit | workflow-audit.md |
End-to-end run audit: timing, cost, gate outcomes (aggregated alphabetically after core slots). |
Aggregation behavior for "op" templates: The aggregator (step 4 in
aggregate.ts) appends any remaining*.mdfile — excludingREADME.mdandarticle*.md— in alphabetical order after theAGGREGATION_ORDERpass (31 entries including alias variants that collapse to 29 logical sections). This means operational templates listed above will appear in the rendered article if they exist in the analysis folder. They sit after the core narrative but before the Sources appendix.analysis/templates/README.mdis the templates-directory index and is excluded by theREADME.mdfilter;election-cycle-analysis.mdis a filename alias ofelection-2026-analysis.md(de-duplicated at render time byFILENAME_ALIASES).
The single article-generation gate is .github/prompts/05-analysis-gate.md. If the gate fails, the analysis must be fixed before aggregation.
| Check | What it protects |
|---|---|
| Artifact existence | Prevents partial articles from incomplete analysis folders. |
| Per-document coverage | Ensures every dok_id in the manifest has a corresponding document analysis. |
| No stubs | Blocks AI_MUST_REPLACE, [REQUIRED], TODO: and placeholder text. |
| Evidence citations | Blocks generic SWOT/ranking claims without dok_id or primary URL evidence. |
| Mermaid diagrams | Requires color-coded diagrams in core synthesis and key lens files. |
| Pass-2 evidence | Requires proof that the AI read and improved the first pass. |
| Family C structure | Requires BLUF, decisions, Key Judgments, PIRs, scenarios, ACH hypotheses and ICD 203 audit. |
| Family D structure | Requires dated forward indicators and coalition/seat-count material. |
The HTML article is a pure projection. If the analysis is weak, the article will be weak. The gate therefore enforces quality at the source of truth: the analysis artifacts.
| File | Responsibility |
|---|---|
scripts/aggregate-analysis.ts |
CLI wrapper for aggregating one folder or all folders. |
scripts/render-lib/aggregator.ts |
Deterministic logic for ordering, reader-guide insertion, cleaning, linking and front matter. |
scripts/render-lib/url-helpers.ts |
GitHub blob/tree URL construction. |
scripts/render-lib/constants.ts |
Shared paths, base URLs and language constants. |
npx tsx scripts/aggregate-analysis.ts \
--date 2026-04-24 \
--subfolder interpellationsFor all existing analysis folders:
npx tsx scripts/aggregate-analysis.ts --all| Input | Output |
|---|---|
Canonical analysis .md files in analysis/daily/$ARTICLE_DATE/$SUBFOLDER/ excluding README.md, article.md, and article.<lang>.md |
analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md |
analysis/daily/$ARTICLE_DATE/$SUBFOLDER/documents/*.md |
Included under ## Per-document intelligence |
Supplementary .md files in the subfolder excluding README.md, article.md, and article.<lang>.md |
Appended after the canonical sequence |
Note:
README.mdis required for the 23-artifact analysis gate and repository readability, but it is intentionally not aggregated into the publishedarticle.md. Existingarticle.mdandarticle.<lang>.mdfiles are also excluded from aggregation.
AGGREGATION_ORDER in scripts/render-lib/aggregator.ts publishes sections in this order:
- Generated
Reader Intelligence Guide— a deterministic navigation layer that surfaces BLUF, Key Judgments, significance, media framing, forward indicators, scenarios, risks and dok_id-level evidence before the technical appendix. executive-brief.mdsynthesis-summary.mdintelligence-assessment.mdsignificance-scoring.mdmedia-framing-analysis.mdstakeholder-perspectives.mdforward-indicators.mdscenario-analysis.mdrisk-assessment.mdswot-analysis.mdthreat-analysis.mddocuments/*-analysis.mdas## Per-document intelligenceelection-2026-analysis.mdcoalition-mathematics.mdvoter-segmentation.mdcomparative-international.mdhistorical-parallels.mdimplementation-feasibility.mddevils-advocate.mdclassification-results.mdcross-reference-map.mdmethodology-reflection.mddata-download-manifest.md- Remaining supplementary
.mdfiles, alphabetically.
The aggregator (see scripts/render-lib/aggregator.ts cleanArtifactBody):
- Requires
executive-brief.md. - Inserts a
Reader Intelligence Guidebefore artifact sections so public readers can find high-value analysis such as media framing and forward indicators without scanning every audit artifact. - Strips YAML front matter from each artifact.
- Removes the first H1 from each artifact and injects its own consistent
## Section Titleheading. - Demotes every internal heading by one level (
##→###,###→####, …, capped at H6) before concatenation. Without this, every artifact's own H2s become siblings of the wrapper-injected## Section Titleand the rendered article ends up with ~170 H2s and a flat outline that violates WCAG 2.4.6 ("Headings and Labels"). Headings inside fenced code blocks are not affected. Tested bytests/render-lib.test.ts > demoteHeadings. - Strips legacy
_Source: file.md_italic preamble lines that some artifact templates author at the top of their body. Source attribution now lives in the auto-generated Reader Intelligence Guide and the## Article Sourcesappendix — repeating it under every heading reads like a folder listing, not journalism. Inline prose mentions like "primary source: data.riksdagen.se/…" are preserved. - Normalises heading slugs to drop leading hyphens emitted by
github-sluggerwhen a heading starts with a stripped character (e.g. emoji like🎯in## 🎯 BLUFslug to-blufand would otherwise becomeid="rm--bluf"once therm-prefix is applied). Bothmarkdown.ts#rehypeSlugWithPrefixandaggregator.ts#anchorForTitlecollapse leading/trailing hyphens to keep heading IDs and Reader Intelligence Guide anchors in lock-step. - Removes leading admin bylines such as
Author,Run ID,Classification,Confidence,Prepared by,Methodologyand similar metadata fields. - Removes trailing
Document control,Audit trail,Generated by, template footer andPass 2self-audit sections. - Rewrites relative Markdown links to absolute GitHub blob URLs.
- Keeps Mermaid fences untouched so the renderer can preserve them.
- Annotates each section heading with an HTML comment of shape
<!-- source: <file> :: <github-blob-url> -->for offline auditors. The comment is dropped byrehype-sanitizeso it never reaches rendered HTML. - Builds front matter with
title,description,date,subfolder,slug,source_folder,generated_at,languageandlayout.
After every artifact section the aggregator emits a single ## Article Sources H2 at the very end of the article. Each entry is a markdown list link to the artifact on GitHub:
## Article Sources
Each section above projects one analysis artifact. The full audited markdown is available on GitHub:
- [`executive-brief.md`](https://github.com/Hack23/riksdagsmonitor/blob/main/analysis/daily/.../executive-brief.md)
- [`synthesis-summary.md`](https://github.com/.../synthesis-summary.md)
- …This replaces the legacy per-section _Source: file.md_ italics. Auditors get one canonical list; readers see clean prose; SEO crawlers see one trustworthy <ul> of primary-source links instead of 25+ duplicated italics.
article.md metadata comes from executive-brief.md:
| Metadata field | Source logic |
|---|---|
title |
First H1 in executive-brief.md, cleaned of boilerplate/date; fallback to BLUF-derived title; fallback to $SUBFOLDER — $DATE. |
description |
Prefer the first paragraph after a BLUF heading; fallback to first prose paragraph; sentence-aware truncation. |
slug |
$ARTICLE_DATE-$SUBFOLDER. |
source_folder |
analysis/daily/$ARTICLE_DATE/$SUBFOLDER. |
The sample file begins with:
---
title: "Interpellation Debates"
description: "A single new interpellation (HD10447, S) was announced today, forcing Energy- och näringsminister Ebba Busch (KD) to defend the 2024 abolition of the high-sick-pay-cost reimbursement by 2026-05-07."
date: 2026-04-24
subfolder: interpellations
slug: 2026-04-24-interpellations
source_folder: analysis/daily/2026-04-24/interpellations
generated_at: 2026-04-24T18:27:52.276Z
language: en
layout: article
---It then emits deterministic sections such as ## Executive Brief, ## Synthesis Summary, ## Intelligence Assessment — Key Judgments, ## Significance Scoring, and so on. Source attribution is provided by the auto-generated ## Reader Intelligence Guide (top of article) and ## Article Sources appendix (bottom of article); the per-section heading carries an HTML comment for offline auditors:
## Executive Brief
<!-- source: executive-brief.md :: https://github.com/Hack23/riksdagsmonitor/blob/main/analysis/daily/2026-04-24/interpellations/executive-brief.md -->
### 🎯 BLUF
…artifact body content, with all internal headings demoted by one level so the outline stays semantically nested…The generated first body section is ## Reader Intelligence Guide, which is intentionally not sourced to a single artifact because it is a deterministic navigation projection of the artifact set.
Every aggregated analysis/daily/$DATE/$SUBFOLDER/article.md is checked by scripts/validate-article.ts — a hard, scripted CI gate that fails the build on any of the following violations:
| Rule code | What it blocks | Why it matters |
|---|---|---|
unresolved-placeholder |
[REQUIRED:…], AI_MUST_REPLACE, <insert …>, TBD:, FILL IN strings surviving Pass-2 |
Templates carry these markers on disk; if they reach article.md the AI agent skipped a substitution. Article is not publishable. |
missing-reader-guide |
Article missing ## Reader Intelligence Guide |
Aggregator-generated; if missing, the aggregator broke. |
missing-executive-brief |
Article missing ## Executive Brief H2 |
Required artifact malformed. |
missing-bluf |
No BLUF heading anywhere |
Editorial product cannot ship without a Bottom-Line-Up-Front. |
missing-sources-appendix |
Article missing ## Article Sources |
Aggregator-generated; if missing, re-aggregate. |
bluf-too-short |
BLUF prose < 80 chars | Stub BLUFs (e.g. TODO, pending) escape Pass-2. A publishable BLUF needs actor + active verb + object + when + so-what. |
bluf-too-long |
BLUF prose > 1200 chars | Runaway dumps belong in Synthesis Summary or Intelligence Assessment, not the 60-second read. |
empty-heading-slug |
Any heading whose permissive slug is empty (e.g. emoji-only) | Empty #anchor would break the Reader Intelligence Guide and SERP deep-links. |
per-doc-missing-dok_id |
Any ### HD…/### FiU… per-document subsection lacking at least one dok_id-style code in its body |
Every per-document subsection must trace to a primary-source identifier; orphan sections are blocked. |
Run locally:
# Validate every aggregated article in the repo:
npm run validate-article
# Validate a single article:
npx tsx scripts/validate-article.ts analysis/daily/2026-04-24/interpellations/article.mdThe validator is wired into npm run validate-all and runs as a hard CI gate after aggregation. It is content-only — structural projections (heading demotion, source-preamble stripping, slug normalisation) are unit-tested in tests/render-lib.test.ts; this script guards the AI-authored contribution: the artifact contents that the aggregator concatenates.
| File | Responsibility |
|---|---|
scripts/render-articles.ts |
CLI wrapper that locates article.md, auto-aggregates if needed, and renders target languages. |
scripts/render-lib/markdown.ts |
Markdown → sanitized HTML pipeline. |
scripts/render-lib/article.ts |
Parses front matter, renders body, builds JSON-LD and source footer. |
scripts/render-lib/chrome.ts |
Shared HTML head/header/footer, language switcher, SEO and compliance links. |
npx tsx scripts/render-articles.ts \
--date 2026-04-24 \
--subfolder interpellations \
--lang en,svFor all existing articles:
npx tsx scripts/render-articles.ts --all --lang en,svscripts/render-lib/markdown.ts processes article Markdown through:
remark-parseremark-gfmremark-rehypewith controlled raw HTML handlingrehype-rawrehype-slugrehype-autolink-headingsrehype-sanitizerehype-stringify
The sanitizer deliberately allows only the extra attributes needed for Mermaid blocks and heading anchors. It does not allow inline <script>, javascript: URLs, <iframe> or arbitrary <style> tags.
For the example article:
news/2026-04-24-interpellations-en.html
news/2026-04-24-interpellations-sv.html
The renderer writes one complete HTML file per requested language.
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#1565C0","primaryTextColor":"#ffffff","primaryBorderColor":"#0A3F7F","lineColor":"#00D9FF","secondaryColor":"#7B1FA2","secondaryTextColor":"#ffffff","tertiaryColor":"#2E7D32","tertiaryTextColor":"#ffffff","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart TB
HTML["<!DOCTYPE html>"] --> HEAD["<head><br/>SEO · Open Graph · Twitter · JSON-LD · hreflang"]
HTML --> BODY["<body class='rm-article-body'>"]
BODY --> SKIP["Skip link"]
BODY --> HEADER["rm-site-header<br/>logo · nav · language switcher"]
HEADER --> SUBNAV["breadcrumb · published date"]
BODY --> MAIN["main#main.rm-article-main"]
MAIN --> ARTICLE["article.rm-article"]
ARTICLE --> ARTICLEHEAD["article header<br/>h1 · dek · date · language · provenance badges"]
ARTICLE --> CONTENT["rm-article-body<br/>sanitized Markdown HTML"]
ARTICLE --> SOURCES["rm-article-sources<br/>links to every artifact"]
BODY --> FOOTER["rm-site-footer<br/>navigation · trust · footer languages"]
FOOTER --> SCRIPTS["Mermaid loader · back-to-top"]
style HEAD fill:#1565C0,color:#ffffff
style HEADER fill:#1A1E3D,color:#ffffff
style CONTENT fill:#7B1FA2,color:#ffffff
style SOURCES fill:#FF9800,color:#000000
style FOOTER fill:#2E7D32,color:#ffffff
The renderer embeds:
| SEO/provenance element | Current implementation |
|---|---|
<title> |
Article title plus — Riksdagsmonitor unless already branded. |
| Meta description | article.md front matter description. |
| Canonical URL | https://riksdagsmonitor.com/news/$DATE-$SUB-$LANG.html. |
| Hreflang | All 14 supported language alternates plus x-default. |
| Open Graph | og:type=article, title, description, URL, locale, image and update timestamp. |
| Twitter card | Summary large image metadata. |
| JSON-LD | NewsArticle with isBasedOn listing every .md / .json source artifact. |
| Article dek and provenance badges | Header-level summary and visible public-source / AI-FIRST / traceability badges. |
| Source footer | Visible Analysis sources section linking source artifacts to GitHub, excluding generated article.md, translated article.<lang>.md and temporary pass1/ snapshots. |
Generated article pages use a dedicated rm-* CSS namespace to avoid collisions with legacy page components. The main styling is in styles.css under Article Pipeline — chrome produced by scripts/render-lib/buildChrome.
| UI element | CSS / HTML support |
|---|---|
| Sticky header | .rm-site-header, .rm-site-header-inner |
| Brand and tagline | .rm-logo, .rm-logo-text, .rm-logo-brand, .rm-logo-tagline |
| Primary navigation | .rm-site-nav |
| Header language switcher | <details class="rm-lang-switcher"> + .rm-lang-switcher-dropdown |
| Decorative hero banner | .hero-banner, .hero-banner-bg; callers can override the image with heroBannerImage |
| Breadcrumb | .rm-breadcrumb inside .rm-site-subnav |
| Article container | .rm-article-main, .rm-article, .rm-article-header, .rm-article-body |
| Source provenance footer | .rm-article-sources, .rm-article-sources-list |
| Site footer | .rm-site-footer, .rm-site-footer-inner, .rm-footer-* |
| Footer language row | .rm-footer-langs, .rm-lang-code |
The multilingual news landing pages (news/index*.html) are generated by scripts/generate-news-indexes/index.ts and styled in styles.css under News Index Visual Upgrade — generated news/index*.html.
| News-index area | Contract |
|---|---|
| Section banner | Uses the shared chrome hero slot with the news-specific decorative asset images/riksdagsmonitornews-banner.webp; the CSS scales it as a full-width editorial banner while preserving alt="", aria-hidden="true" and fixed dimensions for CLS control. |
| Newsroom heading | Renders a semantic <header class="news-page-heading"> with one page <h1>, localized subtitle and a <dl class="news-hero-metrics"> summary for indexed articles, language coverage and latest article date. |
| Filters | Uses a sticky/collapsible <details class="filter-bar-wrapper"> with 44px+ touch targets, URL-synchronized filter state, and a clear-filters affordance only when filters are active. |
| Article cards | Client-side hydration renders <article class="article-card"> cards with wrapped metadata, localized type labels, recency badges, topic/type color accents, safe relative links and filtered tag pills. |
| Topic and tag fallback | parseArticleMetadata infers topics/tags from article metadata, filename and article content so topic filters remain useful even when older article HTML lacks article:tag metadata. |
| Responsive behavior | Mobile defaults to one column with collapsible filters; tablet uses two columns; large screens can expand to a wider four-column editorial grid. RTL pages mirror topic stripes and keep language badges readable with dir="ltr". |
The repository uses CSS custom properties for light and dark color palettes:
:rootdefines the default light-mode accessible palette.@media (prefers-color-scheme: dark)defines dark-mode tokens.html[data-theme="light"]overrides generated article chrome to keep article pages readable in explicit light mode.html[data-theme="dark"]is supported by the site-wide theme bootstrap and dashboard pages.
Theme toggle button (every static landing page and chromed article page) — rendered with two glyphs (☀️ and 🌙) so the button shows what theme will activate on the next press rather than what theme is currently active. CSS hides the inactive glyph based on html[data-theme]:
html[data-theme="light"] .theme-toggle-btn .theme-icon-sun,
html[data-theme="dark"] .theme-toggle-btn .theme-icon-moon {
display: none;
}The icon transition respects prefers-reduced-motion. Aria-label, title, and data-label-{dark,light} are localized through chromeStrings(lang) (themeAria, themeToLight, themeToDark, themeLabel).
The 14 index_*.html landing pages share a single hero block whose content is regenerated on every prebuild by scripts/normalize-static-html-chrome.ts. The script's replaceHero() step rewrites:
| Hero element | Source of truth | Notes |
|---|---|---|
| Theme toggle button | chromeStrings(lang) keys themeAria, themeLabel, themeToLight, themeToDark |
Dual-icon morphing button (.theme-icon-sun + .theme-icon-moon) |
<span class="h1-subtitle"> |
chromeStrings(lang).heroSubtitle |
Renders under <h1>Riksdagsmonitor |
<p class="tagline"> |
chromeStrings(lang).heroTagline |
Editorial summary line |
.election-countdown block |
chromeStrings(lang) keys electionCountdownLabel, electionDateLong |
id="countdown" preserved for runtime JS |
.hero-stats .label (5 stats) |
chromeStrings(lang) keys heroStatPoliticians, heroStatBallots, heroStatDocuments, heroStatBills, heroStatDecisions |
Matched by data-stat-id; numbers stay sourced from CIA stats |
Editing any hero copy means editing scripts/render-lib/chrome-i18n.ts once (per language), not 14 HTML files. The next build (or npx tsx scripts/normalize-static-html-chrome.ts) propagates the change to every variant.
scripts/render-lib/chrome-i18n.ts exports CHROME_I18N: Record<Language, ChromeStrings> and chromeStrings(lang). Every chrome string used by scripts/render-lib/chrome.ts and scripts/normalize-static-html-chrome.ts flows through this table — including the header tagline (headerTagline), hero copy (heroSubtitle, heroTagline, electionCountdownLabel, electionDateLong, heroStat*), theme toggle labels (themeAria, themeToLight, themeToDark, themeLabel), navigation aria-labels (mainNav, breadcrumb, switchLanguage, thisPageInOtherLanguages), CTA copy (transparency*, sponsor*), and footer headings.
The contract is enforced by tests/chrome-i18n-hero.test.ts and tests/render-lib.test.ts:
- Completeness: every language defines every key with a non-empty value.
- Translation discipline: non-English values must differ from English (no copy-paste leaks).
- Render parity:
buildChrome({ lang: 'sv' })emits the Swedish tagline and never the English one.
Article chrome uses cyberpunk tokens such as:
| Token | Typical purpose |
|---|---|
--primary-cyan / fallback #00d9ff |
Article headings, links and borders. |
--primary-magenta / fallback #ff006e |
Hover state and emphasis. |
--primary-yellow / fallback #ffbe0b |
Section headings and source blocks. |
--dark-bg / fallback #0a0e27 |
Article page background. |
--mid-bg / fallback #1a1e3d |
Cards, headers and footer. |
--light-text / fallback #e0e0e0 |
Body text in dark mode. |
Mermaid diagrams are authored directly inside analysis artifacts:
```mermaid
flowchart TB
A[Evidence] --> B[Analysis]
B --> C[Article]
style A fill:#1565C0,color:#ffffff
style B fill:#7B1FA2,color:#ffffff
style C fill:#2E7D32,color:#ffffff
```The rendering path is:
- Markdown contains
```mermaidfences. scripts/render-lib/markdown.tsrewrites them to<pre class="mermaid">before Markdown parsing.rehype-sanitizeallows thepre.mermaidclass.scripts/render-lib/chrome.tsemits an inline imperative bootstrap script that injects a<script type="module" src="/js/lib/mermaid-init.mjs">into<head>at runtime. The DOM-injection pattern is intentional: it bypasses Vite's HTML/script-tag transformer so the loader and the vendored mermaid runtime are not bundled, hashed and re-emitted under/assets/. (The previous static<script type="module" src="…mermaid-init.mjs">pattern caused production 404s like/assets/mermaid.esm.min-XXXX.mjswhenever the pinnedmermaiddevDependency was upgraded between deploys, because Vite would emit a chunk hash that didn't match the file actually deployed to S3.)js/lib/mermaid-init.mjsdynamically imports Mermaid from the same-origin vendored copy underjs/lib/mermaid/(resolved against its ownimport.meta.url), initializes a dark theme and renders all Mermaid blocks after page load.
The same inline bootstrap also injects /js/back-to-top.js (module) and /js/theme-toggle.js (classic, deferred) so the dark/light theme button in the rm-site-header stays functional without going through Vite's bundler. The matching anti-flash bootstrap (html[data-theme] set before first paint) is emitted as an inline <script> in <head> by renderChromeHead.
Single source of truth for runtime JS. The chrome bootstrap injects scripts dynamically from
/js/*.js. Per the HTML spec, dynamically-created<script>tags ignore thedeferattribute, so the runtime modules must self-bootstrap without relying onDOMContentLoaded. Bothjs/theme-toggle.jsandjs/back-to-top.jsare written to that contract. The repo-canonicaljs/tree is therefore deployed verbatim by the Copy JS libraries step (cp -r js/* dist/js/— force-overwrite, sojs/wins over any stale duplicate that Vite may have copied frompublic/js/). Never commit apublic/js/<filename>.jswhose content diverges fromjs/<filename>.js; if you must keep the file underpublic/for Vite's auto-copy to dev (vite preview), keep it byte-identical withjs/.
Hero banner. Every chromed page (article, news index, political-intelligence) inherits the banner slot from
scripts/render-lib/chrome.ts— a<div class="hero-banner">block emitted right after<header class="rm-site-header">with a decorative image (depth-awareprefix,alt="",aria-hidden="true",width=1536/height=1024for CLS). The default image isimages/riksdagsmonitor-banner.webp; section renderers can passheroBannerImagefor a more specific brand asset (the news index usesimages/riksdagsmonitornews-banner.webp). SetheroBanner: falseonBuildChromeOptsfor chrome variants where a full-bleed banner conflicts with the page's own hero (e.g. dashboards).
The Mermaid distribution is vendored at build time:
| Step | Location | What it does |
|---|---|---|
| Pin | package.json devDependencies |
mermaid is pinned (currently 11.4.1) — supply-chain audited like every other dependency, in the npm SBOM. |
| Copy | scripts/copy-vendor-mermaid.ts |
Run as the first step of prebuild (and predev). Copies node_modules/mermaid/dist/mermaid.esm.min.mjs and its required chunks/mermaid.esm.min/*.mjs into js/lib/mermaid/ (≈2.6 MB, 64 files). Sourcemaps, type declarations, mocks and other ESM variants are excluded. |
| Gitignore | .gitignore |
js/lib/mermaid/ is intentionally ignored — the directory is reproducible from the pinned dependency, so we don't commit duplicates of node_modules content. |
| Bundle | .github/workflows/deploy-s3.yml |
The "Copy JS libraries to build output" step merges the full js/ tree (including js/lib/mermaid/) into dist/js/ after the Vite build, alongside chart.umd.4.4.1.js, d3.7.9.0.min.js, etc. |
| Deploy | scripts/deploy-s3.sh |
*.mjs files are uploaded with Content-Type: application/javascript and Cache-Control: public, max-age=31536000, immutable — same long-cache treatment as every other vendored asset. |
| Guard | tests/no-external-cdn.test.ts |
Vitest test that fails CI if any runtime file under js/ or any rendered article under news/ references cdn.jsdelivr.net, cdnjs.cloudflare.com, unpkg.com, esm.sh, cdn.skypack.dev, or ajax.googleapis.com. Riksdagsmonitor serves all JavaScript from its own S3/CloudFront origin — no external CDN allowed. |
CSP impact: scripts can be allowed with script-src 'self' only — no third-party host needs to be added to the policy. SRI hashes for every Mermaid .mjs chunk are produced by vite-plugin-sri-gen because the files now live under the build output.
The analysis gate requires color-coded Mermaid through style directives or Mermaid themeVariables / %%{init} blocks.
Current article Markdown rendering is intentionally static and sanitized. D3 and Chart.js are supported by the broader site and dashboards, not by arbitrary inline article scripts.
| Capability | Current support |
|---|---|
| Chart.js package | Listed in package.json and optimized in vite.config.js. |
| D3 package | Listed in package.json and split into a Vite d3 manual chunk. |
| Dashboard modules | scripts/coalition-dashboard/*, scripts/committees-dashboard/* and shared chart utilities support interactive dashboards. |
| Article chart data | 04-analysis-pipeline.md permits JSON files such as vote-distribution.json, risk-heatmap.json, coalition-math.json, forward-indicators.json, and economic chart data. |
| Article HTML safety | Sanitizer blocks inline scripts. Any future article-level Chart.js/D3 visualization should be implemented as trusted site code that reads artifact JSON, not AI-authored inline JS. |
Important limitation: generated news articles today automatically render Mermaid diagrams and static Markdown tables. They do not automatically instantiate arbitrary D3/Chart.js widgets from Markdown because that would require trusting AI-authored script markup. The supported secure pattern is artifact JSON + trusted site module + accessible fallback.
Recommended future pattern for article-level interactive charts:
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#00897B","primaryTextColor":"#ffffff","lineColor":"#FFBE0B","secondaryColor":"#1565C0","tertiaryColor":"#7B1FA2","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart LR
A["analysis/daily/.../risk-heatmap.json"] --> B["Trusted renderer module<br/>js/article-charts.mjs"]
B --> C["Chart.js / D3 component"]
C --> D["Accessible figure<br/>caption · table fallback · keyboard support"]
style A fill:#1565C0,color:#ffffff
style B fill:#00897B,color:#ffffff
style C fill:#7B1FA2,color:#ffffff
style D fill:#2E7D32,color:#ffffff
This preserves CSP and sanitizer discipline while enabling richer visualizations.
The rendered article chrome supports 14 language alternates:
| Code in URLs | Hreflang | Language |
|---|---|---|
en |
en |
English |
sv |
sv |
Swedish |
da |
da |
Danish |
no |
nb |
Norwegian Bokmål — URLs currently use legacy no; hreflang already uses BCP-47 nb. |
fi |
fi |
Finnish |
de |
de |
German |
fr |
fr |
French |
es |
es |
Spanish |
nl |
nl |
Dutch |
ar |
ar |
Arabic, RTL |
he |
he |
Hebrew, RTL |
ja |
ja |
Japanese |
ko |
ko |
Korean |
zh |
zh |
Chinese |
Norwegian is in a compatibility migration state, not a permanent language-code design decision: generated HTML uses the BCP-47 nb hreflang for Norwegian Bokmål, while existing filenames and URL siblings still use the legacy _no / -no.html pattern for backwards-compatible site output. New code should keep both surfaces in sync until the wider URL migration is completed.
Per-type content workflows render only core languages, normally en,sv. The dedicated translation workflow is:
It consumes already-rendered English/Swedish articles and produces the remaining 12 language variants. This separation prevents every content workflow from trying to translate all languages under the same time and safe-output budget.
The article chrome emits two switchers:
- Header dropdown —
<details class="rm-lang-switcher">withrole="menuitem"links. - Footer language row —
.rm-footer-langs, always visible for discoverability.
The renderer populates hreflang alternates for all languages even when the sibling translated pages are not yet generated. The URLs remain stable and predictable for the translation workflow.
The canonical repository file is .github/workflows/deploy-s3.yml. This is the S3 deployment workflow covered here.
package.json defines the build pipeline:
"prebuild": "npx tsx scripts/aggregate-analysis.ts --all --quiet && npx tsx scripts/render-articles.ts --all --lang en,sv --quiet && npx tsx scripts/generate-news-indexes/index.ts && npx tsx scripts/extract-news-metadata.ts && npx tsx scripts/generate-sitemap-html.ts && npx tsx scripts/generate-political-intelligence.ts && npx tsx scripts/generate-rss.ts && npx tsx scripts/generate-sitemap.ts",
"build": "vite build",
"postbuild": "cp rss.xml dist/rss.xml && cp sitemap.xml dist/sitemap.xml && cp -r cia-data dist/cia-data"This means npm run build regenerates all aggregate/render/index/metadata outputs before Vite compiles the site.
vite.config.js auto-discovers article HTML files under news/:
- It scans
news/recursively. - It registers every non-index
.htmlarticle as a Rollup input. - It ensures new articles are included in
dist/news/and deployed to S3. - It splits Chart.js, D3 and PapaParse into manual chunks for dashboard optimization.
- It uses
vite-plugin-sri-gento generate Subresource Integrity hashes.
| Job | Trigger | Purpose |
|---|---|---|
deploy |
Push to main or manual dispatch with fix_mimetypes=false |
Build and publish the site. |
fix-mimetypes |
Manual dispatch with fix_mimetypes=true |
Repair MIME metadata on existing S3 objects without a full build/deploy. |
The deploy job performs:
step-security/harden-runnerwith egress policyblockand explicit allowed endpoints.- Full checkout with
fetch-depth: 0. - Node.js 25 setup with npm cache.
npm ci.- Guard against broken news article references:
- No
back-to-top.tsreferences in generated article HTML. - No
news-article.jsreferences. - No absolute
/js/lib/script paths in news pages.
- No
npm run build.- Build artifact verification:
dist/dist/index.htmldist/rss.xmldist/sitemap.xmldist/sitemap.html- representative localized sitemaps (
sitemap_sv.html,sitemap_ar.html) - political-intelligence pages in EN/SV/AR representative set
dist/news/
- Copy
docs/todist/docs/if present. - Merge
js/intodist/js/so article dependencies such as Mermaid init and back-to-top are present. - Configure AWS credentials through OIDC (
aws-actions/configure-aws-credentials). - Run
scripts/deploy-s3.shagainstdistand the S3 bucket. - Invalidate CloudFront with
/*.
| File or directory | Generated by | Deployed? | Notes |
|---|---|---|---|
analysis/daily/*/*/article.md |
scripts/aggregate-analysis.ts |
No, unless copied separately; source remains in repo | Canonical Markdown article source. |
news/$DATE-$SUB-$LANG.html |
scripts/render-articles.ts and news-translate.md |
✅ | User-facing articles. |
news/index*.html |
scripts/generate-news-indexes/index.ts |
✅ | News listing pages. |
political-intelligence*.html |
scripts/generate-political-intelligence.ts |
✅ | Political intelligence landing pages. |
rss*.xml |
scripts/generate-rss.ts |
✅ | Copied to dist/ in postbuild. |
sitemap.xml |
scripts/generate-sitemap.ts |
✅ | Copied to dist/ in postbuild. |
sitemap*.html |
scripts/generate-sitemap-html.ts |
✅ | Vite inputs. |
dist/ |
vite build |
✅ | Primary deployment directory. |
dist/js/ |
Vite + deploy workflow merge from js/ |
✅ | Includes js/lib/mermaid-init.mjs. |
dist/docs/ |
deploy workflow copy from docs/ |
✅ | Documentation output when present. |
dist/cia-data/ |
postbuild |
✅ | CIA data copied into build output. |
The rendered HTML source footer and JSON-LD isBasedOn block enumerate source .md and .json files found in the analysis folder. Generated article.md, translated article.<lang>.md, and temporary pass1/ snapshots are excluded so the provenance list stays reader-relevant.
scripts/deploy-s3.sh uploads by extension with explicit MIME types and cache headers. It uses aws s3 cp --recursive for type-specific passes so metadata is corrected even if content is unchanged, then runs a final sync --delete --size-only to remove orphaned objects.
| Extension/type | Content-Type | Cache-Control |
|---|---|---|
.html |
text/html; charset=utf-8 |
public, max-age=3600, must-revalidate |
.css |
text/css |
public, max-age=31536000, immutable |
.js, .mjs |
application/javascript |
public, max-age=31536000, immutable |
Images (.webp, .png, .jpg, .gif, .svg, .ico) |
Explicit image MIME | public, max-age=31536000, immutable |
| Fonts | Explicit font MIME | public, max-age=31536000, immutable |
.xml, .json, .txt, .csv, .webmanifest, .md |
Explicit metadata/data MIME | Usually public, max-age=86400 |
.map, .wasm |
application/json, application/wasm |
Long immutable for maps/wasm |
docs/ |
Explicit per-extension MIME | Mostly public, max-age=86400 |
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#0A66C2","primaryTextColor":"#ffffff","lineColor":"#90CAF9","secondaryColor":"#2E7D32","secondaryTextColor":"#ffffff","tertiaryColor":"#FF9800","tertiaryTextColor":"#000000","errorBkgColor":"#D32F2F","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart TB
A["Push to main / manual dispatch"] --> B["Harden runner<br/>egress block allowlist"]
B --> C["npm ci"]
C --> D["Guard news HTML references"]
D --> E["npm run build<br/>prebuild + Vite + postbuild"]
E --> F["Verify dist artifacts"]
F --> G["Copy docs and js libraries"]
G --> H["AWS OIDC credentials"]
H --> I["scripts/deploy-s3.sh<br/>explicit MIME + cache headers"]
I --> J["CloudFront invalidation /*"]
style B fill:#D32F2F,color:#ffffff
style E fill:#1565C0,color:#ffffff
style F fill:#FF9800,color:#000000
style I fill:#2E7D32,color:#ffffff
style J fill:#0A66C2,color:#ffffff
Per README.md, SECURITY_ARCHITECTURE.md and THREAT_MODEL.md:
| Dimension | Classification |
|---|---|
| Confidentiality | Public |
| Integrity | High |
| Availability | High |
| Privacy | Public-official personal data only, processed for transparency and democratic accountability |
Political opinions are sensitive under GDPR Article 9, but this platform uses public political data about public officials in their official capacity, with public-interest and legitimate-interest grounds. Article generation must remain neutral, proportionate and evidence-based.
| Boundary | Control |
|---|---|
| Public political data → AI context | Prompt hardening, source-integrity rules, evidence standard, no non-public/leaked data. |
| AI analysis → article | Analysis gate, no stubs, Mermaid/evidence/Pass-2 checks, deterministic aggregation. |
| Markdown → HTML | rehype-sanitize, no AI-written HTML scripts, controlled Mermaid handling. |
| Workflow → repository write | Safe outputs and one PR max; AI job has read-only permissions. |
| Build runner → internet | step-security/harden-runner egress allowlist in deploy workflow. |
| GitHub → AWS | OIDC federation, no long-lived AWS keys. |
| S3 → users | CloudFront, TLS, cache headers, invalidation, S3 metadata controls. |
The article pipeline specifically mitigates threats called out in THREAT_MODEL.md:
| Threat | Mitigation in article generation |
|---|---|
| Prompt injection from source content | Prompt modules, no direct tool write, gate before publication. |
| LLM hallucination | Every claim must cite primary evidence; methodology-reflection.md audits evidence sufficiency and ICD 203. |
| Model-generated misinformation | AI-FIRST Pass 2, source diversity, confidence labels and PR review. |
| Data poisoning | Manifest, source URLs, Git-tracked diffs and source footer provenance. |
| XSS / HTML injection | Sanitized Markdown pipeline and no inline AI-authored scripts. |
| Supply-chain risk | Pinned GitHub Actions, npm SBOM, Dependabot, CodeQL, SRI generation. |
- Confirm the workflow (
news-*.md) imports all required prompt modules. - Confirm MCP pre-warm and external endpoint diagnostics completed.
- Confirm public data was downloaded and manifest written.
- Confirm all 23 core artifacts exist.
- Confirm every manifest
dok_idhas adocuments/{dok_id}-analysis.mdfile or documented cluster handling. - Confirm Pass 2 happened and
methodology-reflection.mdrecords audit findings. - Run or verify the
05-analysis-gate.mdinline checks. - Run
aggregate-analysis.tsfor the date/subfolder. - Inspect
article.mdfor title, BLUF, source links and section order. - Run
render-articles.ts --lang en,sv. - Confirm
news/$DATE-$SUB-en.htmlandnews/$DATE-$SUB-sv.htmlexist and contain<article class="rm-article">andrm-article-sources. - Let
news-translate.mdproduce the remaining 12 languages. - Let CI run
validate-news, HTMLHint, build and deployment checks.
ARTICLE_DATE=2026-04-24
SUBFOLDER=interpellations
CORE_LANGUAGES=en,sv
npx tsx scripts/aggregate-analysis.ts \
--date "$ARTICLE_DATE" \
--subfolder "$SUBFOLDER"
npx tsx scripts/render-articles.ts \
--date "$ARTICLE_DATE" \
--subfolder "$SUBFOLDER" \
--lang "$CORE_LANGUAGES"Expected outputs:
analysis/daily/2026-04-24/interpellations/article.md
news/2026-04-24-interpellations-en.html
news/2026-04-24-interpellations-sv.html
This section documents the highest-leverage improvements identified for producing publication-quality political-intelligence articles from the analysis artifact pipeline. Items are organized into three domains: political-intelligence quality, UI/UX of the rendered product, and code quality/architecture.
The current system produces a correct, auditable article. The following improvements would raise its political-intelligence value significantly.
Problem: Generated articles currently expose artifact structure directly to the reader: ## Executive Brief, ## Synthesis Summary, ## Intelligence Assessment — Key Judgments, and so on. Each section is the content of a corresponding source file, pasted in sequence. The reader experiences the article as an annotated folder listing, not a coherent intelligence narrative.
Improvement: Introduce a narrative synthesis step between aggregation and rendering. A trusted synthesis pass should produce a unified article body structured as:
- Lead (lede): one or two punchy sentences naming the most democratically significant finding (DIW-ranked), the principal actor, and the forward trigger.
- Context block: 2–3 sentences of baseline context (coalition status, committee history, relevant legislation).
- Main development: the story, drawing on Key Judgments and per-document analysis.
- Stakes and alternatives: scenario probabilities, SWOT, coalition math, risk scores — woven into the narrative.
- Forward view: dated watch items from
forward-indicators.mdand scenarios. - Technical appendix (optional reader-expand): the full artifact stack for researchers.
This would require the aggregator to emit a clean article-narrative.md (authored in this later synthesis step) that renders in place of the current artifact collage, with the artifact stack demoted to a collapsible appendix.
Problem: AGGREGATION_ORDER is a single static list applied identically to all article types: interpellations, propositions, committee reports, evening analyses, motions, weekly reviews, monthly reviews and forecasting runs.
Improvement: Define a map from article type to a specialized order:
| Article type | Priority lenses |
|---|---|
interpellations |
executive-brief → intelligence-assessment → forward-indicators (ministerial deadline) → stakeholder-perspectives → … |
propositions |
executive-brief → synthesis-summary → implementation-feasibility → coalition-mathematics → scenario-analysis → … |
committee-reports |
executive-brief → synthesis-summary → significance-scoring → stakeholder-perspectives → swot-analysis → … |
evening-analysis |
executive-brief → intelligence-assessment → media-framing-analysis → forward-indicators → … |
motions |
executive-brief → coalition-mathematics → voter-segmentation → scenario-analysis → … |
forecasting |
executive-brief → scenario-analysis → coalition-mathematics → election-2026-analysis → forward-indicators → … |
The aggregateAnalysis function should accept an optional articleType parameter and select the appropriate order from a type map.
Problem: The article description and article header dek are extracted from the first prose paragraph of executive-brief.md (after stripping admin bylines). This is often a summary sentence, not a proper lede.
Improvement: Introduce a dedicated lede field contract for executive-brief.md: the file should contain a ## 🎯 BLUF section whose first sentence is a single tight lede in the form "Who did what to whom, and by when?" The aggregator should prefer this sentence for description and dek, and the analysis gate should enforce that it is present, names at least one real actor with their title, and includes one dok_id or date trigger.
Problem: Confidence labels (HIGH, MEDIUM, LOW) and Admiralty source codes (A2, B3, etc.) appear as plain inline text throughout the articles. Readers must know what these mean.
Improvement:
- Render
CONFIDENCE: HIGH/MEDIUM/LOWspans as styled<mark class="confidence-high|medium|low">chips via a post-processing pass inmarkdown.ts. - Render Admiralty codes
A1–F6as<abbr title="Source reliability A — Completely reliable; Information credibility 1 — Confirmed by other sources">A1</abbr>. - Add a once-per-page methodology note in the article footer explaining the confidence and Admiralty schema.
- The styled chips should work in both light and dark mode.
Problem: media-framing-analysis.md is currently appended as section 5. Its value is highest when readers encounter framing alerts near the finding they apply to, not as a separate section they may never reach.
Improvement: Allow media-framing-analysis.md to emit a ## 🔎 Framing Alerts subsection per key story that can be referenced inline. The aggregator could inject "framing callout" anchors into the main narrative at matching dok_id boundaries. This is a step toward the integrated narrative article model above.
Problem: forward-indicators.md is a Markdown table. Tables work, but for a time-ordered set of dated watch items, a visual timeline communicates urgency more effectively.
Improvement: The analysis agent should emit a forward-indicators.json alongside forward-indicators.md with structured events ({"date": "2026-05-07", "title": "...", "type": "deadline|vote|publication", "probability": 0.75, "dok_id": "HD10447"}). A trusted site module (js/article-forward-indicators.mjs) would render this as a responsive horizontal timeline using the existing Chart.js build chunk. The Markdown table remains as the accessible fallback.
Problem: historical-parallels.md appears late (position 17) in the current order and tends to be read by researchers only, despite containing high-value context for general readers.
Improvement: The synthesis narrative (improvement #1) should weave the single most relevant historical parallel into the context block of the article lead. The full historical-parallels.md section would remain in the technical appendix.
Problem: There is no editorial continuity between articles on the same topic. An interpellation today has no link to the committee hearing next week or the vote result the month after.
Improvement: Introduce a related_articles field in article.md front matter listing prior/subsequent articles in the same legislative sequence. The article chrome renderer should emit a <nav class="rm-article-series"> block. Population could initially be manual or auto-populated by a script that scans analysis/daily/ for shared dok_ids.
Problem: Long-form articles (committee reports, monthly reviews) may produce 8,000–15,000-word HTML pages. Readers on mobile cannot quickly reach the section they need.
Improvement: Generate a sticky in-article table of contents from the aggregated H2/H3 heading structure. This should be:
- Implemented as a
<nav class="rm-article-toc" aria-label="Article sections">block injected byarticle.tsbefore the article body. - Collapsed on mobile (max-width 768px) with an accessible expand toggle.
- Sticky on desktop, highlighting the current section via
IntersectionObserverin a trusted site script.
Problem: All article types (interpellations, propositions, evening-analysis, weekly-review, etc.) render identically. A reader cannot distinguish a fast realtime monitor from a deep monthly review at a glance.
Improvement:
- Add an
article_typefield toarticle.mdfront matter. - Render a prominent
<span class="rm-article-type-badge rm-badge--${type}">in the article header. - Define per-type accent colors in
styles/themes/article-types.css: e.g. cyan for interpellations, yellow for committee reports, magenta for evening-analysis. - Add a matching type-icon via the existing cyberpunk design token set.
Problem: The article site header (logo, nav) takes up vertical space when scrolled. There is no reading-progress indicator.
Improvement:
- Add a
position: sticky; top: 0article header band that collapses to show only the article title and type badge after scrolling past the full header. - Add a thin reading-progress bar (
<div class="rm-reading-progress">) at the very top of the viewport, driven by a minimal trustedjs/reading-progress.mjs.
Problem: risk-assessment.md and significance-scoring.md produce Markdown tables. For high-dimensional data (risk score × risk type × party), a heat-map panel communicates at a glance.
Improvement: The analysis pipeline should emit risk-heatmap.json alongside risk-assessment.md. A trusted js/article-risk-heatmap.mjs renders a CSS-grid-based heat map with keyboard navigation and WCAG 2.1 AA contrast. The Markdown table remains as the accessible fallback.
Problem: Political science researchers and journalists need to cite specific claims from Riksdagsmonitor articles.
Improvement: Add a copy-citation button (<button class="rm-cite-btn" aria-label="Copy citation">) to each article section heading. On click, it copies a formatted citation to the clipboard:
Riksdagsmonitor. (2026-04-24). Interpellation Debates: HD10447 – Sjuklönekostnad.
https://riksdagsmonitor.com/news/2026-04-24-interpellations-en.html#intelligence-assessment-key-judgments
Retrieved: 2026-04-25.
This should be implemented as a minimal trusted site script, not inline AI-authored JS.
Problem: Light mode for article pages applies html[data-theme="light"] overrides but the default :root palette and the cyberpunk token fallbacks are tuned for dark backgrounds. Key elements (article header gradient, source footer) remain dark in light mode.
Improvement: Audit every rm-* CSS rule for contrast and background in light mode. Introduce a fully specified light-mode palette in the :root block so light-mode article pages are as polished as dark ones. Target 4.5:1 contrast ratio for all text throughout.
Problem: No built-in mechanism for sharing a specific finding or annotating a specific paragraph.
Improvement:
- Add per-heading deep-link buttons already surfaced by
rehype-autolink-headings; make them visible on hover. - Add a minimal share API call (
navigator.share) with clipboard fallback for mobile. - Integrate with a future annotation layer (out of scope for current sprint, but reserve the
rm-annotation-*CSS namespace).
Problem: aggregateAnalysis() accepts only date and subfolder. The article type (interpellations, propositions, etc.) is inferred from the subfolder name informally.
Improvement:
- Introduce an
ArticleTypeenum inrender-lib/constants.ts. - Add an
articleTypeparameter toaggregateAnalysis()(optional, defaults to heuristic inference from subfolder name). - Add a
AGGREGATION_ORDER_BY_TYPE: Record<ArticleType, readonly string[]>map. - Test coverage: one test per article type verifying that the correct order is applied and that
media-framing-analysis.mdandforward-indicators.mdprecedeswot-analysis.mdin all types.
Status: Fixed in 2026-04-25 commit. The @module JSDoc now correctly documents the reader-intelligence-first narrative order with all five rounds.
Problem: Current render-lib tests are unit and integration tests. There are no visual regression or accessibility tests for the full rendered article page.
Improvement: Add Playwright test specs covering:
- Article page loads without JS console errors.
<article class="rm-article">is present and has accessible heading structure (h1>h2>h3).- Language switcher dropdown opens and lists correct language codes.
- Mermaid blocks receive the
mermaidclass and are non-empty. rm-article-sourcessection exists and at least one GitHub link is present.- WCAG 2.1 AA axe-core scan passes (no critical violations).
- Light mode and dark mode color contrast requirements (via
@axe-core/playwright).
Problem: tests/render-lib.test.ts primarily tests EN articles. There is no direct test for RTL article rendering (Arabic ar, Hebrew he).
Improvement: Add test cases verifying:
dir="rtl"is set on<html>for Arabic and Hebrew.- RTL articles have
<link rel="alternate" hreflang="ar">and<link rel="alternate" hreflang="he">. - The language switcher dropdown does not contain
ar/hewhen the current language is AR/HE.
Problem: The analysis gate (05-analysis-gate.md) is enforced by the AI agent at runtime. There is no static CI gate that checks rendered article quality after npm run build.
Improvement: Add a validate-articles npm script and corresponding GitHub Actions job that checks every file in news/:
# Minimum article quality checks
for f in news/*.html; do
grep -q '<meta name="description"' "$f" || echo "FAIL: no description in $f"
grep -q '"NewsArticle"' "$f" || echo "FAIL: no JSON-LD in $f"
grep -q 'rm-article-sources' "$f" || echo "FAIL: no sources section in $f"
grep -q 'hreflang="x-default"' "$f" || echo "FAIL: no x-default hreflang in $f"
doneThis should be a failing CI gate, not just a warning.
Problem: tests/render-lib-architecture.test.ts verifies that every exported symbol in each leaf module is re-exported from the barrel. But if a developer adds a new leaf module without adding it to the barrel, the test does not catch the omission.
Improvement: Add a test that scans scripts/render-lib/*.ts for all public export symbols and asserts that scripts/render-lib/index.ts re-exports every file in the directory (except index.ts itself).
Problem: truncateToSentenceBoundary is tested, but there is no test asserting that the actual generated article.md descriptions for every analysis subfolder fall within the 140–200 character window.
Improvement: Add a test that runs aggregateAnalysis on every folder under analysis/daily/ and asserts that the resulting description in front matter is between 50 and 200 characters and contains no raw Markdown syntax ([, *, `).
Problem: scripts/aggregate-analysis.ts --all silently skips subfolders that are missing executive-brief.md. This means a run can complete with exit code 0 while some analysis subfolders produced no article.
Improvement: The --all flag should:
- Print a summary of successful and failed subfolders.
- Exit non-zero if any subfolder fails.
- Accept a
--strictflag that makes even missing-optional-artifact subfolders fail.
| # | Improvement | Intelligence value | User-facing impact | Effort | Priority |
|---|---|---|---|---|---|
| 1 | Integrated narrative article | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | High | P1 |
| 6 | Forward-indicator timelines (JSON+chart) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Medium | P1 |
| 4 | Confidence/Admiralty code chips | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Low | P1 |
| 9 | Sticky in-article ToC | ⭐⭐ | ⭐⭐⭐⭐⭐ | Low | P1 |
| 3 | Proper journalistic lede extraction | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Medium | P2 |
| 2 | Article-type-specific ordering | ⭐⭐⭐⭐ | ⭐⭐⭐ | Medium | P2 |
| 10 | Article type badges | ⭐⭐ | ⭐⭐⭐⭐ | Low | P2 |
| 14 | Full light-mode polish | ⭐⭐ | ⭐⭐⭐⭐ | Medium | P2 |
| 20 | Article quality CI gate | ⭐⭐⭐ | ⭐ | Low | P2 |
| 18 | Playwright E2E accessibility tests | ⭐⭐ | ⭐⭐ | Medium | P2 |
| 5 | Media framing inline callouts | ⭐⭐⭐⭐ | ⭐⭐⭐ | High | P3 |
| 8 | Cross-article series navigation | ⭐⭐⭐ | ⭐⭐⭐ | Medium | P3 |
| 12 | Risk heat map panels | ⭐⭐⭐ | ⭐⭐⭐ | Medium | P3 |
| 7 | Historical parallels integration | ⭐⭐⭐ | ⭐⭐ | Medium | P3 |
| 11 | Reading-progress bar | ⭐ | ⭐⭐⭐ | Low | P3 |
| 13 | Citation copy helper | ⭐⭐ | ⭐⭐⭐ | Low | P3 |
| 16 | Article-type-aware aggregation module | ⭐⭐⭐ | ⭐ | Medium | P3 |
| 22 | SEO contract regression tests | ⭐⭐ | ⭐ | Low | P3 |
| 23 | Aggregate-analysis CLI error handling | ⭐⭐ | ⭐ | Low | P3 |
| 15 | Share and annotation | ⭐ | ⭐⭐ | Medium | P4 |
| 19 | RTL render test coverage | ⭐⭐ | ⭐ | Low | P4 |
| 21 | Barrel architecture enforcement | ⭐ | ⭐ | Low | P4 |
%%{init: {"theme":"dark","themeVariables":{"primaryColor":"#1565C0","primaryTextColor":"#ffffff","lineColor":"#90CAF9","secondaryColor":"#7B1FA2","tertiaryColor":"#2E7D32","tertiaryTextColor":"#ffffff","fontFamily":"Inter, Helvetica, Arial, sans-serif"}}}%%
flowchart TB
N1["#1 Integrated narrative"] --> N5["#5 Media framing inline"]
N1 --> N7["#7 Historical parallels integration"]
N1 --> N8["#8 Series navigation"]
N3["#3 Journalistic lede"] --> N1
N2["#2 Article-type ordering"] --> N1
N2 --> N16["#16 Type-aware aggregation module"]
N6["#6 Forward-indicator JSON+chart"] --> N18["#18 Playwright E2E tests"]
N4["#4 Confidence chips"] --> N18
N12["#12 Risk heat map"] --> N18
N9["#9 Sticky ToC"] --> N11["#11 Reading-progress bar"]
N20["#20 CI article quality gate"] --> N22["#22 SEO regression tests"]
N20 --> N23["#23 CLI error handling"]
style N1 fill:#D32F2F,color:#ffffff
style N2 fill:#1565C0,color:#ffffff
style N3 fill:#1565C0,color:#ffffff
style N6 fill:#7B1FA2,color:#ffffff
style N20 fill:#2E7D32,color:#ffffff
.github/workflows/news-interpellations.md.github/workflows/news-translate.md.github/prompts/README.md.github/prompts/04-analysis-pipeline.md.github/prompts/05-analysis-gate.md.github/prompts/06-article-generation.md
analysis/methodologies/artifact-catalog.mdanalysis/methodologies/ai-driven-analysis-guide.mdanalysis/templates/README.mdanalysis/templates/
scripts/aggregate-analysis.tsscripts/render-articles.tsscripts/render-lib/aggregator.tsscripts/render-lib/markdown.tsscripts/render-lib/article.tsscripts/render-lib/chrome.tsscripts/render-lib/constants.tsscripts/render-lib/url-helpers.ts
styles.cssstyles/themes/article-types.cssjs/lib/mermaid-init.mjsvite.config.jspackage.json.github/workflows/deploy-s3.ymlscripts/deploy-s3.shscripts/fix-s3-mimetypes.sh
analysis/daily/2026-04-24/interpellations/analysis/daily/2026-04-24/interpellations/article.mdnews/2026-04-24-interpellations-en.htmlnews/2026-04-24-interpellations-sv.html
- Analysis is the product; article HTML is the projection.
- No article is generated before the 23-artifact analysis gate passes.
article.mdis deterministic and auditable. It is built from source artifacts, not free-written by the AI.- HTML is sanitized and chrome-wrapped. The renderer owns SEO, provenance, header, footer, language switchers and Mermaid loading.
- Interactive visualization belongs in trusted site code. Mermaid is safely supported now; Chart.js and D3 are available through dashboard modules and future trusted article modules that consume JSON artifacts.
- Deployment preserves integrity. Vite discovers news articles, SRI is generated, S3 objects get explicit MIME/cache metadata, and CloudFront is invalidated after deploy.
- Political intelligence remains ethical and neutral. Evidence, uncertainty, source diversity, GDPR/ISMS alignment and AI-FIRST Pass 2 are non-negotiable.
| 🌐 Platforms | 📦 Open-Source Projects | 🛡️ Governance & Standards |
|---|---|---|
|
🗳️ Riksdagsmonitor — Swedish Parliament intelligence 🇪🇺 EU Parliament Monitor — European coverage 🕵️ Citizen Intelligence Agency — political-data engine 🌐 Hack23 AB — corporate site 📰 Hack23 Blog — engineering & policy 💼 Hack23 on LinkedIn |
🗳️ Hack23/riksdagsmonitor 🕵️ Hack23/cia 🇪🇺 Hack23/euparliamentmonitor 🔌 Hack23/european-parliament-mcp ✅ Hack23/cia-compliance-manager 🥋 Hack23/black-trigram 🏠 Hack23/homepage |
🛡️ Hack23 ISMS-PUBLIC — public ISMS 🔒 Information Security Policy 🤖 AI Policy 🧪 Secure Development Policy 🎯 Threat Modeling Policy 🏷️ Classification Framework |
🗳️ Empower citizens · 🔍 Strengthen democratic accountability · 🕵️ Illuminate the political process
© 2008–2026 Hack23 AB (Org.nr 559534-7807) · Maintainer: James Pether Sörling, CISSP CISM