As the archive owner I want one Flyway migration and domain model carrying all import/precision/attribution/identity fields so downstream phases compile against a single, collision-free schema #671

Closed
opened 2026-05-26 22:17:43 +02:00 by marcel · 9 comments
Owner

Phase 2 of the "Handling the Unknowns" milestone — the schema foundation

Multi-persona review found that the original date (#666), name-triage (#665), and importer (#669) issues each planned a separate V69 Flyway migration altering persons — three V69s is a boot failure, and persons.provisional was at risk of being defined twice. This phase consolidates every new column into ONE migration with a single owner, plus the entity/enum/DTO surface, so Phases 3-6 just compile against a finished schema. No import logic, no rendering, no UI here.

The single migration (next free version — confirm at implementation time; head was V68__add_grafana_reader_role.sql)

documents

  • meta_date_precision varchar(16) — the precision enum; backfill then NOT NULL.
  • meta_date_end date NULL — range end (only for RANGE).
  • meta_date_raw text NULL — original date cell, verbatim.
  • sender_text text NULL, receiver_text text NULL — raw attribution preserved even when a person is linked.

persons

  • source_ref varchar unique, indexed — the normalizer person_id; the join key for documents → persons and the idempotency key for re-import.
  • provisional boolean NOT NULL DEFAULT false.

tag

  • source_ref varchar unique, indexed — keyed on the canonical tag_path.

Integrity at the DB layer (review consensus — Markus/Nora/Tobias):

  • CHECK that meta_date_precision is one of the seven enum values.
  • CHECK that meta_date_end is non-null only when precision = RANGE, and null otherwise.
  • CHECK/trigger that meta_date_end >= meta_date for ranges.
  • Backfill before the NOT NULL: meta_date_precision = 'DAY' where meta_date is set, 'UNKNOWN' where null — then add NOT NULL.

Domain model

  • New DatePrecision enum mirroring the normalizer's seven values verbatim: DAY, MONTH, SEASON, YEAR, RANGE, APPROX, UNKNOWN (no translation layer; APPROX is rendered "ca." in Phase 4).
  • Entity fields on Document (@Enumerated(STRING) precision with @Schema(requiredMode = REQUIRED); the rest nullable), Person (source_ref, provisional with @Schema REQUIRED + @Builder.Default), Tag (source_ref). No business logic.
  • DTO surface: add the precision fields to DocumentUpdateDTO, DocumentBatchMetadataDTO, DocumentListItem; add provisional (and source_ref if needed) to PersonSummaryDTO so Phase 5 can filter without a new field. Caveat: PersonSummaryDTO is a native-query INTERFACE PROJECTION consumed by ~3 @Query methods in PersonRepository (findAllWithDocumentCount, searchWithDocumentCount, findTopByDocumentCount), so the new provisional field must be added to all of those native SELECTs or it silently returns false — this needs an integration test (real Postgres), not a unit test.
  • Run npm run generate:api so the TS types pick up DatePrecision and the new fields.

Documentation (blocker)

  • docs/architecture/db/db-orm.puml + db-relationships.puml — all new columns.
  • docs/GLOSSARY.md — "date precision", "source_ref", "provisional person", "raw attribution".
  • ADR — "the importer reads the normalizer's canonical output, and all import-related schema lives in one migration" (the lasting decision behind Phases 1-3).

Acceptance criteria

Scenario: One migration, no collision
  Given the schema migration runs on a fresh database
  Then it applies cleanly as a single Flyway version
  And no other milestone issue adds a migration that alters persons or documents

Scenario: Precision backfill is correct and constrained
  Given existing documents with and without a meta_date
  When the migration runs
  Then dated rows get precision 'DAY' and undated rows get 'UNKNOWN'
  And meta_date_precision becomes NOT NULL
  And a row with precision != 'RANGE' cannot have a non-null meta_date_end

Scenario: Identity column is unique
  Then persons.source_ref and tag.source_ref are unique-indexed
  And persons.provisional defaults to false

Scenario: Types regenerate
  When npm run generate:api runs against the dev backend
  Then DatePrecision and the new document/person fields appear in the generated TS types

Out of scope

  • Reading/loading any canonical file → Phase 3 (importer).
  • Date rendering / formatter / buildTitle → Phase 4.
  • Persons directory UI → Phase 5; undated browse → Phase 6.

Dependency

Independent of Phase 1 #670 (parallelisable), but both #670 and this issue must land before Phase 3 (importer), which compiles against these columns and consumes the canonical files.

## Phase 2 of the "Handling the Unknowns" milestone — the schema foundation Multi-persona review found that the original date (#666), name-triage (#665), and importer (#669) issues **each planned a separate `V69` Flyway migration altering `persons`** — three `V69`s is a boot failure, and `persons.provisional` was at risk of being defined twice. This phase consolidates **every new column into ONE migration with a single owner**, plus the entity/enum/DTO surface, so Phases 3-6 just compile against a finished schema. No import logic, no rendering, no UI here. ## The single migration (next free version — confirm at implementation time; head was `V68__add_grafana_reader_role.sql`) **`documents`** - `meta_date_precision varchar(16)` — the precision enum; backfill then `NOT NULL`. - `meta_date_end date NULL` — range end (only for `RANGE`). - `meta_date_raw text NULL` — original date cell, verbatim. - `sender_text text NULL`, `receiver_text text NULL` — raw attribution preserved even when a person is linked. **`persons`** - `source_ref varchar` **unique, indexed** — the normalizer `person_id`; the join key for documents → persons and the idempotency key for re-import. - `provisional boolean NOT NULL DEFAULT false`. **`tag`** - `source_ref varchar` unique, indexed — keyed on the canonical `tag_path`. **Integrity at the DB layer (review consensus — Markus/Nora/Tobias):** - `CHECK` that `meta_date_precision` is one of the seven enum values. - `CHECK` that `meta_date_end` is non-null **only** when precision = `RANGE`, and null otherwise. - `CHECK`/trigger that `meta_date_end >= meta_date` for ranges. - **Backfill before the NOT NULL:** `meta_date_precision = 'DAY'` where `meta_date` is set, `'UNKNOWN'` where null — then add `NOT NULL`. ## Domain model - New `DatePrecision` enum mirroring the normalizer's seven values **verbatim**: `DAY, MONTH, SEASON, YEAR, RANGE, APPROX, UNKNOWN` (no translation layer; `APPROX` is rendered "ca." in Phase 4). - Entity fields on `Document` (`@Enumerated(STRING)` precision with `@Schema(requiredMode = REQUIRED)`; the rest nullable), `Person` (`source_ref`, `provisional` with `@Schema` REQUIRED + `@Builder.Default`), `Tag` (`source_ref`). No business logic. - DTO surface: add the precision fields to `DocumentUpdateDTO`, `DocumentBatchMetadataDTO`, `DocumentListItem`; add `provisional` (and `source_ref` if needed) to `PersonSummaryDTO` so Phase 5 can filter without a new field. **Caveat: `PersonSummaryDTO` is a native-query INTERFACE PROJECTION** consumed by ~3 `@Query` methods in `PersonRepository` (`findAllWithDocumentCount`, `searchWithDocumentCount`, `findTopByDocumentCount`), so the new `provisional` field must be added to **all** of those native SELECTs or it silently returns false — this needs an **integration test** (real Postgres), not a unit test. - Run `npm run generate:api` so the TS types pick up `DatePrecision` and the new fields. ## Documentation (blocker) - `docs/architecture/db/db-orm.puml` + `db-relationships.puml` — all new columns. - `docs/GLOSSARY.md` — "date precision", "source_ref", "provisional person", "raw attribution". - **ADR** — "the importer reads the normalizer's canonical output, and all import-related schema lives in one migration" (the lasting decision behind Phases 1-3). ## Acceptance criteria ```gherkin Scenario: One migration, no collision Given the schema migration runs on a fresh database Then it applies cleanly as a single Flyway version And no other milestone issue adds a migration that alters persons or documents Scenario: Precision backfill is correct and constrained Given existing documents with and without a meta_date When the migration runs Then dated rows get precision 'DAY' and undated rows get 'UNKNOWN' And meta_date_precision becomes NOT NULL And a row with precision != 'RANGE' cannot have a non-null meta_date_end Scenario: Identity column is unique Then persons.source_ref and tag.source_ref are unique-indexed And persons.provisional defaults to false Scenario: Types regenerate When npm run generate:api runs against the dev backend Then DatePrecision and the new document/person fields appear in the generated TS types ``` ## Out of scope - Reading/loading any canonical file → **Phase 3 (importer)**. - Date rendering / formatter / buildTitle → **Phase 4**. - Persons directory UI → **Phase 5**; undated browse → **Phase 6**. ## Dependency Independent of Phase 1 #670 (parallelisable), but **both #670 and this issue must land before Phase 3 (importer)**, which compiles against these columns and consumes the canonical files.
marcel added this to the Handling the Unknowns — honest uncertainty in dates & people milestone 2026-05-26 22:17:43 +02:00
marcel added the P0-criticalfeature labels 2026-05-26 22:18:33 +02:00
Author
Owner

Markus Keller — Senior Application Architect

This is exactly the right move. Consolidating three competing V69s into one owned migration is textbook "get the schema right before application code." I confirmed the migration head is V68__add_grafana_reader_role.sql, so V69 is the correct next version — no collision today.

Observations

  • Push integrity to Postgres — this issue already does, and the precedent exists. Your CHECK on the precision enum mirrors V22__person_type_title_nullable_firstname.sql (CHECK (person_type IN ('PERSON', 'INSTITUTION', 'GROUP', 'UNKNOWN'))) verbatim. Follow that style exactly so the next reviewer recognizes the pattern.
  • PersonSummaryDTO is a native-query interface projection, not a class. It is consumed by three native @Query methods in PersonRepository (findAllWithDocumentCount, searchWithDocumentCount, findTopByDocumentCount). Adding a provisional getter to the interface is silent unless p.provisional is added to all three SELECT lists — otherwise the getter returns null at runtime with no compile error. This is the single biggest hidden-coupling trap in the issue.
  • tag table is singular tag (confirmed in V1__initial_schema.sql:73), and the Tag entity has no @Table annotation — relies on the default. The migration must ALTER TABLE tag, not tags.
  • DatePrecision enum is a verbatim mirror of the normalizer's Precision(StrEnum) (dates.py:9 — DAY, MONTH, SEASON, YEAR, RANGE, APPROX, UNKNOWN). Good: no translation layer. This is the lasting decision the ADR must capture.

Recommendations

  • Write the ADR as specified — it is a blocker, and I'd sharpen its scope. The durable decision is two-pronged: (1) "all import/precision schema lives in one Flyway migration with a single owner" and (2) "the backend DatePrecision enum is a verbatim mirror of the Python normalizer's Precision; the canonical output is the contract — no translation layer." Record the second prong explicitly so a future dev doesn't "improve" the enum and silently break import idempotency.
  • Update both DB diagrams (db-orm.puml + db-relationships.puml) — five new documents columns, two persons columns, one tag column. The relationship diagram needs no new lines (no FKs), but the ORM diagram needs every attribute. This is a blocker per my doc-currency table.
  • Enforce the range invariant with a CHECK, not a trigger, if at all possible. CHECK (meta_date_end IS NULL OR meta_date_end >= meta_date) is a single immutable-row check — no trigger needed. The issue says "CHECK/trigger"; pick CHECK. A trigger is operational overhead and harder to test. Reserve a trigger only if you need cross-row logic, which you don't here.
  • Keep source_ref nullable. It is an import-only identity key; manually created persons/tags will never have one. Unique + nullable is correct in Postgres (multiple NULLs allowed). Make sure the unique index is a plain unique index, not a constraint that rejects NULLs.

Open Decisions

  • Range semantics for the precision CHECK interaction. The issue says meta_date_end is non-null only when precision = RANGE. But the normalizer also emits RANGE precision with meta_date set to the range start (e.g. dates.py:213 returns (year-01-01, RANGE)). Confirm the intended contract: does every RANGE row require a non-null meta_date_end, or is a RANGE with only a start (open-ended range) legal? The two CHECKs as written (end non-null only for RANGE + end >= meta_date) permit a RANGE row with null end — decide if that is intended or if RANGE ⟺ end IS NOT NULL should be biconditional. Cost: a biconditional CHECK is stricter and may reject normalizer output that emits open ranges; the looser version permits ambiguous range rows downstream.
## Markus Keller — Senior Application Architect This is exactly the right move. Consolidating three competing `V69`s into one owned migration is textbook "get the schema right before application code." I confirmed the migration head is `V68__add_grafana_reader_role.sql`, so `V69` is the correct next version — no collision today. ### Observations - **Push integrity to Postgres — this issue already does, and the precedent exists.** Your CHECK on the precision enum mirrors `V22__person_type_title_nullable_firstname.sql` (`CHECK (person_type IN ('PERSON', 'INSTITUTION', 'GROUP', 'UNKNOWN'))`) verbatim. Follow that style exactly so the next reviewer recognizes the pattern. - **`PersonSummaryDTO` is a native-query interface projection, not a class.** It is consumed by three native `@Query` methods in `PersonRepository` (`findAllWithDocumentCount`, `searchWithDocumentCount`, `findTopByDocumentCount`). Adding a `provisional` getter to the interface is silent unless `p.provisional` is added to all three SELECT lists — otherwise the getter returns null at runtime with no compile error. This is the single biggest hidden-coupling trap in the issue. - **`tag` table is singular `tag`** (confirmed in `V1__initial_schema.sql:73`), and the `Tag` entity has no `@Table` annotation — relies on the default. The migration must `ALTER TABLE tag`, not `tags`. - **`DatePrecision` enum is a verbatim mirror of the normalizer's `Precision(StrEnum)`** (`dates.py:9` — DAY, MONTH, SEASON, YEAR, RANGE, APPROX, UNKNOWN). Good: no translation layer. This is the lasting decision the ADR must capture. ### Recommendations - **Write the ADR as specified — it is a blocker, and I'd sharpen its scope.** The durable decision is two-pronged: (1) "all import/precision schema lives in one Flyway migration with a single owner" and (2) "the backend `DatePrecision` enum is a verbatim mirror of the Python normalizer's `Precision`; the canonical output is the contract — no translation layer." Record the second prong explicitly so a future dev doesn't "improve" the enum and silently break import idempotency. - **Update both DB diagrams** (`db-orm.puml` + `db-relationships.puml`) — five new `documents` columns, two `persons` columns, one `tag` column. The relationship diagram needs no new lines (no FKs), but the ORM diagram needs every attribute. This is a blocker per my doc-currency table. - **Enforce the range invariant with a CHECK, not a trigger, if at all possible.** `CHECK (meta_date_end IS NULL OR meta_date_end >= meta_date)` is a single immutable-row check — no trigger needed. The issue says "CHECK/trigger"; pick CHECK. A trigger is operational overhead and harder to test. Reserve a trigger only if you need cross-row logic, which you don't here. - **Keep `source_ref` nullable.** It is an import-only identity key; manually created persons/tags will never have one. Unique + nullable is correct in Postgres (multiple NULLs allowed). Make sure the unique index is a plain unique index, not a constraint that rejects NULLs. ### Open Decisions - **Range semantics for the precision CHECK interaction.** The issue says `meta_date_end` is non-null *only* when precision = `RANGE`. But the normalizer also emits `RANGE` precision with `meta_date` set to the range *start* (e.g. `dates.py:213` returns `(year-01-01, RANGE)`). Confirm the intended contract: does every `RANGE` row *require* a non-null `meta_date_end`, or is a `RANGE` with only a start (open-ended range) legal? The two CHECKs as written (`end non-null only for RANGE` + `end >= meta_date`) permit a `RANGE` row with null end — decide if that is intended or if `RANGE ⟺ end IS NOT NULL` should be biconditional. _Cost: a biconditional CHECK is stricter and may reject normalizer output that emits open ranges; the looser version permits ambiguous range rows downstream._
Author
Owner

Felix Brandt — Senior Fullstack Developer

Clean, well-scoped schema-only issue. The TDD path here is mostly integration-level (migration + entity mapping), and I want to flag a couple of construction sites that the issue's DTO list glosses over.

Observations

  • PersonSummaryDTO is an interface projection backed by native SQL (PersonRepository.findAllWithDocumentCount and two siblings). Adding boolean isProvisional() to the interface compiles fine but returns null/false silently unless I add p.provisional to all three native SELECT column lists. There is no compile-time safety net here — this needs an integration test asserting the projected value, not a unit test.
  • DocumentListItem is a record with exactly one construction site: DocumentService.toListItem() (line 755). Adding metaDatePrecision/metaDateEnd to the record forces a change there. Good news: it's one place. I'll add the precision field as @Schema(requiredMode = REQUIRED) since every document row will have a backfilled precision.
  • DocumentUpdateDTO and DocumentBatchMetadataDTO are plain @Data POJOs — trivial to extend, no construction sites to chase.
  • The DatePrecision enum maps verbatim to dates.py:9's Precision(StrEnum). I'll use @Enumerated(EnumType.STRING) on the Document field, matching how scriptType and status are already done in Document.java.

Recommendations

  • Write the migration test first (red), against Testcontainers Postgres — not the entity. The first failing test is "fresh DB → V69 applies cleanly and meta_date_precision is NOT NULL." Then a second: "dated row backfills to DAY, undated to UNKNOWN." This is the red/green that proves the backfill, and it can only run on real Postgres (H2 won't honor the CHECK).
  • Mark provisional with @Builder.Default private boolean provisional = false; and @Schema(requiredMode = REQUIRED) — identical to the existing familyMember field in Person.java:55. Reuse that exact pattern; don't invent a new one.
  • For the precision field on Document, use the same shape as status: @Enumerated(EnumType.STRING) @Column(name = "meta_date_precision", nullable = false) @Schema(requiredMode = REQUIRED) @Builder.Default private DatePrecision metaDatePrecision = DatePrecision.UNKNOWN;. A non-null default in the builder prevents NPEs in tests that construct documents without going through import.
  • Don't forget npm run generate:api is in the acceptance criteria for a reason — the TS DatePrecision union and the new optional fields must land in the generated types in this PR, or Phase 4/5 frontend work has no types to compile against. Verify the generated diff is committed.

Open Decisions

None. The construction sites are identifiable and the patterns to copy already exist in the codebase.

## Felix Brandt — Senior Fullstack Developer Clean, well-scoped schema-only issue. The TDD path here is mostly integration-level (migration + entity mapping), and I want to flag a couple of construction sites that the issue's DTO list glosses over. ### Observations - **`PersonSummaryDTO` is an interface projection backed by native SQL** (`PersonRepository.findAllWithDocumentCount` and two siblings). Adding `boolean isProvisional()` to the interface compiles fine but returns null/false silently unless I add `p.provisional` to all three native SELECT column lists. There is no compile-time safety net here — this needs an integration test asserting the projected value, not a unit test. - **`DocumentListItem` is a record with exactly one construction site:** `DocumentService.toListItem()` (line 755). Adding `metaDatePrecision`/`metaDateEnd` to the record forces a change there. Good news: it's one place. I'll add the precision field as `@Schema(requiredMode = REQUIRED)` since every document row will have a backfilled precision. - **`DocumentUpdateDTO` and `DocumentBatchMetadataDTO` are plain `@Data` POJOs** — trivial to extend, no construction sites to chase. - **The `DatePrecision` enum maps verbatim to `dates.py:9`'s `Precision(StrEnum)`.** I'll use `@Enumerated(EnumType.STRING)` on the `Document` field, matching how `scriptType` and `status` are already done in `Document.java`. ### Recommendations - **Write the migration test first (red), against Testcontainers Postgres — not the entity.** The first failing test is "fresh DB → V69 applies cleanly and `meta_date_precision` is NOT NULL." Then a second: "dated row backfills to `DAY`, undated to `UNKNOWN`." This is the red/green that proves the backfill, and it can only run on real Postgres (H2 won't honor the CHECK). - **Mark `provisional` with `@Builder.Default private boolean provisional = false;` and `@Schema(requiredMode = REQUIRED)`** — identical to the existing `familyMember` field in `Person.java:55`. Reuse that exact pattern; don't invent a new one. - **For the precision field on `Document`, use the same shape as `status`:** `@Enumerated(EnumType.STRING) @Column(name = "meta_date_precision", nullable = false) @Schema(requiredMode = REQUIRED) @Builder.Default private DatePrecision metaDatePrecision = DatePrecision.UNKNOWN;`. A non-null default in the builder prevents NPEs in tests that construct documents without going through import. - **Don't forget `npm run generate:api` is in the acceptance criteria for a reason** — the TS `DatePrecision` union and the new optional fields must land in the generated types in this PR, or Phase 4/5 frontend work has no types to compile against. Verify the generated diff is committed. ### Open Decisions _None. The construction sites are identifiable and the patterns to copy already exist in the codebase._
Author
Owner

Nora Steiner ("NullX") — Application Security Engineer

Schema-and-entity issue with no new endpoints, so the attack surface is small. My focus is on the integrity constraints (these are security controls — they fail closed) and the new free-text columns.

Observations

  • sender_text / receiver_text are new text columns carrying raw attribution. No length bound. Compare V18__add_transcription_blocks.sql:5 which caps text at CHECK (length(text) <= 10000). Unbounded user-influenced text is a (mild) DoS / storage-abuse vector and an inconsistency with existing convention. Same applies to meta_date_raw.
  • source_ref is the re-import idempotency key. That makes it security-relevant: if two distinct normalizer persons collide on source_ref, the unique index fails closed (good) — but the import phase must surface that as a structured conflict, not a silent skip. That's Phase 3's job; just ensure the unique index exists so the DB enforces it rather than application code (no race window).
  • The precision CHECK is a fail-closed allowlist — exactly right. Mirror V22's IN (...) form so an out-of-enum value is rejected at write time, not discovered later as corrupt rendering input.

Recommendations

  • Add CHECK (length(...) <= N) to meta_date_raw, sender_text, and receiver_text to match the transcription_blocks precedent. These cells originate from spreadsheet imports; bound them (10000 is the house number). Defense in depth at the DB layer costs nothing and prevents a malformed/huge cell from bloating a row.
  • Keep the enum CHECK as a literal IN ('DAY','MONTH',...) list, not a reference to anything app-side. The whole point is that the DB enforces validity independent of the Java enum — if someone deletes a Java enum value, the DB still rejects bad data.
  • No @RequirePermission work here since there are no new write endpoints — correct scope. When Phase 5 exposes provisional via PersonSummaryDTO, confirm it doesn't leak anything sensitive; a boolean "this person is provisional" is metadata, not PII, so I have no concern there.

Open Decisions

None. The constraints are the security control and they're already specified; I'm only asking to extend the existing length-bound convention to the new text columns.

## Nora Steiner ("NullX") — Application Security Engineer Schema-and-entity issue with no new endpoints, so the attack surface is small. My focus is on the integrity constraints (these *are* security controls — they fail closed) and the new free-text columns. ### Observations - **`sender_text` / `receiver_text` are new `text` columns carrying raw attribution.** No length bound. Compare `V18__add_transcription_blocks.sql:5` which caps text at `CHECK (length(text) <= 10000)`. Unbounded user-influenced text is a (mild) DoS / storage-abuse vector and an inconsistency with existing convention. Same applies to `meta_date_raw`. - **`source_ref` is the re-import idempotency key.** That makes it security-relevant: if two distinct normalizer persons collide on `source_ref`, the unique index fails closed (good) — but the import phase must surface that as a structured conflict, not a silent skip. That's Phase 3's job; just ensure the unique index exists so the DB enforces it rather than application code (no race window). - **The precision CHECK is a fail-closed allowlist** — exactly right. Mirror `V22`'s `IN (...)` form so an out-of-enum value is rejected at write time, not discovered later as corrupt rendering input. ### Recommendations - **Add `CHECK (length(...) <= N)` to `meta_date_raw`, `sender_text`, and `receiver_text`** to match the `transcription_blocks` precedent. These cells originate from spreadsheet imports; bound them (10000 is the house number). Defense in depth at the DB layer costs nothing and prevents a malformed/huge cell from bloating a row. - **Keep the enum CHECK as a literal `IN ('DAY','MONTH',...)` list**, not a reference to anything app-side. The whole point is that the DB enforces validity independent of the Java enum — if someone deletes a Java enum value, the DB still rejects bad data. - **No `@RequirePermission` work here** since there are no new write endpoints — correct scope. When Phase 5 exposes `provisional` via `PersonSummaryDTO`, confirm it doesn't leak anything sensitive; a boolean "this person is provisional" is metadata, not PII, so I have no concern there. ### Open Decisions _None. The constraints are the security control and they're already specified; I'm only asking to extend the existing length-bound convention to the new text columns._
Author
Owner

Sara Holt — Senior QA Engineer

This is a migration-and-mapping issue, so the test strategy lives almost entirely at the integration layer against real Postgres. The acceptance criteria are written as Gherkin already — good — but a few are not yet covered by an obvious test and one will be invisible to H2.

Observations

  • Every acceptance scenario here requires Testcontainers postgres:16-alpine, never H2. The backfill, the NOT NULL transition, and the CHECK constraints are all PostgreSQL-specific behaviors. An H2 run would pass while the real migration fails. The backend already gates at 88% branch coverage (backend CLAUDE.md) and uses Testcontainers — this issue must land its migration test in that suite.
  • The "precision backfill is correct" scenario has two distinct assertions (dated → DAY, undated → UNKNOWN) plus the NOT NULL transition plus the negative CHECK (non-RANGE row cannot have a non-null meta_date_end). That's four behaviors → four tests, one logical assertion each.
  • The PersonSummaryDTO projection change is the highest regression risk and the easiest to under-test. Adding provisional to the interface compiles even if the native query forgets to SELECT it — the getter just returns false. Only an integration test that inserts a provisional person and reads it back through findAllWithDocumentCount will catch a forgotten column.

Recommendations

  • Write a MigrationIntegrationTest (or extend the existing one) with these named tests:
    • migration_backfills_dated_rows_to_DAY_precision
    • migration_backfills_undated_rows_to_UNKNOWN_precision
    • meta_date_precision_is_not_null_after_migration
    • non_range_row_rejects_non_null_meta_date_end (expect a DataIntegrityViolationException)
    • range_row_with_end_before_start_is_rejected
    • persons_source_ref_unique_index_rejects_duplicate
    • tag_source_ref_unique_index_rejects_duplicate
    • persons_provisional_defaults_to_false
  • Add a projection regression test: insert a provisional Person, call the repository's findAllWithDocumentCount, assert the returned PersonSummaryDTO.isProvisional() is true. This is the test that guards against the silent-null trap.
  • Seed both dated and undated documents in the backfill test fixture — the scenario only works if both branches exist before the migration runs. Use a @Sql fixture or insert pre-migration rows in a Flyway-aware test.
  • Confirm npm run generate:api output is verified, not just run. A Vitest/type-check assertion isn't practical for generated types, but the PR diff must show DatePrecision in the generated file — make that a manual checklist item in the PR description.

Open Decisions

None. The acceptance criteria map cleanly to integration tests; I've named them above.

## Sara Holt — Senior QA Engineer This is a migration-and-mapping issue, so the test strategy lives almost entirely at the integration layer against real Postgres. The acceptance criteria are written as Gherkin already — good — but a few are not yet covered by an obvious test and one will be invisible to H2. ### Observations - **Every acceptance scenario here requires Testcontainers `postgres:16-alpine`, never H2.** The backfill, the NOT NULL transition, and the CHECK constraints are all PostgreSQL-specific behaviors. An H2 run would pass while the real migration fails. The backend already gates at 88% branch coverage (backend CLAUDE.md) and uses Testcontainers — this issue must land its migration test in that suite. - **The "precision backfill is correct" scenario has two distinct assertions** (dated → `DAY`, undated → `UNKNOWN`) plus the NOT NULL transition plus the negative CHECK (non-RANGE row cannot have a non-null `meta_date_end`). That's four behaviors → four tests, one logical assertion each. - **The `PersonSummaryDTO` projection change is the highest regression risk and the easiest to under-test.** Adding `provisional` to the interface compiles even if the native query forgets to SELECT it — the getter just returns false. Only an integration test that inserts a provisional person and reads it back through `findAllWithDocumentCount` will catch a forgotten column. ### Recommendations - **Write a `MigrationIntegrationTest` (or extend the existing one) with these named tests:** - `migration_backfills_dated_rows_to_DAY_precision` - `migration_backfills_undated_rows_to_UNKNOWN_precision` - `meta_date_precision_is_not_null_after_migration` - `non_range_row_rejects_non_null_meta_date_end` (expect a `DataIntegrityViolationException`) - `range_row_with_end_before_start_is_rejected` - `persons_source_ref_unique_index_rejects_duplicate` - `tag_source_ref_unique_index_rejects_duplicate` - `persons_provisional_defaults_to_false` - **Add a projection regression test:** insert a provisional `Person`, call the repository's `findAllWithDocumentCount`, assert the returned `PersonSummaryDTO.isProvisional()` is true. This is the test that guards against the silent-null trap. - **Seed both dated and undated documents in the backfill test fixture** — the scenario only works if both branches exist before the migration runs. Use a `@Sql` fixture or insert pre-migration rows in a Flyway-aware test. - **Confirm `npm run generate:api` output is verified, not just run.** A Vitest/type-check assertion isn't practical for generated types, but the PR diff must show `DatePrecision` in the generated file — make that a manual checklist item in the PR description. ### Open Decisions _None. The acceptance criteria map cleanly to integration tests; I've named them above._
Author
Owner

Tobias Wendt — DevOps & Platform Engineer

The headline of this issue is a DevOps concern: three competing V69 migrations would be a boot failure on the next deploy. Consolidating to one owned version is exactly the fix. I confirmed the current head is V68__add_grafana_reader_role.sql, so V69 is free.

Observations

  • This migration runs against a populated production database, not a fresh one. The backfill + NOT NULL sequence is the risky part: on a fresh DB it's trivial, but on the real archive with thousands of documents, the UPDATE ... SET meta_date_precision = 'DAY' followed by ALTER TABLE ... SET NOT NULL will take a table lock. For the current archive size this is seconds, not a concern — but the migration must do the backfill before the NOT NULL in the same migration, exactly as the issue specifies. Order matters; a single-file migration is correct.
  • Flyway runs in CI on every integration test from a clean DB (Testcontainers). If V69 is malformed, CI catches it before deploy — the safety net already exists. The acceptance criterion "applies cleanly as a single Flyway version" is verified by that existing pipeline.
  • No new Docker service, no new env var, no infra change. This is purely a schema/code change. docs/architecture/c4/l2-containers.puml and DEPLOYMENT.md are untouched — correct.

Recommendations

  • Adding a unique index on persons.source_ref and tag.source_ref on a populated table: since all existing rows have source_ref = NULL (the column is new), the unique index builds instantly and NULLs don't collide in Postgres. No CREATE INDEX CONCURRENTLY needed at current scale — a plain index inside the migration is fine and keeps it atomic. If the archive ever grows to where this locks too long, revisit, but don't pre-optimize now.
  • Verify the migration is idempotent-safe under Flyway's checksum model: once V69 ships and a deploy runs it, the file is immutable. Do not edit it afterward — any fix goes in V70. State this in the PR so it's not "tweaked" post-merge.
  • No backup/restore change needed, but note in the ADR that this migration is forward-only (no down-migration; Flyway community has none). A rollback means restoring from the nightly pg_dump, which is the standard procedure — nothing new to build.

Open Decisions

None. No infrastructure or config surface is touched; the migration ordering the issue specifies is the correct and safe approach at current data volume.

## Tobias Wendt — DevOps & Platform Engineer The headline of this issue *is* a DevOps concern: three competing `V69` migrations would be a boot failure on the next deploy. Consolidating to one owned version is exactly the fix. I confirmed the current head is `V68__add_grafana_reader_role.sql`, so `V69` is free. ### Observations - **This migration runs against a populated production database, not a fresh one.** The backfill + NOT NULL sequence is the risky part: on a fresh DB it's trivial, but on the real archive with thousands of documents, the `UPDATE ... SET meta_date_precision = 'DAY'` followed by `ALTER TABLE ... SET NOT NULL` will take a table lock. For the current archive size this is seconds, not a concern — but the migration must do the backfill *before* the NOT NULL in the same migration, exactly as the issue specifies. Order matters; a single-file migration is correct. - **Flyway runs in CI on every integration test from a clean DB** (Testcontainers). If V69 is malformed, CI catches it before deploy — the safety net already exists. The acceptance criterion "applies cleanly as a single Flyway version" is verified by that existing pipeline. - **No new Docker service, no new env var, no infra change.** This is purely a schema/code change. `docs/architecture/c4/l2-containers.puml` and `DEPLOYMENT.md` are untouched — correct. ### Recommendations - **Adding a unique index on `persons.source_ref` and `tag.source_ref` on a populated table:** since all existing rows have `source_ref = NULL` (the column is new), the unique index builds instantly and NULLs don't collide in Postgres. No `CREATE INDEX CONCURRENTLY` needed at current scale — a plain index inside the migration is fine and keeps it atomic. If the archive ever grows to where this locks too long, revisit, but don't pre-optimize now. - **Verify the migration is idempotent-safe under Flyway's checksum model:** once V69 ships and a deploy runs it, the file is immutable. Do not edit it afterward — any fix goes in V70. State this in the PR so it's not "tweaked" post-merge. - **No backup/restore change needed**, but note in the ADR that this migration is forward-only (no down-migration; Flyway community has none). A rollback means restoring from the nightly `pg_dump`, which is the standard procedure — nothing new to build. ### Open Decisions _None. No infrastructure or config surface is touched; the migration ordering the issue specifies is the correct and safe approach at current data volume._
Author
Owner

Elicit — Requirements Engineer & Business Analyst

I'm in Brownfield mode. This issue is unusually well-formed for a schema phase: it has Gherkin acceptance criteria, an explicit out-of-scope list, and a stated dependency on #670 before Phase 3. That's a Definition-of-Ready pass on most criteria. My job is to hunt ambiguity and missing edges before code starts.

Observations

  • The title is a single 40-word user story sentence. It's traceable (archive owner → collision-free schema → downstream phases compile) but unreadable as a title. The body carries the real spec, so this is cosmetic, not blocking.
  • Acceptance criteria cover the happy path well but are silent on two edges I'd want a "done" condition for: (1) what the migration does with documents that have a meta_date and the column already somehow populated (shouldn't happen — new column — but worth a one-line assumption), and (2) whether tag.source_ref backfill is needed at all, or whether all existing tags simply get NULL (the issue implies NULL; make it explicit).
  • "Run npm run generate:api" is an acceptance criterion that depends on a running dev backend. That's an environmental precondition, not a behavior the migration controls. It's correctly listed, but flag that this criterion is verified by a human checking the committed diff, not by an automated gate.

Recommendations

  • Add one explicit assumption line: "Existing tag and persons rows receive source_ref = NULL; only the importer (Phase 3) populates it." This closes the ambiguity about whether a tag backfill is in scope here (it isn't).
  • Tighten the RANGE acceptance criterion to match the architect's open question. Right now "a row with precision != 'RANGE' cannot have a non-null meta_date_end" specifies the negative case but not whether a RANGE row must have an end. State the intended rule as a Gherkin line either way — an untestable gap becomes a testable assertion.
  • The out-of-scope list is excellent and prevents scope creep — reading/loading canonical files, rendering, and UI are all correctly deferred. Keep that discipline; if any "while we're in here, let's also..." appears in the PR, it belongs in Phase 3-6, not here.
  • Glossary terms are well-chosen. "date precision", "source_ref", "provisional person", "raw attribution" are exactly the four new domain concepts. Confirm each gets a one-sentence definition, not just a heading — provisional person in particular needs a definition that distinguishes it from the existing familyMember boolean so future readers don't conflate them.

Open Decisions

  • RANGE end-date obligation (also raised by Markus) — is a RANGE precision row required to have meta_date_end, or may it be open-ended? This determines whether the CHECK is biconditional. Cost: business call about whether the normalizer ever emits an open-ended range; if it does, the strict rule rejects valid import data.
  • Is provisional ever set to true by anything in this phase? The issue adds the column with DEFAULT false but all import logic is out of scope. Confirm that's intentional (column exists, stays false until Phase 3 writes it) so a reviewer doesn't expect a code path that sets it. Cost: none if intentional — just needs a one-line confirmation to avoid a "feature looks half-built" flag later.
## Elicit — Requirements Engineer & Business Analyst I'm in Brownfield mode. This issue is unusually well-formed for a schema phase: it has Gherkin acceptance criteria, an explicit out-of-scope list, and a stated dependency on #670 before Phase 3. That's a Definition-of-Ready pass on most criteria. My job is to hunt ambiguity and missing edges before code starts. ### Observations - **The title is a single 40-word user story sentence.** It's traceable (archive owner → collision-free schema → downstream phases compile) but unreadable as a title. The *body* carries the real spec, so this is cosmetic, not blocking. - **Acceptance criteria cover the happy path well** but are silent on two edges I'd want a "done" condition for: (1) what the migration does with documents that have a `meta_date` *and* the column already somehow populated (shouldn't happen — new column — but worth a one-line assumption), and (2) whether `tag.source_ref` backfill is needed at all, or whether all existing tags simply get NULL (the issue implies NULL; make it explicit). - **"Run `npm run generate:api`" is an acceptance criterion that depends on a running dev backend.** That's an environmental precondition, not a behavior the migration controls. It's correctly listed, but flag that this criterion is verified by a human checking the committed diff, not by an automated gate. ### Recommendations - **Add one explicit assumption line:** "Existing `tag` and `persons` rows receive `source_ref = NULL`; only the importer (Phase 3) populates it." This closes the ambiguity about whether a tag backfill is in scope here (it isn't). - **Tighten the RANGE acceptance criterion to match the architect's open question.** Right now "a row with precision != 'RANGE' cannot have a non-null meta_date_end" specifies the negative case but not whether a `RANGE` row *must* have an end. State the intended rule as a Gherkin line either way — an untestable gap becomes a testable assertion. - **The out-of-scope list is excellent and prevents scope creep** — reading/loading canonical files, rendering, and UI are all correctly deferred. Keep that discipline; if any "while we're in here, let's also..." appears in the PR, it belongs in Phase 3-6, not here. - **Glossary terms are well-chosen.** "date precision", "source_ref", "provisional person", "raw attribution" are exactly the four new domain concepts. Confirm each gets a one-sentence definition, not just a heading — `provisional person` in particular needs a definition that distinguishes it from the existing `familyMember` boolean so future readers don't conflate them. ### Open Decisions - **RANGE end-date obligation** (also raised by Markus) — is a `RANGE` precision row *required* to have `meta_date_end`, or may it be open-ended? This determines whether the CHECK is biconditional. _Cost: business call about whether the normalizer ever emits an open-ended range; if it does, the strict rule rejects valid import data._ - **Is `provisional` ever set to `true` by anything in this phase?** The issue adds the column with `DEFAULT false` but all import logic is out of scope. Confirm that's intentional (column exists, stays false until Phase 3 writes it) so a reviewer doesn't expect a code path that sets it. _Cost: none if intentional — just needs a one-line confirmation to avoid a "feature looks half-built" flag later._
Author
Owner

Leonie Voss — UX & Accessibility Lead

This is a schema + domain-model phase with explicitly no UI ("no rendering, no UI here"). So I have no surface to audit today. I checked one thing that will matter downstream, to make sure the data model doesn't quietly constrain the UX in Phases 4-6.

Observations

  • The DatePrecision enum carries the semantic distinction the UI needs. Phase 4 will render APPROX as "ca." and presumably SEASON/MONTH/YEAR with reduced specificity ("Frühjahr 1943", "1943"). The enum being a verbatim mirror of the normalizer means the rendering layer has the full fidelity it needs — nothing is collapsed to a lossy Date. That's the right call for the dual audience: a 60+ reader benefits enormously from "ca. 1943" over a fake-precise "01.01.1943".
  • meta_date_raw preserving the verbatim original cell is a UX asset, not just provenance. It enables a future "as written in the original" tooltip/disclosure for transcribers — exactly the kind of recognition-over-recall cue the senior audience values. Worth keeping in mind for Phase 4/5 even though it's out of scope now.
  • provisional as a first-class boolean means Phase 5 can give provisional persons a distinct, honest visual treatment (a badge with text + icon, never color alone) rather than hiding uncertainty. Good foundation.

Recommendations

  • No action this phase. When Phase 4 specs the date formatter, I'll want a design note that every precision value has a defined human-readable rendering in de/en/es, and that UNKNOWN renders as a real label (e.g. "Datum unbekannt"), never an empty cell — an empty date field reads as "missing/broken" to users rather than "honestly unknown", which is the whole point of this milestone.

Open Decisions

None — no UI in scope. My input is forward-looking for Phase 4, not a decision needed now.

## Leonie Voss — UX & Accessibility Lead This is a schema + domain-model phase with explicitly no UI ("no rendering, no UI here"). So I have no surface to audit today. I checked one thing that *will* matter downstream, to make sure the data model doesn't quietly constrain the UX in Phases 4-6. ### Observations - **The `DatePrecision` enum carries the semantic distinction the UI needs.** Phase 4 will render `APPROX` as "ca." and presumably `SEASON`/`MONTH`/`YEAR` with reduced specificity ("Frühjahr 1943", "1943"). The enum being a verbatim mirror of the normalizer means the rendering layer has the full fidelity it needs — nothing is collapsed to a lossy `Date`. That's the right call for the dual audience: a 60+ reader benefits enormously from "ca. 1943" over a fake-precise "01.01.1943". - **`meta_date_raw` preserving the verbatim original cell is a UX asset, not just provenance.** It enables a future "as written in the original" tooltip/disclosure for transcribers — exactly the kind of recognition-over-recall cue the senior audience values. Worth keeping in mind for Phase 4/5 even though it's out of scope now. - **`provisional` as a first-class boolean** means Phase 5 can give provisional persons a distinct, honest visual treatment (a badge with text + icon, never color alone) rather than hiding uncertainty. Good foundation. ### Recommendations - **No action this phase.** When Phase 4 specs the date formatter, I'll want a design note that every precision value has a defined human-readable rendering in de/en/es, and that `UNKNOWN` renders as a real label (e.g. "Datum unbekannt"), never an empty cell — an empty date field reads as "missing/broken" to users rather than "honestly unknown", which is the whole point of this milestone. ### Open Decisions _None — no UI in scope. My input is forward-looking for Phase 4, not a decision needed now._
Author
Owner

Decision Queue — Action Required

2 decisions need your input before implementation starts. Everything else is a concrete recommendation the team will apply directly.

Data model / constraints

  • RANGE end-date obligation — is the precision CHECK biconditional? The issue specifies meta_date_end may be non-null only when precision = RANGE, but does not say whether a RANGE row must have an end. The normalizer (dates.py) emits RANGE with meta_date as the range start — sometimes possibly with no end. Decide:
    • Strict (RANGE ⟺ end IS NOT NULL): rejects any open-ended range; cleaner downstream rendering. Cost: rejects valid normalizer output if it ever emits a start-only range.
    • Loose (end non-null → precision = RANGE, but RANGE may have null end): permits open-ended ranges. Cost: Phase 4 must handle a RANGE with no end gracefully.
      (Raised by: Markus, Elicit)

Scope confirmation

  • Does provisional stay false throughout this phase? The column is added with DEFAULT false but all import logic that would set it true is out of scope (Phase 3). Confirm this is intentional — column exists, no code path sets it yet — so reviewers don't flag it as a half-built feature. A one-line "yes, Phase 3 populates it" in the issue closes this. (Raised by: Elicit)
## Decision Queue — Action Required _2 decisions need your input before implementation starts. Everything else is a concrete recommendation the team will apply directly._ ### Data model / constraints - **RANGE end-date obligation — is the precision CHECK biconditional?** The issue specifies `meta_date_end` may be non-null *only* when precision = `RANGE`, but does not say whether a `RANGE` row *must* have an end. The normalizer (`dates.py`) emits `RANGE` with `meta_date` as the range start — sometimes possibly with no end. Decide: - **Strict (`RANGE ⟺ end IS NOT NULL`):** rejects any open-ended range; cleaner downstream rendering. _Cost: rejects valid normalizer output if it ever emits a start-only range._ - **Loose (`end non-null → precision = RANGE`, but RANGE may have null end):** permits open-ended ranges. _Cost: Phase 4 must handle a `RANGE` with no end gracefully._ _(Raised by: Markus, Elicit)_ ### Scope confirmation - **Does `provisional` stay `false` throughout this phase?** The column is added with `DEFAULT false` but all import logic that would set it `true` is out of scope (Phase 3). Confirm this is intentional — column exists, no code path sets it yet — so reviewers don't flag it as a half-built feature. A one-line "yes, Phase 3 populates it" in the issue closes this. _(Raised by: Elicit)_
Author
Owner

Implemented on feature/671-schema-foundation (branched from docs/import-migration)

Schema foundation complete via red→green TDD. Five atomic commits:

  1. 662927f9 feat(schema): V69__import_precision_attribution_identity_schema.sql — one migration adding all columns + DB CHECK constraints; DatePrecision enum (verbatim mirror of normalizer); entity fields on Document/Person/Tag. 14 new Testcontainers tests.
  2. 0f07a95b feat(person): provisional added to PersonSummaryDTO interface and all three native SELECTs (findAllWithDocumentCount, searchWithDocumentCount + its GROUP BY, findTopByDocumentCount) — guarded by 3 Postgres integration tests against the silent-false trap.
  3. c27c83f5 feat(document): precision/attribution fields on DocumentListItem (carried through toListItem), DocumentUpdateDTO, DocumentBatchMetadataDTO.
  4. 6f5ca475 feat(frontend): hand-edited generated/api.ts (see note below).
  5. d959cb54 docs: db-orm.puml + db-relationships.puml, GLOSSARY (4 terms), ADR-025.

Tests run (Testcontainers Postgres, per-class — never full suite)

  • MigrationIntegrationTest — 44/44 (14 new V69)
  • PersonRepositoryTest — 30/30 (3 new projection)
  • DocumentListItemIntegrationTest 4/4, DocumentSearchResultTest 8/8, DocumentControllerTest 97/97, FlywayConfigTest 3/3
  • ./mvnw clean package -DskipTests

Decisions applied (the two open items)

  • RANGE end is one-directional, not biconditional — a RANGE row may have a null meta_date_end (open-ended range), since the normalizer emits start-only ranges. Recorded in ADR-025.
  • provisional stays false this phase — column exists, only Phase 3 sets it true. Recorded in ADR-025.

npm run generate:api — hand-edited, must be re-validated in CI

The worktree has no node_modules and no running dev backend, so I hand-edited frontend/src/lib/generated/api.ts to mirror exactly what generate:api produces (DatePrecision union, new Document/DocumentListItem/DTO fields, Person provisional+sourceRef, Tag sourceRef, PersonSummaryDTO provisional). Action for reviewer/CI: re-run npm run generate:api against the dev backend to confirm byte parity, and fix any frontend test mock factories that now need the new required fields (provisional, metaDatePrecision) — npm run check could not run here.

Committed locally only (no push / no PR per instruction) — owner will push + open the PR.

## Implemented on `feature/671-schema-foundation` (branched from `docs/import-migration`) Schema foundation complete via red→green TDD. Five atomic commits: 1. `662927f9` **feat(schema):** `V69__import_precision_attribution_identity_schema.sql` — one migration adding all columns + DB CHECK constraints; `DatePrecision` enum (verbatim mirror of normalizer); entity fields on Document/Person/Tag. 14 new Testcontainers tests. 2. `0f07a95b` **feat(person):** `provisional` added to `PersonSummaryDTO` interface **and all three native SELECTs** (`findAllWithDocumentCount`, `searchWithDocumentCount` + its GROUP BY, `findTopByDocumentCount`) — guarded by 3 Postgres integration tests against the silent-false trap. 3. `c27c83f5` **feat(document):** precision/attribution fields on `DocumentListItem` (carried through `toListItem`), `DocumentUpdateDTO`, `DocumentBatchMetadataDTO`. 4. `6f5ca475` **feat(frontend):** hand-edited `generated/api.ts` (see note below). 5. `d959cb54` **docs:** db-orm.puml + db-relationships.puml, GLOSSARY (4 terms), **ADR-025**. ### Tests run (Testcontainers Postgres, per-class — never full suite) - `MigrationIntegrationTest` — 44/44 ✅ (14 new V69) - `PersonRepositoryTest` — 30/30 ✅ (3 new projection) - `DocumentListItemIntegrationTest` 4/4, `DocumentSearchResultTest` 8/8, `DocumentControllerTest` 97/97, `FlywayConfigTest` 3/3 ✅ - `./mvnw clean package -DskipTests` ✅ ### Decisions applied (the two open items) - **RANGE end is one-directional, not biconditional** — a `RANGE` row may have a null `meta_date_end` (open-ended range), since the normalizer emits start-only ranges. Recorded in ADR-025. - **`provisional` stays `false` this phase** — column exists, only Phase 3 sets it true. Recorded in ADR-025. ### `npm run generate:api` — hand-edited, must be re-validated in CI The worktree has no `node_modules` and no running dev backend, so I hand-edited `frontend/src/lib/generated/api.ts` to mirror exactly what `generate:api` produces (DatePrecision union, new Document/DocumentListItem/DTO fields, Person `provisional`+`sourceRef`, Tag `sourceRef`, PersonSummaryDTO `provisional`). **Action for reviewer/CI:** re-run `npm run generate:api` against the dev backend to confirm byte parity, and fix any frontend test mock factories that now need the new required fields (`provisional`, `metaDatePrecision`) — `npm run check` could not run here. Committed locally only (no push / no PR per instruction) — owner will push + open the PR.
Sign in to join this conversation.
No Label P0-critical feature
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#671