IMPORT-POLISH: 4 Importer-Fixes nach Re-Import #2

FIX-1 decision-matching: apply_conflict_decisions/apply_dob_remaps
nutzen jetzt canon_pair(name)[0] als Match-Key (Dedup-Identitaet:
call-name ohne Zucht, v.d.<->von den gefaltet). Workaround-Spelling
v.d. in Victoria Welbys Decision bleibt erhalten; beide Formen
matchen jetzt. Kommentar im decision-Eintrag aktualisiert.

FIX-2 specific-wins: _alleles_compatible aendert '? vs x = False'
-> '? vs x = True' (spezifischer Wert gewinnt). C- vs CC, G- vs Gg,
P? vs PP sind kein Konflikt mehr. Echte Wert-Widersprueche (DD vs Dd,
Ee vs ee, PP vs Pp) bleiben Konflikte. Loest Enya, Ella, Zac
automatisch (Konflikte 8->5 erwartet). 2 bestehende Tests angepasst,
7 neue Tests.

FIX-3 parent-FK backfill: nach dem Wurfchronik-Rueckverknuepfungs-
Block iteriert ImportService.RunAsync ueber bereits importierte
Wuerfe mit null Father/MotherId und setzt fehlende FKs wenn das
Elterntier jetzt ladbar ist. Trockenlauf zaehlt, Execute schreibt.
LitterSummary.ParentFksBackfilled + 2 neue C#-Tests (SQLite).

FIX-4 Skarlett-Artefakt: parse_detail() strippt trailing / +YEAR
aus dem Genotyp-Tail (re.sub). Sterbejahr bleibt als death-Date
erhalten -> Skarlett erscheint als reiner Sterbedatum-Konflikt.
2 neue Python-Tests.

Gate: 124/124 C#-Tests, Python test_extract/test_genotype ALL PASS,
has-pending-model-changes = No.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 14:53:27 +02:00
parent 5eadd89bb6
commit 0c94cfcbf1
6 changed files with 248 additions and 21 deletions

View File

@@ -158,6 +158,10 @@ def parse_detail(text):
tail = text[dob.end():]
tail = re.sub(r"^\s*/?\+?\s?\d[\d.]*", "", tail) # drop any /+death remnant
tail = tail.lstrip(" ,").strip()
# FIX-4 (Skarlett): strip trailing "/ +YEAR" death-year artifacts leaked from compact
# chart cells (e.g. "… rere / +2018"). The DEATH regex still captures the year from
# the full cell text, so it appears as a death-date conflict — not a genotype conflict.
tail = re.sub(r"\s*/\s*\+\d{4}\s*$", "", tail).strip()
if gt.looks_like_genotype(tail):
geno = tail
return (dob.group(1) if dob else "",
@@ -518,10 +522,11 @@ def _alleles_compatible(a, b):
if a == b:
return True
if a == "?" or b == "?":
return False # unknown vs filled = contradiction (D- vs DD)
return True # specific-wins: unknown allele is compatible with any
# specified value (C- vs CC -> CC; G- vs Gg -> Gg)
(ba, ma), (bb, mb) = _split_allele(a), _split_allele(b)
if ba != bb:
return False # different base allele = real value diff (E vs e)
return False # different base allele = real value diff (E vs e, D vs d)
return ma == "" or mb == "" # same base, modifier present-vs-absent -> presence wins
@@ -887,21 +892,22 @@ def apply_dob_remaps(raw_animals, path):
"""PRE-dedup: a conflict-decision carrying `correctDob` marks a record as a DUPLICATE with a
wrong birthdate — remap that raw record's DOB to correctDob so dedup MERGES it into the
canonical same-named animal (e.g. Chelsea *15.10.2021 -> *02.04.2021). Match =
norm_name(name)+norm_dob(dob). Tolerates a missing/garbled file. Returns the remap count.
canon_pair(name)[0]+norm_dob(dob) (same identity as dedup — strips zucht suffix, folds
v.d.<->von den). Tolerates a missing/garbled file. Returns the remap count.
Must run BEFORE dedup (it changes the dedup identity). (god/HUMANQUESTION D — Dubletten.)"""
remaps = {}
try:
with open(path, encoding="utf-8") as fh:
for r in (json.load(fh).get("resolutions") or []):
if r.get("correctDob"):
remaps[(norm_name(r.get("name", "")), norm_dob(r.get("dob", "")))] = r["correctDob"]
remaps[(canon_pair(r.get("name", ""))[0], norm_dob(r.get("dob", "")))] = r["correctDob"]
except (OSError, ValueError):
return 0
if not remaps:
return 0
n = 0
for a in raw_animals:
new = remaps.get((norm_name(a.get("name", "")), norm_dob(a.get("dob", ""))))
new = remaps.get((canon_pair(a.get("name", ""))[0], norm_dob(a.get("dob", ""))))
if new and a.get("dob") != new:
a["dob"] = new
n += 1
@@ -911,15 +917,18 @@ def apply_dob_remaps(raw_animals, path):
def apply_conflict_decisions(merged, conflicts, path):
"""Consume human conflict resolutions (tools/import/conflict-decisions.json) so the wife's
answers UN-QUARANTINE animals. Schema: {"resolutions":[{name, dob, decision, genotype?,
farbschlag?, source}]}. Match = norm_name(name)+norm_dob(dob) (same identity as dedup). A
matching animal: clear its conflict, mark resolvedByDecision; an explicit `genotype`
(breeder notation) is parsed and becomes authoritative, `farbschlag` overrides too. Tolerates
a missing/empty/garbled file. Returns the number of conflicts resolved. (god/HUMANQUESTION D.)"""
farbschlag?, source}]}. Match = canon_pair(name)[0]+norm_dob(dob) — the same dedup identity
(call-name only, zucht stripped, v.d.<->von den folded). A matching animal: clear its
conflict, mark resolvedByDecision; an explicit `genotype` (breeder notation) is parsed and
becomes authoritative, `farbschlag` overrides too. Tolerates a missing/empty/garbled file.
Returns the number of conflicts resolved. (god/HUMANQUESTION D.)"""
decisions = {}
try:
with open(path, encoding="utf-8") as fh:
for r in (json.load(fh).get("resolutions") or []):
decisions[(norm_name(r.get("name", "")), norm_dob(r.get("dob", "")))] = r
# FIX-1: use dedup identity (call-name only, zucht stripped) so that e.g.
# a decision written as "von den" matches a merged record with "v.d." spelling.
decisions[(canon_pair(r.get("name", ""))[0], norm_dob(r.get("dob", "")))] = r
except (OSError, ValueError):
return 0
if not decisions:
@@ -927,7 +936,7 @@ def apply_conflict_decisions(merged, conflicts, path):
resolved = 0
for a in merged:
d = decisions.get((norm_name(a["name"]), norm_dob(a["dob"])))
d = decisions.get((canon_pair(a["name"])[0], norm_dob(a["dob"])))
if not d:
continue
a["resolvedByDecision"] = True