audit(t-paliad-157): Fristen logic — rules, triggers, conditionals

Phase 1 audit (AUDIT ONLY, no implementation). 799 lines, mai/pauli/fristen-logic-audit.

Headline findings:

- THREE parallel deadline-generation systems coexist with overlapping
  intent:
  - Pipeline A (proceeding-driven) — paliad.deadline_rules (172 rows),
    FristenrechnerService.Calculate, drives /tools/fristenrechner +
    SmartTimeline.
  - Pipeline B (single-rule subset of A) — Pathway B cascade click.
  - Pipeline C (event-driven, youpc legacy) — paliad.trigger_events
    (110) + paliad.event_deadlines (77), EventDeadlineService.Calculate,
    drives "Was kommt nach…" tab. Disjoint corpus from A.

- Rule corpus is RICHER than the brief implied: 32 columns, 172 rules
  across 27 proceeding_types (132 fristenrechner + 40 litigation). The
  dual-corpus is a latent footgun: paliad.projects.proceeding_type_id
  accepts both categories with no CHECK constraint, so a project's
  SmartTimeline depends on which code lands first.

- Data model already encodes most of m's mental model:
  multi-deadline triggers via parent_id chains (deepest live: 3
  levels in UPC_INF), conditional via condition_flag (AND-only),
  flag-swap via alt_duration_value / alt_rule_code, court-set via
  heuristic + 4-bucket classification, holiday adjustment via
  HolidayService+CourtService.

- Real gaps (§6, 13 of them):
  - Pipeline A/C redundancy (different capabilities, disjoint data).
  - Litigation vs fristenrechner corpus drift (no contract).
  - is_mandatory + is_optional overlap.
  - deadline_concept_event_types is config layer, NOT trigger model.
  - No real event-driven trigger endpoint.
  - AND-only condition_flag (no OR/NOT/compound).
  - Cross-proceeding spawn half-wired.
  - 9 orphan concepts with rule_count=0 (incl wiedereinsetzung,
    schriftsatznachreichung, weiterbehandlung).
  - condition_rule_id dead column.
  - Instance dimension (LG/OLG/BGH) not on paliad.projects.
  - 1/26 deadlines linked to rule_id (anchor-from-actuals barely
    used).
  - Court-set is heuristic, not first-class column.
  - Pipeline A lacks before / working_days / combine_op.

- The big m's-question: "all in the Rules so we should be able to
  manage" is FALSE today. Rules edits = SQL migrations only. §8
  proposes a 3-step ladder: status-quo / read-only admin / full
  editor with audit log.

- §7 has concrete extension proposal for each §6 gap (migration size
  costed).

- §9 has 15 open questions for m to call before Phase 2 starts.

- Live data sparse: 11/11 projects NULL proceeding_type_id, 1/26
  deadlines with rule_id — demand-side mostly empty even though
  supply-side (rules) is rich.

NOT cronus per memory directive 2026-05-06. NOT self-merged. Awaiting
m's go/no-go.
This commit is contained in:
mAi
2026-05-13 21:33:38 +02:00
parent 7d9935de60
commit b455df265e

View File

@@ -0,0 +1,799 @@
# Audit — Fristen logic (rules, triggers, conditionals)
**Author:** pauli (inventor)
**Date:** 2026-05-13
**Task:** t-paliad-157 (reactivated 2026-05-13 21:23 with broader scope)
**Phase:** 1 of 3 — Audit. Phase 2 = iterative refinement against m. Phase 3 = ship.
**Branch:** `mai/pauli/fristen-logic-audit` (fresh from `origin/main` @ `7d9935d`).
**Status:** AUDIT READY FOR REVIEW — m gates the audit → Phase 2 transition.
m's framing (paliad/head 11:46):
> the main roadmap thing now is "Fristen". We need the full "Fristen logic" and I am happy to work together with an AI to further design it. Most of it should be "straightforward" as specific events trigger specific deadlines, sometimes multiple and sometimes conditional. It is all in the Rules so we should be able to manage.
The audit answers: **is m's mental model already encodable in the existing data model, and where are the gaps?**
Short answer: the rule corpus is substantially richer than the brief implied, **three parallel deadline-generation systems coexist** (with overlapping responsibilities), and the main friction is *managing* rules (SQL-only today) rather than the expressive grammar of the rules themselves.
---
## 0. Premises verified live (live DB + live code, not migration files)
Live state queried via `mcp__supabase__execute_sql` against the `paliad` schema on the youpc Supabase Postgres. Code reads against `mai/pauli/fristen-logic-audit` baseline (origin/main @ `7d9935d`).
### 0.1 Rule corpus is ~5× richer than the brief implied
| Table | Rows | Note |
|---|---|---|
| `paliad.proceeding_types` | **27** | 20 `category='fristenrechner'` + 7 `category='litigation'`. All 27 carry rules. |
| `paliad.deadline_rules` | **172** | 132 against fristenrechner codes + 40 against litigation codes. |
| `paliad.deadline_concepts` | **56** | The "noun" layer (Klageerwiderung, Berufungsschrift, …) above rules. |
| `paliad.event_category_concepts` | **153** | Cascade-leaf → concept junction (with optional `proceeding_type_code` for context-conditional outcomes). |
| `paliad.deadline_concept_event_types` | **32** | Concept → event_type default suggestion (per jurisdiction). |
| `paliad.trigger_events` | **110** | youpc.org legacy import. Used by the "Was kommt nach…" mode. |
| `paliad.event_deadlines` | **77** | trigger_event → deadline_row, with `combine_op` ∈ {min,max} for composite leads. |
| `paliad.event_types` | **40+** | Concrete event types (upc_oral_hearing, upc_statement_of_defence, …). |
| `paliad.event_categories` | **125** (103 leaves) | Cascade taxonomy. Already audited in t-paliad-166. |
| `paliad.courts` | **41** | Forum picker for holiday-calendar regime resolution. |
| `paliad.holidays` | **55** | Seed of public holidays + court closures. |
| `paliad.deadlines` (live) | **26** | Persisted deadline instances. **Only 1 has `rule_id`.** |
| `paliad.project_events` | **89** | Audit-log entries. |
| `paliad.events` | **does not exist** | The brief mentioned `paliad.events`; the actual audit table is `paliad.project_events`. |
### 0.2 Three parallel deadline-generation systems exist today
| Pipeline | Data source | Calculator | Wire surface |
|---|---|---|---|
| **A — Proceeding-driven** | `paliad.deadline_rules` (172 rows) | `FristenrechnerService.Calculate(proceedingCode, triggerDate, opts)` (`internal/services/fristenrechner.go:139`) | POST `/api/tools/fristenrechner` (Pathway A wizard, Pathway B cascade, SmartTimeline projection via `ProjectionService.computeProjections`). |
| **B — Single-rule (subset of A)** | Same table | `FristenrechnerService.CalculateRule` (around line 480) | POST `/api/tools/fristenrechner/calculate-rule` (Pathway B cascade card-click → inline calc). |
| **C — Event-driven (youpc legacy)** | `paliad.trigger_events` + `paliad.event_deadlines` (separate tables) | `EventDeadlineService.Calculate(triggerEventID, triggerDate, courtID)` (`internal/services/event_deadline_service.go:92`) | POST `/api/tools/event-deadlines` (Pathway A wizard's "Was kommt nach…" trigger panel, `frontend/src/client/fristenrechner.ts:833`). |
Pipelines A and C have **disjoint data**, **disjoint capability sets**, and **overlapping intent**. See §2 for the full picture; §6.1 calls out the redundancy.
### 0.3 m's "it is all in the Rules so we should be able to manage" — the false premise
The rule corpus IS in one table (`paliad.deadline_rules`) — 172 rows, 32 columns, expressive. **But there is no application-level rule-management surface.** Every rule edit today is a SQL migration: `internal/db/migrations/{009,012,028,029,031,040,043,044,050,068,…}_*.up.sql`. The Calculate engine reads what's in the table, but the table is seeded by developers, not by m or any user.
m's "we should be able to manage" reads as a call for a first-class rule-editor in the app (see §8). That's the biggest unfilled deliverable in his framing.
### 0.4 Production data is sparse — demand-side largely empty
- **11/11 live projects have NULL `proceeding_type_id`** (per kelvin's t-paliad-178 §0 audit; re-confirmed). The projection pipeline (`projection_service.computeProjections:813`) early-returns when this is NULL, so the SmartTimeline forecast doesn't fire for any production project today.
- **Only 1/26 live deadlines has `rule_id` populated.** The rule → deadline linkage is barely exercised. Most deadlines were created manually (free-text title + due_date) before the rule-anchored flow existed.
- **89 project_events**: structural milestones + audit-log entries. No tight coupling to rule_ids today.
- **trigger_events / event_deadlines** carry 110+77 youpc-legacy rows. Whether they are exercised in production needs Pathway-A "Was kommt nach…" telemetry; out of audit scope.
### 0.5 Anchor files
Backend services that consume / produce rules:
- `internal/services/fristenrechner.go` — Pipeline A + B. The main calculator. **735 LoC.**
- `internal/services/deadline_calculator.go` — pure date-math used by Pipeline C.
- `internal/services/deadline_rule_service.go` — CRUD-ish read API. `List`, `GetRuleTree`, `Get`. Hydrates `ConceptDefaultEventTypeID` from `deadline_concept_event_types` for the create-form's Typ chip.
- `internal/services/event_deadline_service.go` — Pipeline C. **~300 LoC.**
- `internal/services/deadline_service.go` — persistence of `paliad.deadlines` instances.
- `internal/services/event_category_service.go` — cascade leaf → concept resolution (t-paliad-133).
- `internal/services/projection_service.go` — SmartTimeline (consumes Pipeline A).
- `internal/services/holidays.go` + `courts.go` — non-working-day adjustment.
Handlers:
- `internal/handlers/fristenrechner.go` — wires Pipelines A/B/C to HTTP routes.
- `internal/handlers/deadlines.go` — paliad.deadlines persistence.
- `internal/handlers/deadline_rules_db.go` — admin-style rule list endpoint (read-only).
Key migration history (rule corpus evolution):
- **009** (342 LoC) — initial seed (Tier 1, hand-coded).
- **012** (230 LoC) — Fristenrechner seed extension.
- **028** (353 LoC) — youpc.org rules import (Pipeline C tables).
- **029** (128 LoC) — Tier 1 rule fixes.
- **031** (193 LoC) — Tier 2 ports (more proceedings).
- **037 + 038** — concept layer addition.
- **040** (449 LoC) — concept seed + backfill.
- **043** (348 LoC) — DE_INF_OLG / DE_INF_BGH split (instance dimension).
- **044** (280 LoC) — DPMA proceedings.
- **048 + 049** — event_categories taxonomy (cascade).
- **050** — `is_bilateral` backfill (4 rules).
- **052** — Determinator ROP coverage audit fixes.
- **068** — `is_optional` column.
- **073 + 074** — `deadline_concept_event_types` (concept → event_type config layer).
Net rule-related migrations: **>20 files, >3000 LoC of SQL.** The rule corpus has accreted across many small migrations; no single canonical seed.
If anything in this audit conflicts with the live state, the live state wins.
---
## 1. The rule shape today — `paliad.deadline_rules` column-by-column
**32 columns.** Most are used; a few are vestigial. Every column verified against live row distribution.
### 1.1 Identity + relations
| Column | Type | Nullable | Role |
|---|---|---|---|
| `id` | uuid PK | NO | Primary key. Referenced by `paliad.deadlines.rule_id`, `paliad.deadline_rules.parent_id` (self-FK), `paliad.deadline_rules.condition_rule_id` (self-FK; unused — see §1.6). |
| `proceeding_type_id` | int FK → `proceeding_types.id` | YES | Almost always set; NULL would mean a cross-proceeding rule but **no live rule is NULL** today. |
| `parent_id` | uuid self-FK | YES | Rule depends on parent's calculated date as anchor. **108 / 172 rules have parent_id set** (= 63%). Forms a forest, one tree per proceeding. |
| `concept_id` | uuid FK → `deadline_concepts.id` | YES | Links the rule to a concept (cross-proceeding noun). **171 / 172 rules linked** (= 99.4%); the one un-linked rule is a stray. |
### 1.2 Identity strings + labels
| Column | Note |
|---|---|
| `code` | Rule-local code (e.g. `inf.sod`, `ccr.amend`). Used by `AnchorOverrides` map keys (rule_code → date). Mostly unique within a proceeding. |
| `name` (NOT NULL) | DE display name. |
| `name_en` (NOT NULL, default `''`) | EN display name. Empty for some older rules; UI falls back to `name`. |
| `description` | Optional long-form. Sparse. |
| `rule_code` | The *legal-citation* rule code (e.g. `RoP.23.1`, `§276(1) ZPO`). The UI shows this as the `RuleRef`. NOT the rule's identity — `code` is. |
| `legal_source` | Structured citation (e.g. `UPC.RoP.23.1`). Added by mig 038 + 040. **171/172 rules have it.** |
| `deadline_notes` / `deadline_notes_en` | Free-text legal-context notes shown in the UI. |
| `spawn_label` | Used with `is_spawn=true`: human label for "spawned rule" pattern. |
### 1.3 The math: anchor + offset + adjustment
| Column | Note |
|---|---|
| `duration_value` (NOT NULL, default 0) | Integer offset. `0` = court-set / root anchor / filed-with-parent (see §4). |
| `duration_unit` (NOT NULL, default `months`) | Live values: `days`, `months`, `weeks`. **No `working_days`** in live data (`EventDeadlineService` supports it; `deadline_rules` does not). |
| `timing` (default `after`) | Live value: **only `after`** in every row. `before` semantic is theoretically there but unused by Pipeline A. (Pipeline C honours `before` via `applyDuration`.) |
| `anchor_alt` | Single live value: `priority_date`. Used by exactly **one rule**: `EP_GRANT.ep_grant.publish` (Art. 93 EPÜ, 18mo from priority). Otherwise NULL → use parent's date / triggerDate. |
| `alt_duration_value` / `alt_duration_unit` / `alt_rule_code` | Swap-on-flag: when condition_flag is satisfied, the rule renders against the alt values instead of base. Used by UPC_INF `inf.reply` and `inf.rejoin` for the with_ccr swap (RoP.029.a / RoP.029.d). |
### 1.4 Conditional gating
| Column | Note |
|---|---|
| `condition_flag` | `text[]` array. **4 distinct value-sets live**: `[with_amend]`, `[with_cci]`, `[with_ccr]`, `[with_ccr, with_amend]`. Only on UPC_INF + UPC_REV (the 2 richest proceedings). Semantics: rule renders iff **every** element of the array is in caller's `Flags` set. AND semantics; **no OR/NOT today**. |
| `condition_rule_id` | uuid self-FK to another rule. **0 / 172 rows populated**. Dead column. Was intended as "rule X applies only if rule Y was triggered" but never wired up. |
### 1.5 Party + bilateral
| Column | Note |
|---|---|
| `primary_party` | Live values: `claimant`, `defendant`, `both`, `court`. Drives the timeline column / row color. NULL allowed. |
| `is_bilateral` (NOT NULL, default false) | When `primary_party='both'`, this column tells the renderer whether to **mirror the rule into both party columns** in the timeline (true), or **resolve to one side via perspective + appeal_filed_by** (false). Backfilled by mig 050 — only 4 rules carry `true`: DE_NULL r79, DE_NULL r116, EPA_OPP r79, EPA_APP r116. |
### 1.6 Flags + lifecycle
| Column | Note |
|---|---|
| `is_mandatory` (NOT NULL, default true) | "User must address this." Surfaces in UI badge. |
| `is_optional` (NOT NULL, default false) | Added by mig 068. **Distinct from is_mandatory** — semantics today: "the save-modal pre-unchecks these rows; the timeline still renders them." Live: e.g. UPC_INF `inf.cost_app` (RoP.151 Antrag auf Kostenentscheidung) — visible-but-defaulted-off. Naming is confusing (is_mandatory=true + is_optional=true would be self-contradictory); see §6.3. |
| `is_spawn` (NOT NULL, default false) | Marks the rule as a "spawn" — emitted when its parent decision fires, but the spawn itself starts a NEW timeline branch (e.g. Appeal off Decision). Used by 8 live rules: APP/AMD/CCR cross-proceeding spawns. **Spawn execution is half-wired**: `projection_service.go:896-901` notes "Cross-proceeding spawn — the calculator can return rules from another proceeding type (Appeal off Decision). We don't have that rule in our map; skip the dependency annotation but still surface the row." — i.e. the row appears in the response but the dependency-annotation graph breaks. |
| `is_active` (NOT NULL, default true) | Soft-delete. All 172 live rules have `is_active=true`; soft-delete unused so far. |
| `sequence_order` (NOT NULL, default 0) | Calculator walks rules in this order. Must be consistent with topological order on `parent_id` (parents before children). |
| `created_at` / `updated_at` | timestamps. |
| `event_type` (text, nullable) | One of `decision`, `filing`, `hearing`, `order`. **A category, NOT an FK** to `paliad.event_types`. Distinct from concept-level event_type linkage in §3. |
### 1.7 Vestigial / under-used
- `condition_rule_id` — 0 rows populated. Dead column.
- `description` — sparse, used as fallback notes.
- `is_mandatory` vs `is_optional` — overlapping semantics that need a clean re-think (§6.3).
---
## 2. Trigger model today — events to deadlines
There are **three parallel paths** from a user-observable event to a calculated deadline list. Understanding the redundancy is the most important takeaway of this audit.
### 2.1 Path A — Proceeding-driven (the main spine)
Caller: `/tools/fristenrechner` Pathway A (wizard), Pathway B B1 leaf click + B2 search, `ProjectionService.computeProjections` (SmartTimeline).
Flow:
1. User (or projection) picks a **proceeding_code** (e.g. `UPC_INF`) and a **trigger_date**.
2. `FristenrechnerService.Calculate(proceedingCode, triggerDate, opts)` runs.
3. Calculator loads `deadline_rules WHERE proceeding_type_id = $pt AND is_active`.
4. Walks rules in `sequence_order`. For each:
- Apply `condition_flag` gate (suppress if flags missing AND alt_duration_value is NULL; otherwise swap to alt_*).
- Resolve anchor: `anchor_alt='priority_date'` → use priorityDate; else `parent_id` → parent's computed date; else triggerDate.
- Apply `AnchorOverrides[rule_code]` if user set an override.
- 4-bucket court-set classification (§4).
- Calculate offset, apply holiday/weekend adjustment via `HolidayService`, store in `computed[code]` map.
5. Returns `UIResponse{Deadlines: []UIDeadline}` — the full timeline.
Strengths:
- Rich (condition flags, parent chains, anchor_alt, override map, court-set semantics).
- Single source of truth for /tools/fristenrechner + SmartTimeline.
- Backed by 172 rules across 27 proceedings.
Weaknesses:
- Returns the **whole proceeding** every call. No "give me only the rules triggered by event X" mode.
- Cross-proceeding spawn (is_spawn rules) is half-wired (§1.6).
- `condition_flag` is AND-only; no OR, NOT, or compound expression.
### 2.2 Path B — Single-rule (subset of A)
Caller: Pathway B cascade-card click → inline calc panel.
Flow:
1. User clicks a concept card; system picks the rule_id linked to that concept (via `event_category_concepts → deadline_rules`).
2. POST `/api/tools/fristenrechner/calculate-rule` with `{rule_id, trigger_date, flags?}`.
3. `FristenrechnerService.CalculateRule` walks the rule's parent chain only (no siblings), returns one `RuleCalculation`.
Strengths:
- Lightweight (no full-proceeding compute).
- Lets the cascade UI surface "click → see this rule's date" without rebuilding the whole timeline.
Weaknesses:
- Doesn't include side-effects (sibling rules in the proceeding that the user might also care about).
- Shares the same expressiveness limits as Path A.
### 2.3 Path C — Event-driven (youpc legacy, redundant)
Caller: Pathway A wizard's "Was kommt nach…" tab; `frontend/src/client/fristenrechner.ts:833` calls POST `/api/tools/event-deadlines`.
Flow:
1. User picks a **trigger_event** (e.g. "Klageerhebung UPC", "Berufungsschrift OLG", from a 110-row picker list).
2. POST `/api/tools/event-deadlines` with `{triggerEventID, triggerDate, courtID}`.
3. `EventDeadlineService.Calculate` loads `paliad.event_deadlines WHERE trigger_event_id = $te`.
4. For each row: apply `duration_value × duration_unit (+ timing: before/after)`. Supports `working_days` unit (Path A doesn't). Handles `alt_duration_value × combine_op (min/max)` composite leads.
5. Returns flat list of computed deadlines + rule_codes.
Strengths:
- Has the `before` timing semantic (Path A doesn't use it).
- Has `working_days` unit (Path A doesn't have it).
- Has `combine_op` (min/max) for composite duration math (Path A doesn't).
- Trigger-event picker is more discoverable than "pick a proceeding": user says "Klageerhebung happened on date X, what comes after?" without first navigating to the proceeding tree.
Weaknesses:
- **Disjoint corpus.** The 77 `event_deadlines` rows do NOT join to `paliad.deadline_rules`. Changing a rule in Path A doesn't update Path C.
- **No parent_id chains.** Each event_deadline is a single-leg calc off the trigger date. No multi-stage timelines.
- **No condition flags.** No with_ccr / with_amend gating.
- **No SmartTimeline integration.** ProjectionService only knows Path A.
- **Origin:** youpc.org ported (mig 028). Implicitly "legacy", but actively wired.
### 2.4 The concept layer (orthogonal to all three paths)
`paliad.deadline_concepts` (56 rows) is the **noun layer** that lets the cascade + search talk about "Klageerwiderung" without knowing which of the 9 jurisdiction-specific Klageerwiderung rules it means. Every rule has `concept_id` (171/172); every cascade leaf has zero or more `event_category_concepts` rows linking to concepts (153 rows, 100 distinct leaves of 103 → 97% coverage).
`paliad.deadline_concept_event_types` (32 rows, added mig 073/074) maps `(concept_id, jurisdiction) → event_type_id` so when the user creates a Deadline via the form by picking a Regel, the system can pre-fill the Typ chip with the canonical event_type. This is a **CONFIG layer, not a trigger model** — it doesn't say "when event X fires, these deadlines spawn." See §6.4.
### 2.5 Multi-deadline triggers
m's "specific events trigger specific deadlines, sometimes multiple" is implemented via **`parent_id` chains in Path A**. One root event (e.g. UPC_INF `inf.soc` = Klageerhebung) triggers a tree of dependent rules. Today the deepest live chain is **3 levels**:
```text
inf.soc (root, anchor)
├─ inf.sod (3mo after, Klageerwiderung)
│ ├─ inf.def_to_ccr ([with_ccr], 2mo after sod, Erwiderung auf CCR)
│ │ └─ inf.reply_def_ccr ([with_ccr], 2mo after, Replik auf Erwid CCR)
│ │ └─ inf.rejoin_reply_ccr ([with_ccr], 1mo after, Duplik)
│ ├─ inf.app_to_amend ([with_ccr,with_amend], 2mo after sod, Antrag Patentänderung)
│ │ ├─ inf.def_to_amend ([with_ccr,with_amend], 2mo after, Erwiderung)
│ │ └─ inf.reply_def_amd ([with_ccr,with_amend], 1mo after Reply, Replik Amend)
│ ├─ inf.reply (with_ccr → 2mo after sod RoP.029.a; without_ccr → swap to alt)
│ └─ inf.rejoin (with_ccr → 1mo after reply RoP.029.d)
└─ inf.interim (court-set, Zwischenverfahren)
└─ inf.oral (court-set, Mündliche Verhandlung)
└─ inf.decision (court-set, Entscheidung)
└─ inf.cost_app (1mo after decision, is_optional, Antrag Kostenentscheidung)
```
15 rules, 4 condition-flag-gated, 4 court-set placeholders (inf.interim / inf.oral / inf.decision are 0-duration court-set; inf.soc is 0-duration root), 1 optional. The structural fidelity is high.
### 2.6 Conditional triggers — the AND-only ceiling
`condition_flag` is `text[]` with **AND-of-array** semantic. To render the rule, every flag in the array must be in the caller's `Flags` set.
Live flag space: `{with_amend, with_ccr, with_cci}` — three flags, four combinations used. The empty array is the unconditional default.
This is enough to express:
- "with counterclaim for revocation" (with_ccr alone)
- "with counterclaim for revocation AND with amendment" (with_ccr + with_amend)
- "with counterclaim for infringement" (with_cci alone)
But not:
- "with_ccr OR with_cci" — would need OR, today not supported. (Live workaround: duplicate rules with each gate.)
- "NOT with_ccr" — also not supported.
- Compound: "with_ccr AND NOT expedited".
§6 flags this as a real coverage gap.
---
## 3. The 27 proceeding types — what's covered, what's a stub
### 3.1 Inventory
| Category | Code | Jurisdiction | Rule count | Notes |
|---|---|---|---|---|
| **fristenrechner** | DE_INF | DE | 9 | Verletzungsverfahren LG. |
| | DE_INF_OLG | DE | 7 | Berufung OLG. |
| | DE_INF_BGH | DE | 8 | Revision / NZB BGH. |
| | DE_NULL | DE | 10 | Nichtigkeit BPatG. |
| | DE_NULL_BGH | DE | 6 | Berufung BGH (Nichtigkeit). |
| | DPMA_OPP | DPMA | 4 | DPMA Einspruch. |
| | DPMA_BPATG_BESCHWERDE | DPMA | 5 | BPatG-Beschwerde nach DPMA. |
| | DPMA_BGH_RB | DPMA | 4 | Rechtsbeschwerde BGH. |
| | EPA_OPP | EPA | 8 | EPA Einspruch. |
| | EPA_APP | EPA | 8 | EPA Beschwerde. |
| | EP_GRANT | EPA | 7 | EP-Erteilung. One rule uses `anchor_alt='priority_date'`. |
| | UPC_INF | UPC | **15** | Verletzung. Richest corpus. |
| | UPC_REV | UPC | **15** | Nichtigkeit. Richest. |
| | UPC_APP | UPC | 7 | Berufung UPC. |
| | UPC_APP_ORDERS | UPC | 5 | Berufung gegen Anordnungen. |
| | UPC_COST_APPEAL | UPC | 2 | Kostenberufung. |
| | UPC_DAMAGES | UPC | 4 | Schadensbemessung. |
| | UPC_DISCOVERY | UPC | 4 | Bucheinsicht. |
| | UPC_PI | UPC | 4 | Einstweilige Maßnahmen. |
| **litigation** | INF | UPC | 8 | Infringement. |
| | REV | UPC | 7 | Revocation. |
| | CCR | UPC | 7 | Counterclaim for Revocation. |
| | APM | UPC | 4 | Provisional Measures. |
| | APP | UPC | 8 | Appeal. |
| | AMD | UPC | 2 | Application to Amend. |
| | ZPO_CIVIL | DE | 4 | ZPO Civil. |
Total: **172 rules across 27 proceeding types** (132 fristenrechner + 40 litigation).
### 3.2 Litigation vs Fristenrechner — the dual-corpus problem
The **same conceptual proceeding** (e.g. UPC Infringement) appears twice in `paliad.proceeding_types`:
- `INF` (category=`litigation`) — 8 rules, generic UPC labels (Statement of Claim, Statement of Defence, Reply, Rejoinder, Oral Hearing, Interim Conference, Decision, Preliminary Objection).
- `UPC_INF` (category=`fristenrechner`) — 15 rules, German labels + condition_flag variants.
The brief calls this out as "two parallel vocabularies." Live confirms:
- `paliad.projects.proceeding_type_id` accepts BOTH categories (no CHECK constraint enforces one or the other). Today all 11 projects are NULL anyway.
- `FristenrechnerService.Calculate(proceedingCode, …)` is **category-agnostic** — pass it `INF` or `UPC_INF`, you get back the respective corpus's timeline. No category guard.
- The Pathway-A wizard surfaces ONLY `category='fristenrechner'` codes (`internal/services/fristenrechner.go:735`: `WHERE category = 'fristenrechner' AND is_active = true`). So users can't pick `INF` from the wizard.
- `ProjectionService.computeProjections` resolves `proj.ProceedingTypeID → code` and calls Calculate with whatever code is on the project. So a project with `INF` would render the 8-rule litigation timeline; a project with `UPC_INF` would render the 15-rule fristenrechner timeline.
**This is a latent footgun.** Whichever code lands on a project first dictates which corpus drives its SmartTimeline. The two corpuses disagree on:
- Rule count (8 vs 15).
- Granularity (litigation has 1 ccr.defence row; fristenrechner has 7 with_ccr/with_amend gated rows).
- Language (litigation labels are English; fristenrechner German).
No code path treats this divergence intentionally. The likely intent at seed-time was:
- `litigation` codes = "the project model's coarse type enum" (Mandant-level taxonomy).
- `fristenrechner` codes = "the calculator's fine-grained variants".
But the actual schema doesn't enforce that contract. **Flagged as §6.2.**
### 3.3 Coverage observations
- **UPC corpus dominates fristenrechner.** 9 of the 20 fristenrechner codes are UPC (66 rules); 5 are DE (40); 3 are DPMA (13); 3 are EPA (23). Bias matches HLC's mandate mix.
- **DE_INF_OLG, DE_INF_BGH, DE_NULL_BGH** were split out late (mig 043). The instance dimension (LG / OLG / BGH) is NOT on `paliad.projects`, so you can't currently derive whether a DE project is at first instance, OLG, or BGH from the project model. This blocks fine-grained Akte → proceeding-code mapping (cross-referenced in t-paliad-166 §4.2).
- **EP_GRANT** is the only proceeding that uses `anchor_alt`. Other priority-date-anchored rules don't exist (yet).
- **UPC_REV.with_cci** — the [with_cci] flag is used for "revocation action with counterclaim for infringement" — i.e. when the defendant in a revocation files a CCI. Only UPC_REV uses with_cci today (4 rules).
### 3.4 Concept linkage gaps
9 of 56 deadline_concepts have `rule_count = 0` — i.e. cascade-reachable concepts that produce zero calculated deadlines:
| Concept slug | Why it's empty |
|---|---|
| `counterclaim-for-revocation` | The CCR flow is modelled inside UPC_INF via `[with_ccr]` flag-gated rules, not as a separate concept-linked rule. |
| `schriftsatznachreichung` | ZPO §296a "Schriftsatznachreichung" — cross-cutting concept, no rule encoding yet. |
| `versaeumnisurteil-einspruch` | ZPO §339 "Einspruch gegen Versäumnisurteil" — no rule. |
| `weiterbehandlung` | EPA Art. 121 EPÜ / R.135 — no rule. |
| `wiedereinsetzung` | Re-establishment of rights — cross-cutting; no rule. |
| `notice-of-defence-intention` | DE ZPO Verteidigungsanzeige — only ZPO_CIVIL has it; not linked. |
| Plus 3 more sparse concepts. | |
For each, the cascade can route the user to the concept card, but the card has no rule pills underneath. This is a real coverage gap surfaced as §6.
---
## 4. Anchor semantics — the 4-bucket model
Encoded in `fristenrechner.go:272-369`. For each rule with `duration_value = 0`:
| Bucket | parent_id | court-determined? | Behaviour |
|---|---|---|---|
| **1. Root anchor** | NULL | no | Due date = trigger date. `IsRootEvent=true`. The proceeding's "day zero" (e.g. SoC filing). |
| **2. Court-set absolute** | NULL | yes | Due date empty; UI shows "wird vom Gericht bestimmt". `IsCourtSet=true, IsCourtSetIndirect=false`. Used for top-level hearings / decisions that don't follow from another rule. |
| **3. Court-set chained** | set | yes | Due date empty (court determines); ancestor anchor. `IsCourtSet=true`. Used for derivative court actions. |
| **4. Filed-with-parent** | set | no | Inherits parent's calculated date. Used for "X is bundled into Y" (e.g. UPC_REV.rev.app_to_amend, rev.cc_inf — included in the Defence to revocation). |
For rules with `duration_value > 0`:
- **Override wins.** `AnchorOverrides[rule_code]` provided by user → use it; mark `IsOverridden=true`.
- **Parent court-set + no override** → mark `IsCourtSet=true, IsCourtSetIndirect=true`. The rule isn't directly court-determined, but its anchor (the court-set parent) hasn't been bound yet. UI shows "unbestimmt".
- **Otherwise:** baseDate = (anchor_alt=priority_date → priorityDate) || (parent_id → computed[parent.code]) || triggerDate. Add `duration_value × duration_unit`. Apply holiday adjustment. Done.
**Court-set detection** (`isCourtDeterminedRule` in calculator) keys on:
- `primary_party='court'`, OR
- `event_type ∈ {'hearing','decision','order'}`, OR
- Heuristic name match (legacy from migration 028).
This is brittle — the boolean is computed from columns that aren't strictly designed for it. §6.5 suggests promoting a real `is_court_set` column.
### 4.1 `AnchorOverrides` — the override map
The override surface is the bridge between "calculated forecast" and "real ground truth." Two consumers:
- **SmartTimeline (`ProjectionService.collectActualsForOverrides`)** — bind a real `paliad.deadlines` row's date back into the calculator: if a saved deadline has `rule_id=X` and `completed_at='2026-04-10'`, the next projection uses 2026-04-10 as the anchor for any rule whose parent is X.
- **Pathway A wizard "Anchor edits"** — the user can override a per-rule date inline in the timeline (paliad-088 era feature). Applies to court-set rules where the user finally knows the decision date.
The override map propagates **downstream**: child rules see the override as their parent's date.
This is a strong, well-implemented mechanism. No gap.
---
## 5. Adjustment semantics — weekends, holidays, court calendars
### 5.1 `HolidayService.AdjustForNonWorkingDaysWithReason(endDate, country, regime)`
Called after every offset computation. Returns `(adjusted, _, wasAdjusted, reason)`.
- If endDate is a weekend → roll to next Monday. Reason: `kind=weekend, original_weekday`.
- If endDate is a public holiday (region match in `paliad.holidays`) → roll to next business day. Reason: `kind=public_holiday, holidays=[…]`.
- If endDate is inside a court vacation (regime-specific date range) → roll to first non-vacation business day. Reason: `kind=vacation, vacation_name, vacation_start, vacation_end`.
Live `paliad.holidays`: **55 rows**, mix of public holidays and vacation periods. `region` axis covers DE federal + state-specific + UPC court-specific.
### 5.2 `CourtService.CountryRegime(courtID, defaultCountry, defaultRegime)`
`paliad.courts` (41 rows) carries `country` and `regime` per court. Defaults via jurisdiction:
- UPC-flavoured proceedings → DE+UPC (UPC München is the default venue).
- DE proceedings → DE.
- EPA / DPMA → DE.
Live regimes inferred from queries: DE state codes (BY, BW, …), UPC court-specific tags. No formal CHECK constraint listing valid regimes.
### 5.3 Working-day arithmetic — split between calculators
Pipeline C (`EventDeadlineService.addWorkingDays`) supports `duration_unit='working_days'`: step forward N business days, skipping weekends + holidays.
Pipeline A (`FristenrechnerService`) does NOT support working_days; only calendar days/weeks/months. Adjustment is post-hoc (compute the calendar date, then roll forward if it lands on a non-business day).
**The two calculators are not equivalent.** Some real-world deadlines are "10 working days after Z" — those can only be expressed in Pipeline C today. Cross-references §6.6.
---
## 6. Coverage gaps (the heart of the audit)
What m's mental model wants ("specific events trigger specific deadlines, sometimes multiple, sometimes conditional") that the data model cannot express today.
### 6.1 Two trigger systems — Pipeline A vs Pipeline C
**Symptom.** Two disjoint data corpuses (`deadline_rules` 172 vs `trigger_events`+`event_deadlines` 110+77) with overlapping intent. A change to a rule in Pipeline A doesn't propagate to Pipeline C. The user-facing "Was kommt nach…" tab (Pipeline C) renders different numbers than the wizard timeline (Pipeline A) for nominally-similar trigger events.
**Impact.** Pipeline C has capabilities Pipeline A lacks (`before` timing, `working_days` unit, `combine_op` min/max) — but no parent chains, no condition_flag, no court-set semantic. Choosing the "right" pipeline today means picking which subset of capabilities the user actually needs for that case.
**Root cause.** Pipeline C is a youpc.org port (mig 028). Pipeline A is paliad-native (mig 009 → 050 evolution). They were never reconciled.
### 6.2 Litigation vs fristenrechner corpus drift
**Symptom.** `paliad.projects.proceeding_type_id` accepts both `litigation` and `fristenrechner` codes. The same conceptual proceeding has rule corpuses of different size, granularity, and language depending on which category the project lands on.
**Impact.** SmartTimeline forecast for a project depends on which code is chosen at project-create time. Two HLC partners filing identical UPC infringement cases could see different timelines if one picked `INF` and the other `UPC_INF`.
**Root cause.** No CHECK constraint, no documentation, no UI guard. Likely intent: `litigation` for project-model coarse classification, `fristenrechner` for fine-grained calculator — but the contract was never formalised.
### 6.3 `is_mandatory` vs `is_optional` semantic overlap
**Symptom.** Two boolean columns with overlapping meaning. Current usage:
- `is_mandatory=true, is_optional=false` — default (most rules).
- `is_mandatory=true, is_optional=true` — surfaces in timeline but pre-unchecked in save-modal (only UPC_INF.inf.cost_app + a few others).
- `is_mandatory=false` — unclear semantics today; sparsely used.
**Impact.** Confusing for both developers and future rule authors. A rule with `is_mandatory=false, is_optional=true` (legal "may file but not required") versus `is_mandatory=true, is_optional=true` (legal "should file but isn't a hard deadline") versus `is_mandatory=true, is_optional=false` (legal "must file") — the four-way matrix isn't well-defined.
**Root cause.** `is_optional` was added late (mig 068) as a UX hack ("pre-uncheck in save modal") rather than a semantic axis.
### 6.4 `deadline_concept_event_types` is a config layer, not a trigger model
**Symptom.** The table maps `(concept, jurisdiction) → event_type` for the create-form's chip suggestion. It DOES NOT support "when an event of type X fires, spawn deadlines for these rules."
**Impact.** m's "specific events trigger specific deadlines" implies a directional pipeline: user logs an event → system computes the deadlines that flow from it. That pipeline today exists only via:
- Pipeline A's full-proceeding compute (heavy: gives everything, not just X's children).
- Pipeline C's trigger_event picker (decoupled corpus).
There's no event_type-keyed entry point into Pipeline A. The cascade gets close — leaf → concept → rules — but stops at "show the cards"; firing the rules requires the user to manually click a card → calculate-rule.
**Root cause.** Pipeline A was designed proceeding-first (mig 009, 2024). The event-first paradigm came later via concepts (mig 037+) but never produced a dedicated trigger endpoint.
### 6.5 Court-set detection is heuristic
**Symptom.** `isCourtDeterminedRule()` decides court-set status from `primary_party='court' OR event_type IN ('hearing','decision','order') OR name-heuristic`. No dedicated boolean column.
**Impact.** False positives possible if a rule names "decision" but isn't court-set (e.g. "preliminary decision to amend"). False negatives possible if a court-set rule isn't tagged with one of these signals.
**Root cause.** Court-set semantic was never formalised as a first-class column. Inferred at runtime.
### 6.6 Pipeline A lacks `before`, `working_days`, `combine_op`
**Symptom.** Specific gaps in expressive power:
- `before` timing: useful for "must be filed Y days BEFORE oral hearing." Pipeline C honours `timing='before'`; Pipeline A only renders `timing='after'` rules.
- `working_days` unit: useful for procedural deadlines like UPC R.220.3 ("3 working days from notification"). Pipeline C supports it; Pipeline A doesn't.
- `combine_op` (min/max): useful for "earlier of X or Y" (compound deadlines, e.g. EPC R.36 — "shortest of priority date+24mo or filing date+18mo"). Pipeline C supports it; Pipeline A doesn't.
**Impact.** Some legal deadlines can only be expressed in Pipeline C, fragmenting the rule corpus.
**Root cause.** Pipeline A grew from a "tree of forward offsets" model; backward / composite deadlines weren't anticipated.
### 6.7 Condition-flag grammar is AND-only
**Symptom.** `condition_flag` is `text[]` with AND semantics. No OR, no NOT, no nested expression.
**Impact.** Real legal scenarios that need OR (e.g. "rule X applies if CCR OR CCI is filed") get encoded as **two duplicate rules** today — one for each branch. Painful to maintain; easy to drift.
**Root cause.** The flag axis was designed for the small set of UPC variant flags (`with_ccr`, `with_amend`, `with_cci`); compound expressions weren't anticipated.
### 6.8 Cross-proceeding spawn is half-wired
**Symptom.** `is_spawn=true` rules exist (8 live), intended to express "when X happens in proceeding A, also trigger Y in proceeding B." The calculator code at `projection_service.go:896-901` explicitly notes: "Cross-proceeding spawn … We don't have that rule in our map; skip the dependency annotation but still surface the row."
**Impact.** A UPC_INF decision firing an APP proceeding (cross-proceeding) renders the spawned row, but the dependency-graph annotation breaks. SmartTimeline can't fully chain across proceedings.
**Root cause.** Cross-proceeding spawn was a late addition; the calculator's `ruleByID` map is per-proceeding, so it can't resolve spawns from other proceedings. Needs either a global rule index or a smarter resolver.
### 6.9 Nine orphan concepts with `rule_count=0`
Per §3.4: `counterclaim-for-revocation`, `schriftsatznachreichung`, `versaeumnisurteil-einspruch`, `weiterbehandlung`, `wiedereinsetzung`, `notice-of-defence-intention`, plus 3 more.
**Impact.** Cascade leaves can reach these concepts, but the user sees an empty result card. UX feels broken even though it's an unrelated coverage gap (no rules seeded yet).
**Root cause.** Cascade taxonomy was seeded ahead of the rule corpus for some concepts. The seed work never caught up.
### 6.10 No way to express "X is conditional on Y having fired"
**Symptom.** `condition_rule_id` exists as a column but is 0% populated. Was intended for "rule X applies only if rule Y was previously triggered" but never wired.
**Impact.** Today's flag mechanism (condition_flag) gates on **caller-supplied flags** (e.g. user toggles "with_ccr" in the UI). It doesn't gate on **runtime rule firing**. So you can't express "if the defendant filed Preliminary Objection (rule X), then rule Y is suspended for 2mo."
**Root cause.** Column added speculatively; never wired into the calculator.
### 6.11 The instance dimension (LG/OLG/BGH) isn't on `paliad.projects`
**Symptom.** The proceeding_types `DE_INF_OLG` / `DE_INF_BGH` exist, but a project can't carry "I'm at first instance" / "I'm on appeal at OLG" as data. The user has to manually pick a different `proceeding_type_id` if the case moves up the instances.
**Impact.** SmartTimeline forecast can't auto-advance from DE_INF → DE_INF_OLG when a Berufungsschrift fires on the actuals side.
**Root cause.** Project model treats proceeding-type as a static attribute, not a state machine.
### 6.12 No rule audit log
**Symptom.** Rules are modified by SQL migrations only. There's no `paliad.deadline_rule_audit` table tracking "rule X changed from 3mo to 2mo on 2026-04-15 by m, because Y." Migrations are technically the audit trail, but they aren't queryable in-app.
**Impact.** Rule-management UX (§8) needs an answer for "who changed this rule and why." Without an audit trail, rule-editing in-app is a step backward in compliance.
**Root cause.** Never needed before, because rules were never user-editable.
### 6.13 Zero deadline → rule linkage in live data
**Symptom.** Only **1 of 26** live deadlines has `rule_id` populated.
**Impact.** SmartTimeline's "anchor real deadlines into projection" feature (Pipeline A's strongest UX) is unusable on existing data. New deadlines saved via the wizard *do* get rule_id; legacy deadlines don't.
**Root cause.** Schema migrated incrementally; backfill never happened.
---
## 7. Extension proposals (one concrete change per §6 gap)
Each gap from §6 gets a concrete schema / service change, costed (migration + service + UI ripples).
### 7.1 Reconcile Pipelines A and C
**Proposal.** Migrate `paliad.event_deadlines` into `paliad.deadline_rules` with a new column `trigger_event_id` (nullable FK to `paliad.trigger_events`). A rule with `trigger_event_id NOT NULL` is event-triggered (Pipeline C semantics); with NULL it stays proceeding-triggered (Pipeline A).
Add the Pipeline-C-only columns to `deadline_rules`:
- `timing` already exists; backfill non-NULL `before` values.
- `combine_op``{min, max, NULL}` — new column.
- `working_days` as a valid `duration_unit` value — already a string column, no schema change.
Then deprecate Pipelines C, redirecting `/api/tools/event-deadlines` to the unified calculator.
**Cost.**
- Migration: 1 file, ~120 LoC SQL (column adds + data move + idx).
- Service: `FristenrechnerService.Calculate` extends to honour `timing='before'`, `working_days`, `combine_op`. ~80 LoC Go.
- Service: `EventDeadlineService` either deletes (clean) or proxies to FristenrechnerService (transitional).
- Handler: `/api/tools/event-deadlines` either deletes or 302s.
- Frontend: `client/fristenrechner.ts:833` — the "Was kommt nach…" tab can call the unified endpoint.
- Tests: a fresh table-driven test fixture covers the union behaviour.
**Ripple.** No data loss; trigger_event_id is additive. Frontend mostly transparent.
### 7.2 Formalise litigation vs fristenrechner contract
**Proposal.** Two options:
- **(a) Hard-split.** Add `CHECK constraint` to `paliad.projects.proceeding_type_id`: only `category='litigation'` codes allowed. Migrate the 8-rule litigation corpus to be the canonical "project-level proceeding type". Move the fine-grained `category='fristenrechner'` rules under each litigation code via a new `variant` column.
- **(b) Soft-merge.** Drop the `category` discriminator entirely. Every proceeding_type carries its full rule corpus. The dual-corpus today (8-rule INF + 15-rule UPC_INF) merges into ONE 15-rule UPC_INF, with the project model referencing only the rich variant.
**Cost.** (a) is invasive — migration to move 40 litigation-corpus rules under the fristenrechner codes; (b) is less invasive but means projects switch to picking `UPC_INF` instead of `INF`.
**Recommendation.** **(b)**. The dual-corpus is legacy from a project-model + calculator-model that grew separately. One canonical proceeding_type per case is cleaner.
**Ripple.** Project-create form picker changes from "INF / REV / CCR / APM / APP / AMD / ZPO_CIVIL" to the full 20-code fristenrechner picker (or a curated subset). t-paliad-166's mapping helper becomes unnecessary.
### 7.3 Clean up `is_mandatory` vs `is_optional`
**Proposal.** Replace both with a single `deadline_kind` enum:
- `mandatory` — must be addressed.
- `recommended` — should be addressed (pre-checked in save-modal but not required).
- `optional` — may be addressed (pre-unchecked in save-modal).
- `informational` — never saves as a deadline, surfaces as info.
Backfill: `is_mandatory=true, is_optional=false → mandatory`; `is_mandatory=true, is_optional=true → optional`; `is_mandatory=false → recommended`.
**Cost.** Migration ~30 LoC SQL. Service: `UIDeadline` exposes `Kind` instead of `IsMandatory`+`IsOptional`. Frontend: badge logic + save-modal pre-check.
### 7.4 Add a real event-driven trigger endpoint
**Proposal.** `POST /api/tools/event-trigger` with `{event_type_slug, trigger_date, project_id?}`. Resolves:
1. `event_types.slug → event_types.id`
2. `deadline_concept_event_types.event_type_id → concept_id` (per jurisdiction from project or explicit)
3. `deadline_rules.concept_id → rules`
4. Calculate the rules + their parent chains via Pipeline A.
Returns just the rules that flow from this event (filtered Pipeline A response).
**Cost.** Handler + service method, ~100 LoC. No schema change; uses existing junction.
**Ripple.** Lets the cascade UI offer "I just logged this event — here are the deadlines that follow" in one click. Also unlocks Phase-H-style email parsing → deadline spawn.
### 7.5 Promote court-set to a real column
**Proposal.** Add `is_court_set boolean NOT NULL DEFAULT false` to `paliad.deadline_rules`. Backfill from the heuristic. Calculator reads the column instead of inferring.
**Cost.** Migration ~20 LoC SQL (incl. backfill DO$$ block). Service: 1-line change in `isCourtDeterminedRule`.
**Ripple.** Faster + correct + no behaviour surprise. Cheap win.
### 7.6 Pipeline A gains `before` / `working_days` / `combine_op`
Covered in §7.1 (reconciliation).
### 7.7 Compound condition grammar
**Proposal.** Replace `condition_flag text[]` with `condition_expr jsonb`. Schema:
```json
{"op":"and", "args":[{"flag":"with_ccr"},{"op":"not","args":[{"flag":"expedited"}]}]}
```
Backfill: `['with_ccr','with_amend']``{"op":"and","args":[{"flag":"with_ccr"},{"flag":"with_amend"}]}`.
**Cost.** Migration with backfill ~80 LoC. Service: small recursive evaluator (~50 LoC Go). UI: condition picker for rule-editor (§8) — more involved.
**Ripple.** Future rule authors can express OR / NOT cleanly. No data drift; backward-compatible eval.
### 7.8 Wire cross-proceeding spawn
**Proposal.** Change `DeadlineRuleService.List(proceedingTypeID *int)` to allow a "follow spawn" mode that returns rules from spawned proceedings as well. Or: in `projection_service.computeProjections`, when a rule has `is_spawn=true` and the calculator returns a row from a different proceeding code, load the target proceeding's rule corpus lazily.
**Cost.** Service: ~50 LoC. Calculator: ~30 LoC. Risk: cycle prevention (don't infinite-loop A→B→A).
**Ripple.** SmartTimeline can fully chain across proceedings. The dependency-annotation breakage at `projection_service.go:896-901` resolves.
### 7.9 Seed the 9 orphan concepts with rules
**Proposal.** Per concept, add 13 rules to the appropriate proceeding_types. e.g. `wiedereinsetzung` → UPC R.320.1 (`UPC_INF.wiedereinsetzung`), EPA R.136 (`EPA_OPP.wiedereinsetzung`), DE PatG §123 (`DE_INF.wiedereinsetzung`).
**Cost.** Per orphan concept: ~20 LoC SQL. Total ~150 LoC across 9 concepts. Legal review required per rule.
**Ripple.** Cascade no longer dead-ends. This is the "coverage" gap m's t-paliad-167 explicitly called for.
### 7.10 Wire `condition_rule_id` or drop it
**Proposal.** Either:
- (a) Implement: when calculator walks rules, gate a rule's render on `condition_rule_id`'s presence in the `computed` map.
- (b) Drop the dead column.
**Recommendation.** **(b)**. The semantic is rarely needed; `condition_flag` covers most variant cases. Future need can resurrect.
### 7.11 Add `instance_level` to `paliad.projects`
**Proposal.** New column `instance_level text``{first, appeal_olg, appeal_bgh, NULL}`. Combined with `proceeding_type.code` + `jurisdiction`, lets us derive `DE_INF_OLG` vs `DE_INF` from a project.
**Cost.** Migration ~10 LoC SQL. Project form: new picker. SmartTimeline forecast: small refactor in `proceedingCodeForProject`.
### 7.12 Rule audit log
**Proposal.** New table `paliad.deadline_rule_audit (id, rule_id, changed_by, changed_at, before_json, after_json, reason text)`. Trigger on UPDATE/INSERT/DELETE captures the diff. Required if §8 lands.
**Cost.** Migration ~40 LoC SQL (table + trigger). Read API for compliance review.
### 7.13 Backfill `rule_id` on existing deadlines
**Proposal.** One-time migration: for each `paliad.deadlines` row, fuzzy-match `title` against `paliad.deadline_concepts.aliases` + `paliad.deadline_rules.name`, link the highest-confidence match, leave low-confidence unlinked.
**Cost.** Migration ~100 LoC SQL. Run once.
**Ripple.** SmartTimeline anchor-from-actuals starts working for existing data. Bigger UX win than it sounds.
---
## 8. Rule-management UX — does m need an in-app rule editor?
m's "all in the Rules so we should be able to manage" reads as a direct ask.
### 8.1 The case for an in-app rule editor
- **Today: SQL migration only.** Every rule add/edit/disable requires a developer to write a migration, get reviewed, merge, deploy. The feedback loop is hours-to-days.
- **Domain experts ≠ developers.** m is the rule expert. He shouldn't need to write `INSERT INTO paliad.deadline_rules (proceeding_type_id, code, name, duration_value, …)` SQL.
- **Coverage gaps are persistent** (§3.4, §6.9). They stay open longer because the workflow is high-friction.
- **Real-world law changes.** Procedural rules update (e.g. UPC R.49 just had a 2026-Q1 revision). Capturing those in SQL migrations is fragile.
### 8.2 The case against
- **Compliance / audit.** Rules are legal infrastructure. Any user-edit must be auditable, reviewable, reversible.
- **Schema complexity.** 32 columns with semantic nuances (court-set heuristic, parent_id topology, condition_flag grammar). Naive form UI = footgun heaven.
- **Cross-rule validation.** parent_id chains must remain DAGs. sequence_order must be topologically consistent. condition_flag values must be in a valid vocabulary. No live constraint catches all of these today.
- **Build cost.** A real rule-editor with audit log, validation, preview, dry-run, and rollback is 46 PRs of work.
### 8.3 Three options
| Option | Description | Effort | When right |
|---|---|---|---|
| **(A) Status quo: SQL only** | Keep migrations as the rule-edit surface. Build tooling around migration authoring (mAi-assisted SQL gen, schema validators). | Low (~1 sprint of tooling). | If m's rule velocity is < 1 edit/week and audit trail is non-negotiable. |
| **(B) Read-only admin surface** | Add `/admin/rules` page that lists rules, lets m search/filter/inspect. No edits in-app; "edit this rule" links to a Gitea issue template that drafts the migration. | Medium (~1 PR backend listing + 1 PR frontend). | If the friction is "I can't see what's in there" more than "I can't change what's there". |
| **(C) Full rule editor** | `/admin/rules/{id}/edit` with form, validation, audit log, preview-on-trigger-date, "ship draft" migration generator. | High (~4-6 PRs). | If m is genuinely going to edit rules weekly and the rule corpus is going to grow significantly. |
### 8.4 Inventor recommendation
**Start with (B), graduate to (C).**
- (B) immediately removes the "I can't see what's in there" friction, which today requires running SQL by hand or asking a developer. Low risk.
- (B) makes the rule corpus discoverable inside the app which is itself a win for transparency and for spotting coverage gaps 3.4).
- The Gitea-issue handoff preserves the audit trail and review workflow.
- Once the corpus is browsable, the "I keep wanting to edit this thing" pressure tells us whether (C) is worth building.
- **(C) without (B) is over-engineering** we'd be building the form before we know which fields are actually edited often.
Hard requirement for (C) if we get there: `paliad.deadline_rule_audit` table 7.12) with mandatory `reason` field, reviewer workflow, and migration-export so changes still land in version control.
§9 Q5 surfaces this for m's call.
---
## 9. Open questions for m (Phase 2 steering)
These are the 1015 picks for m to make before Phase 2 starts.
**Q1 — Reconciliation of Pipelines A and C.** §6.1 + §7.1. Three options:
- (a) Merge into one table (recommended; ~120 LoC migration + 80 LoC Go).
- (b) Keep both but document the contract (cheap, but the drift continues).
- (c) Deprecate Pipeline C entirely (deletes "Was kommt nach…" tab UX loss).
**Q2 — Litigation vs fristenrechner corpus.** §6.2 + §7.2. Two options:
- (a) Hard-split with CHECK constraint + rule migration (invasive).
- (b) Soft-merge: drop the category discriminator, projects use fristenrechner codes only (recommended).
**Q3 — `is_mandatory` / `is_optional` cleanup.** §6.3 + §7.3. Pick the 4-value enum (`mandatory` / `recommended` / `optional` / `informational`) or keep the two booleans with formal docs.
**Q4 — Event-driven trigger endpoint.** §6.4 + §7.4. Build `POST /api/tools/event-trigger` (concept-keyed) now, or defer until rule corpus is reconciled?
**Q5 — Rule-management UX.** §8. Pick:
- (A) status quo SQL only,
- (B) read-only admin surface (recommended start),
- (C) full editor with audit log.
**Q6 — Compound condition grammar.** §6.7 + §7.7. Move to `condition_expr jsonb` with AND/OR/NOT, or stay with `condition_flag text[]` AND-only and live with duplicate rules?
**Q7 — Cross-proceeding spawn.** §6.8 + §7.8. Wire it (let SmartTimeline chain across proceedings), or accept the current half-wired state?
**Q8 — Orphan concept seed.** §3.4 + §7.9. Priority order for the 9 missing-rule concepts? My guess: wiedereinsetzung > schriftsatznachreichung > versaeumnisurteil > weiterbehandlung > others. Legal review per concept.
**Q9 — Instance level on `paliad.projects`.** §6.11 + §7.11. Add `instance_level` column to support the DE_INF / DE_INF_OLG / DE_INF_BGH ladder, or accept that users manually re-pick proceeding_type on appeal?
**Q10 — Backfill `rule_id` on existing deadlines.** §6.13 + §7.13. Run the one-time fuzzy-match migration, or live with the broken anchor-from-actuals on legacy rows?
**Q11 — `working_days` and `before` semantics in Pipeline A.** §5.3 + §6.6. Add (recommended) or live without them?
**Q12 — Court-set as a real column.** §6.5 + §7.5. Promote (cheap win), or keep the heuristic?
**Q13 — Drop `condition_rule_id` dead column.** §1.6 + §7.10. Drop or wire?
**Q14 — Phase 2 cadence.** How should we structure the iterative refinement? Options:
- (a) m drives via the worker pane — m raises concrete cases ("counterclaim with amendment in expedited proceedings"), worker proposes encoding, commits incrementally.
- (b) Inventor (pauli) drafts a Phase 2 design for the §7 extensions in priority order m picks here, m gates.
- (c) Mixed: m picks the top 2 from §9 (Q1Q13) for Phase 2, the rest deferred to Phase 3.
**Q15 — Phase 3 framing.** Once Phase 2 lands the data-model changes, is the goal:
- (a) Build the rule editor (§8 option C), or
- (b) Backfill coverage gaps (§7.9), or
- (c) Wire SmartTimeline cross-proceeding chains (§7.8), or
- (d) Some other priority m has in mind?
---
## AUDIT READY FOR REVIEW
Awaiting m's go/no-go on §9 Q1Q15 before Phase 2 starts. Inventor (pauli) parks after this commit — no implementation kickoff, no other-skill autoload, m gates the audit → Phase 2 transition.
Recommended Phase 2 worker: depends on m's Q14 pick. If (a) interactive pair-prog, then pauli or feynman. If (b) inventor design pass, pauli has the freshest context. If (c) mixed, pauli for design, hand off to a Sonnet coder for each landed extension. **NOT cronus per memory directive 2026-05-06.**