# Litigation Planner suite — extract Fristenrechner/Verfahrensablauf into a Go package for paliad + youpc.org reuse **Task:** t-paliad-292 (m/paliad#124) · inventor design phase · read-only · NO code, NO migrations **Branch:** `mai/cronus/inventor-litigation` **Author:** cronus (inventor) **Date:** 2026-05-26 **Issue:** https://mgit.msbls.de/m/paliad/issues/124 --- ## §0.5 m's decisions (2026-05-26, AskUserQuestion round) After Q1+Q2 escalation, m chose AskUserQuestion-mode + revised Q2 framing. Four-question round answered live: | Q | Topic | m's pick | Note | |---|---|---|---| | §0.5-Q1 | Composition shape | **Primary + spawned (v1)** with **multi-proceeding peer compose as the goal (v2)** | "Usually it would be 1 — but 2 is the goal. So people can create more complex scenarios." The jsonb spec is architected for N entries from day 1; v1 ships with N=1; v2 adds peers without a schema migration. | | §0.5-Q2 | Scope | **Per-project + abstract** | Scenarios attach to a real Akte (project_id non-NULL) AND can be created standalone on /tools/verfahrensablauf (project_id NULL = abstract saved templates). | | §0.5-Q3 | Trigger dates | **Per-anchor overrides over one base date** | Matches today's compute model — one trigger date + AnchorOverrides for known court-set / extended dates. Per-proceeding dates inside the spawn graph derive from base + parent chain. | | §0.5-Q4 | Storage | **New `paliad.scenarios` table with jsonb spec** | Beats the column-extension option because the spec carries multi-proceeding compose, abstract-vs-project distinction, and absorbs future schema churn inside jsonb. | **Additional clarification (m's text):** *"users should not add their own rules. I want them to setup a scenario with different existing proceeding types / submissions that trigger sequences."* → **Original Slice E (user-authored rules) is DROPPED.** `paliad.deadline_rules.project_id` column is OUT. Scenarios are pure compositions of existing rules/proceedings/submissions — never authored corpus. §6 below is preserved as historical context; the slice plan in §10 is revised. → **New Slice E** = abstract scenarios surface on `/tools/verfahrensablauf` (Speichern affordance + per-user saved-template list). Different chrome, same scenarios table, same engine. §5 has been rewritten to match these picks; §6 is preserved struck-through; §10 slice plan is revised. The package layout in §2 and the public API in §3 are unchanged by these decisions — the library shape was decoupled by design from persistence + UI choices. --- ## §0 TL;DR > **paliad + youpc.org both import a single Go package — `pkg/litigationplanner`** — that owns the deadline-rule model, the calendar arithmetic, the condition-expression gate, the sub-track routing, and the timeline composer. Persistence stays at the call-site: paliad's `internal/services/*` implements the `Catalog` / `HolidayCalendar` / `CourtRegistry` interfaces against Postgres; youpc.org imports `embedded/upc.Catalog()` + friends and runs the same engine against a generator-produced JSON snapshot of paliad's UPC subset. The convergence: 1. Today the calc is 1505 LoC in `internal/services/fristenrechner.go` + 175 LoC in `deadline_calculator.go` + helpers, all paliad-internal. 2. Phase 2 unification (t-paliad-181 / Slices 1-10, all shipped) collapsed Pipelines A + B + C onto a single `paliad.deadline_rules` table — 231 rows, 20 active proceeding types, lifecycle_state-gated, condition_expr-gated, 77 UPC rules. 3. The body to extract is already structurally a library; the work is **moving it across the package boundary atomically and replacing the DB-coupled imports with abstract `Catalog` / `HolidayCalendar` / `CourtRegistry` interfaces**. 4. youpc.org's UPC-restricted reuse rides on an embedded JSON snapshot (UPC subset + UPC/EU holidays + UPC courts) shipped inside the package — `import "mgit.msbls.de/m/paliad/pkg/litigationplanner/embedded/upc"` is the entire integration surface on the youpc side. 5. Scenarios + user-authored rules are paliad-side persistence concerns that the package's request shape (`CalcRequest`) already supports via `RuleOverrides` + per-card `Choices`. The pkg need not grow new contracts for either; only paliad's catalog impl + paliad-side tables grow. Six slices, each independently shippable on the paliad side; one paired PR on the youpc side at the end (revised per §0.5): - **Slice A** — atomic extract of calc + types + condition_expr + sub-track + legal-source helpers into `pkg/litigationplanner`. No behaviour change. paliad's `internal/services/fristenrechner.go` becomes a 60-line shell. - **Slice B** — `Catalog` / `HolidayCalendar` / `CourtRegistry` interfaces + paliad's default loaders implementing them. - **Slice C** — embedded UPC snapshot + `scripts/snapshot-upc/main.go` generator. - **Slice D** — `paliad.scenarios` table + jsonb spec + per-project chip group. Composition-of-existing-rules (NOT user-authored rules — §6 retracted). - **Slice E** — abstract scenarios on `/tools/verfahrensablauf` (Speichern als Vorlage + Meine Vorlagen list). Same table, `project_id IS NULL`. - **Slice F** — youpc.org integration (separate PR on the youpc repo). m's locked decisions (latest = §0.5): - **Package within paliad** (option 1, not a separate repo). ✓ - **AskUserQuestion permitted this session** — m flipped the protocol mid-session. ✓ - **Scenarios = jsonb spec, per-project + abstract, primary+spawned for v1, multi-peer for v2.** ✓ - **No user-authored rules. Composition only.** ✓ --- ## §1 Today's surfaces converging Three surfaces share the same compute core today. The package crystallises that: ### 1. Fristenrechner (`/tools/fristenrechner`) Knowledge-tool surface. Manual deadline calculation: user picks proceeding type + trigger date + optional flags + per-card choices, gets a flat list of computed deadlines. Backed by `FristenrechnerService.Calculate(ctx, proceedingCode, triggerDateStr, opts CalcOptions) (*UIResponse, error)`. Implemented at `internal/services/fristenrechner.go:270` and consumed by `internal/handlers/fristenrechner.go`. Path-A (abstract browse via `?path=a`) and Path-B (Determinator concept-cascade via `?path=b`, t-paliad-166) feed the same Calculate. ### 2. Verfahrensablauf (`/tools/verfahrensablauf`) Knowledge-tool surface. Same compute, different chrome — abstract browse-a-proceeding with variant chips + consolidated-vs-lane view + side-by-side compare (t-paliad-178/179, shipped). The frontend lifts a shared `client/views/verfahrensablauf-core.ts` module; both routes call the SAME `Calculate` endpoint, just with different UI framing. ### 3. SmartTimeline (`/projects/{id}` Verlauf) Per-project read view. `ProjectionService.For(ctx, projectID, opts) (*ResponseEnvelope, error)` at `internal/services/projection_service.go:1` builds the merged stream: - Past actuals from `paliad.deadlines ∪ appointments ∪ project_events` (filtered to opted-in `timeline_kind`) - Future-projected rows from `FristenrechnerService.Calculate(...)` driven by the project's `proceeding_type_id` + anchor map built from already-completed actuals - Off-script events (counterclaim_created, scope_change, custom_milestone) `ProjectionService` is paliad-only — it speaks Postgres directly (loads project + counterclaim children + actuals, builds the anchor override map, merges + sorts the events, applies levelPolicy at Patent/Litigation/Client). The **compute step** inside it (the `FristenrechnerService.Calculate` call at projection_service.go:813) is the only part that crosses into the library. ### What converges, what doesn't | Concern | Goes into `pkg/litigationplanner` | Stays in `internal/services` | |---|---|---| | Rule model (`Rule`, `ProceedingType`) | ✓ | — | | Calendar arithmetic + working-day walker | ✓ | — | | condition_expr jsonb evaluator | ✓ | — | | Sub-track routing | ✓ | — | | Legal-source display/URL formatting | ✓ | — | | `MapLitigationToFristenrechner` | ✓ | — | | `CalcOptions` (flags, anchor overrides, per-card choices, skip rules) | ✓ | — | | `Calculate` + `CalculateRule` (the pure entry points) | ✓ | — | | `Catalog` / `HolidayCalendar` / `CourtRegistry` interfaces | ✓ | — | | paliad-specific Catalog impl (reads `paliad.deadline_rules`) | — | ✓ (slim) | | paliad-specific HolidayCalendar impl (reads `paliad.holidays`) | — | ✓ | | paliad-specific CourtRegistry impl (reads `paliad.courts`) | — | ✓ | | `ProjectionService` (per-project SmartTimeline) | — | ✓ (no change) | | Scenarios persistence (table writes) | — | ✓ | | User-authored rule writes (insert into `paliad.deadline_rules`) | — | ✓ (rule editor) | | Audit log + RLS | — | ✓ | | HTTP handlers + UI response JSON tags | — | ✓ (`UIResponse` shape kept byte-identical) | Verfahrensablauf and `/tools/fristenrechner` become thin wrappers over the package; SmartTimeline still owns its merge logic but delegates the future-projection step to the package. --- ## §2 Target package layout ``` pkg/litigationplanner/ doc.go — package docstring, reuse manifesto, version banner types.go — Rule, ProceedingType, Court, Holiday, CalcOptions, Timeline, TimelineEntry, RuleCalculation, ConditionExpr catalog.go — Catalog interface + ProjectHint holidays.go — HolidayCalendar interface + AdjustForNonWorkingDays + AdjustForNonWorkingDaysBackward courts.go — CourtRegistry interface + DefaultsForJurisdiction expr.go — condition_expr jsonb evaluator (pure) durations.go — applyDuration + addWorkingDays (pure, takes a HolidayCalendar) subtrack.go — SubTrackRouting registry (statically embedded — the 1 entry today is upc.ccr.cfi → upc.inf.cfi) legal_source.go — FormatLegalSourceDisplay + BuildLegalSourceURL proceeding_mapping.go — MapLitigationToFristenrechner + code constants engine.go — Calculate(ctx, req CalcRequest, cat Catalog, hol HolidayCalendar, crt CourtRegistry) (*Timeline, error) + CalculateRule(...) for the single-rule v4 surface embedded/ upc/ catalog.go — //go:embed catalog.json + NewCatalog() Catalog catalog.json — 9 proceeding types × 77 rules (generator output) holidays.go — //go:embed holidays.json + NewHolidayCalendar() holidays.json — UPC summer vacation + UPC public holidays + EU public holidays (regime='UPC' + country='DE'+'FR'+ 'NL'+'IT' subsets paliad already carries) courts.go — //go:embed courts.json + NewCourtRegistry() courts.json — UPC LDs + CFI + CoA + LDC, country/regime tagged version.go — //go:embed VERSION (paliad release tag the snapshot was generated against) snapshot_test.go — golden test: every rule's compute matches the reference fixture compute scripts/ snapshot/ main.go — reads paliad.deadline_rules etc. via $DATABASE_URL, writes embedded/upc/{catalog,holidays,courts}.json + bumps VERSION README.md — "How to regenerate the UPC snapshot" testdata/ fixtures/ — input + expected-output JSON pairs (per proceeding type, per scenario) for golden tests README.md — "How to import this package" CHANGELOG.md — package-internal changelog (semver beats) ``` ### Why a sub-package per snapshot `embedded/upc/` (and any future `embedded/de/`, `embedded/epa/`, …) sits below `pkg/litigationplanner/` so the snapshot is opt-in: youpc.org imports the snapshot, paliad does **not** (paliad runs against its live DB). Importing the snapshot pulls the JSON into the binary; consumers who only want the engine pay zero binary bloat. ### Why no `scenarios/` sub-package m's framing — "create scenarios" — is **paliad-side persistence**: name a state, store it, switch active. The package's `CalcRequest` already accepts the only field that varies per scenario (`Choices` + `AnchorOverrides`). Scenarios are a paliad table; the package needs no new contract. See §5. --- ## §3 Public API + types ### Rule ```go // Rule is the canonical deadline rule shape. JSON tags match the // paliad.deadline_rules columns 1:1 so the generator can dump rows // straight into the snapshot. type Rule struct { ID uuid.UUID `json:"id"` ProceedingTypeID int `json:"proceeding_type_id"` ParentID *uuid.UUID `json:"parent_id,omitempty"` SubmissionCode *string `json:"submission_code,omitempty"` Name string `json:"name"` NameEN string `json:"name_en"` Description *string `json:"description,omitempty"` PrimaryParty *string `json:"primary_party,omitempty"` EventType *string `json:"event_type,omitempty"` DurationValue int `json:"duration_value"` DurationUnit string `json:"duration_unit"` // days|weeks|months|working_days Timing *string `json:"timing,omitempty"` // after|before AltDurationValue *int `json:"alt_duration_value,omitempty"` AltDurationUnit *string `json:"alt_duration_unit,omitempty"` AltRuleCode *string `json:"alt_rule_code,omitempty"` CombineOp *string `json:"combine_op,omitempty"` // min|max AnchorAlt *string `json:"anchor_alt,omitempty"` // priority_date RuleCode *string `json:"rule_code,omitempty"` DeadlineNotes *string `json:"deadline_notes,omitempty"` DeadlineNotesEN *string `json:"deadline_notes_en,omitempty"` SequenceOrder int `json:"sequence_order"` ConditionExpr json.RawMessage `json:"condition_expr,omitempty"` Priority string `json:"priority"` // mandatory|recommended|optional|informational IsCourtSet bool `json:"is_court_set"` IsSpawn bool `json:"is_spawn"` SpawnLabel *string `json:"spawn_label,omitempty"` SpawnProceedingTypeID *int `json:"spawn_proceeding_type_id,omitempty"` LegalSource *string `json:"legal_source,omitempty"` ConceptID *uuid.UUID `json:"concept_id,omitempty"` IsBilateral bool `json:"is_bilateral"` TriggerEventID *int64 `json:"trigger_event_id,omitempty"` ChoicesOffered json.RawMessage `json:"choices_offered,omitempty"` // Lifecycle fields are part of the schema but irrelevant to the // calculator (a non-published rule never reaches the engine). They // ship in the snapshot for traceability but the engine ignores them. LifecycleState string `json:"lifecycle_state"` DraftOf *uuid.UUID `json:"draft_of,omitempty"` PublishedAt *time.Time `json:"published_at,omitempty"` } ``` ### ProceedingType ```go type ProceedingType struct { ID int `json:"id"` Code string `json:"code"` // upc.inf.cfi Name string `json:"name"` NameEN string `json:"name_en"` Description *string `json:"description,omitempty"` Jurisdiction *string `json:"jurisdiction,omitempty"` // UPC|DE|EPA|DPMA DefaultColor string `json:"default_color"` SortOrder int `json:"sort_order"` DisplayOrder int `json:"display_order"` TriggerEventLabelDE *string `json:"trigger_event_label_de,omitempty"` TriggerEventLabelEN *string `json:"trigger_event_label_en,omitempty"` } ``` ### Catalog interface ```go // Catalog supplies proceeding-type metadata + rules. Implementations: // - paliad: SELECTs from paliad.deadline_rules + paliad.proceeding_types, // filtered to lifecycle_state='published' AND is_active=true. Optional // ProjectHint (Slice E) merges in project-scoped rules. // - embedded/upc: in-memory map keyed by code, populated once at init // from the embedded JSON. type Catalog interface { Proceeding(ctx context.Context, code string, hint ProjectHint) (*ProceedingType, []Rule, error) SubTrackRouting(code string) (SubTrackRouting, bool) } // ProjectHint scopes a Catalog call to a specific project. paliad's // catalog uses ProjectID to merge in rules with project_id = hint.ProjectID // (Slice E). youpc's catalog ignores the hint (no projects exist). // // Zero value = no project context (the abstract Verfahrensablauf / // public Fristenrechner case). type ProjectHint struct { ProjectID uuid.UUID } var ErrUnknownProceedingType = errors.New("unknown proceeding type") ``` ### HolidayCalendar interface ```go // HolidayCalendar adjusts dates onto working days for a given // (country, regime) pair. The package's calc only needs three primitives: // // - IsNonWorkingDay — used by addWorkingDays walker // - AdjustForNonWorkingDays — forward snap (timing='after') // - AdjustForNonWorkingDaysBackward — backward snap (timing='before') // // Implementations: // - paliad: reads paliad.holidays, caches per-year, merges DE federal // fallback. Existing HolidayService at internal/services/holidays.go // already does this; gains a thin satisfying-the-interface shim. // - embedded/upc: in-memory year-keyed map populated from the embedded // JSON snapshot. Covers UPC summer vacation, UPC public holidays, // and the country-tagged subsets paliad ships for UPC LDs (DE, FR, // NL, IT — the countries the LDs sit in). type HolidayCalendar interface { IsNonWorkingDay(date time.Time, country, regime string) bool AdjustForNonWorkingDays(date time.Time, country, regime string) (time.Time, bool) AdjustForNonWorkingDaysBackward(date time.Time, country, regime string) (time.Time, bool) AdjustmentReason(date time.Time, country, regime string) *AdjustmentReason } type AdjustmentReason struct { Country string `json:"country,omitempty"` Regime string `json:"regime,omitempty"` Name string `json:"name,omitempty"` IsVacation bool `json:"isVacation,omitempty"` IsClosure bool `json:"isClosure,omitempty"` } ``` ### CourtRegistry interface ```go // CourtRegistry maps a court id (e.g. "upc-ld-paris", "de-bgh") to its // (country, regime) tuple, which drives non-working-day adjustment. // // Implementations: // - paliad: reads paliad.courts. Existing CourtService.CountryRegime // already does this lookup with a fallback to defaults. // - embedded/upc: in-memory map populated from the embedded JSON. // Carries every UPC LD + CFI + CoA, country-tagged. type CourtRegistry interface { CountryRegime(courtID, defaultCountry, defaultRegime string) (country, regime string, err error) } // DefaultsForJurisdiction is the fallback used when CourtID is empty. // Identical to the existing helper in internal/services. Pure function, // no I/O — lives directly in pkg/litigationplanner. func DefaultsForJurisdiction(jurisdiction *string) (country, regime string) ``` ### CalcRequest + CalcOptions ```go type CalcRequest struct { ProceedingCode string TriggerDate time.Time Options CalcOptions } // CalcOptions carries the optional knobs. Same shape as today's // internal/services.CalcOptions — verbatim, no field rename. type CalcOptions struct { PriorityDate *time.Time Flags []string AnchorOverrides map[string]time.Time CourtID string // Catalog hint — only paliad-side catalogs consume this. ProjectHint ProjectHint // Event-driven branch (event_trigger_service callers) TriggerEventIDFilter *int64 // Editor preview / sandbox RuleOverrides []Rule // Per-card choices (t-paliad-265) PerCardAppellant map[string]string SkipRules map[string]struct{} IncludeCCRFor map[string]struct{} IncludeHidden bool } ``` ### Timeline (result) ```go // Timeline is the package's structured return. Aligns with paliad's // internal/services.UIResponse field-for-field so paliad's HTTP // handlers serve it directly with no shim. type Timeline struct { ProceedingType string `json:"proceedingType"` ProceedingName string `json:"proceedingName"` ProceedingNameEN string `json:"proceedingNameEN,omitempty"` TriggerDate string `json:"triggerDate"` Entries []TimelineEntry `json:"deadlines"` ContextualNote string `json:"contextualNote,omitempty"` ContextualNoteEN string `json:"contextualNoteEN,omitempty"` TriggerEventLabel string `json:"triggerEventLabel,omitempty"` TriggerEventLabelEN string `json:"triggerEventLabelEN,omitempty"` HiddenCount int `json:"hiddenCount"` } // TimelineEntry == paliad's UIDeadline. Same JSON tags. type TimelineEntry struct { /* identical to current UIDeadline */ } ``` The package emits `Timeline`; paliad's handler aliases `Timeline → UIResponse` and `TimelineEntry → UIDeadline` (type aliases keep call-sites byte-identical): ```go // internal/services/fristenrechner.go (post-Slice-A, ~60 lines) type ( UIResponse = litigationplanner.Timeline UIDeadline = litigationplanner.TimelineEntry CalcOptions = litigationplanner.CalcOptions AdjustmentReason = litigationplanner.AdjustmentReason ) func (s *FristenrechnerService) Calculate(ctx context.Context, code, dateStr string, opts CalcOptions) (*UIResponse, error) { td, err := time.Parse("2006-01-02", dateStr) if err != nil { return nil, fmt.Errorf("invalid trigger date %q: %w", dateStr, err) } return litigationplanner.Calculate(ctx, litigationplanner.CalcRequest{ ProceedingCode: code, TriggerDate: td, Options: opts, }, s.catalog, s.holidays, s.courts) } ``` paliad's handlers, projection_service, event_deadline_service all call `FristenrechnerService.Calculate(...)` exactly as today. --- ## §4 Catalog interface — embedded snapshot design (Q3.A.1) ### Snapshot generation `pkg/litigationplanner/scripts/snapshot/main.go` is run **inside paliad's repo against paliad's DB** (it's a paliad-side generator, paliad-only `$DATABASE_URL` requirement). It: 1. Connects to `$DATABASE_URL` (paliad's Postgres). 2. Reads `paliad.proceeding_types` filtered to `is_active=true` AND `category='fristenrechner'` AND `jurisdiction IN ('UPC')`. (One generator-level flag selects the jurisdiction subset; today only `UPC` is exported. Future generator runs can produce `embedded/de/` or `embedded/epa/`.) 3. For each proceeding, reads `paliad.deadline_rules` filtered to `proceeding_type_id IN (...) AND is_active=true AND lifecycle_state='published' AND project_id IS NULL`. (project_id filter ensures user-authored rules from Slice E don't leak into the snapshot.) 4. Reads `paliad.holidays` for the past 5 + next 10 years (configurable), filtered to entries that apply to a UPC court (`regime='UPC' OR country IN ('DE','FR','NL','IT')`). 5. Reads `paliad.courts` for the UPC subset (`regime='UPC'`). 6. Writes `embedded/upc/catalog.json` + `embedded/upc/holidays.json` + `embedded/upc/courts.json` + `embedded/upc/VERSION` (= current paliad git tag, e.g. `v0.42.0`). 7. Re-renders the snapshot test's golden fixtures (or fails if the snapshot diverges from the live calc in an unexpected way — the test compute-loop guards against silent corruption). 8. Exits non-zero on any DB integrity check failure (orphan rules, dangling FKs, etc.). ```bash # Regenerate the UPC snapshot. DATABASE_URL=$PALIAD_DB go run ./pkg/litigationplanner/scripts/snapshot \ --jurisdiction=UPC \ --output=./pkg/litigationplanner/embedded/upc \ --tag=$(git describe --tags --always) go test ./pkg/litigationplanner/embedded/upc/... ``` ### Snapshot consumer (youpc.org) youpc.org imports the snapshot once at boot: ```go import ( lp "mgit.msbls.de/m/paliad/pkg/litigationplanner" upc "mgit.msbls.de/m/paliad/pkg/litigationplanner/embedded/upc" ) var ( catalog lp.Catalog = upc.NewCatalog() holidays lp.HolidayCalendar = upc.NewHolidayCalendar() courts lp.CourtRegistry = upc.NewCourtRegistry() ) func handleFristenrechner(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("proceeding") dateStr := r.URL.Query().Get("trigger_date") td, _ := time.Parse("2006-01-02", dateStr) timeline, err := lp.Calculate(r.Context(), lp.CalcRequest{ ProceedingCode: code, TriggerDate: td, // No project hint (youpc has no projects), no flags by default. }, catalog, holidays, courts) // ... render JSON ... } ``` The snapshot constructors are zero-arg, the in-memory maps populate on first call (sync.Once), and the binary footprint of the embedded JSON is small (77 rules × ~1KB per rule + ~5KB courts + ~25KB holidays ≈ 110KB total). ### Why the JSON shape mirrors the schema The snapshot stores rows in close to their DB shape (snake_case JSON tags, same field names) so a future schema addition (a new column on `paliad.deadline_rules`) is a one-line update: add the field to `Rule`, regenerate the snapshot, ship a minor-version bump. The package itself never needs to know what the columns ARE — it just consumes the `Rule` struct. ### What the snapshot does NOT include - `paliad.deadline_concepts` (rule grouping for cascade) — not needed for compute. The cascade UI is paliad-only. - `paliad.event_categories` (Determinator B1 taxonomy) — paliad-only UI. - `paliad.event_types` (concept default event-type hydration) — not needed for compute. youpc rendering can re-derive from rule metadata. - `paliad.trigger_events` (event-driven branch) — out of scope for v1 youpc; if needed later, generator gains a `--with-events` flag. - All paliad-state tables (projects, deadlines, project_event_choices, audit log, …). --- ## §5 Scenarios design (m's picks — REVISED 2026-05-26) ### m's framing (verbatim) > Where we can add proceeding types or specific submissions and then get the whole sequence with options for the steps so we can create scenarios etc as well. […] users should not add their own rules. I want them to setup a scenario with different existing proceeding types / submissions that trigger sequences. A scenario is a **named composition** built from existing proceedings, existing submissions, existing flags, and anchor dates. It is not authored corpus — every rule it references is already in `paliad.deadline_rules`. The scenario glues them together with the user's choices + dates and produces a complete timeline. ### Recommended design — `paliad.scenarios` table with jsonb spec #### Schema ```sql -- Slice D migration (paliad-side, NOT in pkg/litigationplanner). CREATE TABLE paliad.scenarios ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NULL REFERENCES paliad.projects(id) ON DELETE CASCADE, -- project_id NULL = abstract scenario (saved Verfahrensablauf template). -- project_id NOT NULL = attached to an Akte (the common case). name text NOT NULL, description text NULL, spec jsonb NOT NULL, -- spec carries the full composition; see §5.1. created_by uuid NULL REFERENCES paliad.users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), -- Within a single project, names are unique. Abstract scenarios are -- unique per (created_by, name) so two users can each have a "with_ccr" -- saved template without colliding. CONSTRAINT scenarios_unique_per_scope UNIQUE NULLS NOT DISTINCT (project_id, created_by, name) ); CREATE INDEX scenarios_project_id_idx ON paliad.scenarios(project_id); CREATE INDEX scenarios_abstract_user_idx ON paliad.scenarios(created_by) WHERE project_id IS NULL; -- Active scenario per project. NULL = use today's ad-hoc choice state -- (paliad.project_event_choices) — pre-scenario behaviour preserved. ALTER TABLE paliad.projects ADD COLUMN active_scenario_id uuid NULL REFERENCES paliad.scenarios(id) ON DELETE SET NULL; -- RLS: project-scoped scenarios inherit team visibility via -- paliad.can_see_project(project_id). Abstract scenarios are -- created_by-owner-only (private) for v1; later we can add a shared flag. ``` `paliad.project_event_choices` is **NOT modified** by this design. It continues to store the un-named "current" choice state for back-compat; once a project switches to a saved scenario, the engine reads from `scenarios.spec` instead. #### Spec shape (jsonb) Architected for multi-proceeding compose from day 1 — v1 ships with N=1 entries under `proceedings[]`; v2 lifts to N=peer. ```json { "version": 1, "base_trigger_date": "2026-05-26", "proceedings": [ { "code": "upc.inf.cfi", "role": "primary", "flags": ["with_ccr", "with_amend"], "per_card_choices": { "inf.r30_amend": {"appellant": "claimant"}, "inf.rejoin": {"include_ccr": true} }, "anchor_overrides": { "inf.reply": "2026-08-15", "inf.urteil": "2027-02-01" }, "skip_rules": ["inf.r30_amend_response"] } // v2 (future): additional peer proceeding entries here, each with // its own trigger_date override + flags + choices + anchors. ] } ``` Field semantics: - `version` — schema version of the spec format. v1. Bumped when the spec shape changes incompatibly. - `base_trigger_date` — the case's anchor date (e.g. when the Klage was filed). Every proceeding without its own `trigger_date_override` uses this. - `proceedings[]` — array of proceeding entries: - `code` — proceeding type code (e.g. `upc.inf.cfi`). - `role` — `"primary"` (the main proceeding) or `"peer"` (v2 extension; ignored in v1). - `trigger_date_override` — optional ISO date, defaults to `base_trigger_date`. v1 uses the base; v2 enables per-proceeding offsets. - `flags[]` — array of flag strings (`with_ccr`, `with_amend`, `with_cci`). - `per_card_choices` — map of `submission_code → {appellant?, include_ccr?, …}` (today's PerCardAppellant + IncludeCCRFor merged into one shape). - `anchor_overrides` — map of `submission_code → ISO date` (today's AnchorOverrides — court-extended deadlines, court-set decision dates known retroactively). - `skip_rules[]` — array of submission_codes the user has opted out of (today's SkipRules). Cross-proceeding spawns (the existing `is_spawn` graph — AMD / APP / CCR spawned from primary INF) are handled **automatically by the engine inside the primary entry** — the user does not list spawn outcomes in the spec; the engine expands them via `expandCrossProceedingSpawns`. The spec only carries primary picks + (later) peer picks. #### Why jsonb spec - **Multi-proceeding-ready from day 1.** v1 has 1 entry in `proceedings[]`; v2 has N. No schema migration needed for the lift. - **Absorbs future per-card-choice keys** without ALTER TABLE. The choices vocabulary (`appellant`, `include_ccr`, …) is already extension-friendly inside the calculator; the spec inherits that. - **Single round-trip** to load a scenario — no joins. - **Validatable at write time** — a JSON-schema check in the service layer asserts every `code` resolves to an active proceeding, every `submission_code` resolves to a rule, every flag is known. Bad scenarios cannot be saved; the user gets a clear error. #### Engine integration paliad's `FristenrechnerService.Calculate` is unchanged. The new code path is the **request builder**: when a project has `active_scenario_id` non-NULL, paliad reads the scenario's spec, builds `CalcOptions` from the primary proceeding entry, and calls `Calculate(ctx, primary.code, scenario.base_trigger_date, opts)`. The package gets the same `CalcOptions` shape it's always gotten. v2 multi-peer expansion: iterate `proceedings[]`, call `Calculate` per entry, merge results client-side in `ProjectionService.For` (paliad-side). Still no library API change. #### Endpoints - `GET /api/projects/{id}/scenarios` — list project's scenarios + active. - `GET /api/scenarios?abstract=true` — list current user's abstract scenarios. - `POST /api/projects/{id}/scenarios` — create scoped scenario (body: name, spec). - `POST /api/scenarios` — create abstract scenario (body: name, spec). - `PATCH /api/scenarios/{id}` — rename / edit spec. - `PUT /api/projects/{id}/active-scenario` — set active (body: scenario_id, or null to clear). - `DELETE /api/scenarios/{id}` — remove (also clears any project.active_scenario_id pointing at it). - `POST /api/scenarios/{id}/clone` — copy with new name. Supports cloning abstract → project-scoped and vice versa (the clone source gets `project_id` re-targeted). #### UI surfaces **Project page** (Akte-mode): - "Szenarien" chip group above SmartTimeline: `[Aktuell] [mit CCR] [+ Neu]`. - "Aktuell" = the live `project_event_choices` state (no active scenario). - Click chip → set as active → SmartTimeline + Akte-Fristenrechner re-render. - "+ Neu" modal: name + base (Leer / vom aktuellen Stand / abstrakte Vorlage importieren). - Per-scenario "Bearbeiten" affordance opens the same per-card choice surface filtered to that scenario's spec. **/tools/verfahrensablauf** (abstract): - Existing variant chips + per-card choices continue to drive a "live" abstract scenario carried in URL state. - New "Speichern als Vorlage" button → modal: name → POST /api/scenarios (project_id=NULL). - Sidebar list "Meine Vorlagen" lets the user reload a saved abstract scenario into the URL state. - Existing share-via-URL stays — abstract scenarios are *also* shareable URLs; saving is opt-in for stickiness. #### Library-side neutrality The package never sees the scenario. paliad's request builder unpacks the spec and calls `Calculate` with vanilla `CalcOptions`. youpc.org doesn't need scenarios at all (it stays on URL state). The scenarios feature is 100% paliad-side; the package stays clean. #### What the spec is NOT - Not a place to inject user-authored rules. m's clarification: "users should not add their own rules." The spec references existing rules by `submission_code`; it cannot create new ones. - Not a place to add user-authored proceeding types. Same logic — the `code` field must resolve to an existing active `paliad.proceeding_types` row. - Not a SmartTimeline event store. SmartTimeline reads actuals from `paliad.deadlines` / `appointments` / `project_events` as today; scenarios only influence the *projected* future. ### Multi-proceeding peer compose — v2 path (m's "goal") m: *"Usually it would be 1 — but 2 is the goal. So people can create more complex scenarios."* v2 lifts the engine to multi-peer: 1. Frontend: the "+ Verfahren hinzufügen" button on the scenario editor adds a new entry to `proceedings[]` with `role: "peer"`, picks a proceeding code, optionally sets a `trigger_date_override`. 2. Backend: `ProjectionService.For` (or the request builder) iterates `proceedings[]`, calls `Calculate` per entry, tags each timeline event with its origin proceeding (new `Origin` field on `TimelineEntry`), and merges + sorts by date. 3. SmartTimeline renders peers as parallel lanes (reuses the existing lane infrastructure from t-paliad-175 Slice 4 — `LaneInfo` already supports parallel tracks). 4. No package change — the package processes one proceeding per call as today; multi-peer is a paliad-side orchestration. v2 is OUT OF SCOPE for v1 implementation but IN SCOPE for the spec/storage design. v1 silently ignores any `proceedings[]` entry with `role: "peer"`; v2 honours them. --- ## §6 ~~User-authored rules design~~ — REMOVED by m's 2026-05-26 decision > m's clarification: *"users should not add their own rules. I want them to setup a scenario with different existing proceeding types / submissions that trigger sequences."* The original Q2 (a) per-project rule additions via `paliad.deadline_rules.project_id` is **dropped**. No `project_id` column lands on `paliad.deadline_rules`. The corpus stays admin-curated (Slice 11 rule editor) — users compose, they don't author. §5 above replaces this design pillar: the scenario spec is the user's expression surface. The original §6 body is preserved below as historical context (struck-through) for future readers who want to understand what was considered and why it was rejected. --- ~~### m's framing~~ > we can add proceeding types or specific submissions and then get the whole sequence with options for the steps Two scopes hidden in one sentence: **(a) Per-project rule additions** — "on this specific case, we filed an unusual motion X with deadline Y for response". Common enough that a lawyer wants to type it once and have it ride the timeline. **(b) New proceeding TYPES** — entirely new flows (e.g. Spanish patent litigation, Australian Federal Court). Much bigger surface; would need an editor for proceeding-type metadata too. ### Recommended (R) — (a) for v1, defer (b) #### Schema ```sql -- Slice E migration (paliad-side). ALTER TABLE paliad.deadline_rules ADD COLUMN project_id uuid NULL REFERENCES paliad.projects(id) ON DELETE CASCADE; CREATE INDEX deadline_rules_project_id_idx ON paliad.deadline_rules(project_id, proceeding_type_id, sequence_order) WHERE project_id IS NOT NULL; -- Existing rules (231 rows) all have project_id IS NULL → global. -- New project-scoped rules have project_id NOT NULL. -- Pre-existing UNIQUE constraints on (proceeding_type_id, submission_code) -- continue to apply because they don't include project_id; we add a -- supplementary constraint that allows project-scoped rules to use any -- submission_code namespace independent of global rules: ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_unique_proceeding_submission; ALTER TABLE paliad.deadline_rules ADD CONSTRAINT deadline_rules_unique_proceeding_submission UNIQUE NULLS NOT DISTINCT (proceeding_type_id, submission_code, project_id); ``` #### Catalog merge paliad's `DeadlineRuleService.List` learns the project hint: ```go func (s *DeadlineRuleService) ListForProceeding(ctx context.Context, ptID int, hint litigationplanner.ProjectHint) ([]models.DeadlineRule, error) { if hint.ProjectID == uuid.Nil { // Knowledge-tool / abstract surface: global rules only. return s.list(ctx, ptID, nil) } // Per-project surface (SmartTimeline + Akte-mode Fristenrechner): // global rules + this project's scoped rules, merged on sequence_order. return s.list(ctx, ptID, &hint.ProjectID) } ``` The merge ordering uses `sequence_order` — project-scoped rules pick their own slot via the rule-editor UI. Parent-child chains across the boundary work (a project-scoped rule can have `parent_id` pointing at a global rule, but not vice-versa — DB CHECK enforces). #### Rule-editor UI Slice 11 of t-paliad-181 (the admin rule editor, partially shipped) is the natural anchor. Extend it with: - A "Add project-scoped rule" affordance on the project page: opens the same editor scoped to `?project=` → writes with `project_id` set + `created_by` = current user. - Visibility: project-scoped rules show with a "Akte-spezifisch" badge in admin views; non-admin users only see them on the project they belong to. - Reuses the rule editor's lifecycle (`draft → published → archived`) so users can iterate. #### Why (a) for v1, not (b) - (a) adds ONE column. (b) would need user-authored proceeding_types (5 new columns), code-namespace coordination, fristenrechner-vs-archived category routing, and the cascade taxonomy (event_categories) backfilling for the new type. Much bigger. - m's strong-signal use case is "filed an unusual motion" — that's (a), not (b). - (b) can be added on top of (a) when the demand surfaces (e.g. a Spanish jurisdiction expansion). No design dead-end. #### Library-side neutrality The package's `Catalog.Proceeding(ctx, code, hint)` signature already passes `ProjectHint`. paliad's catalog respects it; youpc's catalog ignores it. No package change beyond making the hint field exist (which Slice B does as part of the interface). #### User-authored rules NEVER leak to youpc.org The snapshot generator filters on `project_id IS NULL` (§4). Per-project rules are paliad-owned business data; youpc.org gets the public-knowledge UPC subset only. ### Escalation to head (Q2) The package design works for any of (a) / (a)+(b) / (c) defer. **Recommendation = (a)**. If m wants (b) in scope, we add a Slice E2 for user-authored proceeding types — but I'd argue defer until a real demand surfaces. --- ## §7 youpc.org integration plan ### Repo split discipline - **paliad repo** (`m/paliad`) — owns the package, owns the generator, owns the snapshot. Every paliad release tag is a snapshot vintage. - **youpc repo** (`m/youpc.org`) — owns the youpc-side UI, imports the package via Go module. - **No cross-repo coupling at runtime.** youpc.org talks to its own DB; never reaches into paliad's DB. ### Import path ```go import ( lp "mgit.msbls.de/m/paliad/pkg/litigationplanner" upc "mgit.msbls.de/m/paliad/pkg/litigationplanner/embedded/upc" ) ``` youpc's `go.mod` pins a specific paliad version: ``` require mgit.msbls.de/m/paliad v0.42.0 ``` Updates are explicit — `go get -u mgit.msbls.de/m/paliad@v0.43.0` is the only way to pick up new rules. ### UPC-only restriction The restriction is **structural**: youpc imports `embedded/upc` and only `embedded/upc`. The catalog only knows about UPC proceeding codes; asking for `de.inf.lg` returns `ErrUnknownProceedingType`. The restriction isn't an enforcement flag in the engine; it's a property of the snapshot binding. If youpc.org ever needs EPA, a separate `embedded/epa` ships and youpc imports it explicitly. Two snapshots, two imports, two surface scopes. ### UX on youpc.org Scope: - A new public route, e.g. `/laws/upc/fristenrechner`, that renders the same Fristenrechner UX (variant chips + flat result list + per-card choices). - A new public route, e.g. `/laws/upc/verfahrensablauf`, that renders the abstract Verfahrensablauf (compare two timelines side-by-side, variant chips). Both routes share the package's `Calculate` entry; only the chrome differs. youpc.org's existing visual language drives the rendering — design lift from paliad/frontend is OUT OF SCOPE for this design (covered by a separate task on the youpc repo, Slice F). ### Persistence youpc.org doesn't persist anything related to the planner — no projects, no scenarios, no user-authored rules. URL state IS the state. This matches the existing youpc model where /laws is a knowledge surface, not a workspace. ### Auth The Catalog API is anonymous. youpc.org can put the route behind a login if it wants (premium-beta gate, etc.), but the package itself doesn't gate. ### Telemetry youpc.org can wrap `lp.Calculate` in its own telemetry shim. The package doesn't emit telemetry directly — keeps the dependency chain clean. --- ## §8 Migration plan (Q4: atomic move) ### Why atomic, not duplicate-then-DRY The current `internal/services/fristenrechner.go` body IS the package's reason to exist. Duplicating its logic into `pkg/litigationplanner` while leaving the original in place creates two compute paths that will drift the moment a Wave 3 rule shape lands. The risk of drift outweighs the risk of a single bigger PR. Slice A is atomic. After Slice A: - `pkg/litigationplanner/*.go` — full body, exported. - `internal/services/fristenrechner.go` — ~60 lines, the shell. - `internal/services/deadline_calculator.go` — ~30 lines, the shell (or deleted entirely once `DeadlineCalculator` is just `litigationplanner.Calculator`). - `internal/services/event_deadline_service.go` — uses `litigationplanner.Calculate(...)` with `TriggerEventIDFilter` set. - All tests still pass. ### File-move map (Slice A, illustrative — coder will adjust) | From | To | Notes | |---|---|---| | `internal/services/deadline_calculator.go` | `pkg/litigationplanner/durations.go` | Plus `addWorkingDays` + `applyDuration` from `fristenrechner.go`. | | `internal/services/fristenrechner.go` Calculate body (lines 270–780) | `pkg/litigationplanner/engine.go` | Renamed `litigationplanner.Calculate`. | | `internal/services/fristenrechner.go` CalculateRule body | `pkg/litigationplanner/engine.go` | Renamed `litigationplanner.CalculateRule`. | | `internal/services/fristenrechner.go` evalConditionExpr | `pkg/litigationplanner/expr.go` | Already pure. | | `internal/services/proceeding_mapping.go` (entirety) | `pkg/litigationplanner/proceeding_mapping.go` | Pure. | | `internal/services/proceeding_mapping.go` SubTrackRoutings | `pkg/litigationplanner/subtrack.go` | Pure registry. | | `internal/services/deadline_search_service.go` FormatLegalSourceDisplay + BuildLegalSourceURL | `pkg/litigationplanner/legal_source.go` | Both pure. | | `internal/services/fristenrechner.go` UIResponse, UIDeadline, CalcOptions, CalcRuleParams, AdjustmentReason | `pkg/litigationplanner/types.go` | Renamed Timeline / TimelineEntry / CalcOptions / RuleCalcParams / AdjustmentReason. paliad keeps type aliases for back-compat. | | `internal/services/holidays.go` HolidayService | (stays, becomes Catalog impl) | Plus the package gets the interface. | | `internal/services/courts.go` CourtService | (stays, becomes Catalog impl) | Same. | | `internal/services/deadline_rule_service.go` DeadlineRuleService | (stays, becomes Catalog impl) | Same. | ### Test discipline - All current tests under `internal/services/fristenrechner_test.go`, `deadline_calculator_test.go`, `projection_service_test.go`, `event_deadline_service_test.go` continue to run **unchanged** in paliad — they test the paliad-side wrappers which delegate to the package. - The package gets its own `pkg/litigationplanner/*_test.go` with fixture-driven tests: input JSON → expected output JSON, covering every rule shape (composite, alt-swap, anchor-override, sub-track, condition_expr, court-set indirect, choice-skip, choice-include-CCR, …). - `pkg/litigationplanner/embedded/upc/snapshot_test.go` is a golden test: run the engine against the snapshot for every (proceeding, trigger_date) fixture, assert byte-equality with the expected output. This is the regression net for the snapshot generator. ### Rollback Slice A is one PR. Revert = revert. Once Slice C lands and youpc.org takes a dep on the snapshot (Slice F), revert means coordinating the youpc.org module pin — but that's a normal multi-repo coordination, not a one-way door. ### Slice ordering hard constraints - **A blocks B**: B introduces the Catalog interface; A must put types into the package first. - **B blocks C**: C populates the snapshot via the Catalog interface. - **D + E are independent of A-C** but should land after at least Slice B so paliad's catalog is the merge point. - **F (youpc) blocks on C** at minimum. Ideally also on D + E so the snapshot is stable. --- ## §9 Versioning + release process (Q5) ### Semver discipline on the package - `v0.x.y` — pre-1.0. The package is intentionally below the 1.0 stability bar until paliad-internal + youpc-internal usage is settled (probably 2-3 months of co-existence). - `v1.0.0` — declare API frozen; new fields are additive only; removals require a major bump. ### What counts as a breaking change? - Rename a public function / type / field → breaking. - Change the JSON tags of `Rule` / `ProceedingType` / `Timeline` → breaking (the snapshot file shape changes too). - Change the meaning of a function (e.g. `CountryRegime` now returns an error on unknown court) → breaking. - Drop a `Catalog` / `HolidayCalendar` / `CourtRegistry` method → breaking. ### What is additive (non-breaking)? - Add a new field to `Rule` / `Timeline` / `CalcOptions` → additive (struct fields are open). - Add a new constant / helper / sub-package → additive. - Add a new optional field on `CalcRequest` → additive if zero-value is back-compat. - Add a new entry to `SubTrackRoutings` → additive (data, not API). - Add a new proceeding type to the snapshot → additive (data). - Add new rules to existing proceeding types in the snapshot → additive (data). ### Snapshot versioning vs API versioning Two clocks: - **API version** — bumped on package API change. Lives in `pkg/litigationplanner/doc.go` (`const Version = "..."`). - **Snapshot vintage** — bumped on every regeneration. Lives in `pkg/litigationplanner/embedded/upc/VERSION` (auto-set to the paliad git tag at generator time). A breaking schema addition (e.g. a new column on `paliad.deadline_rules` that the snapshot ships) is API-version-additive (struct gains a field) but snapshot-vintage-bumping. youpc.org sees it on its next `go get -u`. A deletion of a column (rare) is API-version-breaking — requires a major bump. ### Release flow 1. paliad merge to main bumps the package version when it touches `pkg/litigationplanner/`. 2. Snapshot regeneration is triggered MANUALLY by the operator after material rule changes (rule editor Slice 11b, batch migrations, etc.). Process: `go run ./pkg/litigationplanner/scripts/snapshot` → review diff → commit → tag → push. 3. youpc.org's CI runs `go get -u` weekly (or on-demand) and opens a PR with the new dep version. Human reviews + merges. ### Drift detection A test on paliad's side compares the snapshot at HEAD against the live DB and fails if the rule set has diverged without a snapshot bump: ```bash # pkg/litigationplanner/scripts/snapshot/check.sh — runs in paliad CI DATABASE_URL=$PALIAD_DB go run ./pkg/litigationplanner/scripts/snapshot \ --jurisdiction=UPC --dry-run --diff-against=./pkg/litigationplanner/embedded/upc ``` If the diff is non-empty, the build fails with "snapshot drift — regenerate after reviewing". This catches accidental forgetting; nothing prevents intentional drift. ### Why semver, not date-stamped versions Go modules need semver. `v0.42.0` slots into `require`; `v2026-05-26` doesn't. The snapshot VERSION file can carry both (semver + date) for human readability. --- ## §10 Slice plan Each slice is independently shippable on the paliad side. Slice F is a separate PR on the youpc.org repo. ### Slice A — extract calc + types into `pkg/litigationplanner` (no behaviour change) **Scope** — paliad-side extraction. Move: - `Rule` / `ProceedingType` / `Court` / `Holiday` types into `pkg/litigationplanner/types.go`. - `applyDuration`, `addWorkingDays`, `DeadlineCalculator` body into `pkg/litigationplanner/durations.go`. - `evalConditionExpr` + `hasConditionExpr` into `pkg/litigationplanner/expr.go`. - `MapLitigationToFristenrechner` + code constants into `pkg/litigationplanner/proceeding_mapping.go`. - `SubTrackRoutings` + `LookupSubTrackRouting` into `pkg/litigationplanner/subtrack.go`. - `FormatLegalSourceDisplay` + `BuildLegalSourceURL` into `pkg/litigationplanner/legal_source.go`. - `DefaultsForJurisdiction` into `pkg/litigationplanner/courts.go`. - `Calculate` + `CalculateRule` bodies into `pkg/litigationplanner/engine.go` — using the Catalog / HolidayCalendar / CourtRegistry interfaces. - Type aliases in `internal/services/fristenrechner.go` so call-sites continue to import `services.UIResponse` etc. **Test discipline** — all existing tests under `internal/services/*_test.go` continue to pass unchanged. Add package-side tests for the pure functions (which are mostly already there as table-driven tests; move them across). **Exit criteria**: - `go build ./...` clean. - `go test ./...` green, no regressions. - `internal/services/fristenrechner.go` is < 100 lines and consists of the type aliases + thin `Calculate`/`CalculateRule` shells. **~1700 LoC moved, ~60 LoC remaining on the paliad side.** ### Slice B — Catalog / HolidayCalendar / CourtRegistry interfaces + paliad's default loaders **Scope** — define the three interfaces in the package; have paliad's existing `DeadlineRuleService`, `HolidayService`, `CourtService` implement them with shim methods. Slice A already calls Calculate against the interfaces, so this is purely about wiring the production paths. **Files**: - `pkg/litigationplanner/catalog.go` — `Catalog` + `ProjectHint`. - `pkg/litigationplanner/holidays.go` — `HolidayCalendar`. - `pkg/litigationplanner/courts.go` — `CourtRegistry`. - `internal/services/deadline_rule_service.go` — add `Proceeding(ctx, code, hint)` method satisfying `Catalog`. - `internal/services/holidays.go` — already has `IsNonWorkingDay` / `AdjustForNonWorkingDays`; add `AdjustForNonWorkingDaysBackward` shim if not exposed. - `internal/services/courts.go` — `CountryRegime` already exists, satisfies `CourtRegistry`. **Exit criteria**: paliad's wire-up uses the interfaces; no behaviour change. ### Slice C — embedded UPC snapshot + generator script **Scope** — write the generator + emit the first snapshot + the snapshot consumer. **Files**: - `pkg/litigationplanner/scripts/snapshot/main.go` — generator (CLI binary). - `pkg/litigationplanner/scripts/snapshot/README.md` — operator runbook. - `pkg/litigationplanner/embedded/upc/catalog.go` + `.json` + `holidays.go` + `.json` + `courts.go` + `.json` + `VERSION`. - `pkg/litigationplanner/embedded/upc/snapshot_test.go` — golden tests. **Exit criteria**: - `go run ./pkg/litigationplanner/scripts/snapshot --jurisdiction=UPC` emits a non-empty snapshot. - `lp.Calculate(ctx, req, upc.NewCatalog(), upc.NewHolidayCalendar(), upc.NewCourtRegistry())` returns the same Timeline for every fixture as the paliad-side Catalog does. - A snapshot-drift test fails on intentional drift to prove the detector works. ### Slice D — scenarios persistence: per-project (REVISED per m's 2026-05-26 picks) **Scope** — `paliad.scenarios` table + jsonb spec + project-page UI. **Files**: - `internal/db/migrations/134_scenarios.up.sql` + `.down.sql` — create `paliad.scenarios` (project_id, name, spec jsonb, created_by, …) + `paliad.projects.active_scenario_id` FK + RLS via `paliad.can_see_project`. - `internal/services/scenario_service.go` (new) — Create/List/Get/Patch/Clone/SetActive/Delete + JSON-schema validation of `spec` (every code/submission resolves; flags known). - `internal/handlers/scenarios.go` (new) — wire the REST endpoints from §5. - `internal/services/projection_service.go` — when `project.active_scenario_id` is set, build `CalcOptions` from the scenario's spec.primary entry instead of `paliad.project_event_choices`. - `internal/services/event_choice_service.go` — unchanged (still owns the un-named "current" state); a TODO comment notes the v2 collapse if scenarios fully replace ad-hoc choices. - `frontend/src/components/ScenarioChips.tsx` (new) + `frontend/src/client/projects-detail.ts` — chip group + create/edit modal + active switch. - `frontend/src/components/ScenarioEditor.tsx` (new) — per-card choices + flags + anchor-overrides editor scoped to the scenario. **Exit criteria**: - Project page renders scenario chips; create/switch/delete works. - SmartTimeline + Akte-Fristenrechner respect the active scenario. - "Aktuell" (no active scenario) preserves today's behaviour reading from `project_event_choices`. - Per-card choice editor writes into the scenario's spec.anchor_overrides / per_card_choices / skip_rules / flags. ### Slice E — abstract scenarios on /tools/verfahrensablauf (NEW, replaces ex-Slice E) **Scope** — abstract scenarios surface; reuses the same `paliad.scenarios` table with `project_id IS NULL`. **Files**: - `internal/services/scenario_service.go` — Slice D code already supports abstract scenarios; Slice E adds the `created_by`-scoped list endpoint + the clone-abstract-to-project / clone-project-to-abstract flows. - `internal/handlers/scenarios.go` — extend with `?abstract=true` filter on List. - `frontend/src/verfahrensablauf.tsx` + `client/verfahrensablauf.ts` — "Speichern als Vorlage" button + sidebar list "Meine Vorlagen" + load-saved-template → URL state. **Exit criteria**: - /tools/verfahrensablauf gains save + list-my-templates affordances. - A saved abstract scenario reloaded into URL state reproduces the live timeline byte-identically. - Project page "+ Neu" modal supports "abstrakte Vorlage importieren" — copies the abstract scenario into the project's scope. ### v2 — multi-proceeding peer compose (DEFERRED, no slice in this design) m's "goal": *"2 is the goal. So people can create more complex scenarios."* The spec already carries `proceedings[]` as an array; v2 lifts the engine + UI to honour `role: "peer"` entries. Out of scope for v1 — but the design holds because the schema doesn't need migration to support it. ### Slice F — youpc.org integration (separate repo) **Scope** — youpc-side import + UI + routes. This is a youpc repo PR; out-of-scope for this paliad task. Document the integration contract here so the youpc-side worker has a clean handover. **Contract**: - `go get mgit.msbls.de/m/paliad@` in youpc.org. - Boot-time wire: `catalog = upc.NewCatalog()` (sync.Once-protected init). - One handler per knowledge surface (Fristenrechner, Verfahrensablauf). Each calls `lp.Calculate(...)`. - UX lift from paliad's frontend out of scope of the package; youpc.org's design system drives the UI. - youpc's CI sets up a weekly Renovate-style PR for paliad version bumps. **Out of scope of this design** — youpc's UX, youpc's auth gate, youpc's analytics, youpc's i18n. Those belong to a sibling task on the youpc.org repo. ### Slice ordering (revised) ``` A → B → C → F (youpc integration on C) ↘ D → E (paliad-only; E builds on D's table + schema) ``` A is the prerequisite for everything. B is the prerequisite for C and D. C is the prerequisite for F. D + E are sequential (E reuses D's table) but can ship as one PR if a worker prefers. Old "Slice E user-authored rules" is dropped per m's 2026-05-26 clarification (§6). New Slice E is the abstract scenarios surface on /tools/verfahrensablauf. --- ## §11 Risk assessment + rollback ### Risks 1. **Slice A regression risk.** Moving 1700 LoC across a package boundary while preserving behaviour is the biggest single-PR risk in this work. **Mitigation**: every existing test must keep passing; add a regression suite at the package boundary before moving anything; the type-alias bridge means call-sites need no edits. 2. **Snapshot drift between paliad and youpc.** A paliad-side rule change that the operator forgets to snapshot leaves youpc stale. **Mitigation**: snapshot-drift CI check (§9) fails the build on un-snapshotted drift. Operator regenerates manually; one-line command. 3. **API churn during pre-1.0.** Sub-1.0 means breaking changes are allowed, and youpc.org will pick them up on `go get -u`. **Mitigation**: every breaking change is called out in `pkg/litigationplanner/CHANGELOG.md`; youpc.org pins exact versions during pre-1.0. 4. **Catalog merge ambiguity (Slice E).** A project-scoped rule with the same `submission_code` as a global rule — does it override or coexist? **Mitigation**: the unique constraint `(proceeding_type_id, submission_code, project_id) NULLS NOT DISTINCT` allows ONE global + N project-scoped to coexist. The catalog returns both; the engine sees both rows. Ordering by `sequence_order` decides which renders first. Document this explicitly in the rule-editor UX (no silent override). 5. **Scenarios spec validation drift.** Per m's revised design (§5), the `spec` jsonb references `proceeding_type.code` + `submission_code` + flag strings — all of which can be renamed by future migrations. A scenario saved against `upc.inf.cfi.inf.reply` would silently break if the submission_code changes. **Mitigation**: validate-on-load (not just on-write): when paliad reads a scenario, the service resolves every code/submission against the current corpus and surfaces a "Vorlage benötigt Aktualisierung" banner if anything's gone stale. The user can edit + re-save against the current corpus; the old spec is preserved until they do. 6. **youpc.org snapshot binary footprint.** ~110KB embedded JSON in youpc's binary. **Mitigation**: trivial. Not a risk in practice. 7. **Spawn cycle guard ownership.** Today `ProjectionService.expandCrossProceedingSpawns` carries the visited-set + maxSpawnDepth cycle guard. After the move, the guard lives in the package (it's part of `Calculate`). **Mitigation**: lift the test fixtures along with the code; the cycle-guard test stays green by construction. ### Rollback - **Slice A**: revert the PR. paliad runs against the pre-move code; no DB change, no consumer impact. - **Slice B**: revert the PR. Calls to interfaces fall back to direct calls on services. - **Slice C**: revert the PR. paliad unaffected; youpc.org not yet integrated. - **Slice D**: revert + run the down-migration. project_event_choices loses `scenario_name`, projects loses `active_scenario`; user data preserved (one row per choice). - **Slice E**: revert + run the down-migration. Project-scoped rules become orphaned (column drop wipes them — coder should snapshot to a sidecar table before drop if any user has authored rules). Alternatively roll forward with bug fix. - **Slice F**: youpc.org pins a previous paliad version. Trivial. --- ## §12 Out of scope - youpc.org UX, layout, design-system lift — separate task on the youpc repo (Slice F's content). - Cross-firm rule sharing (one firm's user-authored rules visible to another firm). Defer until the firm-multitenancy story is real. - Multi-jurisdiction snapshots (`embedded/de/`, `embedded/epa/`). The generator gains a `--jurisdiction` flag in Slice C; the additional snapshots are a follow-up task per jurisdiction when demand surfaces. - Re-architecting `ProjectionService` to live in the package. It speaks Postgres directly to load project + counterclaim children + actuals, which IS paliad-specific business logic. Only its single `Calculate(...)` call crosses the boundary. - Replacing the existing rule editor UX. Slice E extends it for project-scoped rules; no redesign. - Live cross-repo CI integration (auto-PR on paliad release tagging youpc.org). Manual `go get -u` workflow for now; automation deferred. - Anthropic/Paliadin AI integration with the planner (e.g. natural-language scenario creation). Out of scope. - Frontend type generation from the package's Go types. The TS interfaces in `frontend/src/types.ts` already match the JSON shape; preserve manually until a generator is worth the effort. - Snapshot signing / provenance attestation. youpc.org trusts paliad's release tag; cryptographic integrity is a follow-up. --- ## §13 Open questions — RESOLVED 2026-05-26 Originally Q1 + Q2 were escalated via `mai instruct head`. m switched to AskUserQuestion-mode mid-session; answers folded into §0.5. Original Q1+Q2 framings preserved below as historical context. ### Q1 — Scenarios storage model (material) — ANSWERED §0.5-Q4 m picked **new `paliad.scenarios` table with jsonb spec** (option B in the original framing). Original recommendation (column extension) was retracted in favour of jsonb spec because m's Q1-Q3 picks (multi-proceeding peer compose as goal, per-project + abstract scope) made the jsonb shape strictly better. ### Q2 — User-authored rules scope (material) — REJECTED §0.5 m clarified: *"users should not add their own rules."* Original Slice E dropped. New Slice E = abstract scenarios surface on /tools/verfahrensablauf. Composition replaces authoring. ### Q3 — Locked by m Package-within-project. Embedded snapshot with generator. **Recommendation honoured.** ### Q4 — Locked by inventor (matches brief default) Atomic move from `internal/services` into `pkg/litigationplanner`. **No m gate needed**; head approves the engineering plan. ### Q5 — Locked by inventor (matches brief default) Semver discipline, additive-by-default API, snapshot-vintage separate from API version. **No m gate needed**; head approves. ### Q6 — Snapshot regeneration cadence (tactical) Manual operator command vs CI cron? Recommendation: **manual** during pre-1.0; revisit after 3 months of usage. **Head decides.** ### Q7 — Snapshot drift policy on paliad CI (tactical) Fail the build on un-snapshotted drift, or warn? Recommendation: **warn during pre-1.0** so paliad-side rule edits don't block development; fail starting at v1.0. **Head decides.** ### Q8 — Package version cadence (tactical) Bump on every PR that touches `pkg/litigationplanner/`, or batch into milestone releases? Recommendation: **bump on every PR**, since semver is cheap and the operator burden is one line. **Head decides.** ### Q9 — Rule-editor extension for Slice E (tactical, depends on Q2) If Q2 = (a), where does the "Akte-spezifische Frist" affordance live in the project page? Recommendation: **a small "+ Frist hinzufügen" button under the SmartTimeline header**, opens the existing rule editor scoped to the project. **Head decides** (UX placement). ### Q10 — youpc.org module pin strategy (tactical, advisory) `require mgit.msbls.de/m/paliad v0.x.y` — exact, ^-ranged, or auto-updating? Recommendation: **exact pin** through pre-1.0; tilde-ranged at v1.x. **Head decides + flags to the youpc-side worker.** ### Q11 — Snapshot-bound proceeding scope (tactical) The brief says "youpc UPC-restricted". The 9 UPC proceeding types are explicit. But: should the snapshot also include `epa.grant.exa` (EP grant publication is UPC-relevant for priority-date math)? Recommendation: **start with UPC only**; if a UPC visitor's deadline depends on EPA rules, surface a stub error that points them at paliad.de. **Head decides.** --- ## §14 m's decisions (placeholder — will be filled after head returns answers) This section will be added when `paliad/head` relays m's answers to Q1, Q2, and any escalated tactical Qs. Per inventor protocol, the design doc gets an addendum here; the rest of the doc is the historical record. For now, the inventor's picks (R) stand as proposed; the design holds together with any of the alternative pickings on Q1 / Q2 because the package shape is decoupled from the persistence choices. --- ## §15 Recommended implementer Slice A is the biggest mechanical lift; recommend a **pattern-fluent Sonnet coder** with deep paliad-go-services familiarity. Cronus (this inventor) has carried t-paliad-133, t-paliad-291, and the design context here, so a same-worker shift to coder mode is one of the head's options for Slice A. Slices B + C + D + E are smaller and parallel-friendly. Different coders can pick them up after A lands. Slice F is a youpc-side task; it needs a worker with youpc-go familiarity (a separate hire on the youpc.org project). --- ## §16 Trade-offs flagged - **Atomic Slice A vs incremental** — atomic carries one large-PR regression risk; incremental carries drift risk. Picked atomic. §8 justifies. - **Embedded snapshot vs read-replica access from youpc** — snapshot wins on independence + offline use, loses on freshness. Picked snapshot. §4 justifies. - **`scenario_name` PK extension vs new `scenarios` table** — column extension wins on simplicity + back-compat. Picked column. §5 justifies. - **Per-project rules nullable `project_id` vs separate table** — nullable column wins on merge simplicity (one SELECT, two WHERE clauses). Picked nullable. §6 justifies. - **Type aliases (`UIResponse = Timeline`) vs full rename** — aliases keep call-sites byte-identical; rename forces a separate noise-PR. Picked aliases. §3 justifies. - **Snapshot in package vs separate `paliad-snapshots` repo** — in-package wins on simplicity; separate repo wins on release independence. Picked in-package per m's lock. §4 justifies. - **Public surface (UPC subset only) vs full multi-jurisdiction snapshot** — UPC-only matches the brief; multi-jurisdiction is opt-in via generator flag. Compatible. --- ## §17 Files of note for future workers **Anchor files (read these before touching anything):** - `internal/services/fristenrechner.go` (1505 LoC) — the body to extract. Slice A moves most of it. - `internal/services/deadline_calculator.go` (175 LoC) — already library-shaped. - `internal/services/projection_service.go` (2214 LoC) — STAYS in `internal/services` (paliad-specific). Slice A makes it import from the package. - `internal/services/deadline_rule_service.go` (352 LoC) — Slice B turns this into a Catalog impl. - `internal/services/holidays.go` (413 LoC) — Slice B turns this into a HolidayCalendar impl. - `internal/services/courts.go` (~150 LoC) — Slice B turns this into a CourtRegistry impl. - `internal/services/proceeding_mapping.go` (191 LoC) — Slice A moves it. - `internal/services/event_deadline_service.go` — uses `Calculate(... TriggerEventIDFilter ...)`. Slice A updates the import path. - `internal/services/rule_editor_service.go` — Slice E extends with `project_id` writes. - `internal/services/event_choice_service.go` — Slice D extends with `scenario_name` reads/writes. **Design doc cross-references:** - `docs/audit-fristen-logic-2026-05-13.md` (pauli, t-paliad-157) — the audit that justified the Phase 2 unification. - `docs/design-fristen-phase2-2026-05-15.md` (pauli, t-paliad-181) — the unified rule model, partially / largely shipped. - `docs/design-smart-timeline-2026-05-08.md` (lagrange, t-paliad-169) — `ProjectionService` design. - `docs/design-tools-cleanup-2026-05-12.md` (kelvin, t-paliad-178) — Fristenrechner vs Verfahrensablauf split. - `docs/design-determinator-row-cascade-2026-05-13.md` (pauli, t-paliad-166) — Determinator cascade — paliad-side UI, unaffected by this design. - `docs/design-event-card-choices-2026-05-25.md` — per-card choice surface; the `CalcOptions` extension that this design preserves verbatim. --- *End of design doc.*