4.7 KiB
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/UNKNOWNwhen 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 personsfrom the pre-deploy dump — seedocs/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.