07916df330
CI / detect-changes (pull_request) Successful in 12s
CI / branch-name (pull_request) Successful in 2s
CI / guardrail-integrity (pull_request) Successful in 9s
CI / secret-scan (pull_request) Successful in 9s
CI / dep-audit (pull_request) Failing after 57s
CI / sbom-scan (pull_request) Failing after 58s
CI / build-sha-integrity (pull_request) Successful in 5s
CI / validate-canonical-controls (pull_request) Successful in 5s
CI / loc-budget (pull_request) Successful in 19s
CI / go-lint (pull_request) Successful in 40s
CI / python-lint (pull_request) Failing after 14s
CI / nodejs-lint (pull_request) Failing after 1m8s
CI / nodejs-build (pull_request) Successful in 3m1s
CI / test-go (pull_request) Successful in 1m0s
CI / iace-gt-coverage (pull_request) Successful in 17s
CI / test-python-backend (pull_request) Successful in 23s
CI / test-python-document-crawler (pull_request) Successful in 15s
CI / test-python-dsms-gateway (pull_request) Successful in 13s
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 <noreply@anthropic.com>
186 lines
8.0 KiB
Go
186 lines
8.0 KiB
Go
package ucca
|
|
|
|
import "testing"
|
|
|
|
func bindingRes(label, reg, jur string, score float64) LegalSearchResult {
|
|
return LegalSearchResult{ArticleLabel: label, RegulationShort: reg, SourceClass: "binding_law", AuthorityWeight: 100, Jurisdiction: jur, Score: score}
|
|
}
|
|
|
|
func guidanceRes(label, reg string, score float64) LegalSearchResult {
|
|
return LegalSearchResult{ArticleLabel: label, RegulationShort: reg, SourceClass: "supervisory_guidance", AuthorityWeight: 70, Jurisdiction: "EU", Score: score}
|
|
}
|
|
|
|
func foreignRes(label string, score float64) LegalSearchResult {
|
|
return LegalSearchResult{ArticleLabel: label, RegulationShort: "RevDSG", SourceClass: "foreign_law", AuthorityWeight: 0, Jurisdiction: "CH", Score: score}
|
|
}
|
|
|
|
// Acceptance criteria (Phase 1) expressed as ordering tests.
|
|
func TestRerankByAuthority_Acceptance(t *testing.T) {
|
|
t.Run("guidance does not overtake semantically competitive binding", func(t *testing.T) {
|
|
out := rerankByAuthority("Was gilt hier?", []LegalSearchResult{
|
|
guidanceRes("ENISA Mapping", "ENISA", 0.72),
|
|
bindingRes("CRA Anhang I", "CRA", "EU", 0.66),
|
|
})
|
|
if out[0].RegulationShort != "CRA" {
|
|
t.Fatalf("binding must rank first over competitive guidance, got %q", out[0].RegulationShort)
|
|
}
|
|
})
|
|
|
|
t.Run("foreign law demoted on DE/EU question but kept", func(t *testing.T) {
|
|
in := []LegalSearchResult{foreignRes("RevDSG Art 1", 0.85), bindingRes("Art. 9 DSGVO", "DSGVO", "EU", 0.62)}
|
|
out := rerankByAuthority("Welche Daten sind besonders geschuetzt?", in)
|
|
if out[0].RegulationShort != "DSGVO" {
|
|
t.Fatalf("binding EU must beat foreign on a DE/EU query, got %q", out[0].RegulationShort)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("foreign law must be kept, got len=%d", len(out))
|
|
}
|
|
})
|
|
|
|
t.Run("off-domain binding demoted but not removed", func(t *testing.T) {
|
|
in := []LegalSearchResult{
|
|
bindingRes("Art. 13 EU MDR", "MDR", "EU", 0.70),
|
|
bindingRes("Art. 13 CRA", "CRA", "EU", 0.60),
|
|
}
|
|
out := rerankByAuthority("Welche Pflichten hat der Hersteller von Produkten mit digitalen Elementen?", in)
|
|
if out[0].RegulationShort != "CRA" {
|
|
t.Fatalf("on-domain CRA must beat off-domain MDR, got %q", out[0].RegulationShort)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("off-domain MDR must be kept, got len=%d", len(out))
|
|
}
|
|
})
|
|
|
|
t.Run("same-regime binding wins over guidance", func(t *testing.T) {
|
|
out := rerankByAuthority("Was gilt hier?", []LegalSearchResult{
|
|
bindingRes("Art. 13 CRA", "CRA", "EU", 0.70),
|
|
guidanceRes("ENISA Mapping", "ENISA", 0.60),
|
|
})
|
|
if out[0].RegulationShort != "CRA" {
|
|
t.Fatalf("binding must win, got %q", out[0].RegulationShort)
|
|
}
|
|
})
|
|
|
|
t.Run("BDSG Teil 3 demoted below DSGVO on general DP question", func(t *testing.T) {
|
|
in := []LegalSearchResult{
|
|
bindingRes("§ 48 BDSG", "BDSG", "DE", 0.70), // Teil 3 (law enforcement)
|
|
bindingRes("Art. 9 DSGVO", "DSGVO", "EU", 0.62),
|
|
}
|
|
out := rerankByAuthority("Was sind besondere Kategorien personenbezogener Daten?", in)
|
|
if out[0].RegulationShort != "DSGVO" {
|
|
t.Fatalf("DSGVO must beat BDSG Teil 3 on a general DP question, got %q", out[0].RegulationShort)
|
|
}
|
|
})
|
|
|
|
// 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("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),
|
|
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),
|
|
bindingRes("CRA Anhang I", "CRA", "EU", 0.66),
|
|
foreignRes("RevDSG", 0.5),
|
|
}
|
|
out := rerankByAuthority("Anforderungen an Security Updates?", in)
|
|
if len(out) != len(in) {
|
|
t.Fatalf("rerank must preserve all results, got %d want %d", len(out), len(in))
|
|
}
|
|
if out[0].ArticleLabel != "CRA Anhang I" {
|
|
t.Fatalf("topic+authority must lift CRA Anhang I to top, got %q", out[0].ArticleLabel)
|
|
}
|
|
})
|
|
|
|
t.Run("single result returned unchanged", func(t *testing.T) {
|
|
in := []LegalSearchResult{bindingRes("Art. 1 CRA", "CRA", "EU", 0.5)}
|
|
if out := rerankByAuthority("x", in); len(out) != 1 {
|
|
t.Fatalf("len=%d", len(out))
|
|
}
|
|
})
|
|
}
|