docs(adr): ADR-037 — journey_items serves both Geschichte subtypes (#795)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
78
docs/adr/037-journey-items-serve-both-geschichte-subtypes.md
Normal file
78
docs/adr/037-journey-items-serve-both-geschichte-subtypes.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user