diff --git a/docs/superpowers/specs/2026-06-07-family-timeline-design.md b/docs/superpowers/specs/2026-06-07-family-timeline-design.md index 3858ce5a..c0316093 100644 --- a/docs/superpowers/specs/2026-06-07-family-timeline-design.md +++ b/docs/superpowers/specs/2026-06-07-family-timeline-design.md @@ -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 `` 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 `` 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.