m's 2026-05-20 14:08 reports on /tools/verfahrensablauf:
1. "There seems to be a lacking english term here" — picking
UPC CCR shows "Trigger event: Widerklage auf Nichtigkeit" on EN.
2. "Nothing shows in the roadmap" — the timeline is empty because
upc.ccr.cfi has no native rules (it's an illustrative peer that
normally runs as a sub-track of upc.inf.cfi with with_ccr).
Root cause for (1): UIResponse.proceedingName was DE-only. When a
proceeding had no root rule the frontend fell back to that field, so
EN users saw the DE label. The DB already has bilingual names; this
was pure plumbing.
Root cause for (2): the upc.ccr.cfi proceeding-type row exists for
the picker (mig 096) but ResolveCounterclaimRouting — the helper
that maps it to upc.inf.cfi with the with_ccr flag — was defined
but never called. Calculate queried rules directly off upc.ccr.cfi
and got an empty list.
Fix:
* Add ProceedingNameEN, ContextualNote, ContextualNoteEN to
UIResponse. Frontend triggerEventLabelFor now consults the EN
name on EN, falling back to DE only if the EN field is empty.
* New SubTrackRouting registry in proceeding_mapping.go and a
LookupSubTrackRouting lookup — single source of truth for the
"this proceeding has no native rules, route to a parent with
flags + show a contextual note" pattern. Today's only entry is
upc.ccr.cfi → upc.inf.cfi + with_ccr; the pattern generalises
to other sub-tracks via data-only additions.
* Calculate consults the registry at the top: when a hit, the
proceeding type is re-resolved to the parent for rule lookup, the
default flags are merged into the user's flag set (user flags win
on conflict), and the response identity (Code/Name/NameEN) stays
on the user-picked proceeding so the page header still reads
"Counterclaim for Revocation". The bilingual note surfaces in
ContextualNote{,EN}.
* Frontend renderResults paints a lime-accent banner above the
timeline body when the response carries a note
(.timeline-context-note). escHtml already exported from
views/verfahrensablauf-core — imported here for the banner.
No DB migration: SELECTs against paliad.proceeding_types,
paliad.deadline_rules, and paliad.trigger_events confirm every
active row already has a non-empty name_en / name. The bug was
the API + frontend never reading the EN columns through the
proceedingName fallback path.
Tests: TestSubTrackRoutings pins the registry shape (every entry
has matching key/value, non-empty parent+flags, bilingual notes;
CCR's exact shape is asserted; non-sub-tracks miss). The existing
TestResolveCounterclaimRouting continues to pass because the
helper now consults the registry but the CCR semantics are
unchanged.
192 lines
8.4 KiB
Go
192 lines
8.4 KiB
Go
package services
|
|
|
|
// proceeding_mapping bridges the two proceeding-type vocabularies in the
|
|
// codebase: the **litigation** conceptual category (INF / REV / APP /
|
|
// CCR / AMD / APM / ZPO_CIVIL) used by the historical project-binding
|
|
// + Pipeline-A rules, and the **fristenrechner** code category
|
|
// (upc.inf.cfi / de.inf.lg / epa.opp.opd / …) used by the Determinator
|
|
// cascade + rule engine. Post-Phase-3-Slice-5 (t-paliad-186) projects
|
|
// bind to fristenrechner codes directly, but the litigation→fristenrechner
|
|
// mapping is still needed for the ~40 Pipeline-A rules that remain on
|
|
// litigation proceedings and for any other surface that thinks in
|
|
// litigation terms.
|
|
//
|
|
// The mapping table here is the single source of truth — see
|
|
// docs/design-determinator-row-cascade-2026-05-13.md §4.2 for the
|
|
// design rationale + ambiguity notes, and
|
|
// docs/design-proceeding-code-taxonomy-2026-05-18.md for the
|
|
// lowercase dot-separated naming convention applied by mig 096
|
|
// (t-paliad-206). **Never silent FK promotion**: every ambiguous case
|
|
// returns ok=false so callers can degrade gracefully ("no narrowing")
|
|
// instead of guessing.
|
|
|
|
// Stable code constants — the strings landed by mig 096. Use these
|
|
// throughout the codebase so a future rename only needs to touch this
|
|
// file. The id-anchored FKs (deadline_rules.proceeding_type_id,
|
|
// projects.proceeding_type_id) are unaffected by the rename.
|
|
const (
|
|
CodeUPCInfringement = "upc.inf.cfi"
|
|
CodeUPCRevocation = "upc.rev.cfi"
|
|
CodeUPCCounterclaim = "upc.ccr.cfi"
|
|
CodeUPCPreliminary = "upc.pi.cfi"
|
|
CodeUPCDamages = "upc.dmgs.cfi"
|
|
CodeUPCDiscovery = "upc.disc.cfi"
|
|
CodeUPCAppealMerits = "upc.apl.merits"
|
|
CodeUPCAppealOrder = "upc.apl.order"
|
|
CodeUPCAppealCost = "upc.apl.cost"
|
|
CodeDEInfringementLG = "de.inf.lg"
|
|
CodeDEInfringementOLG = "de.inf.olg"
|
|
CodeDEInfringementBGH = "de.inf.bgh"
|
|
CodeDENullityBPatG = "de.null.bpatg"
|
|
CodeDENullityBGH = "de.null.bgh"
|
|
CodeEPAGrant = "epa.grant.exa"
|
|
CodeEPAOpposition = "epa.opp.opd"
|
|
CodeEPAOppositionAppeal = "epa.opp.boa"
|
|
CodeDPMAOpposition = "dpma.opp.dpma"
|
|
CodeDPMAAppealBPatG = "dpma.appeal.bpatg"
|
|
CodeDPMAAppealBGH = "dpma.appeal.bgh"
|
|
)
|
|
|
|
// MapLitigationToFristenrechner returns the fristenrechner code +
|
|
// condition flags implied by a (litigationCode, jurisdiction) pair.
|
|
//
|
|
// Inputs are case-sensitive — pass the canonical upper-snake form
|
|
// (e.g. "INF", "UPC"). Unrecognised codes or genuinely ambiguous
|
|
// combinations (APP+DE, ZPO_CIVIL+DE) return ok=false with a zero
|
|
// fristenrechner code; callers should treat that as "no narrowing"
|
|
// and leave the cascade wide-open rather than auto-pick.
|
|
//
|
|
// Condition flags are returned as a slice so callers can apply them
|
|
// alongside the fristenrechner code (CCR+UPC → upc.inf.cfi + with_ccr,
|
|
// AMD+UPC → upc.inf.cfi + with_amend). An empty slice means no flag
|
|
// context applies.
|
|
func MapLitigationToFristenrechner(litigationCode, jurisdiction string) (fristenrechnerCode string, conditionFlags []string, ok bool) {
|
|
switch litigationCode {
|
|
case "INF":
|
|
switch jurisdiction {
|
|
case "UPC":
|
|
return CodeUPCInfringement, nil, true
|
|
case "DE":
|
|
return CodeDEInfringementLG, nil, true
|
|
}
|
|
case "REV":
|
|
switch jurisdiction {
|
|
case "UPC":
|
|
return CodeUPCRevocation, nil, true
|
|
case "DE":
|
|
return CodeDENullityBPatG, nil, true
|
|
}
|
|
case "CCR":
|
|
// Counterclaim revocation — UPC fold-in is structural (the
|
|
// counterclaim lives inside an upc.inf.cfi proceeding with the
|
|
// with_ccr flag). DE Nichtigkeit is conceptually the same
|
|
// adversarial-validity test, no separate flag.
|
|
switch jurisdiction {
|
|
case "UPC":
|
|
return CodeUPCInfringement, []string{"with_ccr"}, true
|
|
case "DE":
|
|
return CodeDENullityBPatG, nil, true
|
|
}
|
|
case "AMD":
|
|
// Amendment-application bundled into upc.inf.cfi via with_amend.
|
|
// No DE / EPA / DPMA analogue today.
|
|
if jurisdiction == "UPC" {
|
|
return CodeUPCInfringement, []string{"with_amend"}, true
|
|
}
|
|
case "APP":
|
|
// Appeal is ambiguous in DE (OLG vs BGH) and the project
|
|
// model doesn't carry the instance hint we'd need to
|
|
// disambiguate. UPC is unambiguous — upc.apl.merits covers
|
|
// the merits appeal track for inf/rev/ccr/damages.
|
|
if jurisdiction == "UPC" {
|
|
return CodeUPCAppealMerits, nil, true
|
|
}
|
|
case "APM":
|
|
// Preliminary injunction / urgency procedure — UPC-only
|
|
// concept in the fristenrechner taxonomy.
|
|
if jurisdiction == "UPC" {
|
|
return CodeUPCPreliminary, nil, true
|
|
}
|
|
case "OPP":
|
|
// Opposition — primarily EPA. DPMA has dpma.opp.dpma but it
|
|
// doesn't surface from the litigation vocabulary today.
|
|
if jurisdiction == "EPA" {
|
|
return CodeEPAOpposition, nil, true
|
|
}
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
// ResolveCounterclaimRouting handles the determinator's
|
|
// upc.ccr.cfi illustrative-peer route: the code exists in the dropdown
|
|
// for taxonomic completeness, but no rules are attached to it. When the
|
|
// cascade resolves to upc.ccr.cfi we route the rule lookup back to
|
|
// upc.inf.cfi with a default with_ccr=true flag — see
|
|
// docs/design-proceeding-code-taxonomy-2026-05-18.md §0.3 sub-decision S1.
|
|
//
|
|
// `code` is the proceeding code the cascade resolved to. If it's
|
|
// upc.ccr.cfi, the function returns (CodeUPCInfringement,
|
|
// []string{"with_ccr"}, true). For any other code the function returns
|
|
// (code, nil, false) and callers proceed with the code unchanged. The
|
|
// boolean signals "routing was applied"; the caller can surface the hint
|
|
// "Regeln liegen auf upc.inf.cfi (with_ccr=true); wir leiten Sie dorthin
|
|
// weiter." in the UI.
|
|
func ResolveCounterclaimRouting(code string) (effectiveCode string, defaultFlags []string, routed bool) {
|
|
if route, ok := SubTrackRoutings[code]; ok {
|
|
return route.ParentCode, route.DefaultFlags, true
|
|
}
|
|
return code, nil, false
|
|
}
|
|
|
|
// SubTrackRouting describes a proceeding type that has no native rules
|
|
// of its own and is normally rendered inside a parent proceeding's flow
|
|
// with one or more condition flags enabled. The Procedure Roadmap
|
|
// (verfahrensablauf) routes calc requests for these codes to the parent
|
|
// proceeding + default flags, but preserves the user-picked code/name
|
|
// in the response identity and surfaces a contextual note explaining
|
|
// the framing — see m/paliad#58 and the design doc cited above.
|
|
//
|
|
// Adding a new sub-track is a data-only change here: extend
|
|
// SubTrackRoutings with the (code, parent, flags, note) tuple and the
|
|
// renderer picks it up automatically. The note copy lives in this file
|
|
// because it's semantic to the routing, not UI chrome.
|
|
type SubTrackRouting struct {
|
|
// Code is the user-picked proceeding code (e.g. "upc.ccr.cfi").
|
|
Code string
|
|
// ParentCode is the proceeding whose rules to use (e.g. "upc.inf.cfi").
|
|
ParentCode string
|
|
// DefaultFlags are merged into the user's flag set so the
|
|
// gated rules render. Order is preserved.
|
|
DefaultFlags []string
|
|
// NoteDE / NoteEN are the contextual banner above the timeline,
|
|
// explaining that the proceeding type is normally a sub-track.
|
|
// Plain text — the frontend renders them as a banner.
|
|
NoteDE string
|
|
NoteEN string
|
|
}
|
|
|
|
// SubTrackRoutings — single-source-of-truth registry. Today: just CCR.
|
|
// The pattern generalises to other "sub-track" proceeding types (e.g.
|
|
// R.30 application to amend the patent as a standalone roadmap, R.46
|
|
// preliminary objection) once they have a proceeding-type code of their
|
|
// own. New entries here are picked up by the spawn-as-standalone
|
|
// renderer in FristenrechnerService.Calculate without further wiring.
|
|
var SubTrackRoutings = map[string]SubTrackRouting{
|
|
CodeUPCCounterclaim: {
|
|
Code: CodeUPCCounterclaim,
|
|
ParentCode: CodeUPCInfringement,
|
|
DefaultFlags: []string{"with_ccr"},
|
|
NoteDE: "Die Nichtigkeitswiderklage läuft normalerweise innerhalb eines UPC-Verletzungsverfahrens mit aktiver Nichtigkeitswiderklage. Diese Zeitleiste zeigt das Verletzungsverfahren mit gesetztem with_ccr-Flag.",
|
|
NoteEN: "The counterclaim for revocation normally runs inside a UPC infringement action with the counterclaim flag set. This timeline shows the infringement action with with_ccr automatically enabled.",
|
|
},
|
|
}
|
|
|
|
// LookupSubTrackRouting returns the sub-track routing for a proceeding
|
|
// code, or (zero, false) if the code is not a sub-track. Used by the
|
|
// fristenrechner Calculate path to spawn the parent flow with the sub-
|
|
// track's default flags.
|
|
func LookupSubTrackRouting(code string) (SubTrackRouting, bool) {
|
|
r, ok := SubTrackRoutings[code]
|
|
return r, ok
|
|
}
|