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:
@@ -0,0 +1,80 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user