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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user