feat: add AI-Act-Kompass — standalone EU AI Act compliance toolkit for SMEs

Local-first React/Vite app (no cloud, no telemetry, localStorage only):
- Assessment wizard classifying AI systems per AI Act systematics
  (Art. 3 definition, Art. 5 prohibitions, Art. 6/Annex I+III high-risk
  incl. Art. 6(3) exception and profiling bar, Art. 50 transparency, GPAI)
- Role-specific obligation checklists (provider/deployer/importer/distributor)
  with legal basis, deadlines and implementation status
- Document generator: risk report, action plan, Annex IV skeleton (Markdown)
- AI inventory with export, deadline overview incl. Digital Omnibus shift
- Versioned regulatory content pack modeled as data for future update feed
- Engine fully unit-tested (24 tests), TS strict, static build

Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
Claude 2026-07-04 08:03:08 +00:00
parent 874e4a1ead
commit fb2da0db00
No known key found for this signature in database
24 changed files with 4621 additions and 0 deletions

3
.gitignore vendored
View File

@ -34,3 +34,6 @@ docker-compose.override.yml
app/.next/
dist/
*.local
# TS incremental build
*.tsbuildinfo

38
ai-act-kompass/README.md Normal file
View File

@ -0,0 +1,38 @@
# 🧭 AI-Act-Kompass
**Standalone EU-AI-Act-Compliance-Toolkit für KMU.** Local-first: keine Cloud, keine Telemetrie, keine Datenübertragung — alle Daten bleiben im Browser des Nutzers (localStorage, mit JSON-Export/-Import).
> ⚠️ Strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689 — **keine Rechtsberatung**.
## Was es kann
- **Bewertungs-Wizard**: erfasst ein KI-System und klassifiziert es automatisch nach der Systematik des AI Act — KI-Definition (Art. 3 Nr. 1) → verbotene Praktiken (Art. 5) → Hochrisiko (Art. 6, Annex I/III, inkl. Ausnahme Art. 6 Abs. 3 und Profiling-Sperre) → Transparenzpflichten (Art. 50) → minimales Risiko. GPAI-Pflichten (Art. 53 ff.) werden separat geflaggt.
- **KI-Inventar**: alle Systeme des Unternehmens mit Risikoklasse, Rolle und Verantwortlichen — exportierbar als Markdown (AI Asset Register).
- **Pflichten-Checklisten**: rollenspezifisch (Anbieter/Betreiber/Einführer/Händler) abgeleitete Pflichten mit Rechtsgrundlage, Geltungsfrist und Umsetzungsstatus.
- **Dokumenten-Generator**: Risikoklassifizierungs-Bericht, Maßnahmenplan und Annex-IV-Grundgerüst (technische Dokumentation) als Markdown.
- **Fristen-Übersicht**: alle Geltungsfristen des AI Act inkl. der Digital-Omnibus-Verschiebung (Annex III → 02.12.2027).
## Architektur
| Schicht | Inhalt |
|---|---|
| `src/engine/` | Reine, getestete Regel-Engine: `classify`, `deriveObligations`, Dokumenten-Generatoren. Kein DOM, keine Seiteneffekte. |
| `src/content/` | Versionierter regulatorischer **Content-Pack** (Verbote, Annex-III-Bereiche, Pflichten, Fristen) als Daten — vorbereitet für einen späteren signierten Update-Feed. |
| `src/store/` | localStorage-Persistenz + Export/Import (JSON). |
| `src/ui/` | React-UI (Dashboard, Wizard, Detailansicht, Einstellungen), Deutsch. |
Kein Backend, kein Build-Server nötig — `dist/` ist eine statische Site und läuft von Datei-Share, Intranet oder localhost.
## Entwicklung
```bash
npm install
npm run dev # Dev-Server
npm test # Engine-Tests (Vitest)
npm run typecheck # TS strict
npm run build # Statischer Build nach dist/
```
## Geschäftsmodell (geplant)
Standalone-App + Abo für den **Regulatory-Content-Feed**: Die App gehört dem Kunden und läuft lokal; das Abo liefert aktualisierte Regelinhalte (delegierte Rechtsakte, harmonisierte Normen, Fristenänderungen), Templates und Norm-Mappings. Synergie mit `@shieldx/core`: Art.-15-Pflicht (Robustheit/Cybersicherheit gegen Prompt Injection & Co.) verweist direkt auf ShieldX als technische Umsetzung.

13
ai-act-kompass/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="AI-Act-Kompass — EU AI Act Compliance-Toolkit für KMU. Local-first, keine Cloud." />
<title>AI-Act-Kompass</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2135
ai-act-kompass/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
{
"name": "@shieldx/ai-act-kompass",
"version": "0.1.0",
"private": true,
"description": "AI-Act-Kompass — Standalone EU AI Act Compliance-Toolkit für KMU. Local-first, keine Cloud, keine Datenübertragung.",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vitest": "^2.1.8"
}
}

View File

@ -0,0 +1,389 @@
import type { ContentPack } from '../engine/types';
/**
* Regulatorischer Content-Pack v2026.07.
*
* Kodifiziert den Stand der Verordnung (EU) 2024/1689 (AI Act) inkl. der
* durch das Digital-Omnibus-Paket verschobenen Annex-III-Fristen.
* Dieser Pack ist bewusst als Daten (nicht Code) modelliert, damit er
* später über einen signierten Update-Feed ausgetauscht werden kann.
*
* WICHTIG: Inhalte sind eine strukturierte Arbeitshilfe, keine Rechtsberatung.
*/
export const CONTENT_PACK: ContentPack = {
version: '2026.07.0',
releasedAt: '2026-07-04',
legalBasis: 'Verordnung (EU) 2024/1689 (AI Act), Stand Juli 2026',
aiDefinitionCriteria: [
{
id: 'def-autonomy',
label: 'Das System arbeitet mit einem gewissen Grad an Autonomie (nicht rein regelbasiert deterministisch durch den Nutzer gesteuert).',
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-inference',
label: 'Das System leitet aus Eingaben ab, wie Ausgaben (Vorhersagen, Inhalte, Empfehlungen, Entscheidungen) erzeugt werden.',
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-influence',
label: 'Die Ausgaben können physische oder virtuelle Umgebungen beeinflussen.',
legalRef: 'Art. 3 Nr. 1',
},
],
prohibitedPractices: [
{
id: 'proh-manipulation',
label: 'Unterschwellige oder gezielt manipulative Techniken, die Verhalten wesentlich verändern und erheblichen Schaden verursachen (können).',
legalRef: 'Art. 5 Abs. 1 lit. a',
},
{
id: 'proh-vulnerability',
label: 'Ausnutzung von Schwächen aufgrund von Alter, Behinderung oder sozialer/wirtschaftlicher Lage mit erheblichem Schadensrisiko.',
legalRef: 'Art. 5 Abs. 1 lit. b',
},
{
id: 'proh-social-scoring',
label: 'Social Scoring: Bewertung von Personen anhand ihres Sozialverhaltens mit ungerechtfertigter Schlechterstellung.',
legalRef: 'Art. 5 Abs. 1 lit. c',
},
{
id: 'proh-predictive-policing',
label: 'Risikobewertung von Straftaten allein auf Basis von Profiling oder Persönlichkeitsmerkmalen.',
legalRef: 'Art. 5 Abs. 1 lit. d',
},
{
id: 'proh-face-scraping',
label: 'Ungezieltes Auslesen (Scraping) von Gesichtsbildern aus Internet oder Überwachungskameras zum Aufbau von Gesichtsdatenbanken.',
legalRef: 'Art. 5 Abs. 1 lit. e',
},
{
id: 'proh-emotion-workplace',
label: 'Emotionserkennung am Arbeitsplatz oder in Bildungseinrichtungen (außer aus medizinischen oder Sicherheitsgründen).',
legalRef: 'Art. 5 Abs. 1 lit. f',
},
{
id: 'proh-biometric-categorisation',
label: 'Biometrische Kategorisierung zur Ableitung sensibler Merkmale (z. B. Ethnie, politische Meinung, sexuelle Orientierung).',
legalRef: 'Art. 5 Abs. 1 lit. g',
},
{
id: 'proh-rbi',
label: 'Biometrische Echtzeit-Fernidentifizierung in öffentlich zugänglichen Räumen zu Strafverfolgungszwecken (enge Ausnahmen).',
legalRef: 'Art. 5 Abs. 1 lit. h',
},
],
annexIIICategories: [
{
id: 'a3-biometrics',
label: 'Biometrie: Fernidentifizierung, biometrische Kategorisierung, Emotionserkennung (soweit nicht verboten).',
legalRef: 'Annex III Nr. 1',
},
{
id: 'a3-critical-infra',
label: 'Sicherheitskomponenten kritischer Infrastruktur (Verkehr, Wasser, Gas, Wärme, Strom, digitale Infrastruktur).',
legalRef: 'Annex III Nr. 2',
},
{
id: 'a3-education',
label: 'Bildung: Zulassung, Bewertung von Lernergebnissen, Prüfungsüberwachung.',
legalRef: 'Annex III Nr. 3',
},
{
id: 'a3-employment',
label: 'Beschäftigung: Recruiting/Bewerberauswahl, Beförderung/Kündigung, Aufgabenzuweisung, Leistungsüberwachung.',
legalRef: 'Annex III Nr. 4',
hint: 'Häufigster KMU-Fall: KI-gestütztes Bewerber-Screening.',
},
{
id: 'a3-essential-services',
label: 'Wesentliche Dienste: Kreditwürdigkeitsprüfung, Risiko-Pricing bei Lebens-/Krankenversicherung, Notruf-Triage, Sozialleistungen.',
legalRef: 'Annex III Nr. 5',
},
{
id: 'a3-law-enforcement',
label: 'Strafverfolgung (Beweismittelbewertung, Rückfallrisiko u. a.).',
legalRef: 'Annex III Nr. 6',
},
{
id: 'a3-migration',
label: 'Migration, Asyl, Grenzkontrolle.',
legalRef: 'Annex III Nr. 7',
},
{
id: 'a3-justice',
label: 'Rechtspflege und demokratische Prozesse (Unterstützung richterlicher Entscheidungen, Wahlbeeinflussung).',
legalRef: 'Annex III Nr. 8',
},
],
transparencyTriggers: [
{
id: 'tr-chatbot',
label: 'Das System interagiert direkt mit Menschen (z. B. Chatbot, Voicebot).',
legalRef: 'Art. 50 Abs. 1',
hint: 'Nutzer müssen erkennen können, dass sie mit KI interagieren.',
},
{
id: 'tr-synthetic-content',
label: 'Das System erzeugt synthetische Audio-, Bild-, Video- oder Textinhalte.',
legalRef: 'Art. 50 Abs. 2',
hint: 'Maschinenlesbare Kennzeichnung der Ausgaben erforderlich.',
},
{
id: 'tr-emotion',
label: 'Emotionserkennung oder biometrische Kategorisierung wird eingesetzt (soweit erlaubt).',
legalRef: 'Art. 50 Abs. 3',
},
{
id: 'tr-deepfake',
label: 'Das System erzeugt oder manipuliert Deepfakes (Bild/Audio/Video realer Personen, Orte oder Ereignisse).',
legalRef: 'Art. 50 Abs. 4',
},
],
obligations: [
// ── Alle Risikoklassen / alle Rollen ──────────────────────────────
{
id: 'ob-ai-literacy',
title: 'KI-Kompetenz des Personals sicherstellen',
articles: 'Art. 4',
description:
'Mitarbeitende, die KI-Systeme betreiben oder nutzen, müssen über ausreichende KI-Kompetenz verfügen (Schulungen, Richtlinien). Gilt seit 2. Februar 2025 für alle Anbieter und Betreiber.',
deadline: '2025-02-02',
roles: ['provider', 'deployer', 'importer', 'distributor'],
riskClasses: ['high', 'limited', 'minimal'],
},
// ── Hochrisiko: Anbieter ──────────────────────────────────────────
{
id: 'ob-risk-mgmt',
title: 'Risikomanagementsystem einrichten',
articles: 'Art. 9',
description:
'Kontinuierlicher, iterativer Risikomanagementprozess über den gesamten Lebenszyklus: Risiken identifizieren, bewerten, mindern, testen.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-data-governance',
title: 'Daten-Governance für Trainings-/Test-/Validierungsdaten',
articles: 'Art. 10',
description:
'Datensätze müssen relevant, repräsentativ, möglichst fehlerfrei und vollständig sein; Bias-Prüfung und geeignete Erhebungsverfahren dokumentieren.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-tech-doc',
title: 'Technische Dokumentation (Annex IV) erstellen',
articles: 'Art. 11, Annex IV',
description:
'Vollständige technische Dokumentation vor Inverkehrbringen; für KMU gelten vereinfachte Formulare. Der Dokumenten-Generator dieses Tools erzeugt das Grundgerüst.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-logging',
title: 'Protokollierung (Logging) technisch ermöglichen',
articles: 'Art. 12',
description:
'Automatische Aufzeichnung von Ereignissen über den Lebenszyklus, um Rückverfolgbarkeit und Marktüberwachung zu ermöglichen.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-transparency-instructions',
title: 'Betriebsanleitung und Transparenzinformationen bereitstellen',
articles: 'Art. 13',
description:
'Klare Gebrauchsanweisung: Zweckbestimmung, Genauigkeit, Grenzen, menschliche Aufsicht, erwartete Lebensdauer und Wartung.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-human-oversight',
title: 'Menschliche Aufsicht konzipieren',
articles: 'Art. 14',
description:
'Das System muss wirksam durch Menschen beaufsichtigt werden können (Eingriffs-/Abbruchmöglichkeiten, Automation-Bias-Vorkehrungen).',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-accuracy-robustness',
title: 'Genauigkeit, Robustheit und Cybersicherheit gewährleisten',
articles: 'Art. 15',
description:
'Angemessenes Niveau an Genauigkeit und Widerstandsfähigkeit, inkl. Schutz gegen KI-spezifische Angriffe (Data Poisoning, Adversarial Examples, Prompt Injection).',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-qms',
title: 'Qualitätsmanagementsystem einführen',
articles: 'Art. 17',
description:
'Dokumentiertes QMS: Compliance-Strategie, Prüfverfahren, Datenmanagement, Risikomanagement, Meldeverfahren, Verantwortlichkeiten.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-conformity',
title: 'Konformitätsbewertung durchführen, CE-Kennzeichnung, EU-Datenbank-Registrierung',
articles: 'Art. 43, 48, 49',
description:
'Konformitätsbewertungsverfahren abschließen, EU-Konformitätserklärung ausstellen, CE-Kennzeichnung anbringen und System in der EU-Datenbank registrieren.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
// ── Hochrisiko: Betreiber ─────────────────────────────────────────
{
id: 'ob-deployer-use',
title: 'Nutzung gemäß Betriebsanleitung sicherstellen',
articles: 'Art. 26 Abs. 1',
description:
'Technische und organisatorische Maßnahmen, damit das System entsprechend der Gebrauchsanweisung des Anbieters genutzt wird.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-oversight',
title: 'Kompetente menschliche Aufsicht zuweisen',
articles: 'Art. 26 Abs. 2',
description:
'Aufsicht durch Personen mit erforderlicher Kompetenz, Ausbildung und Befugnis.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-input',
title: 'Relevanz und Repräsentativität der Eingabedaten sicherstellen',
articles: 'Art. 26 Abs. 4',
description:
'Soweit der Betreiber die Eingabedaten kontrolliert, müssen diese für die Zweckbestimmung relevant und hinreichend repräsentativ sein.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-monitoring',
title: 'Betrieb überwachen, Vorfälle melden, Logs aufbewahren',
articles: 'Art. 26 Abs. 56',
description:
'Betrieb anhand der Anleitung überwachen; bei Risiken Anbieter/Behörden informieren; automatisch erzeugte Logs mind. 6 Monate aufbewahren.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-workers',
title: 'Beschäftigte vor Einsatz am Arbeitsplatz informieren',
articles: 'Art. 26 Abs. 7',
description:
'Vor Inbetriebnahme eines Hochrisiko-Systems am Arbeitsplatz sind Arbeitnehmervertreter und betroffene Beschäftigte zu informieren.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-fria',
title: 'Grundrechte-Folgenabschätzung (FRIA) prüfen',
articles: 'Art. 27',
description:
'Pflicht für öffentliche Stellen und private Anbieter öffentlicher Dienste sowie bestimmte Annex-III-Fälle (z. B. Kreditwürdigkeit, Versicherung): Folgen für Grundrechte vor Einsatz bewerten.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
// ── Importeur / Händler ───────────────────────────────────────────
{
id: 'ob-importer-verify',
title: 'Konformität vor Inverkehrbringen prüfen',
articles: 'Art. 23',
description:
'Importeure müssen prüfen: Konformitätsbewertung durchgeführt, technische Dokumentation vorhanden, CE-Kennzeichnung und Ansprechpartner benannt.',
deadline: '2027-12-02',
roles: ['importer'],
riskClasses: ['high'],
},
{
id: 'ob-distributor-verify',
title: 'Sorgfaltspflichten der Vertriebskette erfüllen',
articles: 'Art. 24',
description:
'Händler prüfen CE-Kennzeichnung, Konformitätserklärung und Gebrauchsanweisung; bei Zweifeln keine Bereitstellung.',
deadline: '2027-12-02',
roles: ['distributor'],
riskClasses: ['high'],
},
// ── Transparenzpflichten (limited) ────────────────────────────────
{
id: 'ob-transparency-art50',
title: 'Transparenzpflichten nach Art. 50 umsetzen',
articles: 'Art. 50',
description:
'Je nach Auslöser: KI-Interaktion offenlegen (Chatbots), synthetische Inhalte maschinenlesbar kennzeichnen, Deepfakes sichtbar kennzeichnen, über Emotionserkennung informieren.',
deadline: '2026-08-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited'],
},
// ── GPAI ──────────────────────────────────────────────────────────
{
id: 'ob-gpai',
title: 'GPAI-Modellpflichten erfüllen',
articles: 'Art. 5355',
description:
'Für Anbieter von GPAI-Modellen: technische Modell-Dokumentation, Informationen für nachgelagerte Anbieter, Urheberrechts-Policy, Zusammenfassung der Trainingsdaten. Bei systemischem Risiko zusätzlich Evaluierungen und Incident-Meldungen.',
deadline: '2025-08-02',
roles: ['provider'],
riskClasses: ['high', 'limited', 'minimal'],
},
],
deadlines: [
{
id: 'dl-prohibitions',
date: '2025-02-02',
label: 'Verbote & KI-Kompetenz',
description: 'Verbotene Praktiken (Art. 5) und KI-Kompetenzpflicht (Art. 4) gelten. Bereits in Kraft.',
},
{
id: 'dl-gpai',
date: '2025-08-02',
label: 'GPAI-Pflichten',
description: 'Pflichten für Anbieter von GPAI-Modellen (Art. 53 ff.) gelten. Bereits in Kraft.',
},
{
id: 'dl-general',
date: '2026-08-02',
label: 'Allgemeine Geltung',
description: 'Transparenzpflichten (Art. 50), Sanktionsregime und Governance-Struktur gelten.',
},
{
id: 'dl-annex3',
date: '2027-12-02',
label: 'Hochrisiko (Annex III)',
description: 'Pflichten für Annex-III-Hochrisiko-Systeme gelten (durch Digital Omnibus von Aug 2026 verschoben).',
},
{
id: 'dl-annex1',
date: '2028-08-02',
label: 'Hochrisiko (Annex I)',
description: 'Pflichten für KI als Sicherheitsbauteil regulierter Produkte (Annex I) gelten.',
},
],
};

View File

@ -0,0 +1,111 @@
import { describe, expect, it } from 'vitest';
import { CONTENT_PACK } from '../content/pack';
import { classify } from './classify';
import type { AssessmentAnswers } from './types';
const base: AssessmentAnswers = {
isAISystem: true,
role: 'deployer',
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: [],
art63Exception: false,
involvesProfiling: false,
isAnnexISafetyComponent: false,
transparencyTriggers: [],
};
describe('classify', () => {
it('stuft Nicht-KI-Systeme als out-of-scope ein', () => {
const result = classify({ ...base, isAISystem: false }, CONTENT_PACK);
expect(result.riskClass).toBe('out-of-scope');
expect(result.transparencyDuties).toBe(false);
});
it('stuft verbotene Praktiken als prohibited ein — vor allen anderen Prüfungen', () => {
const result = classify(
{
...base,
prohibitedPractices: ['proh-social-scoring'],
annexIIICategories: ['a3-employment'],
transparencyTriggers: ['tr-chatbot'],
},
CONTENT_PACK,
);
expect(result.riskClass).toBe('prohibited');
expect(result.reasons[0]).toContain('Art. 5');
});
it('stuft Annex-I-Sicherheitsbauteile als high ein', () => {
const result = classify({ ...base, isAnnexISafetyComponent: true }, CONTENT_PACK);
expect(result.riskClass).toBe('high');
});
it('stuft Annex-III-Bereiche als high ein', () => {
const result = classify({ ...base, annexIIICategories: ['a3-employment'] }, CONTENT_PACK);
expect(result.riskClass).toBe('high');
expect(result.reasons.some((r) => r.includes('Annex III'))).toBe(true);
});
it('lässt die Art.-6(3)-Ausnahme greifen, wenn kein Profiling vorliegt', () => {
const result = classify(
{ ...base, annexIIICategories: ['a3-employment'], art63Exception: true },
CONTENT_PACK,
);
expect(result.riskClass).toBe('minimal');
expect(result.art64DocumentationDuty).toBe(true);
});
it('schließt die Art.-6(3)-Ausnahme bei Profiling aus', () => {
const result = classify(
{
...base,
annexIIICategories: ['a3-employment'],
art63Exception: true,
involvesProfiling: true,
},
CONTENT_PACK,
);
expect(result.riskClass).toBe('high');
});
it('kombiniert Art.-6(3)-Ausnahme mit Transparenzpflichten zu limited', () => {
const result = classify(
{
...base,
annexIIICategories: ['a3-employment'],
art63Exception: true,
transparencyTriggers: ['tr-chatbot'],
},
CONTENT_PACK,
);
expect(result.riskClass).toBe('limited');
expect(result.transparencyDuties).toBe(true);
expect(result.art64DocumentationDuty).toBe(true);
});
it('stuft reine Transparenz-Auslöser als limited ein (Chatbot-Fall)', () => {
const result = classify({ ...base, transparencyTriggers: ['tr-chatbot'] }, CONTENT_PACK);
expect(result.riskClass).toBe('limited');
});
it('behält Transparenzpflichten bei Hochrisiko-Systemen bei', () => {
const result = classify(
{ ...base, annexIIICategories: ['a3-employment'], transparencyTriggers: ['tr-chatbot'] },
CONTENT_PACK,
);
expect(result.riskClass).toBe('high');
expect(result.transparencyDuties).toBe(true);
});
it('stuft Systeme ohne Auffälligkeiten als minimal ein', () => {
const result = classify(base, CONTENT_PACK);
expect(result.riskClass).toBe('minimal');
});
it('setzt das GPAI-Flag unabhängig von der Risikoklasse', () => {
const result = classify({ ...base, isGPAIModel: true }, CONTENT_PACK);
expect(result.riskClass).toBe('minimal');
expect(result.gpaiObligations).toBe(true);
});
});

View File

@ -0,0 +1,133 @@
import type { AssessmentAnswers, Classification, ContentPack } from './types';
/**
* Klassifiziert ein KI-System nach dem Risikomodell des EU AI Act.
*
* Prüfreihenfolge (entspricht der Systematik der Verordnung):
* 1. Anwendbarkeit (KI-Definition, Art. 3 Nr. 1)
* 2. Verbotene Praktiken (Art. 5) "prohibited"
* 3. Annex I Sicherheitsbauteil "high"
* 4. Annex III Bereiche, ggf. Ausnahme Art. 6 Abs. 3 "high"
* 5. Transparenzpflichten (Art. 50) "limited"
* 6. Sonst "minimal"
*
* @param answers - Antworten des Bewertungs-Wizards
* @param pack - Regulatorischer Content-Pack (für Begründungstexte)
* @returns Unveränderliches Klassifizierungsergebnis mit Begründungen
*/
export function classify(answers: AssessmentAnswers, pack: ContentPack): Classification {
const reasons: string[] = [];
const transparencyDuties = answers.transparencyTriggers.length > 0;
if (!answers.isAISystem) {
return {
riskClass: 'out-of-scope',
reasons: ['Das System erfüllt die KI-Definition nach Art. 3 Nr. 1 nicht — der AI Act ist nicht anwendbar.'],
gpaiObligations: false,
transparencyDuties: false,
art64DocumentationDuty: false,
};
}
if (answers.prohibitedPractices.length > 0) {
for (const id of answers.prohibitedPractices) {
const item = pack.prohibitedPractices.find((p) => p.id === id);
if (item) {
reasons.push(`Verbotene Praktik: ${item.label} (${item.legalRef})`);
}
}
return {
riskClass: 'prohibited',
reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties: false,
art64DocumentationDuty: false,
};
}
if (answers.isAnnexISafetyComponent) {
reasons.push(
'Das System ist Sicherheitsbauteil eines in Annex I regulierten Produkts bzw. selbst ein solches Produkt (Art. 6 Abs. 1) — Hochrisiko.',
);
return {
riskClass: 'high',
reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties,
art64DocumentationDuty: false,
};
}
if (answers.annexIIICategories.length > 0) {
for (const id of answers.annexIIICategories) {
const item = pack.annexIIICategories.find((c) => c.id === id);
if (item) {
reasons.push(`Annex-III-Bereich: ${item.label} (${item.legalRef})`);
}
}
if (answers.involvesProfiling) {
reasons.push(
'Das System führt Profiling natürlicher Personen durch — die Ausnahme nach Art. 6 Abs. 3 ist damit ausgeschlossen.',
);
return {
riskClass: 'high',
reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties,
art64DocumentationDuty: false,
};
}
if (answers.art63Exception) {
reasons.push(
'Ausnahme nach Art. 6 Abs. 3 in Anspruch genommen (eng begrenzte/vorbereitende Aufgabe ohne wesentliches Risiko). Die Einschätzung ist nach Art. 6 Abs. 4 zu dokumentieren.',
);
return {
riskClass: transparencyDuties ? 'limited' : 'minimal',
reasons: transparencyDuties
? [...reasons, ...transparencyReasons(answers, pack)]
: reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties,
art64DocumentationDuty: true,
};
}
return {
riskClass: 'high',
reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties,
art64DocumentationDuty: false,
};
}
if (transparencyDuties) {
return {
riskClass: 'limited',
reasons: transparencyReasons(answers, pack),
gpaiObligations: answers.isGPAIModel,
transparencyDuties: true,
art64DocumentationDuty: false,
};
}
return {
riskClass: 'minimal',
reasons: [
'Keine verbotene Praktik, kein Hochrisiko-Bereich, keine Transparenzpflicht-Auslöser — minimales Risiko. Es gelten die allgemeine KI-Kompetenzpflicht (Art. 4) und freiwillige Verhaltenskodizes.',
],
gpaiObligations: answers.isGPAIModel,
transparencyDuties: false,
art64DocumentationDuty: false,
};
}
/** Erzeugt Begründungstexte für zutreffende Art.-50-Auslöser. */
function transparencyReasons(answers: AssessmentAnswers, pack: ContentPack): string[] {
return answers.transparencyTriggers.flatMap((id) => {
const item = pack.transparencyTriggers.find((t) => t.id === id);
return item ? [`Transparenzpflicht: ${item.label} (${item.legalRef})`] : [];
});
}

View File

@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { CONTENT_PACK } from '../content/pack';
import { classify } from './classify';
import { actionPlan, annexIVSkeleton, inventoryReport, riskReport } from './documents';
import { deriveObligations } from './obligations';
import type { AISystem, AssessmentAnswers } from './types';
const answers: AssessmentAnswers = {
isAISystem: true,
role: 'provider',
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: ['a3-employment'],
art63Exception: false,
involvesProfiling: true,
isAnnexISafetyComponent: false,
transparencyTriggers: ['tr-chatbot'],
};
const system: AISystem = {
id: 'sys-1',
name: 'HR-Screening',
purpose: 'Vorauswahl von Bewerbungen',
vendor: 'Beispiel GmbH',
owner: 'Jane Doe',
createdAt: '2026-07-01T00:00:00.000Z',
updatedAt: '2026-07-01T00:00:00.000Z',
answers,
classification: classify(answers, CONTENT_PACK),
obligationStatus: { 'ob-risk-mgmt': 'in-progress' },
};
const TODAY = '2026-07-04';
describe('documents', () => {
it('riskReport enthält Klassifizierung, Begründung und Disclaimer', () => {
const md = riskReport(system, CONTENT_PACK, TODAY);
expect(md).toContain('# Risikoklassifizierung');
expect(md).toContain('Hochrisiko');
expect(md).toContain('Annex III');
expect(md).toContain('keine Rechtsberatung');
expect(md).toContain(CONTENT_PACK.version);
});
it('actionPlan listet Pflichten mit Status und Frist', () => {
const obligations = deriveObligations(system.classification, answers, CONTENT_PACK);
const md = actionPlan(system, obligations, CONTENT_PACK, TODAY);
expect(md).toContain('Risikomanagementsystem');
expect(md).toContain('In Umsetzung');
expect(md).toContain('02.12.2027');
});
it('annexIVSkeleton befüllt bekannte Felder und markiert offene Punkte', () => {
const md = annexIVSkeleton(system, CONTENT_PACK, TODAY);
expect(md).toContain('Vorauswahl von Bewerbungen');
expect(md).toContain('_[AUSFÜLLEN]_');
expect(md).toContain('Annex IV');
});
it('inventoryReport listet alle Systeme tabellarisch', () => {
const md = inventoryReport([system], CONTENT_PACK, TODAY);
expect(md).toContain('HR-Screening');
expect(md).toContain('| System |');
expect(md).toContain('Registrierte KI-Systeme: **1**');
});
});

View File

@ -0,0 +1,217 @@
import type { AISystem, ContentPack, Obligation } from './types';
const RISK_LABELS: Record<string, string> = {
'out-of-scope': 'Nicht anwendbar (keine KI i. S. d. Art. 3 Nr. 1)',
prohibited: 'VERBOTEN (Art. 5)',
high: 'Hochrisiko',
limited: 'Begrenztes Risiko (Transparenzpflichten)',
minimal: 'Minimales Risiko',
};
const ROLE_LABELS: Record<string, string> = {
provider: 'Anbieter (Provider)',
deployer: 'Betreiber (Deployer)',
importer: 'Einführer (Importer)',
distributor: 'Händler (Distributor)',
};
const STATUS_LABELS: Record<string, string> = {
open: 'Offen',
'in-progress': 'In Umsetzung',
done: 'Erledigt',
'not-applicable': 'Nicht anwendbar',
};
const DISCLAIMER =
'> **Hinweis:** Dieses Dokument wurde mit dem AI-Act-Kompass erstellt und ist eine strukturierte Arbeitshilfe — **keine Rechtsberatung**. Die rechtliche Bewertung im Einzelfall obliegt dem Unternehmen bzw. seiner Rechtsberatung.';
/** Formatiert ein ISO-Datum als deutsches Datum (TT.MM.JJJJ). */
function formatDate(iso: string): string {
const [y, m, d] = iso.split('-');
return d && m && y ? `${d}.${m}.${y}` : iso;
}
function header(title: string, system: AISystem, pack: ContentPack, today: string): string {
return [
`# ${title}`,
'',
`| | |`,
`|---|---|`,
`| **KI-System** | ${system.name} |`,
`| **Zweckbestimmung** | ${system.purpose || '—'} |`,
`| **Hersteller/Anbieter** | ${system.vendor || '—'} |`,
`| **Verantwortlich (intern)** | ${system.owner || '—'} |`,
`| **Rolle des Unternehmens** | ${ROLE_LABELS[system.answers.role] ?? system.answers.role} |`,
`| **Risikoklasse** | ${RISK_LABELS[system.classification.riskClass] ?? system.classification.riskClass} |`,
`| **Erstellt am** | ${formatDate(today)} |`,
`| **Content-Pack** | v${pack.version} (${pack.legalBasis}) |`,
'',
DISCLAIMER,
'',
].join('\n');
}
/**
* Erzeugt den Risikoklassifizierungs-Bericht für ein System als Markdown.
*
* @param system - Bewertetes KI-System
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
*/
export function riskReport(system: AISystem, pack: ContentPack, today: string): string {
const lines = [header('Risikoklassifizierung nach EU AI Act', system, pack, today)];
lines.push('## Ergebnis', '');
lines.push(`**${RISK_LABELS[system.classification.riskClass] ?? system.classification.riskClass}**`, '');
lines.push('## Begründung', '');
for (const reason of system.classification.reasons) {
lines.push(`- ${reason}`);
}
lines.push('');
if (system.classification.gpaiObligations) {
lines.push('## GPAI', '', 'Für dieses System sind zusätzlich GPAI-Modellpflichten (Art. 53 ff.) relevant.', '');
}
if (system.classification.art64DocumentationDuty) {
lines.push(
'## Dokumentationspflicht Art. 6 Abs. 4',
'',
'Die Inanspruchnahme der Ausnahme nach Art. 6 Abs. 3 wurde dokumentiert. Diese Einschätzung ist auf Verlangen der Marktüberwachung vorzulegen.',
'',
);
}
return lines.join('\n');
}
/**
* Erzeugt den Maßnahmenplan (Pflichten-Checkliste) als Markdown.
*
* @param system - Bewertetes KI-System
* @param obligations - Anwendbare Pflichten
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
*/
export function actionPlan(
system: AISystem,
obligations: readonly Obligation[],
pack: ContentPack,
today: string,
): string {
const lines = [header('Maßnahmenplan EU AI Act', system, pack, today)];
if (obligations.length === 0) {
lines.push('Für dieses System bestehen keine umsetzbaren Pflichten (verboten oder außerhalb des Anwendungsbereichs).');
return lines.join('\n');
}
lines.push('## Pflichten', '');
lines.push('| Pflicht | Rechtsgrundlage | Frist | Status |');
lines.push('|---|---|---|---|');
for (const ob of obligations) {
const status = system.obligationStatus[ob.id] ?? 'open';
lines.push(`| ${ob.title} | ${ob.articles} | ${formatDate(ob.deadline)} | ${STATUS_LABELS[status]} |`);
}
lines.push('', '## Details', '');
for (const ob of obligations) {
lines.push(`### ${ob.title} (${ob.articles})`, '', ob.description, '');
}
return lines.join('\n');
}
/**
* Erzeugt das Grundgerüst der technischen Dokumentation nach Annex IV.
*
* Vorbefüllte Abschnitte stammen aus den Wizard-Antworten; offene Punkte
* sind als Platzhalter markiert.
*
* @param system - Bewertetes KI-System
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
*/
export function annexIVSkeleton(system: AISystem, pack: ContentPack, today: string): string {
const todo = '_[AUSFÜLLEN]_';
const lines = [header('Technische Dokumentation (Annex IV) — Grundgerüst', system, pack, today)];
lines.push(
'## 1. Allgemeine Beschreibung des KI-Systems',
'',
`- **Zweckbestimmung:** ${system.purpose || todo}`,
`- **Anbieter:** ${system.vendor || todo}`,
`- **Versionen und Datum des Inverkehrbringens:** ${todo}`,
`- **Hardware-Anforderungen / Bereitstellungsform:** ${todo}`,
`- **Interaktion mit externer Hard-/Software:** ${todo}`,
'',
'## 2. Detaillierte Beschreibung der Elemente und des Entwicklungsprozesses',
'',
`- **Entwicklungsmethoden und verwendete Werkzeuge:** ${todo}`,
`- **Systemarchitektur und Rechenressourcen:** ${todo}`,
`- **Datenanforderungen (Trainings-, Validierungs-, Testdaten, Herkunft, Aufbereitung):** ${todo}`,
`- **Maßnahmen der menschlichen Aufsicht (Art. 14):** ${todo}`,
`- **Vorab festgelegte Änderungen und Kontinuität der Konformität:** ${todo}`,
'',
'## 3. Überwachung, Funktionsweise und Kontrolle',
'',
`- **Fähigkeiten und Leistungsgrenzen (Genauigkeitsmetriken):** ${todo}`,
`- **Vorhersehbare unbeabsichtigte Ergebnisse und Risikoquellen:** ${todo}`,
`- **Eingabedaten-Spezifikation:** ${todo}`,
'',
'## 4. Risikomanagementsystem (Art. 9)',
'',
`- **Beschreibung des Risikomanagementprozesses:** ${todo}`,
'',
'## 5. Änderungen über den Lebenszyklus',
'',
`- **Änderungshistorie und Update-Prozess:** ${todo}`,
'',
'## 6. Angewandte harmonisierte Normen und Spezifikationen',
'',
`- **Liste der Normen (z. B. ISO/IEC 42001, ISO/IEC 23894):** ${todo}`,
'',
'## 7. EU-Konformitätserklärung',
'',
`- **Kopie der Konformitätserklärung (Art. 47):** ${todo}`,
'',
'## 8. Bewertung nach dem Inverkehrbringen (Art. 72)',
'',
`- **Plan zur Beobachtung nach dem Inverkehrbringen:** ${todo}`,
);
return lines.join('\n');
}
/**
* Erzeugt das KI-Inventar über alle registrierten Systeme als Markdown.
*
* @param systems - Alle registrierten KI-Systeme
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
*/
export function inventoryReport(
systems: readonly AISystem[],
pack: ContentPack,
today: string,
): string {
const lines = [
'# KI-Inventar (AI Asset Register)',
'',
`Stand: ${formatDate(today)} · Content-Pack v${pack.version}`,
'',
DISCLAIMER,
'',
`Registrierte KI-Systeme: **${systems.length}**`,
'',
'| System | Zweck | Rolle | Risikoklasse | Verantwortlich |',
'|---|---|---|---|---|',
];
for (const s of systems) {
lines.push(
`| ${s.name} | ${s.purpose || '—'} | ${ROLE_LABELS[s.answers.role] ?? s.answers.role} | ${
RISK_LABELS[s.classification.riskClass] ?? s.classification.riskClass
} | ${s.owner || '—'} |`,
);
}
return lines.join('\n');
}

View File

@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest';
import { CONTENT_PACK } from '../content/pack';
import { classify } from './classify';
import { deriveObligations, nextDeadline } from './obligations';
import type { AssessmentAnswers } from './types';
const base: AssessmentAnswers = {
isAISystem: true,
role: 'deployer',
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: [],
art63Exception: false,
involvesProfiling: false,
isAnnexISafetyComponent: false,
transparencyTriggers: [],
};
function obligationsFor(answers: AssessmentAnswers): readonly string[] {
const classification = classify(answers, CONTENT_PACK);
return deriveObligations(classification, answers, CONTENT_PACK).map((o) => o.id);
}
describe('deriveObligations', () => {
it('liefert keine Pflichten für verbotene Systeme', () => {
expect(obligationsFor({ ...base, prohibitedPractices: ['proh-social-scoring'] })).toEqual([]);
});
it('liefert keine Pflichten für out-of-scope', () => {
expect(obligationsFor({ ...base, isAISystem: false })).toEqual([]);
});
it('liefert Anbieter-Pflichten für Hochrisiko-Anbieter (Art. 917, 43 ff.)', () => {
const ids = obligationsFor({
...base,
role: 'provider',
annexIIICategories: ['a3-employment'],
});
expect(ids).toContain('ob-risk-mgmt');
expect(ids).toContain('ob-tech-doc');
expect(ids).toContain('ob-conformity');
expect(ids).not.toContain('ob-deployer-oversight');
});
it('liefert Betreiber-Pflichten für Hochrisiko-Betreiber (Art. 26)', () => {
const ids = obligationsFor({ ...base, annexIIICategories: ['a3-employment'] });
expect(ids).toContain('ob-deployer-oversight');
expect(ids).toContain('ob-deployer-workers');
expect(ids).not.toContain('ob-risk-mgmt');
});
it('liefert Transparenzpflicht für Chatbots (limited)', () => {
const ids = obligationsFor({ ...base, transparencyTriggers: ['tr-chatbot'] });
expect(ids).toContain('ob-transparency-art50');
expect(ids).toContain('ob-ai-literacy');
});
it('liefert GPAI-Pflichten nur für Anbieter mit GPAI-Flag', () => {
expect(obligationsFor({ ...base, role: 'provider', isGPAIModel: true })).toContain('ob-gpai');
expect(obligationsFor({ ...base, isGPAIModel: true })).not.toContain('ob-gpai');
expect(obligationsFor({ ...base, role: 'provider' })).not.toContain('ob-gpai');
});
it('sortiert Pflichten nach Frist aufsteigend', () => {
const answers: AssessmentAnswers = {
...base,
annexIIICategories: ['a3-employment'],
transparencyTriggers: ['tr-chatbot'],
};
const classification = classify(answers, CONTENT_PACK);
const deadlines = deriveObligations(classification, answers, CONTENT_PACK).map(
(o) => o.deadline,
);
expect(deadlines).toEqual([...deadlines].sort());
});
});
describe('nextDeadline', () => {
it('findet die nächste bevorstehende Frist', () => {
const answers: AssessmentAnswers = { ...base, annexIIICategories: ['a3-employment'] };
const classification = classify(answers, CONTENT_PACK);
const obligations = deriveObligations(classification, answers, CONTENT_PACK);
expect(nextDeadline(obligations, '2026-07-04')).toBe('2027-12-02');
});
it('liefert null, wenn alle Fristen verstrichen sind', () => {
const answers: AssessmentAnswers = { ...base, annexIIICategories: ['a3-employment'] };
const classification = classify(answers, CONTENT_PACK);
const obligations = deriveObligations(classification, answers, CONTENT_PACK);
expect(nextDeadline(obligations, '2030-01-01')).toBeNull();
});
});

View File

@ -0,0 +1,55 @@
import type { AssessmentAnswers, Classification, ContentPack, Obligation } from './types';
/**
* Leitet die anwendbaren Pflichten für ein klassifiziertes System ab.
*
* Auswahlkriterien je Pflicht: Risikoklasse und Rolle müssen passen;
* GPAI- und Transparenzpflichten werden nur bei entsprechendem Flag
* aufgenommen.
*
* @param classification - Ergebnis von {@link classify}
* @param answers - Wizard-Antworten (Rolle, GPAI-Flag)
* @param pack - Regulatorischer Content-Pack
* @returns Deduplizierte, nach Frist sortierte Pflichtenliste
*/
export function deriveObligations(
classification: Classification,
answers: AssessmentAnswers,
pack: ContentPack,
): readonly Obligation[] {
if (classification.riskClass === 'out-of-scope' || classification.riskClass === 'prohibited') {
return [];
}
const applicable = pack.obligations.filter((ob) => {
if (ob.id === 'ob-gpai') {
return classification.gpaiObligations && answers.role === 'provider';
}
if (ob.id === 'ob-transparency-art50') {
return classification.transparencyDuties && ob.roles.includes(answers.role);
}
return (
ob.riskClasses.includes(classification.riskClass) && ob.roles.includes(answers.role)
);
});
return [...applicable].sort((a, b) => a.deadline.localeCompare(b.deadline));
}
/**
* Ermittelt die nächste noch bevorstehende Geltungsfrist für ein System.
*
* @param obligations - Anwendbare Pflichten des Systems
* @param today - Stichtag im ISO-Format (YYYY-MM-DD)
* @returns Früheste Frist nach dem Stichtag, oder null wenn alle verstrichen
*/
export function nextDeadline(
obligations: readonly Obligation[],
today: string,
): string | null {
const upcoming = obligations
.map((ob) => ob.deadline)
.filter((d) => d >= today)
.sort();
return upcoming[0] ?? null;
}

View File

@ -0,0 +1,111 @@
/**
* Kern-Typen des AI-Act-Kompass.
*
* Alle Datenstrukturen sind unveränderlich gedacht (immutable)
* Funktionen geben neue Objekte zurück statt zu mutieren.
*/
/** Rolle des Unternehmens in Bezug auf das KI-System (Art. 3 AI Act). */
export type Role = 'provider' | 'deployer' | 'importer' | 'distributor';
/** Risikoklasse nach EU AI Act. */
export type RiskClass =
| 'out-of-scope'
| 'prohibited'
| 'high'
| 'limited'
| 'minimal';
/** Antworten des Bewertungs-Wizards für ein einzelnes KI-System. */
export interface AssessmentAnswers {
/** Erfüllt das System die KI-Definition nach Art. 3 Nr. 1? */
readonly isAISystem: boolean;
/** Rolle des Unternehmens für dieses System. */
readonly role: Role;
/** Handelt es sich (auch) um ein GPAI-Modell (Art. 51 ff.)? */
readonly isGPAIModel: boolean;
/** IDs der zutreffenden verbotenen Praktiken (Art. 5). */
readonly prohibitedPractices: readonly string[];
/** IDs der zutreffenden Annex-III-Hochrisiko-Bereiche. */
readonly annexIIICategories: readonly string[];
/** Greift die Ausnahme nach Art. 6 Abs. 3 (eng begrenzte Aufgabe)? */
readonly art63Exception: boolean;
/** Führt das System Profiling natürlicher Personen durch? */
readonly involvesProfiling: boolean;
/** Sicherheitsbauteil eines regulierten Produkts (Annex I)? */
readonly isAnnexISafetyComponent: boolean;
/** IDs der zutreffenden Transparenzpflichten-Auslöser (Art. 50). */
readonly transparencyTriggers: readonly string[];
}
/** Ergebnis der Risikoklassifizierung. */
export interface Classification {
readonly riskClass: RiskClass;
/** Begründungen in Prüfreihenfolge (menschenlesbar, Deutsch). */
readonly reasons: readonly string[];
/** Zusätzlich GPAI-Pflichten (Art. 53 ff.) relevant? */
readonly gpaiObligations: boolean;
/** Transparenzpflichten nach Art. 50 zusätzlich anwendbar? */
readonly transparencyDuties: boolean;
/** Dokumentationspflicht nach Art. 6 Abs. 4 (Ausnahme in Anspruch genommen)? */
readonly art64DocumentationDuty: boolean;
}
/** Eine konkrete Pflicht aus dem AI Act. */
export interface Obligation {
readonly id: string;
readonly title: string;
readonly articles: string;
readonly description: string;
/** ISO-Datum der maßgeblichen Geltungsfrist. */
readonly deadline: string;
readonly roles: readonly Role[];
readonly riskClasses: readonly RiskClass[];
}
/** Status einer Pflicht in der Umsetzung durch den Nutzer. */
export type ObligationStatus = 'open' | 'in-progress' | 'done' | 'not-applicable';
/** Ein registriertes KI-System im Inventar. */
export interface AISystem {
readonly id: string;
readonly name: string;
readonly purpose: string;
readonly vendor: string;
readonly owner: string;
readonly createdAt: string;
readonly updatedAt: string;
readonly answers: AssessmentAnswers;
readonly classification: Classification;
/** Umsetzungsstatus je Pflicht-ID. */
readonly obligationStatus: Readonly<Record<string, ObligationStatus>>;
}
/** Eintrag im regulatorischen Content-Pack (Checklisten-Item mit Rechtsgrundlage). */
export interface ContentItem {
readonly id: string;
readonly label: string;
readonly legalRef: string;
readonly hint?: string;
}
/** Wichtige Geltungsfrist des AI Act. */
export interface Deadline {
readonly id: string;
readonly date: string;
readonly label: string;
readonly description: string;
}
/** Versionierter regulatorischer Content-Pack. */
export interface ContentPack {
readonly version: string;
readonly releasedAt: string;
readonly legalBasis: string;
readonly prohibitedPractices: readonly ContentItem[];
readonly annexIIICategories: readonly ContentItem[];
readonly transparencyTriggers: readonly ContentItem[];
readonly aiDefinitionCriteria: readonly ContentItem[];
readonly obligations: readonly Obligation[];
readonly deadlines: readonly Deadline[];
}

View File

@ -0,0 +1,15 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './ui/App';
import './styles.css';
const container = document.getElementById('root');
if (!container) {
throw new Error('Root-Element #root nicht gefunden.');
}
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@ -0,0 +1,75 @@
import type { AISystem } from '../engine/types';
const STORAGE_KEY = 'ai-act-kompass/systems/v1';
/** Format der Export-/Import-Datei. */
export interface ExportFile {
readonly app: 'ai-act-kompass';
readonly schemaVersion: 1;
readonly exportedAt: string;
readonly systems: readonly AISystem[];
}
/**
* Lädt alle registrierten KI-Systeme aus dem lokalen Speicher.
* Bei defekten Daten wird eine leere Liste zurückgegeben (fail-safe).
*/
export function loadSystems(): readonly AISystem[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as AISystem[]) : [];
} catch {
return [];
}
}
/**
* Persistiert die Systemliste im lokalen Speicher des Browsers.
* Es verlassen keine Daten das Gerät.
*/
export function saveSystems(systems: readonly AISystem[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(systems));
}
/**
* Serialisiert alle Daten als Export-Datei (JSON) für Backup/Umzug.
*
* @param systems - Zu exportierende Systeme
* @param exportedAt - Zeitstempel im ISO-Format
*/
export function exportData(systems: readonly AISystem[], exportedAt: string): string {
const file: ExportFile = {
app: 'ai-act-kompass',
schemaVersion: 1,
exportedAt,
systems,
};
return JSON.stringify(file, null, 2);
}
/**
* Liest eine Export-Datei ein und validiert das Grundformat.
*
* @param json - Inhalt der Export-Datei
* @returns Systeme aus der Datei
* @throws Error bei unbekanntem oder defektem Format
*/
export function importData(json: string): readonly AISystem[] {
const parsed: unknown = JSON.parse(json);
if (
typeof parsed !== 'object' ||
parsed === null ||
(parsed as ExportFile).app !== 'ai-act-kompass' ||
!Array.isArray((parsed as ExportFile).systems)
) {
throw new Error('Unbekanntes Dateiformat — keine gültige AI-Act-Kompass-Exportdatei.');
}
return (parsed as ExportFile).systems;
}
/** Löscht alle lokal gespeicherten Daten unwiderruflich. */
export function clearAll(): void {
localStorage.removeItem(STORAGE_KEY);
}

View File

@ -0,0 +1,190 @@
:root {
--bg: #f5f6f8;
--panel: #ffffff;
--ink: #1a2233;
--muted: #5b6577;
--line: #e3e6ec;
--accent: #1f4fd8;
--accent-ink: #ffffff;
--danger: #b3261e;
--warn: #9a6700;
--ok: #1a7f37;
--radius: 10px;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); }
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 240px;
flex-shrink: 0;
background: #101a33;
color: #d7deee;
padding: 20px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar .brand {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 2px;
}
.sidebar .brand small { display: block; font-size: 11px; font-weight: 400; color: #8fa0c4; margin-top: 4px; }
.sidebar nav { display: flex; flex-direction: column; gap: 2px; margin-top: 18px; }
.sidebar button {
text-align: left;
background: none;
border: none;
color: #c3cde3;
padding: 9px 12px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.sidebar button:hover { background: rgba(255, 255, 255, 0.08); }
.sidebar button.active { background: var(--accent); color: #fff; }
.sidebar .foot { margin-top: auto; font-size: 11px; color: #7787ab; line-height: 1.5; }
.main { flex: 1; padding: 28px 36px; max-width: 1040px; }
h1 { font-size: 22px; margin: 0 0 4px; }
h2 { font-size: 16px; margin: 22px 0 10px; }
.sub { color: var(--muted); font-size: 14px; margin: 0 0 20px; }
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 18px 20px;
margin-bottom: 16px;
}
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
.stat { text-align: left; }
.stat .num { font-size: 28px; font-weight: 700; }
.stat .lbl { font-size: 13px; color: var(--muted); }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
th, td { padding: 9px 10px; border-bottom: 1px solid var(--line); }
tr.clickable { cursor: pointer; }
tr.clickable:hover td { background: #f0f3fa; }
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 99px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.badge.high { background: #fde8e7; color: var(--danger); }
.badge.prohibited { background: var(--danger); color: #fff; }
.badge.limited { background: #fff3d6; color: var(--warn); }
.badge.minimal { background: #e2f5e8; color: var(--ok); }
.badge.out-of-scope { background: #e8eaf0; color: var(--muted); }
.badge.status-open { background: #fde8e7; color: var(--danger); }
.badge.status-in-progress { background: #fff3d6; color: var(--warn); }
.badge.status-done { background: #e2f5e8; color: var(--ok); }
.badge.status-not-applicable { background: #e8eaf0; color: var(--muted); }
button.primary, button.ghost, button.danger {
border-radius: 8px;
padding: 9px 16px;
font-size: 14px;
cursor: pointer;
border: 1px solid transparent;
}
button.primary { background: var(--accent); color: var(--accent-ink); }
button.primary:hover { filter: brightness(1.1); }
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
button.ghost { background: #fff; border-color: var(--line); color: var(--ink); }
button.ghost:hover { border-color: var(--accent); color: var(--accent); }
button.danger { background: #fff; border-color: var(--danger); color: var(--danger); }
label.field { display: block; margin-bottom: 14px; font-size: 14px; }
label.field span { display: block; font-weight: 600; margin-bottom: 5px; }
input[type='text'], select, textarea {
width: 100%;
padding: 9px 11px;
border: 1px solid var(--line);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
background: #fff;
color: var(--ink);
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.check {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
font-size: 14px;
}
.check:hover { border-color: var(--accent); }
.check input { margin-top: 3px; }
.check .ref { display: block; font-size: 12px; color: var(--muted); margin-top: 2px; }
.wizard-steps { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }
.wizard-steps .step {
font-size: 12px;
padding: 5px 11px;
border-radius: 99px;
background: #e8eaf0;
color: var(--muted);
}
.wizard-steps .step.current { background: var(--accent); color: #fff; }
.wizard-steps .step.done { background: #cdd8f6; color: var(--accent); }
.wizard-nav { display: flex; justify-content: space-between; margin-top: 20px; }
.reasons li { margin-bottom: 6px; font-size: 14px; }
.hint { font-size: 13px; color: var(--muted); }
.disclaimer {
font-size: 12px;
color: var(--muted);
border-left: 3px solid var(--warn);
padding: 8px 12px;
background: #fffaf0;
border-radius: 0 8px 8px 0;
margin-top: 20px;
}
.deadline-list { list-style: none; padding: 0; margin: 0; }
.deadline-list li {
display: flex;
gap: 12px;
padding: 9px 0;
border-bottom: 1px solid var(--line);
font-size: 14px;
align-items: baseline;
}
.deadline-list li:last-child { border-bottom: none; }
.deadline-list .date { font-weight: 700; white-space: nowrap; min-width: 92px; }
.deadline-list .past { color: var(--muted); }
.doc-preview {
background: #0f172a;
color: #dbe4f5;
border-radius: 8px;
padding: 16px;
font-size: 12.5px;
overflow-x: auto;
white-space: pre-wrap;
max-height: 420px;
overflow-y: auto;
}
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 16px; }
.empty { text-align: center; color: var(--muted); padding: 40px 0; font-size: 14px; }

View File

@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import type { AISystem } from '../engine/types';
import { loadSystems, saveSystems } from '../store/storage';
import { Dashboard } from './Dashboard';
import { SettingsView } from './SettingsView';
import { SystemDetail } from './SystemDetail';
import { Wizard } from './Wizard';
type View =
| { readonly kind: 'dashboard' }
| { readonly kind: 'wizard'; readonly systemId: string | null }
| { readonly kind: 'detail'; readonly systemId: string }
| { readonly kind: 'settings' };
/** App-Shell: Navigation, State-Verwaltung und Persistenz. */
export function App(): JSX.Element {
const [systems, setSystems] = useState<readonly AISystem[]>(() => loadSystems());
const [view, setView] = useState<View>({ kind: 'dashboard' });
useEffect(() => {
saveSystems(systems);
}, [systems]);
const findSystem = (id: string): AISystem | undefined => systems.find((s) => s.id === id);
const upsert = (system: AISystem): void => {
setSystems((prev) => {
const exists = prev.some((s) => s.id === system.id);
return exists ? prev.map((s) => (s.id === system.id ? system : s)) : [...prev, system];
});
};
const remove = (id: string): void => {
setSystems((prev) => prev.filter((s) => s.id !== id));
setView({ kind: 'dashboard' });
};
return (
<div className="layout">
<aside className="sidebar">
<div className="brand">
🧭 AI-Act-Kompass
<small>EU AI Act Compliance für KMU · local-first</small>
</div>
<nav>
<button
className={view.kind === 'dashboard' ? 'active' : ''}
onClick={() => setView({ kind: 'dashboard' })}
>
Dashboard & Inventar
</button>
<button
className={view.kind === 'wizard' ? 'active' : ''}
onClick={() => setView({ kind: 'wizard', systemId: null })}
>
Neues System bewerten
</button>
<button
className={view.kind === 'settings' ? 'active' : ''}
onClick={() => setView({ kind: 'settings' })}
>
Einstellungen & Backup
</button>
</nav>
<div className="foot">
Keine Cloud. Keine Telemetrie.
<br />
Alle Daten bleiben auf diesem Gerät.
<br />
<br />
Keine Rechtsberatung.
</div>
</aside>
<main className="main">
{view.kind === 'dashboard' && (
<Dashboard
systems={systems}
onOpen={(id) => setView({ kind: 'detail', systemId: id })}
onNew={() => setView({ kind: 'wizard', systemId: null })}
/>
)}
{view.kind === 'wizard' && (
<Wizard
existing={view.systemId ? (findSystem(view.systemId) ?? null) : null}
onSave={(system) => {
upsert(system);
setView({ kind: 'detail', systemId: system.id });
}}
onCancel={() => setView({ kind: 'dashboard' })}
/>
)}
{view.kind === 'detail' &&
(() => {
const system = findSystem(view.systemId);
if (!system) {
return <div className="empty">System nicht gefunden.</div>;
}
return (
<SystemDetail
system={system}
onUpdate={upsert}
onReassess={() => setView({ kind: 'wizard', systemId: system.id })}
onDelete={() => {
if (window.confirm(`${system.name}" wirklich löschen?`)) {
remove(system.id);
}
}}
onBack={() => setView({ kind: 'dashboard' })}
/>
);
})()}
{view.kind === 'settings' && (
<SettingsView
systems={systems}
onImport={(imported) => setSystems(imported)}
onClear={() => setSystems([])}
/>
)}
</main>
</div>
);
}

View File

@ -0,0 +1,127 @@
import { CONTENT_PACK } from '../content/pack';
import { inventoryReport } from '../engine/documents';
import type { AISystem } from '../engine/types';
import { downloadText, RiskBadge, ROLE_LABELS, todayISO } from './common';
interface DashboardProps {
readonly systems: readonly AISystem[];
readonly onOpen: (id: string) => void;
readonly onNew: () => void;
}
/** Übersichtsseite: Kennzahlen, Fristen und das KI-Inventar. */
export function Dashboard({ systems, onOpen, onNew }: DashboardProps): JSX.Element {
const today = todayISO();
const count = (risk: string): number =>
systems.filter((s) => s.classification.riskClass === risk).length;
const openObligations = systems.reduce((acc, s) => {
const done = Object.values(s.obligationStatus).filter(
(st) => st === 'done' || st === 'not-applicable',
).length;
return acc + Math.max(0, Object.keys(s.obligationStatus).length - done);
}, 0);
return (
<div>
<h1>Dashboard</h1>
<p className="sub">
EU-AI-Act-Compliance auf einen Blick. Alle Daten bleiben lokal auf diesem Gerät.
</p>
<div className="panel grid">
<div className="stat">
<div className="num">{systems.length}</div>
<div className="lbl">KI-Systeme im Inventar</div>
</div>
<div className="stat">
<div className="num">{count('high')}</div>
<div className="lbl">Hochrisiko-Systeme</div>
</div>
<div className="stat">
<div className="num">{count('prohibited')}</div>
<div className="lbl">Verbotene Praktiken</div>
</div>
<div className="stat">
<div className="num">{openObligations}</div>
<div className="lbl">Offene Pflichten (begonnen)</div>
</div>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Geltungsfristen des AI Act</h2>
<ul className="deadline-list">
{CONTENT_PACK.deadlines.map((d) => (
<li key={d.id}>
<span className={`date ${d.date < today ? 'past' : ''}`}>
{d.date.split('-').reverse().join('.')}
</span>
<span>
<strong>{d.label}</strong> {d.description}
</span>
</li>
))}
</ul>
</div>
<div className="panel">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}>KI-Inventar</h2>
<div className="toolbar" style={{ margin: 0 }}>
{systems.length > 0 && (
<button
className="ghost"
onClick={() =>
downloadText(`ki-inventar-${today}.md`, inventoryReport(systems, CONTENT_PACK, today))
}
>
Inventar exportieren
</button>
)}
<button className="primary" onClick={onNew}>
+ System bewerten
</button>
</div>
</div>
{systems.length === 0 ? (
<div className="empty">
Noch keine KI-Systeme erfasst. Starten Sie mit der Bewertung Ihres ersten Systems
z. B. Ihres Chatbots, Ihres Bewerber-Screenings oder eines eingekauften KI-Tools.
</div>
) : (
<table>
<thead>
<tr>
<th>System</th>
<th>Rolle</th>
<th>Risikoklasse</th>
<th>Verantwortlich</th>
<th>Zuletzt bewertet</th>
</tr>
</thead>
<tbody>
{systems.map((s) => (
<tr key={s.id} className="clickable" onClick={() => onOpen(s.id)}>
<td>
<strong>{s.name}</strong>
</td>
<td>{ROLE_LABELS[s.answers.role]}</td>
<td>
<RiskBadge risk={s.classification.riskClass} />
</td>
<td>{s.owner || '—'}</td>
<td>{s.updatedAt.slice(0, 10).split('-').reverse().join('.')}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<p className="disclaimer">
Der AI-Act-Kompass ist eine strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689
keine Rechtsberatung. Content-Pack v{CONTENT_PACK.version}.
</p>
</div>
);
}

View File

@ -0,0 +1,98 @@
import { useRef, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import type { AISystem } from '../engine/types';
import { clearAll, exportData, importData } from '../store/storage';
import { downloadText, todayISO } from './common';
interface SettingsViewProps {
readonly systems: readonly AISystem[];
readonly onImport: (systems: readonly AISystem[]) => void;
readonly onClear: () => void;
}
/** Einstellungen: Datensicherung, Import, Löschen, Content-Pack-Info. */
export function SettingsView({ systems, onImport, onClear }: SettingsViewProps): JSX.Element {
const fileInput = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState<string>('');
const handleImport = async (file: File): Promise<void> => {
try {
const imported = importData(await file.text());
onImport(imported);
setMessage(`${imported.length} System(e) importiert.`);
} catch (err) {
setMessage(`⚠️ Import fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`);
}
};
return (
<div>
<h1>Einstellungen</h1>
<p className="sub">Datenhoheit: Alles liegt lokal in diesem Browser kein Cloud-Sync, keine Telemetrie.</p>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Regulatorischer Content-Pack</h2>
<p style={{ fontSize: 14 }}>
Version <strong>{CONTENT_PACK.version}</strong> · Stand {CONTENT_PACK.releasedAt}
<br />
<span className="hint">{CONTENT_PACK.legalBasis}</span>
</p>
<p className="hint">
Der Content-Pack (Verbote, Annex-III-Bereiche, Pflichten, Fristen) ist als austauschbares
Datenpaket modelliert. Updates erscheinen mit neuen App-Versionen bzw. künftig über einen
signierten Update-Feed.
</p>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Datensicherung</h2>
<div className="toolbar">
<button
className="primary"
onClick={() =>
downloadText(
`ai-act-kompass-backup-${todayISO()}.json`,
exportData(systems, new Date().toISOString()),
'application/json',
)
}
>
Backup exportieren (JSON)
</button>
<button className="ghost" onClick={() => fileInput.current?.click()}>
Backup importieren
</button>
<input
ref={fileInput}
type="file"
accept="application/json"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) void handleImport(file);
e.target.value = '';
}}
/>
</div>
{message && <p className="hint">{message}</p>}
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Alle Daten löschen</h2>
<p className="hint">Entfernt alle erfassten KI-Systeme unwiderruflich aus diesem Browser.</p>
<button
className="danger"
onClick={() => {
if (window.confirm('Wirklich alle lokalen Daten unwiderruflich löschen?')) {
clearAll();
onClear();
setMessage('✓ Alle Daten gelöscht.');
}
}}
>
Alles löschen
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,164 @@
import { useMemo, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import { actionPlan, annexIVSkeleton, riskReport } from '../engine/documents';
import { deriveObligations } from '../engine/obligations';
import type { AISystem, ObligationStatus } from '../engine/types';
import { downloadText, RiskBadge, ROLE_LABELS, todayISO } from './common';
interface SystemDetailProps {
readonly system: AISystem;
readonly onUpdate: (system: AISystem) => void;
readonly onReassess: () => void;
readonly onDelete: () => void;
readonly onBack: () => void;
}
const STATUS_OPTIONS: readonly { value: ObligationStatus; label: string }[] = [
{ value: 'open', label: 'Offen' },
{ value: 'in-progress', label: 'In Umsetzung' },
{ value: 'done', label: 'Erledigt' },
{ value: 'not-applicable', label: 'Nicht anwendbar' },
];
type DocKind = 'report' | 'plan' | 'annexIV';
/** Detailansicht eines Systems: Klassifizierung, Pflichten-Checkliste, Dokumente. */
export function SystemDetail({
system,
onUpdate,
onReassess,
onDelete,
onBack,
}: SystemDetailProps): JSX.Element {
const today = todayISO();
const [preview, setPreview] = useState<DocKind | null>(null);
const obligations = useMemo(
() => deriveObligations(system.classification, system.answers, CONTENT_PACK),
[system],
);
const setStatus = (obligationId: string, status: ObligationStatus): void => {
onUpdate({
...system,
updatedAt: new Date().toISOString(),
obligationStatus: { ...system.obligationStatus, [obligationId]: status },
});
};
const docs: Record<DocKind, { label: string; file: string; render: () => string }> = {
report: {
label: 'Risikobericht',
file: `risikobericht-${system.name}-${today}.md`,
render: () => riskReport(system, CONTENT_PACK, today),
},
plan: {
label: 'Maßnahmenplan',
file: `massnahmenplan-${system.name}-${today}.md`,
render: () => actionPlan(system, obligations, CONTENT_PACK, today),
},
annexIV: {
label: 'Annex-IV-Dokumentation',
file: `annex-iv-${system.name}-${today}.md`,
render: () => annexIVSkeleton(system, CONTENT_PACK, today),
},
};
const showAnnexIV = system.classification.riskClass === 'high' && system.answers.role === 'provider';
return (
<div>
<button className="ghost" onClick={onBack}>
Zurück zum Dashboard
</button>
<h1 style={{ marginTop: 14 }}>
{system.name} <RiskBadge risk={system.classification.riskClass} />
</h1>
<p className="sub">
{ROLE_LABELS[system.answers.role]} · {system.purpose || 'Keine Zweckbestimmung erfasst'}
</p>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Begründung der Einstufung</h2>
<ul className="reasons">
{system.classification.reasons.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
<div className="toolbar">
<button className="ghost" onClick={onReassess}>
Neu bewerten
</button>
<button className="danger" onClick={onDelete}>
System löschen
</button>
</div>
</div>
{obligations.length > 0 && (
<div className="panel">
<h2 style={{ marginTop: 0 }}>Pflichten ({obligations.length})</h2>
<table>
<thead>
<tr>
<th>Pflicht</th>
<th>Rechtsgrundlage</th>
<th>Frist</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{obligations.map((ob) => {
const status = system.obligationStatus[ob.id] ?? 'open';
return (
<tr key={ob.id}>
<td>
<strong>{ob.title}</strong>
<div className="hint">{ob.description}</div>
</td>
<td>{ob.articles}</td>
<td>{ob.deadline.split('-').reverse().join('.')}</td>
<td>
<select
value={status}
onChange={(e) => setStatus(ob.id, e.target.value as ObligationStatus)}
>
{STATUS_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
<div className="panel">
<h2 style={{ marginTop: 0 }}>Dokumente generieren</h2>
<div className="toolbar">
{(Object.keys(docs) as DocKind[])
.filter((k) => k !== 'annexIV' || showAnnexIV)
.map((kind) => (
<span key={kind} style={{ display: 'flex', gap: 4 }}>
<button className="ghost" onClick={() => setPreview(preview === kind ? null : kind)}>
{docs[kind].label} {preview === kind ? 'ausblenden' : 'ansehen'}
</button>
<button
className="primary"
onClick={() => downloadText(docs[kind].file, docs[kind].render())}
>
.md
</button>
</span>
))}
</div>
{preview && <pre className="doc-preview">{docs[preview].render()}</pre>}
</div>
</div>
);
}

View File

@ -0,0 +1,355 @@
import { useMemo, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import { classify } from '../engine/classify';
import type { AISystem, AssessmentAnswers, ContentItem, Role } from '../engine/types';
import { RiskBadge, ROLE_LABELS } from './common';
const STEPS = ['Grunddaten', 'KI-Definition', 'Verbote', 'Hochrisiko', 'Transparenz & GPAI', 'Ergebnis'] as const;
interface WizardProps {
/** Bestehendes System bei Neubewertung, sonst null. */
readonly existing: AISystem | null;
readonly onSave: (system: AISystem) => void;
readonly onCancel: () => void;
}
interface Draft {
name: string;
purpose: string;
vendor: string;
owner: string;
role: Role;
defCriteria: readonly string[];
isGPAIModel: boolean;
prohibitedPractices: readonly string[];
annexIIICategories: readonly string[];
art63Exception: boolean;
involvesProfiling: boolean;
isAnnexISafetyComponent: boolean;
transparencyTriggers: readonly string[];
}
function initialDraft(existing: AISystem | null): Draft {
if (!existing) {
return {
name: '',
purpose: '',
vendor: '',
owner: '',
role: 'deployer',
defCriteria: CONTENT_PACK.aiDefinitionCriteria.map((c) => c.id),
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: [],
art63Exception: false,
involvesProfiling: false,
isAnnexISafetyComponent: false,
transparencyTriggers: [],
};
}
const a = existing.answers;
return {
name: existing.name,
purpose: existing.purpose,
vendor: existing.vendor,
owner: existing.owner,
role: a.role,
defCriteria: a.isAISystem ? CONTENT_PACK.aiDefinitionCriteria.map((c) => c.id) : [],
isGPAIModel: a.isGPAIModel,
prohibitedPractices: a.prohibitedPractices,
annexIIICategories: a.annexIIICategories,
art63Exception: a.art63Exception,
involvesProfiling: a.involvesProfiling,
isAnnexISafetyComponent: a.isAnnexISafetyComponent,
transparencyTriggers: a.transparencyTriggers,
};
}
function toggle(list: readonly string[], id: string): readonly string[] {
return list.includes(id) ? list.filter((x) => x !== id) : [...list, id];
}
function CheckList({
items,
selected,
onToggle,
}: {
readonly items: readonly ContentItem[];
readonly selected: readonly string[];
readonly onToggle: (id: string) => void;
}): JSX.Element {
return (
<div>
{items.map((item) => (
<label key={item.id} className="check">
<input
type="checkbox"
checked={selected.includes(item.id)}
onChange={() => onToggle(item.id)}
/>
<span>
{item.label}
<span className="ref">
{item.legalRef}
{item.hint ? `${item.hint}` : ''}
</span>
</span>
</label>
))}
</div>
);
}
/** Mehrstufiger Bewertungs-Wizard: erfasst ein KI-System und klassifiziert es. */
export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element {
const [step, setStep] = useState(0);
const [draft, setDraft] = useState<Draft>(() => initialDraft(existing));
const isAISystem = draft.defCriteria.length === CONTENT_PACK.aiDefinitionCriteria.length;
const answers: AssessmentAnswers = useMemo(
() => ({
isAISystem,
role: draft.role,
isGPAIModel: draft.isGPAIModel,
prohibitedPractices: draft.prohibitedPractices,
annexIIICategories: draft.annexIIICategories,
art63Exception: draft.art63Exception,
involvesProfiling: draft.involvesProfiling,
isAnnexISafetyComponent: draft.isAnnexISafetyComponent,
transparencyTriggers: draft.transparencyTriggers,
}),
[draft, isAISystem],
);
const classification = useMemo(() => classify(answers, CONTENT_PACK), [answers]);
const canNext = step !== 0 || draft.name.trim().length > 0;
const save = (): void => {
const now = new Date().toISOString();
onSave({
id: existing?.id ?? crypto.randomUUID(),
name: draft.name.trim(),
purpose: draft.purpose.trim(),
vendor: draft.vendor.trim(),
owner: draft.owner.trim(),
createdAt: existing?.createdAt ?? now,
updatedAt: now,
answers,
classification,
obligationStatus: existing?.obligationStatus ?? {},
});
};
return (
<div>
<h1>{existing ? `Neubewertung: ${existing.name}` : 'Neues KI-System bewerten'}</h1>
<p className="sub">Beantworten Sie die Fragen die Risikoklasse wird automatisch nach der Systematik des AI Act ermittelt.</p>
<div className="wizard-steps">
{STEPS.map((label, i) => (
<span key={label} className={`step ${i === step ? 'current' : i < step ? 'done' : ''}`}>
{i + 1}. {label}
</span>
))}
</div>
<div className="panel">
{step === 0 && (
<>
<label className="field">
<span>Name des Systems *</span>
<input
type="text"
value={draft.name}
placeholder="z. B. Bewerber-Screening-Tool"
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
/>
</label>
<label className="field">
<span>Zweckbestimmung</span>
<textarea
rows={2}
value={draft.purpose}
placeholder="Wofür wird das System eingesetzt?"
onChange={(e) => setDraft({ ...draft, purpose: e.target.value })}
/>
</label>
<label className="field">
<span>Hersteller / Anbieter</span>
<input type="text" value={draft.vendor} onChange={(e) => setDraft({ ...draft, vendor: e.target.value })} />
</label>
<label className="field">
<span>Verantwortliche Person (intern)</span>
<input type="text" value={draft.owner} onChange={(e) => setDraft({ ...draft, owner: e.target.value })} />
</label>
<label className="field">
<span>Rolle Ihres Unternehmens</span>
<select value={draft.role} onChange={(e) => setDraft({ ...draft, role: e.target.value as Role })}>
{(Object.keys(ROLE_LABELS) as Role[]).map((r) => (
<option key={r} value={r}>
{ROLE_LABELS[r]}
</option>
))}
</select>
<span className="hint">
Anbieter = entwickelt/vertreibt das System unter eigenem Namen. Betreiber = setzt ein fremdes System ein (häufigster KMU-Fall).
</span>
</label>
</>
)}
{step === 1 && (
<>
<h2>Erfüllt das System die KI-Definition (Art. 3 Nr. 1)?</h2>
<p className="hint">Nur wenn alle Kriterien zutreffen, fällt das System unter den AI Act.</p>
<CheckList
items={CONTENT_PACK.aiDefinitionCriteria}
selected={draft.defCriteria}
onToggle={(id) => setDraft({ ...draft, defCriteria: toggle(draft.defCriteria, id) })}
/>
{!isAISystem && (
<p className="hint">
Nicht alle Kriterien erfüllt das System gilt nicht als KI-System, der AI Act ist nicht anwendbar.
</p>
)}
</>
)}
{step === 2 && (
<>
<h2>Verbotene Praktiken (Art. 5)</h2>
<p className="hint">Trifft einer dieser Punkte zu, ist der Einsatz seit 2. Februar 2025 verboten.</p>
<CheckList
items={CONTENT_PACK.prohibitedPractices}
selected={draft.prohibitedPractices}
onToggle={(id) => setDraft({ ...draft, prohibitedPractices: toggle(draft.prohibitedPractices, id) })}
/>
</>
)}
{step === 3 && (
<>
<h2>Hochrisiko-Einstufung (Art. 6)</h2>
<label className="check">
<input
type="checkbox"
checked={draft.isAnnexISafetyComponent}
onChange={(e) => setDraft({ ...draft, isAnnexISafetyComponent: e.target.checked })}
/>
<span>
Das System ist Sicherheitsbauteil eines regulierten Produkts (z. B. Medizinprodukt, Maschine, Aufzug) oder selbst ein solches Produkt.
<span className="ref">Art. 6 Abs. 1, Annex I</span>
</span>
</label>
<h2>Einsatzbereiche nach Annex III</h2>
<CheckList
items={CONTENT_PACK.annexIIICategories}
selected={draft.annexIIICategories}
onToggle={(id) => setDraft({ ...draft, annexIIICategories: toggle(draft.annexIIICategories, id) })}
/>
{draft.annexIIICategories.length > 0 && (
<>
<h2>Mögliche Ausnahme (Art. 6 Abs. 3)</h2>
<label className="check">
<input
type="checkbox"
checked={draft.involvesProfiling}
onChange={(e) => setDraft({ ...draft, involvesProfiling: e.target.checked })}
/>
<span>
Das System führt Profiling natürlicher Personen durch.
<span className="ref">Profiling schließt die Ausnahme aus immer Hochrisiko.</span>
</span>
</label>
<label className="check">
<input
type="checkbox"
checked={draft.art63Exception}
onChange={(e) => setDraft({ ...draft, art63Exception: e.target.checked })}
/>
<span>
Das System erfüllt nur eine eng begrenzte verfahrenstechnische oder vorbereitende Aufgabe bzw. verbessert lediglich das Ergebnis einer zuvor abgeschlossenen menschlichen Tätigkeit ohne wesentliches Risiko für Gesundheit, Sicherheit oder Grundrechte.
<span className="ref">Art. 6 Abs. 3 die Einschätzung muss dokumentiert werden (Art. 6 Abs. 4).</span>
</span>
</label>
</>
)}
</>
)}
{step === 4 && (
<>
<h2>Transparenzpflichten (Art. 50)</h2>
<CheckList
items={CONTENT_PACK.transparencyTriggers}
selected={draft.transparencyTriggers}
onToggle={(id) => setDraft({ ...draft, transparencyTriggers: toggle(draft.transparencyTriggers, id) })}
/>
<h2>General-Purpose AI</h2>
<label className="check">
<input
type="checkbox"
checked={draft.isGPAIModel}
onChange={(e) => setDraft({ ...draft, isGPAIModel: e.target.checked })}
/>
<span>
Wir stellen ein eigenes GPAI-Modell bereit (z. B. selbst trainiertes oder wesentlich verändertes Basismodell).
<span className="ref">Art. 51 ff. die bloße Nutzung von z. B. GPT/Claude über API zählt nicht.</span>
</span>
</label>
</>
)}
{step === 5 && (
<>
<h2>
Ergebnis: <RiskBadge risk={classification.riskClass} />
</h2>
<ul className="reasons">
{classification.reasons.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
{classification.gpaiObligations && <p className="hint">Zusätzlich gelten GPAI-Modellpflichten (Art. 53 ff.).</p>}
{classification.riskClass === 'prohibited' && (
<p className="hint">
Der Einsatz dieses Systems ist nach Art. 5 verboten (Bußgeld bis 35 Mio. oder 7 % des weltweiten Jahresumsatzes). Einsatz einstellen und rechtlichen Rat einholen.
</p>
)}
</>
)}
</div>
<div className="wizard-nav">
<div>
<button className="ghost" onClick={onCancel}>
Abbrechen
</button>
</div>
<div style={{ display: 'flex', gap: 8 }}>
{step > 0 && (
<button className="ghost" onClick={() => setStep(step - 1)}>
Zurück
</button>
)}
{step < STEPS.length - 1 && (
<button
className="primary"
disabled={!canNext}
onClick={() => setStep(isAISystem || step === 0 ? step + 1 : STEPS.length - 1)}
>
Weiter
</button>
)}
{step === STEPS.length - 1 && (
<button className="primary" disabled={draft.name.trim().length === 0} onClick={save}>
Speichern
</button>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import type { RiskClass, Role } from '../engine/types';
/** Deutsche Anzeige-Labels für Risikoklassen. */
export const RISK_LABELS: Readonly<Record<RiskClass, string>> = {
'out-of-scope': 'Nicht anwendbar',
prohibited: 'Verboten',
high: 'Hochrisiko',
limited: 'Begrenztes Risiko',
minimal: 'Minimales Risiko',
};
/** Deutsche Anzeige-Labels für Rollen. */
export const ROLE_LABELS: Readonly<Record<Role, string>> = {
provider: 'Anbieter',
deployer: 'Betreiber',
importer: 'Einführer',
distributor: 'Händler',
};
/** Farbiges Badge für die Risikoklasse eines Systems. */
export function RiskBadge({ risk }: { readonly risk: RiskClass }): JSX.Element {
return <span className={`badge ${risk}`}>{RISK_LABELS[risk]}</span>;
}
/**
* Löst einen Browser-Download für generierten Text aus.
*
* @param filename - Zieldateiname
* @param content - Dateiinhalt
* @param mime - MIME-Type (Default: Markdown)
*/
export function downloadText(filename: string, content: string, mime = 'text/markdown'): void {
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
/** Heutiges Datum im ISO-Format (YYYY-MM-DD). */
export function todayISO(): string {
return new Date().toISOString().slice(0, 10);
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: './',
build: {
outDir: 'dist',
sourcemap: false,
},
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
});