PEDIGREE-LINK: chart parentRefs→litters, box-colour=sex, name-bleed fix

Structural fix (god, Julian-reported via 'C'): the loader ignored
SourceAnimal.ParentRefs, so animals whose ancestry exists only as
Stammbaum chart-position refs loaded with LitterId=null ("unbekannt").
ImportService now synthesizes/reuses a derived litter from parentRefs:
resolves father+mother via name+DOB, groups siblings (same parents+dob)
into one litter, sets Father/Mother + offspring LitterId, dates it to the
offspring DOB, and tags Notes "aus Stammbaum-Diagramm abgeleitet
(Konfidenz: …)" so it's transparent/reversible. Existing animals that
become linkable are re-linked on re-run (sweep-idempotent). Dry-run counts
included. Projected: ~124 loadable animals gain a parent link.

Box-colour = sex (Julian): blue box = male, white box = female. All 11
pedigrees encode this as a solid theme-8 (accent5/blue) fill vs no fill.
xlsx_util.cell_fill_sex reads it; extract.py sets animal.gender from the
box; ImportService.InferGender prefers it over sire/dam name inference.
Result: 306/306 loadable animals now sexed (154♂/152♀).

Extractor noise fix (god): reject Farbschlag values that are actually a
parent NAME bled across cells (contain v.d./von/of/gen.) — cleared phantom
conflicts (e.g. Chayton). Combined with GEN-3 Uw→G: Konflikte 32→21.
Also skip Excel "~$" lock files in the glob.

GEN-3a contract (Kevin): ComposeGenotype appends "Slsl" for WP/Sls
carriers (wild-type sl/sl omitted) so 8-locus strings stay unchanged.

Importer-only. The live re-import into Julian's DB stays a separate
supervised gated step. 95 C# tests + python genotype tests green;
has-pending-model-changes clean (no schema change on this branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 10:48:30 +02:00
parent a57b481a7e
commit ea79703dbf
6 changed files with 304 additions and 58 deletions

View File

@@ -20,6 +20,7 @@ import glob
import shutil
import argparse
import unicodedata
from collections import Counter
import xlsx_util as xu
import genotype as gt
@@ -96,6 +97,18 @@ ZUCHT_ALIASES = {
"zdkc": "kleinechaote", # "Zucht der kleinen Chaoten" (home cattery shorthand)
}
# A Farbschlag value must NOT contain cattery/line connectors (v.d./von/of/gen.) — when it
# does, a parent's NAME has bled into the Farbschlag cell (cross-cell chart read, PEDIGREE-LINK
# bug: e.g. "Victoria Welby gen. Welby v.d. Kleinen Chaoten" became a Farbschlag variant and
# spawned a phantom conflict). Reject such values so they don't pollute farbschlag/conflicts.
_NAME_MARKER = re.compile(r"\bv\.\s?d\.|\bvon\b|\bof\b|\bgen\.", re.IGNORECASE)
def looks_like_animal_name(text):
"""True if a candidate Farbschlag cell actually looks like an animal name (has a
cattery/line connector). Real Farbschläge are short colour words without these."""
return bool(_NAME_MARKER.search(text or ""))
def split_name_zucht(raw):
"""'Luna [ZdkC]' -> ('Luna','ZdkC'); 'Pikachu of Black Forest' ->
@@ -159,6 +172,7 @@ def extract_stammbaum(path):
ss = xu.shared_strings(z)
sheets = xu.sheet_paths(z)
cells = xu.read_cells(z, sheets[0], ss)
fillsex = xu.cell_fill_sex(z, sheets[0]) # box colour -> sex (blue=male, white=female)
# group cells by column for block reconstruction
by_col = {}
@@ -207,7 +221,8 @@ def extract_stammbaum(path):
elif re.search(r"\b(Zucht|Privatzucht)\b", cell) or cell.startswith("("):
breeder = cell
used.add((c, rr))
elif not farbschlag and not re.match(r"^\*?\s?\d", cell):
elif not farbschlag and not re.match(r"^\*?\s?\d", cell) \
and not looks_like_animal_name(cell):
farbschlag = cell
used.add((c, rr))
used.add((c, r))
@@ -224,7 +239,7 @@ def extract_stammbaum(path):
"nameVariants": [],
"dob": dob,
"death": death,
"gender": None,
"gender": fillsex.get((c, r)), # box colour: blue=male, white=female
"farbschlag": farbschlag,
"genotype": genodict,
"deaf": genodict.get("deaf"),
@@ -536,11 +551,14 @@ def dedup(animals):
deaths = set()
deaf_seen = set()
tags_set = set()
genders = []
for a in grp:
variants.add(a["name"])
files.update(a["sourceFiles"])
photos.extend(a["photos"])
parent_refs.extend(a["parentRefs"])
if a.get("gender"):
genders.append(a["gender"])
if a["genotype"]["mapped8locus"]:
genos.add(a["genotype"]["rawGenotype"])
geno_keys.add(_geno_key(a["genotype"]))
@@ -560,7 +578,8 @@ def dedup(animals):
"nameVariants": sorted(v for v in variants if v),
"dob": norm_dob(base["dob"]),
"death": sorted(deaths)[0] if deaths else "",
"gender": None,
# box-colour sex (blue=male, white=female): majority across mentions, else None.
"gender": Counter(genders).most_common(1)[0][0] if genders else None,
"farbschlag": sorted(farb)[0] if farb else "",
"farbschlagVariants": sorted(farb),
"genotype": best,
@@ -829,7 +848,9 @@ def main():
os.makedirs(OUT, exist_ok=True)
raw_animals = []
files = sorted(glob.glob(os.path.join(args.stammbaeume, "*.xlsx")))
# Skip Excel lock/owner files ("~$...") that appear while a workbook is open.
files = sorted(f for f in glob.glob(os.path.join(args.stammbaeume, "*.xlsx"))
if not os.path.basename(f).startswith("~$"))
print(f"Stammbaum-Dateien: {len(files)}")
for path in files:
got = extract_stammbaum(path)