feat(ai-sdk): authority-aware re-ranking for legal RAG retrieval (Phase 1)
CI / detect-changes (pull_request) Successful in 16s
CI / branch-name (pull_request) Successful in 2s
CI / guardrail-integrity (pull_request) Successful in 5s
CI / secret-scan (pull_request) Successful in 6s
CI / dep-audit (pull_request) Failing after 1m1s
CI / sbom-scan (pull_request) Failing after 1m4s
CI / build-sha-integrity (pull_request) Successful in 14s
CI / validate-canonical-controls (pull_request) Successful in 13s
CI / test-go (pull_request) Successful in 1m2s
CI / loc-budget (pull_request) Successful in 24s
CI / go-lint (pull_request) Failing after 20s
CI / python-lint (pull_request) Failing after 23s
CI / nodejs-lint (pull_request) Failing after 1m10s
CI / nodejs-build (pull_request) Successful in 3m26s
CI / iace-gt-coverage (pull_request) Successful in 16s
CI / test-python-backend (pull_request) Successful in 27s
CI / test-python-document-crawler (pull_request) Successful in 13s
CI / test-python-dsms-gateway (pull_request) Successful in 9s
CI / detect-changes (pull_request) Successful in 16s
CI / branch-name (pull_request) Successful in 2s
CI / guardrail-integrity (pull_request) Successful in 5s
CI / secret-scan (pull_request) Successful in 6s
CI / dep-audit (pull_request) Failing after 1m1s
CI / sbom-scan (pull_request) Failing after 1m4s
CI / build-sha-integrity (pull_request) Successful in 14s
CI / validate-canonical-controls (pull_request) Successful in 13s
CI / test-go (pull_request) Successful in 1m2s
CI / loc-budget (pull_request) Successful in 24s
CI / go-lint (pull_request) Failing after 20s
CI / python-lint (pull_request) Failing after 23s
CI / nodejs-lint (pull_request) Failing after 1m10s
CI / nodejs-build (pull_request) Successful in 3m26s
CI / iace-gt-coverage (pull_request) Successful in 16s
CI / test-python-backend (pull_request) Successful in 27s
CI / test-python-document-crawler (pull_request) Successful in 13s
CI / test-python-dsms-gateway (pull_request) Successful in 9s
Re-orders /sdk/v1/rag/search results so binding law from the matching jurisdiction and domain ranks above guidance, foreign and off-domain law — without dropping anything (guidance stays as interpretation context). Internal-only: response schema is unchanged (json:"-" fields), so every consumer benefits without a contract change. - authority.go: classifyAuthority / queryDomain / chunkDomain / scopeClass / topic ontology. Tagged payload (authority_weight/source_class/jurisdiction) wins; deterministic fallback via category + name markers for the untagged corpus. - authority_rerank.go: rerankByAuthority. final = semantic + authority + jurisdiction + domain + scope + topic; the authority score is written back to Score so the multi-collection advisor merge preserves the order. - legal_rag_client: stratified retrieval — the binding-law pool AUGMENTS the semantic pool (mergeDedupHits), then re-rank. - legal_rag_http: searchBinding (source_class filter) + shared doPointsSearch. - table-driven tests for authority/domain/scope/topic + rerank acceptance + a stratified-binding integration test. go test -race green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user