fix(sdk): Align scope types with engine output + project isolation + optional block progress
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 46s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 2s

Type alignment (root cause of client-side crash):
- RiskFlag: id/title/description → severity/category/message/recommendation
- ScopeGap: id/title/recommendation/relatedDocuments → gapType/currentState/targetState/effort
- NextAction: id/priority:number/effortDays → actionType/priority:string/estimatedEffort
- ScopeReasoning: details → factors + impact
- TriggeredHardTrigger: {rule: HardTriggerRule} → flat fields (ruleId, description, etc.)
- All UI components updated to match engine output shape

Project isolation:
- Scope localStorage key now includes projectId (prevents data leak between projects)

Optional block progress:
- Blocks with only optional questions now show green checkmark when any question answered

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-11 14:58:29 +01:00
parent 46048554cb
commit cb48b8289e
7 changed files with 185 additions and 152 deletions

View File

@@ -37,14 +37,31 @@ const TABS: { id: TabId; label: string; icon: string }[] = [
export default function ComplianceScopePage() {
const { state: sdkState, dispatch } = useSDK()
// Project-specific storage key
const projectStorageKey = sdkState.projectId
? `${STORAGE_KEY}_${sdkState.projectId}`
: STORAGE_KEY
// Active tab state
const [activeTab, setActiveTab] = useState<TabId>('overview')
// Migrate old decision format: drop decision if it has old-format fields
const migrateState = (state: ComplianceScopeState): ComplianceScopeState => {
if (state.decision) {
const d = state.decision as Record<string, unknown>
// Old format had 'level' instead of 'determinedLevel', or docs with 'isMandatory'
if (d.level || !d.determinedLevel) {
return { ...state, decision: null }
}
}
return state
}
// Local scope state
const [scopeState, setScopeState] = useState<ComplianceScopeState>(() => {
// Try to load from SDK context first
if (sdkState.complianceScope) {
return sdkState.complianceScope
return migrateState(sdkState.complianceScope)
}
return createEmptyScopeState()
})
@@ -68,14 +85,14 @@ export default function ComplianceScopePage() {
const ctxScope = sdkState.complianceScope
if (ctxScope && ctxScope.answers?.length > 0) {
syncingFromSdk.current = true
setScopeState(ctxScope)
setScopeState(migrateState(ctxScope))
setIsLoading(false)
} else if (isLoading) {
// SDK has no scope data — try localStorage fallback, then give up
try {
const stored = localStorage.getItem(STORAGE_KEY)
const stored = localStorage.getItem(projectStorageKey)
if (stored) {
const parsed = JSON.parse(stored) as ComplianceScopeState
const parsed = migrateState(JSON.parse(stored) as ComplianceScopeState)
if (parsed.answers?.length > 0) {
setScopeState(parsed)
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
@@ -106,7 +123,7 @@ export default function ComplianceScopePage() {
return
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(scopeState))
localStorage.setItem(projectStorageKey, JSON.stringify(scopeState))
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scopeState })
} catch (error) {
console.error('Failed to save compliance scope state:', error)
@@ -194,7 +211,7 @@ export default function ComplianceScopePage() {
const emptyState = createEmptyScopeState()
setScopeState(emptyState)
setActiveTab('overview')
localStorage.removeItem(STORAGE_KEY)
localStorage.removeItem(projectStorageKey)
}, [])
// Calculate completion statistics

View File

@@ -58,22 +58,23 @@ export function ScopeDecisionTab({
return 'from-green-500 to-green-600'
}
const getSeverityBadge = (severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL') => {
const colors = {
LOW: 'bg-gray-100 text-gray-800',
MEDIUM: 'bg-yellow-100 text-yellow-800',
HIGH: 'bg-orange-100 text-orange-800',
CRITICAL: 'bg-red-100 text-red-800',
const getSeverityBadge = (severity: string) => {
const s = severity.toLowerCase()
const colors: Record<string, string> = {
low: 'bg-gray-100 text-gray-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-orange-100 text-orange-800',
critical: 'bg-red-100 text-red-800',
}
const labels = {
LOW: 'Niedrig',
MEDIUM: 'Mittel',
HIGH: 'Hoch',
CRITICAL: 'Kritisch',
const labels: Record<string, string> = {
low: 'Niedrig',
medium: 'Mittel',
high: 'Hoch',
critical: 'Kritisch',
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[severity]}`}>
{labels[severity]}
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[s] || colors.medium}`}>
{labels[s] || severity}
</span>
)
}
@@ -254,9 +255,9 @@ export function ScopeDecisionTab({
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<span className="font-medium text-gray-900">{trigger.rule.description}</span>
<span className="font-medium text-gray-900">{trigger.description}</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-red-200 text-red-800 font-medium">
Min. {trigger.rule.minimumLevel}
Min. {trigger.minimumLevel}
</span>
</div>
<svg
@@ -272,18 +273,18 @@ export function ScopeDecisionTab({
</button>
{expandedTrigger === idx && (
<div className="px-4 pb-4 pt-2 border-t border-gray-200">
<p className="text-sm text-gray-700 mb-2">{trigger.explanation}</p>
{trigger.rule.legalReference && (
<p className="text-sm text-gray-700 mb-2">{trigger.description}</p>
{trigger.legalReference && (
<p className="text-xs text-gray-600 mb-2">
<span className="font-medium">Rechtsgrundlage:</span> {trigger.rule.legalReference}
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
</p>
)}
{trigger.rule.mandatoryDocuments && trigger.rule.mandatoryDocuments.length > 0 && (
{trigger.mandatoryDocuments && trigger.mandatoryDocuments.length > 0 && (
<p className="text-xs text-gray-700">
<span className="font-medium">Pflichtdokumente:</span> {trigger.rule.mandatoryDocuments.join(', ')}
<span className="font-medium">Pflichtdokumente:</span> {trigger.mandatoryDocuments.join(', ')}
</p>
)}
{trigger.rule.dsfaRequired && (
{trigger.requiresDSFA && (
<p className="text-xs text-orange-700 font-medium mt-1">DSFA erforderlich</p>
)}
</div>
@@ -302,10 +303,10 @@ export function ScopeDecisionTab({
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Typ</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Tiefe</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Dokument</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Priorität</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aufwand</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Status</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Trigger</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aktion</th>
</tr>
</thead>
@@ -317,16 +318,16 @@ export function ScopeDecisionTab({
<span className="font-medium text-gray-900">
{DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType}
</span>
{doc.required && (
{doc.requirement === 'mandatory' && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
Pflicht
</span>
)}
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-700">{doc.depth}</td>
<td className="py-3 px-4 text-sm text-gray-700 capitalize">{doc.priority}</td>
<td className="py-3 px-4 text-sm text-gray-700">
{doc.estimatedEffort || '-'}
{doc.estimatedEffort ? `${doc.estimatedEffort}h` : '-'}
</td>
<td className="py-3 px-4">
{doc.triggeredBy && doc.triggeredBy.length > 0 && (
@@ -361,10 +362,12 @@ export function ScopeDecisionTab({
{decision.riskFlags.map((flag, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{flag.title}</h4>
<h4 className="font-semibold text-gray-900">{flag.message}</h4>
{getSeverityBadge(flag.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{flag.description}</p>
{flag.legalReference && (
<p className="text-xs text-gray-500 mb-2">{flag.legalReference}</p>
)}
<p className="text-sm text-gray-600">
<span className="font-medium">Empfehlung:</span> {flag.recommendation}
</p>
@@ -382,26 +385,19 @@ export function ScopeDecisionTab({
{decision.gaps.map((gap, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{gap.title}</h4>
<h4 className="font-semibold text-gray-900">{gap.description}</h4>
{getSeverityBadge(gap.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{gap.description}</p>
<p className="text-sm text-gray-600 mb-2">
<span className="font-medium">Empfehlung:</span> {gap.recommendation}
<p className="text-sm text-gray-700 mb-2">
<span className="font-medium">Ist:</span> {gap.currentState}
</p>
{gap.relatedDocuments && gap.relatedDocuments.length > 0 && (
<div className="mt-2">
<span className="text-xs text-gray-500">Betroffene Dokumente: </span>
{gap.relatedDocuments.map((doc, docIdx) => (
<span
key={docIdx}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 mr-1"
>
{DOCUMENT_TYPE_LABELS[doc] || doc}
</span>
))}
</div>
)}
<p className="text-sm text-gray-600 mb-2">
<span className="font-medium">Soll:</span> {gap.targetState}
</p>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>Aufwand: ~{gap.effort}h</span>
<span>Level: {gap.requiredFor}</span>
</div>
</div>
))}
</div>
@@ -416,21 +412,21 @@ export function ScopeDecisionTab({
{decision.nextActions.map((action, idx) => (
<div key={idx} className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-bold text-purple-700">{action.priority}</span>
<span className="text-sm font-bold text-purple-700">{idx + 1}</span>
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-1">{action.title}</h4>
<p className="text-sm text-gray-700 mb-2">{action.description}</p>
<div className="flex items-center gap-3">
{action.effortDays && (
{action.estimatedEffort > 0 && (
<span className="text-xs text-gray-600">
<span className="font-medium">Aufwand:</span> {action.effortDays} Tage
<span className="font-medium">Aufwand:</span> ~{action.estimatedEffort}h
</span>
)}
{action.relatedDocuments && action.relatedDocuments.length > 0 && (
<span className="text-xs text-gray-600">
<span className="font-medium">Dokumente:</span> {action.relatedDocuments.length}
</span>
{action.sdkStepUrl && (
<a href={action.sdkStepUrl} className="text-xs text-purple-600 hover:text-purple-700">
Zum SDK-Schritt
</a>
)}
</div>
</div>
@@ -469,8 +465,8 @@ export function ScopeDecisionTab({
)}
</div>
{/* Audit Trail */}
{decision.auditTrail && decision.auditTrail.length > 0 && (
{/* Audit Trail (from reasoning) */}
{decision.reasoning && decision.reasoning.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<button
type="button"
@@ -489,17 +485,20 @@ export function ScopeDecisionTab({
</button>
{showAuditTrail && (
<div className="space-y-3">
{decision.auditTrail.map((entry, idx) => (
{decision.reasoning.map((entry, idx) => (
<div key={idx} className="border-l-2 border-purple-300 pl-4 py-2">
<h4 className="font-medium text-gray-900 mb-1">{entry.step}</h4>
<p className="text-sm text-gray-700 mb-2">{entry.description}</p>
{entry.details && entry.details.length > 0 && (
{entry.factors && entry.factors.length > 0 && (
<ul className="text-xs text-gray-600 space-y-1">
{entry.details.map((detail, detailIdx) => (
<li key={detailIdx}> {detail}</li>
{entry.factors.map((factor, factorIdx) => (
<li key={factorIdx}> {factor}</li>
))}
</ul>
)}
{entry.impact && (
<p className="text-xs text-purple-700 font-medium mt-1">{entry.impact}</p>
)}
</div>
))}
</div>

View File

@@ -31,12 +31,12 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
const handleDownloadCSV = useCallback(() => {
if (!decision || !decision.requiredDocuments) return
const headers = ['Typ', 'Tiefe', 'Aufwand (Tage)', 'Pflicht', 'Hard-Trigger']
const headers = ['Typ', 'Priorität', 'Aufwand (Stunden)', 'Pflicht', 'Hard-Trigger']
const rows = decision.requiredDocuments.map((doc) => [
DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType,
doc.depth,
doc.estimatedEffort || '0',
doc.required ? 'Ja' : 'Nein',
doc.priority,
String(doc.estimatedEffort || 0),
doc.requirement === 'mandatory' ? 'Ja' : 'Nein',
doc.triggeredBy.length > 0 ? 'Ja' : 'Nein',
])
@@ -58,8 +58,8 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
markdown += `**Datum:** ${new Date().toLocaleDateString('de-DE')}\n\n`
markdown += `## Einstufung\n\n`
markdown += `**Level:** ${decision.determinedLevel} - ${DEPTH_LEVEL_LABELS[decision.determinedLevel]}\n\n`
if (decision.reasoning) {
markdown += `**Begründung:** ${decision.reasoning}\n\n`
if (decision.reasoning && decision.reasoning.length > 0) {
markdown += `**Begründung:** ${decision.reasoning.map(r => r.description).filter(Boolean).join('. ')}\n\n`
}
if (decision.scores) {
@@ -73,10 +73,10 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
if (decision.triggeredHardTriggers && decision.triggeredHardTriggers.length > 0) {
markdown += `## Aktive Hard-Trigger\n\n`
decision.triggeredHardTriggers.forEach((trigger) => {
markdown += `- **${trigger.rule.label}**\n`
markdown += ` - ${trigger.rule.description}\n`
if (trigger.rule.legalReference) {
markdown += ` - Rechtsgrundlage: ${trigger.rule.legalReference}\n`
markdown += `- **${trigger.description}**\n`
markdown += ` - ${trigger.description}\n`
if (trigger.legalReference) {
markdown += ` - Rechtsgrundlage: ${trigger.legalReference}\n`
}
})
markdown += `\n`
@@ -84,12 +84,12 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
if (decision.requiredDocuments && decision.requiredDocuments.length > 0) {
markdown += `## Erforderliche Dokumente\n\n`
markdown += `| Typ | Tiefe | Aufwand | Pflicht | Hard-Trigger |\n`
markdown += `|-----|-------|---------|---------|-------------|\n`
markdown += `| Typ | Priorität | Aufwand (h) | Pflicht | Hard-Trigger |\n`
markdown += `|-----|-----------|-------------|---------|-------------|\n`
decision.requiredDocuments.forEach((doc) => {
markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depth} | ${
doc.estimatedEffort || '0'
} | ${doc.required ? 'Ja' : 'Nein'} | ${doc.triggeredBy.length > 0 ? 'Ja' : 'Nein'} |\n`
markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.priority} | ${
doc.estimatedEffort || 0
} | ${doc.requirement === 'mandatory' ? 'Ja' : 'Nein'} | ${doc.triggeredBy.length > 0 ? 'Ja' : 'Nein'} |\n`
})
markdown += `\n`
}
@@ -97,19 +97,29 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
if (decision.riskFlags && decision.riskFlags.length > 0) {
markdown += `## Risiko-Flags\n\n`
decision.riskFlags.forEach((flag) => {
markdown += `### ${flag.title} (${flag.severity})\n\n`
markdown += `${flag.description}\n\n`
markdown += `### ${flag.message} (${flag.severity})\n\n`
if (flag.legalReference) markdown += `Rechtsgrundlage: ${flag.legalReference}\n\n`
markdown += `**Empfehlung:** ${flag.recommendation}\n\n`
})
}
if (decision.gaps && decision.gaps.length > 0) {
markdown += `## Gap-Analyse\n\n`
decision.gaps.forEach((gap) => {
markdown += `### ${gap.description} (${gap.severity})\n\n`
markdown += `- **Ist:** ${gap.currentState}\n`
markdown += `- **Soll:** ${gap.targetState}\n`
markdown += `- **Aufwand:** ~${gap.effort}h\n\n`
})
}
if (decision.nextActions && decision.nextActions.length > 0) {
markdown += `## Nächste Schritte\n\n`
decision.nextActions.forEach((action) => {
markdown += `${action.priority}. **${action.title}**\n`
decision.nextActions.forEach((action, idx) => {
markdown += `${idx + 1}. **${action.title}**\n`
markdown += ` ${action.description}\n`
if (action.estimatedEffort) {
markdown += ` Aufwand: ${action.estimatedEffort}\n`
markdown += ` Aufwand: ~${action.estimatedEffort}h\n`
}
markdown += `\n`
})

View File

@@ -51,7 +51,7 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
}
const renderLevelBadge = () => {
if (!decision?.level) {
if (!decision?.determinedLevel) {
return (
<div className="bg-gray-100 border border-gray-300 rounded-xl p-8 text-center">
<div className="inline-flex items-center justify-center w-24 h-24 bg-gray-200 rounded-full mb-4">
@@ -80,13 +80,7 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
}
const renderActiveHardTriggers = () => {
if (!decision?.hardTriggers || decision.triggeredHardTriggers.length === 0) {
return null
}
const activeHardTriggers = decision.triggeredHardTriggers.filter((ht) => ht.matched)
if (activeHardTriggers.length === 0) {
if (!decision?.triggeredHardTriggers || decision.triggeredHardTriggers.length === 0) {
return null
}
@@ -104,23 +98,22 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
<h3 className="text-lg font-semibold text-gray-900">Aktive Hard-Trigger</h3>
</div>
<div className="space-y-3">
{activeHardTriggers.map((trigger, idx) => (
{decision.triggeredHardTriggers.map((trigger, idx) => (
<div
key={idx}
className="border-l-4 border-red-500 bg-red-50 rounded-r-lg p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{trigger.label}</h4>
<p className="text-sm text-gray-600 mt-1">{trigger.description}</p>
<h4 className="font-semibold text-gray-900">{trigger.description}</h4>
{trigger.legalReference && (
<p className="text-xs text-gray-500 mt-2">
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
</p>
)}
{trigger.matchedValue && (
{trigger.minimumLevel && (
<p className="text-xs text-gray-700 mt-1">
<span className="font-medium">Erfasster Wert:</span> {trigger.matchedValue}
<span className="font-medium">Mindest-Level:</span> {trigger.minimumLevel}
</p>
)}
</div>
@@ -137,12 +130,9 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
return null
}
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.isMandatory)
const optionalDocs = decision.requiredDocuments.filter((doc) => !doc.isMandatory)
const totalEffortDays = decision.requiredDocuments.reduce(
(sum, doc) => sum + (doc.effortEstimate?.days ?? 0),
0
)
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.requirement === 'mandatory')
const optionalDocs = decision.requiredDocuments.filter((doc) => doc.requirement === 'recommended')
const totalEffortHours = decision.requiredDocuments.reduce((sum, doc) => sum + (doc.estimatedEffort ?? 0), 0)
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
@@ -157,8 +147,8 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
<div className="text-sm text-gray-600 mt-1">Optional</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-gray-900">{totalEffortDays}</div>
<div className="text-sm text-gray-600 mt-1">Tage Aufwand (geschätzt)</div>
<div className="text-3xl font-bold text-gray-900">{totalEffortHours}h</div>
<div className="text-sm text-gray-600 mt-1">Aufwand (geschätzt)</div>
</div>
</div>
</div>
@@ -227,11 +217,11 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Übersicht</h3>
<div className="space-y-4">
{renderScoreGauge('Risiko-Score', decision.scores?.riskScore)}
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexityScore)}
{renderScoreGauge('Assurance-Score', decision.scores?.assuranceScore)}
{renderScoreGauge('Risiko-Score', decision.scores?.risk_score)}
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexity_score)}
{renderScoreGauge('Assurance-Score', decision.scores?.assurance_need)}
<div className="pt-4 border-t border-gray-200">
{renderScoreGauge('Gesamt-Score', decision.scores?.compositeScore)}
{renderScoreGauge('Gesamt-Score', decision.scores?.composite_score)}
</div>
</div>
</div>

View File

@@ -350,6 +350,10 @@ export function ScopeWizardTab({
const unanswered = getUnansweredRequiredQuestions(answers, block.id)
const hasRequired = block.questions.some(q => q.required)
const allRequiredDone = hasRequired && unanswered.length === 0
// For optional-only blocks: check if any questions were answered
const answeredIds = new Set(answers.map(a => a.questionId))
const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id))
const optionalDone = !hasRequired && hasAnyAnswer
return (
<button
@@ -366,11 +370,12 @@ export function ScopeWizardTab({
<span className={`text-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>
{block.title}
</span>
{allRequiredDone ? (
{allRequiredDone || optionalDone ? (
<span className="flex items-center gap-1 text-xs font-semibold text-green-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.5} d="M5 13l4 4L19 7" />
</svg>
{!hasRequired && <span>(optional)</span>}
</span>
) : !hasRequired ? (
<span className="text-xs text-gray-400">(nur optional)</span>
@@ -383,7 +388,7 @@ export function ScopeWizardTab({
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
<div
className={`h-full transition-all ${
allRequiredDone
allRequiredDone || optionalDone
? 'bg-green-500'
: !hasRequired
? 'bg-gray-300'

View File

@@ -1664,7 +1664,7 @@ export class ComplianceScopeEngine {
step: 'hard_trigger_evaluation',
description: `${triggers.length} Hard Trigger Rule(s) aktiviert`,
factors: triggers.map(
(t) => `${t.ruleId}: ${t.description} (${t.legalReference})`
(t) => `${t.ruleId}: ${t.description}${t.legalReference ? ` (${t.legalReference})` : ''}`
),
impact: `Höchstes Trigger-Level: ${this.getMaxTriggerLevel(triggers)}`,
})

View File

@@ -154,12 +154,20 @@ export interface HardTriggerRule {
* Getriggerter Hard Trigger mit Kontext
*/
export interface TriggeredHardTrigger {
/** Die getriggerte Regel */
rule: HardTriggerRule;
/** Der tatsächlich gefundene Wert */
matchedValue: unknown;
/** Erklärung warum getriggert */
explanation: string;
/** Regel-ID */
ruleId: string;
/** Kategorie */
category: string;
/** Beschreibung */
description: string;
/** Rechtsgrundlage */
legalReference?: string;
/** Mindest-Level */
minimumLevel: ComplianceDepthLevel;
/** DSFA erforderlich? */
requiresDSFA: boolean;
/** Pflichtdokumente */
mandatoryDocuments: string[];
}
// ============================================================================
@@ -229,14 +237,12 @@ export interface RequiredDocument {
documentType: ScopeDocumentType;
/** Anzeigename */
label: string;
/** Ist Pflicht? */
required: boolean;
/** Erforderliche Tiefe (z.B. "Basis", "Standard", "Detailliert") */
depth: string;
/** Konkrete Anforderungen/Inhalte */
detailItems: string[];
/** Geschätzter Aufwand */
estimatedEffort: string;
/** Pflicht oder empfohlen */
requirement: 'mandatory' | 'recommended';
/** Priorität */
priority: 'high' | 'medium' | 'low';
/** Geschätzter Aufwand in Stunden */
estimatedEffort: number;
/** Von welchen Triggern/Regeln gefordert */
triggeredBy: string[];
/** Link zum SDK-Schritt */
@@ -247,14 +253,12 @@ export interface RequiredDocument {
* Risiko-Flag
*/
export interface RiskFlag {
/** Eindeutige ID */
id: string;
/** Schweregrad */
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
/** Titel */
title: string;
severity: string;
/** Kategorie */
category: string;
/** Beschreibung */
description: string;
message: string;
/** Rechtsgrundlage */
legalReference?: string;
/** Empfehlung zur Behebung */
@@ -265,38 +269,44 @@ export interface RiskFlag {
* Identifizierte Lücke in der Compliance
*/
export interface ScopeGap {
/** Eindeutige ID */
id: string;
/** Gap-Typ */
gapType: string;
/** Schweregrad */
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
/** Titel */
title: string;
severity: string;
/** Beschreibung */
description: string;
/** Empfehlung zur Schließung */
recommendation: string;
/** Betroffene Dokumente */
relatedDocuments: ScopeDocumentType[];
/** Erforderlich für Level */
requiredFor: ComplianceDepthLevel;
/** Aktueller Zustand */
currentState: string;
/** Zielzustand */
targetState: string;
/** Aufwand in Stunden */
effort: number;
/** Priorität */
priority: string;
}
/**
* Nächster empfohlener Schritt
*/
export interface NextAction {
/** Eindeutige ID */
id: string;
/** Priorität (1 = höchste) */
priority: number;
/** Aktionstyp */
actionType: 'create_document' | 'establish_process' | 'implement_technical' | 'organizational_change';
/** Titel */
title: string;
/** Beschreibung */
description: string;
/** Geschätzter Aufwand */
estimatedEffort: string;
/** Betroffene Dokumente */
relatedDocuments: ScopeDocumentType[];
/** Priorität */
priority: string;
/** Geschätzter Aufwand in Stunden */
estimatedEffort: number;
/** Dokumenttyp (optional) */
documentType?: ScopeDocumentType;
/** Link zum SDK-Schritt */
sdkStepUrl?: string;
/** Blocker */
blockers: string[];
}
/**
@@ -307,8 +317,10 @@ export interface ScopeReasoning {
step: string;
/** Kurzbeschreibung */
description: string;
/** Detaillierte Punkte */
details: string[];
/** Faktoren */
factors: string[];
/** Auswirkung */
impact: string;
}
// ============================================================================