IMPORT-BACKFILL: toleranter KC-Matcher + Herkunft-Backfill + FarbschlagWouldRebackfill-Zaehler
1) TOLERANTER KC-MATCHER (extract.py norm_zucht): trailing \b nach '.' greift nicht
zwischen two non-word-chars (z.B. nach '.' vor ' '). Fix: r\bv\.\s?d\. (kein \b
am Ende). Alle Schreibvarianten ('Zucht der Kleinen Chaoten', 'kleinen Chaoten',
'v.d. Kleinen Chaoten', '[ZdkC]') -> canon 'kleinechaote'. +is_clan_zucht() helper.
+11 Python-Tests (Regression + neue Varianten). FIX-1-Entscheidungs-Matching
unberuehrt (canon_pair loest v.d. bereits via split_name_zucht).
2) HERKUNFT-BACKFILL (ImportService.cs, fill-NULL-only): POST-Sweep ueber alle
DB-Tiere mit OriginBreeder==null && IsResident==true. Leitet OriginBreeder aus
dem Elterntier (FatherId/MotherId -> OriginBreeder) ab; Fallback 'Zucht der
Kleinen Chaoten'. NIEMALS ueberschreibt nicht-leeren OriginBreeder. Trocken-Lauf
zaehlt HerkunftBackfilled; Execute schreibt. Fixt Tier 'C' (OriginBreeder null).
3) FARBSCHLAG-WOULD-REBACKFILL (no-op Zaehler): zaehlt bereits-importierte Tiere,
bei denen der aktuelle Extraktor einen anderen Farbschlag liefert als in der DB.
Kein Overwrite (Sicherheitsmechanismus ausstehend auf god/Julian-Freigabe);
Optionen A/B/C an god gemeldet, Empfehlung: Option B (Timestamp-basiert).
4) ResidencySummary um HerkunftBackfilled + FarbschlagWouldRebackfill erweitert.
ImportReport-Notizen fuer beide neuen Zaehler.
GATE: 133/133 C#-Tests, 57 Python-Tests, ef has-pending=No.
This commit is contained in:
@@ -82,8 +82,12 @@ namespace GerbilManagerWebAPI.Import
|
||||
ResidencySummary? Residency = null);
|
||||
|
||||
/// <summary>Bestand (resident) vs external pedigree ancestors; FlippedByParentRule = foreign-
|
||||
/// Zuchtname animals made resident because they parented a Clan offspring (rule b).</summary>
|
||||
public sealed record ResidencySummary(int Resident, int External, int FlippedByParentRule);
|
||||
/// 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,
|
||||
|
||||
@@ -137,12 +137,14 @@ namespace GerbilManagerWebAPI.Import
|
||||
// name+DOB -> gid index, across EXISTING rows AND this run's planned animals, so that
|
||||
// chart-position parentRefs (PEDIGREE-LINK) can resolve a parent to a real gerbil id.
|
||||
var existingRows = await _db.Gerbils
|
||||
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.ExternalRef, g.LitterId }).ToListAsync();
|
||||
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.ExternalRef, g.LitterId, g.ColorVarietyId }).ToListAsync();
|
||||
var gidByNameDob = new Dictionary<string, Guid>();
|
||||
foreach (var g in existingRows)
|
||||
gidByNameDob[NameDobKey(g.Name, g.DateOfBirth)] = g.Id;
|
||||
var existingLitterByExtRef = existingRows.Where(g => g.ExternalRef != null)
|
||||
.ToDictionary(g => g.ExternalRef!, g => g.LitterId);
|
||||
var existingColorVarietyByExtRef = existingRows.Where(g => g.ExternalRef != null)
|
||||
.ToDictionary(g => g.ExternalRef!, g => g.ColorVarietyId);
|
||||
|
||||
// PASS 1: assign ids + resolve fb/gender/Wurfchronik link (no writes yet).
|
||||
var plan = new List<AnimalPlan>();
|
||||
@@ -169,7 +171,8 @@ namespace GerbilManagerWebAPI.Import
|
||||
if (!exists) gidByNameDob.TryAdd(NameDobKey(a.Name, ParseDate(a.Dob)), gid);
|
||||
|
||||
var currentLitter = exists && existingLitterByExtRef.TryGetValue(a.Id, out var el) ? el : null;
|
||||
plan.Add(new AnimalPlan(a, gid, exists, wurfLitterId, currentLitter, colorVarietyId, gender));
|
||||
var currentColorVarietyId = exists && existingColorVarietyByExtRef.TryGetValue(a.Id, out var ecv) ? ecv : null;
|
||||
plan.Add(new AnimalPlan(a, gid, exists, wurfLitterId, currentLitter, colorVarietyId, gender, currentColorVarietyId));
|
||||
}
|
||||
|
||||
// PASS 1.5: PEDIGREE-LINK — synthesize/reuse a litter from chart-position parentRefs for
|
||||
@@ -460,6 +463,51 @@ namespace GerbilManagerWebAPI.Import
|
||||
if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// FARBSCHLAG-RE-MATCH — no-op counter (safety mechanism pending god/Julian sign-off).
|
||||
// Counts already-imported animals where the current extract matched a DIFFERENT
|
||||
// ColorVariety than what is currently stored in the DB. Does NOT update any row.
|
||||
int farbschlagWouldRebackfill = plan.Count(p =>
|
||||
p.Exists && p.ColorVarietyId is not null && p.ColorVarietyId != p.CurrentColorVarietyId);
|
||||
|
||||
// HERKUNFT BACKFILL (fill-NULL-only, safe): sweep all resident animals whose
|
||||
// OriginBreeder is null and fill it with a derived value or 'Zucht der Kleinen Chaoten'.
|
||||
// NEVER overwrites a non-null OriginBreeder (Julian: "alle Schreibweisen unterstützen").
|
||||
const string DefaultClanBreeder = "Zucht der Kleinen Chaoten";
|
||||
int herkunftBackfilled = 0;
|
||||
{
|
||||
var needsHerkunft = await _db.Gerbils
|
||||
.Where(g => g.OriginBreeder == null && g.IsResident)
|
||||
.Select(g => new { g.Id, g.LitterId })
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var g in needsHerkunft)
|
||||
{
|
||||
herkunftBackfilled++;
|
||||
if (execute)
|
||||
{
|
||||
// Prefer the OriginBreeder of an existing parent (father first, then mother).
|
||||
string? derived = null;
|
||||
if (g.LitterId is not null)
|
||||
{
|
||||
var parentIds = await _db.Litters
|
||||
.Where(l => l.Id == g.LitterId)
|
||||
.Select(l => new { l.FatherId, l.MotherId })
|
||||
.FirstOrDefaultAsync();
|
||||
if (parentIds?.FatherId is Guid fid)
|
||||
derived = await _db.Gerbils.Where(gb => gb.Id == fid && gb.OriginBreeder != null)
|
||||
.Select(gb => gb.OriginBreeder).FirstOrDefaultAsync();
|
||||
if (derived is null && parentIds?.MotherId is Guid mid)
|
||||
derived = await _db.Gerbils.Where(gb => gb.Id == mid && gb.OriginBreeder != null)
|
||||
.Select(gb => gb.OriginBreeder).FirstOrDefaultAsync();
|
||||
}
|
||||
var row = await _db.Gerbils.FindAsync(g.Id);
|
||||
if (row is not null && row.OriginBreeder is null)
|
||||
row.OriginBreeder = derived ?? DefaultClanBreeder;
|
||||
}
|
||||
}
|
||||
if (execute && herkunftBackfilled > 0) await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (littersWithoutDate > 0)
|
||||
notes.Add($"Würfe ohne Datum: {littersWithoutDate} Wurfchronik-Einträge ohne parsbares Geburtsdatum übersprungen (weder erstellt noch verknüpft).");
|
||||
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
||||
@@ -469,6 +517,10 @@ namespace GerbilManagerWebAPI.Import
|
||||
if (parentFksBackfilled > 0)
|
||||
notes.Add($"Parent-FK-Backfill: {parentFksBackfilled} bereits importierte Würfe haben jetzt eine Eltern-Verknüpfung (Elternteil war zuvor in Quarantäne, jetzt geladen).");
|
||||
notes.Add($"Bestand/Herkunft: {residentTotal} im Bestand (Clan Kleine Chaoten), {externalTotal} externe Ahnen ({flippedByParentRule} davon über die Eltern-Regel als Bestand erkannt).");
|
||||
if (herkunftBackfilled > 0)
|
||||
notes.Add($"Herkunft-Backfill: {herkunftBackfilled} Bestand-Tier(e) mit leerem Herkunft-Feld befüllt (Zucht der Kleinen Chaoten oder von Elternteil abgeleitet). Niemals überschrieben.");
|
||||
if (farbschlagWouldRebackfill > 0)
|
||||
notes.Add($"Farbschlag-Hinweis (kein Overwrite): {farbschlagWouldRebackfill} bereits importierte Tier(e) haben einen veralteten Farbschlag, den der aktuelle Extraktor korrigieren würde — Overwrite-Mechanismus ausstehend (god/Julian Freigabe).");
|
||||
int conflictsResolvedByDecision = loadable.Count(a => a.ResolvedByDecision);
|
||||
if (conflictsResolvedByDecision > 0)
|
||||
notes.Add($"Konfliktauflösungen: {conflictsResolvedByDecision} Tier(e) anhand von conflict-decisions.json un-quarantänet (Genotyp/Farbschlag der Züchterin ist maßgeblich).");
|
||||
@@ -484,7 +536,8 @@ namespace GerbilManagerWebAPI.Import
|
||||
Photos: new PhotoSummary(photosAttached, photosMissing),
|
||||
Samples: samples,
|
||||
Notes: notes,
|
||||
Residency: new ResidencySummary(residentTotal, externalTotal, flippedByParentRule));
|
||||
Residency: new ResidencySummary(residentTotal, externalTotal, flippedByParentRule,
|
||||
HerkunftBackfilled: herkunftBackfilled, FarbschlagWouldRebackfill: farbschlagWouldRebackfill));
|
||||
}
|
||||
|
||||
private T? Load<T>(string file)
|
||||
@@ -574,7 +627,8 @@ namespace GerbilManagerWebAPI.Import
|
||||
/// <summary>Per-animal plan computed before any write so synthesis can run in dry-run too.</summary>
|
||||
private sealed record AnimalPlan(
|
||||
SourceAnimal A, Guid Gid, bool Exists, Guid? WurfLitterId,
|
||||
Guid? CurrentLitterId, Guid? ColorVarietyId, Gender Gender);
|
||||
Guid? CurrentLitterId, Guid? ColorVarietyId, Gender Gender,
|
||||
Guid? CurrentColorVarietyId = null);
|
||||
|
||||
/// <summary>A litter synthesized from chart-position parentRefs (PEDIGREE-LINK).</summary>
|
||||
private sealed record SynthLitter(Guid Id, Guid? Father, Guid? Mother, DateOnly? Date, string Confidence);
|
||||
|
||||
Reference in New Issue
Block a user