Timeline: TimelineEvent entity + Flyway migration (#774) #816

Open
marcel wants to merge 11 commits from feat/issue-774-timeline-event-entity into main
Owner

Closes #774.

Ships the foundational data model for the new timeline domain (Zeitstrahl milestone): entity + enum + repository + migration + ADR + docs. No service/controller/DTO — those land in later issues.

⚠️ Renumber note

The issue body (and all 7 reviewers) said ADR-039 + V76, but both were claimed by the person life-dates work (#773) that merged on 2026-06-12, after the review. This PR uses the next free numbers: ADR-040 and V77. Using the issue's numbers would have produced a duplicate Flyway version (Flyway refuses to boot) and a duplicate ADR file. Per the issue author's instruction, the issue body was left as-is rather than patched.

What's here

  • timeline/EventTypePERSONAL / HISTORICAL, a stable frontend styling contract.
  • timeline/TimelineEvent — date block mirrors Document (eventDate + precision + eventDateEnd); audit footprint deliberately diverges with @Version + NOT NULL createdBy/updatedBy. precision imports document.DatePrecision (not duplicated), defaults to YEAR. Explicit @JoinTable + @BatchSize on both ManyToMany collections.
  • timeline/TimelineEventRepository — empty with a TODO(issue 5) marker.
  • V77__add_timeline_events.sql — table + two join tables (all FK columns ON DELETE CASCADE), chk_timeline_event_range (biconditional eventDateEnd iff RANGE, intentionally stricter than Document), chk_timeline_event_precision (<> 'UNKNOWN'; SEASON/APPROX stay legal), and all FK + query-column indexes up-front (no V62-style retrofit).
  • ADR-040 + doc updates (package tables, GLOSSARY, new C4 l3-backend-timeline.puml, db-orm + db-relationships ER diagrams).

Tests (TDD red→green, Testcontainers postgres:16-alpine, never H2)

  • TimelineEventTest (17): required-field round-trip, YEAR default, linked persons/documents, eventDateEnd null/range, TEXT description (multi-KB), both RANGE-invariant rejections + UNKNOWN rejection (NOT_SUPPORTED), version null→0L, and a parameterized accept-side over DAY/MONTH/SEASON/YEAR/APPROX.
  • TimelineEventCascadeIntegrationTest (2): deleting a linked Person/Document via the domain service drops the join row (direct COUNT check) and leaves the event intact.
  • MigrationIntegrationTest (45) green — V77 boots clean in sequence. mvnw clean package green.

🤖 Generated with Claude Code

Closes #774. Ships the foundational data model for the new `timeline` domain (Zeitstrahl milestone): entity + enum + repository + migration + ADR + docs. No service/controller/DTO — those land in later issues. ## ⚠️ Renumber note The issue body (and all 7 reviewers) said **ADR-039 + V76**, but both were claimed by the person life-dates work (#773) that merged on 2026-06-12, *after* the review. This PR uses the next free numbers: **ADR-040** and **V77**. Using the issue's numbers would have produced a duplicate Flyway version (Flyway refuses to boot) and a duplicate ADR file. Per the issue author's instruction, the issue body was left as-is rather than patched. ## What's here - **`timeline/EventType`** — `PERSONAL` / `HISTORICAL`, a stable frontend styling contract. - **`timeline/TimelineEvent`** — date block mirrors `Document` (`eventDate` + `precision` + `eventDateEnd`); audit footprint deliberately diverges with `@Version` + NOT NULL `createdBy`/`updatedBy`. `precision` imports `document.DatePrecision` (not duplicated), defaults to `YEAR`. Explicit `@JoinTable` + `@BatchSize` on both ManyToMany collections. - **`timeline/TimelineEventRepository`** — empty with a `TODO(issue 5)` marker. - **`V77__add_timeline_events.sql`** — table + two join tables (all FK columns `ON DELETE CASCADE`), `chk_timeline_event_range` (biconditional `eventDateEnd` iff RANGE, intentionally stricter than `Document`), `chk_timeline_event_precision` (`<> 'UNKNOWN'`; SEASON/APPROX stay legal), and all FK + query-column indexes up-front (no V62-style retrofit). - **ADR-040** + doc updates (package tables, GLOSSARY, new C4 `l3-backend-timeline.puml`, db-orm + db-relationships ER diagrams). ## Tests (TDD red→green, Testcontainers `postgres:16-alpine`, never H2) - `TimelineEventTest` (17): required-field round-trip, YEAR default, linked persons/documents, eventDateEnd null/range, TEXT description (multi-KB), both RANGE-invariant rejections + UNKNOWN rejection (`NOT_SUPPORTED`), `version` null→0L, and a parameterized accept-side over `DAY/MONTH/SEASON/YEAR/APPROX`. - `TimelineEventCascadeIntegrationTest` (2): deleting a linked Person/Document via the domain service drops the join row (direct `COUNT` check) and leaves the event intact. - `MigrationIntegrationTest` (45) green — V77 boots clean in sequence. `mvnw clean package` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 7 commits 2026-06-13 00:34:20 +02:00
PERSONAL/HISTORICAL classify a curated timeline event. The string value
names are a stable frontend styling contract (family vs. muted world
accent) — no mapping layer; renaming requires a coordinated frontend
change. First piece of the new timeline domain (Zeitstrahl, issue #774).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Curated timeline event mirroring Document's date block (eventDate /
precision / eventDateEnd) so events and letters share one rendering path.
Audit footprint deliberately diverges from Document: @Version optimistic
lock plus NOT NULL createdBy/updatedBy for the multi-curator edit flow.
precision reuses document.DatePrecision (imported, not duplicated) and
defaults to YEAR. ManyToMany persons/documents with explicit @JoinTable +
@BatchSize, matching Document's join conventions.

Repository is empty for now with a TODO marker for the issue-5 per-person
filter query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creates timeline_events plus the timeline_event_persons and
timeline_event_documents join tables, all FK columns ON DELETE CASCADE
(a person/document delete drops the join row, the event survives —
V71-class hardening). Two CHECK constraints push integrity to Postgres:
chk_timeline_event_range enforces event_date_end non-null IFF RANGE (a
strict biconditional, intentionally tighter than Document's open-ended
ranges), and chk_timeline_event_precision forbids exactly UNKNOWN while
keeping SEASON/APPROX legal. FK and query-column indexes added up-front
to avoid the V62 retrofit debt. Forward-only, additive DDL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DataJpaTest against real Postgres (never H2): required-field round-trip,
YEAR default, linked persons/documents, eventDateEnd null/range round-trip,
TEXT description with no length cap, both RANGE-invariant rejections, the
UNKNOWN-precision rejection (NOT_SUPPORTED so the constraint violation does
not poison the test transaction), version null-before-persist/0-after-save,
and a parameterized accept-side proving DAY/MONTH/SEASON/YEAR/APPROX all
persist. makeEvent() defaults createdBy/updatedBy to random UUIDs so every
red is red for the intended reason.

@SpringBootTest cascade guard: deleting a linked Person/Document via the
domain service drops the join row (verified by direct COUNT) and leaves the
event intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the architectural commitment for the timeline domain: views-not-
entities for issue 3 (ADR-036 rationale), DatePrecision import coupling
(ADR-025), the UNKNOWN-forbidden / SEASON-APPROX-legal precision contract,
the strict biconditional RANGE CHECK as a deliberate divergence from
Document, the @Version + NOT NULL audit-trail decisions, the optimistic-
lock-to-conflict translation contract (CWE-209), the server-populated-only
createdBy/updatedBy forgery guard (CWE-639), and the EventType stable
frontend styling contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add timeline/ to the root and backend package tables, TimelineEvent to the
domain-model entity tables, TimelineEvent/EventType/Zeitstrahl to the
glossary, a new l3-backend-timeline C4 component diagram, and the
timeline_events table + two join tables (with their CHECKs and cascade FKs)
to the db-orm and db-relationships ER diagrams. Bumps the db-orm snapshot to
V77.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test(timeline): allow timeline package in entity-location ArchRule
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m9s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
3a7c86fc87
The entities_reside_in_domain_packages ArchUnit rule has a hardcoded
allow-list of domain packages; add ..timeline.. so TimelineEvent passes.
CI caught this — the new domain package was not yet whitelisted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marcel force-pushed feat/issue-774-timeline-event-entity from bed99ff3fe to 3a7c86fc87 2026-06-13 00:34:20 +02:00 Compare
marcel added 4 commits 2026-06-13 00:48:22 +02:00
idx_timeline_event_persons_event_id and idx_timeline_event_documents_event_id
duplicated the leading column of their composite primary keys — Postgres already
serves timeline_event_id lookups from the PK index, so the extra indexes only
added write overhead. The inverse-side indexes (person_id, document_id) stay;
they cover the FK cascade path.

Deviates from the #774 task list ("all four FK columns") per PR #816 review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createdBy/updatedBy are NOT NULL and createdAt/updatedAt/version are Hibernate-
populated on every persisted row, so per the CLAUDE.md rule they must carry
@Schema(requiredMode = REQUIRED) like id/title/type/eventDate/precision already
do. Keeps the generated TypeScript types honest if the entity ever reaches the
OpenAPI spec (responses in #775 are planned as views, per ADR-040).

Extends the #774 task list (which named only the five domain fields) per PR #816 review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The issue body's milestone-relative ordinals ("issue 3", "issue 5") become
unreadable once the milestone closes. Resolved against the Zeitstrahl milestone:
issue 3 = #775 (CRUD API: service/controller/DTO), issue 5 = #777 (assembly
endpoint with the per-person filter). Mapping anchored by issue 6 = #778
(date-label helper) and issue 9 = #781 (curator forms) in #774's forward notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(adr): record title-length guard obligation for #775
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m53s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
CI / fail2ban Regex (pull_request) Successful in 54s
CI / Semgrep Security Scan (pull_request) Successful in 27s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m13s
bde1237358
title is VARCHAR(255) with no planned service-level check; ADR-040's
consequences listed the optimistic-lock and forgery obligations for #775 but
not this one, so an over-long title would have surfaced as a raw
DataIntegrityViolationException -> HTTP 500. Mirrors GESCHICHTE_TITLE_TOO_LONG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

Review fixes applied (4 commits, 3a7c86fc..bde12373)

All four findings from the code review are addressed:

  1. 6ed5151e — dropped the two redundant join-table indexes. idx_timeline_event_persons_event_id and idx_timeline_event_documents_event_id duplicated the leading column of their composite PKs; Postgres already serves timeline_event_id lookups from the PK index. The inverse-side indexes (person_id, document_id) stay.
    ⚠️ Deviates from the #774 task list, which asked for "explicit indexes on all four FK columns" — the two dropped ones were dead write overhead.
  2. 62b96f71@Schema(requiredMode = REQUIRED) on createdBy/createdAt/updatedBy/updatedAt/version. All are populated on every persisted row; per the CLAUDE.md rule they must carry the annotation like the five domain fields already did.
    ⚠️ Extends the #774 task list, which named only id/title/type/eventDate/precision.
  3. bde12373 — ADR-040 consequences now record the title-length guard obligation for #775 (TIMELINE_TITLE_TOO_LONG, mirroring GESCHICHTE_TITLE_TOO_LONG) so an over-long title doesn't ship as a raw 500.
  4. 788a8048 — relative issue ordinals pinned to real numbers across entity javadoc, repository TODO, ADR-040, and the C4 diagram: issue 3 = #775 (CRUD API), issue 5 = #777 (assembly endpoint / per-person filter). Mapping anchored by the unambiguous forward notes in #774 (issue 6 = #778 date-label helper, issue 9 = #781 curator forms).

Verification: TimelineEventTest 17/17, MigrationIntegrationTest 45/45 (edited V77 boots clean in sequence), ArchitectureTest 13/13 — all green locally against Testcontainers postgres:16-alpine.

Note for anyone with a local dev DB that already ran this branch's V77: the migration file changed, so Flyway will report a checksum mismatch — flyway repair or recreate the dev DB.

🤖 Generated with Claude Code

## Review fixes applied (4 commits, `3a7c86fc..bde12373`) All four findings from the code review are addressed: 1. **`6ed5151e` — dropped the two redundant join-table indexes.** `idx_timeline_event_persons_event_id` and `idx_timeline_event_documents_event_id` duplicated the leading column of their composite PKs; Postgres already serves `timeline_event_id` lookups from the PK index. The inverse-side indexes (`person_id`, `document_id`) stay. ⚠️ *Deviates from the #774 task list*, which asked for "explicit indexes on all four FK columns" — the two dropped ones were dead write overhead. 2. **`62b96f71` — `@Schema(requiredMode = REQUIRED)` on `createdBy`/`createdAt`/`updatedBy`/`updatedAt`/`version`.** All are populated on every persisted row; per the CLAUDE.md rule they must carry the annotation like the five domain fields already did. ⚠️ *Extends the #774 task list*, which named only `id`/`title`/`type`/`eventDate`/`precision`. 3. **`bde12373` — ADR-040 consequences now record the title-length guard obligation for #775** (`TIMELINE_TITLE_TOO_LONG`, mirroring `GESCHICHTE_TITLE_TOO_LONG`) so an over-long title doesn't ship as a raw 500. 4. **`788a8048` — relative issue ordinals pinned to real numbers** across entity javadoc, repository TODO, ADR-040, and the C4 diagram: issue 3 = #775 (CRUD API), issue 5 = #777 (assembly endpoint / per-person filter). Mapping anchored by the unambiguous forward notes in #774 (issue 6 = #778 date-label helper, issue 9 = #781 curator forms). **Verification:** `TimelineEventTest` 17/17, `MigrationIntegrationTest` 45/45 (edited V77 boots clean in sequence), `ArchitectureTest` 13/13 — all green locally against Testcontainers `postgres:16-alpine`. *Note for anyone with a local dev DB that already ran this branch's V77: the migration file changed, so Flyway will report a checksum mismatch — `flyway repair` or recreate the dev DB.* 🤖 Generated with [Claude Code](https://claude.com/claude-code)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m53s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
CI / fail2ban Regex (pull_request) Successful in 54s
CI / Semgrep Security Scan (pull_request) Successful in 27s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m13s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/issue-774-timeline-event-entity:feat/issue-774-timeline-event-entity
git checkout feat/issue-774-timeline-event-entity
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#816