# Unified Fristenrechner v3 — Pathway A (Browse) vs Pathway B (Event → Frist)

**Author:** cronus (inventor)
**Date:** 2026-05-05
**Task:** t-paliad-133
**Mode:** design (LOCKED 2026-05-05 10:33) — implementation now in progress on the same branch.
**Branch:** `mai/cronus/fristenrechner-v3-pathways` (worktree)
**Status:** v3 LOCKED — m approved 2026-05-05 10:33 with answers to all 12 open questions; Maria's two scope additions folded in (forum granularity at court-system level + party-perspective selector absorbing t-paliad-132). Single-branch / sequential-commits / one-final-merge per m's instruction.
**Reads with:** `docs/plans/unified-fristenrechner.md` (v2, shipped) for the concept-layer / search-backend / coverage details that v3 inherits unchanged.

> *m, 2026-05-05 10:06:* "Aber jetzt wurde das beim Fristenrechner Etwas vermischt. Ich wollte die aktuelle 'Schnellübersicht' im Zusammenhang mit 'Was kommt nach…?'. Etwas, wo wir Filter kombinieren können. (…) Es gibt hauptsächlich zwei Ausgangspunkte: Ich möchte mich über den allgemeinen Verfahrensablauf informieren ODER ich möchte wissen, ob ich eine und was für eine Frist ich eintragen muss aufgrund eines Ereignisses. Letzteres sollten wir in zwei teilen: einen Entscheidungsbaum (Button nach Button) (…) Und dann zeigen wir eben die Resultate als Cards unten an, narrowing it down one by one. Einmal über die fixe Abfolge, einmal über Filter."

---

## 0. Why v3

v2 shipped (16 migrations 037-046, then PR-Phase-C/D 047 + handler + frontend). The Fristenrechner page now stacks **three** entry points on `/tools/fristenrechner`:

1. The **Phase D concept-card search bar** at the top — typed query → cards with proceeding pills inside, drill-in pill click jumps into one of the modes below.
2. The **Verfahrensablauf tile grid** (UPC / DE / EPA / DPMA proceeding tiles → date input → timeline / columns view) — original entry, "I know which proceeding I'm in, calculate me the whole tree".
3. The **"Was kommt nach…" tab** — trigger-event picker → date input → flat result list — original second entry.

The borders between (1), (2), (3) are blurred:

- (1) does **what (3) does** for some queries (search "Versäumnisurteil" → trigger-event card pill leads back into (3); search "Berufung" → proceeding-rule pills lead into (2)).
- (1) bypasses (2) for most concept queries — the user types and drills directly, never seeing the proceeding tile grid.
- (3) carries 100+ youpc-imported triggers most of which are unlinked to concepts — they don't surface in (1) and don't have a friendly entry from (2).

Two goals get tangled: **"learn how a proceeding works"** and **"figure out the Frist I need to enter on a real matter"**. m's read: separate them. **v3 forks the page surface** so each mental model has its own entrypoint, and the second one (Frist-eintragen) gets two complementary navigation styles — a guided decision tree (B1) and a free filter+search (B2).

v3 does **not** change the calculator math, the deadline_concepts data, the migrations 037-047 already shipped, or the search-backend mat-view. It restructures the **entry UX** above all of that.

---

## 1. Executive summary

The Fristenrechner landing fork:

```
                     ┌──────────────────────────────────────┐
                     │         Fristenrechner               │
                     │   Was möchten Sie tun?               │
                     ├──────────────────┬───────────────────┤
                     │  📖 Pathway A    │  📅 Pathway B     │
                     │  Verfahrensablauf│  Frist eintragen  │
                     │  informieren     │  aufgrund Ereignis│
                     └──────────────────┴───────────────────┘
                                            │
                          ┌─────────────────┴─────────────────┐
                          ▼                                   ▼
              ┌──────────────────────┐         ┌─────────────────────────┐
              │  Pathway A (Browse)  │         │ Pathway B (Event→Frist) │
              │                      │         │ ┌──────┐ ┌────────────┐ │
              │ • UPC tile grid      │         │ │ B1   │ │ B2         │ │
              │ • DE tile grid       │         │ │ Tree │ │ Filter+    │ │
              │ • EPA tile grid      │         │ │      │ │ Suche      │ │
              │ • DPMA tile grid     │         │ └──────┘ └────────────┘ │
              │                      │         │                         │
              │ → existing wizard    │         │ → narrowing concept     │
              │   (date input,       │         │   cards (same shape     │
              │   flags, timeline,   │         │   as v2 Phase D)        │
              │   columns view)      │         │                         │
              └──────────────────────┘         └─────────────────────────┘
```

**Pathway A — Verfahrensablauf informieren (Browse / Learn).** Identical to today's tile-grid → date-input → timeline flow. The user picks a proceeding type because they know what it is and want to see the whole tree. No event-driven entry; no search bar.

**Pathway B — Frist eintragen aufgrund Ereignis (Event → Deadline).** For users who just had something happen and need to know which Frist(en) to enter on a matter. Subdivides into:

- **B1 — Entscheidungsbaum.** Sequential 3-5-step button cascade. Each step asks one question, offers 2-6 buttons, narrows to a candidate set of Fristen as concept cards below.
- **B2 — Filter / Suche.** Free-text search bar + filter chips (Verfahrensart · Partei · Rechtsquelle · **Gericht/System** · plus the existing Phase D pills). Filters AND together; adding a filter reduces the result set.

Both B1 and B2 produce the **same concept-card UI** that v2 Phase D shipped — one card per concept, proceeding pills inside. Drill-in pill click hands off to Pathway A (proceeding-tree calculator) or to the trigger-event calculator. **B1 and B2 share the same underlying filter state** under the hood — the decision tree is a guided way to build a filter combination. The user can switch B1↔B2 mid-flow without losing context.

**Data addition:** new `paliad.event_categories` table (recursive tree) + `paliad.event_category_concepts` junction. Defines the decision-tree taxonomy as data, not as a hard-coded UI tree. Purely additive; no changes to existing tables.

**Forum filter (Q8 reversal).** v2 dropped forum-filter on the rationale "rules are shared per court system within a jurisdiction". m has now reversed that: even though the *legal source* of a deadline is jurisdiction-wide, the *user* often knows which **forum** they're working at and wants a filter. v3 brings back a **Gericht/System** filter on B2 — multi-select chips, AND-each, default all selected, narrows the concept-card result set.

**Legacy tabs.** v2's two tabs (Verfahrensablauf / Was kommt nach…) are functionally absorbed by Pathway A and Pathway B. **Inventor recommends retire** — keep only the new fork. Defer retirement to Phase E pending m's go.

**Phasing:**

- **Phase A** — `paliad.event_categories` schema + seed (data only, no UX change).
- **Phase B** — landing fork UI; route to existing tile grid (A) or new Pathway-B shell (B). URL state persists choice.
- **Phase C** — B1 decision-tree component, data-driven; result-card list narrows step-by-step.
- **Phase D** — B2 filter expansion; bring back forum chip filter; ensure progressive narrowing semantics.
- **Phase E** — retire legacy tabs (`mode-procedure-tab` / `mode-event-tab`) IF m approves; otherwise keep as "Klassische Ansicht" link in the page footer.

---

## 2. Verified current state

| Area | What's there today (post-t-paliad-131) | Reused in v3 |
|---|---|---|
| `paliad.deadline_concepts` | 57 concepts seeded | Yes (no change) |
| `paliad.deadline_rules` (gained `concept_id`, `legal_source`, `condition_flag text[]`) | 137 rules across 19 fristenrechner trees | Yes (no change) |
| `paliad.trigger_events` | 100 youpc-imported (UPC) + 7 paliad-native at id ≥200 (DE/EPA cross-cutting) | Yes (no change) |
| `paliad.deadline_search` matview | UNION of rule + trigger rows, GIN-indexed | Yes — v3 read path stays identical |
| `GET /api/tools/fristenrechner/search` handler | Phase D concept-card endpoint | Yes — extended with new `forum=` query param (§4) |
| `frontend/src/fristenrechner.tsx` | search bar + chips + 2 tabs (Verfahrensablauf / Was kommt nach…) | Reshape: split into landing fork → Pathway A page-fragment, Pathway B shell with B1/B2 toggle |
| `frontend/src/client/fristenrechner.ts` (≈1500 lines) | wizard logic for both modes | Pathway A keeps existing flow verbatim; Pathway B reorganises search-result rendering into the new shell |
| `frontend/src/client/fristenrechner-search.ts` (Phase D, ≈600 lines) | search → cards → drill-in | Reused as the engine of B2; the DOM scaffolding moves into the Pathway B shell |

**No migrations are reverted.** v3 builds on top of the data layer that v2 shipped. Phase A adds two new tables; nothing in 037-047 changes.

---

## 3. UX surface

### 3.1 Landing fork

```
┌──────────────────────────────────────────────────────────────────┐
│  Fristenrechner                                                  │
│  Berechnung von Verfahrensfristen — UPC, DE, EPA, DPMA           │
│                                                                  │
│  ┌────────────────────────┐  ┌─────────────────────────────────┐ │
│  │ 📖                     │  │ 📅                              │ │
│  │ Verfahrensablauf       │  │ Frist eintragen                 │ │
│  │ informieren            │  │ aufgrund Ereignis               │ │
│  │                        │  │                                 │ │
│  │ Verfahrenstyp wählen   │  │ Ein Ereignis ist eingetreten —  │ │
│  │ und alle dazugehörigen │  │ ich brauche die richtige Frist  │ │
│  │ Fristen auf einer Zeit-│  │ für meine Akte.                 │ │
│  │ leiste sehen.          │  │                                 │ │
│  │                        │  │ → Schritt-für-Schritt           │ │
│  │ → UPC / DE / EPA / DPMA│  │ → Filter / Suche                │ │
│  └────────────────────────┘  └─────────────────────────────────┘ │
│                                                                  │
│  ─────────  oder direkt zu einer Frist springen ──────────       │
│                                                                  │
│  [ Klageerwiderung ] [ Berufung ] [ Einspruch ] [ Replik ] [ … ] │
│                          (chips → Pathway B + B2 prefilled)     │
└──────────────────────────────────────────────────────────────────┘
```

**Default landing:** the fork itself. No automatic forwarding into either pathway — both choices are first-class.

**URL state (sharable, bookmarkable):**

| URL | Surface |
|---|---|
| `/tools/fristenrechner` | Landing fork |
| `/tools/fristenrechner?path=a` | Pathway A (tile grid) |
| `/tools/fristenrechner?path=a&proc=UPC_INF` | Pathway A, proceeding pre-selected (existing behaviour) |
| `/tools/fristenrechner?path=b` | Pathway B, default mode (B1 if user hasn't chosen yet, otherwise the last mode) |
| `/tools/fristenrechner?path=b&mode=tree` | Pathway B, B1 decision tree |
| `/tools/fristenrechner?path=b&mode=tree&b1=cms-eingang.gericht.hinweisbeschluss` | B1 with the path pre-walked to that node |
| `/tools/fristenrechner?path=b&mode=filter` | Pathway B, B2 filter+search |
| `/tools/fristenrechner?path=b&mode=filter&q=klageerwiderung&forum=upc,de_lg` | B2 with query + forum filter pre-applied |

The Phase D quick-pick chips relocate from "above tabs" to "below the fork" with the new framing **"oder direkt zu einer Frist springen"**. Chip click navigates to `?path=b&mode=filter&q=<label>`. They remain a discovery affordance for users who already know what they're looking for and want to skip the choose-a-pathway step.

A persistent **"Klassische Ansicht"** link sits in the page footer for v3.0 — points back to today's tab layout via `?legacy=1`. Removed in Phase E (gate-gated by m).

**Mobile:** stacked single column. Two cards become two full-width buttons; chips wrap.

### 3.2 Pathway A — Verfahrensablauf informieren

Identical to today's wizard:

```
[ ← zurück zur Auswahl ]   ← Pathway A back-link to landing fork

┌──────────────────────────────────────────────────────────────────┐
│ 📖 Verfahrensablauf                                              │
│                                                                  │
│ 1. Verfahrensart wählen                                          │
│    UPC: [VLG] [Nicht] [PI] [Ber.] [Sch.] [Bu.] [Ber-K] [An.]    │
│    DE:  [VLG] [BPatG] [OLG] [BGH-NZB] [BGH-N]                   │
│    EPA: [Einspr.] [Beschw.] [Erteil.]                           │
│    DPMA:[Einspr.] [BPatG-Beschw.] [BGH-RB]                      │
│                                                                  │
│ 2. Datum + Flags                                                 │
│ 3. Ergebnis (Timeline / Spalten)                                 │
└──────────────────────────────────────────────────────────────────┘
```

Functionally unchanged from today — same component, same calculator, same flag checkboxes, same timeline / columns view, same click-to-edit dates, same anchor overrides.

The only delta: the legacy "Was kommt nach…" tab is **not** rendered in Pathway A. Trigger-event entry moved entirely into Pathway B.

### 3.3 Pathway B — Frist eintragen aufgrund Ereignis

```
[ ← zurück zur Auswahl ]

┌──────────────────────────────────────────────────────────────────┐
│ 📅 Frist eintragen                                               │
│                                                                  │
│  Modus:  ◉ Schritt-für-Schritt (Entscheidungsbaum)               │
│          ◯ Filter / Suche                                        │
│                                                                  │
│  ─────────────────────────────────────────────                  │
│                                                                  │
│  [ B1 panel  ] OR [ B2 panel ]                                   │
│                                                                  │
│  ─────────────────────────────────────────────                  │
│                                                                  │
│  Treffer ({count})                                               │
│  ┌───────── Concept Card ─────────┐                              │
│  │ Klageerwiderung · Statement of │                              │
│  │ Defence                        │                              │
│  │ [UPC R.23.1] [DE_INF §276.1]   │                              │
│  │ [BPatG §82.1] [EPA R.79.1]     │                              │
│  │ [DPMA §59.3]                   │                              │
│  └────────────────────────────────┘                              │
│  …                                                               │
└──────────────────────────────────────────────────────────────────┘
```

**Mode toggle** at the top is a 2-radio control, not tabs (tabs imply equal weight; modes imply same-data different-presentation). State persists via `?mode=tree|filter`. Switching modes preserves the underlying filter set when possible (B1's path translates 1:1 to B2 chips; B2 chips that don't correspond to a B1 step show a hint "Filter aktiv: <chip-list>" above the B1 cascade so the user knows context is preserved).

**Result-card area (below the mode panel)** is **shared between B1 and B2** and uses the existing `attachFristenSearch` rendering (Phase D). The cards narrow as the user adds path-steps (B1) or filters (B2).

### 3.4 B1 — Decision Tree (button cascade)

```
┌─────────────────────────────────────────────────────────────┐
│ 1. Was ist passiert?                            [ ↺ Reset ]  │
│                                                             │
│  ┌────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ │
│  │ 📥          │ │ 🎤            │ │ 📊        │ │ 🚫        │ │
│  │ CMS-       │ │ Mündliche    │ │ Beschluss│ │ Frist    │ │
│  │ Eingang    │ │ Verhandlung  │ │ /        │ │ verpasst │ │
│  │            │ │              │ │ Entsch.  │ │          │ │
│  └────────────┘ └──────────────┘ └──────────┘ └──────────┘ │
│  ┌────────────┐                                            │
│  │ ❓ Anderes  │                                            │
│  └────────────┘                                            │
└─────────────────────────────────────────────────────────────┘

(after click 📥 CMS-Eingang)

┌─────────────────────────────────────────────────────────────┐
│ Pfad: 📥 CMS-Eingang ›                       [ ← zurück ]   │
│                                                             │
│ 2. Von wem ist das Schriftstück?                            │
│                                                             │
│  ┌────────────────┐ ┌─────────────────────────┐            │
│  │ ⚖ Vom Gericht │ │ 👥 Von der Gegenseite   │            │
│  └────────────────┘ └─────────────────────────┘            │
└─────────────────────────────────────────────────────────────┘

(after click 👥 Von der Gegenseite)

┌─────────────────────────────────────────────────────────────┐
│ Pfad: 📥 CMS-Eingang › 👥 Gegenseite ›        [ ← zurück ]  │
│                                                             │
│ 3. In welchem Verfahrenstyp?                                │
│                                                             │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ ⚖ UPC INF│ │ ⚖ UPC REV│ │ 🏛 DE LG  │ │ 🏛 BPatG  │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ 🏛 EPA Op │ │ 🏛 EPA App│ │ 🏛 DPMA Op│ │ … weitere  │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
└─────────────────────────────────────────────────────────────┘

(after click ⚖ UPC INF)

┌─────────────────────────────────────────────────────────────┐
│ Pfad: 📥 CMS-Eingang › 👥 Gegenseite › ⚖ UPC INF            │
│                                              [ ← zurück ]   │
│                                                             │
│ 4. Welcher Schriftsatz wurde eingereicht? (optional)        │
│                                                             │
│  ┌──────────────┐ ┌──────────────┐ ┌────────────────┐       │
│  │ Klageschrift │ │ Klageerwid.  │ │ Replik / Duplik│       │
│  └──────────────┘ └──────────────┘ └────────────────┘       │
│  ┌────────────────────────┐ ┌──────────────────────┐        │
│  │ Nichtigkeitswiderklage │ │ Antrag Patentänd.    │        │
│  └────────────────────────┘ └──────────────────────┘        │
│                                                             │
│  ► Alle UPC-INF-Fristen anzeigen (Skip step 4)              │
└─────────────────────────────────────────────────────────────┘

Below the tree at every step:

┌─────────────────────────────────────────────────────────────┐
│ Treffer ({count varies as path narrows})                    │
│                                                             │
│ Step 1 (Was ist passiert? → CMS-Eingang)                    │
│ → ~25 concept cards (everything that follows from a CMS     │
│   receipt: defences, replies, appeals, opposition replies…) │
│                                                             │
│ Step 3 (… → UPC INF)                                        │
│ → ~12 concept cards (UPC_INF deadline tree)                 │
│                                                             │
│ Step 4 (… → Klageschrift)                                   │
│ → 2-3 concept cards: Klageerwiderung (mit/ohne CCR),        │
│   Anzeige Verteidigungsbereitschaft                         │
└─────────────────────────────────────────────────────────────┘
```

**Decision-tree rules (LOCKED):**

- **Depth → unlimited** (m's call). The cascade can grow as the taxonomy reveals deeper structures. Frontend renders one step at a time; depth is a property of `paliad.event_categories` data, never hard-coded in the UI. Most paths terminate at step 3-4 today; future taxonomy edits can extend without code changes.
- **Clickable breadcrumb on every step** — visible when path-depth ≥ 1; each segment jumps back to that level. Optional inline back-arrow per segment.
- **Reset button** on step 1 — restarts the cascade.
- **Cards always visible below the cascade** — empty-set path shows "Keine Treffer für diesen Pfad" with a **"Schritt zurück"** link (one breadcrumb level back) and a secondary "Filter / Suche verwenden →" link to switch to B2.
- **Skip-step link** ("Alle UPC-INF-Fristen anzeigen") at every non-leaf step lets the user finalise early without picking a sub-category. That click selects all sibling buttons' outcomes.
- **URL bookmark** carries the path: `?path=b&mode=tree&b1=cms-eingang.gericht.hinweisbeschluss`. Refresh restores the cascade at that step.
- **Direct deep-link** from the Phase D quick-pick chips: a chip click goes to B2 (filter mode), not B1.

**Cards-at-each-step:** show only the candidates whose concept_id is reachable under the current path. Showing all candidates would defeat the narrowing affordance. Empty-result path → "Schritt zurück" link.

### 3.5 B2 — Filter / Suche (Phase D continued)

```
┌─────────────────────────────────────────────────────────────┐
│  🔍 [ Klageerwiderung, RoP 23, § 82 …       ] [ × ]         │
│                                                             │
│  Filter:                                                    │
│   Verfahrensart  ▾  Partei  ▾  Rechtsquelle  ▾              │
│   Gericht/System ▾                                          │
│                                                             │
│  Aktiv: [✕ Klageerwiderung] [✕ UPC] [✕ DE LG] [Alle löschen]│
└─────────────────────────────────────────────────────────────┘

(below: concept-card list, narrowing as filters AND together)
```

**Filter dimensions:**

| Filter | Multi-select? | Source |
|---|---|---|
| Free-text query | n/a | matview pg_trgm |
| Verfahrensart | yes | `paliad.proceeding_types.code` (8 UPC + 5 DE + 3 EPA + 3 DPMA) |
| Partei | yes | `effective_party` (claimant / defendant / both / court) |
| Rechtsquelle | yes | `legal_source` LIKE prefix (UPC.RoP / EU.EPÜ / EU.EPC-R / DE.ZPO / DE.PatG / EU.RPBA) |
| **Gericht/System** | yes (NEW) | derived: maps proceeding_type → forum bucket |

**Forum buckets — 10 (LOCKED, m + Maria 2026-05-05):**

| # | Forum | Maps to proceeding_type_codes | When user picks |
|---|---|---|---|
| 1 | ⚖ UPC CFI | UPC_INF, UPC_REV, UPC_PI, UPC_DAMAGES, UPC_DISCOVERY, UPC_APP_ORDERS | "I'm at the UPC Court of First Instance (LD or CD — rules identical)" |
| 2 | ⚖ UPC CoA | UPC_APP, UPC_COST_APPEAL | "Berufung beim UPC Berufungsgericht" |
| 3 | 🏛 DE LG | DE_INF | "Verletzungsklage am Landgericht (1. Instanz)" |
| 4 | 🏛 DE OLG | DE_INF_OLG | "Berufung am OLG" |
| 5 | 🏛 DE BGH | DE_INF_BGH, DE_NULL_BGH, DPMA_BGH_RB | "Revision/NZB / Patentnichtigkeit-Berufung / DPMA-Rechtsbeschwerde am BGH" |
| 6 | 🏛 DE BPatG | DE_NULL, DPMA_BPATG_BESCHWERDE | "Nichtigkeit am BPatG / Beschwerde gegen DPMA-Entscheidung" |
| 7 | 🏛 EPA Erteilung | EP_GRANT | "EPA-Erteilungsverfahren (Prüfungsabteilung)" |
| 8 | 🏛 EPA Einspruchsabt. | EPA_OPP | "EPA-Einspruchsverfahren" |
| 9 | 🏛 EPA Beschwerdek. | EPA_APP | "EPA-Beschwerdeverfahren (Beschwerdekammer)" |
| 10 | 🏛 DPMA | DPMA_OPP | "DPMA-Einspruchsverfahren" |

**Cross-cutting concepts** (Wiedereinsetzung, Versäumnisurteil-Einspruch, Schriftsatznachreichung, Weiterbehandlung) — these surface as trigger-event pills regardless of forum filter (they apply across forums by design). Forum filter narrows only the rule pills inside each card.

**Forum filter implementation:**

- Backend: `GET /api/tools/fristenrechner/search` gains a `?forum=` query param (comma-separated forum slugs). Service translates slugs to proceeding_type codes via a hard-coded forum→proceeding_codes map.
- Matview itself doesn't need a forum column — proceeding_type_code is sufficient and the map lives in Go.
- Forum filter applies as `proceeding_code = ANY($forum_codes)` in the query. Trigger pills (`kind='trigger'`, `proceeding_code IS NULL`) bypass the forum filter (they always show).

**AND-narrowing semantics (m's primary ask):**

```
ALL filters AND together. Adding a filter never INCREASES results.
Removing a filter never DECREASES results.
```

Concrete: query "Klageerwiderung" + Verfahrensart=[UPC_INF] + Forum=[UPC] returns the UPC_INF Klageerwiderung pill only. Adding Forum=[DE LG] (multi-select) UNIONS within forum but keeps the AND with Verfahrensart, which then **becomes empty** (UPC_INF is not in [DE LG]). The UI shows this clearly: "0 Treffer für aktive Filter — letzten Filter entfernen" with a one-click undo.

**Filter chip pills** display above the result list, X-removable, follow the existing `.akten-event-type-pill` shape from t-paliad-088.

**Default state:** no filters selected. As soon as a user types or clicks a chip / dropdown, the filters apply. Empty filter state on B2 = the full unified concept-card list (paginated; show first 20).

### 3.6 B1 ↔ B2 state sharing

The decision tree is **a guided way to construct a filter combination**. Each B1 button click ≈ adds a filter chip in B2 land:

| B1 step click | B2 filter equivalent |
|---|---|
| 📥 CMS-Eingang | (event_category filter set; no direct B2 chip) |
| › Vom Gericht | (event_category narrowing) |
| › Hinweisbeschluss | (event_category narrowing → 1 concept) |
| › UPC INF | Verfahrensart=[UPC_INF] |
| › Klageschrift | (event_category narrowing → maps to concept slug `statement-of-defence`) |

When the user toggles from B1 → B2: any equivalent B2 filters are pre-set. `event_category` filters that don't have a direct B2 equivalent (e.g. "Vom Gericht" is a navigation node, not a filter property) get baked into the result-card prefilter so the cards remain narrowed; the chip representation shows them as `Pfad: 📥 CMS-Eingang › 👥 Gegenseite ✕`.

When the user toggles from B2 → B1: B2 filters that DO map onto B1 steps (e.g. Verfahrensart=[UPC_INF] = step-3 = ⚖ UPC INF) automatically advance the cascade to that step. Any B2 filters that don't map (free-text query, party filter) display as a banner above the cascade ("Aktive Filter: 'klageerwiderung', Partei=Beklagte").

The two views are **two presentations of one filter state**.

---

## 4. Data model

### 4.1 New tables (Phase A migration)

```sql
-- The decision-tree taxonomy.
CREATE TABLE paliad.event_categories (
    id                uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    parent_id         uuid REFERENCES paliad.event_categories(id) ON DELETE CASCADE,
    slug              text NOT NULL UNIQUE,
        -- e.g. 'cms-eingang', 'cms-eingang.gericht',
        --      'cms-eingang.gericht.hinweisbeschluss'
        -- Slug is the materialised path-with-dots.
    label_de          text NOT NULL,
    label_en          text NOT NULL,
    description_de    text,
        -- Optional 1-line gloss shown under the button.
    description_en    text,
    step_question_de  text,
        -- The question this node's CHILDREN answer. Rendered above the
        -- button row at this level. NULL on leaves.
    step_question_en  text,
    icon              text,
        -- Single emoji or icon slug for the button face. Optional.
    sort_order        int  NOT NULL DEFAULT 100,
    is_leaf           bool NOT NULL DEFAULT false,
        -- Leaf nodes get their concept outcomes via the junction table
        -- below. Non-leaves are pure navigation.
    is_active         bool NOT NULL DEFAULT true,
    created_at        timestamptz NOT NULL DEFAULT now(),
    updated_at        timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX event_categories_parent_id ON paliad.event_categories (parent_id);
CREATE INDEX event_categories_slug      ON paliad.event_categories (slug);

-- Leaf → concept(s) mapping. Many-to-many.
-- A leaf can produce N candidate concepts. A concept can be reached via
-- multiple leaves (e.g. Klageerwiderung is reachable from "CMS-Eingang
-- › Gegenseite › Klageschrift" AND from a hypothetical "Was muss ich
-- nach Klage einreichen?" path).
CREATE TABLE paliad.event_category_concepts (
    id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    event_category_id   uuid NOT NULL REFERENCES paliad.event_categories(id) ON DELETE CASCADE,
    concept_id          uuid NOT NULL REFERENCES paliad.deadline_concepts(id) ON DELETE CASCADE,
    proceeding_type_code text,
        -- NULL = all contexts of the concept apply (concept-card shows
        --        all its pills).
        -- Set  = limit the card's pill set to this proceeding (e.g.
        --        leaf 'cms-eingang.gericht.hinweisbeschluss' → concept
        --        'response-to-preliminary-opinion' with proceeding_code
        --        = 'DE_NULL'  shows only the BPatG pill, not all
        --        court-systems where Hinweisbeschluss exists).
    sort_order          int NOT NULL DEFAULT 100,
    UNIQUE (event_category_id, concept_id, proceeding_type_code)
);

CREATE INDEX event_category_concepts_category ON paliad.event_category_concepts (event_category_id);
CREATE INDEX event_category_concepts_concept  ON paliad.event_category_concepts (concept_id);
```

**Why a separate junction with `proceeding_type_code`?** A leaf like "Beschluss/Entscheidung › Urteil DE_INF (LG)" → `notice-of-appeal` concept should narrow to the **DE_INF context** of that concept, not show the UPC and EPA pills. The junction's nullable `proceeding_type_code` carries that filter at the leaf-outcome level without polluting the deadline_concepts table.

### 4.2 Why not category_path text[] on existing tables?

Considered alternative: drop the new tables, add `category_path text[]` on `paliad.deadline_concepts` (and on `paliad.trigger_events.concept_id`-linked rows). Each concept declares which decision-tree breadcrumbs lead to it.

**Rejected because:**

1. **Internal navigation nodes have no concept.** "Vom Gericht" / "Von der Gegenseite" are pure step labels with no Frist outcome. A category_path on concepts can't represent them as first-class nodes; the UI would have to derive the tree shape from concept paths' shared prefixes, which is fragile when one prefix has zero concepts (e.g. step "Was war es?" with one button "Verlegt" leading to no Frist).

2. **Step questions are tree properties, not concept properties.** "Was ist passiert?" / "Von wem?" / "In welchem Verfahrenstyp?" naturally live as `step_question_de` on the parent node. Storing them on concepts means duplicating across all leaves under that node.

3. **Context-conditional outcomes.** A junction with nullable `proceeding_type_code` lets the same concept produce different pill sets at different leaves. category_path on concepts can't express that without a parallel junction anyway.

4. **Taxonomy evolves independently of concepts.** The concept layer reflects what the law says (Klageerwiderung exists in ZPO/PatG/RoP/EPC). The taxonomy reflects how lawyers think about events — it can be refined without touching concept data.

The two-table approach is +2 tables of additive schema; the category_path approach is 1 column but pushes complexity into the application layer. Two tables wins.

### 4.3 Seed taxonomy (Phase A)

The taxonomy data lives in migration 048_event_categories.up.sql. Inventor's draft seed (each block = one parent + its children + leaf→concept links). Numbers in `()` show approximate concept reach at that step.

```
ROOT — "Was ist passiert?"  (~57 concepts visible if no narrowing)
│
├── 📥 cms-eingang  "CMS-Eingang (Schriftstück erhalten)"
│   step_question: "Von wem ist das Schriftstück?"
│   │
│   ├── ⚖ cms-eingang.gericht  "Vom Gericht"
│   │   step_question: "Welcher Charakter hat das Schriftstück?"
│   │   │
│   │   ├── 📋 cms-eingang.gericht.hinweisbeschluss  [LEAF]
│   │   │   "Hinweisbeschluss / Vorläufige Würdigung"
│   │   │   → response-to-preliminary-opinion (DE_NULL)
│   │   │   → response-to-preliminary-opinion (DE_INF)
│   │   │
│   │   ├── 📅 cms-eingang.gericht.ladung  [LEAF]
│   │   │   "Ladung zur mündlichen Verhandlung"
│   │   │   → r116-final-submissions (EPA_OPP, EPA_APP)
│   │   │   → schriftsatznachreichung (deferred — actual Frist nach Verhandlung)
│   │   │   (no Frist set by Ladung itself; surfaces hint to user)
│   │   │
│   │   ├── 📨 cms-eingang.gericht.bescheid-mit-frist  [LEAF]
│   │   │   "Bescheid mit explizit gesetzter Frist"
│   │   │   step continues: "Frist eintragen — kein Berechnungsbedarf"
│   │   │   → meta-card: "Frist direkt eintragen — Datum aus Bescheid"
│   │   │
│   │   ├── 📨 cms-eingang.gericht.maengelbescheid  [LEAF]
│   │   │   "Mitteilung der Mängelbeseitigung"
│   │   │   → cross-cutting trigger_events 71/74/80 (Mängelbeseitigung)
│   │   │
│   │   ├── 🏛 cms-eingang.gericht.endentscheidung  "Endentscheidung / Urteil"
│   │   │   step_question: "Welche Instanz / welches Verfahren?"
│   │   │   │
│   │   │   ├── ⚖ …urteil-de-inf  "Urteil LG (Verletzung)"  [LEAF]
│   │   │   │   → notice-of-appeal (DE_INF_OLG)
│   │   │   │   → statement-of-grounds-of-appeal (DE_INF_OLG)
│   │   │   │
│   │   │   ├── ⚖ …urteil-de-inf-olg  "Urteil OLG (Verletzung)"  [LEAF]
│   │   │   │   → nichtzulassungsbeschwerde (DE_INF_BGH)
│   │   │   │   → revisionsfrist (DE_INF_BGH)
│   │   │   │   → revisionsbegruendung (DE_INF_BGH)
│   │   │   │
│   │   │   ├── ⚖ …urteil-de-null  "Urteil BPatG (Nichtigkeit)"  [LEAF]
│   │   │   │   → notice-of-appeal (DE_NULL_BGH)
│   │   │   │   → statement-of-grounds-of-appeal (DE_NULL_BGH)
│   │   │   │
│   │   │   ├── ⚖ …urteil-upc-cfi  "Sachentscheidung UPC (CFI)"  [LEAF]
│   │   │   │   → notice-of-appeal (UPC_APP)
│   │   │   │   → statement-of-grounds-of-appeal (UPC_APP)
│   │   │   │
│   │   │   ├── ⚖ …urteil-upc-coa  "Sachentscheidung UPC (CoA)"  [LEAF]
│   │   │   │   → petition-for-review (UPC) [if exists]
│   │   │   │
│   │   │   ├── ⚖ …entscheidung-epa-opp  "Einspruchsentscheidung EPA"  [LEAF]
│   │   │   │   → notice-of-appeal (EPA_APP)
│   │   │   │   → statement-of-grounds-of-appeal (EPA_APP)
│   │   │   │
│   │   │   ├── ⚖ …entscheidung-epa-boa  "Beschwerdeentscheidung EPA"  [LEAF]
│   │   │   │   → petition-for-review (EPA Art.112a)
│   │   │   │
│   │   │   ├── ⚖ …entscheidung-dpma  "DPMA-Entscheidung"  [LEAF]
│   │   │   │   → notice-of-appeal (DPMA_BPATG_BESCHWERDE)
│   │   │   │
│   │   │   ├── ⚖ …beschluss-bpatg-beschwerde  "Beschluss BPatG (DPMA-Beschwerde)"  [LEAF]
│   │   │   │   → rechtsbeschwerde (DPMA_BGH_RB)
│   │   │   │
│   │   │   └── 📊 …versaeumnisurteil  "Versäumnisurteil (DE)"  [LEAF]
│   │   │       → versaeumnisurteil-einspruch (cross-cutting trigger)
│   │   │
│   │   ├── 💰 cms-eingang.gericht.kostenfestsetzung  [LEAF]
│   │   │   "Kostenfestsetzungsbeschluss"
│   │   │   → notice-of-appeal (UPC_COST_APPEAL) — UPC R.157
│   │   │
│   │   ├── 🚫 cms-eingang.gericht.rechtsverlust-epa  [LEAF]
│   │   │   "Mitteilung über Rechtsverlust (EPA)"
│   │   │   → weiterbehandlung (cross-cutting)
│   │   │   → wiedereinsetzung (cross-cutting EU.EPÜ.122)
│   │   │
│   │   └── 📌 cms-eingang.gericht.anordnung  [LEAF]
│   │       "Anordnung (Order, z.B. PI, Beweissicherung)"
│   │       → application-for-the-review-of-a-case-management-order
│   │
│   └── 👥 cms-eingang.gegenseite  "Von der Gegenseite (Schriftsatz)"
│       step_question: "In welchem Verfahrenstyp?"
│       │
│       ├── ⚖ cms-eingang.gegenseite.upc-inf
│       │   step_question: "Welcher Schriftsatz wurde eingereicht?"
│       │   │
│       │   ├── 📜 …klageschrift  [LEAF]
│       │   │   "Klageschrift"
│       │   │   → notice-of-defence-intention (UPC_INF) [does not apply at UPC]
│       │   │   → statement-of-defence (UPC_INF)  ← R.23
│       │   │   → statement-of-defence (UPC_INF) — variants with CCR flag visible on the card
│       │   │
│       │   ├── 🔄 …klageerwiderung-mit-ccr  [LEAF]
│       │   │   "Klageerwiderung MIT Nichtigkeitswiderklage"
│       │   │   → defence-to-counterclaim-for-revocation (UPC_INF)
│       │   │   → application-to-amend (UPC_INF) [optional]
│       │   │   → reply-to-defence (UPC_INF)
│       │   │
│       │   ├── 🔄 …klageerwiderung-ohne-ccr  [LEAF]
│       │   │   "Klageerwiderung OHNE Nichtigkeitswiderklage"
│       │   │   → reply-to-defence (UPC_INF)
│       │   │
│       │   ├── ↩ …replik  [LEAF]
│       │   │   "Replik des Gegners"
│       │   │   → rejoinder (UPC_INF)
│       │   │
│       │   ├── 🔁 …antrag-patentaenderung  [LEAF]
│       │   │   "Antrag auf Patentänderung (R.30)"
│       │   │   → defence-to-application-to-amend (UPC_INF)
│       │   │   → reply-to-defence-to-application-to-amend (UPC_INF)
│       │   │
│       │   ├── 🔁 …nichtigkeitswiderklage  [LEAF]
│       │   │   "Nichtigkeitswiderklage (R.25)"
│       │   │   → defence-to-counterclaim-for-revocation (UPC_INF)
│       │   │
│       │   └── 📈 …berufungsschrift  [LEAF]
│       │       "Berufungsschrift Gegner"
│       │       → response-to-appeal (UPC_APP)
│       │       → cross-appeal (UPC_APP)
│       │
│       ├── ⚖ cms-eingang.gegenseite.upc-rev
│       │   step_question: "Welcher Schriftsatz?"
│       │   ├── 📜 …nichtigkeitsklage  [LEAF]
│       │   │   "Statement for Revocation"
│       │   │   → defence-to-revocation (UPC_REV)
│       │   ├── 🔄 …defence-to-revocation  [LEAF]
│       │   │   "Defence to revocation (mit/ohne Amend, mit/ohne CCI)"
│       │   │   → reply-to-defence (UPC_REV)
│       │   │   → defence-to-application-to-amend (UPC_REV)  [if amend]
│       │   │   → defence-to-counterclaim-for-infringement (UPC_REV)  [if CCI]
│       │   └── …
│       │
│       ├── 🏛 cms-eingang.gegenseite.de-inf  "DE Verletzungsklage (LG)"
│       │   step_question: "Welcher Schriftsatz?"
│       │   ├── 📜 …klageschrift-de  [LEAF]
│       │   │   → notice-of-defence-intention (DE_INF) — ZPO §276(1)
│       │   │   → statement-of-defence (DE_INF) — ZPO §276(1)
│       │   ├── 🔄 …klageerwiderung-de  [LEAF]
│       │   │   → reply-to-defence (DE_INF) — ZPO §282
│       │   ├── 📈 …berufungsschrift-de  [LEAF]
│       │   │   → response-to-appeal (DE_INF_OLG) — ZPO §521
│       │   └── …
│       │
│       ├── 🏛 cms-eingang.gegenseite.de-null  "DE Nichtigkeitsklage (BPatG)"
│       │   step_question: "Welcher Schriftsatz?"
│       │   ├── 📜 …nichtigkeitsklage-bpatg  [LEAF]
│       │   │   → statement-of-defence (DE_NULL) — PatG §82
│       │   ├── 🔄 …klageerwiderung-bpatg  [LEAF]
│       │   │   → reply-to-defence (DE_NULL) — PatG §83
│       │   └── …
│       │
│       ├── 🏛 cms-eingang.gegenseite.epa-opp  "EPA Einspruch"
│       │   ├── 📜 …einspruchsschrift  [LEAF]
│       │   │   → statement-of-defence (EPA_OPP) — EPC R.79
│       │   └── …
│       │
│       ├── 🏛 cms-eingang.gegenseite.epa-app  "EPA Beschwerde"
│       │   ├── 📜 …beschwerdeschrift  [LEAF]
│       │   │   → response-to-appeal (EPA_APP) — RPBA Art.12
│       │   └── …
│       │
│       └── 🏛 cms-eingang.gegenseite.dpma-opp  "DPMA Einspruch"
│           └── 📜 …einspruchsschrift-dpma  [LEAF]
│               → statement-of-defence (DPMA_OPP) — PatG §59(3)
│
├── 🎤 muendl-verhandlung  "Mündliche Verhandlung"
│   step_question: "Was ist mit der Verhandlung passiert?"
│   ├── 📅 …geladen  [LEAF]
│   │   "Geladen — wann findet sie statt?"
│   │   → r116-final-submissions (EPA_OPP) — EPC R.116
│   │   → r116-final-submissions (EPA_APP)
│   │   → schriftsatznachreichung (DE_INF) — ZPO §296a [tentative]
│   ├── ⌛ …gehalten  [LEAF]
│   │   "Soeben gehalten / heute"
│   │   → schriftsatznachreichung (DE_INF) — ZPO §296a (3 Wochen)
│   └── 🔁 …verlegt  [LEAF]
│       "Verlegt"
│       (no Frist — show informational card)
│
├── 📊 beschluss-entscheidung  "Beschluss / Entscheidung erhalten"
│   step_question: "Welche Art von Entscheidung?"
│   │
│   (Same children as cms-eingang.gericht.endentscheidung — explicit
│    redundancy because users may mentally classify as "Entscheidung"
│    rather than "CMS-Eingang vom Gericht". The leaves point at the
│    same concept outcomes via the junction; no data duplication beyond
│    the navigation node.)
│
├── 🚫 frist-verpasst  "Frist verpasst"
│   step_question: "In welchem System?"
│   ├── 🏛 …de-patg  [LEAF]
│   │   "DE Patentverfahren (PatG §123)"
│   │   → wiedereinsetzung (cross-cutting, trigger 200)
│   ├── 🏛 …de-zpo  [LEAF]
│   │   "DE Zivilverfahren (ZPO §233 — 2 Wochen!)"
│   │   → wiedereinsetzung (cross-cutting, trigger 201)
│   ├── 🏛 …epa  [LEAF]
│   │   "EPA (Art. 122 EPÜ)"
│   │   → wiedereinsetzung (cross-cutting, trigger 202)
│   │   → weiterbehandlung (cross-cutting, trigger 206)
│   └── 🏛 …dpma  [LEAF]
│       "DPMA (PatG §123)"
│       → wiedereinsetzung (cross-cutting, trigger 203)
│
└── ❓ sonstiges  "Anderes / Sonstiges"
    "Filter / Suche verwenden →" (link to switch to B2)
    (no children; or thin leaf set for high-frequency oddities like
    Schutzschrift-Erneuerung if telemetry shows people getting stuck
    here)
```

**Numbers:** ≈ 6 root buckets × 2-5 mid-level × 1-10 leaves ≈ 50-70 leaf nodes. Each leaf has 1-4 concept outcomes via the junction. Total: ≈ 50-70 leaves × ~2 outcomes = ≈ 120 junction rows. Manageable seed; spot-checkable on a single PR review.

**Coverage check** vs concept count: 57 concepts in `paliad.deadline_concepts`. The taxonomy reaches every "submission" and "decision" concept. Pure-administrative concepts that are unlikely B-pathway candidates (`grant`, `publication`, `search-report`, `request-for-examination`, `filing`, `approval-and-translation`) are intentionally not surfaced from the decision tree — they're more naturally discovered via Pathway A (browse `EP_GRANT` proceeding type).

### 4.4 Coverage check: every concept reachable somehow?

Either via decision-tree leaf (B1), or via Pathway A tile (proceeding tree contains the rule), or via B2 free-text search. Inventor commitment: every concept in `paliad.deadline_concepts` is reachable from at least one of the three. The seed migration includes a CHECK query at the end:

```sql
-- After event_category_concepts is seeded, every concept that's
-- "user-actionable" (category in {submission, order, decision, hearing})
-- should be reachable from at least one event_categories leaf.
-- Pure-administrative concepts (filing, grant, publication, search-report,
-- request-for-examination, approval-and-translation) are exempt — they
-- live on Pathway A only.
DO $$
DECLARE
    unreachable_count int;
BEGIN
    SELECT count(*) INTO unreachable_count
    FROM paliad.deadline_concepts dc
    WHERE dc.is_active
      AND dc.category IN ('submission', 'order', 'decision', 'hearing')
      AND dc.slug NOT IN (
          'filing', 'grant', 'publication', 'search-report',
          'request-for-examination', 'approval-and-translation',
          'communication-r71-3'
      )
      AND NOT EXISTS (
          SELECT 1 FROM paliad.event_category_concepts ecc
          WHERE ecc.concept_id = dc.id
      );
    IF unreachable_count > 0 THEN
        RAISE EXCEPTION 'Phase A seed: % concepts have no event_categories leaf', unreachable_count;
    END IF;
END $$;
```

This is the seed-correctness gate. Migration fails to apply if a user-actionable concept is orphaned from the taxonomy.

### 4.5 No matview rebuild

The existing `paliad.deadline_search` matview stays untouched — it indexes concept × rule and concept × trigger. The decision tree adds another **filter** dimension; it doesn't change what's searchable.

The B1 backend reads:

```sql
-- Given a path like 'cms-eingang.gericht.hinweisbeschluss', return concepts.
SELECT DISTINCT ecc.concept_id, ecc.proceeding_type_code
FROM paliad.event_categories ec
JOIN paliad.event_category_concepts ecc
  ON ecc.event_category_id = ec.id
WHERE ec.slug = $1
   OR ec.slug LIKE $1 || '.%'  -- include descendants
   AND ec.is_active;
```

Then re-filters the existing matview:

```sql
SELECT * FROM paliad.deadline_search ds
WHERE ds.concept_id = ANY($concept_ids)
  AND (
      $proceeding_type_codes IS NULL
      OR ds.proceeding_code = ANY($proceeding_type_codes)
      OR ds.kind = 'trigger'  -- triggers always pass forum filter
  );
```

Two queries, both indexed, total < 50 ms.

---

## 5. Backend changes

### 5.1 New endpoint: GET /api/tools/fristenrechner/event-categories

Returns the entire event_categories tree (small — ≈ 80 rows). Cached in Go service for the process lifetime, refreshed only on migration apply (same pattern as `paliad.deadline_search` mat-view refresh).

```json
GET /api/tools/fristenrechner/event-categories

200:
{
  "tree": [
    {
      "id": "...",
      "slug": "cms-eingang",
      "label_de": "CMS-Eingang",
      "label_en": "CMS Receipt",
      "icon": "📥",
      "step_question_de": "Von wem ist das Schriftstück?",
      "is_leaf": false,
      "sort_order": 100,
      "children": [
        {
          "id": "...",
          "slug": "cms-eingang.gericht",
          "label_de": "Vom Gericht",
          "icon": "⚖",
          "step_question_de": "Welcher Charakter hat das Schriftstück?",
          "is_leaf": false,
          "children": [...]
        },
        ...
      ]
    },
    ...
  ]
}
```

Frontend caches via `localStorage` (versioned by `ETag` from response header). Tree static enough that one fetch per user-month is sufficient.

### 5.2 Extended endpoint: GET /api/tools/fristenrechner/search

Existing endpoint (Phase D) gains two new query params:

| Param | Type | Effect |
|---|---|---|
| `event_category_slug` | string | Restricts results to concepts reachable from that node (or any descendant). Implements B1 narrowing. |
| `forum` | comma-separated string | Restricts pill set to proceeding_type_codes that map to the listed forums. Implements B2 forum filter. |

Existing params (`q`, `proc`, `party`, `source`, `limit`) work identically.

```
GET /api/tools/fristenrechner/search
    ?event_category_slug=cms-eingang.gericht.hinweisbeschluss
    &forum=de_lg,de_bpatg
    &q=Stellungnahme
    &party=defendant
```

Back-compat: missing params behave as today's Phase D. New params layer on top.

### 5.3 Forum-bucket map (Go-side, 10 buckets — LOCKED)

```go
// internal/services/fristen_search.go (extend existing)
var forumToProceedingCodes = map[string][]string{
    "upc_cfi":  {"UPC_INF", "UPC_REV", "UPC_PI", "UPC_DAMAGES",
                 "UPC_DISCOVERY", "UPC_APP_ORDERS"},
    "upc_coa":  {"UPC_APP", "UPC_COST_APPEAL"},
    "de_lg":    {"DE_INF"},
    "de_olg":   {"DE_INF_OLG"},
    "de_bgh":   {"DE_INF_BGH", "DE_NULL_BGH", "DPMA_BGH_RB"},
    "de_bpatg": {"DE_NULL", "DPMA_BPATG_BESCHWERDE"},
    "epa_grant":  {"EP_GRANT"},
    "epa_opp":    {"EPA_OPP"},
    "epa_appeal": {"EPA_APP"},
    "dpma":       {"DPMA_OPP"},
}
```

Map lives in Go (not in DB) because the buckets are a presentation choice, not a data property. Adding a forum bucket = code change, not migration.

If telemetry later shows users want finer or coarser buckets, the map updates. The matview and seed don't change.

---

## 6. Frontend layout

### 6.1 File-level changes

| File | Change |
|---|---|
| `frontend/src/fristenrechner.tsx` | Reshape: top of page becomes the landing-fork component; Pathway A and Pathway B render as conditional sub-trees of the same page. The two existing tabs (`mode-procedure-tab` / `mode-event-tab`) MOVE — Verfahrensablauf becomes Pathway A's content; "Was kommt nach…" gets folded into Pathway B's B1+B2 (the trigger-event picker resurfaces as a special leaf in the B1 taxonomy: any leaf that points to a trigger_event id surfaces "Was kommt nach…" calculator inline). |
| `frontend/src/client/fristenrechner.ts` | Add landing-fork toggle handler; URL-state parse for `?path=` + `?mode=`. Existing wizard logic stays. |
| `frontend/src/client/fristenrechner-search.ts` (Phase D) | Reused as B2 engine. Move from "page-level top" to "Pathway B / B2 panel". |
| `frontend/src/client/fristenrechner-tree.ts` (NEW, ~400 lines) | B1 decision-tree component. Renders the cascade from `/api/tools/fristenrechner/event-categories`, manages step state, dispatches narrow-events that the shared concept-card list listens to. |
| `frontend/src/styles/global.css` | New rules: `.fristen-pathway-fork`, `.fristen-pathway-card`, `.fristen-tree-cascade`, `.fristen-tree-step`, `.fristen-tree-button`, `.fristen-tree-breadcrumb`, `.fristen-mode-toggle`, `.fristen-forum-filter`, `.fristen-forum-chip`, `.fristen-forum-chip--active`. |
| `frontend/src/client/i18n.ts` | ~25 new keys: `deadlines.pathway.*` (a-fork, b-fork, mode toggle), `deadlines.tree.*` (step labels, back, reset, breadcrumb), `deadlines.filter.forum.*` (chip labels). |

### 6.2 No new bundle

The existing `frontend/build.ts` already produces `fristenrechner.js` from `frontend/src/client/fristenrechner.ts` — adding new modules grows that bundle (~600 lines for `fristenrechner-tree.ts`). No new entry point.

### 6.3 Drill-in semantics from concept-card pills (unchanged from Phase D)

Pill click on a result card → navigates to:
- `?path=a&proc=UPC_INF&focus=inf.sod` for proceeding rule pills (Pathway A)
- (special) trigger-event pills currently land on the legacy "Was kommt nach…" tab. v3 removes that tab. New behaviour: trigger-event pills open a small inline calculator panel within the result card itself ("Trigger eingetreten am: ___ → Folgefristen"). No page navigation; the user stays in B1/B2 context.

The inline trigger-event calculator panel is a Phase C deliverable. Until shipped, the trigger-event pill click temporarily routes to `?path=a&trigger=<id>` — Pathway A grows a thin "trigger-event mode" alongside the proceeding-tree mode, equivalent to what the legacy "Was kommt nach…" tab did.

---

## 7. Phasing

### Phase A — Data: event_categories + seed taxonomy (1 PR)

- Migration 048: `paliad.event_categories` + `paliad.event_category_concepts` schemas.
- Migration 049: seed taxonomy (≈ 70 leaves, ≈ 120 junction rows). Inline `DO $$` block enforces every user-actionable concept is reachable.
- Service layer: `EventCategoryService` (List, GetTree, GetConceptIDsForSlug-with-descendants).
- Handler: `GET /api/tools/fristenrechner/event-categories` (returns nested JSON; cached).
- Tests: golden tree-shape snapshot; concept-coverage assertion.

No UX change. Backend extension only. Spot-checkable as a single PR.

### Phase B — Landing fork UI (1 PR)

- Reshape `frontend/src/fristenrechner.tsx` to render the landing fork at the top, conditional Pathway A / Pathway B sub-trees below.
- `?path=` URL state, default landing = fork.
- Pathway A wraps the existing Verfahrensablauf wizard verbatim (no logic change).
- Pathway B shell with mode toggle (B1 / B2 radio) but B1/B2 panels are stubs.
- Quick-pick chips relocate from above-tabs to below-fork; chip click → `?path=b&mode=filter&q=<label>`.
- Legacy tabs hidden behind `?legacy=1` for parity testing during transition.

### Phase C — B1 decision tree (1 PR)

- `frontend/src/client/fristenrechner-tree.ts`: cascade renderer + step state machine + back/reset.
- Wire to `GET /api/tools/fristenrechner/search?event_category_slug=…` (extends Phase D handler).
- Result-card area shared with B2 — tree-narrow events update the same card list.
- URL state: `?b1=cms-eingang.gericht.hinweisbeschluss` round-trips.
- Empty-path leaf shows "Frist eintragen — kein Berechnungsbedarf" meta-card.
- Trigger-event leaves (cross-cutting Wiedereinsetzung etc.) open inline calculator panel.

### Phase D — B2 filter expansion (1 PR)

- Add `?forum=` query param + Go-side bucket map.
- Frontend forum-chip filter row (multi-select chips with X-removable active-filter pills).
- AND-narrowing semantics correctness check (Playwright golden tests with combinations).
- 0-result UX with "letzten Filter entfernen" undo.
- B1↔B2 state transitions: switching between modes preserves filters where mappable.

### Phase E — Cleanup (gate-gated, deferred)

- Retire `mode-procedure-tab` / `mode-event-tab` legacy DOM (drop ~200 lines of frontend code).
- Drop the `?legacy=1` parity escape hatch.
- Drop "Klassische Ansicht" footer link.
- Coder shift only after m's go-direction; default = keep legacy until telemetry says no one uses it.

**Each phase is independently shippable** — A is purely data, B is the structural change without B1/B2 logic, C and D are the substantive Pathway-B implementations.

---

## 8. Out of scope (deliberate)

- **AI-driven event detection from court PDFs** (Phase H from v2; deferred).
- **Per-user / firm-level event_categories overrides.** v3 is curated-only; the taxonomy is a maintained corpus, not user-editable.
- **Dynamic decision-tree edges.** No "if X then offer Y" rules at the data-driven level — sub-buttons are static children of the parent. If a future leaf needs conditional offering, model it as a deeper level rather than a runtime rule.
- **Event taxonomy as a reusable axis on `paliad.deadlines`.** The decision tree informs Frist-creation but doesn't tag the resulting Frist with its event_category. (That bridge could be built later — t-paliad-088's event_types is the natural place to merge taxonomies.)
- **Mobile-first redesign of the proceeding tile grid.** Stays as today.
- **Forum filter beyond DE/UPC/EPA/DPMA.** No CFI-vs-CoA-vs-LD breakdown for UPC; rules are shared at that level. If telemetry says LD-vs-CD matters, expand the bucket map.

---

## 9. Risks & mitigations

| Risk | Severity | Mitigation |
|---|---|---|
| Decision-tree taxonomy gets stale as concept layer evolves | Medium | Phase A migration ends with a `DO $$` orphan-check; future concept additions that don't add an event_category leaf fail the migration |
| Seed taxonomy mis-classifies an event (wrong concept outcomes) | Medium | Each leaf surface ≤ 4 concepts; spot-checkable on PR; m or a colleague reviews at concept-coverage matrix in the PR description |
| B1 cascade feels claustrophobic vs B2's open search | Low | "Filter / Suche verwenden →" link visible at every step; "Skip step" affordance lets users finalise early; B1↔B2 mid-flow toggle preserves state |
| Forum filter buckets diverge from user mental model | Medium | Bucket map lives in Go (not DB); rebucket = code change; first-cut buckets matched to existing proceeding_types so coverage is testable |
| Legacy users bookmarked `/tools/fristenrechner?proc=…` URLs from the old layout | Low | URL params preserved — `?proc=…` auto-redirects to `?path=a&proc=…`; same handler reads them |
| Event_categories table grows large and joins get expensive | Low | Mat-view stays primary search engine; event_categories is a join filter, ≤ 100 rows total, fits L1 cache |
| Trigger-event pill drill-in regresses while Phase C inline-calculator unfinished | Medium | Phase B ships the `?path=a&trigger=<id>` shim that mirrors the legacy "Was kommt nach…" tab — pill click works end-to-end before Phase C lands its inline calculator |
| Seeding 120 junction rows is error-prone | Medium | Inline orphan-check + Playwright golden test ("click CMS-Eingang › Gericht › Hinweisbeschluss → see exactly these 2 cards") pins the tree shape |
| Existing `?q=…` URLs from Phase D cease to work | Low | `?q=` in absence of `?path=` redirects to `?path=b&mode=filter&q=…`; same backend |

---

## 10. Locked decisions (m, 2026-05-05 10:33)

m approved all 12 questions in one batch. Locked spec for the implementation:

1. **Legacy tabs → RETIRED in Phase E.** "We are only resorting" — no second-class fallback. Drop `mode-procedure-tab` / `mode-event-tab` DOM and their client logic; remove the `?legacy=1` escape hatch.

2. **Decision-tree depth → UNLIMITED.** m: "theoretisch unbegrenzt — neue Konstruktion, ich habe noch nicht alle Konstellationen durchdacht." The cascade can grow as the taxonomy reveals deeper structures we hadn't enumerated yet. Frontend renders one step at a time; depth is a property of the data, not a hard-coded UI constant.

3. **Navigation → CLICKABLE BREADCRUMB.** Each completed step is clickable to jump back to that level. Equivalent visualisations are fine (e.g. inline back-arrows per breadcrumb segment); the literal "← zurück" button on the previous step can stay or go — breadcrumb suffices.

4. **Partial-path bookmarks → YES.** URL `?b1=cms-eingang.gericht.hinweisbeschluss` round-trips. Refresh restores the cascade at that node.

5. **Forum filter → MULTI-SELECT.** m: "usually we will only want one" — so the chip group defaults to **one chip pre-selected** (or none, with a hint), but the user can hold-shift / cmd-click to multi-select. Adding a 2nd chip UNIONs within forum, AND-intersected with other filters.

6. **Cards at each B1 step → PATH-MATCHING ONLY.** Empty result set shows a **"Schritt zurück"** link (NOT "Pfad lockern" — m: "should be 'Schritt zurück' or similar"). The link sends the user back one breadcrumb level.

7. **Visual language → EMOJIS, no separate colour treatment.** m: "Colors for what?! Emojis are okay generally." Use the same lime-green accent as the rest of Paliad; pathway / step / forum buttons differentiate via emoji (📖 / 📅 / 📥 / 👥 / ⚖ / 🎤 / 📊 / 🚫 / ❓). No purple-for-Lernen / blue-for-Eintragen distinction.

8. **Forum buckets → COURT-SYSTEM granularity (10 buckets):** UPC CFI (LD + CD combined per m: "UPC CFI") · UPC CoA · DE LG · DE OLG · DE BGH · DE BPatG · EPA Erteilung (Prüfungsabt.) · EPA Einspruchsabt. · EPA Beschwerdek. · DPMA. Maria's clarification at 10:16 was about court-system level; m's 10:33 simplified UPC to CFI vs CoA (LD/CD merged because the rules are identical). Map lives in Go (`forumToProceedingCodes`); rebucketing = code change, no migration.

9. **B1↔B2 state-sharing → YES.** Same backend filter state, two presentations. Switching between modes preserves filters where mappable; B2 chips that don't map onto B1 steps surface as a banner ("Aktive Filter: …").

10. **Phasing → SINGLE BRANCH, SEQUENTIAL COMMITS, ONE FINAL MERGE.** m: "no need to separate into PRs, just commits — and in the end we merge." All implementation lands on `mai/cronus/fristenrechner-v3-pathways` as commits A → B → C → D → E. Final merge is a single `--no-ff` to `main`.

11. **Party perspective default → CLAIMANT / PROACTIVE.** URL persists `?my_side=claimant|defendant` (literal column-side semantics: the chosen side becomes the Proactive column). For role-swap proceedings (UPC_APP, DE_INF_OLG, DE_INF_BGH, DE_NULL_BGH, DPMA_BPATG_BESCHWERDE, DPMA_BGH_RB, EPA_APP, UPC_COST_APPEAL), an additional toggle `?appeal_filed_by=claimant|defendant` flips first-instance-claimant ↔ second-instance-appellant mapping. localStorage remembers the last-used choice across sessions.

12. **"Both parties" rule mirroring → BILATERAL FLAG ON RULE ROW.** New column `paliad.deadline_rules.is_bilateral bool NOT NULL DEFAULT false`. When `is_bilateral=true` AND `primary_party='both'`, the rule renders in BOTH party columns (today's behaviour). Otherwise `primary_party='both'` rules apply to the side that actually took the action — which is determined per-rule from the parent rule's party (or, if root, from the perspective selector). Genuinely-bilateral rules (Schriftsatzfristen with Anwaltszwang, mündliche Verhandlung, joint Mängelbeseitigung) are flagged in seed migration 050. Spot-checkable list (~5 rules expected); m or HLC colleague reviews.

### Maria's scope additions (folded in)

**Court-system granularity for forum filter** (head 10:16): the v2 Q8 "drop forum" was about not differentiating Munich vs Paris within ONE UPC LD — not about dropping the system axis. v3 brings the system axis back; locked at the 10-bucket list above.

**Party-perspective selector** (head 10:21, absorbs t-paliad-132): `Ich vertrete: ◉ Klägerseite ◯ Beklagtenseite` selector above the columns view. For appeal proceedings: additional `Wer hat Berufung eingelegt?` toggle. Columns re-flow: Proactive = user's perspective, Reactive = other side. Bilateral rules respect the new `is_bilateral` flag (Q12). The B1 decision tree implicitly encodes perspective: "CMS-Eingang → ich habe etwas erhalten" = Reactive, "Ich will einreichen" = Proactive. Cards in B2 may render party-relative ("Erwiderungsfrist FÜR DICH"). Closes t-paliad-132 as done-by-v3.

### What this means for the spec elsewhere in the doc

Sections updated to match the locked decisions:
- §3.4 — tree depth language changed from "max 4" to "unlimited; cascade renders one step at a time".
- §3.4 — "Pfad lockern" empty-state link renamed to "Schritt zurück".
- §3.5 — forum bucket count changed from 7 to 10; LD/OLG/BGH/BPatG distinctions kept; UPC split into CFI / CoA; EPA split into Erteilung / Einspruchsabt. / Beschwerdek.
- §4.1 — `paliad.deadline_rules` migration adds `is_bilateral bool NOT NULL DEFAULT false` column; seed migration 050 backfills the genuinely-bilateral rules.
- §5.2 — extended endpoint section gains `?my_side=` and `?appeal_filed_by=` query params with the rule-mirroring contract.
- §6.1 — frontend file list adds `frontend/src/client/perspective.ts` (~150 lines) for the party-perspective selector + appeal-filed-by toggle + URL state machine.
- §7 — phasing kept (A → E) but single-branch single-merge per m's "just commits" call.

---

## 11. Proposed cycle

- **Inventor (this shift, cronus, branch `mai/cronus/fristenrechner-v3-pathways`):** v3 design doc + go/no-go gate (this commit).
- **Coder (next shift, after m's go-ahead):** Phase A (1 PR) — schema + seed + handler + tests.
- **Coder (third shift):** Phase B (1 PR) — landing fork + URL state + Pathway A wrap + Pathway B shell with stub panels.
- **Coder (fourth shift):** Phase C (1 PR) — B1 decision tree + state machine + tree narrow integration with concept-card list.
- **Coder (fifth shift):** Phase D (1 PR) — B2 forum filter + AND-narrowing correctness + B1↔B2 state share.
- **Coder (sixth shift, gate-gated):** Phase E (1 PR) — retire legacy tabs IF m approves at that point.

Inventor recommends **cronus** (continuity with t-paliad-131) or **knuth** (frontend-heavy) as implementer. Phase A is data-only and equally fits **curie** (port-heavy seed work — same shape as t-paliad-084 / t-paliad-086). Head decides.

---

## Appendix A — Files & references

**Code paths read / cited:**

- `frontend/src/fristenrechner.tsx` (current 320-line page shell — search bar + 2-tab wizard)
- `frontend/src/client/fristenrechner.ts` (~1500 lines — proceeding wizard + event mode + dates)
- `frontend/src/client/fristenrechner-search.ts` (~600 lines — Phase D concept-card search)
- `internal/services/fristenrechner.go` (calculator with AnchorOverrides + condition_flag arrays)
- `internal/services/event_deadline_service.go` (trigger-event calculator, untouched)
- `internal/handlers/fristenrechner.go` (Phase D search endpoint)
- `internal/db/migrations/037-046` (concept layer + B1-B6 coverage from t-131)
- `internal/db/migrations/047_deadline_search_view.up.sql` (matview, untouched in v3)

**DB read:**

- `paliad.proceeding_types` (19 active fristenrechner-category — 8 UPC + 5 DE + 3 EPA + 3 DPMA)
- `paliad.deadline_concepts` (57 concepts, all categories — submission/decision/order/hearing/...)
- `paliad.deadline_rules` (137 rules)
- `paliad.trigger_events` (107 active — 100 youpc UPC + 7 paliad-native cross-cutting at id ≥ 200)
- `paliad.deadline_search` (matview — UNION of rule + trigger × concept)

**Prior work:**

- `docs/plans/unified-fristenrechner.md` (v2 — search-by-concept + coverage; v3 inherits all of §4-§6 unchanged)
- t-paliad-131 — what just shipped (Phases A-D over 9 PRs)
- t-paliad-088 — Event Types on `paliad.deadlines` (junction layer; could share taxonomy slugs with v3 in a future merger)
- t-paliad-129 — column-timeline view (out of scope here)
- t-paliad-132 — column-timeline party-perspective selector (parked, related)
- m's note (2026-05-05 10:06) — source of truth for the restructuring brief

**Memory references (mai-memory):**

- `paliad t-paliad-131 — Phase B complete (7 PRs shipped, unified Fristenrechner)`
- `Design: Event Types for deadlines + submissions (t-paliad-088, cronus)`
- `paliad t-paliad-088 SHIPPED — Event Types live, smoke 6/6 pass (cronus)`

---

**End of design doc.**

Inventor stays parked on this branch. Next step: m's go/no-go on the open questions in §10. After approval, head assigns a coder for Phase A.
