From 4aca1d528b85ce9a4a1e9f386cc72dbebb6260c8 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 11:49:45 +0200 Subject: [PATCH] EXTRACT band-aware Farbschlag: deep bands (gen>=2) have no colour cell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Julian-confirmed against the real xlsx (Kentucky, Akio Kids): pedigree blocks have two shapes. EARLY bands (gen 0-1, cols B/E/H) are 5-cell Name/DOB/Farbschlag/Genotype/Zucht — real Farbschlag present. DEEP bands (gen>=2, cols K/N/Q...) are 3-cell Name/DOB/Genotype with NO Farbschlag — the colour is derived from the genotype. The old extractor grabbed the NEXT block's name or a stray health note (e.g. "DD-Tumor", "Chronische Ohrenentzündung...") as the deep-band Farbschlag — source of the D1 'name-in-Farbschlag' conflicts and a big chunk of farbschlagUnmatched. Fix: gen_of(col)>=2 => never assign a Farbschlag in that block (Name/DOB/Genotype only); the cell is left for the next block's name search. Deep-band animals load with empty Farbschlag and the UI computes 'Errechnet' from the stored genotype (lossless). Dedup already prefers a real (non-empty) early-band Farbschlag, so an animal appearing in both an early band (Chesnut -> 'Kohlfuchsschimmel') and deep bands keeps the real value; Chesnut and Tennessee stay separate (distinct genotype + name+DOB key). Result: 248/306 loadable animals now have empty Farbschlag (genotype- derived), 0 suspicious name/note values remain, 0 Farbschlag-based conflicts (D1 dissolved). Regression test test_extract.py builds a two-band xlsx and asserts the deep band yields no Farbschlag + the stray note doesn't bleed. Extractor-only (no C# change). python test_genotype + test_extract green; dotnet 118/118. Batches with Kevin's GEN-3f seed for one re-import. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/import/extract.py | 7 ++- tools/import/test_extract.py | 83 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tools/import/test_extract.py diff --git a/tools/import/extract.py b/tools/import/extract.py index 24e9cb9..ffe43ce 100644 --- a/tools/import/extract.py +++ b/tools/import/extract.py @@ -211,6 +211,11 @@ def extract_stammbaum(path): farbschlag = "" geno = geno0 breeder = "" + # BAND-AWARE (Julian-confirmed): early bands (gen 0-1, cols B/E/H) are 5-cell blocks + # WITH a Farbschlag cell; deep bands (gen >= 2, cols K/N/Q...) are 3-cell blocks + # (Name/DOB/Genotype) with NO Farbschlag — colour is derived from the genotype. So in + # deep bands we must NOT grab the next block's name or a stray health note as Farbschlag. + deep_band = gen_of(c) >= 2 for rr in range(r + 1, r + 4): cell = cells.get((c, rr)) if not cell: @@ -221,7 +226,7 @@ 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 deep_band and not farbschlag and not re.match(r"^\*?\s?\d", cell) \ and not looks_like_animal_name(cell): farbschlag = cell used.add((c, rr)) diff --git a/tools/import/test_extract.py b/tools/import/test_extract.py new file mode 100644 index 0000000..72c4187 --- /dev/null +++ b/tools/import/test_extract.py @@ -0,0 +1,83 @@ +"""Zero-dep tests for extract.py band-aware Farbschlag + name-bleed guard. + +Run: python test_extract.py (exit 0 = all pass) +Covers (PEDIGREE-LINK / Julian-confirmed): deep pedigree bands (gen >= 2, cols K/N/Q...) +are Name/DOB/Genotype ONLY — no Farbschlag cell — so a stray health note or the next +block's name must NOT be captured as Farbschlag; early bands (gen 0-1) keep their real +Farbschlag. Plus the looks_like_animal_name guard (a parent name must not be a Farbschlag). +""" +import os +import sys +import zipfile +import tempfile +import extract as e + +failed = 0 + + +def check(name, cond): + global failed + print(("ok: " if cond else "FAIL: ") + name) + if not cond: + failed += 1 + + +def _cell(ref, text): + return f'{text}' + + +def _make_xlsx(path, cells): + """cells: {(colLetter+row): text}. Build a minimal single-sheet xlsx (no styles).""" + rows = {} + for ref, text in cells.items(): + r = int("".join(ch for ch in ref if ch.isdigit())) + rows.setdefault(r, []).append(_cell(ref, text)) + body = "".join(f'{"".join(cs)}' for r, cs in sorted(rows.items())) + sheet = ('' + + body + "") + with zipfile.ZipFile(path, "w") as z: + z.writestr("xl/worksheets/sheet1.xml", sheet) + + +# --- band-aware Farbschlag --- +# col E = gen 0 (early, HAS Farbschlag); col K = col 11 = gen 2 (deep, NO Farbschlag). +tmp = os.path.join(tempfile.gettempdir(), "bandtest.xlsx") +_make_xlsx(tmp, { + # early band (E): Name / *DOB / Farbschlag / Genotype + "E10": "Chesnut", + "E11": "*13.11.2019", + "E12": "Kohlfuchsschimmel", + "E13": "aa CC DD ee GG PP spsp rere", + # deep band (K): Name / *DOB / Genotype / stray NOTE (must NOT become Farbschlag) + "K10": "DeepAnimal", + "K11": "*01.01.2020", + "K12": "aa CC DD EE GG PP spsp rere", + "K13": "DD-Tumor", +}) +try: + animals = e.extract_stammbaum(tmp) + by_name = {a["name"]: a for a in animals} + check("early band keeps real Farbschlag", + by_name.get("Chesnut", {}).get("farbschlag") == "Kohlfuchsschimmel") + check("deep band has NO Farbschlag (note not grabbed)", + by_name.get("DeepAnimal", {}).get("farbschlag") == "") + check("deep-band animal still parsed (Name/DOB/Genotype)", + "DeepAnimal" in by_name and by_name["DeepAnimal"]["dob"].startswith("01.01")) +finally: + try: os.remove(tmp) + except OSError: pass + +gen = e.gen_of +check("gen_of: early bands < 2 (E,H)", gen(5) < 2 and gen(8) < 2) +check("gen_of: deep bands >= 2 (K,N,Q)", gen(11) >= 2 and gen(14) >= 2) + +# --- name-bleed guard (a parent name is not a Farbschlag) --- +check("v.d. name rejected", e.looks_like_animal_name("Tennessee von den Kleinen Chaoten")) +check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. Welby v.d. Kleinen Chaoten")) +check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel")) +check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell")) + +if failed: + print(f"\n{failed} test(s) FAILED") + sys.exit(1) +print("\nALL PASS")