# 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:}` 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 99–107 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 # 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 --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 psql -U postgres -d gerbilmanager <<'SQL' … SQL`. Passwort: `docker exec 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-")`, 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`). - **`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 ""` 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 ` 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.