✅ Comprehensive Quality Assurance for Democratic Intelligence
🎯 Vitest Unit · Cypress E2E · Accessibility · Performance · 14-Language Coverage
📋 Document Owner: CEO | 📄 Version: 1.0 | 🔄 Review Cycle: Quarterly
🏢 Owner: Hack23 AB (Org.nr 559534-7807) | 🏷️ Classification: 🟢 Public
Comprehensive testing strategy covering unit tests (Vitest), E2E tests (Cypress), data validation, accessibility, and performance for all 9 CIA intelligence dashboards.
- >80% Code Coverage - All dashboard JavaScript modules
- Zero Test Skips - Fail-fast principle (no conditionals)
- Comprehensive E2E Coverage - All 9 dashboards × 14 languages
- Data Quality Assurance - CIA CSV validation
- WCAG 2.1 AA Compliance - Automated accessibility testing
- Performance Benchmarks - Chart render time <500ms
| Framework | Purpose | Test Count |
|---|---|---|
| Vitest | Unit & Integration Tests | 1183+ tests |
| Cypress | E2E & Integration Tests | 150+ tests |
| Playwright | Visual Regression (planned) | TBD |
| cypress-axe | Accessibility Testing (planned) | TBD |
tests/
├── csv-validation.test.js # CIA CSV data validation
├── party-dashboard.test.js # Party dashboard unit tests
├── risk-dashboard.test.js # Risk dashboard unit tests (existing)
├── coalition-dashboard.test.js # Coalition dashboard (existing)
├── committees-dashboard.test.js # Committee dashboard (existing)
├── load-cia-stats.test.js # CIA data loading (existing)
└── [... 27 more test files] # News, sitemap, MCP, etc.
cypress/e2e/
├── all-dashboards.cy.js # Comprehensive 9 dashboard tests
├── dashboards.cy.js # Individual dashboard tests (updated)
├── dashboard-page.cy.js # Dashboard page tests (updated)
├── politician-dashboard.cy.js # Politician-specific tests
├── accessibility.cy.js # WCAG 2.1 AA tests
├── homepage.cy.js # Homepage tests
├── multi-language-sanity.cy.js # 14-language validation
├── news-articles.cy.js # News article tests
├── news-page.cy.js # News page tests
└── sitemap.cy.js # Sitemap tests
The aggregate-then-render news pipeline (scripts/aggregate-analysis.ts → scripts/render-articles.ts → scripts/render-lib/) is exercised by the following dedicated test surfaces. Any change to the pipeline must keep them green.
| Surface | What it asserts | Typical harness |
|---|---|---|
| Aggregator ordering | Per-day artifacts in analysis/daily/$DATE/$SUB/ concatenate into article.md in the canonical narrative order defined by analysis/templates/ (executive-brief → synthesis → significance → stakeholders → …). Duplicate H1s and admin footers are stripped; relative links are rewritten to absolute GitHub blob URLs. |
Fixture-based Vitest snapshot over a minimal analysis/daily/fixture/$SUB/ tree |
| Aggregator manifest | A .manifest.json sibling to article.md lists every consumed artifact with a SHA-256 digest. The manifest is reproducible (same input → same manifest). |
Vitest hash assertion |
| Renderer sanitisation | rehype-sanitize drops <script>, <iframe>, inline event handlers, javascript: / non-image data: URIs. It does not drop allow-listed <pre class="mermaid"> blocks, <figure>/<figcaption>, <details>/<summary>, or tables. |
Vitest with adversarial markdown fixtures |
| Mermaid pass-through | A ```mermaid fenced block in the markdown survives sanitisation as <pre class="mermaid">…</pre> and the rendered chrome injects `js/lib/mermaid-init.mjs`. |
Vitest parse assertion + Cypress client-render smoke |
| JSON-LD validity | Each rendered article contains a single well-formed application/ld+json NewsArticle block with populated headline, datePublished, inLanguage, author, publisher, citation[], and about[]. |
Vitest parses the script tag and validates against a lightweight schema |
| Sitemap inclusion | Every news/$DATE-$SUB-$LANG.html appears in sitemap.xml (with the correct hreflang alternates) and sitemap.html. All 14 political-intelligence_*.html pages are listed too. |
Vitest fixture-based assertion in generate-sitemap tests |
| Provenance footer | Every article links to the GitHub blob URL of every methodology and template file it consumed, as recorded in the manifest. | Vitest DOM assertion |
| Naming convention | Rendered files match news/$DATE-$SUB-$LANG.html (hyphens only — never _lang underscore). |
Vitest regex over news/ directory listing |
Three fixture-based tests (generate-news-indexes, two generate-rss) historically failed on pre-existing news/*.html files with malformed article:published_time values. These fixtures are regenerated by the renderer in CI; the tests now scope themselves to the new news/$DATE-$SUB-$LANG.html naming pattern so stale files do not pollute results.
File: tests/csv-validation.test.js
Validates CIA Platform CSV exports for data quality:
describe('CIA CSV Data Validation', () => {
// File existence (18 required files)
it('should have distribution_politician_risk_levels.csv');
// Encoding validation (UTF-8/ASCII, no � characters)
it('should have valid UTF-8 encoding');
// Structure validation (headers, delimiters, data rows)
it('should have proper CSV structure');
// Schema validation (required columns, data types)
it('should have required columns');
it('should have valid numeric data types');
// Column consistency (handles quoted fields)
it('should have consistent column counts');
// Data freshness (warns at 30 days, fails at 90 days)
it('CSV files should not be older than 90 days');
});Coverage:
- ✅ 18 required CSV files validated
- ✅ UTF-8 encoding checks
- ✅ Schema validation with actual column names
- ✅ Numeric data type validation
- ✅ Data freshness warnings (30 days) and enforcement (90 days)
File: tests/party-dashboard.test.js
Tests dashboard DOM structure and configuration:
describe('Party Dashboard', () => {
// Structure tests
it('should have party dashboard container');
it('should have dashboard title and description');
// Chart tests (3 canvas charts + 1 D3 container)
it('should have partyEffectivenessChart canvas');
it('partyEffectivenessChart should have ARIA role and label');
// Accessibility tests
it('should have semantic HTML structure');
it('all charts should have proper ARIA attributes');
it('should have screen reader descriptions');
// Configuration tests
it('should support all 8 Swedish parties');
it('should define party colors for Chart.js');
// Responsive design
it('should use dashboard-grid for responsive layout');
});Note: Dashboard JavaScript files are browser-only IIFEs (not ES6 modules), so we test DOM structure and configuration rather than importing functions.
File: tests/visualizations.test.ts
Covers all 9 public render methods of src/browser/cia/visualizations.ts — the
primary chart rendering engine that drives every CIA intelligence dashboard.
The module is now included in the Vitest coverage gate.
Chart.js mock pattern. The renderer captures Chart at module load time
(const Chart = (globalThis as any).Chart;). To intercept each chart
construction with a per-test spy we:
- Replace
globalThis.Chartwith avi.fn()that pushes{ ctx, type, data, options }into achartCallsarray. - Call
vi.resetModules()and dynamicallyimportthe module so the newChartreference is captured.
async function loadRenderer(): Promise<RendererCtor> {
vi.resetModules();
const mod = await import('../src/browser/cia/visualizations.js');
return mod.CIADashboardRenderer as unknown as RendererCtor;
}
it('passes correct party labels to Chart.js', async () => {
const Renderer = await loadRenderer();
makeCanvas('party-seats-chart');
makeCanvas('party-cohesion-chart');
new Renderer({ partyPerf: fxPartyPerf() }).renderPartyPerformance();
expect(chartCalls[0].type).toBe('bar');
expect(chartCalls[0].data.labels).toEqual(['S','M','SD','C','V','KD','L','MP']);
});Edge cases covered. Empty/missing dashboards (early return warning paths),
missing globalThis.Chart (graceful no-op), and NaN / non-finite numeric
fields (rendered as 'N/A' or replaced with 0).
File: cypress/e2e/all-dashboards.cy.js (150+ tests)
Tests all 9 dashboards systematically:
// Simplified example - actual implementation has specific chart IDs
const dashboards = [
// NOTE: These are simplified placeholder values for documentation purposes.
// For actual chart IDs and counts, see cypress/e2e/all-dashboards.cy.js
{ id: 'party-dashboard', charts: 3, hasD3: true, d3Container: 'coalitionAlignmentChart' },
{ id: 'election-cycle-dashboard', charts: 4, hasD3: true, d3Container: 'decision-heatmap' },
{ id: 'committee-dashboard', charts: 3, hasD3: true, d3Container: 'committeeNetwork' },
{ id: 'coalition-dashboard', charts: 3, hasD3: true, d3Container: 'coalitionNetwork' },
{ id: 'seasonal-patterns-dashboard', charts: 4, hasD3: true, d3Container: 'seasonal-heatmap' },
{ id: 'pre-election-dashboard', charts: 5, hasD3: false },
{ id: 'anomaly-detection-dashboard', charts: 4, hasD3: true, d3Container: 'severity-heatmap' },
{ id: 'ministry-dashboard', charts: 3, hasD3: true, d3Container: 'ministryRiskHeatMap' },
{ id: 'risk-dashboard', charts: 4, hasD3: true, d3Container: 'riskHeatMap' }
];
dashboards.forEach(dashboard => {
describe(dashboard.name, () => {
it('should exist and be visible');
it('should have dashboard heading');
it('should not have error messages');
it('should have data attribution');
// Chart.js validation
dashboard.charts.forEach(chartId => {
it('should have ${chartId} canvas');
it('${chartId} should have Chart.js render monitor class');
});
// D3.js validation (if applicable)
if (dashboard.hasD3) {
it('should render D3 SVG');
it('SVG should have content');
}
// Accessibility
it('should have ARIA labels on charts');
it('should have screen reader descriptions');
// Responsive design
it('should be visible on mobile (375px)');
it('should be visible on tablet (768px)');
it('should be visible on desktop (1440px)');
});
});Zero test skips or conditionals. All tests fail immediately when features are missing:
// ❌ OLD (fail-slow with conditionals)
cy.get('body').then(($body) => {
const chartContainer = $body.find('#coalitionAlignmentChart');
if (chartContainer.length > 0) {
cy.get('#coalitionAlignmentChart').should('exist');
} else {
cy.log('Coalition alignment chart not found - skipping test');
}
});
// ✅ NEW (fail-fast)
cy.get('#coalitionAlignmentChart').should('exist');File: cypress/support/commands.js
Reusable commands for dashboard testing:
// Wait for Chart.js to render
Cypress.Commands.add('waitForChart', (canvasId) => {
cy.get(`#${canvasId}`).should('be.visible');
cy.get(`#${canvasId}`).should(($canvas) => {
expect($canvas[0].width).to.be.greaterThan(0);
expect($canvas[0].height).to.be.greaterThan(0);
});
});
// Wait for D3 visualization to render
Cypress.Commands.add('waitForD3', (containerId) => {
cy.get(`#${containerId} svg`).should('exist');
cy.get(`#${containerId} svg`).should(($svg) => {
expect($svg.children().length).to.be.greaterThan(0);
});
});
// Stub CIA data for testing
Cypress.Commands.add('stubCIAData', () => {
cy.intercept('GET', '**/cia-data/**/*.csv', {
statusCode: 200,
body: 'Year,Quarter,Ballot\n2024,Q1,1000\n2024,Q2,1100'
}).as('ciaData');
});
// Test responsive design
Cypress.Commands.add('testResponsive', (selector) => {
const viewports = [
{ width: 320, height: 568, name: 'Mobile' },
{ width: 768, height: 1024, name: 'Tablet' },
{ width: 1280, height: 720, name: 'Desktop' },
{ width: 1920, height: 1080, name: 'Large Desktop' }
];
viewports.forEach(viewport => {
cy.viewport(viewport.width, viewport.height);
cy.get(selector).should('be.visible');
});
});Tool: Playwright
Planned Coverage:
- Baseline screenshots for all 9 dashboards
- Mobile view screenshots (375px)
- Tablet view screenshots (768px)
- Desktop view screenshots (1440px)
- RTL layout screenshots (Arabic, Hebrew)
- Chart rendering validation
- D3.js visualization validation
Example:
test('party-dashboard should match baseline', async ({ page }) => {
await page.goto('/');
const dashboard = page.locator('#party-dashboard');
// Wait for charts to render
await page.waitForFunction(() => {
const charts = document.querySelectorAll('.chartjs-render-monitor');
return charts.length > 0;
});
// Take screenshot
await expect(dashboard).toHaveScreenshot('party-dashboard.png', {
maxDiffPixels: 100
});
});Tool: cypress-axe
Planned Coverage:
- WCAG 2.1 AA automated validation
- Keyboard navigation tests
- Color contrast validation (4.5:1 minimum)
- ARIA label verification
- Screen reader compatibility
Example:
import 'cypress-axe';
describe('Dashboard Accessibility (WCAG 2.1 AA)', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('should have no accessibility violations', () => {
cy.checkA11y();
});
it('should have sufficient color contrast', () => {
cy.checkA11y(null, {
rules: {
'color-contrast': { enabled: true }
}
});
});
it('should have ARIA labels on all charts', () => {
cy.get('canvas').each($canvas => {
expect($canvas).to.have.attr('role', 'img');
expect($canvas).to.have.attr('aria-label');
});
});
});Goals:
- Chart render time < 500ms
- D3.js heatmap render < 1000ms (349 MPs × 45 rules)
- No memory leaks (10 renders without 50% memory growth)
Example:
describe('Dashboard Performance', () => {
it('should render party chart in < 500ms', async () => {
const start = performance.now();
await renderPartyEffectivenessChart(mockData);
const end = performance.now();
expect(end - start).toBeLessThan(500);
});
it('should not leak memory after multiple renders', async () => {
const initialMemory = process.memoryUsage().heapUsed;
for (let i = 0; i < 10; i++) {
await renderAllDashboards();
destroyAllCharts();
}
global.gc();
const finalMemory = process.memoryUsage().heapUsed;
expect(finalMemory).toBeLessThan(initialMemory * 1.5);
});
});File: .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '25'
- run: npm ci
- run: npm test
- run: npm run test:coverage
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: coverage-report
path: coverage/
e2e-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '25'
- run: npm ci
- run: npm run build
- run: npm run cypress:run
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots/Coverage Reports Published to:
docs/coverage/- HTML coverage reportcoverage/lcov.info- LCOV format for badgescoverage/json- JSON format for analysis
CI fails if any metric falls below the Hack23 Secure Development Policy
floor (≥80 % lines, ≥70 % branches). Thresholds apply to the importable
unit-testable surface — browser-only <script>-loaded modules are exercised
by Cypress E2E and CLI entry points are exercised by the news workflows;
both are deliberately excluded from the Vitest gate via the documented
exclude list in vitest.config.js.
| Metric | Enforced floor (ISMS) | Measured 2026-04-25 |
|---|---|---|
| Statements | ≥ 80 % | 90.26 % |
| Branches | ≥ 70 % | 80.08 % |
| Functions | ≥ 70 % | 93.42 % |
| Lines | ≥ 80 % | 91.74 % |
To re-measure after adding modules, run npm run test:coverage and read
the All files row at the bottom. Update both the enforced thresholds in
vitest.config.js and the measured-baseline column above in the same PR.
Authority: Secure_Development_Policy.md mandates ≥80 % line coverage and ≥70 % branch coverage as a hard CI gate for all Hack23 production codebases.
All PRs must pass:
- ✅ Unit tests (100% pass rate)
- ✅ E2E tests (100% pass rate)
- ✅ CSV data validation (all files valid)
- ✅ No test skips or conditionals
- ✅ Coverage thresholds at the ISMS floor (≥80 % lines, ≥70 % branches — see table above)
| Metric | Value | Target | Status |
|---|---|---|---|
| Unit Tests | 2094 | >1000 | ✅ |
| E2E Tests | 150+ | >100 | ✅ |
| CSV Validation | 159 | 100% | ✅ |
| Code Coverage (lines) | 91.74 % | ≥80 % (ISMS) | ✅ |
| Code Coverage (branches) | 80.08 % | ≥70 % (ISMS) | ✅ |
| Test Skips | 0 | 0 | ✅ |
| Dashboards Covered | 9/9 | 9/9 | ✅ |
| Languages Tested | 1/14 | 14/14 | 🟡 |
| Test Suite | Duration | Target |
|---|---|---|
| Unit Tests (Vitest) | ~63 s | <90 s |
| E2E Tests (Cypress) | TBD | <5 min |
| Visual Tests (Playwright) | TBD | <10 min |
# Run all unit tests
npm test
# Run a specific test file
npm test tests/csv-validation.test.js
# Run a focused subset by name pattern
npm test -- --grep "imf-client"
# Run tests with coverage (enforces thresholds in vitest.config.js)
npm run test:coverage
# Watch mode (interactive)
npm run test:watch
# UI mode (visual test runner)
npm run test:ui# Run all E2E tests (headless)
npm run cypress:run
# Run specific test file
npm run cypress:run -- --spec "cypress/e2e/all-dashboards.cy.js"
# Open Cypress UI (interactive)
npm run cypress:open
# Run critical tests only
npm run cypress:run:critical
# Run with build (full integration)
npm run e2e# Run visual tests
npm run playwright:test
# Update baselines
npm run playwright:test -- --update-snapshots
# Show report
npm run playwright:show-reportimport { describe, it, expect, beforeEach } from 'vitest';
describe('New Dashboard', () => {
beforeEach(() => {
document.body.innerHTML = `
<section id="new-dashboard">
<!-- Dashboard HTML structure -->
</section>
`;
});
it('should have dashboard container', () => {
const dashboard = document.getElementById('new-dashboard');
expect(dashboard).toBeTruthy();
});
// Add more tests...
});describe('New Dashboard E2E', () => {
beforeEach(() => {
cy.stubCIAData();
cy.visit('/');
});
it('should display new dashboard', () => {
cy.get('#new-dashboard').should('be.visible');
});
it('should render charts', () => {
cy.get('#newChart').should('exist');
cy.waitForChart('newChart');
});
// Add more tests...
});# Run with verbose output
npm test -- --reporter=verbose
# Run single test in watch mode
npm run test:watch tests/specific.test.js
# Use debugger
npm run test:ui # Best for debugging# Open Cypress UI for debugging
npm run cypress:open
# Run with video recording
npm run cypress:run # Videos saved to cypress/videos/
# Run with screenshots on failure
# (enabled by default, saved to cypress/screenshots/)- Write fail-fast tests - No conditionals or skips
- Test user behavior - Focus on what users see/do
- Use semantic selectors -
cy.get('#dashboard')notcy.get('div.container') - Validate accessibility - ARIA labels, screen readers
- Test responsive design - Mobile, tablet, desktop
- Validate data quality - CSV structure, encoding, schemas
- Mock external dependencies - Stub CIA data
- Keep tests independent - Each test should work in isolation
- Don't skip tests - Fix the issue or remove the test
- Don't use conditionals - Tests should fail if feature missing
- Don't test implementation details - Test user-facing behavior
- Don't hardcode IDs - Use configuration arrays
- Don't ignore accessibility - WCAG 2.1 AA is mandatory
- Don't test browser internals - Focus on application logic
- Don't use arbitrary waits - Use proper wait commands
| Module | Current | Target | Priority |
|---|---|---|---|
| party-dashboard.js | ~20% | >80% | High |
| risk-dashboard.js | ~60% | >80% | Medium |
| coalition-dashboard.js | ~40% | >80% | High |
| committees-dashboard.js | ~50% | >80% | Medium |
| election-cycle-dashboard.js | ~10% | >80% | High |
| ministry-dashboard.js | ~10% | >80% | High |
| seasonal-patterns-dashboard.js | ~10% | >80% | Medium |
| pre-election-dashboard.js | ~10% | >80% | Medium |
| anomaly-detection-dashboard.js | ~10% | >80% | Medium |
- Vitest Documentation
- Cypress Best Practices
- Playwright Visual Testing
- cypress-axe (Accessibility Testing)
- WCAG 2.1 AA Guidelines
- Chart.js Testing
- D3.js Testing
Last Updated: 2026-02-18
Maintained by: Hack23 AB
Version: 1.0
Effective: 2026-04-24 · Authoritative hub:
analysis/imf/README.md·analysis/imf/agentic-integration.md·analysis/imf/indicators-inventory.json·analysis/imf/data-dictionary.md·.github/aw/ECONOMIC_DATA_CONTRACT.md
| Test file | Purpose |
|---|---|
tests/imf-client.test.ts |
Low-level Datamapper + SDMX client behaviour |
tests/imf-codes.test.ts |
Dataflow / indicator code registry validation |
tests/imf-context.test.ts |
High-level getImfContext({domain, country, vintage}) shape, cache, vintage handling |
tests/imf-inventory.test.ts |
Schema validation of analysis/imf/indicators-inventory.json (13 assertions) |
tests/economic-context-multi-provider.test.ts |
Asserts IMF queried before WB for every economic indicator class |
tests/validate-economic-context.test.ts |
Validates the IMF-first contract end-to-end |
# Offline (mocked) — fast; runs in CI
npm test -- imf
# Online smoke (live IMF API) — opt-in only, not in default CI
IMF_LIVE_SMOKE=1 npm test -- imf-client.live- Zero skips — IMF tests never use conditional skip; if IMF mock data is missing, the test fails immediately
- Vintage assertion — every IMF mock fixture carries an explicit
vintage_label; tests assert presence - Provider precedence assertion —
economic-context-multi-provider.test.tsasserts IMF appears before WB in fetched economic data - Schema integrity —
imf-inventory.test.tsasserts every indicator in the inventory JSON has dataflow + dimensions + vintage + supersedes pointer - Banned-phrase lint —
tests/banned-patterns.test.ts(where present) asserts that WB economic codes (NY.GDP.*,FP.CPI.*,SL.UEM.*,GC.DOD.TOTL.*) are never used as primary citation in article fixtures
tests/imf-precedence-contract.test.ts— synthetic test feeding both IMF + WB GDP series; asserts article cites IMFtests/imf-vintage-discipline.test.ts— asserts cache filenames carry vintage tags
Canonical rule. Every economic claim in a Riksdagsmonitor article cites an IMF dataflow first; World Bank citations are reserved for governance, environment and social residue (the classes IMF does not publish). SCB is the Swedish-specific ground truth layer. See ECONOMIC_DATA_CONTRACT.md v2.1 for the banned-phrase list and vintage discipline (>6 mo → annotation).
Statskontoret coverage is split across focused Vitest suites:
| Test file | Coverage |
|---|---|
tests/statskontoret-client.test.ts |
Download-link extraction, XLSX workbook parsing, CSV ZIP extraction, Swedish decimal handling, injected fetch client behavior. |
tests/statskontoret-fetch.test.ts |
Import-safe CLI parsing, typed CLI errors, source validation, resource classification, numeric parsing primitives. |
tests/statskontoret-inventory.test.ts |
Inventory metadata, dataset coverage parity with STATSKONTORET_SOURCES, provider-decision matrix, client/CLI/persistence declarations. |
Targeted validation command:
npx vitest run tests/statskontoret-client.test.ts tests/statskontoret-fetch.test.ts tests/statskontoret-inventory.test.tsQuality expectation: no live network calls in tests; fixtures model Statskontoret workbook/ZIP assumptions and prevent workflow regressions without depending on upstream availability.
| 🌐 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