fix(genetics): Genotyp-/Farbschlag-Engine-Bugs aus Ticket-Triage

- uw/Uwuw[d] (dichtes Underwhite) wird vor dem uw→G-Locus-Alias korrekt geparst
  (Vance: nicht mehr „Unbekannter Farbschlag", zeigt G-Locus statt international uw). [#34,#32]
- ee[-] = sichtbarer Fuchs (rezessiv homozygot) → korrekt als Fuchs; bare „e-"
  (rezessiv + unbekannt) ist genetisch unmöglich → genotypeInvalid statt still falsch. [#42]
- aa-Colourpoint-Zweig berücksichtigt jetzt D (Dilute) und E (Fuchs), statt fix
  Zobel/Marder/Siam zu erzwingen. [#3]
- Wurf-Farbprognose: unbekanntes Allel (?) wird nicht mehr über die volle Dominanz
  expandiert → keine unmöglichen Dilute-/Schimmel-/„Unbekannt"-Nachkommen. [#37,#39,#40,#41,#38]

Tests: test_genotype.py erweitert; genetics.test.ts +5 Blöcke (135 vitest grün, tsc/eslint sauber).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:04:05 +02:00
parent 5069c0eb1e
commit b437d09312
6 changed files with 182 additions and 26 deletions

View File

@@ -126,7 +126,15 @@ def parse(raw):
for locus in LOCI:
pat = _LOCUS_TOKEN.get(locus)
if pat and pat.match(t):
mapped.setdefault(locus, _alleles_for(locus, t)) # first occurrence wins
alleles = _alleles_for(locus, t)
# #42 (E-locus e-dash): Fuchs (e) is RECESSIVE — a visible fox MUST be
# homozygous "ee" (written "ee[-]", which already parses to [e, e]).
# A lone recessive "e" with an unknown partner ("e-"/"e[-]" -> [e, ?])
# is genetically impossible; mirror the frontend by REJECTING it
# (leave it unmapped/invalid) instead of mapping a wrong colour.
if locus == "E" and alleles in (["e", "?"], ["?", "e"]):
break # invalid -> falls through to unmappedTokens below
mapped.setdefault(locus, alleles) # first occurrence wins
matched = True
break
if matched:

View File

@@ -61,6 +61,27 @@ check("DP -> tag", g.parse("[DP]")["tags"] == ["DP"])
check("tag not in genotype loci", g.parse("WFNZ")["mapped8locus"] == {})
check("tag not in unmapped", g.parse("aa WFNZ")["unmappedTokens"] == [])
# --- #32/#34: dense-underwhite [d] annotation stripped, maps to G locus (never uw) ---
r = g.parse("Uwuw[d]")
check("Uwuw[d] -> Gg (dense underwhite, mixed case)", r["mapped8locus"].get("G") == ["G", "g"])
check("Uwuw[d] leaves nothing unmapped (no stray uw token)", r["unmappedTokens"] == [])
r = g.parse("aa Cc[chm] D- ee Uwuw[d] PP spsp")
check("Vance genotype: G locus = Gg (display via G, not uw)", r["mapped8locus"].get("G") == ["G", "g"])
check("Vance genotype: E locus = ee (Fuchs)", r["mapped8locus"].get("E") == ["e", "e"])
check("Vance genotype: no unmapped tokens", r["unmappedTokens"] == [])
# --- #42: E-locus e-dash semantics ---
# ee[-] = visible Fuchs -> ee (recessive phenotype implies homozygosity)
check("ee[-] -> ee (visible Fuchs)", g.parse("ee[-]")["mapped8locus"].get("E") == ["e", "e"])
check("E- stays [E, ?] (valid unknown partner)", g.parse("E-")["mapped8locus"].get("E") == ["E", "?"])
# bare e- / e[-] is genetically impossible -> invalid (not mapped, surfaced as unmapped)
check("e- invalid -> not mapped at E", g.parse("e-")["mapped8locus"].get("E") is None)
check("e- invalid -> in unmappedTokens", "e-" in g.parse("e-")["unmappedTokens"])
check("e[-] invalid -> not mapped at E", g.parse("e[-]")["mapped8locus"].get("E") is None)
r = g.parse("Aa CC D- ee[-] Gg Pp spsp")
check("Algierfuchs genotype: E = ee (Fuchs)", r["mapped8locus"].get("E") == ["e", "e"])
check("Algierfuchs genotype: no unmapped tokens", r["unmappedTokens"] == [])
# --- looks_like_genotype recognizes Uw-bearing cells ---
check("looks_like_genotype sees Uw as G",
g.looks_like_genotype("aa Cc Uwuw") is True)