Merge remote-tracking branch 'origin/main' into feat/obligation-aggregation
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 5s
CI / validate-canonical-controls (push) Successful in 4s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 5s
CI / validate-canonical-controls (push) Successful in 4s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// Control-Mapping: CRA Annex I -> OWASP ASVS 5.0. Eine Zeile = ein Mapping (Schema: ControlMapping).
|
||||
// Reviewt 2026-06-25 (benjamin): 7 accepted, 13 rejected. accepted = Audit-Wahrheit (Advisor nutzt acceptedOnly).
|
||||
// rejected bleiben als Audit-Spur ("warum verworfen"). KEIN confidence — kuratiert = fachliche Feststellung.
|
||||
// Architekturbeweis: CRA -> OWASP fuer AppSec/Auth/Crypto/Logging; Ops/Update/Attack-Surface/Integritaet -> NIST/BSI.
|
||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.3.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.2.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11 = Cryptography.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11 = Cryptography, richtiger Bereich fuer Verschluesselung.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.7.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11.7 = Key Management.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11.7 = Key Management fuer Verschluesselung/Schluesselverwaltung.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.3", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.4", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, kein Auth — verworfen.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.3.2", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.3", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V1.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V2.4.1", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V15.3.3", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V8.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Evidence-Requirements je OWASP-ASVS-Control (Schema: EvidenceRequirement). Eine Zeile = eine geforderte Evidenz.
|
||||
// Autoriert/kuratiert (nicht Retriever). Der Advisor kann eine CRA-Anforderung erst dann als erfuellt melden,
|
||||
// wenn die required Evidenzen der gemappten, accepted Controls vorliegen + frisch genug sind.
|
||||
// Stand 2026-06-25, Basis: die 7 accepted CRA->OWASP-Mappings (Auth V6, Crypto V11, Logging V16).
|
||||
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "IAM-/Zugriffskonfiguration als Nachweis der Authentisierungs-Anforderung.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "test_report", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "Automatisierter Zugriffstest (CI) belegt funktionierende Zugriffskontrolle.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "pentest", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": false, "rationale": "Jaehrlicher PenTest der Authentisierung — vertieft, aber nicht Pflicht je Release.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V6.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Rollenmodell/Auth-Architektur als Nachweis.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Krypto-Konfiguration (zugelassene Algorithmen) als Nachweis der Verschluesselung.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "sbom", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "SBOM weist die eingesetzten Krypto-Bibliotheken/-Versionen nach.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "policy", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": true, "rationale": "Key-Management-Policy (Rotation, Aufbewahrung) als organisatorischer Nachweis.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration der Schluesselverwaltung als technischer Nachweis.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs belegen, dass sicherheitsrelevante Ereignisse protokolliert werden.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Konfiguration als Nachweis der erfassten Ereignisarten.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V16.3.4", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs.", "version": "2026-06-25"}
|
||||
{"framework": "OWASP ASVS", "control": "V16.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Architektur-Konfiguration als Nachweis.", "version": "2026-06-25"}
|
||||
@@ -0,0 +1,151 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ControlMapping is one persisted, versioned, REVIEWABLE link from a legal
|
||||
// obligation/requirement to a concrete framework control — a node in the curated
|
||||
// compliance graph (Regulation -> Obligation -> Control -> Evidence). The retriever only
|
||||
// PROPOSES candidates (mapping_status=candidate); a human/rule decision turns the good ones
|
||||
// into mapping_status=accepted, which is the audited truth the Advisor uses at runtime.
|
||||
//
|
||||
// There is intentionally NO probabilistic "confidence" field: once curated, a mapping is a
|
||||
// professional statement, not an AI guess. The retriever's score lives only in the rationale
|
||||
// of a candidate, never as structured truth.
|
||||
type ControlMapping struct {
|
||||
SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part I (2)(c)"
|
||||
SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...)
|
||||
TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS"
|
||||
TargetControl string `json:"target_control"` // e.g. "V6.3.1"
|
||||
MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts
|
||||
MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded
|
||||
Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based
|
||||
Rationale string `json:"rationale"`
|
||||
ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id)
|
||||
ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD
|
||||
ReviewReason string `json:"review_reason,omitempty"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Allowed enum values — the deterministic "rule" layer that keeps the curated store clean.
|
||||
var (
|
||||
mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "implements": true, "related": true, "contradicts": true}
|
||||
mappingStatusValues = map[string]bool{"candidate": true, "accepted": true, "rejected": true, "superseded": true}
|
||||
provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true}
|
||||
)
|
||||
|
||||
// Validate checks required fields + enum membership, and enforces the audit trail: any
|
||||
// human/rule DECISION (accepted/rejected) must carry who/when/why. Fail-closed at load.
|
||||
func (m ControlMapping) Validate() error {
|
||||
switch {
|
||||
case m.SourceNorm == "":
|
||||
return fmt.Errorf("control mapping: source_norm required")
|
||||
case m.TargetFramework == "":
|
||||
return fmt.Errorf("control mapping: target_framework required")
|
||||
case m.TargetControl == "":
|
||||
return fmt.Errorf("control mapping: target_control required")
|
||||
case !mappingTypeValues[m.MappingType]:
|
||||
return fmt.Errorf("control mapping: invalid mapping_type %q", m.MappingType)
|
||||
case !mappingStatusValues[m.MappingStatus]:
|
||||
return fmt.Errorf("control mapping: invalid mapping_status %q", m.MappingStatus)
|
||||
case !provenanceValues[m.Provenance]:
|
||||
return fmt.Errorf("control mapping: invalid provenance %q", m.Provenance)
|
||||
}
|
||||
if m.MappingStatus == "accepted" || m.MappingStatus == "rejected" {
|
||||
if m.ReviewedBy == "" || m.ReviewDate == "" || m.ReviewReason == "" {
|
||||
return fmt.Errorf("control mapping %s->%s: status %q requires reviewed_by + review_date + review_reason (audit trail)",
|
||||
m.SourceNorm, m.TargetControl, m.MappingStatus)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAccepted reports whether this mapping is the active audited truth.
|
||||
func (m ControlMapping) IsAccepted() bool { return m.MappingStatus == "accepted" }
|
||||
|
||||
// ControlMappingSet is the loaded, indexed mapping store (forward + reverse lookup).
|
||||
type ControlMappingSet struct {
|
||||
All []ControlMapping
|
||||
bySourceNorm map[string][]ControlMapping
|
||||
byControl map[string][]ControlMapping
|
||||
}
|
||||
|
||||
func controlKey(framework, control string) string { return framework + ":" + control }
|
||||
|
||||
// ControlsFor returns the controls mapped to a source norm. acceptedOnly restricts to the
|
||||
// audited truth (what the Advisor may treat as fact).
|
||||
func (s *ControlMappingSet) ControlsFor(sourceNorm string, acceptedOnly bool) []ControlMapping {
|
||||
return filterAccepted(s.bySourceNorm[sourceNorm], acceptedOnly)
|
||||
}
|
||||
|
||||
// ObligationsFor returns the norms mapped to a framework control (reverse lookup).
|
||||
func (s *ControlMappingSet) ObligationsFor(framework, control string, acceptedOnly bool) []ControlMapping {
|
||||
return filterAccepted(s.byControl[controlKey(framework, control)], acceptedOnly)
|
||||
}
|
||||
|
||||
func filterAccepted(in []ControlMapping, acceptedOnly bool) []ControlMapping {
|
||||
if !acceptedOnly {
|
||||
return in
|
||||
}
|
||||
out := make([]ControlMapping, 0, len(in))
|
||||
for _, m := range in {
|
||||
if m.IsAccepted() {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LoadControlMappings reads every *.jsonl file under dir (one mapping per line; blank and
|
||||
// //-prefixed lines ignored), validates each row, and builds the index. An invalid row
|
||||
// aborts the whole load — fail-closed, because this is the audit truth, not best-effort.
|
||||
func LoadControlMappings(dir string) (*ControlMappingSet, error) {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set := &ControlMappingSet{
|
||||
bySourceNorm: map[string][]ControlMapping{},
|
||||
byControl: map[string][]ControlMapping{},
|
||||
}
|
||||
for _, f := range files {
|
||||
fh, err := os.Open(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc := bufio.NewScanner(fh)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
line := 0
|
||||
for sc.Scan() {
|
||||
line++
|
||||
raw := strings.TrimSpace(sc.Text())
|
||||
if raw == "" || strings.HasPrefix(raw, "//") {
|
||||
continue
|
||||
}
|
||||
var m ControlMapping
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
fh.Close()
|
||||
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
|
||||
}
|
||||
if err := m.Validate(); err != nil {
|
||||
fh.Close()
|
||||
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
|
||||
}
|
||||
set.All = append(set.All, m)
|
||||
set.bySourceNorm[m.SourceNorm] = append(set.bySourceNorm[m.SourceNorm], m)
|
||||
k := controlKey(m.TargetFramework, m.TargetControl)
|
||||
set.byControl[k] = append(set.byControl[k], m)
|
||||
}
|
||||
fh.Close()
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestControlMapping_Validate(t *testing.T) {
|
||||
candidate := ControlMapping{SourceNorm: "CRA Annex I", TargetFramework: "OWASP ASVS", TargetControl: "V6.3.1", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"}
|
||||
if err := candidate.Validate(); err != nil {
|
||||
t.Fatalf("valid candidate rejected: %v", err)
|
||||
}
|
||||
accepted := ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "implements", MappingStatus: "accepted", Provenance: "human_curated", ReviewedBy: "benjamin", ReviewDate: "2026-06-25", ReviewReason: "passt"}
|
||||
if err := accepted.Validate(); err != nil {
|
||||
t.Fatalf("valid accepted rejected: %v", err)
|
||||
}
|
||||
|
||||
bad := []struct {
|
||||
name string
|
||||
m ControlMapping
|
||||
}{
|
||||
{"no source_norm", ControlMapping{TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"}},
|
||||
{"bad mapping_type", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "nope", MappingStatus: "candidate", Provenance: "retriever_candidate"}},
|
||||
{"bad mapping_status", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "maybe", Provenance: "retriever_candidate"}},
|
||||
{"bad provenance", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "guessed"}},
|
||||
{"accepted without audit trail", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "accepted", Provenance: "human_curated"}},
|
||||
{"rejected without reason", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "rejected", Provenance: "human_curated", ReviewedBy: "b", ReviewDate: "2026-06-25"}},
|
||||
}
|
||||
for _, tt := range bad {
|
||||
if err := tt.m.Validate(); err == nil {
|
||||
t.Errorf("%s: expected rejection", tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadControlMappings(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `// header comment, ignored
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.3.1","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","reviewed_by":"benjamin","review_date":"2026-06-25","review_reason":"V6=Auth passt","rationale":"r","version":"2026-06-25"}
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V14.2.4","mapping_type":"related","mapping_status":"candidate","provenance":"retriever_candidate","rationale":"r","version":"2026-06-25"}
|
||||
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "m.jsonl"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set, err := LoadControlMappings(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if len(set.All) != 2 {
|
||||
t.Fatalf("want 2 mappings, got %d", len(set.All))
|
||||
}
|
||||
if got := set.ControlsFor("CRA Annex I", false); len(got) != 2 {
|
||||
t.Errorf("ControlsFor(all): want 2, got %d", len(got))
|
||||
}
|
||||
if got := set.ControlsFor("CRA Annex I", true); len(got) != 1 {
|
||||
t.Errorf("ControlsFor(acceptedOnly): want 1 (only accepted), got %d", len(got))
|
||||
}
|
||||
if got := set.ObligationsFor("OWASP ASVS", "V6.3.1", true); len(got) != 1 {
|
||||
t.Errorf("ObligationsFor accepted reverse lookup: want 1, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadControlMappings_RejectsInvalid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// accepted without the who/when/why audit trail must fail-closed.
|
||||
if err := os.WriteFile(filepath.Join(dir, "bad.jsonl"), []byte(`{"source_norm":"A","target_framework":"X","target_control":"Y","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","rationale":"r","version":"v"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadControlMappings(dir); err == nil {
|
||||
t.Error("accepted mapping without audit trail must fail the load (fail-closed)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlMappings_SeedFileValid(t *testing.T) {
|
||||
// The committed seed store must always load + validate.
|
||||
set, err := LoadControlMappings("../../data/control_mappings")
|
||||
if err != nil {
|
||||
t.Fatalf("seed control_mappings failed to load: %v", err)
|
||||
}
|
||||
if len(set.All) == 0 {
|
||||
t.Fatal("seed control_mappings is empty")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EvidenceRequirement is the last edge of the compliance graph: it says WHAT concrete
|
||||
// evidence proves a framework control is met, and how fresh that evidence must be. This is
|
||||
// what lets the Advisor eventually state "the CRA requirement is fulfilled" — not because a
|
||||
// document exists, but because the required, current evidence is present. Authored/curated,
|
||||
// not retriever-generated.
|
||||
type EvidenceRequirement struct {
|
||||
Framework string `json:"framework"` // e.g. "OWASP ASVS"
|
||||
Control string `json:"control"` // e.g. "V6.3.1"
|
||||
EvidenceType string `json:"evidence_type"` // sbom|test_report|config_export|repo_scan|policy|ticket|audit_log|pentest
|
||||
EvidenceSource string `json:"evidence_source"` // github|ci|scanner|manual_upload
|
||||
FreshnessRequirement string `json:"freshness_requirement"` // per_release|quarterly|annually|continuous
|
||||
Required bool `json:"required"`
|
||||
Rationale string `json:"rationale"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Allowed enum values — the rule layer that keeps the evidence catalog clean.
|
||||
var (
|
||||
evidenceTypeValues = map[string]bool{"sbom": true, "test_report": true, "config_export": true, "repo_scan": true, "policy": true, "ticket": true, "audit_log": true, "pentest": true}
|
||||
evidenceSourceValues = map[string]bool{"github": true, "ci": true, "scanner": true, "manual_upload": true}
|
||||
freshnessValues = map[string]bool{"per_release": true, "quarterly": true, "annually": true, "continuous": true}
|
||||
)
|
||||
|
||||
// Validate checks required fields + enum membership. Fail-closed at load.
|
||||
func (e EvidenceRequirement) Validate() error {
|
||||
switch {
|
||||
case e.Framework == "":
|
||||
return fmt.Errorf("evidence requirement: framework required")
|
||||
case e.Control == "":
|
||||
return fmt.Errorf("evidence requirement: control required")
|
||||
case !evidenceTypeValues[e.EvidenceType]:
|
||||
return fmt.Errorf("evidence requirement: invalid evidence_type %q", e.EvidenceType)
|
||||
case !evidenceSourceValues[e.EvidenceSource]:
|
||||
return fmt.Errorf("evidence requirement: invalid evidence_source %q", e.EvidenceSource)
|
||||
case !freshnessValues[e.FreshnessRequirement]:
|
||||
return fmt.Errorf("evidence requirement: invalid freshness_requirement %q", e.FreshnessRequirement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EvidenceRequirementSet is the loaded, indexed evidence catalog.
|
||||
type EvidenceRequirementSet struct {
|
||||
All []EvidenceRequirement
|
||||
byControl map[string][]EvidenceRequirement
|
||||
}
|
||||
|
||||
// For returns all evidence requirements declared for a framework control.
|
||||
func (s *EvidenceRequirementSet) For(framework, control string) []EvidenceRequirement {
|
||||
return s.byControl[controlKey(framework, control)]
|
||||
}
|
||||
|
||||
// RequiredFor returns only the required evidence for a control — the minimum that must be
|
||||
// present before the control may be treated as met.
|
||||
func (s *EvidenceRequirementSet) RequiredFor(framework, control string) []EvidenceRequirement {
|
||||
out := make([]EvidenceRequirement, 0)
|
||||
for _, e := range s.byControl[controlKey(framework, control)] {
|
||||
if e.Required {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LoadEvidenceRequirements reads every *.jsonl file under dir (one requirement per line;
|
||||
// blank and //-prefixed lines ignored), validates each, and builds the per-control index.
|
||||
// An invalid row aborts the load — fail-closed.
|
||||
func LoadEvidenceRequirements(dir string) (*EvidenceRequirementSet, error) {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set := &EvidenceRequirementSet{byControl: map[string][]EvidenceRequirement{}}
|
||||
for _, f := range files {
|
||||
fh, err := os.Open(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc := bufio.NewScanner(fh)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
line := 0
|
||||
for sc.Scan() {
|
||||
line++
|
||||
raw := strings.TrimSpace(sc.Text())
|
||||
if raw == "" || strings.HasPrefix(raw, "//") {
|
||||
continue
|
||||
}
|
||||
var e EvidenceRequirement
|
||||
if err := json.Unmarshal([]byte(raw), &e); err != nil {
|
||||
fh.Close()
|
||||
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
|
||||
}
|
||||
if err := e.Validate(); err != nil {
|
||||
fh.Close()
|
||||
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
|
||||
}
|
||||
set.All = append(set.All, e)
|
||||
k := controlKey(e.Framework, e.Control)
|
||||
set.byControl[k] = append(set.byControl[k], e)
|
||||
}
|
||||
fh.Close()
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvidenceRequirement_Validate(t *testing.T) {
|
||||
valid := EvidenceRequirement{Framework: "OWASP ASVS", Control: "V6.3.1", EvidenceType: "config_export", EvidenceSource: "github", FreshnessRequirement: "per_release", Required: true}
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid rejected: %v", err)
|
||||
}
|
||||
bad := []struct {
|
||||
name string
|
||||
e EvidenceRequirement
|
||||
}{
|
||||
{"no control", EvidenceRequirement{Framework: "X", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "per_release"}},
|
||||
{"bad evidence_type", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "screenshot", EvidenceSource: "ci", FreshnessRequirement: "per_release"}},
|
||||
{"bad evidence_source", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "email", FreshnessRequirement: "per_release"}},
|
||||
{"bad freshness", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "weekly"}},
|
||||
}
|
||||
for _, tt := range bad {
|
||||
if err := tt.e.Validate(); err == nil {
|
||||
t.Errorf("%s: expected rejection", tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEvidenceRequirements(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `// header
|
||||
{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"config_export","evidence_source":"github","freshness_requirement":"per_release","required":true,"version":"2026-06-25"}
|
||||
{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"pentest","evidence_source":"manual_upload","freshness_requirement":"annually","required":false,"version":"2026-06-25"}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "e.jsonl"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set, err := LoadEvidenceRequirements(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if len(set.All) != 2 {
|
||||
t.Fatalf("want 2, got %d", len(set.All))
|
||||
}
|
||||
if got := set.For("OWASP ASVS", "V6.3.1"); len(got) != 2 {
|
||||
t.Errorf("For: want 2, got %d", len(got))
|
||||
}
|
||||
if got := set.RequiredFor("OWASP ASVS", "V6.3.1"); len(got) != 1 {
|
||||
t.Errorf("RequiredFor: want 1 (pentest is optional), got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvidenceRequirements_SeedFileValid(t *testing.T) {
|
||||
set, err := LoadEvidenceRequirements("../../data/evidence_requirements")
|
||||
if err != nil {
|
||||
t.Fatalf("seed evidence_requirements failed to load: %v", err)
|
||||
}
|
||||
if len(set.All) == 0 {
|
||||
t.Fatal("seed evidence_requirements is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGraph_AcceptedControlsHaveEvidence wires the two layers: every control an accepted
|
||||
// CRA->OWASP mapping points to must have >=1 required evidence — the Obligation -> Control ->
|
||||
// Evidence chain must be connected, no dangling control nodes.
|
||||
func TestGraph_AcceptedControlsHaveEvidence(t *testing.T) {
|
||||
maps, err := LoadControlMappings("../../data/control_mappings")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev, err := LoadEvidenceRequirements("../../data/evidence_requirements")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range maps.All {
|
||||
if !m.IsAccepted() {
|
||||
continue
|
||||
}
|
||||
if len(ev.RequiredFor(m.TargetFramework, m.TargetControl)) == 0 {
|
||||
t.Errorf("accepted control %s %s has no required evidence (dangling graph node)", m.TargetFramework, m.TargetControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# 01 — Retrieval-Pipeline
|
||||
|
||||
**Zweck:** Einen Kandidaten-Pool bauen, der die *richtigen* Quellen enthält (Pflichtquelle **und** Controls) — auch dann, wenn reine Semantik sie verfehlen würde. Re-Ranking (02) kann nur ordnen, was im Pool liegt; deshalb ist der Pool-Aufbau die erste Verteidigungslinie gegen Recall-Lücken.
|
||||
|
||||
## Mechanik
|
||||
|
||||
`searchInternal()` (`legal_rag_client.go`) orchestriert den Pool in fester Reihenfolge — jede Stufe **augmentiert** (ersetzt nie), Fehler degradieren still:
|
||||
|
||||
1. **Embedding** — `bge-m3` (1024-dim) über Ollama, Query auf 2000 Zeichen gekappt.
|
||||
2. **Hybrid (RRF)** — `searchHybrid()`: dense + Volltext via Qdrant Query-API, RRF-Fusion. Fällt bei Fehler auf `searchDense()` (reine Vektorsuche) zurück.
|
||||
3. **Binding-Augmentation** — `searchBinding()`: zieht die Top-`source_class=binding_law`-Treffer dazu, **damit die Pflichtquelle immer Kandidat ist**, auch wenn Guidance semantisch dominiert.
|
||||
4. **Control-Augmentation** — `searchControls()`: nur bei Control-Intent (siehe [05](05-control-intent.md)); tiefer dense-Pull, gefiltert auf Control-Pool-Rollen.
|
||||
5. **Graph-Augmentation** — `expandViaGraph()`: **opt-in**; zieht verbundene Normen über Zitations-Kanten.
|
||||
6. **Merge** — `mergeDedupHits()`: konkateniert, behält die erste Vorkommnis je Punkt-ID, Reihenfolge erhalten.
|
||||
|
||||
Danach: Map auf `LegalSearchResult` → Authority-Rerank (02) → Control-Diversity (05) → Truncate auf `topK`.
|
||||
|
||||
## Konstanten + Warum
|
||||
|
||||
| Konstante | Wert | Warum |
|
||||
|-----------|------|-------|
|
||||
| `prefetchLimit` (hybrid) | `20`, bzw. `topK*4` bei topK>20 | Fusion-Fenster: genug dense-Kontext für RRF, ohne den Volltext-Anteil zu verwässern |
|
||||
| `controlPoolDepth` | `60` | **Gemessen:** für EU-Cyber-Control-Queries liegen die relevanten Control-Quellen (NIST, CRA-Anhang) bei dense-Rang ~8–9 — weit unter dem kleinen top-K. Auf dem größeren (95k) synced Korpus reicht ein fixer Tiefen-Pull von 60, um sie zum Kandidaten zu machen |
|
||||
| `graphSeedCount` | `5` | nur die Top-Hits als Graph-Saat (Begrenzung der Expansion) |
|
||||
| `graphMaxExpand` | `15` | Obergrenze der über Kanten gezogenen Normen |
|
||||
| `graphHopPenalty` | `0.05` | leichte Distanz-Strafe pro Kante (Pool-Expansion, kein Ranking-Hebel) |
|
||||
| `RAG_GRAPH_EXPANSION` | env, default **aus** | **Opt-in:** kein gemessener Rang-Nutzen ggü. der Binding-Augmentation, +1 Qdrant-Call/Suche, Flutungsrisiko über Reverse-Kanten. Bleibt als Recall-Sicherheitsnetz |
|
||||
|
||||
> Forward-Kanten (`references_out`) treiben die Graph-Expansion; Reverse-Kanten (`references_in`) werden **nur als Metadaten** geführt (sonst flutet ein populärer Anhang den Pool).
|
||||
|
||||
## Code
|
||||
|
||||
- `legal_rag_client.go` → `searchInternal()`, `mergeDedupHits()`
|
||||
- `legal_rag_http.go` → `searchHybrid()`, `searchDense()`, `searchBinding()`, `searchControls()`
|
||||
- `legal_rag_graph.go` → `expandViaGraph()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Pflichtquelle nicht im Pool"** → Binding-Augmentation (Stufe 3) garantiert die `binding_law`-Quelle als Kandidat.
|
||||
- **„Control-Quelle unter top-K"** → Control-Augmentation + `controlPoolDepth` (Stufe 4) holt tiefliegende NIST/CRA-Anhang-Treffer.
|
||||
- **„Recall-Lücke bei Synonymen"** → Hybrid (RRF) deckt lexikalische Treffer ab, die rein semantisch fehlen.
|
||||
@@ -0,0 +1,51 @@
|
||||
# 02 — Authority-Re-Ranking
|
||||
|
||||
**Zweck:** Bindendes Recht der passenden Jurisdiktion/Domäne nach oben, Guidance/Fremdrecht/Off-Domain nach unten — **Reihenfolge only, nichts wird gelöscht**. Der `Score` trägt nach dem Rerank den Authority-Score, damit nachgelagerte Multi-Collection-Merges (Advisor) die Ordnung bewahren.
|
||||
|
||||
## Mechanik
|
||||
|
||||
`authorityScore()` (`authority_rerank.go`) berechnet pro Treffer einen normativen Relevanz-Score aus dem rohen Semantik-Score + gewichteter Autorität + Kontext-Bonus/Penalty:
|
||||
|
||||
```
|
||||
score = rawSemantic
|
||||
+ authorityCoef · weight/100 (Autorität, siehe 03)
|
||||
+ jurisdictionGain (DE/EU-Match)
|
||||
− foreignPenalty (CH bei DE/EU-Frage)
|
||||
− unknownPenalty (unbekannte Klasse)
|
||||
+ domainMatchGain (Chunk-Domäne == Query-Domäne)
|
||||
− offDomainPenalty (bindend, aber off-domain)
|
||||
− scopePenalty (BDSG Teil 3 bei allgemeiner DS-Frage)
|
||||
+ topicGain (bevorzugte kanonische Norm)
|
||||
− supersededPenalty (status="superseded")
|
||||
```
|
||||
|
||||
`rerankByAuthority()` sortiert stabil nach diesem Score und schreibt ihn zurück. `liftAboveBinding()` hebt bei **Auslegungs-Intent** eine semantisch konkurrenzfähige Guidance knapp über das bindende Recht — mit Margin-Guard, damit off-topic-Guidance das Gesetz nicht überholt.
|
||||
|
||||
## Konstanten + Warum
|
||||
|
||||
| Konstante | Wert | Warum |
|
||||
|-----------|------|-------|
|
||||
| `authorityCoef` | `0.40` | Gewicht→Score-Multiplikator. Konservativ kalibriert gegen die Offline-Golden-Harness (Phase A): hoch genug, dass bindendes Recht gewinnt, niedrig genug, dass starke Semantik nicht erschlagen wird |
|
||||
| `jurisdictionGain` | `0.05` | leichter Vorzug für DE/EU-Quellen bei DE/EU-Frage |
|
||||
| `foreignPenalty` | `0.60` | Fremdrecht (CH) bei DE/EU-Frage klar demoten — aber **nicht** entfernen (Vergleichsfälle bleiben auffindbar) |
|
||||
| `unknownPenalty` | `0.08` | unklassifizierte Quellen leicht zurückstufen |
|
||||
| `domainMatchGain` | `0.15` | Domänen-Treffer (data_protection / cyber / ai / product_safety) belohnen |
|
||||
| `offDomainPenalty` | `0.10` | bindende, aber fachfremde Norm demoten (z.B. DSGVO bei reiner Cyber-Frage) |
|
||||
| `scopePenalty` | `0.25` | BDSG §45–84 (Justiz/Strafverfolgung) bei allgemeiner DS-Frage zurückstufen — häufige Scope-Verwechslung |
|
||||
| `topicGain` | `0.18` | Verstärker für bevorzugte kanonische Normen (z.B. Art. 37 DSGVO bei DSB-Fragen) |
|
||||
| `supersededPenalty` | `0.50` | abgelöste Alt-Quelle demoten, „damit Default-Fragen die eu-v1-Norm sehen, History aber auffindbar bleibt" |
|
||||
| `intentLiftGain` | `0.10` | Epsilon-Lift einer Guidance über das beste bindende Recht bei Auslegungs-Intent |
|
||||
| `intentLiftMargin` | `0.05` | Guard: Lift nur, wenn die Semantik innerhalb von 0.05 zum besten bindenden Treffer liegt |
|
||||
|
||||
**Auslegungs-Intent-Signale** (`guidanceIntentSignals`): `edpb`, `dsk`, `enisa`, `bsi`, `leitlinie`, `guideline`, `orientierungshilfe`, `auslegung`, `empfiehlt`, `empfehlung`, `sagt`, `laut`, …
|
||||
|
||||
## Code
|
||||
|
||||
- `authority_rerank.go` → `authorityScore()`, `rerankByAuthority()`, `bestBindingSemantic()`, `liftAboveBinding()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Guidance verdrängt Gesetz"** → `authorityCoef`·weight hebt bindendes Recht; `liftAboveBinding` nur mit Margin-Guard.
|
||||
- **„Fremdrecht Top-1"** → `foreignPenalty`.
|
||||
- **„Off-Domain-Gesetz dominiert"** → `domainMatchGain` / `offDomainPenalty` / `scopePenalty`.
|
||||
- **„Veraltete Norm gewinnt"** → `supersededPenalty` (siehe [08](08-explainability.md)).
|
||||
@@ -0,0 +1,49 @@
|
||||
# 03 — `source_class` (Rechtsnatur / Autorität)
|
||||
|
||||
**Zweck:** Die Autoritäts-Achse, die den **Rang** bestimmt (siehe [02](02-authority.md)). Deterministisch abgeleitet — der noch nicht re-ingestierte (ungetaggte) Korpus wird trotzdem klassifiziert, ohne Re-Tagging des Bestands.
|
||||
|
||||
## Mechanik
|
||||
|
||||
`classifyAuthority()` (`authority.go`) entscheidet in dieser Reihenfolge:
|
||||
|
||||
1. **Standard-NAME-Override** — erkannter Standard-Name (NIST/OWASP/ISO 27001/CIS/CSA CCM/Grundschutz) erzwingt `technical_standard` (Gewicht 80), **auch wenn die Payload `supervisory_guidance` sagt**. Grund: der Korpus taggt viele Standards mit generischem guidance-`source_class`; der Name ist autoritativer. `binding_law` bleibt unangetastet.
|
||||
2. **Explizite Payload-Werte** — gesetztes `source_class` / `authority_weight` gewinnen.
|
||||
3. **Marker-Fallback** — foreign → standard → guidance → regulation → unknown.
|
||||
|
||||
`inferJurisdiction()`: Fremd-Marker → `CH`; enthält `§` oder DE-Marker → `DE`; sonst → `EU`.
|
||||
|
||||
## Konstanten + Warum
|
||||
|
||||
**Gewichte je Klasse** (`sourceClassFromWeight()`):
|
||||
|
||||
| `source_class` | Gewicht | Schwelle | Bedeutung |
|
||||
|----------------|---------|----------|-----------|
|
||||
| `binding_law` | `100` | w ≥ 100 | bindendes Recht (Gesetz/VO) |
|
||||
| `technical_standard` | `80` | 80 ≤ w < 100 | Best-Practice-Control-Katalog (NIST/OWASP/ISO) |
|
||||
| `supervisory_guidance` | `70` | 70 ≤ w < 80 | Aufsichts-/Auslegungs-Guidance (ENISA/BSI/EDPB) |
|
||||
| `unknown` | `50` | default | unklassifiziert |
|
||||
| `foreign_law` | `0` | w ≤ 0 | Fremdrecht (CH) |
|
||||
|
||||
**Marker-Listen** (Substring-Match):
|
||||
|
||||
| Liste | Einträge (Auszug) | Wirkung |
|
||||
|-------|-------------------|---------|
|
||||
| `standardMarkers` *(vor guidance geprüft)* | NIST, OWASP, Grundschutz, ISO 27001, ISO/IEC 27001, CSA CCM, Cloud Controls Matrix, CIS Benchmark, CIS Control | → `technical_standard` (80) |
|
||||
| `guidanceMarkers` | DSK, EDPB, BfDI, ENISA, BSI, EUCC, Standards Mapping, Orientierungshilfe, Handreichung, Leitlinie, Empfehlung, OECD, CISA, Blue Guide, … | → `supervisory_guidance` (70) |
|
||||
| `foreignMarkers` | RevDSG, fedlex, (CH) | → `foreign_law` (0) |
|
||||
| `deMarkers` | BDSG, DSK, BfDI, BayLfD, BSI | Signal **DE**-Jurisdiktion |
|
||||
|
||||
## Der Standard-Name-Override (Fix 2026-06-25)
|
||||
|
||||
**Problem:** Der CE-Korpus taggt z.B. `NIST SP 800-82r3` als `source_class=supervisory_guidance` (Gewicht 70), **nicht** technical_standard. `classifyAuthority` vertraute dem Payload-Tag → NIST landete als guidance, **kein `control_standard`** im Pool → die Diversity-Regel ([05](05-control-intent.md)) konnte nichts injizieren.
|
||||
|
||||
**Fix:** Erkannter Standard-Name überschreibt ein fehl-getaggtes guidance/unknown-`source_class` → `technical_standard`. Code-Fix, **kein Re-Ingest** nötig. Bindendes Recht bleibt unangetastet (Sanity geprüft: Rechtsfrage liefert weiterhin binding Top-1).
|
||||
|
||||
## Code
|
||||
|
||||
- `authority.go` → `classifyAuthority()`, `sourceClassFromWeight()`, `inferJurisdiction()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Standard als guidance mistagged → kein control_standard"** → Standard-Name-Override.
|
||||
- **„Fremdrecht falsch eingeordnet"** → `foreignMarkers` + `foreign_law`-Gewicht 0.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 04 — `source_role` (Funktionale Rolle)
|
||||
|
||||
**Zweck:** Die zu `source_class` **orthogonale** Achse: *Was tut die Quelle im Dokument?* Sie bestimmt die **Control-Pool-Zugehörigkeit** bei Umsetzungsfragen — unabhängig von der Rechtsnatur. Deterministisch aus Markern abgeleitet, kein Re-Tagging des Bestands.
|
||||
|
||||
## Die 7 Rollen
|
||||
|
||||
| Konstante | Wert | Definition |
|
||||
|-----------|------|-----------|
|
||||
| `roleObligation` | `obligation` | die abstrakte Pflicht (das WAS) |
|
||||
| `roleOperationalReq` | `operational_requirement` | konkrete bindende Anforderung (z.B. CRA Anhang I) |
|
||||
| `roleProceduralReq` | `procedural_requirement` | Prozess: Meldung/Registrierung/DSFA/Incident |
|
||||
| `roleControlStandard` | `control_standard` | Best-Practice-Katalog (NIST/OWASP/ISO/CIS) |
|
||||
| `roleImplGuidance` | `implementation_guidance` | Umsetzungs-How-to (ENISA Good Practices, BSI) |
|
||||
| `roleInterpretation` | `interpretation` | interpretiert die *Bedeutung* der Norm (EDPB-Leitlinie) |
|
||||
| `roleDefinition` | `definition` | Definitionen / Scope / Recitals |
|
||||
|
||||
**Control-Pool** = `{operational_requirement, procedural_requirement, control_standard, implementation_guidance}` (die vier „wie umsetzen"-Rollen, `isControlPoolRole()`).
|
||||
|
||||
## Mechanik
|
||||
|
||||
`classifyRole()` (`control_role.go`) — Entscheidungsreihenfolge:
|
||||
|
||||
1. `IsRecital` → `definition`
|
||||
2. `source_class == technical_standard` → `control_standard`
|
||||
3. `source_class == supervisory_guidance`:
|
||||
- enthält `implMarker` → `implementation_guidance`
|
||||
- sonst → `interpretation`
|
||||
4. `source_class == binding_law`:
|
||||
- `definitionMarker` → `definition`
|
||||
- `proceduralMarker` → `procedural_requirement`
|
||||
- `annexMarker` **oder** `operationalMarker` → `operational_requirement`
|
||||
- sonst → `obligation`
|
||||
5. default → `obligation`
|
||||
|
||||
`controlRoleOf(payload)` klassifiziert die rohe Qdrant-Payload **vor** dem Mapping — so kann `searchControls` ([01](01-retrieval.md)) seinen tiefen dense-Pull filtern, ohne jeden Treffer voll zu materialisieren.
|
||||
|
||||
## Marker-Listen
|
||||
|
||||
| Liste | Einträge (Auszug) | → Rolle |
|
||||
|-------|-------------------|---------|
|
||||
| `proceduralMarkers` | Meldung, Meldepflicht, Notification, Registrierung, Konformitätserklärung, Incident, Reporting, Folgenabschätzung, DSFA, DPIA, Anzeigepflicht | `procedural_requirement` |
|
||||
| `annexMarkers` | Anhang, Annex, Appendix, Anlage | `operational_requirement` |
|
||||
| `operationalMarkers` | Anforderung, Requirement, essential, wesentliche | `operational_requirement` |
|
||||
| `implMarkers` | Good Practice, Best Practice, Standards Mapping, Umsetzung, Implementation, Handreichung, Maßnahmenkatalog, ICS, SCADA, Technical Guideline, TIG | `implementation_guidance` |
|
||||
| `definitionMarkers` | Begriffsbestimmung, Definition | `definition` |
|
||||
|
||||
## Warum orthogonal zu `source_class`
|
||||
|
||||
`source_class` (Rechtsnatur) und `source_role` (Funktion) sind **zwei Achsen**, nicht eine. ENISA bleibt `supervisory_guidance` (Rechtsnatur) **und** `implementation_guidance` (Funktion) — sie wird **nicht** umgetaggt (fachlich falsch), darf aber bei Umsetzungsfragen in den Control-Pool. So muss der Bestand nicht angefasst werden: `source_role` ist wie `source_class` aus Markern ableitbar.
|
||||
|
||||
`source_role` ist die **Wirbelsäule der Langzeit-Architektur** Regulation → Obligation → Operational Requirement → Control → Evidence ([09](09-framework-layer.md), Prio 4).
|
||||
|
||||
## Code
|
||||
|
||||
- `control_role.go` → `classifyRole()`, `controlRoleOf()`, `isControlPoolRole()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Controls = nur technical_standard"** → vier Control-Pool-Rollen statt einer.
|
||||
- **„abstrakte Pflicht dominiert Umsetzungsfrage"** → `obligation` ist *nicht* im Control-Pool (siehe [05](05-control-intent.md)).
|
||||
@@ -0,0 +1,51 @@
|
||||
# 05 — Control-Intent + Diversity
|
||||
|
||||
**Zweck:** Bei einer **Umsetzungsfrage** („Welche Controls/Maßnahmen passen?") den Control-Pool ([04](04-source-role.md)) über die abstrakte Pflicht heben — und sicherstellen, dass die Ergebnisliste **verschiedene Quellenarten** zeigt, statt dass eine Rolle sie flutet. Bei einer **Rechtsfrage** bleibt alles beim Authority-Rerank ([02](02-authority.md)).
|
||||
|
||||
## Intent-Erkennung
|
||||
|
||||
`queryWantsControls()` (`authority_rerank.go`) — Keyword-Match (`controlIntentSignals`):
|
||||
|
||||
> control, controls, maßnahme, schutzmaßnahme, best practice, umsetzen, implementier, absicher, härt, hardening, nist, owasp, grundschutz, ccm, iso 27001, isms
|
||||
|
||||
Nur wenn dieser Gate `true` ist, feuern `applyControlRoles()` und `ensureControlDiversity()`.
|
||||
|
||||
## Rollen-Boost (`applyControlRoles`)
|
||||
|
||||
Jeder Control-Pool-Treffer bekommt `controlPoolGain + controlRoleBonus[role]` auf den Score:
|
||||
|
||||
| Größe | Wert | Warum |
|
||||
|-------|------|-------|
|
||||
| `controlPoolGain` | `0.15` | hebt **jede** Control-Pool-Rolle über die Nicht-Control-Rollen (obligation/interpretation/definition) — sonst gewinnt die bindende abstrakte `obligation` per Autorität allein |
|
||||
| `controlRoleBonus[operational_requirement]` | `0.100` | weicher Intra-Pool-Vorrang (User 2026-06-24): op_req zuerst |
|
||||
| `controlRoleBonus[procedural_requirement]` | `0.075` | … dann Prozess-Pflichten |
|
||||
| `controlRoleBonus[control_standard]` | `0.050` | … dann Standard-Kataloge |
|
||||
| `controlRoleBonus[implementation_guidance]` | `0.000` | guidance als Basis, kein Bonus |
|
||||
|
||||
> **Bewusst weich, keine harte Hierarchie:** Eine semantisch dominante `implementation_guidance` (z.B. ENISA bei einer EU-Cyber-Umsetzungsfrage) **darf Top-1 bleiben** — das ist fachlich korrekt. Der Boost demoted nur die abstrakte Pflicht, er erzwingt keine Reihenfolge.
|
||||
|
||||
## Control-Diversity-Regel (`ensureControlDiversity`)
|
||||
|
||||
**Problem:** Selbst mit Boost kann eine dichte Wolke gleicher Rolle (viele ENISA-Chunks) `operational_requirement` und `control_standard` aus der Top-K verdrängen — die Quellenarten werden unsichtbar.
|
||||
|
||||
**Lösung (statt harter `+0.30`-Rollenkeule):** Wenn die Top-K nur `implementation_guidance` enthält, **injiziere** den besten `operational_requirement` + besten `control_standard` aus dem Pool, indem der niedrigst-platzierte redundante guidance-Slot verdrängt wird. Algorithmus:
|
||||
|
||||
1. Rolle jedes Treffers bestimmen (`roleAt`).
|
||||
2. Prüfen, welche Rollen in der Top-K vertreten sind.
|
||||
3. Für jede fehlende Wunsch-Rolle (`operational_requirement`, `control_standard`): besten Treffer dieser Rolle unterhalb der Top-K finden, niedrigste `implementation_guidance` in der Top-K überschreiben.
|
||||
4. Truncate auf `topK` (das ursprüngliche Duplikat fällt im Tail weg).
|
||||
|
||||
**Ergebnis live:** Umsetzungsfrage → `1.–4. ENISA · 5. NIST SP 800-82r3 (control_standard) · 6. MaschinenVO Anhang-III (op_req)`. ENISA behält Top-1, die anderen Quellenarten sind sichtbar.
|
||||
|
||||
> **Prinzip:** Nicht raten, nicht erzwingen, sondern relevante Quellenarten sichtbar machen.
|
||||
|
||||
## Code
|
||||
|
||||
- `authority_rerank.go` → `queryWantsControls()`
|
||||
- `control_role.go` → `applyControlRoles()`, `ensureControlDiversity()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„abstrakte Pflicht dominiert Umsetzungsfrage"** → `controlPoolGain`.
|
||||
- **„eine Rolle flutet die Top-K, Quellenarten unsichtbar"** → `ensureControlDiversity`.
|
||||
- **„harte Tier-Ordnung overfittet auf eine Frage"** → weicher Boost statt Keule.
|
||||
@@ -0,0 +1,45 @@
|
||||
# 06 — Assessment
|
||||
|
||||
**Zweck:** Eine **auditierbare Begründungsschicht** über die gerankten Ergebnisse. Sie macht aus einer Trefferliste eine prüfbare Aussage: *Welche Norm ist primär, welche hängen daran, wie eindeutig ist das, braucht es einen Menschen?*
|
||||
|
||||
## Mechanik
|
||||
|
||||
`Assess()` (`legal_rag_assess.go`) nimmt die bereits gerankten `results []LegalSearchResult` und baut ein `LegalAssessment`:
|
||||
|
||||
| Feld | Inhalt |
|
||||
|------|--------|
|
||||
| `PrimaryNorm` | `CitationUnit` bzw. `ArticleLabel` des Top-Treffers |
|
||||
| `PrimaryRegulation` | `RegulationShort` des Top-Treffers |
|
||||
| `ConnectedNorms` | verbundene Normen (`references_out` + `references_in`), gekappt + dedupliziert |
|
||||
| `CrossRegime` | ob mehrere Regulierungen in den Top-N liegen |
|
||||
| `WinnerMargin` | Score-Abstand Top-1 ↔ Top-2 (Proxy für Eindeutigkeit) |
|
||||
| `HumanReviewFlag` | true bei niedriger Eindeutigkeit |
|
||||
| `ScoreReasoning` | kurze deutsche Begründung |
|
||||
|
||||
## Konstanten + Warum
|
||||
|
||||
| Konstante | Wert | Warum |
|
||||
|-----------|------|-------|
|
||||
| `assessConnectedCap` | `12` | Obergrenze der in der Assessment gezeigten verbundenen Normen — verhindert, dass ein stark vernetzter Artikel die Begründung flutet |
|
||||
| `assessCrossRegimeTopN` | `5` | Fenster, über das „Cross-Regime" (mehrere Regulierungen) beurteilt wird |
|
||||
| `assessReviewMargin` | `0.05` | enger Winner-Abstand → Human-Review-Flag (siehe [07](07-confidence.md)) |
|
||||
|
||||
## Human-Review-Logik
|
||||
|
||||
`HumanReviewFlag` wird `true`, wenn **eine** der Bedingungen gilt:
|
||||
|
||||
- `WinnerMargin < 0.05` — Top-1 und Top-2 liegen zu dicht beieinander (uneindeutig),
|
||||
- `CrossRegime == true` — mehrere Regimes betroffen (z.B. DSGVO + CRA),
|
||||
- der Primär-Treffer ist **nicht** `binding_law` — eine Rechtsaussage ohne bindende Primärquelle.
|
||||
|
||||
> Das ist die deterministische Eskalations-Schwelle: das System sagt von sich aus „hier sollte ein Mensch drauf schauen", statt scheinbare Sicherheit vorzutäuschen.
|
||||
|
||||
## Code
|
||||
|
||||
- `legal_rag_assess.go` → `Assess()`, `primaryLabel()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„uneindeutige Antwort wird als sicher verkauft"** → `WinnerMargin` + `HumanReviewFlag`.
|
||||
- **„Cross-Regime übersehen"** → `CrossRegime` über `assessCrossRegimeTopN`.
|
||||
- **„Rechtsaussage ohne bindende Quelle"** → Flag bei nicht-bindendem Primär-Treffer.
|
||||
@@ -0,0 +1,38 @@
|
||||
# 07 — Confidence
|
||||
|
||||
**Zweck:** Eine ehrliche Aussage über die Verlässlichkeit eines Ergebnisses — ohne einen erfundenen „Confidence: 87 %"-Wert, der Scheinsicherheit suggeriert.
|
||||
|
||||
## Bewusste Entscheidung: kein eigenes Confidence-Feld
|
||||
|
||||
Es gibt **kein** explizites `confidence`-Feld in der Engine. Stattdessen wird Verlässlichkeit aus zwei real berechneten, prüfbaren Größen abgeleitet:
|
||||
|
||||
| Größe | Quelle | Bedeutung |
|
||||
|-------|--------|-----------|
|
||||
| `WinnerMargin` | `LegalAssessment` ([06](06-assessment.md)) | Score-Abstand Top-1 ↔ Top-2 — wie klar „gewinnt" die Primärnorm? |
|
||||
| `HumanReviewFlag` | `LegalAssessment` | deterministische Eskalation: ist die Antwort uneindeutig/grenzwertig? |
|
||||
|
||||
**Warum so?** Ein kalibrierter Wahrscheinlichkeitswert würde eine Genauigkeit vortäuschen, die ein regelbasierter Retriever nicht hat. Der **Abstand** zwischen Top-1 und Top-2 ist dagegen eine *gemessene*, erklärbare Größe: ein großer Margin = eindeutige Norm, ein kleiner Margin = mehrere plausible Quellen → Mensch entscheiden lassen.
|
||||
|
||||
## Schwelle
|
||||
|
||||
| Konstante | Wert | Wirkung |
|
||||
|-----------|------|---------|
|
||||
| `assessReviewMargin` | `0.05` | `WinnerMargin < 0.05` ⇒ `HumanReviewFlag = true` |
|
||||
|
||||
`HumanReviewFlag` feuert zusätzlich bei Cross-Regime und bei nicht-bindender Primärquelle ([06](06-assessment.md)).
|
||||
|
||||
## Verhältnis zur Authority-Schicht
|
||||
|
||||
Der `Score`, auf dem der Margin beruht, ist **nicht** der rohe Semantik-Score, sondern der Authority-Score nach dem Rerank ([02](02-authority.md)). Damit misst der Margin die *normative* Eindeutigkeit (Rechtsnatur + Domäne berücksichtigt), nicht nur die semantische Ähnlichkeit.
|
||||
|
||||
## Code
|
||||
|
||||
- `legal_rag_types.go` → `LegalSearchResult.Score`, `LegalAssessment.WinnerMargin`, `LegalAssessment.HumanReviewFlag`
|
||||
- `legal_rag_assess.go` → Berechnung in `Assess()`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Scheinsicherheit"** → kein erfundener Prozentwert; Margin + Flag statt Pseudo-Confidence.
|
||||
- **„knappe Entscheidung wird automatisch durchgewinkt"** → `assessReviewMargin`-Eskalation.
|
||||
|
||||
> **Ausbaustufe:** Echte Citation-Gating-Confidence (Finding nur bei Quelle ∧ Scope ∧ Stichtag) gehört in die Authority-/Freshness-Schicht und an Control → Evidence ([09](09-framework-layer.md)), nicht in einen Modell-Score.
|
||||
@@ -0,0 +1,42 @@
|
||||
# 08 — Explainability, Zitate + Supersede
|
||||
|
||||
**Zweck:** Jedes Ergebnis muss sich **belegen** lassen — woher es kommt, womit es verbunden ist, und ob es noch gilt. Das ist die Grundlage für Zitierfähigkeit und für die spätere Citation-Gating-Logik.
|
||||
|
||||
## Zitate + Graph-Kanten
|
||||
|
||||
Aus der Qdrant-Payload geladen (Phase-2-Graph-Metadaten):
|
||||
|
||||
| Feld | Inhalt | Verwendung |
|
||||
|------|--------|-----------|
|
||||
| `CitationUnit` | kanonischer Artikel-/Anhang-Identifier | Dedup, Primärnorm-Label |
|
||||
| `article_label` | menschenlesbare Fundstelle (z.B. „Art. 13 CRA") | Anzeige, Begründung |
|
||||
| `citation_style` | Zitierformat-Marker | Anzeige |
|
||||
| `references_out` | Normen, die dieser Chunk **zitiert** (Forward-Kanten) | Graph-Expansion ([01](01-retrieval.md)) + `ConnectedNorms` |
|
||||
| `references_in` | Normen, die **diesen** Chunk zitieren (Reverse-Kanten) | **nur** Metadaten — nicht expandiert (Flutungsschutz) |
|
||||
|
||||
`Assess()` ([06](06-assessment.md)) verdichtet die Kanten zu `ConnectedNorms` — so wird sichtbar, dass z.B. Art. 13 CRA auf Anhang I verweist (die eigentliche Pflichtquelle).
|
||||
|
||||
## Supersede-Handling
|
||||
|
||||
Recht ändert sich; ein veralteter Stand darf den aktuellen nicht schlagen — aber Übergangs-/History-Fragen müssen ihn noch finden.
|
||||
|
||||
| Mechanik | Wert / Feld | Verhalten |
|
||||
|----------|-------------|-----------|
|
||||
| **Erkennung** | Payload `status == "superseded"` → `Superseded`-Flag | markiert die abgelöste Alt-Quelle |
|
||||
| **Demotion** | `supersededPenalty = 0.50` (`authorityScore`, [02](02-authority.md)) | konsequente Zurückstufung |
|
||||
| **Philosophie** | — | „Alt-Quelle demoted (nicht versteckt) — Default-Fragen sehen die eu-v1-Norm, History bleibt auffindbar" |
|
||||
|
||||
> **Nicht entfernt, nur bestraft:** Eine abgelöste Norm kann bei einer expliziten History-Frage trotzdem hoch ranken — sie wird nur konsistent demoted, nicht ausgeblendet. Das ist dieselbe „Reihenfolge, nichts löschen"-Linie wie beim Authority-Rerank.
|
||||
|
||||
## Code
|
||||
|
||||
- `legal_rag_client.go` → Payload-Mapping (`references_out/in`, `status`)
|
||||
- `legal_rag_graph.go` → Forward-Kanten-Expansion, Reverse-Kanten als Metadaten
|
||||
- `legal_rag_assess.go` → `ConnectedNorms`
|
||||
- `authority_rerank.go` → `supersededPenalty`
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„Aussage ohne Fundstelle"** → `CitationUnit` / `article_label`.
|
||||
- **„Pflichtquelle hinter Verweis versteckt"** → Forward-Kanten-Expansion (Art. 13 → Anhang I).
|
||||
- **„veralteter Rechtsstand gewinnt"** → `supersededPenalty`, aber auffindbar.
|
||||
@@ -0,0 +1,51 @@
|
||||
# 09 — `framework_*`-Layer (Control-Mapping-Brücke)
|
||||
|
||||
**Zweck:** Einen **konkreten Control adressierbar** machen (z.B. `V14.2.4`), damit das System vom „welches Dokument passt?" zum „welcher konkrete Control erfüllt CRA Annex I?" übergeht. Das ist die Brücke zur nächsten Stufe — **Control → Evidence** — und der eigentliche Burggraben.
|
||||
|
||||
> **Ehrlicher Status:** Dieser Layer lebt **heute in der Qdrant-Payload**, nicht im Retrieval-Code. Die `ucca`-Engine liest/routet `framework_*` (noch) nicht — sie ist die **Datengrundlage**, auf der Prio 4 aufsetzt. `framework_control` reist aktuell im Feld `article` mit und ist daher bereits in den Antworten sichtbar.
|
||||
|
||||
## Schema (pro Chunk)
|
||||
|
||||
| Feld | Beispiel (OWASP) | Bedeutung |
|
||||
|------|------------------|-----------|
|
||||
| `framework` | `OWASP ASVS` | Rahmenwerk |
|
||||
| `framework_version` | `5.0` | Version (mit `superseded`-Mechanik historisierbar, [08](08-explainability.md)) |
|
||||
| `framework_section` | `V6` | Kapitel/Sektion |
|
||||
| `framework_control` | `V6.2.4` | konkrete Requirement-ID — der adressierbare Control |
|
||||
| `framework_section_name` | `Password Security` | menschenlesbarer Kontext |
|
||||
| `asvs_level` | `L1`/`L2`/`L3` | (OWASP-spezifisch) Stufe |
|
||||
|
||||
Analog für NIST geplant: `framework="NIST SP 800-53"`, `framework_family="SI"`, `framework_control="SI-2"`, `framework_revision="5"`.
|
||||
|
||||
## OWASP ASVS 5.0 — die erste Referenz (Parser-4-Muster)
|
||||
|
||||
- **Quelle:** `OWASP/ASVS` GitHub, `5.0/docs_en/...flat.json` (345 Requirements). Lizenz **CC-BY-SA-4.0** (zulässig; nur CC-BY-NC ist geblockt), Attribution `OWASP`.
|
||||
- **Ingestion = per-Requirement Direct-Upsert** (nicht der RAG-Chunker, der `framework_control` zerschneiden würde): 1 Qdrant-Punkt pro Requirement, `id = uuid5("owasp_asvs_5.0_"+req_id)` (idempotent), `source_class=technical_standard` / `authority_weight=80`, bge-m3-Vektor.
|
||||
- **Stand:** 345 Punkte auf macmini-qdrant **und** qdrant-dev, live verifiziert (`„OWASP … Authentifizierung"` → Top-OWASP mit `V`-Codes).
|
||||
- **Lehre:** Künftige Standards (NIST-Re-Tag, BSI Grundschutz) **immer** mit `source_class=technical_standard` + `framework_*` direkt setzen — das NIST-Altskript ließ `source_class` leer, daher der guidance-Mistag ([03](03-source-class.md)).
|
||||
|
||||
## Brücke zu Prio 4 — Control → Evidence
|
||||
|
||||
```
|
||||
Regulation
|
||||
↓ (legal obligation layer)
|
||||
Obligation
|
||||
↓ (source_role: operational_requirement)
|
||||
Operational Requirement ── CRA Annex I
|
||||
↓ (Control-Mapping über framework_control)
|
||||
Control ── OWASP V6.x · NIST SI-2 · BSI OPS.1.1
|
||||
↓
|
||||
Evidence ── der Nachweis, den ein Auditor sehen will
|
||||
```
|
||||
|
||||
Der nächste Schritt verdrahtet `framework_control` in eine **Control-Mapping-Tabelle** (welcher konkrete Control erfüllt welche Obligation) und darunter die **Evidence-Schicht**. NIST + BSI ziehen im selben `framework_*`-Muster nach.
|
||||
|
||||
## Code / Daten
|
||||
|
||||
- Daten: Qdrant `bp_compliance_ce` (Payload-Felder oben), Ingestion-Skripte (`ingest_owasp.py` u.a.)
|
||||
- Retrieval-Verdrahtung: **offen** (Prio 4)
|
||||
|
||||
## Adressierte Fehlerklassen
|
||||
|
||||
- **„nur Dokument-Treffer, kein adressierbarer Control"** → `framework_control` pro Chunk.
|
||||
- **„Control-Katalog ohne Stand"** → `framework_version` + Supersede.
|
||||
@@ -0,0 +1,57 @@
|
||||
# RAG-Retrieval-Engine — Architektur
|
||||
|
||||
Diese Sektion dokumentiert die **deterministische, regelbasierte Retrieval-Engine** des Compliance-SDK (`ai-compliance-sdk/internal/ucca/`). Sie beantwortet für jede Nutzerfrage: *Welche Norm/Quelle ist relevant — und warum?*
|
||||
|
||||
> **Warum diese Doku existiert:** Die Engine trifft viele bewusste `+0.05 / +0.10`-Entscheidungen. Jede Konstante kodiert eine **gemessene** Entscheidung (Golden-Harness, Fehlerklasse) — nicht eine willkürliche Stellschraube. Ohne das *Warum* sind sie in sechs Monaten nicht mehr nachvollziehbar; diese Doku ist die Referenz für Wartung, Onboarding und Audit-/Investoren-Nachweis.
|
||||
|
||||
## Leitprinzip
|
||||
|
||||
> **Nicht raten, nicht erzwingen, sondern relevante Quellenarten sichtbar machen.**
|
||||
|
||||
Der LLM entscheidet **nicht**, was Recht ist — nur, wie eine bereits versionierte, zitierte Norm auf einen Sachverhalt gemappt wird. Wo möglich ist die Engine deterministisch (Marker, Gewichte, Schwellen), nicht modellbasiert. Nichts wird *gelöscht* — Re-Ranking ist reine Reihenfolge, alles bleibt auffindbar.
|
||||
|
||||
## Zwei orthogonale Achsen
|
||||
|
||||
Der Kern des Modells: zwei unabhängige Achsen, die in der Literatur meist vermischt werden.
|
||||
|
||||
| Achse | Frage | Wirkung | Doku |
|
||||
|------|-------|---------|------|
|
||||
| **`source_class`** (Rechtsnatur) | Wie bindend ist die Quelle? | bestimmt den **Rang** | [03](03-source-class.md) |
|
||||
| **`source_role`** (Funktion) | Was tut die Quelle im Dokument? | bestimmt die **Control-Pool-Zugehörigkeit** | [04](04-source-role.md) |
|
||||
|
||||
Beispiel: NIST ist `technical_standard` (source_class) **und** `control_standard` (source_role). ENISA-Good-Practices sind `supervisory_guidance` **und** `implementation_guidance` — sie bleiben guidance, dürfen aber bei Umsetzungsfragen in den Control-Pool.
|
||||
|
||||
## Pipeline (Überblick)
|
||||
|
||||
```
|
||||
Query
|
||||
│ bge-m3 Embedding
|
||||
▼
|
||||
Retrieval-Pool ── hybrid (RRF) + binding-Augmentation + control-Augmentation + (graph) → 01
|
||||
▼
|
||||
Authority-Rerank ── source_class → Rang (bindendes Recht der passenden Jurisdiktion oben) → 02, 03
|
||||
▼
|
||||
Control-Intent ── source_role → Control-Pool + Diversity (Quellenarten sichtbar machen) → 04, 05
|
||||
▼
|
||||
Assessment ── PrimaryNorm · ConnectedNorms · WinnerMargin · CrossRegime → 06
|
||||
▼
|
||||
Confidence/Explainability ── HumanReviewFlag · Zitate · Graph-Kanten · Supersede → 07, 08
|
||||
```
|
||||
|
||||
`framework_*` ([09](09-framework-layer.md)) ist die **Daten-Brücke** zur nächsten Stufe (Control → Evidence) — heute in der Qdrant-Payload, noch nicht im Retrieval-Code verdrahtet.
|
||||
|
||||
## Dokumente
|
||||
|
||||
| # | Dokument | Inhalt |
|
||||
|---|----------|--------|
|
||||
| 01 | [Retrieval-Pipeline](01-retrieval.md) | Pool-Aufbau: hybrid + binding + control + graph |
|
||||
| 02 | [Authority-Re-Ranking](02-authority.md) | source_class → Rang, Bonus/Penalty-System |
|
||||
| 03 | [source_class](03-source-class.md) | Rechtsnatur, Gewichte, Marker, Standard-Name-Override |
|
||||
| 04 | [source_role](04-source-role.md) | 7 Rollen, Control-Pool, Klassifikation |
|
||||
| 05 | [Control-Intent + Diversity](05-control-intent.md) | Intent-Erkennung, Rollen-Bonus, Diversity-Regel |
|
||||
| 06 | [Assessment](06-assessment.md) | Auditierbare Begründungsschicht |
|
||||
| 07 | [Confidence](07-confidence.md) | WinnerMargin, HumanReviewFlag |
|
||||
| 08 | [Explainability + Supersede](08-explainability.md) | Zitate, Graph-Kanten, Supersede |
|
||||
| 09 | [framework_*-Layer](09-framework-layer.md) | Control-Mapping-Brücke (CRA Annex → OWASP V6.x) |
|
||||
|
||||
> **Fehlerklassen-These:** Modell und Korpus sind austauschbar; die *Fehlerklassen + Hebel* sind das IP. Jede Konstante unten adressiert eine benannte Fehlerklasse (z.B. „Guidance verdrängt Gesetz", „Standard als guidance mistagged"). Die Kalibrierung ist sublinear: wenige Klassen, viele Module.
|
||||
+11
@@ -56,6 +56,17 @@ markdown_extensions:
|
||||
|
||||
nav:
|
||||
- Start: index.md
|
||||
- Architektur RAG:
|
||||
- Übersicht: architecture/index.md
|
||||
- 01 Retrieval-Pipeline: architecture/01-retrieval.md
|
||||
- 02 Authority-Re-Ranking: architecture/02-authority.md
|
||||
- 03 source_class: architecture/03-source-class.md
|
||||
- 04 source_role: architecture/04-source-role.md
|
||||
- 05 Control-Intent + Diversity: architecture/05-control-intent.md
|
||||
- 06 Assessment: architecture/06-assessment.md
|
||||
- 07 Confidence: architecture/07-confidence.md
|
||||
- 08 Explainability + Supersede: architecture/08-explainability.md
|
||||
- 09 framework_*-Layer: architecture/09-framework-layer.md
|
||||
- Services:
|
||||
- AI Compliance SDK:
|
||||
- Uebersicht: services/ai-compliance-sdk/index.md
|
||||
|
||||
Reference in New Issue
Block a user