Files
paliad/docs/design-proceeding-types-taxonomy-2026-05-26.md
mAi 3219bff4d4
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
design(taxonomy): proceeding_types kind discriminator + 11 m's decisions (t-paliad-324)
Live audit established that 28 of 46 active proceeding_types have zero
downstream pressure (0 rules, 0 projects, 0 spawn FKs, 0 concepts). Mig
plan is purely additive: ADD COLUMN kind text CHECK (...), four UPDATE
statements to tag phase/side_action/meta rows, deactivate them, and add
a BEFORE INSERT/UPDATE trigger on projects.proceeding_type_id to enforce
kind='proceeding'.

m's call on the 11 AskUserQuestion decisions:
- Model 1 (kind discriminator)
- Phases implicit via procedural_events.event_kind, EXCEPT upc.costs.cfi
  stays kind='proceeding' (standalone R.151 application)
- Side-actions: kind='side_action', rules anchor on parent primary
- Schutzschrift kind='proceeding' (own RoP filing)
- DE inf + DE null + DE-vs-upc.apl unification: all keep discrete
- upc.ccr.cfi: keep status quo per t-paliad-204 S1
- DB trigger on projects only (admin-only writes on sequencing_rules)
- Deactivate non-primary rows (23 active post-mig, all kind='proceeding')
- Parallel-land vs m/paliad#146 — knuth's S3 picks up the filter

Final categorisation: 23 proceeding / 4 phase / 10 side_action / 9 meta.

No code yet — coder gate held per inventor SKILL. Design only.

Closes the inventor pass on m/paliad#147.
2026-05-27 09:54:18 +02:00

41 KiB

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 usedupc.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

-- 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

-- 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:

-- 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:

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:

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:

// 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_idkind='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):

    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).


  • 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).