Compare commits
9 Commits
feature/we
...
feature/we
| Author | SHA1 | Date | |
|---|---|---|---|
| d93e8d1586 | |||
| 114bbd92c8 | |||
| a8d8ae0dfc | |||
| 522f2eec51 | |||
| 3f71d8e28e | |||
| 0e7ec5ab61 | |||
| 12604fba7c | |||
| 311c5461fd | |||
| 3634d6ef9a |
@@ -51,7 +51,7 @@ gelisteten Konflikt-Tiere + Tiere mit Sonder-Kürzeln warten in Quarantäne —
|
|||||||
| ~~C5~~ | ✅ **BEANTWORTET** (2026-06-06): **Es gibt KEIN „Schwarzschimmel"** — das war ein Fehler in unserem Katalog. Die korrekten Schimmelarten: `efef` → **Orangeschimmel** · `efef pp` → **Rotaugenschimmel** · `efef gg` → **Silberschimmel** · Kombis z. B. `c[chm]c[chm] efef` → **CP-Orangeschimmel**. Michael korrigiert den Katalog (Schwarzschimmel raus, efef = Orangeschimmel). | — erledigt |
|
| ~~C5~~ | ✅ **BEANTWORTET** (2026-06-06): **Es gibt KEIN „Schwarzschimmel"** — das war ein Fehler in unserem Katalog. Die korrekten Schimmelarten: `efef` → **Orangeschimmel** · `efef pp` → **Rotaugenschimmel** · `efef gg` → **Silberschimmel** · Kombis z. B. `c[chm]c[chm] efef` → **CP-Orangeschimmel**. Michael korrigiert den Katalog (Schwarzschimmel raus, efef = Orangeschimmel). | — erledigt |
|
||||||
| C6 | **Die 32 Konflikt-Tiere prüfen** → siehe Abschnitt **D**. | diese 32 Tiere werden erst danach geladen |
|
| C6 | **Die 32 Konflikt-Tiere prüfen** → siehe Abschnitt **D**. | diese 32 Tiere werden erst danach geladen |
|
||||||
| C7 | *(optional)* Was hat dir bei **Renner Pro** gefehlt? Lieblings-Auswertungen? | mögliche neue Funktionen |
|
| C7 | *(optional)* Was hat dir bei **Renner Pro** gefehlt? Lieblings-Auswertungen? | mögliche neue Funktionen |
|
||||||
| C8 | **„Himalaya" vs. „Hermelin":** Du hast gesagt `c[h]c[h]` = **Hermelin**. In unserem Katalog gibt es aktuell ZWEI Farben mit `c[h]c[h]`: **Hermelin** (`aa c[h]c[h]`, nicht-agouti) und **Himalaya** (`A- c[h]c[h]`, agouti). Gibt es bei dir „Himalaya" überhaupt, oder ist **alles** mit `c[h]c[h]` einfach **Hermelin** (dann nehmen wir „Himalaya" raus, wie bei Schwarzschimmel)? | Farbschlag-Katalog (Himalaya behalten oder entfernen) |
|
| ~~C8~~ | ✅ **BEANTWORTET** (2026-06-06): **Himalaya gibt es** — Himalaya = **`A- c[h]c[h]`** (agouti), Hermelin = **`aa c[h]c[h]`** (nicht-agouti). Beide bleiben im Katalog; die Engine unterscheidet bereits korrekt nach A-/aa. — erledigt |
|
||||||
| C9 | *(optional, technisch)* Bei den **CP-Fuchs-Farben**: Wodurch unterscheiden sich genetisch **CP-Fuchs** ↔ **CP-Blaufuchs** ↔ **CP-Fuchs-Hell**? (Vermutung: Blaufuchs = `dd`-Verdünnung, „-Hell" = `c[chm]c[h]` statt `c[chm]c[chm]` — stimmt das?) Aktuell rechnet das Programm alle drei als „CP-Fuchs"; mit deiner Regel können wir sie genau unterscheiden. Per Hand auswählbar sind sie schon. | Farb-Engine Feinschliff (niedrige Priorität) |
|
| C9 | *(optional, technisch)* Bei den **CP-Fuchs-Farben**: Wodurch unterscheiden sich genetisch **CP-Fuchs** ↔ **CP-Blaufuchs** ↔ **CP-Fuchs-Hell**? (Vermutung: Blaufuchs = `dd`-Verdünnung, „-Hell" = `c[chm]c[h]` statt `c[chm]c[chm]` — stimmt das?) Aktuell rechnet das Programm alle drei als „CP-Fuchs"; mit deiner Regel können wir sie genau unterscheiden. Per Hand auswählbar sind sie schon. | Farb-Engine Feinschliff (niedrige Priorität) |
|
||||||
|
|
||||||
### Hinweis zu C5 — woher kam das falsche „Schwarzschimmel"? (wie gewünscht notiert)
|
### Hinweis zu C5 — woher kam das falsche „Schwarzschimmel"? (wie gewünscht notiert)
|
||||||
@@ -93,10 +93,10 @@ Bitte je Tier sagen, **welcher Wert stimmt** (die Quellen widersprechen sich bei
|
|||||||
| WildFire v.d. K.C. (*05.10.2017) | P-Locus: **P-** ↔ **PP** | ✅ **PP** — Julian |
|
| WildFire v.d. K.C. (*05.10.2017) | P-Locus: **P-** ↔ **PP** | ✅ **PP** — Julian |
|
||||||
| Zuleika v.d. K.C. (*24.10.2015) | D-Locus: **D-** ↔ **DD** | ✅ **DD, Ee, Gg, PP** (`aa c[chm]c[h] DD Ee Gg PP spsp`) — Julian |
|
| Zuleika v.d. K.C. (*24.10.2015) | D-Locus: **D-** ↔ **DD** | ✅ **DD, Ee, Gg, PP** (`aa c[chm]c[h] DD Ee Gg PP spsp`) — Julian |
|
||||||
| Milka of LennyLengo (*09.12.2018) | C-Locus: **C-** ↔ **Cc[h]** · E-Locus: **E-** ↔ **EE** | ✅ **Cc[h], EE** (`aa Cc[h] dd EE Gg P- Spsp`) — Julian |
|
| Milka of LennyLengo (*09.12.2018) | C-Locus: **C-** ↔ **Cc[h]** · E-Locus: **E-** ↔ **EE** | ✅ **Cc[h], EE** (`aa Cc[h] dd EE Gg P- Spsp`) — Julian |
|
||||||
| Silvain v.d. K.C. (*27.03.2022) | E-Locus: **Ee** ↔ **ee** · P-Locus: **P-** ↔ **Pp** | ⏳ offen |
|
| Silvain v.d. K.C. (*27.03.2022) | E-Locus: **Ee** ↔ **ee** · P-Locus: **P-** ↔ **Pp** | ✅ **ee, Pp** (`aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp`) — Julian |
|
||||||
| Ichika v.d. K.C. (*19.04.2020) | E-Locus: **ee** ↔ **ee[f]** | ✅ **ee[f]** (Beibehalten-Regel: `[f]` war vorhanden) — Julian |
|
| Ichika v.d. K.C. (*19.04.2020) | E-Locus: **ee** ↔ **ee[f]** | ✅ **ee[f]** (Beibehalten-Regel: `[f]` war vorhanden) — Julian |
|
||||||
| Daja of Little Rose (*16.05.2021) | Scheckung: **mit `spsp`** ↔ **ohne** | ✅ **mit `spsp`** (Beibehalten-Regel) — Julian |
|
| Daja of Little Rose (*16.05.2021) | Scheckung: **mit `spsp`** ↔ **ohne** | ✅ **mit `spsp`** (Beibehalten-Regel) — Julian |
|
||||||
| Chelsea v.d. K.C. | ⚠️ **Kein Genotyp-Konflikt** — es gibt **zwei** „Chelsea v.d. K.C." mit verschiedenem Geburtsdatum: **\*02.04.2021** und **\*15.10.2021**. Zwei verschiedene Tiere, oder ist ein Datum falsch? | ⏳ offen |
|
| Chelsea v.d. K.C. | ⚠️ **Kein Genotyp-Konflikt** — zwei „Chelsea" mit verschiedenem Datum (\*02.04.2021 / \*15.10.2021) | ✅ **ein Tier, Geburtsdatum 02.04.2021** (15.10.2021 war falsch → zusammengeführt) — Julian |
|
||||||
|
|
||||||
### D4 · **Marker** unterschiedlich (`WP` / `DP` / `WFNZ` / „hörend" mal vorhanden, mal nicht) — welcher gilt?
|
### D4 · **Marker** unterschiedlich (`WP` / `DP` / `WFNZ` / „hörend" mal vorhanden, mal nicht) — welcher gilt?
|
||||||
> ✅ **REGEL (Julian 2026-06-06):** „Wenn irgendwo etwas vorhanden war, das anderswo fehlte → **immer beibehalten**." Gilt generell für Marker/Flags und Angaben wie `spsp` oder `[f]` (Vorhandensein gewinnt über Fehlen). Wird zur Standard-Regel im Importer → löst alle „mit/ohne"-Fälle automatisch (z. B. Daja `spsp`, Ichika `[f]`). Greift NICHT bei echten Wert-Widersprüchen (z. B. `DD`↔`D-`, `Ee`↔`ee`) — die brauchen weiter deine Entscheidung.
|
> ✅ **REGEL (Julian 2026-06-06):** „Wenn irgendwo etwas vorhanden war, das anderswo fehlte → **immer beibehalten**." Gilt generell für Marker/Flags und Angaben wie `spsp` oder `[f]` (Vorhandensein gewinnt über Fehlen). Wird zur Standard-Regel im Importer → löst alle „mit/ohne"-Fälle automatisch (z. B. Daja `spsp`, Ichika `[f]`). Greift NICHT bei echten Wert-Widersprüchen (z. B. `DD`↔`D-`, `Ee`↔`ee`) — die brauchen weiter deine Entscheidung.
|
||||||
@@ -105,8 +105,8 @@ Bitte je Tier sagen, **welcher Wert stimmt** (die Quellen widersprechen sich bei
|
|||||||
|
|
||||||
| Tier | Konkreter Konflikt — was stimmt? | Status |
|
| Tier | Konkreter Konflikt — was stimmt? | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Vestra von den Schlossmäusen (*08.02.2019) | D-Locus: **D-** ↔ **DD** (WP gleich in beiden) | ⏳ offen |
|
| Vestra von den Schlossmäusen (*08.02.2019) | D-Locus: **D-** ↔ **DD** (WP gleich in beiden) | ✅ **DD** — Julian |
|
||||||
| Victoria Welby gen. Welby v.d. K.C. (*16.01.2023) | E-Locus: **Ee[f]** ↔ **ee[f]** (Fuchs ja/nein; DP gleich in beiden) — **das ist die Mutter von „C"!** Sobald geklärt, bekommt C auch seine Mutter. | ⏳ offen |
|
| Victoria Welby gen. Welby v.d. K.C. (*16.01.2023) | E-Locus: **Ee[f]** ↔ **ee[f]** — **Mutter von „C"!** | ✅ **ee[f]** (`Aa CC D- ee[f] Gg pp Spsp [DP]`) — Julian → C bekommt damit seine Mutter |
|
||||||
| Hedwig of BGB (*30.10.2019) | (WP/DP/hörend) | ✅ auto-gelöst — sind jetzt Flags, kein Konflikt mehr |
|
| Hedwig of BGB (*30.10.2019) | (WP/DP/hörend) | ✅ auto-gelöst — sind jetzt Flags, kein Konflikt mehr |
|
||||||
| Pitari gen. Piti v.d. K.C. (*16.05.2021) | (DP) | ✅ auto-gelöst — DP ist jetzt ein Flag |
|
| Pitari gen. Piti v.d. K.C. (*16.05.2021) | (DP) | ✅ auto-gelöst — DP ist jetzt ein Flag |
|
||||||
| Little Hero of Black Forest (*22.02.2018) | (WFNZ ± spsp) | ✅ kein Genotyp-Konflikt mehr (WFNZ = Flag) |
|
| Little Hero of Black Forest (*22.02.2018) | (WFNZ ± spsp) | ✅ kein Genotyp-Konflikt mehr (WFNZ = Flag) |
|
||||||
|
|||||||
@@ -27,3 +27,17 @@ test('Toggle „Externe Ahnen einblenden“ zeigt externe Tiere mit Extern-Marki
|
|||||||
// Der Bestand bleibt weiterhin sichtbar.
|
// Der Bestand bleibt weiterhin sichtbar.
|
||||||
await expect(page.locator('.gerbil-row', { hasText: 'Krümel' })).toBeVisible()
|
await expect(page.locator('.gerbil-row', { hasText: 'Krümel' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Bearbeiten-Formular kann ein Tier als extern markieren (Bestand-Häkchen)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
// Krümel gehört zum Bestand -> Häkchen entfernen und speichern.
|
||||||
|
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
||||||
|
const check = page.getByRole('checkbox', { name: t.form.isResidentLabel })
|
||||||
|
await expect(check).toBeChecked()
|
||||||
|
await check.uncheck()
|
||||||
|
await page.getByRole('button', { name: t.form.save }).click()
|
||||||
|
|
||||||
|
// Auf der Detailseite ist Krümel jetzt als „Extern“ markiert.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
|
||||||
|
await expect(page.getByText(t.externalBadge, { exact: true })).toBeVisible()
|
||||||
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface FormState {
|
|||||||
receiverContactId: string
|
receiverContactId: string
|
||||||
genotype: string
|
genotype: string
|
||||||
notes: string
|
notes: string
|
||||||
|
isResident: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY: FormState = {
|
const EMPTY: FormState = {
|
||||||
@@ -41,6 +42,7 @@ const EMPTY: FormState = {
|
|||||||
receiverContactId: '',
|
receiverContactId: '',
|
||||||
genotype: '',
|
genotype: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
|
isResident: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function formFromGerbil(g: {
|
function formFromGerbil(g: {
|
||||||
@@ -58,6 +60,7 @@ function formFromGerbil(g: {
|
|||||||
receiverContactId: string | null
|
receiverContactId: string | null
|
||||||
genotype: string | null
|
genotype: string | null
|
||||||
notes: string | null
|
notes: string | null
|
||||||
|
isResident?: boolean | null
|
||||||
}): FormState {
|
}): FormState {
|
||||||
return {
|
return {
|
||||||
name: g.name,
|
name: g.name,
|
||||||
@@ -74,6 +77,7 @@ function formFromGerbil(g: {
|
|||||||
receiverContactId: g.receiverContactId ?? '',
|
receiverContactId: g.receiverContactId ?? '',
|
||||||
genotype: g.genotype ?? '',
|
genotype: g.genotype ?? '',
|
||||||
notes: g.notes ?? '',
|
notes: g.notes ?? '',
|
||||||
|
isResident: g.isResident ?? true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +170,7 @@ export default function GerbilFormPage() {
|
|||||||
receiverContactId: nn(form.receiverContactId),
|
receiverContactId: nn(form.receiverContactId),
|
||||||
genotype: nn(form.genotype),
|
genotype: nn(form.genotype),
|
||||||
notes: nn(form.notes),
|
notes: nn(form.notes),
|
||||||
|
isResident: form.isResident,
|
||||||
}
|
}
|
||||||
const result = await mutation.run(body)
|
const result = await mutation.run(body)
|
||||||
if (result.ok) navigate(`/rennmaeuse/${result.value.id}`)
|
if (result.ok) navigate(`/rennmaeuse/${result.value.id}`)
|
||||||
@@ -355,6 +360,15 @@ export default function GerbilFormPage() {
|
|||||||
<textarea value={form.notes} onChange={(e) => set('notes', e.target.value)} />
|
<textarea value={form.notes} onChange={(e) => set('notes', e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="field field--check" title={t.form.isResidentHint}>
|
||||||
|
<span>{t.form.isResidentLabel}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.isResident}
|
||||||
|
onChange={(e) => set('isResident', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
{mutation.error && <div className="alert alert--error">{mutation.error}</div>}
|
{mutation.error && <div className="alert alert--error">{mutation.error}</div>}
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ export const de = {
|
|||||||
none: '— keine Angabe —',
|
none: '— keine Angabe —',
|
||||||
genotypeHint:
|
genotypeHint:
|
||||||
'Optional. Format z. B. „Aa CC Dd EE GG Pp Spsp rere“. Unbekannte Allele als „-“.',
|
'Optional. Format z. B. „Aa CC Dd EE GG Pp Spsp rere“. Unbekannte Allele als „-“.',
|
||||||
|
// BESTAND-FILTER: Zugehörigkeit zum eigenen Bestand (sonst externe Ahne)
|
||||||
|
isResidentLabel: 'Gehört zum eigenen Bestand',
|
||||||
|
isResidentHint: 'Abwählen für externe Ahnen, die nur für den Stammbaum erfasst sind.',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
saving: 'Speichern …',
|
saving: 'Speichern …',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Key match = normalize(name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.",
|
"_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Special field `correctDob` (DD.MM.YYYY): the matched (name + dob) record is a DUPLICATE with a WRONG birthdate — remap its DOB to `correctDob` BEFORE dedup so it merges into the canonical same-named animal. Key match = normalize(name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.",
|
||||||
"resolutions": [
|
"resolutions": [
|
||||||
{
|
{
|
||||||
"name": "Firefly von den Kleinen Chaoten",
|
"name": "Firefly von den Kleinen Chaoten",
|
||||||
@@ -56,6 +56,34 @@
|
|||||||
"decision": "C-locus = Cc[h], E-locus = EE",
|
"decision": "C-locus = Cc[h], E-locus = EE",
|
||||||
"genotype": "aa Cc[h] dd EE Gg P- Spsp",
|
"genotype": "aa Cc[h] dd EE Gg P- Spsp",
|
||||||
"source": "Julian 2026-06-06 — HUMANQUESTION D3"
|
"source": "Julian 2026-06-06 — HUMANQUESTION D3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Silvain von den Kleinen Chaoten",
|
||||||
|
"dob": "27.03.2022",
|
||||||
|
"decision": "E-locus = ee, P-locus = Pp",
|
||||||
|
"genotype": "aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp",
|
||||||
|
"source": "Julian 2026-06-06 — HUMANQUESTION D3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chelsea von den Kleinen Chaoten",
|
||||||
|
"dob": "15.10.2021",
|
||||||
|
"decision": "duplicate with wrong birthdate — same animal as Chelsea *02.04.2021; merge into it",
|
||||||
|
"correctDob": "02.04.2021",
|
||||||
|
"source": "Julian 2026-06-06 — HUMANQUESTION D3 (Chelsea Dublette)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vestra von den Schlossmäusen",
|
||||||
|
"dob": "08.02.2019",
|
||||||
|
"decision": "D-locus = DD",
|
||||||
|
"genotype": "Aa Cc[chm] DD EE GG PP Spsp [WP]",
|
||||||
|
"source": "Julian 2026-06-06 — HUMANQUESTION D4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Victoria Welby gen. Welby von den Kleinen Chaoten",
|
||||||
|
"dob": "16.01.2023",
|
||||||
|
"decision": "E-locus = ee[f] (Fuchs). NOTE: this is the mother of animal 'C' (c-29042024) — un-quarantining her links C's second parent.",
|
||||||
|
"genotype": "Aa CC D- ee[f] Gg pp Spsp [DP]",
|
||||||
|
"source": "Julian 2026-06-06 — HUMANQUESTION D4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -502,6 +502,52 @@ def _geno_key(genodict):
|
|||||||
return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m))
|
return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m))
|
||||||
|
|
||||||
|
|
||||||
|
# --- "presence wins" merge rule (Julian) -------------------------------------
|
||||||
|
# When two source variants of the SAME animal differ ONLY by a token PRESENT in one and
|
||||||
|
# ABSENT in the other — a whole locus (e.g. spsp recorded in one chart, omitted in another)
|
||||||
|
# or a modifier on the same base allele (e^f vs e, i.e. the [f] marker) — keep the present
|
||||||
|
# token; that is NOT a conflict. A genuine VALUE contradiction (different filled alleles:
|
||||||
|
# E vs e, D vs d, c^h vs c^chm) OR unknown-vs-filled (D- vs DD, the '?' second allele) STILL
|
||||||
|
# quarantines for human decision. (Markers/flags WP/DP/WFNZ/hörend are already tags/flags,
|
||||||
|
# never part of the genotype, so they never reach here.)
|
||||||
|
def _split_allele(a):
|
||||||
|
return tuple(a.split("^", 1)) if "^" in a else (a, "")
|
||||||
|
|
||||||
|
|
||||||
|
def _alleles_compatible(a, b):
|
||||||
|
if a == b:
|
||||||
|
return True
|
||||||
|
if a == "?" or b == "?":
|
||||||
|
return False # unknown vs filled = contradiction (D- vs DD)
|
||||||
|
(ba, ma), (bb, mb) = _split_allele(a), _split_allele(b)
|
||||||
|
if ba != bb:
|
||||||
|
return False # different base allele = real value diff (E vs e)
|
||||||
|
return ma == "" or mb == "" # same base, modifier present-vs-absent -> presence wins
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_compatible(p, q):
|
||||||
|
if len(p) != 2 or len(q) != 2:
|
||||||
|
return p == q
|
||||||
|
return ((_alleles_compatible(p[0], q[0]) and _alleles_compatible(p[1], q[1])) or
|
||||||
|
(_alleles_compatible(p[0], q[1]) and _alleles_compatible(p[1], q[0])))
|
||||||
|
|
||||||
|
|
||||||
|
def _genotype_conflict(mapped_list):
|
||||||
|
"""True only if two variants GENUINELY contradict at a shared locus. A locus present in
|
||||||
|
one variant and absent in another is fine (presence wins); so is a modifier present-vs-
|
||||||
|
absent on the same base allele. Replaces the old `len(distinct geno keys) > 1` test."""
|
||||||
|
loci = set()
|
||||||
|
for m in mapped_list:
|
||||||
|
loci.update(m.keys())
|
||||||
|
for locus in loci:
|
||||||
|
pairs = [m[locus] for m in mapped_list if locus in m]
|
||||||
|
for i in range(len(pairs)):
|
||||||
|
for j in range(i + 1, len(pairs)):
|
||||||
|
if not _pair_compatible(pairs[i], pairs[j]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def dedup(animals):
|
def dedup(animals):
|
||||||
"""Merge by normalise(call-name)+DOB, with the canonical Zucht as
|
"""Merge by normalise(call-name)+DOB, with the canonical Zucht as
|
||||||
DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht =
|
DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht =
|
||||||
@@ -552,6 +598,7 @@ def dedup(animals):
|
|||||||
parent_refs = list(base["parentRefs"])
|
parent_refs = list(base["parentRefs"])
|
||||||
genos = set()
|
genos = set()
|
||||||
geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text
|
geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text
|
||||||
|
mapped_variants = [] # mapped8locus per variant — for the 'presence wins' conflict test
|
||||||
farb = set()
|
farb = set()
|
||||||
deaths = set()
|
deaths = set()
|
||||||
deaf_seen = set()
|
deaf_seen = set()
|
||||||
@@ -567,6 +614,7 @@ def dedup(animals):
|
|||||||
if a["genotype"]["mapped8locus"]:
|
if a["genotype"]["mapped8locus"]:
|
||||||
genos.add(a["genotype"]["rawGenotype"])
|
genos.add(a["genotype"]["rawGenotype"])
|
||||||
geno_keys.add(_geno_key(a["genotype"]))
|
geno_keys.add(_geno_key(a["genotype"]))
|
||||||
|
mapped_variants.append(a["genotype"]["mapped8locus"])
|
||||||
if a["farbschlag"]:
|
if a["farbschlag"]:
|
||||||
farb.add(a["farbschlag"])
|
farb.add(a["farbschlag"])
|
||||||
if a["death"]:
|
if a["death"]:
|
||||||
@@ -603,8 +651,9 @@ def dedup(animals):
|
|||||||
"conflict": False,
|
"conflict": False,
|
||||||
}
|
}
|
||||||
merged.append(out)
|
merged.append(out)
|
||||||
# conflict: same animal, disagreeing NORMALIZED genotype (Uw==G) or farbschlag or death
|
# conflict: same animal, GENUINELY disagreeing genotype (presence-vs-absence is NOT a
|
||||||
if len(geno_keys) > 1 or len(farb) > 1 or len(deaths) > 1:
|
# conflict — Julian's 'presence wins') or >1 distinct farbschlag or >1 distinct death.
|
||||||
|
if _genotype_conflict(mapped_variants) or len(farb) > 1 or len(deaths) > 1:
|
||||||
out["conflict"] = True
|
out["conflict"] = True
|
||||||
conflicts.append({
|
conflicts.append({
|
||||||
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
||||||
@@ -834,6 +883,31 @@ def write_report(merged, conflicts, orphans, raw_count, litters, photo_count,
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------ main
|
# ------------------------------------------------------------------------ main
|
||||||
|
def apply_dob_remaps(raw_animals, path):
|
||||||
|
"""PRE-dedup: a conflict-decision carrying `correctDob` marks a record as a DUPLICATE with a
|
||||||
|
wrong birthdate — remap that raw record's DOB to correctDob so dedup MERGES it into the
|
||||||
|
canonical same-named animal (e.g. Chelsea *15.10.2021 -> *02.04.2021). Match =
|
||||||
|
norm_name(name)+norm_dob(dob). Tolerates a missing/garbled file. Returns the remap count.
|
||||||
|
Must run BEFORE dedup (it changes the dedup identity). (god/HUMANQUESTION D — Dubletten.)"""
|
||||||
|
remaps = {}
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
for r in (json.load(fh).get("resolutions") or []):
|
||||||
|
if r.get("correctDob"):
|
||||||
|
remaps[(norm_name(r.get("name", "")), norm_dob(r.get("dob", "")))] = r["correctDob"]
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return 0
|
||||||
|
if not remaps:
|
||||||
|
return 0
|
||||||
|
n = 0
|
||||||
|
for a in raw_animals:
|
||||||
|
new = remaps.get((norm_name(a.get("name", "")), norm_dob(a.get("dob", ""))))
|
||||||
|
if new and a.get("dob") != new:
|
||||||
|
a["dob"] = new
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
def apply_conflict_decisions(merged, conflicts, path):
|
def apply_conflict_decisions(merged, conflicts, path):
|
||||||
"""Consume human conflict resolutions (tools/import/conflict-decisions.json) so the wife's
|
"""Consume human conflict resolutions (tools/import/conflict-decisions.json) so the wife's
|
||||||
answers UN-QUARANTINE animals. Schema: {"resolutions":[{name, dob, decision, genotype?,
|
answers UN-QUARANTINE animals. Schema: {"resolutions":[{name, dob, decision, genotype?,
|
||||||
@@ -904,8 +978,9 @@ def main():
|
|||||||
litters = extract_wurfchronik(args.wurfchronik)
|
litters = extract_wurfchronik(args.wurfchronik)
|
||||||
print(f"Wurfchronik: {len(litters)} Würfe")
|
print(f"Wurfchronik: {len(litters)} Würfe")
|
||||||
|
|
||||||
merged, conflicts, orphans, zucht_splits = dedup(raw_animals)
|
|
||||||
decisions_path = os.path.join(HERE, "conflict-decisions.json")
|
decisions_path = os.path.join(HERE, "conflict-decisions.json")
|
||||||
|
dob_remaps = apply_dob_remaps(raw_animals, decisions_path) # before dedup (changes identity)
|
||||||
|
merged, conflicts, orphans, zucht_splits = dedup(raw_animals)
|
||||||
resolved_by_decision = apply_conflict_decisions(merged, conflicts, decisions_path)
|
resolved_by_decision = apply_conflict_decisions(merged, conflicts, decisions_path)
|
||||||
match_stats = match_litters(merged, litters)
|
match_stats = match_litters(merged, litters)
|
||||||
photo_count = sum(len(a["photos"]) for a in merged)
|
photo_count = sum(len(a["photos"]) for a in merged)
|
||||||
@@ -924,7 +999,7 @@ def main():
|
|||||||
|
|
||||||
print(f"\nRoh: {len(raw_animals)} → eindeutig: {len(merged)} "
|
print(f"\nRoh: {len(raw_animals)} → eindeutig: {len(merged)} "
|
||||||
f"| Konflikte: {len(conflicts)} | per Entscheidung gelöst: {resolved_by_decision} "
|
f"| Konflikte: {len(conflicts)} | per Entscheidung gelöst: {resolved_by_decision} "
|
||||||
f"| Zucht-Splits: {len(zucht_splits)} "
|
f"| DOB-Remaps: {dob_remaps} | Zucht-Splits: {len(zucht_splits)} "
|
||||||
f"| Orphans: {len(orphans)} | Fotos: {photo_count}")
|
f"| Orphans: {len(orphans)} | Fotos: {photo_count}")
|
||||||
print(f"Wurf-Verknüpfung: {match_stats['parents']} (Datum+Eltern), "
|
print(f"Wurf-Verknüpfung: {match_stats['parents']} (Datum+Eltern), "
|
||||||
f"{match_stats['dateOnly']} (nur Datum), {match_stats['ambiguous']} mehrdeutig "
|
f"{match_stats['dateOnly']} (nur Datum), {match_stats['ambiguous']} mehrdeutig "
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
|
|||||||
## Überblick
|
## Überblick
|
||||||
|
|
||||||
- Rohe Tier-Einträge aus den Stammbäumen: **950**
|
- Rohe Tier-Einträge aus den Stammbäumen: **950**
|
||||||
- Nach Zusammenführung (eindeutige Tiere): **622**
|
- Nach Zusammenführung (eindeutige Tiere): **621**
|
||||||
- davon mit Geburtsdatum: 327
|
- davon mit Geburtsdatum: 326
|
||||||
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
|
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
|
||||||
- Konflikte zur Klärung: **19**
|
- Konflikte zur Klärung: **9**
|
||||||
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
|
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
|
||||||
- Fotos zugeordnet: **137**
|
- Fotos zugeordnet: **137**
|
||||||
- Würfe aus der Wurfchronik: **752**
|
- Würfe aus der Wurfchronik: **752**
|
||||||
@@ -26,20 +26,10 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
|||||||
| Tier | Geburtsdatum | abweichende Genotypen | abweichende Farbschläge | Sterbedaten | Dateien |
|
| Tier | Geburtsdatum | abweichende Genotypen | abweichende Farbschläge | Sterbedaten | Dateien |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
| Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
||||||
| Louis von den Kleinen Chaoten | 15.07.2017 | Aa Cc[] D- Ee Gg P- spsp // Aa Cc[chm] D- Ee Uwuw[d] P- spsp | — | 01.07.2020 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
|
||||||
| Zuleika von den Kleinen Chaoten | 24.10.2015 | aa c[chm]c[h] D- E G P- spsp // aa c[chm]c[h] D- Ee Gg P- spsp // aa c[chm]c[h] DD Ee Gg P- spsp | — | 24.02.2019 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
|
||||||
| Vestra von den Schlossmäusen | 08.02.2019 | Aa Cc[chm] D- EE GG PP Spsp [WP] // Aa Cc[chm] DD EE GG PP Spsp [WP] | — | 26.05.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
|
|
||||||
| Flint von den Kleinen Chaoten | 23.12.2017 | aa Cc[chm] D- ee Gg P- spsp | — | 10.05.2021 // 10.05.2022 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
|
||||||
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
|
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
|
||||||
| Milka of LennyLengo | 09.12.2018 | aa C- dd E- Gg P- Spsp // aa Cc[h] dd EE Gg P- Spsp | — | 22.12.2021 | Stammbaum von Alberto Kids, Stammbaum von Stella Kids |
|
|
||||||
| Silvain von den Kleinen Chaoten | 27.03.2022 | aa c[chm]c[chm] Dd Ee[-] Gg P- Spsp // aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp | — | 31.12.2024 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
|
||||||
| Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
|
| Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
|
||||||
| Little Hero of Black Forest | 22.02.2018 | AA CC DD EE GG PP [WFNZ] // AA CC DD EE GG PP spsp [WFNZ] | — | 18.06.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids, Stammbaum von Valentino Firehearts Kids |
|
|
||||||
| Molly of Black Forest | 13.09.2021 | /+, Aa Cc[chm] D- Ee gg P- spsp // Aa Cc[chm] Dd Ee gg Pp spsp | — | 03.05.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
|
||||||
| Little Runner's Big Ben | 03.02.2020 | Aa Cc[chm] DD Ee Gg PP Spsp // Aa Cc[chm] DD Ee Gg Pp Spsp | — | 14.10.2023 | Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Valentino Firehearts Kids, Stammbaum von Watarus Kids |
|
| Little Runner's Big Ben | 03.02.2020 | Aa Cc[chm] DD Ee Gg PP Spsp // Aa Cc[chm] DD Ee Gg Pp Spsp | — | 14.10.2023 | Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Valentino Firehearts Kids, Stammbaum von Watarus Kids |
|
||||||
| Daja of Little Rose | 16.05.2021 | aa chmchm D- EE Gg P- // aa chmchm D- EE Gg P- spsp | — | — | Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
|
|
||||||
| Vance Jr. von den Kleinen Chaoten | 10.04.2022 | aa Cc[hm] Dd Ee gg P- Spsp // aa Cc[hm] Dd Ee gg P- spsp | Kohlfuchs, hell | — | Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
| Vance Jr. von den Kleinen Chaoten | 10.04.2022 | aa Cc[hm] Dd Ee gg P- Spsp // aa Cc[hm] Dd Ee gg P- spsp | Kohlfuchs, hell | — | Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
||||||
| Ichika von den Kleinen Chaoten | 19.04.2020 | aa CC D- ee Gg pp spsp // aa CC D- ee[f] Gg pp spsp | — | 27.11.2023 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Watarus Kids |
|
|
||||||
| Victoria Welby gen. Welby v.d. Kleinen Chaoten | 16.01.2023 | Aa CC D- Ee[f] Gg pp Spsp [DP] // Aa CC D- ee[f] Gg pp Spsp [DP] | Goldfuchsschimmel Punktschecke DP | 17.02.2026 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
| Victoria Welby gen. Welby v.d. Kleinen Chaoten | 16.01.2023 | Aa CC D- Ee[f] Gg pp Spsp [DP] // Aa CC D- ee[f] Gg pp Spsp [DP] | Goldfuchsschimmel Punktschecke DP | 17.02.2026 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||||
| Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | — | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
| Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | — | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||||
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
|
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
|
||||||
@@ -101,7 +91,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
|||||||
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
|
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
|
||||||
- „Charly of Golden Lights“ → Charly of Golden Lights (*05.04.2016)
|
- „Charly of Golden Lights“ → Charly of Golden Lights (*05.04.2016)
|
||||||
- „Ziwa of Golden Lights“ → Ziwa of Golden Lights (*29.04.2016)
|
- „Ziwa of Golden Lights“ → Ziwa of Golden Lights (*29.04.2016)
|
||||||
- „Chelsea von den Kleinen Chaoten“ → Chelsea von den Kleinen Chaoten (*02.04.2021); Chelsea von den Kleinen Chaoten (*15.10.2021)
|
- „Chelsea von den Kleinen Chaoten“ → Chelsea von den Kleinen Chaoten (*02.04.2021)
|
||||||
- „Pinto of Fiomi“ → Pinto of Fiomi (*28.08.2016)
|
- „Pinto of Fiomi“ → Pinto of Fiomi (*28.08.2016)
|
||||||
- „Living Force's Idefix“ → Living Force's Idefix (*05.04.2016)
|
- „Living Force's Idefix“ → Living Force's Idefix (*05.04.2016)
|
||||||
- „Scarlett of Samsimar“ → Scarlett of Samsimar (*05.09.2018)
|
- „Scarlett of Samsimar“ → Scarlett of Samsimar (*05.09.2018)
|
||||||
@@ -143,12 +133,12 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
|
|||||||
| `/+` | 7 | ? |
|
| `/+` | 7 | ? |
|
||||||
| `-g` | 2 | ? |
|
| `-g` | 2 | ? |
|
||||||
| `C(C)` | 2 | Schreibweise (C trägt c) |
|
| `C(C)` | 2 | Schreibweise (C trägt c) |
|
||||||
| `chmchm` | 2 | Schreibweise (c[chm]c[chm]) |
|
|
||||||
| `Cc[]` | 1 | ? |
|
| `Cc[]` | 1 | ? |
|
||||||
| `-psp` | 1 | ? |
|
| `-psp` | 1 | ? |
|
||||||
| `G(G)` | 1 | ? |
|
| `G(G)` | 1 | ? |
|
||||||
| `/` | 1 | ? |
|
| `/` | 1 | ? |
|
||||||
| `+2018` | 1 | ? |
|
| `+2018` | 1 | ? |
|
||||||
|
| `chmchm` | 1 | Schreibweise (c[chm]c[chm]) |
|
||||||
| `c[chm]chm]` | 1 | ? |
|
| `c[chm]chm]` | 1 | ? |
|
||||||
| `Dea/dea]` | 1 | ? |
|
| `Dea/dea]` | 1 | ? |
|
||||||
| `DD-Tumor` | 1 | ? |
|
| `DD-Tumor` | 1 | ? |
|
||||||
|
|||||||
@@ -101,9 +101,50 @@ check("decision removes both entries from conflicts list", conflicts == [])
|
|||||||
check("apply_conflict_decisions returns resolved count", n == 2)
|
check("apply_conflict_decisions returns resolved count", n == 2)
|
||||||
check("missing decisions file tolerated (returns 0)",
|
check("missing decisions file tolerated (returns 0)",
|
||||||
e.apply_conflict_decisions([], [], os.path.join(tempfile.gettempdir(), "does-not-exist.json")) == 0)
|
e.apply_conflict_decisions([], [], os.path.join(tempfile.gettempdir(), "does-not-exist.json")) == 0)
|
||||||
|
|
||||||
|
# --- correctDob: a wrong-birthdate duplicate is remapped BEFORE dedup so it merges ---
|
||||||
|
dec2 = os.path.join(tempfile.gettempdir(), "decisions-dob.json")
|
||||||
|
_json.dump({"resolutions": [
|
||||||
|
{"name": "Chelsea von den Kleinen Chaoten", "dob": "15.10.2021",
|
||||||
|
"decision": "duplicate wrong birthdate", "correctDob": "02.04.2021", "source": "test"},
|
||||||
|
]}, open(dec2, "w", encoding="utf-8"))
|
||||||
|
raw = [
|
||||||
|
{"name": "Chelsea von den Kleinen Chaoten", "dob": "15.10.2021"}, # the wrong-dob duplicate
|
||||||
|
{"name": "Chelsea von den Kleinen Chaoten", "dob": "02.04.2021"}, # canonical
|
||||||
|
{"name": "Other Animal", "dob": "01.01.2020"},
|
||||||
|
]
|
||||||
|
rn = e.apply_dob_remaps(raw, dec2)
|
||||||
|
check("correctDob remaps the wrong-dob record", raw[0]["dob"] == "02.04.2021")
|
||||||
|
check("correctDob leaves the canonical record alone", raw[1]["dob"] == "02.04.2021")
|
||||||
|
check("correctDob leaves unrelated records alone", raw[2]["dob"] == "01.01.2020")
|
||||||
|
check("apply_dob_remaps returns remap count", rn == 1)
|
||||||
|
check("after remap both Chelsea share one dedup identity (name+dob)",
|
||||||
|
e.norm_dob(raw[0]["dob"]) == e.norm_dob(raw[1]["dob"]))
|
||||||
|
check("missing decisions file tolerated for dob remaps (returns 0)",
|
||||||
|
e.apply_dob_remaps([], os.path.join(tempfile.gettempdir(), "nope.json")) == 0)
|
||||||
|
try: os.remove(dec2)
|
||||||
|
except OSError: pass
|
||||||
|
|
||||||
try: os.remove(dec_path)
|
try: os.remove(dec_path)
|
||||||
except OSError: pass
|
except OSError: pass
|
||||||
|
|
||||||
|
# --- "presence wins" conflict rule (Julian) ---
|
||||||
|
# present-vs-absent (whole locus or [f] modifier) is NOT a conflict; differing filled values are.
|
||||||
|
check("spsp present vs locus absent -> no conflict",
|
||||||
|
not e._genotype_conflict([{"Sp": ["sp", "sp"]}, {}]))
|
||||||
|
check("ee[f] vs ee ([f] modifier present/absent) -> no conflict",
|
||||||
|
not e._genotype_conflict([{"E": ["e", "e^f"]}, {"E": ["e", "e"]}]))
|
||||||
|
check("DD vs D- (unknown vs filled) -> conflict",
|
||||||
|
e._genotype_conflict([{"D": ["D", "D"]}, {"D": ["D", "?"]}]))
|
||||||
|
check("Ee vs ee (different base allele) -> conflict",
|
||||||
|
e._genotype_conflict([{"E": ["E", "e"]}, {"E": ["e", "e"]}]))
|
||||||
|
check("C- vs Cc[h] -> conflict",
|
||||||
|
e._genotype_conflict([{"C": ["C", "?"]}, {"C": ["C", "c^h"]}]))
|
||||||
|
check("c[h] vs c[chm] (different modifiers) -> conflict",
|
||||||
|
not e._alleles_compatible("c^h", "c^chm"))
|
||||||
|
check("identical genotypes -> no conflict",
|
||||||
|
not e._genotype_conflict([{"A": ["A", "a"]}, {"A": ["A", "a"]}]))
|
||||||
|
|
||||||
# --- name-bleed guard (a parent name is not a Farbschlag) ---
|
# --- name-bleed guard (a parent name is not a Farbschlag) ---
|
||||||
check("v.d. name rejected", e.looks_like_animal_name("Tennessee von den Kleinen Chaoten"))
|
check("v.d. name rejected", e.looks_like_animal_name("Tennessee von den Kleinen Chaoten"))
|
||||||
check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. Welby v.d. Kleinen Chaoten"))
|
check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. Welby v.d. Kleinen Chaoten"))
|
||||||
|
|||||||
Reference in New Issue
Block a user