// 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() 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, } }