Files
familienarchiv/docs/adr/044-relationship-dates-localdate-precision.md
Marcel 663bb57334
All checks were successful
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
SDD Gate / RTM Check (pull_request) Successful in 18s
SDD Gate / Contract Validate (pull_request) Successful in 29s
SDD Gate / Constitution Impact (pull_request) Successful in 20s
CI / Unit & Component Tests (pull_request) Successful in 4m38s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m58s
docs(relationship): ADR-044, DB diagrams, deploy runbook, RTM rows
- ADR-044 extends ADR-039 to the relationship edge: LocalDate+DatePrecision,
  update re-validation of create invariants, no @Version (last-write-wins),
  DELETE→404 anti-enumeration alignment, precise derived marriage date, and the
  relationshipDates.ts location reusing the existing person→shared boundary.
- db-orm.puml: person_relationships now carries from_date/from_date_precision/
  to_date/to_date_precision; db-relationships.puml gets a V78 columns-only note.
- DEPLOYMENT.md §5: V78 deploy note — no maintenance window, stop-old-then-start
  ordering (not rolling-deploy-safe), targeted pg_restore rollback.
- CLAUDE.md error-code list gains INVALID_RELATIONSHIP_DATES.
- rtm.md: REQ-001..REQ-019 for #837 mapped to impl + tests, all Done.

Refs #837
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:29:08 +02:00

5.1 KiB

ADR-044 — Relationship dates become LocalDate + DatePrecision; relationships become editable

Status: Accepted Date: 2026-06-14 Issue: #837 (Zeitstrahl milestone; deferred follow-up to #773 / ADR-039)

Context

PersonRelationship stored its span as Integer fromYear/toYear. A wedding could never be more precise than 1923, while Person (ADR-039), Document, and TimelineEvent already carry full DatePrecision. Relationships also supported only create + delete: fixing a wrong type, a wrong person, or adding a date learned later meant deleting and re-creating the edge — losing createdAt. A notes column existed that no form set and nothing displayed.

V78 replaces the two integer columns with from_date/to_date (DATE, nullable) plus from_date_precision/to_date_precision (VARCHAR(16) NOT NULL DEFAULT 'UNKNOWN'), backfilling existing years as YYYY-01-01 at YEAR precision — exactly the V76 / ADR-039 pattern applied to the relationship edge. A new PUT /api/persons/{id}/relationships/{relId} makes relationships editable, and notes is activated end to end.

Decisions

1. Mirror ADR-039 verbatim for the relationship edge

DatePrecision is imported cross-domain from document/ (ADR-039 §1 — value-type sharing, not a layering breach). The precision columns are NOT NULL default UNKNOWN, guarded by five named CHECK constraints (chk_relationship_from_coherence, chk_relationship_to_coherence, chk_relationship_date_order, chk_relationship_{from,to}_precision_values). RelationshipService.validateRelationshipDates enforces the same rules first, so the user gets a structured 400 (INVALID_DATE_PRECISION for coherence, the new INVALID_RELATIONSHIP_DATES for a toDate < fromDate order violation) instead of a constraint-violation 500. The form offers only DAY / MONTH / YEAR; storage still accepts all seven values, and a stored non-offered precision seeds the edit select as YEAR (ADR-039 §2).

2. Update re-runs every create invariant

An edit can violate the same invariants as a create, so updateRelationship re-runs all of them: self-relation (VALIDATION_ERROR), date coherence + order, reverse PARENT_OF (CIRCULAR_RELATIONSHIP), and the (person, relatedPerson, type) unique constraint via saveAndFlush (DUPLICATE_RELATIONSHIP). Editing into a family type flags both endpoints as family members (additive; never auto-unflags). The directed orientation is preserved per viewpoint — whichever endpoint {personId} already holds on the row stays put — so a PARENT_OF edge remains parent→child whether edited from either person's page.

3. No optimistic locking (@Version)

PersonRelationship gains no @Version; the edit is last-write-wins, matching the person edit form. This is a single-writer family archive, and it avoids the managed- setVersion pitfall (a setVersion on a managed entity is silently ignored by Hibernate — see the integration-test note in #496-era work). If concurrent curation ever becomes real, add @Version plus an explicit client-version compare then.

4. IDOR / anti-enumeration: ownership mismatch is 404, for PUT and DELETE

A {relId} that does not belong to {personId} returns 404 RELATIONSHIP_NOT_FOUND (a shared loadOwnedRelationship helper), so a curator cannot probe relationship ids belonging to people they cannot see. This aligns deleteRelationship from its former 403 to 404 in the same change, so the two mutating endpoints behave identically on the same mismatch.

5. Derived marriage events gain precision for free

TimelineEventService.buildMarriageEvents now sources the Heirat date from the SPOUSE_OF row's from_date + from_date_precision (previously LocalDate.of(fromYear, 1, 1) at hard-coded YEAR). A DAY-precision wedding now surfaces the exact day on the Zeitstrahl. RelationshipInferenceService is unchanged — it is time-ignorant and never read the year fields.

6. relationshipDates.ts lives in $lib/person/, no new boundary

formatRelationshipDateRange mirrors personLifeDates.ts and delegates entirely to the already-tested formatDocumentDate (zero new precision logic). It sits in $lib/person/ next to personLifeDates.ts; its only cross-domain import is formatDocumentDate from $lib/shared/utils/, which the existing person → shared rule in eslint.config.js already permits — no new eslint boundary rule is added.

Consequences

  • V78 is one-way (columns dropped) and is not rolling-deploy-safe — the running JAR maps from_year until redeploy. Deploy order: stop old JAR → run Flyway V78 → start new JAR. Rollback = targeted pg_restore -t person_relationships from the pre-deploy dump (see docs/DEPLOYMENT.md §8). No maintenance window needed (single-writer archive).
  • Relationships are fully editable (type, related person, dates, notes) and the read view shows the date range + notes.
  • RelationshipDTO drops fromYear/toYear for fromDate/fromDatePrecision/ toDate/toDatePrecision; the personBirthYear/relatedPersonBirthYear derived fields are unaffected (ADR-039 §3).