# Design — `paliad.proceeding_types` taxonomy cleanup: primary proceedings vs phases vs side-actions vs meta

**Task:** t-paliad-324
**Gitea:** m/paliad#147
**Inventor:** atlas (shift-1)
**Date:** 2026-05-26
**Status:** Draft — coder gate held until m ratifies the 10 design questions in §9
**Branch:** `mai/atlas/inventor-proceeding`

---

## 0. Premises verified live (before designing)

Verified against live youpc Postgres (port 11833, `paliad` schema) on 2026-05-26 22:05. Findings supersede the audit grouping in m/paliad#147 wherever they diverge — the issue body was correct on shape but conservative on counts.

### 0.1 The 46-row table, fully classified by usage

`paliad.proceeding_types` has 49 rows total; 46 active, 3 inactive (`upc.apl.merits/cost/order` — superseded by `upc.apl.unified`, id 160) plus 1 archive bucket (`_archived_litigation`, id 32). Cross-references against the four downstream consumers:

| Consumer | Column | Active rows that point at the 46 active types |
|---|---|---|
| `paliad.sequencing_rules.proceeding_type_id` | rule's anchor proceeding | **18 distinct rows used** — the primaries with corpus. 28 rows have 0 rules. |
| `paliad.sequencing_rules.spawn_proceeding_type_id` | cross-proceeding spawn target | **1 distinct row used** — `upc.apl.merits` (id=11, **inactive!**). 0 active types are spawn targets. |
| `paliad.projects.proceeding_type_id` | project's primary type | **6 distinct rows used** (across 18 projects). All 6 are in the 18 primaries. |
| `paliad.event_category_concepts.proceeding_type_code` | concept's owning proceeding | **18 distinct codes used.** 3 of those codes (`upc.apl.merits`, `upc.apl.order`, `upc.apl.cost`) point at **inactive** rows — pre-existing data drift from the `upc.apl.unified` merger (flagged §8, out of scope here). |

The audit answer in one sentence: **of the 46 active rows, only 18 have any downstream consumer pointing at them today** (the 18 primaries with corpus). The remaining 28 rows are decorative — they exist in the table but nothing references them.

This makes reparenting **trivially safe**: no FK invariant breaks, no SQL update touches existing data, no migration risk.

### 0.2 The 18 primaries with corpus (rules + concepts)

Ordered by `paliad.sequencing_rules` count (descending), with `event_category_concepts` count alongside:

| id | code | jurisdiction | rules | concepts | projects |
|---:|---|---|---:|---:|---:|
| 8 | `upc.inf.cfi` | UPC | 25 | 14 | 1 |
| 9 | `upc.rev.cfi` | UPC | 17 | 10 | 0 |
| 160 | `upc.apl.unified` | UPC | 16 | 0 *(see drift note)* | 0 |
| 12 | `de.inf.lg` | DE | 11 | 4 | 1 |
| 13 | `de.null.bpatg` | DE | 10 | 4 | 1 |
| 14 | `epa.opp.opd` | EPA | 8 | 7 | 1 |
| 15 | `epa.opp.boa` | EPA | 8 | 12 | 0 |
| 16 | `epa.grant.exa` | EPA | 8 | 0 | 0 |
| 17 | `upc.dmgs.cfi` | UPC | 8 | 1 | 0 |
| 26 | `de.inf.bgh` | DE | 8 | 17 | 0 |
| 25 | `de.inf.olg` | DE | 7 | 8 | 0 |
| 10 | `upc.pi.cfi` | UPC | 7 | 3 | 0 |
| 27 | `de.null.bgh` | DE | 6 | 10 | 0 |
| 29 | `dpma.appeal.bpatg` | DPMA | 5 | 6 | 0 |
| 30 | `dpma.appeal.bgh` | DPMA | 4 | 8 | 0 |
| 28 | `dpma.opp.dpma` | DPMA | 4 | 3 | 1 |
| 18 | `upc.disc.cfi` | UPC | 4 | 1 | 0 |
| 35 | `upc.ccr.cfi` | UPC | 1 | 0 | 1 |

These 18 are unambiguously **primary proceedings** in the m/paliad#147 sense — self-contained matters, own filing, own deadline cascade, own ablauf. They survive every model.

### 0.3 The 4 unloaded primaries (Group A continued)

Four more active rows are conceptually primaries but carry **zero rules and zero concepts today** — seeded for catalog completeness, waiting for corpus:

| id | code | jurisdiction | what it is |
|---:|---|---|---|
| 171 | `upc.dni.cfi` | UPC | Negative Feststellungsklage — standalone declaratory action |
| 172 | `upc.epo.review` | UPC | Überprüfung von EPA-Entscheidungen — standalone review action |
| 179 | `upc.bsv.cfi` | UPC | Beweissicherung / saisie — standalone evidence-preservation order |
| 188 | `upc.pl.cfi` | UPC | Schutzschrift — pre-litigation defensive filing |

These are **primary** by character (each has its own RoP-defined filing pathway and its own deadline tree once rules get seeded) but **unloaded** today. Decision: keep them as `kind='proceeding'` so Mode B R3 surfaces them for future rule attachment and `pkg/litigationplanner` accepts them as valid catalog codes.

§9 Q3.b discusses `upc.pl.cfi` (it's the only borderline — Schutzschrift is technically a pre-action filing, not a proceeding at the time of filing). m's call.

### 0.4 The 28 non-primary rows

The 28 active rows that have **zero rules + zero concepts + zero projects pointing at them** group cleanly into three categories:

#### Group B — Phases of a primary CFI proceeding (5 rows)

These describe stages *within* an existing CFI proceeding, not standalone matters. A `upc.inf.cfi` action passes through interim → oral → decision phases; the phase isn't a separately-elected proceeding type.

| id | code | name |
|---:|---|---|
| 173 | `upc.cfi.interim` | CFI - Zwischenverfahren |
| 174 | `upc.cfi.oral` | CFI - Mündliche Verhandlung |
| 175 | `upc.cfi.decision` | CFI - Endentscheidung |
| 176 | `upc.costs.cfi` | Separate Kostenentscheidung *(post-decision sub-phase)* |
| 185 | `upc.default.cfi` | Versäumnisentscheidung *(alt. decision outcome)* |

The "phase" concept already has a natural home in the data model: `paliad.procedural_events.event_kind` (filing/hearing/decision/order). What `upc.cfi.interim` actually represents is "all events with kind=filing under upc.inf.cfi/upc.rev.cfi/upc.pi.cfi/etc."; `upc.cfi.oral` is "all events with kind=hearing"; `upc.cfi.decision` is "all events with kind=decision". The proceeding-type row buys nothing the event_kind already carries.

#### Group C — Side-actions inside a proceeding (10 rows)

Applications and court orders that arise *inside* a primary proceeding. They could each become a `condition_expr`-gated rule on the parent proceeding when corpus arrives; they don't need their own proceeding row.

| id | code | name |
|---:|---|---|
| 178 | `upc.evidence.cfi` | Beweisanordnungen (allgemein) |
| 182 | `upc.experiments.cfi` | Gerichtlich angeordnete Versuche |
| 177 | `upc.security.cfi` | Sicherheitsleistung |
| 184 | `upc.intervention.rop` | Streitbeitritt |
| 165 | `upc.parties.change` | Parteiwechsel / Patentübergang |
| 170 | `upc.optout.cfi` | Antrag auf Opt-out |
| 180 | `upc.inspection.cfi` | Besichtigungsantrag |
| 181 | `upc.freezing.cfi` | Anordnung zur Vermögenssperre |
| 187 | `upc.withdrawal.rop` | Klagerücknahme |
| 183 | `upc.rehearing.coa` | Wiederaufnahmeantrag |

A subtle distinction: `upc.bsv.cfi` (Beweissicherung) IS a standalone primary (its own RoP filing) whereas `upc.evidence.cfi` (Beweisanordnungen allgemein) is a side-action class (orders the court makes inside any proceeding). The two are not duplicates; the categorisation is structural, not nominal.

#### Group D — Cross-cutting administrative / meta (8 rows)

These describe rules-of-procedure mechanics, not matters a lawyer takes on. None of them is a "Verfahren" in any user-facing sense.

| id | code | name |
|---:|---|---|
| 162 | `upc.case.mgmt` | Verfahrensverwaltung |
| 161 | `upc.general.rop` | Allgemeine Bestimmungen |
| 163 | `upc.service.rop` | Zustellung von Schriftsätzen |
| 168 | `upc.language.rop` | Verfahrenssprache |
| 164 | `upc.representation.rop` | Vertretung / Anwaltsprivileg |
| 166 | `upc.fees.court` | Gerichtsgebühren |
| 167 | `upc.legalaid.cfi` | Prozesskostenhilfe |
| 186 | `upc.special.cfi` | Besondere Verfahrenslagen |
| 169 | `upc.reestablishment.rop` | Wiedereinsetzung in den vorigen Stand *(cross-cutting; applies to every proceeding)* |

`upc.reestablishment.rop` lands in Group D because **every** proceeding has a Wiedereinsetzung path — it isn't a kind-of-proceeding, it's a cross-cutting remedy. Today's rules already model it correctly (it's a `condition_expr`-gated rule on each primary, not a separately-elected proceeding type).

### 0.5 Counts reconciled

| Group | Count | Total of 46 |
|---|---:|---:|
| A.1 Primary with corpus (18 rows) | 18 | |
| A.2 Primary, unloaded (4 rows) | 4 | |
| B Phases (5 rows) | 5 | |
| C Side-actions (10 rows) | 10 | |
| D Meta / cross-cutting (9 rows) | 9 | |
| **Total** | | **46 ✓** |

m/paliad#147's audit listed 8 Group-D rows; live data shows 9 once `upc.reestablishment.rop` is moved into the meta bucket (it appeared as ambiguous "cross-cutting admin / meta" — confirming this design's read).

---

## 1. Categorization — ratified

The taxonomy proposal: a row in `paliad.proceeding_types` has exactly one of four **structural kinds**.

| `kind` | What it is | Visible in Mode B R3 wizard? | In `pkg/litigationplanner` catalog? | Eligible for `projects.proceeding_type_id`? |
|---|---|---|---|---|
| `proceeding` | A self-contained matter with its own filing pathway and its own deadline tree | **Yes** | **Yes** (filtered by `kind='proceeding' AND is_active=true`) | **Yes** |
| `phase` | A stage *within* a primary proceeding | No | No | No |
| `side_action` | An application/order that arises inside a primary proceeding | No | No | No |
| `meta` | RoP mechanics, cross-cutting rules, court administration | No | No | No |

This is **Model 1 from m/paliad#147** (kind discriminator on `proceeding_types`). §2 explains why it beats Models 2-4 for the actual data.

The 46 active rows map to the 4 kinds as follows:

- **`proceeding` (22 rows):** all 18 primaries-with-corpus + the 4 unloaded primaries from §0.3. Specifically the union of §0.2 + §0.3.
- **`phase` (5 rows):** the §0.4 Group B list.
- **`side_action` (10 rows):** the §0.4 Group C list.
- **`meta` (9 rows):** the §0.4 Group D list (incl. `upc.reestablishment.rop`).

### 1.1 Edge calls

- **`upc.ccr.cfi` (id 35)** — stays `kind='proceeding'` with the existing routing-to-`upc.inf.cfi` from t-paliad-204 §0.3 S1 (the determinator surfaces it, the mapping returns inf.cfi's id with `with_ccr=true`). Rationale: the routing layer is already built and m ratified it 2026-05-18. This design does not re-open that decision. §9 Q7 lets m revisit.
- **`upc.pl.cfi` (Schutzschrift, id 188)** — borderline. Schutzschrift is filed *before* a proceeding exists; it's a defensive pre-litigation filing. Recommendation: keep as `kind='proceeding'` (it has its own RoP path + its own deadlines once seeded). The alternative — calling it `side_action` of a not-yet-existing inf.cfi — is semantically backwards. §9 Q3.b lets m revisit.
- **`upc.bsv.cfi` (saisie, id 179)** vs **`upc.evidence.cfi` (id 178)** — bsv stays `kind='proceeding'` (own RoP filing under R.192-198), evidence stays `kind='side_action'` (the orders a court makes inside any proceeding under R.190). The codes are not duplicates.

### 1.2 What the categorisation buys

- **Mode B R3 (Fristenrechner overhaul, t-paliad-322)** queries `proceeding_types WHERE is_active AND kind='proceeding'` and gets a clean 22-row pick list — no phase/side-action/meta noise.
- **`projects.proceeding_type_id` integrity** is enforceable: an FK + CHECK (or a triggered constraint, see §3.3) blocks setting a project's type to anything except `kind='proceeding'`.
- **`pkg/litigationplanner` snapshot generator** filters identically; youpc.org's catalog stays UPC-primary-only with no leakage of phase/admin rows.
- **Determinator + dropdowns** get a forward-compatible filter; future feature work (e.g. "show me all side-actions available in this proceeding") becomes a different query against the same table.
- **Forward-compatibility for new rows** — when corpus for a side-action arrives (e.g. `upc.evidence.cfi` gains 4 sequencing_rules with `condition_expr='evidence_order_issued'`), the rules anchor on the *parent* primary, not on the side-action row. The kind classification stays correct; the side-action row remains a taxonomic label.

---

## 2. Model choice — Model 1 (kind discriminator)

### 2.1 The four candidate models, scored

| Model | Schema churn | Models phase parentage? | Mode B R3 filter | Migration risk | Verdict |
|---|---|---|---|---|---|
| **1. `kind` discriminator on `proceeding_types`** | One column + CHECK constraint | No, but doesn't need to | `WHERE kind='proceeding'` | Trivial — UPDATE only | **Recommended** |
| 2. Self-referencing `parent_id` | One column + FK + CHECK | Yes, but parentage is wrong shape (phases are phase-of-EVERY-CFI, not of one) | `WHERE parent_id IS NULL` | Trivial | Over-modelled |
| 3. Separate tables | Three new tables + view/JOINs | Yes, fully | Just query `proceeding_types` | Migration churn + every consumer query learns a new shape | Overkill for 28 unused rows |
| 4. Move phases into `procedural_events` | One mass row-move + DELETE | n/a (phases vanish from `proceeding_types`) | Trivial | Highest — would touch event_kind taxonomy and Fristenrechner result-view structure | Wrong shape (phases ≠ events) |

### 2.2 Why Model 1 wins

The fundamental observation: **the 28 non-primary rows have zero downstream pressure**. No rule, no project, no concept, no spawn FK references them. They exist in the table as taxonomic placeholders — names someone wrote down so future corpus could attach. We don't need to physically restructure the table; we just need to label what's what so consumers can filter correctly.

Model 1 gives us exactly that with one column. The other models pay schema/migration cost to model a parent-child relationship that **no consumer queries**. Mode B R3 doesn't ask "what are the phases of upc.inf.cfi?" — it asks "what are the proceedings I can pick?". The Fristenrechner result view doesn't ask the proceeding-types table about phases — phases live inside `procedural_events.event_kind` and the priority-bucket sub-sections in the §4.2 of the Fristenrechner overhaul doc.

Model 2's `parent_id` is wrong in shape: `upc.cfi.interim` doesn't have ONE parent (`upc.inf.cfi`), it has SEVEN parents (every CFI proceeding). Modelling that as a self-reference would force either (a) duplicating the phase rows per primary, or (b) using NULL parent_id for "applies to all". Both options are uglier than just dropping parent_id and trusting `kind='phase'`.

Model 3's separate tables would create rich relations that no consumer reads. Premature relational normalisation.

Model 4 would force phases into `procedural_events`, but phases aren't events. A phase is a *bucket of events*. The bucket is already implicit in the `event_kind` column (filing → interim, hearing → oral, decision → decision). If anything, Model 4 is *backwards* — phases should disappear into `event_kind`, not become event rows. The way to "delete" the phase rows from proceeding_types is just to deactivate them (or mark them `kind='phase'`); we don't need to re-locate them into another table to claim that conceptual move.

### 2.3 What we don't do — physical deletion

The 28 non-primary rows are NOT dropped from the table. They:

- Get tagged with the right `kind` value.
- Optionally get `is_active=false` flipped (m's call, §9 Q9).
- Stay in the table so consumers that historically referenced them by id (admin tools, audit logs, future schema-rescue scripts) keep working.

`DROP` is a one-way door we don't need to walk through. The CHECK constraint + kind tagging gives us the same logical cleanliness with none of the irreversibility risk.

---

## 3. Schema sketch + migration plan

### 3.1 DDL — the new column

```sql
-- Migration NNN_proceeding_types_kind.up.sql
-- (NNN = whatever MAX(version) + 1 is at write time; see project-status.md
--  for the live numbering. As of 2026-05-26 the head is mig 152 per the
--  recent dedupe of identical sequencing_rule clones.)

ALTER TABLE paliad.proceeding_types
  ADD COLUMN kind text NOT NULL DEFAULT 'proceeding'
    CHECK (kind IN ('proceeding', 'phase', 'side_action', 'meta'));

COMMENT ON COLUMN paliad.proceeding_types.kind IS
  'Structural classification — see docs/design-proceeding-types-taxonomy-2026-05-26.md §1. '
  'proceeding = self-contained matter (own filing + deadline tree); '
  'phase = stage inside a primary CFI proceeding; '
  'side_action = application/order inside a proceeding; '
  'meta = RoP mechanics, court admin, cross-cutting remedies.';

CREATE INDEX proceeding_types_kind_active_idx
  ON paliad.proceeding_types(kind, is_active)
  WHERE is_active = true;
```

The DEFAULT keeps existing inserts (admin tooling, snapshot tests) safe: any new row defaults to `proceeding`. The CHECK enforces the vocabulary at write time.

### 3.2 Data move — UPDATE statements, no INSERT/DELETE

```sql
-- Phases (per m's Q2 carve-out: upc.costs.cfi (176) is NOT a phase, it stays primary)
UPDATE paliad.proceeding_types
  SET kind = 'phase'
  WHERE id IN (173, 174, 175, 185);  -- §0.4 Group B minus 176

-- Side-actions
UPDATE paliad.proceeding_types
  SET kind = 'side_action'
  WHERE id IN (178, 182, 177, 184, 165, 170, 180, 181, 187, 183);  -- §0.4 Group C

-- Meta / cross-cutting
UPDATE paliad.proceeding_types
  SET kind = 'meta'
  WHERE id IN (162, 161, 163, 168, 164, 166, 167, 186, 169);  -- §0.4 Group D

-- Primaries (incl. m's Q2 carve-out for upc.costs.cfi) stay on the DEFAULT
-- 'proceeding' value — no UPDATE needed.

-- Per m's Q9: deactivate the non-primary rows so the admin list surfaces only
-- primaries. The kind column carries the semantic info; is_active controls UI
-- visibility. Reversible — flip is_active back on if a row gains corpus.
UPDATE paliad.proceeding_types
  SET is_active = false
  WHERE kind IN ('phase', 'side_action', 'meta');
```

Per m's Q9, the `is_active=false` flip is mandatory in this mig. After it: 23 active rows (all `kind='proceeding'`), 23 inactive rows (the phase/side_action/meta set), in addition to the pre-existing inactive appeal-triplet + archived bucket. The `kind` column tells consumers what each row IS; `is_active` tells consumers whether to show it.

### 3.3 Optional integrity constraints

If m wants stronger guarantees that `projects.proceeding_type_id` can only point at primaries, add a deferrable FK validator. Cleanest pattern in Postgres:

```sql
-- Option A: trigger-based check (works for any kind set, deferred-friendly).
CREATE OR REPLACE FUNCTION paliad.assert_project_type_is_proceeding()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF NEW.proceeding_type_id IS NOT NULL THEN
    PERFORM 1 FROM paliad.proceeding_types
      WHERE id = NEW.proceeding_type_id AND kind = 'proceeding';
    IF NOT FOUND THEN
      RAISE EXCEPTION 'projects.proceeding_type_id must reference a kind=proceeding row, got id=%', NEW.proceeding_type_id
        USING ERRCODE = '23514';
    END IF;
  END IF;
  RETURN NEW;
END $$;

CREATE TRIGGER projects_proceeding_type_kind_check
  BEFORE INSERT OR UPDATE OF proceeding_type_id ON paliad.projects
  FOR EACH ROW EXECUTE FUNCTION paliad.assert_project_type_is_proceeding();
```

Per m's Q8: **trigger on `projects` only**, no symmetric enforcement on `sequencing_rules`. Projects are written via the public app (the surface most exposed to operator error); rules are edited via the admin `/admin/procedural-events` surface which already validates against active+published lifecycle. The single trigger is enough.

### 3.4 Migration sequencing — single self-contained mig

One migration file:

```
internal/db/migrations/153_proceeding_types_kind.up.sql
internal/db/migrations/153_proceeding_types_kind.down.sql
```

Up does ALTER + UPDATE + (optional) trigger creation. Down does DROP COLUMN (cascading the trigger if present). No data loss on either direction — the kind column is purely additive.

Mig number depends on what knuth lands first; the coder reads `MAX(version)` at write time per the project's mig conventions.

---

## 4. FK reparenting tables

There is no reparenting to do. Below for completeness:

| Source table.column | Pointing at non-primary rows? | Action |
|---|---|---|
| `sequencing_rules.proceeding_type_id` | **0 active rules** (verified §0.1) | None |
| `sequencing_rules.spawn_proceeding_type_id` | **0 active rules** point at non-primaries; 4 active rules point at id=11 (inactive `upc.apl.merits`) | Pre-existing drift, out of scope (§8) |
| `projects.proceeding_type_id` | **0 projects** (all 6 distinct values are primaries) | None |
| `event_category_concepts.proceeding_type_code` | **0 concepts** point at non-primary codes; 30 concepts point at `upc.apl.merits/order/cost` codes (which are inactive but conceptually primaries) | Pre-existing drift, out of scope (§8) |

The "FK reparent" section of the acceptance criteria in m/paliad#147 is a no-op for this design: the 28 rows being re-classified have **no incoming references** to reparent. The migration is pure relabelling.

---

## 5. Worked example — `upc.cfi.interim` after the mig

### 5.1 Today (broken)

Someone created the row `upc.cfi.interim` (id 173, name "CFI - Zwischenverfahren") in `paliad.proceeding_types` with `category='fristenrechner'`. The intent was probably "we'll attach interim-phase rules here later". Result:

- The row appears in the Mode B R3 wizard chip strip (if R3 queries `WHERE is_active=true AND jurisdiction='UPC'`) — confusing to the user, because "Zwischenverfahren" is not a proceeding they pick; it's a stage their proceeding passes through.
- The row could be set as `projects.proceeding_type_id` (no FK constraint forbids it today) — corrupting the SmartTimeline's lane logic, which assumes the project's type is a primary.
- The row appears in admin /admin/proceeding-types lists, polluting the primary-proceedings overview.

### 5.2 After mig 153

The migration runs:

```sql
UPDATE paliad.proceeding_types SET kind = 'phase' WHERE id = 173;
-- Optionally: UPDATE paliad.proceeding_types SET is_active = false WHERE id = 173;
```

Now:

- Mode B R3 query becomes `WHERE is_active=true AND jurisdiction = $1 AND kind='proceeding'`. `upc.cfi.interim` is filtered out — it is not a "Verfahren" the user can pick.
- A future admin who tries to set a project's `proceeding_type_id = 173` either fails the optional trigger from §3.3 (with a clear error) or gets a code-level rejection from `ProjectService.SetProceedingType` (which the coder will harden to filter by `kind='proceeding'`).
- The `pkg/litigationplanner` snapshot generator filter becomes `WHERE is_active=true AND category='fristenrechner' AND kind='proceeding' AND jurisdiction IN ('UPC')`. The row never makes it into the youpc.org catalog.

The row itself stays in the database. Its id is stable. Future work that wants to *use* the phase row as a taxonomic label (e.g. "show me which event_kinds map to which UPC phases") gets a clean shape: query `WHERE kind='phase' AND code LIKE 'upc.cfi.%'`.

### 5.3 Where interim-phase deadlines actually live

The user-facing concept "interim phase" is already modelled correctly, just elsewhere:

- A `procedural_events` row like `upc.inf.cfi.soc` (Statement of Claim) has `event_kind='filing'`. The Fristenrechner overhaul (t-paliad-322 §4.2) groups follow-ups by priority + presents them under the trigger card. There is no UI element that needs a "Zwischenverfahren" proceeding-type label to operate.
- A future "show me the full ablauf of UPC inf, broken down by phase" feature can derive phases from `procedural_events.event_kind` ordering + the rule sequence_order. The `proceeding_types` table doesn't need to carry the phase labels.

---

## 6. Consumer impact

### 6.1 `projects.proceeding_type_id`

| Concern | Before | After mig 153 |
|---|---|---|
| Valid values | Any active proceeding_types row | Any `kind='proceeding'` active row (22 rows) |
| Enforcement | None at DB level | Optional trigger (§3.3 / §9 Q8) |
| Code-level filter in ProjectService | No filter on kind | Filter to `kind='proceeding'` when listing pickable types |
| Existing data | 6 distinct values (all in 22) | No change — all 6 are kind='proceeding' |
| SmartTimeline lane logic | Assumes primary-proceeding shape | Assumption now FK-enforceable |

**No data migration on existing projects.** The 6 currently-used proceeding types are all in the primary set.

### 6.2 `sequencing_rules.proceeding_type_id` + `spawn_proceeding_type_id`

| Concern | Before | After mig 153 |
|---|---|---|
| `proceeding_type_id` valid values | Any active row | Any active row (no enforcement change; admin curation suffices) |
| `spawn_proceeding_type_id` valid values | Any active row | Same — spawns conceptually must point at a primary, but enforcement stays in admin tooling |
| Existing data | 157 rules anchored on 18 primaries | No change — all 157 already on `kind='proceeding'` rows |
| `id=11 spawn pressure` (`upc.apl.merits`, inactive) | 4 active spawn rules point here | Pre-existing drift, out of scope (§8) |

No `sequencing_rules` table changes accompany this mig. The post-mig invariant **"every active rule's `proceeding_type_id` is a `kind='proceeding'` row"** holds without any UPDATE.

### 6.3 Fristenrechner Mode B R3 (t-paliad-322, knuth's S3+)

§3.2 R3 of the Fristenrechner overhaul says:

> Chips: every active `proceeding_type` whose jurisdiction matches R2 AND whose event roster contains at least one event with R1's kind.

After mig 153, the R3 query gains one more AND-clause:

```sql
SELECT pt.id, pt.code, pt.name, pt.name_en, pt.sort_order
FROM paliad.proceeding_types pt
WHERE pt.is_active = true
  AND pt.kind = 'proceeding'        -- NEW
  AND pt.jurisdiction = $1          -- from R2
  AND EXISTS (
    SELECT 1 FROM paliad.sequencing_rules sr
    JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
    WHERE sr.proceeding_type_id = pt.id
      AND pe.event_kind = $2        -- from R1
      AND sr.is_active = true
  )
ORDER BY pt.sort_order, pt.code;
```

The `kind='proceeding'` filter is the only line that changes. Knuth's S3 implementation reads from this query; the chip pool shrinks from "all 35 active UPC types" to "the 14 primary UPC types that have rules" (still narrowed further by R1's event_kind via the EXISTS subquery).

No coder churn beyond adding the AND-clause. The mig 153 lands either alongside knuth's S3 work or independently (§7 sequencing decision).

### 6.4 Litigation Planner suite (t-paliad-292)

The package's catalog snapshot generator (`pkg/litigationplanner/scripts/snapshot/main.go`) currently filters:

```go
// scripts/snapshot/main.go
const proceedingTypesQuery = `
  SELECT id, code, name, name_en, jurisdiction, default_color, sort_order, display_order,
         trigger_event_label_de, trigger_event_label_en
  FROM paliad.proceeding_types
  WHERE is_active = true
    AND category = 'fristenrechner'
    AND jurisdiction = $1
`
```

After mig 153, this query gains the same `AND kind = 'proceeding'` line. The UPC snapshot shrinks from "potentially 35 rows" to a clean primary-only set. Today's snapshot probably already includes the phase/side-action/meta rows (since `is_active=true` is true for all of them) — depending on whether a snapshot has been regenerated since the 161-188 rows landed, the embedded JSON may be carrying decorative rows that the youpc.org catalog never resolves to rules. Mig 153 + a snapshot regen cleans this up.

The package's `Catalog.Proceeding(ctx, code, hint)` interface stays unchanged. A youpc-side call asking for `code='upc.cfi.interim'` previously returned the row + zero rules (technically valid but useless); after mig 153 the snapshot doesn't include it and the call returns `ErrUnknownProceedingType`. That's the correct shape — youpc users never had a reason to ask for a phase row.

The scenarios design (`paliad.scenarios.spec.proceedings[].code`) gains an integrity check at write time: the validator already asserts every code resolves to an active proceeding; now it additionally asserts `kind='proceeding'`. A user trying to compose a scenario with `code='upc.cfi.interim'` gets a clear error. (The validator is paliad-side, not library-side — see Litigation Planner doc §5 "Validatable at write time".)

### 6.5 Admin /admin/procedural-events list (recently shipped, t-paliad-321)

The proceeding-type column in the admin list (m/paliad#144 follow-up, just landed) renders one of the 46 active codes per row. Post-mig 153, the admin filter dropdown can:

- Default to showing only `kind='proceeding'` rows (clean primary view).
- Offer a "show all kinds" toggle for admins triaging the non-primary rows.

This is presentation-only — the underlying admin queries don't need to change immediately. The kind column is a forward-compat hook.

### 6.6 Knowledge-platform pages (Gerichtsverzeichnis, Patentglossar)

Untouched. None of those pages query `proceeding_types` directly.

### 6.7 Fristen export / paliad data export (t-paliad-279)

Untouched. The exporter dumps `proceeding_types` as a whole (no kind-filter); after mig 153 it dumps the same rows with the new kind column. Forward-compat by default.

---

## 7. Migration sequencing decision vs m/paliad#146

m/paliad#146 (Fristenrechner overhaul, t-paliad-322 / 323) is on the S1-S6 train under knuth. m's directive at task brief time: **knuth pauses at the S1+S2 seam waiting for this taxonomy decision**.

Three options were on the table:

(a) **Pause #146 until taxonomy clean** — knuth blocked, this design lands first, then knuth resumes S3+.
(b) **Land #146 against current shape, migrate later** — knuth ships S3-S6 against the current 46-row table, taxonomy mig follows.
(c) **Land taxonomy in parallel, knuth re-targets if needed** — both run, knuth's S3 picks up the new filter when mig 153 is ready.

**Recommendation: (c) parallel-land** with the following caveats:

- The taxonomy mig is **additive** (ADD COLUMN with safe DEFAULT, no DROP, no data move beyond UPDATEs that touch unreferenced rows). Knuth's S3 implementation can be written with or without the `kind='proceeding'` filter — adding the filter is a one-line patch the moment mig 153 lands.
- The R3 chip-pool query in knuth's S3 PR should be **future-proofed by also adding the `kind='proceeding'` filter behind a feature flag or an env-time SQL constant**, defaulting to "no filter" pre-mig and "filter" post-mig. (Or simpler: knuth writes the filter unconditionally; the migration lands first; ordering is mechanical.)
- The mig 153 PR should land **before** knuth's S3 PR ships to main, so the filter is never false-positive (chipping phase rows users can't actually pick). Both PRs can be drafted in parallel; the squeeze happens at merge time.
- Sequence on main: mig 153 → knuth S3 (with filter) → knuth S4-S6.

Option (c) keeps knuth productive (S3 work can start immediately after this design ratifies; doesn't have to wait for the mig to merge) and avoids the option (a) idle cost.

Option (b) was rejected because it leaves the Mode B R3 wizard chipping 35 UPC rows on initial release — exactly the bug m flagged in m/paliad#147 ("half of the 46 active proceeding_types are not primary proceedings"). The user would see phase rows in R3 day one of the Fristenrechner overhaul shipping; we'd be shipping the bug.

Option (a) was rejected as the safest but slowest path. The taxonomy mig is trivial enough (one ALTER + four UPDATE statements + optional trigger) that parallel-running has no real risk.

§9 Q10 gives m the chance to pick differently.

---

## 8. Out of scope (flagged for separate work)

- **`upc.apl.*` data drift.** 30 rows in `paliad.event_category_concepts` reference the inactive `upc.apl.merits` / `upc.apl.order` / `upc.apl.cost` codes (the pre-`upc.apl.unified` triplet). 4 active sequencing_rules reference `spawn_proceeding_type_id=11` (the inactive `upc.apl.merits` row). This is a pre-existing inconsistency from the appeal unification mig — needs its own follow-up ticket. Not blocking this design; can be cleaned up in a separate migration that retargets concepts + spawn FKs to `upc.apl.unified` (id=160).
- **Renaming or relabelling primary proceedings.** Out per m/paliad#147 acceptance — editorial work, not structural.
- **Adding new proceeding types beyond the existing corpus.** Out per m/paliad#147 acceptance.
- **The Fristenrechner UI overhaul itself (m/paliad#146).** Separate track; this design only tells knuth's S3 what set to chip.
- **The scenarios design (m/paliad#124).** Already ratified in `docs/design-litigation-planner-2026-05-26.md` §5; this design only refines the spec validator's "every code resolves to a primary" check.
- **DROPing the non-primary rows physically.** Reversible deactivation via `kind=...` + optional `is_active=false` is enough; physical deletion adds irreversibility risk for no functional gain.
- **Migration of `event_category_concepts.proceeding_type_code` to a real FK.** It's text today, joined softly; converting to FK is a separate hardening task.

---

## 9. Open questions for m (10 decision questions)

Sent via `AskUserQuestion` in 3 batches per inventor SKILL contract (4+3+3). m's picks land in §10 below after the round-trip.

| # | Topic | Recommended pick |
|---|---|---|
| Q1 | Model choice | Model 1 (kind discriminator) |
| Q2 | Phases — linear sub-phases of every CFI, or separately-elected? | Implicit: phases live in `procedural_events.event_kind`, not as proceeding_types |
| Q3.a | Side-actions — triggered by parent event, or initiated out-of-band? | Mixed; today's data has no rules, future rules anchor on the parent primary with `condition_expr` |
| Q3.b | `upc.pl.cfi` (Schutzschrift) — primary or side-action? | Primary (own RoP filing pathway) |
| Q4 | Collapse `de.inf.lg`/`olg`/`bgh` into one `de.inf` with instance_level qualifier? | No — keep discrete |
| Q5 | Collapse `de.null.bpatg`/`bgh` into one `de.null` with instance_level qualifier? | No — keep discrete |
| Q6 | Should DE follow the `upc.apl.unified` pattern? | No (= keep discrete, locks Q4+Q5) |
| Q7 | `upc.ccr.cfi` — proceeding row with routing (status quo), or `with_ccr` flag on `upc.inf.cfi`? | Keep as proceeding (status quo per t-paliad-204 S1) |
| Q8 | Enforce `projects.proceeding_type_id` → `kind='proceeding'` at the DB level? | Yes, via trigger (§3.3) |
| Q9 | Set `is_active=false` on the 28 non-primary rows after mig 153? | Yes (cleanest admin UX) |
| Q10 | Sequencing vs m/paliad#146 — pause / parallel / re-target? | (c) parallel-land — mig first, then knuth S3 with filter |

Q11 in the issue body ("how many rules need new condition_expr disambiguation?") is **empirically answered, no decision needed**: 0 rules need new condition_expr — every active rule is already correctly anchored to a primary. Surfaced in §4 + §6.2.

---

## 10. m's decisions (2026-05-27)

All 11 questions answered via `AskUserQuestion` on 2026-05-27 09:52 (3 batches of 4+4+3). 10 of 11 picks = recommendation; Q9 diverged at the chip-picker but m's follow-up instruction ("I follow your recommendation") flips Q9 to the recommendation as well. Q2 carries a precise carve-out captured verbatim below.

- **Q1 (Model): Model 1 — kind discriminator.** [= recommendation] One column + CHECK constraint + UPDATE statements. **Locks §1, §2, §3.1, §3.2.**
- **Q2 (Phases): Generally option 1 (implicit via `procedural_events.event_kind`), with carve-outs.** [≈ option 1 with carve-out] m's verbatim call:
    > Generally 1, but I agree with costs which are not only a phase but also "standalone" side proceedings. But default decision application is not.
    Concretely:
    - `upc.cfi.interim` (173) → `kind='phase'`
    - `upc.cfi.oral` (174) → `kind='phase'`
    - `upc.cfi.decision` (175) → `kind='phase'`
    - `upc.default.cfi` (185) → `kind='phase'` (m: "default decision application is not [a standalone side proceeding]")
    - **`upc.costs.cfi` (176) → `kind='proceeding'`** (m: "costs are not only a phase but also standalone side proceedings"). The Separate Kostenentscheidung can be filed as its own application under R.151 RoP independently of the parent decision; m's read is that the standalone-application character outweighs the phase-of-CFI character.
    Net: 4 phase rows (not 5 as in the strawman), 23 primary-proceeding rows (not 22). **Updates §0.4 Group B count, §0.5 totals row, §1 categorisation, §3.2 UPDATE statement IDs (drop 176 from the phase UPDATE).**
- **Q3.a (Side-actions): kind='side_action', rules anchor on parent primary.** [= recommendation] All 10 §0.4 Group C rows get `kind='side_action'`. When corpus arrives, rules attach to the parent primary with a `condition_expr` flag. **Locks §1.1, §3.2 side-action UPDATE.**
- **Q3.b (Schutzschrift): kind='proceeding'.** [= recommendation] `upc.pl.cfi` (188) stays in the primary set on the strength of its own RoP filing pathway. **Locks §0.3 unloaded-primary list.**
- **Q4 (DE inf collapse): Keep discrete.** [= recommendation] `de.inf.lg/olg/bgh` stay as 3 separate primaries. No collapse, no instance_level qualifier introduction. **Locks §0.2 + §1 DE-side categorisation.**
- **Q5 (DE null collapse): Keep discrete.** [= recommendation] `de.null.bpatg/bgh` stay separate. Symmetric with Q4. **Locks §0.2 + §1 DE-side categorisation.**
- **Q6 (DE follow upc.apl pattern): No — keep DE discrete.** [= recommendation] Locks Q4+Q5. The `upc.apl.unified` consolidation was about same-court appeal variants; DE appeals are different-court-instance appeals — different problem. **No code-rename work falls out of this design.**
- **Q7 (CCR shape): Keep status quo.** [= recommendation] `upc.ccr.cfi` stays as `kind='proceeding'` with the existing routing-to-`upc.inf.cfi` from t-paliad-204 §0.3 S1. **Locks §1.1.**
- **Q8 (DB trigger): Trigger on `projects` only.** [= recommendation] BEFORE INSERT/UPDATE trigger on `paliad.projects` enforces `proceeding_type_id → kind='proceeding'`. No trigger on `sequencing_rules` (admin tooling already gates). **Locks §3.3 — keep the `projects` trigger DDL, drop the optional `sequencing_rules` variant.**
- **Q9 (Deactivate non-primaries): Yes — deactivate.** [m's chip-pick was "keep active"; flipped to recommendation per m's "I follow your recommendation" instruction] All `kind IN ('phase', 'side_action', 'meta')` rows get `is_active=false` in mig 153. The admin `/admin/proceeding-types` list shows only the 23 active primaries. Rows stay in the table with their `kind` tag so future tooling that wants to surface them can flip `is_active` back on. **Updates §3.2 — uncomment the optional `UPDATE … SET is_active=false` block.**
- **Q10 (Sequencing vs #146): Parallel-land.** [= recommendation] Mig 153 + knuth's S3 PR drafted in parallel; mig merges first; knuth's S3 includes the `kind='proceeding'` filter in R3's chip query from day one. No idle cost; no bug shipped. **Locks §7.**

### 10.1 What changed from the strawman as a result

Two material edits flow from m's picks:

1. **§0.4 Group B (Phases) drops `upc.costs.cfi` (id 176)** — moved into the primary set. Phase count: 5 → 4. Primary count: 22 → 23. §0.2 picks up id 176 as an unloaded primary (zero rules today; future corpus will attach).
2. **§3.2 migration includes the `is_active=false` UPDATE** (was optional in the strawman, now mandatory):

    ```sql
    UPDATE paliad.proceeding_types
      SET is_active = false
      WHERE kind IN ('phase', 'side_action', 'meta');
    ```

    This is what the post-mig 153 cleanup looks like: 23 active rows (all `kind='proceeding'`), 23 inactive rows (4 phase + 10 side_action + 9 meta + the pre-existing 3 inactive appeal-triplet + 1 archived bucket = 27 inactive total, but 23 of those are the freshly-deactivated taxonomy rows).

These edits don't change the §7 sequencing decision or the §6 consumer-impact analysis. They tighten the mig file and shift one row's classification.

### 10.2 Final categorisation (post-decisions)

| `kind` | Count | Codes |
|---|---:|---|
| `proceeding` | **23** | upc.inf.cfi, upc.rev.cfi, upc.pi.cfi, upc.dmgs.cfi, upc.disc.cfi, upc.ccr.cfi, upc.apl.unified, upc.dni.cfi, upc.epo.review, upc.bsv.cfi, upc.pl.cfi, **upc.costs.cfi** (m's Q2 carve-out), de.inf.lg, de.inf.olg, de.inf.bgh, de.null.bpatg, de.null.bgh, epa.opp.opd, epa.opp.boa, epa.grant.exa, dpma.opp.dpma, dpma.appeal.bpatg, dpma.appeal.bgh |
| `phase` | **4** | upc.cfi.interim, upc.cfi.oral, upc.cfi.decision, upc.default.cfi |
| `side_action` | **10** | upc.evidence.cfi, upc.experiments.cfi, upc.security.cfi, upc.intervention.rop, upc.parties.change, upc.optout.cfi, upc.inspection.cfi, upc.freezing.cfi, upc.withdrawal.rop, upc.rehearing.coa |
| `meta` | **9** | upc.case.mgmt, upc.general.rop, upc.service.rop, upc.language.rop, upc.representation.rop, upc.fees.court, upc.legalaid.cfi, upc.special.cfi, upc.reestablishment.rop |
| **Total** | **46** | ✓ |

Post-mig 153: 23 active (all `kind='proceeding'`), 23 deactivated (the phase/side_action/meta set).

---

## 11. Synthesis links

- mBrian topic: `topic-fristenrechner` — file this design as a `[synthesis]` node, link `related_to` the proceeding-code-taxonomy doc (2026-05-18) and the Fristenrechner overhaul (2026-05-26), `triggered_by` t-paliad-324.
- Related design docs: `docs/design-proceeding-code-taxonomy-2026-05-18.md` (the code-shape doc), `docs/design-fristenrechner-overhaul-2026-05-26.md` (knuth's parent design), `docs/design-litigation-planner-2026-05-26.md` §5 (scenarios spec validator).
- Related migrations: 095 (fristen gap-fill, spawn FK invariant), 096 (proceeding code rename), 152 (sequencing_rule dedupe + admin column).
