Files
breakpilot-compliance/admin-compliance/lib/sdk/advisor/evidence-grouping.ts
T
Benjamin Admin a9b04e5286 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>
2026-07-01 15:17:21 +02:00

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,
}
}