Compare commits
3 Commits
54e873c115
...
5a6d1c2541
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a6d1c2541 | |||
| bf074696b4 | |||
| fdad1633cd |
@@ -3,6 +3,7 @@ using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace GerbilManager.Tests
|
||||
{
|
||||
@@ -286,5 +287,56 @@ namespace GerbilManager.Tests
|
||||
Assert.NotNull(father.DateOfDeath); // Todesdatum aus Override angewandt
|
||||
Assert.Equal(GerbilStatus.Deceased, father.Status); // ... und Status daraus abgeleitet
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setzt bzw. entfernt die Scheckungsart im gestageten Payload (Testdaten werden hier
|
||||
/// selbst aufgebaut — der Test hängt bewusst an keiner echten conflict-decisions.json).
|
||||
/// </summary>
|
||||
private void SetPayloadSpottingType(string? value, bool remove = false)
|
||||
{
|
||||
var root = JsonNode.Parse(File.ReadAllText(_resolvedJsonPath))!;
|
||||
var papa = root["Gerbils"]!.AsArray()
|
||||
.First(g => (string?)g!["Name"] == "Papa")!.AsObject();
|
||||
if (remove) papa.Remove("SpottingType");
|
||||
else papa["SpottingType"] = value;
|
||||
File.WriteAllText(_resolvedJsonPath, root.ToJsonString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestResolved_writes_spotting_type_and_keeps_it_when_payload_has_none()
|
||||
{
|
||||
// Ticket a547be62 (Eliza): Die Scheckungsart kennt der Importer nur über
|
||||
// conflict-decisions.json — sie MUSS also durch den Ingest getragen werden.
|
||||
// Umgekehrt darf ein leerer Payload-Wert eine von Hand in der App erfasste
|
||||
// Scheckungsart NICHT nullen (der Importer weiß von ihr nichts).
|
||||
using var db = NewDb();
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?> { { "Import:SourcePath", _dir } })
|
||||
.Build();
|
||||
|
||||
// (1) Neues Tier: Wert aus dem Payload landet in der DB.
|
||||
SetPayloadSpottingType("Ansatzschecke");
|
||||
await new IngestResolvedService(db, config, null!).RunAsync();
|
||||
var father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
|
||||
Assert.Equal("Ansatzschecke", father.SpottingType);
|
||||
|
||||
// (2) Re-Ingest, Feld fehlt im Payload → bestehender Wert bleibt stehen.
|
||||
SetPayloadSpottingType(null, remove: true);
|
||||
await new IngestResolvedService(db, config, null!).RunAsync();
|
||||
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
|
||||
Assert.Equal("Ansatzschecke", father.SpottingType);
|
||||
|
||||
// (3) Re-Ingest mit explizitem null → ebenfalls kein Nullen (Guard im Update-Zweig).
|
||||
SetPayloadSpottingType(null);
|
||||
await new IngestResolvedService(db, config, null!).RunAsync();
|
||||
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
|
||||
Assert.Equal("Ansatzschecke", father.SpottingType);
|
||||
|
||||
// (4) Ein NEUER Wert aus dem Payload gewinnt weiterhin (Entscheidung korrigiert).
|
||||
SetPayloadSpottingType("Mantelschecke");
|
||||
await new IngestResolvedService(db, config, null!).RunAsync();
|
||||
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
|
||||
Assert.Equal("Mantelschecke", father.SpottingType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,10 +411,12 @@ public class ApplicationContext : DbContext
|
||||
("Elfenbein", "AA CC DD EE gg pp spsp rere", 21),
|
||||
("Kohlfuchs", "aa CC DD ee GG PP spsp rere", 22),
|
||||
("Polarfuchs", "AA CC DD ee gg PP spsp rere", 23),
|
||||
("Saphir", "aa CC DD EE GG pp spsp rere", 24),
|
||||
// GEN-6 (Tickets bde4ec70/f89e95ad): C-Zygotie unterscheidet Platin (CC),
|
||||
// Saphir (Ccchm) und Platin-Hell (Cch) — vorher waren alle drei "aa CC ...".
|
||||
("Saphir", "aa Ccchm DD EE GG pp spsp rere", 24),
|
||||
("Orangeschimmel","AA CC DD efef GG PP spsp rere", 25),
|
||||
("Topas", "AA CC DD EE GG pp spsp rere", 26),
|
||||
("Platin-Hell", "aa CC DD EE GG pp spsp rere", 27),
|
||||
("Platin-Hell", "aa Cch DD EE GG pp spsp rere", 27),
|
||||
// --- Dilute (dd) varieties (IDs 29-32) ---
|
||||
("Dilute Agouti", "AA CC dd EE GG PP spsp rere", 28),
|
||||
("Dilute Silberagouti","AA CC dd EE gg PP spsp rere", 29),
|
||||
|
||||
@@ -128,6 +128,11 @@ namespace GerbilManagerWebAPI.Import
|
||||
eg.EnclosureId = g.EnclosureId; eg.ColorVarietyId = g.ColorVarietyId;
|
||||
eg.DateOfBirth = g.DateOfBirth; eg.DateOfDeath = g.DateOfDeath; eg.CauseOfDeath = g.CauseOfDeath;
|
||||
eg.GoHomeDate = g.GoHomeDate; eg.Genotype = g.Genotype; eg.Notes = g.Notes;
|
||||
// SpottingType nur übernehmen, wenn der Payload einen Wert liefert: die
|
||||
// Scheckungsart kennt der Importer ausschließlich über conflict-decisions
|
||||
// (Ticket a547be62). Ohne diesen Guard würde eine von Hand in der App
|
||||
// erfasste Scheckungsart bei jedem Ingest genullt.
|
||||
if (g.SpottingType is not null) eg.SpottingType = g.SpottingType;
|
||||
eg.ImportSource = g.ImportSource; eg.ExternalRef = g.ExternalRef; eg.RawImportData = g.RawImportData;
|
||||
eg.Provenance = g.Provenance; eg.OriginBreeder = g.OriginBreeder;
|
||||
eg.CharacterTraits = g.CharacterTraits; eg.CharacterNote = g.CharacterNote;
|
||||
@@ -143,7 +148,8 @@ namespace GerbilManagerWebAPI.Import
|
||||
OriginContactId = g.OriginContactId, ReceiverContactId = g.ReceiverContactId,
|
||||
EnclosureId = g.EnclosureId, ColorVarietyId = g.ColorVarietyId,
|
||||
DateOfBirth = g.DateOfBirth, DateOfDeath = g.DateOfDeath, CauseOfDeath = g.CauseOfDeath,
|
||||
GoHomeDate = g.GoHomeDate, Genotype = g.Genotype, Notes = g.Notes,
|
||||
GoHomeDate = g.GoHomeDate, Genotype = g.Genotype, SpottingType = g.SpottingType,
|
||||
Notes = g.Notes,
|
||||
ImportSource = g.ImportSource, ExternalRef = g.ExternalRef, RawImportData = g.RawImportData,
|
||||
Provenance = g.Provenance, OriginBreeder = g.OriginBreeder, NameSearch = g.NameSearch,
|
||||
CharacterTraits = g.CharacterTraits, CharacterNote = g.CharacterNote,
|
||||
|
||||
1927
GerbilManagerWebAPI/Migrations/20260819212610_ReseedColorVarietiesGen6Saphir.Designer.cs
generated
Normal file
1927
GerbilManagerWebAPI/Migrations/20260819212610_ReseedColorVarietiesGen6Saphir.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerbilManagerWebAPI.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReseedColorVarietiesGen6Saphir : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000025"),
|
||||
column: "CanonicalGenotype",
|
||||
value: "aa Ccchm DD EE GG pp spsp rere");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000028"),
|
||||
column: "CanonicalGenotype",
|
||||
value: "aa Cch DD EE GG pp spsp rere");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000025"),
|
||||
column: "CanonicalGenotype",
|
||||
value: "aa CC DD EE GG pp spsp rere");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000028"),
|
||||
column: "CanonicalGenotype",
|
||||
value: "aa CC DD EE GG pp spsp rere");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ namespace GerbilManagerWebAPI.Migrations
|
||||
new
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000025"),
|
||||
CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
|
||||
CanonicalGenotype = "aa Ccchm DD EE GG pp spsp rere",
|
||||
Name = "Saphir",
|
||||
SortOrder = 24
|
||||
},
|
||||
@@ -441,7 +441,7 @@ namespace GerbilManagerWebAPI.Migrations
|
||||
new
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000028"),
|
||||
CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
|
||||
CanonicalGenotype = "aa Cch DD EE GG pp spsp rere",
|
||||
Name = "Platin-Hell",
|
||||
SortOrder = 27
|
||||
},
|
||||
|
||||
@@ -1,55 +1,676 @@
|
||||
# Ticket-Triage — Lauf gegen Prod (2026-07-21)
|
||||
|
||||
Untersuchung: 11 Cluster-Agenten + 1 Hiro-Agent (read-only) gegen lokale Quellen (Stammbaum-xlsx, _rpro3.db, Wurfchronik) + Prod-API. Fotos vom Haupt-Loop ausgewertet. Datenfixes zentral in , lokal regeneriert + verifiziert.
|
||||
|
||||
## A) Daten-Fixes — angewandt + lokal in resolved_import.json verifiziert (bereit für Prod-Re-Ingest)
|
||||
|
||||
- **00715df9** Merle: Merles Farbe/Gencode habe ich aus ihrem eigenen Hinweis uebernommen (Aa chmchm D- ee GG P- Spsp), und ihre Eltern Akane und Bonaparte von den Schlossmaeusen sind bereits richtig eingetragen. Nur ihr genaues Geburtsdatum fehlt mir noch, weil der Stammbaum 'Pukas Kids' bei mir nicht auffindbar ist.
|
||||
- **06b6734d** Jamie: Die zwei Jamie-Eintraege gehoeren zusammen. Ich fuehre sie zu einem zusammen (Geburtsdatum 27.12.2010, Sohn von Danny und Jana); dadurch landen auch alle seine Wuerfe und Nachzuchten beim richtigen Jamie.
|
||||
- **1500f0cc** Targa / Druna: Targa hatte zwei Wuerfe, beide mit Marlin of Black Forest: einen am 14. Oktober 2020 und einen am 17. April 2021. Ihre Tochter Druna (Silberagouti) stammt aus dem Wurf vom 17. April 2021 und wird neu angelegt. Beide wurden zusammen an Nicole Nuzzo abgegeben, wo Druna am 29. Juli 2024 und Targa am 24. Oktober 2022 an Tumoren starben.
|
||||
- **1be92139** Living Force's Idefix (*05.04.2016, 9eee3314): Idefix' Mutter ist Living Force's Vally. Ich habe sie zusammen mit seinem Vater (Living Force's Nando) im Stammbaum ergaenzt.
|
||||
- **2c49eebf** Socke → Marty von den Kleinen Chaoten: Socke ist dasselbe Tier wie Marty von den Kleinen Chaoten und war versehentlich dreifach angelegt. Die Doppel-Eintraege werden zusammengefuehrt, der Name auf Marty von den Kleinen Chaoten gesetzt und seine richtigen Eltern Milow und Hina eingetragen (die falsche Zuordnung zu Taro faellt weg).
|
||||
- **3269a722** Targa: Targas Geburtswurf vom 1. April 2019 wird jetzt korrekt als Wurf angelegt: Vater ist South Dakota von den Kleinen Chaoten (Silberschimmel), Mutter ist Chevrolet Corvette of Topolino. Ihre Wurfschwester Tennessee wird demselben Wurf zugeordnet, damit auch deren Eltern stimmen.
|
||||
- **37cf2f40** Ichika von den Kleinen Chaoten: Bei Ichika waren die Eltern falsch. Richtig sind Jeremy von den Kleinen Chaoten (Vater) und Inory von den Kleinen Chaoten (Mutter) - das wird korrigiert.
|
||||
- **44a9b677** Hiro: Hiro- und Mino-Doppeleinträge zusammengeführt (der vollständige mit Stammbaum bleibt); Würfe mit Skarlett und Mino×Misaki hängen jetzt am richtigen Tier; April-Wurf der richtigen Skarlett zugeordnet.
|
||||
- **45576826** Blacky von Privatzucht Seligenstadt: Es gab Blacky versehentlich doppelt (einmal als "von PZ Seligenstadt", einmal als "von Privatzucht Seligenstadt"). Beide Eintraege sind zu einem zusammengefuehrt; der Name "Blacky von Privatzucht Seligenstadt" bleibt stehen. Geburts- und Todesdatum stimmen bei beiden ueberein, es gehen keine Daten verloren.
|
||||
- **49548ac0** Jana: Jana und Jana of little longnoses sind dasselbe Tier. Ich fuehre die beiden zu einem Eintrag zusammen (Name: Jana of little longnoses, Herkunft Clan of little longnoses), mit ihren Eltern Milon und Darina gen. Talina.
|
||||
- **5fe7c884** Ethan von den Kleinen Chaoten: Bei Ethan fehlte die Mutter. Laut Stammbaum sind seine Eltern Braden gen. Scotty (Vater, schon eingetragen) und Shika von den Kleinen Chaoten (Mutter). Shika wird angelegt und als Mutter ergaenzt.
|
||||
- **71e3e6a1** Roni: Ronis Mutter ist Bonnie gen. Amika von den Kleinen Chaoten (sein Vater Kaleo gen. Halio ist bereits eingetragen). Sie wird als Mutter angelegt und bei Roni ergaenzt. Ihr eigenes Geburtsdatum und ihre Eltern stehen in den Unterlagen leider nicht.
|
||||
- **7f9b3724** Sakura = Gwen gen. Sakura of little Angels: Sakura und 'Gwen gen. Sakura of little Angels' werden zu einem Tier zusammengefuehrt (Herkunft: Clan of little Angels), sodass ihr Geburts- und Sterbedatum, ihre Eltern und ihre Wuerfe an einem einzigen Eintrag haengen.
|
||||
- **873aca5d** Jacky v.d. Kleinen Chaoten: Das ist derselbe Hinweis wie beim anderen Jacky-Ticket - Jackys Eltern werden auf Jack von Privatzucht Maintal (Vater) und Holly gen. Lilly of Ulmer Strolche (Mutter) korrigiert; dieses Ticket kann zusammen mit dem anderen geschlossen werden.
|
||||
- **918a9a3f** Jacky v.d. Kleinen Chaoten: Jackys Eltern werden korrigiert: Vater ist Jack von Privatzucht Maintal und Mutter Holly gen. Lilly of Ulmer Strolche (bisher faelschlich Paul und Akina). Damit bekommen Jacky und ihr Bruder Jack Jr dieselben Eltern, genau wie du beschrieben hast.
|
||||
- **9325e316** Domi (D-Wurf 2010): Bei Domi wird jetzt eingetragen, dass er zusammen mit seinem Wurfbruder Dastan an Susanne und Nina Thomas abgegeben wurde.
|
||||
- **9ad13a73** Joghurt v. Privat (*06.09.2013, bab013c9): Joghurt kam aus privater Haltung; seine Eltern sind unbekannt. Bei uns war faelschlich ein erfundener Wurf mit BlackFire als Vater eingetragen -- das habe ich entfernt, seine Eltern bleiben jetzt korrekt unbekannt.
|
||||
- **9bbfb6a0** Stacy: Wir uebernehmen Stacys Daten aus RennerPro: Sie ist die Gold-Schecke, geboren am 23.10.2012, Herkunft Gelnhausener Privatzucht. Ihre Farbe und ihr Erbbild werden ergaenzt und ihr Stammbaum mit den Eltern Vati (Goldfuchs) und Mutti (Agouti-Schecke) wird eingetragen.
|
||||
- **a50116bf** Kuke: Kuke war doppelt angelegt; die beiden Eintraege werden zusammengefuehrt, sodass Farbe (Schwarz-Schecke), Genotyp und ihr Todestag (6.4.2011, gestorben bei einem Ausbruch-Unfall) an einem Tier stehen. Ihre Eltern sind laut RennerPro Marc und Sara. Ihre Nachzucht mit Blacky (Alvin, Flori, Eddy, Feivel) ist bereits richtig.
|
||||
- **aa5238b6** Eliza: Es gab zwei Tiere namens Eliza. Die hier gezeigte Eliza (geboren 2010, gestorben 2011) war nie ein Zuchttier und ist NICHT die Mutter des J5-Wurfs — das war die spaetere Eliza (geboren 2014). Der J5-Wurf haengt bereits an der richtigen Eliza; nur der falsche Hinweis auf ihrer Seite wird entfernt.
|
||||
- **bbf2898b** Malou von den Kleinen Chaoten (frueher 'Schlaganfall Sommer 2019', *10.09.2015): Malou war bei uns versehentlich zweimal angelegt. Ich habe die beiden Eintraege zusammengefuehrt; dabei bekommt Malou ihre richtigen Eltern Koji (Vater) und Zoe (Mutter), und der alte Zusatz 'frueher Schlaganfall Sommer 2019' faellt weg.
|
||||
- **c092f8d2** Jamie: Die beiden Jamie-Eintraege werden zu einem zusammengefuehrt (Geburtsdatum 27.12.2010, aus dem J-Wurf, Sohn von Danny und Jana). Weitere Tiere mit Namen Jamie (eines von 2012, und Jamie Lannister von 2017) sind eigene Tiere und bleiben getrennt.
|
||||
- **d02c2a1b** Blacky von Privatzucht Seligenstadt: Bei Blacky standen zwei falsche Jungtiere als Nachzucht: Antares of Ulmer Strolche und Chagan of Ulmer Strolche. Beide sind nicht von Blacky. Laut RennerPro stammt Antares von Grisu und Carlotta, Chagan von Akuma und Bekter. Sie werden von Blacky geloest; Blackys echte Nachzucht mit Kuke (Alvin, Flori, Eddy, Feivel) bleibt.
|
||||
- **d8e6f1a2** Living Force's Idefix (*05.04.2016, 9eee3314): Bei Idefix war der Stammbaum kaputt: Idefix war irrtuemlich als eigener Elternteil eingetragen. Ich habe seine echten Eltern eingetragen: Vater Living Force's Nando und Mutter Living Force's Vally (beide aus der Zucht Living Force).
|
||||
- **dc64ac0d** Wurf von Danny von PZ Maintal + Jana of little longnoses (J-Wurf): Es gibt keinen Wurf 'Hitze' — das war nur ein Tippfehler. Der Danny-und-Jana-Wurf steht momentan doppelt in der Liste. Sobald die beiden Jana-Eintraege zusammengefuehrt sind, verschmelzen die zwei Wuerfe automatisch zum J-Wurf, und Jamie steht dort zusammen mit seinen Geschwistern Jack, Chip und Jules.
|
||||
- **e0a0c304** Kuke: Das Tier "Kruke" heisst in Wirklichkeit "Kuke" — der Name wird korrigiert.
|
||||
- **e96db323** Jamie: Die beiden Jamie-Eintraege sind dasselbe Tier (Sohn von Jana und Danny) und werden zu einem zusammengefuehrt (Geburtsdatum 27.12.2010).
|
||||
- **f8d55b7a** Stacy: Gleiche Sache wie beim anderen Stacy-Ticket: Wir tragen die Gold-Schecke Stacy mit Geburtsdatum 23.10.2012, Herkunft Gelnhausener Privatzucht, Farbe/Erbbild und ihren Eltern Vati und Mutti aus RennerPro ein.
|
||||
- **ff8d6bd7** Blacky von Privatzucht Seligenstadt: Bei Blacky wird die "Extern"-Markierung entfernt. Er zaehlt als eigenes Bestandstier (zugekauftes Zuchttier), weil er bei dir mit Kuke Nachzucht hatte.
|
||||
|
||||
## B) Bereits korrekt (nur bestätigen)
|
||||
|
||||
- **8b1dbd19** Gaida von den Kleinen Chaoten: Alle Daten aus dem frueheren Gaida-Ticket sind uebernommen: ihr Todesdatum 09.10.2025, die Todesursache Lungenentzuendung mit Fieber, das Abgabedatum 05.01.2025, die Abnehmerin Caroline Emmert und ihre Eltern (eine Geschwisterverpaarung aus dem Wurf von Nisha und Zenon). Nur ihr Status stand faelschlich noch auf 'Zucht' statt 'verstorben' - das wird zusammen mit der anderen Korrektur behoben.
|
||||
- **bd351aa2** Jana: Janas Herkunft steht bereits richtig als Clan of little longnoses. Ihr Sohn Feivel (geboren 25.12.2011) ist bereits als Nicht-Zuchttier gefuehrt — der Feivel, der in der Zucht war, ist ein anderer (der Sohn von Blacky und Kuke). Nur ihr Sohn Jamie war in der Zucht. Sobald die beiden Jana-Eintraege zusammengefuehrt sind, zeigt Janas Seite genau das.
|
||||
|
||||
## C) Rückfrage nötig (→ NeedsInfo)
|
||||
|
||||
- **5851bb94** Spike: Die Verpaarung Spike x Jacky habe ich in deiner Wurfchronik gefunden: dort steht der D7-Wurf (geboren am 14.08.2015, Jungtiere Smartie, Cookie und Cloe) mit den Eltern Spike und Jacky. Spike (geboren 03.04.2015) waere zu diesem Zeitpunkt aber erst gut 4 Monate alt gewesen. War der Vater dieses Wurfs ein anderes Tier - und wenn ja, wer war der richtige Vater von Smartie, Cookie und Cloe? Oder soll die Verpaarung mit Jacky bei Spike einfach entfernt werden?
|
||||
- **a547be62** Eliza: Bei Eliza (geboren am 20.08.2010, Tochter von Blacky und Kuke, verstorben 6.4.2011) stimmt der Gencode nicht — der angezeigte Wert ist nur automatisch aus dem Farbschlag "Schwarz" abgeleitet. Wie lautet ihr korrekter Gencode? (Du hattest ihn frueher in deiner Liste aller Blacky-&-Kuke-Nachkommen genannt; magst du ihn fuer genau diese Eliza kurz wiederholen?)
|
||||
- **f212b73c** Bonaparte von den Schlossmaeusen: Bonapartes Eltern (und Merles genaues Geburtsdatum) finde ich nur im Stammbaum 'Pukas Kids', den ich hier leider nicht oeffnen kann - die Datei ist bei mir nicht vorhanden. Magst du mir Bonapartes Vater und Mutter direkt mit Namen nennen? Und falls im selben Stammbaum vermerkt: an welchem Datum ist Merle geboren? Alternativ kannst du mir die Excel-Datei 'Stammbaum von Pukas Kids' nochmal schicken.
|
||||
- **fbe53c6b** Fumi von den Kleinen Chaoten: Ich finde Fumi weder in den Stammbaum-Excel-Dateien noch in der Wurfchronik als Jungtier - dort steht sie nur als Mutter (zusammen mit Roni). Aus welchem Wurf stammt Fumi bzw. wer sind ihre Eltern (gern mit Geburtsdatum)?
|
||||
|
||||
## D) Code-Fixes (Deploy nötig, KEIN Re-Ingest)
|
||||
|
||||
- **37ab228a** Gaida von den Kleinen Chaoten [backend]: Gaida (04358c1f) hat in Prod dateOfDeath=2025-10-09, goHomeDate=2025-01-05, receiverContactId (Caroline Emmert) UND causeOfDeath gesetzt, aber Status=Breeding -> erscheint deshalb unter dem Default-Filter status=['Breedi
|
||||
- **4b9f49fb** Ungeklärt-Wurf 03.03.2011 (= M-Wurf) [import-logic]: Der Wurf 2c545f8a ('Ungeklaert', 03.03.2011, Eltern Blacky von PZ Seligenstadt x Charlett of little longnoses) ist eine Fehl-Verschmelzung aus 3 Wurfchronik-Quellen mit demselben Datum: (1) Teil1 S.15 = der ECHTE M-Wurf
|
||||
- **8d259edf** Gale von den Kleinen Chaoten (frueher Drake Jr, 7f09dd7b) [import-data]: Umbenennung Drake Jr -> 'Gale von den Kleinen Chaoten', Notiz und Eltern (Vater Drake von den Kleinen Chaoten, Mutter Holly gen Lilly of Ulmer Strolche) sind bereits korrekt gesetzt (conflict-decisions + Prod). OFFEN sin
|
||||
- **9f92c834** CoCo [frontend]: CoCo (5e1931b5, *17.04.2010, Genotyp aa CC DD EE GG PP Spsp rere = Schwarz-Schecke) IST eine Schecke (Sp-Locus Spsp), aber ihr Feld SpottingType ist null. Die Tier-Akte (GerbilDetailPage.tsx Zeile 506: `{g.spottingType &
|
||||
- **f89e95ad** A-Wurf (Blacky von PZ Seligenstadt x Kruke/Kuke, *18.02.2010) [genetics-engine]: Zwei Ursachen. (1) DATEN-Voraussetzung: Die Mutter des A-Wurfs (prod e4898ccc 'Kruke', = die Kuke aus Ticket a50116bf/e0a0c304, *18.09.2009) hat in prod genotype=null. Die erwarteten Farbschlaege werden nur berechnet, we
|
||||
# Ticket-Triage — Lauf gegen Prod (2026-08-19)
|
||||
|
||||
16 offene Tickets (12 Open, 4 Answered) in 13 Bündeln, je ein Triage-Agent gegen Prod-API
|
||||
(http://truenas:8090/api) + lokale Quellen (Stammbaum-xlsx, Wurfchronik, _rpro3.db,
|
||||
tools/import/output/*.json). Ergebnis: **alle 13 Bündel sind umsetzbar, keine Rückfrage nötig.**
|
||||
|
||||
Übergreifender Befund: Prod ist auf dem aktuellen Importer-Stand (2451 Tiere vs. lokal 2450),
|
||||
die beklagten Dubletten stecken also schon in `resolved_import.json`. Mehrere Tickets von heute
|
||||
sind Folge-Tickets aus dem Juli-Lauf, deren Entscheidungen die neuen Chart-Varianten nicht mehr
|
||||
greifen (Wurfchronik-Elternstubs ohne Geburtsdatum).
|
||||
|
||||
## Cluster-Matrix
|
||||
|
||||
| Tickets | Thema | Layer | Cluster | Aufwand | Konfidenz |
|
||||
|---|---|---|---|---|---|
|
||||
| 2322c2a8 | Jay (*24.10.2021) — Farbschlag Zobel-Hell statt Zobel (C-Locus-Tippfehler c[hm]) | import-data | conflict-decisions-genotype-override | S | high |
|
||||
| 8a51ac73 | Kathlin von den Kleinen Chaoten (*20.04.2021) — falsche Eltern (Grossmutter Izumi als Mutter, kein Vater) | import-data | conflict-decisions-parent-override | S | high |
|
||||
| 65266679 | Bijou (*26.06.2022) — falsche Eltern: Arya Stark statt Louis × Nani of Black Forest | import-logic | extract-starless-dob-block | S | high |
|
||||
| 5851bb94 | Spike (*03.04.2015) — Verpaarung mit Jacky bestätigt; Eltern seines Geburtswurfs (T5/TS-Wurf) fehlen | import-data | import-data-conflict-decisions-litterparents | S | high |
|
||||
| 6c89082e | Fast Boy of Golden Lights (*31.03.2017) — beide Eltern im Stammbaum falsch (Großvater als Vater, Schwiegermutter als Mutter) | import-data | conflict-decisions-resolutions-parents | S | high |
|
||||
| 24522f5f | JackJack (b6d8b3ef) — Abgabedatum-Zeile unter Abnehmer in der Tier-Akte (+ kaputtes Abgabedatum 1310-05-13) | frontend | gerbil-detail-abgabedatum-goHomeDate | S | high |
|
||||
| a547be62 | Eliza *20.08.2010 (E-Wurf Blacky x Kuke) — Gencode + Scheckungsart Ansatzschecke | import-logic | decisions-genotype-spottingtype-override | M | high |
|
||||
| 66ef9bdd, 3bbd6ab4 | Jamie (*27.12.2010, J-Wurf Danny x Jana) — Dublette: zwei weitere Wurfchronik-Elternstubs nie gemergt | import-data | conflict-decisions-mergeexternalrefs-wurfchronik-elternstubs | S | high |
|
||||
| 6c9539d2 | Sakura / Gwen gen. Sakura of little Angels — Dublette nach Merge-Fix zurueck (Folge-Ticket zu 7f9b3724) | import-data | wurfchronik-md-stub-dubletten-mergeexternalrefs | S | high |
|
||||
| 4f890829 | „Unbekannt" (be299aaf, *21.06.2014) — namenlose Q3-Dublette + falscher Vater des Q4-Wurfs | import-data | conflict-decisions-wurfchronik-page0009-platzhalter-eltern | S | high |
|
||||
| 7037f5d8 | Merle (2023-06-18): Herkunft leer + noch als Zuchttier im Bestand (Abgabe 22.12.2024 fehlt) | import-data | conflict-decisions-resolutions-merge-and-resolve | M | medium |
|
||||
| bde4ec70, f89e95ad | JackJack (b6d8b3ef) + A-Wurf 18.02.2010 (29f4bae9): Saphir wird als Platin errechnet — C-Locus-Zygotie fehlt im Farbkatalog | genetics-engine | genetik-c-locus-zygotie-saphir-platin | M | high |
|
||||
| 24006ccd, ea803c3d | Kuke (prod e4898ccc): fehlender Zuchtname + alter Name „Kruke" in 5 Wurf-Notizen | import-logic | conflict-decisions+merge_and_resolve-rename | M | high |
|
||||
|
||||
## Befund & Fix je Bündel
|
||||
|
||||
### Jay (*24.10.2021) — Farbschlag Zobel-Hell statt Zobel (C-Locus-Tippfehler c[hm])
|
||||
|
||||
Tickets: `2322c2a8-a616-4967-88f6-031b4519a1ca` · Layer **import-data** · Cluster `conflict-decisions-genotype-override` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Jay steht in Prod auf ColorVarietyId 00000000-0000-0000-0000-000000000053 = "Zobel-Hell" mit Genotype "aa c[chm]c[hm] Dd Ee gg P- spsp". Die Quell-Charts "Stammbaum von Alberto Kids.xlsx" und "Stammbaum von Pukas Kids.xlsx" enthalten diesen String woertlich (sharedStrings geprueft) — das zweite C-Allel ist als "c[hm]" statt "c[chm]" geschrieben (fehlendes c, Tippfehler); das Farbschlag-Label beider Charts lautet korrekt "Zobel". tools/import/genotype.py akzeptiert "hm" in den C-Locus-Regexen (Zeilen 28 und 38), mappt es auf Allel 'c^hm' != 'cchm', dadurch ist in _colourpoint_name (Z. 383/389) both_cchm=False -> "Zobel-Hell". Die Regel "GENOTYPE WINS" in merge_and_resolve.resolve_color_and_genotype (ab Z. 530) laesst diesen falsch abgeleiteten Namen das richtige Quell-Label "Zobel" ueberschreiben. Zusatzschaden: der TS-Parser (gerbil-manager-web/src/genetics/genotype.ts, Allele nur C/cchm/ch) kennt "c[hm]" nicht -> Jays Genotyp ist in UI/Probeverpaarung ungueltig. Dass "c[hm]" = "c[chm]" gemeint ist, ist zweifach belegt: (1) alle anderen Zobel-Tiere der Zucht (Plasma, Smoke, Valentino Fireheart, Kalea "Zobel schecke") haben c[chm]c[chm], "Zobel-Hell" kommt in keinem Chart als Label vor; (2) Genetik-Ausschluss: Vater Hagrid Rubeus of Black Forest = "Aa Cc[chm] dd Ee gg Pp Spsp", Mutter Arya Stark von den Kleinen Chaoten = "aa c[chm]c[chm] DD Ee Gg PP spsp" — kein Elternteil traegt c[h], ein c[h] bei Jay ist also unmoeglich, waehrend c[chm] von beiden Eltern kommen kann (aa/Dd/Ee/gg/spsp passen ebenfalls). Zobel im Katalog = aa c[chm]c[chm] DD EE gg PP spsp (Id ...0004); Dd/Ee/P- sind mit dem Zobel-Phaenotyp vereinbar.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Data-driven Fix in tools/import/conflict-decisions.json — neue Resolution im Array "resolutions" (Mechanismus "genotype"-Override, existiert dort bereits 36x, greift in extract.apply_conflict_decisions via canon(name)+dob):
|
||||
|
||||
{
|
||||
"name": "Jay",
|
||||
"dob": "24.10.2021",
|
||||
"decision": "C-Locus = c[chm]c[chm] — die Quell-Charts schreiben 'c[hm]' (fehlendes c, Tippfehler); Farbschlag-Label beider Charts ist 'Zobel', und c[h] ist genetisch unmoeglich, da beide Eltern kein c[h] tragen (Hagrid Rubeus Cc[chm], Arya Stark c[chm]c[chm]). Ticket 2322c2a8",
|
||||
"genotype": "aa c[chm]c[chm] Dd Ee gg P- spsp",
|
||||
"source": "Zuechterin-Ticket 2322c2a8 (2026-08-19) — „Falsche Farbe. Er ist ein Zobel“"
|
||||
}
|
||||
|
||||
Danach: python tools/import/extract.py && python tools/import/merge_and_resolve.py, dann Upload-Ingest gegen Prod. Erwartetes Ergebnis fuer feebbbe2-c656-505e-a2c1-c0d7dd1a6ebc: Genotype = "aa c[chm]c[chm] Dd Ee gg P- spsp", ColorVarietyId = 00000000-0000-0000-0000-000000000004 (Zobel), Genotyp in der UI wieder parsebar. Kollisionsrisiko geprueft: die zwei weiteren Roh-"Jay"-Saetze (jay, jay-2) haben leeres dob, Key ("jay","") matcht die Resolution nicht.
|
||||
|
||||
Optionale Haertung (separat, weil Genetik-kritisch -> nur mit Tests): in tools/import/genotype.py den Token "hm" aus den C-Locus-Regexen (Zeile 28: "C": re.compile(r"^(C|c)(\[(?:chm|chl|ch|h|hm|e|-)\])?…") und Zeile 38) entfernen ODER "c[hm]" explizit als Alias auf 'c^chm' normalisieren, plus Regressionstest in tools/import/test_genotype.py ("aa c[chm]c[hm] Dd Ee gg P- spsp" darf nicht Zobel-Hell ergeben). Aktuell ist Jay der einzige Datensatz in resolved_import.json mit "hm]"-Token; in den Charts steht "aa Cc[hm] …" noch 2x (Stammbaum von Fire Kids.xlsx, Stammbaum von Stella Kids.xlsx) — dort phaenotypisch irrelevant (C dominant), aber der c[chm]-Carrier-Status fehlt der Probeverpaarung.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Jay wird jetzt richtig als Zobel geführt. In seinen beiden Stammbaum-Dateien hatte sich beim Gencode ein kleiner Schreibfehler eingeschlichen (bei einem der beiden Farb-Bausteine fehlte ein Buchstabe), dadurch hat das Programm ihn als „Zobel-Hell" einsortiert. Sein Gencode lautet nun „aa c[chm]c[chm] Dd Ee gg P- spsp" und passt damit auch zu seinen Eltern Hagrid Rubeus und Arya Stark — die helle Variante wäre bei diesen Eltern gar nicht möglich gewesen. Nebeneffekt: sein Gencode lässt sich jetzt auch wieder für Probeverpaarungen verwenden.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/genotype.py; tools/import/merge_and_resolve.py; tools/import/test_genotype.py
|
||||
|
||||
---
|
||||
|
||||
### Kathlin von den Kleinen Chaoten (*20.04.2021) — falsche Eltern (Grossmutter Izumi als Mutter, kein Vater)
|
||||
|
||||
Tickets: `8a51ac73-c891-4c51-9b09-959856c8c46c` · Layer **import-data** · Cluster `conflict-decisions-parent-override` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Kathlins Eltern wurden per chart-position rekonstruiert und dabei um eine Zeile verrechnet (gleicher Offset-Fehler wie im Akane-Fall 88389f8e: in diesen Charts sitzt das Elternpaar eine Zeile unter dem 3-Zeilen-Kindblock). Prod: LitterId ecff0b8a "Wurf von Izumi + Ella von den Kleinen Chaoten", FatherId=NULL, MotherId=2cb68a8f = Izumi (*07.02.2018) — Izumi ist real Kathlins GROSSMUTTER (Mutter von Yuki). Provenance belegt das Chaos: Vater-Kandidaten Vestra/Unit/Izumi wegen falschem Geschlecht verworfen, Mutter-Kandidat Yuki verworfen, dann Ella ("ein Wurf hat nur einen Vater und eine Mutter") verworfen. Die Stammbaum-xlsx sagen einheitlich in 6 Charts: Kathlin = Vater Yuki von den Kleinen Chaoten (*03.02.2020, maennlich) x Mutter Ella (*10.06.2019) — belegt in "Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx" und "Stammbaum von Sa von Johnny Kids.xlsx" (Kathlin K79-K81, Eltern N80/N81, Grosseltern Q79 Chevrolet Camaro of Topolino + Q80 Izumi bzw. Q81 Louis + Q82 Roswitha, Urgrosseltern R80 "Nduga & Benjiro", R81 "Hannah & Petri", R82 "Jin & Sokrates"), ebenso Blitzis Kids (N67 -> Q67/Q68), Valentino Firehearts Kids (N81 -> Q81/Q82), CP Maedels von Grisu und CP-Polarfuchs von Grisu (K35 -> N36/N37). Zusatzbeleg: die handschriftliche Notiz der Zuechterin direkt unter Kathlins Zelle ("Woher kommt das ch?? Louis? Izumi?") verfolgt genau diese Linie. Genetisch/altersmaessig plausibel (Kathlin AA ee[f] spsp aus Yuki Aa Ee Spsp x Ella Aa ee[f] spsp). Die Vorfahren oberhalb sind in Prod bereits korrekt (Yuki 55fba1b1 -> Wurf fd9310db "Camaro + Izumi"; Ella ebc87c8c -> Wurf d7c4e38c "Louis + Roswitha") — nach dem Fix stimmt die ganze Ahnentafel. Kein Rueckfall: fuer Kathlin existiert bisher kein Eintrag in conflict-decisions.json.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Owner-Regel, data-driven, re-ingest-stabil: EINEN Eintrag in tools/import/conflict-decisions.json -> "resolutions" ergaenzen (kein Live-DB-Patch), danach extract.py + merge_and_resolve.py + Upload-Ingest gegen Prod:
|
||||
|
||||
{
|
||||
"name": "Kathlin von den Kleinen Chaoten",
|
||||
"dob": "20.04.2021",
|
||||
"decision": "Eltern sind Yuki von den Kleinen Chaoten (*03.02.2020, Vater) x Ella (*10.06.2019, Mutter) — so in allen 6 Stammbaum-Charts (z. B. 'Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx' K79 -> N80/N81, Grosseltern Q79 Camaro + Q80 Izumi / Q81 Louis + Q82 Roswitha). Chart-Position hatte die Grossmutter Izumi als Mutter gesetzt und keinen Vater (Zeilen-Offset wie im Akane-Fall).",
|
||||
"father": "Yuki von den Kleinen Chaoten",
|
||||
"fatherDob": "03.02.2020",
|
||||
"mother": "Ella",
|
||||
"motherDob": "10.06.2019",
|
||||
"source": "Ticket 8a51ac73 — Stammbaum-xlsx (Zuechterin verweist ausdruecklich darauf)"
|
||||
}
|
||||
|
||||
Wirkung: extract.apply_conflict_decisions ersetzt Kathlins parentRefs komplett (method=decision, confidence=high), merge_and_resolve legt den virtuellen Wurf "Wurf von Yuki von den Kleinen Chaoten + Ella" (20.04.2021) an; der alte Wurf ecff0b8a (einziges Kind = Kathlin) wird beim Upsert-Ingest stale und entfernt. Namensschluessel "Ella" + motherDob ist erprobt (Danielle-Resolution -> Wurf e1a14122 "Makoto + Ella", motherId ebc87c8c); "Yuki von den Kleinen Chaoten" ist mehrfach vergeben, daher fatherDob zwingend.
|
||||
|
||||
Verifikation nach Ingest: GET /api/gerbils/5a8e3035-... -> litterId zeigt auf "Wurf von Yuki von den Kleinen Chaoten + Ella" mit fatherId 55fba1b1-f59e-5117-92bf-85e950c953f9 und motherId ebc87c8c-0528-5b70-a251-9d32858dbc68; Stammbaum-Ansicht muss darueber Camaro/Izumi und Louis/Roswitha zeigen. Zusaetzlich python test_merge_resolve.py + test_extract.py gruen.
|
||||
|
||||
Getrennt zu ticketn (NICHT Teil dieses Fixes): "Stammbaum von Valentino Firehearts Kids.xlsx" fuehrt K79 "Ophelie von den Kleinen Chaoten" mit *20.04.2021 (Kathlins DOB) und macht Baxter x Kathlin zu ihren Eltern -> dadurch existiert in Prod eine zweite Ophelie b981d41d (*20.04.2021) neben der echten b413df65 (*06.05.2023); vermutlich DOB-Verschreiber/Dublette im neuen Chart.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Kathlins Stammbaum ist korrigiert: Als Eltern stehen jetzt Yuki von den Kleinen Chaoten (*03.02.2020) als Vater und Ella (*10.06.2019) als Mutter — so wie es in Deinen Excel-Stammbäumen steht. Vorher war fälschlich Izumi als Mutter eingetragen (das ist Yukis Mutter, also Kathlins Großmutter) und es fehlte der Vater. Damit stimmen auch die weiteren Vorfahren: über Yuki kommen Chevrolet Camaro of Topolino und Izumi dazu, über Ella Louis und Roswitha.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/extract.py (_reconstruct_parents / apply_conflict_decisions); tools/import/merge_and_resolve.py (virtuelle Wuerfe aus Decision-parentRefs); Prod-Daten: Gerbil 5a8e3035-bf60-5af8-9275-7d40352a27b5, Litter ecff0b8a-ac39-52c1-ac85-2cd442a901c5
|
||||
|
||||
---
|
||||
|
||||
### Bijou (*26.06.2022) — falsche Eltern: Arya Stark statt Louis × Nani of Black Forest
|
||||
|
||||
Tickets: `65266679-9bc3-43da-8280-cf8e7bb5e576` · Layer **import-logic** · Cluster `extract-starless-dob-block` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Prod zeigt Bijou (4b60e1fe) im virtuellen Wurf 237ed36d mit motherId=b4f59085 (Arya Stark von den Kleinen Chaoten) und ohne Vater; Provenance: "Eltern über Position im Stammbaum erkannt (Stammbaum von Alberto Kids.xlsx)" + "⚠ Elternteil Nani of Black Forest verworfen — ein Wurf hat nur einen Vater und eine Mutter". Laut Chart (identisch in "Stammbaum von Alberto Kids.xlsx" und "Stammbaum von Pukas Kids.xlsx", Gen-2-Band Spalte K): Vater = Louis of Black Forest (*22.08.2019, †09.04.2024, Block K44–K46, "Aa CC dd EE Gg P- spsp", blaue Box = männlich), Mutter = Nani of Black Forest (*15.10.2021, †27.11.2025, K48–K50). Arya Stark ist genetisch unmöglich als Mutter (Arya "aa c[chm]c[chm] …" kann Bijou "AA CC …" nicht vererben) — sie ist real die Mutter von Jay, also Bijous Partner, d. h. der Nachbar-Ast im Chart. URSACHE im Importer: tools/import/extract.py, extract_stammbaum → die Blockerkennung verlangt ein Sternchen vor dem Geburtsdatum (`is_block_dob = re.match(r"^\*\s?\d", t)`, ebenso DOB-Regex Zeile 33). In beiden Charts fehlt der Stern bei Louis (Zelle K45 = "22.08.2019/+09.04.2024") → der gesamte Block wird übersprungen, Louis existiert im Chart-Extrakt nicht, `_reconstruct_parents` nimmt als "nächsten Block oberhalb" Arya Stark (K35) aus Jays Ast und Nani als zweite Mutter, die anschließend als überzählige Mutter verworfen wird. Repo-weiter Scan aller 55 Charts: genau 3 solche sternlosen DOB-Blöcke (Louis 2×, Jiminy of Black Forest in "Stammbaum von Stella Kids.xlsx"), also ein klar begrenzter, generischer Importer-Bug — kein Rückfall einer alten conflict-decisions-Entscheidung (zu Bijou/Louis existiert kein Eintrag).
|
||||
|
||||
**Fix:**
|
||||
|
||||
Code-Fix im Importer (kein Live-DB-Patch, kein conflict-decisions-Eintrag nötig, da Louis/Jiminy bereits als deduplizierte Tiere existieren):
|
||||
|
||||
1) tools/import/extract.py, in `extract_stammbaum` direkt nach `cells = xu.read_cells(z, sheets[0], ss)` (ca. Zeile 231, vor `fillsex = …`) einfügen:
|
||||
|
||||
```python
|
||||
# FIX (Ticket 65266679 Bijou): einige Charts vergessen den Stern vor dem Geburtsdatum
|
||||
# ("22.08.2019/+09.04.2024" statt "*22.08.2019/+09.04.2024"). Ohne Stern erkennt die
|
||||
# Blockheuristik das Tier gar nicht (Louis of Black Forest) und _reconstruct_parents
|
||||
# greift in den Nachbar-Ast (Bijou bekam Arya Stark). Nur normalisieren, wenn die Zelle
|
||||
# wirklich in einem Block sitzt: Genotyp <=3 Zeilen darunter UND Namenszelle <=3 darüber.
|
||||
_bare_dob = re.compile(r"^\s*\d{1,2}\.\s?\d{1,2}\.\d{4}(\s*/\s*\+.*)?$")
|
||||
for (c, r), t in list(cells.items()):
|
||||
if not _bare_dob.match(t):
|
||||
continue
|
||||
has_geno = any(gt.looks_like_genotype(cells.get((c, rr), "")) for rr in range(r + 1, r + 4))
|
||||
has_name = any(cells.get((c, rr)) and not re.match(r"^\*?\s?\d", cells[(c, rr)])
|
||||
and not gt.looks_like_genotype(cells[(c, rr)]) for rr in range(r - 3, r))
|
||||
if has_geno and has_name:
|
||||
cells[(c, r)] = "*" + t.strip()
|
||||
```
|
||||
|
||||
2) Regressionstest in tools/import/test_extract.py mit dem vorhandenen `_make_xlsx`-Helper, z. B.:
|
||||
```python
|
||||
tmp = os.path.join(tempfile.gettempdir(), "starless_dob.xlsx")
|
||||
_make_xlsx(tmp, {
|
||||
"K44": "Louis of Black Forest",
|
||||
"K45": "22.08.2019/+09.04.2024", # Stern fehlt (Quelldatei-Tippfehler)
|
||||
"K46": "Aa CC dd EE Gg P- spsp",
|
||||
"N44": "Ignoriertes Datum 01.01.2020", # ohne Genotyp/Name-Kontext -> kein Block
|
||||
})
|
||||
animals = e.extract_stammbaum(tmp)
|
||||
by_name = {a["name"]: a for a in animals}
|
||||
check("sternloses DOB wird als Block erkannt", "Louis of Black Forest" in by_name)
|
||||
check("DOB/Todesdatum trotzdem geparst",
|
||||
by_name["Louis of Black Forest"]["dob"] == "22.08.2019"
|
||||
and by_name["Louis of Black Forest"]["death"] == "09.04.2024")
|
||||
```
|
||||
|
||||
3) Verifikation: `python test_extract.py test_extract_docx.py test_genotype.py test_merge_resolve.py test_extract_contracts.py` grün, dann `python tools/import/extract.py` + `extract_contracts.py` + `merge_and_resolve.py`; in resolved_import.json prüfen: Bijou (*26.06.2022) → Vater "Louis of Black Forest" (louisblackforest-22082019 / Prod b7f0a7bd-6420-58ea-bf59-5ff96979b9d5), Mutter "Nani of Black Forest"; Velvet (*23.03.2022) → Vater Jiminy of Black Forest. Danach Upload-Ingest gegen Prod (`curl -X POST http://truenas:8090/api/import/ingest-resolved/upload -F "resolved=@tools/import/output/resolved_import.json"`).
|
||||
|
||||
Verifizierter Prototyp-Diff (Monkeypatch über alle 55 Charts, verglichen wurden Tier-Set und alle parentRefs): nur 3 Dateien ändern sich, rein additiv — neu erkannt Louis of Black Forest (Alberto Kids, Pukas Kids) und Jiminy of Black Forest (Stella Kids); geänderte Elternlinks nur Bijou (Arya Stark → Louis of Black Forest) und Velvet (Tomomi → Jiminy of Black Forest, deckt sich mit Wurfchronik S20-Wurf 23.03.2022 "Chelsea × Jiminy [Black Forest]"; Velvet hängt in Prod fälschlich am R20-Wurf 21.03.2022 Belica/Zac, Litter e92f2afe). Kein Tier verloren, keine weiteren parentRefs berührt. Der falsche virtuelle Wurf 237ed36d hat nur Bijou als Kind und wird beim Upsert-Ingest als stale entfernt.
|
||||
|
||||
Separat (nicht Teil dieses Fixes, eigenes Ticket sinnvoll): in denselben Charts sind tiefere Ahnen-Paare um eine Zeile versetzt (Akane-Muster) — Arya Stark erhält Vance × Milka statt Vance × Sansa, Nani erhält Udo × Milka statt Udo × Hedwig.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Bijous Eltern sind jetzt richtig: Vater Louis of Black Forest (*22.08.2019), Mutter Nani of Black Forest (*15.10.2021) — genau so, wie es im Stammbaum von Albertos Kids und im Stammbaum von Pukas Kids steht. Vorher stand dort Arya Stark als Mutter; das war der Nachbar-Ast im Stammbaum (Arya ist die Mutter von Jay, Bijous Partner) und konnte auch von der Farbvererbung her nicht passen.
|
||||
|
||||
Ursache: In den Stammbaum-Tabellen fehlte bei Louis das Sternchen vor dem Geburtsdatum ("22.08.2019" statt "*22.08.2019"). Das Programm erkannte das Kästchen deshalb gar nicht als Tier und hat als Vater/Mutter versehentlich die Tiere aus dem darüberliegenden Ast genommen. Das Einlesen versteht solche Datumsangaben jetzt auch ohne Sternchen — du musst in den Excel-Dateien nichts korrigieren.
|
||||
|
||||
Nebenbei mit korrigiert: Bei Velvet (*23.03.2022) ist nun ebenfalls der richtige Vater eingetragen, Jiminy of Black Forest — passend zum S-Wurf von Chelsea und Jiminy aus der Wurfchronik.
|
||||
|
||||
**Betroffen:** tools/import/extract.py; tools/import/test_extract.py; Stammbaum von Alberto Kids.xlsx; Stammbaum von Pukas Kids.xlsx; Stammbaum von Stella Kids.xlsx; Gerbil Bijou 4b60e1fe-8b57-505b-9683-5e1536670b99; Litter 237ed36d-1d65-5e4d-bf38-1ed32542b530; Gerbil Velvet f5ffda6d-458c-570d-9cf6-67ac0c5e37a5
|
||||
|
||||
---
|
||||
|
||||
### Spike (*03.04.2015) — Verpaarung mit Jacky bestätigt; Eltern seines Geburtswurfs (T5/TS-Wurf) fehlen
|
||||
|
||||
Tickets: `5851bb94-582e-4d83-846a-faa3e6df7049` · Layer **import-data** · Cluster `import-data-conflict-decisions-litterparents` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Zwei getrennte Befunde. (1) Die Verpaarung Spike x Jacky ist KORREKT und quellenbelegt: die handschriftliche Detail-Chronik "Wurfchronik Teil 1" S.52 (PNG geprüft) schreibt "Spike + Jacky *14.08.15 => D7-Wurf, WS:3" mit genau den drei Jungtieren Smartie/Cookie/Cloe; die Wurfchronik-xlsx (sheet1 Zeile 197) nennt dieselbe Verpaarung als "C7 | *14.08.2015 | Mutter Jacky | Vater 'Spike /Jack Jr.?'". Die frühere Rückfrage beruhte auf einem Denkfehler von mir: 4 Monate sind für einen Bock KEIN Hindernis (zuchtreif ab 9-12 Wochen, Zeugung ~20.07.2015 => Spike ~15 Wochen). Ein alternativer, älterer "Spike" existiert in keiner Quelle (1b6a76d1 *31.03.2017 = nach dem Wurf geboren, e185e7fa "Spike of colourful furry gerbils" *27.04.2015 = fremde Zucht und jünger, 0d381af5 "Spikey" *13.01.2012 = anderes Tier/anderer Wurf). Die Antwort "OK, dieser Spike existiert tatsächlich" bestätigt damit den Eintrag => nichts zu ändern am Wurf aa48fc4a. (2) Der eigentliche Grund für ihre Frage "Woher hast du dieses Böckchen?" ist ein echter Datenmangel: Spikes Geburtswurf 1e49ad0e ("TS-Wurf", 03.04.2015) hat in prod FatherId=MotherId=NULL, seine Akte zeigt also keine Herkunft. Ursache: die Seiten-Extraktion "Wurfchronik Teil 1_page_0044.md" liefert für ALLE 9 Würfe dieser Seite fatherId/motherId=null (Eltern stehen dort nur im Overview-Fließtext "TS-Wurf (Jack Jr + Lila)"). Die xlsx belegt sie eindeutig: Zeile 162 "T5 | *03.04.2015 | Mutter Lila [Kleine Chaoten] | Vater Jack Jr. [Kleine Chaoten] | 4/6". Beide Elterntiere existieren bereits mit plausibler Lebenszeit (Jack Jr. von den Kleinen Chaoten 595361b4 *19.06.2013; Lila dacb12a3 *13.02.2014, +15.05.2015). Konsistenzcheck: Wurfschwester Viven (*03.04.2015) passt zur bekannten Geschwisterverpaarung Spike x Viven (H9-Wurf 29.01.2016, xlsx Z.256). Kein Rückfall durch die 12 neuen Charts vom 2026-08-18 — der Wurf stammt komplett aus der Wurfchronik, keine frühere Entscheidung betroffen.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Kein Eingriff am D7-Wurf (aa48fc4a) — Spike bleibt Vater. Ein Datenfix, re-ingest-stabil über den bestehenden litterParents-Mechanismus (merge_and_resolve.py ~Z.4098, Match litterName+date, Auflösung via name+dob): neuer Eintrag im Array "litterParents" in C:\Users\gulum\dev\GerbilManager\tools\import\conflict-decisions.json:
|
||||
|
||||
{
|
||||
"litterName": "TS-Wurf",
|
||||
"date": "03.04.2015",
|
||||
"father": "Jack Jr. von den Kleinen Chaoten",
|
||||
"fatherDob": "19.06.2013",
|
||||
"mother": "Lila",
|
||||
"motherDob": "13.02.2014",
|
||||
"source": "Ticket 5851bb94: Wurfchronik-xlsx sheet1 Z.162 (T5-Wurf *03.04.2015, Lila x Jack Jr.) + Overview 'Wurfchronik Teil 1_page_0044.md' ('TS-Wurf (Jack Jr + Lila)'); die Seiten-Extraktion 0044 liefert für alle Würfe fatherId/motherId=null"
|
||||
}
|
||||
|
||||
Danach: python tools/import/merge_and_resolve.py, Stichprobe (Wurf 1e49ad0e hat FatherId 595361b4 / MotherId dacb12a3, Spike 73d087f8 unverändert an diesem Wurf), dann Upload-Ingest gegen prod und Ticket mit fixNoteDraft schließen.
|
||||
|
||||
NICHT in diesem Ticket fixen (eigenes Ticket wert): auf Seite 0044 sind die Wurfbuchstaben OCR-verlesen ("5" als "S": TS=T5, SS=S5, US=U5, VS=V5), und die Buchstaben der Seite 0052 sind gegenüber der xlsx um einen verschoben (xlsx C7 *14.08.2015 = App "D7-Wurf"). Rein kosmetisch, betrifft keine Abstammung, aber verwirrend für die Züchterin.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Der Eintrag ist richtig: In deiner Wurfchronik steht auf der Seite mit den August-2015-Würfen von Hand "Spike + Jacky *14.08.15" mit genau den drei Jungtieren Smartie, Cookie und Cloe - und in deiner Wurfchronik-Tabelle dieselbe Verpaarung (dort mit dem Zusatz "Spike / Jack Jr.?"). Mein Einwand, Spike sei mit gut 4 Monaten zu jung gewesen, war falsch: Böcke können schon ab etwa 2-3 Monaten Vater werden. Die Verpaarung Spike x Jacky bleibt also so stehen. Zusätzlich habe ich ergänzt, woher Spike selbst stammt: aus deinem Wurf vom 03.04.2015 von Lila und Jack Jr. von den Kleinen Chaoten - das stand bisher nirgends bei ihm, weil bei diesem Wurf die Elterntiere fehlten. Damit siehst du auf Spikes Seite jetzt auch seine Eltern und seine Wurfschwester Viven, mit der er später den Wurf vom 29.01.2016 hatte. Falls du sicher weißt, dass doch Jack Jr. der Vater von Smartie, Cookie und Cloe war, sag Bescheid - dann tausche ich den Vater dort.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json (litterParents); Wurf 1e49ad0e TS-Wurf 03.04.2015; Gerbil 73d087f8 Spike; Wurf aa48fc4a D7-Wurf (unverändert)
|
||||
|
||||
---
|
||||
|
||||
### Fast Boy of Golden Lights (*31.03.2017) — beide Eltern im Stammbaum falsch (Großvater als Vater, Schwiegermutter als Mutter)
|
||||
|
||||
Tickets: `6c89082e-4060-4ec8-8ebc-c287bdd6f31d` · Layer **import-data** · Cluster `conflict-decisions-resolutions-parents` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Beide Elternteile sind falsch importiert. Prod zeigt über den virtuellen Wurf 4d3afe0a ("Wurf von Danjo of Golden Lights + Xtra Bounty von den Schlossmäusen", 31.03.2017, einziges Kind = Fast Boy) Vater = Danjo of Golden Lights (cbac1d65, ohne DOB/Geschlecht) und Mutter = Xtra Bounty von den Schlossmäusen (354efa96). Belegt richtig sind Vater = Charly of Golden Lights (*05.04.2016, prod 80987264, male) und Mutter = Ziwa of Golden Lights (*29.04.2016, prod 26986f3a, female): (1) `python tools/import/rpro3_lookup.py tools/import/_rpro3.db "Fast Boy"` → "eigenes Tier, *2017-03-31 … Eltern: Vater Charly · Mutter Ziwa", Nachzucht Erestor *05.04.2018 mit Partner Addison; (2) Paar-Zelle R35 in C:/Users/gulum/dev/Sttammbäume/"Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx" = "Charly of Golden Lights & Ziwa of Golden Lights", auf derselben Zeile wie Fast Boy (Q35); (3) "Stammbaum von Kid von Cassy & Blaster.xlsx": N75 Fast Boy, Q74 Charly *05.04.2016, Q75 Ziwa *29.04.2016. Danjo ist laut rpro3 der Vater von Ziwa, also Fast Boys GROSSVATER (Ziwa *29.04.2016 → Vater Danjo, Mutter Isiri); Xtra Bounty (*20.03.2014, eigenes Tier, Schlossmäuse, keine Nachzucht in rpro3) ist die Mutterlinie von Zuleika, Fast Boys Partnerin, die im Chart einen Block darunter steht (N62/N76 Zuleika mit Q62/Q76 Xtra Bounty + Q63/Q77 Ruby). URSACHE: die Chart-Positions-Heuristik. tools/import/output/animals.json führt für Fast Boy drei widersprüchliche parentRefs (Danjo/father, Xtra Bounty/mother, Ziwa/father) — der echte Vater Charly wurde NIE als parentRef erfasst, weil er im Chart eine Zeile ÜBER dem Kind steht (Q60/Q74), während die Großeltern-Paarzelle R61 "Isiri of Golden Lights & Danjo of Golden Lights" (gehört zu Ziwas Zeile 61) auf Fast Boys Zeile fällt. Anschließend verwarf `pick_parent_ref` Ziwa für die Vaterrolle ("falsches Geschlecht für die Vaterrolle" — steht wörtlich in der Provenance des Tiers) und fiel auf Danjo zurück. Die Zeilen-Offsets sind im xlsx nicht durch mergeCells gestützt (geprüft: im Bereich N/Q Zeilen 56–68 von "Stammbaum von Akio Kids.xlsx" existiert KEIN einziger Merge) und variieren von Block zu Block — ein Code-Fix der Heuristik ist deshalb nicht sicher möglich. Kein Rückfall durch die 12 neuen Charts vom 2026-08-18: es gab für dieses Tier bisher überhaupt keine Entscheidung (kein "Fast Boy"/"Ziwa"/"Charly of Golden" in conflict-decisions.json, kein Eintrag in rpro3-decisions.json).
|
||||
|
||||
**Fix:**
|
||||
|
||||
Owner-Regel: Datenfix im Importer, kein Live-Patch. In tools/import/conflict-decisions.json → Array "resolutions" die folgenden ZWEI Einträge anhängen (Muster identisch zu den 41 bestehenden father/mother-Einträgen; `extract.apply_conflict_decisions` ersetzt damit die kompletten parentRefs mit method "decision"/confidence "high"). fatherDob/motherDob sind zwingend mitzugeben, weil animals.json je zwei Charly-/Ziwa-Datensätze kennt (datenloser Paarzellen-Platzhalter ohne dob + datiertes Tier):
|
||||
|
||||
{
|
||||
"name": "Fast Boy of Golden Lights",
|
||||
"dob": "31.03.2017",
|
||||
"decision": "parents are Charly of Golden Lights x Ziwa of Golden Lights, NOT Danjo (= Ziwa's father, i.e. the grandfather) + Xtra Bounty (= Zuleika's dam line) - chart-position misread",
|
||||
"father": "Charly of Golden Lights",
|
||||
"fatherDob": "05.04.2016",
|
||||
"mother": "Ziwa of Golden Lights",
|
||||
"motherDob": "29.04.2016",
|
||||
"source": "Ticket 6c89082e - RennmausPro III (_rpro3.db: Vater Charly / Mutter Ziwa) + Paarzelle R35 in 'Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx' + Q74/Q75 in 'Stammbaum von Kid von Cassy & Blaster.xlsx'"
|
||||
},
|
||||
{
|
||||
"name": "Ziwa of Golden Lights",
|
||||
"dob": "29.04.2016",
|
||||
"decision": "father = Danjo of Golden Lights, mother = Isiri of Golden Lights (rpro3; die Paarzelle R61 'Isiri of Golden Lights & Danjo of Golden Lights' in 'Stammbaum von Akio Kids.xlsx' ist rollenvertauscht)",
|
||||
"father": "Danjo of Golden Lights",
|
||||
"mother": "Isiri of Golden Lights",
|
||||
"source": "Ticket 6c89082e - RennmausPro III (_rpro3.db)"
|
||||
}
|
||||
|
||||
Der zweite Eintrag ist optional-aber-empfohlen: er hängt Danjo (+ Isiri) dort ein, wo sie hingehören (als Ziwas Eltern = Fast Boys Großeltern), sodass der Stammbaum eine Generation tiefer korrekt wird statt bei Ziwa abzubrechen. Beide Elternteile haben in prod unknown gender/kein DOB, die explizite Rollenangabe ist also nötig, weil die Paar-Reihenfolge im Chart hier vertauscht ist.
|
||||
|
||||
Danach: `python tools/import/extract.py` → `python tools/import/merge_and_resolve.py`, `python test_merge_resolve.py` grün, und in tools/import/output/resolved_import.json prüfen, dass Fast Boy (externalRef `stammbaum-fastboygoldenlights-31032017`) an einem Wurf mit FatherId = Charly of Golden Lights *05.04.2016 und MotherId = Ziwa *29.04.2016 hängt; dann Upload-Ingest gegen Prod (`curl -X POST http://truenas:8090/api/import/ingest-resolved/upload -F "resolved=@tools/import/output/resolved_import.json"`). Erwarteter Effekt: der virtuelle Wurf 4d3afe0a (Danjo × Xtra Bounty) verschwindet, weil er dadurch kinderlos und virtuell ist; ein neuer virtueller Wurf Charly × Ziwa vom 31.03.2017 mit Kind Fast Boy entsteht. Altersplausibilität ok (beide Eltern ~11 Monate bei Geburt, `parent_age_plausible` greift nicht), Geschlechter passen (Charly male → Vaterrolle, Ziwa female → Mutterrolle), Xtra Bounty bleibt unverändert als Elternteil von Zuleika erhalten.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Bei Fast Boy of Golden Lights waren im Stammbaum beide Eltern falsch: als Vater stand Danjo of Golden Lights, der aber in Wirklichkeit sein Großvater ist (Danjo ist der Vater von Ziwa), und als Mutter stand Xtra Bounty von den Schlossmäusen, die zur Familie seiner Partnerin Zuleika gehört. Ursache war ein Verrutschen um eine Zeile beim Auslesen der Stammbaum-Tabellen — dort steht der Vater je nach Block manchmal eine Zeile höher, und dadurch wurde die Elternangabe der Großeltern-Zeile auf Fast Boy bezogen. Korrigiert nach deinen Angaben aus RennmausPro und den Stammbaum-Dateien: Vater ist jetzt Charly of Golden Lights (geb. 05.04.2016), Mutter ist Ziwa of Golden Lights (geb. 29.04.2016). Zusätzlich sind Danjo und Isiri of Golden Lights jetzt als Ziwas Eltern eingetragen, also als Fast Boys Großeltern — der Stammbaum reicht damit eine Generation weiter und stimmt wieder.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json (resolutions); tools/import/extract.py (_reconstruct_parents / Chart-Positions-Heuristik, nur als Ursache — kein Code-Fix); tools/import/merge_and_resolve.py (pick_parent_ref, virtuelle Wuerfe); Stammbaum-Ansicht / Tierakte Fast Boy of Golden Lights; virtueller Wurf 4d3afe0a-1d78-5c55-bd99-4f216bd84afc
|
||||
|
||||
---
|
||||
|
||||
### JackJack (b6d8b3ef) — Abgabedatum-Zeile unter Abnehmer in der Tier-Akte (+ kaputtes Abgabedatum 1310-05-13)
|
||||
|
||||
Tickets: `24522f5f-8c31-46c7-8dd0-d9a88870febb` · Layer **frontend** · Cluster `gerbil-detail-abgabedatum-goHomeDate` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Zwei Ursachen. (1) FRONTEND: gerbil-manager-web/src/pages/GerbilDetailPage.tsx Z.493-495 rendert die Abgabedatum-Zeile nur bei `g.status === 'GivenAway'`, waehrend die Abnehmer-Zeile (Z.552-554) schon bei `g.receiverContactId || g.status !== 'Deceased'` erscheint. JackJack (prod b6d8b3ef, status=Deceased, receiverContactId=3192070e = "Larissa Draeger + Nicolas", goHomeDate gesetzt) zeigt daher den Abnehmer, aber kein Abgabedatum — genau die fehlende Zeile. (2) DATEN (blockiert 1): goHomeDate von JackJack ist "1310-05-13". Quelle ist die Transkription C:\Users\gulum\dev\Wurfchronik_Bilder\Wurfchronik Teil 1_page_0013.md, Eintrag g1000000-...-0002: `"givenAwayDate": "1310-05-13"`, obwohl die Notiz derselben Zeile "→ Larissa Draeger + Nicolas 13.5.10" = 13.05.2010 sagt (Wurfbruder Bodo auf derselben Seite: korrekt "2010-06-06"). parse_date() (tools/import/merge_and_resolve.py Z.595ff) mappt nur 1900<year<2000 (+100) und laesst 4-stellige Jahre <1900 unveraendert durch. Scan von tools/import/output/resolved_import.json: dies ist der EINZIGE Datensatz mit Jahr<1990 in DateOfBirth/DateOfDeath/GoHomeDate. Geprueft: /verified-gerbils zeigt fuer b6d8b3ef einen GerbilOverride (isVerified=false) mit protectedFields [name, spottingType, genotype, isDeaf] — goHomeDate ist NICHT eingefroren, der Importfix landet also nach Re-Ingest. Nebenbefund (gehoert zu Ticket 0cf6b838, hier NICHT fixen): lokal heisst das Tier in resolved_import.json noch "JackSack"; die renameTo-Resolution feuert im aktuellen Lauf nicht mehr, weil der Merge-Partner "Wurfchronik Teil 1_page_0001.md-3d8c071c-..." nach Neu-Transkription nicht mehr existiert — in Prod haelt nur ihr UI-Override den Namen "JackJack".
|
||||
|
||||
**Fix:**
|
||||
|
||||
A) FRONTEND — gerbil-manager-web/src/pages/GerbilDetailPage.tsx
|
||||
|
||||
A1) Z.493-495 loeschen:
|
||||
{g.status === 'GivenAway' && (
|
||||
<Kv label={t.fields.goHomeDate}>{formatDate(g.goHomeDate)}</Kv>
|
||||
)}
|
||||
|
||||
A2) Z.552-554 ersetzen durch (Abgabedatum direkt UNTER Abnehmer):
|
||||
{/* Ticket 24522f5f (JackJack): Abgabedatum gehoert direkt unter den Abnehmer und
|
||||
darf nicht am Status haengen — Tiere, die abgegeben wurden und spaeter beim
|
||||
Abnehmer starben (status=Deceased), haben Abnehmer UND Abgabedatum. */}
|
||||
{(g.receiverContactId || g.goHomeDate || g.status !== 'Deceased') && (
|
||||
<>
|
||||
<Kv label={t.fields.receiver}>{lookup(contactName, g.receiverContactId)}</Kv>
|
||||
<Kv label={t.fields.goHomeDate}>{formatDate(g.goHomeDate)}</Kv>
|
||||
</>
|
||||
)}
|
||||
Keine neuen Strings noetig (de.ts Z.94 `goHomeDate: 'Abgabedatum'`); formatDate(null) -> '—'.
|
||||
|
||||
A3) Optionaler Regressionsschutz: in gerbil-manager-web/e2e/mock-data.ts bei 'willi' (Z.180, Deceased) `receiverContactId: 'con-huber', goHomeDate: '2022-01-15'` ergaenzen und in e2e/tiere.spec.ts assert, dass die Akte de.pages.gerbils.fields.goHomeDate zeigt.
|
||||
|
||||
B) DATEN/IMPORT (zwingend zusammen mit A, sonst zeigt die neue Zeile "13.05.1310")
|
||||
|
||||
B1) tools/import/merge_and_resolve.py — neuen Resolution-Key `goHomeDate` zulassen. Im has_ovr-Tuple (Z.3211-3214) ergaenzen:
|
||||
or d.get("goHomeDate")
|
||||
und im Apply-Block direkt NACH dem dateOfDeath-Zweig (nach Z.3294) einfuegen:
|
||||
# goHomeDate: setzt/korrigiert das Abgabedatum autoritativ (Ticket 24522f5f).
|
||||
# Nötig, weil Wurfchronik-Transkriptionen das Datum falsch zusammenbauen
|
||||
# können (JackJack: "1310-05-13" statt 13.05.2010).
|
||||
if d.get("goHomeDate"):
|
||||
_gh = parse_date(d["goHomeDate"])
|
||||
if _gh and g.get("GoHomeDate") != _gh:
|
||||
g["GoHomeDate"] = _gh
|
||||
if g.get("Status") not in ("Deceased",) and not g.get("ReceiverContactId"):
|
||||
pass # Status bewusst unangetastet
|
||||
applied = True
|
||||
|
||||
B2) tools/import/conflict-decisions.json — bestehenden JackSack-Block (Z.1087-1099) um eine Zeile erweitern (Match ueber name+dob greift, das Tier heisst im resolved noch "JackSack"):
|
||||
{
|
||||
"name": "JackSack",
|
||||
"dob": "18.02.2010",
|
||||
"mergeExternalRefs": [
|
||||
[
|
||||
"Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000002",
|
||||
"Wurfchronik Teil 1_page_0001.md-3d8c071c-f172-44f6-aff5-ca392c284bf3"
|
||||
]
|
||||
],
|
||||
"renameTo": "JackJack",
|
||||
"goHomeDate": "13.05.2010",
|
||||
"decision": "Dublette JackSack/JackJack = dasselbe Tier (A-Wurf, *18.02.2010). Zusammenfuehren, richtiger Name JackJack. Abgabedatum aus der Wurfchronik-Notiz '-> Larissa Draeger + Nicolas 13.5.10' = 13.05.2010 (Quelle transkribiert faelschlich 1310-05-13).",
|
||||
"source": "Zuechterin - Tickets 0cf6b838, 24522f5f"
|
||||
},
|
||||
|
||||
B3) tools/import/merge_and_resolve.py parse_date() — Guard gegen implausible Jahre, damit kuenftige Fehl-Transkriptionen kein 14.-Jahrhundert-Datum importieren. Im ISO-Zweig (Z.601-610) vor `return d`:
|
||||
if year < 1900 or year > datetime.now().year + 1:
|
||||
return None
|
||||
(analog im DD.MM.YYYY-Zweig nach der Jahres-Normalisierung) + Regressionstest in tools/import/test_merge_resolve.py: parse_date("1310-05-13") is None, parse_date("13.5.10") == "2010-05-13".
|
||||
|
||||
C) VERIFIKATION: npx tsc --noEmit, npx vitest run, npx eslint auf GerbilDetailPage.tsx, betroffene playwright-Specs; python test_merge_resolve.py; merge_and_resolve.py neu laufen lassen und pruefen, dass b6d8b3ef GoHomeDate == "2010-05-13" hat; dann Upload-Ingest gegen Prod (POST /api/import/ingest-resolved/upload) und Akte pruefen: "Abnehmer: Larissa Draeger + Nicolas" + "Abgabedatum: 13.05.2010".
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** In der Tier-Akte steht das Abgabedatum jetzt direkt unter dem Abnehmer — und zwar immer, nicht nur bei Tieren mit Status „abgegeben". Bei JackJack war ausserdem das Abgabedatum aus der Wurfchronik falsch uebertragen worden (es stand ein Datum aus dem Jahr 1310); richtig ist der 13.05.2010, wie es in deiner Notiz „→ Larissa Draeger + Nicolas 13.5.10" steht. Das ist korrigiert, und der Importer verwirft solche unmoeglichen Jahreszahlen künftig, statt sie zu uebernehmen.
|
||||
|
||||
**Betroffen:** gerbil-manager-web/src/pages/GerbilDetailPage.tsx; gerbil-manager-web/e2e/mock-data.ts; tools/import/merge_and_resolve.py; tools/import/conflict-decisions.json; tools/import/test_merge_resolve.py
|
||||
|
||||
---
|
||||
|
||||
### Eliza *20.08.2010 (E-Wurf Blacky x Kuke) — Gencode + Scheckungsart Ansatzschecke
|
||||
|
||||
Tickets: `a547be62-3611-43b8-b72b-446978d545bf` · Layer **import-logic** · Cluster `decisions-genotype-spottingtype-override` · Aufwand M · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Eliza (29cbd5e6, Quelle: Wurfchronik-md „Wurfchronik Teil 1_page_0013.md", Roh-Id g1000000-…-011) hat in der Quelle NUR colorDescription „Schwarz-Sp", keinen Gencode. Der angezeigte „aa CC DD EE GG PP Spsp rere" ist reiner Kanon-Fill: resolve_color_and_genotype() nimmt den Katalog-Genotyp der Variety „Schwarz" (ColorVariety 007) und hängt Spsp an, weil das Label scheckig klingt. Die Antwort der Züchterin („aa C- DD E- G- P- Spsp", Ansatzschecke) ist damit echte Neuinformation; genotype.parse() nimmt den String sauber an (unmappedTokens leer) und liefert Farbschlag „Schwarz" → ColorVarietyId 007 bleibt korrekt. Ein Nachtragen scheitert aktuell an ZWEI Lücken: (1) tools/import/merge_and_resolve.py ~Z.3302 wendet einen decision-`genotype` nur an, wenn Genotype LEER ist (`if d.get("genotype") and not g.get("Genotype")`) — genau dieser Guard hat bereits die vorhandene Entscheidung für CHRIS (externalRef „Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000007", soll „aa Cc[-] DD Ee gg P- Spsp", ist „aa CC DD EE gg PP Spsp rere") stillschweigend verschluckt; nur Tiere ohne Quell-Genotyp (Jana, Kyara) bekamen ihren Override. (2) `spottingType` existiert im Importer-Pfad gar nicht, und GerbilManagerWebAPI/Import/IngestResolvedService.cs (Upsert-Block Z.126–152) kopiert Gerbil.SpottingType NICHT — ein Payload-Wert würde ignoriert. Eliza hat zwar einen GerbilOverride, der friert aber nur „causeOfDeath" ein, blockiert also nichts.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1) DATEN — tools/import/conflict-decisions.json: die BESTEHENDE Eliza-Resolution (Key externalRef …-011, Ticket aa5238b6) IN PLACE erweitern, KEINEN zweiten Eintrag mit derselben externalRef anlegen (`_ovr_by_extref` ist ein dict, der letzte gewinnt → isResident/notes gingen verloren):
|
||||
{"externalRef": "Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000011", "isResident": false, "genotype": "aa C- DD E- G- P- Spsp", "spottingType": "Ansatzschecke", "notes": "Schwarz-Schecke; Tochter von Blacky x Kuke (E-Wurf 2010); verstorben 06.04.2011 (Ausbruch-Unfall). War nie Zuchttier. NICHT die Mutter des J5-Wurfs — das ist die spätere Eliza (*13.02.2014).", "decision": "Zweite/2010er Eliza von der J5-Mutter getrennt; irreführende 'Mutter des J5-Wurfs'-Notiz entfernt. Gencode + Scheckungsart von der Züchterin: aa C- DD E- G- P- Spsp, Ansatzschecke (bisheriger Wert war nur Kanon-Fill aus dem Farbschlag 'Schwarz-Sp').", "source": "Ticket aa5238b6 + Ticket a547be62"}
|
||||
|
||||
2) IMPORTER — tools/import/merge_and_resolve.py, Override-Block:
|
||||
(a) has_ovr-Guard (~Z.3211-3216): `or d.get("originBreeder"))` → `or d.get("originBreeder") or d.get("spottingType"))`.
|
||||
(b) Match-Schleife (~Z.3228-3238): Präzision des Matches merken. Vor `if er and er in _ovr_by_extref:` ein `_precise = False` setzen; in den beiden externalRef-Zweigen `_precise = True`; im Zweig `elif (ck, iso) in _ovr_by_namedob:` zusätzlich `_precise = bool(iso)`; der name-only-Zweig `(ck, "")` bleibt unpräzise.
|
||||
(c) Z.3302-3304 ersetzen:
|
||||
# Ein expliziter Gencode aus einer Züchterin-Entscheidung ist AUTORITATIV und
|
||||
# überschreibt auch einen vorhandenen Wert — bei Wurfchronik-Tieren ist der meist
|
||||
# nur der Kanon-Fill des Farbschlags (resolve_color_and_genotype), keine echte
|
||||
# Gencode-Quelle. Nur bei PRÄZISEM Match (externalRef oder name+dob), damit eine
|
||||
# name-only-Entscheidung nicht den Gencode eines gleichnamigen fremden Tieres
|
||||
# überschreibt. (Ticket a547be62 Eliza; repariert auch Chris/page_0013.)
|
||||
if d.get("genotype"):
|
||||
_geno_ovr = d["genotype"].strip()
|
||||
if (_precise or not g.get("Genotype")) and g.get("Genotype") != _geno_ovr:
|
||||
g["Genotype"] = _geno_ovr
|
||||
applied = True
|
||||
# Scheckungsart (freier Text, Werte wie in de.ts `spottingTypes`, z. B. „Ansatzschecke").
|
||||
# Der Importer leitet sie aus keiner Quelle ab — nur per Entscheidung setzbar.
|
||||
if d.get("spottingType"):
|
||||
_sp_ovr = d["spottingType"].strip()
|
||||
if g.get("SpottingType") != _sp_ovr:
|
||||
g["SpottingType"] = _sp_ovr
|
||||
applied = True
|
||||
|
||||
3) BACKEND — GerbilManagerWebAPI/Import/IngestResolvedService.cs (Upsert der Gerbils):
|
||||
im Update-Zweig hinter `eg.GoHomeDate = g.GoHomeDate; eg.Genotype = g.Genotype; eg.Notes = g.Notes;` einfügen:
|
||||
// SpottingType nur übernehmen, wenn der Payload einen Wert liefert (der Importer
|
||||
// kennt die Scheckungsart nur über conflict-decisions) — sonst würde eine von Hand
|
||||
// erfasste Scheckungsart bei jedem Ingest genullt.
|
||||
if (g.SpottingType is not null) eg.SpottingType = g.SpottingType;
|
||||
und im `new Gerbil { … }`-Initializer `SpottingType = g.SpottingType,` ergänzen. (Keine Migration nötig, Spalte existiert; `ResolvedImportData.Gerbils` ist List<Gerbil>, „SpottingType" deserialisiert automatisch.)
|
||||
|
||||
4) TESTS: tools/import/test_merge_resolve.py — Regression: Resolution mit genotype+spottingType per externalRef überschreibt einen NICHT-leeren (kanon-gefüllten) Genotyp und setzt SpottingType; name-only-Resolution überschreibt einen vorhandenen Genotyp NICHT. GerbilManager.Tests/IngestResolvedServiceTests.cs — SpottingType-Roundtrip (Payload-Wert wird geschrieben; null im Payload lässt einen bestehenden Wert stehen).
|
||||
|
||||
5) AUSFÜHREN/PRÜFEN: nur `python tools/import/merge_and_resolve.py` nötig (animals.json/extract unverändert, weil die Entscheidung ein Wurfchronik-md-Tier trifft), dann Upload-Ingest gegen Prod. Erwartet: Eliza → Genotype „aa C- DD E- G- P- Spsp", SpottingType „Ansatzschecke", ColorVarietyId weiter 00000000-0000-0000-0000-000000000007; Chris → „aa Cc[-] DD Ee gg P- Spsp". Genotype-Diff des gesamten Payloads gegen den alten Stand sichten — es dürfen nur Tiere mit eigener Entscheidung wechseln.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Elizas Gencode (*20.08.2010, Tochter von Blacky und Kuke) steht jetzt so in der Akte, wie du ihn angegeben hast: aa C- DD E- G- P- Spsp, Scheckungsart „Ansatzschecke". Vorher stand dort ein automatisch aus dem Farbschlag „Schwarz-Schecke" errechneter Wert, weil die Wurfchronik zu ihr keinen Gencode nennt. Dabei ist außerdem aufgefallen, dass deine Gencode-Angaben bisher nur dann übernommen wurden, wenn noch gar kein Wert vorhanden war — deshalb war z. B. auch bei Chris (gleicher Wurf) noch der automatische Wert zu sehen. Das ist behoben: dein Wert hat jetzt immer Vorrang, und die Scheckungsart lässt sich ebenfalls dauerhaft mitgeben. Beides bleibt bei künftigen Daten-Aktualisierungen erhalten.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/merge_and_resolve.py (Override-Block ~Z.3211/3228/3302); GerbilManagerWebAPI/Import/IngestResolvedService.cs (Gerbil-Upsert Z.126-152); tools/import/test_merge_resolve.py; GerbilManager.Tests/IngestResolvedServiceTests.cs
|
||||
|
||||
---
|
||||
|
||||
### Jamie (*27.12.2010, J-Wurf Danny x Jana) — Dublette: zwei weitere Wurfchronik-Elternstubs nie gemergt
|
||||
|
||||
Tickets: `66ef9bdd-1a92-46cc-a268-8c1ce1bc3aaa, 3bbd6ab4-d9e4-4f02-a4de-952ff0dd3738` · Layer **import-data** · Cluster `conflict-decisions-mergeexternalrefs-wurfchronik-elternstubs` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Kein Rueckfall durch die 12 neuen Charts vom 18.08. (Wurfchronik-md-Dateien in C:\Users\gulum\dev\Wurfchronik_Bilder sind seit Juni unveraendert; Ticket 3bbd6ab4 stammt vom 13.08., also VOR dem Chart-Import). Ursache ist eine unvollstaendige alte Entscheidung: Jamie hat pro Wurfchronik-Seite, auf der er als Elternteil steht, einen eigenen dobless "empty shell"-Record. Der Kanon ist 8ddf3717 "Jamie von den kleinen Chaoten" *27.12.2010 (ExternalRef stammbaum-jamiekleinenchaoten-27122010). Die Resolution aus 06b6734d/e96db323/c092f8d2 enthaelt nur EIN mergeExternalRefs-Paar (Drop "Wurfchronik Teil 1_page_0022.md-page_0022_parent_jamie") — das greift korrekt (page_0022-Stub ist verschwunden). Die Stubs von page_0017 (id f651e1cf-54bc-4d8b-9be0-4b6298e7ab48, "Parent listed on page 0017") und page_0023 (id 7cbb62c5-7a46-4cb4-a3ad-bf7f94080e14, "Father of H2-Wurf") blieben uebrig und wurden vom Namens-Dedup untereinander zur gemeldeten Dublette 4e4e507f verschmolzen (Provenance: mergedRecordCount 2, sourceFiles Wurfchronik + page_0017); gegen den Full-Name-Kanon mergen sie nicht (Call-Name "Jamie" ohne DOB). Auswirkung: 9 Wuerfe haengen am falschen Jamie (U 13.12.2011, X 10.01.2012, E1 26.03.2012, M1 08.07.2012, H2 13.04.2013, L2 20.05.2013, N2 14.06.2013, V2 27.07.2013, W2 29.07.2013), der Kanon hat nur A2, D2 und den Chart-Wurf. Derselbe Fehler bei der H2-Mutter: Stub 336ad972 "Sakura" (page_0023 ...080e15) neben Kanon 4be212ef "Gwen gen. Sakura of little Angels" *17.12.2011 — die Sakura-Resolution (Index 147, Ticket 7f9b3724) nennt im Decision-Text ausdruecklich A2/D2/H2, hat aber nur page_0022_parent_sakura als Drop-Ref. Ohne diesen zweiten Merge bleiben nach dem Jamie-Merge zwei Wuerfe vom 13.04.2013 am Kanon stehen, weil deduped2 gleiches Datum UND beide Eltern identisch verlangt. mergeExternalRefs laeuft vor dem Gerbil-Dedup (merge_and_resolve.py L2743 auf all_processed_gerbils ab L2209) und remappt Litter-Father/MotherId (L3443-3448) — beide Stub-Records existieren zu diesem Zeitpunkt noch getrennt, daher sind beide Drop-Paare noetig.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Nur tools/import/conflict-decisions.json (Owner-Regel, re-ingest-stabil, deterministische Refs).
|
||||
|
||||
(1) In der bestehenden Resolution {"name":"Jamie von den kleinen Chaoten","dob":"27.12.2010", ...} das Feld mergeExternalRefs komplett ersetzen durch:
|
||||
"mergeExternalRefs": [
|
||||
["stammbaum-jamiekleinenchaoten-27122010", "Wurfchronik Teil 1_page_0022.md-page_0022_parent_jamie"],
|
||||
["stammbaum-jamiekleinenchaoten-27122010", "Wurfchronik Teil 1_page_0017.md-f651e1cf-54bc-4d8b-9be0-4b6298e7ab48"],
|
||||
["stammbaum-jamiekleinenchaoten-27122010", "Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e14"]
|
||||
]
|
||||
und im "source"-Feld ergaenzen: " + Tickets 66ef9bdd/3bbd6ab4 (restliche Wurfchronik-Elternstubs page_0017/page_0023)".
|
||||
|
||||
(2) In der Sakura-Resolution (resolutions-Index 147, {"externalRef":"stammbaum-gwengensakuralittleangels-17122011", ...}) mergeExternalRefs ersetzen durch:
|
||||
"mergeExternalRefs": [
|
||||
["gwengensakuralittleangels-17122011", "page_0022_parent_sakura"],
|
||||
["gwengensakuralittleangels-17122011", "Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e15"]
|
||||
]
|
||||
(Pflicht, sonst zeigt Jamie zwei Wuerfe vom 13.04.2013.)
|
||||
|
||||
(3) Optional (mittlere Sicherheit, Haupt-Loop entscheidet — sonst separates Ticket): neue Resolution anhaengen, damit im gemergten Wurf nicht Akina und Alkina doppelt stehen:
|
||||
{
|
||||
"externalRef": "stammbaum-akinakleinenchaoten-13042013",
|
||||
"mergeExternalRefs": [["stammbaum-akinakleinenchaoten-13042013", "Wurfchronik Teil 1_page_0023.md-a2867c29-3733-4f9e-a4b5-bf6e44b9d031"]],
|
||||
"decision": "'Alkina' (Wurfchronik H2-Wurf 13.04.2013, behalten, +01.12.2017) und 'Akina von den Kleinen Chaoten' (*13.04.2013, Mutter R3/G5) sind dasselbe Tier: H2 hatte laut Wurfchronik genau 5 Junge (Enzo, Schmidti, Lilly, Anthrazit, Alkina), Akina kann kein 6. Jungtier sein; Buchstabendreher blockiert den Auto-Merge.",
|
||||
"source": "Tickets 66ef9bdd/3bbd6ab4"
|
||||
}
|
||||
|
||||
Danach: python tools/import/merge_and_resolve.py, python test_merge_resolve.py, Stichprobe (Kanon 8ddf3717 muss U/X/E1/M1/H2+Chart-Wurf/L2/N2/V2/W2 + A2/D2 tragen; 4e4e507f und 336ad972 duerfen in resolved_import.json nicht mehr vorkommen; genau ein Wurf am 13.04.2013), dann Upload-Ingest gegen Prod (POST /api/import/ingest-resolved/upload). Keine Live-DB-Patches.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Die beiden Jamie-Einträge sind jetzt endgültig zusammengeführt: Es bleibt nur der Jamie aus dem J-Wurf von Danny und Jana (geboren 27.12.2010). Beim letzten Mal war nur eine von drei Fundstellen aus der Wurfchronik zusammengeführt worden, deshalb tauchte er wieder doppelt auf. Jetzt landen alle seine Würfe bei ihm: U, X, E1, M1, H2, L2, N2, V2 und W2 zusätzlich zu A2 und D2. Gleichzeitig war seine Partnerin Sakura doppelt vorhanden — auch sie ist jetzt ein Tier (Gwen gen. Sakura of little Angels, geboren 17.12.2011), wodurch der Wurf vom 13.04.2013 nur noch einmal in der Liste steht und alle Jungtiere beisammen sind.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/merge_and_resolve.py (mergeExternalRefs L2743-2786, Litter-Remap L3443-3448, deduped2 L3732); Gerbil 8ddf3717 Jamie von den kleinen Chaoten; Gerbil 4e4e507f Jamie (Dublette); Gerbil 336ad972 Sakura (Dublette); Litter 8f043703 H2-Wurf + Litter 27ab49ae (13.04.2013)
|
||||
|
||||
---
|
||||
|
||||
### Sakura / Gwen gen. Sakura of little Angels — Dublette nach Merge-Fix zurueck (Folge-Ticket zu 7f9b3724)
|
||||
|
||||
Tickets: `6c9539d2-91e2-4323-a76a-c8a1962dbca9` · Layer **import-data** · Cluster `wurfchronik-md-stub-dubletten-mergeexternalrefs` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Kein Effekt der 12 neuen Stammbaum-Charts, sondern eine unvollstaendige alte Entscheidung. Die Wurfchronik-Quelle (LLM-Markdown-Seiten in C:\Users\gulum\dev\Wurfchronik_Bilder, DIR_PATH in tools/import/merge_and_resolve.py) enthaelt ZWEI Sakura-Stubs: "Wurfchronik Teil 1_page_0022.md" (lokale id "page_0022_parent_sakura") und "Wurfchronik Teil 1_page_0023.md" (lokale id 7cbb62c5-7a46-4cb4-a3ad-bf7f94080e15). Die Entscheidung zu Ticket 7f9b3724 (tools/import/conflict-decisions.json, Eintrag externalRef "stammbaum-gwengensakuralittleangels-17122011", Zeilen ~1281-1293) listet in mergeExternalRefs nur den page_0022-Stub. Vor dem Fix wurden beide Stubs vom Call-Name-Dedup zu EINEM Tier verschmolzen (alte Id f9ba065c-dfb7-5c0f-bbf6-0d3c2e4afed9, in Prod jetzt 404). Da mergeExternalRefs VOR dem Dedup laeuft (merge_and_resolve.py Z.2743-2785) und den page_0022-Record entfernt, bleibt der page_0023-Stub als eigenes Cluster uebrig -> neue deterministische Id 336ad972-4e5f-50cb-92c9-71ce266d36a6 = uuid5(NAMESPACE_DNS, "Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e15") (nachgerechnet, stimmt exakt). Nicht gefundene Drop-Refs bzw. uebrig gebliebene Zwillinge werden still uebergangen (`continue`, keine Warnung). Identisches Muster bei JAMIE: Wurfchronik-Stub 4e4e507f-1ede-51e7-9079-ce6e7a4cb4af (ref "…page_0023.md-7cbb62c5-…e14") vs. kanonisches "Jamie von den kleinen Chaoten" 8ddf3717-5608-50d1-8b3d-78b8b198327a; die Jamie-Entscheidung (Z.~"stammbaum-jamiekleinenchaoten-27122010") merged ebenfalls nur page_0022_parent_jamie. Prod-Folgeschaden: H2-Wurf 8f043703 (13.04.2013) haengt an den beiden Stubs, waehrend Gwen 4be212ef zusaetzlich den chart-basierten Wurf 27ab49ae gleichen Datums traegt; L2-Wurf 66d5831a hat Mutter Gwen, aber Vater = Jamie-Stub.
|
||||
|
||||
**Fix:**
|
||||
|
||||
DATENFIX in tools/import/conflict-decisions.json (re-ingest-stabil, deterministisch), zwei Eintraege ergaenzen:
|
||||
|
||||
1) Eintrag mit "externalRef": "stammbaum-gwengensakuralittleangels-17122011" (Ticket 7f9b3724) — mergeExternalRefs erweitern auf:
|
||||
"mergeExternalRefs": [
|
||||
["gwengensakuralittleangels-17122011", "page_0022_parent_sakura"],
|
||||
["gwengensakuralittleangels-17122011", "Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e15"]
|
||||
]
|
||||
(Quelle-Feld ergaenzen: "source": "… + Ticket 6c9539d2 (zweiter Wurfchronik-Stub page_0023)")
|
||||
|
||||
2) Eintrag {"name": "Jamie von den kleinen Chaoten", "dob": "27.12.2010", …} — mergeExternalRefs erweitern auf:
|
||||
"mergeExternalRefs": [
|
||||
["stammbaum-jamiekleinenchaoten-27122010", "Wurfchronik Teil 1_page_0022.md-page_0022_parent_jamie"],
|
||||
["stammbaum-jamiekleinenchaoten-27122010", "Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e14"]
|
||||
]
|
||||
WICHTIG: immer den VOLLEN Ref inkl. Dateiname verwenden — die page_0023-UUIDs unterscheiden sich nur im letzten Zeichen (…e13 = JD, …e14 = Jamie, …e15 = Sakura), Suffix-Matching allein ist zu unscharf.
|
||||
|
||||
Danach: python tools/import/merge_and_resolve.py + Upload-Ingest gegen Prod. Erwartung (verifizieren): "Sakura" 336ad972 und "Jamie" 4e4e507f verschwinden; H2-Wurf und Chart-Wurf 27ab49ae haben nach dem _premerged_ids-Remap (merge_and_resolve.py Z.3445-3448) identische Eltern + Datum und werden vom Litter-Dedup Stage 2 zu einem Wurf kollabiert; L2-Wurf hat dann Vater "Jamie von den kleinen Chaoten".
|
||||
|
||||
OPTIONALE HAERTUNG (verhindert weitere Rueckfaelle, merge_and_resolve.py direkt nach dem mergeExternalRefs-Block ~Z.2782): pro angewandtem Paar pruefen, ob noch ein Record mit ImportSource "Wurfchronik" ohne DateOfBirth und gleichem normalisierten Call-Name wie der Keeper in all_processed_gerbils steht; wenn ja, Zeile in review-report.md/Konsole ausgeben ("Merge-Entscheidung evtl. unvollstaendig: <Name> — weiterer Wurfchronik-Stub <ExternalRef>"). Zusaetzlich beim stillen `continue` (Drop-Ref nicht gefunden) warnen. Systemische Kandidatenliste fuer eine eigene Aufraeum-Aufgabe: 67 DOB-lose Wurfchronik-Stubs, davon 14 mit Call-Name-Kollision zu einem Stammbaum-Tier (Raya, Gale von den Kleinen Chaoten, Jamie, Tai, Katsu, Willow, Qamikaze Queen, Eddard Stark, Xtra Bounty, SA, Joghurt, Maxi King, Fast Boy, Obelix).
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Sakura war tatsaechlich noch ein zweites Mal vorhanden: In der Wurfchronik ist sie auf zwei verschiedenen Seiten als Elterntier notiert, und beim letzten Zusammenfuehren wurde nur der eine der beiden Eintraege mit "Gwen gen. Sakura of little Angels" verschmolzen. Jetzt sind beide Eintraege zusammengefuehrt — dasselbe galt fuer Jamie, der ebenfalls doppelt geführt war. Damit haengen der H2-Wurf und der L2-Wurf vom 13.04. bzw. 20.05.2013 wieder bei den richtigen Eltern, und der doppelte Wurf vom 13.04.2013 ist zu einem Eintrag zusammengefasst.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/merge_and_resolve.py (mergeExternalRefs-Block Z.2743-2785, Litter-Remap Z.3445-3448); C:\Users\gulum\dev\Wurfchronik_Bilder (Wurfchronik Teil 1_page_0022.md / _page_0023.md)
|
||||
|
||||
---
|
||||
|
||||
### „Unbekannt" (be299aaf, *21.06.2014) — namenlose Q3-Dublette + falscher Vater des Q4-Wurfs
|
||||
|
||||
Tickets: `4f890829-c2ad-4709-8d45-399eb077c12f` · Layer **import-data** · Cluster `conflict-decisions-wurfchronik-page0009-platzhalter-eltern` · Aufwand S · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Zwei zusammenhängende Import-Defekte. (1) DUBLETTE: Das Tier be299aaf „Unbekannt" (m, *21.06.2014, ExternalRef „Wurfchronik Teil 1_page_0009.md-90e23e20-3051-4048-b4b1-e2bc00b21736", Notes „Color: Silberagouti. Note: Ni. ? bleibt.") ist ein namenloses Jungtier des Q3-Wurfs (7d5c5218, 21.06.2014, Jacky × Gale/Drake Jr, WS 6). Die Quelle C:/Users/gulum/dev/Wurfchronik_Bilder/„Wurfchronik Teil 1_page_0009.md" listet die 6 Q3-Jungtiere NUR per Farbe ohne Namen; „…page_0028.md" listet dieselben 6 MIT Namen (Pünktchen/Agouti, Quebec/dd-Anthrazit, Kai-Uwe/Blau + 3 namenlose AWP-Tote). Da Platzhalter-Namen im Dedup absichtlich nie gemergt werden (tools/import/merge_and_resolve.py ~Z.2975 „is_placeholder → keep all separate"), bleiben 6 Seite-0009-Dubletten stehen (be299aaf, 911516bb, 1230a554, 089d5d1b, 11bbdb9e, 8dcb9e1f) — genau das hat die Züchterin erkannt („Hast du auch Kai-Uwe oder Pünktchen?"). (2) FALSCHE VERPAARUNG: Der Wurf 751f3dcc „Q4-Wurf" (10.12.2014, Kinder Sweet + Vee rich) trägt in der md-Quelle Seite 0038 die Elternangabe „Eltern: [Unbekannt] + Nelly". Die Eltern-Namensauflösung (merge_and_resolve.py Z.3600-3611, _resolve_name) hat keinen Platzhalter-Guard und löste den „Namen" Unbekannt auf das namenlose Jungtier be299aaf auf → FatherId=be299aaf, MotherId=None. parent_age_plausible greift nicht (Abstand 5,7 Monate = formal plausibel). Scan über alle 965 Würfe: nur dieser eine Wurf betroffen. Echte Eltern belegt: tools/import/output/litters.json Zeile „sheet1-Q4-10.12.2014" (Wurfchronik-xlsx, sheet1 row 131) nennt sireName „Earl [Kleine Chaoten]" + damName „Nelly [Kleine Chaoten]" → Earl d676e74d (m, *22.04.2013), Nelly b3826f4b (w, *12.07.2013); tools/import/_rpro3.db bestätigt das Paar (Earl × Nelly → Wade, *14.01.2015). Warum die xlsx-Angabe nicht griff: (a) `_father_name` war mit „Unbekannt" schon belegt, die Anreicherung füllt nur Lücken (Z.3575-3578), und (b) die Kandidaten-Verengung Z.3568 vergleicht litters.json-`litterId` („Q4") gegen `LitterLetter` („Q") → kein Treffer, 2 gleich-datierte Kandidaten (Q4+R4) → `continue`. Kein Rückfall durch die 12 neuen Charts vom 2026-08-18 — der Fall stammt komplett aus den Wurfchronik-md-Quellen.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Datenfix in tools/import/conflict-decisions.json (re-ingest-stabil, deterministisch), zwei Einträge:
|
||||
|
||||
1) `suppressExternalRefs` — Dubletten-Tier „Unbekannt" entfernen (die Züchterin: „Bitte lösche Unbekannt!"). An das bestehende Array anhängen:
|
||||
"Wurfchronik Teil 1_page_0009.md-90e23e20-3051-4048-b4b1-e2bc00b21736"
|
||||
(be299aaf hat keine Fotos, keine Verträge, keine Kinder und keinen Geburtswurf; die einzige Referenz ist der Q4-Vaterlink. Ein page_0009-Suppress-Block existiert bereits — gleiche Musterstelle.) Optional den `_doc_suppress`-Text ergänzen: „… sowie das namenlose Q3-Jungtier von Seite 0009 (Ticket 4f890829), das als 'Unbekannt'-Vater des Q4-Wurfs aufgelöst wurde."
|
||||
|
||||
2) `litterParents` — echte Eltern des Q4-Wurfs setzen (läuft Z.4098 VOR der Suppression Z.4423, daher kein Dangling-Link):
|
||||
{
|
||||
"litterName": "Q4-Wurf",
|
||||
"date": "10.12.2014",
|
||||
"father": "Earl",
|
||||
"fatherDob": "22.04.2013",
|
||||
"mother": "Nelly",
|
||||
"motherDob": "12.07.2013",
|
||||
"source": "Ticket 4f890829: Wurfchronik-xlsx sheet1 row 131 (Earl [Kleine Chaoten] x Nelly [Kleine Chaoten]); rpro3 bestaetigt das Paar (Wade *14.01.2015). Ersetzt den faelschlich aufgeloesten Platzhalter-Vater 'Unbekannt'."
|
||||
}
|
||||
|
||||
Danach: `python tools/import/extract.py` (optional, unverändert) → `python tools/import/merge_and_resolve.py` → prüfen: (a) kein Gerbil mit Id be299aaf mehr im Payload, (b) Q4-Wurf 751f3dcc hat FatherId d676e74d / MotherId b3826f4b, (c) `suppressExternalRefs: 1 Gerbil(s) … entfernt` in der Konsole → dann Upload-Ingest gegen Prod (`POST /api/import/ingest-resolved/upload`). Der Upsert-Ingest löscht die nun fehlende importierte Zeile automatisch als „stale"; das Ticket behält seine lose gerbilId (FK-frei) und bleibt lesbar.
|
||||
|
||||
OPTIONALE Härtung (separat verifizieren, nicht für dieses Ticket nötig):
|
||||
- merge_and_resolve.py direkt vor der xlsx-Anreicherungs-Schleife (Z.3565): Platzhalter-Elternnamen leeren, damit sie nie auf ein Tier auflösen und die xlsx-Angabe greifen kann —
|
||||
for l in resolved_litters:
|
||||
for _k in ("_father_name", "_mother_name"):
|
||||
_n = "".join(c for c in (l.get(_k) or "").lower() if c.isalnum())
|
||||
if _n in ("unbekannt", "unknown", "unbenannt", "nn", "keineangabe", "ka", "na"):
|
||||
l[_k] = ""
|
||||
- merge_and_resolve.py Z.3568: Verengung auf das Wurf-Namens-Präfix statt `LitterLetter` (z. B. `(l.get("Name") or "").split("-")[0].upper()`), damit gleich-datierte Würfe (Q4/R4) ihre xlsx-Eltern bekommen — würde u. a. auch R4-Wurf dbba6874 (Arrow × Kia) automatisch füllen; größerer Blast-Radius, eigener Verifikationslauf.
|
||||
- Die 5 restlichen namenlosen page_0009-Q3-Dubletten (911516bb, 1230a554, 089d5d1b, 11bbdb9e, 8dcb9e1f) sind derselbe Fehler, wurden aber von der Züchterin nicht explizit benannt → separat anfragen, nicht mit durchlöschen.
|
||||
Restkosmetik: Die Wurf-Notiz „Eltern: [Unbekannt] + Nelly" bleibt als Quelltext-Zitat stehen (kein Override-Mechanismus für Wurf-Notizen).
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Du hattest recht: das Tier „Unbekannt" (geboren 21.06.2014) war eine Dublette — es ist eines der Jungtiere aus dem Q3-Wurf vom 21.06.2014, das in deiner Wurfchronik auf einer Seite nur mit Farbe („Silberagouti, bleibt, Ni.?") und ohne Namen steht, während dieselben Jungtiere auf einer anderen Seite mit Namen aufgeführt sind — darunter genau Kai-Uwe und Pünktchen (und Quebec). Der Eintrag „Unbekannt" ist jetzt gelöscht. Er war außerdem versehentlich als Vater des Q4-Wurfs vom 10.12.2014 eingetragen, weil dort in der Quelle nur „Unbekannt + Nelly" stand und das Programm diesen Platzhalter für einen echten Tiernamen gehalten hat. Laut deiner Wurfchronik sind die Eltern dieses Wurfs Earl und Nelly — das ist jetzt so eingetragen, Sweet und Vee rich haben damit ihre richtigen Eltern.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/merge_and_resolve.py; Wurf Q4-Wurf 751f3dcc (10.12.2014, Sweet + Vee rich); Tier be299aaf 'Unbekannt'; Q3-Wurf 7d5c5218 (21.06.2014)
|
||||
|
||||
---
|
||||
|
||||
### Merle (2023-06-18): Herkunft leer + noch als Zuchttier im Bestand (Abgabe 22.12.2024 fehlt)
|
||||
|
||||
Tickets: `7037f5d8-e759-47f2-9f1c-778614f10f06` · Layer **import-data** · Cluster `conflict-decisions-resolutions-merge-and-resolve` · Aufwand M · Konfidenz medium · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Zwei Ursachen, beide in conflict-decisions.json / merge_and_resolve.py. (1) BESTAND: Merle (Prod-Id 9df4890f-4959-548f-aa96-c9ee8c759e03, dob 2023-06-18, extref stammbaum-merle-18062023) hat Status "Breeding", ReceiverContactId=null, GoHomeDate=null. Die Abgabe, die die Züchterin bereits in Ticket a8f11ac0 (2026-07-13) gemeldet hat ("abgegeben an Swen Pulinckx am 22.12.24, dort in Schneewittchen umbenannt"), wurde nie in conflict-decisions.json übernommen. Die Bestandsansicht (gerbil-manager-web/src/pages/GerbilsPage.tsx, Z.60 + Z.158) filtert genau auf isResident==true AND status=='Breeding' → Merle steht dort zu Recht drin, solange kein Abnehmer/Abgabedatum gesetzt ist. isResident=true ist regelkonform (eigener Wurf "Wurf von Smoke + Merle" 12.07.2024, ShowInChronicle=true) → nicht die Residenz, sondern der Status ist falsch. (2) HERKUNFT: Merle.OriginBreeder ist null, die Akte zeigt "Herkunft: —" (GerbilDetailPage.tsx Z.545-551). Regression durch den Chart-Import vom 2026-08-18: der Override-Block in tools/import/merge_and_resolve.py (Z.3198-3325) wendet pro Tier NUR EINE Decision an (first match: exakter externalRef → endswith → (name,dob) → (name,"")). Der addAnimals-Stub "decision-merle" ist mit dem neuen Chart-Datensatz "Stammbaum von Pukas Kids.xlsx" verschmolzen (daher auch die neue Id; die Ticket-gerbilId ce05fad7 liefert 404) → Merle matcht jetzt resolutions[150] (name+dob) und resolutions[136] (name-only, originBreeder "Clan of Black Forest") fällt still weg. Zusätzlich leakt der tote name-only-Eintrag über den (call-name,"")-Branch auf den Namensvetter "Merle of Samsimar" (43603fcc, dob 2012-09-01), der in Prod deshalb fälschlich originBreeder "Clan of Black Forest" trägt. (3) resolutions unterstützen kein goHomeDate → das Abgabedatum 22.12.2024 lässt sich derzeit gar nicht data-driven setzen.
|
||||
|
||||
**Fix:**
|
||||
|
||||
A) tools/import/conflict-decisions.json — resolutions[136] (der name-only "Merle"-Eintrag mit "externalRef": "decision-merle") KOMPLETT LÖSCHEN: er greift bei Merle nicht mehr und setzt stattdessen fälschlich die Herkunft von "Merle of Samsimar".
|
||||
|
||||
B) tools/import/conflict-decisions.json — resolutions[150] durch diesen konsolidierten Eintrag ERSETZEN (eine Decision pro Tier, gekeyt auf name+dob UND aktuellen externalRef):
|
||||
{
|
||||
"name": "Merle",
|
||||
"dob": "18.06.2023",
|
||||
"externalRef": "stammbaum-merle-18062023",
|
||||
"isResident": true,
|
||||
"originBreeder": "Zucht der Kleinen Chaoten",
|
||||
"receiver": "Swen Pulinckx",
|
||||
"goHomeDate": "22.12.2024",
|
||||
"notes": "Tochter von Akane von den Kleinen Chaoten × Bonaparte von den Schlossmäusen, geboren bei Clan of Black Forest (Akane war dort nur im Zuchttier-Austausch). Herkunft laut Züchterin: Zucht der Kleinen Chaoten. Am 01.04.2024 zurück zu den Kleinen Chaoten (Gewicht 62,1 g), Zuchttier mit Smoke (Würfe 12.07.2024 und 19.08.2024). Am 22.12.2024 zusammen mit ihrer Tochter Speedy an Swen Pulinckx abgegeben und dort in „Schneewittchen“ umbenannt. Weitere Daten siehe Stammbaum Pukas Kids.",
|
||||
"decision": "Ticket 7037f5d8: Herkunft = Zucht der Kleinen Chaoten (Züchterin-Angabe, ersetzt die frühere Angabe „Clan of Black Forest“). Merle bleibt Zuchttier (isResident=true, eigener Wurf mit Smoke in der Wurfchronik), ist aber seit 22.12.2024 abgegeben → Status GivenAway + Abnehmer + Abgabedatum, damit sie nicht mehr in der Bestandsansicht (isResident && status=Breeding) auftaucht. Konsolidiert die alte name-only-Decision, die nach dem Chart-Import auf „Merle of Samsimar“ geleakt hat.",
|
||||
"source": "Ticket 7037f5d8 (Züchterin 2026-07-31) + Ticket a8f11ac0 + Ticket 00715df9 + Ticket 36a3fcde"
|
||||
}
|
||||
Hinweis: "genotype" ist nicht nötig — der Chart liefert schon "Aa c[chm]c[chm] D- ee[-] GG P- Spsp" (deckt sich mit ihrer Angabe), und der genotype-Override greift ohnehin nur bei leerem Feld. Der Kontakt "Swen Pulinckx" existiert noch nicht und wird von _resolve_contact_id_by_name automatisch als Receiver-Kontakt erzeugt.
|
||||
|
||||
C) tools/import/merge_and_resolve.py — goHomeDate-Override ergänzen (bisher nicht unterstützt):
|
||||
1. in der has_ovr-Bedingung (~Z.3211-3215) ergänzen: ... or d.get("dateOfDeath") or d.get("goHomeDate") ...
|
||||
2. direkt NACH dem dateOfDeath-Block (~Z.3290, vor "if d.get(\"correctDob\")") einfügen:
|
||||
# goHomeDate: Abgabedatum aus einem Züchterin-Ticket (nur setzen, wenn leer);
|
||||
# zieht Status GivenAway nach, sofern das Tier nicht verstorben ist.
|
||||
if d.get("goHomeDate") and not g.get("GoHomeDate"):
|
||||
_gh = parse_date(d["goHomeDate"])
|
||||
if _gh:
|
||||
g["GoHomeDate"] = _gh
|
||||
if g.get("Status") != "Deceased":
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
(GoHomeDate wird von IngestResolvedService.cs Z.130/146 mit-upserted, "GivenAway" verhindert außerdem, dass die 6-Jahres-Sterbe-Heuristik greift.)
|
||||
|
||||
D) Danach: python tools/import/merge_and_resolve.py, plausibilisieren (Merle 9df4890f: OriginBreeder "Zucht der Kleinen Chaoten", Status GivenAway, GoHomeDate 2024-12-22, ReceiverContactId gesetzt, IsResident true; "Merle of Samsimar" 43603fcc: OriginBreeder wieder null), python test_merge_resolve.py, dann Upload-Ingest gegen Prod.
|
||||
|
||||
Optionale Nacharbeit (kein Blocker): die drei Merle-Tickets (00715df9, a8f11ac0, 7037f5d8) zeigen auf die tote gerbilId ce05fad7 → Links in der Ticketliste laufen ins Leere; Umhängen auf 9df4890f wäre ein reiner Feedback-Update (keine Importdaten).
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Merle ist jetzt richtig eingetragen: Als Herkunft steht „Zucht der Kleinen Chaoten“ in ihrer Akte, und ihre Abgabe ist erfasst — abgegeben am 22.12.2024 an Swen Pulinckx (dort „Schneewittchen“). Dadurch verschwindet sie aus der Bestandsliste der Zuchttiere und taucht nur noch unter den abgegebenen Tieren auf; ihr Wurf mit Smoke bleibt natürlich in ihrer Akte und in der Wurfchronik. Warum es beim letzten Mal nicht gehalten hat: durch die neu eingelesenen Stammbäume (u. a. „Pukas Kids“) hat Merle einen Datensatz mit Geburtsdatum bekommen, und meine alte Notiz zu ihr passte danach nicht mehr auf sie — sie landete versehentlich bei der viel älteren „Merle of Samsimar“, die deshalb eine falsche Herkunft trug. Beides ist jetzt zusammengeführt und dauerhaft an Namen + Geburtsdatum festgemacht. Offen ist noch ihr zweiter Wurf mit Smoke vom 19.08.2024 (mit Tochter Speedy) — dazu warte ich noch auf deine Antwort im älteren Ticket.
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json; tools/import/merge_and_resolve.py; Prod-Daten: Merle 9df4890f-4959-548f-aa96-c9ee8c759e03; Prod-Daten: Merle of Samsimar 43603fcc-9384-5637-a0cb-48dfe36cea5a; gerbil-manager-web/src/pages/GerbilsPage.tsx (nur Analyse, keine Änderung)
|
||||
|
||||
---
|
||||
|
||||
### JackJack (b6d8b3ef) + A-Wurf 18.02.2010 (29f4bae9): Saphir wird als Platin errechnet — C-Locus-Zygotie fehlt im Farbkatalog
|
||||
|
||||
Tickets: `bde4ec70-d082-43ad-8343-ce82ed9eb39a, f89e95ad-de02-4d35-b2ec-7fa150146458` · Layer **genetics-engine** · Cluster `genetik-c-locus-zygotie-saphir-platin` · Aufwand M · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Katalog-Kollision in der Genetik-Engine: `gerbil-manager-web/src/genetics/catalog.ts` Z.67 (Platin), Z.81 (Saphir) und Z.86 (Platin-Hell) tragen ALLE die identischen Tokens {A:'a',C:'C',D:'D',E:'E',G:'G',P:'p'}. `locusToken()` reduziert den C-Locus auf das dominante Allel, d.h. CC, Cc[chm] und Cc[h] liefern alle den Token 'C' — also gewinnt beim "first match wins" immer die erste Zeile (Platin), Saphir und Platin-Hell sind unerreichbare Synonyme. Reproduziert lokal: `genotype.farbschlag_from_genotype_string('aa Cc[chm] DD E- G- pp Spsp')` == 'Platin' (UI hängt den Sp-Modifier an → "Platin Schecke"). Die Züchterin hat autoritativ geklärt (Ticket f89e95ad, Status Answered): "Saphir ist aa Cc[chm] D- E- G- pp und Platin ist aa CC D- E- G- pp. Es ist nicht dasselbe!" — der C-Locus muss also ZYGOTIE-genau matchen. Belege: Prod JackJack (b6d8b3ef) hat Genotyp 'aa Cc[chm] DD E- G- pp Spsp', gespeicherte ColorVariety ...000025 = Saphir (also schon korrekt) und Notiz "Saphir-Sp" — nur die errechnete Farbe lügt; GerbilOverride vorhanden (protectedFields name/spottingType/genotype/isDeaf, 2026-07-21), der Importer-Stand heißt noch "JackSack" mit Gencode 'aa CC DD EE GG pp Spsp rere'. Zweite, unabhängige Ursache für f89e95ad: die Mutter des A-Wurfs (Kuke, e4898ccc) hat in Prod weiterhin `genotype = null`, deshalb rechnet die WurfDetailPage überhaupt keine erwarteten Farbschläge; rpro3 belegt Kuke = Anthrazit-Schecke, 'aa CC DD Ee gg Pp Spsp'. Verschärfend (Ursache der Fehl-Gencodes im Import): merge_and_resolve.resolve_color_and_genotype schreibt bei einem Farb-LABEL "…Schecke" ohne Gencode den CanonicalGenotype der ColorVariety aus colorVarietySeed.backend.json — solange Saphirs Canonical 'aa CC …' lautet, bekommt ein "Saphir-Sp"-Tier den Platin-Gencode.
|
||||
|
||||
**Fix:**
|
||||
|
||||
CODE-FIX (Engine, kein Live-DB-Patch; nutzt die bereits vorhandene '/'-Paar-Token-Konvention von Siam/Zobel-Hell, daher KEINE Umsortierung des Katalogs → sortOrder und ColorVariety-GUIDs bleiben stabil):
|
||||
|
||||
(1) `gerbil-manager-web/src/genetics/catalog.ts` — `matches()` (direkt nach `locusToken`) ersetzen durch:
|
||||
```ts
|
||||
/**
|
||||
* GEN-6 (Tickets bde4ec70 / f89e95ad): ein Token darf ein EXAKTES Allelpaar
|
||||
* "x/y" (ungeordnet) verlangen statt des exprimierten dominanten Allels.
|
||||
* Nötig, weil sich Platin / Saphir / Platin-Hell NUR in der C-Zygotie
|
||||
* unterscheiden (Züchterin, autoritativ: "Saphir ist aa Cc[chm] D- E- G- pp
|
||||
* und Platin ist aa CC D- E- G- pp. Es ist nicht dasselbe!"):
|
||||
* aa CC D- E- G- pp -> Platin
|
||||
* aa C/c[chm] D- E- G- pp -> Saphir
|
||||
* aa C/c[h] D- E- G- pp -> Platin-Hell (GEN-3g: "-Hell" == c[h]-Allel)
|
||||
* representativeGenotype() versteht die "x/y"-Form bereits (GEN-3f).
|
||||
*/
|
||||
function pairMatches(g: Genotype, locus: LocusKey, token: string): boolean {
|
||||
const [a, b] = resolvedPair(g, locus)
|
||||
const [x, y] = token.split('/') as [string, string]
|
||||
return (a === x && b === y) || (a === y && b === x)
|
||||
}
|
||||
|
||||
function matches(g: Genotype, entry: FarbschlagEntry): boolean {
|
||||
return (Object.keys(entry.tokens) as LocusKey[]).every((locus) => {
|
||||
const token = entry.tokens[locus]!
|
||||
return token.includes('/') ? pairMatches(g, locus, token) : locusToken(g, locus) === token
|
||||
})
|
||||
}
|
||||
```
|
||||
und die drei Einträge (nur der C-Token ändert sich, Reihenfolge bleibt):
|
||||
- Z.67: `{ name: 'Platin', english: 'Lilac', tokens: { A: 'a', C: 'C/C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin.JPG' },`
|
||||
- Z.81: `{ name: 'Saphir', tokens: { A: 'a', C: 'C/cchm', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },`
|
||||
- Z.86: `{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C/ch', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },`
|
||||
(Platin-Hell ist die konsistente Anwendung der schon dokumentierten breeder-Regel GEN-3g und trifft aktuell 0 Tiere; wenn der Haupt-Loop maximal konservativ sein will, kann Schritt für Platin-Hell entfallen — dann bei Platin `C: 'C'` lassen und nur Saphir auf `'C/cchm'` setzen, Saphir muss dann aber VOR Platin stehen.)
|
||||
|
||||
(2) `cd gerbil-manager-web && npm run gen:catalog` — regeneriert colorVarietySeed.generated.json (Saphir → `aa Cc[chm] DD EE GG pp spsp rere`) und colorVarietySeed.backend.json (`aa Ccchm DD EE GG pp spsp rere`). Ohne das ist `src/genetics/__tests__/catalog-drift.test.ts` rot.
|
||||
|
||||
(3) `tools/import/genotype.py` — Mirror; `_matches` ersetzen:
|
||||
```python
|
||||
def _pair_matches(mapped, locus, token):
|
||||
a, b = _resolve_allele_pair(locus, mapped.get(locus, ["?", "?"]))
|
||||
x, y = token.split("/")
|
||||
return (a == x and b == y) or (a == y and b == x)
|
||||
|
||||
|
||||
def _matches(mapped, tokens):
|
||||
return all(
|
||||
_pair_matches(mapped, locus, tok) if "/" in tok else _locus_token(mapped, locus) == tok
|
||||
for locus, tok in tokens.items()
|
||||
)
|
||||
```
|
||||
und in `_BASE_COLORS`: `("Platin", {"A":"a","C":"C/C","D":"D","E":"E","G":"G","P":"p"})`, `("Saphir", {"A":"a","C":"C/cchm","D":"D","E":"E","G":"G","P":"p"})`, `("Platin-Hell", {"A":"a","C":"C/ch","D":"D","E":"E","G":"G","P":"p"})`.
|
||||
|
||||
(4) `GerbilManagerWebAPI/ApplicationContext.cs` → `SeedColorVarieties`: Zeile `("Saphir", "aa CC DD EE GG pp spsp rere", 24)` → `("Saphir", "aa Ccchm DD EE GG pp spsp rere", 24)` und `("Platin-Hell", "aa CC DD EE GG pp spsp rere", 27)` → `("Platin-Hell", "aa Cch DD EE GG pp spsp rere", 27)`; Platin bleibt. Dann `dotnet ef migrations add ReseedColorVarietiesGen6Saphir --project GerbilManagerWebAPI` (erzeugt UpdateData für Id `00000000-0000-0000-0000-000000000025` und `...000000000028`, Muster wie ReseedColorVarietiesAR5). Kein C#-Genetik-Mirror vorhanden (Rpro3ImportService verknüpft nur über Farbnamen) → nichts weiter im Backend.
|
||||
|
||||
(5) Regressionstests PFLICHT (Genetik = korrektheitskritisch):
|
||||
- `src/genetics/__tests__/genetics.test.ts`: `name('aa CC DD EE GG pp spsp rere') === 'Platin'` (Z.164/236 bleiben grün), `name('aa Cc[chm] DD EE GG pp spsp rere') === 'Saphir'`, `name('aa Cc[chm] DD E- G- pp Spsp') === 'Saphir Schecke'` (JackJack), `name('aa Cc[h] DD EE GG pp spsp rere') === 'Platin-Hell'`, plus breed(Blacky `aa Cc[chm] DD EE GG Pp spsp` × Kuke `aa CC DD Ee gg Pp Spsp`) enthält 'Saphir' UND 'Saphir Schecke'.
|
||||
- `tools/import/test_genotype.py`: `farbschlag_from_genotype_string('aa Cc[chm] DD EE GG pp') == 'Saphir'`, `('aa CC DD EE GG pp') == 'Platin'`, `('aa Cc[h] DD EE GG pp') == 'Platin-Hell'`.
|
||||
|
||||
DATEN-FIX (nur für Ticket f89e95ad, damit der Wurf überhaupt Erwartungen rechnet) — die BESTEHENDE Resolution in `tools/import/conflict-decisions.json` (`externalRef: "decision-kruke"`) um zwei Felder ergänzen; sie sagt selbst, dass Farbe/Genotyp auf diese Klärung wartet:
|
||||
```json
|
||||
{
|
||||
"name": "Kruke",
|
||||
"isResident": true,
|
||||
"decision": "'Kruke' war ein Wurfchronik-Tippfehler; korrekt 'Kuke' (rpro3). Bestandstier. Todesdatum 06.04.2011 (Ausbruch-Unfall, rpro3). Farbe/Genotyp jetzt gesetzt: rpro3 'Kuke' (M.Knoss) = Anthrazit-Schecke, Gencode aa CC DD Ee gg Pp Spsp; die Zuechterin hat Saphir/Platin geklaert (Ticket f89e95ad, 2026-08). KEIN Merge mit dem Ahnen-Record 'Kuke von Marion Knoss' — der Merge nullte die Mutter-Verknuepfung aller Blacky-x-Kuke-Wuerfe.",
|
||||
"source": "KI-Triage 2026-07-12 — Ticket 34220307-23bc-420c-80f8-d273615caffd + b7016e5f-9985-4f17-a3f6-2048289958aa + Tickets e0a0c304/a50116bf; Gencode-Nachtrag KI-Triage 2026-08-19 (Ticket f89e95ad, rpro3 _rpro3.db)",
|
||||
"externalRef": "decision-kruke",
|
||||
"dateOfDeath": "06.04.2011",
|
||||
"renameTo": "Kuke",
|
||||
"genotype": "aa CC DD Ee gg Pp Spsp",
|
||||
"farbschlag": "Anthrazit"
|
||||
}
|
||||
```
|
||||
Hinweise: `merge_and_resolve.py` Z.3302/3305 setzen `genotype`/`farbschlag` nur, wenn das Feld leer ist → re-ingest-stabil (Kuke hat beides null). `farbschlag` matcht EXAKT einen Katalognamen via `variety_map`, deshalb "Anthrazit" ohne "-Schecke" — die Scheckung steckt im Sp-Locus des Gencodes. Danach `python tools/import/extract.py` + `extract_contracts.py` + `merge_and_resolve.py`, dann Upload-Ingest gegen Prod (`POST /api/import/ingest-resolved/upload`). Der Engine-Fix selbst braucht KEINEN Re-Ingest (Anzeige wird client-seitig gerechnet), aber ein Re-Ingest nach (2) korrigiert zusätzlich die 5 heute als Platin geführten C-het-Tiere in der gespeicherten ColorVariety.
|
||||
|
||||
Verifikation vor "fertig": `npx tsc --noEmit`, `npx vitest run`, `npx eslint src/genetics`, `python tools/import/test_genotype.py`, `dotnet build && dotnet test`; Stichprobe: JackJack zeigt "Saphir Schecke", A-Wurf listet Saphir + Saphir Schecke.
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Saphir und Platin sind jetzt zwei verschiedene Farben — genau wie du gesagt hast. Bisher hat das Programm beide gleich behandelt und deshalb bei JackJack "Platin Schecke" errechnet, obwohl er mit Cc[chm] eine Saphir-Schecke ist. Ab jetzt gilt: mit zwei vollen C ist es Platin, mit einem c[chm] daneben ist es Saphir (und mit einem c[h] daneben Platin-Hell). JackJack wird deshalb wieder als Saphir Schecke angezeigt, und bei fünf weiteren Tieren (Ina, Tommy, Stich, Sansa, Fenris) steht nun ebenfalls Saphir statt Platin. Beim A-Wurf vom 18.02.2010 fehlte außerdem der Gencode von Kuke — ich habe ihn aus deinen RennerPro-Unterlagen ergänzt (Anthrazit-Schecke, aa CC DD Ee gg Pp Spsp). Damit tauchen bei den erwarteten Farbschlägen des Wurfs jetzt auch Saphir und Saphir Schecke auf.
|
||||
|
||||
**Betroffen:** gerbil-manager-web/src/genetics/catalog.ts; gerbil-manager-web/src/genetics/colorVarietySeed.generated.json; gerbil-manager-web/src/genetics/colorVarietySeed.backend.json; gerbil-manager-web/src/genetics/__tests__/genetics.test.ts; gerbil-manager-web/src/genetics/__tests__/catalog-drift.test.ts; tools/import/genotype.py; tools/import/test_genotype.py; tools/import/conflict-decisions.json; GerbilManagerWebAPI/ApplicationContext.cs; GerbilManagerWebAPI/Migrations (neu: ReseedColorVarietiesGen6Saphir)
|
||||
|
||||
---
|
||||
|
||||
### Kuke (prod e4898ccc): fehlender Zuchtname + alter Name „Kruke" in 5 Wurf-Notizen
|
||||
|
||||
Tickets: `24006ccd-c07c-4617-a016-cfa7587ac2aa, ea803c3d-33f5-4d3b-b05a-fb2bca8bb893` · Layer **import-logic** · Cluster `conflict-decisions+merge_and_resolve-rename` · Aufwand M · Konfidenz high · Aktion handover-to-implementation
|
||||
|
||||
**Ursache:** Zwei getrennte Ursachen, beide im Importer.
|
||||
|
||||
(1) Ticket ea803c3d („überall Kuke"): `renameTo` GREIFT — prod-Name ist „Kuke". Der alte Name steht nur noch in den aus der Wurfchronik übernommenen Wurf-Notizen: B-Wurf 2010 (f0b1fd6f) „Blacky + Kruke v. 17.04.2010; WS=2", D-Wurf 2010 (1eb3e1fb), E-Wurf 2010 (53b55c8c), F-Wurf 2010 (10ddf880), I-Wurf (f4b2b9be) „Eltern: Blacky + Kruke". Grund: `renameTo` (merge_and_resolve.py ~Z.3272-3282) schreibt nur `g["Name"]`, es gibt keine Propagation in Freitext-Notizen. Prod-weiter Scan (2451 Gerbils, 966 Litters, 886 Contacts) + resolved_import.json: kein weiteres sichtbares „Kruke"; intern bleiben nur ExternalRef „decision-kruke" und NameSearch „kruke" (NameSearch wird von keinem Endpoint abgefragt → irrelevant).
|
||||
|
||||
(2) Ticket 24006ccd („Zuchtname fehlt"): Das Tier existiert ZWEIMAL. (a) resident e4898ccc = addAnimals-Stub „Kruke" → renameTo „Kuke", ohne OriginContact, Genotype/ColorVariety null; (b) Chart-Ahnenrecord 6d9f9d66 „Kuke von Marion Knoss" (ExternalRef stammbaum-kukemarionknoss-18092009, gleiches DOB 18.09.2009, gleicher Todestag 06.04.2011, OriginContact 84628989 Marion Knoss, Genotype „aa CC DD Ee Uwuw[d] Pp Spsp" = aa CC DD Ee Gg Pp Spsp, ColorVariety …0007 Schwarz). Der Zuchtname steht also nur am Zwilling, und die Frontend-Regel (gerbil-manager-web/src/format/gerbilName.ts: Suffix nur für eigengezüchtete Tiere) hängt fremden Tieren nichts an → der Zuchtname muss Teil des Namens sein. Ihr eigener Chart-Name lautet exakt „Kuke von Marion Knoss" (tools/import/output/animals.json; Charts Danako/Hana/Jin/Unit/Uriana/Wildfire), rpro3-Herkunft „M.Knoss GG", Eltern Marc x Sara.
|
||||
|
||||
KEIN Rückfall durch die 12 neuen Charts vom 2026-08-18: der Zwilling existierte schon (die neuen Charts haben nur weitere sourceFiles ergänzt). Der Merge war 2026-07-12 bewusst ausgelassen worden („der Merge nullte die Mutter-Verknüpfung aller Blacky-x-Kuke-Würfe", conflict-decisions.json Z.986) — mit keep=decision-kruke ist genau dieser Fehlermodus heute abgedeckt.
|
||||
|
||||
**Fix:**
|
||||
|
||||
A) DATEN — tools/import/conflict-decisions.json, `resolutions`[114] (der Eintrag mit "name": "Kruke", Z.983-991) komplett ersetzen durch:
|
||||
|
||||
{
|
||||
"name": "Kruke",
|
||||
"isResident": true,
|
||||
"decision": "'Kruke' war ein Wurfchronik-Tippfehler; korrekt 'Kuke von Marion Knoss' (so heisst sie in ihren eigenen Stammbaeumen Danako/Hana/Jin/Unit/Uriana/Wildfire; rpro3-Herkunft 'M.Knoss GG', Eltern Marc x Sara). Bestandstier, Todesdatum 06.04.2011 (Ausbruch-Unfall, rpro3). Der Chart-Ahnenrecord wird jetzt HIER hineingemergt (keep = decision-kruke): der Keeper behaelt bis zum Parent-Resolver den Quellnamen 'Kruke', renameTo setzt danach _pre_rename_name (Namensindex) und die Chart-Wuerfe des gedroppten Zwillings werden ueber _premerged_ids umgebogen -> die Mutter-Verknuepfung der Blacky-x-Kuke-Wuerfe bleibt erhalten (das war 2026-07-12 der Grund, den Merge auszulassen).",
|
||||
"source": "KI-Triage 2026-08-19 - Tickets 24006ccd (Zuchtname) + ea803c3d (ueberall 'Kuke'); zuvor e0a0c304 / a50116bf / f89e95ad",
|
||||
"externalRef": "decision-kruke",
|
||||
"dateOfDeath": "06.04.2011",
|
||||
"originBreeder": "Marion Knoss",
|
||||
"renameTo": "Kuke von Marion Knoss",
|
||||
"mergeExternalRefs": [["decision-kruke", "stammbaum-kukemarionknoss-18092009"]]
|
||||
},
|
||||
|
||||
(mergeExternalRefs wird aus `resolutions` gelesen — merge_and_resolve.py Z.2760; Match per ExternalRef-Suffix (endswith), keep = pair[0].)
|
||||
|
||||
B) CODE — tools/import/merge_and_resolve.py: finaler Notiz-Sweep direkt VOR `# 6. Save final output JSON payload` (aktuell ~Z.4462, hinter der Herkunft-Normalisierung) einfügen:
|
||||
|
||||
# ── Umbenennungs-Sweep in Freitext-Notizen (Tickets ea803c3d/24006ccd, Kruke→Kuke) ──
|
||||
# `renameTo` korrigiert nur das Namensfeld; die aus der Wurfchronik übernommenen
|
||||
# Wurf-Notizen ("Blacky + Kruke v. 21.09.2010; WS=4") tragen den alten Namen weiter,
|
||||
# und die Züchterin sieht ihn dort. Finaler Anzeige-Sweep NACH aller Logik (Notes
|
||||
# werden vorher fürs Dedup-Scoring gelesen): Quell-Name → Rufname des neuen Namens.
|
||||
_rename_pairs = []
|
||||
for g in resolved_gerbils:
|
||||
_old = (g.get("_pre_rename_name") or "").strip()
|
||||
_new = (g.get("Name") or "").strip()
|
||||
if _old and _new and _old != _new:
|
||||
_new_call = get_call_name(_new) or _new
|
||||
if normalize_name(_old) != normalize_name(_new_call):
|
||||
_rename_pairs.append((_old, _new_call))
|
||||
_notes_fixed = 0
|
||||
for _old, _new_call in _rename_pairs:
|
||||
_rx = re.compile(r"\b" + re.escape(_old) + r"\b")
|
||||
for _rec in list(resolved_litters) + list(resolved_gerbils):
|
||||
_val = _rec.get("Notes")
|
||||
if _val and _rx.search(_val):
|
||||
_rec["Notes"] = _rx.sub(_new_call, _val)
|
||||
_notes_fixed += 1
|
||||
if _notes_fixed:
|
||||
print(f"renameTo-Notiz-Sweep: {_notes_fixed} Notiz(en) auf den korrigierten Namen gesetzt")
|
||||
|
||||
Erwartetes Ergebnis: „Blacky + Kuke v. 21.09.2010; WS=4" usw. (Rufname, nicht der volle Zuchtname — Notizen bleiben lesbar).
|
||||
|
||||
C) TEST — tools/import/test_merge_resolve.py: Regressionstest, dass (i) ein Gerbil mit `_pre_rename_name` den alten Namen nicht mehr in Litter.Notes/Gerbil.Notes stehen lässt, (ii) wortgenau ersetzt wird (kein Treffer in „Krukelinde"/Teilwörtern), (iii) der Rufname (nicht das Zucht-Suffix) eingesetzt wird.
|
||||
|
||||
D) NACH dem Ingest verifizieren (prod, read-only): GET /api/gerbils/e4898ccc-1896-5258-b1a9-4c89d30cf428 → name „Kuke von Marion Knoss", originContactId 84628989-4725-54c5-a113-4e5ad03dc1e2; GET /api/gerbils?filter=name=*Kuke → nur noch EIN Treffer; die 5 Würfe (f0b1fd6f, 1eb3e1fb, 53b55c8c, 10ddf880, f4b2b9be) haben motherId=e4898ccc UND Notizen ohne „Kruke".
|
||||
|
||||
E) NEBENWIRKUNG (bitte an Ticket f89e95ad weiterreichen, NICHT hier entscheiden): Der Merge füllt Genotype („aa CC DD Ee Uwuw[d] Pp Spsp"; Uw/uw[d] ist laut genotype.py/genotype.ts ein Alias von G/g → aa CC DD Ee Gg Pp Spsp) und ColorVarietyId (…0007 „Schwarz") aus dem Chart. Positiv: der Datenblocker von f89e95ad (Mutter-Genotyp null → keine erwarteten Farbschläge im A-Wurf) fällt weg. Aber rpro3 sagt „Anthrazit-Schecke / aa CC DD Ee gg Pp Spsp" (G-Locus gg statt Gg), und „Schwarz" passt nicht zu Ee/Spsp. ACHTUNG Ordering: `genotype`/`farbschlag` aus conflict-decisions greifen nur bei LEEREM Feld (merge_and_resolve.py ~Z.3302-3308) — nach dem Merge also No-Op. Wenn f89e95ad einen Wert festnageln soll, muss dieser Override gewinnen dürfen (bzw. der Merge-Gap-Fill die per Decision gesetzten Felder überspringen).
|
||||
|
||||
**Changelog-Entwurf für die Züchterin:** Kuke heißt jetzt überall richtig — und mit vollem Namen: „Kuke von Marion Knoss", genau wie in deinen Stammbäumen. Zwei Dinge waren schuld: (1) Kuke war noch zweimal angelegt — einmal als dein Zuchttier (ohne Zuchtnamen) und einmal als Ahnin mit dem vollen Namen aus den Stammbäumen. Die beiden sind jetzt zu einem Tier zusammengeführt; dabei bleiben ihre Würfe mit Blacky und ihre Verknüpfungen erhalten, und ihre Herkunft (Marion Knoss) hängt jetzt direkt am Tier. (2) In fünf Wurf-Notizen stand noch der alte Schreibfehler „Kruke" (B-, D-, E-, F- und I-Wurf 2010) — dort steht jetzt „Kuke". Damit das nie wieder passiert, übernimmt der Import eine Namenskorrektur ab sofort automatisch auch in alle Notizen. Ihre Farbe und ihr Erbbild klären wir noch im separaten Punkt zum A-Wurf (Anthrazit-Schecke bzw. Saphir/Platin).
|
||||
|
||||
**Betroffen:** tools/import/conflict-decisions.json (resolutions[114] 'Kruke'); tools/import/merge_and_resolve.py (Notiz-Sweep vor dem Payload-Dump; mergeExternalRefs); tools/import/test_merge_resolve.py (Regressionstest); Prod-Daten: Gerbil e4898ccc + Dublette 6d9f9d66, Würfe f0b1fd6f/1eb3e1fb/53b55c8c/10ddf880/f4b2b9be; Folgeticket f89e95ad (Genotyp/Farbschlag der Mutter)
|
||||
|
||||
---
|
||||
|
||||
@@ -179,7 +179,15 @@ export function seedDb(): MockDb {
|
||||
// Statistik: Verstorbene + Abgegebene für Verluste/Bestandskurve
|
||||
{ ...gerbil('willi', 'Willi', 'male', '2019-09-09', null, 'cv-schwarz'), status: 'Deceased', dateOfDeath: '2022-04-04' },
|
||||
{ ...gerbil('pauli', 'Pauli', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-huber' },
|
||||
{ ...gerbil('rosa', 'Rosa', 'female', '2020-02-02', null, 'cv-gold'), status: 'Deceased', dateOfDeath: '2023-08-15' },
|
||||
// Ticket 24522f5f (JackJack): abgegeben UND später beim Abnehmer verstorben —
|
||||
// Status Deceased, aber Abnehmer + Abgabedatum gesetzt. Die Akte muss beides zeigen.
|
||||
{
|
||||
...gerbil('rosa', 'Rosa', 'female', '2020-02-02', null, 'cv-gold'),
|
||||
status: 'Deceased',
|
||||
dateOfDeath: '2023-08-15',
|
||||
receiverContactId: 'con-huber',
|
||||
goHomeDate: '2022-01-15',
|
||||
},
|
||||
{ ...gerbil('pauli-breeder', 'Pauli Breeder', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-meier' },
|
||||
{ ...gerbil('pauli-neither', 'Pauli Neither', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-neither' },
|
||||
// ABGABE: zwei Tiere zur Abgabe im Quarantänebecken (enc-leer ist leer, kein Konflikt
|
||||
|
||||
@@ -203,6 +203,44 @@ test('Tier bearbeiten — isDeaf Tristate round-trip (FORM-FIELDS-1)', async ({
|
||||
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true')
|
||||
})
|
||||
|
||||
test('Detailseite: Zuchtpartner-Liste trennt mit Komma HINTEN am Namen (KV-COMMA)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessMock()
|
||||
// Anton ist Vater von Wurf B (Mutter Greta) und Wurf C (Mutter Frieda) → 2 Zuchtpartner.
|
||||
// .ak-kv-value ist ein Flex-Container mit wrap: ein vorangestelltes „, " landete beim
|
||||
// Umbruch am Zeilenanfang („, Frieda"). Das Komma muss am vorherigen Namen kleben.
|
||||
await page.goto('/rennmaeuse/anton')
|
||||
const row = page.locator('.ak-kv', {
|
||||
has: page.locator('.ak-kv-label', { hasText: de.pages.gerbils.detail.zuchtpartner }),
|
||||
})
|
||||
await expect(row).toBeVisible()
|
||||
const value = row.locator('.ak-kv-value')
|
||||
const text = (await value.innerText()).replace(/\s+/g, ' ').trim()
|
||||
expect(text).not.toMatch(/^,/)
|
||||
expect(text).toBe('Greta, Frieda')
|
||||
})
|
||||
|
||||
test('Detailseite: Abgabedatum steht direkt unter dem Abnehmer, auch bei Verstorbenen (GOHOME-ROW)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessMock()
|
||||
// Ticket 24522f5f (JackJack): Rosa wurde abgegeben und starb später beim Abnehmer
|
||||
// (Status „Verstorben", aber Abnehmer + Abgabedatum gesetzt). Früher hing die
|
||||
// Abgabedatum-Zeile am Status „Abgegeben" → sie fehlte genau in diesem Fall.
|
||||
await page.goto('/rennmaeuse/rosa')
|
||||
const receiverRow = page.locator('.ak-kv', {
|
||||
has: page.locator('.ak-kv-label', { hasText: t.fields.receiver }),
|
||||
})
|
||||
await expect(receiverRow).toBeVisible()
|
||||
await expect(receiverRow.locator('.ak-kv-value')).toContainText('Familie Huber')
|
||||
|
||||
// Abgabedatum ist die direkt folgende Zeile (Geschwister-Selektor).
|
||||
const goHomeRow = receiverRow.locator('xpath=following-sibling::*[1]')
|
||||
await expect(goHomeRow.locator('.ak-kv-label')).toHaveText(t.fields.goHomeDate)
|
||||
await expect(goHomeRow.locator('.ak-kv-value')).toHaveText('15.01.2022')
|
||||
})
|
||||
|
||||
/** QOL-ELTERN: Vater/Mutter im Abstammungs-Block des Tier-Formulars setzen. */
|
||||
async function pickParent(page: Page, legend: string, name: string) {
|
||||
const group = page.getByRole('group', { name: legend })
|
||||
|
||||
@@ -913,3 +913,68 @@ describe('GEN-5: parent inference fills unknown alleles (cc9ea3fe / 1a508c04)',
|
||||
expect(res.inferred).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-6: C-Locus-Zygotie Platin / Saphir / Platin-Hell (bde4ec70 / f89e95ad)', () => {
|
||||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||||
|
||||
// Züchterin autoritativ (Ticket f89e95ad): „Saphir ist aa Cc[chm] D- E- G- pp und
|
||||
// Platin ist aa CC D- E- G- pp. Es ist nicht dasselbe!" Vorher trugen Platin,
|
||||
// Saphir und Platin-Hell identische Katalog-Tokens (C:'C') → first-match-wins
|
||||
// lieferte immer Platin.
|
||||
it('aa CC DD EE GG pp -> Platin (zwei volle C)', () => {
|
||||
expect(name('aa CC DD EE GG pp spsp rere')).toBe('Platin')
|
||||
expect(name('aa CC DD EE GG pp Spsp')).toBe('Platin Schecke')
|
||||
})
|
||||
|
||||
it('aa Cc[chm] DD EE GG pp -> Saphir (ein c[chm] daneben)', () => {
|
||||
expect(name('aa Cc[chm] DD EE GG pp spsp rere')).toBe('Saphir')
|
||||
})
|
||||
|
||||
it('JackJack (b6d8b3ef): aa Cc[chm] DD E- G- pp Spsp -> Saphir Schecke, NICHT Platin Schecke', () => {
|
||||
expect(name('aa Cc[chm] DD E- G- pp Spsp')).toBe('Saphir Schecke')
|
||||
})
|
||||
|
||||
it('aa Cc[h] DD EE GG pp -> Platin-Hell (GEN-3g: „-Hell" == c[h]-Allel)', () => {
|
||||
expect(name('aa Cc[h] DD EE GG pp spsp rere')).toBe('Platin-Hell')
|
||||
})
|
||||
|
||||
it('die drei Katalog-Einträge bilden ihren eigenen Namen zurück (kein Synonym-Kollaps)', () => {
|
||||
for (const varietyName of ['Platin', 'Saphir', 'Platin-Hell']) {
|
||||
const entry = BASE_COLORS.find((e) => e.name === varietyName)!
|
||||
expect(genotypeToFarbschlag(representativeGenotype(entry))).toBe(varietyName)
|
||||
}
|
||||
})
|
||||
|
||||
it('Seed-Kompaktnotation „aa Ccchm …" (so schreibt der Import JackSack) -> Saphir Schecke', () => {
|
||||
// merge_and_resolve übernimmt bei Farb-LABEL „Saphir-Sp" ohne Gencode den
|
||||
// CanonicalGenotype aus colorVarietySeed.backend.json (frozen symbols).
|
||||
// Seit GEN-6 ist das die HET-Form — vorher „aa CC …" (= Platin-Gencode).
|
||||
expect(name('aa Ccchm DD EE GG pp Spsp rere')).toBe('Saphir Schecke')
|
||||
expect(name('aa Cch DD EE GG pp spsp rere')).toBe('Platin-Hell')
|
||||
})
|
||||
|
||||
it('A-Wurf 18.02.2010 (Blacky × Kuke) erwartet Saphir UND Saphir Schecke', () => {
|
||||
const blacky = fromDisplayString('aa Cc[chm] DD EE GG Pp spsp') // Vater
|
||||
const kuke = fromDisplayString('aa CC DD Ee gg Pp Spsp') // Mutter (rpro3: Anthrazit-Schecke)
|
||||
const names = breed(blacky, kuke).byFarbschlag.map((f) => f.farbschlag)
|
||||
expect(names).toContain('Saphir')
|
||||
expect(names).toContain('Saphir Schecke')
|
||||
expect(names).not.toContain('Unbekannter Farbschlag')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Schreibfehler-Alias c[hm] == c[chm] (Ticket 2322c2a8, Jay *24.10.2021)', () => {
|
||||
it('c[hm] parst als c[chm] und ergibt Zobel, nicht Zobel-Hell', () => {
|
||||
// Die Quell-Charts (Alberto Kids / Pukas Kids) schreiben „aa c[chm]c[hm] …" —
|
||||
// dem zweiten Allel fehlt das c. Ohne Alias war der Genotyp unparsebar bzw.
|
||||
// (im Python-Mirror) ein fremdes Allel → „Zobel-Hell".
|
||||
const g = fromDisplayString('aa c[chm]c[hm] Dd Ee gg P- spsp')
|
||||
expect(g.C).toEqual(['cchm', 'cchm'])
|
||||
expect(genotypeToFarbschlag(g)).toBe('Zobel')
|
||||
})
|
||||
|
||||
it('Cc[hm] bleibt ein c[chm]-Träger (relevant für die Probeverpaarung)', () => {
|
||||
expect(fromDisplayString('aa Cc[hm] DD EE GG pp spsp').C).toEqual(['C', 'cchm'])
|
||||
expect(genotypeToFarbschlag(fromDisplayString('aa Cc[hm] DD EE GG pp spsp'))).toBe('Saphir')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Algierfuchs', english: 'Dark-Eyed Honey', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs.jpg' },
|
||||
{ name: 'Blau', english: 'Blue', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'blau-schwarz-dd.JPG' },
|
||||
{ name: 'Gold', english: 'Argente Golden', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'gold.jpg' },
|
||||
{ name: 'Platin', english: 'Lilac', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin.JPG' },
|
||||
{ name: 'Platin', english: 'Lilac', tokens: { A: 'a', C: 'C/C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin.JPG' },
|
||||
{ name: 'Goldfuchs', english: 'Yellow Fox', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' }, image: 'goldfuchs.jpg' },
|
||||
{ name: 'Rotfuchs', english: 'Argente Nutmeg', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' }, image: 'rotfuchs.JPG' },
|
||||
{ name: 'Dilute Gold', english: 'dd Argente Golden', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'gold-dd.jpg' },
|
||||
@@ -78,12 +78,12 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Elfenbein', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'g', P: 'p' }, image: 'elfenbein.jpg' },
|
||||
{ name: 'Kohlfuchs', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs.jpg' },
|
||||
{ name: 'Polarfuchs', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs.jpg' },
|
||||
{ name: 'Saphir', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },
|
||||
{ name: 'Saphir', tokens: { A: 'a', C: 'C/cchm', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },
|
||||
// GEN-3a: efef base (agouti, wild C/D/G/P) = Orangeschimmel (breeder C5).
|
||||
// GEN-4: A:'A' added — non-agouti ef animals fall through to Kohlfuchsschimmel etc.
|
||||
{ name: 'Orangeschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' },
|
||||
{ name: 'Topas', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'topas.jpg' },
|
||||
{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },
|
||||
{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C/ch', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },
|
||||
{ name: 'Dilute Agouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' },
|
||||
{ name: 'Dilute Silberagouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-dd.jpg' },
|
||||
{ name: 'Dilute Kohlfuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-dd.jpg' },
|
||||
@@ -182,10 +182,33 @@ function locusToken(g: Genotype, locus: LocusKey): string {
|
||||
return dominantAllele(locus, x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* GEN-6 (Tickets bde4ec70 / f89e95ad): ein Katalog-Token darf ein EXAKTES,
|
||||
* ungeordnetes Allelpaar "x/y" verlangen statt des exprimierten (dominanten)
|
||||
* Allels. Nötig, weil sich Platin / Saphir / Platin-Hell NUR in der C-Zygotie
|
||||
* unterscheiden — die Züchterin autoritativ: "Saphir ist aa Cc[chm] D- E- G- pp
|
||||
* und Platin ist aa CC D- E- G- pp. Es ist nicht dasselbe!":
|
||||
* aa CC D- E- G- pp -> Platin
|
||||
* aa C/c[chm] D- E- G- pp -> Saphir
|
||||
* aa C/c[h] D- E- G- pp -> Platin-Hell (GEN-3g: "-Hell" == c[h]-Allel)
|
||||
* Vorher trugen die drei Einträge identische Tokens (C:'C'), weil locusToken()
|
||||
* den C-Locus auf das dominante Allel reduziert → "first match wins" lieferte
|
||||
* immer Platin, Saphir/Platin-Hell waren unerreichbar. Die "x/y"-Konvention
|
||||
* existierte schon für die het-Colourpoints (Siam/Zobel-Hell, C:'cchm/ch') und
|
||||
* wird von representativeGenotype() bereits verstanden (GEN-3f) — deshalb bleibt
|
||||
* die Katalog-REIHENFOLGE (und damit sortOrder + ColorVariety-GUIDs) unberührt.
|
||||
*/
|
||||
function pairMatches(g: Genotype, locus: LocusKey, token: string): boolean {
|
||||
const [a, b] = resolvedPair(g, locus)
|
||||
const [x, y] = token.split('/') as [string, string]
|
||||
return (a === x && b === y) || (a === y && b === x)
|
||||
}
|
||||
|
||||
function matches(g: Genotype, entry: FarbschlagEntry): boolean {
|
||||
return (Object.keys(entry.tokens) as LocusKey[]).every(
|
||||
(locus) => locusToken(g, locus) === entry.tokens[locus],
|
||||
)
|
||||
return (Object.keys(entry.tokens) as LocusKey[]).every((locus) => {
|
||||
const token = entry.tokens[locus]!
|
||||
return token.includes('/') ? pairMatches(g, locus, token) : locusToken(g, locus) === token
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Saphir",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"canonicalGenotype": "aa Ccchm DD EE GG pp spsp rere",
|
||||
"sortOrder": 24,
|
||||
"image": "saphir.jpg"
|
||||
},
|
||||
@@ -180,7 +180,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Platin-Hell",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"canonicalGenotype": "aa Cch DD EE GG pp spsp rere",
|
||||
"sortOrder": 27,
|
||||
"image": "platin-hell.jpg"
|
||||
},
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Saphir",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp",
|
||||
"canonicalGenotype": "aa Cc[chm] DD EE GG pp spsp",
|
||||
"sortOrder": 24,
|
||||
"image": "saphir.jpg"
|
||||
},
|
||||
@@ -180,7 +180,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Platin-Hell",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp",
|
||||
"canonicalGenotype": "aa Cc[h] DD EE GG pp spsp",
|
||||
"sortOrder": 27,
|
||||
"image": "platin-hell.jpg"
|
||||
},
|
||||
|
||||
@@ -220,6 +220,11 @@ function normalizeToken(tok: string): string | null {
|
||||
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
|
||||
// Ticket 2322c2a8: 'c[hm]' ist ein verschriebenes 'c[chm]' (fehlendes c) — so
|
||||
// steht es in den Quell-Charts (Alberto Kids / Pukas Kids). Ohne dieses Alias
|
||||
// wirft splitToken auf dem Token und der Genotyp ist in UI/Probeverpaarung
|
||||
// unbrauchbar; mit Alias rechnet der TS-Mirror wie tools/import/genotype.py.
|
||||
t = t.replace(/c\[hm\]/g, 'cchm') // Schreibfehler-Alias → Colourpoint-Allel
|
||||
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): a visible Fuchs is RECESSIVE → MUST be homozygous "ee". The herdbook
|
||||
|
||||
@@ -490,9 +490,6 @@ export default function GerbilDetailPage() {
|
||||
<Kv label={t.fields.causeOfDeath}>{g.causeOfDeath || '—'}</Kv>
|
||||
</>
|
||||
)}
|
||||
{g.status === 'GivenAway' && (
|
||||
<Kv label={t.fields.goHomeDate}>{formatDate(g.goHomeDate)}</Kv>
|
||||
)}
|
||||
<Kv label={t.fields.colorVariety}>
|
||||
{storedColorName ? (
|
||||
<>
|
||||
@@ -549,15 +546,24 @@ export default function GerbilDetailPage() {
|
||||
g.originBreeder || '—'
|
||||
)}
|
||||
</Kv>
|
||||
{(g.receiverContactId || g.status !== 'Deceased') && (
|
||||
<Kv label={t.fields.receiver}>{lookup(contactName, g.receiverContactId)}</Kv>
|
||||
{/* Ticket 24522f5f (JackJack): Das Abgabedatum gehört direkt unter den Abnehmer und
|
||||
darf NICHT am Status hängen — Tiere, die abgegeben wurden und später beim
|
||||
Abnehmer starben (status=Deceased), haben Abnehmer UND Abgabedatum. */}
|
||||
{(g.receiverContactId || g.goHomeDate || g.status !== 'Deceased') && (
|
||||
<>
|
||||
<Kv label={t.fields.receiver}>{lookup(contactName, g.receiverContactId)}</Kv>
|
||||
<Kv label={t.fields.goHomeDate}>{formatDate(g.goHomeDate)}</Kv>
|
||||
</>
|
||||
)}
|
||||
{g.isResident !== false && (partners.data?.items.length ?? 0) > 0 && (
|
||||
<Kv label={t.detail.zuchtpartner}>
|
||||
{(partners.data?.items ?? []).map((p, i) => (
|
||||
{/* Trennkomma HINTEN an den Namen, nicht vorne an den nächsten: .ak-kv-value
|
||||
ist ein Flex-Container mit wrap, jeder Eintrag also ein eigenes Flex-Item.
|
||||
Vorangestellt landete das Komma beim Umbruch am ZEILENANFANG („, Zora"). */}
|
||||
{(partners.data?.items ?? []).map((p, i, all) => (
|
||||
<span key={p.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link to={`/rennmaeuse/${p.id}`}>{p.name || de.pages.gerbils.nameless}</Link>
|
||||
{i < all.length - 1 && ','}
|
||||
</span>
|
||||
))}
|
||||
</Kv>
|
||||
@@ -612,12 +618,12 @@ export default function GerbilDetailPage() {
|
||||
{(offspring.data?.items.length ?? 0) > 0 && (
|
||||
<p className="ak-empty">
|
||||
{t.detail.nonResidentOffspringLabel}{' '}
|
||||
{(offspring.data?.items ?? []).map((child, i) => (
|
||||
{(offspring.data?.items ?? []).map((child, i, all) => (
|
||||
<span key={child.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link to={`/rennmaeuse/${child.id}`}>
|
||||
{child.name || de.pages.gerbils.nameless}
|
||||
</Link>
|
||||
{i < all.length - 1 && ','}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
|
||||
@@ -61,7 +61,10 @@ export default function WuerfeListPage() {
|
||||
|
||||
// Parent names (shared by both tabs) — keep the full gerbil so the Zucht suffix applies.
|
||||
const gerbilName = useGerbilName()
|
||||
const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), [])
|
||||
// Alle Tiere laden (nicht nur die ersten 1000): sonst finden Würfe, deren Eltern
|
||||
// alphabetisch hinter dem Fenster liegen, ihren Namen nicht und die Karte zeigt
|
||||
// fälschlich „Unbekannt" (Prod hat >2400 Tiere).
|
||||
const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 10000, orderBy: 'name' }), [])
|
||||
const gerbilById = useMemo(() => {
|
||||
const map = new Map<string, Gerbil>()
|
||||
for (const g of gerbils.data?.items ?? []) map.set(g.id, g)
|
||||
|
||||
@@ -345,11 +345,19 @@
|
||||
"decision": "father is Danny von Privatzucht Maintal (Wurfchronik J-Wurf: 'Eltern: Danny + Jana'). Chart-position had wrong/age-impossible parents.",
|
||||
"father": "Danny von PZ Maintal",
|
||||
"mother": "Jana of little longnoses",
|
||||
"source": "Züchterin 2026-06-22 — Ticket #31 + Tickets 06b6734d/e96db323/c092f8d2 (Merge Stub)",
|
||||
"source": "Züchterin 2026-06-22 — Ticket #31 + Tickets 06b6734d/e96db323/c092f8d2 (Merge Stub) + Tickets 66ef9bdd/3bbd6ab4 (restliche Wurfchronik-Elternstubs page_0017/page_0023)",
|
||||
"mergeExternalRefs": [
|
||||
[
|
||||
"stammbaum-jamiekleinenchaoten-27122010",
|
||||
"Wurfchronik Teil 1_page_0022.md-page_0022_parent_jamie"
|
||||
],
|
||||
[
|
||||
"stammbaum-jamiekleinenchaoten-27122010",
|
||||
"Wurfchronik Teil 1_page_0017.md-f651e1cf-54bc-4d8b-9be0-4b6298e7ab48"
|
||||
],
|
||||
[
|
||||
"stammbaum-jamiekleinenchaoten-27122010",
|
||||
"Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e14"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -983,11 +991,19 @@
|
||||
{
|
||||
"name": "Kruke",
|
||||
"isResident": true,
|
||||
"decision": "'Kruke' war ein Wurfchronik-Tippfehler; korrekt 'Kuke' (rpro3). Bestandstier. Todesdatum 06.04.2011 (Ausbruch-Unfall, rpro3). Farbe/Genotyp (Anthrazit-Schecke vs. Schwarz) folgt separat via Ticket f89e95ad, sobald die Zuechterin Saphir/Platin geklaert hat. KEIN Merge mit dem Ahnen-Record 'Kuke von Marion Knoss' — der Merge nullte die Mutter-Verknuepfung aller Blacky-x-Kuke-Wuerfe.",
|
||||
"source": "KI-Triage 2026-07-12 — Ticket 34220307-23bc-420c-80f8-d273615caffd + b7016e5f-9985-4f17-a3f6-2048289958aa + Tickets e0a0c304/a50116bf",
|
||||
"decision": "'Kruke' war ein Wurfchronik-Tippfehler; korrekt 'Kuke von Marion Knoss' (so heisst sie in ihren eigenen Stammbaeumen Danako/Hana/Jin/Unit/Uriana/Wildfire; rpro3-Herkunft 'M.Knoss GG', Eltern Marc x Sara). Bestandstier, Todesdatum 06.04.2011 (Ausbruch-Unfall, rpro3). Der Chart-Ahnenrecord wird jetzt HIER hineingemergt (keep = decision-kruke): der Keeper behaelt bis zum Parent-Resolver den Quellnamen 'Kruke', renameTo setzt danach _pre_rename_name (Namensindex) und die Chart-Wuerfe des gedroppten Zwillings werden ueber _premerged_ids umgebogen -> die Mutter-Verknuepfung der Blacky-x-Kuke-Wuerfe bleibt erhalten (das war 2026-07-12 der Grund, den Merge auszulassen). Gencode/Farbschlag kommen bewusst AUS DEM MERGE (ihre eigenen Stammbaum-Charts: 'aa CC DD Ee Uwuw[d] Pp Spsp' = aa CC DD Ee Gg Pp Spsp), NICHT als rpro3-Override 'aa CC DD Ee gg Pp Spsp'/Anthrazit: die beiden Quellen widersprechen sich im G-Locus und in der Farbe, und die Stammbaum-Charts sind in diesem Projekt die primaere Quelle. Fuer Ticket f89e95ad zaehlt nur, dass Kuke ueberhaupt einen Gencode hat — den liefert der Merge.",
|
||||
"source": "KI-Triage 2026-08-19 - Tickets 24006ccd (Zuchtname) + ea803c3d (ueberall 'Kuke') + f89e95ad (Gencode der A-Wurf-Mutter); zuvor e0a0c304 / a50116bf / 34220307 / b7016e5f",
|
||||
"externalRef": "decision-kruke",
|
||||
"dateOfDeath": "06.04.2011",
|
||||
"renameTo": "Kuke"
|
||||
"deceased": true,
|
||||
"originBreeder": "Marion Knoss",
|
||||
"renameTo": "Kuke von Marion Knoss",
|
||||
"mergeExternalRefs": [
|
||||
[
|
||||
"decision-kruke",
|
||||
"stammbaum-kukemarionknoss-18092009"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Genna",
|
||||
@@ -1087,6 +1103,7 @@
|
||||
{
|
||||
"name": "JackSack",
|
||||
"dob": "18.02.2010",
|
||||
"externalRef": "Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000002",
|
||||
"mergeExternalRefs": [
|
||||
[
|
||||
"Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000002",
|
||||
@@ -1094,8 +1111,9 @@
|
||||
]
|
||||
],
|
||||
"renameTo": "JackJack",
|
||||
"decision": "Dublette JackSack/JackJack = dasselbe Tier (A-Wurf, *18.02.2010). Zusammenfuehren, richtiger Name JackJack.",
|
||||
"source": "Zuechterin - Ticket 0cf6b838"
|
||||
"decision": "Dublette JackSack/JackJack = dasselbe Tier (A-Wurf, *18.02.2010). Zusammenfuehren, richtiger Name JackJack. Abgabedatum aus der Wurfchronik-Notiz '-> Larissa Draeger + Nicolas 13.5.10' = 13.05.2010 (die Transkription baut daraus faelschlich '1310-05-13'). Adressiert ueber die ExternalRef, weil Wurfchronik-Jungtiere ihr Geburtsdatum erst NACH dem Override-Block aus dem Wurfdatum erhalten — der (name,dob)-Key greift dort nie (deshalb feuerte auch renameTo bisher nicht).",
|
||||
"source": "Zuechterin - Tickets 0cf6b838, 24522f5f",
|
||||
"goHomeDate": "13.05.2010"
|
||||
},
|
||||
{
|
||||
"name": "Montana v.d. Kleinen Chaoten",
|
||||
@@ -1199,14 +1217,6 @@
|
||||
"mother": "Scarlett of Ulmer Strolche",
|
||||
"motherDob": "05.09.2012"
|
||||
},
|
||||
{
|
||||
"name": "Merle",
|
||||
"originBreeder": "Clan of Black Forest",
|
||||
"source": "Ticket a8f11ac0 + Ticket 00715df9",
|
||||
"externalRef": "decision-merle",
|
||||
"genotype": "Aa c[chm]c[chm] D- ee GG P- Spsp",
|
||||
"decision": "Gencode aus Merles eigenem Ticket a8f11ac0. Eltern Akane x Bonaparte bereits über addLitter verknüpft. Geburtsdatum in keiner lesbaren Quelle -> offen."
|
||||
},
|
||||
{
|
||||
"name": "Gino",
|
||||
"dob": "25.10.2010",
|
||||
@@ -1273,8 +1283,10 @@
|
||||
"externalRef": "Wurfchronik Teil 1_page_0013.md-g1000000-0000-0000-0000-000000000011",
|
||||
"isResident": false,
|
||||
"notes": "Schwarz-Schecke; Tochter von Blacky x Kuke (E-Wurf 2010); verstorben 06.04.2011 (Ausbruch-Unfall). War nie Zuchttier. NICHT die Mutter des J5-Wurfs — das ist die spätere Eliza (*13.02.2014).",
|
||||
"decision": "Zweite/2010er Eliza von der J5-Mutter getrennt; irreführende 'Mutter des J5-Wurfs'-Notiz entfernt.",
|
||||
"source": "Ticket aa5238b6"
|
||||
"decision": "Zweite/2010er Eliza von der J5-Mutter getrennt; irreführende 'Mutter des J5-Wurfs'-Notiz entfernt. Gencode + Scheckungsart von der Züchterin: aa C- DD E- G- P- Spsp, Ansatzschecke (der bisherige Wert war nur Kanon-Fill aus dem Farbschlag 'Schwarz-Sp').",
|
||||
"source": "Ticket aa5238b6 + Ticket a547be62",
|
||||
"genotype": "aa C- DD E- G- P- Spsp",
|
||||
"spottingType": "Ansatzschecke"
|
||||
},
|
||||
{
|
||||
"externalRef": "stammbaum-gwengensakuralittleangels-17122011",
|
||||
@@ -1282,12 +1294,16 @@
|
||||
[
|
||||
"gwengensakuralittleangels-17122011",
|
||||
"page_0022_parent_sakura"
|
||||
],
|
||||
[
|
||||
"gwengensakuralittleangels-17122011",
|
||||
"Wurfchronik Teil 1_page_0023.md-7cbb62c5-7a46-4cb4-a3ad-bf7f94080e15"
|
||||
]
|
||||
],
|
||||
"originBreeder": "Clan of little Angels",
|
||||
"isResident": true,
|
||||
"decision": "Sakura (Wurfchronik-Elternteil A2/D2/H2, ohne DOB) und 'Gwen gen. Sakura of little Angels' (*17.12.2011, +05.01.2015, Eltern Chap of little Angels x Julietta gen. Becky of LennyLengo) sind dasselbe Tier. Gwen-Record kanonisch, Wurfchronik-Sakura einmergen. Herkunft Clan of little Angels.",
|
||||
"source": "Ticket 7f9b3724"
|
||||
"source": "Ticket 7f9b3724 + Ticket 6c9539d2 (zweiter Wurfchronik-Stub page_0023)"
|
||||
},
|
||||
{
|
||||
"name": "Marty v.d. Kleinen Chaoten",
|
||||
@@ -1322,9 +1338,49 @@
|
||||
{
|
||||
"name": "Merle",
|
||||
"dob": "18.06.2023",
|
||||
"externalRef": "stammbaum-merle-18062023",
|
||||
"isResident": true,
|
||||
"decision": "Merle (Tochter Akane × Bonaparte, bei Clan of Black Forest geboren) ist Zuchttier. Das Geburtsdatum 18.06.2023 kommt aus „Stammbaum von Pukas Kids“ — seit der addAnimals-Stub mit dem Chart-Datensatz verschmilzt, braucht die Residenz einen expliziten Override (der isResident-Sweep würde sie sonst auf false setzen).",
|
||||
"source": "Ticket 36a3fcde + Ticket a8f11ac0 (Züchterin: Merle = Zuchttier)"
|
||||
"originBreeder": "Zucht der Kleinen Chaoten",
|
||||
"receiver": "Swen Pulinckx",
|
||||
"goHomeDate": "22.12.2024",
|
||||
"notes": "Tochter von Akane von den Kleinen Chaoten × Bonaparte von den Schlossmäusen, geboren bei Clan of Black Forest (Akane war dort nur im Zuchttier-Austausch). Herkunft laut Züchterin: Zucht der Kleinen Chaoten. Am 01.04.2024 zurück zu den Kleinen Chaoten (Gewicht 62,1 g), Zuchttier mit Smoke (Würfe 12.07.2024 und 19.08.2024). Am 22.12.2024 zusammen mit ihrer Tochter Speedy an Swen Pulinckx abgegeben und dort in „Schneewittchen“ umbenannt. Weitere Daten siehe Stammbaum Pukas Kids.",
|
||||
"decision": "Ticket 7037f5d8: Herkunft = Zucht der Kleinen Chaoten (Züchterin-Angabe, ersetzt die frühere Angabe „Clan of Black Forest“). Merle bleibt Zuchttier (isResident=true, eigener Wurf mit Smoke in der Wurfchronik), ist aber seit 22.12.2024 abgegeben → Status GivenAway + Abnehmer + Abgabedatum, damit sie nicht mehr in der Bestandsansicht (isResident && status=Breeding) auftaucht. Konsolidiert die alte name-only-Decision, die nach dem Chart-Import auf „Merle of Samsimar“ geleakt hat.",
|
||||
"source": "Ticket 7037f5d8 (Züchterin 2026-07-31) + Ticket a8f11ac0 + Ticket 00715df9 + Ticket 36a3fcde"
|
||||
},
|
||||
{
|
||||
"name": "Jay",
|
||||
"dob": "24.10.2021",
|
||||
"decision": "C-Locus = c[chm]c[chm] — die Quell-Charts schreiben 'c[hm]' (fehlendes c, Tippfehler); das Farbschlag-Label beider Charts ist 'Zobel', und c[h] ist genetisch unmoeglich, da beide Eltern kein c[h] tragen (Hagrid Rubeus Cc[chm], Arya Stark c[chm]c[chm]).",
|
||||
"genotype": "aa c[chm]c[chm] Dd Ee gg P- spsp",
|
||||
"source": "Zuechterin-Ticket 2322c2a8 (2026-08-19) — „Falsche Farbe. Er ist ein Zobel“"
|
||||
},
|
||||
{
|
||||
"name": "Kathlin von den Kleinen Chaoten",
|
||||
"dob": "20.04.2021",
|
||||
"decision": "Eltern sind Yuki von den Kleinen Chaoten (*03.02.2020, Vater) x Ella (*10.06.2019, Mutter) — so in allen 6 Stammbaum-Charts (z. B. 'Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx' K79 -> N80/N81, Grosseltern Q79 Camaro + Q80 Izumi / Q81 Louis + Q82 Roswitha). Die Chart-Position hatte die Grossmutter Izumi als Mutter gesetzt und keinen Vater (Zeilen-Offset wie im Akane-Fall 88389f8e).",
|
||||
"father": "Yuki von den Kleinen Chaoten",
|
||||
"fatherDob": "03.02.2020",
|
||||
"mother": "Ella",
|
||||
"motherDob": "10.06.2019",
|
||||
"source": "Ticket 8a51ac73 — Stammbaum-xlsx (Zuechterin verweist ausdruecklich darauf)"
|
||||
},
|
||||
{
|
||||
"name": "Fast Boy of Golden Lights",
|
||||
"dob": "31.03.2017",
|
||||
"decision": "Eltern sind Charly of Golden Lights x Ziwa of Golden Lights, NICHT Danjo (= Ziwas Vater, also der Grossvater) + Xtra Bounty (= Zuleikas Mutterlinie) — Chart-Position um eine Zeile verrutscht.",
|
||||
"father": "Charly of Golden Lights",
|
||||
"fatherDob": "05.04.2016",
|
||||
"mother": "Ziwa of Golden Lights",
|
||||
"motherDob": "29.04.2016",
|
||||
"source": "Ticket 6c89082e — RennmausPro III (_rpro3.db: Vater Charly / Mutter Ziwa) + Paarzelle R35 in 'Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx' + Q74/Q75 in 'Stammbaum von Kid von Cassy & Blaster.xlsx'"
|
||||
},
|
||||
{
|
||||
"name": "Ziwa of Golden Lights",
|
||||
"dob": "29.04.2016",
|
||||
"decision": "Vater = Danjo of Golden Lights, Mutter = Isiri of Golden Lights (rpro3); die Paarzelle R61 'Isiri of Golden Lights & Danjo of Golden Lights' in 'Stammbaum von Akio Kids.xlsx' ist rollenvertauscht. Haengt Danjo/Isiri dort ein, wo sie hingehoeren (Fast Boys Grosseltern), statt den Stammbaum bei Ziwa abzubrechen.",
|
||||
"father": "Danjo of Golden Lights",
|
||||
"mother": "Isiri of Golden Lights",
|
||||
"source": "Ticket 6c89082e — RennmausPro III (_rpro3.db)"
|
||||
}
|
||||
],
|
||||
"addAnimals": [
|
||||
@@ -1797,9 +1853,10 @@
|
||||
"Wurfchronik Teil 1_page_0054.md-6a1a1054-0002-4000-8000-000000000002",
|
||||
"Wurfchronik Teil 1_page_0054.md-6a1a1054-0003-4000-8000-000000000003",
|
||||
"stammbaum-verpaarungchevroletcamarotopolino",
|
||||
"Stammbaum-virtual-a7c4a776-e7fc-5672-a004-8aaf5534db97"
|
||||
"Stammbaum-virtual-a7c4a776-e7fc-5672-a004-8aaf5534db97",
|
||||
"Wurfchronik Teil 1_page_0009.md-90e23e20-3051-4048-b4b1-e2bc00b21736"
|
||||
],
|
||||
"_doc_suppress": "suppressExternalRefs: ExternalRefs von bestätigten Phantom-/OCR-Fehllesungs-Dubletten, die final aus dem Payload entfernt werden. Hier: 'Muga' (OCR-Fehllesung von 'Nduga' aus Wurfchronik Seite 0054, Ticket a9c0d0f9, von der Züchterin bestätigt) samt ihrem kinderlosen Phantom-Wurf 'Muga × Benjiro'. Die echten Kinder (Luna/Cleo/Izumi/Pinky) gehören bereits korrekt zum echten Wurf H13 (Nduga × Benjiro, 07.02.2018).",
|
||||
"_doc_suppress": "suppressExternalRefs: ExternalRefs von bestätigten Phantom-/OCR-Fehllesungs-Dubletten, die final aus dem Payload entfernt werden. Hier: 'Muga' (OCR-Fehllesung von 'Nduga' aus Wurfchronik Seite 0054, Ticket a9c0d0f9, von der Züchterin bestätigt) samt ihrem kinderlosen Phantom-Wurf 'Muga × Benjiro'. Die echten Kinder (Luna/Cleo/Izumi/Pinky) gehören bereits korrekt zum echten Wurf H13 (Nduga × Benjiro, 07.02.2018). Ausserdem das namenlose Q3-Jungtier von Seite 0009 (Ticket 4f890829), das fälschlich als Platzhalter-Vater „Unbekannt“ des Q4-Wurfs aufgelöst wurde — die echten Q3-Jungtiere stehen mit Namen auf Seite 0028 (Pünktchen/Quebec/Kai-Uwe).",
|
||||
"addLitters": [
|
||||
{
|
||||
"name": "Akanes Wurf (bei Clan of Black Forest)",
|
||||
@@ -2143,6 +2200,24 @@
|
||||
"mother": "Skarlett von den Kleinen Chaoten",
|
||||
"motherDob": "14.07.2013",
|
||||
"source": "Ticket 44a9b677: April-Wurf hatte fälschlich Ahnin Skarlett *2009 als Mutter"
|
||||
},
|
||||
{
|
||||
"litterName": "TS-Wurf",
|
||||
"date": "03.04.2015",
|
||||
"father": "Jack Jr. von den Kleinen Chaoten",
|
||||
"fatherDob": "19.06.2013",
|
||||
"mother": "Lila",
|
||||
"motherDob": "13.02.2014",
|
||||
"source": "Ticket 5851bb94: Wurfchronik-xlsx sheet1 Z.162 (T5-Wurf *03.04.2015, Lila x Jack Jr.) + Overview 'Wurfchronik Teil 1_page_0044.md' ('TS-Wurf (Jack Jr + Lila)'); die Seiten-Extraktion 0044 liefert fuer alle Wuerfe fatherId/motherId=null"
|
||||
},
|
||||
{
|
||||
"litterName": "Q4-Wurf",
|
||||
"date": "10.12.2014",
|
||||
"father": "Earl",
|
||||
"fatherDob": "22.04.2013",
|
||||
"mother": "Nelly",
|
||||
"motherDob": "12.07.2013",
|
||||
"source": "Ticket 4f890829: Wurfchronik-xlsx sheet1 row 131 (Earl [Kleine Chaoten] x Nelly [Kleine Chaoten]); rpro3 bestaetigt das Paar (Wade *14.01.2015). Ersetzt den faelschlich aufgeloesten Platzhalter-Vater 'Unbekannt'."
|
||||
}
|
||||
],
|
||||
"_doc_litterParents": "litterParents: erzwingt Vater/Mutter eines BESTEHENDEN Wurfs (Match litterName+date), via name+dob. Für Namensvetter-/verstorbenes-Elternteil-Korrekturen, die der Resolver nicht selbst trifft.",
|
||||
|
||||
@@ -222,6 +222,39 @@ def parse_detail(text):
|
||||
geno)
|
||||
|
||||
|
||||
# Sternloses Geburtsdatum: "22.08.2019/+09.04.2024" statt "*22.08.2019/+09.04.2024".
|
||||
# Erlaubt ist optional ein angehaengtes Todesdatum nach "/+" (auch leer, "30.01.2020/+").
|
||||
# Kein Whitespace innerhalb des Datums, damit die DOB-Regex nach dem Praefixen sicher greift.
|
||||
BARE_DOB = re.compile(r"^\s*\d{1,2}\.\d{1,2}\.\d{4}(\s*/\s*\+.*)?$")
|
||||
|
||||
|
||||
def _normalise_starless_dobs(cells):
|
||||
"""Setzt den fehlenden Stern vor sternlose Geburtsdaten in echten Bloecken.
|
||||
|
||||
WARUM (Ticket 65266679 Bijou): Einige Charts vergessen den Stern vor dem
|
||||
Geburtsdatum (Louis of Black Forest in "Alberto Kids"/"Pukas Kids", Jiminy of
|
||||
Black Forest in "Stella Kids"). Ohne Stern erkennt die Blockheuristik das Tier
|
||||
gar nicht -> das Tier fehlt im Extrakt und `_reconstruct_parents` greift in den
|
||||
Nachbar-Ast (Bijou bekam Arya Stark als Mutter statt Louis x Nani).
|
||||
|
||||
Damit nicht jede beliebige Datumszelle (Notizen, Abgabedaten) zum Tier wird,
|
||||
normalisieren wir nur MIT Block-Kontext: ein Genotyp <= 3 Zeilen darunter UND
|
||||
eine Namenszelle <= 3 Zeilen darueber -- genau die Nachbarschaft, die die
|
||||
Blockerkennung anschliessend auch auswertet.
|
||||
"""
|
||||
for (c, r), t in list(cells.items()):
|
||||
if not BARE_DOB.match(t):
|
||||
continue
|
||||
has_geno = any(gt.looks_like_genotype(cells.get((c, rr), ""))
|
||||
for rr in range(r + 1, r + 4))
|
||||
has_name = any(cells.get((c, rr))
|
||||
and not re.match(r"^\*?\s?\d", cells[(c, rr)])
|
||||
and not gt.looks_like_genotype(cells[(c, rr)])
|
||||
for rr in range(r - 3, r))
|
||||
if has_geno and has_name:
|
||||
cells[(c, r)] = "*" + t.strip()
|
||||
|
||||
|
||||
def extract_stammbaum(path):
|
||||
"""Return list of animal dicts for one chart file."""
|
||||
fname = os.path.basename(path)
|
||||
@@ -229,6 +262,7 @@ def extract_stammbaum(path):
|
||||
ss = xu.shared_strings(z)
|
||||
sheets = xu.sheet_paths(z)
|
||||
cells = xu.read_cells(z, sheets[0], ss)
|
||||
_normalise_starless_dobs(cells)
|
||||
fillsex = xu.cell_fill_sex(z, sheets[0]) # box colour -> sex (blue=male, white=female)
|
||||
|
||||
has_col2 = any(c == 2 for (c, r) in cells)
|
||||
|
||||
@@ -84,6 +84,15 @@ def _deaf_value(token):
|
||||
return None
|
||||
|
||||
|
||||
# Schreibfehler-Aliase auf Allel-Ebene (Quelldaten): 'c[hm]' ist ein verschriebenes
|
||||
# 'c[chm]' (fehlendes c) — Ticket 2322c2a8 (Jay *24.10.2021, Stammbaum von Alberto
|
||||
# Kids/Pukas Kids). Ohne Alias landet das Allel als unbekanntes 'c^hm' im C-Locus und
|
||||
# der Farbschlag kippt von Zobel auf Zobel-Hell (both_cchm=False); zusätzlich ist der
|
||||
# String im TS-Parser ungültig. Das Alias normalisiert beim Parsen, damit BEIDE Mirrors
|
||||
# identisch rechnen (genotype.ts normalizeToken macht dasselbe).
|
||||
_SUPERSCRIPT_ALIAS = {("c", "hm"): "chm"}
|
||||
|
||||
|
||||
def _alleles_for(locus, token):
|
||||
pat = _ALLELE_UNIT.get(locus)
|
||||
units = pat.findall(token) if pat else re.findall(r"[A-Za-z](?:\[[a-z]+\])?|-", token)
|
||||
@@ -94,7 +103,8 @@ def _alleles_for(locus, token):
|
||||
else:
|
||||
m = re.match(r"([A-Za-z]+)\[([a-z\-]+)\]", u)
|
||||
if m:
|
||||
alleles.append(m.group(1) if m.group(2) == "-" else f"{m.group(1)}^{m.group(2)}")
|
||||
sup = _SUPERSCRIPT_ALIAS.get((m.group(1), m.group(2)), m.group(2))
|
||||
alleles.append(m.group(1) if sup == "-" else f"{m.group(1)}^{sup}")
|
||||
else:
|
||||
alleles.append(u)
|
||||
if len(alleles) == 1:
|
||||
@@ -224,7 +234,7 @@ _BASE_COLORS = [
|
||||
("Algierfuchs", {"A": "A", "C": "C", "D": "D", "E": "e", "G": "G", "P": "P"}),
|
||||
("Blau", {"A": "a", "C": "C", "D": "d", "E": "E", "G": "G", "P": "P"}),
|
||||
("Gold", {"A": "A", "C": "C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Platin", {"A": "a", "C": "C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Platin", {"A": "a", "C": "C/C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Goldfuchs", {"A": "A", "C": "C", "D": "D", "E": "e", "G": "G", "P": "p"}),
|
||||
("Rotfuchs", {"A": "a", "C": "C", "D": "D", "E": "e", "G": "G", "P": "p"}),
|
||||
("Dilute Gold", {"A": "A", "C": "C", "D": "d", "E": "E", "G": "G", "P": "p"}),
|
||||
@@ -237,10 +247,10 @@ _BASE_COLORS = [
|
||||
("Elfenbein", {"A": "A", "C": "C", "D": "D", "E": "E", "G": "g", "P": "p"}),
|
||||
("Kohlfuchs", {"A": "a", "C": "C", "D": "D", "E": "e", "G": "G", "P": "P"}),
|
||||
("Polarfuchs", {"A": "A", "C": "C", "D": "D", "E": "e", "G": "g", "P": "P"}),
|
||||
("Saphir", {"A": "a", "C": "C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Saphir", {"A": "a", "C": "C/cchm", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Orangeschimmel", {"A": "A", "C": "C", "D": "D", "E": "ef", "G": "G", "P": "P"}),
|
||||
("Topas", {"A": "A", "C": "C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Platin-Hell", {"A": "a", "C": "C", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Platin-Hell", {"A": "a", "C": "C/ch", "D": "D", "E": "E", "G": "G", "P": "p"}),
|
||||
("Dilute Agouti", {"A": "A", "C": "C", "D": "d", "E": "E", "G": "G", "P": "P"}),
|
||||
("Dilute Silberagouti", {"A": "A", "C": "C", "D": "d", "E": "E", "G": "g", "P": "P"}),
|
||||
("Dilute Kohlfuchs", {"A": "a", "C": "C", "D": "d", "E": "e", "G": "G", "P": "P"}),
|
||||
@@ -356,8 +366,22 @@ def _entry_in_e_family(name, tokens, family):
|
||||
return "schimmel" not in n
|
||||
|
||||
|
||||
def _pair_matches(mapped, locus, token):
|
||||
"""GEN-6-Mirror (catalog.ts pairMatches): Token 'x/y' verlangt das EXAKTE,
|
||||
ungeordnete Allelpaar statt des exprimierten dominanten Allels."""
|
||||
a, b = _resolve_allele_pair(locus, mapped.get(locus, ["?", "?"]))
|
||||
x, y = token.split("/")
|
||||
return (a == x and b == y) or (a == y and b == x)
|
||||
|
||||
|
||||
def _matches(mapped, tokens):
|
||||
return all(_locus_token(mapped, locus) == tok for locus, tok in tokens.items())
|
||||
# GEN-6 (Tickets bde4ec70 / f89e95ad): Platin / Saphir / Platin-Hell unterscheiden
|
||||
# sich NUR in der C-Zygotie (aa CC / aa Cc[chm] / aa Cc[h]) — dort muss zygotie-
|
||||
# genau statt auf dem dominanten Allel gematcht werden. Mirror von catalog.ts.
|
||||
return all(
|
||||
_pair_matches(mapped, locus, tok) if "/" in tok else _locus_token(mapped, locus) == tok
|
||||
for locus, tok in tokens.items()
|
||||
)
|
||||
|
||||
|
||||
def _base_colour_for(mapped):
|
||||
|
||||
@@ -606,6 +606,11 @@ def parse_date(d):
|
||||
d = f"{year}-{parts[1]}-{parts[2]}"
|
||||
elif year == 1900:
|
||||
return None
|
||||
# Implausible Jahre verwerfen (Ticket 24522f5f): eine Wurfchronik-Transkription
|
||||
# baute aus „13.5.10" das Datum „1310-05-13" zusammen. Lieber KEIN Datum als ein
|
||||
# Datum aus dem 14. Jahrhundert (oder aus der fernen Zukunft).
|
||||
if year < 1900 or year > datetime.now().year + 1:
|
||||
return None
|
||||
return d
|
||||
# Try DD.MM.YYYY or D.M.YY
|
||||
match = re.match(r"^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$", d)
|
||||
@@ -619,6 +624,9 @@ def parse_date(d):
|
||||
year += 100
|
||||
elif year == 1900:
|
||||
return None
|
||||
# s. o. — Plausibilitäts-Guard NACH der Jahres-Normalisierung
|
||||
if year < 1900 or year > datetime.now().year + 1:
|
||||
return None
|
||||
try:
|
||||
return datetime(year, month, day).strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
@@ -632,6 +640,8 @@ def parse_date(d):
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
elif year == 1900:
|
||||
return None
|
||||
if dt.year < 1900 or dt.year > datetime.now().year + 1:
|
||||
return None
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -1389,6 +1399,244 @@ def enrich_from_contracts(contracts, resolved_gerbils, contact_by_norm_name,
|
||||
return stats, sale_contracts
|
||||
|
||||
|
||||
def apply_rename_note_sweep(resolved_gerbils, resolved_litters):
|
||||
"""Zieht ein `renameTo` in die Freitext-Notizen nach (Tickets ea803c3d/24006ccd).
|
||||
|
||||
`renameTo` korrigiert nur das Namensfeld; die aus der Wurfchronik übernommenen
|
||||
Wurf-Notizen („Blacky + Kruke v. 21.09.2010; WS=4") tragen den alten Namen weiter,
|
||||
und genau dort sieht ihn die Züchterin. Ersetzt wird wortgenau an Wortgrenzen und durch
|
||||
den RUFNAMEN des neuen Namens — Notizen bleiben so lesbar (nicht „Kuke von Marion
|
||||
Knoss" mitten im Satz). MUSS als letzter Schritt vor dem Payload-Dump laufen: die
|
||||
Notizen werden vorher fürs Dedup-Scoring gelesen. Gibt die Zahl der geänderten
|
||||
Notizen zurück.
|
||||
"""
|
||||
_rename_pairs = []
|
||||
for g in resolved_gerbils:
|
||||
_old = (g.get("_pre_rename_name") or "").strip()
|
||||
_new = (g.get("Name") or "").strip()
|
||||
if not _old or not _new or _old == _new:
|
||||
continue
|
||||
_new_call = get_call_name(_new) or _new
|
||||
# Nur ein Zuchtname-Suffix dazubekommen („Jana" → „Jana of little longnoses")
|
||||
# ist keine Namenskorrektur → Notizen unangetastet lassen.
|
||||
if normalize_name(_old) == normalize_name(_new_call):
|
||||
continue
|
||||
# Guards gegen Kollateralschaden (im ersten Probelauf real aufgetreten):
|
||||
# (1) zu kurze Quellnamen („M") treffen Abkürzungen im Freitext
|
||||
# („Natascha M.", „Sch. M. 2", „60g; M: 17.3.19").
|
||||
if len(_old) < 4:
|
||||
continue
|
||||
# (2) mehr als zwei Wörter = kein Rufname, sondern eine Phrase (z. B. der
|
||||
# Sentinel „Zucht der Kleinen Chaoten" als Quellname eines Stubs).
|
||||
if len(_old.split()) > 2:
|
||||
continue
|
||||
# (3) steckt der alte Name als Wort im neuen Rufnamen („Missy" → „Missy gen.
|
||||
# Krümmel"), würde die Notiz den Zusatz bei jedem Lauf verdoppeln.
|
||||
if re.search(r"\b" + re.escape(_old) + r"\b", _new_call):
|
||||
continue
|
||||
_rename_pairs.append((_old, _new_call))
|
||||
_notes_fixed = 0
|
||||
for _old, _new_call in _rename_pairs:
|
||||
_rx = re.compile(r"\b" + re.escape(_old) + r"\b")
|
||||
for _rec in list(resolved_litters) + list(resolved_gerbils):
|
||||
_val = _rec.get("Notes")
|
||||
if not _val or not _rx.search(_val):
|
||||
continue
|
||||
# (4) Notizen, die die Umbenennung SELBST dokumentieren („hieß ursprünglich
|
||||
# Drake Jr, wurde später in Gale umbenannt"), müssen den alten Namen
|
||||
# behalten — sonst wird der Satz sinnlos.
|
||||
_low = _val.lower()
|
||||
if "umbenannt" in _low or "ursprünglich" in _low or "urspruenglich" in _low:
|
||||
continue
|
||||
_rec["Notes"] = _rx.sub(_new_call, _val)
|
||||
_notes_fixed += 1
|
||||
return _notes_fixed
|
||||
|
||||
|
||||
def apply_decision_overrides(resolved_gerbils, decisions, variety_map,
|
||||
resolve_contact_id=None):
|
||||
"""Wendet die Feld-Overrides aus conflict-decisions.json auf die finalen Tiere an.
|
||||
|
||||
Als Modul-Funktion (statt inline in main()) herausgezogen, damit die
|
||||
Override-Semantik regressionsgetestet werden kann — insbesondere die
|
||||
AUTORITATIVEN Felder (genotype/spottingType/goHomeDate), die auch einen
|
||||
bereits gefüllten Wert überschreiben. Gibt die Zahl der geänderten Tiere zurück.
|
||||
"""
|
||||
# ── isResident/notes/receiver-Overrides (Mensch-Entscheidung) ─────────────
|
||||
# NACH dem Dedup anwenden, sonst würde die Dedup-Zusammenführung (IsResident=True
|
||||
# falls eine Variante resident ist) sie wieder überschreiben. Auch NACH der
|
||||
# Residenz-Propagation (die nur auf Stammbaum-Roh-Tiere wirkt) → diese Overrides
|
||||
# sind das letzte Wort. Matcht primär über `externalRef` (präzise, auch für
|
||||
# namenlose Tiere) oder sonst über normalize(call-name)+ISO-dob (leere dob =
|
||||
# name-only, für Vorfahren ohne Datum). Ein Match kann zusätzlich `notes` (→
|
||||
# Notes, autoritativ) und `receiver` (Kontaktname → ReceiverContactId, Status=
|
||||
# GivenAway) setzen — so bekommt ein bereits existierendes Tier (z. B. Akane)
|
||||
# seinen Lebenslauf + Abnehmer, ohne ein addAnimals-Stub zu sein.
|
||||
_ovr_by_extref = {}
|
||||
_ovr_by_namedob = {}
|
||||
for d in decisions:
|
||||
has_ovr = (("isResident" in d) or d.get("notes") or d.get("receiver")
|
||||
or d.get("renameTo") or d.get("deceased") or d.get("dateOfDeath")
|
||||
or d.get("correctDob") or d.get("genotype") or d.get("farbschlag")
|
||||
or d.get("gender") or d.get("father") or d.get("mother")
|
||||
or d.get("originBreeder") or d.get("spottingType")
|
||||
or d.get("goHomeDate"))
|
||||
if not has_ovr:
|
||||
continue
|
||||
er = (d.get("externalRef") or "").strip()
|
||||
if er:
|
||||
_ovr_by_extref[er] = d
|
||||
nm = d.get("name")
|
||||
if nm is not None:
|
||||
ck = normalize_name(get_call_name(nm or ""))
|
||||
iso = parse_date(d.get("dob")) if d.get("dob") else ""
|
||||
_ovr_by_namedob[(ck, iso or "")] = d
|
||||
_isres_applied = 0
|
||||
if _ovr_by_extref or _ovr_by_namedob:
|
||||
for g in resolved_gerbils:
|
||||
er = g.get("ExternalRef") or ""
|
||||
ck = normalize_name(get_call_name(g.get("Name") or ""))
|
||||
iso = g.get("DateOfBirth") or ""
|
||||
# `_precise` = die Entscheidung meint NACHWEISLICH dieses Tier (externalRef
|
||||
# oder name+dob). Ein name-only-Match kann dagegen einen Namensvetter
|
||||
# treffen → autoritative Overrides (Gencode) dürfen dort nichts
|
||||
# überschreiben. (Ticket a547be62 Eliza / 7037f5d8 Merle of Samsimar.)
|
||||
_precise = False
|
||||
if er and er in _ovr_by_extref:
|
||||
d = _ovr_by_extref[er]
|
||||
_precise = True
|
||||
elif er and any(er.endswith(k) for k in _ovr_by_extref):
|
||||
d = next(v for k, v in _ovr_by_extref.items() if er.endswith(k))
|
||||
_precise = True
|
||||
elif (ck, iso) in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, iso)]
|
||||
_precise = bool(iso)
|
||||
elif (ck, "") in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, "")]
|
||||
else:
|
||||
continue
|
||||
applied = False
|
||||
if "originBreeder" in d:
|
||||
val = d["originBreeder"]
|
||||
if g.get("OriginBreeder") != val:
|
||||
g["OriginBreeder"] = val
|
||||
applied = True
|
||||
if "isResident" in d:
|
||||
val = bool(d["isResident"])
|
||||
# Ein expliziter Override (true ODER false) ist das letzte Wort und
|
||||
# darf vom späteren Residenz-Sweep NICHT überschrieben werden.
|
||||
g["_resident_override"] = True
|
||||
if g.get("IsResident") != val:
|
||||
g["IsResident"] = val
|
||||
# Ein Nicht-Bestandstier ist kein eigenes Zuchttier → den eigenen
|
||||
# Zucht-Breeder entfernen (nur, wenn er auf die eigene Zucht zeigt).
|
||||
if not val and g.get("OriginBreeder") == "Zucht der kleinen Chaoten":
|
||||
g["OriginBreeder"] = None
|
||||
applied = True
|
||||
notes_ovr = (d.get("notes") or "").strip()
|
||||
if notes_ovr and g.get("Notes") != notes_ovr:
|
||||
g["Notes"] = notes_ovr
|
||||
applied = True
|
||||
rec_name = d.get("receiver")
|
||||
if rec_name:
|
||||
rec_cid = resolve_contact_id(rec_name) if resolve_contact_id else None
|
||||
if rec_cid and g.get("ReceiverContactId") != rec_cid:
|
||||
g["ReceiverContactId"] = rec_cid
|
||||
if g.get("Status") not in ("Deceased",):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
# renameTo: korrigiert den Anzeigenamen eines bestehenden Tieres. Der
|
||||
# Match-Key bleibt der ursprüngliche name/externalRef (NICHT umdeuten) —
|
||||
# erst HIER wird der Name überschrieben.
|
||||
rename_to = (d.get("renameTo") or "").strip()
|
||||
if rename_to and g.get("Name") != rename_to:
|
||||
# Quell-Namen merken, damit spätere Eltern-Verknüpfungen, die noch den
|
||||
# alten Namen referenzieren, weiter auflösen (siehe Namens-Index unten,
|
||||
# Ticket e0a0c304 Kruke→Kuke: sonst verlieren Kukes Würfe die Mutter).
|
||||
g["_pre_rename_name"] = g.get("Name")
|
||||
g["Name"] = rename_to
|
||||
applied = True
|
||||
# deceased: markiert ein bestehendes Tier als verstorben, falls noch
|
||||
# nicht. dateOfDeath wird bewusst NICHT angefasst.
|
||||
if d.get("deceased") and g.get("Status") != "Deceased":
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# dateOfDeath: setzt das Todesdatum (falls noch keins) + Status Deceased.
|
||||
# rpro3 hat keine Sterbespalte, daher gibt die Züchterin/das Ticket das Datum vor.
|
||||
if d.get("dateOfDeath") and not g.get("DateOfDeath"):
|
||||
_dod = parse_date(d["dateOfDeath"])
|
||||
if _dod:
|
||||
g["DateOfDeath"] = _dod
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# goHomeDate: Abgabedatum aus einer Züchterin-Entscheidung. AUTORITATIV —
|
||||
# überschreibt auch einen vorhandenen Wert, weil Wurfchronik-Transkriptionen
|
||||
# das Datum falsch zusammenbauen können (JackJack: „1310-05-13" statt
|
||||
# 13.05.2010, Ticket 24522f5f). Zieht Status GivenAway nach, sofern das Tier
|
||||
# nicht verstorben ist (Ticket 7037f5d8 Merle) — „GivenAway" verhindert
|
||||
# außerdem, dass die 6-Jahres-Sterbe-Heuristik greift.
|
||||
if d.get("goHomeDate"):
|
||||
_gh = parse_date(d["goHomeDate"])
|
||||
if _gh:
|
||||
if g.get("GoHomeDate") != _gh:
|
||||
g["GoHomeDate"] = _gh
|
||||
applied = True
|
||||
if g.get("Status") not in ("Deceased", "GivenAway"):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
if d.get("correctDob") and not g.get("DateOfBirth"):
|
||||
dob_iso = parse_date(d["correctDob"])
|
||||
g["DateOfBirth"] = dob_iso
|
||||
g["_birth_date"] = dob_iso
|
||||
g["_eff_dob"] = dob_iso
|
||||
applied = True
|
||||
# Ein expliziter Gencode aus einer Züchterin-Entscheidung ist AUTORITATIV
|
||||
# und überschreibt auch einen vorhandenen Wert — bei Wurfchronik-Tieren ist
|
||||
# der meist nur der Kanon-Fill des Farbschlags (resolve_color_and_genotype),
|
||||
# keine echte Gencode-Quelle. Nur bei PRÄZISEM Match, damit eine name-only-
|
||||
# Entscheidung nicht den Gencode eines gleichnamigen fremden Tieres
|
||||
# überschreibt. (Ticket a547be62 Eliza; repariert auch Chris/page_0013.)
|
||||
if d.get("genotype"):
|
||||
_geno_ovr = d["genotype"].strip()
|
||||
if (_precise or not g.get("Genotype")) and g.get("Genotype") != _geno_ovr:
|
||||
g["Genotype"] = _geno_ovr
|
||||
applied = True
|
||||
# Scheckungsart (Freitext, Werte wie in de.ts `spottingTypes`, z. B.
|
||||
# „Ansatzschecke"). Der Importer leitet sie aus keiner Quelle ab — sie ist
|
||||
# ausschließlich per Entscheidung setzbar.
|
||||
if d.get("spottingType"):
|
||||
_sp_ovr = d["spottingType"].strip()
|
||||
if g.get("SpottingType") != _sp_ovr:
|
||||
g["SpottingType"] = _sp_ovr
|
||||
applied = True
|
||||
if d.get("farbschlag") and not g.get("ColorVarietyId"):
|
||||
cv_id = variety_map.get(d["farbschlag"].strip().lower())
|
||||
if cv_id:
|
||||
g["ColorVarietyId"] = cv_id
|
||||
applied = True
|
||||
if d.get("gender") and g.get("Gender") in ("unknown", None):
|
||||
gender = d["gender"].strip().lower()
|
||||
gender = {"m": "male", "männlich": "male", "w": "female",
|
||||
"f": "female", "weiblich": "female"}.get(gender, gender)
|
||||
if gender in ("male", "female"):
|
||||
g["Gender"] = gender
|
||||
applied = True
|
||||
if d.get("father"):
|
||||
g["_override_father_name"] = d["father"].strip()
|
||||
g["_override_father_dob"] = d.get("fatherDob")
|
||||
applied = True
|
||||
if d.get("mother"):
|
||||
g["_override_mother_name"] = d["mother"].strip()
|
||||
g["_override_mother_dob"] = d.get("motherDob")
|
||||
applied = True
|
||||
if applied:
|
||||
_isres_applied += 1
|
||||
if _isres_applied:
|
||||
print("isResident/notes/receiver/dob/geno/spotting/abgabe-Overrides angewandt: "
|
||||
f"{_isres_applied}")
|
||||
return _isres_applied
|
||||
|
||||
|
||||
def main():
|
||||
print("Loading color variety seeds...")
|
||||
variety_map = {}
|
||||
@@ -3196,136 +3444,9 @@ def main():
|
||||
print(f"Deduplicated to {len(resolved_gerbils)} unique gerbil records.")
|
||||
|
||||
# ── isResident/notes/receiver-Overrides (Mensch-Entscheidung) ─────────────
|
||||
# NACH dem Dedup anwenden, sonst würde die Dedup-Zusammenführung (IsResident=True
|
||||
# falls eine Variante resident ist) sie wieder überschreiben. Auch NACH der
|
||||
# Residenz-Propagation (die nur auf Stammbaum-Roh-Tiere wirkt) → diese Overrides
|
||||
# sind das letzte Wort. Matcht primär über `externalRef` (präzise, auch für
|
||||
# namenlose Tiere) oder sonst über normalize(call-name)+ISO-dob (leere dob =
|
||||
# name-only, für Vorfahren ohne Datum). Ein Match kann zusätzlich `notes` (→
|
||||
# Notes, autoritativ) und `receiver` (Kontaktname → ReceiverContactId, Status=
|
||||
# GivenAway) setzen — so bekommt ein bereits existierendes Tier (z. B. Akane)
|
||||
# seinen Lebenslauf + Abnehmer, ohne ein addAnimals-Stub zu sein.
|
||||
_ovr_by_extref = {}
|
||||
_ovr_by_namedob = {}
|
||||
for d in _decisions:
|
||||
has_ovr = (("isResident" in d) or d.get("notes") or d.get("receiver")
|
||||
or d.get("renameTo") or d.get("deceased") or d.get("dateOfDeath")
|
||||
or d.get("correctDob") or d.get("genotype") or d.get("farbschlag")
|
||||
or d.get("gender") or d.get("father") or d.get("mother")
|
||||
or d.get("originBreeder"))
|
||||
if not has_ovr:
|
||||
continue
|
||||
er = (d.get("externalRef") or "").strip()
|
||||
if er:
|
||||
_ovr_by_extref[er] = d
|
||||
nm = d.get("name")
|
||||
if nm is not None:
|
||||
ck = normalize_name(get_call_name(nm or ""))
|
||||
iso = parse_date(d.get("dob")) if d.get("dob") else ""
|
||||
_ovr_by_namedob[(ck, iso or "")] = d
|
||||
_isres_applied = 0
|
||||
if _ovr_by_extref or _ovr_by_namedob:
|
||||
for g in resolved_gerbils:
|
||||
er = g.get("ExternalRef") or ""
|
||||
ck = normalize_name(get_call_name(g.get("Name") or ""))
|
||||
iso = g.get("DateOfBirth") or ""
|
||||
if er and er in _ovr_by_extref:
|
||||
d = _ovr_by_extref[er]
|
||||
elif er and any(er.endswith(k) for k in _ovr_by_extref):
|
||||
d = next(v for k, v in _ovr_by_extref.items() if er.endswith(k))
|
||||
elif (ck, iso) in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, iso)]
|
||||
elif (ck, "") in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, "")]
|
||||
else:
|
||||
continue
|
||||
applied = False
|
||||
if "originBreeder" in d:
|
||||
val = d["originBreeder"]
|
||||
if g.get("OriginBreeder") != val:
|
||||
g["OriginBreeder"] = val
|
||||
applied = True
|
||||
if "isResident" in d:
|
||||
val = bool(d["isResident"])
|
||||
# Ein expliziter Override (true ODER false) ist das letzte Wort und
|
||||
# darf vom späteren Residenz-Sweep NICHT überschrieben werden.
|
||||
g["_resident_override"] = True
|
||||
if g.get("IsResident") != val:
|
||||
g["IsResident"] = val
|
||||
# Ein Nicht-Bestandstier ist kein eigenes Zuchttier → den eigenen
|
||||
# Zucht-Breeder entfernen (nur, wenn er auf die eigene Zucht zeigt).
|
||||
if not val and g.get("OriginBreeder") == "Zucht der kleinen Chaoten":
|
||||
g["OriginBreeder"] = None
|
||||
applied = True
|
||||
notes_ovr = (d.get("notes") or "").strip()
|
||||
if notes_ovr and g.get("Notes") != notes_ovr:
|
||||
g["Notes"] = notes_ovr
|
||||
applied = True
|
||||
rec_name = d.get("receiver")
|
||||
if rec_name:
|
||||
rec_cid = _resolve_contact_id_by_name(rec_name)
|
||||
if rec_cid and g.get("ReceiverContactId") != rec_cid:
|
||||
g["ReceiverContactId"] = rec_cid
|
||||
if g.get("Status") not in ("Deceased",):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
# renameTo: korrigiert den Anzeigenamen eines bestehenden Tieres. Der
|
||||
# Match-Key bleibt der ursprüngliche name/externalRef (NICHT umdeuten) —
|
||||
# erst HIER wird der Name überschrieben.
|
||||
rename_to = (d.get("renameTo") or "").strip()
|
||||
if rename_to and g.get("Name") != rename_to:
|
||||
# Quell-Namen merken, damit spätere Eltern-Verknüpfungen, die noch den
|
||||
# alten Namen referenzieren, weiter auflösen (siehe Namens-Index unten,
|
||||
# Ticket e0a0c304 Kruke→Kuke: sonst verlieren Kukes Würfe die Mutter).
|
||||
g["_pre_rename_name"] = g.get("Name")
|
||||
g["Name"] = rename_to
|
||||
applied = True
|
||||
# deceased: markiert ein bestehendes Tier als verstorben, falls noch
|
||||
# nicht. dateOfDeath wird bewusst NICHT angefasst.
|
||||
if d.get("deceased") and g.get("Status") != "Deceased":
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# dateOfDeath: setzt das Todesdatum (falls noch keins) + Status Deceased.
|
||||
# rpro3 hat keine Sterbespalte, daher gibt die Züchterin/das Ticket das Datum vor.
|
||||
if d.get("dateOfDeath") and not g.get("DateOfDeath"):
|
||||
_dod = parse_date(d["dateOfDeath"])
|
||||
if _dod:
|
||||
g["DateOfDeath"] = _dod
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
if d.get("correctDob") and not g.get("DateOfBirth"):
|
||||
dob_iso = parse_date(d["correctDob"])
|
||||
g["DateOfBirth"] = dob_iso
|
||||
g["_birth_date"] = dob_iso
|
||||
g["_eff_dob"] = dob_iso
|
||||
applied = True
|
||||
if d.get("genotype") and not g.get("Genotype"):
|
||||
g["Genotype"] = d["genotype"].strip()
|
||||
applied = True
|
||||
if d.get("farbschlag") and not g.get("ColorVarietyId"):
|
||||
cv_id = variety_map.get(d["farbschlag"].strip().lower())
|
||||
if cv_id:
|
||||
g["ColorVarietyId"] = cv_id
|
||||
applied = True
|
||||
if d.get("gender") and g.get("Gender") in ("unknown", None):
|
||||
gender = d["gender"].strip().lower()
|
||||
gender = {"m": "male", "männlich": "male", "w": "female",
|
||||
"f": "female", "weiblich": "female"}.get(gender, gender)
|
||||
if gender in ("male", "female"):
|
||||
g["Gender"] = gender
|
||||
applied = True
|
||||
if d.get("father"):
|
||||
g["_override_father_name"] = d["father"].strip()
|
||||
g["_override_father_dob"] = d.get("fatherDob")
|
||||
applied = True
|
||||
if d.get("mother"):
|
||||
g["_override_mother_name"] = d["mother"].strip()
|
||||
g["_override_mother_dob"] = d.get("motherDob")
|
||||
applied = True
|
||||
if applied:
|
||||
_isres_applied += 1
|
||||
if _isres_applied:
|
||||
print(f"isResident/notes/receiver/dob/geno-Overrides angewandt: {_isres_applied}")
|
||||
# Logik siehe apply_decision_overrides() (Modul-Funktion, damit testbar).
|
||||
apply_decision_overrides(resolved_gerbils, _decisions, variety_map,
|
||||
_resolve_contact_id_by_name)
|
||||
|
||||
|
||||
|
||||
@@ -4459,6 +4580,12 @@ def main():
|
||||
if _origin_fixed:
|
||||
print(f"Herkunft normalisiert auf 'Zucht der Kleinen Chaoten': {_origin_fixed} Tier(e)")
|
||||
|
||||
# ── Umbenennungs-Sweep in Freitext-Notizen (Tickets ea803c3d/24006ccd) ──
|
||||
# Finaler Anzeige-Sweep NACH aller Logik — s. apply_rename_note_sweep().
|
||||
_notes_fixed = apply_rename_note_sweep(resolved_gerbils, resolved_litters)
|
||||
if _notes_fixed:
|
||||
print(f"renameTo-Notiz-Sweep: {_notes_fixed} Notiz(en) auf den korrigierten Namen gesetzt")
|
||||
|
||||
# 6. Save final output JSON payload
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
payload = {
|
||||
|
||||
@@ -4,11 +4,11 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
|
||||
|
||||
## Überblick
|
||||
|
||||
- Rohe Tier-Einträge aus den Stammbäumen: **3600**
|
||||
- Rohe Tier-Einträge aus den Stammbäumen: **3603**
|
||||
- Nach Zusammenführung (eindeutige Tiere): **1487**
|
||||
- davon mit Geburtsdatum: 771
|
||||
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 545
|
||||
- Konflikte zur Klärung: **20**
|
||||
- Konflikte zur Klärung: **19**
|
||||
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **757**
|
||||
- Fotos zugeordnet: **510**
|
||||
- Würfe aus der Wurfchronik: **752**
|
||||
@@ -39,13 +39,12 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
||||
| Fatou gen. Sam von den Kleinen Chaoten | 03.02.2020 | aa Cc[chm] DD Ee gg Pp spsp // aa c[chm]c[h] D- Ee GG P- Spsp | — | 19.03.2022 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Kid von Cassy & Blaster, Stammbaum von Pukas Kids, Stammbaum von Sa Sp (YaraFire) & Sa Kids |
|
||||
| Charly of Golden Lights | 05.04.2016 | aa c[chm]c[chm] D- Ee Gg P- Spsp // aa c[chm]c[chm] DD Ee GG P- Spsp | — | — | Stammbaum von Akio Kids, Stammbaum von Kid von Cassy & Blaster |
|
||||
| Xtra Bounty von den Schlossmäusen | 20.03.2014 | aa c[h]c[h] DD Ee Gg PP spsp // aa c[h]c[h] DD Ee gg PP spsp | — | 01.05.2018 | Stammbaum von Akio Kids, Stammbaum von Kid von Cassy & Blaster |
|
||||
| Jay | 24.10.2021 | aa c[chm]c[hm] Dd Ee gg P- spsp | Zobel | 10.01.2024 // 10.01.2025 | Stammbaum von Alberto Kids, Stammbaum von Pukas Kids |
|
||||
| Louis of Black Forest | 22.08.2019 | Aa CC dd EE Gg P- spsp | — | 09.04.2024 // 10.04.2024 | Stammbaum von Alberto Kids, Stammbaum von CP Mädels von Grisu, Stammbaum von CP-Polarfuchs von Grisu, Stammbaum von Kid von Cassy & Blaster, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Pukas Kids, Stammbaum von Rj, Stammbaum von Sa Sp (YaraFire) & Sa Kids |
|
||||
| Inochi gen. Picu von den Kleinen Chaoten | 11.07.2020 | Aa CC dd Ee GG Pp spsp // Aa CC dd Ee Gg Pp spsp | Dilute Agouti | 11.03.2025 // 12.03.2025 | Stammbaum von Alberto Kids, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Picus Son, Stammbaum von Rj |
|
||||
| Snickers of Black Forest | 21.09.2018 | Aa Cc[chm] dd Ee Gg P- spsp // Aa Cc[chm] dd Ee gg Pp Spsp | — | 04.04.2023 // 04.11.2022 | Stammbaum von Alberto Kids, Stammbaum von CP Mädels von Grisu, Stammbaum von CP-Polarfuchs von Grisu, Stammbaum von Jiminy of Black Forest, Stammbaum von Kid von Cassy & Blaster, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Picus Son, Stammbaum von Pukas Kids, Stammbaum von Rj, Stammbaum von Sa Sp (YaraFire) & Sa Kids, Stammbaum von Stella Kids |
|
||||
| Lui von den Kleinen Chaoten | 16.04.2021 | Aa Cc[chm] DD ee[f] Gg PP Spsp // Aa c[chm]c[chm] D- Ee[f] Gg PP spsp | — | 15.12.2020 | Stammbaum von Alberto Kids, Stammbaum von Blitzis Kids, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Picus Son, Stammbaum von Rj |
|
||||
| Odette von den Kleinen Chaoten | 22.02.2021 | 2, AA c[chm]c[chm] DD Ee Gg PP Spsp // AA c[chm]c[chm] DD Ee Gg Pp Spsp DP | — | 30.02.2020 // 30.12.2024 | Stammbaum von Blitzis Kids |
|
||||
| Marlin of Black Forest | 28.05.2019 | Aa C- DD Ee Gg Pp spsp // Aa Cc[chm] DD Ee Gg Pp Spsp // Aa Cc[chm] DD Ee Gg Pp spsp | Agouti | 18.11.2023 | Stammbaum von Blitzis Kids, Stammbaum von Fire Kids, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Marlin of Black Forest, Stammbaum von Sa Sp (YaraFire) & Sa Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Louis of Black Forest | 22.08.2019 | Aa CC dd EE Gg P- spsp | — | 09.04.2024 // 10.04.2024 | Stammbaum von CP Mädels von Grisu, Stammbaum von CP-Polarfuchs von Grisu, Stammbaum von Kid von Cassy & Blaster, Stammbaum von Kids von dd Algierfuchs & Sa, Stammbaum von Rj, Stammbaum von Sa Sp (YaraFire) & Sa Kids |
|
||||
| Osamu | 10.12.2015 | AA CC DD ee gg P- spsp // AA CC DD ee gg PP spsp // AA CC DD ee uw[d]uw[d] PP spsp | — | 01.10.2020 // 18.12.2020 | Stammbaum von Danako, Stammbaum von Ella, Stammbaum von Jin, Stammbaum von Kazuya, Stammbaum von Kentucky, Stammbaum von Kid von Cassy & Blaster, Stammbaum von Martin, Stammbaum von Picus Son, Stammbaum von Rainny, Stammbaum von Ren, Stammbaum von Rj, Stammbaum von South Dakota, Stammbaum von Stella Kids, Stammbaum von Tennessee, Stammbaum von Zenon von Elea |
|
||||
| Taro von den kleinen Chaoten | 01.09.2012 | Aa Cc[chm] D- Ee Gg Pp spsp // aa Cc[chm] DD EE GG PP spsp | — | 01.10.2015 // 21.09.2012 | Stammbaum von Danako, Stammbaum von Kalea |
|
||||
| Quebec v.d. Kleinen Chaoten | 21.06.2014 | aa Cc[chm] dd Ee Gg Pp spsp // aa Cc[chm] dd Ee Uwuw[d] Pp spsp // aa Cc[chm] dd Ee gg Pp spsp | — | 21.10.2016 | Stammbaum von Ella, Stammbaum von Jin, Stammbaum von Kazuya, Stammbaum von Martin, Stammbaum von Picus Son, Stammbaum von Rainny, Stammbaum von Ren, Stammbaum von South Dakota, Stammbaum von Tennessee, Stammbaum von Zenon von Elea |
|
||||
|
||||
@@ -498,6 +498,57 @@ if os.path.exists(_emf_chart):
|
||||
else:
|
||||
print("\nWarning: EMF-Stammbaum nicht gefunden, EMF-Check übersprungen.")
|
||||
|
||||
# --- sternloses Geburtsdatum im Block (Ticket 65266679 Bijou) ---
|
||||
# Einige Charts vergessen den Stern ("22.08.2019/+09.04.2024"); ohne Stern erkannte die
|
||||
# Blockheuristik das Tier gar nicht und _reconstruct_parents griff in den Nachbar-Ast.
|
||||
# Normalisiert wird NUR mit Block-Kontext (Genotyp darunter UND Name darueber), damit
|
||||
# beliebige Datumszellen nicht faelschlich zu Tieren werden.
|
||||
_tmp_star = os.path.join(tempfile.gettempdir(), "starless_dob.xlsx")
|
||||
_make_xlsx(_tmp_star, {
|
||||
# echter Block, aber ohne Stern vor dem Geburtsdatum (Quelldatei-Tippfehler)
|
||||
"K44": "Louis of Black Forest",
|
||||
"K45": "22.08.2019/+09.04.2024",
|
||||
"K46": "Aa CC dd EE Gg P- spsp",
|
||||
# Datum mit Name darueber, aber OHNE Genotyp darunter -> kein Block
|
||||
"N44": "Irgendeine Notiz",
|
||||
"N45": "01.01.2020",
|
||||
# Datum mit Genotyp darunter, aber OHNE Namenszelle darueber -> kein Block
|
||||
"Q45": "02.02.2021",
|
||||
"Q46": "aa CC DD EE GG PP spsp",
|
||||
})
|
||||
try:
|
||||
_star_animals = e.extract_stammbaum(_tmp_star)
|
||||
_star_by_name = {a["name"]: a for a in _star_animals}
|
||||
check("sternloses DOB wird als Block erkannt", "Louis of Black Forest" in _star_by_name)
|
||||
check("sternloses DOB: Geburts- und Todesdatum geparst",
|
||||
_star_by_name.get("Louis of Black Forest", {}).get("dob") == "22.08.2019"
|
||||
and _star_by_name.get("Louis of Black Forest", {}).get("death") == "09.04.2024")
|
||||
check("Datumszelle ohne Block-Kontext wird NICHT zum Tier",
|
||||
len(_star_animals) == 1)
|
||||
finally:
|
||||
try: os.remove(_tmp_star)
|
||||
except OSError: pass
|
||||
|
||||
# Direkt auf der Normalisierung: Todesdatum-only-Zellen ("/+01.09.2024", kommt in
|
||||
# "Stammbaum von Kentucky/Watarus Kids" vor) duerfen NIE zum Geburtsdatum werden.
|
||||
_cells = {
|
||||
(11, 45): "22.08.2019/+09.04.2024", # Block -> bekommt den Stern
|
||||
(11, 44): "Louis of Black Forest",
|
||||
(11, 46): "Aa CC dd EE Gg P- spsp",
|
||||
(14, 66): "/+01.09.2024", # nur Todesdatum -> unangetastet
|
||||
(14, 65): "Nani of Black Forest",
|
||||
(14, 67): "Aa CC dd EE Gg P- spsp",
|
||||
(17, 30): "30.01.2020/+", # Block, Todesdatum leer -> bekommt den Stern
|
||||
(17, 29): "Jiminy of Black Forest",
|
||||
(17, 31): "Aa Cc[chm] Dd ee gg P- Spsp",
|
||||
}
|
||||
e._normalise_starless_dobs(_cells)
|
||||
check("Normalisierung setzt den Stern im Block", _cells[(11, 45)] == "*22.08.2019/+09.04.2024")
|
||||
check("Normalisierung setzt den Stern auch bei leerem Todesdatum",
|
||||
_cells[(17, 30)] == "*30.01.2020/+")
|
||||
check("Normalisierung laesst reine Todesdatum-Zellen unberuehrt",
|
||||
_cells[(14, 66)] == "/+01.09.2024")
|
||||
|
||||
if failed:
|
||||
print(f"\n{failed} test(s) FAILED")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -124,6 +124,23 @@ check("ef/e het -> Fuchsschimmel family (Kohlfuchsschimmel)",
|
||||
fs("aa CC DD ee[f] GG PP spsp") == "Kohlfuchsschimmel")
|
||||
# Unknown allele = copy of the visible partner (GEN-5): A? -> AA, D? -> DD.
|
||||
check("unknown copies known: AA C? DD ee GG pp -> Goldfuchs", fs("AA C- DD ee GG pp spsp") == "Goldfuchs")
|
||||
# GEN-6 / Tickets bde4ec70 + f89e95ad — C-Locus-ZYGOTIE: Platin, Saphir und
|
||||
# Platin-Hell unterscheiden sich NUR im C-Locus. Zuechterin autoritativ: „Saphir ist
|
||||
# aa Cc[chm] D- E- G- pp und Platin ist aa CC D- E- G- pp. Es ist nicht dasselbe!"
|
||||
# Vorher trugen alle drei identische Katalog-Tokens (C:'C') -> immer Platin.
|
||||
check("bde4ec70: aa CC pp -> Platin (zwei volle C)", fs("aa CC DD EE GG pp spsp") == "Platin")
|
||||
check("f89e95ad: aa Cc[chm] pp -> Saphir", fs("aa Cc[chm] DD EE GG pp spsp") == "Saphir")
|
||||
check("bde4ec70 JackJack: aa Cc[chm] DD E- G- pp Spsp -> Saphir (Basisname)",
|
||||
fs("aa Cc[chm] DD E- G- pp Spsp") == "Saphir")
|
||||
check("GEN-3g: aa Cc[h] pp -> Platin-Hell", fs("aa Cc[h] DD EE GG pp spsp") == "Platin-Hell")
|
||||
# Ticket 2322c2a8 (Jay *24.10.2021) — 'c[hm]' ist ein verschriebenes 'c[chm]'
|
||||
# (fehlendes c) in den Quell-Charts; ohne Alias kippt der Farbschlag auf Zobel-Hell.
|
||||
check("2322c2a8: aa c[chm]c[hm] gg -> Zobel (nicht Zobel-Hell)",
|
||||
fs("aa c[chm]c[hm] Dd Ee gg P- spsp") == "Zobel")
|
||||
check("2322c2a8: c[hm]-Alias landet im C-Locus als c^chm",
|
||||
g.parse("aa c[chm]c[hm] Dd Ee gg P- spsp")["mapped8locus"]["C"] == ["c^chm", "c^chm"])
|
||||
check("2322c2a8: Cc[hm] bleibt c[chm]-Traeger -> Saphir",
|
||||
fs("aa Cc[hm] DD EE GG pp spsp") == "Saphir")
|
||||
# An incomplete parse must NOT throw and must not be invented as a real colour.
|
||||
check("empty mapping -> Unbekannt", g.genotype_to_farbschlag({}) == g.UNKNOWN_FARBSCHLAG)
|
||||
|
||||
|
||||
@@ -537,6 +537,169 @@ check("excludeContractMatch: control (no exclude) -> receiver IS set",
|
||||
_excl_gerbils2[0].get("ReceiverContactId") is not None)
|
||||
|
||||
|
||||
# ── parse_date: Plausibilitaets-Guard (Ticket 24522f5f, JackJack) ─────────────
|
||||
# Die Wurfchronik-Transkription baute aus „13.5.10" das Datum „1310-05-13" — solche
|
||||
# unmoeglichen Jahre muessen verworfen werden, ohne die 19xx→20xx-Normalisierung
|
||||
# („13.5.10" → 2010-05-13) zu beschaedigen.
|
||||
check("parse_date: implausibles Jahr 1310 (ISO) -> None",
|
||||
m.parse_date("1310-05-13") is None)
|
||||
check("parse_date: implausibles Jahr 1310 (DD.MM.YYYY) -> None",
|
||||
m.parse_date("13.05.1310") is None)
|
||||
check("parse_date: 2-stelliges Jahr bleibt intakt ('13.5.10' -> 2010-05-13)",
|
||||
m.parse_date("13.5.10") == "2010-05-13")
|
||||
check("parse_date: 19xx wird weiter auf 20xx normalisiert",
|
||||
m.parse_date("22.12.1924") == "2024-12-22")
|
||||
check("parse_date: regulaeres Datum unveraendert",
|
||||
m.parse_date("22.12.2024") == "2024-12-22")
|
||||
check("parse_date: fernes Zukunftsjahr -> None",
|
||||
m.parse_date("2999-01-01") is None)
|
||||
|
||||
|
||||
# ── renameTo-Notiz-Sweep (Tickets ea803c3d/24006ccd, Kruke→Kuke) ──────────────
|
||||
# `renameTo` korrigiert nur das Namensfeld; der alte Name blieb in den aus der
|
||||
# Wurfchronik uebernommenen Freitext-Notizen stehen. Der finale Sweep ersetzt ihn
|
||||
# wortgenau (an Wortgrenzen) durch den RUFNAMEN des neuen Namens.
|
||||
_sw_gerbils = [
|
||||
{"Name": "Kuke von Marion Knoss", "_pre_rename_name": "Kruke",
|
||||
"Notes": "Krukelinde bleibt; Kruke wird ersetzt"},
|
||||
{"Name": "Blacky", "Notes": "Vater von Kruke-Kindern"},
|
||||
]
|
||||
_sw_litters = [
|
||||
{"Notes": "Blacky + Kruke v. 21.09.2010; WS=4"},
|
||||
{"Notes": None},
|
||||
{"Notes": "Eltern: Blacky + Bonnie"},
|
||||
]
|
||||
_sw_n = m.apply_rename_note_sweep(_sw_gerbils, _sw_litters)
|
||||
check("Notiz-Sweep: Wurf-Notiz traegt den neuen Rufnamen",
|
||||
_sw_litters[0]["Notes"] == "Blacky + Kuke v. 21.09.2010; WS=4")
|
||||
check("Notiz-Sweep: Rufname (nicht der volle Zuchtname) wird eingesetzt",
|
||||
"von Marion Knoss" not in _sw_litters[0]["Notes"])
|
||||
check("Notiz-Sweep: Teilwort 'Krukelinde' bleibt unangetastet",
|
||||
_sw_gerbils[0]["Notes"] == "Krukelinde bleibt; Kuke wird ersetzt")
|
||||
check("Notiz-Sweep: auch Gerbil-Notizen anderer Tiere werden gefixt",
|
||||
_sw_gerbils[1]["Notes"] == "Vater von Kuke-Kindern")
|
||||
check("Notiz-Sweep: unbeteiligte Notizen/None bleiben unveraendert",
|
||||
_sw_litters[1]["Notes"] is None
|
||||
and _sw_litters[2]["Notes"] == "Eltern: Blacky + Bonnie")
|
||||
check("Notiz-Sweep: Zaehler = Zahl der geaenderten Notizen", _sw_n == 3)
|
||||
# Kein _pre_rename_name → kein Sweep.
|
||||
_sw_none = [{"Name": "Kuke", "Notes": "Kruke stand hier nie zur Debatte"}]
|
||||
check("Notiz-Sweep: ohne renameTo passiert nichts",
|
||||
m.apply_rename_note_sweep(_sw_none, []) == 0)
|
||||
|
||||
# Guards gegen Kollateralschaden (alle vier im ersten Probelauf real aufgetreten):
|
||||
# (1) Zu kurzer Quellname („M") darf keine Abkuerzungen im Freitext treffen.
|
||||
_sw_short = [{"Name": "JD", "_pre_rename_name": "M",
|
||||
"Notes": "Andreas B.; 60g; M: 17.3.19"},
|
||||
{"Name": "Milly", "Notes": "Abgabe an Natascha M."}]
|
||||
check("Notiz-Sweep: Quellname < 4 Zeichen wird ignoriert",
|
||||
m.apply_rename_note_sweep(_sw_short, []) == 0
|
||||
and _sw_short[1]["Notes"] == "Abgabe an Natascha M.")
|
||||
# (2) Mehrwortige Phrase (Zucht-Sentinel als Quellname) wird ignoriert.
|
||||
_sw_phrase = [{"Name": "Namenlose Tochter von Unity",
|
||||
"_pre_rename_name": "Zucht der Kleinen Chaoten",
|
||||
"Notes": "Herkunft: Zucht der Kleinen Chaoten"}]
|
||||
check("Notiz-Sweep: mehrwortige Phrase als Quellname wird ignoriert",
|
||||
m.apply_rename_note_sweep(_sw_phrase, []) == 0)
|
||||
# (3) Alter Name steckt im neuen Rufnamen → keine Verdopplung des Zusatzes.
|
||||
_sw_dbl = [{"Name": "Missy gen. Krümmel", "_pre_rename_name": "Missy",
|
||||
"Notes": "Kinder: Xaki, Rover, Missy gen. Krümmel"}]
|
||||
check("Notiz-Sweep: kein Verdoppeln, wenn der alte Name im neuen Rufnamen steckt",
|
||||
m.apply_rename_note_sweep(_sw_dbl, []) == 0
|
||||
and _sw_dbl[0]["Notes"] == "Kinder: Xaki, Rover, Missy gen. Krümmel")
|
||||
# (4) Notizen, die die Umbenennung selbst dokumentieren, behalten den alten Namen.
|
||||
_sw_doc = [{"Name": "Gale von den Kleinen Chaoten", "_pre_rename_name": "Drake Jr",
|
||||
"Notes": "Hieß ursprünglich Drake Jr, wurde später in Gale umbenannt."}]
|
||||
_sw_doc_litters = [{"Notes": "Parents: Jacky + Drake Jr"}]
|
||||
_sw_doc_n = m.apply_rename_note_sweep(_sw_doc, _sw_doc_litters)
|
||||
check("Notiz-Sweep: Umbenennungs-Dokumentation bleibt erhalten",
|
||||
_sw_doc[0]["Notes"] == "Hieß ursprünglich Drake Jr, wurde später in Gale umbenannt.")
|
||||
check("Notiz-Sweep: normale Notiz desselben Paares wird trotzdem gefixt",
|
||||
_sw_doc_litters[0]["Notes"] == "Parents: Jacky + Gale" and _sw_doc_n == 1)
|
||||
|
||||
|
||||
# ── Decision-Overrides: goHomeDate / genotype (autoritativ) / spottingType ────
|
||||
# Tickets 24522f5f (JackJack: kaputtes Abgabedatum), 7037f5d8 (Merle: Abgabe zieht
|
||||
# Status GivenAway nach), a547be62 (Eliza/Chris: Gencode + Scheckungsart).
|
||||
def _ovr(gerbils, decisions):
|
||||
return m.apply_decision_overrides(gerbils, decisions, {}, None)
|
||||
|
||||
|
||||
# (1) goHomeDate ist AUTORITATIV — ueberschreibt auch einen (kaputten) Bestandswert.
|
||||
_g_jj = {"Id": "1", "ExternalRef": "wc-0002", "Name": "JackSack",
|
||||
"DateOfBirth": "2010-02-18", "Status": "Deceased",
|
||||
"GoHomeDate": "1310-05-13"}
|
||||
_ovr([_g_jj], [{"externalRef": "wc-0002", "goHomeDate": "13.05.2010"}])
|
||||
check("goHomeDate: kaputtes Abgabedatum wird ueberschrieben",
|
||||
_g_jj["GoHomeDate"] == "2010-05-13")
|
||||
check("goHomeDate: Status Deceased bleibt Deceased",
|
||||
_g_jj["Status"] == "Deceased")
|
||||
|
||||
# (2) goHomeDate zieht bei nicht-verstorbenen Tieren Status GivenAway nach (Merle).
|
||||
_g_merle = {"Id": "2", "ExternalRef": "stammbaum-merle-18062023", "Name": "Merle",
|
||||
"DateOfBirth": "2023-06-18", "Status": "Breeding", "IsResident": True}
|
||||
_ovr([_g_merle], [{"name": "Merle", "dob": "18.06.2023",
|
||||
"externalRef": "stammbaum-merle-18062023",
|
||||
"goHomeDate": "22.12.2024"}])
|
||||
check("goHomeDate: Abgabedatum gesetzt", _g_merle["GoHomeDate"] == "2024-12-22")
|
||||
check("goHomeDate: Status wird GivenAway", _g_merle["Status"] == "GivenAway")
|
||||
check("goHomeDate: Residenz bleibt unangetastet", _g_merle["IsResident"] is True)
|
||||
|
||||
# (3) Unplausibles Datum in der Entscheidung wird verworfen (kein Status-Wechsel).
|
||||
_g_bad = {"Id": "3", "ExternalRef": "wc-9", "Name": "Bad", "Status": "Breeding"}
|
||||
_ovr([_g_bad], [{"externalRef": "wc-9", "goHomeDate": "13.05.1310"}])
|
||||
check("goHomeDate: implausibles Datum -> kein Abgabedatum, kein Status-Wechsel",
|
||||
_g_bad.get("GoHomeDate") is None and _g_bad["Status"] == "Breeding")
|
||||
|
||||
# (4) Gencode-Override ist bei PRAEZISEM Match autoritativ (Eliza: der Bestandswert
|
||||
# ist nur der Kanon-Fill des Farbschlags) und setzt zusaetzlich die Scheckungsart.
|
||||
_g_eliza = {"Id": "4", "ExternalRef": "page_0013.md-g1000000-011", "Name": "Eliza",
|
||||
"DateOfBirth": "2010-08-20", "Genotype": "aa CC DD EE GG PP Spsp rere"}
|
||||
_ovr([_g_eliza], [{"externalRef": "page_0013.md-g1000000-011",
|
||||
"genotype": "aa C- DD E- G- P- Spsp",
|
||||
"spottingType": "Ansatzschecke"}])
|
||||
check("genotype: praeziser Override ueberschreibt den Kanon-Fill",
|
||||
_g_eliza["Genotype"] == "aa C- DD E- G- P- Spsp")
|
||||
check("spottingType: Scheckungsart per Entscheidung gesetzt",
|
||||
_g_eliza["SpottingType"] == "Ansatzschecke")
|
||||
|
||||
# (5) name+dob ist ebenfalls praezise, ein Namensvetter mit anderem dob nicht.
|
||||
_g_named = {"Id": "5", "Name": "Chris", "DateOfBirth": "2010-08-20",
|
||||
"Genotype": "aa CC DD EE gg PP Spsp rere"}
|
||||
_g_cousin = {"Id": "6", "Name": "Chris of Elsewhere", "DateOfBirth": "2014-01-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp"}
|
||||
_ovr([_g_named, _g_cousin], [{"name": "Chris", "dob": "20.08.2010",
|
||||
"genotype": "aa Cc[-] DD Ee gg P- Spsp"}])
|
||||
check("genotype: name+dob-Match ist praezise und ueberschreibt",
|
||||
_g_named["Genotype"] == "aa Cc[-] DD Ee gg P- Spsp")
|
||||
check("genotype: gleichnamiges Tier mit anderem dob bleibt unberuehrt",
|
||||
_g_cousin["Genotype"] == "AA CC DD EE GG PP spsp")
|
||||
|
||||
# (6) name-only-Entscheidung (unpraezise) darf einen vorhandenen Gencode NICHT
|
||||
# ueberschreiben — sie kann auf einen Namensvetter geleakt sein (Merle of Samsimar).
|
||||
_g_only = {"Id": "7", "Name": "Vetter", "DateOfBirth": "2012-09-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp"}
|
||||
_ovr([_g_only], [{"name": "Vetter", "genotype": "aa CC DD ee GG PP Spsp"}])
|
||||
check("genotype: name-only-Override ueberschreibt vorhandenen Gencode NICHT",
|
||||
_g_only["Genotype"] == "AA CC DD EE GG PP spsp")
|
||||
|
||||
# (7) ... fuellt aber weiterhin ein LEERES Feld (bisheriges Verhalten, z. B. Jana/Kyara).
|
||||
_g_empty = {"Id": "8", "Name": "Jana", "DateOfBirth": "2010-01-01", "Genotype": None}
|
||||
_ovr([_g_empty], [{"name": "Jana", "genotype": "aa CC DD Ee GG PP spsp"}])
|
||||
check("genotype: name-only-Override fuellt ein leeres Feld weiterhin",
|
||||
_g_empty["Genotype"] == "aa CC DD Ee GG PP spsp")
|
||||
|
||||
# (8) Eine Entscheidung OHNE die neuen Schluessel bleibt fuer sie ein No-Op.
|
||||
_g_plain = {"Id": "9", "Name": "Plain", "DateOfBirth": "2020-01-01",
|
||||
"Genotype": "AA CC DD EE GG PP spsp", "Status": "Breeding"}
|
||||
_ovr([_g_plain], [{"name": "Plain", "dob": "01.01.2020", "isResident": True}])
|
||||
check("Overrides: ohne genotype/spottingType/goHomeDate keine Nebenwirkung",
|
||||
_g_plain["Genotype"] == "AA CC DD EE GG PP spsp"
|
||||
and _g_plain.get("SpottingType") is None
|
||||
and _g_plain.get("GoHomeDate") is None
|
||||
and _g_plain["Status"] == "Breeding")
|
||||
|
||||
|
||||
# ── Integration: assert the resolved_import.json output reflects the ticket fixes ──
|
||||
# (Only when the pipeline has already been run; tolerant if the file is absent.)
|
||||
import os as _os, json as _json
|
||||
@@ -766,7 +929,9 @@ if _os.path.exists(_resolved):
|
||||
if _el:
|
||||
check("E-Wurf 2010: father is Blacky", _el.get("FatherId") is not None and _G[_el["FatherId"]]["Name"].startswith("Blacky"))
|
||||
# Ticket e0a0c304: 'Kruke' war ein Tippfehler → korrekt 'Kuke'.
|
||||
check("E-Wurf 2010: mother is Kuke", _el.get("MotherId") is not None and _G[_el["MotherId"]]["Name"] == "Kuke")
|
||||
# Ticket 24006ccd: renameTo setzt jetzt den vollen Zuchtnamen
|
||||
# 'Kuke von Marion Knoss' (Merge mit dem Chart-Ahnenrecord), daher Prefix-Pruefung.
|
||||
check("E-Wurf 2010: mother is Kuke", _el.get("MotherId") is not None and _G[_el["MotherId"]]["Name"].startswith("Kuke"))
|
||||
check("Eliza: shares same litter with Eddy", _eliza.get("LitterId") == _eddy.get("LitterId"))
|
||||
|
||||
# Zeus + Beatrice vs Eddy + Sheila same-date litters (2012-06-13)
|
||||
|
||||
Reference in New Issue
Block a user