From 0277039b071ef0df1af7762f5b323539a30fc71c Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 12 Oct 2026 13:28:08 +0200 Subject: [PATCH] Keep confirmed detect cues from opening a second type-conflict review. Co-authored-by: Cursor --- backend/detect_pipeline.py | 2 ++ backend/tests/test_detect_learning.py | 17 ++++++++++++----- backend/tests/test_detect_pipeline.py | 11 +++++++++++ docs/architecture/functional/guardrails.md | 2 ++ .../architecture/technical/admin_diagnostics.md | 2 ++ docs/architecture/technical/privacy_gateway.md | 2 ++ .../detect_self_learning_pipeline.md | 4 ++-- 7 files changed, 33 insertions(+), 7 deletions(-) diff --git a/backend/detect_pipeline.py b/backend/detect_pipeline.py index 748f231..5401eff 100644 --- a/backend/detect_pipeline.py +++ b/backend/detect_pipeline.py @@ -438,6 +438,8 @@ def compare_mentions( protects(student.entity_type) != protects(teacher.entity_type) ): counters["diff_type_conflict"] += 1 + if student.confirmed_cue: + continue review.append( _candidate_from_span( student, diff --git a/backend/tests/test_detect_learning.py b/backend/tests/test_detect_learning.py index ac9b625..65e8863 100644 --- a/backend/tests/test_detect_learning.py +++ b/backend/tests/test_detect_learning.py @@ -209,10 +209,9 @@ def main() -> None: json={"body": "Hanna aß mit uns."}, ) status_ok(hanna_again, "second Hanna turn") - hanna_conflict = hanna_again.json().get("pending_mask_review") or {} expect( - bool(hanna_conflict.get("id")), - "teacher PERSON versus stored not_identity cue stays a visible type conflict", + not hanna_again.json().get("pending_mask_review"), + "stored Hanna cue is not re-asked against teacher PERSON", ) record_sense(profile_id, "Clarissa", identity=True) @@ -382,9 +381,17 @@ def main() -> None: wrapped = MentionSpan("Frau Sushi", 15, 25, "PERSON", "identity", "teacher", False, "frau") expect(same_mention(person, wrapped), "contained Sushi matches Frau Sushi") expect(map_entity_type("person") == "PERSON", "GLiNER person maps to PERSON") - review, counters = compare_mentions(profile_id, dish_line, [person], [food], dish_line) + guessed = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "candidate", False, "aß") + review, counters = compare_mentions(profile_id, dish_line, [guessed], [food], dish_line) expect(counters["diff_type_conflict"] >= 1, "FOOD versus PERSON is a type conflict") - expect(any(item.get("reason") == "type_conflict" for item in review), "type conflict is offered") + expect(any(item.get("reason") == "type_conflict" for item in review), "unconfirmed type conflict is offered") + confirmed_dish = MentionSpan("Sushi", 20, 25, "FOOD", "not_identity", "cue", True, "rohan") + teacher_person = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "teacher", False, "rohan") + kept, kept_counters = compare_mentions( + profile_id, dish_line, [confirmed_dish], [teacher_person], dish_line + ) + expect(kept_counters["diff_type_conflict"] >= 1, "confirmed cue still counts the type conflict") + expect(not kept, "confirmed cue is not re-asked on type conflict") compact = compact_diff_counters(counters) dumped = str(compact) expect("Sushi" not in dumped and "Frau" not in dumped, "compact diff counters carry no plaintext") diff --git a/backend/tests/test_detect_pipeline.py b/backend/tests/test_detect_pipeline.py index d4a102a..57651d5 100644 --- a/backend/tests/test_detect_pipeline.py +++ b/backend/tests/test_detect_pipeline.py @@ -16,6 +16,7 @@ from detect_learning import canonicalize_detect_mode, decision_for_mention from detect_pipeline import ( MentionSpan, compact_diff_counters, + compare_mentions, map_entity_type, protects, same_mention, @@ -38,6 +39,16 @@ def main() -> None: expect(protects("PERSON") and not protects("FOOD"), "PERSON protects, FOOD does not") expect(types_equivalent("PERSON", "PLACE"), "similar protectable types are equivalent") expect(not types_equivalent("FOOD", "PERSON"), "FOOD versus PERSON is never equivalent") + guessed = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "candidate", False, "aß") + teacher_food = MentionSpan("Sushi", 20, 25, "FOOD", "not_identity", "teacher", False, "aß") + offered, offered_counters = compare_mentions("p", "Ich aß Sushi.", [guessed], [teacher_food]) + expect(offered_counters["diff_type_conflict"] == 1, "unconfirmed FOOD versus PERSON is counted") + expect(any(item.get("reason") == "type_conflict" for item in offered), "unconfirmed type conflict is offered") + confirmed = MentionSpan("Sushi", 20, 25, "FOOD", "not_identity", "cue", True, "rohan") + teacher_person = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "teacher", False, "rohan") + kept, kept_counters = compare_mentions("p", "Rohan Sushi essen", [confirmed], [teacher_person]) + expect(kept_counters["diff_type_conflict"] == 1, "confirmed cue still counts FOOD versus PERSON") + expect(not kept, "confirmed cue is not re-asked on type conflict") inner = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "cue", True, "frau") outer = MentionSpan("Frau Sushi", 15, 25, "PERSON", "identity", "teacher", False, "frau") expect(same_mention(inner, outer), "contained span is the same mention") diff --git a/docs/architecture/functional/guardrails.md b/docs/architecture/functional/guardrails.md index 492a06d..158d479 100644 --- a/docs/architecture/functional/guardrails.md +++ b/docs/architecture/functional/guardrails.md @@ -727,6 +727,8 @@ Additiv. Technische Abbildung: `../technical/privacy_gateway.md` §9.7. **Additiv 2026-09-11 (Selbstlern-Pipeline, §6 geschlossen):** Start ist Lernmodus Stufe 1: Schüler (Cues, Verwandtschaft, Homonym-Mini-LLM nur bei beiden Sinnen, GLiNER) parallel zum Lehrer auf dem unveränderten Nutzersatz. Admin: Zielpfad, Lernen Stufe 1/2/manuell, Bypass (alter Voll-Detect, mittelfristig weg). Default nach Umbau: Stufe 1. Neuer Cue bei nur Identity-Hits bleibt sichtbar (Sushi-Tag-21). Cue gilt pro Vorkommen. Lehrer-Miss eines bestätigten Cues ohne Meldung verlernt nicht. FOOD gegen PERSON ist immer Diff. Diagnose nur Zähler ohne Klartext. Gilt für alle persönlichen Detect-Aufrufe einschließlich Journalentwurf. GLiNER bleibt fail-soft im Lernmodus (dann manuelle Review); außerhalb Lernmodus Bestätigung vor öffentlichem Klartext-Egress. Gateway nicht abschaltbar. +**Additiv 2026-10-12:** FOOD gegen PERSON bleibt ein Typkonflikt im Zähler. Ein **bereits bestätigter** Cue wird dabei nicht erneut gefragt und nicht still geändert — derselbe lokale Kontext wie am 2026-09-10. Unbestätigte Nennungen mit abweichendem Schutz bleiben im Popup. + **Nicht:** Gateway abschalten, Detect-Treffer auto-speichern, `Sushi_`/`Sushi+` im Nutzertext, Pattern-Wortliste als Wahrheit. --- diff --git a/docs/architecture/technical/admin_diagnostics.md b/docs/architecture/technical/admin_diagnostics.md index 0ac285d..76d68a9 100644 --- a/docs/architecture/technical/admin_diagnostics.md +++ b/docs/architecture/technical/admin_diagnostics.md @@ -61,6 +61,8 @@ Nicht übernehmen: Mitai-Admin für Körpertarife, Coupons, Training Types als K **Additiv 2026-09-11 (Selbstlern-Pipeline):** `PUT /api/admin/providers/detect-mode` nimmt `student` | `learning_full` | `learning_reduced` | `learning_manual` | `bypass` (Aliase `learning`, `semantic`). Compact-Diagnose darf Diff-Zähler ohne Klartext zeigen, keine Langeweile-Kennzahl. Stufe wechselt nur der Admin. Journalentwurf nutzt dieselbe Bestätigungsfläche. +**Additiv 2026-10-12:** `diff_type_conflict` zählt auch, wenn ein bestätigter Cue dem Lehrer widerspricht. Das Popup bleibt dann zu; der Cue ändert sich nicht. + ## 4.2 Implementierungsstand (Dialog-Testspur) **Additiv 2026-08-26:** Lokale Identitätsregistry unter `/admin/identities`. Detect-Vorschläge sind unbestätigt. Compact-Diagnose enthält Detect-Abdeckung, Chunks, Kosten und Laufzeit, aber keine Labels. diff --git a/docs/architecture/technical/privacy_gateway.md b/docs/architecture/technical/privacy_gateway.md index 4e7cd7a..5220032 100644 --- a/docs/architecture/technical/privacy_gateway.md +++ b/docs/architecture/technical/privacy_gateway.md @@ -275,6 +275,8 @@ Es gibt keine separat zu pflegende Doppeldeutigkeitsliste. Mehrdeutigkeit entste **Additiv 2026-09-11 (Selbstlern-Pipeline, Umsetzung nach §6):** `detect_operating_mode` ist `student` | `learning_full` | `learning_reduced` | `learning_manual` | `bypass`. Aliase: `learning` → `learning_full`, `semantic` → `bypass`. Default im Produkt: `learning_full`. Schülerpfad in `detect_pipeline.run_student_path`. Lehrer Stufe 1 = unveränderter Nutzersatz, Stufe 2 = ohne cue-sichere Oberflächenformen. Vergleich nur User-Spans; Compact-Zähler `diff_agree`, `diff_only_student`, `diff_only_teacher`, `diff_type_conflict`, `diff_new_identity`, `diff_cue_reinforced` ohne Labels. Journalentwurf pausiert über `POST /api/journal/days/{id}/generate/review` und dieselbe Review-UI. Bypass bleibt der alte Detect auf dem gerenderten Prompt. +**Additiv 2026-10-12:** `compare_mentions` zählt `diff_type_conflict` auch bei bestätigtem Cue (FOOD gegen PERSON), öffnet dafür aber kein zweites Popup. Cue bleibt. Unbestätigte Typkonflikte bleiben Review-Kandidaten. + Nach der Bestätigung läuft Generate mit den geprüften Mappings (`precomputed_learning_review`), ohne zweiten Detect-Pass. Compact-Diagnose enthält weiterhin keine Klartextlabels. Tests: `backend/tests/test_detect_learning.py`. ## 10. Offene Fragen diff --git a/docs/work_orders/detect_self_learning_pipeline.md b/docs/work_orders/detect_self_learning_pipeline.md index 6d16652..776c4df 100644 --- a/docs/work_orders/detect_self_learning_pipeline.md +++ b/docs/work_orders/detect_self_learning_pipeline.md @@ -46,7 +46,7 @@ Leitbeispiel: Lange Zeit bedeutet „Sushi“ die Partnerin. Am Tag 21 ist „Su - **Lehrer weg:** alle Schüler-/GLiNER-Funde zur Bestätigung (gleicher Weg wie manuell). - **GLiNER weg:** im Lernmodus manuelle Review nach lokaler Vorauswahl. Außerhalb Lernmodus: Nutzer bestätigt Klartext-Egress an das öffentliche LLM. - **Verwandtschaft** bleibt eigener lokaler Schüler-Schritt vor GLiNER. -- **Vergleich:** gleiche Nennung = gleicher Wortlaut an derselben Stelle oder eine Spanne vollständig in der anderen. Typen `person→PERSON` usw.; ähnliche schützenswerte Typen gleich; FOOD gegen PERSON immer Diff. Nur bei abweichendem Schutz fragen; gleicher Schutz, anderer Zuschnitt → längere Spanne. Nutzer entscheidet Speise/Person; Cue nicht still ändern; Lehrer-Miss ohne Meldung → kein Popup, Cue bleibt. +- **Vergleich:** gleiche Nennung = gleicher Wortlaut an derselben Stelle oder eine Spanne vollständig in der anderen. Typen `person→PERSON` usw.; ähnliche schützenswerte Typen gleich; FOOD gegen PERSON immer Diff. Nur bei abweichendem Schutz fragen; gleicher Schutz, anderer Zuschnitt → längere Spanne. Nutzer entscheidet Speise/Person; Cue nicht still ändern; Lehrer-Miss ohne Meldung → kein Popup, Cue bleibt. **Additiv 2026-10-12:** Bestätigter Cue bei Typkonflikt: zählen, nicht erneut fragen, Cue bleibt. Unbestätigter Typkonflikt bleibt Popup. - **Diagnose:** Zähler ohne Klartext (Einigkeit, nur Schüler, nur Lehrer, Typkonflikt, neue Identität, Cue verstärkt). Keine Langeweile-Kennzahl; Stufe nur per Schalter. - **Mini-LLM** nur wenn beide Sinne schon existieren, auch in Stufe 1. - **Geltungsbereich:** alle persönlichen Detect-Aufrufe (Security-Gateway), einschließlich Journalentwurf. @@ -124,7 +124,7 @@ Nicht in diesem Slice: Fine-Tune, neues NER-Repo, Wortlisten-Editor, Shared-Iden 3. **Stufe 2 vollständig** im ersten Schnitt. 4. **Lehrer weg** → manuelle Review aller GLiNER-/Schülerfunde. **GLiNER weg** → im Lernmodus manuell nach Vorauswahl; sonst Bestätigung vor öffentlichem Klartext-Egress. 5. **Verwandtschaft bleibt** eigener lokaler Schritt vor GLiNER. -6. **Vergleich:** enthaltene Spanne / gleicher Wortlaut; Mapping inkl. FOOD≠PERSON; nur abweichender Schutz fragt; Cue nicht still ändern; Lehrer-Miss ohne Meldung kein Popup. +6. **Vergleich:** enthaltene Spanne / gleicher Wortlaut; Mapping inkl. FOOD≠PERSON; nur abweichender Schutz fragt; Cue nicht still ändern; Lehrer-Miss ohne Meldung kein Popup. Bestätigter Cue bei Typkonflikt zählt, fragt aber nicht erneut. 7. **Diagnose:** Zähler ohne Klartext, keine Langeweile-Kennzahl. 8. **Mini-LLM** nur bei schon beiden Sinnen, auch in Stufe 1. 9. **Alle persönlichen Detect-Aufrufe**, einschließlich Journalentwurf.