# Unified Fristenrechner — Search-by-Concept + Complete Proceeding-Type Coverage

**Author:** cronus (inventor)
**Date:** 2026-05-04 (revised after m's go-direction at 23:10)
**Task:** t-paliad-131
**Mode:** design only — no code, no schema migrations applied
**Branch:** `mai/cronus/unified-fristenrechner-design` (worktree)
**Status:** v2 — m's answers to v1 + v2 open questions both incorporated. Awaiting final go for coder shift.

> *m, 2026-05-04 22:55:* "I want additional possibilities to select / filter the trigger. By Rule, by Proceeding Type, by Party etc... we need to classify each of our deadlines again. In particular we do not have all german patg and zpo procedural deadlines, yet. Let us hire an inventor — and allow research on this — for a unified deadline calculator that is ridiculously easy to use."

---

## 0. m's go-direction (v2 anchor)

m's 10 answers (relayed via head 2026-05-04 23:10) reshape the design materially. They are the binding spec for v2:

1. **Augment, not replace.** Search bar at top **plus** the existing chunky proceeding-type tiles below as browse fallback. The two existing tabs (Verfahrensablauf / Was kommt nach…) **stay**. No subsumption.
2. **Aliases hard-coded** in seed migrations (curated, not user-editable in v1).
3. **Unifier shape (a) — "shared rule with applicable_in"**: one canonical concept ('Klageerwiderung') that adapts duration / legal_source / notes per context. Pick the cleaner of (jsonb on rule) vs (separate context-overrides table).
4. **Counterclaim flag pattern stays**, just add the missing deadlines (R.25, R.30, R.50).
5. **"Full Appeal Chain" checkbox** — default: per-instance pick. Toggle on → render LG → OLG → BGH (or BPatG → BGH for nullity) as one tree.
6. **One result card per concept with proceeding pills inside.** Search "Klageerwiderung" → one card titled "Klageerwiderung" with pills [LG] [OLG] [UPC] [BPatG] [EPA] [DPMA]. Click a pill = drill into that context's specific deadline.
7. **Structured legal_source codes** — `DE.ZPO.282`, `DE.PatG.83`, `UPC.RoP.23.1`, `EU.EPÜ.108`, `DPMA.PatG.59`, etc. Parseable, filterable. Document the canonical format.
8. **Forum NOT a filter** — drop. Rules are shared across the court system within a jurisdiction.
9. **Sequence preservation in columns-view** — undated court-set events (Counterclaim → Defence → Reply → Decision) currently collapse into one row in the t-paliad-129 columns view. They must order by `sequence_order` even without dates. Flag in this doc; ship as a separate follow-up.
10. **Court-set placeholders ARE searchable triggers.** "Verhandlung", "Entscheidung", "Zwischenverfügung" etc. surface in search.

The remainder of this document implements those ten constraints.

---

## 1. Executive summary (v2)

The Unified Fristenrechner is **search-first with proceeding-type tiles as fallback**, organised around **concepts** ("Klageerwiderung") rather than per-proceeding rule rows. A new `paliad.deadline_concepts` layer sits above the existing `paliad.deadline_rules` and groups context-specific instances of the same legal idea. The tree-shape calculator stays unchanged in math; it just gains a `concept_id` on each rule for grouping.

**The single search hit is one card per concept**, with proceeding-pills inside. Type "Klageerwiderung" → one card; click [LG] for ZPO §276, [BPatG] for PatG §82(1), [UPC] for RoP R.23, etc.

**Coverage gap closure:** UPC counterclaim cross-flows (R.25 / R.30 / R.49(2) / R.50 / R.51 / R.52 / R.55 / R.56), DE OLG + BGH-Revision + BGH-NZB + BPatG Hinweisbeschluss-Cycle, DPMA Einspruch + Beschwerde, EPA R.116 / R.79(2)(3) / R.106 / Wiedereinsetzung. ≈ +85 new deadline rules, organised into ≈ 30 concepts.

**The Full Appeal Chain toggle** synthesises a multi-instance tree (LG → OLG → BGH for infringement; BPatG → BGH for nullity) when the user wants the whole journey on one timeline.

**The columns-view sequence-preservation fix** is flagged but **deferred to a separate task** as it's a t-paliad-129 follow-up rather than a unified-Fristenrechner concern.

---

## 2. Current state — verified live

### 2.1 Two backends, two shapes

| | `paliad.deadline_rules` (proceeding-tree) | `paliad.trigger_events` + `paliad.event_deadlines` (event-driven) |
|---|---|---|
| Shape | 74 rules in 12 trees, parent_id chain, sequence_order | 102 triggers, 70 deadlines (1:N), flat |
| PK | uuid | bigint |
| Provenance | hand-seeded migrations 012/029/031 (HLC + KanzlAI port) | youpc verbatim port (migration 028, IDs preserved) |
| Rule-code coverage | 5 distinct UPC RoP codes + 4 PatG/ZPO + 5 EPÜ Art./R | 64 distinct UPC RoP codes (UPC only) |
| Conditional logic | `condition_flag text` + `alt_*` columns | implicit (operator picks the matching trigger event) |
| Composite math | none | `combine_op IN ('max','min')` + `alt_duration_*` |
| Working-day arithmetic | no | yes (`duration_unit='working_days'`) |
| Anchor flexibility | `anchor_alt='priority_date'` (single special case) | trigger date only |
| Calculator | `internal/services/fristenrechner.go::Calculate` | `internal/services/event_deadline_service.go::Calculate` |
| UI | proceeding-button grid → date/flag inputs → timeline / Spalten | search-by-name input → trigger picked → flat results |

Both backends stay structurally separate (the math is genuinely different). The Unifier sits *above* them.

### 2.2 Proceeding types currently shipped

12 fristenrechner-category types live (sort_order 101–303):
- **UPC:** UPC_INF, UPC_REV, UPC_PI, UPC_APP, UPC_DAMAGES, UPC_DISCOVERY, UPC_COST_APPEAL, UPC_APP_ORDERS
- **DE:** DE_INF, DE_NULL
- **EPA:** EPA_OPP, EPA_APP, EP_GRANT

(Plus 7 internal/KanzlAI-port types under `category='litigation'` for matter-attached fristen — out of scope for this design.)

### 2.3 Verified gaps confirmed against m's vision

m's specific complaint:

> "the checkbox for 'counterclaim' does not actually add the submissions to the timeline; it only changes the rejoinder deadline. But for the counterclaim for revocation we also need the application to amend the patent — and in the revocation action we are lacking a 'counterclaim for infringement'."

**Verified:** the current `with_ccr` flag on `UPC_INF` only swaps `inf.reply` (RoP.029.b → RoP.029.a) and `inf.rejoin` (RoP.029.c 1mo → RoP.029.d 2mo) durations. The 5–7 additional submissions (Defence to CCR, Application to amend, Defence to App-to-amend, Reply to Defence to CCR, etc.) are missing from the tree entirely. UPC_REV has the same gap — no Application to amend, no Counterclaim for infringement, no R.55 / R.56 cycles.

The trigger-event tab carries labels for "Verletzungswiderklage" (id=10), "Antrag auf Patentänderung" (id=38), "Nichtigkeitswiderklage" (id=101), but with `num_deadlines=0` for many of them (labels without downstream cycles). DE/EPA/DPMA triggers absent entirely from the trigger-event corpus.

---

## 3. UX — search-first, tiles as fallback

### 3.1 The search bar augments, doesn't replace

`/tools/fristenrechner` keeps its current structure:

```
┌──────────────────────────────────────────────────────────────────┐
│  Fristenrechner                                                  │
│  Berechnung von Verfahrensfristen — UPC, DE, EPA, DPMA           │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ 🔍  Tippe Frist, Rechtsgrundlage oder Verfahren…           │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  [ Häufig: Klageerwiderung │ Berufung │ Einspruch │ Replik │ … ] │
│                                                                  │
│  ─────────────────────────────────────────────                  │
│                                                                  │
│  oder Verfahren wählen:                                          │
│                                                                  │
│  UPC                                                             │
│  [ Verletzungs- ] [ Nichtigkeits- ] [ Einstw. M. ] [ Berufung ]  │
│  [ Schadensbem. ] [ Bucheinsicht  ] [ Berufung-K ] [ Anord.   ]  │
│                                                                  │
│  Deutsche Gerichte                                               │
│  [ Verletz. (LG) ] [ Nichtigk. (BPatG) ] [ Berufung (OLG) ] …    │
│                                                                  │
│  EPA / DPMA                                                      │
│  [ Einspruch EPA ] [ Beschwerde EPA ] [ EP-Erteilung ] [ DPMA ]  │
│                                                                  │
│  ☐ Vollständige Instanzenkette anzeigen (LG → OLG → BGH)         │
└──────────────────────────────────────────────────────────────────┘
```

Behaviour changes vs today:
- Search bar at top is new.
- Quick-pick chips are new.
- The proceeding-type grid below is the existing "Verfahrensablauf" entrypoint, slightly reorganised (DE gets 5 tiles now, EPA picks up DPMA).
- The two existing tabs (Verfahrensablauf / Was kommt nach…) stay reachable — when the user clicks a tile, the Verfahrensablauf flow opens. When the user lands on a search hit that's a court-set placeholder ("Verhandlung", "Entscheidung"), the "Was kommt nach…" flow opens.
- "Full Appeal Chain" checkbox at the bottom (3.4).

### 3.2 Search hit — one card per concept

A typed query resolves to **concept cards** (not rule rows). Each card represents one legal idea ("Klageerwiderung") with the contexts where it applies as proceeding pills inside.

```
Search: "Klageerwiderung"

┌──────────────────────────────────────────────────────────────────┐
│ ⚖ Klageerwiderung · Statement of Defence                          │
│ Erwiderung des Beklagten auf eine Klageschrift, üblicherweise    │
│ mit Verteidigungsanträgen und Sachvortrag.                        │
│                                                                  │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │   UPC    │ │    LG    │ │  BPatG   │ │   EPA    │ │   DPMA   │ │
│ │  R.23.1  │ │  §276.1  │ │  §82.1   │ │  R.79.1  │ │  §59.3   │ │
│ │ 3 Monate │ │ 6 Wochen │ │ 1 Monat  │ │ 4 Monate │ │  4 Mon.  │ │
│ │  Beklag. │ │  Beklag. │ │  Beklag. │ │  PatInh. │ │  PatInh. │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ ⚖ Klageerwiderung mit Nichtigkeitswiderklage                      │
│   (UPC-spezifisch — Trigger für mehrere Folgefristen)            │
│                                                                  │
│ ┌──────────┐ ┌──────────┐                                        │
│ │   UPC    │ │   UPC    │                                        │
│ │ trigger  │ │  R.23.1  │  ← der Trigger versus die Frist        │
│ │ (Was nach│ │ + R.25.1 │                                        │
│ └──────────┘ └──────────┘                                        │
└──────────────────────────────────────────────────────────────────┘
```

- **Top line:** concept name (active locale) + EN translation in muted text.
- **Description line:** 1–2-sentence concept description.
- **Pills row:** one pill per context. Each pill shows: forum chip · structured legal_source · duration · party (compact). Click → drill into that proceeding's calculator at the matching rule.
- **Multiple cards:** when the search term is genuinely ambiguous (e.g. "Erwiderung" matches Klageerwiderung AND Erwiderung Prüfbescheid AND Erwiderung Patentinhaber Einspruch), each is its own card. Cards are sorted by relevance score.

When a single concept also exists as a *trigger event* (court-set placeholder, e.g. "Mündliche Verhandlung"), it gets a special pill labelled "Was kommt nach…" that opens the trigger-event calculator. Per Q10, court-set placeholders ARE searchable.

### 3.3 Drill-in behaviour

Click `[ UPC R.23.1 ]` pill → `/tools/fristenrechner?proc=UPC_INF&focus=inf.sod` opens the proceeding-tree calculator with UPC_INF preselected, jumping straight to the date input, then renders the timeline with `inf.sod` highlighted.

Click `[ UPC trigger (Was nach…) ]` pill → `/tools/fristenrechner?trigger=1` opens the event-driven calculator with the trigger preselected.

### 3.4 Full Appeal Chain checkbox

A checkbox at the bottom of the proceeding tile grid: "**☐ Vollständige Instanzenkette anzeigen** (LG → OLG → BGH)".

When **off** (default): user picks a single instance (LG, OLG, or BGH separately).
When **on**: the tiles grouped by case-type render as multi-instance tile pairs. E.g. the "Verletzungsklage" tile expands to show the full chain when clicked, rather than just the LG step.

Implementation: a synthetic "compound proceeding" rendering option in the calculator. The data model stays per-instance; the calculator stitches three trees together at render time when the toggle is on. Anchor for OLG = "Urteil des LG"; anchor for BGH = "Urteil des OLG". The user enters one date (the LG Klageerhebung) and the chain unfolds; OR the user enters individual stage anchors (1, 2, 3) for known dates.

Mapping for the chain rendering:
- **DE_INF chain:** `DE_INF` (LG) → `DE_INF_OLG` (Berufung) → `DE_INF_BGH` (Revision/NZB)
- **DE_NULL chain:** `DE_NULL` (BPatG) → `DE_NULL_BGH` (Berufung BGH)
- **DPMA chain:** `DPMA_OPP` (DPMA) → `DPMA_BPATG_BESCHWERDE` (BPatG) → `DPMA_BGH_RB` (Rechtsbeschwerde BGH)
- **UPC:** UPC_INF (CFI) → UPC_APP (CoA). Already linkable via Decision → Notice of Appeal.
- **EPA:** EPA_OPP (Einspruch) → EPA_APP (Beschwerde). Already linkable.

Out-of-scope for v1: the toggle as a default-on shortcut. It's an option, not a forced view.

### 3.5 Filters — slimmer than v1 draft

Per Q8 (forum dropped), filters are now:

```
[ Verfahrensart ▾ ]  [ Partei ▾ ]  [ Rechtsquelle ▾ ]
```

- **Verfahrensart:** the 12 (then ≈ 18 after coverage migrations) proceeding types.
- **Partei:** Kläger / Beklagte / Beide / Gericht.
- **Rechtsquelle:** UPC RoP · UPC Statute · EPÜ · ZPO · PatG · DPMAV · others (parses the prefix of `legal_source`).

Filters appear only when search returns more than 6 cards. Single-select for v1.

### 3.6 Empty-state and browse

- **Empty search input:** no hits below the input; the tile grid is the natural fallback.
- **No matches for a typed query:** "Keine Treffer für 'xyz' — meintest du …?" with up to 3 trigram-nearest concept suggestions.
- **The two existing tabs** (Verfahrensablauf / Was kommt nach…) stay alive as today, reachable from the tile grid (Verfahrensablauf default) and from event-trigger pills on cards (Was kommt nach…).

### 3.7 Mobile

Search bar full-width, quick-pick chips wrap, concept cards stack vertically with pills wrapping. Below ≈ 600px, the proceeding tile grid switches from 4-wide to 2-wide. No special handling beyond the existing responsive breakpoints.

---

## 4. Data model — the Unifier shape

### 4.1 Goal

One concept, many contexts. The user thinks "Klageerwiderung" — the system knows that's UPC R.23, ZPO §276, PatG §82, EPÜ R.79, PatG §59, all at once.

### 4.2 Two design alternatives (m's Q3)

m named two shapes:

**Option (X): `per_context jsonb` on a unified rule.**
- Single `paliad.deadline_rules` table; each row is one concept.
- New `per_context jsonb` column holds `{"UPC_INF": {duration, unit, legal_source, parent_concept, ...}, "DE_INF": {...}, ...}`.
- Pros: one row per concept, easy to query "where does X apply".
- Cons: jsonb keys don't index well for joins; the calculator extracts the right key at runtime; tree-chain (parent) lives inside jsonb, hard to traverse with normal SQL; condition_flag arrays in jsonb are awkward.

**Option (Y): split into `deadline_concepts` + per-context rule rows (essentially today's `deadline_rules`).**
- New `paliad.deadline_concepts` (concept_id, slug, name_de, name_en, aliases, party, description).
- Existing `paliad.deadline_rules` keeps its shape, gains a `concept_id` FK column.
- Each row in `deadline_rules` is one (concept × proceeding_type) instance. Tree-chain (parent_id) stays where it is.
- Pros: backward-compatible (existing calculator code unchanged); standard relational joins; sequence_order, parent_id, condition_flag, alt_* all live in normal columns; concept layer is thin.
- Cons: two tables; "where does Klageerwiderung apply" needs a 1-row → N-row join (trivial).

**Pick: Option (Y).** Cleanest, lowest-risk migration, no calculator refactor required, search hits are a single GROUP BY on concept_id.

### 4.3 Schema additions

```sql
-- New: concept layer (the canonical legal idea)
CREATE TABLE paliad.deadline_concepts (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug        text NOT NULL UNIQUE,        -- 'klageerwiderung', 'replik', 'berufungsfrist',
                                           -- 'berufungsbegruendung', 'einspruch', 'beschwerde',
                                           -- 'rejoinder', 'reply-to-defence', 'application-to-amend',
                                           -- 'counterclaim-for-revocation', 'counterclaim-for-infringement', ...
  name_de     text NOT NULL,
  name_en     text NOT NULL,
  description text,                        -- 1–2 sentences for the card body
  aliases     text[] NOT NULL DEFAULT '{}',
  party       text,                        -- canonical (most contexts agree); per-context override on rule row
  category    text NOT NULL,               -- 'submission' | 'decision' | 'order' | 'hearing' | 'other'
  sort_order  int  NOT NULL DEFAULT 100,
  is_active   bool NOT NULL DEFAULT true,
  created_at  timestamptz NOT NULL DEFAULT now(),
  updated_at  timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX deadline_concepts_trgm_de    ON paliad.deadline_concepts USING gin (name_de    gin_trgm_ops);
CREATE INDEX deadline_concepts_trgm_en    ON paliad.deadline_concepts USING gin (name_en    gin_trgm_ops);
CREATE INDEX deadline_concepts_aliases    ON paliad.deadline_concepts USING gin (aliases);

-- Existing: gain concept linkage + structured legal_source + condition_flag array
ALTER TABLE paliad.deadline_rules
  ADD COLUMN concept_id    uuid REFERENCES paliad.deadline_concepts(id),
  ADD COLUMN legal_source  text,                     -- structured code, see §4.5
  ALTER COLUMN condition_flag TYPE text[]            -- Q3: scalar → array; semantic: ALL flags must be set
    USING (CASE WHEN condition_flag IS NULL THEN NULL ELSE ARRAY[condition_flag] END);

CREATE INDEX deadline_rules_concept_id    ON paliad.deadline_rules (concept_id);
CREATE INDEX deadline_rules_legal_source  ON paliad.deadline_rules (legal_source);
CREATE INDEX deadline_rules_legal_src_trgm ON paliad.deadline_rules USING gin (legal_source gin_trgm_ops);

-- Existing: trigger_events also gain concept linkage (a court-set placeholder concept like 'oral-hearing' applies in many proc types)
ALTER TABLE paliad.trigger_events
  ADD COLUMN concept_id text;            -- nullable; by slug for cross-link without uuid FK gymnastics

-- Existing: event_deadlines gain legal_source (concept lives on the rule_codes via event_deadline_rule_codes)
ALTER TABLE paliad.event_deadlines
  ADD COLUMN legal_source text;
```

**Why no `aliases text[]` on `deadline_rules` directly anymore?** Because aliases are concept-level, not rule-level. "Klageerwiderung" matches the concept regardless of which proceeding the rule lives in. Per-rule context-specific aliases would be redundant — the legal_source ("UPC.RoP.23.1") and the proceeding code ("UPC_INF") already disambiguate. Per Q5, aliases are concept-only, hard-coded.

**Why text-by-slug on `trigger_events.concept_id` rather than uuid FK?** trigger_events has bigint PK and was imported verbatim from youpc; introducing a uuid FK touches the import-resync invariant. A text slug (e.g. `'oral-hearing'`) is a soft link sufficient for search.

### 4.4 Concept ↔ rule mapping examples

**Slug naming rule (Q1, locked):** **EN slug** for concepts native to UPC/EPC AND for **shared concepts** that exist in both DE and UPC/EPC. **DE slug** only for concepts that exist exclusively in German law (no UPC/EPC equivalent). `name_de` and `name_en` carry both labels for the user-facing surface; the slug is internal/maintenance-facing.

| concept slug | name_de | name_en | maps to deadline_rules in (proceeding_type, code) |
|---|---|---|---|
| `statement-of-defence` | Klageerwiderung | Statement of Defence | (UPC_INF, inf.sod), (DE_INF, de_inf.erwidg), (DE_NULL, de_null.erwidg), (UPC_REV, rev.defence), (EPA_OPP, epa_opp.erwidg), (DPMA_OPP, dpma_opp.erwidg) — shared, EN |
| `reply-to-defence` | Replik | Reply to Defence | (UPC_INF, inf.reply), (DE_INF, de_inf.replik), (UPC_REV, rev.reply), (UPC_DAMAGES, damages.reply), (UPC_DISCOVERY, disc.reply) — shared, EN |
| `rejoinder` | Duplik | Rejoinder | (UPC_INF, inf.rejoin), (UPC_REV, rev.rejoin), (DE_INF, de_inf.duplik), (UPC_DAMAGES, damages.rejoin), … — shared, EN |
| `notice-of-appeal` | Berufungsschrift | Notice of Appeal | (DE_INF, de_inf.berufung), (DE_NULL, de_null.berufung), (UPC_APP, app.notice), (EPA_APP, epa_app.beschwerde) — shared, EN |
| `statement-of-grounds-of-appeal` | Berufungsbegründung | Statement of Grounds of Appeal | (DE_INF, de_inf.beruf_begr), (DE_NULL, de_null.beruf_begr), (UPC_APP, app.grounds), (EPA_APP, epa_app.begr) — shared, EN |
| `opposition` | Einspruch / Einspruchsfrist | Opposition | (EPA_OPP, epa_opp.frist), (DPMA_OPP, dpma_opp.frist) — shared, EN |
| `re-establishment-of-rights` | Wiedereinsetzung in den vorigen Stand | Re-establishment of Rights | event_trigger only — PatG §123, ZPO §233, EPÜ Art.122, DPMA §123 — shared cross-cutting, EN |
| `application-to-amend` | Antrag auf Patentänderung | Application to Amend the Patent | (UPC_INF, inf.app_to_amend), (UPC_REV, rev.app_to_amend) — UPC/EPC-native, EN |
| `defence-to-application-to-amend` | Erwiderung auf den Antrag auf Patentänderung | Defence to Application to Amend | (UPC_INF, inf.def_to_amend), (UPC_REV, rev.def_to_amend) — UPC-native, EN |
| `counterclaim-for-revocation` | Nichtigkeitswiderklage | Counterclaim for Revocation | (UPC_INF, inf.ccr_filing) — UPC-native, EN |
| `counterclaim-for-infringement` | Verletzungswiderklage | Counterclaim for Infringement | (UPC_REV, rev.cc_inf) — UPC-native, EN |
| `request-for-discretionary-review` | Antrag auf Ermessensüberprüfung | Request for Discretionary Review | (UPC_APP_ORDERS, app_ord.discretion) — UPC-native, EN |
| `oral-hearing` | Mündliche Verhandlung | Oral Hearing | (every UPC tree, oral), (DE_INF, de_inf.termin), (DE_NULL, de_null.termin), (EPA_*, oral) — court-set, shared, EN |
| `decision` | Entscheidung | Decision | court-set, shared, EN |
| `nichtzulassungsbeschwerde` | Nichtzulassungsbeschwerde | Complaint Against Denial of Leave | (DE_INF_BGH, …) — DE-only, DE slug |
| `versaeumnisurteil-einspruch` | Einspruch gegen Versäumnisurteil | Objection to Default Judgment | event_trigger only — ZPO §339 — DE-only, DE slug |
| `hinweisbeschluss-stellungnahme` | Stellungnahme zum Hinweisbeschluss | Response to Court's Preliminary Opinion | (DE_NULL, …) — DE-only, DE slug |

≈ 30 concepts cover the entire seed corpus after Phase B coverage migrations (≈ +85 rules grouped into those 30 concepts).

### 4.5 Structured legal_source codes (Q7)

Canonical format: `<JURIS>.<CODE>.<§/Art./R>.<Para>[.<Sub>]`

| Source | Format | Example |
|---|---|---|
| UPC Rules of Procedure | `UPC.RoP.<rule>[.<para>][.<sub>]` | `UPC.RoP.23.1`, `UPC.RoP.29.a`, `UPC.RoP.220.1.c` |
| UPC Agreement | `UPC.UPCA.<art>[.<para>]` | `UPC.UPCA.49.5` |
| UPC Statute | `UPC.Statute.<art>` | `UPC.Statute.21` |
| EPÜ (German abbrev.) | `EU.EPÜ.<art>[.<para>]` | `EU.EPÜ.108`, `EU.EPÜ.99.1`, `EU.EPÜ.122` |
| EPC implementing rules | `EU.EPC-R.<rule>[.<para>][.<sub>]` | `EU.EPC-R.79.1`, `EU.EPC-R.116.1`, `EU.EPC-R.136` |
| German civil procedure | `DE.ZPO.<§>[.<para>]` | `DE.ZPO.276.1`, `DE.ZPO.520.2`, `DE.ZPO.544.1` |
| German patent law | `DE.PatG.<§>[.<para>]` | `DE.PatG.59.1`, `DE.PatG.82.1`, `DE.PatG.111.1` |
| DPMA Verordnung | `DE.DPMAV.<§>` | `DE.DPMAV.5` |
| RPBA Beschwerdeordnung | `EU.RPBA.<art>[.<para>]` | `EU.RPBA.12.1.c`, `EU.RPBA.13` |

**Why dot-separated rather than the human form `§82(1) PatG`?** Parseable. The first dot-segment is the juris (`UPC` / `EU` / `DE`); the second is the law name (`RoP` / `EPÜ` / `ZPO` / `PatG`). The filter dropdown can do `legal_source LIKE 'DE.PatG.%'` or `legal_source LIKE 'UPC.%'`. The display layer renders `UPC.RoP.23.1` as **"UPC RoP R.23(1)"** (a small format-converter on the frontend, mirroring what mbrian conventions do).

**The display form on the result-card pill is the human-rendered version.** Internal storage + index is the structured form. One field, two views.

**Why `EU.EPÜ` and not `DE.EPÜ`?** EPÜ is European, not German law (despite the German abbreviation). `EU` is the closest namespace; alternatives `EP` or `EPO` would also work. Open for m to override.

**Why `DE.PatG` and not `BPatG.PatG`?** PatG is national German patent law applicable across DPMA, BPatG, BGH. The forum (where the law is applied) varies; the source is the same. So `DE.PatG.110` is the BGH appeal rule, and the `proceeding_type_id` of the rule row tells us which forum.

### 4.6 The unified search view

```sql
CREATE MATERIALIZED VIEW paliad.deadline_search AS
  -- One row per concept × context (so a card is a GROUP BY concept_id of these rows)
  SELECT
    'rule'::text                  AS kind,
    dc.id                         AS concept_id,
    dc.slug                       AS concept_slug,
    dc.name_de                    AS concept_name_de,
    dc.name_en                    AS concept_name_en,
    dc.description                AS concept_description,
    dc.aliases                    AS concept_aliases,
    dc.party                      AS concept_party,
    dc.category                   AS concept_category,
    dr.id                         AS rule_id,
    pt.code                       AS proceeding_code,
    pt.name                       AS proceeding_name_de,
    pt.name_en                    AS proceeding_name_en,
    pt.jurisdiction               AS jurisdiction,
    dr.code                       AS rule_local_code,
    dr.name                       AS rule_name_de,
    dr.name_en                    AS rule_name_en,
    dr.legal_source               AS legal_source,
    dr.rule_code                  AS rule_code,
    dr.duration_value,
    dr.duration_unit,
    dr.timing,
    COALESCE(dr.primary_party, dc.party) AS effective_party
  FROM paliad.deadline_rules dr
  JOIN paliad.proceeding_types pt  ON pt.id = dr.proceeding_type_id
  JOIN paliad.deadline_concepts dc ON dc.id = dr.concept_id
  WHERE dr.is_active AND pt.is_active AND pt.category = 'fristenrechner'

  UNION ALL

  -- Trigger events linked to a concept (court-set placeholders, cross-cutting Wiedereinsetzung, etc.)
  SELECT
    'trigger',
    dc.id, dc.slug, dc.name_de, dc.name_en, dc.description, dc.aliases, dc.party, dc.category,
    NULL::uuid,                  -- no rule_id (trigger lives in event_deadlines)
    NULL,                        -- no proceeding_code (trigger is cross-cutting)
    NULL, NULL,
    'cross-cutting',             -- jurisdiction
    te.code, te.name_de, te.name,
    NULL,                        -- legal_source (will join from event_deadline_rule_codes when surfaced)
    NULL,                        -- rule_code
    NULL, NULL, NULL,
    dc.party
  FROM paliad.trigger_events te
  JOIN paliad.deadline_concepts dc ON dc.slug = te.concept_id
  WHERE te.is_active;

CREATE INDEX deadline_search_concept_id    ON paliad.deadline_search (concept_id);
CREATE INDEX deadline_search_proc_code     ON paliad.deadline_search (proceeding_code);
CREATE INDEX deadline_search_legal_src     ON paliad.deadline_search (legal_source);
CREATE INDEX deadline_search_legal_src_trgm ON paliad.deadline_search USING gin (legal_source gin_trgm_ops);
CREATE INDEX deadline_search_concept_de_trgm ON paliad.deadline_search USING gin (concept_name_de gin_trgm_ops);
CREATE INDEX deadline_search_concept_en_trgm ON paliad.deadline_search USING gin (concept_name_en gin_trgm_ops);
CREATE INDEX deadline_search_aliases       ON paliad.deadline_search USING gin (concept_aliases);
```

`pg_trgm` already enabled (verified). Refresh:

```sql
REFRESH MATERIALIZED VIEW CONCURRENTLY paliad.deadline_search;
```

Triggered by AFTER INSERT/UPDATE/DELETE on `deadline_rules` / `deadline_concepts` / `trigger_events` / `proceeding_types`. Mat-view remains sub-1k rows, so refresh is < 100 ms.

---

## 5. Coverage gaps to map (UPC + DE + EPA + DPMA)

Same legal substance as v1 §5, retargeted to the concept-layer shape. Citations from `data.laws_contents` (youpcdb, UPCRoP) for UPC; from m's authoritative knowledge (spot-checked) for DE / EPA / DPMA.

### 5.1 UPC counterclaim cross-flows (m's primary complaint)

#### UPC_INF with Counterclaim for Revocation (CCR)

R.29 verbatim from `data.laws_contents`:

| When | Who | What | Source code | Duration | Anchor |
|---|---|---|---|---|---|
| (with CCR) | Claimant | Defence to CCR + Reply to SoD + (opt) Application to amend | `UPC.RoP.29.a` | 2 months | service of SoD |
| | Defendant | Defence to App to amend (when claimant filed amend) | `UPC.RoP.32.1` | 2 months | service of App to amend |
| | Defendant | Reply to Defence to CCR + Rejoinder to Reply to SoD + (opt) Defence to App to amend | `UPC.RoP.29.d` | 2 months | service of Defence to CCR |
| | Proprietor | Reply to Defence to App to amend | `UPC.RoP.32.3` | 1 month | service of Defence to App to amend |
| | Claimant | Rejoinder + (opt) Reply to Defence to amend | `UPC.RoP.29.e` | 1 month | service of Reply to Defence to CCR |
| | Defendant | Rejoinder on Reply to amend | `UPC.RoP.32.3` | 1 month | service of Reply to Defence to amend |

**New rules added to `UPC_INF` tree (new concepts in `deadline_concepts`):**

```
inf.def_to_ccr      Defence to Counterclaim for Revocation     2mo  UPC.RoP.29.a   parent=inf.sod   party=claimant   condition_flag={with_ccr}
inf.app_to_amend    Application to amend the patent           2mo  UPC.RoP.30.1   parent=inf.sod   party=claimant   condition_flag={with_ccr,with_amend}
inf.def_to_amend    Defence to App to amend                    2mo  UPC.RoP.32.1   parent=inf.app_to_amend  party=defendant   condition_flag={with_ccr,with_amend}
inf.reply_def_ccr   Reply to Defence to CCR                    2mo  UPC.RoP.29.d   parent=inf.def_to_ccr  party=defendant  condition_flag={with_ccr}
inf.reply_def_amd   Reply to Defence to amend                  1mo  UPC.RoP.32.3   parent=inf.def_to_amend  party=claimant   condition_flag={with_ccr,with_amend}
inf.rejoin_reply_ccr Rejoinder on Reply to Defence to CCR     1mo  UPC.RoP.29.e   parent=inf.reply_def_ccr  party=claimant  condition_flag={with_ccr}
inf.rejoin_amd      Rejoinder on Reply to amend                1mo  UPC.RoP.32.3   parent=inf.reply_def_amd  party=defendant  condition_flag={with_ccr,with_amend}
```

**Concepts created:**
- `defence-to-counterclaim-for-revocation`
- `application-to-amend` (also referenced by UPC_REV, see below)
- `defence-to-application-to-amend`
- `reply-to-defence-to-counterclaim-for-revocation`
- `reply-to-defence-to-application-to-amend`
- `rejoinder-to-reply-to-defence-to-counterclaim-for-revocation`
- `rejoinder-on-reply-to-amend`

**UI:** the existing single `with_ccr` checkbox keeps its label. A nested checkbox **"☐ Mit Antrag auf Patentänderung"** appears below it (only enabled when `with_ccr` is on, since R.30 application is only available with a CCR). Two checkbox states: ccr-only / ccr-with-amend.

#### UPC_REV with Application to Amend + Counterclaim for Infringement

R.49(2) Defence to revocation may include (a) Application to amend (R.55 = R.32 m.m.) and/or (b) Counterclaim for infringement (R.50, R.56 cycle).

| When | Who | What | Source code | Duration | Anchor |
|---|---|---|---|---|---|
| (with amend) | Defendant (proprietor) | Application to amend (within Defence) | `UPC.RoP.49.2.a` | 0 (filed with Defence) | service of SoR |
| | Claimant | Defence to Application to amend | `UPC.RoP.43.3` (= R.32.1 m.m.) | 2 months | service of Application to amend |
| | Defendant | Reply to Defence to amend | `UPC.RoP.32.3` | 1 month | service of Defence to amend |
| | Claimant | Rejoinder on Reply to amend | `UPC.RoP.32.3` | 1 month | service of Reply to Defence to amend |
| (with CCI) | Defendant (proprietor) | Counterclaim for infringement (within Defence) | `UPC.RoP.49.2.b` | 0 (with Defence) | service of SoR |
| | Claimant | Defence to Counterclaim for infringement | `UPC.RoP.56.1` | 2 months | service of CCI |
| | Defendant | Reply to Defence to CCI | `UPC.RoP.56.3` | 1 month | service of Defence to CCI |
| | Claimant | Rejoinder on Reply on CCI | `UPC.RoP.56.4` | 1 month | service of Reply to Defence to CCI |

**New rules added to `UPC_REV` tree, with two parallel independent flag chains** (per Q3 / m's go-direction):

```
rev.app_to_amend    Application to amend                       0    UPC.RoP.49.2.a parent=rev.defence       party=defendant   condition_flag={with_amend}
rev.def_to_amend    Defence to Application to amend            2mo  UPC.RoP.43.3   parent=rev.app_to_amend  party=claimant    condition_flag={with_amend}
rev.reply_def_amd   Reply to Defence to amend                  1mo  UPC.RoP.32.3   parent=rev.def_to_amend  party=defendant   condition_flag={with_amend}
rev.rejoin_amd      Rejoinder on Reply to amend                1mo  UPC.RoP.32.3   parent=rev.reply_def_amd party=claimant    condition_flag={with_amend}
rev.cc_inf          Counterclaim for infringement              0    UPC.RoP.49.2.b parent=rev.defence       party=defendant   condition_flag={with_cci}
rev.def_cci         Defence to CCI                             2mo  UPC.RoP.56.1   parent=rev.cc_inf        party=claimant    condition_flag={with_cci}
rev.reply_def_cci   Reply to Defence to CCI                    1mo  UPC.RoP.56.3   parent=rev.def_cci       party=defendant   condition_flag={with_cci}
rev.rejoin_cci      Rejoinder on Reply on CCI                  1mo  UPC.RoP.56.4   parent=rev.reply_def_cci party=claimant    condition_flag={with_cci}
```

**UI:** UPC_REV gets two independent flags — **"☐ Mit Antrag auf Patentänderung"** and **"☐ Mit Verletzungswiderklage"**. Both can be on. They render parallel cycles that don't interact (no rule has `condition_flag={with_amend,with_cci}` — verified per R.49 + R.55 + R.56 reading).

#### Other UPC gaps remaining (lower priority — t-paliad-084 Tier 2/3 follow-ups)

Already-shipped per `031_tier2_fristenrechner_ports`: R.137.2 / R.139 (damages), R.151 / R.221.1 (cost-decision), R.220.2 / R.220.3 (leave-to-appeal), R.237 / R.238 (cross-appeal), R.142 (lay-open books). Verify nothing slipped before Phase B2 lands.

Cross-cutting (best as event_trigger only): R.16(3)(a) / R.27(2) / R.89(2) / R.207.6(a) / R.229(2) / R.253(2) (correction of deficiencies × 6), R.262(2) (confidentiality), R.197(3) / R.198 (evidence preservation), R.245(2)(a)/(b) (rehearing — needs compound trigger), R.321(3) (refer central division), R.353 (rectification).

#### UPC_APP grounds-anchor bug (open)

`app.grounds.parent_id = app.notice` — wrong per R.224(2)(a). Grounds is 4 months from **service of the decision**, not 2mo + 2mo from notice. Fix as part of Phase B1.

### 5.2 German national — PatG / ZPO procedural deadlines

#### 5.2.1 LG (1. Instanz) — `DE_INF`

| # | Trigger | Source code | Duration | Anchor | Status |
|---|---|---|---|---|---|
| 1 | Klageerhebung | (anchor) | 0 | trigger date | EXISTS |
| 2 | Anzeige der Verteidigungsbereitschaft | `DE.ZPO.276.1` | 2 weeks | service of Klage | **GAP** |
| 3 | Klageerwiderung (schriftliches Vorverfahren) | `DE.ZPO.276.1` | 6 weeks (court-set) | service of Klage | EXISTS (`de_inf.erwidg`) |
| 4 | Replik | `DE.ZPO.282` | court-set, ~4 weeks | service of Klageerwiderung | EXISTS (`de_inf.replik`) |
| 5 | Duplik | `DE.ZPO.282` | court-set, ~4 weeks | service of Replik | EXISTS (`de_inf.duplik`) |
| 6 | Schriftsatznachreichung | `DE.ZPO.296a` | court-set | end of mündl. Verhandlung | **GAP** |
| 7 | Haupttermin | (court event) | court-set | — | EXISTS |
| 8 | Urteil | (court event) | court-set | — | EXISTS |
| 9 | Einspruch gegen Versäumnisurteil | `DE.ZPO.339` | 2 weeks | service of Versäumnisurteil | **GAP** |
| 10 | Berufungsfrist | `DE.ZPO.517` | 1 month | service of Urteil | EXISTS |
| 11 | Berufungsbegründung | `DE.ZPO.520.2` | 2 months | service of Urteil (NOT from Berufungsschrift) | EXISTS — verify anchor |
| 12 | Berufungserwiderung | `DE.ZPO.521.2` | court-set, ~4 weeks | service of Berufungsbegründung | **GAP** |
| 13 | Anschlussberufung | `DE.ZPO.524.2` | until expiry of §521 deadline | (event) | **GAP** |

**New proceeding types needed:** `DE_INF_OLG` (OLG Berufung), `DE_INF_BGH` (BGH NZB / Revision).

| # | Trigger | Source code | Duration | Anchor |
|---|---|---|---|---|
| `DE_INF_OLG` | Berufungsschrift | `DE.ZPO.519` | 1 month from Urteil | (entry trigger) |
| | Berufungsbegründung | `DE.ZPO.520.2` | 2 months | service of Urteil |
| | Berufungserwiderung | `DE.ZPO.521.2` | court-set | service of Begründung |
| | Anschlussberufung | `DE.ZPO.524.2` | until §521 expiry | event |
| | Mündliche Verhandlung | (court) | — | — |
| | Urteil OLG | (court) | — | — |
| `DE_INF_BGH` | Nichtzulassungsbeschwerde | `DE.ZPO.544.1` | 1 month | service of OLG-Urteil |
| | NZB-Begründung | `DE.ZPO.544.4` | 2 months | service of OLG-Urteil |
| | Revisionsfrist | `DE.ZPO.548` | 1 month | service of OLG-Urteil |
| | Revisionsbegründung | `DE.ZPO.551.2` | 2 months | service of OLG-Urteil |
| | Revisionserwiderung | `DE.ZPO.554` | court-set | service of Revisionsbegründung |

#### 5.2.2 BPatG (Nichtigkeit) — `DE_NULL`

| # | Trigger | Source code | Duration | Anchor | Status |
|---|---|---|---|---|---|
| 1 | Nichtigkeitsklage | (anchor) | 0 | trigger date | EXISTS |
| 2 | Klageerwiderung | `DE.PatG.82.1` | 2 months (= 1mo base + 1mo typische richterliche Verlängerung; user can override exact date inline per Q4) | service of Klage | EXISTS as 2mo — keep |
| 3 | Replik | `DE.PatG.83.2` | court-set, ~2 months | service of Erwiderung | **GAP** |
| 4 | Hinweisbeschluss | `DE.PatG.83.1` | court-issued (~6 mo before mündl. Verhandlung) | (court event) | **GAP** |
| 5 | Stellungnahme zum Hinweis | `DE.PatG.83.2` | court-set, ~3 months | service of Hinweisbeschluss | **GAP** |
| 6 | Duplik | `DE.PatG.83.2` | court-set | service of Replik | **GAP** |
| 7 | Mündliche Verhandlung | (court) | — | — | EXISTS |
| 8 | Urteil | (court) | — | — | EXISTS |

**New proceeding type:** `DE_NULL_BGH` (Berufung BGH).

| # | Trigger | Source code | Duration | Anchor | Status |
|---|---|---|---|---|---|
| 9 | Berufungsfrist | `DE.PatG.110.1` | 1 month | service of Urteil | EXISTS (currently in DE_NULL) |
| 10 | Berufungsbegründung | `DE.PatG.111.1` | **3 months** (currently seeded as 1mo — likely bug) | service of Urteil | **EXISTS but wrong** |
| 11 | Berufungserwiderung | `DE.PatG.111.3` (verweist auf ZPO §521) | 2 months | service of Begründung | **GAP** |

**Bug to fix as part of Phase B3:** `de_null.beruf_begr` 1 month → 3 months per current PatG §111(1).

#### 5.2.3 DPMA — new proceeding types

`DPMA_OPP` (Einspruch DPMA), `DPMA_BPATG_BESCHWERDE`, `DPMA_BGH_RB`. Currently 0 rules.

| Trigger | Source code | Duration | Anchor |
|---|---|---|---|
| Einspruchsfrist DPMA | `DE.PatG.59.1` | 9 months | publication of grant |
| Erwiderung Patentinhaber | `DE.PatG.59.3` (court-set) | typical 4 months | service of Einspruchsschriftsatz |
| Beschwerde BPatG | `DE.PatG.73.2` | 1 month | service of DPMA-Entscheidung |
| Beschwerdebegründung BPatG | `DE.PatG.75.1` | 1 month (often extended +1) | service of DPMA-Entscheidung |
| Rechtsbeschwerde BGH | `DE.PatG.100` | 1 month | service of BPatG-Entscheidung |
| Begründung Rechtsbeschwerde | `DE.PatG.102.3` (verweist auf ZPO §551) | 1 month | service of BPatG-Entscheidung |

#### 5.2.4 ZPO cross-cutting deadlines — event-trigger-only

Per Q8 (forum dropped) and concept-card UX, these become *cross-cutting concepts* (no proceeding-type pill, only a "Was kommt nach…" pill on the card):

- `wiedereinsetzung` — `DE.PatG.123.2` (2 months from Wegfall, max 12 months from Fristablauf), `DE.ZPO.233` (2 weeks from Wegfall — different!), `EU.EPÜ.122` + `EU.EPC-R.136` (2 months / 12 months), DPMA equivalent.
- `versaeumnisurteil-einspruch` — `DE.ZPO.339` (2 weeks).
- `schriftsatznachreichung` — `DE.ZPO.296a` (court-set, typical 2-3 weeks).
- `mahnverfahren-widerspruch` — `DE.ZPO.345` (2 weeks).

### 5.3 EPA — EPÜ + RPBA gaps

**EP_GRANT** — additions:
- `weiterbehandlung` — `EU.EPÜ.121` + `EU.EPC-R.135` (2 months from loss-of-rights notice).
- `wiedereinsetzung` — `EU.EPÜ.122` + `EU.EPC-R.136` (2 months / max 12 months).
- `teilanmeldung` — `EU.EPÜ.76` + `EU.EPC-R.36.1` (until end of pending parent — anchor is grant date - 1).
- `pruefbescheid-erwiderung` — court-set, typical 4–6 months.
- `validierungsfrist-national` — `EU.EPÜ.65` + national IPÜG (3 months from publication of grant B1).

**EPA_OPP** — additions:
- `einspruch-stellungnahmen-weitere` — `EU.EPC-R.79.2`/`EU.EPC-R.79.3` (court-set).
- `mvor-eingaben-r116` — `EU.EPC-R.116.1` (1 month before oral proceedings, court-set).
- `wiedereinsetzung` (cross-cutting concept).
- `weiterbehandlung` (cross-cutting concept).

**EPA_APP** — additions:
- `beschwerdeerwiderung` — `EU.RPBA.12.1.c` (4 months from service of grounds).
- `eingaben-vor-mvh` — `EU.EPC-R.116.1` + `EU.RPBA.13` (1 month before oral, court-set).
- `antrag-auf-ueberpruefung` — `EU.EPÜ.112a` (2 months from service of decision).

### 5.4 Coverage delta after migration

| Forum | Today | After migration | Net new |
|---|---|---|---|
| UPC trees | 8 trees / 39 rules | 8 trees / ~62 rules | +23 (counterclaim cross-flows) |
| DE trees | 2 trees / 13 rules | 5 trees / ~43 rules | +30 (OLG, BGH-Rev, BGH-NZB, Hinweisbeschluss, DPMA, BPatG-Beschwerde) |
| EPA trees | 3 trees / 18 rules | 3 trees / ~35 rules | +17 (R.116, R.79.2/3, R.106, Wiedereinsetzung, Weiterbehandlung) |
| Cross-cutting concepts (event-trigger) | 102 triggers / 70 deadlines (UPC only) | +20 triggers (Wiedereinsetzung × 4, Versäumnis, Schriftsatzfristen) | +20 |
| Concepts | 0 (none today) | ~30 | +30 (the new layer) |

≈ **+90 new rules / triggers** + **30 concepts**.

---

## 6. Search & filter — backend mechanics

### 6.1 The single API endpoint

```
GET /api/tools/fristenrechner/search?q=<phrase>
                                    &party=<claimant|defendant|both|court>
                                    &proc=<proceeding_code>
                                    &source=<UPC|EU|DE|DE.PatG|DE.ZPO|UPC.RoP|EU.EPÜ|...>
                                    &limit=<int, default 12, max 30>

Response 200:
{
  "query": "klageerwiderung",
  "filters": {"party": null, "proc": null, "source": null},
  "cards": [
    {
      "concept": {
        "id": "<uuid>",
        "slug": "klageerwiderung",
        "name_de": "Klageerwiderung",
        "name_en": "Statement of Defence",
        "description": "Erwiderung des Beklagten auf eine Klageschrift, üblicherweise mit Verteidigungsanträgen und Sachvortrag.",
        "party": "defendant",
        "category": "submission"
      },
      "matched_aliases": ["Statement of Defence", "Erwiderung Klage"],
      "score": 0.96,
      "pills": [
        {
          "kind": "rule",
          "rule_id": "<uuid>",
          "proceeding": {"code": "UPC_INF", "name_de": "Verletzungsverfahren", "jurisdiction": "UPC"},
          "rule_local_code": "inf.sod",
          "legal_source": "UPC.RoP.23.1",
          "legal_source_display": "UPC RoP R.23(1)",
          "duration": {"value": 3, "unit": "months", "alt": null},
          "party": "defendant",
          "drill_url": "/tools/fristenrechner?proc=UPC_INF&focus=inf.sod"
        },
        {
          "kind": "rule",
          "rule_id": "<uuid>",
          "proceeding": {"code": "DE_INF", "name_de": "Verletzungsklage (LG)", "jurisdiction": "DE"},
          "rule_local_code": "de_inf.erwidg",
          "legal_source": "DE.ZPO.276.1",
          "legal_source_display": "ZPO §276(1)",
          "duration": {"value": 6, "unit": "weeks"},
          "party": "defendant",
          "drill_url": "/tools/fristenrechner?proc=DE_INF&focus=de_inf.erwidg"
        },
        // … BPatG, EPA, DPMA pills
      ]
    },
    // … other cards (e.g. "Klageerwiderung mit Nichtigkeitswiderklage" trigger event)
  ],
  "total_cards": 3,
  "total_pills": 7
}
```

### 6.2 Ranking

```sql
WITH hits AS (
  SELECT
    s.concept_id,
    bool_or(  -- alias hit anywhere?
      s.concept_aliases @> ARRAY[lower($q)]
      OR EXISTS (SELECT 1 FROM unnest(s.concept_aliases) a WHERE similarity(a, $q) > 0.4)
    ) AS alias_hit,
    GREATEST(
      max(similarity(s.concept_name_de, $q))   * 1.0,
      max(similarity(s.concept_name_en, $q))   * 1.0,
      max(similarity(s.legal_source,    $q))   * 0.9,
      max(similarity(s.rule_code,       $q))   * 0.9,
      max(similarity(s.rule_name_de,    $q))   * 0.7,
      max(similarity(s.rule_name_en,    $q))   * 0.7
    ) AS field_score
  FROM paliad.deadline_search s
  WHERE (
       s.concept_name_de   % $q
    OR s.concept_name_en   % $q
    OR s.rule_name_de      % $q
    OR s.rule_name_en      % $q
    OR s.legal_source      % $q
    OR s.rule_code         % $q
    OR s.concept_aliases @> ARRAY[lower($q)]
    OR EXISTS (SELECT 1 FROM unnest(s.concept_aliases) a WHERE a % $q)
  )
  AND ($party  IS NULL OR s.effective_party = $party)
  AND ($proc   IS NULL OR s.proceeding_code = $proc)
  AND ($source IS NULL OR s.legal_source LIKE $source || '%')
  GROUP BY s.concept_id
)
SELECT h.concept_id,
       (h.field_score + CASE WHEN h.alias_hit THEN 0.2 ELSE 0 END) AS score
FROM hits h
ORDER BY score DESC
LIMIT $limit;
```

Then for each returned `concept_id` the API does a second, scoped query to fetch all pills for that concept (the JSON shape above). Two queries per request (one for ranked concept ids + one for pills), both indexed.

Tie-break: sort by `concept.sort_order` then alphabetical.

### 6.3 Why mat-view + pg_trgm rather than ES / Meilisearch / Postgres FTS

Same answer as v1: corpus < 1k rows, no new infra, `pg_trgm` already enabled, no tokeniser tax for legal text. The ONE nuance for v2: cards are concept-grouped, so the search query has a GROUP BY rather than a flat ranking. Postgres handles this comfortably at this scale.

### 6.4 i18n of search

Indexes both `concept_name_de` and `concept_name_en` plus `rule_name_de`/`rule_name_en` — a search hit anywhere routes to the right concept. The result card displays the concept name in active locale. Pills always show the structured `legal_source` (locale-independent code) plus the display form (locale-rendered: `§276(1) ZPO` in DE, `§276(1) ZPO` or `Section 276(1) ZPO` in EN — German law sections stay in German per HLC convention).

---

## 7. Migration path — phases A through D

Per m's "augment, not replace" — Phase E (subsumption) from v1 is dropped.

### Phase A — Concept layer + structural additions (purely additive)

- Migration A1: create `paliad.deadline_concepts` + indexes.
- Migration A2: add `concept_id` FK on `deadline_rules`, `legal_source` text on `deadline_rules` and `event_deadlines`, `concept_id` slug-text on `trigger_events`.
- Migration A3: change `deadline_rules.condition_flag` from `text` to `text[]` (Q3); update existing rows. The `Calculate` function gains a small loop change: instead of `if rule.condition_flag matches one flag in flagSet` it becomes `if rule.condition_flag is empty OR all elements of rule.condition_flag are in flagSet`.
- Migration A4: backfill — seed the ~30 concepts; UPDATE `deadline_rules` SET `concept_id = …` per row; backfill `legal_source` from existing rule_code mapping (algorithm: `'RoP 23'` → `'UPC.RoP.23'`, `'§ 276 ZPO'` → `'DE.ZPO.276'`, `'Art. 108 EPÜ'` → `'EU.EPÜ.108'`, etc. — direct seed, no runtime regex).
- Code A5: extend `CalcOptions` with `AnchorOverrides map[string]string` (rule_code → YYYY-MM-DD). The tree-walk in `Calculate` checks `AnchorOverrides[parent.code]` before reading the `computed[parent.code]` map; if present, the override anchors the child. No DB schema change — purely calculator-side. Enables the user-set-custom-date capability per Q4 (m's 23:36 simplification) and reuses for any case where the user knows a real date better than the calculator's projection (court extensions, court-set decisions, post-hoc corrections).
- Code A6: per-row editable date affordance on the result UI. Each rule row's date display becomes click-to-edit (or has a small ✏ icon); editing fires a re-fetch with the override added to the request. Court-set placeholder rows (`IsCourtSet=true`) get the same treatment — user enters the actual decision date once known, downstream reflows.
- No user-visible behaviour change in A1–A5 *for existing rules* — A6's editable affordance is the only UI delta. Existing rules without overrides compute identically.

### Phase B — Coverage migrations (the bulk of new content)

- B1: UPC counterclaim cross-flows on `UPC_INF` and `UPC_REV` (§5.1) with the new condition_flag arrays.
- B2: any remaining UPC rules from Tier 2/3 of t-paliad-084 audit (most already in 031; verify before B2 lands).
- B3: DE_INF/DE_NULL fixes (PatG §111 1mo→3mo) + new proceeding types DE_INF_OLG, DE_INF_BGH, DE_NULL_BGH; add Hinweisbeschluss-Cycle.
- B4: DPMA — DPMA_OPP, DPMA_BPATG_BESCHWERDE, DPMA_BGH_RB.
- B5: EPA — fill EPA_OPP / EPA_APP / EP_GRANT gaps (R.116, R.79.2/3, R.106, Wiedereinsetzung, Weiterbehandlung, Validierungsfristen).
- B6: cross-cutting concept-only rows — Wiedereinsetzung (4 contexts), Versäumnisurteil-Einspruch, Schriftsatznachreichung.

Each B-migration is independently shippable; B2-B6 have no ordering dependency (different proceeding types).

### Phase C — Search backend

- Mat-view + indexes per §4.6.
- New `DeadlineSearchService` (or method on existing `FristenrechnerService`).
- New handler `GET /api/tools/fristenrechner/search`.
- Tests: golden table with ~15 well-known queries → expected ranked concept cards + pill counts.

### Phase D — Search-bar + concept-card UI

- Add search input to top of `frontend/src/fristenrechner.tsx`.
- New client module `frontend/src/client/fristenrechner-search.ts` — debounce, fetch, render concept cards with pills.
- Drill-in: pill click → `?proc=...&focus=...` URL change → calculator opens with proceeding pre-selected, scrolling to focused rule.
- Quick-pick chips above the proceeding tile grid.
- "Vollständige Instanzenkette anzeigen" checkbox below the tile grid.
- URL state for shareable searches.
- The proceeding tile grid stays in place below the search bar (per "augment not replace").

### Out of scope — separate task

**The columns-view sequence-preservation fix** (Q9): undated court-set events currently collapse into one row in the t-paliad-129 columns view. They have an inherent sequence (Counterclaim → Defence → Reply → Decision per `sequence_order`). This is a t-paliad-129 follow-up — the rule data carries `sequence_order` already, and the columns-view renderer (`frontend/src/client/fristenrechner.ts`) just needs to use it for vertical positioning even when a date is missing. **Recommend: separate task t-paliad-132 (or similar) to file after this design lands.** Out of scope for the unified-Fristenrechner core work.

### Full Appeal Chain — implementation note (Q5 locked)

The toggle is a render option; no new "DE_INF_FULL_CHAIN" proceeding type in the data. The frontend has the proceeding-chain mapping baked in:

```ts
const APPEAL_CHAINS: Record<string, string[]> = {
  DE_INF: ["DE_INF", "DE_INF_OLG", "DE_INF_BGH"],
  DE_NULL: ["DE_NULL", "DE_NULL_BGH"],
  DPMA_OPP: ["DPMA_OPP", "DPMA_BPATG_BESCHWERDE", "DPMA_BGH_RB"],
  EPA_OPP: ["EPA_OPP", "EPA_APP"],
  UPC_INF: ["UPC_INF", "UPC_APP"],
  UPC_REV: ["UPC_REV", "UPC_APP"],
};
```

**Anchor handoff (Q5):** the calculator does NOT guess inter-stage gaps. Instead, when the toggle is on the UI renders **one date input per stage anchor + one date input per terminal decision in the chain**:

```
Vollständige Instanzenkette: Verletzungsklage (LG → OLG → BGH)

Stage 1 — LG:
  Klageerhebung am:           [ 2026-05-01 ]   (required)
  Urteil LG am:               [ ___________ ]  (optional — required for stage 2)

Stage 2 — OLG (Berufung):
  Urteil OLG am:              [ ___________ ]  (optional — required for stage 3)

Stage 3 — BGH (Revision/NZB):
  Urteil BGH am:              [ ___________ ]  (n/a — terminal)

[ Berechnen ]
```

If Stage 1's Urteil date is missing, all Stage 2 / Stage 3 deadlines render as IsCourtSet placeholders (same semantic as the existing court-determined-rule path). The user fills in Stage 2's anchor when the LG decision lands. This keeps the calculator honest — no fabricated "+18 months" between instances.

Render concatenates the per-stage timelines with section headers ("LG", "OLG", "BGH"). Each stage's Calculate call is independent.

---

## 8. Test plan

- **Phase A:** schema migrations only; round-trip up/down; verify `condition_flag` text[] cast preserves existing semantics (single-element array still triggers correctly).
- **Phase B1 (counterclaim):** unit tests on `FristenrechnerService.Calculate` for every (with_ccr × with_amend × with_cci) combination per UPC_INF and UPC_REV; assert each new rule appears with the exact dates from a hand-computed example.
- **Phase B3–B6:** golden-date tables — for each new trigger, hard-coded trigger date + expected computed deadline. Pinned against m's spot-check.
- **Phase C:** mat-view query tests — search "Klageerwiderung" returns one concept card with N pills; "RoP 23" returns the UPC card with the R.23 pill; "§ 82" returns the BPatG card; "Wiedereinsetzung" returns one concept with cross-context pills (PatG §123, ZPO §233, EPÜ Art.122, DPMA §123).
- **Phase D:** Playwright smoke — type "klageerwiderung", click first pill, verify proceeding-tree calculator opens with right tree + focused rule highlighted; refresh URL `?q=klageerwiderung` restores card list.

---

## 9. Risks & mitigations

| Risk | Severity | Mitigation |
|---|---|---|
| Backfilling 90+ legal entries — easy to mistype duration / anchor | High | Each B-migration ships one proceeding family; m spot-checks 2-3 rules each; golden-date tests pin every rule |
| Concept slugs drift between migrations | Medium | One canonical slug list maintained at top of `internal/seed/concept_slugs.go` (or similar); every migration references it |
| Mat-view refresh staleness | Low | AFTER triggers refresh CONCURRENTLY (corpus < 1k rows, < 100 ms) |
| Trigram threshold tuning | Medium | Tune via golden-query test set; per-column threshold if needed |
| `condition_flag text → text[]` migration breaks calculator | Low (calculator change is small) | Test with a manual round-trip before merging A3 |
| Two backends staying separate = double-maintenance for new deadlines | Low | Acceptable; the concept layer is the unifier without forcing schema merge |
| `legal_source` format drift between v1 internal `rule_code` and new structured form | Medium | Phase A normalises both: `rule_code` keeps `RoP.029.b` (period-before-letter) shape; `legal_source` uses the new `UPC.RoP.29.b` shape. Map is 1:1 algorithmic. |
| Aliases hand-curation misses search terms | Low | Spot-check during seed; firm-vocabulary edit-window during early access |
| "Full Appeal Chain" — anchor handoff between trees gets wrong | Medium | Per-tree user override of stage anchors as the safety hatch (user can enter the Urteil OLG date directly even when chain mode is on) |

---

## 10. Out of scope (deliberate)

- **AI-driven Frist-Extraktion** from court PDFs (Phase H, deferred).
- **Per-user / firm-level aliases.** v1 is curated-only.
- **Time-versioned `legal_source`.** Single-snapshot of current law.
- **Cross-jurisdiction equivalence claims** as data ("§82 PatG ≈ R.23 UPC"). Search returns both; data does not assert equivalence beyond shared `concept_id`.
- **Linking out to law text** from `legal_source` (mbrian / youpc.org deeplinks). Future iteration.
- **Internal KanzlAI proceeding types** (INF/REV/CCR/APM/APP/AMD/ZPO_CIVIL category='litigation'). Matter-attached; not in this scope.
- **Forum filter** (Q8 — m dropped).
- **Tab subsumption** (Q1 — m dropped; tabs stay).
- **Columns-view sequence preservation** (Q9 — separate task).

---

## 11. Open questions — m's answers (locked)

All v2 open questions resolved by m on 2026-05-04 23:29. Recorded here as the binding spec for the coder shift:

1. **Concept slug naming convention — mixed.** Use **EN slugs** for concepts that exist primarily in UPC / EPC contexts (`application-to-amend`, `request-for-discretionary-review`, `notice-of-appeal-upc`). Use **DE slugs** for concepts that only exist in German law (`nichtzulassungsbeschwerde`, `versaeumnisurteil-einspruch`, `hinweisbeschluss-stellungnahme`). For **shared concepts that exist in both DE and UPC/EPC** (e.g. Klageerwiderung exists in ZPO §276, PatG §82, UPC R.23, EPA R.79): use the **DE slug** because (a) m works primarily in German, (b) the slug is internal/maintenance-facing only, (c) `name_de` and `name_en` columns carry both labels for the user-facing surface, and (d) it sidesteps EN-translation arguments ("Defence" vs "Statement of Defence" vs "Reply to Application"). Resulting slug examples: `klageerwiderung` (shared, DE wins), `replik` (shared, DE wins), `berufungsfrist` (shared, DE wins), `application-to-amend` (UPC/EPC native, EN), `wiedereinsetzung` (shared cross-cutting, DE wins because the German name dominates HLC vocabulary).

2. **`legal_source` namespace — `EU.` for EPÜ.** Confirmed. Format stays `EU.EPÜ.108`, `EU.EPC-R.79.1`, `EU.RPBA.12.1.c`.

3. **DE_NULL Berufungsbegründung 1 → 3 months — confirmed.** Ship the fix as part of Phase B3. Test pin: `de_null.beruf_begr` 1mo → 3mo, `legal_source = 'DE.PatG.111.1'`.

4. **PatG §82(1) — keep simple seed; user overrides the date inline.** m's revised direction (23:36): drop the customizable-extension flag mechanism — *"sounds complicated, I just want to be able to set a custom date and following deadlines calculate from there."*

   Generalised capability instead: **any computed deadline date in the result is user-overridable**, and downstream rules that chain off it re-compute from the override. So PatG §82's "1 month + court extension to 5 weeks" case is handled by the user typing the actual extended date into the result row, and Replik / Duplik re-flow off it.

   PatG §82(1) seed stays at 2 months (the practical typical) with `deadline_notes` "1 Monat Grundfrist + bis +1 Monat richterliche Verlängerung typisch". No `with_extension` flag, no `flag_param` mechanism. Cleaner for everyone.

   **Calculator change for the override capability:** `CalcOptions` gains an `AnchorOverrides map[string]string` field (rule_code → YYYY-MM-DD). The tree-walk loop in `Calculate` checks `AnchorOverrides[parent.code]` before reading `computed[parent.code]` — if present, that override anchors the child rule.

   **UI change for the override capability:** each result row's date display becomes click-to-edit (or has a small ✏ icon). Editing fires a re-fetch with the override added to the request. The court-set placeholder rows (existing `IsCourtSet=true` rendering) get the same treatment — the user can type the actual decision date once it's known, and downstream deadlines reflow.

   Implementation cost: small. CalcOptions field + 5-line lookup in the tree walk + per-row edit affordance in the timeline / columns view.

5. **Full Appeal Chain — multiple date inputs, decisions need a date.** Confirmed shape (b). When toggle is on, the UI renders **one date input per stage** plus required date inputs for each terminal decision in the chain (LG Urteil, OLG Urteil, BGH Urteil). The intra-stage deadlines compute off the relevant stage anchor; inter-stage handoff is user-entered, never guessed. If the user hasn't yet got a stage's terminal decision date, that stage's downstream deadlines render as IsCourtSet placeholders — same semantic as the existing isCourtDeterminedRule path. Worth noting: this means the Full Appeal Chain isn't a single "calculate-from-one-date" tool; it's a multi-stage timeline view that the user fills in as the case progresses.

6. **Concept description copy — drafted in seed migration, reviewed by m or colleague.** Confirmed. ~30 concepts × 1-2 sentences each; PR-1 will include them; m or a colleague reviews on the PR.

7. **Concept-level `party` = dominant case, per-rule overrides.** Confirmed. Pill displays the per-rule value (e.g. "Patentinhaber" on the EPA_OPP pill of the Klageerwiderung concept, even though concept-level party is "Beklagte" because that's the dominant case).

8. **Quick-pick chip seed — 8 chips approved.** Klageerwiderung · Berufung · Einspruch · Replik · Beschwerde · Statement of Defence · Schadensbemessung · Wiedereinsetzung. Hot-tunable in a follow-up later if telemetry shows different chip preferences.

**Net effect on coder shift scope:** Phase A4 picks up the `flag_param` calculator extension (small code change) so PatG §82's customizable-extension shape works when Phase B3 lands. Otherwise the coder spec is unchanged from §7 above.

---

## 12. Proposed cycle

- **Inventor (this shift, cronus, branch `mai/cronus/unified-fristenrechner-design`):** v2 design doc + go/no-go gate (this commit).
- **Coder (next shift, after m's go-ahead):**
  - PR-1 = Phase A1+A2+A3+A4 (concept layer + structural additions + backfill). Smallest first; verifies the concept slug list and the `condition_flag` array migration in isolation.
  - PR-2 = Phase B1 (UPC counterclaim cross-flows). Closes m's primary complaint.
- **Coder (third shift):** B3 + B4 + B5 + B6 (DE + DPMA + EPA + cross-cutting). Each as a separate PR for spot-checkability, parallelisable.
- **Coder (fourth shift):** Phase C + D (search infra + UI).
- **Separately filed:** t-paliad-132 (or similar) — columns-view sequence preservation per Q9.

I'd recommend **curie** as implementer (port-heavy, careful seed work — same shape as t-paliad-084 / t-paliad-086 audit + import sequence). Alternative for B1 only: **fritz** (bug-fix shape). Head decides.

---

## Appendix A — Files & references

**Code paths read:**
- `internal/services/fristenrechner.go` (proceeding-tree calculator, CalcOptions extension, `isCourtDeterminedRule` helper)
- `internal/services/event_deadline_service.go` (trigger-event calculator, composite-rule resolver, working_days unit)
- `internal/services/deadline_rule_service.go` (CRUD over `paliad.deadline_rules`)
- `internal/services/holidays.go` (DB-driven holidays + UPC vacation gating from t-paliad-121)
- `frontend/src/fristenrechner.tsx` (current 252-line UI shell — proceeding tile grid + 2-tab shell)
- `frontend/src/client/fristenrechner.ts` (1031-line client, both modes)
- migrations 012 / 028 / 029 / 030 / 031 / 033 / 034 / 035 / 036

**DB read:**
- `paliad.proceeding_types` (19 rows, 12 fristenrechner-category)
- `paliad.deadline_rules` (74 rules in fristenrechner trees, 96 total including KanzlAI)
- `paliad.trigger_events` (102) + `paliad.event_deadlines` (70) + `paliad.event_deadline_rule_codes` (72, 64 distinct UPC RoP codes)
- `data.laws_contents` (UPCRoP/UPCA/UPCS texts in EN — used for §5.1 verbatim quotations)

**Prior work:**
- `docs/audit-fristenrechner-completeness-2026-04-30.md` (curie's t-paliad-084 audit — §5 builds on it)
- t-paliad-086 (3-PR shipped: trigger-event Fristenrechner + composite rules + Tier 1 fixes)
- t-paliad-101 / t-paliad-111 / t-paliad-112 (QA bug bundles)
- t-paliad-121 (UPC vacation no-shift)
- t-paliad-127 / t-paliad-129 (columns view + polish)
- t-paliad-088 (Event Types design — relationship: Event Types is matter-attached deadline tagging; this design's concept layer is the calculator-side equivalent. They could share concept slugs eventually but ship independently.)

**Memory references:**
- `paliad t-paliad-084 Fristenrechner audit`
- `paliad t-paliad-086 Fristenrechner youpc-parity`
- `paliad t-paliad-111 SHIPPED — bug-bundle correctness`
- `paliad t-paliad-110 SHIPPED — Fristen+Termine unification`
- `Design: Event Types for deadlines + submissions (t-paliad-088, cronus)`
