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
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:
@@ -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('.')]
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}))
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
]
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
305
admin-compliance/package-lock.json
generated
305
admin-compliance/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user