# 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`.

## 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
```

- **`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`.
- **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.
- **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).

## 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.
