From 05652a18eee286fe3209fc990e16bfe22c526a63 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 11 Jun 2026 13:06:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(adr):=20ADR-037=20=E2=80=94=20journey=5Fit?= =?UTF-8?q?ems=20serves=20both=20Geschichte=20subtypes=20(#795)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...ey-items-serve-both-geschichte-subtypes.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/adr/037-journey-items-serve-both-geschichte-subtypes.md diff --git a/docs/adr/037-journey-items-serve-both-geschichte-subtypes.md b/docs/adr/037-journey-items-serve-both-geschichte-subtypes.md new file mode 100644 index 00000000..099211f4 --- /dev/null +++ b/docs/adr/037-journey-items-serve-both-geschichte-subtypes.md @@ -0,0 +1,78 @@ +# 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.