diff --git a/docs-src/architecture/01-retrieval.md b/docs-src/architecture/01-retrieval.md new file mode 100644 index 00000000..d93ddff6 --- /dev/null +++ b/docs-src/architecture/01-retrieval.md @@ -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. diff --git a/docs-src/architecture/02-authority.md b/docs-src/architecture/02-authority.md new file mode 100644 index 00000000..2728f4d4 --- /dev/null +++ b/docs-src/architecture/02-authority.md @@ -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)). diff --git a/docs-src/architecture/03-source-class.md b/docs-src/architecture/03-source-class.md new file mode 100644 index 00000000..b8c15a87 --- /dev/null +++ b/docs-src/architecture/03-source-class.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. diff --git a/docs-src/architecture/04-source-role.md b/docs-src/architecture/04-source-role.md new file mode 100644 index 00000000..eac06e44 --- /dev/null +++ b/docs-src/architecture/04-source-role.md @@ -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)). diff --git a/docs-src/architecture/05-control-intent.md b/docs-src/architecture/05-control-intent.md new file mode 100644 index 00000000..a2be31cc --- /dev/null +++ b/docs-src/architecture/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. diff --git a/docs-src/architecture/06-assessment.md b/docs-src/architecture/06-assessment.md new file mode 100644 index 00000000..686b6d1f --- /dev/null +++ b/docs-src/architecture/06-assessment.md @@ -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. diff --git a/docs-src/architecture/07-confidence.md b/docs-src/architecture/07-confidence.md new file mode 100644 index 00000000..c5a20dd9 --- /dev/null +++ b/docs-src/architecture/07-confidence.md @@ -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. diff --git a/docs-src/architecture/08-explainability.md b/docs-src/architecture/08-explainability.md new file mode 100644 index 00000000..b04a835d --- /dev/null +++ b/docs-src/architecture/08-explainability.md @@ -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. diff --git a/docs-src/architecture/09-framework-layer.md b/docs-src/architecture/09-framework-layer.md new file mode 100644 index 00000000..d40a1d93 --- /dev/null +++ b/docs-src/architecture/09-framework-layer.md @@ -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. diff --git a/docs-src/architecture/index.md b/docs-src/architecture/index.md new file mode 100644 index 00000000..6b68bff2 --- /dev/null +++ b/docs-src/architecture/index.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index bbeee1d2..c8035ec3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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