From 05d75e803956970b2aa3518cab0dd4bf5e3542b7 Mon Sep 17 00:00:00 2001 From: Benjamin_Boenisch Date: Wed, 24 Jun 2026 09:58:35 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-sdk):=20control-intent=20=E2=80=94=20te?= =?UTF-8?q?chnical=5Fstandard=20may=20win=20implementation=20questions=20(?= =?UTF-8?q?#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/ucca/authority_rerank.go | 81 ++++++++++++------- .../internal/ucca/legal_rag_intent_test.go | 62 ++++++++++++++ 2 files changed, 114 insertions(+), 29 deletions(-) diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank.go b/ai-compliance-sdk/internal/ucca/authority_rerank.go index 84932f5f..1360b1b5 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank.go @@ -7,17 +7,17 @@ import ( // Re-ranking coefficients (validated in the offline golden harness; Phase A — conservative). const ( - authorityCoef = 0.40 // * weight/100 - jurisdictionGain = 0.05 // binding/guidance from DE or EU - foreignPenalty = 0.60 // foreign law on a DE/EU question (demoted, not removed) - unknownPenalty = 0.08 - domainMatchGain = 0.15 - offDomainPenalty = 0.10 // off-domain binding (demoted, not removed) - scopePenalty = 0.25 // BDSG Teil 3 (law enforcement) on a general DP question - topicGain = 0.18 // amplifier only - supersededPenalty = 0.50 // superseded Alt-Quelle (pre-eu-v1): demoted, nicht versteckt - guidanceIntentGain = 0.10 // epsilon a qualifying guideline is lifted ABOVE the best binding hit - guidanceIntentMargin = 0.05 // ...only if the guideline is semantically competitive with binding + authorityCoef = 0.40 // * weight/100 + jurisdictionGain = 0.05 // binding/guidance from DE or EU + foreignPenalty = 0.60 // foreign law on a DE/EU question (demoted, not removed) + unknownPenalty = 0.08 + domainMatchGain = 0.15 + offDomainPenalty = 0.10 // off-domain binding (demoted, not removed) + scopePenalty = 0.25 // BDSG Teil 3 (law enforcement) on a general DP question + topicGain = 0.18 // amplifier only + supersededPenalty = 0.50 // superseded Alt-Quelle (pre-eu-v1): demoted, nicht versteckt + intentLiftGain = 0.10 // epsilon a qualifying interpretative source is lifted ABOVE the best binding + intentLiftMargin = 0.05 // ...only if that source is semantically competitive with binding ) // guidanceIntentSignals mark a query that EXPLICITLY asks for an interpretation / @@ -29,10 +29,19 @@ var guidanceIntentSignals = []string{ "auslegung", "empfiehlt", "empfehlung", "sagt", "laut", } -// queryWantsGuidance reports whether the query explicitly asks for guidance/interpretation. -func queryWantsGuidance(query string) bool { +// controlIntentSignals mark a query that asks HOW to implement / which controls or +// measures fit — rather than WHAT the binding obligation is. Only then may a +// (semantically competitive) technical_standard outrank the binding norm. +var controlIntentSignals = []string{ + "control", "controls", "maßnahme", "massnahme", "schutzmaßnahme", + "best practice", "best-practice", "umsetzen", "implementier", "absicher", + "härt", "haert", "hardening", "nist", "owasp", "grundschutz", + "ccm", "iso 27001", "isms", +} + +func queryMatchesAny(query string, signals []string) bool { q := strings.ToLower(query) - for _, sig := range guidanceIntentSignals { + for _, sig := range signals { if strings.Contains(q, sig) { return true } @@ -40,11 +49,17 @@ func queryWantsGuidance(query string) bool { return false } +// queryWantsGuidance reports whether the query explicitly asks for guidance/interpretation. +func queryWantsGuidance(query string) bool { return queryMatchesAny(query, guidanceIntentSignals) } + +// queryWantsControls reports whether the query asks for implementation controls/measures. +func queryWantsControls(query string) bool { return queryMatchesAny(query, controlIntentSignals) } + // bestBindingSemantic returns the highest RAW semantic score among binding-law -// results (0 if none / intent not requested). Used as the guard threshold so an -// off-topic guideline cannot ride the interpretation-intent boost. -func bestBindingSemantic(results []LegalSearchResult, wantsGuidance bool) float64 { - if !wantsGuidance { +// results (0 if none / no intent). Used as the guard threshold so an off-topic +// interpretative source cannot ride the intent boost. +func bestBindingSemantic(results []LegalSearchResult, wantsIntent bool) float64 { + if !wantsIntent { return 0 } best := 0.0 @@ -104,15 +119,22 @@ func rerankByAuthority(query string, results []LegalSearchResult) []LegalSearchR qDomain := queryDomain(query) qForeign := queryIsForeign(query) wantsGuidance := queryWantsGuidance(query) - bestBindingSem := bestBindingSemantic(results, wantsGuidance) + wantsControls := queryWantsControls(query) + bestBindingSem := bestBindingSemantic(results, wantsGuidance || wantsControls) out := make([]LegalSearchResult, len(results)) copy(out, results) for i := range out { out[i].Score = authorityScore(query, out[i], qDomain, qForeign) } + // Explicit interpretation intent → a competitive guideline may outrank binding; + // explicit implementation intent → a competitive technical_standard may. Both lift + // ABOVE the best binding FINAL, so a pure norm question (neither intent) is untouched. if wantsGuidance { - applyGuidanceIntent(out, results, bestBindingSem) + liftAboveBinding(out, results, bestBindingSem, "supervisory_guidance") + } + if wantsControls { + liftAboveBinding(out, results, bestBindingSem, "technical_standard") } sort.SliceStable(out, func(a, b int) bool { return out[a].Score > out[b].Score @@ -120,13 +142,14 @@ func rerankByAuthority(query string, results []LegalSearchResult) []LegalSearchR return out } -// applyGuidanceIntent lifts semantically-competitive guidance just ABOVE the best -// binding hit (ordered by semantic), so an EXPLICIT interpretation question can -// return guidance Top-1. Obligation questions (no intent → not called) keep -// binding on top. Guidance below the semantic margin is left untouched, so an -// off-topic guideline can never ride the override — and the lift is computed from -// the binding FINAL score, so authority/topic/domain bonuses cannot edge it out. -func applyGuidanceIntent(out, raw []LegalSearchResult, bestBindingSem float64) { +// liftAboveBinding lifts a semantically-competitive interpretative source (the given +// sourceClass — supervisory_guidance or technical_standard) just ABOVE the best binding +// hit, ordered by semantic, so an EXPLICIT guidance/implementation question can return +// that source Top-1. A pure norm question (no intent → not called) keeps binding on top. +// Sources below the semantic margin are left untouched, so an off-topic source can never +// ride the override — and the lift is from the binding FINAL score, so authority/topic/ +// domain bonuses cannot edge it out. +func liftAboveBinding(out, raw []LegalSearchResult, bestBindingSem float64, sourceClass string) { bestBindingFinal := 0.0 for i := range out { if out[i].SourceClass == "binding_law" && out[i].Score > bestBindingFinal { @@ -134,10 +157,10 @@ func applyGuidanceIntent(out, raw []LegalSearchResult, bestBindingSem float64) { } } for i := range out { - if out[i].SourceClass != "supervisory_guidance" || raw[i].Score < bestBindingSem-guidanceIntentMargin { + if out[i].SourceClass != sourceClass || raw[i].Score < bestBindingSem-intentLiftMargin { continue } - lifted := bestBindingFinal + guidanceIntentGain + (raw[i].Score - bestBindingSem) + lifted := bestBindingFinal + intentLiftGain + (raw[i].Score - bestBindingSem) if lifted > out[i].Score { out[i].Score = lifted } diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_intent_test.go b/ai-compliance-sdk/internal/ucca/legal_rag_intent_test.go index a24ec59c..25050b47 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_intent_test.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_intent_test.go @@ -70,3 +70,65 @@ func TestRerank_OffTopicGuidance_BlockedByGuard(t *testing.T) { t.Errorf("off-topic guidance must not win even with intent, got %s", out[0].SourceClass) } } + +func TestQueryWantsControls(t *testing.T) { + wants := []string{ + "Welche Controls passen zu Security Updates?", + "Welche Maßnahmen sollten wir umsetzen?", + "Wie härten wir den Server ab?", + "Gibt es NIST-Controls dafür?", + "OWASP Best Practice für Logging?", + "BSI Grundschutz Bausteine", + } + plain := []string{ + "Welche Anforderungen bestehen an Security Updates?", + "Ab wann braucht man einen Datenschutzbeauftragten?", + } + for _, q := range wants { + if !queryWantsControls(q) { + t.Errorf("should detect control/implementation intent: %q", q) + } + } + for _, q := range plain { + if queryWantsControls(q) { + t.Errorf("should NOT detect control intent (norm question): %q", q) + } + } +} + +func TestRerank_ControlQuestion_StandardMayWin(t *testing.T) { + // Explicit implementation intent + standard semantically competitive → standard wins. + results := []LegalSearchResult{ + intentRes("NIST SP 800-82", "technical_standard", 0.62, 80), + intentRes("CRA", "binding_law", 0.58, 100), + } + out := rerankByAuthority("Welche Controls passen zu Security Updates?", results) + if out[0].SourceClass != "technical_standard" { + t.Errorf("control question: technical_standard should win Top-1, got %s", out[0].SourceClass) + } +} + +func TestRerank_NormQuestion_BindingOverStandard(t *testing.T) { + // "Anforderungen" → no control intent → binding stays Top-1 over the standard. + results := []LegalSearchResult{ + intentRes("NIST SP 800-82", "technical_standard", 0.62, 80), + intentRes("CRA", "binding_law", 0.58, 100), + } + out := rerankByAuthority("Welche Anforderungen bestehen an Security Updates?", results) + if out[0].SourceClass != "binding_law" { + t.Errorf("norm question: binding must stay Top-1 over standard, got %s", out[0].SourceClass) + } +} + +func TestRerank_OffTopicStandard_BlockedByGuard(t *testing.T) { + // Control intent present, but the standard is semantically far below binding → + // the margin guard keeps binding Top-1 (no off-topic standard override). + results := []LegalSearchResult{ + intentRes("NIST SP 800-82", "technical_standard", 0.40, 80), + intentRes("CRA", "binding_law", 0.58, 100), + } + out := rerankByAuthority("Welche Controls passen zu Security Updates?", results) + if out[0].SourceClass != "binding_law" { + t.Errorf("off-topic standard must not win even with control intent, got %s", out[0].SourceClass) + } +}