Compare commits
2 Commits
5a6d1c2541
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c3f87f01 | |||
| 748ce4733f |
37
CLAUDE.md
37
CLAUDE.md
@@ -216,6 +216,16 @@ python test_extract.py test_extract_docx.py test_genotype.py test_merge_resol
|
||||
`gerbil-manager-web/src/genetics`). Unbekanntes Allel = `-` (nicht `?`).
|
||||
E-Locus: `ee`=Fuchs, `eef`=Fuchsschimmel, `efef`=Schimmel. Rezessiver Fuchs ist
|
||||
zwingend homozygot → `e-` ist ungültig (wirft), `ee[-]`→`ee`.
|
||||
- **C-Locus-Zygotie (GEN-6, Tickets bde4ec70/f89e95ad — Regel der Züchterin):** Platin,
|
||||
Saphir und Platin-Hell unterscheiden sich **nur** in der C-Zygotie: `aa CC …pp` = **Platin**,
|
||||
`aa Cc[chm] …pp` = **Saphir**, `aa Cc[h] …pp` = **Platin-Hell**. Der Katalog-Match kann
|
||||
deshalb pro Locus ein **exaktes, ungeordnetes Allelpaar** verlangen — Token-Form `"x/y"`
|
||||
(z. B. `C: 'C/cchm'`), sonst reduziert `locusToken()` auf das dominante Allel und der erste
|
||||
Treffer (Platin) verschluckt die anderen. Gilt in **beiden** Mirrors (`catalog.ts` +
|
||||
`genotype.py`) und im Seed (`colorVarietySeed.*`, `ApplicationContext.SeedColorVarieties`,
|
||||
dort in Kompaktnotation `Ccchm`/`Cch` ohne Klammern).
|
||||
- **`c[hm]` ist ein Schreibfehler-Alias für `c[chm]`** (fehlendes c in einigen Stammbaum-Charts):
|
||||
beide Parser normalisieren ihn, sonst ist der Gencode unparsebar bzw. ergibt „…-Hell".
|
||||
- **Genetik-Engine = korrektheitskritisch: IMMER vollständig mit Tests absichern.**
|
||||
Jede Änderung an `src/genetics/**` (+ Backend-Mirror + `genotype.py`) braucht pro
|
||||
Use-Case/Ticket einen Regressionstest (`src/genetics/__tests__/genetics.test.ts`
|
||||
@@ -330,6 +340,33 @@ python test_extract.py test_extract_docx.py test_genotype.py test_merge_resol
|
||||
- **EMF/WMF sind keine Fotos**: Excel legt neben dem Foto teils ein Vektor-Metafile ab; Browser
|
||||
können es nicht darstellen (kaputte Bildkachel in der Akte) → `extract._attach_photos`
|
||||
überspringt `.emf`/`.wmf` (Regressionstest in `test_extract.py`).
|
||||
- **Resolution-Schlüssel in `conflict-decisions.json` (Stand 2026-08-19)**: neben
|
||||
`father/mother(+Dob)`, `isResident`, `notes`, `receiver`, `dateOfDeath`, `deceased`,
|
||||
`correctDob`, `renameTo`, `mergeExternalRefs`, `suppressExternalRefs`, `farbschlag` auch
|
||||
**`goHomeDate`** (autoritativ, überschreibt auch einen vorhandenen/kaputten Wert und zieht
|
||||
Status `GivenAway` nach, sofern nicht `Deceased`) und **`spottingType`** (Scheckungsart —
|
||||
der Importer leitet sie aus keiner Quelle ab; `IngestResolvedService` überträgt sie nur,
|
||||
wenn der Payload einen Wert hat, sonst bliebe eine von Hand erfasste Angabe nicht stehen).
|
||||
**`genotype`** gewinnt bei **präzisem** Match (externalRef ODER name+dob) auch gegen einen
|
||||
bereits gefüllten Wert — bei Wurfchronik-Tieren ist der meist nur der Kanon-Fill des
|
||||
Farbschlags; ein name-only-Match füllt weiterhin nur leere Felder (Namensvetter-Schutz).
|
||||
- **Pro Tier greift nur EINE Resolution** (first match: externalRef → endswith → name+dob →
|
||||
name-only). Nie einen zweiten Eintrag mit derselben externalRef/demselben (name,dob) anlegen,
|
||||
sondern den bestehenden **in place** erweitern — sonst gewinnt still der letzte und Felder
|
||||
gehen verloren. **Wurfchronik-Jungtiere haben zur Override-Zeit noch kein DOB** (das kommt
|
||||
später aus dem Wurfdatum) → dort ist `externalRef` der einzige zuverlässige Schlüssel.
|
||||
- **`renameTo` zieht durch Freitext-Notizen** (`apply_rename_note_sweep`, letzter Schritt vor
|
||||
dem Payload-Dump): der alte Name blieb sonst in Wurf-Notizen stehen („Blacky + Kruke v. …").
|
||||
Wortgenau, mit Guards — Quellname < 4 Zeichen oder > 2 Wörter wird ignoriert, kein Ersetzen,
|
||||
wenn der alte Name im neuen Rufnamen steckt, und Notizen, die die Umbenennung selbst
|
||||
dokumentieren („umbenannt"/„ursprünglich"), bleiben unberührt. Ohne diese Guards trifft der
|
||||
Sweep Abnehmer-Namen und Gewichtsnotizen (im Probelauf 16 von 22 Ersetzungen falsch).
|
||||
- **`parse_date` verwirft implausible Jahre** (< 1900 oder > aktuelles Jahr + 1) → `None`;
|
||||
Wurfchronik-Transkriptionen bauen Daten sonst falsch zusammen (JackJack: „1310-05-13").
|
||||
- **Sternloses Geburtsdatum in Stammbaum-Charts**: `extract.py` normalisiert Zellen wie
|
||||
`22.08.2019/+09.04.2024` (ohne `*`) zu `*…`, wenn Block-Kontext vorliegt (Genotyp ≤3 Zeilen
|
||||
darunter, Namenszelle ≤3 darüber). Ohne das erkennt die Blockheuristik das Tier nicht und
|
||||
`_reconstruct_parents` greift in den Nachbar-Ast (Ticket 65266679 Bijou).
|
||||
- **`renameTo` ist eltern-verknüpfungssicher**: umbenannte Tiere werden im Eltern-Namensindex
|
||||
zusätzlich unter ihrem **Quell-Namen** (`_pre_rename_name`) geführt, sonst verlieren Würfe, die
|
||||
den alten Namen referenzieren, ihren Eltern-Link (Ticket e0a0c304 Kruke→Kuke). `dateOfDeath`
|
||||
|
||||
@@ -9,6 +9,40 @@ die beklagten Dubletten stecken also schon in `resolved_import.json`. Mehrere Ti
|
||||
sind Folge-Tickets aus dem Juli-Lauf, deren Entscheidungen die neuen Chart-Varianten nicht mehr
|
||||
greifen (Wurfchronik-Elternstubs ohne Geburtsdatum).
|
||||
|
||||
## Ergebnis des Laufs (2026-08-19/20)
|
||||
|
||||
**Alle 16 Tickets sind Resolved**, jeweils mit Changelog fuer die Zuechterin; in Prod sind
|
||||
0 Tickets offen. Umsetzung in 5 datei-disjunkten Workstreams (Importer-Logik + Ingest-Backend,
|
||||
Stammbaum-Extraktion, Genetik-Engine, Frontend, Daten-Entscheidungen), Verifikation zentral:
|
||||
Python-Tests, 158 vitest, 325 Playwright, 290 dotnet-Tests gruen; Migration
|
||||
`ReseedColorVarietiesGen6Saphir`; Deploy via CI; Prod-Ingest per Upload
|
||||
(`-4 stale gerbils`, `-4 stale litters`, +1 Kontakt, 9 Freeze-Overrides respektiert);
|
||||
27/27 Nachpruefungen gegen die Prod-API gruen.
|
||||
|
||||
### Offene Folgeaufgaben (bewusst NICHT in diesem Lauf)
|
||||
|
||||
- **H2-Wurf 13.04.2013**: listet 6 Jungtiere, die Wurfchronik nennt WS=5 — „Akina von den Kleinen
|
||||
Chaoten" und „Alkina" sind vermutlich dasselbe Tier (Merge braucht eine Entscheidung der Zuechterin).
|
||||
- **`tools/import/genotype.py` kann die interne Kompaktnotation nicht parsen** (`Ccchm`, `cchmcchm`,
|
||||
`Cch`, `efef`) und `c[-]`/`e[-]`-Bracket-Unknowns: 88 Tiere laufen deshalb in den C/E-Guard von
|
||||
`resolve_color_and_genotype` und behalten ihr Text-Label; 29 distinkte Gencodes liefern in TS und
|
||||
Python unterschiedliche Namen. Ein Parser-Fix aktiviert die Farbherleitung von ~55 Tieren auf
|
||||
einmal → eigenes Ticket mit Vorher/Nachher-Liste.
|
||||
- **5 vorbestehende Doppel-Keys in `conflict-decisions.json`** (`('lotta','')`, `('sunny','2014-04-10')`,
|
||||
`('jd','')`, `('blue','')`, `('danny','2009-10-09')`): dort gewinnt still der letzte Eintrag →
|
||||
potenzieller Datenverlust, Aufraeum-Ticket.
|
||||
- **Chart-Zeilenversatz bei tieferen Ahnen** (Akane-Muster) in den neuen Charts: Arya Stark erhaelt
|
||||
Vance x Milka statt Vance x Sansa, Nani erhaelt Udo x Milka statt Udo x Hedwig.
|
||||
- **Ophelie-Dublette**: „Stammbaum von Valentino Firehearts Kids.xlsx" fuehrt K79 „Ophelie von den
|
||||
Kleinen Chaoten" mit Kathlins Geburtsdatum (*20.04.2021) → zweite Ophelie neben der echten
|
||||
(*06.05.2023), vermutlich DOB-Verschreiber in der Quelle.
|
||||
- **Kukes Gencode**: Chart sagt `aa CC DD Ee Gg Pp Spsp` (Schwarz-Schecke), rpro3 sagt
|
||||
Anthrazit-Schecke (`gg`). Es gilt der Chart-Wert; im Ticket-Changelog ist die Rueckfrage vermerkt.
|
||||
- **Merle**: zweiter Wurf mit Smoke vom 19.08.2024 (Tochter Speedy) ist noch nicht erfasst — im
|
||||
Changelog erfragt.
|
||||
- Die drei Merle-Tickets zeigen auf eine tote `gerbilId` (ce05fad7) → Links in der Ticketliste
|
||||
laufen ins Leere; Umhaengen waere ein reiner Feedback-Update.
|
||||
|
||||
## Cluster-Matrix
|
||||
|
||||
| Tickets | Thema | Layer | Cluster | Aufwand | Konfidenz |
|
||||
|
||||
@@ -168,6 +168,10 @@ export function seedDb(): MockDb {
|
||||
gerbil('balu', 'Balu', 'male', '2021-04-20', 'w-balu', 'cv-agouti'),
|
||||
gerbil('maja', 'Maja', 'female', '2021-06-11', null, 'cv-schwarz-schecke', 'aa CC DD EE GG PP Spsp rere'),
|
||||
gerbil('karlsson', 'Karlsson', 'male', '2022-02-02', 'w-karlsson', 'cv-blau'),
|
||||
// FARBSCHLAG-MISMATCH: eingetragen „Schwarz Schecke", Gencode ergibt Agouti Schecke
|
||||
// → der Hinweis „Weicht vom eingetragenen Farbschlag ab" MUSS hier erscheinen.
|
||||
gerbil('mismatch-maus', 'Mismatch Maus', 'female', '2023-02-02', null, 'cv-schwarz-schecke',
|
||||
'AA CC DD EE GG PP Spsp rere'),
|
||||
gerbil('smilla', 'Smilla', 'female', '2022-09-30', null, 'cv-himalaya'),
|
||||
gerbil('anton', 'Anton', 'male', '2019-07-07', 'w-anton', 'cv-agouti'),
|
||||
gerbil('greta', 'Greta', 'female', '2019-05-23', null, 'cv-schwarz'),
|
||||
|
||||
@@ -241,6 +241,23 @@ test('Detailseite: Abgabedatum steht direkt unter dem Abnehmer, auch bei Verstor
|
||||
await expect(goHomeRow.locator('.ak-kv-value')).toHaveText('15.01.2022')
|
||||
})
|
||||
|
||||
test('Detailseite: Farbschlag-Hinweis nur bei echter Abweichung (SCHECKE-MISMATCH)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessMock()
|
||||
// Maja: eingetragen „Schwarz Schecke", Gencode ergibt genau das → KEIN Hinweis.
|
||||
// (Vorher wurde der Zusatz „Schecke" nur auf der errechneten Seite abgeschnitten,
|
||||
// dadurch bekam jede Schecke faelschlich „Weicht vom eingetragenen Farbschlag ab".)
|
||||
await page.goto('/rennmaeuse/maja')
|
||||
const genetik = page.locator('.ak-card', { hasText: t.detail.genetics }).first()
|
||||
await expect(genetik.getByText(t.detail.resolvedPrefix, { exact: false })).toBeVisible()
|
||||
await expect(page.getByText(t.detail.farbschlagMismatch)).toBeHidden()
|
||||
|
||||
// Gegenprobe: bei echter Abweichung erscheint der Hinweis weiter.
|
||||
await page.goto('/rennmaeuse/mismatch-maus')
|
||||
await expect(page.getByText(t.detail.farbschlagMismatch)).toBeVisible()
|
||||
})
|
||||
|
||||
/** 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 })
|
||||
|
||||
@@ -24,6 +24,7 @@ import { combineLocus } from '../punnett'
|
||||
import { LOCI, type LocusKey } from '../loci'
|
||||
import {
|
||||
farbschlagFor,
|
||||
formatVarietyName,
|
||||
genotypeToFarbschlag,
|
||||
representativeGenotype,
|
||||
BASE_COLORS,
|
||||
@@ -963,6 +964,22 @@ describe('GEN-6: C-Locus-Zygotie Platin / Saphir / Platin-Hell (bde4ec70 / f89e9
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatVarietyName: Modifier nicht doppeln', () => {
|
||||
it('haengt „Schecke" an einen Basisnamen an', () => {
|
||||
expect(formatVarietyName('Saphir', 'aa Cc[chm] DD E- G- pp Spsp')).toBe('Saphir Schecke')
|
||||
})
|
||||
|
||||
it('doppelt „Schecke" NICHT, wenn der Katalogname es schon traegt', () => {
|
||||
// Sonst „Schwarz Schecke Schecke" — und der Akten-Vergleich „eingetragen vs. errechnet"
|
||||
// meldete eine Abweichung, obwohl beides dasselbe ist.
|
||||
expect(formatVarietyName('Schwarz Schecke', 'aa CC DD EE GG PP Spsp rere')).toBe('Schwarz Schecke')
|
||||
})
|
||||
|
||||
it('ohne Gencode bleibt der Name unveraendert', () => {
|
||||
expect(formatVarietyName('Saphir', null)).toBe('Saphir')
|
||||
})
|
||||
})
|
||||
|
||||
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] …" —
|
||||
|
||||
@@ -365,8 +365,13 @@ export function formatVarietyName(baseName: string, genotypeStr: string | null |
|
||||
try {
|
||||
const g = fromDisplayString(genotypeStr)
|
||||
const modifiers: string[] = []
|
||||
if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke')
|
||||
if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex')
|
||||
// Einen Modifier NICHT doppeln, wenn der Katalogname ihn schon traegt: Bestaende mit
|
||||
// Namen wie „Schwarz Schecke" ergaeben sonst „Schwarz Schecke Schecke" — und der
|
||||
// Vergleich mit dem errechneten Farbschlag meldet dann eine Abweichung, die es nicht gibt.
|
||||
const words = baseName.toLowerCase().split(/[\s-]+/)
|
||||
const alreadyNamed = (modifier: string) => words.includes(modifier.toLowerCase())
|
||||
if (locusToken(g, 'Sp') === 'Sp' && !alreadyNamed('Schecke')) modifiers.push('Schecke')
|
||||
if (locusToken(g, 'Re') === 'Re' && !alreadyNamed('Rex')) modifiers.push('Rex')
|
||||
return [baseName, ...modifiers].join(' ')
|
||||
} catch {
|
||||
return baseName
|
||||
|
||||
@@ -55,6 +55,11 @@ function describeGenotype(genotype: string | null): { display: string; farbschla
|
||||
}
|
||||
|
||||
/** A labelled key/value row inside a card (semantic dt/dd, styled as a grid). */
|
||||
/** „Saphir Schecke" → „Saphir": Modifier-Zusaetze fuer den Namensvergleich abschneiden. */
|
||||
function stripVarietyModifiers(name: string): string {
|
||||
return name.replace(' Schecke', '').replace(' Rex', '').trim()
|
||||
}
|
||||
|
||||
function Kv({ label, children }: { label: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="ak-kv">
|
||||
@@ -594,10 +599,16 @@ export default function GerbilDetailPage() {
|
||||
return (
|
||||
<>
|
||||
{resolvedFarbschlag}
|
||||
{/* Vergleich OHNE die Modifier-Zusaetze auf BEIDEN Seiten: der
|
||||
eingetragene Name traegt „Schecke"/„Rex" bereits (formatVarietyName
|
||||
haengt sie aus dem Gencode an), der errechnete ebenfalls. Vorher wurde
|
||||
der Zusatz nur rechts entfernt → jede Schecke bekam faelschlich den
|
||||
Hinweis „Weicht vom eingetragenen Farbschlag ab" (z. B. JackJack:
|
||||
„Saphir Schecke" vs. „Saphir Schecke"). */}
|
||||
{storedColorName &&
|
||||
resolvedFarbschlag !== de.genetics.unknownFarbschlag &&
|
||||
storedColorName !==
|
||||
resolvedFarbschlag.replace(' Schecke', '').replace(' Rex', '') && (
|
||||
stripVarietyModifiers(storedColorName) !==
|
||||
stripVarietyModifiers(resolvedFarbschlag) && (
|
||||
<small className="ak-mismatch"> ⚠ {t.detail.farbschlagMismatch}</small>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user