docs(timeline): add Person date+precision migration as foundational issue

Replace Person birthYear/deathYear integers with birthDate/deathDate +
DatePrecision so known exact birthdays render precisely. Migration,
re-import preservation rule, and bounded blast radius captured; becomes
issue 1 the timeline's derived events depend on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-07 19:26:13 +02:00
parent d4a25e34d8
commit e63eaadc33

View File

@@ -11,7 +11,7 @@ The archive can capture, transcribe, organize, and browse letters, but the trans
A **hand-curated, year-banded vertical timeline** — the "Zeitstrahl" — that weaves three layers into one chronological view:
1. **Person life-events** derived from already-curated structured data (`Person.birthYear`/`deathYear`, marriage years from `PersonRelationship.fromYear`). Trusted, free, no extra entry.
1. **Person life-events** derived from already-curated structured data (`Person` birth/death dates, marriage years from `PersonRelationship.fromYear`). Trusted, free, no extra entry. (Requires the Person birth/death fields to move from year-integers to date + precision — see foundational issue 1.)
2. **Hand-curated events** the family writes — both **personal** (a move, an illness, emigration) and **historical** (a war, hyperinflation). Editorially controlled, always correct.
3. **Letters**, auto-placed by their existing `documentDate`, optionally hand-linked to an event to cluster them.
@@ -81,14 +81,35 @@ Mirrors the `Document` date model for consistency, so events and letters use one
`PERSONAL` | `HISTORICAL`. Personal events render with a person/family accent; historical events with a "world" accent and muted styling so the two layers are visually separable.
### Prerequisite: migrate `Person` birth/death to date + precision
Today `Person` stores `birthYear`/`deathYear` as `Integer`, so a known exact birthday (e.g. `1901-03-14`) has nowhere to live and derived events are stuck at year precision. This is fixed by a **foundational Person-domain migration** that the timeline depends on (and which delivers value on its own — precise dates then render on person cards, hover cards, and the Stammbaum).
**Change:** replace `birthYear`/`deathYear` (`Integer`) with:
| Field | Type | Notes |
|---|---|---|
| `birthDate` | `LocalDate` (nullable) | most precise date known |
| `birthDatePrecision` | `DatePrecision` (nullable) | `YEAR` for year-only, `DAY` for exact birthdays, etc. |
| `deathDate` | `LocalDate` (nullable) | |
| `deathDatePrecision` | `DatePrecision` (nullable) | |
**Flyway data migration:** existing `birth_year``birth_date = '{year}-01-01'`, `birth_date_precision = 'YEAR'` (same for death); then drop the year columns.
**Re-import preservation (ADR-025):** the canonical importer (`PersonRegisterImporter` / `tools/import-normalizer/persons_tree.py`) only carries the *year*. On re-import it must **not** clobber a hand-entered finer-than-`YEAR` date — if the existing precision is `DAY`/`MONTH`/`SEASON`, preserve it; only refresh from the spreadsheet year when the field is empty or still `YEAR`-from-import.
**Bounding the blast radius:** `PersonNodeDTO` keeps exposing an `Integer birthYear`/`deathYear` *derived* from the new date (`birthDate.getYear()`), so the Stammbaum layout (`familyForest.ts` et al.) is untouched. Display surfaces (person card, hover card) move to a shared precision-aware formatter — extend the existing `frontend/src/lib/person/personLifeDates.ts`. The person edit/new forms gain date inputs with a precision selector.
**Scope note:** `PersonRelationship.fromYear` (marriage year) stays `Integer`/`YEAR` for MVP — precise marriage dates are a later, parallel extension if wanted.
### Derived person-events (not persisted)
Assembled on read from existing curated data; never stored:
Assembled on read from the migrated `Person` data; never stored:
| Source | Derived event | `eventDate` | precision |
|---|---|---|---|
| `Person.birthYear` | *Geburt: {name}* | `{birthYear}-01-01` | `YEAR` |
| `Person.deathYear` | *Tod: {name}* | `{deathYear}-01-01` | `YEAR` |
| `Person.birthDate` | *Geburt: {name}* | `Person.birthDate` | `Person.birthDatePrecision` |
| `Person.deathDate` | *Tod: {name}* | `Person.deathDate` | `Person.deathDatePrecision` |
| `PersonRelationship` `SPOUSE_OF.fromYear` | *Heirat: {A} & {B}* | `{fromYear}-01-01` | `YEAR` |
Emitted in the same DTO shape as a curated event, flagged `derived: true`, `type = PERSONAL`. They cannot be edited from the timeline (they are edited at their source: Person record / relationship). A marriage is derived once per `SPOUSE_OF` edge (symmetric edges are stored once — see existing relationship rules).
@@ -144,17 +165,18 @@ Input DTO `TimelineEventRequest` lives flat in the `timeline/` package. Errors u
## Proposed issue breakdown (milestone "Zeitstrahl / Family Timeline")
Ordered so each issue is independently shippable and reviewable; later issues depend on earlier ones.
Ordered so each issue is independently shippable and reviewable; later issues depend on earlier ones. Issue 1 is a standalone Person-domain improvement and a hard prerequisite for the timeline's derived events.
1. **Backend: `TimelineEvent` entity + migration**entity, `EventType`, Flyway migration + join tables, repository. (foundation)
2. **Backend: TimelineEvent CRUD API**`TimelineEventController` + `TimelineService` write methods, `TimelineEventRequest` DTO, permission gating, `GET /events/{id}`.
3. **Backend: derived person-events** — assemble Geburt/Tod/Heirat from Person + relationship data via their services; unit-tested dedup.
4. **Backend: timeline assembly endpoint**`GET /api/timeline` merging events + derived events + letters into `TimelineDTO`; year-bucketing, precision sort, undated bucket, filters.
5. **Frontend: shared date-label helper + types**`dateLabel.ts`, regen API types.
6. **Frontend: global `/zeitstrahl` view**`TimelineView`, `YearBand`, `EventCard`, `LetterCard`, server load.
7. **Frontend: filters**`TimelineFilters` (person / generation / layer / year range).
8. **Frontend: curator event forms**`/zeitstrahl/events/new` + `/[id]/edit`, gated, with document & person pickers.
9. **Frontend: per-person Lebensweg** — embed `<TimelineView personId>` on Person detail.
10. **Polish & a11y** — mobile layout at 375px, dark mode, axe checks, i18n completeness (de/en/es).
1. **Person birth/death → date + precision (foundational)**replace `birthYear`/`deathYear` with `birthDate`/`deathDate` + precision on `Person`; Flyway data migration (year → `YYYY-01-01`, `YEAR`); update importer with re-import preservation rule; derive year in `PersonNodeDTO` (Stammbaum untouched); move person card / hover card to a precision-aware `personLifeDates.ts`; add date+precision inputs to person new/edit forms. Ships value on its own.
2. **Backend: `TimelineEvent` entity + migration** — entity, `EventType`, Flyway migration + join tables, repository.
3. **Backend: TimelineEvent CRUD API**`TimelineEventController` + `TimelineService` write methods, `TimelineEventRequest` DTO, permission gating, `GET /events/{id}`.
4. **Backend: derived person-events** — assemble Geburt/Tod/Heirat from migrated Person + relationship data via their services; unit-tested dedup.
5. **Backend: timeline assembly endpoint**`GET /api/timeline` merging events + derived events + letters into `TimelineDTO`; year-bucketing, precision sort, undated bucket, filters.
6. **Frontend: shared date-label helper + types**`dateLabel.ts`, regen API types.
7. **Frontend: global `/zeitstrahl` view**`TimelineView`, `YearBand`, `EventCard`, `LetterCard`, server load.
8. **Frontend: filters**`TimelineFilters` (person / generation / layer / year range).
9. **Frontend: curator event forms**`/zeitstrahl/events/new` + `/[id]/edit`, gated, with document & person pickers.
10. **Frontend: per-person Lebensweg** — embed `<TimelineView personId>` on Person detail.
11. **Polish & a11y** — mobile layout at 375px, dark mode, axe checks, i18n completeness (de/en/es).
> An ADR may be warranted for the new `timeline/` domain + entity (per `docs/CLAUDE.md`, significant data-model change). Add as the next sequential ADR number when implementation starts.