Files
breakpilot-compliance/admin-compliance/app/sdk/coverage/page.tsx
T
Benjamin_Boenisch 230dc05287
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / build-sha-integrity (push) Successful in 6s
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 6s
CI / go-lint (push) Has been skipped
CI / loc-budget (push) Successful in 19s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / test-go (push) Successful in 59s
CI / iace-gt-coverage (push) Successful in 22s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(ai-sdk): legal-corpus coverage + Phase-2 citation-graph assessment (#33)
2026-06-24 06:37:22 +00:00

358 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Fragment } from 'react'
import Link from 'next/link'
import {
type UseCaseRow,
type CorpusOverview,
type LegalCorpus,
licenseTierBadgeClass,
commercialBadgeClass,
groupUseCases,
groupCorpusDocs,
} from './_helpers'
const BACKEND_URL =
process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
// The legal-corpus structure comes from the Go SDK (it owns the vector store).
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export const dynamic = 'force-dynamic'
// Fetched from the SDK and isolated in its own try/catch so a vector-store
// hiccup degrades to "no structure shown" instead of blanking the whole page.
async function fetchLegalCorpus(): Promise<LegalCorpus | null> {
try {
const res = await fetch(`${SDK_URL}/sdk/v1/rag/legal-corpus`, {
cache: 'no-store',
})
return res.ok ? await res.json() : null
} catch {
return null
}
}
async function getData(): Promise<{
useCases: UseCaseRow[]
corpus: CorpusOverview | null
legalCorpus: LegalCorpus | null
}> {
try {
const [ucRes, corpusRes, legalCorpus] = await Promise.all([
fetch(`${BACKEND_URL}/api/compliance/v1/controls/use-cases`, {
cache: 'no-store',
}),
fetch(`${BACKEND_URL}/api/compliance/v1/controls/corpus`, {
cache: 'no-store',
}),
fetchLegalCorpus(),
])
return {
useCases: ucRes.ok ? await ucRes.json() : [],
corpus: corpusRes.ok ? await corpusRes.json() : null,
legalCorpus,
}
} catch {
return { useCases: [], corpus: null, legalCorpus: null }
}
}
function Stat({ label, value }: { label: string; value: string | number }) {
return (
<div className="rounded-lg border border-gray-200 bg-white px-4 py-3">
<div className="text-2xl font-semibold text-gray-900">{value}</div>
<div className="text-xs text-gray-500">{label}</div>
</div>
)
}
export default async function CoveragePage() {
const { useCases, corpus, legalCorpus } = await getData()
const groups = groupUseCases(useCases)
const totalRelevant = useCases.reduce((s, u) => s + u.atom_relevant, 0)
const totalAtoms = useCases.reduce((s, u) => s + u.atom_total, 0)
const totalReview = totalAtoms - totalRelevant
return (
<div className="mx-auto max-w-7xl space-y-10 p-6">
<header className="space-y-1">
<h1 className="text-2xl font-bold text-gray-900">
Compliance-Abdeckung
</h1>
<p className="text-sm text-gray-600">
Alle ableitbaren Use Cases und alle Quell-Dokumente im Korpus inkl.
Lizenz damit kein Thema und keine Quelle vergessen wird.
</p>
</header>
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Stat label="Use Cases" value={useCases.length} />
<Stat label="Kern-Pflichten" value={totalRelevant.toLocaleString('de-DE')} />
<Stat label="zur Prüfung" value={totalReview.toLocaleString('de-DE')} />
<Stat label="Quell-Dokumente" value={corpus?.totals.documents ?? 0} />
</section>
{corpus?.license_summary?.length ? (
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">Lizenz-Verteilung</h2>
<div className="flex flex-wrap gap-3">
{corpus.license_summary.map((l) => (
<div
key={String(l.license_rule)}
className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2"
>
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(l.license_rule)}`}
>
Tier {l.license_rule ?? '?'}
</span>
<span className="text-sm text-gray-700">{l.label}</span>
<span className="text-sm font-semibold text-gray-900">
{l.atom_count.toLocaleString('de-DE')}
</span>
</div>
))}
</div>
</section>
) : null}
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Use Cases</h2>
{groups.map((g) => (
<div key={g.group} className="space-y-1">
<h3 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
{g.label} ({g.rows.length})
</h3>
<div className="overflow-hidden rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Use Case</th>
<th className="px-4 py-2">Key</th>
<th className="px-4 py-2 text-right">Kern</th>
<th className="px-4 py-2 text-right">zur Prüfung</th>
<th className="px-4 py-2 text-right">gesamt</th>
<th className="px-4 py-2">Quellen</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{g.rows.map((u) => (
<tr key={u.key} className={u.atom_total === 0 ? 'text-gray-400' : ''}>
<td className="px-4 py-2 font-medium">
<Link
href={`/sdk/coverage/${u.key}`}
className="text-purple-700 hover:underline"
>
{u.label}
</Link>
</td>
<td className="px-4 py-2 font-mono text-xs text-gray-500">{u.key}</td>
<td className="px-4 py-2 text-right font-semibold">
{u.atom_relevant.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-right text-amber-700">
{(u.atom_total - u.atom_relevant).toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-right text-gray-500">
{u.atom_total.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-xs text-gray-500">
{u.regulations.slice(0, 4).join(', ')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</section>
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">
Korpus-Dokumente ({corpus?.documents.length ?? 0})
</h2>
<p className="text-xs text-gray-500">
Quell-Regulierung × Lizenz-Tier × Anzahl Pflichten × gemappter Use Case.
</p>
<div className="max-h-[28rem] overflow-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="sticky top-0 bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Dokument / Quelle</th>
<th className="px-4 py-2">Lizenz</th>
<th className="px-4 py-2 text-right">Pflichten</th>
<th className="px-4 py-2">Use Case</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{groupCorpusDocs(corpus?.documents ?? []).map((cat) => (
<Fragment key={cat.key}>
<tr className="bg-gray-100">
<td
colSpan={4}
className="px-4 py-2 text-sm font-semibold text-gray-800"
>
{cat.label}{' '}
<span className="font-normal text-gray-500">
({cat.families.reduce((s, f) => s + f.docs.length, 0)} Quellen ·{' '}
{cat.total.toLocaleString('de-DE')} Pflichten)
</span>
</td>
</tr>
{cat.families.map((fam) => (
<Fragment key={cat.key + fam.family}>
<tr className="bg-gray-50">
<td
colSpan={4}
className="px-4 py-1 pl-8 text-xs font-medium uppercase tracking-wide text-gray-500"
>
{fam.family}
</td>
</tr>
{fam.docs.map((d) => (
<tr key={d.source_regulation}>
<td className="px-4 py-2 pl-8 text-gray-900">
{d.source_regulation}
</td>
<td className="px-4 py-2">
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(d.license_rule)}`}
title={d.license_tier}
>
Tier {d.license_rule ?? '?'}
</span>
</td>
<td className="px-4 py-2 text-right">
{d.atom_count.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 font-mono text-xs text-gray-600">
{d.use_case ?? (
<span className="text-amber-600"> ungemappt</span>
)}
</td>
</tr>
))}
</Fragment>
))}
</Fragment>
))}
</tbody>
</table>
</div>
</section>
{legalCorpus?.regulations?.length ? (
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">
Ingestierter Rechtskorpus Struktur ({legalCorpus.totals.regulations}{' '}
Rechtsakte)
</h2>
<p className="text-xs text-gray-500">
Woraus jeder ingestierte eur-lex-Rechtsakt tatsächlich besteht:
Artikel (§), Anhänge, Erwägungsgründe und retrievbare Chunks direkt
aus dem Vektorspeicher, damit kein Black-Box-Korpus entsteht.
</p>
<div className="overflow-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Rechtsakt</th>
<th className="px-4 py-2 text-right">Artikel (§)</th>
<th className="px-4 py-2 text-right">Anhänge</th>
<th className="px-4 py-2 text-right">Erwägungsgründe</th>
<th className="px-4 py-2 text-right">Chunks</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{legalCorpus.regulations.map((r) => (
<tr key={r.regulation_short}>
<td className="px-4 py-2 text-gray-900">
<span className="font-medium">{r.regulation_short}</span>
{r.regulation_name !== r.regulation_short ? (
<span className="ml-2 text-xs text-gray-500">
{r.regulation_name}
</span>
) : null}
</td>
<td className="px-4 py-2 text-right font-semibold">
{r.articles.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-right">
{r.annexes > 0 ? (
r.annexes.toLocaleString('de-DE')
) : (
<span className="text-gray-300"></span>
)}
</td>
<td className="px-4 py-2 text-right text-gray-500">
{r.recitals > 0 ? (
r.recitals.toLocaleString('de-DE')
) : (
<span className="text-gray-300"></span>
)}
</td>
<td className="px-4 py-2 text-right text-gray-500">
{r.chunks.toLocaleString('de-DE')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
) : null}
{corpus?.license_catalog?.length ? (
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">
Lizenz-Katalog ({corpus.license_catalog.length} kuratierte Quellen)
</h2>
<p className="text-xs text-gray-500">
Detaillierte Nutzungsrechte je kuratierter Quelle (kommerzielle
Nutzung, Auslieferung im Produkt).
</p>
<div className="overflow-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Quelle</th>
<th className="px-4 py-2">Herausgeber</th>
<th className="px-4 py-2">Lizenz</th>
<th className="px-4 py-2">kommerziell</th>
<th className="px-4 py-2">im Produkt</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{corpus.license_catalog.map((c) => (
<tr key={c.source_id}>
<td className="px-4 py-2 text-gray-900">
{c.terms_url ? (
<a href={c.terms_url} target="_blank" rel="noreferrer" className="hover:underline">
{c.title}
</a>
) : (
c.title
)}
</td>
<td className="px-4 py-2 text-gray-600">{c.publisher ?? '—'}</td>
<td className="px-4 py-2 text-gray-600">{c.license_name ?? c.license_id ?? '—'}</td>
<td className="px-4 py-2">
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${commercialBadgeClass(c.commercial_use)}`}
>
{c.commercial_use ?? 'unbekannt'}
</span>
</td>
<td className="px-4 py-2 text-gray-600">
{c.ship_in_product ? 'ja' : 'nein'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
) : null}
</div>
)
}