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.
60 KiB
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: #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'sinternal/services/*implements theCatalog/HolidayCalendar/CourtRegistryinterfaces against Postgres; youpc.org importsembedded/upc.Catalog()+ friends and runs the same engine against a generator-produced JSON snapshot of paliad's UPC subset.
The convergence:
- Today the calc is 1505 LoC in
internal/services/fristenrechner.go+ 175 LoC indeadline_calculator.go+ helpers, all paliad-internal. - Phase 2 unification (t-paliad-181 / Slices 1-10, all shipped) collapsed Pipelines A + B + C onto a single
paliad.deadline_rulestable — 231 rows, 20 active proceeding types, lifecycle_state-gated, condition_expr-gated, 77 UPC rules. - 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/CourtRegistryinterfaces. - 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. - Scenarios + user-authored rules are paliad-side persistence concerns that the package's request shape (
CalcRequest) already supports viaRuleOverrides+ per-cardChoices. 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'sinternal/services/fristenrechner.gobecomes a 60-line shell. - Slice B —
Catalog/HolidayCalendar/CourtRegistryinterfaces + paliad's default loaders implementing them. - Slice C — embedded UPC snapshot +
scripts/snapshot-upc/main.gogenerator. - Slice D — scenarios persistence (
paliad.project_event_choices.scenario_name+paliad.projects.active_scenario). - Slice E — user-authored rules (
paliad.deadline_rules.project_idnullable 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-intimeline_kind) - Future-projected rows from
FristenrechnerService.Calculate(...)driven by the project'sproceeding_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
// 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
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
// 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
// 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
// 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
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)
// 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):
// 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:
- Connects to
$DATABASE_URL(paliad's Postgres). - Reads
paliad.proceeding_typesfiltered tois_active=trueANDcategory='fristenrechner'ANDjurisdiction IN ('UPC'). (One generator-level flag selects the jurisdiction subset; today onlyUPCis exported. Future generator runs can produceembedded/de/orembedded/epa/.) - For each proceeding, reads
paliad.deadline_rulesfiltered toproceeding_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.) - Reads
paliad.holidaysfor 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')). - Reads
paliad.courtsfor the UPC subset (regime='UPC'). - 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). - 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).
- Exits non-zero on any DB integrity check failure (orphan rules, dangling FKs, etc.).
# 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:
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-eventsflag.- 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
-- 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-choiceslearns to write against the active scenario (or accept an explicitscenario_namequery 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_choicestable already keys on(project_id, submission_code, choice_kind). Addingscenario_nameto 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
CalcOptionsfor it, and callsCalculate. 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
-- 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:
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 withproject_idset +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
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 onceDeadlineCalculatoris justlitigationplanner.Calculator).internal/services/event_deadline_service.go— useslitigationplanner.Calculate(...)withTriggerEventIDFilterset.- 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.gocontinue to run unchanged in paliad — they test the paliad-side wrappers which delegate to the package. - The package gets its own
pkg/litigationplanner/*_test.gowith 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.gois 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.
CountryRegimenow returns an error on unknown court) → breaking. - Drop a
Catalog/HolidayCalendar/CourtRegistrymethod → 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
- paliad merge to main bumps the package version when it touches
pkg/litigationplanner/. - 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. - youpc.org's CI runs
go get -uweekly (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:
# 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/Holidaytypes intopkg/litigationplanner/types.go.applyDuration,addWorkingDays,DeadlineCalculatorbody intopkg/litigationplanner/durations.go.evalConditionExpr+hasConditionExprintopkg/litigationplanner/expr.go.MapLitigationToFristenrechner+ code constants intopkg/litigationplanner/proceeding_mapping.go.SubTrackRoutings+LookupSubTrackRoutingintopkg/litigationplanner/subtrack.go.FormatLegalSourceDisplay+BuildLegalSourceURLintopkg/litigationplanner/legal_source.go.DefaultsForJurisdictionintopkg/litigationplanner/courts.go.Calculate+CalculateRulebodies intopkg/litigationplanner/engine.go— using the Catalog / HolidayCalendar / CourtRegistry interfaces.- Type aliases in
internal/services/fristenrechner.goso call-sites continue to importservices.UIResponseetc.
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.gois < 100 lines and consists of the type aliases + thinCalculate/CalculateRuleshells.
~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— addProceeding(ctx, code, hint)method satisfyingCatalog.internal/services/holidays.go— already hasIsNonWorkingDay/AdjustForNonWorkingDays; addAdjustForNonWorkingDaysBackwardshim if not exposed.internal/services/courts.go—CountryRegimealready exists, satisfiesCourtRegistry.
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=UPCemits 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— addscenario_name+ new PK +active_scenarioonpaliad.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— nullableproject_idcolumn + composite uniqueness adjustment.internal/services/deadline_rule_service.go— learnListForProceeding(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/fristenrechneror 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
-
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.
-
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.
-
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 inpkg/litigationplanner/CHANGELOG.md; youpc.org pins exact versions during pre-1.0. -
Catalog merge ambiguity (Slice E). A project-scoped rule with the same
submission_codeas a global rule — does it override or coexist? Mitigation: the unique constraint(proceeding_type_id, submission_code, project_id) NULLS NOT DISTINCTallows ONE global + N project-scoped to coexist. The catalog returns both; the engine sees both rows. Ordering bysequence_orderdecides which renders first. Document this explicitly in the rule-editor UX (no silent override). -
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 CONSTRAINTis atomic in a single transaction; down-migration restores. -
youpc.org snapshot binary footprint. ~110KB embedded JSON in youpc's binary. Mitigation: trivial. Not a risk in practice.
-
Spawn cycle guard ownership. Today
ProjectionService.expandCrossProceedingSpawnscarries the visited-set + maxSpawnDepth cycle guard. After the move, the guard lives in the package (it's part ofCalculate). 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 losesactive_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--jurisdictionflag in Slice C; the additional snapshots are a follow-up task per jurisdiction when demand surfaces. - Re-architecting
ProjectionServiceto live in the package. It speaks Postgres directly to load project + counterclaim children + actuals, which IS paliad-specific business logic. Only its singleCalculate(...)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 -uworkflow 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.tsalready 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.scenariostable. - 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_namePK extension vs newscenariostable — column extension wins on simplicity + back-compat. Picked column. §5 justifies.- Per-project rules nullable
project_idvs 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-snapshotsrepo — 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 ininternal/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— usesCalculate(... TriggerEventIDFilter ...). Slice A updates the import path.internal/services/rule_editor_service.go— Slice E extends withproject_idwrites.internal/services/event_choice_service.go— Slice D extends withscenario_namereads/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) —ProjectionServicedesign.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; theCalcOptionsextension that this design preserves verbatim.
End of design doc.