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:
parent
874e4a1ead
commit
fb2da0db00
3
.gitignore
vendored
3
.gitignore
vendored
@ -34,3 +34,6 @@ docker-compose.override.yml
|
|||||||
app/.next/
|
app/.next/
|
||||||
dist/
|
dist/
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# TS incremental build
|
||||||
|
*.tsbuildinfo
|
||||||
|
|||||||
38
ai-act-kompass/README.md
Normal file
38
ai-act-kompass/README.md
Normal 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
13
ai-act-kompass/index.html
Normal 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
2135
ai-act-kompass/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
ai-act-kompass/package.json
Normal file
27
ai-act-kompass/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
389
ai-act-kompass/src/content/pack.ts
Normal file
389
ai-act-kompass/src/content/pack.ts
Normal 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. 5–6',
|
||||||
|
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. 53–55',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
111
ai-act-kompass/src/engine/classify.test.ts
Normal file
111
ai-act-kompass/src/engine/classify.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
133
ai-act-kompass/src/engine/classify.ts
Normal file
133
ai-act-kompass/src/engine/classify.ts
Normal 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})`] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
66
ai-act-kompass/src/engine/documents.test.ts
Normal file
66
ai-act-kompass/src/engine/documents.test.ts
Normal 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**');
|
||||||
|
});
|
||||||
|
});
|
||||||
217
ai-act-kompass/src/engine/documents.ts
Normal file
217
ai-act-kompass/src/engine/documents.ts
Normal 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');
|
||||||
|
}
|
||||||
92
ai-act-kompass/src/engine/obligations.test.ts
Normal file
92
ai-act-kompass/src/engine/obligations.test.ts
Normal 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. 9–17, 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
55
ai-act-kompass/src/engine/obligations.ts
Normal file
55
ai-act-kompass/src/engine/obligations.ts
Normal 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;
|
||||||
|
}
|
||||||
111
ai-act-kompass/src/engine/types.ts
Normal file
111
ai-act-kompass/src/engine/types.ts
Normal 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[];
|
||||||
|
}
|
||||||
15
ai-act-kompass/src/main.tsx
Normal file
15
ai-act-kompass/src/main.tsx
Normal 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>,
|
||||||
|
);
|
||||||
75
ai-act-kompass/src/store/storage.ts
Normal file
75
ai-act-kompass/src/store/storage.ts
Normal 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);
|
||||||
|
}
|
||||||
190
ai-act-kompass/src/styles.css
Normal file
190
ai-act-kompass/src/styles.css
Normal 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; }
|
||||||
126
ai-act-kompass/src/ui/App.tsx
Normal file
126
ai-act-kompass/src/ui/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
ai-act-kompass/src/ui/Dashboard.tsx
Normal file
127
ai-act-kompass/src/ui/Dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
ai-act-kompass/src/ui/SettingsView.tsx
Normal file
98
ai-act-kompass/src/ui/SettingsView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
ai-act-kompass/src/ui/SystemDetail.tsx
Normal file
164
ai-act-kompass/src/ui/SystemDetail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
355
ai-act-kompass/src/ui/Wizard.tsx
Normal file
355
ai-act-kompass/src/ui/Wizard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
ai-act-kompass/src/ui/common.tsx
Normal file
45
ai-act-kompass/src/ui/common.tsx
Normal 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);
|
||||||
|
}
|
||||||
21
ai-act-kompass/tsconfig.json
Normal file
21
ai-act-kompass/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
15
ai-act-kompass/vite.config.ts
Normal file
15
ai-act-kompass/vite.config.ts
Normal 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'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user