diff --git a/docs/design-proceeding-types-taxonomy-2026-05-26.md b/docs/design-proceeding-types-taxonomy-2026-05-26.md new file mode 100644 index 0000000..49a47c0 --- /dev/null +++ b/docs/design-proceeding-types-taxonomy-2026-05-26.md @@ -0,0 +1,580 @@ +# 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).