Files
GerbilManager/CLAUDE.md
Gulum 748ce4733f
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m12s
CI / Docker Build & Push (push) Has been cancelled
CI / Deploy auf TrueNAS (Custom App) (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
docs: Dauerwissen aus dem Ticket-Lauf + Ergebnis/Folgeaufgaben
CLAUDE.md: C-Locus-Zygotie (Platin/Saphir/Platin-Hell, Paar-Token "x/y"),
c[hm]-Alias, die neuen Resolution-Schlüssel goHomeDate/spottingType und die
Präzedenz des genotype-Overrides, "pro Tier nur EINE Resolution" (inkl. der
Falle, dass Wurfchronik-Jungtiere zur Override-Zeit kein DOB haben), der
renameTo-Notiz-Sweep mit seinen Guards, der parse_date-Jahresguard und die
sternlosen Geburtsdaten in Stammbaum-Charts.

docs/ticket-triage.md: Ergebnis des Laufs (16/16 Resolved, 27/27 Prod-Checks)
und die acht bewusst offen gelassenen Folgeaufgaben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:16:00 +02:00

435 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — GerbilManager
Leitfaden für die Arbeit an diesem Repo. Bitte vor Änderungen lesen.
## Was ist das?
Verwaltungs-App für eine **Rennmaus-Zucht** („Zucht der kleinen Chaoten"): Tiere,
Würfe, Stammbäume/Ahnentafeln, Genetik (Farbschläge/Genotypen), Gehege, Kontakte
(Züchter/Abnehmer), Abgabeverträge, Statistik und eine öffentliche CMS-Webseite.
Single-User (die Züchterin), Daten größtenteils aus bestehenden Quelldateien
importiert.
## Architektur / Projekte
| Projekt | Zweck |
|---|---|
| `GerbilManager.AppHost` | **.NET Aspire** Orchestrator — startet PostgreSQL (Container) + WebAPI |
| `GerbilManagerWebAPI` | Backend: ASP.NET **Minimal API**, EF Core + Postgres, Endpoints in `Endpoints/`, Entities in `Models/`, DTOs in `Dtos/` |
| `GerbilManager.ServiceDefaults` | Aspire-Defaults (Telemetry/Health) |
| `GerbilManager.Tests` | Backend-Tests (xUnit) |
| `gerbil-manager-web` | Frontend: **React + Vite + TypeScript** (React Router, react-d3-tree für den Stammbaum) |
| `tools/import` | **Python**-Import-Pipeline (zero-dep), wandelt Quelldateien → `resolved_import.json` |
## App starten (IMMER über Aspire, nicht nur die WebAPI)
```bash
dotnet run --project GerbilManager.AppHost
```
- WebAPI: http://localhost:5179 · OpenAPI: `/openapi/v1.json`
- Aspire-Dashboard: https://localhost:17004 (Login-Token steht im AppHost-Log)
- EF-Migrationen werden beim Start automatisch angewandt (`db.Database.Migrate()` in `Program.cs`).
- Postgres läuft als Container (Name wechselt pro Run, z. B. `postgres-xxxx`); die
**Daten liegen in einem persistenten Volume** → überleben Neustarts. DB-Name: `gerbilmanager`.
## Produktion / Deployment (TrueNAS)
Prod läuft als **TrueNAS SCALE Custom-App** `gerbilmanager` auf dem Host **`truenas`**
(LAN-IP `192.168.2.115`, TrueNAS 25.10 „Goldeye", Docker). Erstinstallation 2026-07-19.
- **Zugriff (Bookmark der Züchterin):** **http://truenas:8090/** · API-Doku: `/scalar`.
(Port 80 ist durch den nginx-Reverse-Proxy der NAS belegt → App auf **8090**, LAN-only, keine Auth.)
- **Sichtbar unter Apps**, weil via `midclt call app.create {custom_app:true, app_name:"gerbilmanager",
custom_compose_config_string:<yaml>}` angelegt — **nicht** via `docker compose` (das taucht in Apps
NICHT auf). Container: `ix-gerbilmanager-{db,api,frontend,backup}-1`.
- **5 Dienste:** `db` (postgres:18) · `api` (.NET, EF-Migrationen laufen beim Start) · `frontend`
(nginx: SPA + `/api`-Proxy → `api:8080`, Host-Port 8090) · `publicsite` (nginx, **nur intern**,
Host-Port **8081** — serviert statisches HTML aus dem `publicsite`-Volume, das die API via
`POST /api/publish` schreibt; ohne Publish leer → 404) · `backup` (täglich 03:00 pg_dump +
Foto-Archiv, Shell-Scheduler).
- **App-Home auf dem Pool:** `/mnt/JailStorage/DockerVolumes/gerbilmanager/` mit
`pgdata/ photos/ keys/ backups/ scripts/ publicsite/` und `deploy/truenas/{custom-app.compose.yaml,.env,scripts}`.
Images aus der Gitea-Registry `git.rismer.de/gulum/gerbilmanager-{api,frontend}:latest`.
### Deployen
**Automatisch (Regelfall):** Push auf `main` → Gitea Actions (`.gitea/workflows/ci.yml`): Tests →
Images bauen+pushen → Job `deploy` verbindet sich per SSH auf den NAS-Host und ruft
`deploy/truenas/scripts/truenas-deploy.sh` (→ `midclt app.redeploy`, zieht `:latest` neu, `pgdata`/
`photos` bleiben erhalten). Braucht die Gitea-Repo-Secrets **`NAS_SSH_HOST`** (=192.168.2.115) und
**`NAS_SSH_KEY`** (privater Deploy-Key; Pubkey liegt in `/root/.ssh/authorized_keys`).
**Manuell (auf dem NAS-Host):**
```bash
sh /mnt/JailStorage/DockerVolumes/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh
```
Legt die App an, wenn sie fehlt (create), sonst redeploy. Rendert `custom-app.compose.yaml` mit den
Werten aus `deploy/truenas/.env` (enthält `POSTGRES_PASSWORD` — **nur auf der NAS, nie im Repo**).
**Lokale DB → Prod kopieren** (einmalig gemacht): `pg_dump` der Aspire-DB → `psql`-Restore in den
`ix-gerbilmanager-db-1`-Container (dabei `api` kurz stoppen); Fotos aus `GerbilManagerWebAPI/photo-storage/`
per `tar | ssh` nach `…/gerbilmanager/photos/`.
### Deployment-Stolperfallen
- **`/opt` ist auf Goldeye read-only** → App-Home MUSS auf den Pool (`/mnt/JailStorage/...`).
- **`postgres:18`** legt PGDATA in `/var/lib/postgresql/18/docker` ab und deklariert das Volume als
`/var/lib/postgresql`. Mount auf `/var/lib/postgresql/data` (alte v≤17-Konvention) lässt v18 **nicht
starten** ("data in unused mount/volume") → db unhealthy → App-Rollback. Immer `/var/lib/postgresql`
mounten. (Debian-Image, nicht alpine → Locale `libc/en_US.utf8` = Quell-DB.)
- Gitea-Runner ist **containerisiert** (Label `ubuntu-latest`, kein `goldeye`, kein Host-/`midclt`-Zugriff)
und läuft **`maxParallel=1`** (Jobs sequenziell) — daher der SSH-Weg im Deploy-Job.
- **Auto-Deploy lief bis 2026-08-19 nie durch** (Runs 99107 rot), zwei Ursachen: (1) die Repo-Secrets
`NAS_SSH_HOST`/`NAS_SSH_KEY` waren nie angelegt → `ssh root@` mit leerem Key („Could not resolve
hostname"); jetzt gesetzt, Deploy-Key `gitea-ci-deploy@gerbilmanager` (privat nur im Gitea-Secret,
Pubkey in `/root/.ssh/authorized_keys`). (2) Der Runner hat **kein `rsync`**, und der apt-Guard prüfte
nur `ssh` → „rsync: command not found". Dateitransfer läuft daher per **`tar | ssh`** (tar gibt es auf
beiden Seiten; `-xof` erhält die Modi, Skripte bleiben ausführbar).
- CI-Run-Status notfalls aus der Gitea-DB: `docker exec -e PGPASSWORD=gitea ix-gitea-postgresdb-1
psql -U gitea -d gitea -c "select name,status from action_run_job where run_id=(select max(id) from action_run);"`
(`status`: 1=success, 2=failure, 4=skipped).
## Import → DB (3-stufig, WICHTIG)
Die DB wird **nicht** direkt von Python beschrieben. Nach jeder Import-Code-Änderung
neu generieren UND neu einspielen, sonst zeigt die App den alten Stand.
> **Arbeitsweise (verbindlich, Owner-Regel — aktualisiert 2026-07-12):** Für **variable**
> (von der Züchterin **nicht** angefasste) Import-Tiere gilt weiter: Datenfehler IMMER **im
> Importierer** beheben (Pipeline-Code + `conflict-decisions.json` / `rpro3-decisions.json`)
> und **neu importieren** — für diese Tiere **nicht** die Live-DB patchen. Jeder Importer-Fix
> muss **re-ingest-stabil** sein (data-driven, deterministische GUIDs).
>
> **Neu:** Das Ziel ist **nicht** mehr „Importer perfekt, dann einmal final", sondern
> **Tier für Tier konvergieren**. Die Züchterin **darf** über die UI kuratieren, und das
> **überlebt den Ingest** (der wischt **nicht** mehr alles): (a) **manuell angelegte Tiere/
> Würfe/Kontakte** (`IsManual=true`) und (b) **Overrides** (`GerbilOverride`) für als
> **„vollständig korrekt"** markierte bzw. **von Hand bearbeitete** Tiere. Deren eigene Felder
> werden nach jedem Ingest **eingefroren** (Freeze). Siehe „Geprüfte Tiere / Freeze" unten.
> Zusätzlich zu diesen Overrides überleben weiterhin **Tickets/Feedback**.
```bash
# 1. Stammbäume + Wurfchronik → animals.json/litters.json (+ Fotos)
python tools/import/extract.py
# 2. Abgabeverträge (DOCX) → contracts.json
python tools/import/extract_contracts.py
# 3. Dedup, Eltern-Resolver, Provenance, Vertrags-Anreicherung → resolved_import.json
python tools/import/merge_and_resolve.py
# 4. In die laufende DB laden (API muss laufen): UPSERT + Freeze
curl -X POST http://localhost:5179/import/ingest-resolved
# 4b. Gegen PROD (ohne SSH): resolved_import.json per HTTP hochladen + ingesten
curl -X POST http://truenas:8090/api/import/ingest-resolved/upload \
-F "resolved=@tools/import/output/resolved_import.json" # [-F "photos=@neue_fotos.zip"] nur bei neuen Fotos
```
- **`POST /import/ingest-resolved/upload`** (multipart `resolved` + optional `photos`-ZIP): staged die
hochgeladene Datei serverseitig in `Import:SourcePath` und ruft denselben UPSERT-Ingest — damit läuft
der Import **gegen Prod ohne SSH/`docker cp`**. Der alte `POST /import/ingest-resolved` (liest die
serverseitige Datei) bleibt unverändert. **DB-Copy vom lokalen Stand ist verboten**, sobald Prod
kuratiert wurde — nur der Upsert-Ingest bewahrt manuelle/verified/feedback-Zeilen.
- **`IngestResolvedService` (seit 2026-07-12: UPSERT statt Total-Wipe).** Importierte Zeilen
(Gerbils/Litters/Contacts) werden **per deterministischer Id gematcht und in place
aktualisiert**; **stale** importierte Zeilen (nicht mehr im Payload) werden entfernt (dangling
Referenzen manueller Zeilen werden vorher genullt + im Rückgabestring gewarnt). **Nie gelöscht/
überschrieben:** Zeilen mit `IsManual=true` sowie **user-eingegebene Sub-Records**
(Weights/Health) und Fotos manueller Tiere. **SaleContracts** werden weiter aus dem Payload
neu erzeugt (manuelle Verträge sind noch **nicht** geschützt — Folgeaufgabe). Am Ende werden
alle **`GerbilOverride`** angewandt (**Freeze** der eigenen Felder) und für verifizierte Tiere
der Rohimport-Stand vorher als Drift-Snapshot erfasst.
- **Feedback + GerbilOverride überleben den Ingest** (werden nicht gewischt) — daher haben sie nur
**lose, nullable Guid-Spalten ohne FK** (`Feedback`: GerbilId/LitterId/ContactId; `GerbilOverride`:
GerbilId). Dieses Muster für alles nutzen, was Re-Ingests überleben soll.
- `resolved_import.json` ist **gitignored** (Runtime-Output); `review-report.md` ist getrackt.
- Deterministische GUIDs via `generate_guid("...")` → stabile IDs über Re-Ingests.
### Quelldateien (lokal/Netzwerk)
- Stammbäume: `C:\Users\gulum\dev\Sttammbäume\*.xlsx`
- Wurfchronik: `C:\Users\gulum\dev\Wurfchronik der Kleine Chaoten Teil1.xlsx`
- Abgabeverträge: `\\truenas\Datengrab\Rennmäuse\Verträge\*.docx` (~1,4k; kann zeitweise offline sein)
## Build & Test
**Frontend** (`cd gerbil-manager-web`):
```bash
npx tsc --noEmit # Typecheck (muss sauber sein)
npx vitest run # Unit-Tests
npx eslint <dateien> # Lint
npx playwright test [spec]# e2e (Projekte: desktop + phone), läuft gegen Mock-API
```
- e2e nutzt einen **Mock-Modus** (`e2e/mock-api.ts` + `e2e/mock-data.ts`), kein echtes Backend.
Neue API-Routen/Felder dort spiegeln. `skipUnlessMock()` am Testanfang.
**Backend**:
```bash
dotnet build
dotnet test # GerbilManager.Tests
dotnet ef migrations add <Name> --project GerbilManagerWebAPI # neue Migration
```
- Zum Bauen muss eine laufende WebAPI ggf. gestoppt werden (Datei-Lock auf `bin`).
**Python** (`cd tools/import`, zero-dep, exit 0 = pass):
```bash
python test_extract.py test_extract_docx.py test_genotype.py test_merge_resolve.py test_extract_contracts.py
```
## Konventionen (bitte einhalten)
- **Alle deutschen UI-Texte in `gerbil-manager-web/src/strings/de.ts`** — niemals in
Komponenten hartkodieren.
- **EF-Migration** für jede Schema-Änderung; wird beim Aspire-Start angewandt.
- **Provenance/Datenherkunft** liegt als **JSON-Text-Spalte** (`Provenance`) auf
Gerbil/Contact/Litter → JSON-Feld-Erweiterungen brauchen **keine** Migration.
- **Lose nullable Guid-Spalten ohne FK** für Tabellen, die den Ingest-Wipe überleben.
- Commits: nur wenn der Nutzer es verlangt; auf `main` wird gearbeitet (Remote: `http://truenas:13000/Gulum/GerbilManager.git`). Commit-Messages auf Deutsch, Format `feat(scope): …`.
- Subagenten committen NICHT — Haupt-Loop verifiziert und committet.
## Domänenwissen (Rennmaus-Zucht)
- **Lebensspanne ~6 Jahre** → ein Elternteil kann max. ~6 Jahre älter als sein Kind
sein und muss vorher geboren sein (`parent_age_plausible` im Import; verwirft
unplausible Eltern-Links).
- **Geschwisterverpaarung** (Vollgeschwister als Eltern): erkannt, wenn Vater & Mutter
**dieselbe `litterId`** ODER **dasselbe Geburtsdatum** teilen (gleiches DOB = starkes
Indiz). UI: Diagramm führt den Vorfahren-Ast zusammen (Verweis-Knoten); Akte zeigt
„⚭ Geschwisterverpaarung"-Chip.
- **Box-Farbe im Stammbaum-xlsx = Geschlecht**: weiß = weiblich, blau = männlich.
- **Bestandstier (`isResident`) vs. reiner Vorfahre** (Regel der Züchterin, Ticket 09bcac78):
Ein Tier ist **ihr Zuchttier** ⇔ es hatte bei ihr Nachzucht mit Herkunft **„von den
Kleinen Chaoten"** (nur dann konnten die Jungtiere ihren Zuchtnamen tragen). Trägt die
Nachzucht einen **fremden** Herkunftsnamen (z. B. „of Black Forest", „of Topolino"), ist
das Tier nur **Vorfahre** (`isResident=false`, keine Würfe/Charakter-Bereich). **Ausnahme:
zugekaufte Zuchttiere** (fremder Herkunftsname, aber bei ihr gezüchtet → Nachzucht „von den
Kleinen Chaoten") bleiben **Bestandstiere** (z. B. Echo/Elia of Black Forest).
**Sweep umgesetzt (2026-06-27, Ticket 381f7e51):** Der Wurfchronik-Import-Default ist
**nicht mehr `is_resident=True`**. Neue Regel in `merge_and_resolve.py` (später Sweep
nach Eltern-/`addLitters`-Verknüpfung): `isResident=true` **genau dann**, wenn (1) ein
expliziter `conflict-decisions.json`-`isResident`-Override greift (gewinnt immer — `true`
wie Echo/Elia, `false` wie Akane/Naémi/Bentley), **oder** (2) das Tier **Elternteil eines
eigenen Wurfs** ist (nicht-virtuell ODER aus Wurfchronik ODER Clan-Nachzucht ODER mit
residentem Elternteil ODER `addLitter` mit `ShowInChronicle`). Reine virtuelle
Ahnen-Verpaarungen machen **nicht** resident (z. B. Antares bleibt `false`). Sonst
`false`. `ShowInChronicle` wird **nach** dem Sweep berechnet (nutzt die finale Residenz).
Effekt: resident **1711 → 442**. UI-Label (`de.ts` `isResidentLabel`) = **„Zuchtanwärter/
innen"** — die Züchterin markiert künftige Zuchttiere selbst. **Offene Folgeaufgabe:**
manuelle UI-Residenz-Markierungen überleben den Re-Ingest noch **nicht** (bräuchten eine
ingest-stabile, FK-freie Override-Ablage analog `Feedback`).
- **Genotyp/Farbschlag**: 8-Locus-Notation (siehe `tools/import/genotype.py`,
`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`
und `tools/import/test_genotype.py`), damit nie eine Regression entsteht. `npx vitest
run` + `python test_genotype.py` müssen grün sein, bevor „fertig".
- **Stammbaum-Charts**: Generationen = Spaltenbänder (Proband links in Spalte B/2,
je Generation +3 Spalten); Eltern-Position: Vater oben, Mutter unten. Fotos liegen
je nach Datei links/auf der Namenszelle — die Seite wird heuristisch über die
geringste Spalten-Fehlausrichtung bestimmt (`extract._attach_photos`).
## Stolperfallen
- **Aspire-Containername wechselt** pro Run, das Daten-Volume bleibt → DB-Inhalt
überlebt Neustarts (auch alte Schema-Stände → Migrationskonflikte möglich, s. u.).
- **Migrations-Historie vs. committete Migration-IDs**: Wird eine Migration angewandt
und danach mit neuem Zeitstempel neu generiert, sieht EF sie als „pending" und der
Start crasht („column … already exists"). Schema ist dann korrekt — nur die
`__EFMigrationsHistory`-IDs müssen an die committeten Dateinamen angeglichen werden.
- **PowerShell + psql**: PascalCase-Identifier (`"__EFMigrationsHistory"`) über
PowerShell→docker→psql zerschießen das Quoting. Stattdessen Bash-Heredoc:
`docker exec -i -e PGPASSWORD=$PW <container> psql -U postgres -d gerbilmanager <<'SQL' … SQL`.
Passwort: `docker exec <container> printenv POSTGRES_PASSWORD`.
- **Direkte DB-Schreibzugriffe** (psql) werden vom Auto-Mode-Classifier blockiert —
vorher beim Nutzer rückversichern.
- Bash-Tool ist **Git-Bash (POSIX)**, das andere Tool ist **PowerShell** — je eigene Syntax.
## Wo liegt was (Feature-Landkarte)
- **Stammbaum-Viewer**: `gerbil-manager-web/src/pages/StammbaumPage.tsx`,
`src/pedigree/*` (build.ts = Baum-Aufbau + Geschwister-Erkennung; types.ts).
Desktop passt den ganzen Baum ein; Touch: Long-Press öffnet das Kontextmenü
(ID kopieren / Fehler melden).
- **Datenherkunft/Nachverfolgung**: `src/components/ProvenanceDialog.tsx`; erzeugt im
Import via `build_entity_provenance`/`_build_*_history` (chronologische, datei-
attribuierte `history[]`, inkl. „⚠ … verworfen — Grund; … verwendet").
- **Fehler melden (Feedback)**: `src/components/ReportErrorDialog.tsx`,
`src/api/feedback.ts`, Backend `Endpoints/FeedbackEndpoints.cs` + `Models/Feedback.cs`.
- **Geprüfte Tiere / „vollständig korrekt"-Freeze**: Backend `Models/GerbilOverride.cs` (FK-frei,
ingest-überlebend), `Import/GerbilSnapshotService.cs` (Akten-Snapshot + Freeze-Map + Diff, von App
**und** Test geteilt), `Endpoints/VerifiedGerbilEndpoints.cs` (`/verified-gerbils`: POST markieren,
DELETE entfernen/Toggle, GET Liste+Drift, GET `/export` = Golden-Fixture). Freeze wird in
`IngestResolvedService` angewandt; `GerbilEndpoints` setzt `IsManual=true` bei POST und legt bei
Edit eines importierten Tiers einen per-field-Override an (verifiziert → Golden auto-aktualisiert).
Frontend: `src/api/verified.ts`, Badge/Toggle/Drift-Karte in `pages/GerbilDetailPage.tsx`,
Übersicht `pages/VerifizierteTierePage.tsx` (`/hilfe/verifizierte-tiere`); Texte in `strings/de.ts`
(`verified`-Block). **Modell:** `OverrideJson` = Feld→Wert-Map; `IsVerified=true` = zertifiziert +
voller Freeze + Regression/Export, `false` = „geschützt" (nur geänderte Felder). Status/Gehege und
Abstammung/Nachzucht werden **nicht** eingefroren (nur erkannt/gemeldet). Regressionstest:
`GerbilManager.Tests/VerifiedGoldenTests.cs` gegen `tools/import/verified-golden.json` (via
`GET /verified-gerbils/export` erzeugen + committen; skippt, wenn Fixture/`resolved_import.json` fehlt).
**Scope-Grenze:** Freeze/Override gelten für **Gerbils**; Edits an importierten Würfen/Kontakten und
manuelle SaleContracts sind noch **nicht** geschützt (Folgeaufgaben).
- **Verträge**: `Models/SaleContract.cs`, `Endpoints/ContractEndpoints.cs`,
`ContractGenerator.cs`, Frontend `src/pages/Vertraege*`; Import: `extract_contracts.py`
+ `enrich_from_contracts` in `merge_and_resolve.py`.
- **Import-Kernlogik**: `tools/import/merge_and_resolve.py` (Dedup, Eltern-Resolver mit
Gender-/Alters-Plausibilität, Litter-/Sibling-Dedup, Provenance, Vertrags-Anreicherung).
- **RennmausPro-III-Import (rpro3)**: Backend-Importer `GerbilManagerWebAPI/Import/Rpro3/*`
(`Rpro3ImportService` Analyze/Execute, `Rpro3Dedup` = automatischer Dubletten-Merge, Port von
`tools/import/compare_rpro3.py`). **Manuelle Dubletten-Entscheidungen der Züchterin** (aus den
„rpro3-import"-Tickets) liegen in `GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json` und
werden von `Rpro3Dedup.Run(animals, decisions)` honoriert: `same` = Force-Merge (eine rid zieht
den Auto-Cluster mit), `different` = Force-Split, `fields` = Feld-Overrides (Farbe/DOB/Herkunft/
`genotype` (roher Fcode)/`resident`/Notiz) am Cluster. Der `genotype`-Override ist nötig, wenn
Varianten sich im Gencode unterscheiden (z. B. C-Locus c[h] vs. c[chm]) und der Auto-Merge sonst
den falschen Gencode (erstes Cluster-Mitglied) zöge — die Züchterin gibt den korrekten vor
(z. B. MilkyWay → `aa Cc[h] DD ee Gg P- spsp`). **Schlüssel ist die RPRO3-Nummer (rid), NIE der Variantenbuchstabe**
(Buchstaben verschieben sich bei geänderter Clusterbildung) — rids aus dem Ticket-`agentContext`.
Triage-Helfer `tools/import/rpro3_lookup.py` (Name- oder `--rids`-Modus) zieht Farbe/Gencode/
Eltern/Nachzucht+Partner aus `_rpro3.db` für die Rückfragen an die Züchterin.
- **Eltern an der Tier-Akte bearbeiten (QOL)**: Im Tier-Formular gibt es den Block
**„Abstammung"** (`gerbil-manager-web/src/pages/GerbilFormPage.tsx`) mit Wurf-Auswahl +
Vater/Mutter-Picker (`components/AnimalPicker`), damit die Züchterin die Eltern **ohne** den
Umweg über die Wurf-Seite pflegen kann. **Datenmodell unverändert**: Eltern hängen weiter am
Geburtswurf. Backend `PUT /gerbils/{id}/parents` (`GerbilEndpoints`, DTOs `GerbilParentsInput`/
`GerbilParentsResult`, Tests `GerbilManager.Tests/GerbilParentsTests.cs`) schreibt genau dorthin:
hat das Tier einen Wurf → dessen `FatherId`/`MotherId` (gilt zwangsläufig für alle Geschwister,
die UI warnt mit `siblingCount`); hat es keinen → ein Wurf mit **gleichem Elternpaar + gleichem
Datum** wird verknüpft (`litterAttached`), sonst wird ein Träger-Wurf angelegt (`litterCreated`,
Name „Wurf von X + Y", `ShowInChronicle=false`, `IsManual=true`). Geschlechts-Validierung
wiederverwendet `LitterEndpoints.ValidateParents` (400 `InvalidParentGender`).
**Grenze:** Eltern/Abstammung sind vom Freeze **ausgenommen** (`GerbilSnapshotService.FreezeFields`
ohne `litterId`) → an einem **importierten** Wurf überlebt eine Eltern-Korrektur den Re-Ingest
**nicht** (gleiches Verhalten wie beim Editieren über die Wurf-Seite; dauerhaft nur über den
Importer/`conflict-decisions.json`).
- **Manuell ergänzte Würfe / versteckte Würfe**: `conflict-decisions.json` kennt neben
`resolutions`/`addAnimals` auch **`addLitters`** [{name, mother(+motherDob), father, children[],
showInChronicle, totalBorn, date, notes}] — materialisiert in `merge_and_resolve.py` als
**späten** Schritt (nach Dedup/Parent-Roles, vor json.dump): Wurf mit deterministischer
`generate_guid("decision-litter-<slug>")`, verknüpft Mutter/Vater/Kinder per Name(+dob),
setzt Kinder-`LitterId`. Die name+dob-Resolutions können zusätzlich `isResident`, `notes`,
`receiver` (Kontaktname→ReceiverContactId) auf bestehende Tiere setzen (läuft NACH der
Residenz-Propagation). `Litter.ShowInChronicle` (bool, Default true; Migration
`AddLitterShowInChronicle`): `false` blendet den Wurf aus der Wurfchronik
(`WuerfeListPage`, Filter `showInChronicle==true`) aus, zeigt ihn aber weiter auf der
Tier-Akte (parentLitters-Query, ungefiltert). Use-Case: extern entstandener Wurf eines
nicht-residenten Tieres (z. B. Akane × Bonaparte).
- **Über-gemergte Würfe korrigieren**: `conflict-decisions.json` kennt **`litterChildren`**
[{date, matchChild, keep[], renameTo?, note}] — autoritative Kinder-Whitelist eines Wurfs
(später Schritt in `merge_and_resolve.py` nach `litterOrder`). Match per `date` + `matchChild`
(ein aktuell zugeordnetes Kind, disambiguiert gleich-datierte Würfe); Kinder außerhalb `keep`
werden aus dem Wurf gelöst (`LitterId=None`, Tier bleibt erhalten), optional `renameTo` setzt
den kanonischen Wurf-Namen. Use-Case: Wurfchronik-Abnehmer-Sammelseiten, die fälschlich in einen
datierten Buchstaben-Wurf gemergt wurden (Tickets 4b9f49fb M-Wurf, 8d259edf Gale).
**`add`** [Name | {name, dob}] ist das Gegenstück (seit 2026-08-18): hängt ein Jungtier an
**diesen** Wurf, wenn die Chart-Positions-Heuristik es in einen falschen **virtuellen**
Stammbaum-Wurf gezogen hat; wird der alte Wurf dadurch kinderlos UND virtuell, entfernt der
Importer ihn (sonst bliebe ein Phantom-Wurf in der Wurfchronik stehen). Use-Case Akane
(Ticket 88389f8e): die neuen Stammbäume führen die Eltern eine Zeile unter dem Kind-Block
(Vater N80/Mutter N81) — `_reconstruct_parents` greift dann eine Zeile zu hoch und paart die
Mutter des Nachbar-Blocks mit dem echten Vater.
- **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`
ist als Resolution-Override unterstützt (setzt Todesdatum + Status Deceased; rpro3 hat keine
Sterbespalte).
## Verifikations-Checkliste vor „fertig"
1. `npx tsc --noEmit`, `npx vitest run`, `npx eslint` sauber.
2. Betroffene `npx playwright test`-Specs grün.
3. `dotnet build` + `dotnet test` grün.
4. Bei Import-Änderungen: `python test_*.py` grün + `resolved_import.json` plausibel
(Stichprobe), danach App neu starten + `POST /import/ingest-resolved`.
## Gedächtnis (MemPalace) — IMMER nutzen
Dieses Projekt nutzt **MemPalace** als KI-Langzeitgedächtnis (lokal, kein API-Key).
Die Anbindung ist im Repo persistiert und lädt automatisch:
- `.mcp.json` (committet) registriert den MCP-Server `mempalace-mcp` → **19 MCP-Tools**
(`mempalace_search`, `mempalace_kg_query/add/invalidate/timeline`, `mempalace_add_drawer`,
`mempalace_diary_*` …). Aktiv ab Claude-Neustart (MCP-Server einmal bestätigen).
- `.claude/settings.json` (committet) verdrahtet die Hooks
`mempalace hook run --hook {session-start,stop,precompact} --harness claude-code`:
**SessionStart** lädt relevanten Kontext automatisch, **Stop/PreCompact** persistieren.
- Voraussetzung: `mempalace` ist installiert und auf PATH (`~/.local/bin`). Nach `git clone`
ggf. MemPalace installieren; `mempalace.yaml` (Raum-Config) ist bewusst **gitignored**
(per-project, lokal). Der Palast selbst liegt lokal unter `~/.mempalace` (nicht im Repo).
**Arbeitsweise pro Session:** zu Aufgabenbeginn relevantes Wissen abrufen
(`mempalace_search` / `mempalace_kg_query`), bevor du im Code suchst; am Ende neue
durable Erkenntnisse ablegen.
**Subagenten MÜSSEN MemPalace ebenfalls nutzen** — das im Subagent-Prompt IMMER explizit
anweisen: zu Aufgabenbeginn den Palast abfragen (am einfachsten per CLI, kein Key nötig:
`mempalace search "<keywords>"` bzw. `mempalace search "<…>" --wing gerbilmanager`; falls
MCP-Tools verfügbar auch `mempalace_search`/`mempalace_kg_query`) — relevante Domänenregeln,
frühere Entscheidungen und Stolperfallen holen, BEVOR im Code gesucht wird; durable
Erkenntnisse am Ende ablegen (`mempalace add-drawer`/MCP). Gilt für JEDEN gespawnten Agenten.
### Memory-Policy — WANN / WIE / WO eine Memory anlegen
**WANN** (anlegen): nur **dauerhaft** nützliches, **nicht-offensichtliches** Wissen, das
künftige Sessions braucht — Architektur-/Design-Entscheidungen und ihr *Warum*, Domänenregeln
(z. B. 6-Jahre-Lebensspanne, Geschwisterverpaarung), Konventionen, Stolperfallen, bestätigte
Nutzer-Präferenzen, wiederkehrende Workflows.
**NICHT anlegen**: transienter Task-Status; was Code/Git/CLAUDE.md ohnehin festhalten;
Dinge, die nur für das aktuelle Gespräch zählen.
**WIE** (anlegen/pflegen):
- Fakt mit Gültigkeit → `mempalace_kg_add`. Freitext-Notiz/Erkenntnis → `mempalace_add_drawer`
(vorher `mempalace_check_duplicate`).
- Ändert/erledigt sich ein Fakt → `mempalace_kg_invalidate` (setzt End-Datum, **nicht löschen** —
der Graph ist temporal). So bleibt nichts veraltet stehen.
- Repo-Dateien werden über `mempalace mine <dir>` automatisch gefilet; `mempalace sync --apply
--wing gerbilmanager` entfernt Drawers gelöschter/verschobener Dateien.
**WO** (welcher Speicher):
- **MemPalace-Palast** (`~/.mempalace`, lokal): reichhaltiges, durchsuchbares Recall — der Default
für Memories.
- **CLAUDE.md** (Repo, committet): die **kanonischen** Projektfakten/Konventionen, die geteilt und
versioniert sein müssen. Wichtige durable Erkenntnisse hier spiegeln, damit sie im Repo (nicht nur
lokal) liegen.
Faustregel: durable + projektweit kanonisch → **CLAUDE.md** *und* Palast; sonstiges durable Wissen →
Palast; veraltetes → invalidieren, nicht stehen lassen.