feat(import): isResident-Override + Vorfahren Bentley/Alexandria/Bugatti

- Neuer isResident-Override im Import (merge_and_resolve, NACH dem Dedup, damit die
  Zusammenführung ihn nicht überschreibt). Matcht über externalRef (präzise, auch für
  namenlose Tiere) oder normalize(call-name)+dob. Ein Nicht-Bestandstier verliert den
  eigenen Zucht-Breeder.
- conflict-decisions.json: Bentley, Alexandria, Bugatti = nur Vorfahren (isResident=false),
  bestätigt von der Züchterin (Tickets 09bcac78 / 45cc501b).
- Regressionstests ergänzt/aktualisiert (Bentley/Alexandria/Bugatti isResident; Danielle
  Vater Makoto; Catelyn Eddard×Milena; Gaida Zhuāngzǐ×Zaibunissa). Alle python test_*.py grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 12:29:25 +02:00
parent 41ee4ee3ab
commit fe79be2eee
3 changed files with 81 additions and 2 deletions

View File

@@ -414,6 +414,27 @@
"mother": "Zaibunissa von den Kleinen Chaoten", "mother": "Zaibunissa von den Kleinen Chaoten",
"motherDob": "21.02.2020", "motherDob": "21.02.2020",
"source": "Züchterin 2026-06-23 — Ticket ba63325a" "source": "Züchterin 2026-06-23 — Ticket ba63325a"
},
{
"name": "Bentley",
"externalRef": "Wurfchronik Teil 1_page_0054.md-50505050-0003-4000-8000-000000000003",
"isResident": false,
"decision": "Nur Vorfahre (Clan of Black Forest), kein eigenes Zuchttier. Bestätigt von der Züchterin.",
"source": "Züchterin 2026-06-23 — Ticket 09bcac78"
},
{
"name": "Alexandria",
"externalRef": "Wurfchronik Teil 1_page_0054.md-50505050-0004-4000-8000-000000000004",
"isResident": false,
"decision": "Nur Vorfahre, kein eigenes Zuchttier (wie Bentley).",
"source": "Züchterin 2026-06-23 — Ticket 45cc501b"
},
{
"name": "Bugatti",
"externalRef": "Wurfchronik Teil 1_page_0054.md-40404040-0003-4000-8000-000000000003",
"isResident": false,
"decision": "Bugatti stammt aus Clan of Black Forest, war nie ihr Zuchttier — nur Vorfahre.",
"source": "Züchterin 2026-06-23 — Ticket 09bcac78"
} }
], ],
"addAnimals": [ "addAnimals": [

View File

@@ -3016,6 +3016,51 @@ def main():
print(f"Deduplicated to {len(resolved_gerbils)} unique gerbil records.") print(f"Deduplicated to {len(resolved_gerbils)} unique gerbil records.")
# ── isResident-Overrides (Mensch-Entscheidung) ────────────────────────────
# NACH dem Dedup anwenden, sonst würde die Dedup-Zusammenführung (IsResident=True
# falls eine Variante resident ist) sie wieder überschreiben. Matcht primär über
# `externalRef` (präzise, auch für namenlose Tiere) oder sonst über
# normalize(call-name)+ISO-dob (leere dob = name-only, für Vorfahren ohne Datum).
_isres_by_extref = {}
_isres_by_namedob = {}
for d in _decisions:
if "isResident" not in d:
continue
val = bool(d["isResident"])
er = (d.get("externalRef") or "").strip()
if er:
_isres_by_extref[er] = val
nm = d.get("name")
if nm is not None:
ck = normalize_name(get_call_name(nm or ""))
iso = parse_date(d.get("dob")) if d.get("dob") else ""
_isres_by_namedob[(ck, iso or "")] = val
_isres_applied = 0
if _isres_by_extref or _isres_by_namedob:
for g in resolved_gerbils:
er = g.get("ExternalRef") or ""
ck = normalize_name(get_call_name(g.get("Name") or ""))
iso = g.get("DateOfBirth") or ""
if er and er in _isres_by_extref:
val = _isres_by_extref[er]
elif er and any(er.endswith(k) for k in _isres_by_extref):
val = next(v for k, v in _isres_by_extref.items() if er.endswith(k))
elif (ck, iso) in _isres_by_namedob:
val = _isres_by_namedob[(ck, iso)]
elif (ck, "") in _isres_by_namedob:
val = _isres_by_namedob[(ck, "")]
else:
continue
if g.get("IsResident") != val:
g["IsResident"] = val
# Ein Nicht-Bestandstier ist kein eigenes Zuchttier → den eigenen
# Zucht-Breeder entfernen (nur, wenn er auf die eigene Zucht zeigt).
if not val and g.get("OriginBreeder") == "Zucht der kleinen Chaoten":
g["OriginBreeder"] = None
_isres_applied += 1
if _isres_applied:
print(f"isResident-Overrides angewandt: {_isres_applied}")
# Apply age-based death threshold (6.0 years) to all resolved gerbils # Apply age-based death threshold (6.0 years) to all resolved gerbils
dt_now = datetime.now() dt_now = datetime.now()
for g in resolved_gerbils: for g in resolved_gerbils:

View File

@@ -559,17 +559,30 @@ if _os.path.exists(_resolved):
# Sunny von PZ Karl: father corrected Hiro → Bill von Privat. # Sunny von PZ Karl: father corrected Hiro → Bill von Privat.
_check_parents("Sunny (parents)", "Sunny von PZ Karl", "Bill von Privat", "Melly von Privat") _check_parents("Sunny (parents)", "Sunny von PZ Karl", "Bill von Privat", "Melly von Privat")
# Danielle: mother = Ella *10.06.2019 (the elder one), father unknown. # Danielle: mother = Ella *10.06.2019, father = Makoto (sibling pairing; Ticket 4692fd5c).
_dan = _find("Danielle von den Kleinen") _dan = _find("Danielle von den Kleinen")
_df, _dm = _parents(_dan) _df, _dm = _parents(_dan)
check("Danielle: mother is Ella", (_dm or "") == "Ella") check("Danielle: mother is Ella", (_dm or "") == "Ella")
check("Danielle: father unknown (sibling pairing)", _df is None) check("Danielle: father is Makoto", (_df or "").startswith("Makoto"))
if _dan and _dan.get("LitterId"): if _dan and _dan.get("LitterId"):
_dl = _L.get(_dan["LitterId"]) _dl = _L.get(_dan["LitterId"])
_dmom = _G.get(_dl.get("MotherId")) if _dl else None _dmom = _G.get(_dl.get("MotherId")) if _dl else None
check("Danielle: mother Ella is the *2019-06-10 one (not the *2023 Ella)", check("Danielle: mother Ella is the *2019-06-10 one (not the *2023 Ella)",
_dmom is not None and _dmom.get("DateOfBirth") == "2019-06-10") _dmom is not None and _dmom.get("DateOfBirth") == "2019-06-10")
# Catelyn (Ticket 7bbc045c): father Eddard Stark of Sunset Glow, mother Milena.
_check_parents("Catelyn", "Catelyn Stark von den Kleinen", "Eddard Stark", "Milena")
# Gaida (Ticket ba63325a): Geschwisterverpaarung Zhuāngzǐ × Zaibunissa (beide *2020-02-21).
_check_parents("Gaida", "Gaida von den Kleinen Chaoten", "Zhuāngzǐ", "Zaibunissa")
# Bentley / Alexandria / Bugatti (Ticket 09bcac78 / 45cc501b): nur Vorfahren → isResident False.
for _tag, _ref in [("Bentley", "Wurfchronik Teil 1_page_0054.md-50505050-0003-4000-8000-000000000003"),
("Alexandria", "Wurfchronik Teil 1_page_0054.md-50505050-0004-4000-8000-000000000004"),
("Bugatti", "Wurfchronik Teil 1_page_0054.md-40404040-0003-4000-8000-000000000003")]:
_g = next((g for g in _d["gerbils"] if g.get("ExternalRef") == _ref), None)
check(f"{_tag}: isResident override == False", _g is not None and _g.get("IsResident") is False)
# Cherry Berry's Quqquluuruu: gender override female (box colour misread). # Cherry Berry's Quqquluuruu: gender override female (box colour misread).
_cherry = _find("Cherry Berry") _cherry = _find("Cherry Berry")
check("Cherry Berry: gender override female", check("Cherry Berry: gender override female",