Files
paliad/docs/design-procedural-events-model-2026-05-25.md
mAi 5bb6df69e1 docs: t-paliad-262 — procedural-events data-model design (inventor)
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.
2026-05-25 15:44:35 +02:00

53 KiB
Raw Blame History

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) — proposed proceeding_event_types + proceeding_event_edges; the graph-shape recommendation has not been built (no proceeding_event* tables exist in the live DB as of 2026-05-25, verified via information_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.md and docs/design-submission-page-2026-05-22.md (Slice 1 → Slice A of the Schriftsätze stack — shipped on top of today's deadline_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:

  1. The procedural-event templatesubmission_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.
  2. The legal-norm citationlegal_source, rule_code, alt_rule_code, rule_codes[]. This is "the source-of-law anchor": § 102 PatG, UPC RoP R.220(1).
  3. The sequencing ruleparent_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-364rule.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.go template fallback.
  • "Show me every sequencing rule that triggers a given procedural event across all proceedings." Today: requires joining deadline_rules to itself on submission_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.deadlines row,
  • 4 live submission_drafts rows,
  • 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:

  1. 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 revert of the Slice A commit.
  2. 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.
  3. The Schriftsätze surface doesn't care which slice we ship — Slice A leaves it on event_type='filing'; Slice B flips it to event_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_codeadmin.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/rules stays — 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 column rule_id.
  • Go struct names: DeadlineRule stays. The renames here are prose, not code. Renaming DeadlineRule to ProceduralEvent couples Slice A to Slice B's table rename — keep them decoupled.
  • JSON envelope keys on the wire (POST /api/admin/rules/:id still accepts submission_code in 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/submissions etc.).
  • paliad.deadlines.rule_id FK column name.
  • The variable-bag's legacy {{rule.X}} keys — kept forever as aliases (cheap, zero rot).
  • The submission_drafts table's submission_code text 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 to event_kind and 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)

  1. Create procedural_events, sequencing_rules, legal_sources.
  2. Backfill legal_sources from DISTINCT legal_source on deadline_rules (70 rows). Populate pretty_de/pretty_en by calling the existing legalSourcePretty() function in a one-shot SQL/Go shim during the migration. Verify COUNT(DISTINCT legal_source FROM deadline_rules) = COUNT(*) FROM legal_sources.
  3. Backfill procedural_events from DISTINCT submission_code on deadline_rules WHERE submission_code IS NOT NULL. Take name, name_en, event_type → event_kind, primary_party, concept_id, description from the lowest-id rule row for each code (tie-breaker: lowest sequence_order). Verify COUNT(*) FROM procedural_events = COUNT(DISTINCT submission_code FROM deadline_rules WHERE submission_code IS NOT NULL) (= 158).
  4. Backfill sequencing_rules 1:1 from deadline_rules (254 rows). FK procedural_event_id resolved by code lookup; sequencing-rule row inherits the deadline_rules.id (so existing deadlines.rule_id FKs continue to resolve via the new column for the dual-write window — see Phase 3).
  5. Add paliad.deadlines.procedural_event_id + sequencing_rule_id columns, backfill from deadlines.rule_id join.
  6. Add paliad.submission_drafts.procedural_event_id, backfill from submission_code join.

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)

  1. Update RuleEditorService to write to both deadline_rules (legacy) and (procedural_events, sequencing_rules, legal_sources) on every Create/Update/Publish/Archive. Audit log writes one row per side.
  2. Update read paths to read from the new tables, falling back to deadline_rules if the new row is missing (defense-in-depth during backfill catch-up).
  3. 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)

  1. Flip read paths to only the new tables (SubmissionVarsService.loadPublishedRule, DeadlineRuleService.*, SubmissionService.list, ProjectionService, FristenrechnerCalc, etc.).
  2. Stop writing to deadline_rules.
  3. paliad.deadlines.rule_id is kept as a no-op alias for one more week; new writes go to procedural_event_id + sequencing_rule_id.
  4. submission_drafts.submission_code is kept as the URL anchor; the FK procedural_event_id is the primary join key going forward.

§5.4 Phase 4 — Drop legacy (downtime window, destructive)

  1. paliad.deadline_rules_pre_<slice-B-mig> snapshot of the entire table.
  2. DROP TABLE paliad.deadline_rules (after CASCADE-safe FK rewires).
  3. DROP COLUMN paliad.deadlines.rule_id (keep rule_code + custom_rule_text as 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:

  • RuleEditorService splits into ProceduralEventService + SequencingRuleService + LegalSourceService. The Save / Publish / Archive flow on the editor coordinates all three.
  • DeadlineRuleService.GetByID becomes SequencingRuleService.GetByID; the submission_code lookup moves to ProceduralEventService.GetByCode.
  • SubmissionVarsService.loadPublishedRule becomes loadPublishedProceduralEvent and returns a triple (event, defaultSequencingRule, legalSource); the variable-bag emission consumes all three.
  • ProjectionService and the Fristenrechner calculator read from sequencing_rules (same column set, same logic — only the table name changes).
  • SubmissionService.list (handlers/submissions.go) filters procedural_events.event_kind IN ('filing', 'reply').
  • Backfill orphans + audit triggers (mig 079 / 089) are re-pointed at sequencing_rules + a new procedural_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:

  1. Add 8 new placeholder keys to the variable bag in submission_vars.go (1:1 with the existing 8 rule.* keys). Keep the legacy keys.
  2. Update frontend/src/client/submission-draft.ts placeholder catalog labels.
  3. Rebind admin i18n keys per §7.1 (with legacy keys retained).
  4. Update admin page titles + section headings.
  5. Update Go struct comments + service docstrings in submission_vars.go, deadline_rule_service.go, rule_editor_service.go, submission_draft_service.go, submissions.go handler. No code-flow change.
  6. Update internal/handlers/submissions.go doc comments.
  7. Add a short docs/glossary.md entry (or extend an existing one) for "procedural event" / "Verfahrensschritt" — single source of truth for the term.
  8. Tests: rename strings in existing test fixtures + add a regression test that the variable bag emits both the legacy rule.X and the canonical procedural_event.X keys with the same value. (Critical — without this test, a future commit could drop the legacy alias and silently break user templates.)
  9. 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 .docx from 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 DeadlineRuleSequencingRule + ProceduralEvent; rename JSON API envelope keys with a deprecation header. Independent of B.4.
  • B.6 — Rename admin URL paths /admin/rules/admin/procedural-events with 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_codecode 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_concepts to 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_edges graph 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 DeadlineRule to SequencingRule or ProceduralEvent in 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_codecode, event_typeevent_kind on the wire). Slice B only.
  • URL path renames (/admin/rules/admin/procedural-events). Slice B.6, optional.
  • Touching paliad.trigger_events beyond keeping the FK path open (today deadline_rules.trigger_event_id; Slice B maps to sequencing_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_iddeadlines.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.