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

@@ -78,6 +78,46 @@ def read_cells(z, sheet_path, ss=None):
return cells
def cell_fill_sex(z, sheet_path):
"""{(colnum, row): 'male' | 'female'} from the cell's box fill colour.
Breeder convention (Julian, 2026-06-06): a BLUE box = männlich (male), a WHITE box =
weiblich (female). In these Stammbaum templates every coloured box uses one solid theme
fill (Office accent5 = blue); unfilled/none cells render white. So: a cell whose style
uses a real solid fill -> 'male'; an unfilled (none/gray125) cell -> 'female'.
"""
try:
st = z.read("xl/styles.xml").decode("utf-8")
except KeyError:
return {}
fills = re.search(r"<fills.*?</fills>", st, re.S)
colored = set()
if fills:
for i, fb in enumerate(re.findall(r"<fill>(.*?)</fill>", fills.group(0), re.S)):
if 'patternType="solid"' in fb and re.search(r"<fgColor\s", fb) and "gray125" not in fb:
colored.add(i) # fillId of a real solid colour (blue)
xfs = re.search(r"<cellXfs.*?</cellXfs>", st, re.S)
idx2fill = {}
if xfs:
for i, xf in enumerate(re.findall(r"<xf\b([^>]*?)/?>", xfs.group(0))):
m = re.search(r'fillId="(\d+)"', xf)
idx2fill[i] = int(m.group(1)) if m else 0
raw = z.read(sheet_path).decode("utf-8")
out = {}
for m in re.finditer(r"<c\s+([^>]*?)>", raw):
a = dict(_ATTR.findall(m.group(1)))
ref = a.get("r")
if not ref:
continue
mm = re.match(r"([A-Z]+)(\d+)", ref)
if not mm:
continue
s = int(a.get("s", "0"))
out[(col_to_num(mm.group(1)), int(mm.group(2)))] = (
"male" if idx2fill.get(s, 0) in colored else "female")
return out
def header_row(cells):
"""Return {colnum: header_text} for the topmost row that has text."""
if not cells: