feat(advisor): evidence-framed header + bindingness contract seam
Rework the Compliance Advisor header ("Diese Antwort stuetzt sich auf")
to describe the EVIDENCE rather than the documents: binding
Rechtsgrundlagen split from Leitlinien (soft-law guidance), a
per-regulation breakdown, plus Abbildungen, Fussnoten and Evidence Units.
No fabricated trust score — objective counts only.
- bindingness is a canonical Legal-KG fact (APEX rule): added an optional
EvidenceUnit.bindingness contract seam; the FE renders the split from it
and degrades to a neutral per-regulation breakdown when it is absent
(SDK/RAG asked via board to populate it in /retrieve).
- evidence-grouping.ts: pure, tested grouping/counting model.
- route.ts: optional `audience` field (tonality) kept out of the retrieval
question; answers lead with a "Kurz gesagt" summary, structured by theme.
- E2E + unit tests updated for the evidence framing.
Not deployed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { FileText, Hash, Image as ImageIcon, Library } from 'lucide-react'
|
||||
import { BookMarked, FileText, Hash, Image as ImageIcon, Library, Scale } from 'lucide-react'
|
||||
import type { AdvisorResponse } from '@/lib/sdk/advisor/contract'
|
||||
import { resolveRegulation } from '@/lib/sdk/advisor/regulation-display'
|
||||
import {
|
||||
provisionSummary,
|
||||
summarizeEvidence,
|
||||
type FamilyGroup,
|
||||
} from '@/lib/sdk/advisor/evidence-grouping'
|
||||
|
||||
function Card({
|
||||
const plural = (n: number, one: string, many: string) => (n === 1 ? one : many)
|
||||
|
||||
function Count({
|
||||
icon,
|
||||
value,
|
||||
label,
|
||||
@@ -30,26 +36,100 @@ function Card({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "Antwort basiert auf" — objective counts only (no fabricated trust score). Regelwerke = distinct
|
||||
* document families. Leitlinien deliberately omitted until bindingness exists in the Legal-KG.
|
||||
*/
|
||||
export function EvidenceSummary({ response }: { response: AdvisorResponse }) {
|
||||
const families = new Set(
|
||||
response.evidence.map((e) => resolveRegulation({ code: e.document, short: e.document }).familyKey),
|
||||
).size
|
||||
const cls = 'h-4 w-4'
|
||||
function GroupRow({ group, icon }: { group: FamilyGroup; icon: React.ReactNode }) {
|
||||
// A single-unit guidance doc needs no "1 Fundstelle" noise; norms always show their provisions.
|
||||
const detail = group.sections.length === 0 && group.units <= 1 ? '' : provisionSummary(group)
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
|
||||
Antwort basiert auf
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<Card icon={<Library className={cls} />} value={families} label="Regelwerke" />
|
||||
<Card icon={<FileText className={cls} />} value={response.evidence.length} label="Evidence Units" />
|
||||
<Card icon={<ImageIcon className={cls} />} value={response.visual_evidence.length} label="Diagramme" dim={response.visual_evidence.length === 0} />
|
||||
<Card icon={<Hash className={cls} />} value={response.footnotes.length} label="Fußnoten" dim={response.footnotes.length === 0} />
|
||||
</div>
|
||||
<div className="flex items-start gap-2 py-0.5">
|
||||
<span className="mt-0.5 shrink-0 text-gray-400">{icon}</span>
|
||||
<span className="min-w-0 flex-1 text-[12px] leading-snug text-gray-700">{group.label}</span>
|
||||
{detail && <span className="shrink-0 text-[11px] font-medium text-gray-500">{detail}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
groups,
|
||||
icon,
|
||||
}: {
|
||||
title: string
|
||||
groups: FamilyGroup[]
|
||||
icon: React.ReactNode
|
||||
}) {
|
||||
if (groups.length === 0) return null
|
||||
return (
|
||||
<div className="mt-2 first:mt-0">
|
||||
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-wide text-gray-400">{title}</div>
|
||||
{groups.map((g) => (
|
||||
<GroupRow key={g.key} group={g} icon={icon} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "Diese Antwort stützt sich auf" — describes the EVIDENCE (not the documents), objective counts
|
||||
* only (no fabricated trust score). When the Legal-KG ships `bindingness`, binding Rechtsgrundlagen
|
||||
* are split from Leitlinien (soft-law guidance); until then it shows a neutral evidence breakdown.
|
||||
*/
|
||||
export function EvidenceSummary({ response }: { response: AdvisorResponse }) {
|
||||
const m = summarizeEvidence(response.evidence)
|
||||
const figures = response.visual_evidence.length
|
||||
const notes = response.footnotes.length
|
||||
const cls = 'h-4 w-4'
|
||||
const smallIcon = 'h-3.5 w-3.5'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
|
||||
Diese Antwort stützt sich auf
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{m.hasBindingness && (
|
||||
<>
|
||||
<Count
|
||||
icon={<Scale className={cls} />}
|
||||
value={m.normProvisions}
|
||||
label={plural(m.normProvisions, 'Rechtsgrundlage', 'Rechtsgrundlagen')}
|
||||
/>
|
||||
<Count
|
||||
icon={<BookMarked className={cls} />}
|
||||
value={m.guidanceCount}
|
||||
label={plural(m.guidanceCount, 'Leitlinie', 'Leitlinien')}
|
||||
dim={m.guidanceCount === 0}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Count
|
||||
icon={<ImageIcon className={cls} />}
|
||||
value={figures}
|
||||
label={plural(figures, 'Abbildung', 'Abbildungen')}
|
||||
dim={figures === 0}
|
||||
/>
|
||||
<Count
|
||||
icon={<Hash className={cls} />}
|
||||
value={notes}
|
||||
label={plural(notes, 'Fußnote', 'Fußnoten')}
|
||||
dim={notes === 0}
|
||||
/>
|
||||
<Count icon={<FileText className={cls} />} value={m.unitCount} label="Evidence Units" />
|
||||
</div>
|
||||
|
||||
{m.groups.length > 0 && (
|
||||
<div className="mt-2.5 rounded-lg border border-gray-100 bg-gray-50/60 px-2.5 py-1.5">
|
||||
{m.hasBindingness ? (
|
||||
<>
|
||||
<Section title="Rechtsgrundlagen" groups={m.norms} icon={<Scale className={smallIcon} />} />
|
||||
<Section title="Leitlinien" groups={m.guidance} icon={<BookMarked className={smallIcon} />} />
|
||||
<Section title="Weitere" groups={m.other} icon={<FileText className={smallIcon} />} />
|
||||
</>
|
||||
) : (
|
||||
m.groups.map((g) => <GroupRow key={g.key} group={g} icon={<Library className={smallIcon} />} />)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user