refactor: preparatory infrastructure for PersonNameParser enhancements (#209-#212) #213
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Issues #209, #210, #211, and #212 all modify
PersonNameParser.split()and thePersonentity. Each adds a pipeline step and a field. Doing them independently causes migration collisions, repeatedSplitNamerecord changes, and frontend churn.This issue lays the shared foundation so #209-#212 become clean, additive feature issues.
Scope
Part 1 - Redesign
SplitNamerecordCurrent (
PersonNameParser.java:26):Target:
Call sites to update (1 production, 13 test):
PersonService.findOrCreateByAlias()(line 64) — only consumerPersonNameParserTest.java— 13 test methods callingsplit()All existing call sites pass
nullfor the new fields initially. Each feature issue populates its field.Part 2 - Extract
split()pipeline into named methodsCurrent
split()is a monolithic method. Extract each normalization step into a named method and compose them as a pipeline with explicit ordering:Initially each extracted method is a pass-through (returns input unchanged). Each feature issue fills in its logic.
Part 3 -
Personentity changesAdd fields:
Make
firstNamenullable:Part 4 -
PersonTypeenumPart 5 -
PersonNameAliasTypeadditionsAdd to existing enum:
Part 6 - Migration
Single migration file. Version number must be coordinated (next after current max).
Part 7 -
Person.getDisplayName()— single source of truthExposed via
@Schemaso it appears in the generated API types.Part 8 - Frontend: replace all name concatenation with
displayName17+ files currently concatenate
firstName + " " + lastNamedirectly. After addingdisplayNameto the API response, all of these becomeperson.displayName:Components (display):
PersonChip.svelte${person.firstName} ${person.lastName}${person.displayName}PersonCard.svelte{person.firstName} {person.lastName}{person.displayName}PersonMultiSelect.svelte{person.lastName}, {person.firstName}{person.displayName}PersonTypeahead.svelte${person.firstName} ${person.lastName}${person.displayName}DocumentMetadataDrawer.svelte${person.firstName} ${person.lastName}${person.displayName}DocumentList.svelte{doc.sender.firstName} {doc.sender.lastName}{doc.sender.displayName}CorrespondentSuggestionsDropdown.svelte${person.firstName.charAt(0)}${person.lastName.charAt(0)}ConversationTimeline.svelte(both)${r.firstName} ${r.lastName}${r.displayName}MentionEditor.svelte${user.firstName} ${user.lastName}${user.displayName}OverflowPillButton.sveltePersonChipRow.svelteDashboardRecentDocuments.svelteDocumentTopBar.sveltepersons/+page.sveltepersons/[id]/+page.svelte${receiver.firstName} ${receiver.lastName}${receiver.displayName}users/[id]/+page.svelteUtilities:
personFormat.tsabbreviateName()/abbreviateCompact()mention.ts${user.firstName} ${user.lastName}Server files:
briefwechsel/+page.server.ts${p.firstName} ${p.lastName}${p.displayName}conversations/+page.server.ts${p.firstName} ${p.lastName}${p.displayName}+page.server.ts(root)${senderObj.firstName} ${senderObj.lastName}${senderObj.displayName}documents/new/+page.server.ts${data.firstName} ${data.lastName}Initials helper — extract a utility function:
Part 9 - Regenerate API types
After backend changes, run
npm run generate:apito get the newdisplayName,title,personTypefields and nullablefirstNameinto the TypeScript types.Part 10 - i18n keys
Add translation keys for:
PersonTypevalues: PERSON, INSTITUTION, GROUP, UNKNOWNPersonNameAliasType.MAIDEN_NAME: de "Geburtsname", en "Maiden name", es "Apellido de soltera"What this does NOT include
These become clean, additive changes after this foundation lands.
Depends on
Unblocks
Design decisions
displayNameis computed, not stored —@Transientgetter on the entity, exposed via OpenAPI. No database column. Single source of truth.firstName— honest representation of unknown data. No more "?" placeholders for new records. Existing "?" records can be cleaned up in a data migration later.split()method body.lastNamefor full name,firstName = null— no newdisplayNamecolumn needed.getDisplayName()handles this naturally.👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
stripMaidenName(),normalizeDotCompressed(),stripAnnotation(),stripTitle(), andsplitByKnownLastNameOrFallback(). The refactor phase needs a "run all 35 existing tests, confirm green" step before any new behavior is added. This is the safety net.SplitNameconstructor change touches 13 test call sites. Every test that callssplit()and asserts on the result needs updating — not the assertion, but the expected record shape. With 5 fields, assertingresult.firstName()andresult.lastName()still works, but tests should also assertresult.title() == null,result.maidenName() == null, etc. to document the contract that pass-through methods produce nulls.getInitials()helper — the proposed signature takes{ displayName: string; firstName?: string; lastName: string }. But for non-persons (#211),lastNameholds the full institution name.getInitials("Arthur Collignon GmbH")producing "AR" (first two chars) is awkward. Should non-persons show initials at all, or a type icon instead?displayNamemigration (Part 8) touches every component that displays a person name. This is the riskiest part of the issue — one missed file and "null Molly" shows up. Consider splitting Part 8 into its own commit with agrep -r "firstName.*lastName\|lastName.*firstName" frontend/src/verification step.Suggestions
SplitNamefields, update test assertions. (3) Backend entity + migration. (4) Regenerate API types. (5) FrontenddisplayNamemigration. Each step is a commit.tscwill flag every place that accessesfirstNameon a type where it's now optional. Let the compiler find the misses.getInitials()helper should checkpersonType(once available) and return a type icon placeholder for non-persons rather than trying to extract initials from an institution name.🏗️ Markus Keller — Application Architect
Questions & Observations
@Transient getDisplayName()and OpenAPI exposure. A@Transientgetter is not automatically included in the OpenAPI spec by SpringDoc — it needs@Schemaannotation to appear. Confirm the field is annotated with@Schema(accessMode = Schema.AccessMode.READ_ONLY)so it appears in responses but is not accepted in request bodies. Without this, the frontend TypeScript types won't includedisplayName.SKIPvalue inPersonTypeenum. The #211 discussion decided to addSKIPto the enum, but the issue body lists onlyPERSON, INSTITUTION, GROUP, UNKNOWN. The migration's CHECK constraint must includeSKIPif it's in the Java enum, even though it's never persisted — otherwise JPA could theoretically attempt an INSERT withSKIPand hit a constraint violation. Alternatively, ifSKIPis truly never persisted, enforce that in the service layer and leave it out of the CHECK constraint. Either way, the enum and constraint must be aligned.PersonMultiSelectcurrently showslastName, firstName. Part 8 changes this toperson.displayName. ButdisplayNameassembles as[title] [firstName] lastName— forward order. This changes the display format from "Cram, Clara" to "Clara Cram". Is that intentional? Some users may prefer the reversed format for alphabetical scanning. If so, consider agetDisplayNameReversed()or let the component format it locally.MaidenNameResult,AnnotationResult,TitleResult,NameParts) — are these inner records ofPersonNameParser, or top-level classes? As internal pipeline types that no caller sees, they should be package-private inner records. Keep them out of the public API.Suggestions
SKIPto the issue body's Part 4 enum definition so implementers don't miss it.privateinner records ofPersonNameParser. They are implementation details, not API.PersonMultiSelectdisplay format change, decide upfront: doesdisplayNamereplace ALL display patterns (including reversed), or do some components need their own formatting? If the answer is "displayName for everything," update the issue. If some components need reversed order,displayNameis insufficient and those components need to keep accessingfirstName/lastNamedirectly.🧪 Sara Holt — QA Engineer
Questions & Observations
displayNamemigration introduces a new display contract — every component that previously showedfirstName + " " + lastNamenow showsdisplayName. Component tests should verify: (a)displayNamewith title, firstName, and lastName all present, (b)displayNamewith null firstName, (c)displayNamewith null title. These three cases exercise thegetDisplayName()logic end-to-end.getInitials()helper is new behavior. It needs unit tests: firstName present (two initials), firstName null (first two chars of lastName), empty/short lastName edge cases.first_nameand adds two columns. A Testcontainers integration test should verify: (a) an existing person with non-null firstName is unaffected, (b) a new person can be inserted with null firstName, (c) a person can be inserted withperson_type = 'INSTITUTION', (d) insertingperson_type = 'INVALID'fails the CHECK constraint.Suggestions
npm run checkstep after the frontend migration to catch any TypeScript errors from nullablefirstNameor missingdisplayNamereferences. This is the fastest way to find missed files.🔒 Nora "NullX" Steiner — Security Engineer
Questions & Observations
displayNameas a computed field in the API response. Since it's@Transient, it's read-only. But verify that Jackson/SpringDoc does not acceptdisplayNamein request bodies (e.g., PUT/PATCH on a Person). If it does, the value should be silently ignored — never let a client override a computed field. The@Schema(accessMode = READ_ONLY)annotation handles this.titlefield accepts freeform strings. The migration definesVARCHAR(50)with no CHECK constraint on content. When #212 implements the edit form, input validation must be applied (max length, no HTML/script content). For this refactor issue, the field is only populated programmatically (from the parser), so no immediate risk. But flag it for #212.personTypeCHECK constraint alignment with the Java enum. If the Java enum includesSKIPbut the CHECK constraint doesn't, and a bug in the service layer accidentally persists a Person with typeSKIP, the constraint will reject it — which is the correct behavior (defense in depth). This is actually good: the CHECK constraint acts as a safety net for the service-layer invariant. LeaveSKIPout of the CHECK constraint intentionally.firstNameand search queries. Any JPQL query withWHERE p.firstName LIKE :querywill now exclude persons with null firstName from search results. Verify thatDocumentSpecificationsand any person search code handles null firstName withOR p.firstName IS NULLwhere appropriate.Suggestions
titleis added in #212, apply@Size(max = 50)and@Patternvalidation to reject HTML/script content. Not needed in this issue sincetitleis only set by the parser.SKIPfrom the DB CHECK constraint. The constraint enforcing only persistable values is the right design — it catches bugs rather than accommodating them.🎨 Leonie Voss — UI/UX Design Lead
Questions & Observations
displayNamereplaces all name concatenation — but not all display formats are the same. The issue maps everyfirstName + " " + lastNametodisplayName. But some components currently show reversed format:PersonMultiSelectshows{person.lastName}, {person.firstName}. Switching todisplayNamechanges this to forward order. Is that an intentional UX change? Reversed format is common in list contexts for alphabetical scanning. If some components need reversed order, they can't rely ondisplayNamealone.getInitials()helper falls back tolastName.substring(0, 2)when firstName is null. For "Gesellschafter des Verlages", initials would be "GE" — meaningless. Non-person entries should show a type icon (building icon, people icon) instead of initials. The helper needs apersonTypeparameter or the component needs to branch on type.titlefield is not displayed yet in this issue. Part 8 replaces concatenation withdisplayName, which includes the title viagetDisplayName(). So "Tante Molly" will display correctly. But the title is not visually distinguished — it renders in the same weight/size as the name. The #212 discussion with Leonie should define the visual treatment (muted style for title prefix).Suggestions
displayNamereplace the reversed format inPersonMultiSelectandPersonTypeahead, or do those components keep their own formatting? If forward order everywhere, document it as an intentional change.getInitials()helper should acceptpersonTypeand return a type-specific placeholder for INSTITUTION/GROUP/UNKNOWN instead of extracting initials from an institution name.displayNamerenders title in the same style as the name. That's fine as a starting point. The visual distinction (muted title) can come in #212.🛠️ Tobias Wendt — DevOps Engineer
Questions & Observations
V21__add_person_name_aliases.sql), this would be V22. Claim it now in the issue to prevent collisions.npm run generate:apirequires a running backend. Part 9 says regenerate API types after backend changes. The backend must be running with--spring.profiles.active=devfor the OpenAPI spec to be accessible. Ensure the implementation instructions include: (1) apply migration, (2) start backend with dev profile, (3) runnpm run generate:api, (4) commit the regenerated types. If the implementer forgets step 2-3, the frontend types will be stale.Suggestions
npm run generate:api && git diff --exit-code). This would catch the "forgot to regenerate" class of bugs permanently. Not blocking for this issue, but a good improvement to add alongside it.🏗️ Markus Keller — Application Architect (Discussion Summary)
Interactive discussion with Marcel covering 5 open items from the review comments. All resolved.
Resolved Items
SKIP in Java enum vs DB CHECK constraint —
SKIPis added to thePersonTypeJava enum (5 values: PERSON, INSTITUTION, GROUP, UNKNOWN, SKIP). Intentionally omitted from the DB CHECK constraint (4 persistable values). The constraint acts as defense-in-depth — if a bug tries to persist a SKIP record, the DB rejects it. Update Part 4 to show 5-value enum, add a comment in Part 6 noting the intentional omission.Reversed display format — Decision: forward order everywhere.
displayNamereplaces ALL display formats, including the reversed "Cram, Clara" inPersonMultiSelectandPersonTypeahead. This is an intentional UX change. Rationale: the person list is sorted bylastNameanyway (alphabetical scanning works from sort order), and keeping reversed format means each component reinvents null-firstName/title handling — exactly the duplicationdisplayNameeliminates.getInitials()for non-persons — Ship the simple helper now (persons only, nopersonTypecheck). Extend with a non-person branch in #211 when classification actually creates non-person records. No speculative code in this refactor.@Schemaannotation forgetDisplayName()— Must be annotated@Schema(accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.REQUIRED). Without this, SpringDoc won't includedisplayNamein the OpenAPI spec and the frontend TypeScript types won't have the field. Explicitly noted in Part 7.Nullable
firstNameimpact on search — Add an explicit acceptance criterion: "Verify all person search queries (DocumentSpecifications, person search, any JPQL touching firstName) handle null firstName correctly." ThedisplayNamefield is@Transientand cannot be used in JPQL — search must continue usingfirstName/lastName/titleindividually. This is an implementation checklist item, not a design change.Issue Body Updates Needed
SKIPto the enum definitionSKIPintentionally omitted from CHECK constraint@Schema(accessMode = READ_ONLY, requiredMode = REQUIRED)annotationdisplayNameOverall this issue is well-scoped and architecturally sound. The 10-part structure is clear and the dependency chain is correctly ordered.
Implementation Complete
All 10 parts of this issue have been implemented on branch
feat/issues-209-213-person-parser-enhancements.Commits
1e1921edea16358101ddb9f1464892f1a11de2cc670ce803cf11d8a39caef1e1aabd98Key decisions applied
SKIPin Java enum, intentionally omitted from DB CHECK constraint (defense-in-depth)@Schema(accessMode = READ_ONLY, requiredMode = REQUIRED)ongetDisplayName()displayNameeverywhere (including PersonMultiSelect dropdown)displayNamevia default method (computed from title/firstName/lastName)getInitials()without personType check (deferred to #211)Test results