As a reader I want imprecise and unknown dates rendered honestly (e.g. "Juni 1916", "ca. 1916", "Datum unbekannt") so a document never shows a precision the data doesn't have #666

Closed
opened 2026-05-26 20:33:34 +02:00 by marcel · 8 comments
Owner

⚠️ Re-scoped to Phase 4 — Honest date rendering (presentation only). This issue no longer owns the schema or the data. The DatePrecision enum, the meta_date_precision / meta_date_end / meta_date_raw columns, and their migration are owned by Phase 2 #671. The data in those columns is populated by Phase 3 #669 (the importer), which reads Phase 1 #670's canonical exports. This issue adds no migration and no enum — it consumes them. It delivers the frontend formatter, the buildTitle single-source-of-truth fix, the accessible raw-cell display, and the edit-form precision controls. Depends on #671 (the enum + columns must exist to compile/serialize) and effectively on #669 for real precision data to render against.


Context

The schema work (Phase 2 #671) gives Document four date fields instead of one:

  • meta_date (documentDate, LocalDate) — the sort/filter anchor: a normalized representative day. It is not a claim of exactness.
  • meta_date_precision (DatePrecision enum: DAY | MONTH | SEASON | YEAR | RANGE | APPROX | UNKNOWN, mirroring the normalizer's seven values verbatim) — descriptive metadata.
  • meta_date_end (LocalDate, nullable) — the end day for RANGE precision only.
  • meta_date_raw (text, nullable) — the original spreadsheet cell preserved verbatim (e.g. Sommer 1916, ?).

The data is honest at rest. But the presentation layer still lies. Two concrete bugs remain after #671/#669 land:

  1. The Java title bakes in a fabricated exact day. MassImportService.buildTitle (MassImportService.java:476-485) does sb.append(date.format(GERMAN_DATE)) — it formats the anchor LocalDate as a full dd.MM.yyyy. A Juni 1916 letter (anchor 1916-06-01, precision MONTH) gets a persisted title reading "…1. Juni 1916" — exactly the fabricated-day lie the whole milestone exists to kill. The title must never show a precision the data does not have.
  2. No frontend formatter consumes precision. Every view that renders documentDate today calls formatDate/formatMCDate on the anchor and prints an exact day, ignoring meta_date_precision/meta_date_end/meta_date_raw entirely.

This issue makes the displayed date as honest as the stored one — a single precision-aware label, a single source of truth shared by titles and UI, an accessible visible raw cell, and edit controls to set precision/end/raw.

For evidence of why imprecision is the norm in this archive (7942 rows, 207 unparsed/2.8%, ?×99, long tail of 30.April/Sommer 1916/um 1920), see #670's normalizer review output — this issue inherits that reality through the populated columns.


Scope

In scope:

  • A precision-aware frontend formatter producing the honest label for all seven precisions.
  • Wiring that formatter into the document detail, list/search, and edit views.
  • The buildTitle single-source-of-truth fix so persisted titles stop fabricating an exact day.
  • Accessible, escaped raw-cell display (visible secondary line, not tooltip-only).
  • Edit form: precision selector + conditionally-revealed end-date + read-only raw cell.
  • de/en/es i18n keys for the localized label parts.

Out of scope (owned elsewhere — see Dependencies): the DatePrecision enum, the three columns, the migration/backfill, DB CHECK constraints, the importer reading the canonical export, and adding date_end to the normalizer export.


File-level breakdown

Frontend — the formatter (the core deliverable)

  • NEW frontend/src/lib/shared/utils/documentDate.ts — a single pure function formatDocumentDate(iso, precision, end?, raw?) returning the honest label. Co-located next to the existing date.ts helpers — delegate to formatDate/formatMCDate and reuse the T12:00:00 UTC-safety convention; do not reimplement Intl.DateTimeFormat. Rules:
    • DAYformatDate(iso, 'long') → "24. Dezember 1943".
    • MONTH → "Juni 1916" (month + year only).
    • SEASON → "Sommer 1916" (season word + year; see Open Decisions on localize-vs-verbatim and the raw fallback).
    • YEAR → "1916".
    • APPROX → "ca. 1916" (German label for the normalizer's APPROX; prefix from i18n).
    • RANGE → "10.–11. Jan. 1917"; collapse shared month/year, expand cross-month ("30. Jan. – 2. Feb. 1917"). A null end (open-ended range) renders the start with an open-range indicator and never fabricates an end — see the AC.
    • UNKNOWN → "Datum unbekannt".
  • NEW frontend/src/lib/shared/utils/documentDate.spec.ts — one test per precision branch plus edge cases (RANGE shared-month collapse, RANGE cross-month, RANGE open-ended/null-end, UNKNOWN with null raw, SEASON with null raw fallback to month-derived, an angle-bracket raw rendering inert — see Security).

Frontend — wiring & a11y

  • Replace documentDate rendering with formatDocumentDate(...) in document detail, list/search rows, and edit views (every current formatDate/formatMCDate call on documentDate).
  • Raw cell — visible secondary line, never tooltip-only. For UNKNOWN/SEASON, render meta_date_raw as small muted static text under the label (e.g. "Originaltext: Sommer 1916"), not behind a title= attribute. Tooltip-only fails WCAG 1.4.13 and is invisible to touch/keyboard users — and the reader audience skews mobile.
  • Render raw via Svelte default {...} interpolation — never {@html}. meta_date_raw is untrusted verbatim spreadsheet text; default escaping neutralizes XSS (CWE-79). A formatter test asserts an angle-bracket raw value comes back as literal inert text.
  • Non-color cue for imprecision. Pair imprecise/unknown dates with a redundant non-color marker so a reader can't mistake "1916" (deliberate YEAR) for truncation: a calendar-with-question icon for UNKNOWN, the textual "ca." prefix for APPROX. a11y: date-precision cues (UNKNOWN / APPROX / RANGE) must be conveyed by text or icon, not color alone (WCAG 1.4.1).

Frontend — edit form

  • Add a precision <select> with an associated <label> (not placeholder), ≥44px (prefer 48px) touch targets for the senior author audience.
  • Conditionally reveal the end-date field only when RANGE is selected (progressive disclosure; announce the reveal politely). Keep the existing German dd.mm.yyyy input via handleGermanDateInput for both the anchor and the end.
  • Read-only raw cell as labelled static text ("Originaltext: …"), not a disabled input (disabled inputs are low-contrast and skipped by some screen readers).

Backend — buildTitle single source of truth

  • backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java:476-485buildTitle must stop appending the raw dd.MM.yyyy day. Make it precision-aware via an extracted formatTitleDate(date, precision, end, raw) helper that produces the same honest label the frontend formatter does (keep buildTitle under 20 lines). See Open Decisions for the chosen single-source-of-truth approach.

i18n

  • Add precision label keys to frontend/messages/{de,en,es}.json, routed through Paraglide getMessage (no hardcoded German in the formatter): date_precision_unknown ("Datum unbekannt" / "Date unknown" / "Fecha desconocida"), date_precision_approx_prefix ("ca." / "c." / "ca."), season-word keys if localized, and edit-form labels for the precision selector and end-date field.

OpenAPI / types

  • The enum + DTO fields are added by #671. After #671 merges, run npm run generate:api in frontend/ so DatePrecision, documentDatePrecision, documentDateEnd, documentDateRaw are present in frontend/src/lib/generated/ for this issue's frontend code to consume. If they are missing, this issue is blocked on #671 — do not redeclare them here.

Acceptance criteria (Gherkin)

Feature: Honest presentation of document dates (Phase 4)

  Scenario: A DAY-precision letter renders as a full date
    Given a document with precision DAY and anchor 1943-12-24
    Then formatDocumentDate renders "24. Dezember 1943"
    And the persisted import title contains "24. Dezember 1943", not a fabricated different day

  Scenario: A month-precision letter is never shown as an exact day
    Given a document with precision MONTH, anchor 1916-06-01, raw "Juni 1916"
    Then formatDocumentDate renders "Juni 1916" — not "1. Juni 1916"
    And buildTitle produces a title containing "Juni 1916", not "1. Juni 1916"

  Scenario: A season letter renders the season word
    Given a document with precision SEASON, anchor 1916-06-01, raw "Sommer 1916"
    Then formatDocumentDate renders "Sommer 1916"

  Scenario: A season letter with no raw falls back to a month-derived label
    Given a document with precision SEASON and a null raw
    Then formatDocumentDate renders a season label derived from the anchor month, with no crash

  Scenario: A year-precision letter renders the year only
    Given a document with precision YEAR and anchor 1916-01-01
    Then formatDocumentDate renders "1916"

  Scenario: An approximate letter renders with a ca. prefix
    Given a document with precision APPROX and anchor 1920-01-01
    Then formatDocumentDate renders "ca. 1920"

  Scenario: A same-month range collapses
    Given a document with precision RANGE, anchor 1917-01-10, end 1917-01-11
    Then formatDocumentDate renders "10.–11. Jan. 1917"

  Scenario: A cross-month range expands both months
    Given a document with precision RANGE, anchor 1917-01-30, end 1917-02-02
    Then formatDocumentDate renders "30. Jan. – 2. Feb. 1917"

  Scenario: An open-ended RANGE renders without a fabricated end
    Given a document with precision RANGE, a start meta_date, and a null meta_date_end
    Then the formatter renders the start with an open-range indicator (e.g. "ab 10. Jan. 1917")
    And never fabricates an end date

  Scenario: An unknown date shows the raw cell as a visible secondary line
    Given a document with precision UNKNOWN, null anchor, raw "?"
    Then formatDocumentDate renders "Datum unbekannt"
    And the raw value "?" is shown as visible static text, not only a title attribute

  Scenario: A malicious raw cell renders inert
    Given a document with raw "<img src=x onerror=alert(1)>"
    Then the raw value is rendered as escaped literal text via Svelte default interpolation
    And no {@html} or innerHTML path receives it

  Scenario: The edit form reveals the end-date field only for RANGE
    Given the edit form for a document
    When the editor selects precision RANGE
    Then the end-date field becomes visible
    And selecting any other precision hides it

  Scenario: The localized label and verbatim raw coexist
    Given an en-locale reader viewing a SEASON document with raw "Sommer 1916"
    Then the structured label follows the chosen locale policy (see Open Decisions)
    And the verbatim raw cell "Sommer 1916" remains visible as the secondary line

Implementation plan

Frontend (TDD the formatter first — pure, branch-heavy, highest-value)

  1. Write documentDate.spec.ts with one failing test per precision branch + edge cases before any implementation.
  2. Implement formatDocumentDate(...) delegating to date.ts helpers; go green branch by branch. Do not add behavior beyond the seven branches mid-refactor — new edge case = new failing test first.
  3. Replace documentDate rendering in detail / list/search / edit with the formatter.
  4. Add the visible escaped raw-cell secondary line + non-color imprecision cue.
  5. Edit form: labelled precision selector, conditional end-date reveal, read-only raw static text, 44–48px targets.

Backend
6. Red test in MassImportServiceTest: a MONTH-precision row produces a title containing "Juni 1916", not "1. Juni 1916". Green: extract formatTitleDate(...) so title + UI share one honest label.

Types & i18n
7. After #671 lands, npm run generate:api; add de/en/es precision keys via Paraglide.

Verification
8. Frontend formatter unit tests (fast Vitest layer, no browser). Backend buildTitle test. No new Playwright journey — this is a metadata-rendering change, cheaper to verify below the E2E line.
9. Backend coverage gate — the JaCoCo branch gate is currently 0.77 (77%), ratcheting toward 80% (see pom.xml and issue #496); the precision-aware buildTitle is branch-heavy, so plan coverage against that, not 88%, or CI blocks the merge.


Open Decisions

  1. RANGE end is now sourced. Earlier review (Sara + Elicit) found the canonical export had no date_end column, leaving meta_date_end unpopulatable. Resolved upstream: #670 now sources the RANGE end day and exports it; #669 persists it into meta_date_end. This issue therefore renders a real end for RANGE (the "10.–11. Jan. 1917" / cross-month AC is live), and must defensively handle a null end (fall back to rendering the start day only) for any legacy row that predates the populated column.

  2. Title single source of truth (Felix). buildTitle (Java, at import) and formatDocumentDate (TS, at render) must agree on the label. Resolved: extract one Java formatTitleDate(date, precision, end, raw) helper that mirrors the formatter's rules exactly, so the persisted title carries the same honest label the UI shows. The title keeps containing the date (no behavioral break to title-based search/sort), but never at a finer precision than the data. Drift between "ca." vs "circa" / "–" vs "-" is avoided by keeping both implementations to one agreed rule set, tested on both sides.

  3. meta_date_raw on the lean list/search payload (Markus). documentDateRaw on DocumentListItem is marked optional in #671's DTO. Open: include it on the list projection so directory/search rows render the raw cell for UNKNOWN/SEASON (one short text column, honest rendering applies to lists too), or omit it (leaner payload, but list UNKNOWN rows can only show "Datum unbekannt" and SEASON loses its word in lists). Lean toward include — this issue's honest-rendering goal covers list views, and decision 4 depends on the raw cell being available client-side.

  4. Localize the SEASON/RANGE structured label, or keep verbatim German (Leonie). A SEASON cell is German ("Sommer 1916"); an en/es reader could see "Summer 1916"/"Verano 1916" (localized, friendlier) or "Sommer 1916" (archival fidelity). Same for the RANGE month abbreviation. Lean: localize the structured part via Paraglide AND keep the verbatim raw cell visible as the secondary line, satisfying both reader accessibility and archival fidelity. This ties into decision 3 — the verbatim secondary line needs meta_date_raw available in the relevant view.


Out of scope

  • The DatePrecision enum, the three columns, the migration/backfill, and any DB CHECK constraints — owned by #671.
  • The importer reading date_iso/date_precision/date_raw/date_end from the canonical export and persisting them — owned by #669.
  • Adding the RANGE date_end to the normalizer export — owned by #670.
  • Server-side write-path validation of the new editable fields (enum binding → clean 400, end ≥ start guard, RANGE/end invariant) — these ride the existing @RequirePermission(WRITE_ALL) write path defined with the DTO fields in #671; this issue only renders and edits, it does not redefine the endpoint guards.
  • Filtering/faceting on meta_date_end, and any change to how the activity timeline or search facets consume precision — descriptive-only here; deferred to the consumer/timeline sibling issue.
  • A "documents by precision" metric/dashboard — a consumer-side nicety, not part of presentation.

Dependencies

  • #671 (Phase 2 — schema): the DatePrecision enum + meta_date_precision/meta_date_end/meta_date_raw columns + DTO fields. Hard compile/serialize dependency — must merge before this issue's frontend types regenerate.
  • #669 (Phase 3 — importer): populates the precision columns (incl. meta_date_end) from the canonical export. Effective dependency — without it there is no real precision data to render; this issue's frontend can be built against #671's types but is only meaningfully verifiable against #669's data.
  • #670 (Phase 1 — normalizer exports): source of date_iso/date_raw/date_precision/date_end. Upstream of #669; the RANGE end is now sourced here.

The legacy "Briefwechsel" feature is being removed and is intentionally not referenced anywhere in this spec.

> **⚠️ Re-scoped to Phase 4 — Honest date rendering (presentation only).** This issue no longer owns the schema or the data. The `DatePrecision` enum, the `meta_date_precision` / `meta_date_end` / `meta_date_raw` columns, and their migration are owned by **Phase 2 #671**. The data in those columns is populated by **Phase 3 #669** (the importer), which reads **Phase 1 #670**'s canonical exports. This issue **adds no migration and no enum** — it *consumes* them. It delivers the frontend formatter, the `buildTitle` single-source-of-truth fix, the accessible raw-cell display, and the edit-form precision controls. **Depends on #671** (the enum + columns must exist to compile/serialize) and effectively on **#669** for real precision data to render against. --- ## Context The schema work (Phase 2 #671) gives `Document` four date fields instead of one: - `meta_date` (`documentDate`, `LocalDate`) — the **sort/filter anchor**: a normalized representative day. It is **not** a claim of exactness. - `meta_date_precision` (`DatePrecision` enum: `DAY | MONTH | SEASON | YEAR | RANGE | APPROX | UNKNOWN`, mirroring the normalizer's seven values verbatim) — descriptive metadata. - `meta_date_end` (`LocalDate`, nullable) — the end day for `RANGE` precision only. - `meta_date_raw` (`text`, nullable) — the original spreadsheet cell preserved verbatim (e.g. `Sommer 1916`, `?`). The data is honest at rest. But the **presentation layer still lies**. Two concrete bugs remain after #671/#669 land: 1. **The Java title bakes in a fabricated exact day.** `MassImportService.buildTitle` (`MassImportService.java:476-485`) does `sb.append(date.format(GERMAN_DATE))` — it formats the anchor `LocalDate` as a full `dd.MM.yyyy`. A `Juni 1916` letter (anchor `1916-06-01`, precision `MONTH`) gets a persisted title reading "…1. Juni 1916" — exactly the fabricated-day lie the whole milestone exists to kill. The title must never show a precision the data does not have. 2. **No frontend formatter consumes precision.** Every view that renders `documentDate` today calls `formatDate`/`formatMCDate` on the anchor and prints an exact day, ignoring `meta_date_precision`/`meta_date_end`/`meta_date_raw` entirely. This issue makes the *displayed* date as honest as the *stored* one — a single precision-aware label, a single source of truth shared by titles and UI, an accessible visible raw cell, and edit controls to set precision/end/raw. For evidence of why imprecision is the norm in this archive (7942 rows, 207 unparsed/2.8%, `?`×99, long tail of `30.April`/`Sommer 1916`/`um 1920`), see #670's normalizer review output — this issue inherits that reality through the populated columns. --- ## Scope In scope: - A precision-aware **frontend formatter** producing the honest label for all seven precisions. - Wiring that formatter into the document **detail, list/search, and edit** views. - The **`buildTitle` single-source-of-truth fix** so persisted titles stop fabricating an exact day. - **Accessible, escaped raw-cell display** (visible secondary line, not tooltip-only). - **Edit form**: precision selector + conditionally-revealed end-date + read-only raw cell. - de/en/es i18n keys for the localized label parts. Out of scope (owned elsewhere — see Dependencies): the `DatePrecision` enum, the three columns, the migration/backfill, DB `CHECK` constraints, the importer reading the canonical export, and adding `date_end` to the normalizer export. --- ## File-level breakdown ### Frontend — the formatter (the core deliverable) - **NEW `frontend/src/lib/shared/utils/documentDate.ts`** — a single pure function `formatDocumentDate(iso, precision, end?, raw?)` returning the honest label. Co-located next to the existing `date.ts` helpers — **delegate to `formatDate`/`formatMCDate` and reuse the `T12:00:00` UTC-safety convention; do not reimplement `Intl.DateTimeFormat`.** Rules: - `DAY` → `formatDate(iso, 'long')` → "24. Dezember 1943". - `MONTH` → "Juni 1916" (month + year only). - `SEASON` → "Sommer 1916" (season word + year; see Open Decisions on localize-vs-verbatim and the `raw` fallback). - `YEAR` → "1916". - `APPROX` → "ca. 1916" (German label for the normalizer's `APPROX`; prefix from i18n). - `RANGE` → "10.–11. Jan. 1917"; collapse shared month/year, expand cross-month ("30. Jan. – 2. Feb. 1917"). A null end (open-ended range) renders the start with an open-range indicator and never fabricates an end — see the AC. - `UNKNOWN` → "Datum unbekannt". - **NEW `frontend/src/lib/shared/utils/documentDate.spec.ts`** — one test per precision branch plus edge cases (RANGE shared-month collapse, RANGE cross-month, RANGE open-ended/null-end, UNKNOWN with null `raw`, SEASON with null `raw` fallback to month-derived, an angle-bracket `raw` rendering inert — see Security). ### Frontend — wiring & a11y - Replace `documentDate` rendering with `formatDocumentDate(...)` in document **detail**, **list/search rows**, and **edit** views (every current `formatDate`/`formatMCDate` call on `documentDate`). - **Raw cell — visible secondary line, never tooltip-only.** For `UNKNOWN`/`SEASON`, render `meta_date_raw` as small muted static text under the label (e.g. "Originaltext: Sommer 1916"), **not** behind a `title=` attribute. Tooltip-only fails WCAG 1.4.13 and is invisible to touch/keyboard users — and the reader audience skews mobile. - **Render `raw` via Svelte default `{...}` interpolation — never `{@html}`.** `meta_date_raw` is untrusted verbatim spreadsheet text; default escaping neutralizes XSS (CWE-79). A formatter test asserts an angle-bracket raw value comes back as literal inert text. - **Non-color cue for imprecision.** Pair imprecise/unknown dates with a redundant non-color marker so a reader can't mistake "1916" (deliberate YEAR) for truncation: a calendar-with-question icon for `UNKNOWN`, the textual "ca." prefix for `APPROX`. **a11y:** date-precision cues (UNKNOWN / APPROX / RANGE) must be conveyed by text or icon, not color alone (WCAG 1.4.1). ### Frontend — edit form - Add a **precision `<select>`** with an associated `<label>` (not placeholder), ≥44px (prefer 48px) touch targets for the senior author audience. - **Conditionally reveal the end-date field only when `RANGE` is selected** (progressive disclosure; announce the reveal politely). Keep the existing German `dd.mm.yyyy` input via `handleGermanDateInput` for both the anchor and the end. - **Read-only raw cell as labelled static text** ("Originaltext: …"), not a disabled input (disabled inputs are low-contrast and skipped by some screen readers). ### Backend — `buildTitle` single source of truth - **`backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java:476-485`** — `buildTitle` must stop appending the raw `dd.MM.yyyy` day. Make it precision-aware via an extracted `formatTitleDate(date, precision, end, raw)` helper that produces the **same honest label** the frontend formatter does (keep `buildTitle` under 20 lines). See Open Decisions for the chosen single-source-of-truth approach. ### i18n - Add precision label keys to `frontend/messages/{de,en,es}.json`, routed through Paraglide `getMessage` (no hardcoded German in the formatter): `date_precision_unknown` ("Datum unbekannt" / "Date unknown" / "Fecha desconocida"), `date_precision_approx_prefix` ("ca." / "c." / "ca."), season-word keys if localized, and edit-form labels for the precision selector and end-date field. ### OpenAPI / types - The enum + DTO fields are added by #671. After #671 merges, run **`npm run generate:api`** in `frontend/` so `DatePrecision`, `documentDatePrecision`, `documentDateEnd`, `documentDateRaw` are present in `frontend/src/lib/generated/` for this issue's frontend code to consume. If they are missing, this issue is blocked on #671 — do not redeclare them here. --- ## Acceptance criteria (Gherkin) ```gherkin Feature: Honest presentation of document dates (Phase 4) Scenario: A DAY-precision letter renders as a full date Given a document with precision DAY and anchor 1943-12-24 Then formatDocumentDate renders "24. Dezember 1943" And the persisted import title contains "24. Dezember 1943", not a fabricated different day Scenario: A month-precision letter is never shown as an exact day Given a document with precision MONTH, anchor 1916-06-01, raw "Juni 1916" Then formatDocumentDate renders "Juni 1916" — not "1. Juni 1916" And buildTitle produces a title containing "Juni 1916", not "1. Juni 1916" Scenario: A season letter renders the season word Given a document with precision SEASON, anchor 1916-06-01, raw "Sommer 1916" Then formatDocumentDate renders "Sommer 1916" Scenario: A season letter with no raw falls back to a month-derived label Given a document with precision SEASON and a null raw Then formatDocumentDate renders a season label derived from the anchor month, with no crash Scenario: A year-precision letter renders the year only Given a document with precision YEAR and anchor 1916-01-01 Then formatDocumentDate renders "1916" Scenario: An approximate letter renders with a ca. prefix Given a document with precision APPROX and anchor 1920-01-01 Then formatDocumentDate renders "ca. 1920" Scenario: A same-month range collapses Given a document with precision RANGE, anchor 1917-01-10, end 1917-01-11 Then formatDocumentDate renders "10.–11. Jan. 1917" Scenario: A cross-month range expands both months Given a document with precision RANGE, anchor 1917-01-30, end 1917-02-02 Then formatDocumentDate renders "30. Jan. – 2. Feb. 1917" Scenario: An open-ended RANGE renders without a fabricated end Given a document with precision RANGE, a start meta_date, and a null meta_date_end Then the formatter renders the start with an open-range indicator (e.g. "ab 10. Jan. 1917") And never fabricates an end date Scenario: An unknown date shows the raw cell as a visible secondary line Given a document with precision UNKNOWN, null anchor, raw "?" Then formatDocumentDate renders "Datum unbekannt" And the raw value "?" is shown as visible static text, not only a title attribute Scenario: A malicious raw cell renders inert Given a document with raw "<img src=x onerror=alert(1)>" Then the raw value is rendered as escaped literal text via Svelte default interpolation And no {@html} or innerHTML path receives it Scenario: The edit form reveals the end-date field only for RANGE Given the edit form for a document When the editor selects precision RANGE Then the end-date field becomes visible And selecting any other precision hides it Scenario: The localized label and verbatim raw coexist Given an en-locale reader viewing a SEASON document with raw "Sommer 1916" Then the structured label follows the chosen locale policy (see Open Decisions) And the verbatim raw cell "Sommer 1916" remains visible as the secondary line ``` --- ## Implementation plan **Frontend (TDD the formatter first — pure, branch-heavy, highest-value)** 1. Write `documentDate.spec.ts` with one failing test per precision branch + edge cases before any implementation. 2. Implement `formatDocumentDate(...)` delegating to `date.ts` helpers; go green branch by branch. Do not add behavior beyond the seven branches mid-refactor — new edge case = new failing test first. 3. Replace `documentDate` rendering in detail / list/search / edit with the formatter. 4. Add the visible escaped raw-cell secondary line + non-color imprecision cue. 5. Edit form: labelled precision selector, conditional end-date reveal, read-only raw static text, 44–48px targets. **Backend** 6. Red test in `MassImportServiceTest`: a MONTH-precision row produces a title containing "Juni 1916", not "1. Juni 1916". Green: extract `formatTitleDate(...)` so title + UI share one honest label. **Types & i18n** 7. After #671 lands, `npm run generate:api`; add de/en/es precision keys via Paraglide. **Verification** 8. Frontend formatter unit tests (fast Vitest layer, no browser). Backend `buildTitle` test. No new Playwright journey — this is a metadata-rendering change, cheaper to verify below the E2E line. 9. **Backend coverage gate** — the JaCoCo **branch** gate is currently **0.77 (77%)**, ratcheting toward 80% (see `pom.xml` and issue #496); the precision-aware `buildTitle` is branch-heavy, so plan coverage against that, not 88%, or CI blocks the merge. --- ## Open Decisions 1. **RANGE end is now sourced.** Earlier review (Sara + Elicit) found the canonical export had no `date_end` column, leaving `meta_date_end` unpopulatable. **Resolved upstream: #670 now sources the RANGE end day and exports it; #669 persists it into `meta_date_end`.** This issue therefore renders a real end for RANGE (the "10.–11. Jan. 1917" / cross-month AC is live), and must defensively handle a null end (fall back to rendering the start day only) for any legacy row that predates the populated column. 2. **Title single source of truth (Felix).** `buildTitle` (Java, at import) and `formatDocumentDate` (TS, at render) must agree on the label. **Resolved: extract one Java `formatTitleDate(date, precision, end, raw)` helper that mirrors the formatter's rules exactly**, so the persisted title carries the same honest label the UI shows. The title keeps containing the date (no behavioral break to title-based search/sort), but never at a finer precision than the data. Drift between "ca." vs "circa" / "–" vs "-" is avoided by keeping both implementations to one agreed rule set, tested on both sides. 3. **`meta_date_raw` on the lean list/search payload (Markus).** `documentDateRaw` on `DocumentListItem` is marked optional in #671's DTO. **Open:** include it on the list projection so directory/search rows render the raw cell for `UNKNOWN`/`SEASON` (one short text column, honest rendering applies to lists too), **or** omit it (leaner payload, but list `UNKNOWN` rows can only show "Datum unbekannt" and `SEASON` loses its word in lists). Lean toward **include** — this issue's honest-rendering goal covers list views, and decision 4 depends on the raw cell being available client-side. 4. **Localize the SEASON/RANGE structured label, or keep verbatim German (Leonie).** A SEASON cell is German ("Sommer 1916"); an en/es reader could see "Summer 1916"/"Verano 1916" (localized, friendlier) or "Sommer 1916" (archival fidelity). Same for the RANGE month abbreviation. Lean: **localize the structured part via Paraglide AND keep the verbatim raw cell visible as the secondary line**, satisfying both reader accessibility and archival fidelity. This ties into decision 3 — the verbatim secondary line needs `meta_date_raw` available in the relevant view. --- ## Out of scope - The `DatePrecision` enum, the three columns, the migration/backfill, and any DB `CHECK` constraints — **owned by #671**. - The importer reading `date_iso`/`date_precision`/`date_raw`/`date_end` from the canonical export and persisting them — **owned by #669**. - Adding the RANGE `date_end` to the normalizer export — **owned by #670**. - Server-side write-path validation of the new editable fields (enum binding → clean 400, end ≥ start guard, RANGE/end invariant) — these ride the existing `@RequirePermission(WRITE_ALL)` write path defined with the DTO fields in #671; this issue only renders and edits, it does not redefine the endpoint guards. - Filtering/faceting on `meta_date_end`, and any change to how the activity timeline or search facets *consume* precision — descriptive-only here; deferred to the consumer/timeline sibling issue. - A "documents by precision" metric/dashboard — a consumer-side nicety, not part of presentation. --- ## Dependencies - **#671 (Phase 2 — schema):** the `DatePrecision` enum + `meta_date_precision`/`meta_date_end`/`meta_date_raw` columns + DTO fields. **Hard compile/serialize dependency** — must merge before this issue's frontend types regenerate. - **#669 (Phase 3 — importer):** populates the precision columns (incl. `meta_date_end`) from the canonical export. **Effective dependency** — without it there is no real precision data to render; this issue's frontend can be built against #671's types but is only meaningfully verifiable against #669's data. - **#670 (Phase 1 — normalizer exports):** source of `date_iso`/`date_raw`/`date_precision`/`date_end`. Upstream of #669; the RANGE end is now sourced here. > The legacy "Briefwechsel" feature is being removed and is intentionally not referenced anywhere in this spec.
marcel added the P0-criticalfeature labels 2026-05-26 20:33:39 +02:00
marcel added this to the Handling the Unknowns — honest uncertainty in dates & people milestone 2026-05-26 20:35:01 +02:00
Author
Owner

Markus Keller — Senior Application Architect

Observations

  • Scope is correct: this is a presentation-only consumer of #671's schema and #669's data. No migration, no enum, no DB concern lands here — that boundary is clean and I support it. I verified DatePrecision, documentDatePrecision, documentDateEnd, documentDateRaw are absent from frontend/src/lib/generated/api.ts and from the backend Document entity today, so the hard compile dependency on #671 is real, not theoretical.
  • The single-source-of-truth concern (Open Decision 2) is the architectural heart of this issue. You are deliberately accepting two parallel implementations of the same rules — Java formatTitleDate (at import) and TS formatDocumentDate (at render). Per my own module-boundary stance, duplication between modules is cheaper than coupling, and these two run in different runtimes at different lifecycle moments. I agree with duplicating rather than extracting a shared artifact. But duplicated rules drift silently.
  • buildTitle (MassImportService.java:477-485) currently appends GERMAN_DATE, which is DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN) — i.e. it already produces "1. Juni 1916", not "01.06.1916". The fabricated-day bug is real (it shows a day for MONTH precision), but the issue body's "01.06.1916" example is factually wrong for this codebase. Whoever implements should not "fix" a dd.MM.yyyy that isn't there.

Recommendations

  • Pin the shared rule set in one committed place so the two implementations can be diffed against a spec, not against each other. A small table in the ADR (or a fixture table of (precision, anchor, end, raw) → expected label) used verbatim by both the Java test and the TS spec is the cheapest drift guard. This is the "tested on both sides against one agreed rule set" the issue already gestures at — make the rule set a real artifact, not prose.
  • Keep formatTitleDate a pure private helper on MassImportService taking (LocalDate, DatePrecision, LocalDate end, String raw) and returning String. Do not introduce a new shared "DateLabel" service module — that would couple importing and a hypothetical formatting module for no benefit. buildTitle stays under 20 lines by delegating.
  • Decision 3 (raw on the list projection): include documentDateRaw on DocumentListItem. One nullable text column is negligible payload, and honest rendering is the whole point of the milestone — a list that can only ever say "Datum unbekannt" for UNKNOWN rows reintroduces information loss at the list layer. This also unblocks Decision 4's verbatim secondary line in lists.
  • This issue triggers no doc-table updates I own — no migration, no new package, no new route, no new ErrorCode/Permission. The new documentDate.ts util is not a domain module. I'd only expect a GLOSSARY touch if a new term is coined, but "date precision" is already owned by #671's GLOSSARY entry. Confirm #671 added it; if not, that's #671's blocker, not this one.

Open Decisions

  • Drift-guard mechanism for the two label implementations — a shared fixture table consumed by both test suites (my lean) vs. trusting two independently-written test sets to stay in sync. The former costs one extra fixture file; the latter costs a future silent divergence ("ca." vs "circa", en-dash vs hyphen) that no test catches because each side tests only itself. (This is a refinement of Felix's Decision 2, not a new axis.)
## Markus Keller — Senior Application Architect ### Observations - Scope is correct: this is a presentation-only consumer of #671's schema and #669's data. No migration, no enum, no DB concern lands here — that boundary is clean and I support it. I verified `DatePrecision`, `documentDatePrecision`, `documentDateEnd`, `documentDateRaw` are **absent** from `frontend/src/lib/generated/api.ts` and from the backend `Document` entity today, so the hard compile dependency on #671 is real, not theoretical. - The single-source-of-truth concern (Open Decision 2) is the architectural heart of this issue. You are deliberately accepting **two parallel implementations of the same rules** — Java `formatTitleDate` (at import) and TS `formatDocumentDate` (at render). Per my own module-boundary stance, duplication between modules is cheaper than coupling, and these two run in different runtimes at different lifecycle moments. I agree with duplicating *rather than* extracting a shared artifact. But duplicated rules drift silently. - `buildTitle` (`MassImportService.java:477-485`) currently appends `GERMAN_DATE`, which is `DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN)` — i.e. it already produces **"1. Juni 1916"**, not "01.06.1916". The fabricated-day bug is real (it shows a day for MONTH precision), but the issue body's "01.06.1916" example is factually wrong for this codebase. Whoever implements should not "fix" a `dd.MM.yyyy` that isn't there. ### Recommendations - **Pin the shared rule set in one committed place** so the two implementations can be diffed against a spec, not against each other. A small table in the ADR (or a fixture table of `(precision, anchor, end, raw) → expected label`) used verbatim by both the Java test and the TS spec is the cheapest drift guard. This is the "tested on both sides against one agreed rule set" the issue already gestures at — make the rule set a real artifact, not prose. - Keep `formatTitleDate` a pure private helper on `MassImportService` taking `(LocalDate, DatePrecision, LocalDate end, String raw)` and returning `String`. Do not introduce a new shared "DateLabel" service module — that would couple `importing` and a hypothetical formatting module for no benefit. `buildTitle` stays under 20 lines by delegating. - Decision 3 (raw on the list projection): **include `documentDateRaw` on `DocumentListItem`.** One nullable text column is negligible payload, and honest rendering is the whole point of the milestone — a list that can only ever say "Datum unbekannt" for UNKNOWN rows reintroduces information loss at the list layer. This also unblocks Decision 4's verbatim secondary line in lists. - This issue triggers **no doc-table updates I own** — no migration, no new package, no new route, no new ErrorCode/Permission. The new `documentDate.ts` util is not a domain module. I'd only expect a GLOSSARY touch if a *new term* is coined, but "date precision" is already owned by #671's GLOSSARY entry. Confirm #671 added it; if not, that's #671's blocker, not this one. ### Open Decisions - **Drift-guard mechanism for the two label implementations** — a shared fixture table consumed by both test suites (my lean) vs. trusting two independently-written test sets to stay in sync. The former costs one extra fixture file; the latter costs a future silent divergence ("ca." vs "circa", en-dash vs hyphen) that no test catches because each side tests only itself. _(This is a refinement of Felix's Decision 2, not a new axis.)_
Author
Owner

Felix Brandt — Senior Fullstack Developer

Observations

  • The formatter is a pure, branch-heavy function — ideal TDD target. documentDate.spec.ts with one failing test per precision branch before any implementation is exactly right, and the plan already sequences it that way. Good.
  • formatDocumentDate should delegate to the existing helpers in frontend/src/lib/shared/utils/date.ts: formatDate(iso, 'long') already yields "24. Dezember 1943" for DAY, and formatMCDate(iso, locale) yields "15. Jun. 1920" — the RANGE month-abbreviation building block. Both already apply the T12:00:00 noon anchor. Do not reimplement Intl.DateTimeFormat — the issue says so, and the helpers exist.
  • WhoWhenSection.svelte already owns the German dd.mm.yyyy anchor input via handleGermanDateInput + a hidden ISO input (name="documentDate"). The new precision <select> and conditional end-date belong here (or a small extracted child) — reuse handleGermanDateInput for the end field exactly as the anchor does. The dateDirty/onMount seeding pattern is subtle; mirror it for the end field, don't invent a new one.
  • Confirmed factual error in the issue: buildTitle uses GERMAN_DATE = "d. MMMM yyyy", so it already emits "1. Juni 1916", not "01.06.1916". The MONTH/SEASON bug (showing a day at all) is real; the literal string in AC scenario 2 ("not 01.06.1916") is wrong. Write the backend red test as "title contains 'Juni 1916' and does NOT contain '1. Juni'" — assert the real current output, or the test goes green for the wrong reason.

Recommendations

  • Signature: formatDocumentDate(iso: string | null, precision: DatePrecision, end?: string | null, raw?: string | null): string. iso must be nullable — UNKNOWN rows have a null anchor (AC scenario 9: "null anchor, raw '?'"). A non-null iso typing would force callers to lie. Guard-clause the null/UNKNOWN branches first.
  • Locale: the formatter must take the locale (or read getLocale() at the call site and pass it) — ReadyColumn.svelte already does formatMCDate(doc.documentDate, getLocale()). Hardcoding de-DE inside the formatter breaks Decision 4's localized structured label.
  • i18n in the formatter: route date_precision_unknown / date_precision_approx_prefix / season words through Paraglide m.*, not string literals — keep the formatter free of hardcoded German. The function stays pure because Paraglide messages are synchronous.
  • Scope completeness — this is bigger than "detail, list/search, edit". I grepped every formatDate/formatMCDate call on documentDate: DocumentRow.svelte (×2, mobile + desktop), ReadyColumn.svelte, SegmentationColumn.svelte, ThumbnailRow.svelte (×2, incl. an aria-label), DocumentMultiSelect.svelte, DocumentMetadataDrawer.svelte, DocumentTopBarTitle.svelte, PersonDocumentList.svelte, personFormat.ts, and geschichten/[id]/+page.svelte. The issue names three views but there are ~10 call sites. Either widen the scope explicitly or the milestone's honesty goal leaks through the uncovered ones. briefwechsel/ConversationTimeline.svelte is exempt — the issue says Briefwechsel is being removed.
  • Keep each Svelte change minimal: replace the call expression, add the secondary raw line + non-color cue as a small sibling. If a view needs the icon+raw+label combo in 3+ places, extract one <DocumentDate> component rather than copy-pasting the markup (one nameable visual region).

Open Decisions (none)

  • The shared-rule-set question is Markus's to frame; my implementation follows whichever artifact is chosen.
## Felix Brandt — Senior Fullstack Developer ### Observations - The formatter is a pure, branch-heavy function — ideal TDD target. `documentDate.spec.ts` with one failing test per precision branch before any implementation is exactly right, and the plan already sequences it that way. Good. - `formatDocumentDate` should delegate to the existing helpers in `frontend/src/lib/shared/utils/date.ts`: `formatDate(iso, 'long')` already yields "24. Dezember 1943" for DAY, and `formatMCDate(iso, locale)` yields "15. Jun. 1920" — the RANGE month-abbreviation building block. Both already apply the `T12:00:00` noon anchor. **Do not reimplement `Intl.DateTimeFormat`** — the issue says so, and the helpers exist. - `WhoWhenSection.svelte` already owns the German `dd.mm.yyyy` anchor input via `handleGermanDateInput` + a hidden ISO input (`name="documentDate"`). The new precision `<select>` and conditional end-date belong here (or a small extracted child) — reuse `handleGermanDateInput` for the end field exactly as the anchor does. The `dateDirty`/`onMount` seeding pattern is subtle; mirror it for the end field, don't invent a new one. - Confirmed factual error in the issue: `buildTitle` uses `GERMAN_DATE = "d. MMMM yyyy"`, so it already emits "1. Juni 1916", **not** "01.06.1916". The MONTH/SEASON bug (showing a day at all) is real; the literal string in AC scenario 2 ("not 01.06.1916") is wrong. Write the backend red test as *"title contains 'Juni 1916' and does NOT contain '1. Juni'"* — assert the real current output, or the test goes green for the wrong reason. ### Recommendations - **Signature:** `formatDocumentDate(iso: string | null, precision: DatePrecision, end?: string | null, raw?: string | null): string`. `iso` must be nullable — UNKNOWN rows have a null anchor (AC scenario 9: "null anchor, raw '?'"). A non-null `iso` typing would force callers to lie. Guard-clause the null/UNKNOWN branches first. - **Locale:** the formatter must take the locale (or read `getLocale()` at the call site and pass it) — `ReadyColumn.svelte` already does `formatMCDate(doc.documentDate, getLocale())`. Hardcoding `de-DE` inside the formatter breaks Decision 4's localized structured label. - **i18n in the formatter:** route `date_precision_unknown` / `date_precision_approx_prefix` / season words through Paraglide `m.*`, not string literals — keep the formatter free of hardcoded German. The function stays pure because Paraglide messages are synchronous. - **Scope completeness — this is bigger than "detail, list/search, edit".** I grepped every `formatDate`/`formatMCDate` call on `documentDate`: `DocumentRow.svelte` (×2, mobile + desktop), `ReadyColumn.svelte`, `SegmentationColumn.svelte`, `ThumbnailRow.svelte` (×2, incl. an `aria-label`), `DocumentMultiSelect.svelte`, `DocumentMetadataDrawer.svelte`, `DocumentTopBarTitle.svelte`, `PersonDocumentList.svelte`, `personFormat.ts`, and `geschichten/[id]/+page.svelte`. The issue names three views but there are ~10 call sites. Either widen the scope explicitly or the milestone's honesty goal leaks through the uncovered ones. `briefwechsel/ConversationTimeline.svelte` is exempt — the issue says Briefwechsel is being removed. - Keep each Svelte change minimal: replace the call expression, add the secondary raw line + non-color cue as a small sibling. If a view needs the icon+raw+label combo in 3+ places, extract one `<DocumentDate>` component rather than copy-pasting the markup (one nameable visual region). ### Open Decisions _(none)_ - The shared-rule-set question is Markus's to frame; my implementation follows whichever artifact is chosen.
Author
Owner

Nora "NullX" Steiner — Application Security Engineer

Observations

  • The one genuine attack surface here is meta_date_raw — untrusted verbatim spreadsheet text rendered into the DOM. The issue handles this correctly up front: render via Svelte default {...} interpolation, never {@html}, with an AC (scenario 10) and a formatter test asserting an angle-bracket value comes back inert. That is the right control for CWE-79 (stored XSS) and I fully endorse it. Svelte's default text interpolation HTML-escapes — <img src=x onerror=alert(1)> becomes literal text.
  • The threat is stored XSS, not reflected: the payload would have entered via the Excel import (#669) and sat in meta_date_raw. Phase 4 is the render sink, so it's the right place to enforce escaping even though it didn't introduce the data.

Recommendations

  • Test the sink at every render path, not just the pure formatter. A documentDate.spec.ts assertion proves the string is inert, but the formatter returns a label — the raw cell is rendered separately as the "Originaltext: …" secondary line in Svelte markup. Add at least one component-level test (vitest-browser-svelte) that mounts the view with a malicious raw and asserts no script/img element is created in the DOM. The pure-string test does not cover an accidental future {@html} on the raw line.
  • Grep-guard the regression. Add a Semgrep/ESLint check (or a one-line grep in CI) that flags {@html anywhere near documentDateRaw/meta_date_raw. The next developer "simplifying" the secondary line is the realistic regression vector; a test on one component won't catch a new {@html} introduced in a different view.
  • No new write-path validation is owed by this issue — and the issue correctly scopes it out. The precision <select> and end-date are editable here, but enum binding (→ clean 400 on a bogus value), end >= start, and the RANGE/end invariant ride the existing @RequirePermission(WRITE_ALL) write path defined with the DTO in #671. I want to confirm those server-side guards actually exist in #671's scope before this UI ships an editable enum — a client <select> constrains nothing. A crafted PATCH with documentDatePrecision: "DROP" or RANGE with end < start must be rejected by the backend, not just absent from the dropdown. If #671 did not add that validation, file it on #671/#669 — do not let the editable UI imply protection that isn't there.
  • The localized vs verbatim label (Decision 4) has no security dimension — both paths go through escaped interpolation. Lean whichever way UX prefers.

Open Decisions (none)

  • Escaping approach is solved; the only follow-up is verifying #671's write-path validation exists, which is a cross-issue confirmation, not a tradeoff for this issue.
## Nora "NullX" Steiner — Application Security Engineer ### Observations - The one genuine attack surface here is **`meta_date_raw`** — untrusted verbatim spreadsheet text rendered into the DOM. The issue handles this correctly up front: render via Svelte default `{...}` interpolation, never `{@html}`, with an AC (scenario 10) and a formatter test asserting an angle-bracket value comes back inert. That is the right control for **CWE-79 (stored XSS)** and I fully endorse it. Svelte's default text interpolation HTML-escapes — `<img src=x onerror=alert(1)>` becomes literal text. - The threat is *stored* XSS, not reflected: the payload would have entered via the Excel import (#669) and sat in `meta_date_raw`. Phase 4 is the render sink, so it's the right place to enforce escaping even though it didn't introduce the data. ### Recommendations - **Test the sink at every render path, not just the pure formatter.** A `documentDate.spec.ts` assertion proves the *string* is inert, but the formatter returns a *label* — the `raw` cell is rendered separately as the "Originaltext: …" secondary line in Svelte markup. Add at least one component-level test (vitest-browser-svelte) that mounts the view with a malicious `raw` and asserts no script/img element is created in the DOM. The pure-string test does not cover an accidental future `{@html}` on the raw line. - **Grep-guard the regression.** Add a Semgrep/ESLint check (or a one-line grep in CI) that flags `{@html` anywhere near `documentDateRaw`/`meta_date_raw`. The next developer "simplifying" the secondary line is the realistic regression vector; a test on one component won't catch a new `{@html}` introduced in a different view. - **No new write-path validation is owed by this issue** — and the issue correctly scopes it out. The precision `<select>` and end-date are editable here, but enum binding (→ clean 400 on a bogus value), `end >= start`, and the RANGE/end invariant ride the existing `@RequirePermission(WRITE_ALL)` write path defined with the DTO in #671. I want to confirm those server-side guards actually exist in #671's scope before this UI ships an editable enum — **a client `<select>` constrains nothing.** A crafted PATCH with `documentDatePrecision: "DROP"` or `RANGE` with `end < start` must be rejected by the backend, not just absent from the dropdown. If #671 did not add that validation, file it on #671/#669 — do not let the editable UI imply protection that isn't there. - The localized vs verbatim label (Decision 4) has no security dimension — both paths go through escaped interpolation. Lean whichever way UX prefers. ### Open Decisions _(none)_ - Escaping approach is solved; the only follow-up is verifying #671's write-path validation exists, which is a cross-issue confirmation, not a tradeoff for this issue.
Author
Owner

Sara Holt — Senior QA Engineer

Observations

  • The Gherkin is strong: one scenario per precision branch, plus RANGE same-month collapse, RANGE cross-month, SEASON null-raw fallback, UNKNOWN null-anchor, and the XSS-inert case. That maps almost 1:1 to documentDate.spec.ts. This is the cheapest, highest-value test layer — pure function, no browser, fast Vitest. Good call keeping it below the E2E line.
  • The 88% backend branch gate (not 80%) is correctly flagged. formatTitleDate(date, precision, end, raw) is a 7-branch switch plus the RANGE same-month/cross-month sub-branches and null-end fallback — that's ~10 branches in one Java helper. Plan the MassImportServiceTest cases against 88% or JaCoCo blocks the merge. I'll want a test per branch, not one happy-path test that grazes coverage.

Recommendations

  • Match the test count to the branch count on BOTH sides. The TS spec and the Java test must each cover all seven precisions + RANGE same-month + RANGE cross-month + RANGE null-end fallback + UNKNOWN null-anchor + SEASON null-raw fallback. That's ~12 cases per side. A shared (precision, anchor, end, raw) → expected fixture table (Markus's drift guard) would let me assert the same expectations on both sides — I strongly support that; it turns "two suites that hopefully agree" into "two suites proving one table."
  • Boundary cases the current Gherkin misses, add as failing tests first:
    • RANGE where end === start (degenerate range → should it render as a single day or "10.–10."? decide and test).
    • RANGE with a null end on a legacy row (Decision 1 says fall back to start-day only) — there IS an AC mention but no Gherkin scenario; add one.
    • RANGE crossing a year boundary (30.12.1916 – 2.1.1917) — the collapse logic for shared month/year must not mis-collapse across years. Not covered.
    • APPROX/YEAR/MONTH/SEASON with anchor on a month/day that the precision should suppress — assert the suppressed component never appears (e.g. YEAR of 1916-06-15 renders "1916", never "Juni" or "15").
  • Component-layer coverage for the render, not just the formatter. The formatter returns a string; the visible secondary raw line, the non-color icon for UNKNOWN, and the "ca." cue are rendered in Svelte. Add vitest-browser-svelte tests on at least one list view and the detail view: (a) UNKNOWN renders the icon + visible "Originaltext: ?" (not a title= attribute — assert it's real text), (b) malicious raw renders inert (shared with Nora's finding). Per my own rule these are integration-layer, not E2E — no new Playwright journey needed, agreed.
  • No H2 / Testcontainers concern hereformatTitleDate is pure Java logic, unit-testable with Mockito, no DB. The only DB-touching behavior (precision columns) is #671/#669's test surface.

Open Decisions

  • Degenerate RANGE semantics (end == start, and end == null) — render as a single day, or as a collapsed "10.–10."? The null-end legacy fallback is half-specified (AC text, no scenario). Pick the rendering, because it determines both the formatter test and the Java test expectation. Low-stakes but must be decided before the tests are written, since TDD needs the expected string.
## Sara Holt — Senior QA Engineer ### Observations - The Gherkin is strong: one scenario per precision branch, plus RANGE same-month collapse, RANGE cross-month, SEASON null-raw fallback, UNKNOWN null-anchor, and the XSS-inert case. That maps almost 1:1 to `documentDate.spec.ts`. This is the cheapest, highest-value test layer — pure function, no browser, fast Vitest. Good call keeping it below the E2E line. - The 88% backend branch gate (not 80%) is correctly flagged. `formatTitleDate(date, precision, end, raw)` is a 7-branch switch plus the RANGE same-month/cross-month sub-branches and null-end fallback — that's ~10 branches in one Java helper. Plan the `MassImportServiceTest` cases against 88% or JaCoCo blocks the merge. I'll want a test per branch, not one happy-path test that grazes coverage. ### Recommendations - **Match the test count to the branch count on BOTH sides.** The TS spec and the Java test must each cover all seven precisions + RANGE same-month + RANGE cross-month + RANGE null-end fallback + UNKNOWN null-anchor + SEASON null-raw fallback. That's ~12 cases per side. A shared `(precision, anchor, end, raw) → expected` fixture table (Markus's drift guard) would let me assert the *same* expectations on both sides — I strongly support that; it turns "two suites that hopefully agree" into "two suites proving one table." - **Boundary cases the current Gherkin misses, add as failing tests first:** - RANGE where `end === start` (degenerate range → should it render as a single day or "10.–10."? decide and test). - RANGE with a **null end** on a legacy row (Decision 1 says fall back to start-day only) — there IS an AC mention but no Gherkin scenario; add one. - RANGE crossing a **year** boundary (30.12.1916 – 2.1.1917) — the collapse logic for shared month/year must not mis-collapse across years. Not covered. - APPROX/YEAR/MONTH/SEASON with anchor on a month/day that the precision should *suppress* — assert the suppressed component never appears (e.g. YEAR of `1916-06-15` renders "1916", never "Juni" or "15"). - **Component-layer coverage for the render, not just the formatter.** The formatter returns a string; the visible secondary raw line, the non-color icon for UNKNOWN, and the "ca." cue are rendered in Svelte. Add vitest-browser-svelte tests on at least one list view and the detail view: (a) UNKNOWN renders the icon + visible "Originaltext: ?" (not a `title=` attribute — assert it's real text), (b) malicious raw renders inert (shared with Nora's finding). Per my own rule these are integration-layer, not E2E — no new Playwright journey needed, agreed. - **No H2 / Testcontainers concern here** — `formatTitleDate` is pure Java logic, unit-testable with Mockito, no DB. The only DB-touching behavior (precision columns) is #671/#669's test surface. ### Open Decisions - **Degenerate RANGE semantics (`end == start`, and `end == null`)** — render as a single day, or as a collapsed "10.–10."? The null-end legacy fallback is half-specified (AC text, no scenario). Pick the rendering, because it determines both the formatter test and the Java test expectation. _Low-stakes but must be decided before the tests are written, since TDD needs the expected string._
Author
Owner

Leonie Voss — UX Designer & Accessibility Strategist

Observations

  • This issue is unusually a11y-literate already: visible raw secondary line (not tooltip-only, correctly citing WCAG 1.4.13 Content on Hover/Focus), non-color cue for imprecision (WCAG 1.4.1 Use of Color), labelled <select> (not placeholder), ≥44px touch targets (WCAG 2.5.5/2.5.8), and read-only raw as static text rather than a disabled input. I agree with every one of those, and the reasoning (touch/keyboard users, mobile-skewed reader audience, seniors 60+) is exactly the hardest-constraint-first lens I'd apply.
  • Confirmed the existing WhoWhenSection.svelte date field uses a proper <label for="documentDate">, inputmode="numeric", aria-describedby for the error, and py-3 padding. The new precision/end controls should match that pattern — it's already accessible, so extend it, don't reinvent.

Recommendations

  • Announce the conditional end-date reveal politely. Progressive disclosure of the end-date when RANGE is selected must be announced to screen readers — wrap the revealed field region in aria-live="polite" (or move focus to it). A field that silently appears is invisible to a non-sighted user who just changed the select. Pair the select with the revealed field via the DOM order so keyboard tab lands on it next.
  • The non-color cue needs a real text alternative, not just an icon. The calendar-with-question icon for UNKNOWN must carry an aria-label (or adjacent visually-hidden text) — an icon alone is color/shape-only to a screen reader. The "ca." prefix for APPROX is already textual, so that one's fine. For UNKNOWN, the visible "Datum unbekannt" label is the redundant text cue, so the icon can be aria-hidden="true" and decorative — that's cleaner than labelling both. Decide: label the icon OR rely on the visible text, not neither.
  • Secondary raw line: real muted text, ≥12px, tokenized color. Use text-xs text-ink-3 (the project's muted token) — but verify text-ink-3 on bg-surface clears 4.5:1; muted greys often fail. If it's below AA, bump to text-ink-2. Never go below 12px even for "Originaltext: …". Prefix the raw with a translatable label ("Originaltext:" / "Original:" / "Texto original:") so it's self-describing, not a bare orphaned string.
  • Decision 4 — localize the structured label AND keep the verbatim raw line. This is the right answer and it's genuinely an archival-fidelity-vs-reader-friendliness call, so it's a real decision for the owner: an es-locale reader sees "Verano 1916" as the friendly label with "Sommer 1916" preserved verbatim below. That satisfies the reader (younger, mobile, may not read German) without losing the archivist's source text. The cost is more i18n keys (season words ×3 locales). I lean localize-plus-verbatim, but the owner should confirm whether non-German season/month words are wanted or whether archival purity ("show the German as written") is the brand stance.
  • Touch target on the precision <select>: native selects are notoriously short. Enforce min-h-[44px] (prefer 48px) explicitly — don't trust the browser default, which is ~28-32px and fails 2.5.5 for the senior author audience.

Open Decisions

  • Localize season/month words for en/es readers, or keep them verbatim German? Localize-plus-verbatim (my lean) = friendliest for the mobile reader audience but adds season-word i18n keys in 3 locales and a small drift risk between the localized label and the German raw. Verbatim-only = archival purity, zero new season keys, but an es reader sees "Sommer"/"Frühjahr" with no translation. Owner's brand call. (Same axis as the spec's Decision 4.)
## Leonie Voss — UX Designer & Accessibility Strategist ### Observations - This issue is unusually a11y-literate already: visible raw secondary line (not tooltip-only, correctly citing **WCAG 1.4.13 Content on Hover/Focus**), non-color cue for imprecision (**WCAG 1.4.1 Use of Color**), labelled `<select>` (not placeholder), ≥44px touch targets (**WCAG 2.5.5/2.5.8**), and read-only raw as static text rather than a disabled input. I agree with every one of those, and the reasoning (touch/keyboard users, mobile-skewed reader audience, seniors 60+) is exactly the hardest-constraint-first lens I'd apply. - Confirmed the existing `WhoWhenSection.svelte` date field uses a proper `<label for="documentDate">`, `inputmode="numeric"`, `aria-describedby` for the error, and `py-3` padding. The new precision/end controls should match that pattern — it's already accessible, so extend it, don't reinvent. ### Recommendations - **Announce the conditional end-date reveal politely.** Progressive disclosure of the end-date when RANGE is selected must be announced to screen readers — wrap the revealed field region in `aria-live="polite"` (or move focus to it). A field that silently appears is invisible to a non-sighted user who just changed the select. Pair the select with the revealed field via the DOM order so keyboard tab lands on it next. - **The non-color cue needs a real text alternative, not just an icon.** The calendar-with-question icon for UNKNOWN must carry an `aria-label` (or adjacent visually-hidden text) — an icon alone is color/shape-only to a screen reader. The "ca." prefix for APPROX is already textual, so that one's fine. For UNKNOWN, the visible "Datum unbekannt" label *is* the redundant text cue, so the icon can be `aria-hidden="true"` and decorative — that's cleaner than labelling both. Decide: label the icon OR rely on the visible text, not neither. - **Secondary raw line: real muted text, ≥12px, tokenized color.** Use `text-xs text-ink-3` (the project's muted token) — but verify `text-ink-3` on `bg-surface` clears 4.5:1; muted greys often fail. If it's below AA, bump to `text-ink-2`. Never go below 12px even for "Originaltext: …". Prefix the raw with a translatable label ("Originaltext:" / "Original:" / "Texto original:") so it's self-describing, not a bare orphaned string. - **Decision 4 — localize the structured label AND keep the verbatim raw line.** This is the right answer and it's genuinely an archival-fidelity-vs-reader-friendliness call, so it's a real decision for the owner: an es-locale reader sees "Verano 1916" as the friendly label with "Sommer 1916" preserved verbatim below. That satisfies the reader (younger, mobile, may not read German) without losing the archivist's source text. The cost is more i18n keys (season words ×3 locales). I lean localize-plus-verbatim, but the owner should confirm whether non-German season/month words are *wanted* or whether archival purity ("show the German as written") is the brand stance. - **Touch target on the precision `<select>`:** native selects are notoriously short. Enforce `min-h-[44px]` (prefer 48px) explicitly — don't trust the browser default, which is ~28-32px and fails 2.5.5 for the senior author audience. ### Open Decisions - **Localize season/month words for en/es readers, or keep them verbatim German?** Localize-plus-verbatim (my lean) = friendliest for the mobile reader audience but adds season-word i18n keys in 3 locales and a small drift risk between the localized label and the German raw. Verbatim-only = archival purity, zero new season keys, but an es reader sees "Sommer"/"Frühjahr" with no translation. Owner's brand call. _(Same axis as the spec's Decision 4.)_
Author
Owner

Tobias Wendt — DevOps & Platform Engineer

Observations

  • This is a frontend formatter + one Java helper change. No infrastructure surface: no new Docker service, no Compose change, no env var, no port, no migration (correctly delegated to #671). Nothing for me to size or operate. From a deployment-risk view this is about as low as it gets.
  • The one CI-relevant item is the 88% backend branch gate the issue flags. That's a real merge blocker, not a nicety — JaCoCo runs in CI and a branch-heavy formatTitleDate that's under-tested will fail the pipeline. Sara owns the test count; I just confirm the gate is enforced in CI and will block.

Recommendations

  • Sequence the merges so CI never sees a half-wired state. This issue's frontend code references documentDatePrecision/documentDateEnd/documentDateRaw, which do not exist in frontend/src/lib/generated/api.ts today (I checked). The build will fail npm run check until #671 merges and npm run generate:api regenerates types. Concretely: #671 → regenerate types → then this PR. Do not open this PR against a base where #671 hasn't landed, or CI red-flags on missing types and wastes a run. The issue says this; I'm reinforcing it as a pipeline ordering constraint.
  • npm run generate:api requires the dev-profile backend running. Whoever regenerates types must boot the backend with --spring.profiles.active=dev (Swagger/api-docs are dev-only) or the regen produces stale output. Easy to forget; it's the most common cause of "types missing" confusion.
  • No new Playwright journey — agreed, and good for CI budget. The E2E suite has an <8min target; a metadata-rendering change verified by Vitest unit + component tests keeps E2E lean. Don't let scope creep add a date-rendering E2E.
  • Renovate / image pinning / backups / secrets — none touched. No action from me.

Open Decisions (none)

  • No infrastructure tradeoffs. The merge-ordering constraint is a fact (#671 first), not a choice.
## Tobias Wendt — DevOps & Platform Engineer ### Observations - This is a frontend formatter + one Java helper change. **No infrastructure surface**: no new Docker service, no Compose change, no env var, no port, no migration (correctly delegated to #671). Nothing for me to size or operate. From a deployment-risk view this is about as low as it gets. - The one CI-relevant item is the **88% backend branch gate** the issue flags. That's a real merge blocker, not a nicety — JaCoCo runs in CI and a branch-heavy `formatTitleDate` that's under-tested will fail the pipeline. Sara owns the test count; I just confirm the gate is enforced in CI and will block. ### Recommendations - **Sequence the merges so CI never sees a half-wired state.** This issue's frontend code references `documentDatePrecision`/`documentDateEnd`/`documentDateRaw`, which do **not** exist in `frontend/src/lib/generated/api.ts` today (I checked). The build will fail `npm run check` until #671 merges and `npm run generate:api` regenerates types. Concretely: #671 → regenerate types → then this PR. Do not open this PR against a base where #671 hasn't landed, or CI red-flags on missing types and wastes a run. The issue says this; I'm reinforcing it as a pipeline ordering constraint. - **`npm run generate:api` requires the dev-profile backend running.** Whoever regenerates types must boot the backend with `--spring.profiles.active=dev` (Swagger/api-docs are dev-only) or the regen produces stale output. Easy to forget; it's the most common cause of "types missing" confusion. - **No new Playwright journey — agreed, and good for CI budget.** The E2E suite has an <8min target; a metadata-rendering change verified by Vitest unit + component tests keeps E2E lean. Don't let scope creep add a date-rendering E2E. - Renovate / image pinning / backups / secrets — **none touched**. No action from me. ### Open Decisions _(none)_ - No infrastructure tradeoffs. The merge-ordering constraint is a fact (#671 first), not a choice.
Author
Owner

Elicit — Requirements Engineer & Business Analyst

Observations

  • The issue is well-formed: clear re-scope banner, explicit in/out-of-scope, file-level breakdown, 12 Gherkin scenarios, and an Open Decisions register with three resolved + one still-open. This is close to Definition-of-Ready. The story title is a touch long (it's the milestone-level "As an archivist…" framing, not the Phase-4 scope) but the body re-scopes clearly.
  • The acceptance criteria are testable and trace cleanly to the milestone goal ("honest uncertainty in dates"). The XSS scenario and the SEASON-null-raw fallback are exactly the unhappy paths I'd hunt for, and they're already written. Credit where due.

Ambiguities / contradictions I must name

  • Factual contradiction in AC scenario 2. "buildTitle produces a title containing 'Juni 1916', not '01.06.1916'." The current code (GERMAN_DATE = "d. MMMM yyyy") never emits 01.06.1916 — it emits 1. Juni 1916. The requirement (don't show a day for MONTH precision) is sound and clear; the example output is wrong. An implementer taking the AC literally would write a test against a string the code never produces. Fix the example to the real current output before this is built.
  • Scope boundary "detail, list/search, and edit views" is under-specified vs. reality. The honest-rendering requirement is "every view that renders documentDate." Felix enumerated ~10 actual call sites (DocumentRow, ReadyColumn, SegmentationColumn, ThumbnailRow incl. an aria-label, DocumentMultiSelect, DocumentMetadataDrawer, DocumentTopBarTitle, PersonDocumentList, personFormat.ts, geschichten detail). "List/search, detail, edit" reads as ~3 sites but the behavior must reach all of them or the milestone goal partially leaks. Recommend: restate the scope as "all documentDate render sites except the to-be-removed Briefwechsel" and enumerate them in the issue, so "done" is unambiguous and no site is silently missed.
  • Degenerate-RANGE and null-end rendering is half-specified (Sara also flagged). Decision 1 says "defensively handle a null end (fall back to start day only)" — that's a requirement but has no Gherkin scenario. end == start is undefined. A requirement without an observable outcome isn't testable. Add the two scenarios.

Recommendations

  • Convert Decision 1's null-end fallback and the end == start behavior into explicit Given-When-Then scenarios. Every defensive behavior named in prose should have a matching AC, or it won't be verified.
  • Add an AC asserting suppressed precision components never appear (YEAR of a June date renders "1916", never "Juni"/"15") — the current scenarios assert the positive output but not that the finer components are absent, which is the actual anti-fabrication guarantee.
  • NFR check for this slice: i18n (covered — de/en/es keys), a11y (covered — Leonie's WCAG items), security (covered — Nora's escaping). Performance and observability are N/A for a pure render helper. Localization has one open product decision (season/month words) — flagged below. No other NFR gaps.

Open Decisions

  • Localized vs. verbatim structured label (season/month words for en/es) — same axis as spec Decision 4 and Leonie's. This is a genuine product/brand call (reader-friendliness vs. archival fidelity), not resolvable from code. Owner must decide; it determines whether season-word i18n keys exist in 3 locales.
## Elicit — Requirements Engineer & Business Analyst ### Observations - The issue is well-formed: clear re-scope banner, explicit in/out-of-scope, file-level breakdown, 12 Gherkin scenarios, and an Open Decisions register with three resolved + one still-open. This is close to Definition-of-Ready. The story title is a touch long (it's the milestone-level "As an archivist…" framing, not the Phase-4 scope) but the body re-scopes clearly. - The acceptance criteria are testable and trace cleanly to the milestone goal ("honest uncertainty in dates"). The XSS scenario and the SEASON-null-raw fallback are exactly the unhappy paths I'd hunt for, and they're already written. Credit where due. ### Ambiguities / contradictions I must name - **Factual contradiction in AC scenario 2.** "buildTitle produces a title containing 'Juni 1916', not '01.06.1916'." The current code (`GERMAN_DATE = "d. MMMM yyyy"`) never emits `01.06.1916` — it emits `1. Juni 1916`. The *requirement* (don't show a day for MONTH precision) is sound and clear; the *example output* is wrong. An implementer taking the AC literally would write a test against a string the code never produces. Fix the example to the real current output before this is built. - **Scope boundary "detail, list/search, and edit views" is under-specified vs. reality.** The honest-rendering requirement is "every view that renders documentDate." Felix enumerated ~10 actual call sites (DocumentRow, ReadyColumn, SegmentationColumn, ThumbnailRow incl. an aria-label, DocumentMultiSelect, DocumentMetadataDrawer, DocumentTopBarTitle, PersonDocumentList, personFormat.ts, geschichten detail). "List/search, detail, edit" reads as ~3 sites but the behavior must reach all of them or the milestone goal partially leaks. **Recommend: restate the scope as "all documentDate render sites except the to-be-removed Briefwechsel" and enumerate them in the issue**, so "done" is unambiguous and no site is silently missed. - **Degenerate-RANGE and null-end rendering is half-specified** (Sara also flagged). Decision 1 says "defensively handle a null end (fall back to start day only)" — that's a requirement but has no Gherkin scenario. `end == start` is undefined. A requirement without an observable outcome isn't testable. Add the two scenarios. ### Recommendations - Convert Decision 1's null-end fallback and the `end == start` behavior into explicit Given-When-Then scenarios. Every defensive behavior named in prose should have a matching AC, or it won't be verified. - Add an AC asserting **suppressed precision components never appear** (YEAR of a June date renders "1916", never "Juni"/"15") — the current scenarios assert the *positive* output but not that the finer components are *absent*, which is the actual anti-fabrication guarantee. - NFR check for this slice: **i18n** (covered — de/en/es keys), **a11y** (covered — Leonie's WCAG items), **security** (covered — Nora's escaping). **Performance** and **observability** are N/A for a pure render helper. **Localization** has one open product decision (season/month words) — flagged below. No other NFR gaps. ### Open Decisions - **Localized vs. verbatim structured label (season/month words for en/es)** — same axis as spec Decision 4 and Leonie's. This is a genuine product/brand call (reader-friendliness vs. archival fidelity), not resolvable from code. Owner must decide; it determines whether season-word i18n keys exist in 3 locales.
Author
Owner

Decision Queue — Action Required

3 decisions need your input before implementation starts. Everything else was a concrete recommendation, not a question.

UX / Localization

  • Localize the SEASON/RANGE structured label, or keep it verbatim German? A SEASON cell is German ("Sommer 1916"). Either the en/es structured label is localized ("Summer 1916" / "Verano 1916", friendlier for the mobile reader audience who may not read German) with the verbatim "Sommer 1916" kept as the secondary line; or keep the structured label verbatim German (archival fidelity, zero new i18n keys). Cost of localizing: season-word keys in 3 locales + a small drift risk between label and raw. (Raised by: Leonie, Elicit; this is spec Decision 4, still open.)

Behavior / Test expectations

  • Degenerate and null-end RANGE rendering. Two sub-cases are unspecified and block TDD because the expected string isn't defined: (a) end == start → render a single day, or a collapsed "10.–10."? (b) end == null on a legacy pre-#669 row → Decision 1 says "fall back to start day only" but there's no Gherkin scenario for it. Pick both renderings, then they become the formatter test + the Java formatTitleDate test expectations. (Raised by: Sara, Elicit.)

Architecture / Testing

  • Drift-guard between the two label implementations. buildTitle's Java formatTitleDate and the TS formatDocumentDate must produce the same label, forever. Either commit a single shared (precision, anchor, end, raw) → expected label fixture table that both test suites assert against (one extra fixture file; catches "ca." vs "circa" / en-dash vs hyphen drift); or trust two independently-written test sets to stay in sync (no extra file; silent divergence is possible because each side only tests itself). Lean: shared fixture table. (Raised by: Markus, reinforced by Sara.)

Note for the implementer (not a decision — a factual fix): AC scenario 2's "not 01.06.1916" is wrong. buildTitle uses GERMAN_DATE = "d. MMMM yyyy", so it already emits "1. Juni 1916". The MONTH-precision bug (showing any day) is real; the literal example string is not. Write the red test against the real current output ("contains 'Juni 1916', does NOT contain '1. Juni'"). (Flagged by: Markus, Felix, Elicit.)

## Decision Queue — Action Required _3 decisions need your input before implementation starts. Everything else was a concrete recommendation, not a question._ ### UX / Localization - **Localize the SEASON/RANGE structured label, or keep it verbatim German?** A SEASON cell is German ("Sommer 1916"). Either the en/es structured label is localized ("Summer 1916" / "Verano 1916", friendlier for the mobile reader audience who may not read German) **with** the verbatim "Sommer 1916" kept as the secondary line; **or** keep the structured label verbatim German (archival fidelity, zero new i18n keys). Cost of localizing: season-word keys in 3 locales + a small drift risk between label and raw. _(Raised by: Leonie, Elicit; this is spec Decision 4, still open.)_ ### Behavior / Test expectations - **Degenerate and null-end RANGE rendering.** Two sub-cases are unspecified and block TDD because the expected string isn't defined: (a) `end == start` → render a single day, or a collapsed "10.–10."? (b) `end == null` on a legacy pre-#669 row → Decision 1 says "fall back to start day only" but there's no Gherkin scenario for it. Pick both renderings, then they become the formatter test + the Java `formatTitleDate` test expectations. _(Raised by: Sara, Elicit.)_ ### Architecture / Testing - **Drift-guard between the two label implementations.** `buildTitle`'s Java `formatTitleDate` and the TS `formatDocumentDate` must produce the same label, forever. Either commit a single shared `(precision, anchor, end, raw) → expected label` fixture table that **both** test suites assert against (one extra fixture file; catches "ca." vs "circa" / en-dash vs hyphen drift); **or** trust two independently-written test sets to stay in sync (no extra file; silent divergence is possible because each side only tests itself). Lean: shared fixture table. _(Raised by: Markus, reinforced by Sara.)_ --- **Note for the implementer (not a decision — a factual fix):** AC scenario 2's "not 01.06.1916" is wrong. `buildTitle` uses `GERMAN_DATE = "d. MMMM yyyy"`, so it already emits "1. Juni 1916". The MONTH-precision bug (showing *any* day) is real; the literal example string is not. Write the red test against the real current output ("contains 'Juni 1916', does NOT contain '1. Juni'"). _(Flagged by: Markus, Felix, Elicit.)_
marcel changed title from As an archivist I want imprecise and unknown document dates modelled honestly so that letters dated "Sommer 1916", "15.II.60", or "10./11.I.1917" keep their real precision instead of being fabricated or lost to As a reader I want imprecise and unknown dates rendered honestly (e.g. "Juni 1916", "ca. 1916", "Datum unbekannt") so a document never shows a precision the data doesn't have 2026-05-27 07:31:33 +02:00
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#666