FEAT-8b: bake Julian's authoritative Wurfchronik semantics into the extractor

- survivedToGoHome: unlabeled Tabelle1 col E detected positionally; value-
  adaptive row parsing recovers it from schema-shifted Tabelle2 rows too
  (118 recovered, 79 confirmed by the E=F-TG-s identity)
- breakdown G -> males/females/stillborn/diedLater ('s' = died before Abgabe)
- validation: E = F - TG - s; 113 mismatches as German review-report warnings
- (name, Zucht) canonicalisation: [brackets] == of/von suffix ([ZdkC] ==
  von den Kleinen Chaoten); Zucht = dedup discriminator (0 splits in data)
- animal->litter matching via DOB+(Vater,Mutter): 95 high-confidence,
  40 date-only, 9 ambiguous; litterRef in animals.json
- regenerated report: 889 raw -> 574 unique (279 dated), 32 conflicts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 00:56:14 +02:00
parent a9778ad1d2
commit 7ea2f97e72
3 changed files with 475 additions and 43 deletions

View File

@@ -19,12 +19,29 @@ See the format analysis in `FEAT-8a-format-spec.md` (Pam's hive workspace).
- Embedded photos (`xl/media`) → `output/photos/<animal-slug>/`, mapped to the
animal by drawing anchor position.
2. **Dedup + review (stage 2)**
- Merge animals on `normalise(name) + DOB` (corroborated by DOB+genotype).
- Merge animals on `normalise(call-name) + DOB`, with the **Zucht as
discriminator** (Julian's ruling: Wurfchronik `[brackets]` ≡ Stammbaum
`of/von <line>` suffix — both are the breeding line; same name+DOB but
different Zucht stays two animals).
- Match animals onto Wurfchronik litters (`litterRef`) via DOB + (Vater,
Mutter) — the Pam-validated build order (chronicle litters are canonical).
- Emit a German-language `output/review-report.md` for the breeder to verify
(merges, **conflicts**, ambiguous/incomplete entries, unmapped genotype tokens).
(merges, **conflicts**, ambiguous/incomplete entries, unmapped genotype
tokens, litter data-quality warnings).
- **Nothing is loaded into the database** — stage 3 (API load) is separate and
waits on DATA-2 + FEAT-1b phase 2.
### Wurfchronik column semantics (Julian, authoritative)
`A` Wurfbezeichnung · `B` Geburtsdatum · `C` Mutter · `D` Vater (`[…]` = Zucht,
`&` = multiple sires) · `E` **survivedToGoHome** (Tabelle1 only, unlabeled —
detected positionally) · `F` Wurfstärke → `totalBorn` · `G` breakdown
`Männchen,Weibchen,TG,s``males/females/stillborn/diedLater` (`s` = died
after birth, before Abgabe) · last column → `note`. Validation: `E` should
equal `F TG s`; mismatches become German warnings in the review report
(data-quality signal, not an import blocker). A few Tabelle2 rows shift these
columns — they are read value-adaptively and flagged with a warning.
Genotypes are mapped to the frozen 8-locus contract (A C D E G P Sp Re) while
preserving everything: `genotype.mapped8locus`, `genotype.rawGenotype` (verbatim),
`genotype.unmappedTokens` (e.g. the `Uw` locus, markers `WFNZ/WP/DP`). A `-`