fix(tickets): 16 Tickets der Züchterin — Genetik, Importer, Daten, Akte
Genetik-Engine (bde4ec70, f89e95ad, 2322c2a8):
- Saphir/Platin/Platin-Hell unterscheiden sich NUR in der C-Zygotie und waren im
Katalog identisch (locusToken reduziert auf das dominante Allel) → Saphir war
unerreichbar. Token darf jetzt ein exaktes, ungeordnetes Allelpaar "x/y"
verlangen: Platin C/C, Saphir C/cchm, Platin-Hell C/ch. TS- und Python-Mirror
identisch, Seeds regeneriert, Migration ReseedColorVarietiesGen6Saphir.
- c[hm] wird als Alias auf c[chm] normalisiert (Tippfehler in zwei Charts) —
vorher war Jays Gencode unparsebar und ergab "Zobel-Hell".
- 6 Tiere wechseln den errechneten Farbschlag (5x Platin→Saphir, Jay→Zobel).
Importer-Logik:
- renameTo-Notiz-Sweep: der alte Name blieb in Wurf-Notizen stehen ("Blacky +
Kruke") — jetzt wortgenau ersetzt, mit vier Guards gegen Kollateralschaden.
- Neuer Resolution-Schlüssel goHomeDate (autoritativ, zieht GivenAway nach) und
spottingType; Gencode-Overrides gewinnen jetzt bei präzisem Match (vorher
wurden Entscheidungen stumm verschluckt, z. B. Eliza und Chris).
- parse_date verwirft implausible Jahre (JackJack hatte 1310-05-13).
- extract.py erkennt Stammbaum-Blöcke auch ohne Stern vor dem Geburtsdatum
(Bijou bekam dadurch den Nachbar-Ast als Mutter).
- Backend-Ingest überträgt SpottingType (nur bei Payload-Wert).
Daten (conflict-decisions.json, re-ingest-stabil): Kuke-Merge + Zuchtname,
Jamie- und Sakura-Dubletten, Phantom-Tier "Unbekannt", Eltern von Kathlin,
Fast Boy/Ziwa und dem Q4-/TS-Wurf, Eliza-Gencode, Merle-Abgabe, JackJack.
Frontend (24522f5f): Abgabedatum steht in der Tier-Akte jetzt direkt unter dem
Abnehmer und hängt nicht mehr am Status.
Tests: +25 Checks test_merge_resolve, +7 test_genotype, +8 genetics.test.ts,
neuer Extract- und Ingest-Test, e2e GOHOME-ROW. Alles grün.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -537,6 +537,169 @@ check("excludeContractMatch: control (no exclude) -> receiver IS set",
|
||||
_excl_gerbils2[0].get("ReceiverContactId") is not None)
|
||||
|
||||
|
||||
# ── parse_date: Plausibilitaets-Guard (Ticket 24522f5f, JackJack) ─────────────
|
||||
# Die Wurfchronik-Transkription baute aus „13.5.10" das Datum „1310-05-13" — solche
|
||||
# unmoeglichen Jahre muessen verworfen werden, ohne die 19xx→20xx-Normalisierung
|
||||
# („13.5.10" → 2010-05-13) zu beschaedigen.
|
||||
check("parse_date: implausibles Jahr 1310 (ISO) -> None",
|
||||
m.parse_date("1310-05-13") is None)
|
||||
check("parse_date: implausibles Jahr 1310 (DD.MM.YYYY) -> None",
|
||||
m.parse_date("13.05.1310") is None)
|
||||
check("parse_date: 2-stelliges Jahr bleibt intakt ('13.5.10' -> 2010-05-13)",
|
||||
m.parse_date("13.5.10") == "2010-05-13")
|
||||
check("parse_date: 19xx wird weiter auf 20xx normalisiert",
|
||||
m.parse_date("22.12.1924") == "2024-12-22")
|
||||
check("parse_date: regulaeres Datum unveraendert",
|
||||
m.parse_date("22.12.2024") == "2024-12-22")
|
||||
check("parse_date: fernes Zukunftsjahr -> None",
|
||||
m.parse_date("2999-01-01") is None)
|
||||
|
||||
|
||||
# ── renameTo-Notiz-Sweep (Tickets ea803c3d/24006ccd, Kruke→Kuke) ──────────────
|
||||
# `renameTo` korrigiert nur das Namensfeld; der alte Name blieb in den aus der
|
||||
# Wurfchronik uebernommenen Freitext-Notizen stehen. Der finale Sweep ersetzt ihn
|
||||
# wortgenau (an Wortgrenzen) durch den RUFNAMEN des neuen Namens.
|
||||
_sw_gerbils = [
|
||||
{"Name": "Kuke von Marion Knoss", "_pre_rename_name": "Kruke",
|
||||
"Notes": "Krukelinde bleibt; Kruke wird ersetzt"},
|
||||
{"Name": "Blacky", "Notes": "Vater von Kruke-Kindern"},
|
||||
]
|
||||
_sw_litters = [
|
||||
{"Notes": "Blacky + Kruke v. 21.09.2010; WS=4"},
|
||||
{"Notes": None},
|
||||
{"Notes": "Eltern: Blacky + Bonnie"},
|
||||
]
|
||||
_sw_n = m.apply_rename_note_sweep(_sw_gerbils, _sw_litters)
|
||||
check("Notiz-Sweep: Wurf-Notiz traegt den neuen Rufnamen",
|
||||
_sw_litters[0]["Notes"] == "Blacky + Kuke v. 21.09.2010; WS=4")
|
||||
check("Notiz-Sweep: Rufname (nicht der volle Zuchtname) wird eingesetzt",
|
||||
"von Marion Knoss" not in _sw_litters[0]["Notes"])
|
||||
check("Notiz-Sweep: Teilwort 'Krukelinde' bleibt unangetastet",
|
||||
_sw_gerbils[0]["Notes"] == "Krukelinde bleibt; Kuke wird ersetzt")
|
||||
check("Notiz-Sweep: auch Gerbil-Notizen anderer Tiere werden gefixt",
|
||||
_sw_gerbils[1]["Notes"] == "Vater von Kuke-Kindern")
|
||||
check("Notiz-Sweep: unbeteiligte Notizen/None bleiben unveraendert",
|
||||
_sw_litters[1]["Notes"] is None
|
||||
and _sw_litters[2]["Notes"] == "Eltern: Blacky + Bonnie")
|
||||
check("Notiz-Sweep: Zaehler = Zahl der geaenderten Notizen", _sw_n == 3)
|
||||
# Kein _pre_rename_name → kein Sweep.
|
||||
_sw_none = [{"Name": "Kuke", "Notes": "Kruke stand hier nie zur Debatte"}]
|
||||
check("Notiz-Sweep: ohne renameTo passiert nichts",
|
||||
m.apply_rename_note_sweep(_sw_none, []) == 0)
|
||||
|
||||
# Guards gegen Kollateralschaden (alle vier im ersten Probelauf real aufgetreten):
|
||||
# (1) Zu kurzer Quellname („M") darf keine Abkuerzungen im Freitext treffen.
|
||||
_sw_short = [{"Name": "JD", "_pre_rename_name": "M",
|
||||
"Notes": "Andreas B.; 60g; M: 17.3.19"},
|
||||
{"Name": "Milly", "Notes": "Abgabe an Natascha M."}]
|
||||
check("Notiz-Sweep: Quellname < 4 Zeichen wird ignoriert",
|
||||
m.apply_rename_note_sweep(_sw_short, []) == 0
|
||||
and _sw_short[1]["Notes"] == "Abgabe an Natascha M.")
|
||||
# (2) Mehrwortige Phrase (Zucht-Sentinel als Quellname) wird ignoriert.
|
||||
_sw_phrase = [{"Name": "Namenlose Tochter von Unity",
|
||||
"_pre_rename_name": "Zucht der Kleinen Chaoten",
|
||||
"Notes": "Herkunft: Zucht der Kleinen Chaoten"}]
|
||||
check("Notiz-Sweep: mehrwortige Phrase als Quellname wird ignoriert",
|
||||
m.apply_rename_note_sweep(_sw_phrase, []) == 0)
|
||||
# (3) Alter Name steckt im neuen Rufnamen → keine Verdopplung des Zusatzes.
|
||||
_sw_dbl = [{"Name": "Missy gen. Krümmel", "_pre_rename_name": "Missy",
|
||||
"Notes": "Kinder: Xaki, Rover, Missy gen. Krümmel"}]
|
||||
check("Notiz-Sweep: kein Verdoppeln, wenn der alte Name im neuen Rufnamen steckt",
|
||||
m.apply_rename_note_sweep(_sw_dbl, []) == 0
|
||||
and _sw_dbl[0]["Notes"] == "Kinder: Xaki, Rover, Missy gen. Krümmel")
|
||||
# (4) Notizen, die die Umbenennung selbst dokumentieren, behalten den alten Namen.
|
||||
_sw_doc = [{"Name": "Gale von den Kleinen Chaoten", "_pre_rename_name": "Drake Jr",
|
||||
"Notes": "Hieß ursprünglich Drake Jr, wurde später in Gale umbenannt."}]
|
||||
_sw_doc_litters = [{"Notes": "Parents: Jacky + Drake Jr"}]
|
||||
_sw_doc_n = m.apply_rename_note_sweep(_sw_doc, _sw_doc_litters)
|
||||
check("Notiz-Sweep: Umbenennungs-Dokumentation bleibt erhalten",
|
||||
_sw_doc[0]["Notes"] == "Hieß ursprünglich Drake Jr, wurde später in Gale umbenannt.")
|
||||
check("Notiz-Sweep: normale Notiz desselben Paares wird trotzdem gefixt",
|
||||
_sw_doc_litters[0]["Notes"] == "Parents: Jacky + Gale" and _sw_doc_n == 1)
|
||||
|
||||
|
||||
# ── Decision-Overrides: goHomeDate / genotype (autoritativ) / spottingType ────
|
||||
# Tickets 24522f5f (JackJack: kaputtes Abgabedatum), 7037f5d8 (Merle: Abgabe zieht
|
||||
# Status GivenAway nach), a547be62 (Eliza/Chris: Gencode + Scheckungsart).
|
||||
def _ovr(gerbils, decisions):
|
||||
return m.apply_decision_overrides(gerbils, decisions, {}, None)
|
||||
|
||||
|
||||
# (1) goHomeDate ist AUTORITATIV — ueberschreibt auch einen (kaputten) Bestandswert.
|
||||
_g_jj = {"Id": "1", "ExternalRef": "wc-0002", "Name": "JackSack",
|
||||
"DateOfBirth": "2010-02-18", "Status": "Deceased",
|
||||
"GoHomeDate": "1310-05-13"}
|
||||
_ovr([_g_jj], [{"externalRef": "wc-0002", "goHomeDate": "13.05.2010"}])
|
||||
check("goHomeDate: kaputtes Abgabedatum wird ueberschrieben",
|
||||
_g_jj["GoHomeDate"] == "2010-05-13")
|
||||
check("goHomeDate: Status Deceased bleibt Deceased",
|
||||
_g_jj["Status"] == "Deceased")
|
||||
|
||||
# (2) goHomeDate zieht bei nicht-verstorbenen Tieren Status GivenAway nach (Merle).
|
||||
_g_merle = {"Id": "2", "ExternalRef": "stammbaum-merle-18062023", "Name": "Merle",
|
||||
"DateOfBirth": "2023-06-18", "Status": "Breeding", "IsResident": True}
|
||||
_ovr([_g_merle], [{"name": "Merle", "dob": "18.06.2023",
|
||||
"externalRef": "stammbaum-merle-18062023",
|
||||
"goHomeDate": "22.12.2024"}])
|
||||
check("goHomeDate: Abgabedatum gesetzt", _g_merle["GoHomeDate"] == "2024-12-22")
|
||||
check("goHomeDate: Status wird GivenAway", _g_merle["Status"] == "GivenAway")
|
||||
check("goHomeDate: Residenz bleibt unangetastet", _g_merle["IsResident"] is True)
|
||||
|
||||
# (3) Unplausibles Datum in der Entscheidung wird verworfen (kein Status-Wechsel).
|
||||
_g_bad = {"Id": "3", "ExternalRef": "wc-9", "Name": "Bad", "Status": "Breeding"}
|
||||
_ovr([_g_bad], [{"externalRef": "wc-9", "goHomeDate": "13.05.1310"}])
|
||||
check("goHomeDate: implausibles Datum -> kein Abgabedatum, kein Status-Wechsel",
|
||||
_g_bad.get("GoHomeDate") is None and _g_bad["Status"] == "Breeding")
|
||||
|
||||
# (4) Gencode-Override ist bei PRAEZISEM Match autoritativ (Eliza: der Bestandswert
|
||||
# ist nur der Kanon-Fill des Farbschlags) und setzt zusaetzlich die Scheckungsart.
|
||||
_g_eliza = {"Id": "4", "ExternalRef": "page_0013.md-g1000000-011", "Name": "Eliza",
|
||||
"DateOfBirth": "2010-08-20", "Genotype": "aa CC DD EE GG PP Spsp rere"}
|
||||
_ovr([_g_eliza], [{"externalRef": "page_0013.md-g1000000-011",
|
||||
"genotype": "aa C- DD E- G- P- Spsp",
|
||||
"spottingType": "Ansatzschecke"}])
|
||||
check("genotype: praeziser Override ueberschreibt den Kanon-Fill",
|
||||
_g_eliza["Genotype"] == "aa C- DD E- G- P- Spsp")
|
||||
check("spottingType: Scheckungsart per Entscheidung gesetzt",
|
||||
_g_eliza["SpottingType"] == "Ansatzschecke")
|
||||
|
||||
# (5) name+dob ist ebenfalls praezise, ein Namensvetter mit anderem dob nicht.
|
||||
_g_named = {"Id": "5", "Name": "Chris", "DateOfBirth": "2010-08-20",
|
||||
"Genotype": "aa CC DD EE gg PP Spsp rere"}
|
||||
_g_cousin = {"Id": "6", "Name": "Chris of Elsewhere", "DateOfBirth": "2014-01-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp"}
|
||||
_ovr([_g_named, _g_cousin], [{"name": "Chris", "dob": "20.08.2010",
|
||||
"genotype": "aa Cc[-] DD Ee gg P- Spsp"}])
|
||||
check("genotype: name+dob-Match ist praezise und ueberschreibt",
|
||||
_g_named["Genotype"] == "aa Cc[-] DD Ee gg P- Spsp")
|
||||
check("genotype: gleichnamiges Tier mit anderem dob bleibt unberuehrt",
|
||||
_g_cousin["Genotype"] == "AA CC DD EE GG PP spsp")
|
||||
|
||||
# (6) name-only-Entscheidung (unpraezise) darf einen vorhandenen Gencode NICHT
|
||||
# ueberschreiben — sie kann auf einen Namensvetter geleakt sein (Merle of Samsimar).
|
||||
_g_only = {"Id": "7", "Name": "Vetter", "DateOfBirth": "2012-09-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp"}
|
||||
_ovr([_g_only], [{"name": "Vetter", "genotype": "aa CC DD ee GG PP Spsp"}])
|
||||
check("genotype: name-only-Override ueberschreibt vorhandenen Gencode NICHT",
|
||||
_g_only["Genotype"] == "AA CC DD EE GG PP spsp")
|
||||
|
||||
# (7) ... fuellt aber weiterhin ein LEERES Feld (bisheriges Verhalten, z. B. Jana/Kyara).
|
||||
_g_empty = {"Id": "8", "Name": "Jana", "DateOfBirth": "2010-01-01", "Genotype": None}
|
||||
_ovr([_g_empty], [{"name": "Jana", "genotype": "aa CC DD Ee GG PP spsp"}])
|
||||
check("genotype: name-only-Override fuellt ein leeres Feld weiterhin",
|
||||
_g_empty["Genotype"] == "aa CC DD Ee GG PP spsp")
|
||||
|
||||
# (8) Eine Entscheidung OHNE die neuen Schluessel bleibt fuer sie ein No-Op.
|
||||
_g_plain = {"Id": "9", "Name": "Plain", "DateOfBirth": "2020-01-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp", "Status": "Breeding"}
|
||||
_ovr([_g_plain], [{"name": "Plain", "dob": "01.01.2020", "isResident": True}])
|
||||
check("Overrides: ohne genotype/spottingType/goHomeDate keine Nebenwirkung",
|
||||
_g_plain["Genotype"] == "AA CC DD EE GG PP spsp"
|
||||
and _g_plain.get("SpottingType") is None
|
||||
and _g_plain.get("GoHomeDate") is None
|
||||
and _g_plain["Status"] == "Breeding")
|
||||
|
||||
|
||||
# ── Integration: assert the resolved_import.json output reflects the ticket fixes ──
|
||||
# (Only when the pipeline has already been run; tolerant if the file is absent.)
|
||||
import os as _os, json as _json
|
||||
@@ -766,7 +929,9 @@ if _os.path.exists(_resolved):
|
||||
if _el:
|
||||
check("E-Wurf 2010: father is Blacky", _el.get("FatherId") is not None and _G[_el["FatherId"]]["Name"].startswith("Blacky"))
|
||||
# Ticket e0a0c304: 'Kruke' war ein Tippfehler → korrekt 'Kuke'.
|
||||
check("E-Wurf 2010: mother is Kuke", _el.get("MotherId") is not None and _G[_el["MotherId"]]["Name"] == "Kuke")
|
||||
# Ticket 24006ccd: renameTo setzt jetzt den vollen Zuchtnamen
|
||||
# 'Kuke von Marion Knoss' (Merge mit dem Chart-Ahnenrecord), daher Prefix-Pruefung.
|
||||
check("E-Wurf 2010: mother is Kuke", _el.get("MotherId") is not None and _G[_el["MotherId"]]["Name"].startswith("Kuke"))
|
||||
check("Eliza: shares same litter with Eddy", _eliza.get("LitterId") == _eddy.get("LitterId"))
|
||||
|
||||
# Zeus + Beatrice vs Eddy + Sheila same-date litters (2012-06-13)
|
||||
|
||||
Reference in New Issue
Block a user