Keep confirmed detect cues from opening a second type-conflict review.
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Test Suite / pytest-backend (push) Successful in 3m7s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 16s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-10-12 13:28:08 +02:00
parent 011cc9dd81
commit 0277039b07
7 changed files with 33 additions and 7 deletions

View File

@ -438,6 +438,8 @@ def compare_mentions(
protects(student.entity_type) != protects(teacher.entity_type) protects(student.entity_type) != protects(teacher.entity_type)
): ):
counters["diff_type_conflict"] += 1 counters["diff_type_conflict"] += 1
if student.confirmed_cue:
continue
review.append( review.append(
_candidate_from_span( _candidate_from_span(
student, student,

View File

@ -209,10 +209,9 @@ def main() -> None:
json={"body": "Hanna aß mit uns."}, json={"body": "Hanna aß mit uns."},
) )
status_ok(hanna_again, "second Hanna turn") status_ok(hanna_again, "second Hanna turn")
hanna_conflict = hanna_again.json().get("pending_mask_review") or {}
expect( expect(
bool(hanna_conflict.get("id")), not hanna_again.json().get("pending_mask_review"),
"teacher PERSON versus stored not_identity cue stays a visible type conflict", "stored Hanna cue is not re-asked against teacher PERSON",
) )
record_sense(profile_id, "Clarissa", identity=True) 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") wrapped = MentionSpan("Frau Sushi", 15, 25, "PERSON", "identity", "teacher", False, "frau")
expect(same_mention(person, wrapped), "contained Sushi matches Frau Sushi") expect(same_mention(person, wrapped), "contained Sushi matches Frau Sushi")
expect(map_entity_type("person") == "PERSON", "GLiNER person maps to PERSON") 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(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) compact = compact_diff_counters(counters)
dumped = str(compact) dumped = str(compact)
expect("Sushi" not in dumped and "Frau" not in dumped, "compact diff counters carry no plaintext") expect("Sushi" not in dumped and "Frau" not in dumped, "compact diff counters carry no plaintext")

View File

@ -16,6 +16,7 @@ from detect_learning import canonicalize_detect_mode, decision_for_mention
from detect_pipeline import ( from detect_pipeline import (
MentionSpan, MentionSpan,
compact_diff_counters, compact_diff_counters,
compare_mentions,
map_entity_type, map_entity_type,
protects, protects,
same_mention, same_mention,
@ -38,6 +39,16 @@ def main() -> None:
expect(protects("PERSON") and not protects("FOOD"), "PERSON protects, FOOD does not") expect(protects("PERSON") and not protects("FOOD"), "PERSON protects, FOOD does not")
expect(types_equivalent("PERSON", "PLACE"), "similar protectable types are equivalent") expect(types_equivalent("PERSON", "PLACE"), "similar protectable types are equivalent")
expect(not types_equivalent("FOOD", "PERSON"), "FOOD versus PERSON is never 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") inner = MentionSpan("Sushi", 20, 25, "PERSON", "identity", "cue", True, "frau")
outer = MentionSpan("Frau Sushi", 15, 25, "PERSON", "identity", "teacher", False, "frau") outer = MentionSpan("Frau Sushi", 15, 25, "PERSON", "identity", "teacher", False, "frau")
expect(same_mention(inner, outer), "contained span is the same mention") expect(same_mention(inner, outer), "contained span is the same mention")

View File

@ -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-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. **Nicht:** Gateway abschalten, Detect-Treffer auto-speichern, `Sushi_`/`Sushi+` im Nutzertext, Pattern-Wortliste als Wahrheit.
--- ---

View File

@ -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-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) ## 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. **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.

View File

@ -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-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`. 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 ## 10. Offene Fragen

View File

@ -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). - **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. - **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. - **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. - **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. - **Mini-LLM** nur wenn beide Sinne schon existieren, auch in Stufe 1.
- **Geltungsbereich:** alle persönlichen Detect-Aufrufe (Security-Gateway), einschließlich Journalentwurf. - **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. 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. 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. 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. 7. **Diagnose:** Zähler ohne Klartext, keine Langeweile-Kennzahl.
8. **Mini-LLM** nur bei schon beiden Sinnen, auch in Stufe 1. 8. **Mini-LLM** nur bei schon beiden Sinnen, auch in Stufe 1.
9. **Alle persönlichen Detect-Aufrufe**, einschließlich Journalentwurf. 9. **Alle persönlichen Detect-Aufrufe**, einschließlich Journalentwurf.