GEN-3f: reconcile cchm colourpoint catalog to the breeder's CP- naming

Computed Farbschlag now == the breeder's dropdown/seed names for colourpoints,
so the mismatch hint stops and the live re-import name-matches her data
(god: she uses the CP- prefix form + Marder/Siam/Zobel/Zobel-Hell; ZERO of the
old portal suffix names). FULL collapse (god-approved, data-safe).

Catalog (BASE_COLORS) 73 -> 61: the 24 portal cchm rows collapse to 12 breeder
varieties — Marder, Siam, Zobel, Zobel-Hell (aa marten/sable group; E & D
irrelevant per the merged GEN-3e rule) and CP-Agouti, CP-Silberagouti,
CP-Algierfuchs, CP-Polarfuchs, CP-Fuchs, CP-Fuchs-Hell, CP-Blaufuchs,
CP-Orangeschimmel (A- 'CP-<base>'; -Hell shade variants collapsed).

- representativeGenotype() now splits a heterozygous 'x/y' token, so the het
  colourpoints Siam & Zobel-Hell (C: 'cchm/ch') carry the correct het canonical
  genotype and the engine computes their own names (verified).
- Engine covers all fox(ee)/dilute(dd) A- cchm bases via the E-family fallback
  (no Unbekannt). KNOWN BEST-EFFORT GAP: CP-Fuchs-Hell and CP-Blaufuchs both
  compute 'CP-Fuchs' (the het-lighter / blue-dilute distinction isn't encoded);
  their distinct rows remain for hand-pick + import name-match. Closing this
  fully needs the breeder's exact fox-CP genotype rule.

Regenerated colorVarietySeed.generated.json (61 rows) for Pam's re-seed
migration. Gate green: build, eslint, vitest 78, e2e 80. de.ts untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 11:47:33 +02:00
parent 029ffb9895
commit 972570b142
3 changed files with 166 additions and 188 deletions

View File

@@ -179,8 +179,10 @@ describe('Farbschlag catalog', () => {
expect(match.name).toBe('Unbekannter Farbschlag')
})
it('has the expected catalogue coverage (GEN-2: frozen 18 + 55 portal varieties)', () => {
expect(CATALOG_SIZE).toBe(73)
it('has the expected catalogue coverage (GEN-3f: 61 after cchm CP reconciliation)', () => {
// GEN-3f collapsed the 24 portal cchm colourpoint rows to 12 breeder-named
// varieties (Marder/Siam/Zobel/Zobel-Hell + CP-<base>), so 73 -> 61.
expect(CATALOG_SIZE).toBe(61)
})
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
@@ -206,7 +208,11 @@ describe('Farbschlag catalog', () => {
expect(new Set(names).size).toBe(names.length)
for (const entry of BASE_COLORS) {
for (const [locus, value] of Object.entries(entry.tokens)) {
expect(LOCI[locus as LocusKey].alleles).toContain(value)
// GEN-3f: a token may be a heterozygous pair "x/y" (e.g. C: 'cchm/ch'
// for the het colourpoints Siam/Zobel-Hell) — validate each allele.
for (const allele of value.split('/')) {
expect(LOCI[locus as LocusKey].alleles).toContain(allele)
}
}
}
})
@@ -429,3 +435,46 @@ describe('GEN-3e: C-locus colourpoint naming', () => {
expect(name('aa cchmcchm DD EE GG PP Spsp rere')).toBe('Marder Schecke')
})
})
describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her live data)', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
it('every CP- name her data uses exists as a catalog row (import name-match)', () => {
for (const n of [
'CP-Agouti', 'CP-Silberagouti', 'CP-Algierfuchs', 'CP-Polarfuchs',
'CP-Fuchs', 'CP-Fuchs-Hell', 'CP-Blaufuchs', 'CP-Orangeschimmel',
'Marder', 'Siam', 'Zobel', 'Zobel-Hell',
]) {
expect(has(n)).toBe(true)
}
})
it('the removed portal suffix-names are gone (collapsed)', () => {
for (const n of [
'Agouti CP', 'Agouti CP-Hell', 'Silberagouti CP', 'Algierfuchs CP',
'Polarfuchs CP', 'Blaufuchs CP', 'Kohlfuchs CP', 'Kohlfuchsschimmel CP',
'Marder dd', 'Zobel dd', 'Siam (Marder-Hell)', 'Agouti dd CP',
]) {
expect(has(n)).toBe(false)
}
})
it('the engine computes the agouti CP- names her data uses (E-family fallback covers fox/dilute)', () => {
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
expect(name('AA cchmcchm DD ee GG PP spsp rere')).toBe('CP-Algierfuchs')
expect(name('AA cchmcchm DD ee gg PP spsp rere')).toBe('CP-Polarfuchs')
// dd agouti fox has no dedicated base -> the eFamily fallback yields 'CP-Fuchs'.
expect(name('AA cchmcchm dd ee GG PP spsp rere')).toBe('CP-Fuchs')
// A- cchm efef -> CP-Orangeschimmel (Schimmel base under full C).
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
})
it('het cchm/ch colourpoints (Siam, Zobel-Hell) resolve to their own names', () => {
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
})
})