# ADR-039 — Person life dates become LocalDate + DatePrecision **Status:** Accepted **Date:** 2026-06-12 **Issue:** #773 (Zeitstrahl milestone, foundational) ## Context `Person` stored `birthYear`/`deathYear` as `Integer`. A known exact birthday (`1901-03-14`) had nowhere to live, and every display was stuck at year precision. The Zeitstrahl's derived life-events need real dates with precision metadata, and the document domain already solved exactly this problem with `Document.documentDate` + `metaDatePrecision`. V76 replaces the two integer columns with `birth_date`/`death_date` (`DATE`, nullable) plus `birth_date_precision`/`death_date_precision` (`VARCHAR(16) NOT NULL DEFAULT 'UNKNOWN'`), backfilling existing years as `YYYY-01-01` at `YEAR` precision. ## Decisions ### 1. `DatePrecision` stays in `document/` and is imported cross-domain The `person` package imports `org.raddatz.familienarchiv.document.DatePrecision` directly. The layering rule (controllers → services → own repository) governs service-to-repository coupling, **not** value-type sharing — an enum has no behaviour and no persistence side effects. Creating a `common/` package for one enum would be premature structure; if a third domain needs it, revisit then. The enum remains a verbatim mirror of the import normalizer's `Precision` values (ADR-025) — changes must stay in sync with `tools/import-normalizer/dates.py`. ### 2. Precision columns are NOT NULL with default `UNKNOWN` Mirrors `Document.metaDatePrecision`. The illegal state "date present, precision null" cannot exist: the named CHECK constraints (`chk_person_birth_date_precision_coherence`, `…_values`, `chk_person_birth_before_death`, and the death-side twins) enforce `(date IS NULL) = (precision = 'UNKNOWN')` and the temporal order at the DB level; `PersonService.validateLifeDates` enforces the same rules first so users get a structured 400 (`INVALID_DATE_PRECISION` / `BIRTH_AFTER_DEATH`) instead of a constraint-violation 500. Storage accepts all seven `DatePrecision` values (enum-to-string mapping consistency), but the person new/edit form offers only **DAY / MONTH / YEAR** — `RANGE` and `SEASON` are semantically nonsensical for a birth or death, and `APPROX` is excluded from the form to reduce cognitive load for the senior author audience. Legacy `APPROX` rows still render correctly (display delegates to `formatDocumentDate`). ### 3. Derived-year pattern for backward-compatible DTOs `PersonNodeDTO` (Stammbaum) and `RelationshipDTO` keep `Integer birthYear/deathYear`, derived null-safely in the relationship services (`birthDate != null ? birthDate.getYear() : null` — never 0, never empty string; REQ-PERSON-DATE-01). The native queries behind `PersonSummaryDTO` project `CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear`. ### 4. `PersonSummaryDTO` intentionally exposes years only The person list/search views show year precision only; full precision lives on the person detail page. Do **not** add `LocalDate getBirthDate()` to the interface without updating all four native queries (`findAllWithDocumentCount`, `searchWithDocumentCount`, `findTopByDocumentCount`, `findByFilter`) — the interface projection is satisfied purely by the SQL SELECT aliases. ### 5. `preferHumanDate` extends ADR-025's human-edit-preserve rule The importer stays year-shaped: `PersonUpsertCommand` keeps `Integer birthYear/ deathYear` because the spreadsheet only knows a year — pushing `LocalDate` into the importer would fabricate precision. On upsert, `PersonService.preferHumanDate` returns a `DatePrecisionPair` record (date and precision travel as one value so they cannot go out of sync): - existing precision DAY/MONTH/SEASON/RANGE/APPROX → hand-entered, preserved verbatim; - existing precision YEAR/UNKNOWN → refreshed from the canonical year as `YYYY-01-01` + `YEAR` (or cleared to null/`UNKNOWN` when the sheet has no year). The original integer/string `preferHuman` overloads remain for non-date fields. ### 6. Known limitation — mixed-precision comparison `validateLifeDates` compares stored `LocalDate` values. A DAY-precision birth late in the same year as a YEAR-precision death (stored as Jan 1st) is rejected with `BIRTH_AFTER_DEATH`. This is intentional; the `error_birth_after_death` i18n message carries the workaround hint (enter the following year as the death year). ## Consequences - V76 is one-way (columns dropped). Rollback = targeted `pg_restore -t persons` from the pre-deploy dump — see `docs/DEPLOYMENT.md` §5. - Exact dates now render on person cards, hover cards, and the mention dropdown; the Stammbaum and person list are visually unchanged. - The Zeitstrahl can derive birth/death life-events at full precision.