fix: Datenschutz-Scrub in Tests/Kommentaren + Spec-Nachtrag Vorschlags-Algorithmus

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 21:59:29 +02:00
parent 7ca64c91fe
commit 8eee62ee19
3 changed files with 51 additions and 25 deletions

View File

@@ -79,3 +79,26 @@ Kandidatenliste berichtet (nur Chat, kein Commit). Fable-Gate je Task.
**Außerhalb des Scopes:** halbjährliche Rhythmen (nicht im Datenmodell), **Außerhalb des Scopes:** halbjährliche Rhythmen (nicht im Datenmodell),
automatische Übernahme ohne Nutzer-Klick, Einnahmen-Prognose des automatische Übernahme ohne Nutzer-Klick, Einnahmen-Prognose des
Geschäftskontos. Geschäftskontos.
## Nachtrag (nach Live-Release-Gate, gleiche Session)
Das erste Live-Gate scheiterte an einem Duplikat: ein kuratierter
„variabel"-Fixposten unter Alias-Namen des Anbieters wurde vom
Bestandsabgleich (a)/(b) nicht erkannt. Daraus zwei Ergänzungen:
- **Bestandsabgleich-Regel (c) Token-Match:** Vorschlag entfällt auch, wenn
ein Fixposten mit gleichem Rhythmus, Fälligkeitstag ±2 und mindestens
einem gemeinsamen Namens-Token (≥ 5 Zeichen, normalisiert, Split an
Nicht-Alphanumerik) existiert.
- **Volatilitäts-Hinweis:** Wurde die neueste Buchung einer Empfänger-Gruppe
durch den Betrags-Cluster-Split abgetrennt UND gehört sie zu keiner
anderen qualifizierten Serie der Gruppe, erhält der Vorschlag den Zusatz
„Beträge schwanken stark letzte Buchung weicht ab" (keine
Unterdrückung; die Ausnahme verhindert False-Positives bei parallelen
Verträgen desselben Anbieters).
Bewiesene Pipeline-Eigenschaft (bindend fürs Verständnis): der
Umfirmierungs-Merge kann die Vorschlagsanzahl nie ändern (Aktiv-Check/
Fenster erledigen das allein); sein Nutzen ist Kategorie-/Historien-
Kontinuität. Nach einer Umfirmierung entsteht eine Vorschlags-Lücke, bis
der neue Name selbst die Mindestbelege erreicht.

View File

@@ -70,15 +70,17 @@ MERGE_GAP_MAX_NUM, MERGE_GAP_MAX_DEN = 16, 10 # 1.6
MERGE_DUE_DAY_TOL = 3 # Schritt 6: Faelligkeitstag-Toleranz in Tagen MERGE_DUE_DAY_TOL = 3 # Schritt 6: Faelligkeitstag-Toleranz in Tagen
BESTAND_DUE_DAY_TOL = 2 # Schritt 8: Faelligkeitstag-Toleranz in Tagen BESTAND_DUE_DAY_TOL = 2 # Schritt 8: Faelligkeitstag-Toleranz in Tagen
# Bestandsabgleich, Token-Match (Live-Gate A9-Fund, Nachtrag 4): kuratierte # Bestandsabgleich, Token-Match (Live-Gate-Fund, Nachtrag 4): kuratierte
# Fixposten tragen haeufig einen Alias-/Variabel-Namen, der weder Substring # Fixposten tragen haeufig einen Alias-/Variabel-Namen, der weder Substring
# noch betragsaehnlich zum automatisch erkannten Vorschlag ist (Beispiel aus # noch betragsaehnlich zum automatisch erkannten Vorschlag ist (Muster:
# der echten Datenbasis: Fixposten "Mastercard-Abrechnung Volksbank # ein Sammel-Fixposten fuer eine Kreditkartenabrechnung mit variablem Betrag
# (variabel, letzter Wert)" vs. erkannter Vorschlag "Volksbank Ulm-Biberach # unter einem Alias-Namen des Anbieters deckt den vom Algorithmus erkannten
# eG" - Betrag weicht um >10% ab, kein Substring-Treffer). Ein gemeinsames, # Vorschlag desselben Anbieters unter seinem regulaeren Empfaenger-Namen
# hinreichend spezifisches Namens-Token (>=5 Zeichen, um generische Woerter # nicht ab, weil weder Substring noch Betrags-Toleranz greifen). Ein
# wie "Bank" nicht faelschlich matchen zu lassen) bei gleichem Rhythmus und # gemeinsames, hinreichend spezifisches Namens-Token (>=5 Zeichen, um
# nahem Faelligkeitstag gilt als ausreichendes Indiz fuer denselben Fixposten. # generische Woerter wie "Bank" nicht faelschlich matchen zu lassen) bei
# gleichem Rhythmus und nahem Faelligkeitstag gilt als ausreichendes Indiz
# fuer denselben Fixposten.
TOKEN_MIN_LEN = 5 TOKEN_MIN_LEN = 5
# Volatilitaets-Hinweis (Live-Gate A9-Fund, Nachtrag 4): wenn der # Volatilitaets-Hinweis (Live-Gate A9-Fund, Nachtrag 4): wenn der
@@ -239,10 +241,10 @@ def _covered_by_existing(cand_name: str, cand_amount: Decimal, rhythm: str, due_
(c) Token-Match: gleicher Rhythmus, Faelligkeitstag-Differenz <= 2 UND (c) Token-Match: gleicher Rhythmus, Faelligkeitstag-Differenz <= 2 UND
mindestens ein gemeinsames Namens-Token (>=5 Zeichen) - faengt mindestens ein gemeinsames Namens-Token (>=5 Zeichen) - faengt
kuratierte Alias-/Variabel-Fixposten, deren Name UND Betrag stark kuratierte Alias-/Variabel-Fixposten, deren Name UND Betrag stark
vom automatisch erkannten Vorschlag abweichen (Live-Gate A9-Fund: vom automatisch erkannten Vorschlag abweichen (Live-Gate-Fund: ein
Fixposten "Mastercard-Abrechnung Volksbank (variabel, letzter Sammel-Fixposten unter Alias-Namen des Anbieters deckt den
Wert)" deckt den Vorschlag "Volksbank Ulm-Biberach eG" ab, obwohl automatisch erkannten Vorschlag desselben Anbieters unter seinem
weder (a) noch (b) greifen).""" regulaeren Empfaenger-Namen ab, obwohl weder (a) noch (b) greifen)."""
cand_norm = _norm(cand_name) cand_norm = _norm(cand_name)
cand_tokens = _tokens(cand_name) cand_tokens = _tokens(cand_name)
for item in existing: for item in existing:

View File

@@ -329,25 +329,26 @@ def test_suggest_umfirmierung_merge_verschiebt_kategorie_mehrheit(db):
assert out[0]["category_id"] == cat_a.id assert out[0]["category_id"] == cat_a.id
# ------------------------------------------------- Nachtrag 4 (A9-Live-Gate-Fund) # ------------------------------------------------- Nachtrag 4 (Live-Gate-Fund)
# Echter Fall aus dem Live-Gate: Fixposten 44 "Mastercard-Abrechnung Volksbank # Live-Gate-Fund (Muster, keine echten Kontodaten - Namen/Betraege hier rein
# (variabel, letzter Wert)" (monthly, due_day 7, -296.07) deckte den Vorschlag # synthetisch): ein kuratiertes Sammel-Fixposten unter Alias-Namen des
# "Volksbank Ulm-Biberach eG" (monthly, due_day 5, -584.43) nicht ab, weil # Anbieters ("Kreditkarten-Abrechnung ... (variabel)") deckte den vom
# weder Substring- noch Betrags-Toleranz-Regel griffen. Betraege/Namen hier # Algorithmus erkannten Vorschlag desselben Anbieters unter dessen
# synthetisch nachgebildet (keine echten Kontodaten, siehe CLAUDE.md). # regulaerem Empfaenger-Namen nicht ab, weil weder Substring- noch
# Betrags-Toleranz-Regel griffen.
def test_suggest_alias_recurring_item_token_match(db): def test_suggest_alias_recurring_item_token_match(db):
acc = _acc(db) acc = _acc(db)
db.add(RecurringItem(name="Mastercard-Abrechnung Volksbank (variabel, letzter Wert)", db.add(RecurringItem(name="Kreditkarten-Abrechnung Musterbank (variabel)",
amount=Decimal("-296.07"), rhythm="monthly", due_day=7)) amount=Decimal("-250.00"), rhythm="monthly", due_day=7))
for d, a in [(date(2026, 4, 5), "-560.00"), (date(2026, 5, 5), "-580.00"), for d, a in [(date(2026, 4, 5), "-560.00"), (date(2026, 5, 5), "-575.00"),
(date(2026, 6, 5), "-590.00"), (date(2026, 7, 5), "-580.00")]: (date(2026, 6, 5), "-590.00"), (date(2026, 7, 5), "-575.00")]:
_tx(db, acc, d, a, "Volksbank Ulm-Biberach eG") _tx(db, acc, d, a, "Musterbank Neustadt eG")
db.commit() db.commit()
# Substring-Match (a) schlaegt fehl (kein Teilstring gemeinsam), Betrags- # Substring-Match (a) schlaegt fehl (kein Teilstring gemeinsam), Betrags-
# Toleranz (b) auch (-580 vs. -296.07, >10%) - erst der Token-Match (c) # Toleranz (b) auch (-575 vs. -250.00, >10%) - erst der Token-Match (c)
# ueber das gemeinsame Token "volksbank" (Rhythmus gleich, due_day 5 vs. 7 # ueber das gemeinsame Token "musterbank" (Rhythmus gleich, due_day 5 vs. 7
# -> Differenz 2 <= 2) deckt den Vorschlag ab. # -> Differenz 2 <= 2) deckt den Vorschlag ab.
assert suggest_recurring(db, today=TODAY) == [] assert suggest_recurring(db, today=TODAY) == []