Files
paliad/docs/audit-fristen-logic-2026-05-13.md
mAi b455df265e 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.
2026-05-13 21:33:38 +02:00

54 KiB
Raw Permalink Blame History

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).
  • 050is_bilateral backfill (4 rules).
  • 052 — Determinator ROP coverage audit fixes.
  • 068is_optional column.
  • 073 + 074deadline_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:

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:

{"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.