feat(ai-sdk): legal-corpus coverage + Phase-2 citation-graph assessment (#33)
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / build-sha-integrity (push) Successful in 6s
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 6s
CI / go-lint (push) Has been skipped
CI / loc-budget (push) Successful in 19s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / test-go (push) Successful in 59s
CI / iace-gt-coverage (push) Successful in 22s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

This commit was merged in pull request #33.
This commit is contained in:
2026-06-24 06:37:22 +00:00
parent b83c3e6e00
commit 230dc05287
14 changed files with 941 additions and 14 deletions
@@ -0,0 +1,134 @@
package ucca
import (
"fmt"
"strings"
)
const (
assessConnectedCap = 12 // cap connected norms surfaced in the assessment
assessCrossRegimeTopN = 5 // window over which "cross regime" is judged
assessReviewMargin = 0.05 // a tighter winner gap → recommend human review
)
// Assess builds the auditable explanation layer over a ranked result set:
// primary norm, the norms it connects to (citation graph), cross-regime, a
// human-review flag, the winner margin and a short reasoning string. Pure →
// unit-testable. It EXPLAINS the ranking, it does not change it. Returns nil for
// an empty result set.
func Assess(results []LegalSearchResult) *LegalAssessment {
if len(results) == 0 {
return nil
}
// Norm-level view: collapse multiple chunks of the same article/annex so the
// margin and cross-regime are judged between DISTINCT norms, not near-identical
// chunks of one norm (which would make every winner margin ~0).
norms := distinctNorms(results)
p := norms[0]
primary := primaryLabel(p)
connected := dedupStrings(p.ReferencesOut, p.ReferencesIn, p.CitationUnit)
if len(connected) > assessConnectedCap {
connected = connected[:assessConnectedCap]
}
window := norms
if len(window) > assessCrossRegimeTopN {
window = window[:assessCrossRegimeTopN]
}
regimes := make(map[string]bool)
for _, r := range window {
if r.RegulationShort != "" {
regimes[r.RegulationShort] = true
}
}
crossRegime := len(regimes) > 1
margin := 0.0
if len(norms) > 1 {
margin = norms[0].Score - norms[1].Score
}
primaryBinding := p.SourceClass == "binding_law"
humanReview := margin < assessReviewMargin || crossRegime || !primaryBinding
return &LegalAssessment{
PrimaryNorm: primary,
PrimaryRegulation: p.RegulationShort,
ConnectedNorms: connected,
CrossRegime: crossRegime,
HumanReviewFlag: humanReview,
WinnerMargin: margin,
ScoreReasoning: assessReasoning(p, margin, crossRegime, primaryBinding),
}
}
func primaryLabel(p LegalSearchResult) string {
if p.CitationUnit != "" {
return p.CitationUnit
}
if p.ArticleLabel != "" {
return p.ArticleLabel
}
return strings.TrimSpace(p.RegulationShort + " " + p.Article)
}
// assessReasoning renders a short, human-readable justification (German).
func assessReasoning(p LegalSearchResult, margin float64, crossRegime, primaryBinding bool) string {
label := primaryLabel(p)
parts := make([]string, 0, 4)
if primaryBinding {
parts = append(parts, fmt.Sprintf("Primärtreffer %s: bindendes Recht (Autorität %d).", label, p.AuthorityWeight))
} else {
parts = append(parts, fmt.Sprintf("Primärtreffer %s ist keine bindende Norm (Leitlinie/Standard) — Quelle prüfen.", label))
}
if margin > 0 {
parts = append(parts, fmt.Sprintf("Vorsprung %.2f vor #2.", margin))
}
if margin < assessReviewMargin {
parts = append(parts, "Knapper Vorsprung — Alternativtreffer prüfen.")
}
if crossRegime {
parts = append(parts, "Mehrere Regime betroffen — Querbezug prüfen.")
}
return strings.Join(parts, " ")
}
// distinctNorms collapses results that share a citation (multiple chunks of the
// same article/annex) to the first — i.e. highest-ranked — occurrence. Results
// without any citation identity are each kept, since they cannot be matched.
func distinctNorms(results []LegalSearchResult) []LegalSearchResult {
seen := make(map[string]bool, len(results))
out := make([]LegalSearchResult, 0, len(results))
for _, r := range results {
key := r.CitationUnit
if key == "" {
key = r.ArticleLabel
}
if key != "" {
if seen[key] {
continue
}
seen[key] = true
}
out = append(out, r)
}
return out
}
// dedupStrings concatenates out+in, drops empties and the excluded value, and
// returns a stable de-duplicated slice (insertion order preserved).
func dedupStrings(out, in []string, exclude string) []string {
seen := map[string]bool{exclude: true}
res := make([]string, 0, len(out)+len(in))
for _, list := range [][]string{out, in} {
for _, s := range list {
if s == "" || seen[s] {
continue
}
seen[s] = true
res = append(res, s)
}
}
return res
}