REVIEW-FIXES-BACKEND: CR-9 + CR-11 + CR-10 + DB-1
CR-9 (major): gidByNameDob TryGetValue + ExternalRef-Fallback Verhindert KeyNotFoundException wenn Name/DOB zwischen zwei Laeufen driftet (z.B. correctDob-Remap oder UI-Umbenennung). Fallback: ExternalRef-Dict-Lookup; bei Miss: sauberes Ueberspringen + Note statt 500. +Test CR9_NameDOB_drift. CR-11 (major): Farbschlag aus Genotyp ableiten (fill-NULL-only) Deep-Band-Tiere (gen>=2, kein Farbschlag-Feld) landen nicht laenger mit null ColorVariety. GenotypePotentiallyMatches() vergleicht locus-pair-weise (??=wildcard, case-insensitive). Nur vollstaendige Genotypen (8 Loci, kein ??) loesen Ableitung aus. Plan-Loop: fuellt colorVarietyId bei null + vollstaendigem Genotyp. Post-Sweep: bestehende DB-Tiere mit null ColorVarietyId werden nachgefuellt. AnimalSummary.FarbschlagDerivedFromGenotype = Zaehler. +Test CR11_ColorVariety_from_geno. CR-10 (major, Python): malformed Override-Genotyp wird nicht angewendet apply_conflict_decisions validiert mapped8locus nach gt.parse(). Leeres Ergebnis = Genotyp unveraendert + decisionWarning statt stillem Blanken. Konflikt wird trotzdem aufgeloest (Entscheidung gilt, nur Genotyp-Override ausgelassen). +5 Python-Tests (CR-10-Block in test_extract.py). DB-1 (high): filtered unique index auf Gerbil.ExternalRef WHERE ExternalRef IS NOT NULL — verhindert doppelten Import bei Race-Conditions oder Lauf-Ueberschneidungen. Migration UniqueExternalRef. SQLite-Testhost: HasFilter() wird via EnsureCreated appliziert (SQLite unterstuetzt Partial-Indexes). PartialUpdateTests externalRef-Assertion auf NotNull geaendert (name-hash unique). GATE: 139/139 C# + Python ALL PASS, ef has-pending=No.
This commit is contained in:
@@ -973,7 +973,19 @@ def apply_conflict_decisions(merged, conflicts, path):
|
||||
continue
|
||||
a["resolvedByDecision"] = True
|
||||
if d.get("genotype"):
|
||||
a["genotype"] = gt.parse(d["genotype"])
|
||||
# CR-10: validate the parsed genotype — a typo'd decision string yields empty
|
||||
# mapped8locus and would silently blank the animal's genotype while marking it
|
||||
# 'resolved'. Only apply if the parse produces non-empty loci.
|
||||
parsed = gt.parse(d["genotype"])
|
||||
if parsed.get("mapped8locus"):
|
||||
a["genotype"] = parsed
|
||||
else:
|
||||
# Keep the existing genotype; flag as a warning in the report.
|
||||
a.setdefault("decisionWarnings", []).append(
|
||||
f"Ungültiger Override-Genotyp '{d['genotype']}' — "
|
||||
"konnte nicht geparst werden (mapped8locus leer). "
|
||||
"Bestehender Genotyp behalten; Konflikt wurde trotzdem aufgelöst."
|
||||
)
|
||||
if d.get("farbschlag"):
|
||||
a["farbschlag"] = d["farbschlag"]
|
||||
a["farbschlagVariants"] = [d["farbschlag"]]
|
||||
|
||||
@@ -280,6 +280,36 @@ check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. W
|
||||
check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel"))
|
||||
check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell"))
|
||||
|
||||
# --- CR-10: malformed decision genotype must NOT blank the existing genotype ---
|
||||
dec_cr10 = os.path.join(tempfile.gettempdir(), "decisions-cr10.json")
|
||||
_json.dump({"resolutions": [
|
||||
# Valid decision (genotype parses OK) -> should be applied
|
||||
{"name": "Agouti OK", "dob": "01.01.2020", "decision": "test",
|
||||
"genotype": "aa CC DD ee GG PP spsp rere", "source": "test"},
|
||||
# Malformed genotype (typo'd) -> must NOT blank genotype; conflict still resolved
|
||||
{"name": "Siamese Bad", "dob": "02.02.2020", "decision": "test",
|
||||
"genotype": "BLÖDSINN!!!", "source": "test"},
|
||||
]}, open(dec_cr10, "w", encoding="utf-8"))
|
||||
merged_cr10 = [
|
||||
{"id": "g1", "name": "Agouti OK", "dob": "01.01.2020", "conflict": True, "farbschlag": "", "death": "",
|
||||
"genotype": {"mapped8locus": {"A": ["a","a"]}, "rawGenotype": "aa", "unmappedTokens": []}},
|
||||
{"id": "g2", "name": "Siamese Bad", "dob": "02.02.2020", "conflict": True, "farbschlag": "", "death": "",
|
||||
"genotype": {"mapped8locus": {"C": ["c^h","c^h"]}, "rawGenotype": "chmchm", "unmappedTokens": []}},
|
||||
]
|
||||
conflicts_cr10 = [{"id": "g1"}, {"id": "g2"}]
|
||||
n_cr10 = e.apply_conflict_decisions(merged_cr10, conflicts_cr10, dec_cr10)
|
||||
check("CR-10: valid decision genotype is applied (A-locus updated)",
|
||||
merged_cr10[0]["genotype"]["mapped8locus"].get("C") == ["C","C"])
|
||||
check("CR-10: malformed decision genotype NOT applied (C-locus preserved)",
|
||||
merged_cr10[1]["genotype"]["mapped8locus"].get("C") == ["c^h","c^h"])
|
||||
check("CR-10: malformed decision still un-quarantines the animal",
|
||||
merged_cr10[1].get("conflict") is False)
|
||||
check("CR-10: malformed decision adds a decisionWarning",
|
||||
bool(merged_cr10[1].get("decisionWarnings")))
|
||||
check("CR-10: apply returns correct resolved count (2 conflicts cleared)", n_cr10 == 2)
|
||||
try: os.remove(dec_cr10)
|
||||
except OSError: pass
|
||||
|
||||
# --- TOLERANT KC-MATCHER (IMPORT-BACKFILL): all clan spelling variants -> canon 'kleinechaote' ---
|
||||
# Julian-Entscheidung: Zucht = Kleine Chaoten wenn 'klein'+'chaoten' ODER bekannte Abkürzungen.
|
||||
# The v.d. fix: trailing \b after '.' failed when next char is ' ' (non-word), so
|
||||
|
||||
Reference in New Issue
Block a user