From b1b121bb23a83703534ec2835cd11e2b48d4081d Mon Sep 17 00:00:00 2001 From: noroot Date: Sat, 29 Aug 2026 09:39:41 +0200 Subject: [PATCH] feat: Ableitungen und harte Filter (Schritt 3 der Baureihenfolge) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reine Auswertungslogik in internal/rules, operiert auf rules.Antworten (geparst aus antrag.antworten) und den drei Regelwerken aus Schritt 1 — ohne DB-/Web-Zugriff, vollständig isoliert testbar. - EvaluateDatenklasse: "höchste zutreffende Stufe gewinnt" (höherer Rang). Trifft keine Stufe zu (z. B. B7 fälschlich "nein" trotz keiner anderen Kategorie), wird konservativ "intern" angenommen statt "oeffentlich" — im Zweifel mehr Schutz. Nicht fachlich bestätigt, siehe rules/OPEN.md Punkt 5. - EvaluateEinstufung: Prüfreihenfolge wie im Regelwerk (verboten zuerst = K.-o.-Prüfung), erste zutreffende Stufe/Variante gewinnt. IstVerboten prüft die K.-o.-Bedingung direkt. - DeriveAnforderungen: Anforderungsprofil aus Datenklasse+Einstufung. - FilterWerkzeuge/ErfuelltAnforderung: harter Filter gegen einen Werkzeugkatalog. WerkzeugEigenschaften ist ein eigener, schlanker Typ statt store.Werkzeug — internal/rules bleibt unabhängig von internal/store. Nur technische Werkzeug-Eigenschaften (avv_erforderlich, eu_verarbeitung, kein_training_auf_eingabe) werden hart gefiltert; Prozess-Anforderungen (menschliche_aufsicht, kennzeichnungspflicht, dsfa_erforderlich) sortieren kein Werkzeug aus, sondern werden später als Auflage vermerkt — Annahme, siehe rules/OPEN.md Punkt 6. rules/OPEN.md um Punkt 4 (Auswirkung der fehlenden Löschfristen auf den Filter), Punkt 5 (Datenklasse-Fallback) und Punkt 6 (welche Anforderungen hart filtern) ergänzt — Annahmen dokumentiert statt geraten, damit Schritt 3 nicht auf die fachliche Klärung warten musste. 16 neue Tests gegen die ECHTEN rules/*.yaml-Dateien (nicht nur synthetische Fixtures) — deckt Rang-Konflikte, "Unsicher zählt wie Ja", alle vier Einstufungsstufen inkl. Auffangregel, Anforderungs-Ableitung und Werkzeug-Filterung ab. Noch nicht ans Web angebunden (Schritt 4). --- CLAUDE.md | 29 +++-- internal/rules/evaluate.go | 207 +++++++++++++++++++++++++++++++ internal/rules/evaluate_test.go | 208 ++++++++++++++++++++++++++++++++ rules/OPEN.md | 30 ++++- 4 files changed, 465 insertions(+), 9 deletions(-) create mode 100644 internal/rules/evaluate.go create mode 100644 internal/rules/evaluate_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 8803702..df3b8b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,11 +139,18 @@ Werkzeug: `avv_erforderlich`, `eu_verarbeitung`, `menschliche_aufsicht`, `kennzeichnungspflicht`, `dsfa_erforderlich`. `internal/rules` lädt und validiert diese drei Dateien (eindeutige IDs, -eindeutige Ränge, jede Anforderung braucht mindestens einen Auslöser). -**Die Auswertung gegen echte Fragebogen-Antworten ist noch nicht -gebaut** — das ist Schritt 3 der Baureihenfolge (siehe unten), bewusst -erst, wenn der Fragebogen (Schritt 2) die exakten Fakten-Feldnamen -festlegt. +eindeutige Ränge, jede Anforderung braucht mindestens einen Auslöser) +und wertet sie seit Schritt 3 auch aus: `EvaluateDatenklasse`, +`EvaluateEinstufung`, `IstVerboten`, `DeriveAnforderungen` operieren auf +`rules.Antworten` (geparst aus `antrag.antworten` via `ParseAntworten`) +— reine, für sich getestete Funktionen ohne DB-/Web-Zugriff. +`FilterWerkzeuge`/`ErfuelltAnforderung` filtern einen Werkzeugkatalog +hart gegen die abgeleiteten Anforderungen (`WerkzeugEigenschaften` ist +ein schlanker, von `store.Werkzeug` unabhängiger Typ, damit +`internal/rules` weiterhin ohne `internal/store` auskommt). **Noch +nicht verdrahtet:** eine Web-Seite, die das für einen konkreten Antrag +tatsächlich aufruft und anzeigt — das ist Schritt 4 +("Ergebnisdarstellung mit Herleitung"). --- @@ -318,7 +325,10 @@ wiederverwendet aus dem alten Produkt). `bewertung`/`entscheidung`/ JavaScript. `antworten`-JSON nutzt exakt die Fakten-Schlüssel aus `rules/*.yaml` (b1-b7, c1-c5, c2_folge, c3_art) — das hat auch die OPEN.md-Frage zu den "verboten"-Fragebogen-Feldern final geklärt.) -3. Ableitungen und harte Filter (Auswertung gegen echte Antworten) +3. ~~Ableitungen und harte Filter~~ — **erledigt** (`internal/rules`: + `EvaluateDatenklasse`/`EvaluateEinstufung`/`DeriveAnforderungen`/ + `IstVerboten`/`FilterWerkzeuge`, vollständig getestet gegen die + echten `rules/*.yaml`-Dateien. Noch nicht ans Web angebunden.) 4. Ergebnisdarstellung mit Herleitung 5. Entscheidung, Snapshot, Audit-Log 6. Registereintrag und Export @@ -532,8 +542,11 @@ journalctl -u deklarix -f ## Offene Punkte - **Rangfolge der Datenklassen und Grenzfälle im Anforderungsprofil** - (siehe `rules/OPEN.md`) — vor Schritt 3 mit dem/der fachlich - Verantwortlichen (z. B. Datenschutzbeauftragte/r) bestätigen. + (siehe `rules/OPEN.md`, Punkte 1/2/5/6) — mit dem/der fachlich + Verantwortlichen (z. B. Datenschutzbeauftragte/r) bestätigen. Schritt + 3 ist trotzdem schon umgesetzt (auf Basis dieser dokumentierten + Annahmen) — nicht auf die Klärung gewartet, um nicht blockiert zu + bleiben, aber die Ableitung kann sich noch ändern. - ~~Genaue Fragebogen-Felder für die drei "verboten"-Varianten~~ — **erledigt in Schritt 2**, C3 hat jetzt eine Folgefrage `c3_art`. Weiterhin offen: ob die drei Varianten fachlich vollständig sind diff --git a/internal/rules/evaluate.go b/internal/rules/evaluate.go new file mode 100644 index 0000000..74890f1 --- /dev/null +++ b/internal/rules/evaluate.go @@ -0,0 +1,207 @@ +package rules + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Antworten ist die im Antrag gespeicherte Fragebogen-Antwort-Menge — +// dieselbe Struktur, die internal/web/antrag_handlers.go erzeugt +// (antwortenFromForm): ein flaches JSON-Objekt mit denselben +// Fakten-Schlüsseln wie in den Regelwerk-YAML-Dateien (b1..b7, c1..c5, +// c2_folge, c3_art). +type Antworten map[string]any + +// ParseAntworten liest antrag.antworten (rohes JSON aus der Datenbank). +func ParseAntworten(raw []byte) (Antworten, error) { + if len(raw) == 0 { + return Antworten{}, nil + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("rules: antworten parsen: %w", err) + } + return Antworten(m), nil +} + +// istJaOderUnsicher prüft eine B-Frage. "Unsicher zählt wie Ja" ist eine +// Auswertungsregel (siehe CLAUDE.md, Fragebogen-Abschnitt B) — deshalb +// hier und nicht schon beim Speichern des Antrags angewendet. +func (a Antworten) istJaOderUnsicher(key string) bool { + v, _ := a[key].(string) + return v == "ja" || v == "unsicher" +} + +// EvaluateDatenklasse leitet die Datenklasse aus den B-Antworten ab. +// "Höchste zutreffende Stufe gewinnt" (höherer Rang gewinnt). Trifft +// keine Stufe zu (z. B. wenn versehentlich auch B7 mit "nein" +// beantwortet wurde, obwohl keine andere Kategorie zutrifft — ein +// eigentlich widersprüchlicher Fragebogen-Zustand), wird konservativ +// "intern" angenommen statt "oeffentlich": im Zweifel mehr Schutz, nicht +// weniger. Siehe rules/OPEN.md, Punkt 5. +func EvaluateDatenklasse(regelwerk DatenklasseRegelwerk, antworten Antworten) string { + const fallback = "intern" + var gewinner *DatenklasseStufe + for i := range regelwerk.Stufen { + st := ®elwerk.Stufen[i] + for _, ausloeser := range st.Ausloeser { + if antworten.istJaOderUnsicher(ausloeser) { + if gewinner == nil || st.Rang > gewinner.Rang { + gewinner = st + } + break + } + } + } + if gewinner == nil { + return fallback + } + return gewinner.ID +} + +// variantePasst prüft, ob alle Schlüssel-Werte-Paare einer Variante zu +// den Antworten passen (UND-Verknüpfung innerhalb der Variante). +func variantePasst(variante EinstufungVariante, antworten Antworten) bool { + for key, want := range variante { + got, ok := antworten[key] + if !ok { + return false + } + switch wantVal := want.(type) { + case bool: + gotBool, ok := got.(bool) + if !ok || gotBool != wantVal { + return false + } + case string: + gotStr, ok := got.(string) + if !ok || gotStr != wantVal { + return false + } + default: + return false + } + } + return true +} + +// EvaluateEinstufung leitet die KI-VO-Einstufung aus den C-Antworten ab. +// Prüfreihenfolge: die erste zutreffende Stufe gewinnt (siehe +// Bewertungslogik, K.-o.-Prüfung) — deshalb steht "verboten" in +// rules/kivo_einstufung.yaml an erster Stelle. Liefert einen Fehler nur, +// wenn das Regelwerk selbst keine Auffangregel definiert (sollte durch +// LoadEinstufung bereits verhindert sein). +func EvaluateEinstufung(regelwerk EinstufungRegelwerk, antworten Antworten) (string, error) { + for _, st := range regelwerk.Stufen { + if len(st.Varianten) == 0 { + return st.ID, nil // Auffangregel, z. B. "minimal" + } + for _, variante := range st.Varianten { + if variantePasst(variante, antworten) { + return st.ID, nil + } + } + } + return "", fmt.Errorf("rules: keine einstufung trifft zu und keine auffangregel definiert") +} + +// IstVerboten prüft die K.-o.-Bedingung (Art. 5 KI-VO): bei "verboten" +// erfolgt sofortige Ablehnung, keine Werkzeugsuche. +func IstVerboten(einstufungID string) bool { + return einstufungID == "verboten" +} + +// DeriveAnforderungen leitet aus Datenklasse und Einstufung die Menge +// der Anforderungen ab, die ein Werkzeug erfüllen muss. +func DeriveAnforderungen(regelwerk AnforderungsRegelwerk, datenklasseID, einstufungID string) []string { + var out []string + for _, a := range regelwerk.Anforderungen { + if containsString(a.AusDatenklassen, datenklasseID) || containsString(a.AusEinstufungen, einstufungID) { + out = append(out, a.ID) + } + } + return out +} + +func containsString(list []string, v string) bool { + for _, x := range list { + if x == v { + return true + } + } + return false +} + +// WerkzeugEigenschaften sind die für die harte Filterung relevanten +// Felder eines Katalogeintrags — bewusst ein eigener, schlanker Typ +// statt store.Werkzeug direkt zu verwenden: internal/rules bleibt so +// unabhängig von internal/store und für sich allein testbar. +type WerkzeugEigenschaften struct { + AVVVerfuegbar bool + Verarbeitungsort string + TrainingOptOut bool + TrainingStandard bool + AufbewahrungTage int +} + +// ErfuelltAnforderung prüft, ob ein Werkzeug eine einzelne Anforderung +// erfüllt. Anforderungen, die einen Prozess statt eine technische +// Werkzeug-Eigenschaft betreffen (menschliche Aufsicht, Kennzeichnung, +// DSFA) werden hier nicht hart gefiltert — sie werden als Auflage +// vermerkt (spätere Ausbaustufe), nicht als Ausschlussgrund für das +// Werkzeug selbst. loeschfrist_max_tage hat noch keine fachlich +// festgelegten Tageswerte (rules/OPEN.md, Punkt 4) und wird deshalb +// ebenfalls noch nicht hart gefiltert. +func ErfuelltAnforderung(anforderungID string, w WerkzeugEigenschaften) bool { + switch anforderungID { + case "avv_erforderlich": + return w.AVVVerfuegbar + case "eu_verarbeitung": + return w.Verarbeitungsort == "EU" + case "kein_training_auf_eingabe": + return w.TrainingStandard + default: + return true + } +} + +// AusschlussGrund hält fest, warum ein Werkzeug aussortiert wurde — +// auch aussortierte Werkzeuge werden im Ergebnis gezeigt (siehe +// Bewertungslogik), nie stillschweigend weggelassen. +type AusschlussGrund struct { + WerkzeugID string + NichtErfuellt []string +} + +// WerkzeugKandidat ist ein Katalogeintrag im harten Filter. +type WerkzeugKandidat struct { + ID string + Eigenschaften WerkzeugEigenschaften +} + +// FilterWerkzeuge sortiert Werkzeuge aus, die mindestens eine +// Pflichtanforderung nicht erfüllen, und hält für jedes ausgeschlossene +// Werkzeug fest, welche Anforderungen fehlten. +func FilterWerkzeuge(kandidaten []WerkzeugKandidat, anforderungIDs []string) (zulaessig []string, ausgeschlossen []AusschlussGrund) { + for _, k := range kandidaten { + var fehlend []string + for _, reqID := range anforderungIDs { + if !ErfuelltAnforderung(reqID, k.Eigenschaften) { + fehlend = append(fehlend, reqID) + } + } + if len(fehlend) == 0 { + zulaessig = append(zulaessig, k.ID) + } else { + ausgeschlossen = append(ausgeschlossen, AusschlussGrund{WerkzeugID: k.ID, NichtErfuellt: fehlend}) + } + } + return zulaessig, ausgeschlossen +} + +// String liefert eine lesbare Begründung, z. B. für die spätere +// Ergebnisdarstellung (Schritt 4). +func (a AusschlussGrund) String() string { + return fmt.Sprintf("%s: erfüllt nicht %s", a.WerkzeugID, strings.Join(a.NichtErfuellt, ", ")) +} diff --git a/internal/rules/evaluate_test.go b/internal/rules/evaluate_test.go new file mode 100644 index 0000000..983be87 --- /dev/null +++ b/internal/rules/evaluate_test.go @@ -0,0 +1,208 @@ +package rules_test + +import ( + "testing" + + "github.com/netcell-it/deklarix/internal/rules" +) + +func loadRealRegelwerke(t *testing.T) (rules.DatenklasseRegelwerk, rules.EinstufungRegelwerk, rules.AnforderungsRegelwerk) { + t.Helper() + fsys := realRulesFS(t) + dk, err := rules.LoadDatenklasse(fsys, "datenklasse.yaml") + if err != nil { + t.Fatalf("LoadDatenklasse: %v", err) + } + ei, err := rules.LoadEinstufung(fsys, "kivo_einstufung.yaml") + if err != nil { + t.Fatalf("LoadEinstufung: %v", err) + } + an, err := rules.LoadAnforderungen(fsys, "anforderungen.yaml") + if err != nil { + t.Fatalf("LoadAnforderungen: %v", err) + } + return dk, ei, an +} + +func TestEvaluateDatenklasseOnlyOeffentlich(t *testing.T) { + dk, _, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"b1": "nein", "b2": "nein", "b3": "nein", "b4": "nein", "b5": "nein", "b6": "nein", "b7": "ja"} + got := rules.EvaluateDatenklasse(dk, antworten) + if got != "oeffentlich" { + t.Fatalf("EvaluateDatenklasse = %q, want oeffentlich", got) + } +} + +func TestEvaluateDatenklasseHighestWins(t *testing.T) { + dk, _, _ := loadRealRegelwerke(t) + // b1 (personenbezogen) UND b2 (besondere_kategorie) beide ja -> + // besondere_kategorie hat den hoeheren Rang und muss gewinnen. + antworten := rules.Antworten{"b1": "ja", "b2": "ja", "b7": "nein"} + got := rules.EvaluateDatenklasse(dk, antworten) + if got != "besondere_kategorie" { + t.Fatalf("EvaluateDatenklasse = %q, want besondere_kategorie", got) + } +} + +func TestEvaluateDatenklasseUnsicherZaehltWieJa(t *testing.T) { + dk, _, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"b3": "unsicher"} + got := rules.EvaluateDatenklasse(dk, antworten) + if got != "berufsgeheimnis" { + t.Fatalf("EvaluateDatenklasse = %q, want berufsgeheimnis (unsicher zaehlt wie ja)", got) + } +} + +func TestEvaluateDatenklasseFallsBackToInternWhenNothingMatches(t *testing.T) { + dk, _, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"b1": "nein", "b2": "nein", "b3": "nein", "b4": "nein", "b5": "nein", "b6": "nein", "b7": "nein"} + got := rules.EvaluateDatenklasse(dk, antworten) + if got != "intern" { + t.Fatalf("EvaluateDatenklasse = %q, want intern (konservativer Fallback)", got) + } +} + +func TestEvaluateEinstufungVerboten(t *testing.T) { + _, ei, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"c3": true, "c3_art": "social_scoring"} + got, err := rules.EvaluateEinstufung(ei, antworten) + if err != nil { + t.Fatalf("EvaluateEinstufung: %v", err) + } + if got != "verboten" { + t.Fatalf("EvaluateEinstufung = %q, want verboten", got) + } + if !rules.IstVerboten(got) { + t.Error("IstVerboten sollte true liefern") + } +} + +func TestEvaluateEinstufungHochrisiko(t *testing.T) { + _, ei, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"c2": true, "c2_folge": "kreditwuerdigkeit", "c3": false} + got, err := rules.EvaluateEinstufung(ei, antworten) + if err != nil { + t.Fatalf("EvaluateEinstufung: %v", err) + } + if got != "hochrisiko" { + t.Fatalf("EvaluateEinstufung = %q, want hochrisiko", got) + } +} + +func TestEvaluateEinstufungTransparenzpflicht(t *testing.T) { + _, ei, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"c1": true, "c2": false, "c3": false, "c5": false} + got, err := rules.EvaluateEinstufung(ei, antworten) + if err != nil { + t.Fatalf("EvaluateEinstufung: %v", err) + } + if got != "transparenzpflicht" { + t.Fatalf("EvaluateEinstufung = %q, want transparenzpflicht", got) + } +} + +func TestEvaluateEinstufungMinimalFallback(t *testing.T) { + _, ei, _ := loadRealRegelwerke(t) + antworten := rules.Antworten{"c1": false, "c2": false, "c3": false, "c4": false, "c5": true} + got, err := rules.EvaluateEinstufung(ei, antworten) + if err != nil { + t.Fatalf("EvaluateEinstufung: %v", err) + } + if got != "minimal" { + t.Fatalf("EvaluateEinstufung = %q, want minimal", got) + } + if rules.IstVerboten(got) { + t.Error("IstVerboten sollte fuer minimal false liefern") + } +} + +func TestDeriveAnforderungenPersonenbezogen(t *testing.T) { + _, _, an := loadRealRegelwerke(t) + got := rules.DeriveAnforderungen(an, "personenbezogen", "minimal") + want := map[string]bool{"avv_erforderlich": true, "kein_training_auf_eingabe": true, "loeschfrist_max_tage": true} + gotSet := map[string]bool{} + for _, id := range got { + gotSet[id] = true + } + for id := range want { + if !gotSet[id] { + t.Errorf("expected Anforderung %q for personenbezogen/minimal, got %v", id, got) + } + } + if gotSet["eu_verarbeitung"] { + t.Error("eu_verarbeitung sollte fuer personenbezogen (ohne berufsgeheimnis/besondere_kategorie) nicht ausgeloest werden") + } +} + +func TestDeriveAnforderungenHochrisikoAddsAufsichtUndDsfa(t *testing.T) { + _, _, an := loadRealRegelwerke(t) + got := rules.DeriveAnforderungen(an, "oeffentlich", "hochrisiko") + gotSet := map[string]bool{} + for _, id := range got { + gotSet[id] = true + } + if !gotSet["menschliche_aufsicht"] || !gotSet["dsfa_erforderlich"] { + t.Errorf("expected menschliche_aufsicht und dsfa_erforderlich fuer hochrisiko, got %v", got) + } +} + +func TestErfuelltAnforderungAVV(t *testing.T) { + if rules.ErfuelltAnforderung("avv_erforderlich", rules.WerkzeugEigenschaften{AVVVerfuegbar: false}) { + t.Error("erwartet: nicht erfuellt ohne AVV") + } + if !rules.ErfuelltAnforderung("avv_erforderlich", rules.WerkzeugEigenschaften{AVVVerfuegbar: true}) { + t.Error("erwartet: erfuellt mit AVV") + } +} + +func TestErfuelltAnforderungUnbekannteAnforderungIstUnkritisch(t *testing.T) { + if !rules.ErfuelltAnforderung("menschliche_aufsicht", rules.WerkzeugEigenschaften{}) { + t.Error("Prozess-Anforderungen duerfen ein Werkzeug nicht hart aussortieren") + } +} + +func TestFilterWerkzeugeSortsOutMissingAVV(t *testing.T) { + kandidaten := []rules.WerkzeugKandidat{ + {ID: "mit-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: true, Verarbeitungsort: "EU", TrainingStandard: true}}, + {ID: "ohne-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: false, Verarbeitungsort: "EU", TrainingStandard: true}}, + } + zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, []string{"avv_erforderlich"}) + if len(zulaessig) != 1 || zulaessig[0] != "mit-avv" { + t.Fatalf("zulaessig = %v, want [mit-avv]", zulaessig) + } + if len(ausgeschlossen) != 1 || ausgeschlossen[0].WerkzeugID != "ohne-avv" { + t.Fatalf("ausgeschlossen = %v, want genau ohne-avv", ausgeschlossen) + } + if len(ausgeschlossen[0].NichtErfuellt) != 1 || ausgeschlossen[0].NichtErfuellt[0] != "avv_erforderlich" { + t.Fatalf("NichtErfuellt = %v, want [avv_erforderlich]", ausgeschlossen[0].NichtErfuellt) + } +} + +func TestFilterWerkzeugeNoRequirementsAllowsEverything(t *testing.T) { + kandidaten := []rules.WerkzeugKandidat{{ID: "x"}, {ID: "y"}} + zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, nil) + if len(zulaessig) != 2 || len(ausgeschlossen) != 0 { + t.Fatalf("zulaessig=%v ausgeschlossen=%v, want beide zulaessig", zulaessig, ausgeschlossen) + } +} + +func TestParseAntwortenRoundTrip(t *testing.T) { + raw := []byte(`{"b1":"ja","c2":true,"c2_folge":"bildung"}`) + a, err := rules.ParseAntworten(raw) + if err != nil { + t.Fatalf("ParseAntworten: %v", err) + } + if a["b1"] != "ja" || a["c2_folge"] != "bildung" { + t.Fatalf("ParseAntworten = %+v, unerwartete Werte", a) + } +} + +func TestParseAntwortenEmpty(t *testing.T) { + a, err := rules.ParseAntworten(nil) + if err != nil { + t.Fatalf("ParseAntworten: %v", err) + } + if len(a) != 0 { + t.Fatalf("ParseAntworten(nil) = %+v, want empty", a) + } +} diff --git a/rules/OPEN.md b/rules/OPEN.md index c8d8edb..d3c91bd 100644 --- a/rules/OPEN.md +++ b/rules/OPEN.md @@ -54,4 +54,32 @@ Datenklasse", ohne die konkreten Tageswerte pro Stufe zu nennen. `rules/anforderungen.yaml` listet die Anforderung nur als grundsätzlich anwendbar (welche Datenklassen sie überhaupt auslösen), ohne Zahlenwerte — die eigentlichen Fristen (z. B. "personenbezogen: 90 -Tage") müssen vor Phase 3 fachlich festgelegt werden. +Tage") müssen vor Phase 3 fachlich festgelegt werden. **Auswirkung auf +Schritt 3 (`internal/rules.ErfuelltAnforderung`):** ohne Zahlenwerte +wird diese Anforderung aktuell NICHT hart gegen Werkzeuge gefiltert +(jedes Werkzeug gilt hier als "erfüllt") — sobald Fristen feststehen, +muss die Filterfunktion entsprechend erweitert werden. + +## 5. Fallback, wenn keine Datenklasse zutrifft + +Wenn ein Antrag bei allen B-Fragen "nein" beantwortet — auch bei B7 +("nur allgemein zugängliche/erfundene Inhalte") — trifft laut Tabelle +keine Stufe zu (ein eigentlich widersprüchlicher Zustand: irgendeine +Kategorie sollte immer zutreffen). `internal/rules.EvaluateDatenklasse` +nimmt in diesem Fall konservativ `intern` an, nicht `oeffentlich` — im +Zweifel mehr Schutzanforderungen, nicht weniger. **Nicht fachlich +bestätigt**, nur eine sichere Standardannahme. + +## 6. Welche Anforderungen werden hart gegen ein Werkzeug gefiltert? + +`internal/rules.ErfuelltAnforderung` filtert aktuell hart nur +`avv_erforderlich`, `eu_verarbeitung` und `kein_training_auf_eingabe` — +das sind technische Eigenschaften eines Werkzeugs +(`werkzeug.avv_verfuegbar`, `verarbeitungsort`, `training_standard`). +`menschliche_aufsicht`, `kennzeichnungspflicht` und `dsfa_erforderlich` +betreffen dagegen einen Prozess rund um die Nutzung, nicht das Werkzeug +selbst — ein Werkzeug wird deswegen nicht aussortiert, das wird +stattdessen als Auflage vermerkt (geplant für Schritt 4/5). **Diese +Aufteilung ist eine Annahme von Schritt 3, nicht ausdrücklich in der +Spezifikation so festgelegt** — vor Schritt 4 gegenprüfen, ob das der +gewünschten Produktlogik entspricht.