diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java index 39b8191b..fe619e0b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java @@ -31,13 +31,13 @@ public interface PersonRepository extends JpaRepository { // Exact-case alias lookup — the first resolution step in findOrCreateByAlias. // Case-colliding aliases across persons (müller / Müller) are valid human labels, NOT - // duplicates: source_ref is the stable identity (ADR-025/032), alias is editable. Do NOT - // add a unique(lower(alias)) constraint — see ADR-032. + // duplicates: source_ref is the stable identity (ADR-025/033), alias is editable. Do NOT + // add a unique(lower(alias)) constraint — see ADR-033. Optional findByAlias(String alias); // Plural case-insensitive alias lookup — the fallback step. Returns ALL case-folding // siblings so the service can pick a deterministic one (lowest id) instead of letting a - // derived Optional<…>IgnoreCase throw NonUniqueResultException. See ADR-032. + // derived Optional<…>IgnoreCase throw NonUniqueResultException. See ADR-033. List findAllByAliasIgnoreCase(String alias); // Lookup by the normalizer person_id, used for idempotent canonical re-import (Phase 3). @@ -46,7 +46,7 @@ public interface PersonRepository extends JpaRepository { // Exact-case first+last name match — the first step of filename-based sender resolution. // Explicit `=` (HQL, not a derived query) so a null firstName binds as `first_name = NULL` // — never a match — instead of the derived-query fold to `first_name IS NULL`, which would - // pull a last-name-only row in as a sender (a provenance defect). See ADR-032. + // pull a last-name-only row in as a sender (a provenance defect). See ADR-033. @Query("SELECT p FROM Person p WHERE p.firstName = :firstName AND p.lastName = :lastName") Optional findByFirstNameAndLastName(@Param("firstName") String firstName, @Param("lastName") String lastName); @@ -54,7 +54,7 @@ public interface PersonRepository extends JpaRepository { // Plural case-insensitive first+last name match — lets findByName bail to empty on 2+ matches // instead of letting a derived Optional<…>IgnoreCase throw NonUniqueResultException. Same // null fail-closed guarantee as above: LOWER(:firstName) is NULL for a null arg, so a null - // first name resolves to no match (not first_name IS NULL widening). See ADR-032. + // first name resolves to no match (not first_name IS NULL widening). See ADR-033. @Query("SELECT p FROM Person p WHERE LOWER(p.firstName) = LOWER(:firstName) " + "AND LOWER(p.lastName) = LOWER(:lastName)") List findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase(@Param("firstName") String firstName, diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java index 30d1395f..81fbb406 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java @@ -122,7 +122,7 @@ public class PersonService { // UNSET rather than picking the lowest id. The archive's value is correct provenance — a // confidently-wrong pre-filled "Hans Müller" is worse than an empty field, because a // reviewer won't re-check a pre-filled value. Do NOT "consistency-clean" this into the - // lowest-id fallback. See ADR-032. + // lowest-id fallback. See ADR-033. return caseInsensitive.size() == 1 ? Optional.of(caseInsensitive.get(0)) : Optional.empty(); } @@ -140,7 +140,7 @@ public class PersonService { // Aliases differing only by case (müller / Müller) are valid distinct persons, not // duplicates, so a CASE-COLLISION must not throw: exact-case first, then the lowest-id - // case-insensitive sibling, then create. Mirrors the tag path — see ADR-032. + // case-insensitive sibling, then create. Mirrors the tag path — see ADR-033. // Scope (#731): "ambiguous" means case-insensitive. Two BYTE-IDENTICAL same-case aliases // are a true data anomaly out of scope here; the exact Optional below would surface that // as the opaque INTERNAL_ERROR (never a wrong row), not silently pick one. diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/README.md b/backend/src/main/java/org/raddatz/familienarchiv/person/README.md index f3d36719..ffbe6c72 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/README.md +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/README.md @@ -20,8 +20,8 @@ Features: person CRUD, name alias management, person merge (deduplication), fami | `getById(UUID)` | document, geschichte, ocr | Fetch one person by ID | | `getAllById(List)` | document | Bulk fetch for sender/receiver resolution | | `findAll(String q)` | document, dashboard | List all persons | -| `findByName(String firstName, String lastName)` | document | Filename-based **sender resolution** in `storeDocument`: exact-case match → single case-insensitive match → else **empty** (ambiguous names leave the sender unset; a null first name never matches). See ADR-032. | -| `findOrCreateByAlias(String rawName)` | importing | Idempotent create during mass import; type classification happens internally. Resolves exact-case → lowest-id case-insensitive sibling → create — never throws on case-colliding aliases. See ADR-032. | +| `findByName(String firstName, String lastName)` | document | Filename-based **sender resolution** in `storeDocument`: exact-case match → single case-insensitive match → else **empty** (ambiguous names leave the sender unset; a null first name never matches). See ADR-033. | +| `findOrCreateByAlias(String rawName)` | importing | Idempotent create during mass import; type classification happens internally. Resolves exact-case → lowest-id case-insensitive sibling → create — never throws on case-colliding aliases. See ADR-033. | | `findAllFamilyMembers()` | dashboard | Family member list for stats | | `findCorrespondents()` | document | Correspondent list for conversation filter | | `count()` | dashboard | Total person count for stats | diff --git a/docs/adr/032-tag-name-resolution-tolerates-case-collisions.md b/docs/adr/033-tag-name-resolution-tolerates-case-collisions.md similarity index 99% rename from docs/adr/032-tag-name-resolution-tolerates-case-collisions.md rename to docs/adr/033-tag-name-resolution-tolerates-case-collisions.md index 090ee9fd..bb907b4a 100644 --- a/docs/adr/032-tag-name-resolution-tolerates-case-collisions.md +++ b/docs/adr/033-tag-name-resolution-tolerates-case-collisions.md @@ -1,4 +1,4 @@ -# ADR-032 — Tag-name resolution tolerates case-collisions: exact-case first, then a deterministic lowest-id fallback, and never a `unique(lower(name))` constraint +# ADR-033 — Tag-name resolution tolerates case-collisions: exact-case first, then a deterministic lowest-id fallback, and never a `unique(lower(name))` constraint **Date:** 2026-06-06 **Status:** Accepted