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:
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user