Verifies PersonRepository.findByGeneration handles match, no-match (empty
list not NPE), and null-generation persons (excluded). Also confirms
TimelineService.assemble() returns a persisted curated event in the
correct year band against real Postgres via Testcontainers.
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TimelineController exposes GET /api/timeline with @RequirePermission(READ_ALL)
and @Validated so @Min(0) on generation fires a 400. Delegates to
TimelineService.assemble(TimelineFilter). DomainException 404/400 propagate
via GlobalExceptionHandler (no extra mapping needed).
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonRepository.findByGeneration(Integer) — boxed to match nullable entity field.
DocumentRepository.findAllForTimeline() — Document.list entity-graph, single query.
Both services delegate with one-liner methods.
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GLOSSARY.md entries for derived event, DerivedEventType, derivedType,
and assembleDerivedEvents() to cover the vocabulary introduced by #776.
Update l3-backend-timeline.puml: remove stale "planned, #775" labels,
add Rel from TimelineEventService to personDomain for assembleDerivedEvents
batch-fetch calls, document the on-read strategy in the component notes.
Refs #776
Co-Authored-By: claude-sonnet-4-6 <noreply@anthropic.com>
RTM: add REQ-001–REQ-016 rows with Done status, implementation files, and test IDs.
CLAUDE.md: expand timeline package entry with TimelineEntryDTO, DerivedEventType,
and assembleDerivedEvents(); add TimelineEntryDTO to domain model table.
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns all SPOUSE_OF edges with JOIN FETCH on both person sides,
preventing N+1 in TimelineService.assembleDerivedEvents() (REQ-011).
Reuses existing findAllByRelationTypeIn query which already JOIN FETCHes.
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DerivedEventType: BIRTH / DEATH / MARRIAGE discriminator for derived events.
TimelineEntryDTO: unified String-id DTO for both curated and derived events;
id is String (not UUID) to accommodate synthetic prefixed ids (birth:/death:/marriage:).
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers three pre-implementation decisions for issue #776:
1. On-read assembly, never persisted (no migration)
2. Synthetic prefixed String ids (birth:/death:/marriage:)
3. assembleDerivedEvents() as the public cross-issue contract on TimelineService
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two ADR-041 files landed on main in parallel (sdd-adoption and
renovate-runner-setup). Renames the SDD one to 042 and repoints its references
(SPEC_DRIVEN_DEVELOPMENT, constitution, .specify/adrs/README, sdd-gate.yml).
The renovate ADR keeps 041; its references are left untouched. Riding this PR
per request.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flip REQ-001..006 for the timeline date-label feature from Planned to Done and
fill the implementing files plus the concrete dateLabel.spec.ts test names that
prove each requirement.
Closes#778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Note above the DatePrecision type that it mirrors the Java DatePrecision enum,
must be updated manually in lockstep with that enum, and must not be migrated
to the OpenAPI-generated type — it drives the shared client-side formatter
shared by documents and the timeline date-label facade.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timelineDateLabel delegates to the shared formatDocumentDate so a timeline
chip renders identically to the same date on a document, in the active
locale (REQ-001/REQ-002). UNKNOWN precision and null/undefined/'' eventDate
short-circuit to null with no formatter call (REQ-003/REQ-004); raw is always
null since timeline events carry no verbatim spreadsheet cell. The facade
owns no precision logic of its own (REQ-005).
Register the new `timeline` frontend domain in the eslint boundaries config
(allowed to import only `shared`) and add src/lib/timeline/** to the vitest
coverage include (REQ-006). The spec partially mocks the paraglide runtime
via importOriginal so getLocale is stubbed while the formatter still resolves
real season/range message exports.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Red phase for the timeline date-label helper. Asserts delegation to the
shared formatDocumentDate (localized DAY de/en, SEASON de, same-year RANGE)
and the null cases for UNKNOWN/empty eventDate. The runtime mock path keeps
the `.js` suffix so it matches the import under test.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
REQ-001..006 -> #778, Status: Planned. Spec (EARS) lives in the issue body;
these rows are the committed traceability anchor. Flipped to Done as tests land.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins @stoplight/spectral-cli@6.16.0 and caches ~/.npm keyed on that version, so
Spectral is fetched once and reused across runs instead of re-downloaded each
time. A version bump busts the cache key deterministically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates the full SDD workflow into one view at the top of the guide: a
Mermaid flowchart (skills, the three gates, the TDD loop), a one-time
prerequisites checklist, and a gates table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spectral v6 ships no implicit ruleset — the CI job exited 'no ruleset found'.
Adds .spectral.yaml (extends spectral:oas, documentation-only warnings relaxed
for design-time stubs), adds operation tags to the _example contract so it lints
clean (0 results), and aligns the api-contract-stub note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Front of the SDD funnel: the requirements engineer interviews the user, elicits
EARS REQ-NNN requirements + measurable acceptance criteria (probing hard for the
Unwanted-behavior clauses specs usually miss), then creates the Gitea feature
issue (issue body = spec), labels it, and emits RTM rows. Authors only — hands
off to /review-issue rather than self-approving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Gitea issue body is the single source of truth for a spec; the only
per-feature artifact in git is the RTM row (REQ-ID -> issue # -> test). Drops
per-feature spec.md/tasks.md/checklist files from the workflow (the _example
stays as a template/reference). Updates the guide, ADR-041, AGENTS.md, CLAUDE.md,
templates, the RTM (adds an Issue column), the implement/review-pr skills, and
replaces the file-spec CI jobs with an rtm-check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
review-issue becomes the SDD spec-review gate (adds the requirements engineer,
pairs each .claude persona identity with its .specify checklist, EARS/REQ-NNN
aware). review-pr verifies the diff against the constitution and the spec's
REQ-NNN traceability. implement reads the spec artifacts, plans from tasks.md,
ties tasks to REQ-NNN, and keeps the RTM and generated API types in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds SPEC_DRIVEN_DEVELOPMENT.md (8-step workflow, before/after issue, persona
review example, agent-prompt example, maintenance rules, cheatsheet) and points
CLAUDE.md, COLLABORATING.md, and CONTRIBUTING.md at the new .specify/ workflow
without altering the existing cycle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds .gitea/ISSUE_TEMPLATE/{feature,bug}.md (the feature template mirrors the
EARS feature-spec) and .gitea/workflows/sdd-gate.yml — spec-lint, contract
validation (Spectral), traceability check (all non-blocking during adoption),
and a constitution-impact PR comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces the SDD root: a v1.0.0 constitution and machine-readable AGENTS.md
grounded in the project's real conventions; six EARS-aware persona spec-review
checklists that cross-reference .claude/personas/; feature-spec/ADR/threat-model/
api-contract templates; a fully worked _example feature; a living RTM; and an
adrs/ pointer that reuses the existing docs/adr/ archive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the decision to layer SDD (EARS requirements, OpenSpec-style delta
artifacts, a versioned constitution, and AGENTS.md) on top of the existing
Gitea-issue + multi-persona-review workflow. Numbered 041 to extend the
existing docs/adr/ archive rather than starting a parallel one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DB CHECK chk_timeline_event_range enforces only the presence
biconditional (eventDateEnd non-null IFF RANGE), not date ordering, so a
RANGE event with eventDateEnd before eventDate persisted silently and
rendered as a negative span. validateRangeInvariant now also rejects
end-before-start (INVALID_DATE_RANGE); equal dates remain a valid one-day
closed range.
Also compute effectivePrecision once per create/update and thread it into
validateRangeInvariant and applyUpdate instead of recomputing.
Addresses review of #822 (#775).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerated frontend/src/lib/generated/api.ts from the OpenAPI spec — adds the
/api/timeline/events paths and TimelineEventRequest/TimelineEventView schemas.
CI has no OpenAPI drift guard, so the regen is committed here. (Operation-id
churn create->create_1 etc. is cosmetic; the typed client keys off paths, not
operation ids; the timeline PersonView merges with geschichte's identical one.)
Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
errors.ts ErrorCode union + getErrorMessage() cases for the four new codes,
with de/en/es i18n keys. Conflict messages are calm/recoverable
('...wurde zwischenzeitlich geändert. Bitte neu laden.'). Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R9 doc-sync: add TIMELINE_EVENT_NOT_FOUND, TIMELINE_EVENT_CONFLICT,
TIMELINE_TITLE_TOO_LONG, and the generic CONFLICT to the valid-error-codes
list in CLAUDE.md and the error-code reference in docs/ARCHITECTURE.md. Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two service-level integration tests against real Postgres (V77 CHECKs are
Postgres-specific): (1) view-assembly round-trip proving the
@Transactional(readOnly=true) LazyInit guard populates persons/documents after
an em.clear()ed fresh getEvent, with a serialized-JSON assertion that no
notes/provisional/password leak; (2) real optimistic-lock 409 — editor B's
stale version yields TIMELINE_EVENT_CONFLICT end-to-end (the unit test only
proves the catch/guard branches).
Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spec's prescribed mechanism (load managed entity -> setVersion(clientVersion)
-> saveAndFlush -> catch ObjectOptimisticLockingFailureException) does NOT engage
the lock: Hibernate ignores a manually-set @Version on a managed entity and uses
its own loaded-version snapshot for the UPDATE ... WHERE version=? clause, so a
stale client write silently succeeds. The integration test the issue mandated to
'prove the lock engages end-to-end' caught exactly this.
Replace it with requireVersionMatch: an explicit compare of the client's
last-seen token against the freshly-loaded version (the true semantics of the Q1
client-supplied-token decision). The native @Version increment still fires on
every save, and the saveAndFlush+catch is retained as the backstop for two
transactions flushing concurrently. Null token => last-write-wins, unchanged.
Deviation from #775's reviewed setVersion mechanism (per maintainer direction the
issue body is left as-is); version unit tests updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Centralized @ExceptionHandler(ObjectOptimisticLockingFailureException) net so
any write path losing a @Version race becomes a generic 409 (CONFLICT code) —
never a 500 + Sentry + Hibernate internals (CWE-209). No Sentry, class-name-
only parameterized logging, body free of id/version/class. Entity-agnostic by
design (no switch on getPersistentClassName); the service catch keeps the
precise TIMELINE_EVENT_CONFLICT. Per #775 Q2/R4/R8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST→201, PUT→200, DELETE→204, GET→200; @RequirePermission(WRITE_ALL) on the
three writes, GET via global auth baseline (no annotation, documented). @Valid
request body; all bodies are TimelineEventView. Injects UserService + private
requireUserId wrapper. Controller slice tests cover 401/403/exact-status per
verb, GET 404, service PERSON_NOT_FOUND→404, Bean-Validation 400s carrying
code=VALIDATION_ERROR, and ArgumentCaptor proof that actorId is the resolved
session principal (not a forged body field) on both write paths.
Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
create/update/delete write methods (@Transactional) + getEvent read
(@Transactional(readOnly=true) for the LazyInit guard). Persons resolved via
PersonService.getAllById with a distinct-size check; documents via per-id
DocumentService.getDocumentById loop; both dedupe-first, fail-closed. RANGE
invariant (both directions), title-length guard, YEAR date normalization, and
default precision. Audit fields server-set (createdBy+updatedBy on create;
only updatedBy on update). Optimistic-lock conflict translated to
TIMELINE_EVENT_CONFLICT via saveAndFlush+catch. Views assembled after flush.
Per #775 / ADR-040.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TimelineEventView + nested PersonView + timeline-local DocumentRef. Explicit
field allow-list, never the raw entity (lazy-collection 500 + curator-field
leak). DocumentRef stays timeline-local by design (#775 R7). Per ADR-040 §2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flat input DTO with Bean Validation (@NotBlank/@NotNull/@Size). createdBy/
updatedBy deliberately absent (server-populated; CWE-639). version is an
optional concurrency token, exempt from the server-only audit rule. Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add TIMELINE_EVENT_NOT_FOUND (404), TIMELINE_EVENT_CONFLICT (409),
TIMELINE_TITLE_TOO_LONG (400), and a generic CONFLICT (409) used by the
optimistic-lock backstop. Per #775 / ADR-040.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
@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>
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>