From 1845d37476499e5179b04e0b0015ea90c9f5cf33 Mon Sep 17 00:00:00 2001 From: Gulum Date: Mon, 22 Jun 2026 15:11:53 +0200 Subject: [PATCH] fix(import): Eltern-Ref-Auswahl geschlechts-bewusst machen (Regression behoben) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die in 03ad9e6 eingeführte DOB-Bevorzugung war zu grob: ein datierter, aber geschlechts-falscher Ref (z. B. weibliche „Danielle") gewann den Vater-Slot gegen einen undatieren männlichen/unbekannten (z. B. „Hagrid Rubeus") — wodurch Mollys korrekte Mutter Arya Stark in der Rollen-Normalisierung verloren ging. pick_parent_ref wertet jetzt zuerst das (aus den Tierdaten ermittelte) Geschlecht für die Rolle: ein klar falsches Geschlecht wird stark abgewertet, erst danach zählt plausibles DOB > kein DOB > unplausibles DOB. Fixt Molly (Hagrid × Arya) UND behält Solice (Lui × Molly = Geschwister von Silvain). Keine Abdeckungs- Regression (0 Selbst-Verpaarungen / 0 Gender-Fehler / 0 Alter-Verletzungen). Co-Authored-By: Claude Opus 4.8 --- tools/import/merge_and_resolve.py | 52 ++++++++++++++++++++++++------ tools/import/test_merge_resolve.py | 21 ++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/tools/import/merge_and_resolve.py b/tools/import/merge_and_resolve.py index 1bf6c95..66bf8fc 100644 --- a/tools/import/merge_and_resolve.py +++ b/tools/import/merge_and_resolve.py @@ -596,27 +596,38 @@ def parent_age_plausible(parent_dob, litter_date): return 0 < (ld - pd) <= MAX_PARENT_AGE_DAYS -def pick_parent_ref(parent_refs, role, child_dob, avoid_name=None): +def pick_parent_ref(parent_refs, role, child_dob, avoid_name=None, gender_of=None): """Choose the best parent ref for a role from possibly-conflicting chart refs. A Stammbaum lists an animal at several positions, so its parentRefs can carry - contradictory guesses (the first one is not necessarily right). Prefer a ref - whose own DOB is age-plausible for the child, then a ref with no DOB, and - avoid re-using the other role's animal (full-sibling charts repeat the same - name in both parent slots). Returns the chosen ref dict or None. + contradictory guesses (the first one is not necessarily right). Rank candidates + (lower = better): + 0 right/unknown gender for the role, age-plausible dated ref + 1 right/unknown gender, no DOB (usable, but a plausible dated ref wins) + 2 right/unknown gender, dated but age-impossible + 3 resolved gender is clearly WRONG for the role (e.g. a female father) + 4 would duplicate the animal chosen for the other role + + Gender is decisive over DOB: a dated female ref must not win the father slot + over an undated male/unknown one. `gender_of(name)` returns 'male'/'female' + or None (unknown/ambiguous → not penalised). Returns the chosen ref or None. """ role_refs = [p for p in parent_refs if p.get("roleGuess") == role] if not role_refs: return None avoid = normalize_name(avoid_name) if avoid_name else None + expected = "male" if role == "father" else "female" def rank(p): if avoid is not None and normalize_name(p.get("name")) == avoid: - return 3 # would duplicate the other parent role + return 4 # would duplicate the other parent role + g = gender_of(p.get("name")) if gender_of else None + if g in ("male", "female") and g != expected: + return 3 # wrong sex for this role dob = p.get("dob") if not dob: - return 1 # unknown age — usable, but a plausible-dated ref wins - return 0 if parent_age_plausible(dob, child_dob) else 2 # dated & impossible → last + return 1 + return 0 if parent_age_plausible(dob, child_dob) else 2 order = sorted(range(len(role_refs)), key=lambda i: (rank(role_refs[i]), i)) return role_refs[order[0]] @@ -959,14 +970,35 @@ def main(): key = (normalize_name(f_name), normalize_name(m_name), ldate) md_litters_idx[key] = rl + # Gender index for parent-ref selection: normalized name → 'male' | 'female' + # | 'ambiguous'. Drives the gender-aware ranking in pick_parent_ref so a dated + # but wrong-sex ref (e.g. female „Danielle“) cannot win the father slot over + # an undated male/unknown one (e.g. „Hagrid Rubeus“). + gender_idx = {} + for a in stammbaum_only_animals: + g = (a.get("gender") or "").lower().strip() + g = g if g in ("male", "female") else None + for key in {normalize_name(a.get("name")), normalize_name(get_call_name(a.get("name") or ""))}: + if not key: + continue + if key not in gender_idx: + gender_idx[key] = g + elif gender_idx[key] != g: + gender_idx[key] = "ambiguous" + + def gender_of_name(name): + v = gender_idx.get(normalize_name(name)) + return v if v in ("male", "female") else None + # Create virtual litters for stammbaum animals created_virtual_litters = {} for a in stammbaum_only_animals: parent_refs = a.get("parentRefs", []) child_dob_raw = a.get("dob") - father_ref = pick_parent_ref(parent_refs, "father", child_dob_raw) + father_ref = pick_parent_ref(parent_refs, "father", child_dob_raw, gender_of=gender_of_name) mother_ref = pick_parent_ref(parent_refs, "mother", child_dob_raw, - avoid_name=father_ref.get("name") if father_ref else None) + avoid_name=father_ref.get("name") if father_ref else None, + gender_of=gender_of_name) a["_mapped_litter_scoped_id"] = None if father_ref and mother_ref: diff --git a/tools/import/test_merge_resolve.py b/tools/import/test_merge_resolve.py index b7b95a0..668bd54 100644 --- a/tools/import/test_merge_resolve.py +++ b/tools/import/test_merge_resolve.py @@ -191,6 +191,27 @@ check("pick: no ref for role → None", check("pick: single ref is returned", m.pick_parent_ref([pref("Solo", "father")], "father", "2022-03-27")["name"] == "Solo") +# Gender-aware: a dated FEMALE ref must not win the father slot over an undated +# male/unknown one (Molly regression: Hagrid (unknown, no DOB) vs Danielle +# (female, dated) → father must be Hagrid, not Danielle). +molly_refs = [ + pref("Hagrid Rubeus of Black Forest", "father"), + pref("Arya Stark von den Kleinen Chaoten", "mother", "30.06.2020"), + pref("Danielle von den Kleinen Chaoten", "father", "04.03.2020"), + pref("Hagrid Rubeus of Black Forest", "mother", "18.07.2019"), +] +gender = { + m.normalize_name("Hagrid Rubeus of Black Forest"): None, # unknown + m.normalize_name("Danielle von den Kleinen Chaoten"): "female", + m.normalize_name("Arya Stark von den Kleinen Chaoten"): "female", +} +gof = lambda name: gender.get(m.normalize_name(name)) +fr = m.pick_parent_ref(molly_refs, "father", "13.09.2021", gender_of=gof) +check("pick(gender): father = unknown-sex Hagrid, not dated female Danielle", + fr and fr["name"] == "Hagrid Rubeus of Black Forest") +mr = m.pick_parent_ref(molly_refs, "mother", "13.09.2021", avoid_name=fr["name"], gender_of=gof) +check("pick(gender): mother = Arya (female)", mr and mr["name"].startswith("Arya")) + if check.failed: print(f"\n{check.failed} test(s) FAILED")