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

@@ -3016,6 +3016,51 @@ def main():
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
dt_now = datetime.now()
for g in resolved_gerbils: