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.
117 lines
4.9 KiB
C#
117 lines
4.9 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace GerbilManagerWebAPI.Import
|
|
{
|
|
// ---- Source shapes (tools/import/output JSON, produced by extract.py) ----
|
|
|
|
public sealed class SourceAnimal
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Name { get; set; } = "";
|
|
public List<string> NameVariants { get; set; } = new();
|
|
public string Dob { get; set; } = "";
|
|
public string Death { get; set; } = "";
|
|
public string? Gender { get; set; }
|
|
public string Farbschlag { get; set; } = "";
|
|
public List<string> FarbschlagVariants { get; set; } = new();
|
|
public SourceGenotype Genotype { get; set; } = new();
|
|
public string Zucht { get; set; } = "";
|
|
public string ZuchtCanon { get; set; } = ""; // declension-folded Zucht key (residency rule a)
|
|
public List<SourceParentRef> ParentRefs { get; set; } = new();
|
|
public List<string> Photos { get; set; } = new();
|
|
public List<string> SourceFiles { get; set; } = new();
|
|
public bool Conflict { get; set; }
|
|
public SourceLitterRef? LitterRef { get; set; }
|
|
|
|
// GEN-3b normalization: hearing/deaf phenotype flag (null = not stated) and
|
|
// provenance/breeding tags (WFNZ/RV/GV/DP) — neither is genotype.
|
|
public bool? Deaf { get; set; }
|
|
public List<string> Tags { get; set; } = new();
|
|
|
|
// Set by extract.py when a human conflict-decision (conflict-decisions.json) un-quarantined
|
|
// this animal (its genotype/farbschlag are then authoritative). For reporting.
|
|
public bool ResolvedByDecision { get; set; }
|
|
}
|
|
|
|
public sealed class SourceGenotype
|
|
{
|
|
public Dictionary<string, List<string>> Mapped8locus { get; set; } = new();
|
|
public string RawGenotype { get; set; } = "";
|
|
public List<string> UnmappedTokens { get; set; } = new();
|
|
}
|
|
|
|
public sealed class SourceParentRef
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Dob { get; set; } = "";
|
|
public string RoleGuess { get; set; } = ""; // "father" | "mother"
|
|
public string Method { get; set; } = ""; // e.g. "chart-position"
|
|
public string Confidence { get; set; } = ""; // "hoch" | "medium" | "niedrig"
|
|
}
|
|
|
|
public sealed class SourceLitterRef
|
|
{
|
|
public string LitterId { get; set; } = "";
|
|
public string Method { get; set; } = "";
|
|
public string Confidence { get; set; } = ""; // "hoch" | "niedrig" | (ambiguous => candidates)
|
|
public List<string>? Candidates { get; set; }
|
|
}
|
|
|
|
public sealed class SourceLitter
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string LitterId { get; set; } = "";
|
|
public string Date { get; set; } = "";
|
|
public string DamName { get; set; } = "";
|
|
public string SireName { get; set; } = "";
|
|
public int? TotalBorn { get; set; }
|
|
public string Zuchtnummer { get; set; } = "";
|
|
public string Note { get; set; } = "";
|
|
public List<string> Warnings { get; set; } = new();
|
|
}
|
|
|
|
// ---- Report shapes (Julian-readable: counts per category + samples) ----
|
|
|
|
public sealed record ImportReport(
|
|
bool Executed,
|
|
LitterSummary Litters,
|
|
AnimalSummary Animals,
|
|
PhotoSummary Photos,
|
|
IReadOnlyList<string> Samples,
|
|
IReadOnlyList<string> Notes,
|
|
ResidencySummary? Residency = null);
|
|
|
|
/// <summary>Bestand (resident) vs external pedigree ancestors; FlippedByParentRule = foreign-
|
|
/// Zuchtname animals made resident because they parented a Clan offspring (rule b).
|
|
/// HerkunftBackfilled = resident animals whose null OriginBreeder was filled (safe, no overwrite).
|
|
/// FarbschlagWouldRebackfill = already-imported animals with a stale ColorVariety that the
|
|
/// current extract would update (no-op counter until the safety mechanism is ratified).</summary>
|
|
public sealed record ResidencySummary(int Resident, int External, int FlippedByParentRule,
|
|
int HerkunftBackfilled = 0, int FarbschlagWouldRebackfill = 0);
|
|
|
|
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported,
|
|
int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0,
|
|
int ParentFksBackfilled = 0, int WithoutDate = 0);
|
|
|
|
public sealed record AnimalSummary(
|
|
int InSource,
|
|
int Created,
|
|
int LinkedToLitter,
|
|
int FarbschlagMatched,
|
|
int FarbschlagUnmatched,
|
|
int AlreadyImported,
|
|
QuarantineSummary Quarantined,
|
|
int ParentLinksFromChart = 0,
|
|
int ConflictsResolvedByDecision = 0,
|
|
int FarbschlagDerivedFromGenotype = 0);
|
|
|
|
public sealed record QuarantineSummary(
|
|
int Conflicts,
|
|
int Stubs,
|
|
int DateOnlyLinks,
|
|
int AmbiguousLinks,
|
|
int Total);
|
|
|
|
public sealed record PhotoSummary(int Attached, int SourceFilesMissing);
|
|
}
|