Slice A (cosmetic rename) + Slice B (structural rework) for the deadline_rules → procedural_events / sequencing_rules / legal_sources split. Recommendation (R)=C (cosmetic now, structural follow-up). Umbrella-term lock: procedural event / Verfahrensschritt. Read-only design phase. No code or schema changes here. m/paliad#93.
53 KiB
Design — Procedural-Events Data Model (t-paliad-262)
Author: cronus (inventor)
Date: 2026-05-25
Issue: m/paliad#93 (mai task t-paliad-262)
Branch: mai/cronus/inventor-procedural
Status: DESIGN — read-only, no schema or code changes in this branch
Prior art read:
docs/design-deadline-data-model-2026-05-08.md(einstein, t-paliad-158) — proposedproceeding_event_types+proceeding_event_edges; the graph-shape recommendation has not been built (noproceeding_event*tables exist in the live DB as of 2026-05-25, verified viainformation_schema.tables).docs/design-fristen-phase2-2026-05-15.md(Phase 2/3 unified-rule columns — migs 078/079/091, shipped).docs/design-submission-generator-2026-05-19.mdanddocs/design-submission-page-2026-05-22.md(Slice 1 → Slice A of the Schriftsätze stack — shipped on top of today'sdeadline_rules).
This doc names a single conflation in the schema and proposes a two-slice fix (cosmetic immediate, structural follow-up). It is intentionally narrower than einstein's 2026-05-08 graph proposal — it does not re-litigate the proceeding-as-DAG question.
§0 TL;DR
paliad.deadline_rules today is one row that wears three hats:
- The procedural-event template —
submission_code,name,name_en,description,event_type,primary_party. This is "what kind of step is this in the proceeding": Rechtsbeschwerdebegründung, mündliche Verhandlung, Entscheidung, etc. - The legal-norm citation —
legal_source,rule_code,alt_rule_code,rule_codes[]. This is "the source-of-law anchor": § 102 PatG, UPC RoP R.220(1). - The sequencing rule —
parent_id,trigger_event_id,duration_value,duration_unit,timing,alt_duration_*,combine_op,condition_expr,is_spawn,spawn_*,sequence_order,is_court_set,priority,anchor_alt,proceeding_type_id. This is "how and when does it fire relative to other events".
The conflation surfaces most painfully in the submission-draft editor's variable sidebar (m's report 2026-05-25 15:02), where the lawyer sees field labels like {{rule.submission_code}} for what is plainly a procedural-event code, {{rule.event_type}} for what is plainly the procedural-event kind, and {{rule.legal_source_pretty}} for what is plainly the legal norm — all under a rule.* namespace that reads as if the lawyer were filling in arithmetic.
Recommendation = Q1 option (C):
- Slice A (immediate, this design's coder shift): cosmetic rename — placeholders, i18n labels, Go struct-comment naming, admin-UI page titles all shift to
procedural_event.*as the canonical name. Database schema, table name, column names, FK directions, JSON envelope keys on the wire all stay exactly as they are. Old{{rule.*}}placeholders remain emitted in the variable bag as legacy aliases so existing Word templates and saved drafts keep working. - Slice B (planned follow-up, separate mai task, separate slice plan): structural rework — extract
paliad.procedural_events,paliad.sequencing_rules,paliad.legal_sources, with a phased dual-write migration. Not shipped here. This doc defines the target shape (§4) and the migration shape (§5) so the eventual coder has a brief, not so the eventual coder is hired today.
Umbrella term lock = Q2 option (R): "procedural event" (DE: "Verfahrensschritt") as the umbrella covering filings, hearings, decisions, orders. Justification in §2.
Both Slice A and the eventual Slice B preserve the Schriftsätze surface (t-paliad-238/242/243): the submissions list query changes its predicate from dr.event_type = 'filing' to pe.event_kind IN ('filing', 'reply') (Slice B only) — same rows, cleaner predicate.
§1 Premises verified live (2026-05-25)
Every load-bearing claim was checked against the running paliad codebase + youpc Supabase. Numbers and schema facts are point-in-time as of 2026-05-25 15:30.
| Claim | Verification |
|---|---|
paliad.deadline_rules carries the 38 columns listed in §0's three-hats decomposition. |
information_schema.columns WHERE table_schema='paliad' AND table_name='deadline_rules' — 38 rows; columns confirmed verbatim. |
| Live row count = 254. | SELECT COUNT(*) FROM paliad.deadline_rules → 254. |
177 rows carry a submission_code (procedural-event identity); 158 distinct values. |
COUNT(*) FILTER (WHERE submission_code IS NOT NULL) → 177; COUNT(DISTINCT submission_code) → 158. |
102 rows carry a legal_source; 70 distinct citations. |
Same query, legal_source column. |
125 rows are linked to a deadline_concepts row via concept_id. |
COUNT(*) FILTER (WHERE concept_id IS NOT NULL) → 125 (49 % of the corpus). |
event_type distribution: 130 filing · 77 NULL · 25 decision · 21 hearing · 1 order. |
SELECT event_type, count(*) GROUP BY event_type — confirmed; the 77 NULL rows are structural / parent-only rows in the proceeding tree. |
10 submission_code values appear on more than one row (jurisdictional / bilateral variants). |
All 10 today are _archived_litigation.* codes (claimant/defendant splits + multi-stage hearing rows). Live non-archived codes are 1:1 with rows in the current corpus. |
paliad.deadlines joins to deadline_rules via column rule_id (uuid, FK). The text rule_code and free-text custom_rule_text (mig 122, t-paliad-258) are denormalized for display when the rule row is deleted. |
internal/services/deadline_service.go:69-127; live column list confirms rule_id, rule_code, custom_rule_text — there is no deadline_rule_id column on deadlines (issue body called it deadlines.deadline_rule_id — that's a doc-side typo; the column is rule_id). |
paliad.submission_drafts keys to a procedural event via submission_code text — no FK to deadline_rules. |
information_schema.columns for submission_drafts: submission_code text plus (project_id, submission_code) as the joint identifier. Confirms the Schriftsätze surface filters on the text key, not on deadline_rules.id. |
The Schriftsätze list (t-paliad-238) filters deadline_rules by event_type='filing' and submission_code IS NOT NULL. |
internal/handlers/submissions.go:193-211 — verbatim. |
The variable bag emits exactly 8 rule.* placeholders. |
internal/services/submission_vars.go:349-364 — rule.submission_code, rule.name, rule.name_de, rule.name_en, rule.legal_source, rule.legal_source_pretty, rule.primary_party, rule.event_type. Frontend i18n labels at frontend/src/client/submission-draft.ts:158-185. |
Admin rule-edit form binds the same rule.X fields. |
frontend/src/admin-rules-edit.tsx:74-110 + frontend/src/client/admin-rules-edit.ts:253-278 — same eight columns surfaced as form inputs. |
The Fristenrechner client surface refers to calc.rule.nameDE / calc.rule.nameEN. |
frontend/src/client/fristenrechner.ts:1592,1655. |
einstein's 2026-05-08 proceeding_event_types + proceeding_event_edges are not in the DB. |
SELECT table_name FROM information_schema.tables WHERE table_schema='paliad' AND table_name LIKE '%proceeding_event%' → 0 rows. The graph-shape proposal was never built. |
paliad.deadline_concepts (57 rows in the original einstein audit; live count not directly queried this shift) still exists and is referenced via deadline_rules.concept_id. |
information_schema.tables confirms deadline_concepts, deadline_concept_event_types, deadline_event_types, event_types, trigger_events, event_categories all still present — the deadline-knowledge graph from the einstein design lives on alongside the unified rule columns. |
Phase 2/3 columns (priority, condition_expr, is_court_set, lifecycle_state, draft_of, published_at, rule_codes[]) are live and load-bearing. |
internal/models/models.go:622-684 + mig 091. Slice B's structural rework must preserve every one of these on the new sequencing_rules table — they are not legacy. |
Live paliad.deadlines references to rules are sparse (1 row in prod). |
SELECT COUNT(*) FROM paliad.deadlines → 1. The 4 submission_drafts rows reference a procedural event by submission_code text only. Tiny live FK surface → migrations can be aggressive without losing user data. |
Migration tracker is paliad.paliad_schema_migrations; next available number is 124 (mig 123 = Backup Mode Slice A, just shipped). |
internal/db/migrations/ directory listing; latest applied = 123. |
Doc-side bug flagged for this issue's body: the deliverable spec writes paliad.deadlines.deadline_rule_id in §3 (Q3 migration shape). The live column is paliad.deadlines.rule_id. Slice B's rename target is therefore paliad.deadlines.procedural_event_id, not paliad.deadlines.procedural_event_id after a non-existent deadline_rule_id step. Updating the issue body is m's call — flagged here so it doesn't propagate into a coder brief.
§2 m's vocabulary call (Q2 — lock the umbrella term)
m proposed "procedural event" in the report. Options weighed:
| Option | Reads as | Collisions | Verdict |
|---|---|---|---|
| "procedural event" (DE: "Verfahrensschritt") | Umbrella that naturally covers filings, hearings, decisions, orders. Matches lawyer mental model: "the next thing that happens in the proceeding". | None — no paliad.procedural_event* table or column today (verified). |
(R) — adopt as canonical. |
| "submission" | Today the Schriftsätze surface uses this for filings only (event_type='filing'). Expanding the meaning would silently change Slice A's semantics for an existing UI. |
Surface-level collision with the Schriftsätze nomenclature already in production. | Reject — would lose precision for an existing concept. |
| "event" / "event_type" | Existing deadline_rules.event_type column. |
Hard collision with paliad.events (audit feed, distinct table, distinct meaning). Renaming around it would be worse than the conflation we're trying to fix. |
Reject. |
| "Verfahrensschritt" only (no English) | Cleanest German but no English fallback. | Bilingual UI (DE primary, EN secondary per project CLAUDE.md) requires both. | Reject in isolation — but adopt as the canonical German rendering of "procedural event". |
| "Verfahrensereignis" | Closer literal translation of "procedural event". | None. | Reject in favor of "Verfahrensschritt" — m's broader vocabulary uses "Schritt" (e.g. "Antragsschritt") more naturally than "Ereignis", which already maps to paliad.events in the audit-feed sense. |
Lock:
| Surface | Canonical |
|---|---|
| English | procedural event (lowercase except sentence-initial) |
| German | Verfahrensschritt (m. — der Verfahrensschritt) |
| Plural EN | procedural events |
| Plural DE | Verfahrensschritte |
| Code identifier (Go struct names, TS types) | ProceduralEvent, ProceduralEventKind, ProceduralEventTemplate |
| Snake-case (DB columns, JSON keys, i18n keys, placeholders) | procedural_event, procedural_event_kind, procedural_events (table) |
| Slice A: variable-bag placeholder namespace | procedural_event.* (with rule.* kept as legacy alias) |
| Slice B: table name (if shipped) | paliad.procedural_events |
event_type (the column) becomes event_kind in Slice B — using "kind" rather than "type" to free up the word "type" for the proceeding-level taxonomy (paliad.proceeding_types, untouched) and to mirror the "event_type vs event_kind" disambiguation einstein hit in the 2026-05-08 doc. In Slice A the column stays event_type (no DB change).
Q2 is locked by inventor recommendation. It costs nothing structurally and clears noise across every downstream conversation. If m disagrees in the head round-trip, the only thing that flips is the term — Slice A's scope shape stays.
§3 Scope decision (Q1 — A vs B vs C)
Recommendation = (C) — cosmetic rename now, structural rework as a planned follow-up.
Why not (A) — cosmetic only and stop
(A) leaves the model wrong forever. The conflation isn't just a labelling annoyance — it makes future questions harder to answer cleanly:
- "How many distinct procedural events does paliad model?" Today: ambiguous (rows vs distinct
submission_codes vs distinct(submission_code, proceeding_type_id)tuples). - "Where can we attach a per-procedural-event Word template that's independent of which proceeding it appears in?" Today: nowhere — the FK chain forces a per-row template registry, see
internal/handlers/files.gotemplate fallback. - "Show me every sequencing rule that triggers a given procedural event across all proceedings." Today: requires joining
deadline_rulesto itself onsubmission_code+parent_id, brittle.
If m signals (A) anyway — fine; the cosmetic-only slice is a strict subset of (C)'s Slice A and ships the same value (label clarity in the editor). But the recommendation is to write down the structural target now while the analysis is fresh.
Why not (B) — restructure immediately
(B) means: one slice plan, one cutover. With:
- 254 live rule rows,
- 1 live
paliad.deadlinesrow, - 4 live
submission_draftsrows, - 12 Go services + 6 handlers touching
deadline_rules+ 8 placeholder strings on the wire + the admin rule-editor UI bound to the column shape,
…doing this in one cutover means a big-bang migration during a downtime window. m has granted exactly one such window in recent memory (2026-05-15 for mig 091's destructive drops), and that one was constrained to a 4-column drop. A four-table restructure has a meaningfully larger blast radius; it warrants its own task with its own slice plan and its own risk review.
Why (C) — cosmetic-rename Slice A this design, structural Slice B as a separate task
Three properties of (C) make it the safe call:
- Slice A is reversible at any time — every change is in i18n strings, Go struct comments, admin-UI page titles, and the variable-bag aliases. No DB migration. No drop. A revert is a
git revertof the Slice A commit. - Slice B is fully designed but uncommitted — §4 and §5 below define the target shape and migration plan, but the design doc itself ships in Slice A. m can read it, redirect it, or park it without pressure to ship it now.
- The Schriftsätze surface doesn't care which slice we ship — Slice A leaves it on
event_type='filing'; Slice B flips it toevent_kind IN ('filing', 'reply')over a dual-write window. Either way, the lawyer-facing behavior is unchanged.
Slice A's deliverable boundary (what gets renamed, what stays)
Renamed in Slice A:
- i18n keys for the admin rule-editor field labels:
admin.rules.edit.field.submission_code→admin.rules.edit.field.procedural_event_code, etc. (16 keys total —name,name_en,description,submission_code,rule_code,legal_source,primary_party,event_type× DE/EN — full list in §7.1.) - Variable-bag placeholder labels in
submission-draft.ts:158-185: the visible label ({ de: "Schriftsatz-Code", en: "Submission code" }) is unchanged for filings (filings are still Schriftsätze on that surface), but the namespace shown next to the placeholder string changes: lawyer sees{{procedural_event.code}}in the placeholder column with the same Schriftsatz-Code label and same value. The old{{rule.submission_code}}stays in the catalog as an "(alt)" entry pointing at the same field. - Variable-bag emission (
internal/services/submission_vars.go:351-364): the bag emits both key-names for every value, so any Word template / saved draft holding{{rule.X}}keeps working without a touch. New templates and the in-app catalog show the canonical{{procedural_event.X}}name. - Admin page titles + section headings: "Regel bearbeiten" → "Verfahrensschritt bearbeiten" (DE), "Edit rule" → "Edit procedural event" (EN). "Regeln verwalten" → "Verfahrensschritte verwalten" / "Procedural events". The URL path
/admin/rulesstays — URL renames have downstream cost (bookmarks, audit log entries) and would need their own redirect slice (out of scope here). - Go struct comments + service docstrings + worker-facing log lines that refer to "the rule" → "the procedural event" where the referent is the procedural-event aspect (not the sequencing-rule aspect). Function names, type names, table name stay (Slice B handles those).
- The "Submission Code / Einreichung-Kennung" label itself stays (it's the lawyer's anchor — they recognize it). The framing around it changes: it now reads as "the code that identifies this procedural event", not "the code attached to this rule".
Untouched in Slice A:
- Database schema. Table name (
paliad.deadline_rules). Column names. FK directions. Indexes. RLS policies. Triggers. Audit log columnrule_id. - Go struct names:
DeadlineRulestays. The renames here are prose, not code. RenamingDeadlineRuletoProceduralEventcouples Slice A to Slice B's table rename — keep them decoupled. - JSON envelope keys on the wire (
POST /api/admin/rules/:idstill acceptssubmission_codein the body — Slice B's API rename is a breaking change with its own deprecation window). - URL paths (
/admin/rules,/api/admin/rules/:id,/api/projects/:id/submissionsetc.). paliad.deadlines.rule_idFK column name.- The variable-bag's legacy
{{rule.X}}keys — kept forever as aliases (cheap, zero rot). - The
submission_draftstable'ssubmission_codetext key.
This boundary makes Slice A a one-day coder shift: scoped, reversible, label-only.
What Slice B inherits
Slice B inherits a codebase + a UI where every prose surface already speaks "procedural event". It also inherits a legacy alias contract (the dual emission in the variable bag) that gives it freedom to rename the JSON keys on the wire and the Go struct in two separate sub-slices without rushing.
§4 Restructure schema (Q3 — if/when we ship Slice B)
This is the target the eventual Slice B coder would land. Nothing here ships in this task.
§4.1 Three new tables (plus the rename of deadline_rules)
-- 1. Procedural event templates — one row per (procedural-event identity)
-- For now the live corpus is 1:1 with non-archived submission_codes
-- (148 of the 158 distinct codes), so we get ~177 rows minus the 10
-- multi-row codes' duplicates. Bilateral / jurisdictional variants
-- are modeled at the sequencing_rules layer.
CREATE TABLE paliad.procedural_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code text NOT NULL UNIQUE, -- former submission_code
name text NOT NULL, -- DE
name_en text NOT NULL,
description text,
event_kind text NOT NULL, -- filing|reply|hearing|decision|order|other
primary_party_default text, -- claimant|defendant|both|court
legal_source_id uuid REFERENCES paliad.legal_sources(id),
concept_id uuid REFERENCES paliad.deadline_concepts(id),
lifecycle_state text NOT NULL DEFAULT 'published', -- draft|published|archived
draft_of uuid REFERENCES paliad.procedural_events(id),
published_at timestamptz,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- 2. Legal sources — the source-of-law citations the procedural event
-- anchors against. ~70 distinct values today (live corpus).
CREATE TABLE paliad.legal_sources (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
citation text NOT NULL UNIQUE, -- "DE.PatG.102", "UPC.RoP.220.1", …
jurisdiction text NOT NULL, -- DE|UPC|EPA|DPMA|other
pretty_de text NOT NULL, -- "§ 102 PatG"
pretty_en text NOT NULL, -- "Section 102 PatG"
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- 3. Sequencing rules — the timing / trigger / condition mechanics that
-- today live alongside the procedural-event identity on deadline_rules.
-- One row per (procedural_event × proceeding × variant). The 10
-- "_archived_litigation.*" codes that today have 2-5 rows become
-- 2-5 sequencing_rules rows for the same procedural_events row.
CREATE TABLE paliad.sequencing_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
procedural_event_id uuid NOT NULL REFERENCES paliad.procedural_events(id),
proceeding_type_id integer REFERENCES paliad.proceeding_types(id),
parent_id uuid REFERENCES paliad.sequencing_rules(id), -- structural tree, today's parent_id
trigger_event_id bigint REFERENCES paliad.trigger_events(id), -- event-rooted variant
duration_value integer NOT NULL DEFAULT 0,
duration_unit text NOT NULL DEFAULT 'months',
timing text DEFAULT 'after',
alt_duration_value integer,
alt_duration_unit text,
alt_rule_code text, -- legacy free-text alt citation, retained
anchor_alt text,
combine_op text, -- max|min
condition_expr jsonb,
primary_party text, -- per-rule override of the procedural_event default
sequence_order integer NOT NULL DEFAULT 0,
is_spawn boolean NOT NULL DEFAULT false,
spawn_label text,
spawn_proceeding_type_id integer REFERENCES paliad.proceeding_types(id),
is_bilateral boolean NOT NULL DEFAULT false,
is_court_set boolean NOT NULL DEFAULT false,
priority text NOT NULL DEFAULT 'mandatory',
rule_code text, -- legacy short-form citation, retained on the rule
rule_codes text[], -- multi-citation array (mig pre-091)
deadline_notes text,
deadline_notes_en text,
lifecycle_state text NOT NULL DEFAULT 'published',
draft_of uuid REFERENCES paliad.sequencing_rules(id),
published_at timestamptz,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- 4. Rename downstream FK + add the link to procedural_events.
ALTER TABLE paliad.deadlines
ADD COLUMN procedural_event_id uuid REFERENCES paliad.procedural_events(id),
ADD COLUMN sequencing_rule_id uuid REFERENCES paliad.sequencing_rules(id);
-- (rule_id stays as a transitional alias during the dual-write window;
-- dropped at end of Slice B)
-- 5. Submission drafts: add procedural_event_id FK alongside submission_code.
ALTER TABLE paliad.submission_drafts
ADD COLUMN procedural_event_id uuid REFERENCES paliad.procedural_events(id);
-- (submission_code stays — it's the cosmetic anchor lawyers recognize
-- in URLs and chat, and it doubles as the procedural_events.code value)
§4.2 What goes where (column-by-column map)
Every column on today's paliad.deadline_rules lands on exactly one of the three new tables:
Today's deadline_rules column |
Lands on | Notes |
|---|---|---|
id, created_at, updated_at |
sequencing_rules |
The current row's identity becomes a sequencing-rule row. procedural_events.id is new — backfilled from submission_code. |
submission_code |
procedural_events.code |
Promoted up. Multi-row codes (10 in corpus, all _archived_litigation.*) collapse to one row on the new table; the 2-5 sequencing rows hang off it. |
name, name_en, description |
procedural_events |
Procedural-event identity. |
primary_party |
procedural_events.primary_party_default AND sequencing_rules.primary_party |
Both. The procedural event has a default party (claimant for Klage etc.); the sequencing rule can override per-jurisdiction (bilateral variants — e.g. litigation.reply claimant vs defendant become two sequencing rows with overridden party). |
event_type |
procedural_events.event_kind |
Hat 1, with rename to event_kind (term lock §2). |
legal_source |
legal_sources.citation + FK from procedural_events.legal_source_id |
The citation moves to its own row; the procedural event points at it. pretty_de / pretty_en materialize the existing legalSourcePretty() function output as columns (with the function retained as the migration source). |
rule_code, alt_rule_code, rule_codes[] |
sequencing_rules |
Short-form citation arrays stay on the sequencing rule — they're rule-specific. |
proceeding_type_id, parent_id, trigger_event_id, spawn_proceeding_type_id, is_spawn, spawn_label, is_bilateral, is_court_set, combine_op |
sequencing_rules |
Hat 3 (mechanics) — exact copies. |
duration_value, duration_unit, timing, alt_duration_value, alt_duration_unit, anchor_alt |
sequencing_rules |
Hat 3 (mechanics). |
condition_expr (jsonb) |
sequencing_rules |
Hat 3. The grammar from mig 091 stays. |
priority, sequence_order |
sequencing_rules |
Hat 3. |
is_active, lifecycle_state, draft_of, published_at |
BOTH procedural_events AND sequencing_rules |
A procedural event can be retired independently of any one of its sequencing variants. Backfill: copy onto both during dual-write; new rows go through the rule-editor service which writes both sides together. |
concept_id (FK to deadline_concepts) |
procedural_events.concept_id |
The concept layer (einstein 2026-05-08) attaches to the procedural event, not the sequencing rule. |
deadline_notes, deadline_notes_en |
sequencing_rules |
They're rule-specific notes ("filing the appeal in DE costs €X if you also did Y") — not procedural-event-wide. |
Three columns disappear:
- The semantically-overloaded part of
event_type(renamed toevent_kindand moved). - The "what is this thing" vs "how does it fire" name conflict — gone by construction.
- Any column that exists only because of the conflation (none of today's columns are pure overhead — they all carry data — so the count stays at 38 across the three new tables).
§4.3 Indexes + RLS
paliad.can_see_project() is the canonical RLS predicate (mig 055). None of the three new tables hold project-scoped data — they're firm-wide reference tables. RLS = none, same posture as today's deadline_rules (which is firm-wide and unrestricted at the row level; access control is via the lifecycle_state='published' filter in the read paths).
Indexes inherited from today:
paliad.legal_sources(citation)— UNIQUE.paliad.procedural_events(code)— UNIQUE.paliad.procedural_events(concept_id)— for the deadline-concept join.paliad.sequencing_rules(procedural_event_id, proceeding_type_id, lifecycle_state)— primary read path for the calculator.paliad.sequencing_rules(parent_id)— tree walk.paliad.sequencing_rules(trigger_event_id)— event-rooted variant.
§5 Migration plan (Slice B — when it ships, not in this task)
Phased dual-write, so the cutover is never a single instant where the wire format flips. m gets to roll back any one phase with a git revert + an ALTER TABLE if a phase misbehaves in prod.
§5.1 Phase 1 — Additive (no down-time)
- Create
procedural_events,sequencing_rules,legal_sources. - Backfill
legal_sourcesfromDISTINCT legal_sourceondeadline_rules(70 rows). Populatepretty_de/pretty_enby calling the existinglegalSourcePretty()function in a one-shot SQL/Go shim during the migration. VerifyCOUNT(DISTINCT legal_source FROM deadline_rules) = COUNT(*) FROM legal_sources. - Backfill
procedural_eventsfromDISTINCT submission_codeondeadline_rules WHERE submission_code IS NOT NULL. Takename,name_en,event_type → event_kind,primary_party,concept_id,descriptionfrom the lowest-idrule row for each code (tie-breaker: lowestsequence_order). VerifyCOUNT(*) FROM procedural_events = COUNT(DISTINCT submission_code FROM deadline_rules WHERE submission_code IS NOT NULL)(= 158). - Backfill
sequencing_rules1:1 fromdeadline_rules(254 rows). FKprocedural_event_idresolved by code lookup; sequencing-rule row inherits thedeadline_rules.id(so existingdeadlines.rule_idFKs continue to resolve via the new column for the dual-write window — see Phase 3). - Add
paliad.deadlines.procedural_event_id+sequencing_rule_idcolumns, backfill fromdeadlines.rule_idjoin. - Add
paliad.submission_drafts.procedural_event_id, backfill fromsubmission_codejoin.
This phase ships behind a feature flag (or just behind unused code) — readers + writers stay on deadline_rules. No behavior change.
§5.2 Phase 2 — Dual-write (no down-time)
- Update
RuleEditorServiceto write to bothdeadline_rules(legacy) and (procedural_events,sequencing_rules,legal_sources) on every Create/Update/Publish/Archive. Audit log writes one row per side. - Update read paths to read from the new tables, falling back to
deadline_rulesif the new row is missing (defense-in-depth during backfill catch-up). - Run for ≥ 1 week (m's call on length). Compare row counts and a hash digest of the union daily — if drift, surface.
§5.3 Phase 3 — Cutover (no down-time, but reversible only via re-application of the dual-write)
- Flip read paths to only the new tables (
SubmissionVarsService.loadPublishedRule,DeadlineRuleService.*,SubmissionService.list,ProjectionService,FristenrechnerCalc, etc.). - Stop writing to
deadline_rules. paliad.deadlines.rule_idis kept as a no-op alias for one more week; new writes go toprocedural_event_id+sequencing_rule_id.submission_drafts.submission_codeis kept as the URL anchor; the FKprocedural_event_idis the primary join key going forward.
§5.4 Phase 4 — Drop legacy (downtime window, destructive)
paliad.deadline_rules_pre_<slice-B-mig>snapshot of the entire table.- DROP TABLE paliad.deadline_rules (after CASCADE-safe FK rewires).
- DROP COLUMN paliad.deadlines.rule_id (keep
rule_code+custom_rule_textas the human-readable denormalized columns — they're the safety net for orphaned deadlines per t-paliad-258).
m grants this destructive phase its own window (precedent: mig 091 on 2026-05-15). Until then, the legacy table sits dormant.
§5.5 Migration tracker
- Slice B uses migration numbers 124 (Phase 1 — create tables + backfill) and onward — a 4-5 migration sequence, one per phase boundary, mirroring the Phase 2/3 slicing that shipped under t-paliad-195.
- Each migration includes a
paliad.audit_reason = 'mig <n>: <slice-B-phase>'set_config like mig 091 did, so the audit log captures the schema journey.
§6 Service-layer impact
§6.1 Slice A — prose-only changes
| File | Change |
|---|---|
internal/services/submission_vars.go |
addRuleVars → also emit procedural_event.code, procedural_event.name, procedural_event.name_de, procedural_event.name_en, procedural_event.legal_source, procedural_event.legal_source_pretty, procedural_event.primary_party, procedural_event.event_kind (8 new keys, 1:1 with the 8 existing rule.* keys, same values). Rename docstrings + the package-level placeholder map comment ("rule.*" → "procedural_event.* (with legacy alias rule.*)"). |
internal/services/deadline_rule_service.go |
Top-of-file comment + struct comment renames only. Method names stay (DeadlineRuleService, GetByID, etc.). |
internal/services/rule_editor_service.go |
Same. |
internal/services/projection_service.go, deadline_service.go, fristenrechner.go, submission_draft_service.go, event_trigger_service.go, event_deadline_service.go, proceeding_mapping.go, export_service.go |
No code changes. Comments mentioning "the rule"/"rules" stay accurate as long as the file is about sequencing — only services that surface the identity aspect of the rule (submission_vars.go) need a prose pass. |
internal/handlers/submissions.go |
No SQL change. Type+comment renames: the catalog response type stays submissionListEntry (it's still a Schriftsatz-level list); doc comments speak of "procedural events whose kind is filing" instead of "rules of type filing". |
internal/handlers/admin_rules.go |
URL path stays. JSON envelope stays. Page-render comments + log-line text shift to "procedural event". |
internal/handlers/submission_drafts.go, deadlines.go, fristenrechner.go |
No service-layer change. |
§6.2 Slice B — structural
Mostly load-bearing; not enumerated here in detail (out of scope per (R)=C). The shape:
RuleEditorServicesplits intoProceduralEventService+SequencingRuleService+LegalSourceService. The Save / Publish / Archive flow on the editor coordinates all three.DeadlineRuleService.GetByIDbecomesSequencingRuleService.GetByID; thesubmission_codelookup moves toProceduralEventService.GetByCode.SubmissionVarsService.loadPublishedRulebecomesloadPublishedProceduralEventand returns a triple (event,defaultSequencingRule,legalSource); the variable-bag emission consumes all three.ProjectionServiceand the Fristenrechner calculator read fromsequencing_rules(same column set, same logic — only the table name changes).SubmissionService.list(handlers/submissions.go) filtersprocedural_events.event_kind IN ('filing', 'reply').- Backfill orphans + audit triggers (mig 079 / 089) are re-pointed at
sequencing_rules+ a newprocedural_events_audit.
§7 UI / i18n impact
§7.1 i18n keys (Slice A)
Existing keys (DE + EN) at frontend/src/client/i18n.ts lines ~2834-2920 and ~5800-5890 — surface area is labels, not placeholders-in-Word:
| Old key | New key (Slice A) | DE label | EN label |
|---|---|---|---|
admin.rules.list.title |
admin.procedural_events.list.title |
"Verfahrensschritte verwalten — Paliad" | "Manage procedural events — Paliad" |
admin.rules.list.heading |
admin.procedural_events.list.heading |
"Verfahrensschritte verwalten" | "Manage procedural events" |
admin.rules.list.subtitle |
admin.procedural_events.list.subtitle |
"Verfahrensschritte anlegen, bearbeiten und freigeben. Lifecycle: draft → published → archived." | "Create, edit and publish procedural events. Lifecycle: draft → published → archived." |
admin.rules.list.new |
admin.procedural_events.list.new |
"+ Neuer Verfahrensschritt" | "+ New procedural event" |
admin.rules.col.submission_code |
admin.procedural_events.col.code |
"Code" (drop "/ Einreichung-Kennung" — the new heading already disambiguates) | "Code" |
admin.rules.col.legal_citation |
admin.procedural_events.col.legal_source |
"Rechtsgrundlage" | "Legal source" |
admin.rules.col.name |
admin.procedural_events.col.name |
"Bezeichnung" | "Name" |
admin.rules.col.proceeding |
admin.procedural_events.col.proceeding |
"Verfahrenstyp" | "Proceeding" |
admin.rules.col.priority |
admin.procedural_events.col.priority |
"Priorität" | "Priority" |
admin.rules.col.lifecycle |
admin.procedural_events.col.lifecycle |
"Lifecycle" | "Lifecycle" |
admin.rules.col.modified |
admin.procedural_events.col.modified |
"Zuletzt geändert" | "Last modified" |
admin.rules.edit.title |
admin.procedural_events.edit.title |
"Verfahrensschritt bearbeiten — Paliad" | "Edit procedural event — Paliad" |
admin.rules.edit.heading.loading |
admin.procedural_events.edit.heading.loading |
"Verfahrensschritt laden…" | "Loading procedural event…" |
admin.rules.edit.breadcrumb |
admin.procedural_events.edit.breadcrumb |
"← Verfahrensschritte verwalten" | "← Manage procedural events" |
admin.rules.edit.field.submission_code |
admin.procedural_events.edit.field.code |
"Code (Schriftsatz-Code / Einreichung-Kennung)" — keep the parenthetical so lawyers familiar with the old label know what they're looking at. | "Code (submission / procedural-event identifier)" |
admin.rules.edit.field.rule_code |
admin.procedural_events.edit.field.short_citation |
"Rechtsgrundlage (Kurzform)" | "Legal source (short form)" |
admin.rules.edit.field.legal_source |
admin.procedural_events.edit.field.legal_source |
"Rechtsgrundlage (Langform)" | "Legal source (long form)" |
admin.rules.edit.field.name |
admin.procedural_events.edit.field.name |
"Bezeichnung (DE)" | "Name (DE)" |
admin.rules.edit.field.name_en |
admin.procedural_events.edit.field.name_en |
"Bezeichnung (EN)" | "Name (EN)" |
admin.rules.edit.field.proceeding |
admin.procedural_events.edit.field.proceeding |
"Verfahrenstyp" | "Proceeding type" |
admin.rules.edit.field.trigger |
admin.procedural_events.edit.field.trigger |
"Trigger-Ereignis" | "Trigger event" |
admin.rules.edit.field.parent |
admin.procedural_events.edit.field.parent |
"Übergeordneter Verfahrensschritt (UUID)" | "Parent procedural event (UUID)" |
admin.rules.edit.field.concept |
admin.procedural_events.edit.field.concept |
"Konzept (UUID)" | "Concept (UUID)" |
admin.rules.edit.field.sequence_order |
admin.procedural_events.edit.field.sequence_order |
"Reihenfolge" | "Order" |
admin.rules.edit.field.duration_value |
admin.procedural_events.edit.field.duration_value |
"Dauer" | "Duration" |
admin.rules.edit.field.primary_party |
admin.procedural_events.edit.field.primary_party |
"Partei (typisch)" | "Primary party" |
admin.rules.edit.field.event_type |
admin.procedural_events.edit.field.event_kind |
"Art des Verfahrensschritts" | "Procedural-event kind" |
admin.rules.edit.field.description |
admin.procedural_events.edit.field.description |
"Beschreibung" | "Description" |
Legacy keys retained as aliases so any existing translation imports or external integrations keep working — old keys point at the same DE/EN values during a deprecation window of one full Slice B cycle.
§7.2 Variable-bag placeholders (Slice A)
frontend/src/client/submission-draft.ts:155-185 — the catalog of placeholders the lawyer sees in the sidebar:
| Old placeholder (kept as legacy alias) | New canonical placeholder | DE label | EN label |
|---|---|---|---|
{{rule.submission_code}} |
{{procedural_event.code}} |
"Code (Verfahrensschritt)" | "Code (procedural event)" |
{{rule.name}} |
{{procedural_event.name}} |
"Bezeichnung" | "Name" |
{{rule.name_de}} |
{{procedural_event.name_de}} |
"Bezeichnung (DE)" | "Name (DE)" |
{{rule.name_en}} |
{{procedural_event.name_en}} |
"Bezeichnung (EN)" | "Name (EN)" |
{{rule.legal_source}} |
{{procedural_event.legal_source}} |
"Rechtsgrundlage (Code)" | "Legal source (code)" |
{{rule.legal_source_pretty}} |
{{procedural_event.legal_source_pretty}} |
"Rechtsgrundlage" | "Legal source" |
{{rule.primary_party}} |
{{procedural_event.primary_party}} |
"Partei (typisch)" | "Primary party" |
{{rule.event_type}} |
{{procedural_event.event_kind}} |
"Art des Verfahrensschritts" | "Procedural-event kind" |
The catalog renders the canonical name in the "copy-this-placeholder" button. The variable bag (submission_vars.go) emits both names with identical values, so any Word template the lawyer already has continues to work; new templates are encouraged to use the canonical name.
§7.3 Admin rule-editor form (Slice A)
frontend/src/admin-rules-edit.tsx:74-110 — i18n key rebinds + heading text update. The DOM id attributes (f-submission-code, f-rule-code, f-legal-source, …) stay — they're internal, the rename here is cosmetic, the form still POSTs the same JSON envelope (Slice A doesn't touch the API). The fieldset legend for the "Identität" section changes to "Verfahrensschritt-Identität" (DE) / "Procedural-event identity" (EN). The "Verfahren & Trigger" section heading stays — that section is about sequencing, and Slice A doesn't rename sequencing-level labels (those are Slice B).
§7.4 Project-detail Schriftsätze tab + dashboard
frontend/src/client/submissions.ts, submissions-index.ts: no surface-level label change in Slice A. The Schriftsätze tab continues to show Schriftsätze (the lawyer's preferred term for filings specifically). The tab is a filtered view onto procedural events of kind filing/reply — that distinction surfaces only in admin contexts.
§7.5 Help text + docs
A short addition to the in-app help: "What is a procedural event?" — one-paragraph definition explaining the umbrella term, with examples (Klage, Klageerwiderung, mündliche Verhandlung, Endurteil). Stored in frontend/src/client/i18n.ts under help.procedural_events.intro. Out of scope for the URL/router changes — added as static copy where it fits naturally.
§8 Slice plan
§8.1 Slice A (this design's downstream task)
Scope: prose-only rename per §3 ("renamed in Slice A" list).
Mechanics:
- Add 8 new placeholder keys to the variable bag in
submission_vars.go(1:1 with the existing 8rule.*keys). Keep the legacy keys. - Update
frontend/src/client/submission-draft.tsplaceholder catalog labels. - Rebind admin i18n keys per §7.1 (with legacy keys retained).
- Update admin page titles + section headings.
- Update Go struct comments + service docstrings in
submission_vars.go,deadline_rule_service.go,rule_editor_service.go,submission_draft_service.go,submissions.gohandler. No code-flow change. - Update
internal/handlers/submissions.godoc comments. - Add a short
docs/glossary.mdentry (or extend an existing one) for "procedural event" / "Verfahrensschritt" — single source of truth for the term. - Tests: rename strings in existing test fixtures + add a regression test that the variable bag emits both the legacy
rule.Xand the canonicalprocedural_event.Xkeys with the same value. (Critical — without this test, a future commit could drop the legacy alias and silently break user templates.) - Manual smoke: open the admin rule editor, confirm the new title appears. Open the submission-draft editor, confirm both
{{rule.X}}and{{procedural_event.X}}placeholders are listed (with canonical first). Generate a.docxfrom a project using each placeholder name — both render identically.
Risk: very low. No DB change, no API change, fully reversible.
No hours estimate per project CLAUDE.md.
§8.2 Slice B (separate mai task — designed here, hired later)
Scope: structural rework per §4 + §5.
Mechanics: Phase 1 → Phase 4 per §5.
Prerequisite: m greenlights via a new mai task with this doc + §11's open items addressed. Not part of Slice A.
Sub-slices (suggested for Slice B's own task):
- B.0 — Re-validate this doc's premises against live DB (numbers shift over weeks).
- B.1 — Phase 1 additive migration + backfill (mig 124).
- B.2 — Phase 2 dual-write + read-fallback.
- B.3 — Phase 3 read cutover (no schema change).
- B.4 — Phase 4 destructive drop (downtime window).
- B.5 — Rename Go types
DeadlineRule→SequencingRule+ProceduralEvent; rename JSON API envelope keys with a deprecation header. Independent of B.4. - B.6 — Rename admin URL paths
/admin/rules→/admin/procedural-eventswith redirects. Optional / low-priority.
§8.3 Why splitting is the right call
The conflation is real, but the fix for the most-painful surface (the editor sidebar) is independent of the table restructure. Splitting lets m ship the fix this week, see whether the prose change alone resolves enough of the cognitive friction, and then decide whether the structural rework is still worth the migration cost. If after Slice A m says "this reads fine now, B isn't worth it", that's a legitimate outcome — Slice B is a good refactor, not an urgent one.
§9 Risk assessment
§9.1 Slice A risks
| Risk | Likelihood | Severity | Mitigation |
|---|---|---|---|
Lawyer's existing Word template has {{rule.submission_code}} baked in; a future commit drops the legacy alias and breaks templates. |
Low (Slice A keeps the alias) | High if it happens | Regression test (§8.1 step 8) asserts both keys emit. Add an audit-log line on every variable-bag call recording which keys were consumed by the merge engine — gives a 30-day window of evidence before we'd consider deprecating the legacy keys. |
| i18n key rename misses a binding, leaving an English string visible to a DE user. | Medium | Low | The build pipeline (bun test / bun build) fails on missing i18n keys in i18n-keys.ts. Add the new keys to the type union; leave the old keys in the union with @deprecated JSDoc. |
| Renamed admin page heading confuses returning admin users ("Where did 'Regeln verwalten' go?"). | Medium | Low | One-time changelog entry; the URL /admin/rules is unchanged so muscle memory still lands them on the page. Internal users only (whitelist-gated). |
| Slice A reads as "we're done" and Slice B never ships. | Medium | Medium (the model stays wrong) | This doc files the Slice B design as a separate task entry before Slice A merges, so the to-do is visible. m's call whether to schedule it. |
§9.2 Slice B risks (deferred; recorded for the future task)
| Risk | Mitigation |
|---|---|
Backfill collapses too eagerly: 10 multi-row submission_codes today are _archived_litigation.* — confirm they should collapse into one procedural event with 2-5 sequencing variants, vs. each row becoming its own procedural event. |
The _archived_litigation.* codes are archived per their prefix — collapse is safe. Decision-flag for Slice B's own design pass. |
deadline_concepts linkage (125 of 254 rules link to a concept) — does the concept attach to the procedural event or the sequencing rule? §4.2 says procedural event; verify this is right when re-validating premises in B.0. |
Read-path audit: every consumer that joins deadline_rules.concept_id (rule_editor, projection, fristenrechner) operates on the rule-level today. Reconfirm none of them depend on per-jurisdiction concept-attachment. |
| The dual-write window introduces drift if a write hits one side and fails on the other. | Atomicity via single transaction per write in RuleEditorService. Daily drift-check job (one SELECT pair, alert if mismatched). |
paliad.deadlines.rule_id (1 live row, but more in future) — backfilling procedural_event_id + sequencing_rule_id must not orphan the live row. |
The 1 live row joins cleanly. Backfill in the same migration that adds the new columns. |
The submission-draft submission_code text key — what if two procedural_events.code values collide post-rename (e.g. a draft was saved against a code that we then archive)? |
Slice B Phase 1 enforces procedural_events.code UNIQUE; the backfill verifies no collision on the existing 158 distinct values. Drafts with codes that no longer exist as published procedural events are handled by the existing submission_drafts.submission_code text fallback (no FK enforcement). |
Slice B's API-key rename (submission_code → code in JSON) breaks external integrations. |
None exist today (paliad is internal-only); add a one-Slice deprecation header (X-Deprecated-Field: submission_code) before flipping. |
Coordination risk with future fristen/calculator work. The Fristenrechner calculator reads deadline_rules directly today. Slice B Phase 2's read-fallback handles this, but a parallel calculator feature in flight could land changes that need re-merging. |
B.0's job: confirm no in-flight task touches deadline_rules table shape before scheduling. |
§9.3 What rolls Slice A back
git revert <slice-a-commit> + reload. Zero data side-effects (no DB writes). 30 seconds.
§9.4 What rolls Slice B back
Per phase — Phases 1-3 reversible via reverting code + DROP TABLE. Phase 4 reversible only by restoring deadline_rules from the _pre_<n> snapshot taken at the start of Phase 4. Same posture as mig 091 — m's call when to commit to this point.
§10 Out of scope
- Renaming
paliad.events(the audit feed). Distinct table, distinct concept. The umbrella-term lock (§2) deliberately uses "procedural event" not "event" to avoid colliding with it. - Renaming
paliad.deadline_conceptsto align with the procedural-event taxonomy. The concept layer is the cross-proceeding semantic bridge (einstein 2026-05-08 Q5); the relationship "procedural event has-a concept" already reads cleanly under the new term. - Per-jurisdiction variations of the same procedural event (issue body's explicit out-of-scope). The 10 multi-row codes in the corpus today stay multi-row.
- Multi-tenant / cross-firm sharing of procedural events — paliad is single-tenant per deploy via
FIRM_NAME; cross-firm is a separate design. - einstein's
proceeding_event_edgesgraph proposal. That design proposed a graph of typed event-types connected by typed edges. This design's procedural-events / sequencing-rules split is compatible with that graph shape (the edges would attach to procedural-event-IDs rather than sequencing-rule-IDs), but the graph layer is a Slice C, not Slice B. Flagged for future continuity, not part of either slice here. - Renaming Go type
DeadlineRuletoSequencingRuleorProceduralEventin Slice A. Slice A is prose; Slice B's B.5 sub-slice handles the type rename. Coupling them costs the reversibility property. - API-envelope key renames (
submission_code→code,event_type→event_kindon the wire). Slice B only. - URL path renames (
/admin/rules→/admin/procedural-events). Slice B.6, optional. - Touching
paliad.trigger_eventsbeyond keeping the FK path open (todaydeadline_rules.trigger_event_id; Slice B maps tosequencing_rules.trigger_event_id). - Touching
paliad.event_categories/ Pathway-B navigation. Independent layer.
§11 Open questions for m (escalated via mai instruct head per project CLAUDE.md)
Per project CLAUDE.md "Head answers questions — NO AskUserQuestion" rule, these are surfaced to head, not picked-as-chip with the user.
| ID | Question | Inventor recommendation | Material to head? |
|---|---|---|---|
| Q1 | Scope: cosmetic-only (A) · full restructure (B) · cosmetic now + B as planned follow-up (C). | (R) = C | Yes — material. Defines whether Slice B is hired today or filed as a future task. |
| Q2 | Umbrella term: "procedural event" (DE: Verfahrensschritt) · "submission" (filings only) · "Verfahrensereignis" · other. | (R) = procedural event / Verfahrensschritt | Yes — material. The term ripples through every label in §7. Inventor's pick is the canonical choice; head can override with a single message. |
| Q3 | Slice B migration shape: confirmed (§4 + §5) or rescope. | (R) = §4 + §5 as written, decision deferred until Slice B is hired | No — informational. Locked when Slice B's own design pass runs. |
| Q4 | Effect on Schriftsätze surface: filter procedural_events.event_kind IN ('filing', 'reply') is acceptable replacement for today's event_type='filing'. |
(R) = yes, semantically equivalent under Slice B; no behavior change to lawyer. | No — informational. |
| Q5 | Are the 10 archived multi-row submission_codes (_archived_litigation.*) safe to collapse into single procedural events with multiple sequencing variants in Slice B? |
(R) = yes, prefix indicates archival; collapse-safe. | No — informational, defers to Slice B. |
| Q6 | concept_id attaches to procedural event, not sequencing rule. Confirmable? |
(R) = yes, per §4.2 (one concept per identity, not per jurisdiction). | No — informational, defers to Slice B. |
| Q7 | Keep the legacy {{rule.X}} placeholder aliases forever, or set a deprecation horizon (e.g. 1 year)? |
(R) = forever, with @deprecated annotation in the catalog. Removing them risks breaking lawyer-authored templates that paliad doesn't see. |
Yes — material to Slice A's contract (test in §8.1 step 8 asserts both keys emit). |
| Q8 | Document side: update m/paliad#93 issue body to fix the deadlines.deadline_rule_id → deadlines.rule_id typo (§1 last paragraph). |
(R) = yes, head's call when to edit. | No — informational, doc hygiene. |
| Q9 | After Slice A ships, do we file Slice B as a new mai task now (so it's visible), or wait for m to ask? | (R) = file now, status:planning, no owner. Visibility >> deferred surprise. | Yes — material to "does the model stay wrong forever". |
Q1, Q2, Q7, Q9 are the four head needs to answer before the coder shift. Q3-Q6, Q8 defer cleanly.
§12 Appendix — verbatim m quote
From m's report 2026-05-25 15:02 (paliad#93 body):
This shows how our 'rule' table system may need a revision?! It feels like we are rule based not submission based. But here we have a specific submission that is connected to a rule (as in: legal norm). And of course also connected to other 'procedural events' (which is a good term for it all) by rules how they are sequenced. But it makes it sound weird in the fields...
The design above takes m's three-way split — the procedural event / the legal norm / the rule by which they are sequenced — at face value and turns it into a column-level map (§4.2), a slice plan (§8), and a deprecation contract (§9.1).
End of design.