# Design — Deadline + procedural-events system revision (Phase 2 of RFC m/paliad#149)

**Task:** t-paliad-329
**Gitea:** m/paliad#149 (Phase 2)
**Inventor:** atlas (shift-1)
**Date:** 2026-05-27
**Status:** Draft — coder gate held; awaiting m's go on the slice train
**Branch:** `mai/atlas/inventor-deadline-system`

**Builds on:**
- `docs/assessment-deadline-system-2026-05-27.md` (athena Phase 1, 738 lines — premises here are athena's)
- `docs/design-fristenrechner-followup-rules-2026-05-27.md` (atlas t-paliad-327, pre-ratified subset: cross-party display + scenario SSoT + spawn-only picker exclusion)
- `docs/design-proceeding-types-taxonomy-2026-05-26.md` (mig 153 shipped; `kind` discriminator)
- `docs/design-fristenrechner-overhaul-2026-05-26.md` (Entry B foundation S1-S6 shipped)

m authorised Phase 2 at 2026-05-27 11:33 ("Go on"). m's "big picture" direction at 13:53 ("yeah, b - big! We need an overall schema for all procedural events and how they are connected") makes the connection graph itself the spine of this design.

---

## §0 Premises — reconciliation with athena's audit

Athena established the live data; this design takes that as given. Three cross-checks ran 2026-05-27 against the live `paliad` schema; counts match athena's §0/§3 numbers (chain-linked 107 / PT-roots 46 / legacy globals 73 / overlap 2). The only material refinement is athena's R3 finding ("4 spawn rules point at INACTIVE id=11") — which m's Q5 answer now re-interprets as **correct** rather than broken (see §3.1).

### §0.1 The athena↔RFC conflicts surfaced

| Item | RFC said | Athena found | Picked side |
|---|---|---|---|
| Scenario state shape | "`projects.scenarios` jsonb (mig 145)" exists | `paliad.scenarios` table exists; `projects.scenarios` jsonb does **not** | Athena. Use new `projects.scenario_flags jsonb` column (Q4) — different from both. |
| Three stores diverge | "Three independent stores. No single source of truth." | All three stores empty (0 rows in `project_event_choices`, 0 in `scenarios`, DOM-only). Risk dormant. | Athena. Design picks one store going forward; nothing to migrate. |
| Spawn FK is "broken" | Implied | Athena R3: 4 spawn rules point at inactive `upc.apl.merits`. | m's Q5 inverts: the unification was the bug, not the FK. Re-split apl into merits/cost/order (§3.1). |

### §0.2 The pre-ratified subset from t-paliad-327

m ratified the following on 2026-05-27 (via `AskUserQuestion`, all on-recommendation in that task) — Phase 2 carries them forward unchanged:

- Cross-party display: backend stops filtering by party, `is_cross_party` derived field, "Gegenseitig" badge, muted/greyed visual, unchecked default, write-back excluded unconditionally. (Folded into §2.4.)
- Scenario flag SSoT: `paliad.projects.scenario_flags jsonb` column + GET/PATCH `/api/projects/{id}/scenario-flags`. (Folded into §2.3.)
- Spawn-only event picker exclusion: `SearchEvents` SQL adds `AND sr.is_spawn = false`. (Folded into §2.2.)

These are not re-asked. They are the foundation Phase 2 builds on.

---

## §1 The overall connection schema (m's "big picture")

Per m's direction: document the canonical connection graph across all procedural_events + sequencing_rules + proceeding_types as a unified model.

### §1.1 Conceptual model in one paragraph

A **rule** (`paliad.sequencing_rules` row) is the atomic node. It carries one deadline for one event, on one proceeding-type. Every rule has at most one **predecessor edge** via `parent_id` → another rule whose own deadline must elapse before this one starts. The chain root (rule with `parent_id IS NULL`) is anchored to its **proceeding-type root event** (typically a filing — Klageerhebung, Veröffentlichung, Anmeldung). A small number of rules are **spawn rules** (`is_spawn=true`) — they don't compute their own deadline; instead they open a fresh proceeding of a different type, edge labelled by `spawn_proceeding_type_id`. Conditional rules carry a `condition_expr` jsonb predicate over a small flag vocabulary (`with_ccr`, `with_amend`, `with_cci`); the active subset of the graph for a given project is the rules whose predicate is satisfied by `projects.scenario_flags`. **The only canonical predecessor link is `parent_id`. The `trigger_event_id` column is deprecated** (Q1). Trigger discoverability is **derived from data**: any event whose anchor rule has `EXISTS (non-spawn child WHERE child.parent_id = anchor.id)` is a valid trigger; everything else (spawn-only consequences, terminal leaves) is filtered out at the picker (Q3, §2.2).

### §1.2 The shape — ASCII tree per representative PT

Showing 3 representative PTs (the rest follow the same structural pattern; counts in §1.4).

#### upc.inf.cfi (25 rules, depth 5, the densest tree)

```
upc.inf.cfi (Verletzungsverfahren CFI)
├─ RoP.013.1   soc                       Klageerhebung                       [claimant · M]   ← anchor
│  ├─ RoP.019.1  prelim                  Vorl. Einwendungen                  [defendant · O]
│  ├─ RoP.262.2  confidentiality_response  Vertraulichkeit                   [both · O]
│  ├─ RoP.023    sod                     Klageerwiderung                     [defendant · M]
│  │  └─ RoP.029.b  reply                Replik                              [claimant · M · ?with_ccr]
│  │     └─ RoP.029.c  rejoin            Duplik                              [defendant · M · ?with_ccr]
│  ├─ RoP.025    ccr                     Widerklage auf Nichtigkeit          [defendant · O · ?with_ccr]
│  │  └─ RoP.029.a  def_to_ccr           Erwiderung auf CCR                  [claimant · M · ?with_ccr]
│  │     └─ RoP.029.d  reply_def_ccr     Replik auf Erw. CCR                 [defendant · M · ?with_ccr]      ← X-party from claimant
│  │        └─ RoP.029.e  rejoin_reply_ccr  Duplik auf Replik CCR            [claimant · M · ?with_ccr]
│  │     └─ RoP.030.1  app_to_amend      Antrag auf Patentänderung           [claimant · M · ?with_amend]
│  │        └─ RoP.032.1  def_to_amend   Erwiderung auf Änderung             [defendant · M · ?with_amend]
│  │           └─ RoP.032.3  reply_def_amd  Replik auf Erw. Änderung         [claimant · M · ?with_amend]
│  │              └─ RoP.032.3  rejoin_amd  Duplik auf Replik Änderung       [defendant · M · ?with_amend]
│  ├─ RoP.333.2  cmo_review              Antrag CMO-Überprüfung              [both · O]
│  ├─ RoP.109.1  translation_request     Übersetzungsantrag                  [both · O]
│  ├─ RoP.109.5  translations_lodge      Übersetzungen einreichen            [both · M]
│  ├─ RoP.118.4  cons_orders             Antrag Folgenanordnungen            [both · O]
│  ├─ RoP.151    cost_app                Kostenantrag                        [both · O]
│  ├─ RoP.353    rectification           Berichtigungsantrag                 [both · O]
│  └─ RoP.220.1.a  appeal_spawn          ⇲ Berufungsverfahren öffnen         [both · O · SPAWN→ upc.apl.merits]
├─ RoP.104     interim                   Zwischenanhörung                    [court · M]
├─        (n/a)  oral                    Mündliche Verhandlung               [court · M]
├─        (n/a)  decision                Endentscheidung                     [court · M]
│  (Note: interim/oral/decision are court-set; they're chain-anchored but
│   have no scheduled rule of their own — phase markers carried via event_kind.)
└─ RoP.109.4   interpreter_cost          Dolmetscherkosten                   [court · M]
```

**Legend.** `[party · M|O · ?flag · SPAWN→target]`. `M` = mandatory, `O` = optional. `?flag` = conditional on the scenario flag. ← X-party = cross-party row vs claimant perspective; see §2.4 for display. SPAWN → opens a new proceeding under that PT.

#### upc.rev.cfi (17 rules, depth 4, mirrors inf.cfi shape)

Same SoC → SoD → Reply → Rejoinder spine; CCR mirrored as Erwiderung auf Widerklage on revocation. `with_cci` (Widerklage auf Verletzung — the inverse of with_ccr) replaces `with_ccr`. Same `with_amend` branch for R.30. 13 chain-linked, 5 roots, 1 spawn (→ upc.apl.merits, post-Q5 split).

#### upc.apl (POST-Q5 SPLIT — 3 trees, 16 rules total)

After §3.1 mig: id=160 `upc.apl.unified` is retired; rules re-bound to the 3 reactivated PTs (id=11 `upc.apl.merits` 7 rules / id=19 `upc.apl.cost` 2 rules / id=20 `upc.apl.order` 7 rules). Trees:

```
upc.apl.merits (7 rules)
├─ RoP.224.1.a  notice          Berufungseinlegung
│  └─ RoP.224.2.a  grounds      Berufungsbegründung
│     └─ RoP.235.1   response   Berufungserwiderung
│        └─ RoP.237    cross_a  Anschlussberufung
│           └─ RoP.238.1  cross_a_reply  Erwiderung Anschlussberufung
├─        (n/a)  oral          Mündliche Verhandlung      [court · M]
└─        (n/a)  decision      Entscheidung                [court · M]

upc.apl.cost (2 rules)
├─ RoP.221.1   leave_app       Antrag auf Berufungszulassung
└─        (n/a)  decision      Kostenfestsetzungsbeschluss

upc.apl.order (7 rules)
├─        (n/a)  order              angegriffene Entscheidung
│  ├─ RoP.220.2  with_leave         Berufung mit Zulassung
│  └─ RoP.220.3  discretion         Ermessensüberprüfung
├─ RoP.224.2.b  grounds_orders     Berufungsbegründung (Orders Track)
│  └─ RoP.235.2  response_orders   Berufungserwiderung (Orders Track)
└─ RoP.237      cross              Anschlussberufung
   └─ RoP.238.2  cross_reply       Erwiderung Anschlussberufung
```

The 3 trees are independent. Determinator UX (proceeding_mapping.go) keeps a single user-facing "Berufung" entry that fans out to one of the 3 based on what's being appealed (judgment → merits, cost decision → cost, order → order). Routing layer unchanged from t-paliad-204 S1; only the data shape changes.

The remaining 14 ruled PTs (de.inf.lg / .olg / .bgh, de.null.bpatg / .bgh, dpma.opp / .appeal.bpatg / .bgh, epa.opp.opd / .opp.boa / .grant.exa, upc.dmgs.cfi, upc.disc.cfi, upc.pi.cfi) follow the same shape — root anchored on a filing/grant event, chain depth 1-3, optionals and conditionals branching off the root or first-hop. Athena's §4 gap map gives the per-PT P/R counts; see also §1.4 below.

### §1.3 Cross-PT edges — the spawn graph (post-Q5)

```mermaid
graph LR
    upc_inf_cfi[upc.inf.cfi<br/>Verletzungsverfahren CFI] -.->|R.220.1.a<br/>appeal_spawn| upc_apl_merits[upc.apl.merits<br/>Berufung Hauptsache]
    upc_rev_cfi[upc.rev.cfi<br/>Nichtigkeitsverfahren CFI] -.->|R.220.1.a<br/>appeal_spawn| upc_apl_merits
    upc_dmgs_cfi[upc.dmgs.cfi<br/>Schadensbemessung] -.->|R.220.1.a<br/>appeal_spawn| upc_apl_merits
    upc_pi_cfi[upc.pi.cfi<br/>Einstweilige Maßnahmen] -.->|R.220.1.a<br/>appeal_spawn| upc_apl_order[upc.apl.order<br/>Berufung Orders Track]
```

4 spawn edges, all in the UPC CFI cluster. PI appeals go to the orders track (not main proceedings); the rest go to merits. The cost-decision-appeal track (`upc.apl.cost`) is reached not via spawn but via direct filing (`leave_app` rule); cost decisions arrive within their parent proceeding and the cost-appeal opens as a standalone application.

DE-side, EPA-side, DPMA-side: no spawn edges today. Each tier-of-court is a separate `proceeding_type` (de.inf.lg / .olg / .bgh) with its own root + chain; chained-by-instance is not modelled as a spawn (the user explicitly creates a new project for the appeal stage). m may revisit this if DE-side workflow benefits from spawn edges; out of scope for this revision.

### §1.4 Per-PT health summary (post-Q5)

| PT code | rules | roots | chained | conditional | spawns | gap |
|---|--:|--:|--:|--:|--:|---|
| upc.inf.cfi | 25 | 4 | 21 | 10 | 1 | 84% chained — strongest |
| upc.rev.cfi | 17 | 4 | 13 | 8 | 1 | 76% |
| upc.apl.merits | 7 | 3 | 4 | 0 | 0 | post-Q5 split — to be re-rooted |
| upc.apl.order | 7 | 3 | 4 | 0 | 0 | post-Q5 split |
| upc.apl.cost | 2 | 1 | 1 | 0 | 0 | post-Q5 split |
| de.inf.lg | 9 | 5 | 4 | 0 | 0 | 44% — gappy |
| de.null.bpatg | 10 | 4 | 6 | 0 | 0 | 60% |
| de.inf.olg | 7 | 1 | 6 | 0 | 0 | 86% |
| de.inf.bgh | 8 | 1 | 7 | 0 | 0 | 88% |
| de.null.bgh | 6 | 1 | 5 | 0 | 0 | 83% |
| dpma.opp.dpma | 4 | 1 | 3 | 0 | 0 | 75% |
| dpma.appeal.bpatg | 5 | 1 | 4 | 0 | 0 | 80% |
| dpma.appeal.bgh | 4 | 1 | 3 | 0 | 0 | 75% |
| epa.opp.opd | 8 | 2 | 6 | 0 | 0 | 75% |
| epa.opp.boa | 8 | 3 | 5 | 0 | 0 | 63% |
| epa.grant.exa | 7 | 4 | 3 | 0 | 0 | 43% |
| upc.dmgs.cfi | 8 | 4 | 4 | 0 | 1 | 50% |
| upc.pi.cfi | 7 | 3 | 4 | 0 | 1 | 57% |
| upc.disc.cfi | 4 | 1 | 3 | 0 | 0 | 75% |
| **Empty (Q6)** | | | | | | |
| upc.bsv.cfi | 0 | — | — | — | — | unruled — badge "Keine Regeln" |
| upc.ccr.cfi | 0 | — | — | — | — | unruled — badge |
| upc.costs.cfi | 0 | — | — | — | — | unruled — badge |
| upc.dni.cfi | 0 | — | — | — | — | unruled — badge |
| upc.epo.review | 0 | — | — | — | — | unruled — badge |
| upc.pl.cfi | 0 | — | — | — | — | unruled — badge |

Plus **73 legacy globals** sitting in the corpus with `proceeding_type_id IS NULL` — these are the editorial backfill target (Q2 / §4.2). Each needs to be reparented onto one of the 23 PTs.

---

## §2 Tier 1 — model decisions (m ratified all 4 on-recommendation)

### §2.1 `parent_id` is the canonical predecessor link

`paliad.sequencing_rules.parent_id` (uuid FK to another rule) is the **only** predecessor pointer going forward. `paliad.sequencing_rules.trigger_event_id` (bigint FK to legacy `paliad.trigger_events`) gets dropped at the end of the migration train (§5).

**Implication for the 75 rules that currently use `trigger_event_id`:**

- The 73 legacy globals (proceeding_type_id IS NULL): editorial walk reparents each onto a real PT chain (Q2, §4.2). Slow but right — no data is lost, just structurally normalised.
- The 2 hybrid rules (both parent_id AND trigger_event_id set): keep `parent_id`, NULL out `trigger_event_id`. No data loss — `parent_id` already carries the live edge.

After backfill, `trigger_event_id` is unused — safe to drop the column (§5, Mig P4).

### §2.2 Trigger discoverability — derive from data

A `procedural_event` is a **picker-eligible trigger** when EXISTS a published+active non-spawn rule with `parent_id` pointing at this event's anchor rule. The picker SQL gains:

```sql
WHERE EXISTS (
  SELECT 1 FROM paliad.sequencing_rules child
   WHERE child.parent_id = anchor.id
     AND child.is_active = true
     AND child.lifecycle_state = 'published'
     AND child.is_spawn = false  -- spawn-only consequences not pickable (t-paliad-327 §3a)
)
```

No new column. No materialised view. The EXISTS subquery uses the existing `sequencing_rules.parent_id` index. At today's scale (226 rules) it's cheap; at 10× scale still fine (parent_id is indexed; child lookup is index-only scan).

Mode A's `SearchEvents` (`internal/services/fristenrechner_search_events.go`) and Mode B R4's chip-strip both apply this filter. Terminal leaves (Duplik etc.) stay pickable — they have a non-spawn anchor rule and result in an empty follow-up list, which is honest UX (t-paliad-327 §3a.4, m ratified).

### §2.3 Scenario state SSoT — `projects.scenario_flags jsonb`

Reconfirmed from t-paliad-327 §3.2:

```sql
ALTER TABLE paliad.projects
  ADD COLUMN scenario_flags jsonb NOT NULL DEFAULT '{}'::jsonb;
```

Shape:
```json
{ "with_ccr": true, "with_amend": false, "with_cci": false }
```

Whitelist-validated against the set of flag names appearing in `sequencing_rules.condition_expr` (today: `with_ccr`, `with_amend`, `with_cci`).

API: `GET /api/projects/{id}/scenario-flags` returns the map; `PATCH /api/projects/{id}/scenario-flags` accepts partial deltas (null deletes a key).

**Kontextfrei (no project):** stays on localStorage. No DB writes when `project_id IS NULL`.

**Relationship with `paliad.scenarios`:** complementary, not duplicate. `scenarios.spec.flags[]` (the Litigation Planner Slice D shape) is a *named snapshot*; activating a scenario copies its flag array into `projects.scenario_flags`. Live edits write to `scenario_flags`. `paliad.project_event_choices` (the legacy empty table) is deprecated (§4.3).

### §2.4a Selection state + detail-level view-mode filter

m's reframe (14:40): the real ask isn't "rarity" — it's **detail-level control over the timeline**. Every event/rule is a card; the user picks which optional cards belong to *their* scenario; the Verfahrensablauf has a view-mode toggle that controls how much of the picture surfaces.

m's quote (14:40): *"It is more that I want a grade of detail in our swimlane display […] I want to show them but also be able to 'focus' by not displaying optional things. And we can select these options somehow, for example like we do with the appeal in the Decision dropdown. And if none is selected, none are displayed. We need an option 'Show unselected options' or 'show only selected' or 'mandatory' […] It would be great to basically filter events from the timeline based on whether they are selected in this scenario."*

The underlying mental model:

- **Mandatory rules** are always in the scenario. They render in every view-mode. The user cannot deselect them.
- **Recommended rules** are *selected by default* in the scenario. The user can deselect them.
- **Optional rules** are *not selected by default*. The user opts in via the same UI mechanism that already exists for `with_ccr` / `with_amend` (a chip / dropdown / "Aufnehmen" CTA per rule).
- **Conditional rules** (with `condition_expr`) are gated by scenario flags first, then by selection (a conditional rule whose flag is on still respects its priority's default selection rule).

The Verfahrensablauf gets a three-way **detail-level toggle** (§3.3a):

- **Nur Pflicht (Mandatory only)** — only `priority='mandatory'` cards.
- **Gewählt (Selected)** — mandatory + every rule the scenario has explicitly selected. Default.
- **Alle Optionen (All considered)** — every rule that *could* belong, including unselected optionals (rendered with a dotted border + "Aufnehmen" CTA) and conditional rules whose flag isn't set (rendered greyed with a "wenn-…" hint).

#### Schema — no new column on `sequencing_rules`

The original §2.4a strawman proposed `is_edge_case boolean` as a chain-head flag. m's reframe makes that wrong: **every** optional rule is potentially "rare" depending on the lawyer's scenario; the dimension isn't a property of the rule, it's a property of the scenario.

Instead, the selection state lives entirely in **`projects.scenario_flags jsonb`** (already on the table from P0, §2.3) with an extended shape:

```json
{
  "with_ccr": true,
  "with_amend": false,
  "with_cci": false,
  "rule:<uuid_of_recommended_X>": false,
  "rule:<uuid_of_optional_Y>": true
}
```

The flat-map shape stays — entries are either named scenario flags (`with_*`) or per-rule selection deviations (`rule:<uuid>`). Storage only carries **deviations from the priority default**:
- `priority='recommended'` is selected-by-default; `rule:X = false` records an explicit deselection.
- `priority='optional'` is unselected-by-default; `rule:X = true` records an explicit selection.
- `priority='mandatory'` is always selected; trying to store `rule:X = false` is rejected (422 from the PATCH endpoint).

Whitelist (Q9 catalog) gains a wildcard pattern `rule:<uuid>` — any well-formed UUID matches; the handler validates that the UUID resolves to an active+published rule on the project's proceeding_type before persisting.

Kontextfrei (no project): localStorage stores the same shape under a per-PT key (`scenario:upc.inf.cfi`). Different PT → different stored selection set; this matches how kontextfrei users explore.

#### Visual — generalising the CCR dropdown to per-rule chips

The existing `with_ccr` / `with_amend` checkboxes are *coarse* scenario flags. The new per-rule selection is *fine-grained* but uses the same UI vocabulary:

- **Selected rule**: solid card, normal background. (Identical to today's mandatory render.)
- **Selected optional that's deselectable**: solid card with a small `[Entfernen]` chip; click removes from `selected_optionals` (writes `rule:X = false`).
- **Unselected optional (default state in "Alle Optionen" mode)**: dotted-border card, muted background, `[Aufnehmen]` CTA. Click writes `rule:X = true`.
- **Conditional rule whose flag isn't set**: greyed card with a "Aktivieren via 'Mit Widerklage' im Szenario" hint; clicking the hint scrolls to the scenario-flags strip.
- **Cross-party** (§2.4): orthogonal — applies its `Gegenseitig` badge and muted style on top of whichever state above.

Each card thus carries up to four orthogonal axes of display state — priority, selection, conditional-gate, cross-party. The 4 axes compose; no axis dominates.

#### Subtree semantics — implicit via parent chain

When a chain head is deselected (e.g. R.109.1 Übersetzungsantrag = `false`), its descendants in the parent_id tree (R.109.4 Mitteilung etc.) **inherit the deselected state for display** without needing their own entries in `selected_optionals`. The tree renderer walks the chain; if any ancestor is unselected, the descendant doesn't render in "Gewählt" mode. In "Alle Optionen" mode, the whole subtree renders greyed under the deselected head.

If a descendant has its own explicit `rule:X = true` entry, that overrides the ancestor — the user has explicitly pulled this leaf into their scenario despite not selecting the parent. Edge case; documented but no special UI affordance.

#### Default population on project creation

When a project is created with `proceeding_type_id = X`, the server seeds `scenario_flags = {}`. Nothing in the map. The tree renderer computes per-rule selection on-the-fly from priority + scenario_flags entries. No upfront write-storm of "rule:X = true" for every recommended rule — only deviations land in storage.

#### Why this beats the `is_edge_case` boolean

- **No new column.** All state lives in the existing `projects.scenario_flags jsonb` from P0.
- **Generalised.** Every optional rule is selectable, not just the few flagged as "rare". m's "sequence density is very high" complaint is solved by the user controlling which optionals belong to *their* scenario, rather than the editorial process having to decide globally which rules deserve dotted-border treatment.
- **Composable with condition_expr.** A conditional rule is selectable when its flag is on; the selection state is independent of the flag state.
- **Matches m's stated UX prior art.** The CCR dropdown pattern *is* the model; we're just generalising it from 3 named flags to N per-rule selections.

### §2.4 Cross-party display

From t-paliad-327 §2 (m ratified on-recommendation all 8 sub-Qs):

- Backend: drop the perspective WHERE clause in `queryFollowUpRows`; return all rows; add server-computed `is_cross_party` boolean.
- UI: render cross-party rows with a `Gegenseitig` badge, muted/greyed style, unchecked by default, date visible.
- Write-back: cross-party rows are **unconditionally excluded** from the project-deadline bulk insert, even if the user manually checks the box.

Composite `condition_expr` (and-of-flags) — checkbox is read-only in the result view; Verfahrensablauf is the canonical toggle surface for individual flags.

Sync: `document.dispatchEvent(new CustomEvent('scenario-flag-changed', { detail: { flag, value } }))`. Single-tab v1; cross-tab in Akte mode deferred.

---

## §3 Tier 2 — surface decisions

### §3.1 Appeal re-split: revert upc.apl.unified → merits/cost/order (m's Q5 divergent pick)

**m's call (2026-05-27):** *"Reverse the unification as suggested in 3. They are different proceedings, I only wanted the approach to be unified in the 'determinator' — but they are actually different proceedings!"*

The current state (mig 096 unified the appeal track):
- id=160 `upc.apl.unified` is `is_active=true`, holds 16 rules.
- id=11 `upc.apl.merits` is `is_active=false`.
- id=19 `upc.apl.cost` is `is_active=false`.
- id=20 `upc.apl.order` is `is_active=false`.
- 4 spawn rules point at id=11 (inactive) — looks like the R3 bug but is actually correctly aimed at merits since cost+order arrive differently (athena R3 partially mis-classified the situation).
- Event codes already carry the split prefix: `upc.apl.{merits,cost,order}.*`. 16 events split cleanly into 7 merits + 2 cost + 7 order.

The migration:

```sql
-- Mig P1: re-activate the three discrete appeal PTs and retire the unified row.
UPDATE paliad.proceeding_types SET is_active = true WHERE id IN (11, 19, 20);
UPDATE paliad.proceeding_types SET is_active = false WHERE id = 160;

-- Mig P1: re-target each rule whose proceeding_type_id is currently 160
-- to the right reactivated PT based on its event_code prefix.
UPDATE paliad.sequencing_rules sr
   SET proceeding_type_id = 11
  FROM paliad.procedural_events pe
 WHERE pe.id = sr.procedural_event_id
   AND sr.proceeding_type_id = 160
   AND pe.code LIKE 'upc.apl.merits.%';

UPDATE paliad.sequencing_rules sr
   SET proceeding_type_id = 19
  FROM paliad.procedural_events pe
 WHERE pe.id = sr.procedural_event_id
   AND sr.proceeding_type_id = 160
   AND pe.code LIKE 'upc.apl.cost.%';

UPDATE paliad.sequencing_rules sr
   SET proceeding_type_id = 20
  FROM paliad.procedural_events pe
 WHERE pe.id = sr.procedural_event_id
   AND sr.proceeding_type_id = 160
   AND pe.code LIKE 'upc.apl.order.%';

-- 4 spawn FKs: stay at id=11 (merits) for inf/rev/dmgs; update upc.pi.cfi's
-- spawn to point at id=20 (order) — appeals against PI orders go to the
-- orders track, not merits.
UPDATE paliad.sequencing_rules
   SET spawn_proceeding_type_id = 20
 WHERE is_spawn AND procedural_event_id = (
   SELECT id FROM paliad.procedural_events WHERE code = 'upc.pi.cfi.appeal_spawn'
 );
-- The other 3 spawn rules (inf/rev/dmgs) keep spawn_proceeding_type_id = 11
-- (correct after re-activation).
```

**Determinator UX preserved.** `internal/services/proceeding_mapping.go` (t-paliad-204 S1) keeps its single "Berufung" front door. The mapping fans out to id=11/19/20 based on what's being appealed (judgment / cost decision / order). No user-facing routing change. The change is purely structural.

**Active scenarios / projects pointing at id=160:** none (`paliad.scenarios` and `paliad.projects.active_scenario_id` both empty per athena §0; only 6 projects have any `proceeding_type_id` set and none of them is 160). Zero data migration on the project side.

### §3.2 Empty PTs — show with "Keine Regeln gepflegt" badge

Per m's Q6 — option 2 with a follow-on editorial note ("We need to publish rules then... but yeah, show with the badge for now"):

Picker query for `/api/tools/proceeding-types` gains a flag-not-filter:

```sql
SELECT pt.*,
       EXISTS (
         SELECT 1 FROM paliad.sequencing_rules sr
          WHERE sr.proceeding_type_id = pt.id
            AND sr.is_active AND sr.lifecycle_state = 'published'
       ) AS has_rules
  FROM paliad.proceeding_types pt
 WHERE pt.is_active AND pt.kind = 'proceeding';
```

Frontend renders the chip with a muted/disabled treatment + badge "Keine Regeln gepflegt" when `has_rules = false`. Project creation can still bind to an empty PT (admin override), but Mode A/B/Verfahrensablauf surface a clear "this proceeding has no seeded rules yet" message.

Editorial follow-up: m publishes rules for the 6 empty PTs (`upc.bsv.cfi`, `upc.ccr.cfi`, `upc.costs.cfi`, `upc.dni.cfi`, `upc.epo.review`, `upc.pl.cfi`) over time; each new published rule auto-removes the badge for its PT. Not blocking this design.

### §3.3 Entry A — extend /tools/verfahrensablauf

Per m's Q7. The existing `/tools/verfahrensablauf` page (used by `frontend/src/client/verfahrensablauf.ts` + shared `views/verfahrensablauf-core.ts`) already serves the pick-a-PT shape. Extend it to:

- Render the parent_id chain as a **collapsible tree** (top-down chronological). Same data shape as §1.2's ASCII trees.
- Expose **optionals + conditionals as toggleable checkboxes** in the tree itself. Ticking writes via `PATCH /api/projects/{id}/scenario-flags` (Akte mode) or localStorage (kontextfrei).
- Reflect cross-party rows with the same muted style as §2.4 (Gegenseitig badge).
- Spawn rows render as **leaf with edge annotation** (⇲ Berufungsverfahren öffnen) and a "create child case" CTA in Akte mode.
- Optionally: a "Zur Frist-Ansicht" deeplink on each tree node → opens Mode B Fristenrechner with that event pre-locked as the trigger.

Backend: extend `/api/tools/fristenrechner` (the proceeding-type fan-out endpoint) to return a tree-shaped payload (`parent_id` resolved into nested children). New handler param or new endpoint `/api/tools/verfahrensablauf/tree?proceeding_type_code=X&project=Y`.

The legacy `/tools/fristenrechner?legacy=1` Procedure-mode page deprecates naturally — same scope, replaced by this Entry A view.

### §3.3a Verfahrensablauf view-mode toggle

A three-way segmented control above the tree at the Verfahrensablauf surface:

```
┌─ Anzeige ──────────────────────────────────────┐
│ ( ) Nur Pflicht   (•) Gewählt   ( ) Alle Optionen │
└────────────────────────────────────────────────┘
```

Behaviour:
- **Nur Pflicht**: only `priority='mandatory'` cards render. Tightest view.
- **Gewählt** (default): mandatory + every rule that resolves to "selected" given current scenario state (mandatory always; recommended unless explicitly deselected via `rule:X = false`; optional only if explicitly selected via `rule:X = true`; conditional only if its flag predicate holds AND the priority-default-or-deviation puts it in the selected set). Honest summary of what *this* lawyer has chosen for *this* project.
- **Alle Optionen**: everything that could belong, with unselected optionals rendered with the dotted-border + `[Aufnehmen]` CTA, and conditional rules whose flag isn't set rendered greyed with the activation hint.

**Persistence**: per-user, per-browser via `localStorage` under key `verfahrensablauf:view_mode`. Not project-scoped — the same user looking at two different projects probably wants the same verbosity. Not in `scenario_flags` either — view-mode is a UI preference, not a scenario fact. No new schema; no API; no migration.

Cross-surface sync: the **Mode B result view** does NOT carry its own view-mode toggle. It always renders in "Gewählt" semantics (mandatory + selected). Rationale: Mode B locks a single trigger event and lists its follow-ups; the lawyer isn't browsing the full ablauf, they're focused on one moment. The view-mode toggle is a Verfahrensablauf-only affordance.

The view-mode toggle composes with the scenario-flags strip (§2.3). Toggling "Mit Widerklage auf Nichtigkeit" off in "Gewählt" mode removes the CCR conditional branch from view; flipping to "Alle Optionen" re-renders the CCR branch greyed with the activation hint. The user can see what they're *not* currently considering without losing the simplified default view.

### §3.4 Legacy `/api/tools/event-deadlines` deprecation

Per m's Q8. Sequence:

1. **Mig P3 — 73-globals reparenting completes** (§4.2, editorial work). Once `paliad.sequencing_rules WHERE proceeding_type_id IS NULL` is empty, the legacy route has no live data shape it uniquely serves.
2. **Code drop:** remove `/api/tools/event-deadlines` route + `EventDeadlineService` + the `deadline_rule_service.go:226-285` label-fallback path + the `ExportService:1680` workbook sheet.
3. **Table drop:** `DROP TABLE paliad.trigger_events` (mig P4, §4.3).
4. **Snapshot generator:** `cmd/gen-upc-snapshot/main.go` stops reading `paliad.trigger_events`; UPC snapshot for youpc.org only carries the unified rule shape.

The cleanup is gated on §4.2 completion. If editorial backfill is slow, the route can live behind a `/api/legacy/` prefix until done — but the design assumption is that we close the loop within the slice train.

---

## §4 Tier 3 — editorial + cleanup framework

### §4.1 `condition_expr` grammar formalisation

Per m's Q9. The grammar:

```ts
type CondExpr =
  | { flag: KnownFlag }                                 // leaf
  | { op: 'and' | 'or'; args: CondExpr[] }              // composite (recursive)

type KnownFlag = 'with_ccr' | 'with_amend' | 'with_cci' // closed set; extensible via admin
```

Implementation:

- A JSON-schema validator in `RuleEditorService.create`/`update` rejects writes that don't match. Today's 18 rules all conform; no data migration.
- Known-flag whitelist sourced from a small Go constant + an admin-editable `paliad.scenario_flag_catalog(name, description, added_at)` table — keeps the vocabulary discoverable. (Lightweight ALTER, not a major migration.)
- Engine consumer (`pkg/litigationplanner/expr.go`, currently a switch over string literals) gains exhaustive-case enforcement against the same catalog. Linter catches drift between catalog and engine.

`choices_offered` and `applies_to_target` (athena R11) — same grammar treatment in a separate ticket (not blocking this revision). Document their 3 known shapes (`appellant`, `skip`, `include_ccr`) in code comments meanwhile.

### §4.2 Editorial backfill workflow — `/admin/procedural-events` parent-NULL filter

Per m's Q10:

- Add filter chip "parent: nicht gesetzt" to the admin list at `/admin/procedural-events`. The filter URL `?parent_filter=null` (or similar).
- Track completion per PT via the existing gap-map query (athena §3.1) — show as a progress bar in the admin shell ("upc.inf.cfi: 4/4 roots OK" / "de.inf.lg: 2/5 roots remain").
- For the 73 globals: a separate filter `?orphan=true` showing only `proceeding_type_id IS NULL` rules. m clicks each, assigns a PT + parent rule via the editor.
- Each save flips lifecycle_state to draft (unchanged from existing editor flow); m publishes a batch when satisfied with a PT.

No new code surface — the existing admin list + editor handle everything once the filter is added.

This is editorial work, not coder work. The design captures the framework; m drives the content at his own cadence. No mig is gated on completion (the parent-NULL filter is a feature add; rules stay valid in their current shape during the walk).

#### §4.2.1 Worked editorial example — R.109 translation chain

m flagged this case (14:35) as a concrete instance of malformed parent-chain shape. The current data for `upc.inf.cfi`:

| rule | event | current parent | current primary_party | correct shape |
|---|---|---|---|---|
| `RoP.109.1` | `upc.inf.cfi.translation_request` (Antrag auf Simultanübersetzung) | upc.inf.cfi root (Mündliche Verhandlung) | both | parent stays at MV; flagged optional (default-unselected) |
| `RoP.109.4` | `upc.inf.cfi.interpreter_cost` (Mitteilung Dolmetscherkosten) | upc.inf.cfi root (Mündliche Verhandlung) — **WRONG** | court — **WRONG** | parent = R.109.1; primary_party = both (parties give the Mitteilung, not the court); condition_expr = `{"flag": "with_interpreter_denied"}` |
| `RoP.109.5` | `upc.inf.cfi.translations_lodge` (Übersetzungen einreichen) | upc.inf.cfi root | both | parent = R.109.1 (lodging follows the request); priority stays mandatory but conditional via `{"flag": "with_translation_granted"}` |

Two new scenario flags introduced (`with_interpreter_denied`, `with_translation_granted`) get added to the `scenario_flag_catalog` (§4.1) when the editor saves these rules.

Editorial walk for m:
1. Open `/admin/procedural-events?orphan=false&parent_filter=null&proceeding_type=upc.inf.cfi`.
2. Find R.109.1, R.109.4, R.109.5 — they sit at depth 1 under the root.
3. Edit R.109.4: set `parent_id = <R.109.1's id>`; set `primary_party = both`; set `condition_expr = {"flag": "with_interpreter_denied"}`. Save (draft).
4. Edit R.109.5: set `parent_id = <R.109.1's id>`; set `condition_expr = {"flag": "with_translation_granted"}`. Save (draft).
5. Publish both.
6. The catalog accepts the two new flag names; the validator updates.

Result in the Verfahrensablauf tree (post-fix):

```
upc.inf.cfi root
├─ Mündliche Verhandlung (court · M)
├─ Antrag auf Simultanübersetzung (RoP.109.1) [both · O]
│  ├─ Mitteilung Dolmetscherkosten (RoP.109.4) [both · M · ?with_interpreter_denied]
│  └─ Übersetzungen einreichen (RoP.109.5) [both · M · ?with_translation_granted]
```

In **Gewählt** mode without scenario flags: only the root + Mündliche Verhandlung surface. R.109.1 is an unselected optional → hidden. R.109.4 + R.109.5 are conditional + below an unselected ancestor → hidden.

In **Gewählt** mode after the user clicks `[Aufnehmen]` on R.109.1: R.109.1 appears. R.109.4 still hidden (its flag `with_interpreter_denied` isn't set; the user would need to know the court denied the Antrag, then tick the flag in the Szenario-Flags strip). R.109.5 similarly hidden until `with_translation_granted` is on.

In **Alle Optionen** mode: every rule renders, conditionals greyed with their flag hint, R.109.1 dotted with `[Aufnehmen]`.

This is the model in miniature: the editorial fix is data-only (no schema change, just `parent_id` + `condition_expr` + `primary_party` UPDATEs via the editor); the display fix is policy that the existing scenario_flags + view-mode mechanism already supports.

### §4.3 `paliad.trigger_events` table fate — drop

Per m's Q11. Sequence (chained to §3.4):

1. After 73-globals reparented + route dropped + label-fallback ported to `procedural_events.name`:
2. `DROP TABLE paliad.trigger_events` (mig P5, last in the train).
3. Migrate `cmd/gen-upc-snapshot/main.go` to no longer SELECT from this table.
4. Remove the `ref__trigger_events` sheet from `ExportService` workbook output.

The bigint PK / parallel taxonomy disappears entirely. `procedural_events` (uuid PK) is the only event catalog.

---

## §5 Schema delta + migration plan (slice train)

Six slices, sequential where data-coupled, parallelisable where not. Each slice ships as one or two PRs.

| Slice | Mig | What ships | Reversible? |
|---|---|---|---|
| **P0 — Scenario SSoT** | mig 154 | `ALTER TABLE projects ADD COLUMN scenario_flags jsonb`; GET/PATCH endpoints w/ extended whitelist (named flags + `rule:<uuid>` per-rule entries, validated against project's PT rule set); Verfahrensablauf + result-view binding; `scenario_flag_catalog` table (§4.1) | Yes — DROP COLUMN |
| **P1 — Appeal re-split** | mig 155 | UPDATE proceeding_types (re-activate 11/19/20, deactivate 160); UPDATE sequencing_rules (rebind 16 rules to merits/cost/order by event_code prefix); UPDATE pi.cfi spawn FK → 20 | Reversible by inverse UPDATEs; documented in down mig |
| **S1+S1a from t-paliad-327** | — | Cross-party display backend + frontend; spawn-only picker filter (`sr.is_spawn = false` in SearchEvents) | Yes — code-only |
| **P2 — Empty-PT badge** | — | `has_rules` flag on /api/tools/proceeding-types; frontend muted-chip rendering | Yes — code-only |
| **P3 — Entry A (Verfahrensablauf tree)** | — | Tree endpoint + tree UI in /tools/verfahrensablauf; three-way view-mode toggle (localStorage); per-rule `[Aufnehmen]`/`[Entfernen]` chips wire to scenario_flags `rule:<uuid>` entries; subtree-hide-on-unselected-ancestor render logic | Yes — code-only |
| **P4 — Editorial walk (73 globals)** | — | parent-NULL filter on /admin/procedural-events; editorial work by m (no coder task per se) | Trivially reversible |
| **P5 — trigger_event_id deprecation** | mig 156 | DROP `/api/tools/event-deadlines`; DROP `EventDeadlineService`; port label-fallback in deadline_rule_service.go; remove ref__trigger_events sheet; `ALTER TABLE sequencing_rules DROP COLUMN trigger_event_id`; `DROP TABLE trigger_events`; condition_expr write-time validator | Last; downgrade requires re-adding column + re-populating — irreversible in practice |

Constraint: **P5 is gated on P4 completion** (no rules can have NULL proceeding_type_id when DROP runs). All other slices ship independently.

Ordering rationale:
- P0 unblocks the Fristenrechner-side bugs immediately (no waiting on appeal-split editorial).
- P1 is data-only, low risk, can land in parallel with P0.
- S1+S1a are code-only follow-ons to P0 (same scenario-flag plumbing).
- P2 ships once P1 lands (re-activated PTs need badge support too).
- P3 builds on P2 + the tree endpoint; depends on P0 for flag persistence.
- P4 is m's editorial work — duration depends on m's cadence, not coder velocity.
- P5 is the cleanup at the end. Only safe when P4 is done.

---

## §6 Entry A UI spec (sequence-from-proceeding-type)

Live URL: `/tools/verfahrensablauf?project=<id>&proceeding_type=upc.inf.cfi`.

### §6.1 Layout

```
┌─ Akte / kontextfrei ─────────┐  ┌─ Verfahren ──┐  ┌─ Anzeige ──────────────────────────┐
│ HL-2024-001 ▼ │ ohne Akte    │  │ upc.inf.cfi ▼│  │ Nur Pflicht ⦿ Gewählt ○ Alle Optionen │
└──────────────────────────────┘  └──────────────┘  └────────────────────────────────────┘

┌─ Szenario-Flags ──────────────────────────────────┐
│ ☑ Mit Widerklage auf Nichtigkeit (with_ccr)        │
│ ☐ Mit Antrag auf Patentänderung R.30 (with_amend) │
│ ☐ Mit Widerklage auf Verletzung (with_cci)         │
└────────────────────────────────────────────────────┘

┌─ Ablauf ── (view-mode: Gewählt) ───────────────────────────────────┐
│ 📥 Klageerhebung                          [claimant · mandatory]    │
│   ├─ Klageerwiderung                      [defendant · mandatory]   │
│   │     └─ Replik                         [claimant · M · ?with_ccr]│
│   │           └─ Duplik                   [defendant · M · ?with_ccr]│
│   ├─ Widerklage auf Nichtigkeit           [defendant · O · ?with_ccr][Entfernen]│  ← selected optional
│   │     └─ Erwiderung auf CCR             [claimant · M · ?with_ccr]│
│   │           └─ Replik auf Erw. CCR      [defendant · M · ?with_ccr][Gegenseitig]│
│   │                 └─ Duplik auf Replik  [claimant · M · ?with_ccr]│
│   └─ ⇲ Berufungsverfahren öffnen          [SPAWN → upc.apl.merits]  │
│ 🏛️ Zwischenanhörung                       [court · mandatory]       │
│ 🏛️ Mündliche Verhandlung                  [court · mandatory]       │
│ ⚖️ Endentscheidung                        [court · mandatory]       │
└────────────────────────────────────────────────────────────────────┘

      ↓ (user flips view-mode to "Alle Optionen")

┌─ Ablauf ── (view-mode: Alle Optionen) ─────────────────────────────┐
│ 📥 Klageerhebung                          [claimant · mandatory]    │
│   ├─ ┄ Vorl. Einwendungen      [defendant · O] [Aufnehmen]┄         │  ← unselected, dotted
│   ├─ Klageerwiderung                      [defendant · mandatory]   │
│   ├─ Widerklage auf Nichtigkeit           [defendant · O · ?with_ccr][Entfernen]│
│   ├─ ┄ Antrag auf Patentänderung          [O · ?with_amend] greyed │  ← flag not set
│   │     └─ wenn 'Mit Patentänderung' im Szenario aktiv             │
│   ├─ ┄ Antrag auf Simultanübersetzung     [O] [Aufnehmen]┄         │  ← post-§4.2.1
│   │     ├─ ┄ Mitteilung Dolmetscherkosten [M · ?with_interpreter_denied]│
│   │     └─ ┄ Übersetzungen einreichen     [M · ?with_translation_granted]│
│   ├─ ┄ Antrag CMO-Überprüfung             [both · O] [Aufnehmen]┄  │
│   ├─ ┄ Antrag Folgenanordnungen R.118(4)  [both · O] [Aufnehmen]┄  │
│   └─ ⇲ Berufungsverfahren öffnen          [SPAWN → upc.apl.merits]  │
│ 🏛️ ...                                                              │
└────────────────────────────────────────────────────────────────────┘
```

### §6.2 Behaviour

- **Project picker (Step 0)** unchanged from Fristenrechner.
- **Proceeding-type picker** chips → switching re-fetches the tree.
- **View-mode toggle (§3.3a)** — three-way segmented control (Nur Pflicht / Gewählt / Alle Optionen). State in `localStorage["verfahrensablauf:view_mode"]`. Default = "Gewählt". Re-renders the tree on toggle; no network call.
- **Szenario-Flags strip** reads/writes `projects.scenario_flags` (Akte) or localStorage (kontextfrei). Same `scenario-flag-changed` CustomEvent as Mode B's result view — both surfaces stay in sync. Flag entries (`with_ccr` etc.) live alongside per-rule entries (`rule:<uuid>`) in the same jsonb.
- **Per-rule selection chips** — every non-mandatory rule's card carries `[Aufnehmen]` (unselected → tick selects) or `[Entfernen]` (selected → tick deselects). The handler PATCHes `projects.scenario_flags` with `{ "rule:<uuid>": true|false }` and fires the same `scenario-flag-changed` event.
- **Subtree hide-on-deselect** — when a chain head (any rule with children via `parent_id`) is unselected in "Gewählt" mode, its descendants don't render. The tree walker checks each rule's full ancestor chain; any unselected ancestor hides the descendant. In "Alle Optionen" mode, descendants render greyed under the unselected ancestor.
- **Cross-party rows** render with `Gegenseitig` badge, muted style (same as Mode B result view §2.4). Composes with selection state and view-mode independently.
- **Spawn rows** render as leaves with the ⇲ symbol + "Neues Verfahren öffnen" CTA (Akte mode only; kontextfrei shows the badge without the CTA). Spawn rows ignore selection state — they always render in "Gewählt" + "Alle Optionen" modes since they represent a possible next-procedure rather than an in-scenario deadline.
- **Empty PT** (the 6 unruled): tree area renders an inline "Für dieses Verfahren sind noch keine Regeln gepflegt" message + a link to /admin if the user is admin.
- **Deeplink to Mode B:** each tree node has a "Frist berechnen" link that opens `/tools/fristenrechner?event=<code>&trigger_date=…&project=…`.

### §6.3 Backend

New handler: `GET /api/tools/verfahrensablauf/tree?proceeding_type=upc.inf.cfi&project=<id>` returns:

```jsonc
{
  "proceeding_type": { "code": "upc.inf.cfi", "name_de": "...", "name_en": "..." },
  "scenario_flags": { "with_ccr": true, "with_amend": false },
  "tree": [
    {
      "rule_id": "...", "event_code": "upc.inf.cfi.soc",
      "name_de": "Klageerhebung", "primary_party": "claimant",
      "priority": "mandatory", "has_condition": false, "is_spawn": false,
      "is_cross_party": false,
      "children": [
        { "rule_id": "...", "event_code": "upc.inf.cfi.sod", ... , "children": [...] },
        ...
      ]
    },
    ...  // chain-anchored roots
  ]
}
```

The tree is the result of walking `parent_id` recursively from the PT's root rules (those with `parent_id IS NULL` for this PT). Computed via one recursive CTE; cached per-PT (the tree shape changes only on rule edits).

`is_cross_party` is computed against `projects.our_side` (Akte mode) or the request's `?party=` query param (kontextfrei).

---

## §7 Entry B UI spec — reaffirms shipped Fristenrechner Mode A+B

Mode A (`/tools/fristenrechner?mode=search`) and Mode B (`?mode=wizard`) — both shipped via t-paliad-322 S1-S6. Surgical follow-ons from t-paliad-327 design (§0.2):

- Mode A search: add `AND sr.is_spawn = false` to `SearchEvents` WHERE block + add the derived-trigger filter `EXISTS (non-spawn child)` from §2.2. Compiled together as one PR (S1+S1a).
- Mode B R4 chip-strip: identical filter on the wizard's event-pool query.
- Result view: stop filtering follow-ups by party server-side (§2.4); render cross-party with badge.
- Scenario flag binding: result-view CONDITIONAL group reads/writes `projects.scenario_flags` via the new API (P0). Same CustomEvent sync as Entry A.

No layout changes. The mode tabs (⚡ Direkt suchen / 🧭 Geführt) stay as today. The 3rd entry path is Entry A on the verfahrensablauf page — not a Mode C.

---

## §8 Worked examples

### §8.1 Entry A — claimant on HL-2024-001 (upc.inf.cfi, with_ccr=true)

User opens `/tools/verfahrensablauf?project=HL-2024-001&proceeding_type=upc.inf.cfi`.

- Project context loads. `scenario_flags = {with_ccr: true}`.
- Tree GET returns the §1.2 shape, with conditional rules' `has_condition` flagged.
- UI renders: top-level SoC anchor → branches. The CCR branch is fully expanded because `with_ccr=true`. The R.30 amend branch renders but conditionals are greyed (with_amend=false).
- User clicks "Mit Antrag auf Patentänderung R.30" in the Szenario-Flags strip.
- Frontend fires `PATCH /api/projects/HL-2024-001/scenario-flags { with_amend: true }`. Server stores. CustomEvent dispatches.
- Tree re-renders: R.30 amend branch ungreys; conditional rules become live.
- User scrolls to "Erwiderung auf CCR" → clicks "Frist berechnen" → deeplinks to Mode B with `event=upc.inf.cfi.def_to_ccr&trigger_date=<today>&project=HL-2024-001`.
- Mode B result view loads. Cross-party RoP.029.d (defendant Replik) shows with `Gegenseitig` badge.

### §8.2 Entry B — Mode A search after picker filter

User types "Berufung" in Mode A.

- Backend SQL (post-§2.2 + post-spawn filter):
  ```sql
  WHERE pe.name % 'Berufung' OR pe.code % 'Berufung'
    AND sr.is_active AND sr.is_spawn = false
    AND EXISTS (
      SELECT 1 FROM paliad.sequencing_rules child
       WHERE child.parent_id = sr.id AND child.is_active AND NOT child.is_spawn
    )
  ```
- Returns: real triggers in the appeal track (`upc.apl.merits.notice`, `upc.apl.merits.grounds`, `upc.apl.order.with_leave`, etc. — post-Q5 split). Does NOT return: `upc.{inf,rev,pi,dmgs}.cfi.appeal_spawn` (spawn-only) or terminal leaves (no children).

User picks `upc.apl.merits.notice` → result view loads its follow-ups. Tree renders cleanly because the Q5 split gave merits its own chain root.

### §8.3 Editorial flow — m reparents a legacy global

m opens `/admin/procedural-events?orphan=true`. Sees the 73-row list.

- m clicks row "Antrag auf Verlängerung der Klagefrist" (one of the legacy globals with `proceeding_type_id NULL`).
- Editor opens. m assigns `proceeding_type_id = upc.inf.cfi` and `parent_id = <RoP.013.1 soc rule>`.
- Save. Rule lifecycle flips to draft. m clicks Publish.
- The rule now sits under upc.inf.cfi's tree as a hop-1 child of SoC. Mode A picker EXISTS check passes for SoC (was already passing); the tree gains one more chip.
- 72 globals to go. m walks at own cadence; no coder time blocked.

---

## §9 Out of scope

- **Calculator (`pkg/litigationplanner.CalculateRule`).** Working as designed.
- **Holiday / working-day logic.** Out of scope.
- **`choices_offered` + `applies_to_target` formalisation** (athena R11). Same shape as condition_expr would warrant — separate ticket once condition_expr formalisation ships.
- **Adding new proceeding_types.** The 23 are stable; editorial work fills the 6 unruled ones.
- **DE-side spawn edges** (LG → OLG → BGH as spawns instead of separate projects). Possible v2; not driven by current pain.
- **AI-extracted deadlines from documents.** Deferred per memory `b6a11b55…`.
- **Cross-tab scenario-flag sync in Akte mode.** Single-tab v1; SSE/WebSocket if it matters later.
- **`event_kind` ENUM-ing** (athena R10). Cosmetic; vocab is stable.

---

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

All 12 questions answered via `AskUserQuestion` on 2026-05-27 ~13:55 (3 batches of 4). 11 picks on-recommendation; Q5 diverged with verbatim reasoning. Plus 8 pre-ratified picks from t-paliad-327 carried forward (§0.2).

### Tier 1 — model decisions

- **Q1 (Trigger link canonical): `parent_id` wins, deprecate `trigger_event_id`.** [= recommendation] **Locks §2.1.** Drop the column after backfill completes.
- **Q2 (73 legacy globals fate): Reparent onto PT chains via editorial walk.** [= recommendation] **Locks §4.2.** m drives the walk at admin /admin/procedural-events; the orphan filter is the only new UI surface.
- **Q3 (Trigger discoverability): Derive from data.** [= recommendation] **Locks §2.2.** EXISTS subquery on parent_id; no new column, no view.
- **Q4 (Scenario SSoT shape): `projects.scenario_flags jsonb`.** [= recommendation; confirms t-paliad-327 design under wider scrutiny] **Locks §2.3.**

### Tier 2 — surface decisions

- **Q5 (Appeal taxonomy): Reverse the unification — split upc.apl.unified back into merits/cost/order.** [≠ recommendation; m picked option 3, "reverse the unification"] m's verbatim:
    > yes, reverse the unification as suggested in 3. They are different proceedings, I only wanted the approach to be unified in the "determinator" - but they are actually different proceedings!
    **Updates §1.4 + §3.1.** Mig P1 re-activates id=11/19/20, retires id=160, rebinds 16 rules by event_code prefix, retargets the pi.cfi spawn FK to id=20. Determinator routing layer (proceeding_mapping.go) keeps the single "Berufung" front door but fans out to the 3 PTs.
- **Q6 (Empty PTs): Show with "Keine Regeln gepflegt" badge for now.** [= recommendation; option 2] m's note: "We need to publish rules then... but yeah, show with the badge for now." **Locks §3.2.** Editorial follow-up is m's; not blocking the design.
- **Q7 (Entry A location): Fold into /tools/verfahrensablauf.** [= recommendation] **Locks §3.3 + §6.**
- **Q8 (Legacy /event-deadlines route): Drop after Tier 1 + 73-globals reparenting.** [= recommendation] **Locks §3.4. Gated on §4.2 completion.**

### Tier 3 — editorial + cleanup framework

- **Q9 (condition_expr grammar): Lock to `{flag: "X"} | {op: "and"|"or", args: [...]}`.** [= recommendation] **Locks §4.1.** Write-time JSON-schema validator + known-flag catalog table.
- **Q10 (Editorial backfill workflow): Admin /admin/procedural-events with parent-NULL filter.** [= recommendation] **Locks §4.2.** No new UI surface beyond the filter chip.
- **Q11 (`trigger_events` table fate): Drop after route is gone.** [= recommendation] **Locks §4.3.** Sequenced as Mig P5, last in the slice train.
- **Q12 (Visual format): ASCII trees per PT + Mermaid for spawn edges.** [= recommendation] **Locks §1.2 + §1.3.**

### 10.0a Post-ratification additions (m, 2026-05-27 14:34–14:40)

After the §10 main grilling, m added three directions on top of the ratified design. None re-opened a Tier 1 decision; all extended the Verfahrensablauf surface.

- **Selection state + detail-level filter (m 14:40, supersedes earlier "rarity" framing).** Every optional rule becomes a per-scenario selectable card; selection state lives in the existing `projects.scenario_flags jsonb` with extended shape (`{flag: bool, "rule:<uuid>": bool}`). Recommended = default-selected; optional = default-unselected; mandatory = locked. Deviations only land in storage. No new column on `sequencing_rules`. **Locks §2.4a.** Replaces the pre-clarification strawman that proposed `is_edge_case boolean` — m's reframe makes that wrong (rarity is a scenario property, not a rule property).
- **View-mode toggle on Verfahrensablauf.** Three-way segmented control: Nur Pflicht / Gewählt / Alle Optionen. Per-user persistence via `localStorage["verfahrensablauf:view_mode"]`. Default "Gewählt". **Locks §3.3a.** Mode B result view does NOT carry the toggle — it's a Verfahrensablauf-only affordance.
- **R.109 chain editorial worked example.** m flagged R.109.1 / R.109.4 / R.109.5 as a concrete editorial-backfill case (wrong parent_id, wrong primary_party on R.109.4, missing condition_expr on R.109.4/.5). Folded as **§4.2.1** worked example demonstrating the parent-NULL filter workflow without code change. Two new scenario-flag names introduced (`with_interpreter_denied`, `with_translation_granted`); both land in the `scenario_flag_catalog` (§4.1) at edit time.

These additions don't change the slice train sequence (§5). They tighten P0 (the `scenario_flags` PATCH endpoint now validates `rule:<uuid>` keys against the project's active rule set) and P3 (Entry A tree now renders the view-mode toggle + per-rule selection chips), but no new mig is added.

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

Beyond §10.0a additions, the Q5 divergence is the only material change:

- **Mig P1 (appeal re-split)** is now part of the slice train. It was NOT in the strawman; the strawman assumed athena's R3 was a simple FK retarget. m's pick recasts the unification itself as the bug.
- §1.4 per-PT table shows 3 separate appeal PT rows (merits/cost/order) instead of one unified row. The 16 rules under id=160 redistribute to id=11/19/20.
- §1.3 spawn graph fan-out has merits (3 edges from inf/rev/dmgs) + order (1 edge from pi) as distinct targets instead of all 4 pointing at a single unified row.

All other §1-§8 sections hold as originally drafted.

---

## §11 Synthesis links

- mBrian: file as `[synthesis]` linked `triggered_by` t-paliad-329; `related_to` athena's assessment (`document-assessment-deadline-system`) + my proceeding_types taxonomy synthesis + Fristenrechner overhaul synthesis + t-paliad-327 follow-up rules synthesis.
- Cross-refs: `docs/assessment-deadline-system-2026-05-27.md` (athena), `docs/design-fristenrechner-followup-rules-2026-05-27.md` (atlas, pre-ratified subset), `docs/design-fristenrechner-overhaul-2026-05-26.md` (cronus, S1-S6 shipped), `docs/design-proceeding-types-taxonomy-2026-05-26.md` (atlas, mig 153 shipped).
- Related migrations: 084 (condition_expr backfill), 136 (procedural_events additive), 140 (drop legacy deadline_rules), 145 (`scenarios` table), 153 (proceeding_types.kind).
- Coder phase (deferred per inventor SKILL): runs after m ratifies. Slice ordering per §5. NOT cronus (parked) / NOT atlas (inventor). A pattern-fluent Sonnet coder picks up P0 first; P1 + S1/S1a can parallelise; P3 follows; P4 + P5 are gated on each other.
