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)
|
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
|
* meta rows dropped, 17 matched the frozen names). Genotypes normalized from
|
||||||
* portal notation (c[chm]->cchm, c[h]->ch, e[f]->ef, '-'/'--' = unknown).
|
* 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 {
|
import {
|
||||||
makeGenotype,
|
makeGenotype,
|
||||||
toDisplayString,
|
toDisplayString,
|
||||||
@@ -145,13 +145,22 @@ export interface FarbschlagMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expressed token at a locus, or null when UNKNOWN (a '?' allele) — null acts as
|
* Expressed token at a locus. GEN-3d: an UNKNOWN allele ('?') is resolved to the
|
||||||
* a wildcard in matching. The E locus is PAIR-aware so the Fuchs/Schimmel family
|
* MOST-DOMINANT allele of the locus (the safer default) rather than acting as a
|
||||||
* is distinguishable: ee->'e', e/ef->'eef' (Fuchsschimmel), ef/ef->'ef' (Schimmel).
|
* 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 {
|
function locusToken(g: Genotype, locus: LocusKey): string {
|
||||||
const [x, y] = g[locus]
|
// Default an unknown allele to the WILD-TYPE reading: most-dominant for the
|
||||||
if (x === WILDCARD || y === WILDCARD) return null
|
// 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 (locus === 'E') {
|
||||||
if (x === y) return x // ee->'e', efef->'ef', EE->'E'
|
if (x === y) return x // ee->'e', efef->'ef', EE->'E'
|
||||||
if ((x === 'e' && y === 'ef') || (x === 'ef' && y === 'e')) return 'eef'
|
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 {
|
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(
|
||||||
return (Object.keys(entry.tokens) as LocusKey[]).every((locus) => {
|
(locus) => locusToken(g, locus) === entry.tokens[locus],
|
||||||
const t = locusToken(g, locus)
|
)
|
||||||
return t === null || t === entry.tokens[locus]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user