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>
@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>
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>
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>
The canonical upsert path skips validateLifeDates, so a spreadsheet row
with birth_year > death_year - or a preserved hand-entered birth date
conflicting with a canonical death year - violated the V76 CHECK
constraint at flush time and aborted the whole import batch with a raw
500. Resolve the pairs first and, on conflict, keep the person's stored
life dates (empty for a new person), drop the canonical refresh, and log
a WARN with the sourceRef (REQ-IMP-001: never abort the batch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mention dropdown renders precise life dates but receives
PersonSummaryDTO items from /api/persons, which only carried the derived
years - the date fields were silently undefined at runtime. Add
birth/death date + precision to the projection and all four native
queries (searchWithDocumentCount's GROUP BY already listed the columns).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-check aborts on corrupt year data, backfills YYYY-01-01/YEAR,
adds five named CHECK constraints, drops birth_year/death_year.
Staged-Flyway Testcontainers test covers pre-check aborts, backfill
shapes, and post-migration schema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend enum, frontend ErrorCode mirror, getErrorMessage cases, and
error message i18n keys (de/en/es) incl. the mixed-precision workaround
hint in error_birth_after_death.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop unused MAX_CANDIDATES constant (not referenced in service)
- Keep detached-entity safety comment in resolveTags()
- Add 3 new partial-name match tests (23a/b/c) from #763
- Use resolveByName() API in test 28 (replaces findByDisplayNameContaining)
- Add NameMatches glossary entry from #763
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Assert that when the same person id is returned by two different token
fetches, the person appears exactly once in the result -- pinning
fetchPool's putIfAbsent dedup so a future refactor can't silently
double-classify a candidate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AC#4 (maiden alias -> direct) and AC#5 (alias first name -> fetchable +
classifiable) were each split across PersonRepositoryTest (the fetch) and
PersonServiceTest (the classifier with stubs) -- nothing walked
searchByName -> resolveByName end-to-end on real Postgres. Add two tests
in the existing @DataJpaTest slice that build a real PersonService over
the autowired repositories, persist a person with a MAIDEN_NAME alias and
one with an alias firstName, and assert both classify as direct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GLOSSARY entry for NameMatches (direct vs partial name-match strength and how
the search layer maps it); person/README adds resolveByName to the public
surface. No ADR — the matching rule is localized and justified inline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolveNames now delegates to PersonService.resolveByName and maps by match
strength: 1 direct → resolved (auto-select), ≥2 direct → ambiguous, 0 direct
with partials → ambiguous suggestions, 0 candidates → folded into full-text.
A single direct match no longer forces the picker when looser substring hits
coexist. The MAX_CANDIDATES cap moved into PersonService (after classification);
the MAX_NAME_LENGTH guard, resolved-cap overflow, and sender/receiver mapping
are preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Token-set containment over all of a person's name components (firstName,
lastName, alias, each PersonNameAlias first+last, title) decides direct vs
partial. Orchestrates tokenize → cap(8) → fetch pool → classify → cap(10)
after classification, with an empty-token guard and a PII-free debug log of
the outcome bucket. MAX_TOKENS is a DoS control; the after-classify cap keeps a
direct match that sorts past position 10 among partials. Read-only transaction
keeps lazy nameAliases reachable during classification (ADR-022).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The direct-match classifier accepts alias firstName tokens, so the fetch must
surface candidates matchable only via an alias first name. Add a.firstName to
the searchByName LIKE clause (reuses the bound :query — injection-proof). The
person_name_aliases.first_name column already exists; no migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lowercase, split on whitespace/hyphen/apostrophe, drop empties. Applied
symmetrically to query and candidate name components so "Anna-Maria" and
"Anna Maria" tokenize alike. Foundation for resolveByName direct matching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifies the recursive CTE in findDescendantIdsByName expands a parent tag
to include all child IDs, and that findByNameContainingIgnoreCase matches
both parent and child names when the fragment appears in both.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers multi-tag match, no-match FTS fallback, mixed resolution, personRole
bypass, cap at 10, short-keyword skip, dedup, rawQuery suppression when all
keywords resolve, flag independence, colour propagation via resolveEffectiveColors,
and colour=null when depth constraint prevents resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keywords that substring-match the tag taxonomy become OR-union tag filters;
non-matching keywords stay as FTS text. Resolved tags surface in the
NlQueryInterpretation as TagHint objects with effective colours. The
rawQuery fallback is now guarded by hadStructuredMatch to prevent
double-apply when all keywords resolve.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Positional record fields added; all 3 construction sites updated with neutral
defaults; NlQueryParserService wired for TagService (4th constructor arg);
NlQueryParserServiceTest and NlSearchControllerTest synced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @Markus review: tags fetched by findByNameContaining live outside
any transaction; Hibernate's dirty-check never fires on them. The comment
removes the ambiguity for cold readers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies the recursive CTE in findDescendantIdsByName expands a parent tag
to include all child IDs, and that findByNameContainingIgnoreCase matches
both parent and child names when the fragment appears in both.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers multi-tag match, no-match FTS fallback, mixed resolution, personRole
bypass, cap at 10, short-keyword skip, dedup, rawQuery suppression when all
keywords resolve, flag independence, colour propagation via resolveEffectiveColors,
and colour=null when depth constraint prevents resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keywords that substring-match the tag taxonomy become OR-union tag filters;
non-matching keywords stay as FTS text. Resolved tags surface in the
NlQueryInterpretation as TagHint objects with effective colours. The
rawQuery fallback is now guarded by hadStructuredMatch to prevent
double-apply when all keywords resolve.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Positional record fields added; all 3 construction sites updated with neutral
defaults; NlQueryParserService wired for TagService (4th constructor arg);
NlQueryParserServiceTest and NlSearchControllerTest synced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NL search recovered after deploy but went 503 again after a few minutes:
Ollama unloads the model after its default ~5 min keep-alive, so the next
query cold-loads the 4.7 GB model and exceeds the backend's 30s read
timeout (ResourceAccessException -> SMART_SEARCH_UNAVAILABLE). Warm
inference is ~18s; the cold load after idle is what timed out.
- docker-compose.{prod,yml}: set OLLAMA_KEEP_ALIVE=-1 on the ollama
service so the model stays resident and never pays a cold-load penalty
during normal operation (verified on staging: `ollama ps` -> UNTIL
"Forever"; host has 47 GB free).
- application.yaml: raise app.ollama.timeout-seconds 30 -> 60 so the one
unavoidable cold load (first query after an Ollama restart, before the
model is pinned) completes instead of timing out.
Refs #758
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- error_smart_search_unavailable/rate_limited now use "Sie" (formal) to
match the tone of all existing German error messages
- Replace inline FQNs in DocumentService.buildPersonSpec with proper
JoinType + Predicate imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch to wiremock-jetty12 artifact and force ee10 Jetty deps to 12.1.8
to resolve compatibility with Spring Boot 4's Jetty 12.1.8 core.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>