Files
familienarchiv/docs/adr/037-journey-items-serve-both-geschichte-subtypes.md
2026-06-11 13:06:39 +02:00

4.1 KiB

ADR-037 — journey_items serves both STORY and JOURNEY Geschichte subtypes

Status: Accepted Date: 2026-06-11 Issue: #795 (restore document management for STORY-type Geschichten), PR #804 review

Context

V72 added the journey_items table as the backing store for Lesereisen (JOURNEY-type Geschichten). At the same time, the previous geschichten_documents join table was dropped (#753) and the restoration of a STORY-level document attachment mechanism was deferred to a future issue.

JourneyItemService.append() contained an application-level type guard that rejected append() calls on STORY-type Geschichten with GESCHICHTE_TYPE_MISMATCH. This guard was the only place where the STORY restriction was encoded — the database schema never enforced it (no CHECK constraint, no partial index on type='JOURNEY').

When #795 restored document attachment for STORY-type Geschichten, the type guard was the only obstacle. Two implementation paths were considered:

  1. Keep an allowlist (if type not in (JOURNEY, STORY) throw ...) — dead code today because GeschichteType is a two-constant enum; the branch can never be reached and would fail the JaCoCo branch-coverage gate.
  2. Delete the guard entirely — the schema never encoded the restriction; deleting dead application logic rather than replacing it with more dead logic.

Path 2 was chosen.

Decision

journey_items is the document-attachment mechanism for both STORY and JOURNEY subtypes. No application-level type guard governs which subtype may hold items. The only behavioral difference between the two subtypes' use of items is at the UI layer:

  • JOURNEY: items form an ordered reading sequence rendered as a Lesereise.
  • STORY: items are a set of attached reference documents rendered as a sidebar panel.

Both subtypes share the same capacity cap (100 items), dedup index, position semantics, and DEFERRABLE constraint — enforced at the database layer, not re-implemented per subtype.

The GESCHICHTE_TYPE_MISMATCH error code was removed end-to-end (backend enum, frontend ErrorCode type + getErrorMessage() case, all three locale files). GESCHICHTE_TYPE_IMMUTABLE is unrelated and was left intact.

Naming asymmetry (intentional)

The error codes JOURNEY_AT_CAPACITY and JOURNEY_DOCUMENT_ALREADY_ADDED carry journey-flavored names. Renaming them would ripple through ErrorCode.java, errors.ts, and three locale files for zero behavior change. StoryDocumentPanel remaps these two codes to story-worded user messages at the presentation layer — the asymmetry is a documented decision, not an accident.

Alternatives rejected

  • Separate story_documents join table for STORY — creates two nearly-identical schemas for the same concept (document attachment with dedup and ordering), doubles the migration surface, and splits the capacity/dedup logic. Rejected as unnecessary duplication.
  • Allowlist type guard (if type not in (JOURNEY, STORY)) — unreachable dead code under a two-constant enum; fails the JaCoCo branch gate. Rejected.
  • Per-subtype application validation — the schema never encoded the restriction; an application-only rule with no schema backing is the weakest kind of invariant and was removed when the product decision reversed it.

Consequences

  • JourneyItemService.append() accepts items for any Geschichte, regardless of subtype. The 100-item cap and dedup constraint apply to all.
  • GLOSSARY.md and ARCHITECTURE.md updated to reflect that JourneyItem is not JOURNEY-specific.
  • The l3-backend-3g-supporting.puml C4 diagram updated: type-guard language removed, geschQuerySvc rel label reads "Checks Geschichte existence" (not "and type").
  • StoryDocumentPanel.svelte is the STORY-side consumer; JourneyEditor.svelte is the JOURNEY-side consumer. Neither is aware of the other.
  • Known pre-existing constraint conflict: ON DELETE SET NULL on journey_items.document_id combined with chk_journey_item_not_empty causes a DB-level 500 when a document linked via a note-less item is deleted. Pre-existing; tracked in follow-up issue.