GEN-3b: import notation normalization (Uw=G, Sls, deaf flag, tags)

genotype.py:
- Uw/uw aliased to G/g (same locus) so the D2 conflict group + pure-Uw
  cases stop being conflicts (Gg == Uwuw).
- Sls/WP recognized as a SECOND spotting locus (S(l)s(l)=WP het); carried
  into mapped8locus alongside Sp (Sp+Sls = Superschecke).
- dea/Dea/taub/hörend -> hearing/deaf phenotype FLAG (not a locus).
- WFNZ/RV/GV/DP -> provenance/breeding tags (not genotype, not conflicts).
- test_genotype.py: zero-dep unit tests for all four.

extract.py: surface deaf+tags on animals; dedup conflict detection now
compares the NORMALIZED genotype key (mapped8locus) instead of the raw
string, so Uw=G no longer triggers a conflict. Result: Konflikte 32 -> 27,
Zucht-Splits stays 0. Dedup identity = name + DOB + Zucht.

Backend: Gerbil.IsDeaf (bool?) + additive migration AddGerbilDeafFlag
(has-pending-model-changes clean) + GerbilDto/GerbilInput round-trip.
ImportService sets IsDeaf from animal.deaf and preserves Sls + tags + deaf
in RawImportData (kept out of the 8-locus compact Genotype contract until
GEN-3a adopts them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 10:26:01 +02:00
parent bd9cc83205
commit 2f089a902d
13 changed files with 1645 additions and 62 deletions

View File

@@ -27,7 +27,8 @@ namespace GerbilManagerWebAPI.Dtos
string? ExternalRef,
string? OriginBreeder,
List<string> CharacterTraits,
string? CharacterNote);
string? CharacterNote,
bool? IsDeaf);
public record LitterDto(
Guid Id,
@@ -75,7 +76,8 @@ namespace GerbilManagerWebAPI.Dtos
string? ExternalRef,
string? OriginBreeder,
List<string>? CharacterTraits,
string? CharacterNote);
string? CharacterNote,
bool? IsDeaf);
public record LitterInput(
string Name,

View File

@@ -97,12 +97,13 @@ namespace GerbilManagerWebAPI.Endpoints
g.OriginBreeder = i.OriginBreeder;
g.CharacterTraits = i.CharacterTraits ?? new List<string>();
g.CharacterNote = i.CharacterNote;
g.IsDeaf = i.IsDeaf;
}
internal static GerbilDto ToDto(Gerbil g) => new(
g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath,
g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder,
g.CharacterTraits, g.CharacterNote);
g.CharacterTraits, g.CharacterNote, g.IsDeaf);
}
}

View File

@@ -21,6 +21,11 @@ namespace GerbilManagerWebAPI.Import
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();
}
public sealed class SourceGenotype

View File

@@ -174,6 +174,7 @@ namespace GerbilManagerWebAPI.Import
LitterId = litterId,
ColorVarietyId = colorVarietyId,
Genotype = ComposeGenotype(a.Genotype),
IsDeaf = a.Deaf,
ImportSource = ImportSourceTag,
ExternalRef = a.Id,
OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(),
@@ -181,6 +182,11 @@ namespace GerbilManagerWebAPI.Import
{
a.Genotype.RawGenotype,
a.Genotype.UnmappedTokens,
// GEN-3b: Sls (2nd spotting locus) preserved here until Kevin's GEN-3a
// parser adopts it into the compact Genotype contract; tags + deaf too.
Sls = a.Genotype.Mapped8locus.TryGetValue("Sls", out var sls) ? sls : null,
a.Tags,
a.Deaf,
a.Zucht,
a.SourceFiles,
FarbschlagRaw = a.Farbschlag,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GerbilManagerWebAPI.Migrations
{
/// <inheritdoc />
public partial class AddGerbilDeafFlag : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsDeaf",
table: "Gerbils",
type: "boolean",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsDeaf",
table: "Gerbils");
}
}
}

View File

@@ -781,6 +781,9 @@ namespace GerbilManagerWebAPI.Migrations
b.Property<string>("ImportSource")
.HasColumnType("text");
b.Property<bool?>("IsDeaf")
.HasColumnType("boolean");
b.Property<Guid?>("LitterId")
.HasColumnType("uuid");

View File

@@ -67,6 +67,12 @@ namespace GerbilManagerWebAPI.Models
/// <summary>FEAT-14: free-text character note; feeds the AI Verkaufstext.</summary>
public string? CharacterNote { get; set; }
/// <summary>GEN-3b: hearing/deaf phenotype flag (NOT a genotype locus — it's the
/// downstream effect of high white load / Sp×Sls). null = not stated, true = deaf
/// (dea/taub), false = hearing (Dea/hörend). Set by the FEAT-8 import from the
/// after-spsp deafness annotation; see hive/agents/god/GENETIK-notation.md.</summary>
public bool? IsDeaf { get; set; }
}
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>