a9b04e5286
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>
81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
// Pure grouping/counting for the "Diese Antwort stützt sich auf" evidence header. No React, testable.
|
|
// Splits evidence into binding norms (Kernnormen) vs. soft-law guidance (Leitlinien) using the
|
|
// Legal-KG-owned `bindingness` fact (APEX rule) — the FE never derives bindingness itself. When the
|
|
// fact is absent it degrades to a neutral per-regulation breakdown (no norm/guidance labels, no
|
|
// fabricated legal classification).
|
|
|
|
import type { EvidenceUnit } from './contract'
|
|
import { resolveRegulation } from './regulation-display'
|
|
|
|
export type Bindingness = 'binding' | 'guidance' | 'unknown'
|
|
|
|
export interface FamilyGroup {
|
|
key: string // stable family key (grouping)
|
|
label: string // human-readable regulation name
|
|
sections: string[] // distinct provisions in first-seen order (e.g. "Art. 13", "§ 25")
|
|
units: number // raw evidence units in this family
|
|
bindingness: Bindingness
|
|
}
|
|
|
|
export interface EvidenceSummaryModel {
|
|
groups: FamilyGroup[]
|
|
norms: FamilyGroup[] // bindingness === 'binding'
|
|
guidance: FamilyGroup[] // bindingness === 'guidance'
|
|
other: FamilyGroup[] // bindingness unknown
|
|
hasBindingness: boolean // at least one unit carries the Legal-KG fact
|
|
normProvisions: number // distinct binding provisions (Kernnormen)
|
|
guidanceCount: number // distinct guidance documents (Leitlinien)
|
|
unitCount: number // total evidence units
|
|
}
|
|
|
|
export function groupByFamily(evidence: EvidenceUnit[]): FamilyGroup[] {
|
|
const byKey = new Map<string, FamilyGroup>()
|
|
for (const e of evidence) {
|
|
const { familyKey, familyLabel } = resolveRegulation({
|
|
code: e.regulation_code || e.document,
|
|
short: e.document,
|
|
})
|
|
let g = byKey.get(familyKey)
|
|
if (!g) {
|
|
g = { key: familyKey, label: familyLabel, sections: [], units: 0, bindingness: 'unknown' }
|
|
byKey.set(familyKey, g)
|
|
}
|
|
g.units += 1
|
|
if (e.section && !g.sections.includes(e.section)) g.sections.push(e.section)
|
|
if (e.bindingness && g.bindingness === 'unknown') g.bindingness = e.bindingness
|
|
}
|
|
return [...byKey.values()]
|
|
}
|
|
|
|
/** distinct provisions for a family; falls back to raw unit count when no section is known. */
|
|
export function provisionCount(g: FamilyGroup): number {
|
|
return g.sections.length || g.units
|
|
}
|
|
|
|
/** "5 Artikel" / "§ 25" / "3 Fundstellen" — the noun follows the family's own citation style. */
|
|
export function provisionSummary(g: FamilyGroup): string {
|
|
const n = g.sections.length
|
|
if (n === 0) return `${g.units} ${g.units === 1 ? 'Fundstelle' : 'Fundstellen'}`
|
|
if (n === 1) return g.sections[0]
|
|
if (g.sections.every((s) => /^\s*art/i.test(s))) return `${n} Artikel`
|
|
if (g.sections.every((s) => s.trim().startsWith('§'))) return `${n} §§`
|
|
return `${n} Fundstellen`
|
|
}
|
|
|
|
export function summarizeEvidence(evidence: EvidenceUnit[]): EvidenceSummaryModel {
|
|
const groups = groupByFamily(evidence)
|
|
const norms = groups.filter((g) => g.bindingness === 'binding')
|
|
const guidance = groups.filter((g) => g.bindingness === 'guidance')
|
|
const other = groups.filter((g) => g.bindingness === 'unknown')
|
|
return {
|
|
groups,
|
|
norms,
|
|
guidance,
|
|
other,
|
|
hasBindingness: norms.length > 0 || guidance.length > 0,
|
|
normProvisions: norms.reduce((n, g) => n + provisionCount(g), 0),
|
|
guidanceCount: guidance.length,
|
|
unitCount: evidence.length,
|
|
}
|
|
}
|