diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs
index d7de178..fa77e5a 100644
--- a/GerbilManagerWebAPI/ApplicationContext.cs
+++ b/GerbilManagerWebAPI/ApplicationContext.cs
@@ -400,25 +400,28 @@ public class ApplicationContext : DbContext
("Dilute Anthrazit", "aa CC dd EE gg PP spsp rere", 31),
// --- Schimmel / Fuchsschimmel (IDs 33-37) ---
("Silberschimmel", "AA CC DD efef gg PP spsp rere", 36),
- ("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere", 37),
- ("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere", 38),
- ("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere", 39),
- ("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere", 40),
+ // GEN-5 (ticket 5826e8e2): *Fuchsschimmel = HET ef/e (internal 'efe'),
+ // not hom ef/ef — a Schimmel-modified Fox. Pure *schimmel stay efef.
+ ("Polarfuchsschimmel", "AA CC DD efe gg PP spsp rere", 37),
+ ("Algierfuchsschimmel", "AA CC DD efe GG PP spsp rere", 38),
+ ("Kohlfuchsschimmel", "aa CC DD efe GG PP spsp rere", 39),
+ ("Blaufuchsschimmel", "aa CC DD efe gg PP spsp rere", 40),
// --- Hell variants (IDs 38-48) ---
("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere", 41),
("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere", 42),
- ("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere", 43),
+ ("Goldfuchsschimmel", "AA CC DD efe GG pp spsp rere", 43),
("Gold-Hell", "AA CC DD EE GG pp spsp rere", 44),
("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere", 45),
- ("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere", 46),
+ ("Rotfuchsschimmel", "aa CC DD efe GG pp spsp rere", 46),
("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere", 47),
- ("Kohlfuchsschimmel, hell","aa CC DD efef GG PP spsp rere", 48),
+ ("Kohlfuchsschimmel, hell","aa CC DD efe GG PP spsp rere", 48),
("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere", 49),
("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere", 50),
("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere", 51),
// --- Dilute (dd) renamed variants (IDs 49-50) ---
("Dilute Topas", "AA CC dd EE GG pp spsp rere", 52),
- ("Dilute Blaufuchs","aa CC dd ee gg pp spsp rere", 53),
+ // GEN-5 (ticket 3deab547): Blaufuchs is black-eyed; dilution is P-independent.
+ ("Dilute Blaufuchs","aa CC dd ee gg PP spsp rere", 53),
// --- Marder / Siam / CP- series (IDs 51-66) ---
("Marder", "aa cchmcchm DD EE GG PP spsp rere", 54),
("Siam", "aa cchmch DD EE GG PP spsp rere", 55),
diff --git a/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs
new file mode 100644
index 0000000..866a660
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs
@@ -0,0 +1,1548 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ [DbContext(typeof(ApplicationContext))]
+ [Migration("20260623065520_Gen5FuchsschimmelHetSeedFix")]
+ partial class Gen5FuchsschimmelHetSeedFix
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("PageId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PageId");
+
+ b.ToTable("Blocks");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000001"),
+ Data = "{\"text\":\"Startseite\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000001"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000002"),
+ Data = "{\"text\":\"Über die Zucht\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000002"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000003"),
+ Data = "{\"text\":\"Abgabetiere\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000003"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000004"),
+ Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000004"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000005"),
+ Data = "{\"text\":\"Farben & Genetik\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000005"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000006"),
+ Data = "{\"text\":\"Kontakt\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000006"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000010"),
+ Data = "{\"mode\":\"auto\",\"intro\":\"\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000003"),
+ Type = "AbgabetiereList"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000007"),
+ Data = "{\"text\":\"Impressum\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000007"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000070"),
+ Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000007"),
+ Type = "RichText"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000008"),
+ Data = "{\"text\":\"Datenschutz\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000008"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000080"),
+ Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000008"),
+ Type = "RichText"
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("City")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Homepage")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NameSuffix")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ZuchtName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("BreederSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("11111111-1111-1111-1111-000000000001"),
+ Address = "",
+ City = "",
+ Email = "",
+ Homepage = "",
+ Name = "",
+ NameSuffix = "",
+ Phone = "",
+ ZuchtName = ""
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanonicalGenotype")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("ColorVarieties");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
+ Name = "REW",
+ SortOrder = 0
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000002"),
+ CanonicalGenotype = "aa chch DD EE GG PP spsp rere",
+ Name = "Hermelin",
+ SortOrder = 1
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000003"),
+ CanonicalGenotype = "AA chch DD EE GG PP spsp rere",
+ Name = "Himalaya",
+ SortOrder = 2
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000004"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel",
+ SortOrder = 3
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000005"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Rotaugenschimmel",
+ SortOrder = 4
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000006"),
+ CanonicalGenotype = "AA CC DD EE GG PP spsp rere",
+ Name = "Agouti",
+ SortOrder = 5
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000007"),
+ CanonicalGenotype = "aa CC DD EE GG PP spsp rere",
+ Name = "Schwarz",
+ SortOrder = 6
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000008"),
+ CanonicalGenotype = "AA CC DD EE gg PP spsp rere",
+ Name = "Silberagouti",
+ SortOrder = 7
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000009"),
+ CanonicalGenotype = "aa CC DD EE gg PP spsp rere",
+ Name = "Anthrazit",
+ SortOrder = 8
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000010"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs",
+ SortOrder = 9
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000011"),
+ CanonicalGenotype = "aa CC dd EE GG PP spsp rere",
+ Name = "Blau",
+ SortOrder = 10
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000012"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold",
+ SortOrder = 11
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000013"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin",
+ SortOrder = 12
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000014"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs",
+ SortOrder = 13
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000015"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs",
+ SortOrder = 14
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000016"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Dilute Gold",
+ SortOrder = 15
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000017"),
+ CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
+ Name = "Dilute Platin",
+ SortOrder = 16
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000018"),
+ CanonicalGenotype = "aa CC DD EE gg pp spsp rere",
+ Name = "Altweiss (REW)",
+ SortOrder = 17
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000019"),
+ CanonicalGenotype = "AA CC DD ee gg pp spsp rere",
+ Name = "Apricot (Blassfuchs)",
+ SortOrder = 18
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000020"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs",
+ SortOrder = 19
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000021"),
+ CanonicalGenotype = "aa CC DD ee gg pp spsp rere",
+ Name = "C-Separator",
+ SortOrder = 20
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000022"),
+ CanonicalGenotype = "AA CC DD EE gg pp spsp rere",
+ Name = "Elfenbein",
+ SortOrder = 21
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000023"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs",
+ SortOrder = 22
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000024"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs",
+ SortOrder = 23
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000025"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Saphir",
+ SortOrder = 24
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000026"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Orangeschimmel",
+ SortOrder = 25
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000027"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Topas",
+ SortOrder = 26
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000028"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin-Hell",
+ SortOrder = 27
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000029"),
+ CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
+ Name = "Dilute Agouti",
+ SortOrder = 28
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000030"),
+ CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
+ Name = "Dilute Silberagouti",
+ SortOrder = 29
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000031"),
+ CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
+ Name = "Dilute Kohlfuchs",
+ SortOrder = 30
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000032"),
+ CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
+ Name = "Dilute Anthrazit",
+ SortOrder = 31
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000033"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Silberschimmel",
+ SortOrder = 36
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000034"),
+ CanonicalGenotype = "AA CC DD efe gg PP spsp rere",
+ Name = "Polarfuchsschimmel",
+ SortOrder = 37
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000035"),
+ CanonicalGenotype = "AA CC DD efe GG PP spsp rere",
+ Name = "Algierfuchsschimmel",
+ SortOrder = 38
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000036"),
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
+ Name = "Kohlfuchsschimmel",
+ SortOrder = 39
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000037"),
+ CanonicalGenotype = "aa CC DD efe gg PP spsp rere",
+ Name = "Blaufuchsschimmel",
+ SortOrder = 40
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000038"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs, hell",
+ SortOrder = 41
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000039"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs, hell",
+ SortOrder = 42
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000040"),
+ CanonicalGenotype = "AA CC DD efe GG pp spsp rere",
+ Name = "Goldfuchsschimmel",
+ SortOrder = 43
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000041"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold-Hell",
+ SortOrder = 44
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000042"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs, hell",
+ SortOrder = 45
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000043"),
+ CanonicalGenotype = "aa CC DD efe GG pp spsp rere",
+ Name = "Rotfuchsschimmel",
+ SortOrder = 46
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000044"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs, hell",
+ SortOrder = 47
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000045"),
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
+ Name = "Kohlfuchsschimmel, hell",
+ SortOrder = 48
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000046"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs, hell",
+ SortOrder = 49
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000047"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs-Hell",
+ SortOrder = 50
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000048"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs, hell",
+ SortOrder = 51
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000049"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Dilute Topas",
+ SortOrder = 52
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000050"),
+ CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
+ Name = "Dilute Blaufuchs",
+ SortOrder = 53
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000051"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Marder",
+ SortOrder = 54
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000052"),
+ CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere",
+ Name = "Siam",
+ SortOrder = 55
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000053"),
+ CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere",
+ Name = "Zobel-Hell",
+ SortOrder = 56
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000054"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "CP-Agouti",
+ SortOrder = 57
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000055"),
+ CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
+ Name = "CP-Silberagouti",
+ SortOrder = 59
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000056"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "CP-Algierfuchs",
+ SortOrder = 61
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000057"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "CP-Polarfuchs",
+ SortOrder = 63
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000058"),
+ CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere",
+ Name = "CP-Fuchs",
+ SortOrder = 65
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000059"),
+ CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere",
+ Name = "CP-Fuchs-Hell",
+ SortOrder = 66
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000060"),
+ CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere",
+ Name = "CP-Blaufuchs",
+ SortOrder = 67
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000061"),
+ CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere",
+ Name = "CP-Orangeschimmel",
+ SortOrder = 68
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000062"),
+ CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere",
+ Name = "CP-Agouti-Hell",
+ SortOrder = 58
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000063"),
+ CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere",
+ Name = "CP-Silberagouti-Hell",
+ SortOrder = 60
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000064"),
+ CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere",
+ Name = "CP-Algierfuchs-Hell",
+ SortOrder = 62
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000065"),
+ CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere",
+ Name = "CP-Polarfuchs-Hell",
+ SortOrder = 64
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000066"),
+ CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere",
+ Name = "CP-Orangeschimmel-Hell",
+ SortOrder = 69
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000067"),
+ CanonicalGenotype = "AA CC dd ee GG PP spsp rere",
+ Name = "Dilute Algierfuchs",
+ SortOrder = 32
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000068"),
+ CanonicalGenotype = "AA CC dd ee GG pp spsp rere",
+ Name = "Dilute Goldfuchs",
+ SortOrder = 33
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000069"),
+ CanonicalGenotype = "aa CC dd ee GG pp spsp rere",
+ Name = "Dilute Rotfuchs",
+ SortOrder = 34
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000070"),
+ CanonicalGenotype = "AA CC dd ee gg PP spsp rere",
+ Name = "Dilute Polarfuchs",
+ SortOrder = 35
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("IsBreeder")
+ .HasColumnType("boolean");
+
+ b.Property("IsReceiver")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("NameSuffix")
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .HasColumnType("text");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contacts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Enclosures");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Caption")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EnclosureId")
+ .HasColumnType("uuid");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EnclosureId");
+
+ b.ToTable("EnclosurePhotos");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ClientTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Context")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EntityName")
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Url")
+ .HasColumnType("text");
+
+ b.Property("UserAgent")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.ToTable("Feedback");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CauseOfDeath")
+ .HasColumnType("text");
+
+ b.Property("CharacterNote")
+ .HasColumnType("text");
+
+ b.Property("CharacterTraits")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ColorVarietyId")
+ .HasColumnType("uuid");
+
+ b.Property("DateOfBirth")
+ .HasColumnType("date");
+
+ b.Property("DateOfDeath")
+ .HasColumnType("date");
+
+ b.Property("EnclosureId")
+ .HasColumnType("uuid");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Genotype")
+ .HasColumnType("text");
+
+ b.Property("GoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ImportSource")
+ .HasColumnType("text");
+
+ b.Property("IsCastrated")
+ .HasColumnType("boolean");
+
+ b.Property("IsDeaf")
+ .HasColumnType("boolean");
+
+ b.Property("IsResident")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("NameSearch")
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("OriginBreeder")
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("OriginContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.Property("RawImportData")
+ .HasColumnType("text");
+
+ b.Property("ReceiverContactId")
+ .HasColumnType("uuid");
+
+ b.Property("SpottingType")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ColorVarietyId");
+
+ b.HasIndex("EnclosureId");
+
+ b.HasIndex("ExternalRef")
+ .IsUnique()
+ .HasFilter("\"ExternalRef\" IS NOT NULL");
+
+ b.HasIndex("LitterId");
+
+ b.HasIndex("OriginContactId");
+
+ b.HasIndex("ReceiverContactId");
+
+ b.ToTable("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Caption")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("GerbilPhotos");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Veterinarian")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("HealthRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("DeathsWithin8Weeks")
+ .HasColumnType("integer");
+
+ b.Property("ExpectedGoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("FatherId")
+ .HasColumnType("uuid");
+
+ b.Property("LitterLetter")
+ .HasColumnType("text");
+
+ b.Property("MotherId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("PairingCode")
+ .HasColumnType("text");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.Property("TotalBorn")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExternalRef")
+ .IsUnique()
+ .HasFilter("\"ExternalRef\" IS NOT NULL");
+
+ b.HasIndex("FatherId");
+
+ b.HasIndex("MotherId");
+
+ b.ToTable("Litters");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AppPasswordProtected")
+ .HasColumnType("text");
+
+ b.Property("BackgroundPollEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Folder")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GmailAddress")
+ .HasColumnType("text");
+
+ b.Property("LastUid")
+ .HasColumnType("bigint");
+
+ b.Property("PollIntervalMinutes")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("MailSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("ab0c0000-0000-0000-0000-000000000001"),
+ BackgroundPollEnabled = false,
+ Folder = "INBOX",
+ LastUid = 0L,
+ PollIntervalMinutes = 15
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Alt")
+ .HasColumnType("text");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Height")
+ .HasColumnType("integer");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Width")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Media");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("SeoDescription")
+ .HasColumnType("text");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Pages");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000001"),
+ Slug = "start",
+ Status = "Published",
+ Title = "Startseite"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000002"),
+ Slug = "ueber-die-zucht",
+ Status = "Published",
+ Title = "Über die Zucht"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000003"),
+ Slug = "abgabetiere",
+ Status = "Published",
+ Title = "Abgabetiere"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000004"),
+ Slug = "abgabebedingungen",
+ Status = "Published",
+ Title = "Abgabebedingungen"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000005"),
+ Slug = "farben-genetik",
+ Status = "Published",
+ Title = "Farben & Genetik"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000006"),
+ Slug = "kontakt",
+ Status = "Published",
+ Title = "Kontakt"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000007"),
+ Slug = "impressum",
+ Status = "Published",
+ Title = "Impressum"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000008"),
+ Slug = "datenschutz",
+ Status = "Published",
+ Title = "Datenschutz"
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AnsweredAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("AssignedContactId")
+ .HasColumnType("uuid");
+
+ b.Property("BodyText")
+ .HasColumnType("text");
+
+ b.Property("DraftReply")
+ .HasColumnType("text");
+
+ b.Property("FromAddress")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FromName")
+ .HasColumnType("text");
+
+ b.Property("GmailMessageId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InReplyToMessageId")
+ .HasColumnType("text");
+
+ b.Property("ReceivedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReferencesHeader")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Subject")
+ .HasColumnType("text");
+
+ b.Property("ThreadId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssignedContactId");
+
+ b.HasIndex("GmailMessageId")
+ .IsUnique();
+
+ b.ToTable("Requests");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ContractDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("HandoverDate")
+ .HasColumnType("date");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContactId");
+
+ b.ToTable("SaleContracts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.Property("SaleContractId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("PhotoId")
+ .HasColumnType("uuid");
+
+ b.HasKey("SaleContractId", "GerbilId");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("SaleContractAnimal");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DefaultLocale")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NavOrder")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Sites");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("5172e000-0000-0000-0000-000000000001"),
+ DefaultLocale = "de",
+ NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]"
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("WeightGrams")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("WeightRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Page", null)
+ .WithMany("Blocks")
+ .HasForeignKey("PageId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null)
+ .WithMany()
+ .HasForeignKey("EnclosureId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety")
+ .WithMany()
+ .HasForeignKey("ColorVarietyId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure")
+ .WithMany("Gerbils")
+ .HasForeignKey("EnclosureId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter")
+ .WithMany()
+ .HasForeignKey("LitterId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact")
+ .WithMany()
+ .HasForeignKey("OriginContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact")
+ .WithMany()
+ .HasForeignKey("ReceiverContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("ColorVariety");
+
+ b.Navigation("Enclosure");
+
+ b.Navigation("Litter");
+
+ b.Navigation("OriginContact");
+
+ b.Navigation("ReceiverContact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father")
+ .WithMany()
+ .HasForeignKey("FatherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother")
+ .WithMany()
+ .HasForeignKey("MotherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Father");
+
+ b.Navigation("Mother");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact")
+ .WithMany()
+ .HasForeignKey("AssignedContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("AssignedContact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact")
+ .WithMany()
+ .HasForeignKey("ContactId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Contact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil")
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null)
+ .WithMany("Animals")
+ .HasForeignKey("SaleContractId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Gerbil");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Navigation("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b =>
+ {
+ b.Navigation("Blocks");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Navigation("Animals");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs
new file mode 100644
index 0000000..b350637
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs
@@ -0,0 +1,131 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ ///
+ public partial class Gen5FuchsschimmelHetSeedFix : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000034"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efe gg PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000035"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efe GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000036"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efe GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000037"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efe gg PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000040"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efe GG pp spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000043"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efe GG pp spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000045"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efe GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000050"),
+ column: "CanonicalGenotype",
+ value: "aa CC dd ee gg PP spsp rere");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000034"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efef gg PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000035"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efef GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000036"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efef GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000037"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efef gg PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000040"),
+ column: "CanonicalGenotype",
+ value: "AA CC DD efef GG pp spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000043"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efef GG pp spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000045"),
+ column: "CanonicalGenotype",
+ value: "aa CC DD efef GG PP spsp rere");
+
+ migrationBuilder.UpdateData(
+ table: "ColorVarieties",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000050"),
+ column: "CanonicalGenotype",
+ value: "aa CC dd ee gg pp spsp rere");
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
index 32575c2..9d60eca 100644
--- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
+++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
@@ -483,28 +483,28 @@ namespace GerbilManagerWebAPI.Migrations
new
{
Id = new Guid("00000000-0000-0000-0000-000000000034"),
- CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ CanonicalGenotype = "AA CC DD efe gg PP spsp rere",
Name = "Polarfuchsschimmel",
SortOrder = 37
},
new
{
Id = new Guid("00000000-0000-0000-0000-000000000035"),
- CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ CanonicalGenotype = "AA CC DD efe GG PP spsp rere",
Name = "Algierfuchsschimmel",
SortOrder = 38
},
new
{
Id = new Guid("00000000-0000-0000-0000-000000000036"),
- CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
Name = "Kohlfuchsschimmel",
SortOrder = 39
},
new
{
Id = new Guid("00000000-0000-0000-0000-000000000037"),
- CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
+ CanonicalGenotype = "aa CC DD efe gg PP spsp rere",
Name = "Blaufuchsschimmel",
SortOrder = 40
},
@@ -525,7 +525,7 @@ namespace GerbilManagerWebAPI.Migrations
new
{
Id = new Guid("00000000-0000-0000-0000-000000000040"),
- CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ CanonicalGenotype = "AA CC DD efe GG pp spsp rere",
Name = "Goldfuchsschimmel",
SortOrder = 43
},
@@ -546,7 +546,7 @@ namespace GerbilManagerWebAPI.Migrations
new
{
Id = new Guid("00000000-0000-0000-0000-000000000043"),
- CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
+ CanonicalGenotype = "aa CC DD efe GG pp spsp rere",
Name = "Rotfuchsschimmel",
SortOrder = 46
},
@@ -560,7 +560,7 @@ namespace GerbilManagerWebAPI.Migrations
new
{
Id = new Guid("00000000-0000-0000-0000-000000000045"),
- CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
Name = "Kohlfuchsschimmel, hell",
SortOrder = 48
},
@@ -595,7 +595,7 @@ namespace GerbilManagerWebAPI.Migrations
new
{
Id = new Guid("00000000-0000-0000-0000-000000000050"),
- CanonicalGenotype = "aa CC dd ee gg pp spsp rere",
+ CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
Name = "Dilute Blaufuchs",
SortOrder = 53
},
diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts
index 5fd503c..45fb045 100644
--- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts
+++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts
@@ -17,6 +17,8 @@ import {
wildType,
extractGenotypeFlags,
displayGenotypeSafe,
+ resolveAllelePair,
+ inferUnknownsFromParents,
} from '../genotype'
import { combineLocus } from '../punnett'
import { LOCI, type LocusKey } from '../loci'
@@ -239,10 +241,11 @@ describe('Farbschlag catalog', () => {
})
describe('Partially-unknown parents (wildcards)', () => {
- it('handles an A? parent (phenotype agouti, genotype unknown)', () => {
- // A? x aa at the A locus -> father gamete: 1/2 A, 1/4 (each of A,a) from "?"
- // = effectively 3/4 A, 1/4 a ; mother always a.
- // Offspring: 3/4 Aa, 1/4 aa.
+ it('GEN-5: an A? parent reads as AA (unknown copies the known allele)', () => {
+ // GEN-5 (ticket 3e643ef1, breeder rule): the unknown allele '?' is a COPY of
+ // the known partner 'A', so A? = AA. AA × aa → all Aa → 100% Agouti.
+ // (Previously '?' spread uniformly → 3/4 Agouti : 1/4 Schwarz, which invented
+ // a recessive 'a' gamete the parent demonstrably does not show.)
const father = makeGenotype({
A: ['A', '?'],
C: ['C', 'C'],
@@ -257,10 +260,10 @@ describe('Partially-unknown parents (wildcards)', () => {
const mother = fromDisplayString('aa CC DD EE GG PP spsp rere')
const result = breed(father, mother)
- const agouti = result.byFarbschlag.find((f) => f.farbschlag === 'Agouti')!
- const schwarz = result.byFarbschlag.find((f) => f.farbschlag === 'Schwarz')!
- expect(agouti.probability.text).toBe('3/4')
- expect(schwarz.probability.text).toBe('1/4')
+ expect(result.offspring).toHaveLength(1)
+ expect(result.offspring[0].farbschlag).toBe('Agouti')
+ expect(result.offspring[0].probability.text).toBe('1')
+ expect(result.byFarbschlag.some((f) => f.farbschlag === 'Schwarz')).toBe(false)
})
})
@@ -603,7 +606,10 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => {
expect(name('AA CC dd EE GG PP spsp rere')).toBe('Dilute Agouti')
expect(name('aa CC dd EE gg PP spsp rere')).toBe('Dilute Anthrazit')
expect(name('aa CC dd ee GG PP spsp rere')).toBe('Dilute Kohlfuchs')
- expect(name('aa CC dd ee gg pp spsp rere')).toBe('Dilute Blaufuchs')
+ // GEN-5 (ticket 3deab547): Blaufuchs is BLACK-eyed (P), and dilution is
+ // independent of the P-locus, so Dilute Blaufuchs is aa dd ee gg P- (was
+ // wrongly P:'p'). The pink-eyed variant is a different (REW-adjacent) colour.
+ expect(name('aa CC dd ee gg PP spsp rere')).toBe('Dilute Blaufuchs')
expect(name('AA CC dd EE GG pp spsp rere')).toBe('Dilute Gold')
expect(name('aa CC dd EE GG pp spsp rere')).toBe('Dilute Platin')
})
@@ -628,10 +634,13 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => {
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.
+ // GEN-5 (ticket 5826e8e2): het ef/e is the FUCHSSCHIMMEL family — it resolves to
+ // a *fuchsschimmel variety, NEVER a pure Schimmel (Rotaugen-/Orangeschimmel).
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')
+ // Agouti ef/e black-eyed → Algierfuchsschimmel (A:A,E:ef,G:G,P:P), NOT Orangeschimmel.
+ expect(genotypeToFarbschlag(fromDisplayString('AA CC DD eef GG PP spsp rere'))).toBe('Algierfuchsschimmel')
+ // hom ef/ef agouti black-eyed → the pure Orangeschimmel (Schimmel family).
+ expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef 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)
@@ -789,3 +798,118 @@ describe('GEN-3h: breeder bracket-notation display + E-locus e-before-ef order',
expect(displayGenotypeSafe('')).toBe('')
})
})
+
+describe('GEN-5: unknown allele = copy of known (resolveAllelePair, breeder rule)', () => {
+ it('a single-unknown pair resolves to the homozygote of the KNOWN allele', () => {
+ expect(resolveAllelePair('A', ['A', '?'])).toEqual(['A', 'A'])
+ expect(resolveAllelePair('A', ['?', 'a'])).toEqual(['a', 'a'])
+ expect(resolveAllelePair('D', ['D', '?'])).toEqual(['D', 'D'])
+ expect(resolveAllelePair('E', ['e', '?'])).toEqual(['e', 'e']) // ee[-] = Fuchs
+ expect(resolveAllelePair('E', ['ef', '?'])).toEqual(['ef', 'ef']) // ef[-] = Schimmel
+ })
+ it('a fully-unknown pair falls back to wild-type (markers stay unmarked)', () => {
+ expect(resolveAllelePair('A', ['?', '?'])).toEqual(['A', 'A'])
+ expect(resolveAllelePair('C', ['?', '?'])).toEqual(['C', 'C'])
+ expect(resolveAllelePair('Sp', ['?', '?'])).toEqual(['sp', 'sp']) // never implies Schecke
+ })
+ it('a fully-known pair is returned unchanged', () => {
+ expect(resolveAllelePair('C', ['C', 'ch'])).toEqual(['C', 'ch'])
+ })
+})
+
+describe('GEN-5: genetics-engine ticket reproductions (real stored genotypes)', () => {
+ const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
+
+ it('5826e8e2: ee[f] het (Fuchsschimmel) + pp → Goldfuchsschimmel, NOT Rotaugenschimmel', () => {
+ // Tier bf6f4507: Aa C- D- ee[f] G- pp Spsp — het ef/e is a Fuchsschimmel.
+ expect(name('Aa C- D- ee[f] G- pp Spsp')).toBe('Goldfuchsschimmel Schecke')
+ // and the pure hom ef/ef pp stays the pure Schimmel:
+ expect(name('AA CC DD efef GG pp spsp')).toBe('Rotaugenschimmel')
+ })
+
+ it('b034ddd2: ee[-] (e + unknown) → ee Fuchs → Algierfuchs, NOT Agouti', () => {
+ // Tier 33a7c1f9: Aa CC D- ee[-] Gg Pp spsp. Old engine read [e,?] as e/E → Agouti.
+ expect(name('Aa CC D- ee[-] Gg Pp spsp')).toBe('Algierfuchs')
+ })
+
+ it('3deab547 / efa2b232: aa cchm dd ee[-] gg P- → Dilute CP-Blaufuchs, NOT Zobel/blau/Unbekannt', () => {
+ // Tier 6864eaef. Non-agouti Fuchs colourpoint is NOT a marten (Zobel) — it
+ // derives a CP-fox base with the Dilute prefix.
+ expect(name('aa c[chm]c[chm] dd ee[-] gg Pp Spsp')).toBe('Dilute CP-Blaufuchs Schecke')
+ })
+
+ it('473dc345 / 5151ab20: Vance uw[d] (dense underwhite) parses (no crash) → Kohlfuchs', () => {
+ // Tier f31eb1f9: aa Cc[chm] D- ee Uwuw[d] PP spsp. uw[d] is the G locus;
+ // it must parse and NEVER render 'uw'.
+ const g = fromDisplayString('aa Cc[chm] D- ee Uwuw[d] PP spsp')
+ expect(g.G).toEqual(['G', 'g'])
+ expect(toDisplayString(g)).not.toContain('uw')
+ expect(genotypeToFarbschlag(g)).toBe('Kohlfuchs')
+ })
+
+ it('Fuchsschimmel family never resolves to a pure Schimmel variety', () => {
+ // Agouti het ef/e black-eyed → Algierfuchsschimmel; hom ef/ef → Orangeschimmel.
+ expect(name('AA CC DD eef GG PP spsp')).toBe('Algierfuchsschimmel')
+ expect(name('AA CC DD efef GG PP spsp')).toBe('Orangeschimmel')
+ })
+})
+
+describe('GEN-5: no phantom colours in the expected-litter list (3e643ef1/c8ce27e2/3c46d0b4/1e7b66e6)', () => {
+ it('Mamta Mini (D-, Ee[-]) × Gold (D-, Ee): unknown D copies known D → no Dilute, no Unbekannt, no efef', () => {
+ // Real litter 98bfdf92. Both parents carry D- (unknown D) and an unknown E
+ // partner. The old uniform-spread invented dd / ef / 'Unbekannt' offspring.
+ const father = fromDisplayString('AA CC D- Ee[-] Gg PP spsp') // Mamta Mini
+ const mother = fromDisplayString('Aa CC D- Ee Gg pp spsp') // Gold
+ const result = breed(father, mother)
+
+ const names = result.byFarbschlag.map((f) => f.farbschlag)
+ expect(names).not.toContain('Unbekannter Farbschlag')
+ expect(names.some((n) => n.startsWith('Dilute'))).toBe(false)
+ expect(result.offspring.every((o) => !o.genotype.includes('e[f]'))).toBe(true)
+ // Probabilities still sum to exactly 1.
+ const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
+ expect(sum).toBeCloseTo(1, 10)
+ // Only agouti vs silver-agouti can fall here (G locus segregates; everything else fixed).
+ expect(new Set(names)).toEqual(new Set(['Agouti', 'Silberagouti']))
+ })
+
+ it('D- × D- never yields a dd (dilute) offspring at all', () => {
+ const p = fromDisplayString('AA CC D- EE GG PP spsp')
+ const result = breed(p, p)
+ expect(result.offspring.every((o) => !o.genotype.includes('dd'))).toBe(true)
+ })
+})
+
+describe('GEN-5: parent inference fills unknown alleles (cc9ea3fe / 1a508c04)', () => {
+ it('Mamta Mini Ee[-] + homozygous ee father Geely → Ee', () => {
+ const child = fromDisplayString('AA CC DD Ee[-] Gg PP spsp')
+ const geely = fromDisplayString('aa CC DD ee gg PP spsp') // father: ee (hom fox)
+ const res = inferUnknownsFromParents(child, geely, null)
+ expect(res.genotype.E).toEqual(['E', 'e'])
+ expect(toDisplayString(res.genotype)).toBe('AA CC DD Ee Gg PP spsp')
+ expect(res.inferred).toEqual([{ locus: 'E', allele: 'e', from: 'father' }])
+ })
+
+ it('falls back to the mother when only she is homozygous', () => {
+ const child = fromDisplayString('AA CC DD Ee[-] GG PP spsp')
+ const father = fromDisplayString('AA CC DD Ee GG PP spsp') // het → no force
+ const mother = fromDisplayString('aa CC DD ee GG PP spsp') // ee → forces e
+ const res = inferUnknownsFromParents(child, father, mother)
+ expect(res.genotype.E).toEqual(['E', 'e'])
+ expect(res.inferred).toEqual([{ locus: 'E', allele: 'e', from: 'mother' }])
+ })
+
+ it('leaves the genotype untouched when no parent is homozygous at the unknown locus', () => {
+ const child = fromDisplayString('AA CC DD Ee[-] GG PP spsp')
+ const father = fromDisplayString('AA CC DD Ee GG PP spsp')
+ const res = inferUnknownsFromParents(child, father, null)
+ expect(res.inferred).toEqual([])
+ expect(res.genotype.E).toEqual(child.E)
+ })
+
+ it('no-op when there is nothing unknown', () => {
+ const child = fromDisplayString('AA CC DD Ee GG PP spsp')
+ const res = inferUnknownsFromParents(child, child, child)
+ expect(res.inferred).toEqual([])
+ })
+})
diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts
index 2502c30..e1d74be 100644
--- a/gerbil-manager-web/src/genetics/catalog.ts
+++ b/gerbil-manager-web/src/genetics/catalog.ts
@@ -20,12 +20,12 @@
* meta rows dropped, 17 matched the frozen names). Genotypes normalized from
* portal notation (c[chm]->cchm, c[h]->ch, e[f]->ef, '-'/'--' = unknown).
*/
-import { LOCI, LOCUS_ORDER, dominantAllele, type LocusKey } from './loci'
+import { LOCUS_ORDER, dominantAllele, type LocusKey } from './loci'
import {
makeGenotype,
+ resolveAllelePair,
toDisplayString,
wildType,
- WILDCARD,
type AllelePair,
type Genotype,
} from './genotype'
@@ -113,7 +113,11 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
{ name: 'Kohlfuchs-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-hell-2.jpg' },
{ name: 'Algierfuchs, hell', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-hell.JPG' },
{ name: 'Dilute Topas', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
- { name: 'Dilute Blaufuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
+ // GEN-5 (ticket 3deab547): Blaufuchs is black-eyed (P, line 75); dilution dd is
+ // independent of the eye-pigment P-locus, so the dilute form is ALSO P:'P'
+ // (was P:'p', which made it an unreachable phantom and left dd CP-fox animals
+ // 'Unbekannt'/'blau'). Now aa cchm dd ee gg P- → 'Dilute CP-Blaufuchs'.
+ { name: 'Dilute Blaufuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'P' }, image: 'blaufuchs-dd.jpg' },
// ── GEN-3f/3g: c^chm colourpoint varieties ──
// GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell).
@@ -163,13 +167,10 @@ export interface FarbschlagMatch {
* variety specifically.
*/
function locusToken(g: Genotype, locus: LocusKey): string {
- // Default an unknown allele to the WILD-TYPE reading: most-dominant for the
- // 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))
+ // GEN-5: an unknown allele is a COPY of the known partner (resolveAllelePair),
+ // so e.g. [e,?] reads as ee (Fuchs), NOT e/E. Only a fully-unknown locus falls
+ // back to the wild-type reading (most-dominant colour / unmarked marker).
+ const [x, y] = resolveAllelePair(locus, g[locus])
if (locus === 'E') {
if (x === y) return x // ee->'e', efef->'ef', EE->'E'
// GEN-4: het ef/e → 'ef' (ef is dominant for the Schimmel phenotype;
@@ -195,29 +196,50 @@ function matches(g: Genotype, entry: FarbschlagEntry): boolean {
* computed farbschlag output (the farbschlagFor category guard blocks them).
*/
function eFamily(g: Genotype): string | null {
- const [x, y] = g.E
+ // GEN-5: resolve unknown E as a copy of the known allele first ([e,?]→ee Fuchs,
+ // [ef,?]→ef/ef Schimmel, [E,?]→EE full), so families are decided consistently.
+ const [x, y] = resolveAllelePair('E', g.E)
if (x === 'e' && y === 'e') return 'Fuchs'
if ((x === 'e' && y === 'ef') || (x === 'ef' && y === 'e')) return 'Fuchsschimmel'
if (x === 'ef' && y === 'ef') return 'Schimmel'
- if ((x === 'e' || y === 'e') && (x === WILDCARD || y === WILDCARD)) return 'Fuchs'
return null
}
/** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */
-/** Resolve an allele pair to concrete alleles, defaulting unknown to wild-type. */
+/**
+ * Resolve an allele pair to concrete alleles. GEN-5: an unknown allele copies the
+ * known partner (resolveAllelePair); a fully-unknown locus falls back to wild-type.
+ */
function resolvedPair(g: Genotype, locus: LocusKey): [string, string] {
- 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))
- return [x, y]
+ return resolveAllelePair(locus, g[locus])
+}
+
+/**
+ * Whether a catalog entry belongs to the given E-family by NAME. The Schimmel
+ * entries split into two breeder groups that share the same E:'ef' token but
+ * differ by zygosity of the live animal:
+ * - 'Fuchsschimmel' family (ef/e het) → only *fuchsschimmel entries
+ * (Goldfuchsschimmel, Kohlfuchsschimmel, …).
+ * - 'Schimmel' family (ef/ef hom) → the pure roan entries whose name ends
+ * in 'schimmel' but NOT 'fuchsschimmel' (Rotaugenschimmel, Orangeschimmel,
+ * Silberschimmel, …).
+ * GEN-5 (ticket 5826e8e2): this is why ef/e must NOT match a pure-Schimmel entry
+ * (Rotaugenschimmel) — a het Fuchsschimmel animal is a Goldfuchsschimmel.
+ */
+function entryInEFamily(entry: FarbschlagEntry, family: string): boolean {
+ if (entry.tokens.E === undefined) return false
+ const n = entry.name.toLowerCase()
+ if (family === 'Fuchsschimmel') return n.includes('fuchsschimmel')
+ if (family === 'Schimmel') return n.includes('schimmel') && !n.includes('fuchsschimmel')
+ // 'Fuchs' family: fox entries are E:'e' (no 'schimmel' in the name).
+ return !n.includes('schimmel')
}
/** Base colour name (no modifiers, no colourpoint prefix), via E-family + matches. */
function baseColourFor(g: Genotype): string | null {
const family = eFamily(g)
const base = family
- ? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
+ ? (BASE_COLORS.find((e) => entryInEFamily(e, family) && matches(g, e)) ?? null)
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
// GEN-4: never fall back to the family name — Fuchs/Fuchsschimmel/Schimmel are
// Farbarten (categories), not concrete Farbschläge. If no catalog entry matches,
@@ -242,32 +264,20 @@ function colourpointName(g: Genotype): string | null {
// Remaining: cchm/cchm or cchm/ch (colourpoint, no full C, not chch).
const bothCchm = c[0] === 'cchm' && c[1] === 'cchm'
const agouti = resolvedPair(g, 'A').includes('A')
- if (!agouti) {
- // #3: the aa colourpoint branch must respect D (dilute) and E (Fuchs/Schimmel)
- // instead of hard-coding Marder/Siam/Zobel. The frozen breeder names
- // Marder/Siam/Zobel/Zobel-Hell only describe the wild D + full-extension case
- // (aa cchm DD EE [gg]); they are kept for that case. Any non-wild D or E (e.g.
- // dd dilute or ee Fuchs) is named from the resolved base colour, so
- // 'aa cchm dd ee gg' no longer collapses to Zobel.
- const [d1, d2] = resolvedPair(g, 'D')
- const wildD = d1 === 'D' && d2 === 'D'
- const fullExtension = eFamily(g) === null // E expresses full 'E' (not Fuchs/Schimmel)
- if (wildD && fullExtension) {
- const [g1, g2] = resolvedPair(g, 'G')
- const grey = g1 === 'g' && g2 === 'g'
- if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
- return bothCchm ? 'Marder' : 'Siam'
- }
- // dilute and/or Fuchs/Schimmel aa colourpoint → derive from the base colour.
- const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
- if (!base) return null
- const DILUTE = 'Dilute '
- if (base.startsWith(DILUTE)) {
- return `${DILUTE}CP-${base.slice(DILUTE.length)}${bothCchm ? '' : '-Hell'}`
- }
- return `CP-${base}${bothCchm ? '' : '-Hell'}`
+ // GEN-5 (tickets 3deab547 / efa2b232): the aa marten names (Marder/Siam/Zobel/
+ // Zobel-Hell) are FULL-EXTENSION (E) sable varieties only. A non-agouti
+ // colourpoint that is Fuchs (ee) or Schimmel (ef) is NOT a Marder/Zobel — it
+ // must derive its base generically like the A- branch, so e.g.
+ // aa cchm dd ee gg → 'Dilute CP-Polarfuchs' (dilute + fox + grey), never Zobel.
+ if (!agouti && eFamily(g) === null) {
+ const [g1, g2] = resolvedPair(g, 'G')
+ const grey = g1 === 'g' && g2 === 'g'
+ if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
+ return bothCchm ? 'Marder' : 'Siam'
}
- // A- colourpoint: base as if C were full; het (cchm/ch) -> '-Hell' suffix.
+ // Colourpoint base derivation: name the colour as if C were full, then prefix
+ // 'CP-'; het (cchm/ch) gets the '-Hell' suffix. Used by A- and by non-agouti
+ // Fuchs/Schimmel colourpoints (which have no dedicated marten name).
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
if (!base) return null
// GEN-4: if base is a Dilute variety, prefix ordering is 'Dilute CP-X' not 'CP-Dilute X'.
@@ -333,12 +343,22 @@ export function genotypeToFarbschlag(g: Genotype): string {
export function representativeGenotype(entry: FarbschlagEntry): Genotype {
const base = wildType()
const out = {} as Record
+ // GEN-5 (ticket 5826e8e2): a *Fuchsschimmel variety is the HETEROZYGOUS ef/e
+ // animal (a Schimmel-modified Fox), whereas the pure *schimmel varieties
+ // (Rotaugen-/Orange-/Silberschimmel) are HOMOZYGOUS ef/ef. The E token is the
+ // shared phenotype letter 'ef'; the representative genotype must encode the
+ // right zygosity so each entry round-trips back to its own family.
+ const isFuchsschimmel = entry.name.toLowerCase().includes('fuchsschimmel')
for (const locus of LOCUS_ORDER) {
const token = entry.tokens[locus]
if (!token) {
out[locus] = base[locus]
continue
}
+ if (locus === 'E' && token === 'ef' && isFuchsschimmel) {
+ out[locus] = ['ef', 'e'] // het Fuchsschimmel (ef/e), not hom ef/ef
+ continue
+ }
// GEN-3f: a token may encode a HETEROZYGOUS pair as "x/y" (e.g. the het
// colourpoints Siam/Zobel-Hell use C: 'cchm/ch'); otherwise it's homozygous.
const [a, b] = token.includes('/') ? (token.split('/') as [string, string]) : [token, token]
diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json b/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json
index 3625c49..0b1dd93 100644
--- a/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json
+++ b/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json
@@ -236,25 +236,25 @@
},
{
"name": "Polarfuchsschimmel",
- "canonicalGenotype": "AA CC DD efef gg PP spsp rere",
+ "canonicalGenotype": "AA CC DD efe gg PP spsp rere",
"sortOrder": 37,
"image": "polarfuchsschimmel.jpg"
},
{
"name": "Algierfuchsschimmel",
- "canonicalGenotype": "AA CC DD efef GG PP spsp rere",
+ "canonicalGenotype": "AA CC DD efe GG PP spsp rere",
"sortOrder": 38,
"image": "algierfuchsschimmel.jpg"
},
{
"name": "Kohlfuchsschimmel",
- "canonicalGenotype": "aa CC DD efef GG PP spsp rere",
+ "canonicalGenotype": "aa CC DD efe GG PP spsp rere",
"sortOrder": 39,
"image": "kohlfuchsschimmel.jpg"
},
{
"name": "Blaufuchsschimmel",
- "canonicalGenotype": "aa CC DD efef gg PP spsp rere",
+ "canonicalGenotype": "aa CC DD efe gg PP spsp rere",
"sortOrder": 40,
"image": "blaufuchsschimmel.jpg"
},
@@ -272,7 +272,7 @@
},
{
"name": "Goldfuchsschimmel",
- "canonicalGenotype": "AA CC DD efef GG pp spsp rere",
+ "canonicalGenotype": "AA CC DD efe GG pp spsp rere",
"sortOrder": 43,
"image": "goldfuchsschimmel.jpg"
},
@@ -290,7 +290,7 @@
},
{
"name": "Rotfuchsschimmel",
- "canonicalGenotype": "aa CC DD efef GG pp spsp rere",
+ "canonicalGenotype": "aa CC DD efe GG pp spsp rere",
"sortOrder": 46,
"image": "rotfuchsschimmel.jpg"
},
@@ -302,7 +302,7 @@
},
{
"name": "Kohlfuchsschimmel, hell",
- "canonicalGenotype": "aa CC DD efef GG PP spsp rere",
+ "canonicalGenotype": "aa CC DD efe GG PP spsp rere",
"sortOrder": 48,
"image": "kohlfuchsschimmel-hell.jpg"
},
@@ -332,7 +332,7 @@
},
{
"name": "Dilute Blaufuchs",
- "canonicalGenotype": "aa CC dd ee gg pp spsp rere",
+ "canonicalGenotype": "aa CC dd ee gg PP spsp rere",
"sortOrder": 53,
"image": "blaufuchs-dd.jpg"
},
diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json
index cf58486..f598579 100644
--- a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json
+++ b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json
@@ -236,25 +236,25 @@
},
{
"name": "Polarfuchsschimmel",
- "canonicalGenotype": "AA CC DD e[f]e[f] gg PP spsp",
+ "canonicalGenotype": "AA CC DD ee[f] gg PP spsp",
"sortOrder": 37,
"image": "polarfuchsschimmel.jpg"
},
{
"name": "Algierfuchsschimmel",
- "canonicalGenotype": "AA CC DD e[f]e[f] GG PP spsp",
+ "canonicalGenotype": "AA CC DD ee[f] GG PP spsp",
"sortOrder": 38,
"image": "algierfuchsschimmel.jpg"
},
{
"name": "Kohlfuchsschimmel",
- "canonicalGenotype": "aa CC DD e[f]e[f] GG PP spsp",
+ "canonicalGenotype": "aa CC DD ee[f] GG PP spsp",
"sortOrder": 39,
"image": "kohlfuchsschimmel.jpg"
},
{
"name": "Blaufuchsschimmel",
- "canonicalGenotype": "aa CC DD e[f]e[f] gg PP spsp",
+ "canonicalGenotype": "aa CC DD ee[f] gg PP spsp",
"sortOrder": 40,
"image": "blaufuchsschimmel.jpg"
},
@@ -272,7 +272,7 @@
},
{
"name": "Goldfuchsschimmel",
- "canonicalGenotype": "AA CC DD e[f]e[f] GG pp spsp",
+ "canonicalGenotype": "AA CC DD ee[f] GG pp spsp",
"sortOrder": 43,
"image": "goldfuchsschimmel.jpg"
},
@@ -290,7 +290,7 @@
},
{
"name": "Rotfuchsschimmel",
- "canonicalGenotype": "aa CC DD e[f]e[f] GG pp spsp",
+ "canonicalGenotype": "aa CC DD ee[f] GG pp spsp",
"sortOrder": 46,
"image": "rotfuchsschimmel.jpg"
},
@@ -302,7 +302,7 @@
},
{
"name": "Kohlfuchsschimmel, hell",
- "canonicalGenotype": "aa CC DD e[f]e[f] GG PP spsp",
+ "canonicalGenotype": "aa CC DD ee[f] GG PP spsp",
"sortOrder": 48,
"image": "kohlfuchsschimmel-hell.jpg"
},
@@ -332,7 +332,7 @@
},
{
"name": "Dilute Blaufuchs",
- "canonicalGenotype": "aa CC dd ee gg pp spsp",
+ "canonicalGenotype": "aa CC dd ee gg PP spsp",
"sortOrder": 53,
"image": "blaufuchs-dd.jpg"
},
diff --git a/gerbil-manager-web/src/genetics/genotype.ts b/gerbil-manager-web/src/genetics/genotype.ts
index fb089ab..5c05eeb 100644
--- a/gerbil-manager-web/src/genetics/genotype.ts
+++ b/gerbil-manager-web/src/genetics/genotype.ts
@@ -40,6 +40,38 @@ export function canonicalPair(locus: LocusKey, a: string, b: string): AllelePair
return rank(a) <= rank(b) ? [a, b] : [b, a]
}
+/**
+ * GEN-5 — the breeder's UNKNOWN-allele rule (ticket 3e643ef1, confirmed by the
+ * Züchterin): an unknown allele '?' is a COPY of the known, visible partner
+ * allele until the gene is determined. So a single-unknown pair resolves to the
+ * homozygote of the KNOWN allele:
+ * A? → AA D? → DD [e,?] → ee [E,?] → EE [ef,?] → ef/ef
+ * Only when BOTH alleles are unknown is the locus genuinely undetermined; it then
+ * falls back to the wild-type reading (most-dominant colour allele, but the
+ * recessive UNMARKED allele for the Sp/Re/Sls markers so an unknown marker never
+ * implies Schecke/Rex/WP).
+ *
+ * This single rule is shared by phenotype/catalog resolution (catalog.ts) and the
+ * Punnett gamete weights (punnett.ts), so an unknown allele never invents a
+ * recessive phenotype (no phantom Dilute/efef/Unbekannt in offspring lists).
+ */
+export function resolveAllelePair(locus: LocusKey, pair: AllelePair): [string, string] {
+ const [a, b] = pair
+ const aUnknown = a === WILDCARD
+ const bUnknown = b === WILDCARD
+ if (!aUnknown && !bUnknown) return [a, b]
+ if (aUnknown && bUnknown) {
+ // Fully unknown: wild-type reading (markers default to the unmarked recessive).
+ const alleles = LOCI[locus].alleles
+ const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls'
+ const fb = isMarker ? alleles[alleles.length - 1] : alleles[0]
+ return [fb, fb]
+ }
+ // Exactly one unknown → copy of the known partner allele (homozygous).
+ const known = aUnknown ? b : a
+ return [known, known]
+}
+
function assertAllele(locus: LocusKey, allele: string): void {
if (allele === WILDCARD) return
if (!LOCI[locus].alleles.includes(allele)) {
@@ -180,27 +212,27 @@ function normalizeToken(tok: string): string | null {
let t = tok
if (t === 'WP') t = 'Slsl'
t = t.replace(/S\(l\)/g, 'Sl').replace(/s\(l\)/g, 'sl')
- // GEN-3b (#32/#34): Underwhite == G locus. Strip the breeder's "[d]" (dense
- // underwhite) annotation from the uw/Uw token BEFORE aliasing to G/g, so that
- // "Uwuw[d]" → "Gg" and "uw[d]uw[d]" → "gg" (mirrors tools/import/genotype.py
- // _rewrite_uw). Without this the "[d]" survived → splitToken("Gg[d]") threw and
- // the frontend fell back to "Unbekannter Farbschlag" / leaked the raw uw token.
- t = t.replace(/(Uw|uw)\[d\]/g, '$1')
+ // GEN-5: dense-underwhite modifier uw[d]/Uw[d] (G-locus). The German "[d]"
+ // dense marker is a shade qualifier, not a separate allele — strip it BEFORE
+ // the Uw→G alias so e.g. "Uwuw[d]" / "uw[d]uw[d]" parse as Gg / gg, not "Gg[d]"
+ // (which crashes splitToken). Mirrors tools/import/genotype.py _rewrite_uw.
+ t = t.replace(/uw\[d\]/gi, 'uw')
t = t.replace(/Uw/g, 'G').replace(/uw/g, 'g')
// GEN-3h: accept bracket display notation → canonical internal symbols.
t = t.replace(/e\[f\]/g, 'ef') // Schimmel allele display form → internal
t = t.replace(/c\[chm\]/g, 'cchm') // Colourpoint display form → internal
t = t.replace(/c\[h\]/g, 'ch') // Himalayan display form → internal
- // #42 (E-locus e-dash): Fuchs (e) is RECESSIVE — a visible fox MUST be
- // homozygous "ee". The herdbook form "ee[-]" (fox allele + unknown E-type
- // second allele) therefore resolves to "ee" (Fuchs), NOT [e,?]; the recessive
- // phenotype implies homozygosity. A bare "e-" / "e[-]" (a single recessive
- // fox allele with an unknown partner) is genetically impossible and is left to
- // be rejected by splitToken (invalid → genotypeInvalid path).
+ // #42 (E-locus): a visible Fuchs is RECESSIVE → MUST be homozygous "ee". The herdbook
+ // form "ee[-]" (fox allele + unknown E-type partner) therefore resolves to "ee" (Fuchs),
+ // NOT [e,?] — the recessive phenotype implies homozygosity. A bare "e-"/"e[-]" (a lone
+ // recessive fox with an unknown partner) is genetically impossible and is rejected below.
t = t.replace(/ee\[-\]/g, 'ee').replace(/ee-/g, 'ee')
- // CR-1a: allele-prefixed bracket-unknown like cc[-]: when e[-]/c[-] is PRECEDED
- // by a letter it is the second unknown allele in a 2-allele token. Lookbehind
- // strips only the bracket part; the leading allele stays.
+ // CR-1a: allele-prefixed bracket-unknown like ee[-] (Silvain).
+ // When e[-] is PRECEDED by a letter it is the second unknown allele in a
+ // 2-allele token (e.g. ee[-] → e + e[-] → e + ?). Lookbehind strips only
+ // the e[-] part; the leading allele stays. Standalone e[-] falls through to
+ // the generic [-]→? rule below (which makes the bracket-dash a wildcard,
+ // leaving the leading allele intact for splitToken).
t = t.replace(/(?<=[A-Za-z])e\[-\]/g, '?')
t = t.replace(/(?<=[A-Za-z])c\[-\]/g, '?')
t = t.replace(/(?<=[A-Za-z])c$/g, '?')
@@ -266,12 +298,14 @@ export function fromDisplayString(input: string): Genotype {
const locus = ALLELE_TO_LOCUS[refAllele]
if (!locus) throw new Error(`Unknown allele "${refAllele}" in token "${token}"`)
if (acc[locus]) throw new Error(`Locus ${locus} given twice`)
- // #42: a lone recessive Fuchs allele with an unknown partner ("e-"/"e[-]" →
- // [e,?]) is genetically impossible — fox is recessive, so a fox allele is
- // only visible homozygous (ee, written "ee[-]"). Reject it so the UI surfaces
- // the genotypeInvalid message instead of silently mis-computing the colour.
+ // #42: a lone recessive Fuchs allele with an unknown partner ("e-"/"e[-]" → [e,?]) is
+ // genetically impossible — fox is recessive, so a fox allele is only visible homozygous
+ // ("ee", written "ee[-]"). Reject it so the UI surfaces the invalid-genotype message
+ // instead of silently mis-computing the colour. ("ee[-]" was already normalized to "ee".)
if (locus === 'E' && ((a === 'e' && b === WILDCARD) || (a === WILDCARD && b === 'e'))) {
- throw new Error(`Invalid E-locus token "${token}": lone recessive "e" with unknown partner (use "ee[-]" for Fuchs or "E-" for unknown)`)
+ throw new Error(
+ `Invalid E-locus token "${token}": lone recessive "e" with unknown partner (use "ee[-]" for Fuchs or "E-" for unknown)`,
+ )
}
acc[locus] = canonicalPair(locus, a, b)
}
@@ -301,3 +335,75 @@ export function displayGenotypeSafe(raw: string | null | undefined): string {
export function hasUnknown(g: Genotype): boolean {
return LOCUS_ORDER.some((l) => g[l][0] === WILDCARD || g[l][1] === WILDCARD)
}
+
+/** Per-locus note about an allele that parent-inference filled in. */
+export interface ParentInferredLocus {
+ readonly locus: LocusKey
+ /** The allele a homozygous parent forced onto the child. */
+ readonly allele: string
+ /** 'father' | 'mother' — which parent was homozygous. */
+ readonly from: 'father' | 'mother'
+}
+
+export interface ParentInferenceResult {
+ readonly genotype: Genotype
+ /** Loci whose unknown allele was resolved from a parent (empty = nothing changed). */
+ readonly inferred: ParentInferredLocus[]
+}
+
+/**
+ * GEN-5 (tickets cc9ea3fe / 1a508c04, breeder rule via Mendel): a child's UNKNOWN
+ * allele can be filled in from a HOMOZYGOUS parent, which can only pass that one
+ * allele. E.g. Mamta Mini stored E = [E,?]; her father Geely is ee (homozygous
+ * fox) so he must pass an 'e' — the child's unknown E allele therefore IS 'e',
+ * giving Ee (not the copy-of-known EE default).
+ *
+ * Rule, per locus, ONLY for an allele still unknown ('?') in the child:
+ * - if a parent is homozygous (both alleles equal and known), that allele is
+ * forced onto the child's unknown slot.
+ * - the father is checked first; if he doesn't resolve it, the mother is tried.
+ * - a parent allele is only accepted if it is one the child could legitimately
+ * carry at that locus (it always is for a real parent, but we guard anyway).
+ * Pairs with no unknown, or where no parent is homozygous, are left untouched
+ * (still subject to the copy-of-known display/colour rule elsewhere).
+ */
+export function inferUnknownsFromParents(
+ child: Genotype,
+ father: Genotype | null | undefined,
+ mother: Genotype | null | undefined,
+): ParentInferenceResult {
+ const out = {} as Record
+ const inferred: ParentInferredLocus[] = []
+ for (const locus of LOCUS_ORDER) {
+ const [a, b] = child[locus]
+ const aUnknown = a === WILDCARD
+ const bUnknown = b === WILDCARD
+ if (!aUnknown && !bUnknown) {
+ out[locus] = child[locus]
+ continue
+ }
+ const homForced = (p: Genotype | null | undefined): string | null => {
+ if (!p) return null
+ const [pa, pb] = p[locus]
+ if (pa === WILDCARD || pb === WILDCARD) return null
+ return pa === pb ? pa : null
+ }
+ const fatherAllele = homForced(father)
+ const forced = fatherAllele ?? homForced(mother)
+ const from: 'father' | 'mother' = fatherAllele ? 'father' : 'mother'
+ if (forced && (aUnknown !== bUnknown)) {
+ // Exactly one unknown slot → fill it with the forced parent allele.
+ const known = aUnknown ? b : a
+ out[locus] = canonicalPair(locus, known, forced)
+ inferred.push({ locus, allele: forced, from })
+ } else if (forced && aUnknown && bUnknown) {
+ // Both unknown but a parent is homozygous → that allele is certain on one
+ // slot; the other stays unknown.
+ out[locus] = canonicalPair(locus, forced, WILDCARD)
+ inferred.push({ locus, allele: forced, from })
+ } else {
+ out[locus] = child[locus]
+ }
+ }
+ return { genotype: makeGenotype(out), inferred }
+}
diff --git a/gerbil-manager-web/src/genetics/index.ts b/gerbil-manager-web/src/genetics/index.ts
index d439aef..b5a2eef 100644
--- a/gerbil-manager-web/src/genetics/index.ts
+++ b/gerbil-manager-web/src/genetics/index.ts
@@ -21,9 +21,16 @@ export {
fromJSON,
hasUnknown,
displayGenotypeSafe,
+ resolveAllelePair,
+ inferUnknownsFromParents,
WILDCARD,
} from './genotype'
-export type { Genotype, AllelePair } from './genotype'
+export type {
+ Genotype,
+ AllelePair,
+ ParentInferenceResult,
+ ParentInferredLocus,
+} from './genotype'
export { LOCI, LOCUS_ORDER } from './loci'
export type { LocusKey, LocusDef } from './loci'
diff --git a/gerbil-manager-web/src/genetics/punnett.ts b/gerbil-manager-web/src/genetics/punnett.ts
index a416548..0995fc4 100644
--- a/gerbil-manager-web/src/genetics/punnett.ts
+++ b/gerbil-manager-web/src/genetics/punnett.ts
@@ -10,72 +10,40 @@
* Wildcard ("?") alleles are expanded uniformly over the locus' allele set
* before combining, so a parent known only by phenotype can still be paired.
*/
-import { add, frac, multiply, ONE, type Fraction } from './fraction'
-import { dominanceRank, LOCI, LOCUS_ORDER, type LocusKey } from './loci'
+import { add, frac, multiply, type Fraction, ONE } from './fraction'
+import { LOCUS_ORDER, type LocusKey } from './loci'
import {
canonicalPair,
+ resolveAllelePair,
toDisplayString,
- WILDCARD,
type AllelePair,
type Genotype,
} from './genotype'
-/**
- * #37/#39/#40/#41: which concrete alleles an UNKNOWN partner allele may actually be,
- * given the KNOWN allele it is paired with at this locus.
- *
- * A hidden allele is constrained by the recorded (visible) one:
- * 1. It can NEVER be more dominant than the known allele — otherwise the animal's
- * phenotype would be different from what the breeder recorded. So the unknown
- * only ranges over alleles with dominance rank >= rank(known) (equal or more
- * recessive). This kills impossible more-dominant offspring morphs.
- * 2. It can never be an allele that is VISIBLE in the heterozygote, unless the
- * animal already expresses it. At the E locus 'ef' (Schimmel/roan) shows even
- * heterozygously, so a non-Schimmel animal (known E or e) cannot secretly carry
- * 'ef'. Excluding it removes the phantom Schimmel/efef predictions (#41).
- *
- * When BOTH alleles are unknown the locus is genuinely unconstrained → full set.
- */
-function unknownPartnerOptions(locus: LocusKey, known: string): readonly string[] {
- const alleles = LOCI[locus].alleles
- if (known === WILDCARD) return alleles // fully unknown locus: any allele
- const knownRank = dominanceRank(locus, known)
- return alleles.filter((a) => {
- if (dominanceRank(locus, a) < knownRank) return false // can't outrank the visible allele
- // E-locus 'ef' is visible in het: only possible if the animal is itself Schimmel.
- if (locus === 'E' && a === 'ef' && known !== 'ef') return false
- return true
- })
-}
-
/** A probability distribution over outcomes of type T (keyed by a string). */
export interface DistEntry {
readonly value: T
readonly probability: Fraction
}
-/** Expand a (possibly wildcard) parent allele pair into weighted concrete alleles. */
+/**
+ * Expand a (possibly partly-unknown) parent allele pair into weighted concrete
+ * gamete alleles.
+ *
+ * GEN-5 (ticket 3e643ef1, breeder rule): an unknown allele '?' is a COPY of the
+ * known partner allele (A?→AA, D?→DD, [e,?]→ee), NOT a uniform spread over the
+ * whole allele set. Spreading wrongly invented recessive gametes (d, ef, e) that
+ * produced impossible offspring colours — phantom Dilute, efef Schimmel and
+ * 'Unbekannter Farbschlag' in the expected-litter list. After resolution each of
+ * the two (now concrete) alleles contributes 1/2 of the gamete. A fully-unknown
+ * locus resolves to the wild-type homozygote (see resolveAllelePair).
+ */
function parentAlleleWeights(locus: LocusKey, pair: AllelePair): Map {
const weights = new Map()
const addWeight = (allele: string, w: Fraction) => {
weights.set(allele, add(weights.get(allele) ?? frac(0, 1), w))
}
- // The "other" allele of the pair tells us what an unknown is allowed to be:
- // an unknown partner is constrained by the known visible allele (see
- // unknownPartnerOptions), not blown up uniformly over every allele.
- const [a0, a1] = pair
- for (let i = 0; i < 2; i++) {
- const a = pair[i]
- if (a === WILDCARD) {
- const known = i === 0 ? a1 : a0
- const options = unknownPartnerOptions(locus, known)
- // Unknown allele contributes 1/2 of the gamete, split over its possible values.
- const share = frac(1, 2 * options.length)
- for (const concrete of options) addWeight(concrete, share)
- } else {
- addWeight(a, frac(1, 2))
- }
- }
+ for (const a of resolveAllelePair(locus, pair)) addWeight(a, frac(1, 2))
return weights
}
diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx
index 605c145..3bbfc51 100644
--- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx
+++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx
@@ -7,7 +7,14 @@ import { listColorVarieties, listContacts, listEnclosures, listLitters } from '.
import { useApi, useMutation } from '../hooks/useApi'
import { formatDate, genderLabel, statusLabel } from '../format/labels'
import { ALL_TRAITS, TRAIT_CATEGORIES } from '../format/traits'
-import { fromDisplayString, genotypeToFarbschlag, displayGenotypeSafe } from '../genetics'
+import {
+ fromDisplayString,
+ genotypeToFarbschlag,
+ displayGenotypeSafe,
+ toDisplayString,
+ hasUnknown,
+ inferUnknownsFromParents,
+} from '../genetics'
import type { Gender, GerbilStatus } from '../api/types'
import FarbschlagImage from '../components/FarbschlagImage'
import GerbilAcquisitionSection from '../components/GerbilAcquisitionSection'
@@ -140,6 +147,37 @@ export default function GerbilDetailPage() {
const showEnclosure = g.status !== 'Deceased' && g.status !== 'GivenAway'
const geno = describeGenotype(g.genotype)
+ // GEN-5 (Tickets cc9ea3fe / 1a508c04): unbekannte Gencode-Buchstaben aus einem
+ // reinerbigen Elternteil ergänzen (Mendel: ein reinerbiger Elternteil kann nur
+ // dieses eine Allel vererben). Greift nur, wenn der Genotyp ein '-' enthält UND
+ // mindestens ein Elternteil mit Genotyp am eigenen Wurf hinterlegt ist.
+ const genoInferred = (() => {
+ if (!g.genotype) return null
+ let child
+ try {
+ child = fromDisplayString(g.genotype)
+ } catch {
+ return null
+ }
+ if (!hasUnknown(child)) return null
+ const parse = (s: string | null | undefined) => {
+ if (!s) return null
+ try {
+ return fromDisplayString(s)
+ } catch {
+ return null
+ }
+ }
+ const f = parse(father.data?.genotype)
+ const m = parse(mother.data?.genotype)
+ if (!f && !m) return null
+ const res = inferUnknownsFromParents(child, f, m)
+ if (res.inferred.length === 0) return null
+ const display = toDisplayString(res.genotype)
+ if (display === geno?.display) return null
+ return { display, farbschlag: genotypeToFarbschlag(res.genotype) }
+ })()
+
const lookup = (map: Map, key: string | null) => (key ? (map.get(key) ?? '—') : '—')
const storedColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null
@@ -346,14 +384,29 @@ export default function GerbilDetailPage() {
{geno.display}
+ {genoInferred && (
+
+ {' → '}
+ {genoInferred.display}{' '}
+ ⮑ {t.detail.genotypeInferred}
+
+ )}
- {geno.farbschlag}
- {storedColorName &&
- geno.farbschlag !== de.genetics.unknownFarbschlag &&
- storedColorName !== geno.farbschlag.replace(' Schecke', '').replace(' Rex', '') && (
- ⚠ {t.detail.farbschlagMismatch}
- )}
+ {(() => {
+ const resolvedFarbschlag = (genoInferred ?? geno).farbschlag
+ return (
+ <>
+ {resolvedFarbschlag}
+ {storedColorName &&
+ resolvedFarbschlag !== de.genetics.unknownFarbschlag &&
+ storedColorName !==
+ resolvedFarbschlag.replace(' Schecke', '').replace(' Rex', '') && (
+ ⚠ {t.detail.farbschlagMismatch}
+ )}
+ >
+ )
+ })()}
) : (
diff --git a/gerbil-manager-web/src/pages/rennmausakte.css b/gerbil-manager-web/src/pages/rennmausakte.css
index 7305160..c19fbd1 100644
--- a/gerbil-manager-web/src/pages/rennmausakte.css
+++ b/gerbil-manager-web/src/pages/rennmausakte.css
@@ -286,6 +286,20 @@
color: var(--ak-warn);
font-weight: 600;
}
+/* GEN-5: parent-inferred genotype hint (Ee[-] → Ee aus den Eltern ergänzt). */
+.ak-inferred {
+ white-space: nowrap;
+}
+.ak-inferred-chip {
+ display: inline-block;
+ background: var(--ak-tan);
+ color: var(--color-text);
+ border-radius: 999px;
+ padding: 1px 9px;
+ font-weight: 600;
+ font-size: 12px;
+ vertical-align: middle;
+}
/* ---------- Character chips ---------- */
.ak-cgroup {
diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts
index dca0c5a..a88ba1b 100644
--- a/gerbil-manager-web/src/strings/de.ts
+++ b/gerbil-manager-web/src/strings/de.ts
@@ -112,6 +112,10 @@ export const de = {
resolvedPrefix: 'Errechnet',
farbschlagMismatch: 'Weicht vom eingetragenen Farbschlag ab.',
genotypeNotSet: 'Kein Genotyp hinterlegt.',
+ // GEN-5: ein unbekannter Gencode-Buchstabe wurde aus einem reinerbigen
+ // Elternteil ergänzt (z. B. Vater ee ⇒ Kind erbt e). %s = aufgelöster Genotyp.
+ genotypeInferred: 'Aus den Eltern ergänzt',
+ genotypeInferredTitle: 'Ein unbekanntes Gen wurde aus einem reinerbigen Elternteil abgeleitet.',
testMating: 'Probeverpaarung',
edit: 'Bearbeiten',
back: 'Zurück zur Liste',
diff --git a/tools/import/test_genotype.py b/tools/import/test_genotype.py
index 4267c18..d614ad7 100644
--- a/tools/import/test_genotype.py
+++ b/tools/import/test_genotype.py
@@ -28,6 +28,11 @@ r = g.parse("uwuw")
check("uwuw -> gg", r["mapped8locus"].get("G") == ["g", "g"])
r = g.parse("uw[d]uw[d]")
check("uw[d]uw[d] -> gg (dense underwhite)", r["mapped8locus"].get("G") == ["g", "g"])
+# GEN-5 (ticket 5151ab20 / Vance): the het 'Uwuw[d]' (one Underwhite, one dense
+# underwhite) must parse to the G locus as Gg — never crash on the [d] modifier.
+r = g.parse("aa Cc[chm] D- ee Uwuw[d] PP spsp")
+check("Uwuw[d] -> Gg (Vance, het dense underwhite)", r["mapped8locus"].get("G") == ["G", "g"])
+check("Uwuw[d] full string: nothing unmapped", r["unmappedTokens"] == [])
# Gg and Uwuw must produce the SAME mapped locus (so they stop being a conflict)
check("Gg identical to Uwuw at G locus",