989d9f6f91
CI / detect-changes (pull_request) Successful in 14s
CI / branch-name (pull_request) Successful in 1s
CI / guardrail-integrity (pull_request) Successful in 16s
CI / secret-scan (pull_request) Successful in 18s
CI / dep-audit (pull_request) Failing after 1m2s
CI / sbom-scan (pull_request) Failing after 1m10s
CI / build-sha-integrity (pull_request) Successful in 13s
CI / validate-canonical-controls (pull_request) Successful in 14s
CI / loc-budget (pull_request) Successful in 23s
CI / go-lint (pull_request) Successful in 50s
CI / python-lint (pull_request) Failing after 18s
CI / nodejs-lint (pull_request) Failing after 1m8s
CI / nodejs-build (pull_request) Successful in 3m7s
CI / test-go (pull_request) Successful in 1m6s
CI / iace-gt-coverage (pull_request) Successful in 26s
CI / test-python-backend (pull_request) Successful in 33s
CI / test-python-document-crawler (pull_request) Successful in 21s
CI / test-python-dsms-gateway (pull_request) Successful in 21s
Add an `assessment` object to the legal RAG search response: primary norm, connected norms (from the citation graph references_out/in of the primary), cross_regime, human_review_flag, a norm-level winner_margin and a short reasoning string. The margin is computed over DISTINCT norms, so a long article split into several chunks no longer fabricates uncertainty. The per-result schema stays frozen — graph fields are internal (json:"-"). Also wire optional citation-graph expansion (RAG_GRAPH_EXPANSION=true, default off): top hits pull their referenced norms into the candidate pool via the precise edge (e.g. Art. 13 CRA -> Anhang I). Measured to add no rank gain over the existing binding-law augmentation, with +1 Qdrant call per search and reverse-edge fan-out risk, so it ships off-by-default as a recall safety net. The graph EXPLAINS retrieval (assessment), it does not expand it by default. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
package ucca
|
|
|
|
import "testing"
|
|
|
|
func ares(reg, cu, sc string, score float64, weight int, out, in []string) LegalSearchResult {
|
|
return LegalSearchResult{
|
|
RegulationShort: reg, CitationUnit: cu, SourceClass: sc, Score: score,
|
|
AuthorityWeight: weight, ReferencesOut: out, ReferencesIn: in,
|
|
}
|
|
}
|
|
|
|
func TestAssess_Empty(t *testing.T) {
|
|
if Assess(nil) != nil {
|
|
t.Error("empty results → nil assessment")
|
|
}
|
|
}
|
|
|
|
func TestAssess_BindingPrimary_NoReview(t *testing.T) {
|
|
results := []LegalSearchResult{
|
|
ares("CRA", "Art. 13 CRA", "binding_law", 1.05, 100,
|
|
[]string{"CRA Anhang I", "Art. 14 CRA"}, []string{"Art. 12 CRA"}),
|
|
ares("CRA", "Art. 14 CRA", "binding_law", 0.80, 100, nil, nil),
|
|
}
|
|
a := Assess(results)
|
|
if a == nil {
|
|
t.Fatal("nil assessment")
|
|
}
|
|
if a.PrimaryNorm != "Art. 13 CRA" || a.PrimaryRegulation != "CRA" {
|
|
t.Errorf("primary wrong: %+v", a)
|
|
}
|
|
if len(a.ConnectedNorms) != 3 { // out(2) + in(1), self excluded, deduped
|
|
t.Errorf("connected norms: %v", a.ConnectedNorms)
|
|
}
|
|
if a.CrossRegime {
|
|
t.Error("single regime must not be cross-regime")
|
|
}
|
|
if a.WinnerMargin < 0.24 || a.WinnerMargin > 0.26 {
|
|
t.Errorf("margin = %v, want ~0.25", a.WinnerMargin)
|
|
}
|
|
if a.HumanReviewFlag {
|
|
t.Error("clean binding + healthy margin + single regime → no review")
|
|
}
|
|
}
|
|
|
|
func TestAssess_CrossRegimeFlagsReview(t *testing.T) {
|
|
a := Assess([]LegalSearchResult{
|
|
ares("CRA", "Art. 13 CRA", "binding_law", 1.05, 100, nil, nil),
|
|
ares("DORA", "Art. 6 DORA", "binding_law", 0.70, 100, nil, nil),
|
|
})
|
|
if !a.CrossRegime || !a.HumanReviewFlag {
|
|
t.Errorf("cross-regime must flag review: %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestAssess_NonBindingFlagsReview(t *testing.T) {
|
|
a := Assess([]LegalSearchResult{
|
|
ares("ENISA", "ENISA SBOM", "supervisory_guidance", 0.90, 70, nil, nil),
|
|
ares("ENISA", "ENISA X", "supervisory_guidance", 0.40, 70, nil, nil),
|
|
})
|
|
if !a.HumanReviewFlag {
|
|
t.Error("non-binding primary → review")
|
|
}
|
|
}
|
|
|
|
func TestAssess_TightMarginFlagsReview(t *testing.T) {
|
|
a := Assess([]LegalSearchResult{
|
|
ares("CRA", "Art. 13 CRA", "binding_law", 1.00, 100, nil, nil),
|
|
ares("CRA", "Art. 14 CRA", "binding_law", 0.98, 100, nil, nil),
|
|
})
|
|
if a.WinnerMargin >= 0.05 || !a.HumanReviewFlag {
|
|
t.Errorf("tight margin → review: %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestAssess_MarginIsNormLevelNotChunkLevel(t *testing.T) {
|
|
// Two near-identical chunks of the SAME norm at the top, then a distinct norm.
|
|
results := []LegalSearchResult{
|
|
ares("CRA", "Art. 13 CRA", "binding_law", 1.050, 100, []string{"CRA Anhang I"}, nil),
|
|
ares("CRA", "Art. 13 CRA", "binding_law", 1.049, 100, nil, nil), // same norm
|
|
ares("CRA", "Art. 14 CRA", "binding_law", 0.800, 100, nil, nil),
|
|
}
|
|
a := Assess(results)
|
|
if a.WinnerMargin < 0.24 || a.WinnerMargin > 0.26 { // Art.13 vs Art.14, not chunk vs chunk
|
|
t.Errorf("margin must be norm-level (~0.25), got %v", a.WinnerMargin)
|
|
}
|
|
if a.HumanReviewFlag {
|
|
t.Error("healthy norm-level margin → no review")
|
|
}
|
|
}
|
|
|
|
func TestDistinctNorms(t *testing.T) {
|
|
got := distinctNorms([]LegalSearchResult{
|
|
{CitationUnit: "Art. 13 CRA"},
|
|
{CitationUnit: "Art. 13 CRA"}, // duplicate norm → collapsed
|
|
{CitationUnit: "Art. 14 CRA"},
|
|
{CitationUnit: ""}, // no identity → kept
|
|
{CitationUnit: ""}, // no identity → kept
|
|
})
|
|
if len(got) != 4 {
|
|
t.Errorf("want 4 (2 distinct + 2 unidentified), got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestDedupStrings(t *testing.T) {
|
|
got := dedupStrings([]string{"a", "b", "", "a"}, []string{"b", "c"}, "self")
|
|
if len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" {
|
|
t.Errorf("dedup: %v", got)
|
|
}
|
|
if len(dedupStrings([]string{"self"}, nil, "self")) != 0 {
|
|
t.Error("excluded value must be dropped")
|
|
}
|
|
}
|