fix(import): Stammbaum-/Eltern-/isResident-Tickets der Züchterin (18)

Import-Logik:
- Externe Gründer (Zooladen/„von Privat"/„from …, <Land>") bekommen keine
  erfundenen Chart-Eltern mehr → Bill, Cooky, Zadar haben korrekt unbekannte
  Eltern (is_external_origin). [#5,#13,#28]
- Hagrid: externe Zucht wird nicht mehr als Bestand markiert (isResident=false)
  + Leerhüllen-Dedup → ein Datensatz mit Eltern Snickers × Milka. [#17,#18,#20a]
- Gender-Index: eindeutiges Geschlecht schlägt unbekanntes Duplikat → Rollen-
  Auflösung repariert (Vance→Mutter Enya, Zac→Vater Vance/Mutter Dorie). [#33,#35]
- „Eltern: X + Y"-Wurfnotizen werden geparst (~46 Würfe); v.d.↔von-den-Namens-
  kanon (Theodore→BlackFire). [#9,#11,#30]

Daten-Overrides (conflict-decisions.json, jetzt auch Gender + exakte Eltern):
- Mozart→weiblich [#2], Yuki=Camaro×Izumi [#23], Gold-Mutter=Chelsea [#36],
  Arya=Vance×Sansa (Geschwisterverpaarung) [#16], Tony→Sammy [#12],
  Odelia [#15], Jamie→Danny [#31], Silver=Taro×Beatrice [#11].

Frontend: GerbilDetailPage blendet für isResident=false Würfe + Charakter aus
(Hinweis nonResidentNote). [#17,#20b]

Tests: test_extract/test_merge_resolve erweitert; vitest 135, playwright tiere 34 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:16:53 +02:00
parent 9c6ca2118a
commit ce1704c3b7
9 changed files with 465 additions and 43 deletions

View File

@@ -154,6 +154,24 @@ check("names_overlap: matching father name", m.names_overlap(litter(None, fname=
check("names_overlap: nothing in common", not m.names_overlap(litter(None, fname="A"), litter(None, mname="B")))
# ── canon_name_key: v.d. ↔ von den abbreviation folds to one key (#30) ──
check("canon_name_key: v.d. and von den fold equal",
m.canon_name_key("BlackFire v.d. Kleinen Chaoten")
== m.canon_name_key("BlackFire von den Kleinen Chaoten"))
check("canon_name_key: distinct names stay distinct",
m.canon_name_key("Theodore von den Kleinen Chaoten")
!= m.canon_name_key("Tony von den Kleinen Chaoten"))
# ── is_external_origin: pet-shop / private / foreign founders (#5/#13/#28) ──
check("merge is_external_origin: 'von Privat'", m.is_external_origin("Bill von Privat"))
check("merge is_external_origin: 'vom Zooladen (OBI)'",
m.is_external_origin("Cooky vom Zooladen (OBI)"))
check("merge is_external_origin: foreign Croatia",
m.is_external_origin("Zadar from Zeko i ptica, Croatia"))
check("merge is_external_origin: 'of Black Forest' NOT external",
not m.is_external_origin("Hagrid Rubeus of Black Forest", "Black Forest"))
# ── parent_age_plausible: born before child, within ~6y lifespan ──
check("age: parent 1y before child → plausible", m.parent_age_plausible("16.04.2021", "27.03.2022"))
check("age: parent born AFTER child → implausible", not m.parent_age_plausible("2023-01-01", "2022-03-27"))
@@ -414,6 +432,68 @@ check("contracts: animal-less record has empty Animals list",
_sale3 and _sale3[0]["Animals"] == [])
# ── 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
_resolved = _os.path.join(_os.path.dirname(__file__), "output", "resolved_import.json")
if _os.path.exists(_resolved):
_d = _json.load(open(_resolved, encoding="utf-8"))
_G = {g["Id"]: g for g in _d["gerbils"]}
_L = {l["Id"]: l for l in _d["litters"]}
def _find(sub, dob=None):
sub = sub.lower()
for g in _d["gerbils"]:
if sub in g["Name"].lower() and (dob is None or g.get("DateOfBirth") == dob):
return g
return None
def _parents(g):
l = _L.get(g.get("LitterId")) if g else None
if not l:
return (None, None)
f = _G.get(l.get("FatherId"))
m = _G.get(l.get("MotherId"))
return (f["Name"] if f else None, m["Name"] if m else None)
# #5/#13/#28: external founders → no parents
for tag, nm in [("#5 Bill", "Bill von Privat"),
("#13 Cooky", "Cooky vom Zooladen"),
("#28 Zadar", "Zadar from Zeko")]:
g = _find(nm)
check(f"{tag}: external founder has no litter/parents",
g is not None and not g.get("LitterId"))
# #18: Hagrid is a SINGLE resolved record (the DOB-less shell merged away)
_hag = [g for g in _d["gerbils"] if g["Name"].lower() == "hagrid rubeus of black forest"]
check("#18 Hagrid: exactly one resolved record", len(_hag) == 1)
if _hag:
# #17/#20: external ancestor is NOT resident; parents Snickers × Milka
check("#17/#20 Hagrid: isResident == False", _hag[0].get("IsResident") is False)
f, mo = _parents(_hag[0])
check("#18 Hagrid: father Snickers, mother Milka",
(f or "").startswith("Snickers") and (mo or "").startswith("Milka"))
# #2 Mozart → female; #16 Arya, #23 Yuki, #36 Gold parent corrections
_moz = _find("Mozart of Lennylengo")
check("#2 Mozart: gender female", _moz is not None and _moz.get("Gender") == "female")
def _check_parents(tag, nm, exp_f, exp_m):
g = _find(nm)
f, mo = _parents(g)
check(f"{tag}: father ~ {exp_f}", (f or "").lower().startswith(exp_f.lower()))
check(f"{tag}: mother ~ {exp_m}", (mo or "").lower().startswith(exp_m.lower()))
_check_parents("#16 Arya", "Arya Stark von den Kleinen", "Vance", "Sansa Stark")
_check_parents("#23 Yuki", "Yuki von den Kleinen", "Chevrolet Camaro", "Izumi")
_check_parents("#36 Gold", "Gold v.d. Kleinen", "Trogir", "Chelsea")
_check_parents("#35 Zac", "Zac gen. Action", "Vance", "Dorie")
_check_parents("#9 Beatrice", "Beatrice von den kleinen", "Dante", "Malina")
_check_parents("#30 Theodore", "Theodore von den Kleinen", "BlackFire", "Katara")
else:
print("note: output/resolved_import.json not present — skipped integration assertions")
if check.failed:
print(f"\n{check.failed} test(s) FAILED")
sys.exit(1)