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")