Compare commits
5 Commits
mai/knuth/
...
mai/cronus
| Author | SHA1 | Date | |
|---|---|---|---|
| 528fe35540 | |||
| 9c2788ed8c | |||
| 568eac0aff | |||
| 733d21c930 | |||
| b05bcf7eeb |
342
cmd/seed-orphan-concept-drafts/main.go
Normal file
342
cmd/seed-orphan-concept-drafts/main.go
Normal file
@@ -0,0 +1,342 @@
|
||||
// Command seed-orphan-concept-drafts stages draft sequencing_rules for
|
||||
// deadline_concepts that have rule_count=0 ("orphans"). It calls the
|
||||
// same services.RuleEditorService.Create that POST
|
||||
// /admin/api/procedural-events runs internally, so the audit trigger
|
||||
// + INSTEAD-OF view trigger fan-out into procedural_events +
|
||||
// sequencing_rules + legal_sources fire identically. No HTTP/auth
|
||||
// shell, no direct SQL writes by this command.
|
||||
//
|
||||
// All rules are created with lifecycle_state='draft' (forced by the
|
||||
// service). The admin still reviews + publishes via
|
||||
// /admin/procedural-events.
|
||||
//
|
||||
// t-paliad-320: editorial backlog from t-paliad-193, four remaining
|
||||
// orphan concepts: counterclaim-for-revocation, versaeumnisurteil-
|
||||
// einspruch, schriftsatznachreichung, weiterbehandlung. The
|
||||
// weiterbehandlung concept gets two drafts (EPC Art. 121 + R. 135
|
||||
// versus DPatG § 123a) since the two regimes have different durations
|
||||
// and jurisdictions.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// DATABASE_URL=postgres://… go run ./cmd/seed-orphan-concept-drafts \
|
||||
// [-dry-run] [-reason "free-text audit reason"]
|
||||
//
|
||||
// Idempotency: the command refuses to insert if any rule for a given
|
||||
// (concept, proceeding_type, rule_code) already exists. Safe to re-run
|
||||
// after a partial failure.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"mgit.msbls.de/m/paliad/internal/services"
|
||||
)
|
||||
|
||||
// draftSpec captures one CreateRuleInput plus the metadata the command
|
||||
// needs to resolve concept_id + proceeding_type_id from human-readable
|
||||
// slugs/codes. ProceedingCode == "" means event-rooted
|
||||
// (proceeding_type_id = NULL), used for cross-cutting rules whose
|
||||
// jurisdiction has no matching proceeding_type yet.
|
||||
type draftSpec struct {
|
||||
Label string // human label for log output
|
||||
ConceptSlug string
|
||||
ProceedingCode string // "" → NULL proceeding_type_id (event-rooted)
|
||||
SubmissionCode string
|
||||
Name string
|
||||
NameEN string
|
||||
EventKind string
|
||||
PrimaryParty string // "" → omit (NULL)
|
||||
DurationValue int
|
||||
DurationUnit string
|
||||
Timing string
|
||||
Priority string
|
||||
IsCourtSet bool
|
||||
RuleCode string
|
||||
LegalSource string
|
||||
DeadlineNotes string
|
||||
DeadlineNotesEn string
|
||||
}
|
||||
|
||||
func drafts() []draftSpec {
|
||||
return []draftSpec{
|
||||
// ─── 1. counterclaim-for-revocation (UPC R.25.1 ∧ R.23) ───────
|
||||
{
|
||||
Label: "counterclaim-for-revocation → upc.ccr.cfi",
|
||||
ConceptSlug: "counterclaim-for-revocation",
|
||||
ProceedingCode: "upc.ccr.cfi",
|
||||
SubmissionCode: "upc.ccr.cfi.lodge",
|
||||
Name: "Widerklage auf Nichtigkeit (CCR)",
|
||||
NameEN: "Counterclaim for Revocation (CCR)",
|
||||
EventKind: "filing",
|
||||
PrimaryParty: "defendant",
|
||||
DurationValue: 3,
|
||||
DurationUnit: "months",
|
||||
Timing: "after",
|
||||
Priority: "mandatory",
|
||||
IsCourtSet: false,
|
||||
RuleCode: "RoP.025",
|
||||
LegalSource: "UPC.RoP.25.1",
|
||||
DeadlineNotes: "Die Widerklage auf Nichtigkeit (Counterclaim for Revocation, CCR) ist gemeinsam mit der Klageerwiderung (Statement of Defence) einzureichen — d. h. innerhalb von 3 Monaten ab Zustellung der Klageschrift " +
|
||||
"(R. 23 i. V. m. R. 25.1 RoP). Inhaltliche Anforderungen folgen R. 25-30 RoP (insbes. R. 25.1(a)-(c) zu Antrag, Tatsachen und Beweismitteln; R. 27 zu Verfahren nach Einreichung; R. 30 zu einem Antrag auf Änderung des Patents).",
|
||||
DeadlineNotesEn: "The Counterclaim for Revocation (CCR) must be lodged together with the Statement of Defence — i.e. within 3 months of service of the Statement of Claim " +
|
||||
"(Rule 23 in conjunction with Rule 25.1 RoP). Substantive requirements follow Rules 25-30 RoP (in particular R. 25.1(a)-(c) on the application, facts and evidence; R. 27 on post-filing procedure; R. 30 on any application to amend the patent).",
|
||||
},
|
||||
|
||||
// ─── 2. versaeumnisurteil-einspruch (ZPO § 339) ───────────────
|
||||
{
|
||||
Label: "versaeumnisurteil-einspruch → de.inf.lg",
|
||||
ConceptSlug: "versaeumnisurteil-einspruch",
|
||||
ProceedingCode: "de.inf.lg",
|
||||
SubmissionCode: "de.inf.lg.einspruch_vu",
|
||||
Name: "Einspruch gegen Versäumnisurteil",
|
||||
NameEN: "Objection to default judgment",
|
||||
EventKind: "filing",
|
||||
PrimaryParty: "defendant",
|
||||
DurationValue: 2,
|
||||
DurationUnit: "weeks",
|
||||
Timing: "after",
|
||||
Priority: "mandatory",
|
||||
IsCourtSet: false,
|
||||
RuleCode: "§ 339 ZPO",
|
||||
LegalSource: "DE.ZPO.339.1",
|
||||
DeadlineNotes: "Notfrist von 2 Wochen ab Zustellung des Versäumnisurteils (§ 339(1) ZPO). " +
|
||||
"Bei Auslandszustellung oder öffentlicher Bekanntmachung bestimmt das Gericht die Einspruchsfrist gesondert im Versäumnisurteil oder durch nachträglichen Beschluss (§ 339(2) ZPO) — in diesem Fall die gerichtlich festgesetzte Frist mit „Datum setzen“ überschreiben. " +
|
||||
"Form: schriftlich oder zu Protokoll der Geschäftsstelle (§ 340(1) ZPO); die Einspruchsbegründung kann bis zum Verhandlungstermin nachgereicht werden (§ 340(3) ZPO).",
|
||||
DeadlineNotesEn: "Statutory two-week emergency period (Notfrist) from service of the default judgment (§ 339(1) ZPO). " +
|
||||
"If service is abroad or by public notice, the court sets the objection period separately in the default judgment or by a subsequent order (§ 339(2) ZPO) — in that case override with the court-set date. " +
|
||||
"Form: in writing or before the registry clerk (§ 340(1) ZPO); substantive grounds may be filed up to the oral hearing (§ 340(3) ZPO).",
|
||||
},
|
||||
|
||||
// ─── 3. schriftsatznachreichung (ZPO § 283) ───────────────────
|
||||
{
|
||||
Label: "schriftsatznachreichung → de.inf.lg",
|
||||
ConceptSlug: "schriftsatznachreichung",
|
||||
ProceedingCode: "de.inf.lg",
|
||||
SubmissionCode: "de.inf.lg.nachreichung",
|
||||
Name: "Schriftsatznachreichung",
|
||||
NameEN: "Subsequent written submission",
|
||||
EventKind: "filing",
|
||||
PrimaryParty: "", // concept.party = "both" → no default
|
||||
DurationValue: 3,
|
||||
DurationUnit: "weeks",
|
||||
Timing: "after",
|
||||
Priority: "optional",
|
||||
IsCourtSet: true,
|
||||
RuleCode: "§ 283 ZPO",
|
||||
LegalSource: "DE.ZPO.283",
|
||||
DeadlineNotes: "Vom Gericht in der mündlichen Verhandlung gesetzte Schriftsatzfrist gem. § 283 ZPO. " +
|
||||
"Sie wird nur auf Antrag einer Partei bestimmt, die sich auf neues Vorbringen des Gegners nicht erklären konnte. " +
|
||||
"Die konkrete Frist (in der Praxis 2-3 Wochen) und der nachfolgende Verkündungstermin werden im Sitzungsprotokoll bzw. in der prozessleitenden Verfügung festgelegt — Default-Frist hier 3 Wochen, mit „Datum setzen“ überschreiben, sobald die Verfügung vorliegt. " +
|
||||
"Nach Fristablauf darf das Gericht keine weiteren Erklärungen mehr berücksichtigen (§ 283 S. 2, § 296a ZPO).",
|
||||
DeadlineNotesEn: "Court-set written-submission period under § 283 ZPO, granted on a party's application when it could not respond at the oral hearing to the opponent's new submissions. " +
|
||||
"The actual period (in practice 2-3 weeks) and the announcement date are set in the hearing record / case-management order — default 3 weeks here, override via „set date“ once the order is on the file. " +
|
||||
"After expiry, the court will disregard further submissions (§ 283 sent. 2, § 296a ZPO).",
|
||||
},
|
||||
|
||||
// ─── 4. weiterbehandlung — EPC variant (Art. 121 + R. 135) ────
|
||||
{
|
||||
Label: "weiterbehandlung (EPC) → epa.grant.exa",
|
||||
ConceptSlug: "weiterbehandlung",
|
||||
ProceedingCode: "epa.grant.exa",
|
||||
SubmissionCode: "epa.grant.exa.weiterbeh",
|
||||
Name: "Antrag auf Weiterbehandlung",
|
||||
NameEN: "Request for further processing",
|
||||
EventKind: "filing",
|
||||
PrimaryParty: "claimant",
|
||||
DurationValue: 2,
|
||||
DurationUnit: "months",
|
||||
Timing: "after",
|
||||
Priority: "mandatory",
|
||||
IsCourtSet: false,
|
||||
RuleCode: "Art. 121 EPÜ",
|
||||
LegalSource: "EU.EPC-R.135.1",
|
||||
DeadlineNotes: "Antrag auf Weiterbehandlung gem. Art. 121 EPÜ i. V. m. R. 135(1) EPÜ — 2 Monate ab Zustellung der Mitteilung über die Fristversäumung bzw. den eingetretenen Rechtsverlust. " +
|
||||
"Der Antrag wird durch Zahlung der vorgeschriebenen Weiterbehandlungsgebühr gestellt; die versäumte Handlung muss innerhalb derselben 2-Monats-Frist nachgeholt werden (R. 135(1) EPÜ). " +
|
||||
"Die Frist ist nicht verlängerbar. Ausgeschlossen sind insbesondere die Frist für die Weiterbehandlung selbst sowie die in R. 135(2) EPÜ ausdrücklich aufgeführten Fristen (u. a. die Beschwerdefrist nach Art. 108 EPÜ, die Prioritätsfrist nach Art. 87 EPÜ und die Frist zur Wiedereinsetzung).",
|
||||
DeadlineNotesEn: "Request for further processing under Article 121 EPC in conjunction with Rule 135(1) EPC — two months from notification of the communication concerning the missed time limit or the loss of rights. " +
|
||||
"The request is made by payment of the further-processing fee; the omitted act must be completed within the same two-month period (Rule 135(1) EPC). " +
|
||||
"The period is non-extendable. Excluded: the further-processing period itself and the periods listed in Rule 135(2) EPC (notably the appeal period under Art. 108 EPC, the priority period under Art. 87 EPC, and the re-establishment period).",
|
||||
},
|
||||
|
||||
// ─── 5. weiterbehandlung — DPatG § 123a variant ───────────────
|
||||
// No `dpma.grant.*` proceeding_type exists yet, so this rule is
|
||||
// event-rooted (proceeding_type_id NULL) — same pattern as 78
|
||||
// other cross-cutting rules. Editorial follow-up: create a
|
||||
// `dpma.grant.dpma` proceeding_type and reassign.
|
||||
{
|
||||
Label: "weiterbehandlung (DPatG § 123a) → event-rooted (NULL proceeding_type)",
|
||||
ConceptSlug: "weiterbehandlung",
|
||||
ProceedingCode: "", // event-rooted
|
||||
SubmissionCode: "dpma.grant.weiterbeh",
|
||||
Name: "Antrag auf Weiterbehandlung (DPMA)",
|
||||
NameEN: "Request for further processing (DPMA, § 123a PatG)",
|
||||
EventKind: "filing",
|
||||
PrimaryParty: "claimant",
|
||||
DurationValue: 1,
|
||||
DurationUnit: "months",
|
||||
Timing: "after",
|
||||
Priority: "mandatory",
|
||||
IsCourtSet: false,
|
||||
RuleCode: "§ 123a PatG",
|
||||
LegalSource: "DE.PatG.123a.1",
|
||||
DeadlineNotes: "Antrag auf Weiterbehandlung einer DPMA-Patentanmeldung gem. § 123a PatG — 1 Monat ab Zustellung der Mitteilung über die Rechtsfolge der Fristversäumung. " +
|
||||
"Innerhalb dieser Frist müssen (i) der Antrag schriftlich gestellt, (ii) die versäumte Handlung nachgeholt und (iii) die Weiterbehandlungsgebühr nach Patentkostengesetz (PatKostG) gezahlt werden. " +
|
||||
"§ 123a PatG erfasst ausschließlich Anmeldungsfristen, deren Versäumung kraft Gesetzes die Zurückweisung der Anmeldung zur Folge hat. Für sonstige Fristversäumnisse kommt nur die Wiedereinsetzung nach § 123 PatG in Betracht (1 Monat ab Wegfall des Hindernisses, max. 1 Jahr ab Fristablauf). " +
|
||||
"HINWEIS — Taxonomie: bisher kein dpma.grant.* proceeding_type vorhanden; Regel daher event-rooted (proceeding_type_id NULL). Editorial follow-up: dpma.grant.dpma proceeding_type anlegen und diese Regel umhängen.",
|
||||
DeadlineNotesEn: "Request for further processing of a DPMA patent application under § 123a PatG — 1 month from notification of the consequence of the missed deadline. " +
|
||||
"Within this period the applicant must (i) file the written request, (ii) complete the omitted act, and (iii) pay the further-processing fee under the German Patent Costs Act (PatKostG). " +
|
||||
"§ 123a PatG covers only application-stage deadlines whose statutory consequence is rejection. For other missed deadlines, re-establishment under § 123 PatG is the only route (1 month from removal of the obstacle, max 1 year from the missed deadline). " +
|
||||
"TAXONOMY NOTE: no dpma.grant.* proceeding_type exists yet; this rule is event-rooted (proceeding_type_id NULL). Editorial follow-up: create a dpma.grant.dpma proceeding_type and reassign this rule.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
dryRun := flag.Bool("dry-run", false, "log the planned drafts but do not write")
|
||||
reason := flag.String("reason", "t-paliad-320: editorial seed of orphan deadline-concept rules (researcher darwin + lex)", "audit reason recorded with each Create()")
|
||||
flag.Parse()
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
log.Fatal("DATABASE_URL not set — export the paliad postgres URL before running")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := sqlx.Connect("postgres", dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("connect db: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
rules := services.NewDeadlineRuleService(conn)
|
||||
editor := services.NewRuleEditorService(conn, rules)
|
||||
|
||||
conceptIDs := map[string]uuid.UUID{}
|
||||
proceedingIDs := map[string]int{}
|
||||
specs := drafts()
|
||||
|
||||
for _, s := range specs {
|
||||
if _, ok := conceptIDs[s.ConceptSlug]; ok {
|
||||
continue
|
||||
}
|
||||
var id uuid.UUID
|
||||
if err := conn.GetContext(ctx, &id,
|
||||
`SELECT id FROM paliad.deadline_concepts WHERE slug = $1`, s.ConceptSlug); err != nil {
|
||||
log.Fatalf("lookup concept %q: %v", s.ConceptSlug, err)
|
||||
}
|
||||
conceptIDs[s.ConceptSlug] = id
|
||||
}
|
||||
for _, s := range specs {
|
||||
if s.ProceedingCode == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := proceedingIDs[s.ProceedingCode]; ok {
|
||||
continue
|
||||
}
|
||||
var id int
|
||||
if err := conn.GetContext(ctx, &id,
|
||||
`SELECT id FROM paliad.proceeding_types WHERE code = $1`, s.ProceedingCode); err != nil {
|
||||
log.Fatalf("lookup proceeding_type %q: %v", s.ProceedingCode, err)
|
||||
}
|
||||
proceedingIDs[s.ProceedingCode] = id
|
||||
}
|
||||
|
||||
fmt.Printf("Seeding %d drafts (dry-run=%v)\n", len(specs), *dryRun)
|
||||
|
||||
for i, s := range specs {
|
||||
conceptID := conceptIDs[s.ConceptSlug]
|
||||
var procID *int
|
||||
if s.ProceedingCode != "" {
|
||||
p := proceedingIDs[s.ProceedingCode]
|
||||
procID = &p
|
||||
}
|
||||
|
||||
// Idempotency: refuse if a rule with the same (concept, proceeding,
|
||||
// rule_code) already exists in any lifecycle state.
|
||||
if existing, err := findExisting(ctx, conn, conceptID, procID, s.RuleCode); err != nil {
|
||||
log.Fatalf("[%d] idempotency check failed for %s: %v", i+1, s.Label, err)
|
||||
} else if existing != uuid.Nil {
|
||||
fmt.Printf(" [%d] SKIP %s — already exists as %s\n", i+1, s.Label, existing)
|
||||
continue
|
||||
}
|
||||
|
||||
input := services.CreateRuleInput{
|
||||
Name: s.Name,
|
||||
NameEN: s.NameEN,
|
||||
ProceedingTypeID: procID,
|
||||
DurationValue: s.DurationValue,
|
||||
DurationUnit: s.DurationUnit,
|
||||
Priority: s.Priority,
|
||||
IsCourtSet: s.IsCourtSet,
|
||||
}
|
||||
input.ConceptID = &conceptID
|
||||
code := s.SubmissionCode
|
||||
input.SubmissionCode = &code
|
||||
ek := s.EventKind
|
||||
input.EventType = &ek
|
||||
t := s.Timing
|
||||
input.Timing = &t
|
||||
rc := s.RuleCode
|
||||
input.RuleCode = &rc
|
||||
ls := s.LegalSource
|
||||
input.LegalSource = &ls
|
||||
dn := s.DeadlineNotes
|
||||
input.DeadlineNotes = &dn
|
||||
dne := s.DeadlineNotesEn
|
||||
input.DeadlineNotesEn = &dne
|
||||
if s.PrimaryParty != "" {
|
||||
pp := s.PrimaryParty
|
||||
input.PrimaryParty = &pp
|
||||
}
|
||||
|
||||
if *dryRun {
|
||||
fmt.Printf(" [%d] DRY %s (concept=%s, proc=%s, code=%s, %d %s, %s)\n",
|
||||
i+1, s.Label, conceptID, codeOrNil(procID), code, s.DurationValue, s.DurationUnit, s.RuleCode)
|
||||
continue
|
||||
}
|
||||
|
||||
row, err := editor.Create(ctx, input, *reason)
|
||||
if err != nil {
|
||||
log.Fatalf(" [%d] CREATE failed for %s: %v", i+1, s.Label, err)
|
||||
}
|
||||
fmt.Printf(" [%d] OK %s → id=%s lifecycle=%s\n",
|
||||
i+1, s.Label, row.ID, row.LifecycleState)
|
||||
}
|
||||
|
||||
fmt.Println("Done.")
|
||||
}
|
||||
|
||||
func findExisting(ctx context.Context, conn *sqlx.DB, conceptID uuid.UUID, procID *int, ruleCode string) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
q := `
|
||||
SELECT sr.id
|
||||
FROM paliad.sequencing_rules sr
|
||||
JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
|
||||
WHERE pe.concept_id = $1
|
||||
AND sr.rule_code IS NOT DISTINCT FROM $2
|
||||
AND sr.proceeding_type_id IS NOT DISTINCT FROM $3
|
||||
LIMIT 1`
|
||||
err := conn.GetContext(ctx, &id, q, conceptID, ruleCode, procID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func codeOrNil(p *int) string {
|
||||
if p == nil {
|
||||
return "<NULL>"
|
||||
}
|
||||
return fmt.Sprintf("%d", *p)
|
||||
}
|
||||
553
docs/design-fristenrechner-overhaul-2026-05-26.md
Normal file
553
docs/design-fristenrechner-overhaul-2026-05-26.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# Design — Fristenrechner complete UX overhaul (dual-mode + project write-back)
|
||||
|
||||
**Task:** t-paliad-322
|
||||
**Gitea:** m/paliad#146
|
||||
**Inventor:** cronus (shift-1)
|
||||
**Date:** 2026-05-26
|
||||
**Status:** Draft for m's ratification — coder gate held
|
||||
|
||||
## 0. Premises verified live (before designing)
|
||||
|
||||
Verified against the live youpc Postgres (port 11833, paliad schema) and the live source tree on `mai/cronus/inventor-fristenrechner` @ HEAD.
|
||||
|
||||
### 0.1 Rule-and-event corpus today
|
||||
|
||||
| Table | Active+published rows | Notes |
|
||||
|---|---|---|
|
||||
| `paliad.procedural_events` | 222 (236 total) | The events that anchor a deadline. 4 `event_kind` buckets: `filing`, `hearing`, `decision`, `order`, plus `NULL` for legacy/dpma stragglers. |
|
||||
| `paliad.sequencing_rules` | 231 | The deadlines themselves, anchored on `procedural_event_id` and (sometimes) `trigger_event_id`. 80 carry a `trigger_event_id`, 4 are `is_spawn=true`, 45 are `is_court_set=true`, 18 carry a `condition_expr`. |
|
||||
| `paliad.deadline_concepts` | 57 | Hub layer above events (Klageerhebung, Wiedereinsetzung, …). |
|
||||
| `paliad.proceeding_types` | 46 fristenrechner | 4 jurisdictions: UPC (35), DE (5), EPA (3), DPMA (3). |
|
||||
| `paliad.event_categories` | 125 (103 leaves) | The current cascade tree — 5 user-bucket roots (`cms-eingang`, `muendl-verhandlung`, `beschluss-entscheidung`, `frist-verpasst`, `ich-moechte-einreichen`) + `sonstiges` leaf. UI hides the forward-workflow root (`HIDDEN_CASCADE_ROOTS` in `client/fristenrechner.ts:2605`). |
|
||||
| `paliad.deadlines` | 10 (8 with `sequencing_rule_id`) | Demand-side still tiny. The 2 without `sequencing_rule_id` are manual entries. |
|
||||
|
||||
Live `primary_party` vocabulary on `sequencing_rules`: `claimant`, `defendant`, `both`, `court`, `NULL`. Live `priority` vocabulary: `mandatory`, `recommended`, `optional` (no `informational` rows yet — Phase 2 reserved the slot but seeding is deferred).
|
||||
|
||||
### 0.2 The legacy `deadline_rules` reader is a view
|
||||
|
||||
`paliad.deadline_rules_unified` (mig 139, Slice B.3) is a **view** over `sequencing_rules ⋈ procedural_events ⋈ legal_sources`. All Go calculator paths read through it (see `deadline_rule_service.go:70`). The physical `paliad.deadline_rules` table was dropped in mig 140; the view is the canonical legacy-shape reader. Important for this design: there is no "trigger event" table parallel to events — the rule rows themselves are the things the wizard must land on. `trigger_events` (110 rows) is the youpc-parity legacy table used by `/api/tools/event-deadlines` only.
|
||||
|
||||
### 0.3 The frontend today (`/tools/fristenrechner`)
|
||||
|
||||
Two server-rendered surfaces share the same page (`frontend/src/fristenrechner.tsx`, 657 lines) — the legacy "Procedure mode" (R1 step-list, proceeding picker, trigger date, flag checkboxes) and the **Pathway-B row stack** (`buildRowStack` at `client/fristenrechner.ts:2848`, 4009 lines total). Row stack composes three row kinds via a single `.fristen-row` primitive:
|
||||
|
||||
| Row | Source | Filter or qualifier today |
|
||||
|---|---|---|
|
||||
| R1 Perspective (Beide / Klägerseite / Beklagtenseite) | `currentPerspective`, prefilled from `project.our_side` | hybrid — narrows party-tagged cascade chips AND is used as a column-bucket hint in the result view |
|
||||
| R2 Inbox channel (CMS / beA / Postal / Alle) | `currentInboxChannel` | filter — narrows cascade by forum (CMS → upc, beA → de, …) |
|
||||
| R3..Rn Cascade chain | `event_categories` tree | each step narrows children by `inboxFilterAllowsForums` + `perspectiveAllowsParty` + `cascadeChildAllowsProject` |
|
||||
|
||||
The cascade auto-walks single-child branches under a project context and stops at the first branching point. The user picks a leaf; the leaf's slug feeds `/api/tools/fristenrechner/search?event_category_slug=…` which returns concept cards. Each card expands inline to a calc panel (trigger-date input + flags + computed deadline + "in Akte" CTA).
|
||||
|
||||
### 0.4 What is broken in this UI (m's verdict, 2026-05-26 21:21)
|
||||
|
||||
m's brief in m/paliad#146 enumerates four visible bugs:
|
||||
|
||||
1. **"Beide" as default perspective** is incoherent for the headline use case ("file a deadline because something happened" — you ARE one side).
|
||||
2. **R2 (inbox) does not constrain R3 (cascade)** the way a user expects — picking CMS still leaves "Mündliche Verhandlung" / "Frist verpasst" on the next step. (Cause: those roots have `forums=NULL` in the seed → neutral → visible from every inbox.)
|
||||
3. **Mixed axes** — the form layers filters (forum, inbox channel) on top of qualifiers (event-kind, perspective, proceeding_type) without making the difference visible. The user can't tell which picks narrow and which define.
|
||||
4. **Trigger vs follow-up confusion** — the wizard's purpose is to identify the *trigger event*, then surface the *follow-up deadlines*. Today that split is not reflected in the form. After landing on a leaf, the user gets a flat list of concept cards and has to figure out which one is "the thing that happened" vs "the thing I have to file next".
|
||||
|
||||
m's verdict: "complete overhaul. Should be easy to use."
|
||||
|
||||
### 0.5 Anchor files for the eventual coder
|
||||
|
||||
- `frontend/src/client/fristenrechner.ts` (4009 LoC) — page brain. `buildRowStack` @ L2848, `renderRowStack` @ L3112, `runB1Search` (concept-card render) downstream, `expandCardCalc` @ L1337 (inline calc panel), `openSaveModal` @ L290 + `submitSave` @ L374 (project write-back).
|
||||
- `frontend/src/fristenrechner.tsx` (657 LoC) — server-rendered shell. Contains both the Procedure-mode form **and** the Pathway-B row-stack scaffold. The new design replaces the row-stack scaffold; the procedure-mode form survives.
|
||||
- `internal/handlers/fristenrechner.go` + `_search.go` + `_event_categories.go` — three handler files. `POST /api/tools/fristenrechner` (procedure-mode calc), `GET /search` (concept cards), `GET /event-categories` (cascade tree).
|
||||
- `internal/services/fristenrechner.go` (661 LoC) — calculator adapter to `pkg/litigationplanner`. The calculator is **not** touched by this design.
|
||||
- `internal/handlers/deadlines.go:167` + `services/deadline_service.go:411` (`CreateBulk`) — the project write-back endpoint (`POST /api/projects/{id}/deadlines/bulk`). This survives; the design extends its caller.
|
||||
|
||||
### 0.6 Adjacent design docs to read alongside
|
||||
|
||||
- `docs/design-determinator-row-cascade-2026-05-13.md` — the row-cascade pillars (Project-driven narrowing / Visual hierarchy overhaul / Persistent row stack). This overhaul **keeps** Pillars 2 and 3 and reworks Pillar 1's contract.
|
||||
- `docs/design-fristen-phase2-2026-05-15.md` — the unified `sequencing_rules` model the calculator already runs on.
|
||||
- `docs/audit-fristen-logic-2026-05-13.md` — the trigger-event / Pipeline-A-vs-C distinction.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision
|
||||
|
||||
**One page, two complementary entry paths, one result surface, one write-back.**
|
||||
|
||||
```text
|
||||
┌───────────────────────── /tools/fristenrechner ─────────────────────────┐
|
||||
│ │
|
||||
│ ╭──────── Akte / kontextfrei ────────╮ (Step 0 — unchanged today) │
|
||||
│ │ HL-2024-001 ▼ | ohne Akte │ │
|
||||
│ ╰─────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ╭────── Entry mode tabs ──────╮ │
|
||||
│ │ [⚡ Direkt suchen] │ ◀── A: power user, search + chips │
|
||||
│ │ [🧭 Geführt] │ ◀── B: 3-5 question wizard │
|
||||
│ ╰─────────────────────────────╯ │
|
||||
│ │
|
||||
│ ┌── Mode A: Suche ──────────────┐ ┌── Mode B: Wizard ────────────────┐│
|
||||
│ │ search-box ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢ │ │ R1 Was ist passiert? ✓ filing ││
|
||||
│ │ filter chips: │ │ R2 Forum? ✓ UPC ││
|
||||
│ │ Forum · Proceeding · Event- │ │ R3 Verfahren? ✓ INF ││
|
||||
│ │ Kind · Partei │ │ R4 Welcher Schritt? [active] ││
|
||||
│ │ ┌ Ergebnis-Liste ────────────┐│ │ R5 Welche Seite? ✓ Kläger ││
|
||||
│ │ │ procedural_event hits ││ │ ││
|
||||
│ │ │ [Trigger einrasten →] ││ │ (Direkt-suchen ←) ││
|
||||
│ │ └────────────────────────────┘│ └───────────────────────────────────┘│
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ════ shared from here ═══════════════════════════════════════════════ │
|
||||
│ │
|
||||
│ ┌── Trigger event (locked) ──────────────────────────────────────────┐ │
|
||||
│ │ 📥 Klageschrift wurde eingereicht │ │
|
||||
│ │ upc.inf.cfi · Verletzungsverfahren · Klägerseite │ │
|
||||
│ │ Trigger-Datum: [📅 2026-05-20] (heute) │ │
|
||||
│ │ ändern ↩ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌── Folge-Fristen ────────────────────────────────────────────────────┐ │
|
||||
│ │ ◉ MANDATORY (auto-checked) │ │
|
||||
│ │ ☑ Klageerwiderung (3 Monate) — 20.08.2026 — RoP 23 ✏ Datum │ │
|
||||
│ │ ☑ ... │ │
|
||||
│ │ ◇ OPTIONAL │ │
|
||||
│ │ □ Wiedereinsetzungsantrag (R.320) — bei Versäumnis │ │
|
||||
│ │ ◊ CONDITIONAL │ │
|
||||
│ │ □ Erwiderung auf Nichtigkeitswiderklage nur wenn CCR │ │
|
||||
│ │ ⇲ SPAWNED │ │
|
||||
│ │ ☑ Berufung gegen Endurteil (kein Datum) │ │
|
||||
│ │ ╭────────────────────────────╮ │ │
|
||||
│ │ │ 4 ausgewählt → in Akte ▶ │ │ │
|
||||
│ │ ╰────────────────────────────╯ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The two modes never compete: they're two front doors into the **same** locked-trigger-event → follow-up-list → write-back flow.
|
||||
|
||||
---
|
||||
|
||||
## 2. Axis taxonomy — ratified (filters vs qualifiers)
|
||||
|
||||
The headline source of today's UX confusion is the unmarked mixing of *filters* (narrowing the question space without committing to an answer) and *qualifiers* (parts of the eventual deadline definition).
|
||||
|
||||
| Axis | Role | Source | Constrains | Visual in new UI |
|
||||
|---|---|---|---|---|
|
||||
| `forum` | **filter** | derived from `proceeding_types.jurisdiction` (UPC/DE/EPA/DPMA), or from `project.proceeding_type_id`, or user pick | which `proceeding_types` are reachable; which `event_categories` are visible | Mode A: filter chip strip. Mode B: explicit wizard row (R2). Pre-filled + collapsed when there's a project. |
|
||||
| `proceeding_type` | **qualifier** | `project.proceeding_type_id` (binds via mig 096 codes) OR user pick during wizard | the set of `sequencing_rules` rows that can apply | Mode A: filter chip strip. Mode B: explicit wizard row (R3). Pre-filled + collapsed when there's a project. |
|
||||
| `event_kind` | **filter** | `procedural_events.event_kind` (filing / hearing / decision / order) | which `procedural_events` are reachable as triggers | Mode A: filter chip strip. Mode B: explicit wizard row (R1 — the headline question). |
|
||||
| `inbox channel` | **filter** (today) → **out of scope row** (new) | user pick | nothing the user can see (the rule corpus has no "inbox" column; it was only used to recolour the cascade) | Removed from the primary wizard. Pushed into Mode A's secondary chips (off by default). See §3.3. |
|
||||
| `perspective (our_side)` | **qualifier in file-mode**, **filter in explore-mode** | `project.our_side` OR user pick OR implicit-via-event-kind | `sequencing_rules.primary_party`; result-view column bucketing | Wizard tail (R5) **only when** the trigger event's follow-ups actually differ by side. Pre-filled when project has `our_side`. |
|
||||
| `instance_level` (first / appeal / cassation) | qualifier | `project.instance_level` (mig 084) — sparse | rare — used to disambiguate APP+DE | Surfaced only when the wizard hits APP+DE-style ambiguity. |
|
||||
|
||||
**Rule:** a **filter** narrows the visible options without locking in a deadline answer; it can be cleared and re-applied. A **qualifier** is part of the resulting deadline calculation and is locked the moment it's picked. Filters must propagate forward (Mode A's forum-chip narrows the proceeding-chip's options). Qualifiers are picked once and the answer view reads them.
|
||||
|
||||
The "Beide" perspective default (today's bug) is wrong because perspective is a *qualifier* in the headline use case ("file a deadline because something happened — you are one side"), not a *filter*. New default in Mode B: derive from the project's `our_side`, otherwise force a R5 pick (no "Beide"). See Q8 for the explore-mode exception.
|
||||
|
||||
---
|
||||
|
||||
## 3. Mode taxonomy
|
||||
|
||||
### 3.1 Mode A — "⚡ Direkt suchen" (power user)
|
||||
|
||||
Two visually distinct strips (per m §11.Q3):
|
||||
|
||||
```text
|
||||
┌── Filter (eingrenzen) ─────────────────────────────────────────────────┐
|
||||
│ Forum: [UPC] [DE] [EPA] [DPMA] [Alle] │
|
||||
│ Verfahren: [upc.inf.cfi] [...] [Alle] │
|
||||
│ Was passierte: [📥 Eingereicht] [🏛️ Termin] [⚖️ Entscheidung] [📜 Verfügung] [Alle] │
|
||||
│ Partei: [Klägerseite] [Beklagtenseite] [Beide] │
|
||||
├── Suchen ──────────────────────────────────────────────────────────────┤
|
||||
│ 🔎 [_______________________________________________________________] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌── Ergebnisse (klicken = als Trigger einrasten) ────────────────────────┐
|
||||
│ 📥 Klageerhebung · upc.inf.cfi · UPC · 3 Folge-Fristen → │
|
||||
│ ... │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Single text input, four filter chip strips above it (Forum · Proceeding · Event-Kind · Partei), and a ranked result list of `procedural_events` underneath. The "Filter" strip is visibly grouped (e.g. light background + "Filter (eingrenzen)" header) so users see at a glance that those picks narrow but don't commit; clicking a result row IS the commit (the qualifier action).
|
||||
|
||||
- Search hits `/api/tools/fristenrechner/search` (extended to return events, not just concepts — see §6.1).
|
||||
- Filter chips compose with the text query (`?forum=upc&pt=upc.inf.cfi&kind=filing&party=defendant&q=Klageerwiderung`).
|
||||
- Result rows are individual `procedural_events` (not aggregated concept-cards). Each row shows: name (DE/EN), proceeding_type code, jurisdiction badge, event_kind icon, the rule-count it triggers ("3 Folge-Fristen").
|
||||
- Click a row → "lock as trigger event" → page transitions to the §4 result view.
|
||||
- Power affordance: a row with multiple linked rules can be locked in **per-rule** ("nur diese Frist") via a kebab menu on the row. (Sane default: lock the whole event; the kebab is for the lawyer who only wants one specific reactive deadline.)
|
||||
|
||||
### 3.2 Mode B — "🧭 Geführt" (the wizard)
|
||||
|
||||
A 3-5 question row stack that lands on one `procedural_events` row.
|
||||
|
||||
**Question order (strawman; m to ratify in Q5):**
|
||||
|
||||
1. **R1 — Was ist passiert?** Chips: 📥 Eingereicht (`filing`) · 🏛️ Termin (`hearing`) · ⚖️ Entscheidung (`decision`) · 📜 Verfügung (`order`) · 🕒 Frist versäumt (special bucket, routes to Wiedereinsetzung). One chip = one `event_kind` (or the special). Always asked. ~6 chips, fits one row.
|
||||
2. **R2 — Vor welchem Gericht / bei welchem Amt?** Chips: UPC · LG/OLG/BGH · EPA · DPMA. Pre-filled from `project.proceeding_type → jurisdiction` (or `project.court` substring). **Skipped if R1 narrows to a single forum** (e.g. "Termin" + project has UPC → R2 is implied).
|
||||
3. **R3 — In welchem Verfahren?** Chips: every active `proceeding_type` whose jurisdiction matches R2 AND whose event roster contains at least one event with R1's kind. Pre-filled from `project.proceeding_type_id`. **Auto-skipped** when the narrowed scope has only one candidate.
|
||||
4. **R4 — Welches Schriftstück / Welcher Termin?** This is the wizard's landing question. Chips = `procedural_events` filtered by (R2 forum, R3 proceeding_type, R1 event_kind). Typical scope: 1-12 events. If the user types into this row, the chip layout flips to a search list (same widget as Mode A's result list, narrowed to the wizard's filters).
|
||||
5. **R5 — Vertreten Sie Kläger- oder Beklagtenseite?** Asked **only when** the selected event's `sequencing_rules` have follow-ups that differ by `primary_party` (a quick "are there both claimant- and defendant-tagged rules among the follow-ups?" check on the catalog). Pre-filled from `project.our_side`. **Skipped otherwise.**
|
||||
|
||||
**Row badges** (per m §11.Q3): each wizard row carries a small "Filter" or "Qualifier" tag next to its row-number badge. R1 (event_kind), R2 (forum) → "Filter". R3 (proceeding_type), R4 (procedural_event), R5 (perspective) → "Qualifier". A user can tell at a glance which picks lock in vs which narrow.
|
||||
|
||||
Branching policy (locked):
|
||||
|
||||
- Pre-fill + collapse a row when the answer is implied by the project (Determinator §4 pattern, unchanged).
|
||||
- Auto-skip a row when the narrowed scope has exactly one option (the user has effectively no choice). Show the skipped row as a compact `.fristen-row.is-prefilled` line with "(aus Akte)" or "(implizit aus R1)" annotation. *Don't hide the row* — m's "see your selections" pillar from the row-cascade design demands every decision stays visible.
|
||||
- A user-edited upstream answer **preserves compatible downstream picks** (m §11.Q10): if a re-picked R2 (forum) keeps the existing R1 (event_kind) legal, R1 stays; if it makes R3 (proceeding_type) illegal, R3 resets to active. Rows whose pick was carried across an upstream change render with a one-render "erhalten" annotation so the user notices.
|
||||
- "Welches Schriftstück?" (R4) is the landing question. Once R4 is answered, the wizard exits and the §4 result view takes over.
|
||||
|
||||
### 3.3 The dropped `inbox channel` row
|
||||
|
||||
R2-inbox in today's row stack is removed from the primary surface for both modes. Rationale:
|
||||
|
||||
- The rule corpus has no `inbox` column. The cascade's `forums=['cms']` etc. tags were a presentation-layer reflection of which forum naturally arrives on which channel — but the rule itself doesn't change based on whether a UPC document arrived via CMS or by post (it can't; only CMS is legal). So the only honest role for "inbox" is to nudge the forum filter on Mode A.
|
||||
- Mode A keeps inbox as a *secondary* chip strip ("Erweitert" toggle, off by default). Picking CMS auto-sets the forum chip to UPC; picking beA auto-sets it to DE. The user can override.
|
||||
- Mode B never asks. The wizard derives forum from project context or from R2.
|
||||
|
||||
This collapses one bug class entirely (R2-not-constraining-R3) by retiring R2 from the headline path.
|
||||
|
||||
---
|
||||
|
||||
## 4. Shared result view — "follow-up deadlines"
|
||||
|
||||
Once a trigger event is locked (via Mode A click or Mode B R4 pick), the same result view renders.
|
||||
|
||||
### 4.1 Trigger card (sticky header)
|
||||
|
||||
```text
|
||||
┌─ Trigger-Ereignis ─────────────────────────────────────────────────────┐
|
||||
│ 📥 Klageerhebung │
|
||||
│ upc.inf.cfi · Verletzungsverfahren · UPC │
|
||||
│ ⓘ "Einreichung der Klageschrift gemäß R.13 RoP" │
|
||||
│ Trigger-Datum: 📅 2026-05-20 [ändern ↩] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`Trigger-Datum` defaults to today. The user can change it inline (date picker). Changing it re-renders the follow-ups with new computed dates.
|
||||
|
||||
The "ändern" link drops back to whichever mode the user came from with R1-R4 still answered. (Per Q4: the wizard preserves compatible upstream picks rather than rebooting.)
|
||||
|
||||
### 4.2 Follow-up groups
|
||||
|
||||
Group `sequencing_rules` rows that have the trigger event as **anchor** (i.e. `sr.procedural_event_id = trigger.id`) into 4 visible groups:
|
||||
|
||||
1. **MANDATORY** (`priority='mandatory'`) — pre-checked. The bread-and-butter follow-ups.
|
||||
2. **RECOMMENDED** (`priority='recommended'`) — pre-checked. Best-practice fillings (R.19 EPÜ Einspruch, replication briefs).
|
||||
3. **OPTIONAL** (`priority='optional'`) — unchecked. Discretionary actions (R.320 Wiedereinsetzung).
|
||||
4. **CONDITIONAL** (`condition_expr IS NOT NULL`) — unchecked, with the condition rendered ("nur wenn CCR im Verfahren"). Lawyer ticks if applicable.
|
||||
|
||||
Plus a fifth implicit bucket:
|
||||
|
||||
5. **SPAWNED / CROSS-PROCEEDING** (`is_spawn=true`, `spawn_proceeding_type_id IS NOT NULL`) — surfaced as a separate sub-section with a clear "leitet ein neues Verfahren ein" annotation. Pre-checked when mandatory.
|
||||
|
||||
Recommendation (Q6): **4 visible groups, with SPAWNED inlined into whichever priority bucket it belongs to but tagged with a "⇲ neues Verfahren" badge.** Five groups is too many for a one-page result; folding SPAWNED into its priority keeps the math right (mandatory spawned = mandatory) while still flagging the cross-proceeding implication.
|
||||
|
||||
### 4.3 Per-rule row
|
||||
|
||||
```text
|
||||
☑ Klageerwiderung ✏ Datum
|
||||
3 Monate nach Klageerhebung 20.08.2026
|
||||
RoP 23 · Beklagtenseite
|
||||
ⓘ Schriftlich, mit Vollmacht. Erstmaliges Bestreiten der Patentverletzung.
|
||||
```
|
||||
|
||||
Columns: checkbox · title (DE/EN) · duration phrase · computed due date · rule citation · party stance · expandable notes.
|
||||
|
||||
Inline date editor (✏ Datum) lets the lawyer override the computed date for this rule (same affordance as today's `wireDateEditClicks`). The override flows into the write-back payload.
|
||||
|
||||
`is_court_set=true` rules render with the "wird vom Gericht bestimmt" placeholder instead of a date and are unchecked-by-default (matches the current `openSaveModal` behaviour).
|
||||
|
||||
### 4.4 Result-view footer (write-back CTA)
|
||||
|
||||
```text
|
||||
┌─ Auswahl ──────────────────────────────────────────────────────────────┐
|
||||
│ 4 Fristen ausgewählt → In Akte HL-2024-001 eintragen ▶ │
|
||||
│ (oder: 2 mit eigenem Datum, 2 mit Standardberechnung) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The CTA opens a **confirm-and-edit-dates modal** (per m §11.Q6) where the lawyer can revise each selected deadline's due date one last time, then commits via `POST /api/projects/{id}/deadlines/bulk` (today's endpoint).
|
||||
|
||||
**Kontextfrei mode (no Akte)** — per m §11.Q7, the entire write-back footer **does not render** when `project == null`. The result view stays informational. In its place, an inline nudge appears above the deadline groups:
|
||||
|
||||
```text
|
||||
ⓘ Tipp: Wähle oben eine Akte, um diese Fristen einzutragen.
|
||||
```
|
||||
|
||||
The "oben" link focuses the Akte picker. Once a project is picked, the nudge collapses and the footer materialises; no page reload, no result-view rebuild (the trigger event and date persist across the project pick).
|
||||
|
||||
Modal payload per deadline (extends today's `CreateDeadlineInput`):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Klageerwiderung",
|
||||
"rule_code": "RoP 23",
|
||||
"due_date": "2026-08-20",
|
||||
"original_due_date": "2026-08-20",
|
||||
"source": "fristenrechner",
|
||||
"rule_id": "<sequencing_rules.id>", /* maps to deadlines.sequencing_rule_id */
|
||||
"notes": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**audit_reason wording (per Q12):** every row inserted via this flow carries an audit-log breadcrumb on the project (matches the deadline `Verlauf` pattern). Default reason string:
|
||||
|
||||
> `Aus Fristenrechner — Trigger: {trigger_event_name} ({trigger_date_iso})`
|
||||
|
||||
e.g. `Aus Fristenrechner — Trigger: Klageerhebung (2026-05-20)`. Falls into `paliad.project_events` with `kind='deadline_created'` via the existing `DeadlineService.CreateBulk` audit hook; no schema change needed.
|
||||
|
||||
---
|
||||
|
||||
## 5. URL / state representation
|
||||
|
||||
The new flow keeps Pathway-B's URL-as-state contract, simplified:
|
||||
|
||||
| Param | Owner | Meaning |
|
||||
|---|---|---|
|
||||
| `project` | Step 0 | Active project UUID. Drives the prefills. |
|
||||
| `mode` | mode tab | `wizard` (default) or `search`. |
|
||||
| `q` | Mode A | Free text query. |
|
||||
| `forum` | Mode A | Comma-separated forum codes (`upc,de`). Mode B writes this only when the wizard derives it. |
|
||||
| `pt` | Mode A | Selected proceeding_type code. |
|
||||
| `kind` | Mode A | event_kind chip pick. |
|
||||
| `party` | both | Perspective. Mode A's chip; Mode B's R5. |
|
||||
| `wizard` | Mode B | Dotted state cursor encoding which row is active and the picks made: `wizard=kind:filing,forum:upc,pt:upc.inf.cfi,active:event`. |
|
||||
| `event` | both | The locked trigger `procedural_events.code`. Once set, the result view renders. |
|
||||
| `trigger_date` | result | ISO date. Default = today; URL only carries it when overridden. |
|
||||
| `selected` | result | Comma-separated `sequencing_rules.id` checkbox state. Only carried when it differs from the priority default. |
|
||||
|
||||
Deep links work end-to-end: `?project=…&event=upc.inf.cfi.soc&trigger_date=2026-05-20&selected=…` jumps a colleague straight to the result view with the same picks. (Per Q11 — query string, not pathname.)
|
||||
|
||||
`popstate` rebuilds the page from the params alone (same pattern as today). The wizard state cursor lets browser back/forward step the wizard rows instead of dropping back to the page root.
|
||||
|
||||
---
|
||||
|
||||
## 6. Backend contract changes
|
||||
|
||||
### 6.1 Extend `/api/tools/fristenrechner/search`
|
||||
|
||||
Today returns concept-cards. Add an alternate response shape (or a `?kind=events` flag) that returns `procedural_events` rows directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "Klageerhebung",
|
||||
"filters": { "forum": "upc", "pt": null, "kind": "filing", "party": null },
|
||||
"events": [
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"code": "upc.inf.cfi.soc",
|
||||
"name_de": "Klageerhebung",
|
||||
"name_en": "Statement of Claim",
|
||||
"event_kind": "filing",
|
||||
"proceeding_type": { "code": "upc.inf.cfi", "jurisdiction": "UPC", "name": "..." },
|
||||
"follow_up_count": 3,
|
||||
"concept_id": "<uuid>",
|
||||
"score": 0.92
|
||||
}
|
||||
],
|
||||
"total": 12
|
||||
}
|
||||
```
|
||||
|
||||
The concept-card shape stays available for the legacy Pathway-B-filter route (kept as a deep-link compat surface, not user-facing).
|
||||
|
||||
### 6.2 New `/api/tools/fristenrechner/follow-ups`
|
||||
|
||||
Given a trigger event id + trigger date + optional party qualifier, return the follow-up `sequencing_rules` rows, grouped + with computed dates. Wire shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"trigger": { "id": "...", "code": "upc.inf.cfi.soc", "name_de": "Klageerhebung", "event_kind": "filing", "proceeding_type": { "code": "upc.inf.cfi", "name_de": "Verletzungsverfahren", "jurisdiction": "UPC" } },
|
||||
"trigger_date": "2026-05-20",
|
||||
"party": "claimant",
|
||||
"follow_ups": [
|
||||
{
|
||||
"rule_id": "<uuid>",
|
||||
"title_de": "Klageerwiderung",
|
||||
"title_en": "Defence",
|
||||
"priority": "mandatory",
|
||||
"primary_party": "defendant",
|
||||
"duration_phrase": "3 Monate",
|
||||
"due_date": "2026-08-20",
|
||||
"is_court_set": false,
|
||||
"is_spawn": false,
|
||||
"condition_expr": null,
|
||||
"rule_code": "RoP 23",
|
||||
"notes_de": "...",
|
||||
"spawn_label": null,
|
||||
"spawn_proceeding_type": null,
|
||||
"appeal_target": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Implementation: `FristenrechnerService.LookupFollowUps(ctx, eventID, triggerDate, party)` — wraps `catalog.LookupEvents(axes={EventID:…, Depth:Next})` (already implemented for the Litigation Planner per `services/fristenrechner.go:251`) and runs the result through `pkg/litigationplanner.Calculate` to fill the dates. The calculator is unchanged.
|
||||
|
||||
### 6.3 No schema changes
|
||||
|
||||
This design is pure UX + handler shape. The unified `sequencing_rules` model already has every column needed (priority, condition_expr, spawn_*, is_court_set, primary_party, applies_to_target). No migration accompanies this design.
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration plan — from current row stack to the overhaul
|
||||
|
||||
Drop nothing on day one; co-exist for one release. The cutover is by URL flag.
|
||||
|
||||
| Phase | What changes | What survives | Branch |
|
||||
|---|---|---|---|
|
||||
| **S1 — Backend** | Add `GET /search?kind=events`. Add `GET /follow-ups`. Both feature-flagged behind a request header (off by default). | Existing endpoints. | one PR |
|
||||
| **S2 — Result view** | New `frontend/src/client/fristenrechner-result.ts` module — given a trigger event + date, render the §4 result view. Mount under a `?overhaul=1` query flag on /tools/fristenrechner. The legacy `renderProcedureResults` stays. | All today's UI. | one PR |
|
||||
| **S3 — Mode A** | New search-with-filter-chips UI. Mount alongside the row stack under `?overhaul=1`. | Row stack still primary. | one PR |
|
||||
| **S4 — Mode B (wizard)** | New `frontend/src/client/fristenrechner-wizard.ts` — the 3-5 row stack. Replaces today's `buildRowStack` only when `?overhaul=1`. Project prefill logic from `buildRowStack` ports 1:1. | The legacy row stack stays in place under no flag. | one PR |
|
||||
| **S5 — Flip the flag** | `?overhaul=1` becomes the default. Legacy row stack and `event_categories`-based cascade rendered with a hard-coded `?legacy=1` for two weeks. | Procedure mode (the upper half of `fristenrechner.tsx`) is unchanged throughout. | one PR |
|
||||
| **S6 — Cleanup** | Drop the `buildRowStack` function tree and the `event_categories`-served cascade endpoint (the table can stay — it's still semantically a useful taxonomy for future tools, just not the Fristenrechner's UI). Drop the `HIDDEN_CASCADE_ROOTS` constant and the cascade-segment bridge. | None of today's row-stack code. | one PR |
|
||||
|
||||
Single project per slice; each PR rebases off main; no shared branches.
|
||||
|
||||
The `event_categories` table itself **stays** — `audit-fristen-logic-2026-05-13.md` §2.4 already calls it "a config layer" useful for taxonomy work. The Fristenrechner just no longer reads it. Future tools (the "Ich möchte einreichen" forward-workflow surface m hid in `HIDDEN_CASCADE_ROOTS`) can resurrect it without DB migration.
|
||||
|
||||
---
|
||||
|
||||
## 8. Worked example — "PA at LG Düsseldorf bekommt einen Hinweisbeschluss via CMS in einer aktiven Akte"
|
||||
|
||||
Project: `HL-2024-001`, proceeding_type=`de.inf.lg` (Verletzungsverfahren LG), `our_side='defendant'`, `court='LG Düsseldorf'`.
|
||||
|
||||
### 8.1 Wizard path (Mode B, default)
|
||||
|
||||
User opens /tools/fristenrechner with that project in Step 0. Mode tab defaults to "🧭 Geführt".
|
||||
|
||||
Wizard rows render top-to-bottom, pre-filled where the project implies:
|
||||
|
||||
```text
|
||||
[1] Was ist passiert? [ active — chips for filing/hearing/decision/order/missed ]
|
||||
[2] Vor welchem Gericht? ✓ LG (aus Akte: HL-2024-001) ← prefilled+collapsed
|
||||
[3] In welchem Verfahren? ✓ Verletzungsverfahren (de.inf.lg) ← prefilled+collapsed
|
||||
```
|
||||
|
||||
User clicks ⚖️ Entscheidung in R1.
|
||||
|
||||
Row stack updates:
|
||||
```text
|
||||
[1] Was ist passiert? ✓ Entscheidung ← answered
|
||||
[2] Vor welchem Gericht? ✓ LG (aus Akte) ← prefilled
|
||||
[3] In welchem Verfahren? ✓ Verletzungsverfahren (de.inf.lg) ← prefilled
|
||||
[4] Welche Entscheidung konkret? [ active — chips: Urteil, Beschluss, Hinweisbeschluss, ... ]
|
||||
```
|
||||
|
||||
R4 chip set is the `procedural_events` whose `proceeding_type_id` = de.inf.lg AND `event_kind` = 'decision'. (Hinweisbeschluss is in this set — `de.inf.lg.hinweisbeschluss` or similar.)
|
||||
|
||||
User clicks Hinweisbeschluss. The wizard checks: do the follow-up rules differ by `primary_party`? In this case yes (the Hinweis triggers a reply window for the defendant only). So R5 fires:
|
||||
|
||||
```text
|
||||
[5] Welche Seite vertreten Sie? ✓ Beklagtenseite (aus Akte) ← prefilled
|
||||
```
|
||||
|
||||
R5 is pre-filled from `project.our_side='defendant'`. The user could click ändern to override, but doesn't.
|
||||
|
||||
Wizard transitions to the §4 result view. Trigger card: "📜 Hinweisbeschluss · de.inf.lg · LG · Beklagtenseite". Trigger date defaults to today.
|
||||
|
||||
### 8.2 Result view
|
||||
|
||||
Three follow-ups in scope (illustrative):
|
||||
|
||||
```text
|
||||
MANDATORY
|
||||
☑ Stellungnahme zum Hinweisbeschluss (Frist 4 Wochen) — 24.06.2026 — ZPO §139
|
||||
RECOMMENDED
|
||||
☑ Anpassung der Klageerwiderung — 24.06.2026 — best practice
|
||||
OPTIONAL
|
||||
□ Antrag auf Fristverlängerung (begründet) — auf Antrag
|
||||
```
|
||||
|
||||
User unchecks "Anpassung", changes the Stellungnahme date inline to 2026-06-20 (one weekday earlier), clicks "In Akte HL-2024-001 eintragen ▶".
|
||||
|
||||
Modal opens with the 1 selected deadline + the user's date override. User confirms.
|
||||
|
||||
### 8.3 Write-back
|
||||
|
||||
Server-side: `POST /api/projects/HL-2024-001/deadlines/bulk` with one `CreateDeadlineInput`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Stellungnahme zum Hinweisbeschluss",
|
||||
"rule_code": "ZPO §139",
|
||||
"due_date": "2026-06-20",
|
||||
"original_due_date": "2026-06-24",
|
||||
"source": "fristenrechner",
|
||||
"rule_id": "<sr-uuid>",
|
||||
"notes": null
|
||||
}
|
||||
```
|
||||
|
||||
`DeadlineService.CreateBulk` inserts the row into `paliad.deadlines` (with `sequencing_rule_id` populated from `rule_id`), creates the audit event with the wording "Aus Fristenrechner — Trigger: Hinweisbeschluss (2026-05-26)", and the user is redirected to `/deadlines?project_id=…` with a green success toast.
|
||||
|
||||
### 8.4 Mode A path for the same user
|
||||
|
||||
User flips the mode tab to "⚡ Direkt suchen". Filter chips auto-load to Forum=DE + Proceeding=de.inf.lg (from project context). User types "Hinweis" — the result list shows `de.inf.lg.hinweisbeschluss` (and maybe `upc.inf.cfi.hinweis` filtered out because Forum=DE narrows it). User clicks. Same result view appears.
|
||||
|
||||
Total clicks Mode A: 2 (type + click). Mode B: 2 (R1 chip + R4 chip; R2/R3/R5 prefilled). The wizard wins for trainees who don't know vocabulary; search wins for power users who know "Hinweisbeschluss" and can type 4 chars.
|
||||
|
||||
---
|
||||
|
||||
## 9. What's NOT in scope
|
||||
|
||||
- **Replacing the `sequencing_rules` model.** Phase 3 schema is already what the calculator runs on.
|
||||
- **Paliadin (LLM) integration into the wizard.** A "Frist-Extraktion aus Dokument" path is filed elsewhere (memory `b6a11b55…`) and stays out of this design. The wizard could later call out to Paliadin for "the user typed something we don't know" — Phase 2 of *this* overhaul, not Phase 1.
|
||||
- **Calendar / Outlook sync** of created deadlines. Separate t-paliad ticket per project-status.md long-term goals.
|
||||
- **Editing `sequencing_rules`** from the result view. Read-only here. The admin surface at `/admin/procedural-events` handles editing.
|
||||
- **The Procedure-mode surface** (upper half of `fristenrechner.tsx`). The proceeding-picker + trigger-date + flag-checkbox UI stays exactly as it is today. That surface answers a different question ("show me the full procedural ablauf for upc.inf.cfi") and is the right tool for that question; the overhaul targets only the Pathway-B / row-stack half of the page.
|
||||
|
||||
---
|
||||
|
||||
## 10. Open questions for m (12 questions, batched for `AskUserQuestion`)
|
||||
|
||||
All 12 questions tracked in m/paliad#146 § "Open design questions". Each gets a recommended option (listed first in the AskUserQuestion call). Bundled into 3 batches of 4.
|
||||
|
||||
| # | Topic | Recommended pick |
|
||||
|---|---|---|
|
||||
| Q1 | Single page or stepper? | Single page with mode-tabs + collapsible rows. |
|
||||
| Q2 | Mode switcher placement | Tab pair under Step-0 ("Akte / kontextfrei"). |
|
||||
| Q3 | Filter-vs-qualifier UX | Qualifiers carry a small "(Pflichtangabe)" tag; filters render in a slimmer pill. |
|
||||
| Q4 | Cascade tree (keep/replace) | Replace with the 5-question wizard. Drop `event_categories` from the Fristenrechner UI (table stays). |
|
||||
| Q5 | Result grouping | 4 visible groups (Mandatory / Recommended / Optional / Conditional), SPAWNED folded with badge. |
|
||||
| Q6 | Project write-back UX | Confirm-and-edit-dates modal (revise each date once before commit). |
|
||||
| Q7 | No-project mode | CTA disabled with hint ("Wähle eine Akte oben"). Match today's pattern. |
|
||||
| Q8 | Perspective semantics by mode | Mode B (file): qualifier — required pick. Mode A (search): filter — optional. |
|
||||
| Q9 | Trigger-date input timing | In the result-view trigger card; default today; inline editable. |
|
||||
| Q10 | Backward navigation | Preserve compatible downstream picks; reset only those invalidated. |
|
||||
| Q11 | Deep-link encoding | Query string (`?event=…&trigger_date=…`). |
|
||||
| Q12 | Audit reason wording | `Aus Fristenrechner — Trigger: {name} ({date})`. |
|
||||
|
||||
(Recommendations land as the "first option" in each AskUserQuestion call per the inventor SKILL contract.)
|
||||
|
||||
---
|
||||
|
||||
## 11. m's decisions (2026-05-26)
|
||||
|
||||
All 12 questions answered via `AskUserQuestion` on 2026-05-26 21:30. Recording each pick + the reasoning where it diverges from the inventor's recommendation. Sections of the design that are now load-bearing on these answers carry a "(m §11.Q{n})" cross-reference.
|
||||
|
||||
- **Q1 (Page layout): Single page, mode-tabs.** [= recommendation] Both modes share /tools/fristenrechner; the mode-tabs swap the question surface in place. Result view is shared. **Locks §3, §4, §5.**
|
||||
- **Q2 (Mode switcher): Tab pair under Step-0.** [= recommendation] "⚡ Direkt suchen" / "🧭 Geführt" tabs render directly below the Akte picker. Project context survives the tab flip; compatible filter picks (forum, proceeding) carry across.
|
||||
- **Q3 (Filter-vs-qualifier UX): Section split — Filter above, Qualifier below.** [≠ recommendation; m picked option 2.] Mode A's filter chips render in a "Filter (eingrenzen)" strip on top; below it, the result list is the qualifier surface (clicking a row locks). Mode B wizard rows carry a small "Filter" / "Qualifier" badge in the row badge area (e.g. R1/R2 = Filter, R3/R5 = Qualifier). The "(Pflichtangabe)" tag from the original recommendation is replaced by this section-level visual hierarchy. **Updates §3.1 (Mode A layout) and §3.2 (wizard row badges).**
|
||||
- **Q4 (Cascade tree): Replace with wizard, keep table.** [= recommendation] The Fristenrechner UI stops reading `paliad.event_categories`. The table stays for future tools (the hidden "Ich möchte einreichen" forward-workflow). **Locks §3.2 and the cleanup in §7 S6.**
|
||||
- **Q5 (Result grouping): 4 groups + SPAWNED badge.** [= recommendation] Mandatory / Recommended / Optional / Conditional are the four sub-sections; spawned rules fold into their priority bucket with a `⇲ neues Verfahren` badge. **Locks §4.2.**
|
||||
- **Q6 (Write-back UX): Confirm-and-edit-dates modal.** [= recommendation] Inline checkbox selection in the result view → "In Akte eintragen ▶" → modal with editable due-date fields per row + Akte picker. **Locks §4.4.**
|
||||
- **Q7 (No-project mode): Hide the CTA entirely.** [≠ recommendation; m picked option 3.] In kontextfrei mode the result view renders without the write-back footer at all — no disabled-with-hint button. Rationale (inferred from m's pick): the result view is informational by design in explore mode, and a permanently-disabled CTA is visual noise. **Updates §4.4** — the CTA is conditional on `project != null`, not on `disabled`. The hint message moves into the Step-0 picker: when a user is in kontextfrei mode and reaches a result view, a one-line nudge appears above the result groups ("Tipp: Wähle oben eine Akte, um diese Fristen einzutragen") with a link to focus the Akte picker. This preserves the affordance discovery without polluting the footer.
|
||||
- **Q8 (Perspective semantics): Mode B qualifier, Mode A filter.** [= recommendation] Wizard Mode B's R5 is required and Klagerseite/Beklagtenseite only (no "Beide"); Mode A's perspective chip is a filter with a "Beide" option, off by default. **Locks §2 axis table and §3.2 R5 description.**
|
||||
- **Q9 (Trigger-date input): In the result-view trigger card.** [= recommendation] The sticky header card on the result view shows the date; default today; inline editable. Changing it re-renders follow-up dates live. **Locks §4.1.**
|
||||
- **Q10 (Backward navigation): Preserve compatible picks.** [= recommendation] Re-opening any wizard row keeps downstream picks that are still legal under the new upstream value; resets only the picks the new value invalidates. A small chip-strip annotation ("erhalten") appears for one render-cycle on rows whose pick was carried so the user notices. **Updates §3.2 branching policy.**
|
||||
- **Q11 (Deep-link encoding): Query string.** [= recommendation] `?project=…&mode=…&event=…&trigger_date=…&selected=…&forum=…&pt=…&kind=…&party=…` — every state piece is a query param. `popstate` rebuilds the page from params. **Locks §5.**
|
||||
- **Q12 (Audit reason wording): `Aus Fristenrechner — Trigger: {name} ({date})`.** [= recommendation] German-locale, includes the trigger event name and its ISO date. Stored as `paliad.project_events.metadata->>'audit_reason'` via the existing `DeadlineService.CreateBulk` audit hook. **Locks §4.4.**
|
||||
|
||||
### 11.1 What changed from the strawman as a result
|
||||
|
||||
Two follow-on edits flow from m's picks:
|
||||
|
||||
1. **§3.1 Mode A layout** — top strip is "Filter (eingrenzen)" with the four filter chip groups (Forum · Proceeding · Event-Kind · Partei); the result list directly below carries the implicit "click here to lock" qualifier action. No "(Pflichtangabe)" tag.
|
||||
2. **§4.4 Write-back footer** — the footer is rendered conditionally on `project != null`. The kontextfrei-mode informational nudge moves into the result view body above the deadline groups.
|
||||
|
||||
These edits don't change the §7 migration plan or the §6 backend contracts.
|
||||
|
||||
---
|
||||
|
||||
## 12. Synthesis links
|
||||
|
||||
- mBrian topic: `topic-fristenrechner` (existing) — file this design as a `[synthesis]` node linked `triggered_by` t-paliad-322 and `related_to` the row-cascade + Phase 2 designs.
|
||||
- Related memories: row-cascade design `0fbd2c1a-…`, Phase 2 design `a454dc86-…`, audit logic `f6c0c3a2-…`.
|
||||
Reference in New Issue
Block a user