fix(stammbaum): Geschwister-Verpaarung erkennen und im Diagramm zusammenführen
Vollgeschwister-Verpaarung (Vater & Mutter aus demselben Wurf) ist in der
Gerbil-Zucht häufig. Bisher zeigten Diagramm und Import sie nicht korrekt.
Import (merge_and_resolve.py):
- Wurf-Dedup gehärtet: asymmetrischer Merge nur bei kompatiblen Eltern-Namen;
bei Date=None nur mit positivem Namens-Match (verhindert blindes Verschmelzen
unverwandter datumloser Stubs). Falsch-Merges 112 -> 70.
- Parent-Resolver robust: Gender ist Präferenz statt hartem Filter, sodass
vertauschte Eltern (z. B. weibliches Tier in der Vater-Position) trotzdem
auflösen. Aufgelöste Eltern 200/206 -> 240/260.
- Neue Rollen-Normalisierung: weist jede Maus rollenrichtig nach Geschlecht zu,
entfernt Selbst-Verpaarungen und unmögliche Doppelrollen.
Selbst-Verpaarungen 16 -> 0, Gender-Rollen-Fehler 30 -> 0.
- Reine Helfer (litter_compatible/assign_parent_roles/names_*) auf Modulebene
extrahiert und in test_merge_resolve.py (26 Tests) abgesichert.
Frontend (pedigree):
- buildAnimal markiert den Mutter-Knoten, wenn beide Eltern dieselbe litterId
teilen (Vollgeschwister). toRawNodeDatum führt dessen Vorfahren-Ast zu einem
Verweis-Knoten zusammen ("Geschwister von <Vater>, Eltern siehe oben"); die
Vaterlinie zeigt die gemeinsamen Großeltern einmal.
- Druck-Ahnentafel bleibt vollständig (ignoriert die Marke).
- Build-Tests (+3) und e2e-Test (Inzucht-Kind-Fixture) ergänzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
160
tools/import/test_merge_resolve.py
Normal file
160
tools/import/test_merge_resolve.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Zero-dep tests for merge_and_resolve.py litter-dedup & parent-role logic.
|
||||
|
||||
Run: python test_merge_resolve.py (exit 0 = all pass)
|
||||
|
||||
Covers the sibling-pairing data fix:
|
||||
- litter_compatible(): same-date / compatible-parent dedup, incl. the dateless
|
||||
guard that stops unrelated nameless stubs from blind-merging.
|
||||
- assign_parent_roles(): gender-correct role assignment, self-pairing removal,
|
||||
no two same-role parents — the fix for the 16 self-pairings / 30 gender-role
|
||||
errors that the old simple swap missed.
|
||||
"""
|
||||
import sys
|
||||
import merge_and_resolve as m
|
||||
|
||||
|
||||
def check(name, cond):
|
||||
if not cond:
|
||||
print(f"FAIL: {name}")
|
||||
check.failed += 1
|
||||
else:
|
||||
print(f"ok: {name}")
|
||||
check.failed = 0
|
||||
|
||||
|
||||
def litter(date, fid=None, mid=None, fname=None, mname=None):
|
||||
return {
|
||||
"Date": date,
|
||||
"FatherId": fid,
|
||||
"MotherId": mid,
|
||||
"_father_name": fname,
|
||||
"_mother_name": mname,
|
||||
}
|
||||
|
||||
|
||||
# ── litter_compatible: both sides fully parented ──
|
||||
check(
|
||||
"same date + same parents → compatible",
|
||||
m.litter_compatible(litter("2018-09-22", "F", "M"), litter("2018-09-22", "F", "M")),
|
||||
)
|
||||
check(
|
||||
"same date + different parents → NOT compatible",
|
||||
not m.litter_compatible(litter("2018-09-22", "F", "M"), litter("2018-09-22", "X", "Y")),
|
||||
)
|
||||
check(
|
||||
"different date → NOT compatible",
|
||||
not m.litter_compatible(litter("2018-09-22", "F", "M"), litter("2019-01-01", "F", "M")),
|
||||
)
|
||||
|
||||
# ── asymmetric (one resolved, one not), real date ──
|
||||
check(
|
||||
"asymmetric same real date, names agree → merge",
|
||||
m.litter_compatible(
|
||||
litter("2018-09-22", "F", "M", "Wonderman", "Unique"),
|
||||
litter("2018-09-22", None, None, "Wonderman", "Unique"),
|
||||
),
|
||||
)
|
||||
check(
|
||||
"asymmetric same real date, conflicting names → NO merge",
|
||||
not m.litter_compatible(
|
||||
litter("2018-09-22", "F", "M", "Wonderman", "Unique"),
|
||||
litter("2018-09-22", None, None, "Someone", "Else"),
|
||||
),
|
||||
)
|
||||
check(
|
||||
"asymmetric real date, parented side nameless stub → merge (no conflict)",
|
||||
m.litter_compatible(
|
||||
litter("2018-09-22", "F", "M", "Wonderman", "Unique"),
|
||||
litter("2018-09-22", None, None, None, None),
|
||||
),
|
||||
)
|
||||
|
||||
# ── dateless guard: the bug that wrongly merged unrelated stubs ──
|
||||
check(
|
||||
"dateless asymmetric, NO name evidence → do NOT merge (was the bug)",
|
||||
not m.litter_compatible(
|
||||
litter(None, "F", "M", "Akina", "Arrow"),
|
||||
litter(None, None, None, None, None),
|
||||
),
|
||||
)
|
||||
check(
|
||||
"dateless asymmetric WITH positive name match → merge",
|
||||
m.litter_compatible(
|
||||
litter(None, "F", "M", "Akina", "Arrow"),
|
||||
litter(None, None, None, "Akina", None),
|
||||
),
|
||||
)
|
||||
check(
|
||||
"dateless asymmetric, contradicting names → do NOT merge",
|
||||
not m.litter_compatible(
|
||||
litter(None, "F", "M", "Akina", "Arrow"),
|
||||
litter(None, None, None, "Mismatch", None),
|
||||
),
|
||||
)
|
||||
|
||||
# ── neither side parented → never blind-merge ──
|
||||
check(
|
||||
"neither parented, same date → NOT compatible",
|
||||
not m.litter_compatible(litter("2018-09-22"), litter("2018-09-22")),
|
||||
)
|
||||
|
||||
|
||||
# ── assign_parent_roles ──
|
||||
GENDER = {"bock": "male", "bock2": "male", "maus": "female", "maus2": "female", "u": "unknown", "u2": "unknown"}
|
||||
gof = lambda gid: GENDER.get(gid)
|
||||
|
||||
check("correct roles stay put", m.assign_parent_roles("bock", "maus", gof) == ("bock", "maus"))
|
||||
check("reversed roles get swapped", m.assign_parent_roles("maus", "bock", gof) == ("bock", "maus"))
|
||||
check(
|
||||
"self-pairing collapses to gender-correct single role (male→father)",
|
||||
m.assign_parent_roles("bock", "bock", gof) == ("bock", None),
|
||||
)
|
||||
check(
|
||||
"self-pairing collapses to gender-correct single role (female→mother)",
|
||||
m.assign_parent_roles("maus", "maus", gof) == (None, "maus"),
|
||||
)
|
||||
check(
|
||||
"female in father slot, empty mother → moved to mother",
|
||||
m.assign_parent_roles("maus", None, gof) == (None, "maus"),
|
||||
)
|
||||
check(
|
||||
"male in mother slot, empty father → moved to father",
|
||||
m.assign_parent_roles(None, "bock", gof) == ("bock", None),
|
||||
)
|
||||
check(
|
||||
"two males → keep one father, drop impossible second",
|
||||
m.assign_parent_roles("bock", "bock2", gof) == ("bock", None),
|
||||
)
|
||||
check(
|
||||
"two females → keep one mother, drop impossible second",
|
||||
m.assign_parent_roles("maus", "maus2", gof) == (None, "maus"),
|
||||
)
|
||||
check(
|
||||
"male + unknown → unknown fills mother",
|
||||
m.assign_parent_roles("bock", "u", gof) == ("bock", "u"),
|
||||
)
|
||||
check(
|
||||
"female + unknown → unknown fills father",
|
||||
m.assign_parent_roles("u", "maus", gof) == ("u", "maus"),
|
||||
)
|
||||
check("both empty → both None", m.assign_parent_roles(None, None, gof) == (None, None))
|
||||
check(
|
||||
"single unknown parent kept as father",
|
||||
m.assign_parent_roles("u", None, gof) == ("u", None),
|
||||
)
|
||||
|
||||
|
||||
# ── name helpers ──
|
||||
check("names_no_conflict: one side empty", m.names_no_conflict(litter(None, fname="A"), litter(None)))
|
||||
check(
|
||||
"names_no_conflict: contradiction detected",
|
||||
not m.names_no_conflict(litter(None, fname="A"), litter(None, fname="B")),
|
||||
)
|
||||
check("names_overlap: matching father name", m.names_overlap(litter(None, fname="A"), litter(None, fname="A")))
|
||||
check("names_overlap: nothing in common", not m.names_overlap(litter(None, fname="A"), litter(None, mname="B")))
|
||||
|
||||
|
||||
if check.failed:
|
||||
print(f"\n{check.failed} test(s) FAILED")
|
||||
sys.exit(1)
|
||||
print("\nAll merge_and_resolve tests passed.")
|
||||
Reference in New Issue
Block a user