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:
@@ -149,6 +149,38 @@ def is_clan_zucht(z):
|
||||
return norm_zucht(z) == "kleinechaote"
|
||||
|
||||
|
||||
# Name/breeder markers for an externally-acquired animal whose ancestry is NOT
|
||||
# in the breeder's own charts: pet shops, "von Privat" private hobbyists and
|
||||
# foreign catteries ("from … , Croatia"). Such founders have genuinely UNKNOWN
|
||||
# parents — the chart-position heuristic must not fabricate parents for them.
|
||||
_EXTERNAL_MARKERS = re.compile(
|
||||
r"\b(zooladen|zoohandlung|obi|fressnapf|dehner|von\s+privat|privatkauf|"
|
||||
r"vom\s+bauern|aus\s+der\s+zoohandlung)\b", re.IGNORECASE)
|
||||
# Foreign acquisition: "<name> from <cattery>, <Country>" (a comma + country word).
|
||||
_FOREIGN_FROM = re.compile(
|
||||
r"\bfrom\b.+,\s*(croatia|kroatien|poland|polen|netherlands|niederlande|"
|
||||
r"belgium|belgien|france|frankreich|austria|österreich|switzerland|schweiz|"
|
||||
r"italy|italien|spain|spanien|czech|tschechien|hungary|ungarn)\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def is_external_origin(name, zucht=None, breeder=None):
|
||||
"""True if an animal is an externally-acquired founder with genuinely unknown
|
||||
ancestry (pet shop, private hobbyist, foreign cattery). For such animals the
|
||||
positional chart heuristic must NOT invent parents — their parents are unknown.
|
||||
|
||||
NOT external: own stock ('… von den Kleinen Chaoten') and ordinary German
|
||||
catteries that appear as in-chart ancestors (e.g. 'of Black Forest') — those
|
||||
can legitimately have charted ancestors elsewhere; this gate is only for the
|
||||
leaf/founder markers above.
|
||||
"""
|
||||
blob = " ".join(p for p in (name, zucht, breeder) if p)
|
||||
if _EXTERNAL_MARKERS.search(blob):
|
||||
return True
|
||||
if _FOREIGN_FROM.search(name or ""):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def canon_pair(raw):
|
||||
"""Full raw name -> (normalised call-name, canonical zucht)."""
|
||||
name, zucht = split_name_zucht(raw)
|
||||
@@ -319,6 +351,11 @@ def _reconstruct_parents(animals):
|
||||
if not nxt:
|
||||
continue
|
||||
for a in group:
|
||||
# Externally-acquired founders (pet shop / von Privat / foreign cattery)
|
||||
# have genuinely UNKNOWN ancestry — do NOT invent chart-position parents
|
||||
# for them (tickets #5 Bill von Privat, #13 Cooky vom Zooladen, #28 Zadar).
|
||||
if is_external_origin(a.get("name"), a.get("zucht"), a.get("breeder")):
|
||||
continue
|
||||
r = a["_row"]
|
||||
above = [x for x in nxt if x["_row"] <= r]
|
||||
below = [x for x in nxt if x["_row"] > r]
|
||||
@@ -1103,6 +1140,12 @@ def apply_conflict_decisions(merged, conflicts, path):
|
||||
a["farbschlagVariants"] = [d["farbschlag"]]
|
||||
if d.get("dateOfDeath"): # D5 death-date resolutions
|
||||
a["death"] = norm_dob(d["dateOfDeath"])
|
||||
if d.get("gender"): # gender override (box colour misread)
|
||||
g = d["gender"].strip().lower()
|
||||
g = {"m": "male", "männlich": "male", "w": "female",
|
||||
"f": "female", "weiblich": "female"}.get(g, g)
|
||||
if g in ("male", "female"):
|
||||
a["gender"] = g
|
||||
if "father" in d or "mother" in d:
|
||||
new_refs = []
|
||||
if d.get("father"):
|
||||
|
||||
Reference in New Issue
Block a user