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.
36 KiB
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 (
paliadschema), 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_exprpopulated: 18 rules. Three distinct keys:flag(14),op+args(4 each — always nested AND).is_spawn = true: 4 rules. All four point at the inactiveupc.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_offeredpopulated: 28 rules. Three shapes:{appellant:[…]}(20),{skip:[…]}(6),{include_ccr:[…]}(2).applies_to_targetpopulated: 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; readsprocedural_events.edit.field.{code,event_kind,parent}i18n keys.frontend/src/verfahrensablauf.tsx— proceeding-type ablauf page (mode C); hits/api/tools/fristenrechnerwith 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 infrontend/src/client/i18n.ts:3193-3204, 6338-6346+.
1.4 Offline snapshot
cmd/gen-upc-snapshot/main.go:150-268— readspaliad.trigger_events, the legacypaliad.deadline_rulesprojection (now via the unified view), andpaliad.proceeding_types. Writes JSON topkg/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 carriesTriggerEventID,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
DualWriteServiceparity. Every CRUD on the editor surface keeps sequencing_rules + procedural_events + legal_sources locked, asserted bydual_write_test.go:50-202.- Admin editor (
/admin/procedural-events/*). Full create / edit / publish / audit loop. Drafts state respected. - Mode A picker via search.
DeadlineSearchServicefilters byevent_kind/primary_party/jurisdiction; returns child-rule counts (fristenrechner_search_events.go:159). - Mode B Verfahrensablauf calc.
pkg/litigationplanner.CalculateRule- the
proceeding_typefan-out works for every type that has any rule (17/23).
- the
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-proceedingproceeding_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 viapaliad.proceeding_typeswill return an inactive row. See R3. Cited atsequencing_rules4 rows; service code infristenrechner_followups.go:388SELECTsspt.codeviaLEFT JOIN paliad.proceeding_types spt ON spt.id = sr.spawn_proceeding_type_id— nois_activefilter 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 NULLandtrigger_event_id NOT NULL. These all anchor on legacynull.<8hex>event codes that don't match anyproceeding_types.codeprefix. 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-deadlinesroute. Live, used by Pipeline-Cevent_typesconsumers (EventTypeService). TheExportService:1680also still emitsref__trigger_eventsto 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+NULLrules but drops cross-party follow-ups. From the corpus there are 39 active rules whoseprimary_partydiffers from their parent's primary_party (excludingcourt). Example:upc.inf.cfi.def_to_ccris claimant-filed; its child ruleRoP.029.d → reply_def_ccris defendant-filed. Withparty=claimantselected 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 bype.id/pe.code/sr.idwith 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.EventChoiceServicereads + writes it viainternal/services/event_choice_service.go:74,123,180.paliad.scenarios(mig 145) — 0 rows, 0/18 projects bound viaactive_scenario_id.ScenarioService.LoadScenariosininternal/services/fristenrechner.go:583-627reads it.- DOM state on the result view — Verfahrensablauf checkbox state
only lives client-side. Confirmed by absence of a write path
from
verfahrensablauf.tsxto 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_typeshave 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_eventstable. 110 rows; columns(id, code, name, name_de, description, is_active, created_at, concept_id). Bigint PK. Noparent_id, noproceeding_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-
proceedingkinds. 23 rows total (phase× 4 +side_action× 10 +meta× 9), allis_active=falseafter 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_idset (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 1–6 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_idset - 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 theirprimary_partyis the other side. Result-view reports "Keine Folge-Fristen" on chains that continue cross-party (e.g.def_to_ccrclaimant-filed →reply_def_ccrdefendant-filed inupc.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_type — high
- Where: 4 rows in
paliad.sequencing_ruleswithis_spawn=trueandspawn_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 returnsupc.apl.meritsto the frontend, which may surface as a CTA pointing at a stale type slug. - Plus consequence:
upc.apl.unifiedevents kept legacy code prefixesupc.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 onnull.<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-deadlinesand 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-deadlinesroute.internal/services/event_type_service.go:40-414— Pipeline-C event types bridge (event_types.trigger_event_id).internal/services/export_service.go:1680—ref__trigger_eventsworkbook 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. Allis_active=true,kind='proceeding', 0 active+published rules, 0 events with their code prefix. - Effect: pickable on
/api/tools/proceeding-types, bindable onpaliad.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 inpkg/litigationplanner/engine.go); writer ininternal/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 forgetsWHERE 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 atfrontend/src/admin-rules-edit.tsx:187listsfiling / hearing / decision / orderin 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 usechoices_offered. Three observedchoices_offeredshapes:{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)
- Trigger semantics. Keep
parent_idas the canonical link? What is the role oftrigger_event_idafter 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? - Trigger discoverability. Derive from data (events that
parent ≥1 rule = 67 today), maintain a materialised view, or carry
an explicit
is_triggerflag onprocedural_events? Affects R2. - 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 ispaliad.scenarioswith aspecjsonb column (mig 145). Confirm which storage the inventor reasons from. - 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
- Spawn → consequence-only events. Stop surfacing spawn-only events in the picker (R2), or keep them and tag visually?
- Re-target the 4 spawn rules (R3) — point at id=160 vs reseed legacy ids; align event code prefixes vs. accept the mismatch.
- Sequence-from-proceeding-type view (Entry A). Where does it live? How do its toggles persist to the chosen scenario store?
- Legacy
/api/tools/event-deadlinesdeprecation (R5). Drop, redirect, or keep behind a flag during transition?
Tier 3 — editorial + cleanup
- Editorial backfill plan. Which of the 119 parent-NULL rules are real roots vs. unseeded leaves (a per-PT walkthrough by m).
- Empty proceeding_types (R7). Stub with placeholder rules, or hide from the picker until rules land?
condition_exprformalisation (R8). Pick a grammar, document it, add write-time validation. Same question forchoices_offeredapplies_to_target(R11).
- Legacy
trigger_eventstable 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.