dedup: 'presence wins' — present-vs-absent token is not a conflict (Julian)

Breeder merge rule: 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
charted in one source, omitted in another) or a modifier on the same base
allele (e^f vs e, the [f] marker) — keep the present token; that is NOT a
conflict. Genuine VALUE contradictions still quarantine: different base
alleles (Ee↔ee), unknown-vs-filled (D-↔DD), different modifiers (c[h]↔c[chm]),
C-↔Cc[h], P-↔Pp.

Replaces the old `len(distinct normalized geno keys) > 1` test with
_genotype_conflict() (per-locus, per-allele compatibility; '?'-vs-filled is a
contradiction, modifier-present-vs-absent and whole-locus-absence are not).
Markers/flags (WP/DP/WFNZ/hörend) are already tags/flags, never genotype, so
they never reach conflict detection; empty Farbschlag/death already don't
conflict (only non-empty values are compared).

Clears Daja (keep spsp), Ichika (keep ee[f]) and the D4 marker cases:
Konflikte 19 -> 15. test_extract covers spsp/[f] present-vs-absent =
no conflict and the four genuine-contradiction shapes. python + dotnet
121/121 green; extractor-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:11:21 +02:00
parent 12604fba7c
commit 3f71d8e28e
3 changed files with 69 additions and 7 deletions

View File

@@ -104,6 +104,23 @@ check("missing decisions file tolerated (returns 0)",
try: os.remove(dec_path)
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) ---
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"))