Merge feature/gen-3b: genotype.py+extract.py normalization (Uw→G, Sls, flags), conflicts 32→27, Gerbil.IsDeaf additive migration [god-QA pending]

This commit is contained in:
2026-06-06 10:32:57 +02:00
13 changed files with 1645 additions and 62 deletions

View File

@@ -102,6 +102,23 @@ namespace GerbilManager.Tests
Assert.Equal(2, await db.Litters.CountAsync());
}
[Fact]
public async Task Execute_persists_deaf_flag_and_preserves_sls_and_tags()
{
using var db = NewDb();
await new ImportService(db, _dir, _dir).RunAsync(execute: true);
var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1");
// GEN-3b: deafness is a persisted phenotype flag (NOT a genotype locus).
Assert.True(a1.IsDeaf);
// Sls (2nd spotting locus) + provenance tags are preserved in RawImportData
// (kept out of the 8-locus compact Genotype contract until GEN-3a adopts them).
Assert.Contains("Sls", a1.RawImportData!);
Assert.Contains("WFNZ", a1.RawImportData!);
// and Sls must NOT leak into the compact 8-locus genotype string
Assert.DoesNotContain("Sl", a1.Genotype!);
}
[Fact]
public void ComposeGenotype_strips_carets_and_fills_missing_loci()
{
@@ -139,7 +156,8 @@ namespace GerbilManager.Tests
[
{"id":"a1","name":"Kind Eins","dob":"01.02.2020","death":"","gender":null,
"farbschlag":"Agouti","farbschlagVariants":["Agouti"],
"genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"]},"rawGenotype":"aa CC D- ee[f]","unmappedTokens":[]},
"genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"],"Sls":["Sl","sl"]},"rawGenotype":"aa CC D- ee[f] WP dea WFNZ","unmappedTokens":[]},
"deaf":true,"tags":["WFNZ"],
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
"litterRef":{"litterId":"L1","method":"geburtsdatum+eltern","confidence":"hoch"}},
{"id":"a2","name":"Streit","dob":"01.01.2019","death":"","gender":null,

View File

@@ -27,7 +27,8 @@ namespace GerbilManagerWebAPI.Dtos
string? ExternalRef,
string? OriginBreeder,
List<string> CharacterTraits,
string? CharacterNote);
string? CharacterNote,
bool? IsDeaf);
public record LitterDto(
Guid Id,
@@ -75,7 +76,8 @@ namespace GerbilManagerWebAPI.Dtos
string? ExternalRef,
string? OriginBreeder,
List<string>? CharacterTraits,
string? CharacterNote);
string? CharacterNote,
bool? IsDeaf);
public record LitterInput(
string Name,

View File

@@ -97,12 +97,13 @@ namespace GerbilManagerWebAPI.Endpoints
g.OriginBreeder = i.OriginBreeder;
g.CharacterTraits = i.CharacterTraits ?? new List<string>();
g.CharacterNote = i.CharacterNote;
g.IsDeaf = i.IsDeaf;
}
internal static GerbilDto ToDto(Gerbil g) => new(
g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath,
g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder,
g.CharacterTraits, g.CharacterNote);
g.CharacterTraits, g.CharacterNote, g.IsDeaf);
}
}

View File

@@ -21,6 +21,11 @@ namespace GerbilManagerWebAPI.Import
public List<string> SourceFiles { get; set; } = new();
public bool Conflict { get; set; }
public SourceLitterRef? LitterRef { get; set; }
// GEN-3b normalization: hearing/deaf phenotype flag (null = not stated) and
// provenance/breeding tags (WFNZ/RV/GV/DP) — neither is genotype.
public bool? Deaf { get; set; }
public List<string> Tags { get; set; } = new();
}
public sealed class SourceGenotype

View File

@@ -174,6 +174,7 @@ namespace GerbilManagerWebAPI.Import
LitterId = litterId,
ColorVarietyId = colorVarietyId,
Genotype = ComposeGenotype(a.Genotype),
IsDeaf = a.Deaf,
ImportSource = ImportSourceTag,
ExternalRef = a.Id,
OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(),
@@ -181,6 +182,11 @@ namespace GerbilManagerWebAPI.Import
{
a.Genotype.RawGenotype,
a.Genotype.UnmappedTokens,
// GEN-3b: Sls (2nd spotting locus) preserved here until Kevin's GEN-3a
// parser adopts it into the compact Genotype contract; tags + deaf too.
Sls = a.Genotype.Mapped8locus.TryGetValue("Sls", out var sls) ? sls : null,
a.Tags,
a.Deaf,
a.Zucht,
a.SourceFiles,
FarbschlagRaw = a.Farbschlag,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GerbilManagerWebAPI.Migrations
{
/// <inheritdoc />
public partial class AddGerbilDeafFlag : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsDeaf",
table: "Gerbils",
type: "boolean",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsDeaf",
table: "Gerbils");
}
}
}

View File

@@ -781,6 +781,9 @@ namespace GerbilManagerWebAPI.Migrations
b.Property<string>("ImportSource")
.HasColumnType("text");
b.Property<bool?>("IsDeaf")
.HasColumnType("boolean");
b.Property<Guid?>("LitterId")
.HasColumnType("uuid");

View File

@@ -67,6 +67,12 @@ namespace GerbilManagerWebAPI.Models
/// <summary>FEAT-14: free-text character note; feeds the AI Verkaufstext.</summary>
public string? CharacterNote { get; set; }
/// <summary>GEN-3b: hearing/deaf phenotype flag (NOT a genotype locus — it's the
/// downstream effect of high white load / Sp×Sls). null = not stated, true = deaf
/// (dea/taub), false = hearing (Dea/hörend). Set by the FEAT-8 import from the
/// after-spsp deafness annotation; see hive/agents/god/GENETIK-notation.md.</summary>
public bool? IsDeaf { get; set; }
}
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>

View File

@@ -227,6 +227,8 @@ def extract_stammbaum(path):
"gender": None,
"farbschlag": farbschlag,
"genotype": genodict,
"deaf": genodict.get("deaf"),
"tags": genodict.get("tags", []),
"breeder": breeder,
"zucht": zraw,
"parentRefs": [],
@@ -251,7 +253,8 @@ def extract_stammbaum(path):
animals.append({
"id": None, "name": part, "nameVariants": [],
"dob": "", "death": "", "gender": None, "farbschlag": "",
"genotype": gt.parse(""), "breeder": "", "zucht": zraw,
"genotype": gt.parse(""), "deaf": None, "tags": [],
"breeder": "", "zucht": zraw,
"parentRefs": [], "photos": [], "sourceFiles": [fname],
"_gen": gen_of(c), "_col": c, "_row": r, "_file": fname,
"_zucht": norm_zucht(zraw),
@@ -472,10 +475,17 @@ def _to_int(s):
# ------------------------------------------------------------- stage 2: dedup
def _geno_key(genodict):
"""Canonical, order-independent key of a genotype's mapped loci — used for conflict
detection so Uw==G (and allele ordering) no longer count as a conflict."""
m = genodict.get("mapped8locus", {})
return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m))
def dedup(animals):
"""Merge by normalise(call-name)+DOB, with the canonical Zucht as
DISCRIMINATOR (Julian: same name+DOB but different Zucht = different
animal). Returns (merged, conflicts, orphans, zucht_splits)."""
DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht =
different animal). Returns (merged, conflicts, orphans, zucht_splits)."""
groups = {}
orphans = []
for a in animals:
@@ -521,19 +531,26 @@ def dedup(animals):
photos = list(base["photos"])
parent_refs = list(base["parentRefs"])
genos = set()
geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text
farb = set()
deaths = set()
deaf_seen = set()
tags_set = set()
for a in grp:
variants.add(a["name"])
files.update(a["sourceFiles"])
photos.extend(a["photos"])
parent_refs.extend(a["parentRefs"])
if a["genotype"]["rawGenotype"]:
if a["genotype"]["mapped8locus"]:
genos.add(a["genotype"]["rawGenotype"])
geno_keys.add(_geno_key(a["genotype"]))
if a["farbschlag"]:
farb.add(a["farbschlag"])
if a["death"]:
deaths.add(norm_dob(a["death"]))
if a.get("deaf") is not None:
deaf_seen.add(a["deaf"])
tags_set.update(a.get("tags", []))
# pick the richest genotype (most mapped loci, then longest raw)
best = max((a["genotype"] for a in grp),
key=lambda gd: (len(gd["mapped8locus"]), len(gd["rawGenotype"])))
@@ -554,13 +571,16 @@ def dedup(animals):
"photos": sorted(set(photos)),
"sourceFiles": sorted(files),
"mentions": len(grp),
# GEN-3b: hearing/deaf phenotype flag (deaf wins if any mention says so) + tags.
"deaf": (True if True in deaf_seen else (False if False in deaf_seen else None)),
"tags": sorted(tags_set),
# FEAT-8c: machine-readable quarantine marker so the API loader can skip
# conflicting records without parsing the German review report.
"conflict": False,
}
merged.append(out)
# conflict: same animal, disagreeing genotype or farbschlag or death
if len(genos) > 1 or len(farb) > 1 or len(deaths) > 1:
# conflict: same animal, disagreeing NORMALIZED genotype (Uw==G) or farbschlag or death
if len(geno_keys) > 1 or len(farb) > 1 or len(deaths) > 1:
out["conflict"] = True
conflicts.append({
"id": out["id"], "name": base["name"], "dob": out["dob"],

View File

@@ -1,20 +1,26 @@
"""Parse the breeder's free-text genotype notation into our frozen 8-locus
contract while losing nothing (FEAT-8b ruling from god):
"""Parse the breeder's free-text genotype notation into our locus model while
losing nothing (FEAT-8b + GEN-3b normalization, per hive/agents/god/GENETIK-notation.md):
- mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re
- rawGenotype : the verbatim source string
- unmappedTokens: tokens we couldn't map (Uw/Sls/Dea, markers like WFNZ/WP/DP, …)
- mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re (+ Sls when present)
- rawGenotype : the verbatim source string
- unmappedTokens: tokens we still couldn't place
- deaf : True (dea/taub) | False (Dea/hörend) | None (not stated) — phenotype FLAG, not a locus
- tags : provenance/breeding markers (WFNZ/RV/GV/DP/extern …) — never genotype
GEN-3b normalizations (wife + research confirmed):
- Uw/uw == G/g (international vs German notation for the SAME locus) -> aliased to G/g.
- Sls/WP is a SECOND spotting locus (S(l)s(l) = WP/Minimalschecke het). WP -> Sls het.
- Dea/dea/taub -> hearing/deaf flag (written after spsp), NOT a Punnett locus.
- WFNZ/RV/GV -> provenance/breeding tags, NOT genotype, NOT conflict-bearing.
Conventions in the source data:
- allele superscripts are bracketed: c[chm] -> c^chm, c[h] -> c^h, e[f] -> e^f
- a single '-' for the second allele means "unknown" -> mapped to '?'
(frozen-contract wildcard; assumption pending the wife's confirmation)
"""
import re
LOCI = ["A", "C", "D", "E", "G", "P", "Sp", "Re"]
# locus -> regex that matches that locus's token (longest alternatives first)
_LOCUS_TOKEN = {
"Sp": re.compile(r"^(Sp|sp)(Sp|sp|-)?$"),
"Re": re.compile(r"^(Re|re)(Re|re|-)?$"),
@@ -25,12 +31,7 @@ _LOCUS_TOKEN = {
"G": re.compile(r"^(G|g)(G|g|-)?$"),
"P": re.compile(r"^(P|p)(P|p|-)?$"),
}
# loci our model does NOT have but the data uses
_KNOWN_UNMAPPED = re.compile(r"^(Uw|uw)(\[d\])?(Uw|uw)?(\[d\])?$|^(Sls|sls|Dea|dea)$", re.I)
_MARKER = re.compile(r"^\[?(WFNZ|WP|DP|GV|RV)\]?$|^\((taub|hörend|hoerend|RV|GV|extern[^)]*)\)$", re.I)
# one allele unit per locus (longest-match alternatives first); '-' = unknown
_ALLELE_UNIT = {
"Sp": re.compile(r"Sp|sp|-"),
"Re": re.compile(r"Re|re|-"),
@@ -42,10 +43,41 @@ _ALLELE_UNIT = {
"P": re.compile(r"[Pp]|-"),
}
# Provenance/breeding tags (never genotype): Wildfangnachzucht, Rückverpaarung,
# Geschwisterverpaarung, DarkPatch, external origin.
_TAG = re.compile(r"^\[?(WFNZ|RV|GV|DP)\]?$|^\((RV|GV|extern[^)]*)\)$", re.I)
def _rewrite_uw(token):
"""Uw/uw notation -> G/g (same locus). 'Uwuw[d]' -> 'Gg', 'UwUw' -> 'GG', 'uw[d]uw[d]' -> 'gg'."""
if "uw" not in token.lower():
return token
return token.replace("uw[d]", "g").replace("Uw", "G").replace("uw", "g")
def _sls_alleles(token):
"""Sls (second spotting locus) alleles, or None. WP == Sls het (Minimalschecke);
S(l)S(l) homozygous = lethal. Allele symbols: 'Sl' / 'sl'."""
n = token.strip("[]").replace("(l)", "l").replace("(L)", "l")
if n in ("WP", "Sls"):
return ["Sl", "sl"] # heterozygous (WP phenotype)
if n.lower() == "sls":
return ["sl", "sl"] # wild-type (no extra spotting)
units = re.findall(r"Sl|sl", n)
return units if len(units) == 2 else None
def _deaf_value(token):
"""dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None. Case-sensitive for Dea/dea."""
t = token.strip("()[]")
if t == "dea" or t.lower() == "taub":
return True
if t == "Dea" or t.lower() in ("hörend", "hoerend"):
return False
return None
def _alleles_for(locus, token):
"""Extract the (allele1, allele2) pair from a single locus token, handling
two-letter alleles (Sp/Re) and bracketed superscripts (c[chm] -> c^chm)."""
pat = _ALLELE_UNIT.get(locus)
units = pat.findall(token) if pat else re.findall(r"[A-Za-z](?:\[[a-z]+\])?|-", token)
alleles = []
@@ -55,7 +87,6 @@ def _alleles_for(locus, token):
else:
m = re.match(r"([A-Za-z]+)\[([a-z\-]+)\]", u)
if m:
# [-] = sub-allele unknown -> keep the base letter only
alleles.append(m.group(1) if m.group(2) == "-" else f"{m.group(1)}^{m.group(2)}")
else:
alleles.append(u)
@@ -65,37 +96,60 @@ def _alleles_for(locus, token):
def parse(raw):
"""raw: a genotype string (may include trailing free text/markers).
"""raw: a genotype string (may include trailing markers/flags).
Returns dict {mapped8locus, rawGenotype, unmappedTokens}.
Returns {mapped8locus, rawGenotype, unmappedTokens, deaf, tags}.
"""
raw = (raw or "").strip()
mapped = {}
unmapped = []
# tokenise on whitespace; keep order
tags = []
deaf = None
for tok in raw.split():
t = tok.strip().rstrip(",")
if not t:
continue
# GEN-3b: Uw/uw is an alias of the G locus — rewrite before matching.
t = _rewrite_uw(t)
# 8 standard loci
matched = False
for locus in LOCI:
pat = _LOCUS_TOKEN.get(locus)
if pat and pat.match(t):
if locus not in mapped: # first occurrence wins
mapped[locus] = _alleles_for(locus, t)
mapped.setdefault(locus, _alleles_for(locus, t)) # first occurrence wins
matched = True
break
if matched:
continue
if _KNOWN_UNMAPPED.match(t) or _MARKER.match(t):
unmapped.append(t)
else:
# anything else (stray notes, malformed tokens) -> unmapped, nothing lost
unmapped.append(t)
# Sls (second spotting locus); WP is its heterozygous phenotype
sls = _sls_alleles(t)
if sls is not None:
mapped.setdefault("Sls", sls)
continue
# deafness flag (after spsp): dea/taub vs Dea/hörend
d = _deaf_value(t)
if d is not None:
deaf = d
continue
# provenance/breeding tags
if _TAG.match(t):
tags.append(re.sub(r"[()\[\]]", "", t).upper())
continue
unmapped.append(t)
return {
"mapped8locus": mapped,
"rawGenotype": raw,
"unmappedTokens": unmapped,
"deaf": deaf,
"tags": tags,
}
@@ -103,7 +157,7 @@ def looks_like_genotype(text):
"""Heuristic: does this cell text contain >=3 recognisable locus tokens?"""
n = 0
for tok in text.split():
t = tok.rstrip(",")
t = _rewrite_uw(tok.rstrip(","))
if any(p.match(t) for p in _LOCUS_TOKEN.values()):
n += 1
return n >= 3

View File

@@ -4,15 +4,15 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
## Überblick
- Rohe Tier-Einträge aus den Stammbäumen: **889**
- Nach Zusammenführung (eindeutige Tiere): **574**
- davon mit Geburtsdatum: 279
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 146
- Konflikte zur Klärung: **32**
- Rohe Tier-Einträge aus den Stammbäumen: **950**
- Nach Zusammenführung (eindeutige Tiere): **622**
- davon mit Geburtsdatum: 327
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
- Konflikte zur Klärung: **27**
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
- Fotos zugeordnet: **123**
- Fotos zugeordnet: **137**
- Würfe aus der Wurfchronik: **752**
- Tiere mit Wurf verknüpft: **135** (davon über Geburtsdatum **und** Eltern: 95, nur über Geburtsdatum: 40; mehrdeutig: 9)
- Tiere mit Wurf verknüpft: **159** (davon über Geburtsdatum **und** Eltern: 110, nur über Geburtsdatum: 49; mehrdeutig: 10)
- Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema)
## Zusammenführungs-Schlüssel
@@ -28,19 +28,15 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
| Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
| ZoneFire | 07.12.2020 | Aa c[chm]c[chm] D- Ee Gg P- Spsp | CP-Agouti Kragenschecke // Kalea von den Kleinen Chaoten | — | Stammbaum von Akio Kids |
| Louis von den Kleinen Chaoten | 15.07.2017 | Aa Cc[] D- Ee Gg P- spsp // Aa Cc[chm] D- Ee Uwuw[d] P- spsp | Roswitha von den Kleinen Chaoten | 01.07.2020 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
| Roswitha von den Kleinen Chaoten | 10.09.2018 | aa CC D- ee[f] Gg P- spsp // aa CC D- ee[f] Uwuw[d] P- spsp | — | 05.08.2021 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
| Firefly von den Kleinen Chaoten | 18.12.2019 | /+, Aa c[chm]c[chm] D- Ee Gg PP Spsp // Aa c[chm]c[chm] DD Ee Gg PP Spsp | — | 2024 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
| Zuleika von den Kleinen Chaoten | 24.10.2015 | aa c[chm]c[h] D- E G P- spsp // aa c[chm]c[h] D- Ee Gg P- spsp // aa c[chm]c[h] DD Ee Gg P- spsp | — | 24.02.2019 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
| WildFire von den Kleinen Chaoten | 05.10.2017 | aa c[chm]c[chm] D- Ee gg P- spsp // aa c[chm]c[chm] D- Ee gg PP spsp | — | — | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
| Vestra von den Schlossmäusen | 08.02.2019 | Aa Cc[chm] D- EE GG PP Spsp [WP] // Aa Cc[chm] DD EE GG PP Spsp [WP] | — | 26.05.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
| Flint von den Kleinen Chaoten | 23.12.2017 | aa Cc[chm] D- ee Gg P- spsp | — | 10.05.2021 // 10.05.2022 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
| Silenos gen. Adonis v.d. Kleinen Chaoten | 11.10.2015 | aa Cc[chm] D- Ee Gg PP spsp // aa Cc[chm] D- Ee Uwuw[d] PP spsp | — | 18.07.2019 | Stammbaum von Akio Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Watarus Kids |
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
| Bruno of Black Forest | 01.06.2022 | aa C- dd Ee Gg P- spsp | Blau // Mystique of Black Forest | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
| Milka of LennyLengo | 09.12.2018 | aa C- dd E- Gg P- Spsp // aa Cc[h] dd EE Gg P- Spsp | — | 22.12.2021 | Stammbaum von Alberto Kids, Stammbaum von Stella Kids |
| Hedwig of BGB | 30.10.2019 | aa CC DD E- G- P- Spsp WP // aa CC DD E- G- P- Spsp WP DP (hörend) | — | 30.08.2023 | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
| Silvain von den Kleinen Chaoten | 27.03.2022 | aa c[chm]c[chm] Dd Ee[-] Gg P- Spsp // aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp | — | 31.12.2024 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
| Pitari gen. Piti von den Kleinen Chaoten | 16.05.2021 | Aa CC dd ee Gg P- Spsp DP // Aa CC dd ee Gg P- Spsp [DP] | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
| Brandon Stark von den Kleinen Chaoten | 13.12.2017 | aa Cc[chm] D- Ee Gg P- spsp // aa Cc[chm] D- Ee Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
| Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
| Little Hero of Black Forest | 22.02.2018 | AA CC DD EE GG PP [WFNZ] // AA CC DD EE GG PP spsp [WFNZ] | — | 18.06.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids, Stammbaum von Valentino Firehearts Kids |
| Molly of Black Forest | 13.09.2021 | /+, Aa Cc[chm] D- Ee gg P- spsp // Aa Cc[chm] Dd Ee gg Pp spsp | — | 03.05.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
@@ -52,11 +48,10 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
| Chayton v.d. Kleinen Chaoten (extern SC) | 04.02.2022 | aa Cc[-] D- e[f]e[f] Gg Pp spsp | Orangeschimmel, hell // Victoria Welby gen. Welby v.d. Kleinen Chaoten | 30.04.2024 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
| Victoria Welby gen. Welby v.d. Kleinen Chaoten | 16.01.2023 | Aa CC D- Ee[f] Gg pp Spsp [DP] // Aa CC D- ee[f] Gg pp Spsp [DP] | Goldfuchsschimmel Punktschecke DP | 17.02.2026 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
| Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | Belica gen. Emi von den Kleinen Chaoten | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
| Chelsea von den Kleinen Chaoten | 02.04.2021 | /+, Aa CC Dd ee gg Pp spsp // Aa CC Dd ee gg Pp spsp | — | — | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Valentino Firehearts Kids |
| Chesnut | 13.11.2019 | aa C- D- ee[f] GG PP spsp | Kohlfuchsschimmel // Tennessee von den Kleinen Chaoten | 22.11.2023 | Stammbaum von Kentucky |
| Ethan von den Kleinen Chaoten | 09.07.2020 | Aa Cc[chm] D- ee[f] Gg Pp Spsp | Ichika von den Kleinen Chaoten // Orangeschimmel, hell Kragenschecke | 30.07.2024 | Stammbaum von Kentucky, Stammbaum von Watarus Kids |
| Quied Soldier of Black Forest | 07.06.2018 | /+, Aa C- D- ee[f] GG Pp Spsp [DP] // Aa C- D- ee[f] GG Pp Spsp DP | Hoshi von den Kleinen Chaoten | — | Stammbaum von Kentucky |
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
| Skarlett v.d. Kleinen Chaoten | 14.07.2013 | / +2018, Aa Cc[chm] DD ee uw[d]uw[d] PP spsp // Aa Cc[chm] DD ee uw[d]uw[d] PP spsp | — | 17.04.2016 // 2018 | Stammbaum von Vance |
## Mehrdeutige / unvollständige Einträge
@@ -105,7 +100,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
## Wahrscheinliche Zuordnungen unvollständiger Einträge
38 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung):
39 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung):
- „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019)
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
@@ -137,6 +132,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
- „Zadar from Zeko i ptica, Croatia“ → Zadar from Zeko i ptica, Croatia (*12.04.2019)
- „Living Force's Vally“ → Living Force's Vally (*01.11.2014)
- „Pinto of Fiomi“ → Pinto of Fiomi (*28.08.2016)
- „Hanse Renner's Poseidon“ → Hanse Renner's Poseidon (*14.08.2014)
- „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019)
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
- „Lilo of LennyLengo“ → Lilo of LennyLengo (*04.11.2018)
@@ -152,29 +148,21 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
| Token | Vorkommen | Bedeutung (Vermutung) |
|---|---|---|
| `[DP]` | 15 | Marker (Dunkelpigment?) |
| `[WFNZ]` | 13 | Marker |
| `DP` | 9 | Marker |
| `/+` | 8 | ? |
| `WP` | 7 | Marker |
| `Uwuw[d]` | 4 | 9. Locus Uw (nicht im Modell) |
| `-g` | 2 | ? |
| `C(C)` | 2 | Schreibweise (C trägt c) |
| `[WP]` | 2 | Marker |
| `chmchm` | 2 | Schreibweise (c[chm]c[chm]) |
| `Cc[]` | 1 | ? |
| `-psp` | 1 | ? |
| `G(G)` | 1 | ? |
| `UwUw` | 1 | 9. Locus Uw |
| `uw[d]uw[d]` | 1 | ? |
| `[DP` | 1 | ? |
| `/` | 1 | ? |
| `+2018` | 1 | ? |
| `c[chm]chm]` | 1 | ? |
| `Dea/dea]` | 1 | ? |
| `DD-Tumor` | 1 | ? |
| `bei` | 1 | ? |
| `Geschwistern` | 1 | ? |
| `C-D-` | 1 | ? |
| `Sls` | 1 | ? |
| `(hörend)` | 1 | ? |
| `-DD` | 1 | ? |
## Wurfchronik — Datenqualitäts-Hinweise

View File

@@ -0,0 +1,71 @@
"""Zero-dep tests for genotype.py GEN-3b normalization.
Run: python test_genotype.py (exit 0 = all pass)
Covers: Uw/uw -> G/g alias, Sls/WP second spotting locus, dea/Dea/taub
hearing-deaf flag, WFNZ/RV/GV/DP provenance tags. Per hive/agents/god/GENETIK-notation.md.
"""
import sys
import genotype as g
def check(name, cond):
if not cond:
print(f"FAIL: {name}")
check.failed += 1
else:
print(f"ok: {name}")
check.failed = 0
# --- Uw/uw == G/g (same locus) ---
r = g.parse("aa Cc Dd Ee Uwuw Pp spsp rere")
check("Uw->G: G locus mapped", r["mapped8locus"].get("G") == ["G", "g"])
check("Uw->G: nothing left in unmapped", r["unmappedTokens"] == [])
r = g.parse("UwUw")
check("UwUw -> GG", r["mapped8locus"].get("G") == ["G", "G"])
r = g.parse("uwuw")
check("uwuw -> gg", r["mapped8locus"].get("G") == ["g", "g"])
r = g.parse("uw[d]uw[d]")
check("uw[d]uw[d] -> gg (dense underwhite)", r["mapped8locus"].get("G") == ["g", "g"])
# Gg and Uwuw must produce the SAME mapped locus (so they stop being a conflict)
check("Gg identical to Uwuw at G locus",
g.parse("Gg")["mapped8locus"]["G"] == g.parse("Uwuw")["mapped8locus"]["G"])
# --- Sls / WP second spotting locus ---
check("WP -> Sls het", g.parse("WP")["mapped8locus"].get("Sls") == ["Sl", "sl"])
check("[WP] (bracketed) -> Sls het", g.parse("[WP]")["mapped8locus"].get("Sls") == ["Sl", "sl"])
check("Sls token -> Sls het", g.parse("Sls")["mapped8locus"].get("Sls") == ["Sl", "sl"])
check("sls -> Sls wild", g.parse("sls")["mapped8locus"].get("Sls") == ["sl", "sl"])
check("S(l)s(l) -> Sl,sl", g.parse("S(l)s(l)")["mapped8locus"].get("Sls") == ["Sl", "sl"])
# Sp and Sls are TWO distinct loci on the same animal (Superschecke)
r = g.parse("spsp WP")
check("Sp + Sls coexist (two spotting loci)",
r["mapped8locus"].get("Sp") == ["sp", "sp"] and r["mapped8locus"].get("Sls") == ["Sl", "sl"])
# --- deafness flag (after spsp), case-sensitive ---
check("dea (lower) -> deaf True", g.parse("spsp dea")["deaf"] is True)
check("taub -> deaf True", g.parse("taub")["deaf"] is True)
check("Dea (upper) -> hearing False", g.parse("spsp Dea")["deaf"] is False)
check("(hörend) -> hearing False", g.parse("(hörend)")["deaf"] is False)
check("no deaf token -> None", g.parse("aa Cc")["deaf"] is None)
# deafness is NOT a genotype locus and must not pollute mapped/unmapped silently
check("deaf flag not in unmapped", "dea" not in g.parse("spsp dea")["unmappedTokens"])
# --- provenance / breeding tags (never genotype, never conflict) ---
check("WFNZ -> tag", g.parse("aa WFNZ")["tags"] == ["WFNZ"])
check("RV -> tag", g.parse("(RV)")["tags"] == ["RV"])
check("GV -> tag", g.parse("(GV)")["tags"] == ["GV"])
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"] == [])
# --- looks_like_genotype recognizes Uw-bearing cells ---
check("looks_like_genotype sees Uw as G",
g.looks_like_genotype("aa Cc Uwuw") is True)
if check.failed:
print(f"\n{check.failed} test(s) FAILED")
sys.exit(1)
print("\nALL PASS")