Person: migrate birth/death year to LocalDate + DatePrecision #773
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?
Milestone: Zeitstrahl — Family Timeline · Foundational (build first)
Spec:
docs/superpowers/specs/2026-06-07-family-timeline-design.md§ "Prerequisite: migrate Person birth/death to date + precision"Context
PersonstoresbirthYear/deathYearasInteger, so a known exact birthday (e.g.1901-03-14) has nowhere to live and any date display is stuck at year precision. The Zeitstrahl's derived life-events need real dates with precision. This change also stands on its own: precise dates then render on person cards, hover cards, and the Stammbaum.Scope
Replace the integer year fields on
Personwith date + precision:birthDateLocalDate(nullable)birthDatePrecisionDatePrecisionNOT NULLUNKNOWN; mirrorsDocument.metaDatePrecisiondeathDateLocalDate(nullable)deathDatePrecisionDatePrecisionNOT NULLUNKNOWNReuse the existing
DatePrecisionenum (document/DatePrecision.java).Precision-column nullability decision (resolved): precision columns are NOT NULL with default
UNKNOWN, matchingDocument.metaDatePrecision. This prevents the illegal "date present, precision null" state and lets the DB enforce the invariant via CHECK constraints. The edit form always sends a precision value.Valid precision set for person dates (resolved): the DB/storage accepts all 7
DatePrecisionvalues (needed for enum-to-string mapping consistency), but the person new/edit form exposes only DAY / MONTH / YEAR.RANGEandSEASONare semantically nonsensical for a birth or death;APPROXis excluded from the form to reduce cognitive load for the 60+ author audience. Pass aPERSON_DATE_PRECISIONS = ['DAY', 'MONTH', 'YEAR']filtered array to the precision<select>rather than forking the component.Out of scope:
PersonRelationship.fromYear(marriage) staysInteger/YEARfor now.Data Model Change
DB schema (V72 migration — single transactional file)
File:
V72__person_birth_death_to_localdate.sqlSteps (all in one file, runs atomically in Flyway's default Postgres transaction):
NOT NULL DEFAULT 'UNKNOWN'.UPDATE persons SET birth_date = make_date(birth_year, 1, 1), birth_date_precision = 'YEAR' WHERE birth_year IS NOT NULL(same for death).CHECK (death_date IS NULL OR birth_date IS NULL OR birth_date <= death_date)— preserves the existing birth ≤ death invariant atLocalDategranularity.CHECK ((birth_date IS NULL) = (birth_date_precision = 'UNKNOWN'))— forbids "date present but precision UNKNOWN" and "precision set but no date".CHECK (birth_date_precision IN ('DAY','MONTH','SEASON','YEAR','RANGE','APPROX','UNKNOWN'))— guards against future enum-drift writing invalid strings (defence-in-depth).death_date/death_date_precision.birth_yearanddeath_yearcolumns.Note (Tobias): latest migration is
V71__person_delete_on_delete_fk.sql— this must be exactlyV72.Entity:
Person.javaReplace
Integer birthYear/deathYearwith:@Schema(requiredMode = REQUIRED)goes onbirthDatePrecisionanddeathDatePrecision(always populated).birthDate/deathDateare nullable, noREQUIRED.API
Input DTOs
PersonUpdateDTOandPersonUpsertCommandchanges:PersonUpdateDTO(from the frontend edit form): gainsLocalDate birthDate,DatePrecision birthDatePrecision,LocalDate deathDate,DatePrecision deathDatePrecision. Jackson rejects unknown enum values by default — confirm no lenient-enum config exists; a bad precision must 400, not be silently coerced.PersonUpsertCommand(from the importer): keepsInteger birthYear/Integer deathYear. The importer only knows a year; the service translatesyear → LocalDate(year, 1, 1) + YEARduring upsert. Do not pushLocalDateinto the importer — it implies a precision the spreadsheet does not have.PersonNodeDTO(Stammbaum backward compat)PersonNodeDTOis a Java record used by the Stammbaum. Keep exposing derivedInteger birthYear/deathYear:REQ-PERSON-DATE-01: When
Person.birthDateis null,PersonNodeDTO.birthYearshall be null. When non-null, it equalsEXTRACT(YEAR FROM birthDate). This prevents NPE frombirthDate.getYear()on persons with no year entered.The native SQL queries in
PersonRepositorythat projectbirth_year AS birthYearfor the Stammbaum layout must projectEXTRACT(YEAR FROM p.birth_date) AS birthYearafter migration.Service Logic
PersonService.validateLifeDates(replacesvalidateYears)Server-side validation — must not regress existing rules:
Use
DomainException.conflict()— never rawResponseStatusException.PersonService.preferHumanDate(new — replaces integerpreferHuman)Re-import precision-preservation rule (ADR-025 extension):
Paired method for precision: when preserving, keep existing precision; when refreshing, set
YEAR.Frontend
personLifeDates.ts— new signatureDelegates all precision rendering to the already-tested
formatDocumentDate— zero new logic.Person new/edit forms
Replace two
<input type="number">fields for birth/death year with two date + precision groups (4 controls total). Group visually in the existing card/section pattern — one card for birth, one for death — to prevent form-explosion on narrow screens.Reuse
WhoWhenSection.svelte's German date input pattern (handleGermanDateInput,DateInputprimitive) and precision<select>— but passPERSON_DATE_PRECISIONS = ['DAY', 'MONTH', 'YEAR']to constrain to 3 options.Label the precision control in plain language: pair
<select>with helper text "Wie genau ist dieses Datum bekannt?" so non-technical transcribers understand the choice. Touch targets ≥ 44px (prefer 48px) per WCAG 2.2.MentionDropdown.svelte(unlisted call-site — add to scope)Line ~328 calls
formatLifeDateRange(person.birthYear, person.deathYear). After API regen,birthYear/deathYearare gone. Update to use the new signature. Verify the dropdown row useswhitespace-normal(nottruncate/whitespace-nowrap) — a DAY-precision date string is ~18 chars longer than a year-only string and must wrap in the narrow autocomplete column.PersonCard.svelte/PersonHoverCard.svelte{#if person.birthDate || person.deathDate}guard — render nothing, not an empty* –.* / †glyphs arearia-hiddendecorative.* 14. März 1901 – † 2. November 1944must wrap, not overflow.Tasks
Person.java: replacebirthYear/deathYearwith four fields (birthDate,birthDatePrecision NOT NULL DEFAULT UNKNOWN,deathDate,deathDatePrecision NOT NULL DEFAULT UNKNOWN).V72__person_birth_death_to_localdate.sql(single file, atomic): add columns → backfillYYYY-01-01/YEAR→ add CHECK constraints (birth≤death, date↔precision coherence, enum-value guard) → dropbirth_year/death_year.PersonRepositorynative SQL queries: replace all projections ofp.birth_year AS birthYear/p.death_year AS deathYearwithEXTRACT(YEAR FROM p.birth_date) AS birthYear/EXTRACT(YEAR FROM p.death_date) AS deathYear. Update theGROUP BYclause (currently listsp.birth_year, p.death_year) top.birth_date, p.birth_date_precision, p.death_date, p.death_date_precision.preferHumanDateinPersonService; updatePersonRegisterImporter/PersonTreeImporter+tools/import-normalizer/persons_tree.pyto call it.PersonUpsertCommandstays year-shaped (Integer birthYear/deathYear); service translates toLocalDate + YEAR.PersonUpdateDTO: addLocalDate birthDate,DatePrecision birthDatePrecision,LocalDate deathDate,DatePrecision deathDatePrecision. Jackson enum rejection must be in effect (no lenient-enum config).PersonService.validateLifeDates(replacesvalidateYears): cross-field birth≤death check atLocalDategranularity + date/precision coherence check. UseDomainException.conflict().PersonNodeDTO: null-safe year derivation in service (birthDate != null ? birthDate.getYear() : null).npm run generate:api) — run before touching any frontend code.personLifeDates.ts: new 5-param signature delegating toformatDocumentDate; update all callers.MentionDropdown.svelte: updateformatLifeDateRangecall to new signature; verifywhitespace-normal.PersonEditForm.svelte,+page.server.ts): date input + precision selector (DAY/MONTH/YEAR only) per birth/death, grouped in section cards;+page.server.tsparses German dd.mm.yyyy → ISO + precision.PersonCard.svelte/PersonHoverCard.svelte: empty-state guard,aria-hiddenon glyphs, 320px wrap test.PersonServiceTest,PersonControllerTest,PersonImportUpsertTest,PersonTreeImporterTest,PersonCard.svelte.test.ts,PersonHoverCard.svelte.spec.ts,PersonEditForm.svelte.test.ts,persons/page.svelte.test.ts— all referencebirthYear/deathYearand must be updated in the same PR.PersonNodeDTO, precision-preservation rule (ADR-025 extension), and precision-column NOT NULL/UNKNOWN nullability decision.docs/architecture/db/db-orm.pumlanddb-relationships.puml(merge blocker).Acceptance Criteria
YEAR-precision dates (YYYY-01-01,birth_date_precision = 'YEAR'); no data loss;birth_year/death_yearcolumns no longer exist post-migration.14. März 1901/March 14, 1901) on the person card and hover card, localized forde/en/es.birthDatePrecision = DAY, when re-imported, then the hand-entered date is unchanged.birthDatePrecision = YEARor whose birth is empty, when re-imported with a different spreadsheet year, then the birth date updates to{new-year}-01-01withYEARprecision.* –).BadSqlGrammarException.YEAR-precision formatted dates) via native query paths, not just the entity layer.Tests
Migration (Testcontainers
postgres:16-alpine— NOT H2; H2 won't honor CHECK constraints)birth_yearpresent,death_yearnull →birth_date='YYYY-01-01'/YEAR, death remains null/UNKNOWN.0000-01-01).birth_yearnull,death_yearset).birth_date > death_daterow (assert post-backfill).birth_yearafter migration fails with an error.personRepository.findSummaries()returns rows withbirthYear = 1901(viaEXTRACT) after migration — proves the query aliases are correct.GROUP BY p.birth_year) to confirm it works after theGROUP BYclause update.Service / importer (unit, Mockito)
preferHumanDate: re-import preserves DAY/MONTH/SEASON-precision hand-entered date.preferHumanDate: re-import refreshes a YEAR/UNKNOWN-precision date when the spreadsheet year changes (proves it's not "never overwrite").preferHumanDate: re-import into an empty field fills at YEAR precision.validateLifeDates: birth after death rejected; equal dates allowed; null sides allowed.validateLifeDates: birthDate non-null with precision UNKNOWN → rejected (coherence check).validateLifeDates: birthDate null with precision DAY → rejected (coherence check).Python (
persons_tree.py)_parse_yearextraction is unchanged; emitted shape still carries the year; importer-side precision defaults to YEAR. Test confirms year extraction only — the Python side never sees precision.Frontend (
*.spec.ts, browser mode)personLifeDates.spec.ts: one assertion perDatePrecision(DAY/MONTH/YEAR/UNKNOWN) × {birth-only, death-only, both}. Include localeen/esfor at least DAY/MONTH to catch German-month-leak class of bug.PersonEditForm.svelte.test.ts(4 fields, not 2):dd.mm.yyyyin the date input.+page.server.tsparses German date + precision correctly.MentionDropdown.svelte.spec.ts: a person with DAY-precisionbirthDaterenders a full date string in the dropdown (behavioral test, not just TS compile check).PersonCard.svelte.test.ts/PersonHoverCard.svelte.spec.ts: updated for new field shapes; 320px wrap does not overflow.Decisions Resolved
UNKNOWNDocument.metaDatePrecision; DB forbids "date present, precision null" via CHECK; stronger invariant with one extra migration lineRANGEandSEASONare nonsensical for birth/death; prevents trap choices for 60+ author audience; passPERSON_DATE_PRECISIONSarray to the selectPersonNodeDTOyear derivationbirthDate != null ? birthDate.getYear() : null)EXTRACT(YEAR FROM p.birth_date) AS birthYearPersonUpsertCommandshapeInteger birthYear/deathYearLocalDate + YEAR; avoids implying a precision the spreadsheet doesn't haveformatLifeDateRangecall and verifywhitespace-normalV71__person_delete_on_delete_fk.sqlis latest)🏗️ Markus Keller — Application Architect
Observations
The issue is architecturally clean. Key strengths:
(date IS NULL) = (precision = 'UNKNOWN')invariant to the DB via CHECK is exactly right — enforced atomically, not app-layer only.PersonNodeDTOkeepingInteger birthYear/deathYearas a derived value is the correct backward-compat pattern for the Stammbaum.PersonUpsertCommandstaying year-shaped keeps the importer contract simple and prevents false-precision leakage from the spreadsheet.Three architectural gaps I found that the issue does not address:
1.
PersonSummaryDTOinterface projection is the primary blast radius the issue underspecifies.The interface at
PersonSummaryDTO.javahasInteger getBirthYear()andInteger getDeathYear(). This is the return type of every native SQL query inPersonRepository—findAllWithDocumentCount,searchWithDocumentCount,findTopByDocumentCount,findByFilter. The issue mentionsGROUP BYupdates but does not explicitly listPersonSummaryDTOin the interface update task, nor does it cover the@Schemaimplications on that interface. The native queries projectp.birth_year AS birthYearin 4 separate SQL strings — all must change toEXTRACT(YEAR FROM p.birth_date) AS birthYear, plusGROUP BY p.id, ..., p.birth_date, p.birth_date_precision, p.death_date, p.death_date_precision. This is listed in the Tasks but the interface change toPersonSummaryDTOis absent.2. ADR-025 cross-reference is under-specified.
The
preferHumanDatefunction extends ADR-025. ADR-025 covers thepreferHumanpattern for string/integer fields. The newpreferHumanDateforLocalDate + precisionis a compound type — the issue describes it well in prose but the ADR-035 task should explicitly note it supersedes the integerpreferHumanfor date fields. Otherwise a future developer might apply the old integerpreferHumanto the new date fields.3. The
PersonSummaryDTOinterface projection needsgetBirthDate()/getBirthDatePrecision()or we lose the ability to render precision-aware dates in the persons list.Currently the list only shows year (
birthYear). After migration, if someone wants to show a DAY-precision date in the person directory,PersonSummaryDTOwould need the new fields. However, the issue resolves this by keepingEXTRACT(YEAR FROM ...)— a conscious scope decision. This is fine for MVP but should be noted in ADR-035 as a known limitation: the person directory always shows year precision even for persons with exact birth dates.Recommendations
PersonSummaryDTOinterface changes explicitly to the task list:Integer getBirthYear()stays (derived viaEXTRACT), but the 4 native SQL queries each need theGROUP BYclause updated — list all 4 by method name so nothing is missed during implementation.PersonSummaryDTOintentionally does not exposebirthDate/birthDatePrecision— person list shows year precision only; full precision is available on the person detail page. This prevents a future refactor that accidentally addsLocalDate getBirthDate()to the interface without updating the 4 native SQL queries.FILTER_WHEREconstant inPersonRepositoryis shared betweenfindByFilterandcountByFilter— theGROUP BYclause is not inFILTER_WHERE, so both queries need independent updates. TheGROUP BYinsearchWithDocumentCountalso referencesp.birth_year, p.death_yearexplicitly and must be updated.Open Decisions (none — all key decisions are resolved in the issue)
👨💻 Felix Brandt — Senior Fullstack Developer
Observations
Codebase-grounded review after reading
Person.java,PersonRepository.java,PersonService.java,PersonUpdateDTO.java,PersonSummaryDTO.java,PersonNodeDTO.java,personLifeDates.ts,MentionDropdown.svelte, andPersonEditForm.svelte.The issue has excellent implementation detail, but a few code-level gaps:
1.
PersonUpdateDTOcurrently uses@Min/@MaxBean Validation on thegenerationfield — the newLocalDate/DatePrecisionfields need similar validation annotations or the DTO leaves Jackson as sole defence.Jackson rejects unknown enum values by default (as the issue notes), but
LocalDatedeserialization from a badly-formatted string will throw a 400 automatically only if you have Jackson'sJavaTimeModuleregistered (which Spring Boot autoconfigures). Confirm this is active. The bigger risk: the issue says "Jackson rejects unknown enum values" but doesn't list adding@JsonCreatoror confirmingDeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIESis in effect. Since the issue's own check says "confirm no lenient-enum config exists", add this as an explicit test assertion inPersonControllerTest, not just prose.2.
preferHumanDateis defined at the service level but the issue's pseudocode returnsLocalDatewithout the paired precision.The precision-return case is mentioned ("Paired method for precision: when preserving, keep existing precision") but is described as a separate method. Consider making
preferHumanDatereturn a recordrecord DatePrecisionPair(LocalDate date, DatePrecision precision) {}to keep the two values atomic and eliminate the risk of the date and precision going out of sync if only one method is called.3.
PersonEditForm.sveltecurrently has two<input type="number">fields for birth/death year (confirmed at lines 92–113 of the file). The issue describes replacing these with date + precision groups usingWhoWhenSection.svelte's pattern.The
handleGermanDateInpututility already exists inWhoWhenSection.svelte— but it is not currently exported from a shared utility module. Before implementing, check whether it needs to be extracted to$lib/shared/utils/or can be imported directly fromWhoWhenSection.svelte. Copying the logic creates a duplication violation.4.
PersonSummaryDTOis an interface projection, not a class. The native query returnsEXTRACT(YEAR FROM p.birth_date) AS birthYear— this aliases to the gettergetBirthYear(). Test this carefully: Spring Data JPA interface projections use the getter name as the alias key. The alias in the native query must exactly match the getter (case-insensitive in Postgres, but verify). Write a@DataJpaTestslice test forPersonRepository.findAllWithDocumentCount()post-migration — this is listed in the issue as a test but should be called out explicitly as a@DataJpaTestnot a full@SpringBootTest.5. The new
BIRTH_AFTER_DEATHandINVALID_DATE_PRECISIONerror codes are referenced invalidateLifeDatesbut the existingErrorCode.javadoes not contain them (it hasINVALID_DATE_RANGEwhich is different). These must be added toErrorCode.javaand mirrored to the frontenderrors.ts— this is documented in the CLAUDE.md pattern but not explicitly in the task list. Add it.Recommendations
preferHumanDate+preferHumanPrecisioninto one method returning a small record/pair — avoids the invariant split.handleGermanDateInputto$lib/shared/utils/germanDate.ts(or confirm the existing location) before the PersonEditForm PR touches it — keeps DRY.BIRTH_AFTER_DEATHandINVALID_DATE_PRECISIONtoErrorCode.java+errors.ts+ i18n message files."PersonEditForm.svelteserver action (+page.server.ts) must parse the Germandd.mm.yyyydate string to ISO before sending to the API. Add an explicit test for the malformed date input path (e.g."not-a-date"→ form returns 400 with a human-readable message, not a Jackson deserialization error stack trace).🔒 Nora Steiner — Application Security Engineer
Observations
No authentication or authorization changes in this issue — the
@RequirePermission(Permission.WRITE_ALL)pattern on person write endpoints already covers this feature, and I confirmed thatPersonControlleruses it. No newPermissionvalues are introduced. Focused review on input validation and enum security.Three security findings:
1. Jackson enum rejection — "confirm no lenient-enum config" is a manual step, not a test. This needs a test.
The issue correctly notes that Jackson rejects unknown enum values by default, and asks to "confirm no lenient-enum config exists." The correct fix is not just a check — it's an automated test. A
PersonControllerTestcase that sends"birthDatePrecision": "BOGUS_VALUE"toPUT /api/persons/{id}should return 400. Without this test, a future developer can accidentally add@JsonProperty(access = WRITE_ONLY)or configureALLOW_COERCION_OF_SCALARSand silently break enum validation. Add this as a test case.2. The
+page.server.tsdate parsing from German format (dd.mm.yyyy → ISO) is a new user-controlled string transformation that needs sanitization.If someone enters
"../../etc/passwd"or"99.99.9999"as a date, the server action must validate before forwarding to the backend. The backend will reject it via Jackson'sLocalDatedeserializer, but the error message returned to the browser must be a friendly i18n message, not Jackson's deserialization exception detail (which would leak implementation info). Explicitly test the malformed-input path: the form action must catch the backend 400 and surface a localized error string viafail(400, { error: ... }).3. The DB CHECK constraints are the right defense layer — verify they are expressed as named constraints for error diagnosis.
CHECK (birth_date IS NULL OR death_date IS NULL OR birth_date <= death_date)is correct. Recommend naming these constraints:Named constraints produce readable error messages in Postgres (
ERROR: new row for relation "persons" violates check constraint "chk_person_birth_before_death") instead of generic ones. This speeds up debugging when the constraint fires in production or tests.Recommendations
PersonControllerTestcase:PUT /api/persons/{id}with"birthDatePrecision": "INVALID_ENUM_VALUE"→ expect 400, not 500.PersonControllerTestcase:PUT /api/persons/{id}with"birthDate": "not-a-date"→ expect 400, not 500 with Jackson stack trace.+page.server.tsaction should surface the backend 400 as a localized form error, not a raw API error — verify this inPersonEditForm.svelte.test.ts.Open Decisions (none)
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
The test coverage plan in the issue is the most complete I've seen for a data model migration. Real Postgres via Testcontainers for migration tests — correct.
@DataJpaTestfor repository projection tests — correct. MultiplepreferHumanDateedge cases called out explicitly. Good foundation.Gaps I found after cross-referencing the test list against the codebase:
1.
familyForest.test.ts— not listed in the test update list but will break.At
/home/marcel/Desktop/familienarchiv/frontend/src/lib/person/genealogy/layout/familyForest.test.tslines 8–136, the test constructsPersonNodeDTOobjects withbirthYeardirectly. After the API regen,PersonNodeDTOstill exposesbirthYear/deathYear(per the issue's backward-compat decision), so these tests should still compile. However, themakePerson()factory function in those tests passesbirthYearto a type-annotatedPersonNodeDTO— verify the generated TypeScript type forPersonNodeDTOstill includesbirthYearas an optional integer after regen. If it does,familyForest.test.tsneeds no changes. If the regen changes the field names, it breaks. Add to the "verify after regen" checklist.2. Migration rollback test is absent.
The issue tests the forward migration thoroughly (backfill, constraints, column drop). But since Flyway migrations on Postgres are transactional, a failed migration rolls back the transaction. Add one test: introduce a row that would violate
birth_date > death_datebefore the migration runs (pre-seed it as a data defect), and verify that either (a) the migration detects and corrects it before dropping columns, or (b) the constraint fires and the migration aborts cleanly. Currently the issue assumes the backfillUPDATEcannot create abirth_date > death_daterow — but that is only safe if the existingbirth_year > death_yeardata was already clean. The issue should add a data-quality pre-check step to the migration:SELECT COUNT(*) FROM persons WHERE birth_year > death_year— if non-zero, fail with a clear message rather than letting the CHECK constraint fail mid-migration.3.
PersonControllerTestcurrently testsbirthYearin the JSON response (line 586 of the file). After migration,Personentity exposesbirthDate/birthDatePrecision— the controller response shape changes. The issue lists this test file for update but the specific assertions about the JSON response shape need to be rewritten, not just the builder calls. Verify the acceptance criterion "Stammbaum unchanged" has a test: the response fromGET /api/persons/stammbaum(or equivalent) must still containbirthYearas a number.4. The
APPROXprecision is excluded from the person edit form (per the issue) but thepersonLifeDates.spec.tsmust still testAPPROXprecision rendering — because existing persons imported with APPROX-precision dates (if any) should still render correctly inPersonCardandPersonHoverCard. The test plan says "one assertion per DatePrecision" — confirmAPPROXis in that list.Recommendations
DO $$ BEGIN IF EXISTS (SELECT 1 FROM persons WHERE birth_year IS NOT NULL AND death_year IS NOT NULL AND birth_year > death_year) THEN RAISE EXCEPTION 'Data quality issue: % persons have birth_year > death_year', (SELECT COUNT(*) FROM persons WHERE birth_year > death_year); END IF; END $$;— run before the backfill so the migration aborts early with a clear message rather than mid-migration.familyForest.test.tsto the post-regen verification step (even if no changes needed — confirm it still compiles and passes).APPROXprecision is inpersonLifeDates.spec.tstest matrix, sinceformatDocumentDatehandles it and person data might haveAPPROXfrom legacy imports.@DataJpaTestslice that callsPersonRepository.findAllWithDocumentCount()and assertsgetBirthYear()returns a non-nullInteger— not just a prose statement.🎨 Leonie Voss — UI/UX Design Lead
Observations
The issue's UX spec is notably well-considered for a data model migration. The form design specifics (DAY/MONTH/YEAR only, helper text, touch targets,
whitespace-normalin dropdown) reflect real user needs. I'm reviewing from the 60+ transcriber audience perspective — this is their data entry path.Four UX concerns after inspecting the current
PersonEditForm.svelte(lines 92–113):1. The current form has two simple number inputs for birth/death year. Replacing these with 4 controls (2 date inputs + 2 precision selects) doubles the field count. On a narrow screen this creates significant form weight.
The issue correctly says "group visually in the existing card/section pattern — one card for birth, one for death." I recommend an explicit layout decision: the date input and its precision select should sit on one row within each card using a flex layout (
flex gap-3 items-start). This way:This prevents the 4 controls from stacking into 4 separate rows on mobile, which creates a long scroll and cognitive mismatch between "birth" and "death" sections.
2. The precision select helper text "Wie genau ist dieses Datum bekannt?" is good, but three options (Tag / Monat / Jahr) without visual differentiation may confuse seniors.
Recommend making the select a visually distinct block with a
<fieldset>+<legend>instead of a<label>+<select>:The option labels should be descriptive German phrases, not raw enum names. "DAY", "MONTH", "YEAR" displayed as-is would fail the 60+ audience immediately.
3. Empty-state behavior when a person has neither date nor precision.
The issue correctly specifies: render nothing (not an empty
* –). Verify this is also true for the case where onlybirthDateis set butdeathDateis null — the output should be* 14. März 1901(birth only, no dash, no†). The currentpersonLifeDates.tscode handles this correctly for year-only, but confirm the new 5-param signature does too.4.
MentionDropdown.sveltewhitespace-normal verification is listed but needs a specific visual scenario.The dropdown is a narrow column. A DAY-precision date like
* 14. März 1901 – † 2. November 1944is ~37 chars. In a narrow autocomplete dropdown (approx 220–280px wide at typical screen sizes), this will wrap to 2 lines. That's fine, but the row height must increase to accommodate wrapping — confirm the dropdown row hasmin-heightorpypadding that doesn't clip the second line. The issue says "verifywhitespace-normal" — I'd add "and confirmoverflow: visibleon the row container."Recommendations
flex flex-col sm:flex-row gap-2ensures mobile stacks, desktop is inline.<option>label text in German (not raw enum values) — "Genaues Datum", "Nur Monat bekannt", "Nur Jahreszahl" — and add a<legend>to the precision group.Open Decisions (none)
🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
Migration V72 confirmed as next in sequence — V71 is the current latest (
V71__person_delete_on_delete_fk.sql). Good. The issue explicitly calls this out, which prevents the collision risk when parallel branches are in flight. One Flyway migration file, atomic in Postgres — correct for a change of this scope.Three DevOps/infrastructure observations:
1. The deploy runbook note is listed as a task ("after V72, the first canonical re-import must be spot-checked") but there is no rollback procedure.
If V72 runs in production and a data problem is discovered post-deployment (e.g., a person whose year data was inconsistent and the backfill produced a wrong date), there is no path forward. The columns are dropped in the same migration. Document the recovery procedure in the runbook note: a partial restore from the pre-migration backup (the nightly
pg_dump) is the only path to recover dropped columns. This is a one-way migration — say so explicitly in the runbook note and confirm a backup was taken immediately before the deploy.2. CI: the Testcontainers migration test will run
postgres:16-alpine— good. But the existingRenameUsersToAppUsersMigrationTest.javasets the precedent for migration-specific tests. Confirm the newV72MigrationTestfollows the same pattern (Testcontainers, not H2, not@SpringBootTestfull context). Full@SpringBootTestruns all migrations but doesn't let you inspect intermediate state. A dedicated migration test class with@DataJpaTestor a rawJdbcTemplateprobe is cleaner for verifying column-specific states.3. The Testcontainers
postgres:16-alpineimage should be pinned to the same digest used in existing tests to ensure consistency.Check
/home/marcel/Desktop/familienarchiv/backend/src/test/java/org/raddatz/familienarchiv/user/RenameUsersToAppUsersMigrationTest.javafor the exact image tag used — if it'spostgres:16-alpine(unpinned), that's consistent with project convention. Confirm V72 test uses the same string, notpostgres:latestor a different minor version.Recommendations
personstable from pre-migration backup; V72 cannot be rolled back via Flyway (columns are dropped)."pg_dumpbackup runs before the deploy window — check the cron schedule indocs/infrastructure/and explicitly note it in the runbook.GROUP BY p.birth_date, p.birth_date_precision, ...) should run as a@DataJpaTestslice with the realPersonRepositoryagainst a Testcontainers postgres — not a unit test with mocked SQL. This is the class of bug that only surfaces on real Postgres with a real schema.Open Decisions (none)
📋 Elicit — Requirements Engineer
Observations
This is an exceptionally well-specified issue. The resolved decisions table, the EARS-style requirement (
REQ-PERSON-DATE-01), and the explicit scope boundary onPersonRelationship.fromYearare all hallmarks of a ready-to-implement spec. The acceptance criteria are testable. Almost all the ambiguities I'd normally flag are already resolved.Two requirements-level gaps I found:
1. AC-4 ("equal dates are allowed") is not reflected in the service pseudocode — but it is in the validation method.
birthDate.isAfter(deathDate)correctly allows equal dates (a person born and died on the same day — unusual but historically documented, e.g., stillbirths). Good. However, the acceptance criterion does not address the case wherebirthDatehasDAYprecision anddeathDatehasYEARprecision pointing to the same calendar year but a day before the birth. Example:birthDate = 1901-11-15 (DAY),deathDate = 1901-01-01 (YEAR). TheLocalDatecomparison1901-11-15.isAfter(1901-01-01)istrue→ rejected. But the intent ofdeathDate = 1901 (YEAR precision)might be "died sometime in 1901, unknown day." The current approach rejects this as a birth-after-death error, which is arguably false — the person could have died in November 1901 after being born. This is a known limitation that should be called out explicitly rather than silently producing a validation error that confuses the transcriber ("but they died in 1901 and were born in November 1901!").Recommendation: add a note in the error message or the spec: "When comparing
DAY-precision birth againstYEAR-precision death (or vice versa), the comparison uses the storedLocalDateas-is (1901-01-01for a YEAR-precision 1901 death). Transcribers should enter1902for the death year if they only know the person died after a November 1901 birthday."2. The i18n scope is underspecified for the new form controls.
The issue says "add i18n keys in
messages/{de,en,es}.json" but does not list the required key names. For a 3-language project with a solo developer, missing key names is a common source of inconsistency (German gets full text, English/Spanish get an empty string or a German fallback). At minimum, define the key names for:person_label_birth_date/person_label_death_date(replacingperson_label_birth_year/person_label_death_year)person_label_birth_date_precision/person_label_death_date_precisionperson_precision_hint("Wie genau ist dieses Datum bekannt?")person_precision_day/person_precision_month/person_precision_year(option labels in the select)person_date_placeholder("leer lassen, wenn unbekannt")BIRTH_AFTER_DEATHandINVALID_DATE_PRECISIONWithout explicit key names, the developer has to invent them during implementation, creating drift between locales.
Recommendations
LocalDatedirectly. TheYEAR-precision date1901-01-01may cause false rejection when compared against a laterDAY-precision date in 1901. Transcribers should use the next calendar year if they only know the death year is the same as a late-in-year birthday."persons/page.svelte.test.ts— confirm this file actually exists; a missing test file in the task list creates confusion during implementation.Open Decisions (none — all key decisions resolved in the spec)