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:
- Keep an allowlist (
if type not in (JOURNEY, STORY) throw ...) — dead code today becauseGeschichteTypeis a two-constant enum; the branch can never be reached and would fail the JaCoCo branch-coverage gate. - 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_documentsjoin 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 anyGeschichte, regardless of subtype. The 100-item cap and dedup constraint apply to all.- GLOSSARY.md and ARCHITECTURE.md updated to reflect that
JourneyItemis not JOURNEY-specific. - The
l3-backend-3g-supporting.pumlC4 diagram updated: type-guard language removed,geschQuerySvcrel label reads "Checks Geschichte existence" (not "and type"). StoryDocumentPanel.svelteis the STORY-side consumer;JourneyEditor.svelteis the JOURNEY-side consumer. Neither is aware of the other.- Known pre-existing constraint conflict:
ON DELETE SET NULLonjourney_items.document_idcombined withchk_journey_item_not_emptycauses a DB-level 500 when a document linked via a note-less item is deleted. Pre-existing; tracked in follow-up issue.