From 9bca8c4323a8185c8d7804b363d18d12e02fe80b Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 21:49:42 +0200 Subject: [PATCH] GEN-4b: 7 Farbarten-Kategorien blockiert (Engine-Guard, FK=0 verifiziert) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ✓ --- .../src/genetics/__tests__/genetics.test.ts | 33 +++++++++++-- gerbil-manager-web/src/genetics/catalog.ts | 46 ++++++++++++++----- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts index 7180a70..2740676 100644 --- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts +++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts @@ -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)) diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts index 786f3f9..fd802a8 100644 --- a/gerbil-manager-web/src/genetics/catalog.ts +++ b/gerbil-manager-web/src/genetics/catalog.ts @@ -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 = 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 } }