From 623d80b6c8a02fb019fca4c50404c1d8e76cfa9e Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 26 Jun 2026 21:28:12 +0200 Subject: [PATCH 1/3] fix(ai-sdk): national-law subsidiarity in authority rerank (DSGVO > BDSG for general questions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authority reranker (wired in legal_rag_client.go:168) had no national-subsidiarity dimension, so a general BDSG paragraph could outrank the primary DSGVO article. Surfaced by the KB-2026.1 BDSG pilot (dp_05/08/11 + cr_07). - authorityScore: DE binding_law in an EU-primary domain WITHOUT a co-primary topic match -> soft demote (subsidiarityPen 0.18), not exclusion. National special rules stay co-primary via the topic ontology (DSB Art.37+§38, special categories Art.9+§22, ...). - queryDomain: fall back to a regulation-name mention (DSGVO/BDSG/CRA) so a question phrased around the act is domain-scoped even without a topical keyword (fixes cr_07: BDSG Teil-3 §64). - data_protection keyword stem 'auftragsverarbeit' (catches Auftragsverarbeitungsvertrag). Pure ranking logic, no data manipulation; soft demotes keep national rules visible. Build result (DSGVO+BDSG): degraded=0, must_not=0. go build/vet/test ./... green; 6 new table tests. Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/internal/ucca/authority.go | 22 +++++- .../internal/ucca/authority_rerank.go | 10 +++ .../internal/ucca/authority_rerank_test.go | 67 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/ai-compliance-sdk/internal/ucca/authority.go b/ai-compliance-sdk/internal/ucca/authority.go index 6627b57e..691fea4b 100644 --- a/ai-compliance-sdk/internal/ucca/authority.go +++ b/ai-compliance-sdk/internal/ucca/authority.go @@ -112,7 +112,7 @@ var domains = []domainDef{ {"data_protection", []string{"DSGVO", "GDPR", "BDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"}, []string{"personenbezogen", "betroffene", "datenschutz", "datenschutzbeauftrag", "dsb", - "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeiter"}}, + "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeit"}}, {"cyber", []string{"CRA", "NIS2", "NIS-2", "ENISA", "DORA", "EUCC"}, []string{"security update", "sicherheitsupdate", "sicherheitsaktualisierung", "schwachstelle", "sbom", @@ -126,6 +126,16 @@ var domains = []domainDef{ nil}, } +// euPrimaryDomains are domains whose PRIMARY binding act is an EU regulation/directive +// (DSGVO, CRA/NIS2, AI Act, MaschinenVO). In these domains a NATIONAL implementing law +// (e.g. BDSG) is subsidiary for general questions — see nationalSubsidiarityPenalty. +var euPrimaryDomains = map[string]bool{ + "data_protection": true, + "cyber": true, + "ai": true, + "product_safety": true, +} + func queryDomain(query string) string { ql := strings.ToLower(query) for _, d := range domains { @@ -135,6 +145,16 @@ func queryDomain(query string) string { } } } + // Fallback: an explicit regulation mention (e.g. "DSGVO", "BDSG", "CRA") also signals the + // domain — so a question phrased around the act ("... gilt die DSGVO ...") is scoped even + // without a topical keyword. Keyword match wins first (more specific). + for _, d := range domains { + for _, reg := range d.regs { + if strings.Contains(ql, strings.ToLower(reg)) { + return d.name + } + } + } return "" } diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank.go b/ai-compliance-sdk/internal/ucca/authority_rerank.go index 611b0111..79042937 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank.go @@ -14,6 +14,7 @@ const ( 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 + subsidiarityPen = 0.18 // national implementing law (BDSG) on a general EU-primary question: SOFT demote, not exclusion 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 @@ -102,6 +103,15 @@ func authorityScore(query string, r LegalSearchResult, qDomain string, qForeign if qDomain == "data_protection" && scopeClass(r) == "law_enforcement" { score -= scopePenalty } + // Subsidiarity: a national implementing law (DE binding, e.g. BDSG) is subsidiary to the + // primary EU act for GENERAL questions in an EU-primary domain — UNLESS the query hits a + // topic where the national norm is co-primary (DSB §38, special categories §22, ...). The + // topic boost below lifts those; here we only SOFT-demote the non-topic national norm, so + // it stays visible and can still win on a strongly matching topic. No hard exclusion. + if euPrimaryDomains[qDomain] && info.sourceClass == "binding_law" && + info.jurisdiction == "DE" && !resultMatchesTopic(query, r) { + score -= subsidiarityPen + } if resultMatchesTopic(query, r) { score += topicGain // Verstaerker, kein Override } diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go index 65e5f16c..3da6acf7 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go @@ -72,6 +72,73 @@ func TestRerankByAuthority_Acceptance(t *testing.T) { } }) + // Subsidiarity (KB-2026.1 BDSG-pilot regression): a national implementing § that is NOT a + // co-primary topic norm must not outrank the primary DSGVO article on a general question. + t.Run("subsidiarity dp_05: BDSG §23 below DSGVO Art.6 (Rechtsgrundlage)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 23 BDSG", "BDSG", "DE", 0.70), + bindingRes("Art. 6 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche Rechtsgrundlagen erlauben eine Verarbeitung personenbezogener Daten?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.6 must beat general BDSG §, got %q", out[0].ArticleLabel) + } + if len(out) != 2 { + t.Fatalf("BDSG must stay visible (soft demote), got len=%d", len(out)) + } + }) + + t.Run("subsidiarity dp_08: BDSG §70 below DSGVO Art.28 (Auftragsverarbeitung)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 70 BDSG", "BDSG", "DE", 0.70), // Teil 3 → scope + subsidiarity + bindingRes("Art. 28 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Was muss ein Auftragsverarbeitungsvertrag enthalten?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.28 must beat BDSG §70, got %q", out[0].ArticleLabel) + } + }) + + t.Run("subsidiarity dp_11: BDSG §22 below DSGVO Art.32 on a TOM question", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 22 BDSG", "BDSG", "DE", 0.70), + bindingRes("Art. 32 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche technischen und organisatorischen Massnahmen verlangt das Datenschutzrecht?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.32 must beat BDSG §22 on a non-topic TOM question, got %q", out[0].ArticleLabel) + } + }) + + t.Run("cr_07: a 'DSGVO' mention scopes the domain so BDSG Teil-3 §64 is demoted", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 64 BDSG", "BDSG", "DE", 0.70), // Teil 3 (law enforcement) + bindingRes("Art. 32 DSGVO", "DSGVO", "EU", 0.66), + } + // Query has no DP keyword but names the DSGVO → domain fallback scopes it data_protection, + // so scope+subsidiarity demote the law-enforcement § below the primary norm. + out := rerankByAuthority("Welche rechtliche Grundlage gilt fuer technische und organisatorische Massnahmen - DSGVO oder ein Standard?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO must win on a DSGVO-mention question, got %q", out[0].ArticleLabel) + } + }) + + t.Run("co-primary dp_01: BDSG §38 stays top on a DSB question (national special rule)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 38 BDSG", "BDSG", "DE", 0.66), + bindingRes("Art. 37 DSGVO", "DSGVO", "EU", 0.64), + } + out := rerankByAuthority("Ab wann muss ein Datenschutzbeauftragter benannt werden?", in) + // DSB topic → §38 is co-primary (topic-matched, NOT subsidiarity-demoted) and keeps its + // semantic lead; Art. 37 stays a close second. Both remain top-2. + if out[0].RegulationShort != "BDSG" { + t.Fatalf("BDSG §38 (DSB co-primary) must stay top, got %q", out[0].ArticleLabel) + } + if out[1].RegulationShort != "DSGVO" { + t.Fatalf("Art. 37 DSGVO must stay co-primary second, got %q", out[1].ArticleLabel) + } + }) + t.Run("nothing is dropped and topic amplifies", func(t *testing.T) { in := []LegalSearchResult{ guidanceRes("ENISA", "ENISA", 0.72), From 4ad681741dbd9a433a1631f8c3796be266dd91c0 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 27 Jun 2026 06:43:30 +0200 Subject: [PATCH 2/3] ci: re-trigger ai-sdk build (transient registry 502 + last-build tag-bug) Runde 1 build-ai-sdk failed on a transient registry.meghsakha.com 502 during the image push (image built fine, test-go green). mark-last-build then advanced last-build/main to the merge commit despite the failure, so the rerun (Runde 2) skipped build-ai-sdk (detect-changes saw no diff). This no-op Dockerfile comment forces detect-changes to rebuild + deploy the authority-rerank subsidiarity fix. Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ai-compliance-sdk/Dockerfile b/ai-compliance-sdk/Dockerfile index b87ea258..b1588140 100644 --- a/ai-compliance-sdk/Dockerfile +++ b/ai-compliance-sdk/Dockerfile @@ -1,4 +1,6 @@ # Build stage +# ci-retrigger 2026-06-27: transient registry.meghsakha.com 502 on push (Runde 1) + last-build +# tag-bug skipped the rerun (Runde 2). No logic change — forces detect-changes to rebuild ai-sdk. FROM golang:1.24-alpine AS builder WORKDIR /app From 07916df3307bcf1ef3bb9668127adf548585e8d7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 27 Jun 2026 07:19:00 +0200 Subject: [PATCH 3/3] =?UTF-8?q?feat(ai-sdk):=20ePrivacy/cookie=20topic=20?= =?UTF-8?q?=E2=80=94=20=C2=A725=20TDDDG=20co-primary=20for=20cookie=20ques?= =?UTF-8?q?tions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TDDDG (ex-TTDSG) pilot revealed §25 TDDDG (terminal-equipment / cookie consent) ranked #3 on a cookie query — the subsidiarity rule demoted it as DE law subsidiary to the DSGVO, but TDDDG is lex specialis (ePrivacy) for cookies. Topic-based fix (NOT blanket TDDDG > DSGVO): - cookie/ePrivacy topic (cookie/endeinrichtung/endgeraet/tracking -> §25 TDDDG) so it is co-primary (topic-matched -> topicGain, no subsidiarity demote). - TDDDG/TTDSG added to the data_protection domain (chunkDomain recognition). - cookie-specific keywords (NOT bare 'Einwilligung') so a general consent question still resolves to Art. 7 DSGVO. Acceptance on the DSGVO+BDSG+TDDDG build: cookie -> §25 TDDDG top-1; Rechtsgrundlage -> DSGVO; DSB -> Art.37+§38 BDSG (not TDDDG); degraded=0, must_not=0. go build/vet/test green; 2 new table tests. Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/internal/ucca/authority.go | 10 +++++++-- .../internal/ucca/authority_rerank_test.go | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/ai-compliance-sdk/internal/ucca/authority.go b/ai-compliance-sdk/internal/ucca/authority.go index 691fea4b..d80eb178 100644 --- a/ai-compliance-sdk/internal/ucca/authority.go +++ b/ai-compliance-sdk/internal/ucca/authority.go @@ -110,9 +110,10 @@ type domainDef struct { // Deterministic order (slice, not map) — important for stable classification + tests. var domains = []domainDef{ {"data_protection", - []string{"DSGVO", "GDPR", "BDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"}, + []string{"DSGVO", "GDPR", "BDSG", "TDDDG", "TTDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"}, []string{"personenbezogen", "betroffene", "datenschutz", "datenschutzbeauftrag", "dsb", - "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeit"}}, + "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeit", + "cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking"}}, {"cyber", []string{"CRA", "NIS2", "NIS-2", "ENISA", "DORA", "EUCC"}, []string{"security update", "sicherheitsupdate", "sicherheitsaktualisierung", "schwachstelle", "sbom", @@ -200,6 +201,11 @@ var topics = []topicDef{ {[]string{"bussgeld", "geldbusse"}, []string{"Art. 83"}}, {[]string{"security update", "sicherheitsupdate", "schwachstelle", "sbom", "cybersicherheitsanforderung"}, []string{"CRA Anhang I"}}, {[]string{"meldepflicht", "sicherheitsvorfall"}, []string{"Art. 14 CRA"}}, + // ePrivacy / cookies: § 25 TDDDG (ex-TTDSG) is lex specialis for terminal-equipment access / + // cookie consent. Co-primary on a cookie/tracking query, so the subsidiarity rule does NOT + // demote it like general-DP DE law subsidiary to the DSGVO. Keywords are cookie-specific + // (NOT bare "Einwilligung") so a general consent question still resolves to Art. 7 DSGVO. + {[]string{"cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking", "speicherung von informationen", "zugriff auf informationen"}, []string{"§ 25 TDDDG"}}, } // resultMatchesTopic reports whether the result is a preferred norm of a topic the query hits. diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go index 3da6acf7..857b1a01 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go @@ -123,6 +123,28 @@ func TestRerankByAuthority_Acceptance(t *testing.T) { } }) + t.Run("ePrivacy: a cookie query lifts §25 TDDDG above DSGVO consent (lex specialis topic)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("Art. 7 DSGVO", "DSGVO", "EU", 0.70), // higher semantic + bindingRes("§ 25 TDDDG", "TDDDG", "DE", 0.66), + } + out := rerankByAuthority("Wann ist eine Einwilligung fuer das Speichern von Cookies auf Endgeraeten erforderlich?", in) + if out[0].RegulationShort != "TDDDG" { + t.Fatalf("§25 TDDDG must win a cookie question (lex specialis topic), got %q", out[0].ArticleLabel) + } + }) + + t.Run("a general consent question still resolves to DSGVO, not §25 TDDDG", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 25 TDDDG", "TDDDG", "DE", 0.70), // higher semantic but no cookie topic + bindingRes("Art. 7 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche Anforderungen gelten an eine wirksame Einwilligung?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("a general consent question must resolve to DSGVO (TDDDG demoted), got %q", out[0].ArticleLabel) + } + }) + t.Run("co-primary dp_01: BDSG §38 stays top on a DSB question (national special rule)", func(t *testing.T) { in := []LegalSearchResult{ bindingRes("§ 38 BDSG", "BDSG", "DE", 0.66),