feat: Control-Detail Provenance + Atomare Controls Seite
All checks were successful
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 41s
CI/CD / test-python-backend-compliance (push) Successful in 40s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 4s

Backend: provenance endpoint (obligations, doc refs, merged duplicates,
regulations summary) + atomic-stats aggregation endpoint.
Frontend: ControlDetail mit Provenance-Sektionen, klickbare Navigation,
neue /sdk/atomic-controls Seite mit Stats-Bar und gefilterer Liste.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-24 10:38:34 +01:00
parent 200facda6a
commit 6d3bdf8e74
8 changed files with 1210 additions and 5 deletions

View File

@@ -10,7 +10,9 @@ import {
CanonicalControl, EFFORT_LABELS, BACKEND_URL,
SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge,
ObligationTypeBadge, GenerationStrategyBadge,
ExtractionMethodBadge, RegulationCountBadge,
VERIFICATION_METHODS, CATEGORY_OPTIONS,
ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary,
} from './helpers'
interface SimilarControl {
@@ -54,6 +56,13 @@ interface TraceabilityData {
decomposition_method: string
}>
source_count: number
// Extended provenance fields
obligations?: ObligationInfo[]
obligation_count?: number
document_references?: DocumentReference[]
merged_duplicates?: MergedDuplicate[]
merged_duplicates_count?: number
regulations_summary?: RegulationSummary[]
}
interface ControlDetailProps {
@@ -63,6 +72,7 @@ interface ControlDetailProps {
onDelete: (controlId: string) => void
onReview: (controlId: string, action: string) => void
onRefresh?: () => void
onNavigateToControl?: (controlId: string) => void
// Review mode navigation
reviewMode?: boolean
reviewIndex?: number
@@ -78,6 +88,7 @@ export function ControlDetail({
onDelete,
onReview,
onRefresh,
onNavigateToControl,
reviewMode,
reviewIndex = 0,
reviewTotal = 0,
@@ -94,7 +105,11 @@ export function ControlDetail({
const loadTraceability = useCallback(async () => {
setLoadingTrace(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=traceability&id=${ctrl.control_id}`)
// Try provenance first (extended data), fall back to traceability
let res = await fetch(`${BACKEND_URL}?endpoint=provenance&id=${ctrl.control_id}`)
if (!res.ok) {
res = await fetch(`${BACKEND_URL}?endpoint=traceability&id=${ctrl.control_id}`)
}
if (res.ok) {
setTraceability(await res.json())
}
@@ -296,6 +311,11 @@ export function ControlDetail({
Rechtsgrundlagen ({traceability.source_count} {traceability.source_count === 1 ? 'Quelle' : 'Quellen'})
</h3>
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
{traceability.regulations_summary && traceability.regulations_summary.map(rs => (
<span key={rs.regulation_code} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-200 text-violet-800">
{rs.regulation_code}
</span>
))}
{loadingTrace && <span className="text-xs text-violet-400">Laden...</span>}
</div>
<div className="space-y-3">
@@ -329,9 +349,18 @@ export function ControlDetail({
</div>
<p className="text-xs text-violet-600 mt-1">
via{' '}
<span className="font-mono font-medium text-purple-700 bg-purple-50 px-1 py-0.5 rounded">
{link.parent_control_id}
</span>
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(link.parent_control_id)}
className="font-mono font-medium text-purple-700 bg-purple-50 px-1 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{link.parent_control_id}
</button>
) : (
<span className="font-mono font-medium text-purple-700 bg-purple-50 px-1 py-0.5 rounded">
{link.parent_control_id}
</span>
)}
{link.parent_title && (
<span className="text-violet-500 ml-1"> {link.parent_title}</span>
)}
@@ -378,6 +407,100 @@ export function ControlDetail({
</section>
)}
{/* Document References (atomic controls) */}
{traceability && traceability.is_atomic && traceability.document_references && traceability.document_references.length > 0 && (
<section className="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<FileText className="w-4 h-4 text-indigo-600" />
<h3 className="text-sm font-semibold text-indigo-900">
Original-Dokumente ({traceability.document_references.length})
</h3>
</div>
<div className="space-y-2">
{traceability.document_references.map((dr, i) => (
<div key={i} className="flex items-center gap-2 text-sm bg-white/60 border border-indigo-100 rounded-lg p-2">
<span className="font-semibold text-indigo-900">{dr.regulation_code}</span>
{dr.article && <span className="text-indigo-700">{dr.article}</span>}
{dr.paragraph && <span className="text-indigo-600 text-xs">{dr.paragraph}</span>}
<span className="ml-auto flex items-center gap-1.5">
<ExtractionMethodBadge method={dr.extraction_method} />
{dr.confidence !== null && (
<span className="text-xs text-gray-500">{(dr.confidence * 100).toFixed(0)}%</span>
)}
</span>
</div>
))}
</div>
</section>
)}
{/* Obligations (rich controls) */}
{traceability && !traceability.is_atomic && traceability.obligations && traceability.obligations.length > 0 && (
<section className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Scale className="w-4 h-4 text-amber-600" />
<h3 className="text-sm font-semibold text-amber-900">
Abgeleitete Pflichten ({traceability.obligation_count ?? traceability.obligations.length})
</h3>
</div>
<div className="space-y-2">
{traceability.obligations.map((ob) => (
<div key={ob.candidate_id} className="bg-white/60 border border-amber-100 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-mono text-xs text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded">{ob.candidate_id}</span>
<span className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${
ob.normative_strength === 'must' ? 'bg-red-100 text-red-700' :
ob.normative_strength === 'should' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{ob.normative_strength === 'must' ? 'MUSS' :
ob.normative_strength === 'should' ? 'SOLL' : 'KANN'}
</span>
{ob.action && <span className="text-xs text-amber-600">{ob.action}</span>}
{ob.object && <span className="text-xs text-amber-500"> {ob.object}</span>}
</div>
<p className="text-xs text-gray-700 leading-relaxed">
{ob.obligation_text.slice(0, 300)}
{ob.obligation_text.length > 300 ? '...' : ''}
</p>
</div>
))}
</div>
</section>
)}
{/* Merged Duplicates */}
{traceability && traceability.merged_duplicates && traceability.merged_duplicates.length > 0 && (
<section className="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<GitMerge className="w-4 h-4 text-slate-600" />
<h3 className="text-sm font-semibold text-slate-900">
Zusammengefuehrte Duplikate ({traceability.merged_duplicates_count ?? traceability.merged_duplicates.length})
</h3>
</div>
<div className="space-y-1.5">
{traceability.merged_duplicates.map((dup) => (
<div key={dup.control_id} className="flex items-center gap-2 text-sm">
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(dup.control_id)}
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{dup.control_id}
</button>
) : (
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{dup.control_id}</span>
)}
<span className="text-gray-700 flex-1 truncate">{dup.title}</span>
{dup.source_regulation && (
<span className="text-xs text-slate-500 bg-slate-100 px-1.5 py-0.5 rounded">{dup.source_regulation}</span>
)}
</div>
))}
</div>
</section>
)}
{/* Child controls (rich controls that have atomic children) */}
{traceability && traceability.children.length > 0 && (
<section className="bg-emerald-50 border border-emerald-200 rounded-lg p-4">
@@ -390,7 +513,16 @@ export function ControlDetail({
<div className="space-y-1.5">
{traceability.children.map((child) => (
<div key={child.control_id} className="flex items-center gap-2 text-sm">
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{child.control_id}</span>
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(child.control_id)}
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{child.control_id}
</button>
) : (
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{child.control_id}</span>
)}
<span className="text-gray-700 flex-1 truncate">{child.title}</span>
<SeverityBadge severity={child.severity} />
</div>