Inventor design for m/paliad#124. Atomic extract of FristenrechnerService / DeadlineCalculator / proceeding_mapping / SubTrackRoutings / legal-source helpers into pkg/litigationplanner with Catalog / HolidayCalendar / CourtRegistry interfaces. youpc.org reuse via embedded UPC snapshot (catalog.json + holidays.json + courts.json) shipped inside the package. 6 slices: A extract, B catalog interface, C embedded snapshot + generator, D scenarios persistence (project_event_choices.scenario_name), E user-authored rules (deadline_rules.project_id), F youpc-side PR. Q1 + Q2 (material) escalated to head per inventor protocol — NOT AskUserQuestion. Q3-Q5 locked. Decision picks (R) noted; doc holds together under any answer to the open Qs because pkg shape is decoupled from persistence choices.
1035 lines
60 KiB
Markdown
1035 lines
60 KiB
Markdown
# 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 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:
|
||
|
||
- **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** — scenarios persistence (`paliad.project_event_choices.scenario_name` + `paliad.projects.active_scenario`).
|
||
- **Slice E** — user-authored rules (`paliad.deadline_rules.project_id` nullable column + catalog merge).
|
||
- **Slice F** — youpc.org integration (separate PR on the youpc repo).
|
||
|
||
m's two locked decisions:
|
||
- **Package within paliad** (option 1, not a separate repo). ✓
|
||
- **Inventor must escalate Q1+Q2 to head via `mai instruct`, not AskUserQuestion.** ✓
|
||
|
||
---
|
||
|
||
## §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 (Q1)
|
||
|
||
### m's framing
|
||
|
||
> 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.
|
||
|
||
"Scenarios" = named "what if" projections for a project. The user has a UPC INF case; they want to compare:
|
||
- "what if we accept the offer to amend" (with_amend flag)
|
||
- "what if we file a CCR" (with_ccr flag)
|
||
- "what if both" (with_amend + with_ccr)
|
||
- "court-extended replik by 1 month" (anchor override on inf.reply)
|
||
|
||
Each of these is a complete set of choices. Today they're URL state (`?with_ccr&with_amend&choices=...`). Scenarios make them named + persisted + switchable from the project page.
|
||
|
||
### Recommended (R) — Option A from the brief, refined
|
||
|
||
Extend `paliad.project_event_choices` with a `scenario_name` column; add `paliad.projects.active_scenario` to track the user's current pick. No new table.
|
||
|
||
#### Schema
|
||
|
||
```sql
|
||
-- Slice D migration (paliad-side, NOT in pkg/litigationplanner).
|
||
ALTER TABLE paliad.project_event_choices
|
||
ADD COLUMN scenario_name text NOT NULL DEFAULT 'default';
|
||
|
||
-- Composite PK now includes scenario_name so a project can have N parallel
|
||
-- scenarios each with their own choice set.
|
||
ALTER TABLE paliad.project_event_choices
|
||
DROP CONSTRAINT IF EXISTS project_event_choices_pkey;
|
||
ALTER TABLE paliad.project_event_choices
|
||
ADD PRIMARY KEY (project_id, scenario_name, submission_code, choice_kind);
|
||
|
||
ALTER TABLE paliad.projects
|
||
ADD COLUMN active_scenario text NOT NULL DEFAULT 'default';
|
||
|
||
CREATE INDEX project_event_choices_scenario_idx
|
||
ON paliad.project_event_choices(project_id, scenario_name);
|
||
```
|
||
|
||
The default scenario (`'default'`) is automatic — every existing row backfills there, every new project starts there. The user is never *required* to name a scenario.
|
||
|
||
#### Endpoints
|
||
|
||
- `GET /api/projects/{id}/scenarios` — list scenario names + which is active.
|
||
- `POST /api/projects/{id}/scenarios` — create a new named scenario (clones the choices of the source scenario name or empty if first).
|
||
- `PUT /api/projects/{id}/scenarios/{name}/active` — set as active scenario.
|
||
- `DELETE /api/projects/{id}/scenarios/{name}` — remove (cannot delete `'default'`; cannot delete the active one without picking another first).
|
||
- The existing `POST /api/projects/{id}/event-choices` learns to write against the active scenario (or accept an explicit `scenario_name` query param).
|
||
|
||
#### UI
|
||
|
||
- Project page sub-header gains a "Szenario" chip group above SmartTimeline: `[default ▾] + Neu`.
|
||
- Click chip → switch active → SmartTimeline re-renders against new choice set.
|
||
- "+ Neu" → modal: name + base ("Leer" or clone-from-current).
|
||
- SmartTimeline + Akte-mode Fristenrechner both read the active scenario's choices.
|
||
|
||
#### Why this design
|
||
|
||
- **No new table** — the existing `project_event_choices` table already keys on `(project_id, submission_code, choice_kind)`. Adding `scenario_name` to the PK partitions naturally.
|
||
- **Backward compat** — every existing row gets `scenario_name='default'`; nothing breaks.
|
||
- **Library-side neutrality** — the package never sees scenarios. paliad's catalog/handler reads which scenario is active, builds the `CalcOptions` for it, and calls `Calculate`. youpc.org doesn't need scenarios at all (no projects).
|
||
- **Symmetry with Verfahrensablauf** — Verfahrensablauf stays URL-only (no projects, no persistence) — that surface is for abstract exploration, where URL-state IS the "scenario" (shareable, ephemeral, no name).
|
||
|
||
#### Rejected: Option B (new `paliad.scenarios` table)
|
||
|
||
Cleaner separation but duplicates the (project_id, choices) relationship. The composite-PK extension on `project_event_choices` is one column + one DEFAULT + one PK swap — strictly less work, no duplication.
|
||
|
||
#### Rejected: Option C (URL-only, no persistence)
|
||
|
||
Loses the "name it, switch to it" affordance m's framing implies. URL-only scenarios already exist on Verfahrensablauf; the project-page version needs more.
|
||
|
||
### Escalation to head (Q1)
|
||
|
||
The package design works for any of A / B / C. The schema choice is paliad-side. **Recommendation = A** as designed above. If m flags a need for cross-project scenario sharing later (firm-shared templates), B becomes the cleaner base; A → B is a one-migration upgrade.
|
||
|
||
---
|
||
|
||
## §6 User-authored rules design (Q2)
|
||
|
||
### 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=<id>` → 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 (Q1)
|
||
|
||
**Scope** — paliad-side schema + endpoints + UI.
|
||
|
||
**Files**:
|
||
- `internal/db/migrations/134_project_event_choices_scenario.up.sql` + `.down.sql` — add `scenario_name` + new PK + `active_scenario` on `paliad.projects`.
|
||
- `internal/services/scenario_service.go` (new) — Create/List/SetActive/Delete.
|
||
- `internal/handlers/scenarios.go` (new) — wire endpoints.
|
||
- `internal/services/event_choice_service.go` — learn to write/read against active scenario.
|
||
- `internal/services/projection_service.go` — consume active scenario's choices when building CalcOptions.
|
||
- `frontend/src/components/ScenarioChips.tsx` (new) + `frontend/src/client/projects-detail.ts` — UI surface.
|
||
|
||
**Exit criteria**:
|
||
- Project page shows scenario chips; create/switch/delete works.
|
||
- SmartTimeline + Akte-mode Fristenrechner respect active scenario.
|
||
- Existing projects function unchanged (everything in 'default').
|
||
|
||
### Slice E — user-authored rules (Q2)
|
||
|
||
**Scope** — paliad-side schema + rule-editor extension.
|
||
|
||
**Files**:
|
||
- `internal/db/migrations/135_deadline_rules_project_id.up.sql` + `.down.sql` — nullable `project_id` column + composite uniqueness adjustment.
|
||
- `internal/services/deadline_rule_service.go` — learn `ListForProceeding(ctx, code, hint)` with merge semantics.
|
||
- `internal/services/rule_editor_service.go` — extend to scope writes to a project_id when set.
|
||
- `internal/handlers/rule_editor.go` — accept `?project=<id>` scope param.
|
||
- `frontend/src/projects-detail.tsx` + `client/projects-detail.ts` — "Akte-spezifische Frist hinzufügen" affordance.
|
||
|
||
**Exit criteria**:
|
||
- A project-scoped rule appears in that project's SmartTimeline + Akte-Fristenrechner.
|
||
- The same rule does NOT appear in the public `/tools/fristenrechner` or in any other project.
|
||
- The snapshot generator's filter (`project_id IS NULL`) prevents leakage to youpc.
|
||
|
||
### 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@<tag>` 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
|
||
|
||
```
|
||
A → B → C → F (youpc integration on C)
|
||
↘
|
||
D + E (parallel, paliad-only, depend on B for catalog merge points)
|
||
```
|
||
|
||
A is the prerequisite for everything. B is the prerequisite for C, D, E. C is the prerequisite for F. D and E are parallel after B.
|
||
|
||
---
|
||
|
||
## §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 PK migration is destructive.** Slice D drops the existing PK and recreates it. **Mitigation**: small table (~50 rows in live data), standard `DROP CONSTRAINT` + `ADD CONSTRAINT` is atomic in a single transaction; down-migration restores.
|
||
|
||
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 for m (escalated via `mai instruct head`)
|
||
|
||
Per the inventor → head gate, the inventor does NOT call AskUserQuestion. The questions below are forwarded to `paliad/head`; m's answers come back via head's reply.
|
||
|
||
### Q1 — Scenarios storage model (material)
|
||
|
||
Recommendation: **Option A — extend `paliad.project_event_choices` with `scenario_name` text NOT NULL DEFAULT 'default' + add `paliad.projects.active_scenario`**. Reasoning §5. Alternatives:
|
||
- B: new `paliad.scenarios` table.
|
||
- C: URL-only scenarios, no persistence.
|
||
|
||
m's call gates Slice D. The library's shape doesn't depend on which is picked.
|
||
|
||
### Q2 — User-authored rules scope (material)
|
||
|
||
Recommendation: **Option (a) — per-project rule additions via `paliad.deadline_rules.project_id` nullable column**. Reasoning §6. Alternatives:
|
||
- (a)+(b): also support new user-authored proceeding types.
|
||
- (c): defer entirely.
|
||
|
||
m's call gates Slice E. If (b) is in scope, an additional Slice E2 is needed.
|
||
|
||
### 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.*
|