GEN-4b: 7 Farbarten-Kategorien blockiert (Engine-Guard, FK=0 verifiziert)

FK-Check: Keine der 7 Kategorie-Namen (Standard/Colourpoint/Dilute/Fuchs/
Fuchsschimmel/Schimmel/Colourpoint Dilute) in BASE_COLORS, import-data
oder colorVarietySeed — FK-Risiko = 0. Mechanism: Engine-Guard (kein
Catalog-Eintrag vorhanden, kein Loeschen noetig).

catalog.ts:
- locusToken E: ef/e -> 'ef' (het Schimmel matcht spezifische Eintraege
  wie Kohlfuchsschimmel statt auf 'Fuchsschimmel'-Family-Fallback zu landen)
- baseColourFor: Family-Fallback entfernt (null statt family-Name)
- CATEGORY_NAMES Guard in farbschlagFor: 7 Farbarten -> Unbekannter Farbschlag
- Orangeschimmel: A:'A' Einschraenkung (non-agouti ef -> Kohlfuchsschimmel)

genetics.test.ts:
- 'eef with unknown other loci': 'Fuchsschimmel' -> 'Kohlfuchsschimmel'
- GEN-4b describe: FK-Check, het-ef/e-Fixture, Unbekannt-Fixture
Gate: build ✓  eslint ✓  vitest 110/110 ✓  e2e 160/160 ✓
This commit is contained in:
2026-06-06 21:49:42 +02:00
parent 90f998d189
commit 9bca8c4323
2 changed files with 63 additions and 16 deletions

View File

@@ -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)', () => {
it('eef with unknown other loci -> Fuchsschimmel (the reported bug case)', () => {
it('eef with unknown other loci -> specific Schimmel variety (GEN-4b: Fuchsschimmel is a category)', () => {
// GEN-4b: locusToken ef/e -> 'ef' enables catalog match; 'Fuchsschimmel' family blocked.
// aa + ef/e resolved via GEN-3d (C->C, D->D, G->G, P->P) -> Kohlfuchsschimmel.
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', () => {
@@ -405,13 +407,36 @@ describe('GEN-3d: dominance tiebreak for unknown loci', () => {
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-4b update)', () => {
// GEN-4b: 'Fuchsschimmel' is a Farbart/category — engine returns specific name.
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
'Fuchsschimmel',
'Kohlfuchsschimmel',
)
})
})
describe('GEN-4b: 7 Farbarten-Kategorien nie als Engine-Ergebnis (FK=0 bestaetigt)', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
it('FK-Check: keine der 7 Kategorie-Namen in BASE_COLORS (kein DB-Eintrag, kein Orphan-Risiko)', () => {
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('het ef/e matches specific catalog varieties, not family fallback Fuchsschimmel', () => {
expect(name('aa CC DD eef GG PP spsp rere')).toBe('Kohlfuchsschimmel')
expect(name('AA CC DD eef GG PP spsp rere')).toBe('Orangeschimmel')
})
it('uncovered genotype -> Unbekannter Farbschlag (not a category name)', () => {
const result = farbschlagFor(fromDisplayString('aa CC dd eef GG PP spsp rere'))
expect(result.unknown).toBe(true)
expect(result.name).toBe('Unbekannter Farbschlag')
})
})
describe('GEN-3e: C-locus colourpoint naming', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))

View File

@@ -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: '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' },
// GEN-3a: efef base (otherwise 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-3a: efef base (agouti, wild C/D/G/P) = Orangeschimmel (breeder C5).
// GEN-4b: 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: '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' },
@@ -153,10 +154,12 @@ export interface FarbschlagMatch {
* 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.)
* a c^h/c^chm colourpoint white. The E locus uses the phenotypically EXPRESSED
* allele for catalog matching: ee->'e', ef/ef->'ef', e/ef->'ef' (ef is dominant
* for the Schimmel/roan phenotype, so het ef/e animals match Schimmel catalog
* entries such as Kohlfuchsschimmel). GEN-4b: 'eef' removed — 'Fuchsschimmel'
* is a Farbart/category, not a concrete Farbschlag; the catalog must name
* the specific variety.
*/
function locusToken(g: Genotype, locus: LocusKey): string {
// Default an unknown allele to the WILD-TYPE reading: most-dominant for the
@@ -168,7 +171,9 @@ function locusToken(g: Genotype, locus: LocusKey): string {
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'
// GEN-4b: 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(locus, x, y)
@@ -181,10 +186,12 @@ function matches(g: Genotype, entry: FarbschlagEntry): boolean {
}
/**
* GEN-3c family fallback: the E locus alone names the Fuchs/Schimmel family even
* when other loci are unknown (so genotypes never fall through to "Unbekannt").
* ee -> Fuchs | e/ef -> Fuchsschimmel | ef/ef -> Schimmel | e/? -> Fuchs (for now)
* Returns null when E is dominant (full colour) or fully unknown.
* E-locus family: used to scope the catalog search to E-aware entries.
* Returns a family tag ('Fuchs'/'Fuchsschimmel'/'Schimmel') when the E locus
* implies a non-dominant extension pair, or null for full-extension/unknown.
* GEN-4b: these family names are Farbarten (categories), NOT concrete Farbschläge.
* They are ONLY used here as catalog-search filters; they NEVER appear as
* computed farbschlag output (the farbschlagFor category guard blocks them).
*/
function eFamily(g: Genotype): string | null {
const [x, y] = g.E
@@ -211,7 +218,10 @@ function baseColourFor(g: Genotype): string | null {
const base = family
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
return base?.name ?? family
// GEN-4b: 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
}
/**
@@ -271,6 +281,18 @@ export function farbschlagFor(g: Genotype): FarbschlagMatch {
if (!baseName) {
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
}
// GEN-4b safety guard: Farbarten (category/family names) must never appear as
// computed Farbschlag output — they are not concrete varieties. FK-check
// confirmed these 7 names have zero existing ColorVariety DB entries (not in
// BASE_COLORS → not seeded → not in import data exact matches).
const CATEGORY_NAMES: ReadonlySet<string> = new Set([
'Standard', 'Colourpoint', 'Dilute',
'Fuchs', 'Fuchsschimmel', 'Schimmel',
'Colourpoint Dilute',
])
if (CATEGORY_NAMES.has(baseName)) {
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
}
const name = [baseName, ...modifiers].join(' ')
return { name, base: null, unknown: false }
}