Merge: t-paliad-273 — B.0 procedural-events design doc + live-DB re-validation findings (m/paliad#93)
This commit is contained in:
277
docs/design-procedural-events-b0-findings-2026-05-26.md
Normal file
277
docs/design-procedural-events-b0-findings-2026-05-26.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# Slice B.0 — Live DB re-validation findings (t-paliad-273)
|
||||||
|
|
||||||
|
**Author:** curie (researcher)
|
||||||
|
**Date:** 2026-05-26
|
||||||
|
**Branch:** `mai/curie/researcher-slice-b-zero`
|
||||||
|
**Predecessor:** `docs/design-procedural-events-model-2026-05-25.md` (cronus, t-paliad-262)
|
||||||
|
**Scope:** READ-ONLY re-validation of the design doc's §1 premises against the live youpc Supabase `paliad` schema. No migration SQL written, no writes to `deadline_rules` or any table. B.1 (additive migration) remains blocked pending m's greenlight.
|
||||||
|
|
||||||
|
This document does **not** redesign the schema. It does **not** propose new structural changes. It records what the live DB looks like ~24 hours after the design was authored, flags every claim that drifted, and gives the eventual B.1 coder a current-as-of-2026-05-26 baseline to plan against.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0 TL;DR
|
||||||
|
|
||||||
|
The design doc's §1 premises were sound on 2026-05-25. **All numeric premises drifted in the 24 hours since.** The qualitative model (`deadline_rules` conflates three concepts; live `deadlines.rule_id` FK; snapshot precedent established; no `proceeding_event*` tables) still holds.
|
||||||
|
|
||||||
|
The Q5 default ("10 archived multi-row submission_codes collapse safely") is now **moot**: those rows were removed from the live DB between 2026-05-25 15:30 and 2026-05-26 13:30. There are now **zero** multi-row submission codes; every active submission_code maps 1:1 to one rule row. B.1 backfill no longer needs the multi-row collapse logic that §5 of the design doc anticipated.
|
||||||
|
|
||||||
|
The Q6 default ("concept_id attaches to procedural event, not sequencing rule") is **directionally correct but needs refinement**. The empirical attachment is **above** the procedural-event level — `deadline_concepts` rows cluster legal meaning *across* jurisdictional procedural-event variants. One concept_id can span 15 distinct submission_codes (e.g. "Berufungsfrist" across BGH / BPatG / LG / OLG for both PatG and ZPO paths). The FK in §4.1's draft schema (`procedural_events.concept_id REFERENCES deadline_concepts(id)`, N:1) is **already correctly shaped** for this — no schema change needed. The verbal claim in the design doc should be tightened to "one `deadline_concept` row may be referenced by many procedural events; the FK lives on `procedural_events`."
|
||||||
|
|
||||||
|
Migration tracker drift: the design's "next available mig = 124" is stale; live head is 133 (`upc_dmgs_pi_court_followup`, 2026-05-25 15:27 — applied **after** the design was written). **Next available is 134.** Ten migrations landed since the doc was authored — 124..133. None of them touched `deadline_rules` schema, but they did mutate row content (the missing 23 rows and the new event_type/legal_source distribution come from migs 127/128/132/133).
|
||||||
|
|
||||||
|
The design's claimed migration tracker `paliad.paliad_schema_migrations` is the legacy golang-migrate v1 native counter (stuck at v106). The **canonical** tracker is `paliad.applied_migrations` (one row per applied migration, with checksum + applied_at). `internal/db/migrate.go:9-21` is the source of truth. Project CLAUDE.md still says `paliad.paliad_schema_migrations`; that's a stale doc, not a B.0-scope fix.
|
||||||
|
|
||||||
|
One doc-side bug fixed by this slice: design doc §1 + m/paliad#93 issue body referenced `paliad.deadlines.deadline_rule_id`. Live column is `paliad.deadlines.rule_id`. Both files patched on this branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 Headline-count drift table
|
||||||
|
|
||||||
|
All numbers taken 2026-05-26 ~13:30 UTC against the live `paliad` schema.
|
||||||
|
|
||||||
|
| Metric | Design (2026-05-25) | Live (2026-05-26) | Δ | Notes |
|
||||||
|
|---|--:|--:|--:|---|
|
||||||
|
| `deadline_rules` row count | 254 | **231** | -23 | All rows `is_active = true`. No soft-deletes in flight. |
|
||||||
|
| Rows with `submission_code` | 177 | **153** | -24 | |
|
||||||
|
| Distinct `submission_code` values | 158 | **153** | -5 | **All 5 lost are the multi-row `_archived_litigation.*` codes** — see §2. |
|
||||||
|
| Rows with `legal_source` | 102 | **112** | +10 | |
|
||||||
|
| Distinct `legal_source` values | 70 | **87** | +17 | New jurisdictional variants seeded by recent migs (127/132/133). |
|
||||||
|
| Rows with `concept_id` (linked to `deadline_concepts`) | 125 | **129** | +4 | 56% of the corpus is concept-linked, vs 49% in the design. |
|
||||||
|
| `paliad.deadlines` rows | 1 | **5** | +4 | Still tiny — destructive cutover stays cheap. |
|
||||||
|
| `paliad.submission_drafts` rows | 4 | **7** | +3 | |
|
||||||
|
| Rules in `lifecycle_state = 'draft'` | 4 | **0** | -4 | All 4 design-era drafts were published or discarded. |
|
||||||
|
|
||||||
|
### event_type distribution
|
||||||
|
|
||||||
|
| `event_type` | Design | Live | Δ |
|
||||||
|
|---|--:|--:|--:|
|
||||||
|
| `filing` | 130 | 105 | -25 |
|
||||||
|
| NULL | 77 | 89 | +12 |
|
||||||
|
| `decision` | 25 | 21 | -4 |
|
||||||
|
| `hearing` | 21 | 15 | -6 |
|
||||||
|
| `order` | 1 | 1 | 0 |
|
||||||
|
| **Total** | **254** | **231** | -23 |
|
||||||
|
|
||||||
|
The -23 row delta lands almost entirely in `filing` (-25) and `hearing` (-6), offset by +12 NULL — consistent with the disappearance of the `_archived_litigation.*` filings and a few archived `hearing` rows, plus seeding of new structural / parent-only rows by recent migrations.
|
||||||
|
|
||||||
|
### What did NOT drift (qualitative claims, still valid)
|
||||||
|
|
||||||
|
- `paliad.deadline_rules` carries 39 columns (design said 38 — drift +1; likely from mig 128 `deadline_rules_unit_check` which adds a CHECK without adding a column — or one of migs 124-133 added a column. Not investigated further; out of B.0 scope).
|
||||||
|
- `paliad.deadlines.rule_id` (uuid, nullable) is the FK column to `paliad.deadline_rules.id`. **Confirmed via `information_schema.referential_constraints`** — `rule_id → paliad.deadline_rules(id)`. The doc-side mention of `deadline_rule_id` was always a typo.
|
||||||
|
- `paliad.deadlines.rule_code` + `paliad.deadlines.custom_rule_text` both still present (the denormalized-display columns from mig 122).
|
||||||
|
- `paliad.submission_drafts` uses `(project_id uuid nullable, submission_code text NOT NULL)` as its key — **no FK to deadline_rules**. Confirms the design's claim that the Schriftsätze surface filters on a text key, not on `deadline_rules.id`.
|
||||||
|
- No `paliad.proceeding_event*` tables exist (einstein's 2026-05-08 graph design was never built — still the case).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 Archived submission_code audit (Q5 re-confirm)
|
||||||
|
|
||||||
|
**Premise re-checked:** "10 archived multi-row submission_codes (`_archived_litigation.*`) collapse safely into single procedural events with multiple sequencing variants."
|
||||||
|
|
||||||
|
**Finding:** the premise is **moot in the live DB**.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT submission_code, COUNT(*)
|
||||||
|
FROM paliad.deadline_rules
|
||||||
|
WHERE submission_code LIKE '_archived_litigation.%'
|
||||||
|
GROUP BY submission_code;
|
||||||
|
-- 0 rows
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT submission_code, COUNT(*)
|
||||||
|
FROM paliad.deadline_rules
|
||||||
|
WHERE submission_code IS NOT NULL
|
||||||
|
GROUP BY submission_code
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
-- 0 rows
|
||||||
|
```
|
||||||
|
|
||||||
|
Every active submission_code in the live corpus is 1:1 with its `deadline_rules` row. The 10 multi-row codes the design anticipated no longer exist.
|
||||||
|
|
||||||
|
**Consequence for B.1 backfill:**
|
||||||
|
|
||||||
|
- The §5.1 / §5.2 backfill SQL the design sketched (collapsing N rows-with-same-submission_code into 1 procedural_event + N sequencing_rules) is **simpler than expected**: a straight 1:1 backfill, no GROUP-BY-and-collapse step needed.
|
||||||
|
- B.1's `INSERT INTO paliad.procedural_events ... SELECT DISTINCT submission_code ...` becomes equivalent to `INSERT ... SELECT submission_code, ... FROM deadline_rules WHERE submission_code IS NOT NULL`. No deduplication needed.
|
||||||
|
- The 78 rows where `submission_code IS NULL` (231 - 153) still need a B.1 decision: do they become `procedural_events` rows (with synthetic codes), do they become free-standing `sequencing_rules` with `procedural_event_id` NULL, or do they get parked? This was implicit in the design (the 77 NULLs were framed as "structural / parent-only rows in the proceeding tree"); B.1 should make the decision explicit and document it in the migration's `.up.sql` comments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 concept_id attachment shape (Q6 re-confirm)
|
||||||
|
|
||||||
|
**Premise re-checked:** "concept_id attaches to procedural event, not sequencing rule."
|
||||||
|
|
||||||
|
**Finding:** **partly true.** The FK direction the design proposes (`procedural_events.concept_id → deadline_concepts.id`, N:1) is correct. The verbal phrasing in Q6's default needs refinement — the empirical attachment is **above** the procedural-event level, not "at" it.
|
||||||
|
|
||||||
|
### Empirical pattern
|
||||||
|
|
||||||
|
129 of 231 rows carry a `concept_id`. Those 129 rows reference **53 distinct `deadline_concepts`** rows. Averages: 2.43 rows-per-concept, 2.42 submission-codes-per-concept (the two are nearly identical because today's corpus has no multi-row submission codes — see §2). Span distribution:
|
||||||
|
|
||||||
|
- 33 of 53 concepts (62%) attach to exactly 1 submission_code → procedural-event-scoped.
|
||||||
|
- 20 of 53 concepts (38%) attach to >1 submission_code → cross-procedural-event scoped.
|
||||||
|
- Maximum: 1 concept attaches to **15 distinct submission_codes**.
|
||||||
|
|
||||||
|
### Example: one concept, four procedural events
|
||||||
|
|
||||||
|
The concept `b85b2e5a-4064-40b2-b862-24b7abaa5b94` ("Berufungsfrist / Berufungsschrift") is referenced by 4 `deadline_rules` rows that today carry these 4 distinct submission_codes:
|
||||||
|
|
||||||
|
| rule_code | submission_code | court | name |
|
||||||
|
|---|---|---|---|
|
||||||
|
| § 110 PatG | `de.null.bgh.berufung` | BGH | Berufungsschrift |
|
||||||
|
| § 110 PatG | `de.null.bpatg.berufung` | BPatG | Berufungsfrist |
|
||||||
|
| § 517 ZPO | `de.inf.lg.berufung` | LG | Berufungsfrist |
|
||||||
|
| § 517 ZPO | `de.inf.olg.berufung` | OLG | Berufungsfrist |
|
||||||
|
|
||||||
|
Under Slice B's target schema (§4.1), each of these four rows becomes a separate `procedural_events` row (different `code`s, different jurisdiction-specific names, different `legal_source_id`s), but **all four reference the same `deadline_concepts.id`**.
|
||||||
|
|
||||||
|
### Implication for B.1
|
||||||
|
|
||||||
|
- `procedural_events.concept_id` should be **nullable** (62% of rows today have no concept link — the §4.1 sketch already allows this).
|
||||||
|
- The constraint must be **N:1, not 1:1** (one `deadline_concept` may be referenced by many `procedural_events`). The §4.1 sketch (`concept_id uuid REFERENCES paliad.deadline_concepts(id)`) is already correctly N:1; a hypothetical "UNIQUE INDEX on `procedural_events.concept_id`" would break the existing data. **Do not add UNIQUE.**
|
||||||
|
- The design doc's Q6 phrasing can be tightened to: "concept_id attaches to procedural event (N procedural events → 1 concept). Sequencing rules do not carry concept_id." — but this is a wording nit, not a structural change. It does **not** block B.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 Snapshot precedent audit
|
||||||
|
|
||||||
|
**Premise re-checked:** the `paliad.deadline_rules_pre_<N>` snapshot pattern is established and ready for B.4's destructive drop.
|
||||||
|
|
||||||
|
**Finding:** confirmed and consistent.
|
||||||
|
|
||||||
|
Snapshot tables in `paliad`:
|
||||||
|
|
||||||
|
| Snapshot table | Origin migration |
|
||||||
|
|---|---|
|
||||||
|
| `deadlines_pre_089` | mig 089 |
|
||||||
|
| `deadline_rules_pre_091` | mig 091 (destructive drop of legacy columns) |
|
||||||
|
| `event_deadlines_pre_092` | mig 092 |
|
||||||
|
| `event_deadline_rule_codes_pre_092` | mig 092 |
|
||||||
|
| `deadline_rules_pre_093` | mig 093 |
|
||||||
|
| `proceeding_types_pre_093` | mig 093 |
|
||||||
|
| `projects_pre_094` | mig 094 |
|
||||||
|
| `deadline_rules_pre_095` | mig 095 |
|
||||||
|
| `proceeding_types_pre_096` | mig 096 |
|
||||||
|
| `deadline_rules_pre_098` | mig 098 |
|
||||||
|
|
||||||
|
Pattern: `<original_table>_pre_<migration_number>`. Always created in the `.up.sql` of the destructive migration as `CREATE TABLE paliad.<t>_pre_<N> AS TABLE paliad.<t>;` (followed by the destructive DROP / ALTER).
|
||||||
|
|
||||||
|
**B.4's template:** before `DROP TABLE paliad.deadline_rules;` (and `ALTER TABLE paliad.deadlines DROP COLUMN rule_id;`), `mig <N>.up.sql` must include:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE paliad.deadline_rules_pre_<N> AS TABLE paliad.deadline_rules;
|
||||||
|
-- (optional) CREATE TABLE paliad.deadlines_pre_<N> AS TABLE paliad.deadlines;
|
||||||
|
```
|
||||||
|
|
||||||
|
This is non-negotiable per m's snapshot policy and the precedent of migs 089-098. B.4 should not enter the deploy queue without it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 deadlines.rule_id doc bug — verified + patched
|
||||||
|
|
||||||
|
**Premise re-checked:** the live column on `paliad.deadlines` referencing `deadline_rules` is named `rule_id`, not `deadline_rule_id`.
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_schema='paliad' AND table_name='deadlines' AND column_name LIKE '%rule%';
|
||||||
|
-- rule_id (uuid, nullable)
|
||||||
|
-- rule_code (text, nullable)
|
||||||
|
-- custom_rule_text (text, nullable)
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT kcu.column_name, ccu.table_name, ccu.column_name
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu ON ...
|
||||||
|
JOIN information_schema.constraint_column_usage ccu ON ...
|
||||||
|
WHERE tc.constraint_type='FOREIGN KEY' AND tc.table_schema='paliad' AND tc.table_name='deadlines';
|
||||||
|
-- rule_id → paliad.deadline_rules.id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix applied on this branch:**
|
||||||
|
|
||||||
|
- `docs/design-procedural-events-model-2026-05-25.md` — §1 row 51 already says "the column is `rule_id` (issue body called it `deadlines.deadline_rule_id` — that's a doc-side typo)". §1 row 63 (the "Doc-side bug flagged" line) already names the fix target. **No change needed to the design doc — the inventor already flagged and described the bug; B.0 just re-confirms it.**
|
||||||
|
- `m/paliad#93` issue body — line 56 says `paliad.deadlines.deadline_rule_id` in the Q3 migration shape. Patched via Gitea API on this slice. See §6 of this report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6 Migration tracker drift (out-of-scope context)
|
||||||
|
|
||||||
|
The design doc said "next available mig number is 124 (mig 123 = Backup Mode Slice A, just shipped)". Live state on 2026-05-26 13:30:
|
||||||
|
|
||||||
|
- Latest applied migration: **133** (`upc_dmgs_pi_court_followup`, 2026-05-25 15:27).
|
||||||
|
- Next available: **134**.
|
||||||
|
- Migrations 124-133 (all applied after the design was authored):
|
||||||
|
|
||||||
|
```
|
||||||
|
124 de_inf_lg_replik_duplik_sequencing (2026-05-25 13:49)
|
||||||
|
125 cross_cutting_filter_legal_source (2026-05-25 14:13)
|
||||||
|
126 users_inbox_seen_at (2026-05-25 13:51)
|
||||||
|
127 wave0_tier0_deadline_fixes (2026-05-25 14:13)
|
||||||
|
128 deadline_rules_unit_check (2026-05-25 14:13)
|
||||||
|
129 project_event_choices (2026-05-25 15:02)
|
||||||
|
130 submission_drafts_language (2026-05-25 15:05)
|
||||||
|
131 submission_drafts_party_selection (2026-05-25 15:02)
|
||||||
|
132 wave1_tier1_rule_additions (2026-05-25 15:40)
|
||||||
|
133 upc_dmgs_pi_court_followup (2026-05-25 15:27)
|
||||||
|
```
|
||||||
|
|
||||||
|
These touched `deadline_rules` content (wave0/wave1 rule additions, sequencing fixes, unit checks) and adjacent tables, but did not change the conflated-three-concepts shape that motivates Slice B. The structural premise of the design holds; the row-level numbers shifted.
|
||||||
|
|
||||||
|
**Side observation (not a B.0 fix scope):** the project's `CLAUDE.md` says "Migration tracker is `paliad.paliad_schema_migrations` (avoids collision with other apps on the shared `public.schema_migrations`)." That sentence is stale. The **canonical tracker is `paliad.applied_migrations`** (per `internal/db/migrate.go:9-21,53,105`). `paliad.paliad_schema_migrations` is the legacy golang-migrate v1 counter, frozen at v106; the migrate runner uses it only to bootstrap `applied_migrations` on first deploy of the new runner (`internal/db/migrate.go:219-240`). Recommend a separate doc-fix slice (out of B.0 scope) to update `.claude/CLAUDE.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7 Updated B.1 brief (no-op / minor adjustments only)
|
||||||
|
|
||||||
|
What the live data means for the design's §5 migration plan:
|
||||||
|
|
||||||
|
1. **Backfill is simpler.** No multi-row collapse logic needed (§2). One-to-one `INSERT INTO paliad.procedural_events SELECT submission_code, name, name_en, description, event_type AS event_kind, primary_party, ... FROM paliad.deadline_rules WHERE submission_code IS NOT NULL` against 153 rows.
|
||||||
|
2. **The 78 NULL-submission_code rows need an explicit decision in B.1.** Either:
|
||||||
|
- (a) Skip them — they remain `deadline_rules`-only and become orphan-once-deadline_rules-is-dropped. Not acceptable; B.4 would lose them.
|
||||||
|
- (b) Mint synthetic codes (`null.<uuid8>` or similar) for the structural rows and create `procedural_events` for them.
|
||||||
|
- (c) Treat them as "sequencing-rule-only" (a `sequencing_rules` row with NULL `procedural_event_id`) — would require `sequencing_rules.procedural_event_id` to be nullable, which contradicts §4.1's NOT NULL FK.
|
||||||
|
- Default recommendation: **(b)** — mint codes, preserve every row. B.1 must document the mint rule in the `.up.sql`. Surface this to head before scheduling B.1.
|
||||||
|
3. **concept_id stays N:1 on procedural_events.** No UNIQUE constraint. §4.1's sketch already does this; just don't accidentally tighten it.
|
||||||
|
4. **Use migration number 134** (or whatever's the live `MAX(version)+1` at B.1-write-time; re-check at the moment of writing the file).
|
||||||
|
5. **Snapshot before drop in B.4:** `CREATE TABLE paliad.deadline_rules_pre_<N> AS TABLE paliad.deadline_rules;` per §4 precedent. **This is the hard-stop pre-condition for B.4 entering the deploy queue.**
|
||||||
|
6. **Submission_drafts.submission_code → procedural_events.code text join** continues to work unchanged through B.1-B.3 because both names match. No B.5 dual-write needed for `submission_drafts`. (The design's §6.3 already noted this.)
|
||||||
|
|
||||||
|
None of these change the **shape** of the design — they tighten the backfill SQL and surface one explicit decision (point 2) for head.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8 Outputs of this slice (B.0)
|
||||||
|
|
||||||
|
| Artifact | Status |
|
||||||
|
|---|---|
|
||||||
|
| `docs/design-procedural-events-b0-findings-2026-05-26.md` (this file) | created on `mai/curie/researcher-slice-b-zero` |
|
||||||
|
| `docs/design-procedural-events-model-2026-05-25.md` | cherry-picked from `mai/cronus/inventor-procedural` onto this branch (design doc was never merged to main; B.0 brings it onto a branch off main so the doc bug fix has somewhere to land) |
|
||||||
|
| m/paliad#93 issue body — `deadline_rule_id` → `rule_id` correction | patched via Gitea API |
|
||||||
|
| Gitea comment on m/paliad#93 summarizing this report | posted (see §6 trailing summary on the issue) |
|
||||||
|
|
||||||
|
**Nothing migrated, nothing written to `paliad.deadline_rules` or any other live data table.** Only `mai.reports` (progress) and the GitHub issue body / repo files were touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §9 Hard-stop status
|
||||||
|
|
||||||
|
**B.0 COMPLETE. AWAITING B.1 GREENLIGHT.**
|
||||||
|
|
||||||
|
Per the original instruction:
|
||||||
|
|
||||||
|
- B.1 (additive migration creating `paliad.procedural_events`, `paliad.sequencing_rules`, `paliad.legal_sources` + backfill) requires explicit m approval before any new tables get created.
|
||||||
|
- B.4 (destructive drop of `paliad.deadline_rules` + `paliad.deadlines.rule_id`) requires m's downtime-window approval AND a `paliad.deadline_rules_pre_<N>` snapshot table in the same migration.
|
||||||
|
- This researcher (curie) stays parked until head re-hires.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §10 Decisions worth surfacing to m before B.1 starts
|
||||||
|
|
||||||
|
1. **NULL-submission_code rows (78 of them) — what to do during backfill?** Recommendation (b): mint synthetic codes. m should confirm or pick (a)/(c).
|
||||||
|
2. **B.5 deprecation header window length** — the design (§8.2) says "one slice". For 7 active submission_drafts that's safe; the question is whether external integrations (Word templates with `{{rule.X}}`) need a longer window. The variable-bag alias contract (`submission_vars.go`) covers Word templates without a wire-format change, so "one slice" is defensible. m should confirm.
|
||||||
|
3. **Migration number reservation** — by the time B.1 ships, the live head may be 135+. The B.1 coder must re-check `MAX(version)` at write-time. (Not a decision; just a process note.)
|
||||||
|
|
||||||
|
These are the only open questions the B.0 audit surfaced. Everything else in the design holds.
|
||||||
571
docs/design-procedural-events-model-2026-05-25.md
Normal file
571
docs/design-procedural-events-model-2026-05-25.md
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
# 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.
|
||||||
|
**B.0 re-validation:** see `docs/design-procedural-events-b0-findings-2026-05-26.md` (curie, 2026-05-26) for the live-DB premise re-check. Numeric §1 claims drifted; Q5 multi-row collapse premise is moot (no `_archived_litigation.*` rows remain); Q6 N:1 attachment confirmed; mig number target updated 124 → 134.
|
||||||
|
**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 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.
|
||||||
|
2. **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).
|
||||||
|
3. **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`, renamed directly from `paliad.deadlines.rule_id` — there is no intermediate `deadline_rule_id` step (no such column exists). Updating the issue body is m's call — flagged here so it doesn't propagate into a coder brief. *(B.0 update 2026-05-26: issue body patched. See `docs/design-procedural-events-b0-findings-2026-05-26.md` §5.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §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_code`s 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_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/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`)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 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()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 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()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 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()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
7. 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.
|
||||||
|
8. 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).
|
||||||
|
9. 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)
|
||||||
|
|
||||||
|
10. Flip read paths to **only** the new tables (`SubmissionVarsService.loadPublishedRule`, `DeadlineRuleService.*`, `SubmissionService.list`, `ProjectionService`, `FristenrechnerCalc`, etc.).
|
||||||
|
11. Stop writing to `deadline_rules`.
|
||||||
|
12. `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`.
|
||||||
|
13. `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)
|
||||||
|
|
||||||
|
14. `paliad.deadline_rules_pre_<slice-B-mig>` snapshot of the entire table.
|
||||||
|
15. DROP TABLE paliad.deadline_rules (after CASCADE-safe FK rewires).
|
||||||
|
16. 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 `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-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_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_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_code` → `code`, `event_type` → `event_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_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.*
|
||||||
Reference in New Issue
Block a user