Merge feature/gen-3d: dominance tiebreak — unknown locus → wild-type reading (unknown-C=full colour not white; markers unmarked) [god-QA pending]

This commit is contained in:
2026-06-06 11:01:04 +02:00
2 changed files with 39 additions and 12 deletions

View File

@@ -379,3 +379,23 @@ describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', ()
expect(farbschlagFor(fromDisplayString('aa C- DD EE GG PP spsp rere')).unknown).toBe(false)
})
})
describe('GEN-3d: dominance tiebreak for unknown loci', () => {
it('unknown-C reads as full-colour, NOT a c^h/c^chm white', () => {
// 'aa C- DD EE GG PP' -> Schwarz (dominant C reading), never PEW/Hermelin/Himalaya.
const name = genotypeToFarbschlag(fromDisplayString('aa C- DD EE GG PP spsp rere'))
expect(name).toBe('Schwarz')
expect(['Pink Eyed White (PEW)', 'Hermelin', 'Himalaya']).not.toContain(name)
})
it('unknown second marker allele defaults UNMARKED, not Schecke (marker-aware)', () => {
// 'sp-' (one sp + unknown) -> sp/sp -> no Schecke (naive most-dominant would wrongly add it).
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD EE GG PP sp- rere'))).toBe('Agouti')
})
it('still: eef with unknowns -> Fuchsschimmel (family pin unaffected by tiebreak)', () => {
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
'Fuchsschimmel',
)
})
})

View File

@@ -20,7 +20,7 @@
* meta rows dropped, 17 matched the frozen names). Genotypes normalized from
* portal notation (c[chm]->cchm, c[h]->ch, e[f]->ef, '-'/'--' = unknown).
*/
import { LOCUS_ORDER, dominantAllele, type LocusKey } from './loci'
import { LOCI, LOCUS_ORDER, dominantAllele, type LocusKey } from './loci'
import {
makeGenotype,
toDisplayString,
@@ -145,13 +145,22 @@ export interface FarbschlagMatch {
}
/**
* Expressed token at a locus, or null when UNKNOWN (a '?' allele) — null acts as
* a wildcard in matching. The E locus is PAIR-aware so the Fuchs/Schimmel family
* is distinguishable: ee->'e', e/ef->'eef' (Fuchsschimmel), ef/ef->'ef' (Schimmel).
* Expressed token at a locus. GEN-3d: an UNKNOWN allele ('?') is resolved to the
* MOST-DOMINANT allele of the locus (the safer default) rather than acting as a
* match-anything wildcard — so an unknown-C animal reads as full-colour 'C', not
* a c^h/c^chm colourpoint white. The E locus stays PAIR-aware so the Fuchs/
* Schimmel family is distinguishable: ee->'e', e/ef->'eef', ef/ef->'ef'.
* (The Fuchs/Schimmel FAMILY for unknown-E is still handled by eFamily on the
* raw pair, which runs before this.)
*/
function locusToken(g: Genotype, locus: LocusKey): string | null {
const [x, y] = g[locus]
if (x === WILDCARD || y === WILDCARD) return null
function locusToken(g: Genotype, locus: LocusKey): string {
// Default an unknown allele to the WILD-TYPE reading: most-dominant for the
// colour loci (unknown-C => full-colour 'C', not a white), but the recessive
// UNMARKED allele for the spotting/rex markers (unknown-Sp must NOT imply Schecke).
const alleles = LOCI[locus].alleles
const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls'
const fallback = isMarker ? alleles[alleles.length - 1] : alleles[0]
const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a))
if (locus === 'E') {
if (x === y) return x // ee->'e', efef->'ef', EE->'E'
if ((x === 'e' && y === 'ef') || (x === 'ef' && y === 'e')) return 'eef'
@@ -161,11 +170,9 @@ function locusToken(g: Genotype, locus: LocusKey): string | null {
}
function matches(g: Genotype, entry: FarbschlagEntry): boolean {
// Unknown loci (null token) match anything (treated as wildcard, GEN-3c).
return (Object.keys(entry.tokens) as LocusKey[]).every((locus) => {
const t = locusToken(g, locus)
return t === null || t === entry.tokens[locus]
})
return (Object.keys(entry.tokens) as LocusKey[]).every(
(locus) => locusToken(g, locus) === entry.tokens[locus],
)
}
/**