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 }