Workflow shifted to hand-written numbered migrations; the audit-row SQL
export tool no longer has any consumers. Pure deletion — /admin/rules
and /admin/rules/{id}/edit stay; only the export-to-SQL flow goes.
Deleted:
- frontend/src/admin-rules-export.tsx
- frontend/src/client/admin-rules-export.ts
Removed:
- routes GET /admin/rules/export and GET /admin/api/rules/export-migrations
- handleAdminExportRuleMigrations + handleAdminRulesExportPage
- RuleEditorService.ExportMigrationsSince + ExportResult + sqlEscape helper
- build.ts entries (import, client bundle, dist HTML write)
- Sidebar "Regel-Migrations" nav item + "Migrations exportieren" button on /admin/rules
- all admin.rules.export.* + nav.admin.rules_export + admin.rules.list.export i18n keys (DE+EN)
- .admin-rules-export-* CSS rules (dead after page deletion)
Doc references in design-fristen-phase2-2026-05-15.md and
design-paliad-data-export-2026-05-19.md updated to mark the endpoint as
removed (acceptance #2 requires grep to return zero hits).
60 KiB
Design — Fristen logic Phase 2 (post-audit extensions, prioritized slicing)
Author: pauli (inventor)
Date: 2026-05-15
Task: t-paliad-181 (child of t-paliad-157, audit merged at 79f6be3)
Branch: mai/pauli/fristen-phase2-design (fresh from origin/main @ 79f6be3)
Status: DESIGN READY FOR REVIEW — head gates the design → Phase 3 transition (no m-gate; autonomy mandate from 2026-05-15 00:01).
Predecessor: docs/audit-fristen-logic-2026-05-13.md §9 + the lock-in summary in t-paliad-181's task description.
0. Premises verified live (drift check since the 2026-05-13 audit)
The audit's live findings were captured on 2026-05-13. Re-checked on 2026-05-15 against paliad schema on the youpc Supabase instance. Latest migration: 077 (audit was at 074).
0.1 Row-count drift since the audit
| Surface | 2026-05-13 audit | 2026-05-15 now | Δ |
|---|---|---|---|
paliad.deadline_rules |
172 | 172 | 0 |
paliad.proceeding_types (all) |
27 | 26 | -1 |
↪ category='fristenrechner' active |
20 | 19 | -1 |
↪ category='litigation' active |
7 | 7 | 0 |
paliad.deadline_concepts |
56 | 57 | +1 |
paliad.deadline_concept_event_types |
32 | 32 | 0 |
paliad.event_categories |
125 | 125 | 0 |
paliad.event_types |
~40 | 45 | +5 |
paliad.trigger_events |
110 | 110 | 0 |
paliad.event_deadlines |
77 | 77 | 0 |
paliad.deadlines (live) |
26 | 26 | 0 |
paliad.deadlines WHERE rule_id IS NOT NULL |
1 | 1 | 0 |
paliad.projects |
11 | 11 | 0 |
paliad.projects WHERE proceeding_type_id IS NOT NULL |
0 | 0 | 0 |
paliad.courts |
41 | 41 | 0 |
paliad.holidays |
55 | 55 | 0 |
Conclusion: every key finding from the audit holds. The single deactivated fristenrechner code is informational, not structural; the design treats is_active as the canonical liveness signal. Demand-side is still empty (11/11 NULL proceeding_type_id, 1/26 rule_id populated) — the migration cutover plan in §3 retains the same graceful-degrade contract.
0.2 New migrations 075–077 are SmartTimeline-related, not Fristen-logic
- 075
project_events_timeline_kind— t-paliad-171 SmartTimeline Slice 1. - 076
smart_timeline_slice_2— t-paliad-173 Slice 2 (projection meta). - 077
projects_counterclaim_of— t-paliad-174 Slice 3 (CCR sub-project FK onpaliad.projects).
None touch paliad.deadline_rules, paliad.trigger_events, paliad.event_deadlines, or paliad.deadline_concepts. Fristen-logic data model is unchanged. But §7 (instance level) intersects with paliad.projects so we account for the counterclaim_of neighbour when adding instance_level.
0.3 Audit doc is on main
Audit merged at 79f6be3 (docs/audit-fristen-logic-2026-05-13.md, 799 lines). All §6/§7 cross-references in this design point at the merged commit. Anchor as-of-2026-05-15 is stable.
0.4 Anchor files (re-confirmed)
Same as audit §0.5:
internal/services/fristenrechner.go(~735 LoC) — Pipeline A + B.internal/services/deadline_calculator.go— pure date math.internal/services/deadline_rule_service.go— read API +ConceptDefaultEventTypeIDhydration.internal/services/event_deadline_service.go(~300 LoC) — Pipeline C.internal/services/projection_service.go— SmartTimeline (Pipeline A consumer).internal/services/deadline_service.go—paliad.deadlinespersistence.internal/services/event_category_service.go— cascade → concept.internal/services/holidays.go+courts.go— non-working-day adjustment.
If anything in this design conflicts with the live state, the live state wins.
1. Vision
m's locked decisions on the audit's §9 (verbatim from t-paliad-181):
- Q1 — Pipeline reconciliation: reconcile and combine Pipeline A + C into ONE corpus. "Needs to be all the same basis."
- Q2 — Litigation vs fristenrechner: (b) soft-merge to fristenrechner-only on projects. "I dont even get 'litigation corpus'? If pauli leans b, go for b."
- Q3 — Mandatory/optional: 4-way enum (
mandatory/recommended/optional/informational). - Q5 — Rule-management UX: (C) full editor with audit log. Admin-only.
- Q6 — Condition grammar: move to
condition_expr jsonb(AND/OR/NOT). - Q11 — Pipeline A capability gaps: add
beforetiming,working_daysunit,combine_op(min/max). Absorbed into Q1. - Q14 — Phase-2 cadence: (b) pauli drafts the Phase 2 design.
Head's autonomous defaults on the unanswered 8:
- Q4 — Event-trigger endpoint: BUILD as part of the unification (
POST /api/tools/event-trigger). - Q7 — Cross-proceeding spawn: WIRE.
- Q8 — Orphan concept order:
wiedereinsetzung > schriftsatznachreichung > versaeumnisurteil-einspruch > weiterbehandlung > others. - Q9 — Instance-level on projects: ADD column.
- Q10 — Backfill
rule_idon legacy deadlines: YES (fuzzy-match one-off). - Q12 — Court-set as real column: PROMOTE.
- Q13 —
condition_rule_iddead column: DROP (replaced by Q6's jsonb grammar). - Q15 — Phase 3 framing: cover all locked Qs + defaults across slices.
1.1 Scope of this design
Produce a detailed Phase 3 implementation roadmap that operationalises all 15 picks above. The output is THIS document; Phase 3 = the slices in §10 executed by Sonnet coders (or pauli in pair-prog mode) under head's gates.
1.2 Explicitly out of scope for Phase 2
- No implementation, no migrations, no SQL. Design only.
- No UI design for the rule editor's individual form fields — §4 lays out the architecture; per-field forms get drafted in Phase 3 slice §10.7 with frontend-design input.
- No coverage-gap rule authoring — orphan concepts (§7.9 in the audit) are queued behind the schema landing; Phase 3 slice §10.10 schedules legal-review-gated rule additions.
1.3 Why this matters
Three intertwined problems collapse into one fix:
- The 3-parallel-systems mess (Pipeline A vs B vs C) forces the cascade UI, SmartTimeline, and the "Was kommt nach…" tab to consult different corpuses for the same conceptual deadline. The unified model eliminates the drift surface.
- The "all in the Rules so we should be able to manage" promise is unfulfilled today. Q5(C) lands the editor, and Q12/Q3/Q6 give it cleaner columns to edit.
- Coverage gaps (9 orphan concepts, missing
before/working_days/combine_op, half-wired spawn) cap the system's ability to express real-world law. The unified model has room for them.
2. Unified rule model — post-merge schema
This is the target data shape after Phase 3 lands. The migration order in §3 walks the corpus to this shape without destructive change.
2.1 The single rule table: paliad.deadline_rules (evolved, not replaced)
Keep the name. Evolve the schema. Existing FKs survive.
paliad.deadline_rules
├── identity
│ ├── id uuid PK
│ ├── proceeding_type_id int FK → proceeding_types(id) NULLABLE
│ ├── trigger_event_id int FK → trigger_events(id) NULLABLE ← NEW (Q1)
│ ├── parent_id uuid self-FK NULLABLE
│ ├── concept_id uuid FK → deadline_concepts NULLABLE
│ ├── spawn_proceeding_type_id int FK → proceeding_types(id) NULLABLE ← NEW (Q7)
│ └── code text (rule-local code, e.g. `inf.sod`)
│
├── labels + citation
│ ├── name text NOT NULL
│ ├── name_en text NOT NULL DEFAULT ''
│ ├── description text
│ ├── rule_code text (legal citation rule code)
│ ├── legal_source text (structured citation, e.g. UPC.RoP.23.1)
│ ├── deadline_notes text (DE)
│ ├── deadline_notes_en text (EN)
│ └── spawn_label text
│
├── math: anchor + offset + adjustment
│ ├── duration_value int NOT NULL DEFAULT 0
│ ├── duration_unit text NOT NULL DEFAULT 'months' ← values: days|weeks|months|working_days ← NEW (Q11)
│ ├── timing text NOT NULL DEFAULT 'after' ← values: after|before ← NEW (Q11)
│ ├── anchor_alt text (e.g. priority_date)
│ ├── alt_duration_value int
│ ├── alt_duration_unit text
│ ├── alt_rule_code text
│ └── combine_op text ← values: NULL|max|min ← NEW (Q11)
│
├── conditional gating
│ ├── condition_expr jsonb ← NEW (Q6) replaces condition_flag
│ └── (condition_flag text[] DROPPED — superseded after backfill — see §3)
│ └── (condition_rule_id uuid DROPPED — Q13)
│
├── party + bilateral
│ ├── primary_party text (claimant|defendant|both|court)
│ └── is_bilateral boolean NOT NULL DEFAULT false
│
├── lifecycle
│ ├── priority text NOT NULL DEFAULT 'mandatory' ← values: mandatory|recommended|optional|informational ← NEW (Q3)
│ ├── (is_mandatory bool DROPPED — backfilled into priority — see §3)
│ ├── (is_optional bool DROPPED — backfilled — see §3)
│ ├── is_court_set boolean NOT NULL DEFAULT false ← NEW (Q12) replaces heuristic
│ ├── is_spawn boolean NOT NULL DEFAULT false
│ ├── is_active boolean NOT NULL DEFAULT true
│ ├── sequence_order int NOT NULL DEFAULT 0
│ ├── event_type text (category: filing|hearing|decision|order)
│ ├── created_at timestamptz NOT NULL DEFAULT now()
│ └── updated_at timestamptz NOT NULL DEFAULT now()
│
└── rule-editor lifecycle (NEW, Q5)
├── lifecycle_state text NOT NULL DEFAULT 'published' ← values: draft|published|archived
├── draft_of uuid self-FK NULLABLE (when state=draft, points at the published row this draft replaces)
└── published_at timestamptz (NULL while draft)
Net delta vs today: +6 columns (trigger_event_id, spawn_proceeding_type_id, combine_op, condition_expr, priority, is_court_set, lifecycle_state, draft_of, published_at), −3 columns (condition_flag, condition_rule_id, is_mandatory, is_optional). Net +5 columns, table grows from 32 to 37 columns.
2.2 Why keep paliad.deadline_rules instead of a new table
- 171/172 rules carry
concept_id— no need to re-link. - 108/172 carry
parent_id— self-FK survives column adds. paliad.deadlines.rule_idFK points atdeadline_rules.id— 1 live deadline depends on this.paliad.event_category_concepts → deadline_concepts → deadline_ruleschain stays intact.
Renaming the table = ~12 FK + ~6 service files + tests rewrite for zero gain.
2.3 The 4-way priority enum (Q3)
| Value | UI badge | Save-modal default | Semantic |
|---|---|---|---|
mandatory |
red dot | ☑ pre-checked | "Muss adressiert werden" — legal must. |
recommended |
amber dot | ☑ pre-checked | "Sollte adressiert werden" — strong best-practice. |
optional |
grey dot | ☐ pre-unchecked | "Kann adressiert werden" — discretionary. |
informational |
no dot | (never saves) | "Zur Kenntnis" — surfaces in timeline but is NEVER saved as a deadline. Used for procedural milestones like Hauptverhandlung the lawyer doesn't "do" but needs to know about. |
Backfill rules (audit §7.3):
is_mandatory=true, is_optional=false→mandatory(~155 rules).is_mandatory=true, is_optional=true→optional(~6 rules).is_mandatory=false(no live rows exist, but defensive) →recommended.- For court-set rules (where
is_court_set=trueafter Q12 promotion) that the lawyer doesn't "do" — flag theminformationalin a Phase 3 slice (legal review per rule).
2.4 The condition_expr jsonb grammar (Q6)
The audit's §7.7 sketch, formalised:
{
"op": "and",
"args": [
{ "flag": "with_ccr" },
{ "op": "not", "args": [ { "flag": "expedited" } ] }
]
}
Node types:
- Flag reference:
{ "flag": "<name>" }— true iff<name>is in the caller'sFlagsset. - Logical AND:
{ "op": "and", "args": [...] }— true iff every arg is true. Empty args =true. - Logical OR:
{ "op": "or", "args": [...] }— true iff any arg is true. Empty args =false. - Logical NOT:
{ "op": "not", "args": [<single node>] }— true iff the arg is false. Exactly one arg. - (Reserved for future) rule reference:
{ "rule_code": "<code>" }— true iff the calculator already processed that rule in this run. Reservation only; Phase 3 doesn't implement (would resurrect the deadcondition_rule_idsemantic).
null or {} = unconditional (every rule renders). Default for newly-created rules.
Backfill from condition_flag text[]:
NULLor'{}'→null.['with_ccr']→{"flag":"with_ccr"}(simplified single-flag — no need to wrap in AND).['with_ccr', 'with_amend']→{"op":"and","args":[{"flag":"with_ccr"},{"flag":"with_amend"}]}.
After Phase 3 slice §10.2, the AND-of-array semantic continues to work; new rules can use OR/NOT.
2.5 The unified trigger model — proceeding-driven AND event-driven
Today: Pipeline A (proceeding_type_id) and Pipeline C (trigger_event_id) live in disjoint tables. After unification:
- A rule with
proceeding_type_idset +trigger_event_idNULL = proceeding-rooted (today's Pipeline A behaviour). 172 existing rules land here. - A rule with
proceeding_type_idNULL +trigger_event_idset = event-rooted (Pipeline C). 77 migrated rules frompaliad.event_deadlinesland here. - A rule with BOTH set = "this is a proceeding rule, AND it can be triggered as a standalone event". Future-friendly composability — e.g.
inf.sodcould become event-callable via its trigger_event_id. - A rule with NEITHER set = invalid; CHECK constraint rejects.
The calculator becomes one function — Calculate(filter, triggerDate, opts) — where filter is {proceeding_code?, event_type?, rule_id?}. The internal dispatch reads condition_expr, parent_id chains, anchor_alt etc. uniformly.
2.6 The spawn_proceeding_type_id column (Q7)
Today: 8 rules have is_spawn=true. The calculator notes (projection_service.go:896-901) that cross-proceeding spawn fails to resolve the target rules. After this column lands:
is_spawn=true, spawn_proceeding_type_id=X— when this rule fires (e.g. APP.notice off INF.decision), the calculator follows the FK to load proceeding X's rule set and emits its root rule chain.- The dependency annotation now resolves across proceedings (the global rule index in §6 makes this O(1)).
- Cycle guard: pre-flight check during
Calculate()to detectA → B → Aloops; abort withErrCyclicSpawn.
2.7 instance_level on paliad.projects (Q9)
New column:
ALTER TABLE paliad.projects
ADD COLUMN instance_level text;
-- values: NULL | 'first' | 'appeal' | 'cassation' (BGH-Revision / EPA-Beschwerde)
Combined with proceeding_type.code + jurisdiction, lets the SmartTimeline auto-derive:
- DE_INF + first → use
DE_INF. - DE_INF + appeal → use
DE_INF_OLG. - DE_INF + cassation → use
DE_INF_BGH.
UI: a small picker on the project-detail page ("Stand: erste Instanz / Berufung / Revision") that auto-advances when a Berufungsschrift fires on the actuals side (Phase 3 slice §10.8).
2.8 paliad.deadline_rule_audit — the audit log table (Q5)
paliad.deadline_rule_audit
├── id uuid PK DEFAULT gen_random_uuid()
├── rule_id uuid FK → deadline_rules(id) -- NOT NULL
├── changed_by uuid FK → auth.users(id) -- NOT NULL
├── changed_at timestamptz NOT NULL DEFAULT now()
├── action text NOT NULL -- create|update|publish|archive|restore
├── before_json jsonb -- row state pre-change (NULL on create)
├── after_json jsonb -- row state post-change (NULL on archive)
├── reason text NOT NULL -- required justification (>= 10 chars)
└── migration_exported boolean NOT NULL DEFAULT false -- true once this delta has been folded into a checked-in migration
RLS: visible to is_global_admin=true users only.
A DB trigger on deadline_rules INSERT/UPDATE/DELETE captures the change. The trigger reads auth.uid() for changed_by. The trigger also requires a session-level setting paliad.audit_reason to be set (cleared after each statement), enforcing the "reason" requirement.
Defense-in-depth: the rule-editor service also writes audit rows from Go (so the reason text is captured pre-trigger), and the trigger is the backstop for direct SQL.
2.9 What survives unchanged
paliad.deadline_concepts— no schema change.paliad.event_category_concepts— no schema change.paliad.deadline_concept_event_types— no schema change. Now used by both Pipeline-A and Pipeline-C unified rules.paliad.event_categories(cascade taxonomy) — no schema change.paliad.proceeding_types— no schema change, butcategorybecomes informational-only after Q2 soft-merge (every project pickscategory='fristenrechner'codes; litigation codes stay but become unused for project-binding).paliad.courts+paliad.holidays— no change.
3. Migration path
The cutover sequence preserves all live data and keeps the system functional at every step. Each step is a discrete migration file and a discrete Phase 3 slice.
3.1 Step-by-step (with file-numbers TBD, currently next is 078)
Step A — Additive schema
- Migration 078: add columns to
paliad.deadline_rules:trigger_event_id,spawn_proceeding_type_id,combine_op,condition_expr jsonb,priority text DEFAULT 'mandatory',is_court_set bool DEFAULT false,lifecycle_state text DEFAULT 'published',draft_of uuid,published_at timestamptz. - Migration 079: create
paliad.deadline_rule_audit+ RLS + trigger. - Migration 080: add
paliad.projects.instance_level text. - Migration 081: add
paliad.proceeding_typesdisplay_orderconstraints (already in place from mig 051 — verify, no-op if so).
No data change yet. Calculator still reads old columns. Editor not yet built. Verifies build + tests pass against the dual schema.
Step B — Backfill
- Migration 082: backfill
is_court_setfrom the runtime heuristic (primary_party='court' OR event_type IN ('hearing','decision','order')). Defensive — log delta count. - Migration 083: backfill
priority:UPDATE paliad.deadline_rules SET priority='optional' WHERE is_mandatory=true AND is_optional=true; UPDATE paliad.deadline_rules SET priority='recommended' WHERE is_mandatory=false; -- everything else stays default 'mandatory' - Migration 084: backfill
condition_exprfromcondition_flag:condition_flag IS NULL OR condition_flag = '{}'→ leavecondition_exprNULL.- 1 element →
{"flag":"<name>"}. - N elements →
{"op":"and","args":[{"flag":"<a>"},{"flag":"<b>"},...]}.
Step C — Pipeline C migration
-
Migration 085: data-move from
paliad.event_deadlines(77 rows) intopaliad.deadline_rules:- Each event_deadline → new deadline_rule with:
proceeding_type_id = NULLtrigger_event_id = source.trigger_event_idconcept_idresolved best-effort byaliasesmatch againstpaliad.deadline_concepts; NULL if no match.duration_value,duration_unit,timingcopied (Pipeline C'sworking_daysunit,beforetiming, all survive).alt_duration_value,alt_duration_unit,combine_opcopied.name,name_en,deadline_notes,deadline_notes_encopied.parent_id = NULL(Pipeline C has no parent chains).condition_expr = NULL(Pipeline C has no flags).is_active = true.legal_sourcepopulated from rule_codes if available.
- Each event_deadline → new deadline_rule with:
-
Migration 086: backfill any
paliad.deadlines.rule_idthat points at the oldevent_deadlines.id(none today — but defensive). Service-layer code already does fuzzy linkage; this is just future-proofing.
Step D — Calculator unification
- Service refactor (no migration):
FristenrechnerService.Calculatereadscondition_expr(with fallback tocondition_flagfor unbackfilled rows during the transition window); readsis_court_set(with heuristic fallback); readspriority(withis_mandatory+is_optionalfallback). Same compatibility shim fortiming='before'andworking_days. EventDeadlineService.Calculaterewires to call the unified calculator with{trigger_event_id}filter.POST /api/tools/event-deadlinescontinues to work transparently (calls into the unified backend).
Step E — Drop legacy columns + tables
- Migration 087: drop
paliad.deadline_rules.condition_flag(after verifying every rule has eithercondition_exprset or bothcondition_flag+condition_exprare NULL). - Migration 088: drop
paliad.deadline_rules.condition_rule_id(0 live rows; safe drop). - Migration 089: drop
paliad.deadline_rules.is_mandatory+is_optional(afterpriorityis established). - Migration 090: drop
paliad.trigger_events+paliad.event_deadlinestables (after all callers route through unified calculator). DESTRUCTIVE — flagged in §9 risk; Phase 3 slice §10.6 includes a dry-run + verification gate.
Step F — Project model soft-merge (Q2)
- Migration 091: backfill
paliad.projects.proceeding_type_idfrom court / metadata where unambiguous (currently 11/11 NULL — likely 0 rows updated; defensive). - Migration 092: add a CHECK constraint to
paliad.projects.proceeding_type_idthat it points at acategory='fristenrechner'row. (Doesn't break anything live since all 11 are NULL.) - Phase 3 slice §10.5 also updates the project-create form's proceeding-type picker to only surface
fristenrechnercodes. - Litigation codes (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL) stay in
paliad.proceeding_typesbut become unused for project-binding. They remain reachable via cascade leaves (where ECC.proceeding_type_code is set) for the rare case the cascade routes a query against the litigation rules. No data loss.
Step G — Spawn wiring (Q7)
- Migration 093: backfill
paliad.deadline_rules.spawn_proceeding_type_idfor the 8 existingis_spawn=truerules. Per audit §1.6:APP.app.notice←INF.inf.decision,REV.rev.decision,CCR.ccr.decision→ spawn_proceeding_type_id = APP's id.- Etc.
- Service refactor:
FristenrechnerService.Calculateresolves spawns via global rule index (§6).
Step H — Instance level (Q9)
- Service refactor: when project has
instance_levelset, the project's effective proceeding code is derived from(proceeding_code, instance_level)lookup:DE_INF + first→DE_INF.DE_INF + appeal→DE_INF_OLG.DE_INF + cassation→DE_INF_BGH.DE_NULL + first→DE_NULL;DE_NULL + cassation→DE_NULL_BGH. (DE_NULL has no OLG instance — bypassed.)EPA_OPP + appeal→EPA_APP;EPA_OPP + first→EPA_OPP.- DPMA flavours similarly.
- Phase 3 slice §10.8 includes a project-detail UI for the picker.
Step I — Backfill deadline rule_id on legacy data (Q10)
- Migration 094: one-time fuzzy match. For each
paliad.deadlinesrow withrule_id IS NULL:- Lookup
paliad.deadline_concepts.aliasescontainingLOWER(deadlines.title). - If unique match → link to first rule with
concept_idmatching. - If ambiguous or no match → leave NULL, log to
paliad.deadline_rule_auditwith arestoreaction.
- Lookup
- Targets 25/26 deadlines. Expected match rate: ~50–70%.
Step J — Rule editor goes live (Q5)
- Multiple migrations (TBD numbering) + frontend work. Phase 3 slice §10.7.
Net migration count: ~17. Most are < 50 LoC SQL each.
3.2 Cutover ordering — critical invariants
- Additive schema lands first. Calculator reads new columns optionally; old code paths continue.
- Backfill before service rewrite. Service rewrite assumes new columns are populated; backfills run earlier.
- Service rewrite before drops. Service rewrite must successfully consume new columns before legacy columns get dropped.
- Drops in dependency order.
condition_flagdrop comes after all readers cut tocondition_expr. Pipeline C tables drop last. - Soft-merge (Q2) is last data change. CHECK constraint on
projects.proceeding_type_idlands after Phase 3 slice §10.5 updates the form. - Audit log is live BEFORE the rule editor. Trigger goes in (Step A.079) well before the editor surface (Step J). Any rule edit captured forever.
3.3 Rollback strategy
Each migration ships a real *.down.sql that's been dry-run tested. Backfills are reversible (the source columns are kept until Step E). The destructive drops (Step E) are gated on a manual go from head — m's escalation path triggers only if a drop would lose data we haven't migrated.
4. Rule editor architecture (Q5 option C)
m: "C please — I need to see these things. Admin only, ofc."
The editor is the largest single surface in Phase 3. ~3-4 PRs of work depending on slicing.
4.1 Routes + RLS
| Route | Method | Auth | Purpose |
|---|---|---|---|
GET /admin/rules |
GET | is_global_admin=true |
List all rules. Filterable by proceeding, concept, priority, condition_expr-present, lifecycle_state. |
GET /admin/rules/{id} |
GET | global_admin | View a single rule + its parent chain + its children. |
GET /admin/rules/{id}/edit |
GET | global_admin | Form view (HTML rendered server-side from rule data). |
POST /api/admin/rules/{id}/draft |
POST | global_admin | Create a draft of an existing rule. Sets lifecycle_state='draft', draft_of=parent.id. |
PATCH /api/admin/rules/{id} |
PATCH | global_admin | Apply changes to a draft. Requires reason body field. |
POST /api/admin/rules/{id}/publish |
POST | global_admin | Promote draft to published. Archives the prior published version (lifecycle_state='archived'). |
POST /api/admin/rules/{id}/archive |
POST | global_admin | Archive a published rule. Cascade-check parent_id references. |
POST /api/admin/rules |
POST | global_admin | Create a new rule from scratch (starts as lifecycle_state='draft'). |
GET /admin/rules/{id}/audit |
GET | global_admin | Audit log for this rule. |
POST /admin/rules/{id}/preview |
POST | global_admin | Preview-on-trigger-date — runs calculator with this draft replacing its published peer; returns the resulting timeline (no persistence). |
| (removed t-paliad-297) migration-export endpoint | — | — | Was a SQL-export tool generating *.up.sql from audit rows. Workflow shifted to hand-written numbered migrations; tool removed in m/paliad#129. |
4.2 Draft → published lifecycle
[create]
│
▼
draft ───[edit, validate, preview]───▶ draft
│
│ [publish] (only if validation green + reason supplied)
▼
published ───[edit creates new draft_of this]───▶ draft (child)
│ │
│ │ [publish replaces parent]
│ ▼
│ [archive] (only if no live parent_id depends on it)
▼
archived
Invariants:
- A published rule's columns are immutable. Edits create a draft.
- A draft is independent — calculator skips drafts (
is_active=true AND lifecycle_state='published'). - On publish: the draft's columns are copied onto the parent's id (preserves FKs from
paliad.deadlines.rule_id), draft row'slifecycle_stateflips toarchived, prior parent row state archives separately into audit log. - On archive of published: cascade-check children with
parent_id=this. Reject if any active children.
4.3 Form layout
The form's left column is the identity (proceeding, concept, trigger event), the right column is the math + flags, with a third lifecycle column for priority / court-set / spawn. Below the form, a live preview pane shows the calculator's output with the draft applied.
Fields are grouped by audit's §1 sections (identity / labels / math / conditional / party / lifecycle). Validation runs on every change and shows inline.
4.4 Validation rules (server-side, mirrored client-side)
namenon-empty.duration_value≥ 0; if = 0 → eitheris_court_set=trueORparent_idset OR no event_type other thanfiling. (Today's 4-bucket rule.)duration_unitin{days, weeks, months, working_days}.timingin{after, before}.combine_opin{NULL, max, min}; non-NULL requires both base and alt_* set.parent_idif set: must point at an active rule in same proceeding (or in a proceeding reachable via spawn).parent_idif set: must have lowersequence_order(parents before children).condition_expr: validate against jsonb schema (recursive descent, flag-names from a known vocabulary).priorityin{mandatory, recommended, optional, informational}.is_spawn=truerequiresspawn_proceeding_type_id NOT NULL.is_court_set=trueANDduration_value=0ANDparent_id IS NULLallowed (top-level court action).is_court_set=trueANDduration_value > 0allowed (court-set anchor with offset chain).codeif set: unique within(proceeding_type_id, code)pair.- Spawn cycle check on draft → published: walk spawn graph from this rule, abort if cycle reaches this rule.
reasonbody field non-empty + ≥ 10 chars.
4.5 Preview-on-trigger-date
The preview pane runs the calculator with a synthetic rule corpus where this draft replaces its published peer (or is appended if it's a new rule). Returns a UIResponse with the full timeline. The user enters a trigger date + flag set; the preview redraws.
Edge cases:
- If the draft introduces a cycle, preview shows the error inline.
- If the draft references a non-existent
parent_id, preview shows the parent-resolution error inline. - If the draft's
condition_expris malformed, preview shows the jsonb validator error.
4.6 Migration-export (compliance hook)
Rules are legal infrastructure. Live edits must end up in version control or the audit trail decouples from the codebase. The export endpoint solves this:
- Aggregates all audit rows since
migration_exported=trueANDlifecycle_state IN ('published','archived'). - Generates a
*.up.sqlmigration with UPDATE / INSERT / soft-delete statements. - Also generates a matching
*.down.sql. - Returns the SQL blob for the human to commit.
- Marks the audit rows
migration_exported=true.
The migration files don't automatically get committed — that's a human step. But the export means every live edit is reproducible from version control after the fact.
4.7 The audit trail UX
GET /admin/rules/{id}/audit returns a chronologically ordered list of audit rows. Each entry:
- timestamp + actor name
- action (create / update / publish / archive / restore)
- diff view (before_json vs after_json)
- reason text
migration_exportedbadge- link to "Revert to this state" (creates a new draft of the rule with the state from this audit row)
4.8 What stays out of scope of Phase 3 editor
- Cross-rule bulk edit (e.g. "rename
with_ccrflag everywhere") — manual SQL or a future v2 feature. - Rule import from CSV / JSON — manual SQL or future v2.
- Inline preview of the rule on production SmartTimeline projects — preview-on-trigger-date is sufficient.
- Multi-user collaborative editing — single-admin assumption (m).
5. Event-trigger endpoint — POST /api/tools/event-trigger
Q4 — preserves Pipeline C's contract through the unified model.
5.1 Contract
Request:
{
"event_type_slug": "upc_oral_hearing",
"trigger_date": "2026-06-15",
"court_id": "upc-ld-paris",
"flags": ["with_ccr"]
}
event_type_slug(required) — points atpaliad.event_types.slug.trigger_date(required) — ISO date.court_id(optional) — picks the holiday-calendar regime; defaults to UPC München for UPC-jurisdiction event types, DE for DE/EPA/DPMA.flags(optional) — same asFristenrechnerService.Calculate'sFlags.
Response:
{
"event_type": {"slug": "upc_oral_hearing", "label_de": "Mündliche Verhandlung", "category": "hearing"},
"trigger_date": "2026-06-15",
"deadlines": [
{
"ruleId": "...",
"code": "rop151.cost_app",
"name": "Antrag auf Kostenentscheidung",
"ruleRef": "RoP.151",
"dueDate": "2026-07-16",
"originalDate": "2026-07-15",
"wasAdjusted": true,
"isMandatory": true,
"party": "both",
...
},
...
]
}
Same UIDeadline shape as POST /api/tools/fristenrechner.
5.2 Resolution flow
- Look up
event_typeby slug. Ifproceeding_type_codeis encoded viapaliad.deadline_concept_event_types(jurisdiction=…), resolve forward. - Find all rules where
trigger_event_id = event_type.trigger_event_id(a new join — the migration in §3 Step C populates this). - Plus: find all rules where
concept_idmatches event_type's default concept (fromdeadline_concept_event_types), in case the migration didn't cover all event_types. - Calculator runs on the rule set with
triggerDate+flags.
5.3 Why this matters
The cascade UI already lets the user click a leaf → see linked concepts → see rules. After this endpoint:
- The cascade can offer "I just logged this event — calculate its deadlines now" with one click.
- Email-parsing flows (future Phase H Frist-Extraktion) call this endpoint directly when they detect an event_type.
- The "Was kommt nach…" tab on Pathway A migrates to this endpoint (replaces
EventDeadlineServicecall).
5.4 Edge cases
- No matching rules → return empty
deadlinesarray (not 404). Caller can show "Keine Fristen abgeleitet" inline. - Multiple matching trigger_event_ids → calculator merges per
sequence_order. Deduplication on(rule_id). - Project context (caller passes
project_id) — defer to v2; today's wizard semantics work without it.
6. Cross-proceeding spawn wiring
Q7 — replaces the "We don't have that rule in our map" half-wired state from projection_service.go:896-901.
6.1 The global rule index
Today: DeadlineRuleService.List(proceedingTypeID *int) returns rules of one proceeding. The calculator's ruleByID map (in FristenrechnerService.Calculate) only contains those.
After Phase 3 slice §10.4:
type GlobalRuleIndex struct {
byID map[uuid.UUID]models.DeadlineRule
byCode map[string]models.DeadlineRule // (proceeding_code . rule_code)
bySpawnTarget map[int][]uuid.UUID // proceeding_type_id → rules whose spawn_proceeding_type_id = this
}
func (s *DeadlineRuleService) GlobalIndex(ctx context.Context) (*GlobalRuleIndex, error) {
// One SELECT, hydrate all active rules with proceeding_type metadata.
}
The calculator builds a GlobalRuleIndex once at request start; spawn resolution becomes O(1) lookup.
6.2 Spawn execution
When the calculator hits a rule with is_spawn=true:
- Resolve
spawn_proceeding_type_id→ target proceeding code. - Append the target proceeding's root rule chain to the response, computing offsets off the spawning rule's date (which is the parent's anchor for the spawned chain).
- Mark spawned rows with
SpawnedFrom: <source_rule_code>in the response so the frontend can render the spawn boundary clearly.
6.3 Cycle guard
A pre-flight check during Calculate():
- Walk spawn graph from the target proceeding.
- If any reachable proceeding is the starting proceeding, abort with
ErrCyclicSpawn. - Logged + surfaced as an error in the response (not silent).
6.4 SmartTimeline integration
After spawn wiring lands, ProjectionService.computeProjections resolves cross-proceeding rules correctly. The dependency-annotation graph (DependsOnRuleCode / Date / Name) crosses proceeding boundaries cleanly.
7. Instance-level on paliad.projects
Q9 — DE_INF → DE_INF_OLG → DE_INF_BGH ladder without manual proceeding_type re-pick.
7.1 Schema
ALTER TABLE paliad.projects ADD COLUMN instance_level text;
ALTER TABLE paliad.projects ADD CONSTRAINT projects_instance_level_check
CHECK (instance_level IS NULL OR instance_level IN ('first', 'appeal', 'cassation'));
7.2 Mapping table (in code, not DB)
// internal/services/instance_mapping.go
func ResolveInstanceCode(baseCode string, level string) string {
if level == "" || level == "first" {
return baseCode // first instance uses the base code as-is
}
switch baseCode + "+" + level {
case "DE_INF+appeal": return "DE_INF_OLG"
case "DE_INF+cassation": return "DE_INF_BGH"
case "DE_NULL+cassation": return "DE_NULL_BGH"
case "EPA_OPP+appeal": return "EPA_APP"
case "DPMA_OPP+appeal": return "DPMA_BPATG_BESCHWERDE"
case "DPMA_OPP+cassation":return "DPMA_BGH_RB"
case "DPMA_BPATG_BESCHWERDE+cassation":return "DPMA_BGH_RB"
// UPC has no instance ladder today — UPC_CFI / UPC_APP are distinct proceedings.
default: return baseCode // unknown combination — degrade
}
}
7.3 Auto-advance
When a Berufungsschrift / Revisionsschrift fires on the actuals side (a paliad.deadlines or paliad.project_events row is created with the right concept), the project's instance_level advances automatically. Phase 3 slice §10.8 includes the auto-advance logic with a manual override.
7.4 UI
Project-detail page gets a small "Verfahrensstand" picker:
- "Erste Instanz" / "Berufung" / "Revision"
- A subtle hint "auto-advanced am 2026-06-12 vom inf.appeal" when the system advanced.
- A manual override.
7.5 Why this isn't free with proceeding_type_id alone
Today the project model conflates "what kind of case is it" (the base proceeding) with "where in the ladder are we" (the instance). A user filing a Berufung today must manually pick DE_INF_OLG instead of DE_INF — the model loses the first-instance history. After this column, the project carries both base + level, and the SmartTimeline / calculator can derive the right rule set.
8. Frontend ripples
Five consumers need changes after the rule model unifies. Each lands in its own Phase 3 slice (or shares one if small).
8.1 /tools/fristenrechner Pathway A — wizard timeline
frontend/src/fristenrechner.tsx— proceeding-type picker continues to surfacecategory='fristenrechner'codes (no change in source; soft-merge means litigation codes are unused on projects but the picker query already filters by category).frontend/src/client/fristenrechner.ts— therenderTimelineBody+renderColumnsBodyrenderers consumeUIDeadlinewhich gains:priority: 'mandatory'|'recommended'|'optional'|'informational'(replacesisMandatory+isOptional).isCourtSet: bool(now from real column; semantic identical).- Renderer maps
priorityto badge colours per §2.3 table.
8.2 /tools/fristenrechner Pathway B — cascade + calculate-rule card
- Cascade UI — unchanged. The taxonomy → concept → rule chain stays.
calculate-rulecard — readsUIDeadlinefrom/api/tools/fristenrechner/calculate-rule; samepriorityrename.- B2 search results — concept-cards continue to render rule pills. Pill badge updates for
priority.
8.3 /tools/fristenrechner "Was kommt nach…" tab (Pipeline C migrates)
frontend/src/client/fristenrechner.ts:833— switches from/api/tools/event-deadlinesto/api/tools/event-trigger(new endpoint per §5). Response shape is the unifiedUIDeadlinearray. Local rendering needs no change beyond thepriorityrename.- Trigger-event picker — switches from
paliad.trigger_events(110 rows) topaliad.event_types(45 rows, growing). The new vocabulary is finer-grained; a per-jurisdiction filter chip helps users pick.
8.4 SmartTimeline (/projects/{id} Verlauf + /projects/{id}/chart)
internal/services/projection_service.go— already callsFristenrechnerService.Calculate. After unification, no public-API change. The cross-proceeding spawn rows now resolve correctly (Q7).frontend/src/client/views/shape-timeline.ts+shape-timeline-chart.ts— theTimelineEventstruct gains optionalSpawnedFromfield; renderer can show a spawn-boundary divider.- Dependency annotations — already in the wire shape (
DependsOnRuleCode/Date/Name); now resolve across proceedings.
8.5 /tools/verfahrensablauf (abstract-browse surface)
frontend/src/client/views/verfahrensablauf-core.ts— pure-functional, consumesUIDeadline. Samepriorityrename, otherwise no change.- Variant chips (Slice 3 of t-paliad-178, not yet shipped) consume
condition_exprflag names; the chip strip surfaces top-level flags fromcondition_expr's vocabulary. AND of selected chips becomes theFlagspayload to the calculator.
8.6 Project-detail (/projects/{id}) — instance_level picker
frontend/src/projects-detail.tsx+client/projects-detail.ts— new picker (Slice §10.8). Auto-advance toast.
8.7 Admin surface (/admin/rules) — net-new
- All-new TSX + client + CSS for the rule editor (§4). Lands in §10.7 (data-model and audit-log slices) and §10.11 (editor UI slice). Heaviest single Phase 3 deliverable.
9. Risk + tradeoffs
9.1 Destructive migrations
Step E drops paliad.event_deadlines, paliad.trigger_events, paliad.deadline_rules.condition_flag, condition_rule_id, is_mandatory, is_optional. Each drop is gated on a manual verification step. If verification fails, head escalates to m via otto/head delegation (telegram + pwa-form).
Mitigation:
- Every drop has a backup snapshot taken pre-drop (CSV export to S3 or in-DB archive table for 30 days).
- Drops are batched in a single migration window (single deploy), so rollback is one revert.
- Each drop has a paired
*.down.sqlthat recreates from the snapshot.
9.2 Audit-log compliance gap during cutover
The audit log table lands in Step A; the rule editor in Step J. Between A and J, rules can be edited via SQL migrations (the existing workflow). Those edits will be captured by the DB trigger but the reason field requires session-level setting.
Mitigation:
- Phase 3 slice §10.1 includes a hook in the migration tooling: each migration that touches
paliad.deadline_rulesmust callSET LOCAL paliad.audit_reason = 'migration: <name>'before its UPDATE statements. - A pre-commit hook checks for this in
internal/db/migrations/*.up.sql. The dev workflow is mostly automated.
9.3 Cross-corpus drift during the cutover
Between Step C (event_deadlines migrated into deadline_rules) and Step E (Pipeline C tables dropped), two parallel data corpuses exist. If someone edits event_deadlines during this window, the change DOESN'T propagate to deadline_rules.
Mitigation:
- Step C migration also installs a DB trigger on
paliad.event_deadlinesthat firesRAISE EXCEPTION 'event_deadlines is read-only during migration window; edit paliad.deadline_rules instead'. The trigger lives until Step E drops the table. - The trigger is documented in the migration file; Step E removes it cleanly.
9.4 condition_expr jsonb performance
Today's condition_flag text[] GIN-indexable. jsonb is queryable but the predicates we care about (recursive eval) aren't indexable cleanly. Each rule evaluation walks the expression tree.
Mitigation:
- 172 rules × per-request eval is trivial (~10µs per rule). No perceptible perf cost.
- If future scaling is concerned: keep a
condition_flagmaterialised column (text[]) for the simple AND-only case + flag-based GIN index for batch filters. Not needed for v1.
9.5 Migration-export is a manual step
The compliance hook (§4.6) generates the migration SQL but doesn't automatically commit it. If m forgets to export + commit after editing rules, the audit log diverges from version control.
Mitigation:
- The editor UI surfaces a banner: "Migration-Export ausstehend: N unexported audit entries". Banner persists until export runs.
- A cron job in
maiflags it weekly if the unexported count grows.
9.6 Soft-merge friction
After Step F, project-create form shows 19 fristenrechner codes instead of 7 litigation codes. The picker is denser; some users may pick the wrong granularity (e.g. UPC_INF when they should pick UPC_PI).
Mitigation:
- Picker gets jurisdiction-group headers (UPC / DE / EPA / DPMA) and short descriptions.
- The existing court field stays as a separate hint.
- Frontend slice §10.5 includes UX polish.
9.7 Spawn cycle false positives
A legitimate Re-establishment → original-proceeding spawn might LOOK like a cycle but isn't (the spawned proceeding is a NEW instance, not the original). The cycle guard might over-trigger.
Mitigation:
- Cycle detection walks the graph of proceeding codes, not instances. A→B→A on proceeding codes IS a cycle.
- Re-establishment isn't a spawn into the same proceeding — it's a wholly separate calculator call from the user side. Not affected.
9.8 Instance-level + spawn interaction
What if a project at instance_level='appeal' (DE_INF_OLG) has an inf.appeal-style spawn that points at... DE_INF_BGH? The spawn semantic is "spawn the appeal proceeding"; with instance-level resolution, that's already what the user is in.
Mitigation:
- Spawn resolution happens against the BASE proceeding code, not the instance-resolved code. So
DE_INFrules carryspawn_proceeding_type_id=DE_INF_OLG; when a project is at DE_INF_OLG already, the calculator skips that spawn (already at the spawned target). - If the project is at DE_INF_BGH (cassation), spawn from DE_INF.decision is suppressed (you're past it).
9.9 Backfill ambiguity for rule_id fuzzy match (Step I)
The migration tries to link 25/26 legacy free-text deadlines to rules. ~30–50% will be unambiguous (e.g. "Klageerwiderung" → 1 candidate per jurisdiction × project). Others will be ambiguous.
Mitigation:
- Migration leaves ambiguous cases NULL.
- A follow-up UI on
/deadlines/{id}lets users manually pick the rule. Phase 3 slice §10.6 includes this.
10. Slicing for Phase 3 — 12 prioritized slices
Each slice is independently mergeable + shippable + reverttable. Ordering: data-model foundation first → consumer-side ripples → rule-editor surface last.
Slice 1 — Additive schema (Step A)
Migrations 078–081.
Adds columns to paliad.deadline_rules (trigger_event_id, spawn_proceeding_type_id, combine_op, condition_expr, priority, is_court_set, lifecycle_state, draft_of, published_at). Creates paliad.deadline_rule_audit. Adds paliad.projects.instance_level. No data change.
Go: tests confirm schema lands; calculator still reads old columns (compat mode).
Implementer effort: ~150 LoC SQL + 50 LoC Go model updates + tests. Single PR.
Slice 2 — Backfill (Step B)
Migrations 082–084.
Backfill is_court_set, priority, condition_expr. Each backfill is idempotent and logs delta count.
Go: read-side service code stays compat-mode (reads new columns with fallback to old).
Implementer effort: ~100 LoC SQL + verification scripts. Single PR.
Slice 3 — Pipeline C migration (Step C)
Migrations 085–086 + read-only trigger.
Data-move from paliad.event_deadlines → paliad.deadline_rules (77 rows). Installs the read-only trigger on paliad.event_deadlines to prevent edits during cutover.
Go: EventDeadlineService.Calculate rewires to call FristenrechnerService.Calculate with {trigger_event_id} filter.
Implementer effort: ~200 LoC SQL + 100 LoC Go + integration test for the unified call path. Single PR.
Slice 4 — Calculator unification (Step D)
No migration; service refactor only.
FristenrechnerService.Calculate reads condition_expr (with fallback), is_court_set column (with fallback), priority (with fallback). Handles timing='before', working_days, combine_op. Spawn resolution uses GlobalRuleIndex.
POST /api/tools/event-deadlines continues to work; routes into unified.
Implementer effort: ~300 LoC Go + heavy test coverage (the calculator's behaviour is critical). Single PR.
Slice 5 — Project model soft-merge (Step F, Q2)
Migration 091–092 + frontend.
Optional backfill of paliad.projects.proceeding_type_id (none today, defensive). CHECK constraint on paliad.projects.proceeding_type_id for category='fristenrechner'.
Frontend: project-create form's proceeding-type picker shows 19 fristenrechner codes with jurisdiction headers.
Implementer effort: ~50 LoC SQL + ~80 LoC frontend + i18n. Single PR.
Slice 6 — Event-trigger endpoint (Q4)
Handler + service + frontend.
POST /api/tools/event-trigger per §5. Rewires frontend/src/client/fristenrechner.ts:833 to call this instead of /api/tools/event-deadlines. Trigger-event picker switches to paliad.event_types.
Implementer effort: ~100 LoC Go handler + 50 LoC service helper + 150 LoC frontend + tests. Single PR.
Slice 7 — Spawn wiring (Step G, Q7)
Migration 093 + service.
Backfill spawn_proceeding_type_id on 8 live is_spawn=true rules. Service: GlobalRuleIndex + spawn resolution + cycle guard.
Frontend: SmartTimeline renders spawn boundary divider (small CSS + render tweak).
Implementer effort: ~50 LoC SQL + 200 LoC Go (index + spawn execution + cycle check + tests) + 50 LoC frontend. Single PR.
Slice 8 — Instance level (Step H, Q9)
Migration 080 (already in Slice 1) + service + frontend.
instance_level resolution helper. Auto-advance logic on deadline / event triggers. Project-detail picker UI.
Implementer effort: ~80 LoC Go + 120 LoC frontend (picker + auto-advance toast) + tests. Single PR.
Slice 9 — Drop legacy columns + tables (Step E)
Migrations 087–090.
Drop condition_flag, condition_rule_id, is_mandatory, is_optional. Drop paliad.trigger_events, paliad.event_deadlines (after Slice 6 cuts the last reader). Verification gate; backup snapshots.
Implementer effort: ~80 LoC SQL + verification scripts. Single PR. DESTRUCTIVE — head escalates to m if verification fails.
Slice 10 — rule_id backfill on legacy deadlines (Step I, Q10)
Migration 094.
One-time fuzzy match. Targets 25/26 deadlines. Ambiguous cases left NULL.
Frontend: /deadlines/{id} gains a manual "Regel zuordnen" picker for the ambiguous tail.
Implementer effort: ~100 LoC SQL (with paliad.deadline_concepts.aliases JOIN) + 80 LoC frontend. Single PR.
Slice 11 — Rule editor (Q5C) — admin surface + audit-log UI
Handlers + service + frontend.
Routes per §4.1. Audit log table is already in place from Slice 1. Draft → published lifecycle + validation. Preview-on-trigger-date. Migration-export endpoint. Audit UI.
Implementer effort: ~400 LoC Go (handler + service + validation + migration exporter) + ~700 LoC frontend (form, preview pane, audit timeline) + ~150 LoC CSS + i18n + tests. 2 sub-PRs:
- 11a: backend + read-only
/admin/ruleslisting + audit view. - 11b: editor form + validation + preview + migration-export.
Heaviest slice. Lands last so schema is stable.
Slice 12 — Orphan concept seed (Q8)
Multiple small migrations + legal review.
Per the order: wiedereinsetzung > schriftsatznachreichung > versaeumnisurteil-einspruch > weiterbehandlung > others. Each concept gets 1-3 rules per applicable jurisdiction. Per-concept legal review before merging.
Implementer effort: ~30 LoC SQL × 9 concepts = ~270 LoC. Legal review gates each. Lands after editor (Slice 11) so future seed work happens through the editor, not direct migrations.
Recap — slice ordering
foundation: 1 → 2 → 3 → 4 (schema + backfill + Pipeline C move + calculator unification)
consumers: 5 → 6 → 7 → 8 (project model + event-trigger + spawn + instance-level)
cleanup: 9 → 10 (drop legacy + backfill rule_id)
admin surface: 11a → 11b (rule editor backend + frontend)
content: 12 (orphan concept seed)
Slices 1–4 are sequentially dependent (each depends on the prior schema/backfill state). Slices 5–8 can run in parallel after Slice 4 (independent consumers). Slice 9 needs all readers cut over. Slice 10 is independent — can run any time after Slice 3. Slice 11 needs schema stability (Slice 1 + 9 done). Slice 12 needs the editor (Slice 11) — or could run before via SQL if head prefers.
11. Files the implementer will touch (Slice 1 only)
Per the brief, only the FIRST slice's file map gets enumerated. Subsequent slices land their own file maps.
Migrations
internal/db/migrations/078_unified_rule_columns.up.sql(NEW, ~80 LoC)internal/db/migrations/078_unified_rule_columns.down.sql(NEW)internal/db/migrations/079_deadline_rule_audit.up.sql(NEW, ~60 LoC — table + RLS + trigger)internal/db/migrations/079_deadline_rule_audit.down.sql(NEW)internal/db/migrations/080_projects_instance_level.up.sql(NEW, ~10 LoC)internal/db/migrations/080_projects_instance_level.down.sql(NEW)
Go models
internal/models/deadline_rule.go— add fields:TriggerEventID,SpawnProceedingTypeID,CombineOp,ConditionExpr (json.RawMessage),Priority,IsCourtSet,LifecycleState,DraftOf,PublishedAt. KeepIsMandatory+IsOptional+ConditionFlagas compat fields.internal/models/project.go— addInstanceLevel *string.internal/models/deadline_rule_audit.go(NEW, ~30 LoC) — audit row struct.
Go services (read-side fallback compat)
internal/services/deadline_rule_service.go— extendruleColumnsconst to include new columns; column list reflects the dual-shape during transition. ~10 LoC.
Tests
internal/services/deadline_rule_service_test.go— assert SELECT returns new columns. Pure DB sanity.internal/db/migrations_test.go(if exists) — assert schema lands cleanly. Smoke.
Net Slice 1 LoC: ~250 SQL + 100 Go.
Single PR. Reviewable end-to-end. Mergeable independently.
12. Open questions for HEAD (not m)
Sub-decisions head will resolve at slice-start. None block this design's approval; all are tactical.
Q-H-1 — Migration window for Slice 9 (destructive drops)
Slice 9 drops paliad.trigger_events + paliad.event_deadlines + 4 deadline_rules columns. Should it deploy:
- (a) During a maintenance window (announce 24h prior, ~10min downtime expected).
- (b) Live during normal hours (zero-downtime; no migration locks the table).
- (c) Behind a feature flag (no drop; columns renamed to
_legacyfor a grace period).
Recommended: (a). Live edits during a destructive migration are stress.
Q-H-2 — Rule editor's draft lifecycle for v1
The full lifecycle is draft → published → archived with audit trail. v1 simplification options:
- (a) Full lifecycle as designed.
- (b) Skip the draft layer initially — edits go straight to published with audit-only trail.
- (c) Draft is in-memory only — no DB row until publish.
Recommended: (a). Audit traceability requires DB rows; the draft FK is the smallest extra cost for the largest reviewability win.
Q-H-3 — Audit-log retention
How long to retain paliad.deadline_rule_audit rows?
- (a) Forever (legal-archive grade).
- (b) 7 years (typical legal retention).
- (c) 2 years rolling + annual export to S3.
Recommended: (a). Rules are legal infrastructure; the audit table is small (~hundreds of rows/year max).
Q-H-4 — Preview-on-trigger-date data isolation
The preview endpoint runs the calculator with a draft replacing its published peer. Implementation options:
- (a) Pass the draft as an in-memory rule override to the calculator (calculator gains an
Overrides []models.DeadlineRuleparameter). - (b) Temporary materialised view that combines published + draft, scoped to the request.
- (c) Run inside a DB transaction with the draft applied + rolled back.
Recommended: (a). Cheapest, deterministic, no concurrency surface.
Q-H-5 — Migration-export format
The export endpoint emits SQL. Format options:
- (a) Pure SQL (UPDATE / INSERT / DELETE statements).
- (b) SQL wrapped in a Go migration helper that uses application-layer types (more readable but more code).
- (c) JSON dump + apply-tool (decouples from SQL syntax).
Recommended: (a). The existing migration corpus is pure SQL; consistency wins.
Q-H-6 — Slice 6 (event-trigger) timing vs Slice 4 (calculator unification)
Slice 6 builds the new endpoint on top of the unified calculator. If Slice 4 isn't done, Slice 6 routes against the compat-mode calculator. Both work, but Slice 4 has more test coverage.
- (a) Strict ordering — Slice 6 only after Slice 4.
- (b) Allow parallel — Slice 6 uses the partial unified calc.
Recommended: (a). Calculator is critical; serial.
Q-H-7 — Slice 7 (spawn) cycle-guard strictness
Today no live cycles exist. The cycle guard prevents future bad data. Options:
- (a) Reject on insert/update (CHECK constraint with PL/pgSQL function).
- (b) Reject at calculator runtime only.
- (c) Both.
Recommended: (c). Defense-in-depth. The CHECK constraint catches editor mistakes; runtime guard handles corrupted state.
Q-H-8 — Instance-level (Slice 8) UI placement
The picker can live in:
- (a) Project-detail header bar.
- (b) A "Verfahrensstand" mini-section in the sidebar.
- (c) Inline in the proceeding-type display ("UPC_INF · Berufung").
Recommended: (a). Highest discoverability.
Q-H-9 — Slice 11 (rule editor) UX testing scope
Before shipping the editor, what testing surfaces?
- (a) Unit tests + Playwright smoke (load form, edit, publish, audit).
- (b) Internal dogfood with m on staging.
- (c) Both.
Recommended: (c). Editor is admin-only; m IS the dogfood user; pair-prog sessions during slice work.
Q-H-10 — Migration 094 ambiguity tail (Slice 10)
The fuzzy-match backfill leaves ambiguous deadlines NULL. How to surface for cleanup?
- (a) Inline picker on
/deadlines/{id}only whenrule_id IS NULLand confidence < threshold. - (b) Dedicated
/admin/deadlines/needs-linkingqueue. - (c) Skip — leave NULL forever; future-deadline rule_id capture handles new flows.
Recommended: (a). Smallest UX surface; surfaces naturally as the user opens deadlines.
Q-H-11 — Orphan concept seed (Slice 12) ordering vs editor (Slice 11)
Either:
- (a) Seed via SQL migrations before the editor lands (faster coverage).
- (b) Seed via the editor after it lands (proves the editor works end-to-end).
- (c) Mixed — easy concepts via SQL, complex via editor.
Recommended: (b). Editor needs a real-world workout; orphan-concept rule authoring IS the workout.
Q-H-12 — Telemetry for the migration
Should each slice deploy include metrics dashboards (e.g. "rules with priority set" / "calls to /api/tools/event-trigger")? Otherwise migration progress is invisible.
- (a) Yes, add ~3-5 Grafana panels per slice.
- (b) No, rely on existing dashboards.
- (c) Add only for Slices 4 (calculator unification) and 9 (destructive drops).
Recommended: (c). Focused on high-risk slices.
DESIGN READY FOR REVIEW
Inventor (pauli) parks after this commit — head gates the design → Phase 3 transition. No m-gate (autonomy mandate 2026-05-15 00:01).
Recommended Phase 3 implementer rotation: pattern-fluent Sonnet coder per slice. Slices 1–4 sequential, 5–8 parallel, 9–10 cleanup, 11a→11b editor surface, 12 orphan seed. NOT cronus per memory directive 2026-05-06.
When picking up the next slice the implementer should:
- Read this doc end-to-end.
- Cross-reference the audit (
docs/audit-fristen-logic-2026-05-13.mdon main @79f6be3). - Check live state in §0 for any drift.
- Lock the slice's §12 head-decision via mai before starting.
— pauli