fix(quality): Ruff/CVE/TS-Fixes, 104 neue Tests, Complexity-Refactoring
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s

- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell
- CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns)
- TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes
- Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed
- Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A)
- Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-07 19:00:33 +01:00
parent 6509e64dd9
commit 95fcba34cd
124 changed files with 2533 additions and 15709 deletions

View File

@@ -515,7 +515,7 @@ export function setContextPath(ctx: TemplateContext, dotPath: string, value: unk
return {
...ctx,
[section]: {
...(ctx[section] as Record<string, unknown>),
...(ctx[section] as unknown as Record<string, unknown>),
[key]: value,
},
}
@@ -526,6 +526,6 @@ export function setContextPath(ctx: TemplateContext, dotPath: string, value: unk
*/
export function getContextPath(ctx: TemplateContext, dotPath: string): unknown {
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
const sectionObj = ctx[section] as Record<string, unknown>
const sectionObj = ctx[section] as unknown as Record<string, unknown>
return sectionObj?.[rest.join('.')]
}

View File

@@ -313,7 +313,7 @@ function ContextSectionForm({
onChange: (section: keyof TemplateContext, key: string, value: unknown) => void
}) {
const fields = SECTION_FIELDS[section]
const sectionData = context[section] as Record<string, unknown>
const sectionData = context[section] as unknown as Record<string, unknown>
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -523,7 +523,7 @@ function GeneratorSection({
}, [template.id]) // eslint-disable-line react-hooks/exhaustive-deps
// Computed flags pills config
const flagPills: { key: keyof typeof ruleResult.computedFlags; label: string; color: string }[] = ruleResult ? [
const flagPills: { key: string; label: string; color: string }[] = ruleResult ? [
{ key: 'IS_B2C', label: 'B2C', color: 'bg-blue-100 text-blue-700' },
{ key: 'SERVICE_IS_SAAS', label: 'SaaS', color: 'bg-green-100 text-green-700' },
{ key: 'HAS_PENALTY', label: 'Vertragsstrafe', color: 'bg-orange-100 text-orange-700' },
@@ -842,7 +842,7 @@ function DocumentGeneratorPageInner() {
useEffect(() => {
if (state?.companyProfile) {
const profile = state.companyProfile
const p = profile as Record<string, string>
const p = profile as unknown as Record<string, string>
setContext((prev) => ({
...prev,
PROVIDER: {
@@ -919,7 +919,7 @@ function DocumentGeneratorPageInner() {
(section: keyof TemplateContext, key: string, value: unknown) => {
setContext((prev) => ({
...prev,
[section]: { ...(prev[section] as Record<string, unknown>), [key]: value },
[section]: { ...(prev[section] as unknown as Record<string, unknown>), [key]: value },
}))
},
[]

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { StepHeader } from '@/components/sdk/StepHeader'
// =============================================================================
// TYPES
@@ -321,16 +321,7 @@ export default function EmailTemplatesPage() {
return (
<div className="space-y-6">
<StepHeader stepId="email-templates" explanation={STEP_EXPLANATIONS['email-templates'] || {
title: 'E-Mail-Templates',
description: 'Verwalten Sie Vorlagen fuer alle DSGVO-relevanten Benachrichtigungen.',
steps: [
'Template-Typen und Variablen pruefen',
'Inhalte im Editor anpassen',
'Vorschau pruefen und publizieren',
'Branding-Einstellungen konfigurieren',
],
}} />
<StepHeader stepId="email-templates" />
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">

View File

@@ -15,7 +15,6 @@ interface Message {
interface ComplianceAdvisorWidgetProps {
currentStep?: string
enableDraftingEngine?: boolean
}
// =============================================================================
@@ -68,13 +67,7 @@ const COUNTRIES: { code: Country; label: string }[] = [
{ code: 'EU', label: 'EU' },
]
export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) {
// Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead
if (enableDraftingEngine) {
const { DraftingEngineWidget } = require('./DraftingEngineWidget')
return <DraftingEngineWidget currentStep={currentStep} enableDraftingEngine />
}
export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) {
const [isOpen, setIsOpen] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const [messages, setMessages] = useState<Message[]>([])

View File

@@ -1,300 +0,0 @@
'use client'
/**
* DraftEditor - Split-Pane Editor fuer Compliance-Dokument-Entwuerfe
*
* Links (2/3): Gerenderter Draft mit Section-Headern
* Rechts (1/3): Chat-Panel fuer iterative Verfeinerung
* Oben: Document-Type Label, Depth-Level Badge, Constraint-Compliance
*/
import { useState, useRef, useCallback } from 'react'
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
import type {
DraftRevision,
ConstraintCheckResult,
ValidationResult,
} from '@/lib/sdk/drafting-engine/types'
interface DraftEditorProps {
draft: DraftRevision
documentType: ScopeDocumentType | null
constraintCheck: ConstraintCheckResult | null
validationResult: ValidationResult | null
isTyping: boolean
onAccept: () => void
onValidate: () => void
onClose: () => void
onRefine: (instruction: string) => void
}
export function DraftEditor({
draft,
documentType,
constraintCheck,
validationResult,
isTyping,
onAccept,
onValidate,
onClose,
onRefine,
}: DraftEditorProps) {
const [refineInput, setRefineInput] = useState('')
const [activeSection, setActiveSection] = useState<string | null>(null)
const contentRef = useRef<HTMLDivElement>(null)
const handleRefine = useCallback(() => {
if (!refineInput.trim() || isTyping) return
onRefine(refineInput.trim())
setRefineInput('')
}, [refineInput, isTyping, onRefine])
const handleRefineKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleRefine()
}
}
const docLabel = documentType
? DOCUMENT_TYPE_LABELS[documentType]?.split(' (')[0] || documentType
: 'Dokument'
return (
<div className="fixed inset-0 bg-gray-900/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl h-[85vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-3 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<div>
<div className="font-semibold text-sm">{docLabel} - Entwurf</div>
<div className="text-xs text-white/70">
{draft.sections.length} Sections | Erstellt {new Date(draft.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Constraint Badge */}
{constraintCheck && (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
constraintCheck.allowed
? 'bg-green-500/20 text-green-100'
: 'bg-red-500/20 text-red-100'
}`}>
{constraintCheck.allowed ? 'Constraints OK' : 'Constraint-Verletzung'}
</span>
)}
{/* Validation Badge */}
{validationResult && (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
validationResult.passed
? 'bg-green-500/20 text-green-100'
: 'bg-amber-500/20 text-amber-100'
}`}>
{validationResult.passed ? 'Validiert' : `${validationResult.errors.length} Fehler`}
</span>
)}
<button
onClick={onClose}
className="text-white/80 hover:text-white transition-colors p-1"
aria-label="Editor schliessen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Adjustment Warnings */}
{constraintCheck && constraintCheck.adjustments.length > 0 && (
<div className="px-6 py-2 bg-amber-50 border-b border-amber-200 shrink-0">
{constraintCheck.adjustments.map((adj, i) => (
<p key={i} className="text-xs text-amber-700 flex items-start gap-1">
<svg className="w-3.5 h-3.5 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
{adj}
</p>
))}
</div>
)}
{/* Main Content: 2/3 Editor + 1/3 Chat */}
<div className="flex-1 flex overflow-hidden">
{/* Left: Draft Content (2/3) */}
<div className="w-2/3 border-r border-gray-200 overflow-y-auto" ref={contentRef}>
{/* Section Navigation */}
<div className="sticky top-0 bg-white border-b border-gray-100 px-4 py-2 flex items-center gap-1 overflow-x-auto z-10">
{draft.sections.map((section) => (
<button
key={section.id}
onClick={() => {
setActiveSection(section.id)
document.getElementById(`section-${section.id}`)?.scrollIntoView({ behavior: 'smooth' })
}}
className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
activeSection === section.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
{section.title}
</button>
))}
</div>
{/* Sections */}
<div className="p-6 space-y-6">
{draft.sections.map((section) => (
<div
key={section.id}
id={`section-${section.id}`}
className={`rounded-lg border transition-colors ${
activeSection === section.id
? 'border-blue-300 bg-blue-50/30'
: 'border-gray-200 bg-white'
}`}
>
<div className="px-4 py-2.5 border-b border-gray-100 flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900">{section.title}</h3>
{section.schemaField && (
<span className="text-xs text-gray-400 font-mono">{section.schemaField}</span>
)}
</div>
<div className="px-4 py-3">
<div className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed">
{section.content}
</div>
</div>
</div>
))}
</div>
</div>
{/* Right: Refinement Chat (1/3) */}
<div className="w-1/3 flex flex-col bg-gray-50">
<div className="px-4 py-3 border-b border-gray-200 bg-white">
<h3 className="text-sm font-semibold text-gray-900">Verfeinerung</h3>
<p className="text-xs text-gray-500">Geben Sie Anweisungen zur Verbesserung</p>
</div>
{/* Validation Summary (if present) */}
{validationResult && (
<div className="px-4 py-3 border-b border-gray-100">
<div className="space-y-1.5">
{validationResult.errors.length > 0 && (
<div className="flex items-center gap-1.5 text-xs text-red-600">
<span className="w-2 h-2 rounded-full bg-red-500" />
{validationResult.errors.length} Fehler
</div>
)}
{validationResult.warnings.length > 0 && (
<div className="flex items-center gap-1.5 text-xs text-amber-600">
<span className="w-2 h-2 rounded-full bg-amber-500" />
{validationResult.warnings.length} Warnungen
</div>
)}
{validationResult.suggestions.length > 0 && (
<div className="flex items-center gap-1.5 text-xs text-blue-600">
<span className="w-2 h-2 rounded-full bg-blue-500" />
{validationResult.suggestions.length} Vorschlaege
</div>
)}
</div>
</div>
)}
{/* Refinement Area */}
<div className="flex-1 p-4 overflow-y-auto">
<div className="space-y-3">
<p className="text-xs text-gray-500">
Beschreiben Sie, was geaendert werden soll. Der Agent erstellt eine ueberarbeitete Version unter Beachtung der Scope-Constraints.
</p>
{/* Quick Refinement Buttons */}
<div className="space-y-1.5">
{[
'Mehr Details hinzufuegen',
'Platzhalter ausfuellen',
'Rechtliche Referenzen ergaenzen',
'Sprache vereinfachen',
].map((suggestion) => (
<button
key={suggestion}
onClick={() => onRefine(suggestion)}
disabled={isTyping}
className="w-full text-left px-3 py-1.5 text-xs bg-white hover:bg-blue-50 border border-gray-200 rounded-md transition-colors text-gray-600 disabled:opacity-50"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
{/* Refinement Input */}
<div className="border-t border-gray-200 p-3 bg-white">
<div className="flex gap-2">
<input
type="text"
value={refineInput}
onChange={(e) => setRefineInput(e.target.value)}
onKeyDown={handleRefineKeyDown}
placeholder="Anweisung eingeben..."
disabled={isTyping}
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
/>
<button
onClick={handleRefine}
disabled={!refineInput.trim() || isTyping}
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="border-t border-gray-200 px-6 py-3 bg-white flex items-center justify-between shrink-0">
<div className="flex items-center gap-2">
<button
onClick={onValidate}
disabled={isTyping}
className="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 disabled:opacity-50 transition-colors"
>
Validieren
</button>
</div>
<div className="flex items-center gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors"
>
Abbrechen
</button>
<button
onClick={onAccept}
disabled={isTyping}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
Draft akzeptieren
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,443 +0,0 @@
'use client'
/**
* DraftingEngineWidget - Erweitert den ComplianceAdvisor um 4 Modi
*
* Mode-Indicator Pills: Explain / Ask / Draft / Validate
* Document-Type Selector aus requiredDocuments der ScopeDecision
* Feature-Flag enableDraftingEngine fuer schrittweises Rollout
*/
import { useState, useEffect, useRef, useCallback } from 'react'
import { useSDK } from '@/lib/sdk/context'
import { useDraftingEngine } from '@/lib/sdk/drafting-engine/use-drafting-engine'
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
import type { AgentMode } from '@/lib/sdk/drafting-engine/types'
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
import { DraftEditor } from './DraftEditor'
import { ValidationReport } from './ValidationReport'
interface DraftingEngineWidgetProps {
currentStep?: string
enableDraftingEngine?: boolean
}
const MODE_CONFIG: Record<AgentMode, { label: string; color: string; activeColor: string; icon: string }> = {
explain: { label: 'Explain', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-purple-100 text-purple-700 ring-1 ring-purple-300', icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
ask: { label: 'Ask', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-amber-100 text-amber-700 ring-1 ring-amber-300', icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
draft: { label: 'Draft', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-blue-100 text-blue-700 ring-1 ring-blue-300', icon: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z' },
validate: { label: 'Validate', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-green-100 text-green-700 ring-1 ring-green-300', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
}
const EXAMPLE_QUESTIONS: Record<AgentMode, string[]> = {
explain: [
'Was ist ein Verarbeitungsverzeichnis?',
'Wann brauche ich eine DSFA?',
'Was sind TOM nach Art. 32 DSGVO?',
],
ask: [
'Welche Luecken hat mein Compliance-Profil?',
'Was fehlt noch fuer die Zertifizierung?',
'Welche Dokumente muss ich noch erstellen?',
],
draft: [
'Erstelle einen VVT-Eintrag fuer unseren Hauptprozess',
'Erstelle TOM fuer unsere Cloud-Infrastruktur',
'Erstelle eine Datenschutzerklaerung',
],
validate: [
'Pruefe die Konsistenz meiner Dokumente',
'Stimmen VVT und TOM ueberein?',
'Gibt es Luecken bei den Loeschfristen?',
],
}
export function DraftingEngineWidget({
currentStep = 'default',
enableDraftingEngine = true,
}: DraftingEngineWidgetProps) {
const { state } = useSDK()
const engine = useDraftingEngine()
const [isOpen, setIsOpen] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const [inputValue, setInputValue] = useState('')
const [showDraftEditor, setShowDraftEditor] = useState(false)
const [showValidationReport, setShowValidationReport] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
// Available document types from scope decision
const availableDocumentTypes: ScopeDocumentType[] =
state.complianceScope?.decision?.requiredDocuments
?.filter(d => d.required)
.map(d => d.documentType as ScopeDocumentType) ?? ['vvt', 'tom', 'lf']
// Auto-scroll
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [engine.messages])
// Open draft editor when a new draft arrives
useEffect(() => {
if (engine.currentDraft) {
setShowDraftEditor(true)
}
}, [engine.currentDraft])
// Open validation report when new results arrive
useEffect(() => {
if (engine.validationResult) {
setShowValidationReport(true)
}
}, [engine.validationResult])
const handleSendMessage = useCallback(
(content: string) => {
if (!content.trim()) return
setInputValue('')
engine.sendMessage(content)
},
[engine]
)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage(inputValue)
}
}
const exampleQuestions = EXAMPLE_QUESTIONS[engine.currentMode]
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-[5.5rem] w-14 h-14 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-110 z-50"
aria-label="Drafting Engine oeffnen"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
)
}
// Draft Editor full-screen overlay
if (showDraftEditor && engine.currentDraft) {
return (
<DraftEditor
draft={engine.currentDraft}
documentType={engine.activeDocumentType}
constraintCheck={engine.constraintCheck}
onAccept={() => {
engine.acceptDraft()
setShowDraftEditor(false)
}}
onValidate={() => {
engine.validateDraft()
}}
onClose={() => setShowDraftEditor(false)}
onRefine={(instruction: string) => {
engine.requestDraft(instruction)
}}
validationResult={engine.validationResult}
isTyping={engine.isTyping}
/>
)
}
return (
<div className={`fixed bottom-6 right-6 ${isExpanded ? 'w-[700px] h-[80vh]' : 'w-[420px] h-[560px]'} max-h-screen bg-white rounded-2xl shadow-2xl flex flex-col z-50 border border-gray-200 transition-all duration-200`}>
{/* Header */}
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-3 rounded-t-2xl flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<div>
<div className="font-semibold text-sm">Drafting Engine</div>
<div className="text-xs text-white/80">Compliance-Dokumententwurf</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-white/80 hover:text-white transition-colors p-1"
aria-label={isExpanded ? 'Verkleinern' : 'Vergroessern'}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isExpanded ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9L4 4m0 0v4m0-4h4m6 6l5 5m0 0v-4m0 4h-4" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
)}
</svg>
</button>
<button
onClick={() => {
engine.clearMessages()
setIsOpen(false)
}}
className="text-white/80 hover:text-white transition-colors p-1"
aria-label="Schliessen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Mode Pills */}
<div className="flex items-center gap-1 px-3 py-2 border-b border-gray-100 bg-white">
{(Object.keys(MODE_CONFIG) as AgentMode[]).map((mode) => {
const config = MODE_CONFIG[mode]
const isActive = engine.currentMode === mode
return (
<button
key={mode}
onClick={() => engine.setMode(mode)}
className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium transition-all ${isActive ? config.activeColor : config.color} hover:opacity-80`}
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
</svg>
{config.label}
</button>
)
})}
</div>
{/* Document Type Selector (visible in draft/validate mode) */}
{(engine.currentMode === 'draft' || engine.currentMode === 'validate') && (
<div className="px-3 py-2 border-b border-gray-100 bg-gray-50/50">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 shrink-0">Dokument:</span>
<select
value={engine.activeDocumentType || ''}
onChange={(e) => engine.setDocumentType(e.target.value as ScopeDocumentType)}
className="flex-1 text-xs border border-gray-200 rounded-md px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<option value="">Dokumenttyp waehlen...</option>
{availableDocumentTypes.map((dt) => (
<option key={dt} value={dt}>
{DOCUMENT_TYPE_LABELS[dt] || dt}
</option>
))}
</select>
</div>
</div>
)}
{/* Gap Banner */}
{(() => {
const gaps = state.complianceScope?.decision?.gaps?.filter(
(g: { severity: string }) => g.severity === 'HIGH' || g.severity === 'CRITICAL'
) ?? []
if (gaps.length > 0) {
return (
<div className="mx-3 mt-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-amber-800">
<svg className="w-4 h-4 text-amber-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<span className="font-medium">{gaps.length} kritische Luecke{gaps.length !== 1 ? 'n' : ''} erkannt</span>
</div>
<button
onClick={() => handleSendMessage('Was fehlt noch in meinem Compliance-Profil?')}
className="text-xs font-medium text-amber-700 hover:text-amber-900 px-2 py-0.5 rounded hover:bg-amber-100 transition-colors"
>
Analysieren
</button>
</div>
)
}
return null
})()}
{/* Error Banner */}
{engine.error && (
<div className="mx-3 mt-2 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-xs text-red-700 flex items-center justify-between">
<span>{engine.error}</span>
<button onClick={() => engine.clearMessages()} className="text-red-500 hover:text-red-700 ml-2">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Validation Report Inline */}
{showValidationReport && engine.validationResult && (
<div className="mx-3 mt-2 max-h-48 overflow-y-auto">
<ValidationReport
result={engine.validationResult}
onClose={() => setShowValidationReport(false)}
compact
/>
</div>
)}
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{engine.messages.length === 0 ? (
<div className="text-center py-6">
<div className="w-14 h-14 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg className="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={MODE_CONFIG[engine.currentMode].icon} />
</svg>
</div>
<h3 className="text-sm font-medium text-gray-900 mb-1">
{engine.currentMode === 'explain' && 'Fragen beantworten'}
{engine.currentMode === 'ask' && 'Luecken erkennen'}
{engine.currentMode === 'draft' && 'Dokumente entwerfen'}
{engine.currentMode === 'validate' && 'Konsistenz pruefen'}
</h3>
<p className="text-xs text-gray-500 mb-4">
{engine.currentMode === 'explain' && 'Stellen Sie Fragen zu DSGVO, AI Act und Compliance.'}
{engine.currentMode === 'ask' && 'Identifiziert Luecken in Ihrem Compliance-Profil.'}
{engine.currentMode === 'draft' && 'Erstellt strukturierte Compliance-Dokumente.'}
{engine.currentMode === 'validate' && 'Prueft Cross-Dokument-Konsistenz.'}
</p>
<div className="text-left space-y-2">
<p className="text-xs font-medium text-gray-700 mb-2">Beispiele:</p>
{exampleQuestions.map((q, idx) => (
<button
key={idx}
onClick={() => handleSendMessage(q)}
className="w-full text-left px-3 py-2 text-xs bg-white hover:bg-purple-50 border border-gray-200 rounded-lg transition-colors text-gray-700"
>
{q}
</button>
))}
</div>
{/* Quick Actions for Draft/Validate */}
{engine.currentMode === 'draft' && engine.activeDocumentType && (
<button
onClick={() => engine.requestDraft()}
className="mt-4 px-4 py-2 bg-blue-600 text-white text-xs font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Draft fuer {DOCUMENT_TYPE_LABELS[engine.activeDocumentType]?.split(' (')[0] || engine.activeDocumentType} erstellen
</button>
)}
{engine.currentMode === 'validate' && (
<button
onClick={() => engine.validateDraft()}
className="mt-4 px-4 py-2 bg-green-600 text-white text-xs font-medium rounded-lg hover:bg-green-700 transition-colors"
>
Validierung starten
</button>
)}
</div>
) : (
<>
{engine.messages.map((message, idx) => (
<div
key={idx}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] rounded-lg px-3 py-2 ${
message.role === 'user'
? 'bg-indigo-600 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}
>
<p className={`text-sm ${message.role === 'assistant' ? 'whitespace-pre-wrap' : ''}`}>
{message.content}
</p>
{/* Draft ready indicator */}
{message.metadata?.hasDraft && engine.currentDraft && (
<button
onClick={() => setShowDraftEditor(true)}
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-md text-xs text-blue-700 hover:bg-blue-100 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Im Editor oeffnen
</button>
)}
{/* Validation ready indicator */}
{message.metadata?.hasValidation && engine.validationResult && (
<button
onClick={() => setShowValidationReport(true)}
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-green-50 border border-green-200 rounded-md text-xs text-green-700 hover:bg-green-100 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Validierungsbericht anzeigen
</button>
)}
</div>
</div>
))}
{engine.isTyping && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 rounded-lg px-3 py-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Input Area */}
<div className="border-t border-gray-200 p-3 bg-white rounded-b-2xl">
<div className="flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
engine.currentMode === 'draft'
? 'Anweisung fuer den Entwurf...'
: engine.currentMode === 'validate'
? 'Validierungsfrage...'
: 'Frage eingeben...'
}
disabled={engine.isTyping}
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50"
/>
{engine.isTyping ? (
<button
onClick={engine.stopGeneration}
className="px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
title="Generierung stoppen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 6h12v12H6z" />
</svg>
</button>
) : (
<button
onClick={() => handleSendMessage(inputValue)}
disabled={!inputValue.trim()}
className="px-3 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
)}
</div>
</div>
</div>
)
}

View File

@@ -17,9 +17,9 @@ export interface StepTip {
interface StepHeaderProps {
stepId: string
title: string
description: string
explanation: string
title?: string
description?: string
explanation?: string
tips?: StepTip[]
showNavigation?: boolean
showProgress?: boolean
@@ -95,10 +95,10 @@ const tipIconColors = {
export function StepHeader({
stepId,
title,
description,
explanation,
tips = [],
title: titleProp,
description: descriptionProp,
explanation: explanationProp,
tips: tipsProp,
showNavigation = true,
showProgress = true,
onComplete,
@@ -109,6 +109,13 @@ export function StepHeader({
const { state, dispatch } = useSDK()
const [showHelp, setShowHelp] = useState(false)
// Look up defaults from STEP_EXPLANATIONS when props are not provided
const preset = STEP_EXPLANATIONS[stepId as keyof typeof STEP_EXPLANATIONS]
const title = titleProp ?? preset?.title ?? stepId
const description = descriptionProp ?? preset?.description ?? ''
const explanation = explanationProp ?? preset?.explanation ?? ''
const tips = tipsProp ?? preset?.tips ?? []
const currentStep = getStepById(stepId)
const prevStep = getPreviousStep(stepId)
const nextStep = getNextStep(stepId)
@@ -996,6 +1003,50 @@ export const STEP_EXPLANATIONS = {
},
],
},
}
'email-templates': {
title: 'E-Mail-Templates',
description: 'Verwalten Sie Vorlagen fuer alle DSGVO-relevanten Benachrichtigungen',
explanation: 'E-Mail-Templates definieren die Texte und das Layout fuer automatisierte DSGVO-Benachrichtigungen: Einwilligungsbestaetigung, Widerrufsbestaetigung, Auskunftsantwort, Loeschbestaetigung und weitere Lifecycle-E-Mails. Alle 16 Template-Typen koennen individuell angepasst und mit Variablen personalisiert werden.',
tips: [
{
icon: 'info' as const,
title: '16 Lifecycle-E-Mails',
description: 'Von der Registrierungsbestaetigung bis zur Kontoloeschung — alle relevanten Touchpoints sind mit Vorlagen abgedeckt.',
},
{
icon: 'warning' as const,
title: 'Pflichtangaben',
description: 'Stellen Sie sicher, dass jede E-Mail die gesetzlich vorgeschriebenen Angaben enthaelt: Impressum, Datenschutzhinweis und Widerrufsmoeglichkeit.',
},
{
icon: 'lightbulb' as const,
title: 'Variablen',
description: 'Nutzen Sie Platzhalter wie {{name}}, {{email}} und {{company}} fuer automatische Personalisierung.',
},
],
},
'use-case-workshop': {
title: 'Use Case Workshop',
description: 'Erfassen und bewerten Sie Ihre KI-Anwendungsfaelle im Workshop-Format',
explanation: 'Im Use Case Workshop erfassen Sie Ihre KI-Anwendungsfaelle strukturiert in einem gefuehrten Prozess. Der Workshop leitet Sie durch Identifikation, Beschreibung, Datenkategorien, Risikobewertung und Stakeholder-Analyse. Die Ergebnisse fliessen direkt in die Compliance-Bewertung ein.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Vollstaendigkeit',
description: 'Erfassen Sie alle KI-Anwendungsfaelle — auch solche, die nur intern genutzt werden oder sich noch in der Planungsphase befinden.',
},
{
icon: 'info' as const,
title: 'Stakeholder einbeziehen',
description: 'Beziehen Sie Fachbereiche und IT in den Workshop ein, um alle Anwendungsfaelle zu identifizieren.',
},
{
icon: 'warning' as const,
title: 'Risikobewertung',
description: 'Jeder Anwendungsfall wird nach EU AI Act Risikostufen klassifiziert. Hochrisiko-Systeme erfordern zusaetzliche Dokumentation.',
},
],
},
} satisfies Record<string, { title: string; description: string; explanation: string; tips: StepTip[] }>
export default StepHeader

View File

@@ -1,220 +0,0 @@
'use client'
/**
* ValidationReport - Strukturierte Anzeige von Validierungsergebnissen
*
* Errors (Scope-Violations) in Rot
* Warnings (Inkonsistenzen) in Amber
* Suggestions in Blau
*/
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
import type { ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
interface ValidationReportProps {
result: ValidationResult
onClose: () => void
/** Compact mode for inline display in widget */
compact?: boolean
}
const SEVERITY_CONFIG = {
error: {
bg: 'bg-red-50',
border: 'border-red-200',
text: 'text-red-700',
icon: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
label: 'Fehler',
dotColor: 'bg-red-500',
},
warning: {
bg: 'bg-amber-50',
border: 'border-amber-200',
text: 'text-amber-700',
icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z',
label: 'Warnungen',
dotColor: 'bg-amber-500',
},
suggestion: {
bg: 'bg-blue-50',
border: 'border-blue-200',
text: 'text-blue-700',
icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
label: 'Vorschlaege',
dotColor: 'bg-blue-500',
},
}
function FindingCard({ finding, compact }: { finding: ValidationFinding; compact?: boolean }) {
const config = SEVERITY_CONFIG[finding.severity]
const docLabel = DOCUMENT_TYPE_LABELS[finding.documentType]?.split(' (')[0] || finding.documentType
if (compact) {
return (
<div className={`flex items-start gap-2 px-2.5 py-1.5 ${config.bg} rounded-md border ${config.border}`}>
<span className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${config.dotColor}`} />
<div className="min-w-0">
<p className={`text-xs font-medium ${config.text}`}>{finding.title}</p>
<p className="text-xs text-gray-500 truncate">{finding.description}</p>
</div>
</div>
)
}
return (
<div className={`${config.bg} rounded-lg border ${config.border} p-3`}>
<div className="flex items-start gap-2">
<svg className={`w-4 h-4 mt-0.5 shrink-0 ${config.text}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
</svg>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h4 className={`text-sm font-medium ${config.text}`}>{finding.title}</h4>
<span className="text-xs text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded">{docLabel}</span>
</div>
<p className="text-xs text-gray-600 mt-1">{finding.description}</p>
{finding.crossReferenceType && (
<p className="text-xs text-gray-500 mt-1">
Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType}
</p>
)}
{finding.legalReference && (
<p className="text-xs text-gray-500 mt-1 font-mono">{finding.legalReference}</p>
)}
{finding.suggestion && (
<div className="mt-2 flex items-start gap-1.5 px-2.5 py-1.5 bg-white/60 rounded border border-gray-100">
<svg className="w-3.5 h-3.5 mt-0.5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<p className="text-xs text-gray-600">{finding.suggestion}</p>
</div>
)}
</div>
</div>
</div>
)
}
export function ValidationReport({ result, onClose, compact }: ValidationReportProps) {
const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length
if (compact) {
return (
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-100">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${result.passed ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-xs font-medium text-gray-700">
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
</span>
<span className="text-xs text-gray-400">
({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'})
</span>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-2 space-y-1.5 max-h-36 overflow-y-auto">
{result.errors.map((f) => <FindingCard key={f.id} finding={f} compact />)}
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} compact />)}
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} compact />)}
</div>
</div>
)
}
return (
<div className="space-y-4">
{/* Summary Header */}
<div className={`rounded-lg border p-4 ${result.passed ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${result.passed ? 'bg-green-100' : 'bg-red-100'}`}>
<svg className={`w-5 h-5 ${result.passed ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={result.passed ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'} />
</svg>
</div>
<div>
<h3 className={`text-sm font-semibold ${result.passed ? 'text-green-800' : 'text-red-800'}`}>
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
</h3>
<p className="text-xs text-gray-500">
Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')}
</p>
</div>
</div>
{/* Stats */}
<div className="flex items-center gap-3">
{result.errors.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-red-500" />
<span className="text-xs font-medium text-red-700">{result.errors.length}</span>
</div>
)}
{result.warnings.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amber-500" />
<span className="text-xs font-medium text-amber-700">{result.warnings.length}</span>
</div>
)}
{result.suggestions.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-xs font-medium text-blue-700">{result.suggestions.length}</span>
</div>
)}
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 ml-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
{/* Errors */}
{result.errors.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-2">
Fehler ({result.errors.length})
</h4>
<div className="space-y-2">
{result.errors.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
{/* Warnings */}
{result.warnings.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2">
Warnungen ({result.warnings.length})
</h4>
<div className="space-y-2">
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
{/* Suggestions */}
{result.suggestions.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-blue-700 uppercase tracking-wide mb-2">
Vorschlaege ({result.suggestions.length})
</h4>
<div className="space-y-2">
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
</div>
)
}

View File

@@ -1,12 +1,14 @@
import { describe, it, expect } from 'vitest'
import { STEP_EXPLANATIONS } from '../StepHeader'
type StepExplanationKey = keyof typeof STEP_EXPLANATIONS
// Focus on testing the STEP_EXPLANATIONS data structure
// Component tests require more complex SDK context mocking
describe('STEP_EXPLANATIONS', () => {
it('should have explanations for all Phase 1 steps', () => {
const phase1Steps = [
const phase1Steps: StepExplanationKey[] = [
'use-case-workshop',
'screening',
'modules',
@@ -29,7 +31,7 @@ describe('STEP_EXPLANATIONS', () => {
})
it('should have explanations for all Phase 2 steps', () => {
const phase2Steps = [
const phase2Steps: StepExplanationKey[] = [
'ai-act',
'obligations',
'dsfa',
@@ -93,8 +95,8 @@ describe('STEP_EXPLANATIONS', () => {
expect(dsfa.explanation.length).toBeGreaterThan(50)
})
it('should cover all 19 SDK steps', () => {
const allStepIds = [
it('should cover all core SDK steps', () => {
const coreStepIds: StepExplanationKey[] = [
// Phase 1
'use-case-workshop',
'screening',
@@ -118,10 +120,11 @@ describe('STEP_EXPLANATIONS', () => {
'escalations',
]
expect(Object.keys(STEP_EXPLANATIONS).length).toBe(allStepIds.length)
allStepIds.forEach(stepId => {
coreStepIds.forEach(stepId => {
expect(STEP_EXPLANATIONS[stepId]).toBeDefined()
})
// Ensure we have at least the core steps plus additional module explanations
expect(Object.keys(STEP_EXPLANATIONS).length).toBeGreaterThanOrEqual(coreStepIds.length)
})
})

View File

@@ -1,722 +0,0 @@
import type { ScopeProfilingAnswer, ComplianceDepthLevel, ScopeDocumentType } from './compliance-scope-types'
export interface GoldenTest {
id: string
name: string
description: string
answers: ScopeProfilingAnswer[]
expectedLevel: ComplianceDepthLevel | null // null for prefill tests
expectedMinDocuments?: ScopeDocumentType[]
expectedHardTriggerIds?: string[]
expectedDsfaRequired?: boolean
tags: string[]
}
export const GOLDEN_TESTS: GoldenTest[] = [
// GT-01: 2-Person Freelancer, nur B2B, DE-Hosting → L1
{
id: 'GT-01',
name: '2-Person Freelancer B2B',
description: 'Kleinstes Setup ohne besondere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '2' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_racial_ethnic', value: false },
{ questionId: 'data_political_opinion', value: false },
{ questionId: 'data_religious', value: false },
{ questionId: 'data_union_membership', value: false },
{ questionId: 'data_sexual_orientation', value: false },
{ questionId: 'data_criminal', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<100' },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
expectedHardTriggerIds: [],
expectedDsfaRequired: false,
tags: ['baseline', 'freelancer', 'b2b'],
},
// GT-02: Solo IT-Berater → L1
{
id: 'GT-02',
name: 'Solo IT-Berater',
description: 'Einzelperson, minimale Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'it_services' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'solo', 'minimal'],
},
// GT-03: 5-Person Agentur, Website, kein Tracking → L1
{
id: 'GT-03',
name: '5-Person Agentur ohne Tracking',
description: 'Kleine Agentur, einfache Website ohne Analytics',
answers: [
{ questionId: 'org_employee_count', value: '5' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'marketing' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: false },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
tags: ['baseline', 'agency', 'simple'],
},
// GT-04: 30-Person SaaS B2B, EU-Cloud → L2 (scale trigger)
{
id: 'GT-04',
name: '30-Person SaaS B2B',
description: 'Scale-Trigger durch Mitarbeiterzahl',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'software' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: false },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER'],
tags: ['scale', 'saas', 'growth'],
},
// GT-05: 50-Person Handel B2C, Webshop → L2 (B2C+Webshop)
{
id: 'GT-05',
name: '50-Person E-Commerce B2C',
description: 'B2C mit Webshop erhöht Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['b2c', 'webshop', 'retail'],
},
// GT-06: 80-Person Dienstleister, Cloud → L2 (scale)
{
id: 'GT-06',
name: '80-Person Dienstleister',
description: 'Größerer Betrieb mit Cloud-Services',
answers: [
{ questionId: 'org_employee_count', value: '80' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'professional_services' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV'],
tags: ['scale', 'services'],
},
// GT-07: 20-Person Startup mit GA4 Tracking → L2 (tracking)
{
id: 'GT-07',
name: 'Startup mit Google Analytics',
description: 'Tracking-Tools erhöhen Compliance-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '20' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'technology' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'tech_tracking_tools', value: 'google_analytics' },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['tracking', 'analytics', 'startup'],
},
// GT-08: Kita-App (Minderjaehrige) → L3 (HT-B01)
{
id: 'GT-08',
name: 'Kita-App für Eltern',
description: 'Datenverarbeitung von Minderjährigen unter 16',
answers: [
{ questionId: 'org_employee_count', value: '15' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNG', 'AVV'],
tags: ['hard-trigger', 'minors', 'education'],
},
// GT-09: Krankenhaus-Software → L3 (HT-A01)
{
id: 'GT-09',
name: 'Krankenhaus-Verwaltungssoftware',
description: 'Gesundheitsdaten Art. 9 DSGVO',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'health', 'art9'],
},
// GT-10: HR-Scoring-Plattform → L3 (HT-C01)
{
id: 'GT-10',
name: 'HR-Scoring für Bewerbungen',
description: 'Automatisierte Entscheidungen im HR-Bereich',
answers: [
{ questionId: 'org_employee_count', value: '40' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'hr_tech' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'profiling' },
{ questionId: 'tech_adm_impact', value: 'employment' },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'adm', 'profiling'],
},
// GT-11: Fintech Kreditscoring → L3 (HT-H05 + C01)
{
id: 'GT-11',
name: 'Fintech Kreditscoring',
description: 'Finanzsektor mit automatisierten Entscheidungen',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'finance' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'scoring' },
{ questionId: 'tech_adm_impact', value: 'credit' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H05', 'HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'finance', 'scoring'],
},
// GT-12: Bildungsplattform Minderjaehrige → L3 (HT-B01)
{
id: 'GT-12',
name: 'Online-Lernplattform für Schüler',
description: 'Bildungssektor mit minderjährigen Nutzern',
answers: [
{ questionId: 'org_employee_count', value: '35' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'minors'],
},
// GT-13: Datenbroker → L3 (HT-H02)
{
id: 'GT-13',
name: 'Datenbroker / Adresshandel',
description: 'Geschäftsmodell basiert auf Datenhandel',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'data_broker' },
{ questionId: 'data_is_core_business', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'data-broker'],
},
// GT-14: Video + ADM → L3 (HT-D05)
{
id: 'GT-14',
name: 'Videoüberwachung mit Gesichtserkennung',
description: 'Biometrische Daten mit automatisierter Verarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '60' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'security' },
{ questionId: 'data_biometric', value: true },
{ questionId: 'tech_has_video_surveillance', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-D05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'biometric', 'video'],
},
// GT-15: 500-MA Konzern ohne Zert → L3 (HT-G04)
{
id: 'GT-15',
name: 'Großunternehmen ohne Zertifizierung',
description: 'Scale-Trigger durch Unternehmensgröße',
answers: [
{ questionId: 'org_employee_count', value: '500' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'cert_has_iso27001', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'enterprise'],
},
// GT-16: ISO 27001 Anbieter → L4 (HT-F01)
{
id: 'GT-16',
name: 'ISO 27001 zertifizierter Cloud-Provider',
description: 'Zertifizierung erfordert höchste Compliance',
answers: [
{ questionId: 'org_employee_count', value: '150' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01'],
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV', 'CERT_ISO27001'],
tags: ['hard-trigger', 'certification', 'iso'],
},
// GT-17: TISAX Automobilzulieferer → L4 (HT-F04)
{
id: 'GT-17',
name: 'TISAX-zertifizierter Automobilzulieferer',
description: 'Automotive-Branche mit TISAX-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '300' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'automotive' },
{ questionId: 'cert_has_tisax', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F04'],
tags: ['hard-trigger', 'certification', 'tisax'],
},
// GT-18: ISO 27701 Cloud-Provider → L4 (HT-F02)
{
id: 'GT-18',
name: 'ISO 27701 Privacy-zertifiziert',
description: 'Privacy-spezifische Zertifizierung',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F02'],
tags: ['hard-trigger', 'certification', 'privacy'],
},
// GT-19: Grosskonzern + Art.9 + >1M DS → L4 (HT-G05)
{
id: 'GT-19',
name: 'Konzern mit sensiblen Massendaten',
description: 'Kombination aus Scale und Art. 9 Daten',
answers: [
{ questionId: 'org_employee_count', value: '2000' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'insurance' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-G05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'art9'],
},
// GT-20: Nur B2C Webshop → L2 (HT-H01)
{
id: 'GT-20',
name: 'Reiner B2C Webshop',
description: 'B2C-Trigger ohne weitere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '12' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
tags: ['b2c', 'webshop'],
},
// GT-21: Keine Daten, keine MA → L1
{
id: 'GT-21',
name: 'Minimale Datenverarbeitung',
description: 'Absolute Baseline ohne Risiken',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'tech_has_website', value: false },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'minimal'],
},
// GT-22: Alle Art.9 Kategorien → L3 (HT-A09)
{
id: 'GT-22',
name: 'Alle Art. 9 Kategorien',
description: 'Multiple sensible Datenkategorien',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'research' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_genetic', value: true },
{ questionId: 'data_biometric', value: true },
{ questionId: 'data_racial_ethnic', value: true },
{ questionId: 'data_political_opinion', value: true },
{ questionId: 'data_religious', value: true },
{ questionId: 'data_union_membership', value: true },
{ questionId: 'data_sexual_orientation', value: true },
{ questionId: 'data_criminal', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A09'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'art9', 'multiple-categories'],
},
// GT-23: Drittland + Art.9 → L3 (HT-E04)
{
id: 'GT-23',
name: 'Drittlandtransfer mit Art. 9 Daten',
description: 'Kombination aus Drittland und sensiblen Daten',
answers: [
{ questionId: 'org_employee_count', value: '45' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'us' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'tech_has_third_country_transfer', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-E04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'third-country', 'art9'],
},
// GT-24: Minderjaehrige + Art.9 → L4 (HT-B02)
{
id: 'GT-24',
name: 'Minderjährige mit Gesundheitsdaten',
description: 'Kombination aus vulnerabler Gruppe und Art. 9',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'minors', 'health', 'combined-risk'],
},
// GT-25: KI autonome Entscheidungen → L3 (HT-C02)
{
id: 'GT-25',
name: 'KI mit autonomen Entscheidungen',
description: 'AI Act relevante autonome Systeme',
answers: [
{ questionId: 'org_employee_count', value: '70' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'ai_services' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'autonomous_decision' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'ai', 'adm'],
},
// GT-26: Multiple Zertifizierungen → L4 (HT-F01-05)
{
id: 'GT-26',
name: 'Multiple Zertifizierungen',
description: 'Mehrere Zertifizierungen kombiniert',
answers: [
{ questionId: 'org_employee_count', value: '250' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'cert_has_soc2', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01', 'HT-F02', 'HT-F03'],
tags: ['hard-trigger', 'certification', 'multiple'],
},
// GT-27: Oeffentlicher Sektor + Gesundheit → L3 (HT-H07 + A01)
{
id: 'GT-27',
name: 'Öffentlicher Sektor mit Gesundheitsdaten',
description: 'Behörde mit Art. 9 Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2g' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'public_sector' },
{ questionId: 'org_is_public_sector', value: true },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H07', 'HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'public-sector', 'health'],
},
// GT-28: Bildung + KI + Minderjaehrige → L4 (HT-B03)
{
id: 'GT-28',
name: 'EdTech mit KI für Minderjährige',
description: 'Triple-Risiko: Bildung, KI, vulnerable Gruppe',
answers: [
{ questionId: 'org_employee_count', value: '55' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B03'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'ai', 'minors', 'triple-risk'],
},
// GT-29: Freelancer mit 1 Art.9 → L3 (hard trigger override despite low score)
{
id: 'GT-29',
name: 'Freelancer mit Gesundheitsdaten',
description: 'Hard Trigger überschreibt niedrige Score-Bewertung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'override', 'art9', 'freelancer'],
},
// GT-30: Enterprise, alle Prozesse vorhanden → L3 (good process maturity)
{
id: 'GT-30',
name: 'Enterprise mit reifer Prozesslandschaft',
description: 'Große Organisation mit allen Compliance-Prozessen',
answers: [
{ questionId: 'org_employee_count', value: '450' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'process_has_dsb', value: true },
{ questionId: 'process_has_training', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
tags: ['enterprise', 'mature', 'all-processes'],
},
// GT-31: SMB, nur 1 Block beantwortet → L1 (graceful degradation)
{
id: 'GT-31',
name: 'Unvollständige Profilerstellung',
description: 'Test für graceful degradation bei unvollständigen Antworten',
answers: [
{ questionId: 'org_employee_count', value: '8' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'org_industry', value: 'consulting' },
// Nur Block 1 (Organization) beantwortet, Rest fehlt
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['incomplete', 'degradation', 'edge-case'],
},
// GT-32: CompanyProfile Prefill Konsistenz → null (prefill test, no expected level)
{
id: 'GT-32',
name: 'CompanyProfile Prefill Test',
description: 'Prüft ob CompanyProfile-Daten korrekt in ScopeProfile übernommen werden',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_hosting_location', value: 'eu' },
// Diese Werte sollten mit CompanyProfile-Prefill übereinstimmen
],
expectedLevel: null,
tags: ['prefill', 'integration', 'consistency'],
},
]

View File

@@ -1,153 +0,0 @@
import { IntentClassifier } from '../intent-classifier'
describe('IntentClassifier', () => {
const classifier = new IntentClassifier()
describe('classify - Draft mode', () => {
it.each([
['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'],
['Generiere eine TOM-Dokumentation', 'draft'],
['Schreibe eine Datenschutzerklaerung', 'draft'],
['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'],
['Create a DSFA document', 'draft'],
['Draft a privacy policy for us', 'draft'],
['Neues VVT anlegen', 'draft'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Validate mode', () => {
it.each([
['Pruefe die Konsistenz meiner Dokumente', 'validate'],
['Ist mein VVT korrekt?', 'validate'],
['Validiere die TOM gegen das VVT', 'validate'],
['Check die Vollstaendigkeit', 'validate'],
['Stimmt das mit der DSFA ueberein?', 'validate'],
['Cross-Check VVT und TOM', 'validate'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Ask mode', () => {
it.each([
['Was fehlt noch in meinem Profil?', 'ask'],
['Zeige mir die Luecken', 'ask'],
['Welche Dokumente fehlen noch?', 'ask'],
['Was ist der naechste Schritt?', 'ask'],
['Welche Informationen brauche ich noch?', 'ask'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.6)
})
})
describe('classify - Explain mode (fallback)', () => {
it.each([
['Was ist DSGVO?', 'explain'],
['Erklaere mir Art. 30', 'explain'],
['Hallo', 'explain'],
['Danke fuer die Hilfe', 'explain'],
])('"%s" should classify as %s (fallback)', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
})
})
describe('classify - confidence thresholds', () => {
it('should have high confidence for clear draft intents', () => {
const result = classifier.classify('Erstelle ein neues VVT')
expect(result.confidence).toBeGreaterThanOrEqual(0.85)
})
it('should have lower confidence for ambiguous inputs', () => {
const result = classifier.classify('Hallo')
expect(result.confidence).toBeLessThan(0.6)
})
it('should boost confidence with document type detection', () => {
const withDoc = classifier.classify('Erstelle VVT')
const withoutDoc = classifier.classify('Erstelle etwas')
expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence)
})
it('should boost confidence with multiple pattern matches', () => {
const single = classifier.classify('Erstelle Dokument')
const multi = classifier.classify('Erstelle und generiere ein neues Dokument')
expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence)
})
})
describe('detectDocumentType', () => {
it.each([
['VVT erstellen', 'vvt'],
['Verarbeitungsverzeichnis', 'vvt'],
['Art. 30 Dokumentation', 'vvt'],
['TOM definieren', 'tom'],
['technisch organisatorische Massnahmen', 'tom'],
['Art. 32 Massnahmen', 'tom'],
['DSFA durchfuehren', 'dsfa'],
['Datenschutz-Folgenabschaetzung', 'dsfa'],
['Art. 35 Pruefung', 'dsfa'],
['DPIA erstellen', 'dsfa'],
['Datenschutzerklaerung', 'dsi'],
['Privacy Policy', 'dsi'],
['Art. 13 Information', 'dsi'],
['Loeschfristen definieren', 'lf'],
['Loeschkonzept erstellen', 'lf'],
['Retention Policy', 'lf'],
['Auftragsverarbeitung', 'av_vertrag'],
['AVV erstellen', 'av_vertrag'],
['Art. 28 Vertrag', 'av_vertrag'],
['Einwilligung einholen', 'einwilligung'],
['Consent Management', 'einwilligung'],
['Cookie Banner', 'einwilligung'],
])('"%s" should detect document type %s', (input, expectedType) => {
const result = classifier.detectDocumentType(input)
expect(result).toBe(expectedType)
})
it('should return undefined for unrecognized types', () => {
expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined()
expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined()
})
})
describe('classify - Umlaut handling', () => {
it('should handle German umlauts correctly', () => {
// With actual umlauts (ä, ö, ü)
const result1 = classifier.classify('Prüfe die Vollständigkeit')
expect(result1.mode).toBe('validate')
// With ae/oe/ue substitution
const result2 = classifier.classify('Pruefe die Vollstaendigkeit')
expect(result2.mode).toBe('validate')
})
it('should handle ß correctly', () => {
const result = classifier.classify('Schließe Lücken')
// Should still detect via normalized patterns
expect(result).toBeDefined()
})
})
describe('classify - combined mode + document type', () => {
it('should detect both mode and document type', () => {
const result = classifier.classify('Erstelle ein VVT fuer unsere Firma')
expect(result.mode).toBe('draft')
expect(result.detectedDocumentType).toBe('vvt')
})
it('should detect validate + document type', () => {
const result = classifier.classify('Pruefe mein TOM auf Konsistenz')
expect(result.mode).toBe('validate')
expect(result.detectedDocumentType).toBe('tom')
})
})
})

View File

@@ -1,312 +0,0 @@
import { StateProjector } from '../state-projector'
import type { SDKState } from '../../types'
describe('StateProjector', () => {
const projector = new StateProjector()
// Helper: minimal SDKState
function makeState(overrides: Partial<SDKState> = {}): SDKState {
return {
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'user1',
subscription: 'PROFESSIONAL',
customerType: null,
companyProfile: null,
complianceScope: null,
sourcePolicy: null,
currentPhase: 1,
currentStep: 'company-profile',
completedSteps: [],
checkpoints: {},
importedDocuments: [],
gapAnalysis: null,
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
...overrides,
} as SDKState
}
function makeDecisionState(level: string = 'L2'): SDKState {
return makeState({
companyProfile: {
companyName: 'Test GmbH',
industry: 'IT-Dienstleistung',
employeeCount: 50,
businessModel: 'SaaS',
isPublicSector: false,
} as any,
complianceScope: {
decision: {
id: 'dec-1',
determinedLevel: level,
scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 },
triggeredHardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] },
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] },
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
],
riskFlags: [
{ id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' },
],
gaps: [
{ id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] },
],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
answers: [],
} as any,
vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[],
toms: [],
retentionPolicies: [],
})
}
describe('projectForDraft', () => {
it('should return a DraftContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result).toHaveProperty('decisions')
expect(result).toHaveProperty('companyProfile')
expect(result).toHaveProperty('constraints')
expect(result.decisions.level).toBe('L2')
})
it('should project company profile', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Test GmbH')
expect(result.companyProfile.industry).toBe('IT-Dienstleistung')
expect(result.companyProfile.employeeCount).toBe(50)
})
it('should provide defaults when no company profile', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Unbekannt')
expect(result.companyProfile.industry).toBe('Unbekannt')
expect(result.companyProfile.employeeCount).toBe(0)
})
it('should extract constraints and depth requirements', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.depthRequirements).toBeDefined()
expect(result.constraints.boundaries.length).toBeGreaterThan(0)
})
it('should extract risk flags', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.riskFlags.length).toBe(1)
expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung')
})
it('should include existing document data when available', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.existingDocumentData).toBeDefined()
expect((result.existingDocumentData as any).totalCount).toBe(1)
})
it('should return undefined existingDocumentData when none exists', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'tom')
expect(result.existingDocumentData).toBeUndefined()
})
it('should filter required documents', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.requiredDocuments.length).toBe(3)
expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true)
})
it('should handle empty state gracefully', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.level).toBe('L1')
expect(result.decisions.hardTriggers).toEqual([])
expect(result.decisions.requiredDocuments).toEqual([])
})
})
describe('projectForAsk', () => {
it('should return a GapContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result).toHaveProperty('unansweredQuestions')
expect(result).toHaveProperty('gaps')
expect(result).toHaveProperty('missingDocuments')
})
it('should identify missing documents', () => {
const state = makeDecisionState()
// vvt exists, tom and lf are missing
const result = projector.projectForAsk(state)
expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true)
expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true)
})
it('should not list existing documents as missing', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
// vvt exists in state
expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false)
})
it('should include gaps from scope decision', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result.gaps.length).toBe(1)
expect(result.gaps[0].title).toBe('TOM fehlt')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForAsk(state)
expect(result.gaps).toEqual([])
expect(result.missingDocuments).toEqual([])
})
})
describe('projectForValidate', () => {
it('should return a ValidationContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result).toHaveProperty('documents')
expect(result).toHaveProperty('crossReferences')
expect(result).toHaveProperty('scopeLevel')
expect(result).toHaveProperty('depthRequirements')
})
it('should include all requested document types', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents.length).toBe(2)
expect(result.documents.map(d => d.type)).toContain('vvt')
expect(result.documents.map(d => d.type)).toContain('tom')
})
it('should include cross-references', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.crossReferences).toHaveProperty('vvtCategories')
expect(result.crossReferences).toHaveProperty('tomControls')
expect(result.crossReferences).toHaveProperty('retentionCategories')
expect(result.crossReferences.vvtCategories.length).toBe(1)
expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung')
})
it('should include scope level', () => {
const state = makeDecisionState('L3')
const result = projector.projectForValidate(state, ['vvt'])
expect(result.scopeLevel).toBe('L3')
})
it('should include depth requirements per document type', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.depthRequirements).toHaveProperty('vvt')
expect(result.depthRequirements).toHaveProperty('tom')
})
it('should summarize documents', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents[0].contentSummary).toContain('1')
expect(result.documents[1].contentSummary).toContain('Keine TOM')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.scopeLevel).toBe('L1')
expect(result.crossReferences.vvtCategories).toEqual([])
expect(result.crossReferences.tomControls).toEqual([])
})
})
describe('token budget estimation', () => {
it('projectForDraft should produce compact output', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
const json = JSON.stringify(result)
// Rough token estimation: ~4 chars per token
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500
})
it('projectForAsk should produce very compact output', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600
})
it('projectForValidate should stay within budget', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000
})
})
})

View File

@@ -95,11 +95,11 @@ export function buildAllowedFacts(
const scope = state.complianceScope
return {
companyName: profile?.name ?? 'Unbekannt',
companyName: profile?.companyName ?? 'Unbekannt',
legalForm: profile?.legalForm ?? '',
industry: profile?.industry ?? '',
location: profile?.location ?? '',
employeeCount: profile?.employeeCount ?? 0,
location: profile?.headquartersCity ?? '',
employeeCount: parseEmployeeCount(profile?.employeeCount),
teamStructure: deriveTeamStructure(profile),
itLandscape: deriveItLandscape(profile),
@@ -213,11 +213,33 @@ export function checkForDisallowedContent(
// Private Helpers
// ============================================================================
/**
* Parst den employeeCount-String (z.B. "1-9", "50-249", "1000+") in eine Zahl.
* Verwendet den Mittelwert des Bereichs oder den unteren Wert bei "+".
*/
function parseEmployeeCount(value: string | undefined | null): number {
if (!value) return 0
// Handle "1000+" style
const plusMatch = value.match(/^(\d+)\+$/)
if (plusMatch) return parseInt(plusMatch[1], 10)
// Handle "50-249" style ranges
const rangeMatch = value.match(/^(\d+)-(\d+)$/)
if (rangeMatch) {
const low = parseInt(rangeMatch[1], 10)
const high = parseInt(rangeMatch[2], 10)
return Math.round((low + high) / 2)
}
// Try plain number
const num = parseInt(value, 10)
return isNaN(num) ? 0 : num
}
function deriveTeamStructure(profile: CompanyProfile | null): string {
if (!profile) return ''
// Ableitung aus verfuegbaren Profildaten
if (profile.employeeCount > 500) return 'Konzernstruktur'
if (profile.employeeCount > 50) return 'mittelstaendisch'
const count = parseEmployeeCount(profile.employeeCount)
if (count > 500) return 'Konzernstruktur'
if (count > 50) return 'mittelstaendisch'
return 'Kleinunternehmen'
}
@@ -225,15 +247,15 @@ function deriveItLandscape(profile: CompanyProfile | null): string {
if (!profile) return ''
return profile.businessModel?.includes('SaaS') ? 'Cloud-First' :
profile.businessModel?.includes('Cloud') ? 'Cloud-First' :
profile.isPublicSector ? 'On-Premise' : 'Hybrid'
'Hybrid'
}
function deriveSpecialFeatures(profile: CompanyProfile | null): string[] {
if (!profile) return []
const features: string[] = []
if (profile.isPublicSector) features.push('Oeffentlicher Sektor')
if (profile.employeeCount > 250) features.push('Grossunternehmen')
if (profile.dataProtectionOfficer) features.push('Interner DSB benannt')
const count = parseEmployeeCount(profile.employeeCount)
if (count > 250) features.push('Grossunternehmen')
if (profile.dpoName) features.push('Interner DSB benannt')
return features
}
@@ -253,5 +275,5 @@ function deriveTriggeredRegulations(
function derivePrimaryUseCases(state: SDKState): string[] {
if (!state.useCases || state.useCases.length === 0) return []
return state.useCases.slice(0, 3).map(uc => uc.name || uc.title || 'Unbenannt')
return state.useCases.slice(0, 3).map(uc => uc.name || 'Unbenannt')
}

View File

@@ -1,373 +0,0 @@
/**
* Intent Classifier - Leichtgewichtiger Pattern-Matcher
*
* Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call.
* Deutsche und englische Muster werden unterstuetzt.
*
* Confidence-Schwellen:
* - >0.8: Hohe Sicherheit, automatisch anwenden
* - 0.6-0.8: Mittel, Nutzer kann bestaetigen
* - <0.6: Fallback zu 'explain'
*/
import type { AgentMode, IntentClassification } from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
// ============================================================================
// Pattern Definitions
// ============================================================================
interface ModePattern {
mode: AgentMode
patterns: RegExp[]
/** Base-Confidence wenn ein Pattern matched */
baseConfidence: number
}
const MODE_PATTERNS: ModePattern[] = [
{
mode: 'draft',
baseConfidence: 0.85,
patterns: [
/\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i,
/\b(draft|create|generate|write|compose)\b/i,
/\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i,
/\b(vorlage|template)\s+(erstell|generier)/i,
/\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i,
],
},
{
mode: 'validate',
baseConfidence: 0.80,
patterns: [
/\b(pruef|validier|check|kontrollier|ueberpruef)\b/i,
/\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i,
/\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i,
/\b(validate|verify|check|review)\b/i,
/\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i,
/\bcross[\s-]?check\b/i,
/\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i,
],
},
{
mode: 'ask',
baseConfidence: 0.75,
patterns: [
/\bwas\s+fehlt\b/i,
/\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i,
/\b(unvollstaendig|unfertig|offen)\b/i,
/\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i,
/\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i,
/\bworan\s+(muss|soll)\b/i,
],
},
]
/** Dokumenttyp-Erkennung */
const DOCUMENT_TYPE_PATTERNS: Array<{
type: ScopeDocumentType
patterns: RegExp[]
}> = [
{
type: 'vvt',
patterns: [
/\bv{1,2}t\b/i,
/\bverarbeitungsverzeichnis\b/i,
/\bverarbeitungstaetigkeit/i,
/\bprocessing\s+activit/i,
/\bart\.?\s*30\b/i,
],
},
{
type: 'tom',
patterns: [
/\btom\b/i,
/\btechnisch.*organisatorisch.*massnahm/i,
/\bart\.?\s*32\b/i,
/\bsicherheitsmassnahm/i,
],
},
{
type: 'dsfa',
patterns: [
/\bdsfa\b/i,
/\bdatenschutz[\s-]?folgenabschaetzung\b/i,
/\bdpia\b/i,
/\bart\.?\s*35\b/i,
/\bimpact\s+assessment\b/i,
],
},
{
type: 'dsi',
patterns: [
/\bdatenschutzerklaerung\b/i,
/\bprivacy\s+policy\b/i,
/\bdsi\b/i,
/\bart\.?\s*13\b/i,
/\bart\.?\s*14\b/i,
],
},
{
type: 'lf',
patterns: [
/\bloeschfrist/i,
/\bloeschkonzept/i,
/\bretention/i,
/\baufbewahr/i,
],
},
{
type: 'av_vertrag',
patterns: [
/\bavv?\b/i,
/\bauftragsverarbeit/i,
/\bdata\s+processing\s+agreement/i,
/\bart\.?\s*28\b/i,
],
},
{
type: 'betroffenenrechte',
patterns: [
/\bbetroffenenrecht/i,
/\bdata\s+subject\s+right/i,
/\bart\.?\s*15\b/i,
/\bauskunft/i,
],
},
{
type: 'einwilligung',
patterns: [
/\beinwillig/i,
/\bconsent/i,
/\bcookie/i,
],
},
{
type: 'datenpannen',
patterns: [
/\bdatenpanne/i,
/\bdata\s*breach/i,
/\bart\.?\s*33\b/i,
/\bsicherheitsvorfall/i,
/\bincident/i,
/\bmelde.*vorfall/i,
],
},
{
type: 'daten_transfer',
patterns: [
/\bdrittland/i,
/\btransfer/i,
/\bscc\b/i,
/\bstandardvertragsklausel/i,
/\bart\.?\s*44\b/i,
],
},
{
type: 'vertragsmanagement',
patterns: [
/\bvertragsmanagement/i,
/\bcontract\s*management/i,
],
},
{
type: 'schulung',
patterns: [
/\bschulung/i,
/\btraining/i,
/\bawareness/i,
/\bmitarbeiterschulung/i,
],
},
{
type: 'audit_log',
patterns: [
/\baudit/i,
/\blogging\b/i,
/\bprotokollierung/i,
/\bart\.?\s*5\s*abs\.?\s*2\b/i,
],
},
{
type: 'risikoanalyse',
patterns: [
/\brisikoanalyse/i,
/\brisk\s*assessment/i,
/\brisikobewertung/i,
],
},
{
type: 'notfallplan',
patterns: [
/\bnotfallplan/i,
/\bkrisenmanagement/i,
/\bbusiness\s*continuity/i,
/\bnotfall/i,
],
},
{
type: 'zertifizierung',
patterns: [
/\bzertifizierung/i,
/\biso\s*27001\b/i,
/\biso\s*27701\b/i,
/\bart\.?\s*42\b/i,
],
},
{
type: 'datenschutzmanagement',
patterns: [
/\bdsms\b/i,
/\bdatenschutzmanagement/i,
/\bpdca/i,
],
},
{
type: 'iace_ce_assessment',
patterns: [
/\biace\b/i,
/\bce[\s-]?kennzeichnung/i,
/\bai\s*act\b/i,
/\bki[\s-]?verordnung/i,
],
},
]
// ============================================================================
// Redirect Patterns (nicht-draftbare Dokumente → Document Generator)
// ============================================================================
const REDIRECT_PATTERNS: Array<{
pattern: RegExp
response: string
}> = [
{
pattern: /\bimpressum\b/i,
response: 'Impressum-Templates finden Sie unter /sdk/document-generator → Kategorie "Impressum". Der Drafting Agent erstellt keine Impressen, da diese nach DDG §5 unternehmensspezifisch sind.',
},
{
pattern: /\b(agb|allgemeine.?geschaefts)/i,
response: 'AGB-Vorlagen erstellen Sie im Document Generator unter /sdk/document-generator → Kategorie "AGB". Der Drafting Agent erstellt keine AGB, da diese nach BGB §305ff individuell gestaltet werden muessen.',
},
{
pattern: /\bwiderruf/i,
response: 'Widerrufs-Templates finden Sie unter /sdk/document-generator → Kategorie "Widerruf".',
},
{
pattern: /\bnda\b/i,
response: 'NDA-Vorlagen finden Sie unter /sdk/document-generator.',
},
{
pattern: /\bsla\b/i,
response: 'SLA-Vorlagen finden Sie unter /sdk/document-generator.',
},
]
// ============================================================================
// Classifier
// ============================================================================
export class IntentClassifier {
/**
* Klassifiziert die Nutzerabsicht anhand des Inputs.
*
* @param input - Die Nutzer-Nachricht
* @returns IntentClassification mit Mode, Confidence, Patterns
*/
classify(input: string): IntentClassification {
const normalized = this.normalize(input)
// Redirect-Check: Nicht-draftbare Dokumente → Document Generator
for (const redirect of REDIRECT_PATTERNS) {
if (redirect.pattern.test(normalized)) {
return {
mode: 'explain',
confidence: 0.90,
matchedPatterns: [redirect.pattern.source],
suggestedResponse: redirect.response,
}
}
}
let bestMatch: IntentClassification = {
mode: 'explain',
confidence: 0.3,
matchedPatterns: [],
}
for (const modePattern of MODE_PATTERNS) {
const matched: string[] = []
for (const pattern of modePattern.patterns) {
if (pattern.test(normalized)) {
matched.push(pattern.source)
}
}
if (matched.length > 0) {
// Mehr Matches = hoehere Confidence (bis zum Maximum)
const matchBonus = Math.min(matched.length - 1, 2) * 0.05
const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99)
if (confidence > bestMatch.confidence) {
bestMatch = {
mode: modePattern.mode,
confidence,
matchedPatterns: matched,
}
}
}
}
// Dokumenttyp erkennen
const detectedDocType = this.detectDocumentType(normalized)
if (detectedDocType) {
bestMatch.detectedDocumentType = detectedDocType
// Dokumenttyp-Erkennung erhoeht Confidence leicht
bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99)
}
// Fallback: Bei Confidence <0.6 immer 'explain'
if (bestMatch.confidence < 0.6) {
bestMatch.mode = 'explain'
}
return bestMatch
}
/**
* Erkennt den Dokumenttyp aus dem Input.
*/
detectDocumentType(input: string): ScopeDocumentType | undefined {
const normalized = this.normalize(input)
for (const docPattern of DOCUMENT_TYPE_PATTERNS) {
for (const pattern of docPattern.patterns) {
if (pattern.test(normalized)) {
return docPattern.type
}
}
}
return undefined
}
/**
* Normalisiert den Input fuer Pattern-Matching.
* Ersetzt Umlaute, entfernt Sonderzeichen.
*/
private normalize(input: string): string {
return input
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/Ä/g, 'Ae')
.replace(/Ö/g, 'Oe')
.replace(/Ü/g, 'Ue')
}
}
/** Singleton-Instanz */
export const intentClassifier = new IntentClassifier()

View File

@@ -243,7 +243,7 @@ function sanitizeAddress(
*/
export function validateNoRemainingPII(facts: SanitizedFacts): string[] {
const warnings: string[] = []
const allValues = extractAllStringValues(facts)
const allValues = extractAllStringValues(facts as unknown as Record<string, unknown>)
for (const { path, value } of allValues) {
if (path === '__sanitized') continue

View File

@@ -1,342 +0,0 @@
/**
* State Projector - Token-budgetierte Projektion des SDK-State
*
* Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die
* relevanten Slices fuer den jeweiligen Agent-Modus.
*
* Token-Budgets:
* - Draft: ~1500 Tokens
* - Ask: ~600 Tokens
* - Validate: ~2000 Tokens
*/
import type { SDKState, CompanyProfile } from '../types'
import type {
ComplianceScopeState,
ScopeDecision,
ScopeDocumentType,
ScopeGap,
RequiredDocument,
RiskFlag,
DOCUMENT_SCOPE_MATRIX,
DocumentDepthRequirement,
} from '../compliance-scope-types'
import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types'
import type {
DraftContext,
GapContext,
ValidationContext,
} from './types'
// ============================================================================
// State Projector
// ============================================================================
export class StateProjector {
/**
* Projiziert den SDKState fuer Draft-Operationen.
* Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints.
*
* ~1500 Tokens
*/
projectForDraft(
state: SDKState,
documentType: ScopeDocumentType
): DraftContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
const depthReq = DOC_MATRIX[documentType]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
return {
decisions: {
level,
scores: decision?.scores ?? {
risk_score: 0,
complexity_score: 0,
assurance_need: 0,
composite_score: 0,
},
hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({
id: t.rule.id,
label: t.rule.label,
legalReference: t.rule.legalReference,
})),
requiredDocuments: (decision?.requiredDocuments ?? [])
.filter(d => d.required)
.map(d => ({
documentType: d.documentType,
depth: d.depth,
detailItems: d.detailItems,
})),
},
companyProfile: this.projectCompanyProfile(state.companyProfile),
constraints: {
depthRequirements: depthReq,
riskFlags: (decision?.riskFlags ?? []).map(f => ({
severity: f.severity,
title: f.title,
recommendation: f.recommendation,
})),
boundaries: this.deriveBoundaries(decision, documentType),
},
existingDocumentData: this.extractExistingDocumentData(state, documentType),
}
}
/**
* Projiziert den SDKState fuer Ask-Operationen.
* Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente.
*
* ~600 Tokens
*/
projectForAsk(state: SDKState): GapContext {
const decision = state.complianceScope?.decision ?? null
// Fehlende Pflichtdokumente ermitteln
const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required)
const existingDocTypes = this.getExistingDocumentTypes(state)
const missingDocuments = requiredDocs
.filter(d => !existingDocTypes.includes(d.documentType))
.map(d => ({
documentType: d.documentType,
label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType,
depth: d.depth,
estimatedEffort: d.estimatedEffort,
}))
// Gaps aus der Scope-Decision
const gaps = (decision?.gaps ?? []).map(g => ({
id: g.id,
severity: g.severity,
title: g.title,
description: g.description,
relatedDocuments: g.relatedDocuments,
}))
// Unbeantwortete Fragen (aus dem Scope-Profiling)
const answers = state.complianceScope?.answers ?? []
const answeredIds = new Set(answers.map(a => a.questionId))
return {
unansweredQuestions: [], // Populated dynamically from question catalog
gaps,
missingDocuments,
}
}
/**
* Projiziert den SDKState fuer Validate-Operationen.
* Fokus: Cross-Dokument-Konsistenz, Scope-Compliance.
*
* ~2000 Tokens
*/
projectForValidate(
state: SDKState,
documentTypes: ScopeDocumentType[]
): ValidationContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
// Dokument-Zusammenfassungen sammeln
const documents = documentTypes.map(type => ({
type,
contentSummary: this.summarizeDocument(state, type),
structuredData: this.extractExistingDocumentData(state, type),
}))
// Cross-Referenzen extrahieren
const crossReferences = {
vvtCategories: (state.vvt ?? []).map(v =>
typeof v === 'object' && v !== null && 'name' in v ? String((v as Record<string, unknown>).name) : ''
).filter(Boolean),
dsfaRisks: state.dsfa
? ['DSFA vorhanden']
: [],
tomControls: (state.toms ?? []).map(t =>
typeof t === 'object' && t !== null && 'name' in t ? String((t as Record<string, unknown>).name) : ''
).filter(Boolean),
retentionCategories: (state.retentionPolicies ?? []).map(p =>
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
).filter(Boolean),
}
// Depth-Requirements fuer alle angefragten Typen
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
for (const type of documentTypes) {
depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
}
return {
documents,
crossReferences,
scopeLevel: level,
depthRequirements: depthRequirements as Record<ScopeDocumentType, DocumentDepthRequirement>,
}
}
// ==========================================================================
// Private Helpers
// ==========================================================================
private projectCompanyProfile(
profile: CompanyProfile | null
): DraftContext['companyProfile'] {
if (!profile) {
return {
name: 'Unbekannt',
industry: 'Unbekannt',
employeeCount: 0,
businessModel: 'Unbekannt',
isPublicSector: false,
}
}
return {
name: profile.companyName ?? profile.name ?? 'Unbekannt',
industry: profile.industry ?? 'Unbekannt',
employeeCount: typeof profile.employeeCount === 'number'
? profile.employeeCount
: parseInt(String(profile.employeeCount ?? '0'), 10) || 0,
businessModel: profile.businessModel ?? 'Unbekannt',
isPublicSector: profile.isPublicSector ?? false,
...(profile.dataProtectionOfficer ? {
dataProtectionOfficer: {
name: profile.dataProtectionOfficer.name ?? '',
email: profile.dataProtectionOfficer.email ?? '',
},
} : {}),
}
}
/**
* Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf.
*/
private deriveBoundaries(
decision: ScopeDecision | null,
documentType: ScopeDocumentType
): string[] {
const boundaries: string[] = []
const level = decision?.determinedLevel ?? 'L1'
// Grundregel: Scope-Engine ist autoritativ
boundaries.push(
`Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})`
)
// DSFA-Boundary
if (documentType === 'dsfa') {
const dsfaRequired = decision?.triggeredHardTriggers?.some(
t => t.rule.dsfaRequired
) ?? false
if (!dsfaRequired && level !== 'L4') {
boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.')
}
}
// Dokument nicht in requiredDocuments?
const isRequired = decision?.requiredDocuments?.some(
d => d.documentType === documentType && d.required
) ?? false
if (!isRequired) {
boundaries.push(
`Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.`
)
}
return boundaries
}
/**
* Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State.
*/
private extractExistingDocumentData(
state: SDKState,
documentType: ScopeDocumentType
): Record<string, unknown> | undefined {
switch (documentType) {
case 'vvt':
return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined
case 'tom':
return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined
case 'lf':
return state.retentionPolicies?.length
? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length }
: undefined
case 'dsfa':
return state.dsfa ? { assessment: state.dsfa } : undefined
case 'dsi':
return state.documents?.length
? { entries: state.documents.slice(0, 3), totalCount: state.documents.length }
: undefined
case 'einwilligung':
return state.consents?.length
? { entries: state.consents.slice(0, 5), totalCount: state.consents.length }
: undefined
default:
return undefined
}
}
/**
* Ermittelt welche Dokumenttypen bereits im State vorhanden sind.
*/
private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] {
const types: ScopeDocumentType[] = []
if (state.vvt?.length) types.push('vvt')
if (state.toms?.length) types.push('tom')
if (state.retentionPolicies?.length) types.push('lf')
if (state.dsfa) types.push('dsfa')
if (state.documents?.length) types.push('dsi')
if (state.consents?.length) types.push('einwilligung')
if (state.cookieBanner) types.push('einwilligung')
if (state.risks?.length) types.push('risikoanalyse')
if (state.escalationWorkflows?.length) types.push('datenpannen')
if (state.iaceProjects?.length) types.push('iace_ce_assessment')
if (state.obligations?.length) types.push('zertifizierung')
if (state.dsrConfig) types.push('betroffenenrechte')
return types
}
/**
* Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung.
*/
private summarizeDocument(
state: SDKState,
documentType: ScopeDocumentType
): string {
switch (documentType) {
case 'vvt':
return state.vvt?.length
? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst`
: 'Keine VVT-Eintraege vorhanden'
case 'tom':
return state.toms?.length
? `${state.toms.length} TOM-Massnahmen definiert`
: 'Keine TOM-Massnahmen vorhanden'
case 'lf':
return state.retentionPolicies?.length
? `${state.retentionPolicies.length} Loeschfristen definiert`
: 'Keine Loeschfristen vorhanden'
case 'dsfa':
return state.dsfa
? 'DSFA vorhanden'
: 'Keine DSFA vorhanden'
default:
return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}`
}
}
}
/** Singleton-Instanz */
export const stateProjector = new StateProjector()

View File

@@ -1,343 +0,0 @@
'use client'
/**
* useDraftingEngine - React Hook fuer die Drafting Engine
*
* Managed: currentMode, activeDocumentType, draftSessions, validationState
* Handled: State-Projection, API-Calls, Streaming
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
*/
import { useState, useCallback, useRef } from 'react'
import { useSDK } from '../context'
import { stateProjector } from './state-projector'
import { intentClassifier } from './intent-classifier'
import { constraintEnforcer } from './constraint-enforcer'
import type {
AgentMode,
DraftSession,
DraftRevision,
DraftingChatMessage,
ValidationResult,
ConstraintCheckResult,
DraftContext,
GapContext,
ValidationContext,
} from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
export interface DraftingEngineState {
currentMode: AgentMode
activeDocumentType: ScopeDocumentType | null
messages: DraftingChatMessage[]
isTyping: boolean
currentDraft: DraftRevision | null
validationResult: ValidationResult | null
constraintCheck: ConstraintCheckResult | null
error: string | null
}
export interface DraftingEngineActions {
setMode: (mode: AgentMode) => void
setDocumentType: (type: ScopeDocumentType) => void
sendMessage: (content: string) => Promise<void>
requestDraft: (instructions?: string) => Promise<void>
validateDraft: () => Promise<void>
acceptDraft: () => void
stopGeneration: () => void
clearMessages: () => void
}
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
const { state, dispatch } = useSDK()
const abortControllerRef = useRef<AbortController | null>(null)
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
const [isTyping, setIsTyping] = useState(false)
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
const [error, setError] = useState<string | null>(null)
// Get state projection based on mode
const getProjection = useCallback(() => {
switch (currentMode) {
case 'draft':
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
case 'ask':
return stateProjector.projectForAsk(state)
case 'validate':
return activeDocumentType
? stateProjector.projectForValidate(state, [activeDocumentType])
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
default:
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
}
}, [state, currentMode, activeDocumentType])
const setMode = useCallback((mode: AgentMode) => {
setCurrentMode(mode)
}, [])
const setDocumentType = useCallback((type: ScopeDocumentType) => {
setActiveDocumentType(type)
}, [])
const sendMessage = useCallback(async (content: string) => {
if (!content.trim() || isTyping) return
setError(null)
// Auto-detect mode if needed
const classification = intentClassifier.classify(content)
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
setCurrentMode(classification.mode)
}
if (classification.detectedDocumentType && !activeDocumentType) {
setActiveDocumentType(classification.detectedDocumentType)
}
const userMessage: DraftingChatMessage = {
role: 'user',
content: content.trim(),
}
setMessages(prev => [...prev, userMessage])
setIsTyping(true)
abortControllerRef.current = new AbortController()
try {
const projection = getProjection()
const response = await fetch('/api/sdk/drafting-engine/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: content.trim(),
history: messages.map(m => ({ role: m.role, content: m.content })),
sdkStateProjection: projection,
mode: currentMode,
documentType: activeDocumentType,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
}
const agentMessageId = `msg-${Date.now()}-agent`
setMessages(prev => [...prev, {
role: 'assistant',
content: '',
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
}])
// Stream response
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let accumulated = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
accumulated += decoder.decode(value, { stream: true })
const text = accumulated
setMessages(prev =>
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
)
}
setIsTyping(false)
} catch (err) {
if ((err as Error).name === 'AbortError') {
setIsTyping(false)
return
}
setError((err as Error).message)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Fehler: ${(err as Error).message}`,
}])
setIsTyping(false)
}
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
const requestDraft = useCallback(async (instructions?: string) => {
if (!activeDocumentType) {
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
return
}
setError(null)
setIsTyping(true)
try {
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
const response = await fetch('/api/sdk/drafting-engine/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType,
draftContext,
instructions,
existingDraft: currentDraft,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
}
setCurrentDraft(result.draft)
setConstraintCheck(result.constraintCheck)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const validateDraft = useCallback(async () => {
setError(null)
setIsTyping(true)
try {
const docTypes: ScopeDocumentType[] = activeDocumentType
? [activeDocumentType]
: ['vvt', 'tom', 'lf']
const validationContext = stateProjector.projectForValidate(state, docTypes)
const response = await fetch('/api/sdk/drafting-engine/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType || 'vvt',
draftContent: currentDraft?.content || '',
validationContext,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Validierung fehlgeschlagen')
}
setValidationResult(result)
const summary = result.passed
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
setMessages(prev => [...prev, {
role: 'assistant',
content: summary,
metadata: { mode: 'validate', hasValidation: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const acceptDraft = useCallback(() => {
if (!currentDraft || !activeDocumentType) return
// Dispatch the draft data into SDK state
switch (activeDocumentType) {
case 'vvt':
dispatch({
type: 'ADD_PROCESSING_ACTIVITY',
payload: {
id: `draft-vvt-${Date.now()}`,
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
case 'tom':
dispatch({
type: 'ADD_TOM',
payload: {
id: `draft-tom-${Date.now()}`,
name: 'TOM-Entwurf',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
default:
dispatch({
type: 'ADD_DOCUMENT',
payload: {
id: `draft-${activeDocumentType}-${Date.now()}`,
type: activeDocumentType,
content: currentDraft.content,
sections: currentDraft.sections,
},
})
}
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft wurde in den SDK-State uebernommen.`,
}])
setCurrentDraft(null)
}, [currentDraft, activeDocumentType, dispatch])
const stopGeneration = useCallback(() => {
abortControllerRef.current?.abort()
setIsTyping(false)
}, [])
const clearMessages = useCallback(() => {
setMessages([])
setCurrentDraft(null)
setValidationResult(null)
setConstraintCheck(null)
setError(null)
}, [])
return {
currentMode,
activeDocumentType,
messages,
isTyping,
currentDraft,
validationResult,
constraintCheck,
error,
setMode,
setDocumentType,
sendMessage,
requestDraft,
validateDraft,
acceptDraft,
stopGeneration,
clearMessages,
}
}

View File

@@ -199,11 +199,14 @@ describe('DSFAMitigation type', () => {
describe('DSFASectionProgress type', () => {
it('should track completion for all 5 sections', () => {
const progress: DSFASectionProgress = {
section_0_complete: false,
section_1_complete: true,
section_2_complete: true,
section_3_complete: false,
section_4_complete: false,
section_5_complete: false,
section_6_complete: false,
section_7_complete: false,
}
expect(progress.section_1_complete).toBe(true)

View File

@@ -554,6 +554,15 @@ export function TOMGeneratorProvider({
[]
)
const bulkUpdateTOMs = useCallback(
(updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => {
for (const { id, data } of updates) {
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
}
},
[]
)
// Gap analysis
const runGapAnalysis = useCallback(() => {
if (!rulesEngineRef.current) return
@@ -666,6 +675,7 @@ export function TOMGeneratorProvider({
deriveTOMs,
updateDerivedTOM,
bulkUpdateTOMs,
runGapAnalysis,

View File

@@ -1,11 +1,11 @@
{
"name": "breakpilot-admin-v2",
"name": "breakpilot-compliance-sdk-admin",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "breakpilot-admin-v2",
"name": "breakpilot-compliance-sdk-admin",
"version": "1.0.0",
"dependencies": {
"bpmn-js": "^18.0.1",
@@ -1560,15 +1560,15 @@
}
},
"node_modules/@next/env": {
"version": "15.5.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz",
"integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz",
"integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==",
"cpu": [
"arm64"
],
@@ -1582,9 +1582,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz",
"integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==",
"cpu": [
"x64"
],
@@ -1598,9 +1598,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz",
"integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==",
"cpu": [
"arm64"
],
@@ -1614,9 +1614,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz",
"integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==",
"cpu": [
"arm64"
],
@@ -1630,9 +1630,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz",
"integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==",
"cpu": [
"x64"
],
@@ -1646,9 +1646,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz",
"integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==",
"cpu": [
"x64"
],
@@ -1662,9 +1662,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz",
"integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==",
"cpu": [
"arm64"
],
@@ -1678,9 +1678,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz",
"integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==",
"cpu": [
"x64"
],
@@ -1857,9 +1857,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -1871,9 +1871,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -1885,9 +1885,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -1899,9 +1899,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -1913,9 +1913,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -1927,9 +1927,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -1941,9 +1941,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -1955,9 +1955,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -1969,9 +1969,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -1983,9 +1983,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -1997,9 +1997,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -2011,9 +2011,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -2025,9 +2025,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -2039,9 +2039,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -2053,9 +2053,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -2067,9 +2067,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -2081,9 +2081,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -2095,9 +2095,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -2109,9 +2109,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -2123,9 +2123,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -2137,9 +2137,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -2151,9 +2151,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -2165,9 +2165,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -2179,9 +2179,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -2193,9 +2193,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -3661,11 +3661,14 @@
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -4200,12 +4203,12 @@
}
},
"node_modules/jspdf": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
"integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
@@ -4441,12 +4444,12 @@
}
},
"node_modules/next": {
"version": "15.5.9",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz",
"integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.9",
"@next/env": "15.5.12",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -4459,14 +4462,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.7",
"@next/swc-darwin-arm64": "15.5.12",
"@next/swc-darwin-x64": "15.5.12",
"@next/swc-linux-arm64-gnu": "15.5.12",
"@next/swc-linux-arm64-musl": "15.5.12",
"@next/swc-linux-x64-gnu": "15.5.12",
"@next/swc-linux-x64-musl": "15.5.12",
"@next/swc-win32-arm64-msvc": "15.5.12",
"@next/swc-win32-x64-msvc": "15.5.12",
"sharp": "^0.34.3"
},
"peerDependencies": {
@@ -5333,9 +5336,9 @@
}
},
"node_modules/rollup": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5349,31 +5352,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},