Atomic extraction of the deadline-rule compute engine + types from
internal/services into a new pkg/litigationplanner package that paliad
+ youpc.org can both import. No behaviour change — every existing test
passes against the post-move shape.
Package contents (~1850 LoC):
- doc.go package docstring + reuse manifesto
- types.go Rule, ProceedingType, NullableJSON, AdjustmentReason,
HolidayDTO, CalcOptions, CalcRuleParams, Timeline,
TimelineEntry, RuleCalculation*, FristenrechnerType,
ProjectHint, sentinel errors
- catalog.go Catalog interface (proceeding + rule lookups)
- holidays.go HolidayCalendar interface
- courts.go CourtRegistry interface + DefaultsForJurisdiction +
country/regime constants
- expr.go EvalConditionExpr + HasConditionExpr +
ExtractFlagsFromExpr (jsonb gate evaluator)
- durations.go ApplyDuration + AddWorkingDays (pure compute)
- subtrack.go SubTrackRouting + LookupSubTrackRouting registry
- legal_source.go FormatLegalSourceDisplay + BuildLegalSourceURL
- proceeding_mapping.go MapLitigationToFristenrechner + code constants
(CodeUPCInfringement, CodeDEInfringementLG, ...)
- engine.go Calculate + CalculateRule + the trigger-event
branch + applyRuleOverrides (the big move)
paliad side (~1900 LoC net deletion):
- internal/services/fristenrechner.go shrinks from 1505 → ~290 lines
(thin paliad Catalog adapter + type aliases for back-compat).
- internal/models/models.go: DeadlineRule, ProceedingType, NullableJSON
become type aliases to litigationplanner.* — every sqlx scan and
every projection_service caller compiles unchanged.
- internal/services/holidays.go: AdjustmentReason + HolidayDTO become
aliases to lp.* (canonical definitions now in the package).
- internal/services/proceeding_mapping.go: rewritten as thin re-exports
of lp constants + helpers.
- internal/services/deadline_search_service.go: FormatLegalSourceDisplay
+ BuildLegalSourceURL replaced with delegating wrappers to lp.
Catalog interface satisfaction:
- DeadlineRuleService → paliadCatalog adapter (wraps the existing
service, replicates the original SELECT shapes).
- HolidayService → satisfies lp.HolidayCalendar directly (compile-
time assertion at end of fristenrechner.go).
- CourtService → satisfies lp.CourtRegistry directly.
Wire shape is byte-identical. JSON tags on Rule / ProceedingType /
Timeline / TimelineEntry / RuleCalculation match the historical
UIResponse / UIDeadline shape; the frontend reads the same bytes.
Slice B (Catalog interface + paliad loader cleanup) is folded into
this commit since Slice A already needs the interfaces to call
Calculate across the boundary. Slice C (embedded UPC snapshot +
generator) is the next coder shift; the Berufung unification m
called out lands in Slice B/C per head's brief.
Refs: docs/design-litigation-planner-2026-05-26.md
124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
package litigationplanner
|
|
|
|
import "strings"
|
|
|
|
// FormatLegalSourceDisplay renders a structured legal_source code into
|
|
// the form HLC users read in pleadings:
|
|
//
|
|
// UPC.RoP.23.1 → "UPC RoP R.23(1)"
|
|
// UPC.RoP.139 → "UPC RoP R.139"
|
|
// DE.PatG.82.1 → "PatG §82(1)"
|
|
// DE.ZPO.276.1 → "ZPO §276(1)"
|
|
// EU.EPÜ.108 → "EPÜ Art.108"
|
|
// EU.EPC-R.79.1 → "EPC R.79(1)"
|
|
// EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
|
|
//
|
|
// Returns the empty string for an empty input. Unknown jurisdictions
|
|
// fall through with the structured form preserved (caller decides
|
|
// whether to display).
|
|
func FormatLegalSourceDisplay(src string) string {
|
|
src = strings.TrimSpace(src)
|
|
if src == "" {
|
|
return ""
|
|
}
|
|
parts := strings.Split(src, ".")
|
|
if len(parts) < 3 {
|
|
// Malformed — return as-is so the caller still has something.
|
|
return src
|
|
}
|
|
code := parts[1]
|
|
rest := parts[2:]
|
|
var prefix string
|
|
switch code {
|
|
case "RoP":
|
|
prefix = "UPC RoP R."
|
|
case "PatG":
|
|
prefix = "PatG §"
|
|
case "ZPO":
|
|
prefix = "ZPO §"
|
|
case "EPÜ":
|
|
prefix = "EPÜ Art."
|
|
case "EPC-R":
|
|
prefix = "EPC R."
|
|
case "RPBA":
|
|
prefix = "RPBA Art."
|
|
default:
|
|
prefix = code + " "
|
|
}
|
|
var b strings.Builder
|
|
b.Grow(len(prefix) + len(src))
|
|
b.WriteString(prefix)
|
|
b.WriteString(rest[0])
|
|
for _, p := range rest[1:] {
|
|
b.WriteByte('(')
|
|
b.WriteString(p)
|
|
b.WriteByte(')')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// BuildLegalSourceURL maps a structured legal_source code to a
|
|
// youpc.org/laws permalink when the cited body is hosted there. Today
|
|
// youpc only carries the UPC corpus (UPCA, UPCS, UPCRoP); DE national
|
|
// codes (PatG, ZPO) and EPO bodies (EPÜ, EPC-R, RPBA) have no youpc
|
|
// home yet, so the helper returns the empty string for those and the
|
|
// caller renders the display string as plain text.
|
|
//
|
|
// Inputs mirror FormatLegalSourceDisplay — structured dot-separated
|
|
// codes like UPC.RoP.23.1, UPC.UPCA.83. Sub-paragraph segments beyond
|
|
// the law-number position are dropped; youpc resolves the page at
|
|
// <type>.<number> granularity. The law-number is zero-padded to 3
|
|
// digits to match how youpc stores law_number (laws-data.json carries
|
|
// "001" / "023" / "220" forms).
|
|
//
|
|
// UPC.RoP.23.1 → https://youpc.org/laws#UPCRoP.023
|
|
// UPC.RoP.220.1 → https://youpc.org/laws#UPCRoP.220
|
|
// UPC.RoP.29.a → https://youpc.org/laws#UPCRoP.029
|
|
// UPC.UPCA.83 → https://youpc.org/laws#UPCA.083
|
|
// DE.ZPO.276.1 → "" (no youpc home — render display text plain)
|
|
func BuildLegalSourceURL(src string) string {
|
|
src = strings.TrimSpace(src)
|
|
if src == "" {
|
|
return ""
|
|
}
|
|
parts := strings.Split(src, ".")
|
|
if len(parts) < 3 {
|
|
return ""
|
|
}
|
|
var lawType string
|
|
switch parts[0] + "." + parts[1] {
|
|
case "UPC.RoP":
|
|
lawType = "UPCRoP"
|
|
case "UPC.UPCA":
|
|
lawType = "UPCA"
|
|
case "UPC.UPCS":
|
|
lawType = "UPCS"
|
|
default:
|
|
return ""
|
|
}
|
|
number := padLawNumber(parts[2])
|
|
if number == "" {
|
|
return ""
|
|
}
|
|
return "https://youpc.org/laws#" + lawType + "." + number
|
|
}
|
|
|
|
// padLawNumber zero-pads a pure-digit law-number segment to 3 digits.
|
|
// Non-digit-only inputs (e.g. "112a" if youpc ever ingests EPÜ Art.
|
|
// 112a) pass through unchanged so the URL still resolves. Empty input
|
|
// returns the empty string.
|
|
func padLawNumber(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
for _, c := range s {
|
|
if c < '0' || c > '9' {
|
|
return s
|
|
}
|
|
}
|
|
if len(s) >= 3 {
|
|
return s
|
|
}
|
|
return strings.Repeat("0", 3-len(s)) + s
|
|
}
|