docs(timeline): pin relative issue ordinals to Gitea issue numbers

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>
This commit is contained in:
Marcel
2026-06-13 00:47:22 +02:00
parent 62b96f718f
commit 788a804810
4 changed files with 10 additions and 10 deletions

View File

@@ -122,7 +122,7 @@ public class TimelineEvent {
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
/** /**
* Optimistic-lock version for the multi-curator edit flow (issue 3). Object {@code Long} * Optimistic-lock version for the multi-curator edit flow (#775). Object {@code Long}
* (not primitive) so it is {@code null} before first persist; Hibernate sets {@code 0} on * (not primitive) so it is {@code null} before first persist; Hibernate sets {@code 0} on
* insert. A concurrent-write conflict must be translated to {@code DomainException.conflict} * insert. A concurrent-write conflict must be translated to {@code DomainException.conflict}
* in the service layer (ADR-040) — otherwise it surfaces as HTTP 500 with Hibernate * in the service layer (ADR-040) — otherwise it surfaces as HTTP 500 with Hibernate

View File

@@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID; import java.util.UUID;
public interface TimelineEventRepository extends JpaRepository<TimelineEvent, UUID> { public interface TimelineEventRepository extends JpaRepository<TimelineEvent, UUID> {
// TODO(issue 5): findByPersonsContaining(Person) needed for the per-person filter // TODO(#777): findByPersonsContaining(Person) needed for the per-person filter
} }

View File

@@ -23,11 +23,11 @@ audit footprint deliberately diverges (see below).
Curated timeline events are their own concern, not a Lesereise/`Geschichte` subtype. Curated timeline events are their own concern, not a Lesereise/`Geschichte` subtype.
The domain owns `TimelineEvent`, `EventType`, and `TimelineEventRepository`. The domain owns `TimelineEvent`, `EventType`, and `TimelineEventRepository`.
### 2. Responses (issue 3) will be views, not serialized entities ### 2. Responses (#775) will be views, not serialized entities
`TimelineEvent` has two LAZY `ManyToMany` collections (`persons`, `documents`) and `TimelineEvent` has two LAZY `ManyToMany` collections (`persons`, `documents`) and
`open-in-view` is `false` — exactly the shape that motivated ADR-036 for `geschichte`. `open-in-view` is `false` — exactly the shape that motivated ADR-036 for `geschichte`.
Issue 3 must assemble `TimelineEventView`/`TimelineEventSummary` inside the service #775 must assemble `TimelineEventView`/`TimelineEventSummary` inside the service
transaction; a serialized entity is a 500 waiting to happen. Decided up front so it is transaction; a serialized entity is a 500 waiting to happen. Decided up front so it is
not retrofitted later. not retrofitted later.
@@ -64,7 +64,7 @@ inferred. This divergence is deliberate: a future "bug fix" must not relax it to
`Document` has neither a version nor a creator. A curated entity edited by multiple curators `Document` has neither a version nor a creator. A curated entity edited by multiple curators
warrants real protection, so `TimelineEvent` adds: warrants real protection, so `TimelineEvent` adds:
- `@Version Long version` — optimistic locking for the multi-curator edit flow (issue 3). - `@Version Long version` — optimistic locking for the multi-curator edit flow (#775).
Object `Long` (not primitive) so it is `null` before first persist; Hibernate sets `0` on Object `Long` (not primitive) so it is `null` before first persist; Hibernate sets `0` on
insert. The service **must** catch `ObjectOptimisticLockingFailureException` and translate insert. The service **must** catch `ObjectOptimisticLockingFailureException` and translate
it to `DomainException.conflict(...)`. Without that translation a concurrent-write conflict it to `DomainException.conflict(...)`. Without that translation a concurrent-write conflict
@@ -82,7 +82,7 @@ session principal before every `save()`, or the timestamp moves while the "who"
### 7. `createdBy`/`updatedBy` are server-populated only — never bound from client input ### 7. `createdBy`/`updatedBy` are server-populated only — never bound from client input
Both are set from the session principal in the service, never from a request body. Binding Both are set from the session principal in the service, never from a request body. Binding
them from client input is an authorship-forgery / mass-assignment vector (CWE-639). Issue 3's them from client input is an authorship-forgery / mass-assignment vector (CWE-639). #775's
regression suite must include forgery cases on **both** write paths (`POST` body with regression suite must include forgery cases on **both** write paths (`POST` body with
`createdBy`, `PUT` body with `updatedBy`) — create and update are separate binding paths, so `createdBy`, `PUT` body with `updatedBy`) — create and update are separate binding paths, so
testing only one leaves half the vector open. The update test must assert `updatedBy` equals testing only one leaves half the vector open. The update test must assert `updatedBy` equals
@@ -108,5 +108,5 @@ the event intact (V71/ADR-032 hardening — a person delete must never 500).
`timeline_events`). No rollback script, no rollback test. `timeline_events`). No rollback script, no rollback test.
- The `timeline → document.DatePrecision` compile coupling is permanent until a shared-package - The `timeline → document.DatePrecision` compile coupling is permanent until a shared-package
refactor; precedent already exists (`importing/DocumentImporter`, `person`). refactor; precedent already exists (`importing/DocumentImporter`, `person`).
- The service/controller/DTO layer (issue 3) inherits the view-assembly, optimistic-lock - The service/controller/DTO layer (#775) inherits the view-assembly, optimistic-lock
translation, forgery-guard, and permission obligations recorded above. translation, forgery-guard, and permission obligations recorded above.

View File

@@ -6,10 +6,10 @@ title Component Diagram: API Backend — Timeline (Zeitstrahl)
ContainerDb(db, "PostgreSQL", "PostgreSQL 16") ContainerDb(db, "PostgreSQL", "PostgreSQL 16")
System_Boundary(backend, "API Backend (Spring Boot)") { System_Boundary(backend, "API Backend (Spring Boot)") {
Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in a later issue.") Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in #777.")
Component(timelineSvc, "TimelineEventService", "Spring Service (planned, issue 3)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.") Component(timelineSvc, "TimelineEventService", "Spring Service (planned, #775)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.")
Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, issue 3)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).") Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, #775)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).")
} }
System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters") System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters")