feat(ai-sdk): legal-corpus coverage + Phase-2 citation-graph assessment (#33)
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

This commit was merged in pull request #33.
This commit is contained in:
2026-06-24 06:37:22 +00:00
parent b83c3e6e00
commit 230dc05287
14 changed files with 941 additions and 14 deletions
+83 -3
View File
@@ -3,6 +3,7 @@ import Link from 'next/link'
import {
type UseCaseRow,
type CorpusOverview,
type LegalCorpus,
licenseTierBadgeClass,
commercialBadgeClass,
groupUseCases,
@@ -11,28 +12,46 @@ import {
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] = await Promise.all([
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 }
return { useCases: [], corpus: null, legalCorpus: null }
}
}
@@ -46,7 +65,7 @@ function Stat({ label, value }: { label: string; value: string | number }) {
}
export default async function CoveragePage() {
const { useCases, corpus } = await getData()
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)
@@ -221,6 +240,67 @@ export default async function CoveragePage() {
</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">