OWNERSHIP: residency classification (Bestand vs external ancestors)
Per hive/agents/god/OWNERSHIP-residency.md (Julian): "Clan kleine Chaoten"
is the wife's own Zucht. Classify each animal:
(a) RESIDENT if zuchtCanon contains 'kleinechaote' (separator/declension-
insensitive: "v.d. Kleinen Chaoten", "Clan-Kleine-Chaoten", …); OR
(b) PARENT EXCEPTION — a parent of a Clan offspring is resident even if its
own Zuchtname is foreign (you can only breed a Clan litter if the parents
were in your care). Runs AFTER the parentRefs→litter linking.
Otherwise EXTERNAL (pedigree ancestor, not living stock — kept, not deleted).
Stored as a dedicated Gerbil.IsResident bool (distinct from Status/Abgabe —
origin/identity, not current location), defaulting true (own stock unless
marked external; DB HasDefaultValue(true)). Additive migration
AddGerbilResidency; has-pending-model-changes clean. Round-trips in
GerbilDto/GerbilInput and is Gridify-filterable (isResident==true) so Kevin
can build a "nur Bestand" filter. ImportService computes it at (re-)import
and refreshes existing rows on the idempotent sweep. ResidencySummary added
to the import report.
Projected on real data: 306 loadable → 227 resident (188 rule a, +39 via
parent exception), 79 external ancestors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,17 @@ namespace GerbilManagerWebAPI.Import
|
||||
parentLinksAdded++;
|
||||
}
|
||||
|
||||
// litter id -> (father, mother) gids, across synthesized + Wurfchronik (by name) litters.
|
||||
// Used by the residency rule (b) below; augmented with existing DB litters under execute.
|
||||
var litterParents = new Dictionary<Guid, (Guid? F, Guid? M)>();
|
||||
foreach (var sl in synthLitters.Values)
|
||||
litterParents[sl.Id] = (sl.Father, sl.Mother);
|
||||
foreach (var sl in litters)
|
||||
if (litterIdMap.TryGetValue(sl.Id, out var lid))
|
||||
litterParents[lid] = (
|
||||
createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.SireName)), out var fid) ? fid : (Guid?)null,
|
||||
createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.DamName)), out var mid) ? mid : (Guid?)null);
|
||||
|
||||
// PASS 2: write (litters synthesized first so offspring FK resolves), then animals + photos.
|
||||
if (execute)
|
||||
{
|
||||
@@ -200,7 +211,10 @@ namespace GerbilManagerWebAPI.Import
|
||||
.Select(l => new { l.Id, l.FatherId, l.MotherId, l.Date }).ToListAsync();
|
||||
var litterByParentsDate = new Dictionary<string, Guid>();
|
||||
foreach (var l in existingLitterRows)
|
||||
{
|
||||
litterByParentsDate[$"{l.FatherId}|{l.MotherId}|{l.Date:yyyy-MM-dd}"] = l.Id;
|
||||
litterParents[l.Id] = (l.FatherId, l.MotherId);
|
||||
}
|
||||
|
||||
foreach (var sl in synthLitters.Values.ToList())
|
||||
{
|
||||
@@ -226,11 +240,31 @@ namespace GerbilManagerWebAPI.Import
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// OWNERSHIP/RESIDENCY (runs AFTER litter links exist): (a) Zuchtname matches the Clan
|
||||
// kennel (zuchtCanon contains 'kleinechaote'); (b) parent of a Clan offspring, even if
|
||||
// the parent's own Zuchtname is foreign. See hive/agents/god/OWNERSHIP-residency.md.
|
||||
static bool ClanCanon(string? zc) =>
|
||||
(zc ?? "").Contains("kleinechaote", StringComparison.OrdinalIgnoreCase);
|
||||
var resident = new HashSet<Guid>();
|
||||
foreach (var p in plan) if (ClanCanon(p.A.ZuchtCanon)) resident.Add(p.Gid); // (a)
|
||||
int residentByA = resident.Count, flippedByParentRule = 0;
|
||||
foreach (var p in plan)
|
||||
{
|
||||
if (!ClanCanon(p.A.ZuchtCanon)) continue;
|
||||
var litId = p.WurfLitterId ?? (synthLitterForGid.TryGetValue(p.Gid, out var s) ? s : (Guid?)null);
|
||||
if (litId is null || !litterParents.TryGetValue(litId.Value, out var par)) continue;
|
||||
if (par.F is Guid gf && resident.Add(gf)) flippedByParentRule++; // (b)
|
||||
if (par.M is Guid gm && resident.Add(gm)) flippedByParentRule++;
|
||||
}
|
||||
int residentTotal = plan.Count(p => resident.Contains(p.Gid));
|
||||
int externalTotal = plan.Count - residentTotal;
|
||||
|
||||
foreach (var p in plan)
|
||||
{
|
||||
Guid? litterId = p.WurfLitterId
|
||||
?? (synthLitterForGid.TryGetValue(p.Gid, out var slid) ? slid : (Guid?)null);
|
||||
if (litterId is not null) linked++;
|
||||
bool isResident = resident.Contains(p.Gid);
|
||||
|
||||
if (p.ColorVarietyId is null) fbUnmatched++; else fbMatched++;
|
||||
|
||||
@@ -242,11 +276,15 @@ namespace GerbilManagerWebAPI.Import
|
||||
if (p.Exists)
|
||||
{
|
||||
animalsExisting++;
|
||||
// re-link an existing animal that just became linkable (sweep idempotency).
|
||||
if (execute && p.CurrentLitterId is null && litterId is not null)
|
||||
// sweep idempotency: re-link a now-linkable animal + refresh its residency.
|
||||
if (execute)
|
||||
{
|
||||
var row = await _db.Gerbils.FirstOrDefaultAsync(g => g.Id == p.Gid);
|
||||
if (row is not null) row.LitterId = litterId;
|
||||
if (row is not null)
|
||||
{
|
||||
if (p.CurrentLitterId is null && litterId is not null) row.LitterId = litterId;
|
||||
row.IsResident = isResident;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -266,6 +304,7 @@ namespace GerbilManagerWebAPI.Import
|
||||
ColorVarietyId = p.ColorVarietyId,
|
||||
Genotype = ComposeGenotype(p.A.Genotype),
|
||||
IsDeaf = p.A.Deaf,
|
||||
IsResident = isResident,
|
||||
ImportSource = ImportSourceTag,
|
||||
ExternalRef = p.A.Id,
|
||||
OriginBreeder = string.IsNullOrWhiteSpace(p.A.Zucht) ? null : p.A.Zucht.Trim(),
|
||||
@@ -328,6 +367,7 @@ namespace GerbilManagerWebAPI.Import
|
||||
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
||||
if (parentLinksAdded > 0)
|
||||
notes.Add($"Stammbaum-Diagramm: {parentLinksAdded} Tiere über Eltern-Verknüpfung einem (abgeleiteten) Wurf zugeordnet ({derivedLitters} abgeleitete Würfe).");
|
||||
notes.Add($"Bestand/Herkunft: {residentTotal} im Bestand (Clan Kleine Chaoten), {externalTotal} externe Ahnen ({flippedByParentRule} davon über die Eltern-Regel als Bestand erkannt).");
|
||||
if (!execute) notes.Add("DRY-RUN: nichts gespeichert. /import/execute lädt die konfliktfreien Daten.");
|
||||
|
||||
return new ImportReport(
|
||||
@@ -339,7 +379,8 @@ namespace GerbilManagerWebAPI.Import
|
||||
parentLinksAdded),
|
||||
Photos: new PhotoSummary(photosAttached, photosMissing),
|
||||
Samples: samples,
|
||||
Notes: notes);
|
||||
Notes: notes,
|
||||
Residency: new ResidencySummary(residentTotal, externalTotal, flippedByParentRule));
|
||||
}
|
||||
|
||||
private T? Load<T>(string file)
|
||||
|
||||
Reference in New Issue
Block a user