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

@@ -417,6 +417,54 @@ if os.path.exists(kazuya_path):
else:
print("\nWarning: Kazuya stammbaum file not found, skipping photo-shift checks.")
# --- External-origin founders get NO fabricated chart-position parents -------
# Tickets #5 (Bill von Privat), #13 (Cooky vom Zooladen), #28 (Zadar from … Croatia):
# pet-shop / private / foreign-import animals have genuinely unknown ancestry, so
# _reconstruct_parents must not invent parents for them from neighbouring blocks.
check("is_external_origin: 'von Privat' name", e.is_external_origin("Bill von Privat"))
check("is_external_origin: 'vom Zooladen (OBI)' name",
e.is_external_origin("Cooky vom Zooladen (OBI)"))
check("is_external_origin: foreign 'from …, Croatia'",
e.is_external_origin("Zadar from Zeko i ptica, Croatia"))
check("is_external_origin: clan animal is NOT external",
not e.is_external_origin("Silver von den kleinen Chaoten", "Kleine Chaoten"))
check("is_external_origin: ordinary cattery 'of Black Forest' is NOT external",
not e.is_external_origin("Hagrid Rubeus of Black Forest", "Black Forest"))
# _reconstruct_parents must skip the external leaf but still parent the clan child.
_ext_animals = [
{"name": "Kind von den Kleinen Chaoten", "_gen": 0, "_row": 5, "dob": "01.01.2020",
"zucht": "Kleine Chaoten", "breeder": "", "gender": None, "parentRefs": []},
{"name": "Cooky vom Zooladen (OBI)", "_gen": 1, "_row": 4, "dob": "01.01.2018",
"zucht": "", "breeder": "", "gender": "female", "parentRefs": []},
{"name": "Papa von den Kleinen Chaoten", "_gen": 1, "_row": 6, "dob": "01.01.2017",
"zucht": "Kleine Chaoten", "breeder": "", "gender": "male", "parentRefs": []},
{"name": "OmaUnbekannt", "_gen": 2, "_row": 3, "dob": "", "zucht": "", "breeder": "",
"gender": None, "parentRefs": []},
]
e._reconstruct_parents(_ext_animals)
_cooky = next(a for a in _ext_animals if a["name"].startswith("Cooky"))
check("_reconstruct_parents: external Cooky gets NO parentRefs",
_cooky["parentRefs"] == [])
_kind = next(a for a in _ext_animals if a["name"].startswith("Kind"))
check("_reconstruct_parents: clan child still gets its chart-position parents",
len(_kind["parentRefs"]) >= 1)
# --- gender override (conflict-decisions) — Mozart misread male, should be female ---
_g_merged = [{"id": "moz", "name": "Mozart of Lennylengo", "dob": "12.03.2017",
"gender": "male", "genotype": e.gt.parse(""), "parentRefs": [],
"conflict": False}]
_g_dec = os.path.join(tempfile.gettempdir(), "conflict-gender-test.json")
import json as _json
_json.dump({"resolutions": [
{"name": "Mozart of Lennylengo", "dob": "12.03.2017", "gender": "female",
"decision": "box colour misread"}
]}, open(_g_dec, "w", encoding="utf-8"))
e.apply_conflict_decisions(_g_merged, [], _g_dec)
check("gender override: Mozart flipped male -> female", _g_merged[0]["gender"] == "female")
try: os.remove(_g_dec)
except OSError: pass
if failed:
print(f"\n{failed} test(s) FAILED")
sys.exit(1)