GEN-4 Addendum: 7 Farbarten-Kategorien blockiert + locusToken ef/e-Fix
catalog.ts: - locusToken E: ef/e -> 'ef' (phenotypisch ausgedrücktes Allel; enables catalog-Match fuer het Schimmel-Tiere wie Kohlfuchsschimmel/Orangeschimmel). Vorher: 'eef' matchte keinen Katalogeintrag -> Family-Fallback 'Fuchsschimmel'. - baseColourFor: Family-Fallback entfernt (null statt family-Name) — Familie ist nur noch Katalog-Suchfilter, nie direkter Ausgabewert. - CATEGORY_NAMES Guard in farbschlagFor: Fuchs/Fuchsschimmel/Schimmel/Standard/ Colourpoint/Dilute/Colourpoint Dilute -> Unbekannter Farbschlag wenn Engine diese Namen zurueckgeben wuerde. - Orangeschimmel: A:'A' hinzugefuegt (Agouti-Einschraenkung), damit aa-ef-Tiere durch zu Kohlfuchsschimmel fallen. - FK-Check: Keine der 7 Kategorie-Namen sind in BASE_COLORS -> 0 FK-Risiko. genetics.test.ts: - 'eef with unknown other loci': erwartet jetzt 'Kohlfuchsschimmel' (korrekt!) - GEN-4 Farbarten-Fixture: Kategorie-Guard-Test + FK-Check-Assert. Gate: build ✓ eslint ✓ vitest 96/96 ✓ e2e 148/148 ✓
This commit is contained in:
@@ -377,9 +377,11 @@ describe("GEN-3c: unknown allele displays as '-' (stored as '?')", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', () => {
|
describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', () => {
|
||||||
it('eef with unknown other loci -> Fuchsschimmel (the reported bug case)', () => {
|
it('eef with unknown other loci -> specific Schimmel variety (GEN-4: Fuchsschimmel is a category)', () => {
|
||||||
|
// GEN-4: locusToken ef/e -> 'ef' enables catalog match; family fallback 'Fuchsschimmel' blocked.
|
||||||
|
// aa + ef/e + C/D/G/P resolved via GEN-3d -> Kohlfuchsschimmel (A:a, C:C, D:D, E:ef, G:G, P:P).
|
||||||
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
||||||
'Fuchsschimmel',
|
'Kohlfuchsschimmel',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('ee -> Fuchs family, efef -> a Schimmel (never Unbekannt) even with unknowns', () => {
|
it('ee -> Fuchs family, efef -> a Schimmel (never Unbekannt) even with unknowns', () => {
|
||||||
@@ -405,9 +407,11 @@ describe('GEN-3d: dominance tiebreak for unknown loci', () => {
|
|||||||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD EE GG PP sp- rere'))).toBe('Agouti')
|
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD EE GG PP sp- rere'))).toBe('Agouti')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('still: eef with unknowns -> Fuchsschimmel (family pin unaffected by tiebreak)', () => {
|
it('still: eef with unknowns -> specific variety, not category (GEN-4 update)', () => {
|
||||||
|
// GEN-4: 'Fuchsschimmel' is a Farbart/category; the engine now resolves to the
|
||||||
|
// specific catalog entry (Kohlfuchsschimmel) via the locusToken ef/e -> 'ef' fix.
|
||||||
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
||||||
'Fuchsschimmel',
|
'Kohlfuchsschimmel',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -561,6 +565,21 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => {
|
|||||||
expect(name('AA Cch DD EE GG pp spsp rere')).not.toBe('REW') // Cc[h] + pp
|
expect(name('AA Cch DD EE GG pp spsp rere')).not.toBe('REW') // Cc[h] + pp
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Farbarten (categories) never appear as computed results', () => {
|
||||||
|
// 'Fuchs', 'Fuchsschimmel', 'Schimmel' etc. are Farbarten — blocked by category guard.
|
||||||
|
// het ef/e now resolves to specific variety via locusToken ef/e -> 'ef' fix.
|
||||||
|
expect(genotypeToFarbschlag(fromDisplayString('aa CC DD eef GG PP spsp rere'))).toBe('Kohlfuchsschimmel')
|
||||||
|
// Agouti ef/e: 'Orangeschimmel' wins (same token-set as Algierfuchsschimmel, listed first)
|
||||||
|
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD eef GG PP spsp rere'))).toBe('Orangeschimmel')
|
||||||
|
// Unusual combo not in catalog -> Unbekannt (not 'Fuchsschimmel')
|
||||||
|
expect(farbschlagFor(fromDisplayString('aa CC dd eef GG PP spsp rere')).unknown).toBe(true)
|
||||||
|
// FK check: none of the 7 category names are in BASE_COLORS (no DB entries -> no FK risk)
|
||||||
|
const CATS = ['Standard', 'Colourpoint', 'Dilute', 'Fuchs', 'Fuchsschimmel', 'Schimmel', 'Colourpoint Dilute']
|
||||||
|
for (const cat of CATS) {
|
||||||
|
expect(BASE_COLORS.some(e => e.name === cat)).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('bare Fuchs never appears — dilute-fox combinations are named specifically', () => {
|
it('bare Fuchs never appears — dilute-fox combinations are named specifically', () => {
|
||||||
expect(name('AA CC dd ee GG PP spsp rere')).toBe('Dilute Algierfuchs')
|
expect(name('AA CC dd ee GG PP spsp rere')).toBe('Dilute Algierfuchs')
|
||||||
expect(name('AA CC dd ee GG pp spsp rere')).toBe('Dilute Goldfuchs')
|
expect(name('AA CC dd ee GG pp spsp rere')).toBe('Dilute Goldfuchs')
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
|||||||
{ name: 'Kohlfuchs', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs.jpg' },
|
{ name: 'Kohlfuchs', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs.jpg' },
|
||||||
{ name: 'Polarfuchs', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs.jpg' },
|
{ name: 'Polarfuchs', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs.jpg' },
|
||||||
{ name: 'Saphir', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },
|
{ name: 'Saphir', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },
|
||||||
// GEN-3a: efef base (otherwise wild C/D/G/P) = Orangeschimmel (breeder C5).
|
// GEN-3a: efef base (agouti, wild C/D/G/P) = Orangeschimmel (breeder C5).
|
||||||
{ name: 'Orangeschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' },
|
// GEN-4: A:'A' added — non-agouti ef animals fall through to Kohlfuchsschimmel etc.
|
||||||
|
{ name: 'Orangeschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' },
|
||||||
{ name: 'Topas', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'topas.jpg' },
|
{ name: 'Topas', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'topas.jpg' },
|
||||||
{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },
|
{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },
|
||||||
{ name: 'Dilute Agouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' },
|
{ name: 'Dilute Agouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' },
|
||||||
@@ -94,6 +95,7 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
|||||||
{ name: 'Dilute Polarfuchs', tokens: { A: 'A', C: 'C', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
{ name: 'Dilute Polarfuchs', tokens: { A: 'A', C: 'C', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
||||||
// GEN-3a: efef gg base = Silberschimmel (breeder C5) — listed before the
|
// GEN-3a: efef gg base = Silberschimmel (breeder C5) — listed before the
|
||||||
// A-specific Polarfuchsschimmel so the canonical efef-gg reverse-matches here.
|
// A-specific Polarfuchsschimmel so the canonical efef-gg reverse-matches here.
|
||||||
|
// No A restriction: both agouti (AA) and non-agouti (aa) ef/gg = Silberschimmel.
|
||||||
{ name: 'Silberschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'silberschimmel.jpg' },
|
{ name: 'Silberschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'silberschimmel.jpg' },
|
||||||
{ name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' },
|
{ name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' },
|
||||||
{ name: 'Algierfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'algierfuchsschimmel.jpg' },
|
{ name: 'Algierfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'algierfuchsschimmel.jpg' },
|
||||||
@@ -153,10 +155,12 @@ export interface FarbschlagMatch {
|
|||||||
* Expressed token at a locus. GEN-3d: an UNKNOWN allele ('?') is resolved to the
|
* 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
|
* 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
|
* 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/
|
* a c^h/c^chm colourpoint white. The E locus uses the PHENOTYPICALLY EXPRESSED
|
||||||
* Schimmel family is distinguishable: ee->'e', e/ef->'eef', ef/ef->'ef'.
|
* allele for catalog matching: ee->'e', ef/ef->'ef', e/ef->'ef' (ef is dominant
|
||||||
* (The Fuchs/Schimmel FAMILY for unknown-E is still handled by eFamily on the
|
* for the Schimmel/roan phenotype, so het ef/e animals match Schimmel catalog
|
||||||
* raw pair, which runs before this.)
|
* entries such as Kohlfuchsschimmel). GEN-4: 'eef' removed — 'Fuchsschimmel'
|
||||||
|
* is a Farbart/category, not a concrete Farbschlag; the catalog must name the
|
||||||
|
* variety specifically.
|
||||||
*/
|
*/
|
||||||
function locusToken(g: Genotype, locus: LocusKey): string {
|
function locusToken(g: Genotype, locus: LocusKey): string {
|
||||||
// Default an unknown allele to the WILD-TYPE reading: most-dominant for the
|
// Default an unknown allele to the WILD-TYPE reading: most-dominant for the
|
||||||
@@ -168,7 +172,9 @@ function locusToken(g: Genotype, locus: LocusKey): string {
|
|||||||
const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a))
|
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'
|
// GEN-4: het ef/e → 'ef' (ef is dominant for the Schimmel phenotype;
|
||||||
|
// enables catalog entries like Kohlfuchsschimmel to match het animals).
|
||||||
|
if ((x === 'e' && y === 'ef') || (x === 'ef' && y === 'e')) return 'ef'
|
||||||
return dominantAllele('E', x, y) // E/ef, E/e -> 'E'
|
return dominantAllele('E', x, y) // E/ef, E/e -> 'E'
|
||||||
}
|
}
|
||||||
return dominantAllele(locus, x, y)
|
return dominantAllele(locus, x, y)
|
||||||
@@ -181,10 +187,12 @@ function matches(g: Genotype, entry: FarbschlagEntry): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GEN-3c family fallback: the E locus alone names the Fuchs/Schimmel family even
|
* E-locus family: used to scope the catalog search to E-aware entries.
|
||||||
* when other loci are unknown (so genotypes never fall through to "Unbekannt").
|
* Returns a family tag ('Fuchs'/'Fuchsschimmel'/'Schimmel') when the E locus
|
||||||
* ee -> Fuchs | e/ef -> Fuchsschimmel | ef/ef -> Schimmel | e/? -> Fuchs (for now)
|
* implies a non-dominant extension pair, or null for full-extension/unknown.
|
||||||
* Returns null when E is dominant (full colour) or fully unknown.
|
* GEN-4: these family names are Farbarten (categories), NOT concrete Farbschläge.
|
||||||
|
* They are ONLY used here as catalog-search filters; they must NEVER appear as
|
||||||
|
* computed farbschlag output (the farbschlagFor category guard blocks them).
|
||||||
*/
|
*/
|
||||||
function eFamily(g: Genotype): string | null {
|
function eFamily(g: Genotype): string | null {
|
||||||
const [x, y] = g.E
|
const [x, y] = g.E
|
||||||
@@ -211,7 +219,10 @@ function baseColourFor(g: Genotype): string | null {
|
|||||||
const base = family
|
const base = family
|
||||||
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
|
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
|
||||||
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
|
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
|
||||||
return base?.name ?? family
|
// GEN-4: never fall back to the family name — Fuchs/Fuchsschimmel/Schimmel are
|
||||||
|
// Farbarten (categories), not concrete Farbschläge. If no catalog entry matches,
|
||||||
|
// return null so farbschlagFor emits 'Unbekannter Farbschlag'.
|
||||||
|
return base?.name ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,7 +279,15 @@ export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseName = colourpointName(g) ?? baseColourFor(g)
|
const baseName = colourpointName(g) ?? baseColourFor(g)
|
||||||
if (!baseName) {
|
// GEN-4: safety guard — Farbarten (categories/families) are NEVER valid as
|
||||||
|
// a computed Farbschlag output. If baseName is a category label, treat as
|
||||||
|
// Unbekannt instead of leaking an invalid name into the UI.
|
||||||
|
const CATEGORY_NAMES: ReadonlySet<string> = new Set([
|
||||||
|
'Standard', 'Colourpoint', 'Dilute',
|
||||||
|
'Fuchs', 'Fuchsschimmel', 'Schimmel',
|
||||||
|
'Colourpoint Dilute',
|
||||||
|
])
|
||||||
|
if (!baseName || CATEGORY_NAMES.has(baseName)) {
|
||||||
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
|
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
|
||||||
}
|
}
|
||||||
const name = [baseName, ...modifiers].join(' ')
|
const name = [baseName, ...modifiers].join(' ')
|
||||||
|
|||||||
Reference in New Issue
Block a user