Files
paliad/docs/assessment-deadline-system-2026-05-27.md
mAi 810b65463e
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
docs(assessment): Phase 1 audit — deadline + procedural-events system (m/paliad#149)
t-paliad-328. Read-only audit of every consumer of paliad.sequencing_rules
+ paliad.procedural_events + the legacy paliad.trigger_events, plus the
rules-corpus quality on the live database. No design — Phase 2 (inventor)
gates on this landing.

Highlights:
- 226 active+published rules / 222 events (1:1 since mig 136)
- parent_id chain vs trigger_event_id are functionally disjoint
  (2/226 overlap); 73 legacy globals own the trigger_event_id lane
- 11 risk items captured with file:line; B1 (cross-party follow-up
  filter) and B2 (picker accepts spawn-only + leaves) confirmed
  from code at fristenrechner_followups.go:358-367 and :241-287
- 4 spawn rules still point at the inactive upc.apl.merits (id=11);
  the active appeal type is id=160 (upc.apl.unified)
- 6 active proceeding_types are entirely unruled
- 3 scenario stores wired (project_event_choices, scenarios table,
  DOM state); all currently empty, so divergence is dormant
- 738 lines (under the 800 cap)

Recommendation §6 sequences Tier 1 model decisions ahead of Tier 2
surface decisions and Tier 3 editorial cleanup for the inventor.
2026-05-27 11:02:38 +02:00

36 KiB
Raw Permalink Blame History

Assessment — Deadline + Procedural-Events System

Phase 1 of RFC m/paliad#149. Read-only audit of every consumer of paliad.sequencing_rules + paliad.procedural_events + the legacy paliad.trigger_events, the corpus they project, and the surfaces that read them.

  • Author: athena (consultant role)
  • Date: 2026-05-27
  • Live data: youpc Supabase (paliad schema), counts captured during the audit window (mig 153 applied).
  • Scope: assessment only. No design proposals; no schema sketches; no recommendations on column shape. Phase 2 (inventor) decides those.

0. Headline numbers

Bucket Total Active + published Notes
procedural_events 236 222 5 drafts, 9 archived/inactive
sequencing_rules 236 226 1:1 row-mirror with events (mig 136 + 140)
trigger_events (legacy) 110 bigint-keyed catalog; lives parallel to events
proceeding_types 50 23 kind=proceeding; 0 active in kind=phase/side_action/meta (mig 153 flipped them off)

Rules-corpus shape (active + published, 226 rows):

Classification Rows
Parent only (chain-linked) 105
Both parent + legacy trigger 2
Legacy trigger_event_id only — proceeding_type_id IS NULL 73
Neither (root) — proceeding_type_id set 46

Other corpus signals:

  • condition_expr populated: 18 rules. Three distinct keys: flag (14), op + args (4 each — always nested AND).
  • is_spawn = true: 4 rules. All four point at the inactive upc.apl.merits (id=11). The active appeal type is id=160 (upc.apl.unified). See risk R3.
  • is_court_set = true: 46 rules.
  • is_bilateral = true: 4 rules.
  • choices_offered populated: 28 rules. Three shapes: {appellant:[…]} (20), {skip:[…]} (6), {include_ccr:[…]} (2).
  • applies_to_target populated: 16 rules.
  • 67 distinct events act as chain-anchors (= parent of ≥1 active rule). That is the derived trigger set today.
  • paliad.project_event_choices: schema present, 0 rows live.
  • paliad.scenarios (mig 145): table created, 0 rows. paliad.projects.active_scenario_id: 0/18 projects populated.

A more granular per-proceeding-type breakdown is in §4.


1. Audit — consumers of sequencing_rules + procedural_events

Every read site, by surface. File paths are repo-relative.

1.1 Direct services

Service File What it reads Surface(s) it backs
DeadlineRuleService internal/services/deadline_rule_service.go:14-365 paliad.deadline_rules_unified view (sequencing_rules + procedural_events + legal_sources), + paliad.trigger_events for parent-chain labels (:226-285) Admin rules list/editor, Fristenrechner result panel
FristenrechnerService internal/services/fristenrechner.go:115-172,1-700+ sequencing_rules + procedural_events (proceeding-type catalog; EXISTS over rules); scenarios table (:583-627) /api/tools/fristenrechner (Mode A + Mode B + Mode C)
FristenrechnerService.LookupFollowUps internal/services/fristenrechner_followups.go:87-403 resolves anchor by pe.id/pe.code/sr.id (:241-287); one-hop children via parent_id (:345-403) /api/tools/fristenrechner/follow-ups
DeadlineSearchService internal/services/fristenrechner_search_events.go:143-170,194,233,696 sequencing_rules ⋈ procedural_events ⋈ proceeding_types + legal_sources; counts child rules via parent_id subquery /api/tools/fristenrechner/search
EventDeadlineService internal/services/event_deadline_service.go:31-79,186-195,244 paliad.trigger_events + sequencing_rules WHERE trigger_event_id IS NOT NULL /api/tools/event-deadlines (legacy bigint surface)
EventTriggerService internal/services/event_trigger_service.go:24-230 event_types.trigger_event_id bridge + sequencing_rules /api/tools/event-trigger
RuleEditorService internal/services/rule_editor_service.go:104,136,232,371,381,459,625-843 full CRUD on sequencing_rules + procedural_events; reads trigger_event_id as an optional filter on list /admin/api/procedural-events/* (Slice B.5)
RuleEditorOrphans internal/services/rule_editor_orphans.go:218-224 sub-select on sequencing_rules for orphaned deadlines /admin/api/orphans
DualWriteService internal/services/dual_write.go (+ dual_write_test.go:50-300) parity assertion between legacy + unified projection internal — write-side guard, no HTTP
ProjectionService (SmartTimeline) internal/services/projection_service.go:3+ composes the timeline by reading via DeadlineRuleService + FristenrechnerService; does NOT touch sequencing_rules directly GET /api/projects/{id}/timeline, milestone + counterclaim endpoints in internal/handlers/projection.go:35-436+
ExportService internal/services/export_service.go:1680 bulk-exports paliad.trigger_events as the ref__trigger_events workbook sheet /api/admin/export/*
EventChoiceService internal/services/event_choice_service.go:15-180 reads + writes paliad.project_event_choices per-project flag persistence (no rows live today)
EventTypeService internal/services/event_type_service.go:40-414 user-defined paliad.event_types rows with optional trigger_event_id bridge /api/event-types + Pipeline C compose
ProjectService.validateProceedingTypeCategory internal/services/project_service.go:1176-1267 reads paliad.proceeding_types.category + kind + is_active binding guard for projects.proceeding_type_id (sister to mig-153 trigger)

The handlers behind each route are listed in §1.2.

1.2 HTTP routes

Every route that ultimately surfaces sequencing/event data. Path literals + handler file:line cited.

Knowledge-tool surface (public-ish, behind auth):

Route Handler Reads
POST /api/tools/fristenrechner internal/handlers/fristenrechner.go:39-95+ FristenrechnerService.CalculateForProceeding → engine in pkg/litigationplanner
GET /api/tools/fristenrechner/search internal/handlers/fristenrechner_search.go (filter params: event_kind, primary_party, jurisdiction) DeadlineSearchService.SearchEvents
GET /api/tools/fristenrechner/follow-ups internal/handlers/fristenrechner_followups.go:27-65 FristenrechnerService.LookupFollowUps
GET /api/tools/proceeding-types internal/handlers/event_types.go proceeding_types filter (event_kind, jurisdiction)
GET /api/tools/trigger-events internal/handlers/event_types.go trigger_events catalog (active only)
POST /api/tools/event-trigger internal/handlers/event_trigger.go:39-106 unified Pipeline-A + Pipeline-C compose
POST /api/tools/event-deadlines internal/handlers/deadline_rules_db.go:67+ legacy bigint trigger_event_id → rule list

SmartTimeline surface (project-bound):

Route Handler Reads
GET /api/projects/{id}/timeline internal/handlers/projection.go:35-109 ProjectionService.Render (no direct rule reads — composes via services)
POST /api/projects/{id}/timeline/milestone internal/handlers/projection.go:445+ milestone insert; reads proceeding_type.kind via service
POST /api/projects/{id}/timeline/counterclaim internal/handlers/projection.go:387-436 spawns CCR project; reads parent_id on response composition

Admin editor surface (/admin/procedural-events/*):

Route Handler Reads
GET /admin/procedural-events internal/handlers/admin_rules.go:399-402 page shell
GET /admin/procedural-events/{id}/edit :403-470 editor form (full rule + event JSON)
GET /admin/api/procedural-events :101-160 paginated list w/ canonical code + event_kind (Slice B.5 wrapper)
GET /admin/api/procedural-events/{id} :161-179 single rule fetch
POST /admin/api/procedural-events :180-204 create draft
PATCH /admin/api/procedural-events/{id} :205-233 edit draft
POST /admin/api/procedural-events/{id}/publish :257-279 publish flow
GET /admin/api/procedural-events/{id}/audit :326-361 audit log
GET /admin/api/orphans :471-484 orphaned deadlines (Slice 10 backfill UI)
POST /admin/api/orphans/{id}/resolve :485-520 link orphan to rule
/admin/rules/*/admin/procedural-events/* :761-772 301 redirects (legacy bookmarks; one-slice deprecation window)
?trigger_event_id=… query param :119-122 exposes legacy trigger filter on the admin list

Scenarios surface (mig 145):

Route Handler
`GET /api/scenarios?project= abstract=true`
GET /api/scenarios/{id} :92-113
POST /api/scenarios :115-136
PATCH /api/scenarios/{id} :138-164
DELETE /api/scenarios/{id} :166-200+
POST /api/paliadin/suggest/deadline internal/handlers/paliadin_suggest.go:63+ (deadline drafts via Paliadin; does not read rules directly — calls into DeadlineService)

Registration: internal/handlers/handlers.go:497-501, 880.

1.3 Frontend (TypeScript) consumers

These call the routes above; no direct DB access. References per the i18n key search and frontend/src/client/* greps:

  • frontend/src/admin-rules-list.tsx:24-105+ — admin list page shell; hits /admin/api/procedural-events*.
  • frontend/src/admin-rules-edit.tsx:29-187+ — admin editor form; reads procedural_events.edit.field.{code,event_kind,parent} i18n keys.
  • frontend/src/verfahrensablauf.tsx — proceeding-type ablauf page (mode C); hits /api/tools/fristenrechner with proceeding shape.
  • frontend/src/client/fristenrechner-wizard.ts:80 — Mode A wizard; r4: string // procedural_events.code.
  • frontend/src/client/fristenrechner-mode-a.ts — Mode A search; hits /api/tools/fristenrechner/search?kind=events.
  • frontend/src/client/fristenrechner-result.ts — result panel; hits /api/tools/fristenrechner/follow-ups.
  • frontend/src/client/projects-new.ts — type-aware project wizard; hits /api/tools/fristenrechner?proceeding_type_code=….
  • frontend/src/client/deadlines-detail.ts — deadline CRUD detail.
  • i18n keys: admin.procedural_events.list/edit/col.* and translations in frontend/src/client/i18n.ts:3193-3204, 6338-6346+.

1.4 Offline snapshot

  • cmd/gen-upc-snapshot/main.go:150-268 — reads paliad.trigger_events, the legacy paliad.deadline_rules projection (now via the unified view), and paliad.proceeding_types. Writes JSON to pkg/litigationplanner/embedded/upc/{trigger_events.json, rules.json, proceeding_types.json, meta.json}.
  • pkg/litigationplanner/catalog.go + engine.go + types.go:73-156 — Rule struct carries TriggerEventID, SpawnProceedingTypeID, ConditionExpr, Priority, IsCourtSet, PrimaryParty, IsSpawn, SpawnLabel, CombineOp. youpc.org consumes this snapshot.

1.5 Migrations touching the tables (chronological)

internal/db/migrations/:

028_youpc_deadlines_import, 030_event_types, 033_trigger_events_de, 035_event_deadlines_title_de_backfill, 038_concept_links_and_legal_source, 046_cross_cutting_triggers, 047_deadline_search_view, 051_proceeding_display_order, 063_frist_verpasst_upc, 078_unified_rule_columns, 091_drop_legacy_rule_columns, 098_submission_codes_prefix_and_rename, 125_cross_cutting_filter_legal_source, 132_wave1_tier1_rule_additions, 136_procedural_events_additive (the schema-authoritative additive split), 139_deadline_rules_unified_view, 140_drop_deadline_rules (legacy projection dropped), 151_dedupe_null_procedural_events, 152_dedupe_identical_sequencing_rule_clones, 153_proceeding_types_kind (kind discriminator + projects FK trigger).

Mig 145 is scenario-side: creates paliad.scenarios (table, not a scenarios jsonb column on projects — the RFC text was imprecise) and paliad.projects.active_scenario_id FK.


2. Health-check per consumer

2.1 Works — green

  • DualWriteService parity. Every CRUD on the editor surface keeps sequencing_rules + procedural_events + legal_sources locked, asserted by dual_write_test.go:50-202.
  • Admin editor (/admin/procedural-events/*). Full create / edit / publish / audit loop. Drafts state respected.
  • Mode A picker via search. DeadlineSearchService filters by event_kind / primary_party / jurisdiction; returns child-rule counts (fristenrechner_search_events.go:159).
  • Mode B Verfahrensablauf calc. pkg/litigationplanner.CalculateRule
    • the proceeding_type fan-out works for every type that has any rule (17/23).
  • gen-upc-snapshot. UPC snapshot for youpc.org keeps shipping; no DB writes; reads only.
  • Counterclaim spawn project creation. internal/handlers/projection.go:387-436 + mig 153 trigger guard reject any non-proceeding proceeding_type_id.
  • EventChoiceService SQL is wired and tested — but see §2.3.

2.2 Works with known caveats — yellow

  • Spawn rules. Behaviour is correct in the abstract (rule fires, user can spawn a child case), but every spawn target points at the inactive upc.apl.merits (id=11). Surfaces that resolve the spawn target via paliad.proceeding_types will return an inactive row. See R3. Cited at sequencing_rules 4 rows; service code in fristenrechner_followups.go:388 SELECTs spt.code via LEFT JOIN paliad.proceeding_types spt ON spt.id = sr.spawn_proceeding_type_id — no is_active filter on the join. Frontend renders an "open Berufungsverfahren" CTA that points at a UI flow expecting the active id=160 (upc.apl.unified).
  • Legacy 73 globals. 73 rules with proceeding_type_id IS NULL and trigger_event_id NOT NULL. These all anchor on legacy null.<8hex> event codes that don't match any proceeding_types.code prefix. They are consumed via /api/tools/event-deadlines (the bigint route) AND surface on the unified view. They have no place in the Mode B "proceeding-type ablauf" view because they have no proceeding. See R4.
  • Legacy /api/tools/event-deadlines route. Live, used by Pipeline-C event_types consumers (EventTypeService). The ExportService:1680 also still emits ref__trigger_events to the workbook. Deprecation has been deferred — see R5.

2.3 Broken / leaky — red

  • B1 — Follow-up cross-party filter is over-broad. fristenrechner_followups.go:358-367:

    if party == "claimant" || party == "defendant" {
        args = append(args, party)
        where = append(where, fmt.Sprintf(
            "(sr.primary_party = $%d OR sr.primary_party = 'both' OR sr.primary_party IS NULL)",
            len(args)))
    }
    

    The filter keeps both + NULL rules but drops cross-party follow-ups. From the corpus there are 39 active rules whose primary_party differs from their parent's primary_party (excluding court). Example: upc.inf.cfi.def_to_ccr is claimant-filed; its child rule RoP.029.d → reply_def_ccr is defendant-filed. With party=claimant selected on the result view, the defendant child is hidden and the user reads "Keine Folge-Fristen" — a lie. This is the exact bug the RFC §"What's actually broken" item 2 calls out.

  • B2 — Picker doesn't distinguish triggers from leaves. LookupFollowUps (fristenrechner_followups.go:241-287) resolves by pe.id / pe.code / sr.id with no "is-this-event-actually-a-trigger" gate. The data already supports derivation — 67 of 222 active events act as a chain anchor. The picker just isn't wired to the derivation. Compounding: 4 events are spawn-only consequences (upc.{inf,rev,pi,dmgs}.cfi.appeal_spawn) — picking one returns the spawn rule itself with no follow-ups, which surfaces as "Keine Folge-Fristen".

  • B3 — Scenario state is forked across three stores by design but zero stores by data.

    • paliad.project_event_choices (mig 129) — schema present, 0 rows. EventChoiceService reads + writes it via internal/services/event_choice_service.go:74,123,180.
    • paliad.scenarios (mig 145) — 0 rows, 0/18 projects bound via active_scenario_id. ScenarioService.LoadScenarios in internal/services/fristenrechner.go:583-627 reads it.
    • DOM state on the result view — Verfahrensablauf checkbox state only lives client-side. Confirmed by absence of a write path from verfahrensablauf.tsx to either DB-side store.

    The RFC's "three independent stores" claim is architecturally true today, but every store is empty. Risk is dormant — until someone enables persistence on either path and the divergence materialises. See R6.

  • B4 — 6 active proceeding_types have zero rules. upc.bsv.cfi, upc.ccr.cfi, upc.costs.cfi, upc.dni.cfi, upc.epo.review, upc.pl.cfi. They appear in /api/tools/proceeding-types (is_active=true + kind='proceeding') but produce empty timelines when chosen. The Mode A picker can bind a project to them; the Mode B result view is blank.

2.4 Dead-or-decaying code

  • paliad.trigger_events table. 110 rows; columns (id, code, name, name_de, description, is_active, created_at, concept_id). Bigint PK. No parent_id, no proceeding_type_id. Consumed by: deadline_rule_service.go:226-285 (label fallback), event_deadline_service.go (legacy route), event_type_service.go (Pipeline C bridge), export_service.go:1680 (workbook sheet), and 80 active sequencing_rules' trigger_event_id (which is in turn primarily a bridge for the 73 globals + 7 hybrid rules with a real proceeding).
  • Inactive proceeding_types still referenced by spawn rules. id 11 (upc.apl.merits), 19 (upc.apl.cost), 20 (upc.apl.order). Mig 138 (appeal_target_backfill_merits_order) split them, mig later unified them onto id 160. The 4 spawn rules' FK was not updated.
  • 3 non-proceeding kinds. 23 rows total (phase × 4 + side_action × 10 + meta × 9), all is_active=false after mig 153. Live in the table for audit; unused by any active surface. The Slice 10 orphan-resolution path (rule_editor_orphans.go) could theoretically encounter them, but active = false filters them out.

3. Rules-corpus quality audit (live data)

3.1 parent_id coverage

  • 107/226 active+published rules have parent_id set (47%, matches RFC).
  • 119/226 do not. Decomposition (active+published):
Subset Rows Meaning
parent_id NULL AND trigger_event_id IS NULL AND proceeding_type_id set 46 Genuine proceeding-level roots (each PT has 16 such).
parent_id NULL AND trigger_event_id set AND proceeding_type_id NULL 73 The legacy globals — no place in the new chain model yet.

Of the 46 proceeding-level roots:

proceeding_type.code roots active rules
de.inf.lg 5 9
de.null.bpatg 4 10
epa.grant.exa 4 7
upc.apl.unified 6 16
epa.opp.boa 3 8
upc.pi.cfi 3 7
epa.opp.opd 2 8
de.inf.bgh, de.inf.olg, de.null.bgh, dpma.appeal.bgh, dpma.appeal.bpatg, dpma.opp.dpma, upc.disc.cfi 1 each various
upc.dmgs.cfi, upc.inf.cfi, upc.rev.cfi 4 each 8/25/17

Most "root" rules are legitimate (the chain start event has no logical predecessor — Klageerhebung, Zustellung, Veröffentlichung, Anmeldung, etc.). A small number are leaves whose parent chain just hasn't been seeded (e.g. de.inf.lg.berufung / de.inf.lg.beruf_begr list "Berufungsfrist" and "Berufungsbegründung" as parent-NULL despite both having a logical predecessor in de.inf.lg.urteil).

3.2 condition_expr usage

18 rules use the column. Three keys total:

Key Uses Sample shape
flag 14 {"flag":"with_ccr"}, {"flag":"with_amend"}, {"flag":"with_cci"}
op 4 {"op":"and","args":[{"flag":"with_ccr"},{"flag":"with_amend"}]}
args 4 always nested under an op:and

Distinct expressions (4 total, all UPC inf/rev): {"flag":"with_ccr"} (×6), {"op":"and","args":[{"flag":"with_ccr"},{"flag":"with_amend"}]} (×4), {"flag":"with_cci"} (×4), {"flag":"with_amend"} (×4).

No formal validation at write time — RuleEditorService accepts the column as freeform jsonb. The 3 flags are de-facto convention.

3.3 Spawn distribution

4 rules, all in the UPC CFI cluster, all priority='optional' + primary_party='both' + spawn target id=11 (upc.apl.merits, inactive):

Anchor event Spawn label Target
upc.inf.cfi.appeal_spawn "Berufungsverfahren öffnen" id=11 (inactive)
upc.rev.cfi.appeal_spawn "Berufungsverfahren öffnen" id=11 (inactive)
upc.pi.cfi.appeal_spawn "Berufungsverfahren öffnen" id=11 (inactive)
upc.dmgs.cfi.appeal_spawn "Berufungsverfahren öffnen" id=11 (inactive)

3.4 primary_party distribution

Excluding the 73 globals (all NULL), the published+active rules split:

proceeding_type cluster claimant defendant both court
upc.inf.cfi (25) 6 7 8 4
upc.rev.cfi (17) 6 7 1 3
upc.apl.unified (16) 0 0 12 4
de.null.bpatg (10) 2 2 3 3
de.inf.lg (9) 2 3 2 2
epa.opp.opd (8) 0 1 6 1
epa.opp.boa (8) 0 0 6 2
de.inf.bgh (8) 0 0 6 2
upc.dmgs.cfi (8) 2 2 1 3

39 rules have a primary_party value that differs from their parent rule's primary_party (excluding court ↔ anything, which is trivial). All 39 are legitimate "ball-in-other-court" hand-offs (claimant SoC → defendant SoD → claimant Reply → defendant Rejoinder …). The /follow-ups filter (§2.3 B1) hides all of them when the user picks a perspective.

3.5 is_court_set coverage

46 rules carry is_court_set=true. Distribution: every proceeding has at least one (the decision / order / oral-hearing rows). Highest: de.inf.lg (5), epa.grant.exa (4), upc.apl.unified (4), upc.inf.cfi (3), upc.rev.cfi (3), upc.pi.cfi (3), upc.dmgs.cfi (3). Calculator skips these in date math — they surface as "wird vom Gericht bestimmt" markers.

3.6 Legacy trigger_event_id overlap with parent_id

Combination Rows
parent_id set AND trigger_event_id set 2
parent_id set AND trigger_event_id NULL 105
parent_id NULL AND trigger_event_id set 73
parent_id NULL AND trigger_event_id NULL 46

Overlap is 2 rules out of 226 (0.9%). The two models are effectively disjoint in the corpus: the 73 legacy globals own the trigger_event_id lane; the 105 chain-linked rules own parent_id. The schema permits both columns to be set simultaneously, and 2 rules exercise that — but they are outliers, not a documented pattern.

The legacy paliad.trigger_events table is still read for label display by deadline_rule_service.go:226-285 (the "abhängig von …" chip rule fallback when parent_id isn't set) and for the legacy /api/tools/event-deadlines route.


4. Editorial gap map

Per proceeding_type (active, kind=proceeding). Columns:

  • A = active+published rules
  • P = rules with parent_id set
  • R = rules without parent_id (roots + leaves with missing parent)
  • E = active+published events whose code matches this PT's prefix
PT code A P R E Health
upc.inf.cfi 25 21 4 25 84% chained — strongest
upc.rev.cfi 17 13 4 17 76%
upc.apl.unified 16 10 6 16 † 63% — code-prefix issue, see below
de.null.bpatg 10 6 4 10 60%
de.inf.lg 9 4 5 9 44% — gappy
epa.opp.opd 8 6 2 8 75%
epa.opp.boa 8 5 3 8 63%
de.inf.bgh 8 7 1 8 88%
upc.dmgs.cfi 8 4 4 8 50%
upc.pi.cfi 7 4 3 7 57%
de.inf.olg 7 6 1 7 86%
epa.grant.exa 7 3 4 7 43%
de.null.bgh 6 5 1 6 83%
dpma.appeal.bpatg 5 4 1 5 80%
dpma.appeal.bgh 4 3 1 4 75%
dpma.opp.dpma 4 3 1 4 75%
upc.disc.cfi 4 3 1 4 75%
upc.bsv.cfi 0 0 0 0 unruled
upc.ccr.cfi 0 0 0 0 unruled
upc.costs.cfi 0 0 0 0 unruled
upc.dni.cfi 0 0 0 0 unruled
upc.epo.review 0 0 0 0 unruled
upc.pl.cfi 0 0 0 0 unruled

upc.apl.unified (id=160) is the active type, but its 16 events retain the legacy code prefixes upc.apl.{merits,cost,order}.* from the pre-unification taxonomy. The rules' proceeding_type_id was rebound to 160; the event codes were not renamed. Functional but inconsistent — see R3.

Events with no rule: 0. Every active+published event has at least one rule (corpus is 1:1 since mig 136). Editorial gap is therefore parent-chain-shaped, not rule-coverage-shaped.

Unmatched-prefix events: 69 events with code LIKE 'null.%'. They have rules (the 73 legacy globals — note the disparity: 73 rules but 69 events, because dedupe in mig 151 collapsed some duplicates while the rules still point at the canonical event). They do not belong to any proceeding_type and never will under the current taxonomy.


5. Risk register

Eleven items. Each: what, where, severity. Severity scale: critical (user-visible incorrect output / data loss possible) → high (user-visible UX lie, no data corruption) → medium (developer-trap; breaks at next refactor) → low (cosmetic / dead code, deferred maintenance).

R1 — Cross-party follow-up filter drops legitimate hand-offs — high

  • Where: internal/services/fristenrechner_followups.go:358-367.
  • Effect: with party=claimant|defendant, 39 active rules are hidden because their primary_party is the other side. Result-view reports "Keine Folge-Fristen" on chains that continue cross-party (e.g. def_to_ccr claimant-filed → reply_def_ccr defendant-filed in upc.inf.cfi).
  • Impact: UX lies to users about chain completion; can lead to missed deadlines on the opposing side's view.

R2 — Picker accepts spawn-only and leaf events — high

  • Where: internal/services/fristenrechner_followups.go:241-287 (anchor resolution does not check chain-anchor status); internal/services/fristenrechner_search_events.go (search returns every event).
  • Effect: Picking upc.{inf,rev,pi,dmgs}.cfi.appeal_spawn (spawn-only) shows the spawn rule itself but no follow-ups → "Keine Folge-Fristen". Picking a leaf event (e.g. upc.inf.cfi.def_to_ccr) only reaches whatever hop-1 children exist on the leaf's own party, see R1.
  • 67/222 active events are chain-anchors. Today's picker shows all 222 with equal weight.

R3 — 4 spawn rules point at an inactive proceeding_typehigh

  • Where: 4 rows in paliad.sequencing_rules with is_spawn=true and spawn_proceeding_type_id=11 (upc.apl.merits, is_active=false). The active appeal type is id=160 (upc.apl.unified).
  • Effect: any consumer that joins on spt.is_active=true (none today, but the moment any does) returns NULL for the spawn target. Today the join is permissive (fristenrechner_followups.go:394) — it returns upc.apl.merits to the frontend, which may surface as a CTA pointing at a stale type slug.
  • Plus consequence: upc.apl.unified events kept legacy code prefixes upc.apl.{merits,cost,order}.* even though the type rebinds to 160. Code/PT mismatch is harmless today; trap for any future code-prefix routing.

R4 — 73 "global" legacy rules orphan from the chain model — medium

  • Where: paliad.sequencing_rules WHERE proceeding_type_id IS NULL AND trigger_event_id IS NOT NULL (73 rows). Anchored on null.<8hex> procedural_events (69 distinct events, 73 rules — small overlap from pre-dedupe history).
  • Effect: invisible to Mode B (proceeding-type ablauf) because they don't bind to any PT; visible to the legacy bigint route /api/tools/event-deadlines and to /admin/procedural-events.
  • Migration debt: any "deprecate trigger_event_id" plan must decide whether to (a) reparent these onto a PT + parent chain, (b) keep them as floating cross-cutting rules in a separate lane, or (c) drop them.

R5 — Legacy paliad.trigger_events table is read by 5 surfaces — medium

  • Where:
    • internal/services/deadline_rule_service.go:226-285 — bulk-load for "abhängig von …" chip label fallback.
    • internal/services/event_deadline_service.go:79,244 — legacy /api/tools/event-deadlines route.
    • internal/services/event_type_service.go:40-414 — Pipeline-C event types bridge (event_types.trigger_event_id).
    • internal/services/export_service.go:1680ref__trigger_events workbook sheet.
    • cmd/gen-upc-snapshot/main.go:185-202 — UPC offline snapshot for youpc.org.
  • Effect: 110-row catalog with bigint PK lives alongside the 222 active procedural_events (UUID PK). Two ID spaces, two label sources, partial overlap.

R6 — Three scenario stores: 0 rows each, but 3 live read/write paths — medium

  • Stores: paliad.project_event_choices (0 rows), paliad.scenarios (0 rows), DOM state on Verfahrensablauf checkboxes.
  • Paths:
    • EventChoiceService (internal/services/event_choice_service.go:15-180) reads + writes the table.
    • ScenarioService.LoadScenarios + handlers (internal/services/fristenrechner.go:583-627, internal/handlers/scenarios.go:14-200+) read + write the table.
    • Verfahrensablauf result view writes nothing back — DOM only.
  • Effect today: nothing — empty tables. Effect tomorrow: the moment any surface starts persisting, the three paths can diverge. The RFC (§"What's actually broken" item 3) calls out the symptom: toggling "Mit Widerklage" on Verfahrensablauf doesn't drive conditional checkboxes in result-view submission cards.

R7 — 6 active proceeding_types are entirely unruled — medium

  • Where: upc.bsv.cfi, upc.ccr.cfi, upc.costs.cfi, upc.dni.cfi, upc.epo.review, upc.pl.cfi. All is_active=true, kind='proceeding', 0 active+published rules, 0 events with their code prefix.
  • Effect: pickable on /api/tools/proceeding-types, bindable on paliad.projects.proceeding_type_id (mig 153 only rejects non- proceeding kind, not zero-rule). Binding succeeds → SmartTimeline + Mode B render an empty result. UX lies.

R8 — condition_expr is freeform jsonb — medium

  • Where: column declaration in mig 136; consumer in deadline_rule_service.go (selected + passed to engine in pkg/litigationplanner/engine.go); writer in internal/services/rule_editor_service.go:625-843 (no validation).
  • Effect: 4 distinct shapes used today, 3 keys (flag, op, args). No write-time validation. New keys can be silently added; the engine consumes by switching on string literals. Refactor trap.

R9 — Inactive proceeding_types rows linger (23) — low

  • Where: mig 153 flipped 4 phase + 10 side_action + 9 meta rows to is_active=false. They still exist for audit.
  • Effect: snapshots and snapshots-of-snapshots (proceeding_types_pre_153, procedural_events_pre_151, sequencing_rules_pre_151/_pre_152) accumulate without a decay policy. Storage cost is trivial; query-shape cost is real if any query forgets WHERE kind='proceeding' AND is_active=true.

R10 — event_kind is nullable + not enumerated in DB — low

  • Where: paliad.procedural_events.event_kind text NULL. Code at frontend/src/admin-rules-edit.tsx:187 lists filing / hearing / decision / order in the UI but the DB accepts anything.
  • Effect: drift between UI vocab and persisted values is possible. Currently 5 buckets: filing, hearing, decision, order, NULL (per RFC).

R11 — applies_to_target + choices_offered lack a schema — low

  • Where: paliad.sequencing_rules.applies_to_target text[], choices_offered jsonb.
  • Effect: 16 rules use applies_to_target, 28 use choices_offered. Three observed choices_offered shapes: {appellant:[…]} (20), {skip:[…]} (6), {include_ccr:[…]} (2). Wire-level convention, no documentation. New shapes silently land if a future editor decides on one.

6. Recommendation — order of operations for the inventor

Phase 2 design starts with the highest-stakes, hardest-to-rewind decisions and finishes with editorial/cleanup. Each step is a question for m, not a design choice for the inventor.

Tier 1 — model decisions (grill first)

  1. Trigger semantics. Keep parent_id as the canonical link? What is the role of trigger_event_id after this RFC ships? If deprecated, what happens to the 73 legacy globals (R4) — reparent onto PTs, keep as a separate "cross-cutting" lane, or drop?
  2. Trigger discoverability. Derive from data (events that parent ≥1 rule = 67 today), maintain a materialised view, or carry an explicit is_trigger flag on procedural_events? Affects R2.
  3. Scenario state — single home. Of the three stores in R6, which wins? Migration shape for the others? The RFC mis-spoke about projects.scenarios jsonb — the table is paliad.scenarios with a spec jsonb column (mig 145). Confirm which storage the inventor reasons from.
  4. Cross-party display semantics. Backend stops filtering, frontend groups by side? Or backend tags + frontend renders an "andere Partei" group? Affects R1.

Tier 2 — surface decisions

  1. Spawn → consequence-only events. Stop surfacing spawn-only events in the picker (R2), or keep them and tag visually?
  2. Re-target the 4 spawn rules (R3) — point at id=160 vs reseed legacy ids; align event code prefixes vs. accept the mismatch.
  3. Sequence-from-proceeding-type view (Entry A). Where does it live? How do its toggles persist to the chosen scenario store?
  4. Legacy /api/tools/event-deadlines deprecation (R5). Drop, redirect, or keep behind a flag during transition?

Tier 3 — editorial + cleanup

  1. Editorial backfill plan. Which of the 119 parent-NULL rules are real roots vs. unseeded leaves (a per-PT walkthrough by m).
  2. Empty proceeding_types (R7). Stub with placeholder rules, or hide from the picker until rules land?
  3. condition_expr formalisation (R8). Pick a grammar, document it, add write-time validation. Same question for choices_offered
    • applies_to_target (R11).
  4. Legacy trigger_events table fate. Drop, archive, or repurpose? Depends on Q1 + Q2 above.

The inventor should grill m on Tier 1 before sketching anything. Tier 2 follows from Tier 1's decisions. Tier 3 is mechanical once Tier 1+2 land.


Appendix — query receipts

All counts in this assessment came from the live paliad schema on the youpc Supabase instance during the audit window (2026-05-27). Representative queries:

-- §0 + §3.1 + §3.6
SELECT
  CASE
    WHEN parent_id IS NOT NULL AND trigger_event_id IS NOT NULL THEN 'both'
    WHEN parent_id IS NOT NULL AND trigger_event_id IS NULL THEN 'parent only'
    WHEN parent_id IS NULL AND trigger_event_id IS NOT NULL THEN 'legacy only'
    ELSE 'neither (root)'
  END AS classification,
  proceeding_type_id IS NULL AS pt_null, count(*) AS rules
FROM paliad.sequencing_rules
WHERE is_active AND lifecycle_state = 'published'
GROUP BY classification, pt_null
ORDER BY classification, pt_null;
-- → both/false=2, legacy only/true=73, neither/false=46, parent only/false=105

-- §3.4
SELECT pt.code, sr.primary_party, count(*)
FROM paliad.sequencing_rules sr
LEFT JOIN paliad.proceeding_types pt ON pt.id = sr.proceeding_type_id
WHERE sr.is_active AND sr.lifecycle_state='published'
GROUP BY pt.code, sr.primary_party ORDER BY pt.code, count(*) DESC;

-- §4 (gap map)
SELECT pt.code, count(sr.id) AS active_rules,
       count(*) FILTER (WHERE sr.parent_id IS NULL) AS roots
FROM paliad.proceeding_types pt
LEFT JOIN paliad.sequencing_rules sr ON sr.proceeding_type_id = pt.id
  AND sr.is_active AND sr.lifecycle_state='published'
WHERE pt.is_active AND pt.kind='proceeding'
GROUP BY pt.code ORDER BY pt.code;

-- §3.2 (condition_expr keys)
WITH expanded AS (
  SELECT jsonb_object_keys(condition_expr) AS k
  FROM paliad.sequencing_rules
  WHERE condition_expr IS NOT NULL AND condition_expr::text <> '{}'
) SELECT k, count(*) FROM expanded GROUP BY k ORDER BY count(*) DESC;
-- → flag=14, args=4, op=4

Full set of queries used during the audit is available in the agent transcript; reproducible against any read-only Supabase role.

— end of assessment.