Commit Graph

529 Commits

Author SHA1 Message Date
Marcel
b41e1335d2 refactor(person/relationship): move relationship sub-package under person domain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:35:09 +02:00
Marcel
b466dfcec6 refactor(person): move person domain to package org.raddatz.familienarchiv.person
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:32:06 +02:00
Marcel
a39fd9928c refactor(notification): move notification domain to package org.raddatz.familienarchiv.notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:24:20 +02:00
Marcel
0ad3f3e58d refactor(geschichte): move geschichte domain to package org.raddatz.familienarchiv.geschichte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:19:56 +02:00
Marcel
3643fa357c refactor(tag): move tag domain to package org.raddatz.familienarchiv.tag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:13:55 +02:00
Marcel
89e9a2452e refactor(test): remove issue reference from makeService javadoc
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m44s
CI / OCR Service Tests (pull_request) Successful in 41s
CI / Backend Unit Tests (pull_request) Failing after 3m16s
CI / Unit & Component Tests (push) Failing after 4m5s
CI / OCR Service Tests (push) Successful in 57s
CI / Backend Unit Tests (push) Failing after 3m12s
Issue numbers in code comments rot as the codebase evolves. The why
(keeping real-database fidelity without pulling full service trees in)
is what matters, not the fix number.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 10:37:06 +02:00
Marcel
2506523f3b refactor(transcription/annotation): break mutual repo dependency
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m2s
CI / OCR Service Tests (push) Successful in 42s
CI / Backend Unit Tests (push) Failing after 3m17s
CI / Unit & Component Tests (pull_request) Failing after 3m49s
CI / OCR Service Tests (pull_request) Successful in 39s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
TranscriptionService injected AnnotationRepository; AnnotationService injected
TranscriptionBlockRepository. Each side now talks through the other domain's
service:

- TranscriptionService.deleteByAnnotationId — new write delegation; called
  from AnnotationService.deleteAnnotation in place of the foreign repo.
- AnnotationService.deleteById / deleteAllById — new write delegations; called
  from TranscriptionService for cascading annotation cleanup.
- AnnotationService.findById (added in #417 commit 6) replaces the read.
- @Lazy on AnnotationService's TranscriptionService field breaks the
  resulting two-bean cycle at construction time, mirroring the existing
  @Lazy self-reference pattern in SenderModelService.

Refs #417 (C6.2 violations #10 and #11).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:48:26 +02:00
Marcel
f5151f3949 refactor(ocr-training): route SenderModelService and OcrTrainingService through TranscriptionBlockQueryService
Both services injected TranscriptionBlockRepository directly to read block
counts. They now go through TranscriptionBlockQueryService (count() and
countManualKurrentBlocksByPerson() added as 1-line delegations) — chosen over
TranscriptionService to avoid the existing
SenderModelService → TrainingDataExportService → TranscriptionBlockQueryService
chain reaching back into TranscriptionService and creating a cycle.

Refs #417 (C6.2 violations #8 and #9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:40:34 +02:00
Marcel
310bb5b2d5 refactor(training-export): route export services through owning services
SegmentationTrainingExportService and TrainingDataExportService each injected
TranscriptionBlockRepository, AnnotationRepository and DocumentRepository
directly. They now go through:

- TranscriptionBlockQueryService (extended) for the three eligible-block
  queries — used over TranscriptionService to keep
  SenderModelService → TrainingDataExportService → TranscriptionService cycle-free.
- AnnotationService.findById (new) — read API on the annotation domain.
- DocumentService.findById (already added in #417 commit 3).

The TrainingDataExportServiceTest @DataJpaTest delegates the new service reads
to the real JPA repositories via Mockito stubs in the new makeService helper,
so the integration coverage stays unchanged.

Refs #417 (C6.2 violations #6 and #7).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:36:20 +02:00
Marcel
0ca95d5ad7 refactor(import): route MassImportService through DocumentService
MassImportService injected DocumentRepository for the find-or-create pattern
during ODS/Excel import. Move the two repository touchpoints (findByOriginalFilename,
save) onto DocumentService as 1-line delegations and update the consumer.

Refs #417 (C6.2 violation #1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:27:30 +02:00
Marcel
8b177b9430 refactor(transcription-queue): route through DocumentService projections
TranscriptionQueueService injected DocumentRepository to fetch the four queue
projections. Move the four read methods (findSegmentationQueue,
findTranscriptionQueue, findReadyToReadQueue, findWeeklyStats) onto
DocumentService as 1-line delegations and update the consumer.

Refs #417 (C6.2 violation #5).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:23:25 +02:00
Marcel
e2e7b79067 refactor(thumbnail): route document access through DocumentService
The Thumbnail trio (ThumbnailService, ThumbnailBackfillService,
ThumbnailAsyncRunner) all injected DocumentRepository directly. They now go
through three new DocumentService delegations:

- findById(UUID): Optional<Document> — no-throw variant for the runner's
  log-and-skip behaviour on missing documents.
- findForThumbnailBackfill() — wraps the existing
  findByFilePathIsNotNullAndThumbnailKeyIsNull query.
- updateThumbnailMetadata(Document) — wraps save() for the post-thumbnail
  entity update.

DocumentService also gains @Lazy on its existing ThumbnailAsyncRunner field
to break the new DocumentService ↔ ThumbnailAsyncRunner cycle. lombok.config
adds @Lazy to copyableAnnotations so the field annotation reaches the
generated constructor parameter.

Refs #417 (C6.2 violations #2, #3, #4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 07:20:01 +02:00
Marcel
5c1332cb0e refactor(auth): route password reset through service layer + e2e helper
- PasswordResetService injects UserService instead of AppUserRepository.
- New UserService.findByEmailOptional preserves the silent-fail behaviour of
  the old findByEmail-returning-Optional path; the existing throwing
  findByEmail is unchanged.
- New PasswordResetService.findLatestActiveTokenForEmail exposes the latest
  active reset token without leaking the repository upward.
- New @Profile("e2e") PasswordResetTestHelper wraps that read so the
  AuthE2EController no longer touches PasswordResetTokenRepository directly.
  Profile guard moves from the controller-only annotation to also cover the
  helper bean, so the production graph never instantiates either.

Refs #417 (C6.1 violation #2 + C6.2 violation #12).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 22:26:11 +02:00
Marcel
d5e0e969ef refactor(stats): introduce StatsService and require READ_ALL
StatsController previously injected PersonRepository and DocumentRepository
directly, violating the controller→service→repository layering rule. Move the
two count() calls into a thin StatsService that delegates to PersonService.count
and DocumentService.count. While here, add the missing @RequirePermission(READ_ALL)
flagged by AUDIT-2 §7 — anonymous callers were able to read aggregate document/
person counts.

Refs #417 (C6.1 violation #1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 22:20:14 +02:00
Marcel
eedf5e3ac1 fix(backend): rename users table to app_users
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m43s
CI / OCR Service Tests (pull_request) Successful in 39s
CI / Backend Unit Tests (pull_request) Failing after 3m15s
CI / Unit & Component Tests (push) Failing after 3m37s
CI / OCR Service Tests (push) Successful in 41s
CI / Backend Unit Tests (push) Failing after 3m2s
Aligns the auth-account table name with the AppUser entity. The historical
mismatch (table 'users' alongside table 'persons') misled schema-first readers
into assuming the two were related; renaming to 'app_users' makes the
deliberate split between auth accounts and historical persons explicit at the
schema layer.

Scope: the table itself, the users_groups join table, and the three FK columns
whose name was literally 'user_id'. Semantic FK columns (audit_log.actor_id,
notifications.recipient_id, document_versions.editor_id, etc.) keep their
names — the role they describe is the documentation, not the type.

Closes #418. Unblocks #407 (REFACTOR-1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 21:44:21 +02:00
Marcel
678d9dab38 refactor(training): extract kurrentLabels helper + clarify query comments
Extract repeated `new java.util.HashSet<>(Set.of(TrainingLabel.KURRENT_RECOGNITION))`
into a `kurrentLabels()` helper in TrainingBlockQueryTest and add `import java.util.HashSet`.

Add clarifying comments on the two person-scoped queries in TranscriptionBlockRepository
explaining that they use `MEMBER OF d.trainingLabels` — aligned with the pre-existing
`findEligibleKurrentBlocks()` pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:30:11 +02:00
Marcel
32e4e30e40 test(training): strengthen TrainingBlockQueryTest assertions
Some checks failed
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:25:54 +02:00
Marcel
dd9c4d57ee fix(training): use KURRENT_RECOGNITION label for sender-based block queries
scriptType is only set after OCR runs, which can't happen before we have
a trained model. Both sender-based queries now filter on the training label
instead, consistent with findEligibleKurrentBlocks.

Also adds missing test coverage for findManualKurrentBlocksByPerson and
countManualKurrentBlocksByPerson (4 cases + count parity check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:25:54 +02:00
Marcel
0802889ea9 feat(geschichten): filter by multiple persons with AND semantics
GET /api/geschichten now accepts repeated personId query params and
returns only stories that mention every person supplied. Refactors the
list path to a JPA Specification chain (one EXISTS subquery per id,
mirroring DocumentSpecifications.hasTags) and embeds the
COALESCE(publishedAt, updatedAt) DESC ordering inside the spec so a
single repository.findAll covers all filter combinations.
2026-05-02 19:17:39 +02:00
Marcel
18e5d18cc7 feat(geschichte): V59 grants BLOG_WRITE to existing WRITE_ALL groups
Without this, the Geschichten feature ships dark on prod day-one — no group
holds BLOG_WRITE, so the editor controls never render even for admins. The
mapping "anyone who can write documents can also author family stories" is
the safest default and admins can revoke afterwards via the new checkbox UI.

Closes Tobias's review S5 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:42:46 +02:00
Marcel
e5024fc804 test(geschichte): add Testcontainers integration test and fix V58 author FK
The end-to-end test creates a DRAFT, verifies it is hidden from a READ_ALL
reader (list and getById), publishes it, verifies the reader sees it, then
deletes it and confirms the join rows go with it but the linked Person
remains. Also corrects the V58 author FK to reference the actual users
table (not app_users).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:33:52 +02:00
Marcel
9fc96a15cf feat(geschichte): add REST controller with BLOG_WRITE permission gates
GET endpoints are open to authenticated users (the service layer enforces
DRAFT visibility). POST/PATCH/DELETE require @RequirePermission(BLOG_WRITE).
WebMvcTest slice covers 401/403/200/201/204 paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:31:43 +02:00
Marcel
08d96e5b0f feat(geschichte): add GeschichteService with HTML sanitization and DRAFT visibility rules
DRAFT stories are 404 to readers without BLOG_WRITE (NOT_FOUND, not FORBIDDEN,
to avoid leaking existence). list() forces status=PUBLISHED for non-writers
even when they pass status=null. Body HTML is sanitised via OWASP allow-list
(p, br, strong, em, h2, h3, ul, ol, li) on every save. publishedAt is set on
every transition into PUBLISHED and cleared on retract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:29:11 +02:00
Marcel
b7a2f6c2fe feat(geschichte): add repository and update DTO
GeschichteRepository.search filters by status / personId / documentId in a
single JPQL query so the controller can serve the index page, the person
discovery card, and the document drawer column from one method. The DTO is
shared between create and update like DocumentUpdateDTO.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:25:34 +02:00
Marcel
b944ae9510 feat(geschichte): add entity, status enum, and V58 schema migration
Geschichte holds family memory stories (issue #381). Body is unbounded TEXT
(Tiptap HTML, no length limit). Two join tables link a story to historical
Persons and Documents. A partial index speeds the public index query
(status='PUBLISHED' ORDER BY published_at DESC) and reverse-lookup indexes
support the ?personId and ?documentId filters.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:24:31 +02:00
Marcel
71b249bf31 feat(security): add BLOG_WRITE permission and GESCHICHTE_NOT_FOUND error code
Foundation for the Geschichten (story) domain (issue #381). BLOG_WRITE gates
authoring of family memory stories; GESCHICHTE_NOT_FOUND is also returned for
DRAFTs requested by users without BLOG_WRITE so existence is not leaked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:23:03 +02:00
Marcel
fb6bffd7ee test(TranscriptionService): verify clear() removes prior mentions before applying DTO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:27:26 +02:00
Marcel
bc0824b934 refactor(TranscriptionBlock): document EAGER fetch rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:55:16 +02:00
Marcel
835dc77382 fix(transcription): persist mentionedPersons on block update; eager-load collection
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m22s
CI / OCR Service Tests (push) Successful in 38s
CI / Backend Unit Tests (push) Failing after 3m3s
CI / Unit & Component Tests (pull_request) Failing after 3m21s
CI / OCR Service Tests (pull_request) Successful in 37s
CI / Backend Unit Tests (pull_request) Failing after 3m0s
TranscriptionService.updateBlock was not writing mentionedPersons from the DTO
back to the entity, so @mentions were lost on every save. Clear-then-addAll
pattern avoids Hibernate orphan issues with @ElementCollection.

Switch @ElementCollection fetch to EAGER so callers can read mentionedPersons
outside an active transaction without a LazyInitializationException.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 18:27:18 +02:00
Marcel
2d19ca7244 refactor(backend): delete rename-propagation listener and its infrastructure
PersonMentionPropagationListener rewrites @DisplayName tokens on person rename.
Under the new design, displayName is archival (what the transcriber typed), so
the listener would corrupt transcriptions rather than correct them.

Deletes PersonMentionPropagationListener, PersonDisplayNameChangedEvent, and the
optimistic-lock catch path in PersonService.updatePerson. Removes PERSON_RENAME_CONFLICT
from ErrorCode and all tests that exercised the now-deleted code path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 14:58:18 +02:00
Marcel
091f6c7592 migration(transcription): add unique constraint on (block_id, person_id) sidecar
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m4s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 2m59s
CI / Unit & Component Tests (push) Failing after 3m5s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 2m59s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:42:05 +02:00
Marcel
3a6f90441e test(transcription): add null-text edge case for rewriteBlockText guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:40:52 +02:00
Marcel
13e0801b30 refactor(transcription): extract rewriteBlockText from propagation loop
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m2s
CI / OCR Service Tests (push) Successful in 47s
CI / Backend Unit Tests (push) Failing after 3m16s
CI / Unit & Component Tests (pull_request) Failing after 3m16s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m6s
Extracts the Pattern+Matcher+replaceAll block into a private helper so the
loop body reads as three lines: rewrite text, update sidecar entries, nothing
else. Moves the boundary-condition rationale comment to the helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:04:26 +02:00
Marcel
4c3aa159c5 test(transcription): add updateBlock 400 test for null personId in mention
createBlock has both validation guards (displayName length + personId null).
updateBlock had only the displayName test. Add the symmetric null-personId case
so a future @Valid drop from updateBlock's @RequestBody would be caught.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:03:00 +02:00
Marcel
eb51155b4e test(transcription): rename latency floor test to reflect 5s assertion
Method said inUnderTwoSeconds; assertion checks isLessThan(5000L) with message
"5s". Three sources of truth, three different values. Rename aligns method name
with the assertion that was intentionally raised from 2s to 5s in a prior commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:02:00 +02:00
Marcel
43f474fc5b refactor(repository): remove dead findByMentionedPersons_PersonId derived query
The listener exclusively calls findByPersonIdWithMentionsFetched (JOIN FETCH).
Zero callers exist in production or test code. Leaving it is a maintenance
trap: a future caller would silently trigger N+1 loads on the lazy collection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:00:56 +02:00
Marcel
8ca3f37817 fix(test): update optimistic-lock mock to use JOIN FETCH query method
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m45s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 3m5s
CI / Unit & Component Tests (pull_request) Failing after 3m13s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m14s
PersonServiceTest wired the mock on findByMentionedPersons_PersonId; the listener
now calls findByPersonIdWithMentionsFetched so the mock returned an empty list,
suppressing the saveAllAndFlush call and breaking the exception-propagation test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:22:01 +02:00
Marcel
1dc812bd47 test(transcription): raise latency floor to 5s to prevent false CI failures
2s was generous for correctness but tight for a shared VPS-hosted CI runner
(cold JVM, Testcontainers startup, competing processes). 5s still catches
O(n²) regressions and N+1 queries while eliminating flaky failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:19:09 +02:00
Marcel
7a647b5633 refactor(test): rename test to reflect actual invariant (displayName fields unchanged)
updatePerson_doesNotPublishEvent_whenOnlyAliasChanges implied that alias is
processed by updatePerson — it isn't. The invariant is that the event is
suppressed when title/firstName/lastName are all unchanged regardless of
which non-displayName field changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:17:52 +02:00
Marcel
5f76d4a1ac test(person): controller returns 409 PERSON_RENAME_CONFLICT on optimistic-lock
Add updatePerson_returns409_whenRenameConflict to PersonControllerTest: exercises
the full controller→exception-handler path, not just the service layer. Verifies
HTTP 409 + $.code = PERSON_RENAME_CONFLICT when updatePerson throws a conflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:16:53 +02:00
Marcel
c7958681f5 fix(transcription): eliminate N+1 lazy load in propagation listener
Switch from findByMentionedPersons_PersonId (derived query, returns blocks with
LAZY mentionedPersons) to findByPersonIdWithMentionsFetched (JOIN FETCH, loads
full collections in one round-trip). 200-block propagation: from 201 queries to 2.
Add @Transactional comment documenting join-transaction semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:15:38 +02:00
Marcel
1f3f879f9c test(transcription): JOIN FETCH query loads all block mentions for propagation
Add findByPersonIdWithMentionsFetched to TranscriptionBlockRepository: subquery
finds blocks referencing the renamed person, outer JOIN FETCH loads their full
mentionedPersons collection. Avoids N+1 lazy selects in the propagation listener.
Filtered JOIN FETCH (WHERE m.personId=:personId) was rejected — it loads only one
mention entry per block, risking data loss on saveAllAndFlush.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:14:07 +02:00
Marcel
2d48821f95 refactor(test): TranscriptionServiceTest uses DTO @Builder instead of @AllArgsConstructor
Felix self-review / Sara (PR #366 review). The trailing-`List.of()` pattern
introduced when mentionedPersons was added to the DTOs is brittle: every
future field forces another grep-and-edit pass across this file. Switch
the 8 call sites (1 Create, 7 Update) to .builder() so the test only
specifies the fields it cares about — future DTO growth is invisible to
tests that don't touch the new field.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:40:29 +02:00
Marcel
0def9e9b9d test(transcription): mirror displayName length-cap regression on PUT endpoint
Sara #4 (PR #366 review). The 400-on-201-chars regression guard previously
only covered POST /api/documents/{id}/transcription-blocks. The same @Valid
cascade applies to PUT /api/documents/{id}/transcription-blocks/{blockId}
via UpdateTranscriptionBlockDTO, but no test asserted it — meaning a
silent removal of @Valid on the PUT @RequestBody parameter would slip past
CI. Mirror the test for symmetry.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:39:13 +02:00
Marcel
acffcc8516 refactor(transcription): listener @Component → @Service
Markus #6 (PR #366 review). The class lives in service/ and is service-tier
business logic — wire-by-stereotype consistency calls for @Service. Both
annotations participate in @ComponentScan equivalently, so the bean
registration is unchanged.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:38:06 +02:00
Marcel
48492330a7 test(person): optimistic-lock test exercises real listener saveAllAndFlush path
Sara #3 / Felix #5 (PR #366 review). The previous version stubbed
eventPublisher.publishEvent to throw, which proved the catch-and-translate
syntax but skipped the listener entirely. The test could not have detected
a regression where the listener swallowed the exception or re-wrapped it
with a non-OptimisticLocking type.

Replace with a real PersonMentionPropagationListener instance backed by a
mocked TranscriptionBlockRepository whose saveAllAndFlush throws
ObjectOptimisticLockingFailureException (the actual Spring exception
Hibernate raises). The publisher mock routes the event to the real
listener via doAnswer so the call chain is the production one:
PersonService.updatePerson → publishEvent → listener.onPersonDisplayNameChanged
→ blockRepository.saveAllAndFlush throws → exception bubbles through the
synchronous event dispatcher → PersonService catches → DomainException.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:36:54 +02:00
Marcel
d924d9059c refactor(transcription): drop dead existsById orphan guard from listener
Felix #2 / Markus #1 (PR #366 review). In the synchronous-transactional
path the existsById check could never return false — the rename and the
propagation share one transaction, so the renamed Person is guaranteed to
still exist when the listener runs. The check was forward-protection for
an eventual @Async refactor but its presence today is misleading: it
suggests a runtime branch that no test could reach against the real flow.

Delete the call, drop the PersonService dependency from the listener, drop
the now-unused PersonService.existsById, and remove the orphan-guard test
(it asserted a behaviour that the synchronous path cannot produce). When
async is added later the guard re-enters the codebase deliberately as part
of that refactor.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:35:15 +02:00
Marcel
99aee777de fix(transcription): word-boundary regex prevents single-word displayName corruption
Felix #1 / Markus #5 / Sara #1 (PR #366 review). The naive
text.replace("@" + old, "@" + new) silently corrupted any composite mention
that began with the renamed single-name person — e.g. renaming the
single-name "Hans" turned "@Hans Müller" into "@Henry Müller", obliterating
the historical reference to Hans Müller without warning.

Replace with a regex matching "@OldName" only at a token boundary: not
followed by a letter/digit/hyphen (catches @Hans-Peter) and not followed by
"<space><uppercase>" (catches @Hans Müller). False negatives — e.g.
sentence-initial "@Hans Bekam" — are accepted as the conservative
trade-off; corruption is irrecoverable, missed renames are not.

The new failing test reproduced the reviewer scenario exactly: two persons
("Hans Müller" + single-name "Hans"), one block referencing both, rename
Hans → Henry. Pre-fix output corrupted "@Hans Müller" to "@Henry Müller";
post-fix preserves the composite mention and only updates the standalone.

The existing partial-name guard test (Hans-Peter Müller / Hans Müller) and
multiple-occurrences test still pass — the regex is a strict superset of
the boundary constraints already covered.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:33:15 +02:00
Marcel
5ebe1f1a5a feat(person): require READ_ALL permission on GET /api/persons and /api/persons/{id}
Defense in depth: until now both list and single-person reads only required
authentication, while the write endpoints (POST/PUT/DELETE) were already
gated with @RequirePermission. The hover-card and typeahead introduced in
issue #362 expose person details (life dates, notes, family relationships)
to anyone who can authenticate — adding READ_ALL aligns the GETs with the
write endpoints and matches the access tier already enforced for documents
and transcription blocks.

Two new controller-slice tests assert 403 when an authenticated user lacks
READ_ALL; existing 200-path tests now stipulate `authorities = "READ_ALL"`
explicitly.

Refs #362

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:02:29 +02:00
Marcel
221a6af838 test(transcription): rename propagation across 200 blocks must stay under 2 seconds
Latency floor (Sara): a merge-blocking regression check, not a benchmark.
Seeds 200 blocks each with one mention of the same person, fires the rename,
and asserts the listener completes the entire find/mutate/saveAllAndFlush
cycle in less than two seconds against the Testcontainers Postgres.

Confirms the partial reload (one Auguste → Augusta) actually persisted so
the timing isn't measuring an empty path.

Refs #362

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:58:55 +02:00