Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Admin a5b675d999 feat(ai-sdk): control-intent — technical_standard may win implementation questions
CI / detect-changes (pull_request) Successful in 6s
CI / branch-name (pull_request) Successful in 1s
CI / guardrail-integrity (pull_request) Successful in 6s
CI / secret-scan (pull_request) Successful in 7s
CI / dep-audit (pull_request) Failing after 56s
CI / sbom-scan (pull_request) Failing after 57s
CI / build-sha-integrity (pull_request) Successful in 10s
CI / validate-canonical-controls (pull_request) Successful in 7s
CI / loc-budget (pull_request) Successful in 20s
CI / go-lint (pull_request) Successful in 48s
CI / python-lint (pull_request) Failing after 15s
CI / nodejs-lint (pull_request) Failing after 1m12s
CI / nodejs-build (pull_request) Successful in 3m20s
CI / test-go (pull_request) Successful in 56s
CI / iace-gt-coverage (pull_request) Successful in 15s
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
Mirror the guidance interpretation-intent (PR #35) for control frameworks: when a
query explicitly asks HOW to implement / which controls or measures fit (control,
maßnahme, umsetzen, härten, nist, owasp, grundschutz, ...), a semantically
competitive technical_standard is lifted just above the best binding hit — so
"Welche Controls passen zu Security Updates?" can return NIST/OWASP Top-1, while
"Welche Anforderungen bestehen an Security Updates?" keeps CRA (binding) Top-1.

Generalize applyGuidanceIntent -> liftAboveBinding(sourceClass) and reuse it for
both supervisory_guidance (interpretation intent) and technical_standard
(implementation intent). Same semantic guard keeps off-topic sources demoted.
Rename the shared coefficients guidanceIntent* -> intentLift*.

Tested: queryWantsControls detection + 3 rerank cases (standard wins on control
question, binding stays on norm question, off-topic standard blocked by guard).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 11:49:49 +02:00
4 changed files with 6 additions and 36 deletions
+3 -13
View File
@@ -9,8 +9,8 @@ import (
// authorityInfo is the normative classification of a search result, used internally
// for re-ranking only (Phase 1 changes ordering, not the response contract).
type authorityInfo struct {
weight int // 100 binding, 80 technical_standard, 70 guidance, 0 foreign, 50 unknown
sourceClass string // binding_law | technical_standard | supervisory_guidance | foreign_law | unknown
weight int // 100 binding_law, 70 guidance, 0 foreign_law, 50 unknown
sourceClass string // binding_law | supervisory_guidance | foreign_law | unknown
jurisdiction string // DE | EU | CH
}
@@ -18,13 +18,7 @@ var (
guidanceMarkers = []string{
"DSK", "EDPB", "BfDI", "BFDI", "BayLfD", "Baylfb", "ENISA", "BSI", "EUCC",
"Standards Mapping", "Kpnr", "Orientierungshilfe", "Handreichung", "Beschluss",
"Leitlinie", "Guidance", "Empfehlung", "OECD", "CISA", "Blue Guide",
}
// Technical standards / control frameworks (best-practice controls). Checked BEFORE
// guidanceMarkers so a "BSI Grundschutz" chunk classifies as a standard, not BSI guidance.
standardMarkers = []string{
"NIST", "OWASP", "Grundschutz", "ISO 27001", "ISO/IEC 27001",
"CSA CCM", "Cloud Controls Matrix", "CIS Benchmark", "CIS Control",
"Leitlinie", "Guidance", "Empfehlung", "NIST", "OECD", "CISA", "Blue Guide",
}
foreignMarkers = []string{"RevDSG", "fedlex", "(CH)"}
deMarkers = []string{"BDSG", "DSK", "BfDI", "BFDI", "BayLfD", "Baylfb", "BSI"}
@@ -54,8 +48,6 @@ func classifyAuthority(r LegalSearchResult) authorityInfo {
switch {
case containsAny(hay, foreignMarkers):
return authorityInfo{weight: 0, sourceClass: "foreign_law", jurisdiction: "CH"}
case r.Category == "standard" || containsAny(hay, standardMarkers):
return authorityInfo{weight: 80, sourceClass: "technical_standard", jurisdiction: jur}
case r.Category == "guidance" || containsAny(hay, guidanceMarkers):
return authorityInfo{weight: 70, sourceClass: "supervisory_guidance", jurisdiction: jur}
case r.Category == "regulation" || r.Category == "eu_recht" || normPattern.MatchString(r.ArticleLabel):
@@ -69,8 +61,6 @@ func sourceClassFromWeight(w int) string {
switch {
case w >= 100:
return "binding_law"
case w >= 80:
return "technical_standard"
case w >= 70:
return "supervisory_guidance"
case w <= 0:
@@ -64,7 +64,7 @@ func bestBindingSemantic(results []LegalSearchResult, wantsIntent bool) float64
}
best := 0.0
for _, r := range results {
if classifyAuthority(r).sourceClass == "binding_law" && r.Score > best {
if r.SourceClass == "binding_law" && r.Score > best {
best = r.Score
}
}
@@ -152,14 +152,12 @@ func rerankByAuthority(query string, results []LegalSearchResult) []LegalSearchR
func liftAboveBinding(out, raw []LegalSearchResult, bestBindingSem float64, sourceClass string) {
bestBindingFinal := 0.0
for i := range out {
if classifyAuthority(out[i]).sourceClass == "binding_law" && out[i].Score > bestBindingFinal {
if out[i].SourceClass == "binding_law" && out[i].Score > bestBindingFinal {
bestBindingFinal = out[i].Score
}
}
for i := range out {
// Classify (not raw payload) so the untagged legacy corpus — e.g. NIST ingested
// before source_class tagging — is still recognized as its interpretative class.
if classifyAuthority(out[i]).sourceClass != sourceClass || raw[i].Score < bestBindingSem-intentLiftMargin {
if out[i].SourceClass != sourceClass || raw[i].Score < bestBindingSem-intentLiftMargin {
continue
}
lifted := bestBindingFinal + intentLiftGain + (raw[i].Score - bestBindingSem)
@@ -14,10 +14,6 @@ func TestClassifyAuthority(t *testing.T) {
{"tagged guidance DE", LegalSearchResult{AuthorityWeight: 70, SourceClass: "supervisory_guidance", Jurisdiction: "DE"}, 70, "supervisory_guidance", "DE"},
{"tagged foreign CH", LegalSearchResult{AuthorityWeight: 0, SourceClass: "foreign_law", Jurisdiction: "CH"}, 0, "foreign_law", "CH"},
{"untagged ENISA guidance", LegalSearchResult{RegulationShort: "ENISA", ArticleLabel: "ENISA CRA Standards Mapping"}, 70, "supervisory_guidance", "EU"},
{"untagged NIST standard", LegalSearchResult{RegulationShort: "NIST SP 800-82r3", ArticleLabel: "AU-8"}, 80, "technical_standard", "EU"},
{"BSI Grundschutz standard beats BSI guidance", LegalSearchResult{RegulationShort: "BSI Grundschutz", ArticleLabel: "BSI Grundschutz Baustein"}, 80, "technical_standard", "DE"},
{"weight-only 85 TRGS standard", LegalSearchResult{AuthorityWeight: 85, RegulationShort: "TRGS 529"}, 85, "technical_standard", "EU"},
{"tagged technical_standard", LegalSearchResult{AuthorityWeight: 80, SourceClass: "technical_standard", Jurisdiction: "EU"}, 80, "technical_standard", "EU"},
{"untagged CRA binding", LegalSearchResult{RegulationShort: "CRA", ArticleLabel: "Art. 13 CRA", Category: "regulation"}, 100, "binding_law", "EU"},
{"untagged BDSG binding DE", LegalSearchResult{RegulationShort: "BDSG", ArticleLabel: "§ 38 BDSG"}, 100, "binding_law", "DE"},
{"untagged RevDSG foreign", LegalSearchResult{RegulationShort: "RevDSG", ArticleLabel: "RevDSG (CH)"}, 0, "foreign_law", "CH"},
@@ -132,17 +132,3 @@ func TestRerank_OffTopicStandard_BlockedByGuard(t *testing.T) {
t.Errorf("off-topic standard must not win even with control intent, got %s", out[0].SourceClass)
}
}
func TestRerank_ControlQuestion_UntaggedNISTLifted(t *testing.T) {
// The existing NIST corpus is UNtagged (no source_class). It must still be classified
// technical_standard via markers and lifted on a control question — the whole reason
// the lift path classifies instead of trusting the raw payload field.
results := []LegalSearchResult{
{RegulationShort: "NIST SP 800-82r3", ArticleLabel: "AU-8", Score: 0.62},
{RegulationShort: "CRA", ArticleLabel: "Art. 13 CRA", Category: "regulation", Score: 0.58},
}
out := rerankByAuthority("Welche Controls passen zu Security Updates?", results)
if out[0].RegulationShort != "NIST SP 800-82r3" {
t.Errorf("untagged NIST should be lifted Top-1 on a control question, got %q", out[0].RegulationShort)
}
}