3c840c0366a6c3b5098bba4f6e71ff058aff51a7
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 3c840c0366 |
fix(litigationplanner): respect trigger_event_id + suppress optional from default (yoUPC#178 + #2568/#2570)
Two paired engine semantics fixes:
1. trigger_event_id is now the authoritative semantic anchor. When a
rule carries trigger_event_id, the engine no longer falls back to
the proceeding's trigger date — it resolves the anchor via
CalcOptions.TriggerEventAnchors keyed by paliad.trigger_events.code.
Missing anchor renders the rule as IsConditional (empty date) and
propagates via courtSet so descendants also surface as conditional.
Fixes the RoP.109.5 bug where the engine fabricated a date 2 weeks
before the user's SoC instead of waiting for the oral_hearing date.
2. priority='optional' rules are suppressed from the default
Calculate output. Callers (paliad /tools/procedures,
youpc.org/deadlines) opt in via CalcOptions.IncludeOptional=true to
restore the legacy "show optional applications" behaviour. The
suppression cascades through skippedIDs so child rules drop too.
Wire shape additions:
- CalcOptions.IncludeOptional bool
- CalcOptions.TriggerEventAnchors map[string]string
- Timeline.RulesAwaitingAnchor int (count of suppressed-by-missing-
anchor rules, for caller telemetry / "N rules need an anchor" UX)
Existing before-court-set-anchor tests opt in to IncludeOptional=true
to preserve their non-optional-related test intent.
Refs: youpcorg/head delegations #2568 + #2570, m/paliad#153 (Litigation
Builder PRD path).
|
|||
| 7d7b20651d |
feat(litigationplanner): appeal-target synthetic trigger row + appeal-role stamping (t-paliad-307, m/paliad#136)
Engine side of the four Verfahrensablauf appeal bugs in m/paliad#136. Bug 2 — Missing trigger event row. When CalcOptions.AppealTarget is set, Calculate now prepends a synthetic TimelineEntry to the deadlines slice dated to the trigger date, carrying the per-appeal-target label from TriggerEventLabelForAppealTarget (Endentscheidung (R.118), Kosten- entscheidung, Anordnung, Schadensbemessung, Bucheinsicht). Marked IsRootEvent + IsTriggerEvent + party=court + priority=informational so the frontend renders it as a dimmed anchor card without a save button / choices caret / click-to-edit affordance. Empty Code so it doesn't collide with real rule UUIDs downstream. Bug 1 (engine half) — Side selector dead on appeal. Every appeal filing rule carries primary_party='both' in the catalog, so the column bucketer couldn't distinguish Berufungskläger vs Berufungs- beklagter filings from primary_party alone. Engine now stamps the new TimelineEntry.AppealRole field with appellant/appellee from the rule-semantic AppealFilerRole mapping (appeal_role.go) when an appeal_target is in scope. The frontend half of the fix (next commit) consumes this to route each "both" rule into the user-perspective column once the user picks a side. Mapping covers all 12 appeal filing rules across the three applies_to_target tracks (endentscheidung/schadensbemessung, kostenentscheidung, anordnung/bucheinsicht). Court-issued events (merits.decision, merits.oral, cost.decision, order.order) stay empty — they continue to route on Party='court'. Unmapped submission_codes return empty so a new appeal rule we forgot to map falls through to the bucketer's legacy path rather than silently picking a side. Tests: TestAppealFilerRole pins the mapping; TestCalculate_Appeal SyntheticTriggerRow covers (a) synthetic row prepended + AppealRole stamped when target is set, (b) no synthetic row + no AppealRole when target is unset (regression guard), (c) unknown target short-circuits to no-op. Existing tests untouched — both behaviours gate on opts.AppealTarget != "". No DB migration — the bugs are calc-side. deadline_rules untouched. |
|||
| a75731a902 | Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133) | |||
| 3097df3918 |
mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
t-paliad-302 / m/paliad#133. Surface each event card's rule duration ("2 Mo. nach") on /tools/verfahrensablauf — by default as a hover tooltip on the date span, and optionally inline via a new "Dauern anzeigen" header toggle (localStorage key paliad.verfahrensablauf.durations-show). The issue scoped this as pure-frontend on the assumption that the duration fields were already on the /api/tools/fristenrechner payload. They were not: lp.TimelineEntry exposed only the computed dueDate, not the rule's (duration_value, duration_unit, timing) tuple. Added these as three additive optional fields and populated them in both engine emission sites (Calculate + CalculateByTriggerEvent) from the rule row directly. Source values are the base rule fields, not the post-alt-swap arithmetic — the tooltip reads as a property of the rule rather than a recap of which branch fired. Frontend wiring: - formatDurationLabel() in verfahrensablauf-core builds the "<value> <unit> <timing>" string from the existing deadlines.event.unit.<unit>.{one,many} + deadlines.event.timing.* i18n keys, reused from /tools/fristenrechner's event-mode renderer. - deadlineCardHtml attaches the label as title= on the date span (hover, default) and, when CardOpts.showDurations is on, emits an inline <span class="timeline-duration"> in the meta row. - Court-set / zero-duration rules (trigger event, hearings) skip the affordance — durationValue <= 0 short-circuits in formatDurationLabel. - Toggle persisted in localStorage under paliad.verfahrensablauf.durations-show, default off; sits next to the existing "Hinweise anzeigen" toggle. bun run build clean, go test ./pkg/litigationplanner/... and ./internal/... clean, bun test src/client/views clean (89/89). |
|||
| 9da4715137 |
feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
Two bugs from the Slice B1 Berufung rollout, one fix surface:
Bug A — duplicate side selectors collapse into ONE proactive-side
picker with per-proceeding role labels. The Verfahrensablauf used to
show both ?side= (Klägerseite/Beklagtenseite) AND ?appellant= (same
labels in case-form) on the Berufung tile. Now: one side picker, with
labels that swap to Berufungskläger/Berufungsbeklagter on the unified
upc.apl.unified tile (and Antragsteller/Antragsgegner Nichtigkeit on
upc.rev.cfi, Einsprechende(r)/Patentinhaber(in) on epa.opp.*).
Bug B — 'Auslösendes Ereignis' label derives from appeal_target on
the unified Berufung tile (5 target-specific strings) instead of the
proceeding's own trigger_event_label. Endentscheidung (R.118) /
Kostenentscheidung / Anordnung / Entscheidung im
Schadensbemessungsverfahren / Anordnung der Bucheinsicht.
Migration 137 (additive, no triggers on proceeding_types — verified
via mcp__supabase__execute_sql before drafting; no updated_at on the
table — lesson from mig 134 HOTFIX 3; no audit_reason setup needed):
- ADD COLUMN role_proactive_label_de (text NULL)
- ADD COLUMN role_proactive_label_en (text NULL)
- ADD COLUMN role_reactive_label_de (text NULL)
- ADD COLUMN role_reactive_label_en (text NULL)
- Audit-first DO block lists the rows the UPDATE will touch.
- Backfill 4 proceedings (upc.apl.unified + upc.rev.cfi +
epa.opp.opd + epa.opp.boa); every other proceeding stays NULL
and the renderer falls back to default labels.
- Down drops the 4 columns.
Package additions (pkg/litigationplanner):
- ProceedingType gains 4 *string fields (RoleProactive/Reactive
LabelDE/EN) — db tags match the new columns; existing scans pick
them up via the proceedingTypeColumns extension.
- TriggerEventLabelForAppealTarget(target, lang) — Go-side map of
the 5 appeal-target slugs to their DE/EN trigger-event labels.
Empty result on unknown target signals "fall back to proceeding's
own trigger_event_label".
- Engine override: when CalcOptions.AppealTarget is set, the
resulting Timeline.TriggerEventLabel/EN are replaced from the
per-target map.
Frontend:
- Removed #appellant-row div (was a separate 3-radio selector
duplicating side).
- Dropped ?appellant= URL state + the change handler + the init
readback. The engine still consumes "appellant" — sourced from
currentSide for role-swap proceedings; null otherwise.
- applyRoleLabels(proceedingType) swaps the side-row radio labels
from a hardcoded ROLE_LABELS map mirroring mig 137's backfill.
Falls back to deadlines.side.claimant/defendant i18n keys for
proceedings without overrides.
- syncTriggerEventLabel reads data.triggerEventLabel from the calc
response — which the engine override now sets per appeal_target,
so no client-side mapping needed.
- i18n cleanup: removed orphan deadlines.appellant.* keys (label /
claimant / defendant / none) in both DE + EN.
Tests:
- pkg/litigationplanner/appeal_target_label_test.go pins the 5×2
label matrix + a coverage test that fails if a new entry in
AppealTargets is added without populating the label switch.
Acceptance:
- go build + go test all green (incl. new lp test).
- bun run build clean (i18n codegen drops 4 keys, regenerates).
- Live-DB audit before drafting confirmed: 4 target columns don't
exist on proceeding_types, zero triggers on the table, exact
column inventory matches the design.
|
|||
| 989941c648 |
feat(litigationplanner): primary_party CHECK constraint + IsValidPrimaryParty helper (Slice B3, m/paliad#124 §18.3)
Tightens paliad.deadline_rules.primary_party from free-text to a CHECK
constraint over the canonical four-value vocab (claimant / defendant /
court / both). NULL stays valid for the 78 cross-cutting orphan
concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch,
Schriftsatznachreichung, Weiterbehandlung) — they have no
proceeding_type_id binding so they're outside the calculator's path;
loosening the CHECK to "IS NULL OR IN (…)" keeps them valid without
backfill gymnastics.
Migration 135 (audit-first):
- DO block RAISEs NOTICE for every non-conforming row + RAISEs
EXCEPTION if any dirty rows exist (manual cleanup required).
Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
on the current corpus; the audit pass stays in the migration as
safety against future drift.
- ALTER TABLE … ADD CONSTRAINT deadline_rules_primary_party_chk
CHECK (primary_party IS NULL OR primary_party IN
('claimant', 'defendant', 'court', 'both'))
- Post-migration distribution NOTICE so the operator sees the
final per-value count.
- Down = DROP CONSTRAINT. No data revert needed.
Package additions (pkg/litigationplanner):
- PrimaryParty* constants (PrimaryPartyClaimant / Defendant / Court
/ Both) + PrimaryParties[] ordered list + IsValidPrimaryParty(s)
predicate. Empty string is "no value supplied" = valid (NULL maps
to empty on the wire); non-empty must match one of the four
canonical values.
- Sibling unit tests (primary_party_test.go) pin the four-value
vocab + the chip order + IsValidAppealTarget's matching shape.
Rule-editor validation hook (rule_editor_service.go):
- Create() validates input.PrimaryParty before INSERT.
- UpdateDraft() validates patch.PrimaryParty before UPDATE.
- Both surface a user-friendly 400 with the canonical vocab listed
instead of leaking the raw PG CHECK constraint-violation message.
- Uses errors.Is(err, ErrInvalidInput) so handler 400 routing
continues to work.
services/fristenrechner.go cleanup:
- The B2-inlined isValidPartyForLookup helper is replaced with the
canonical lp.IsValidPrimaryParty. No behaviour change.
No frontend changes — the rule-editor's primary_party UI already
constrains to the four values via a select; the validation hook is
defense-in-depth.
Audit:
- go build + go test (incl. new lp unit tests) all green
- Pre-migration audit confirmed: 26 claimant + 26 defendant + 38
court + 63 both + 78 NULL = 231 total, all in canonical vocab
- event_categories.party (text[] array, narrower semantic) is
NOT touched in this migration per the design doc's
"out of scope, separate follow-up" decision
|
|||
| d5bf82314a |
feat(litigationplanner): multi-axis catalog query API (Slice B2, m/paliad#124 §18.2)
New Catalog.LookupEvents(ctx, axes, depth) method exposes a unified
graph query over paliad.deadline_rules + paliad.proceeding_types + the
deadline_concept_event_types junction. Used by the Determinator
cascade, the scenarios surface (Slice D), and any future "show me
events matching X" query — centralises a fan-out that today is
duplicated across multiple client-side paths.
Package additions (pkg/litigationplanner):
- EventLookupAxes: optional Jurisdiction / *ProceedingTypeID / Party
/ *EventCategoryID / AppealTarget. All fields optional; the empty
value (or nil pointer) is "no filter on this axis". Multiple
non-zero axes apply as AND.
- EventLookupDepth: "next" (1 hop downstream) or "all-following"
(full chain).
- EventMatch: Rule + ProceedingType + Priority + DepthFromAnchor +
*ParentRuleID (populated only when the parent itself is in the
returned set, so the frontend can render a tree).
- Catalog interface gains LookupEvents.
paliad-side implementation (internal/services/fristenrechner.go):
- SQL pass with progressively-built WHERE clauses (one $N
placeholder per non-zero axis). EventCategoryID uses an EXISTS
subquery against paliad.event_category_concepts joined via
concept_id.
- Post-fetch parent_id graph walk in Go for depth control. Loads
the per-proceeding rule corpus via DeadlineRuleService.List so
children whose parent_id is in the anchor set can be added even
when those children don't match the axes themselves. AllFollowing
iterates to fixpoint; Next stops after one pass.
- DepthFromAnchor computed by walking each result row up the
parent_id chain until it hits an anchor (iteration-bounded to
prevent infinite loops on hypothetical cycles).
- Unknown axis values (jurisdiction="XX", party="foo",
appealTarget="invalid") silently fall through as "no filter on
this axis" — a stale frontend chip should not drop the entire
result set.
- "published + active" gate (lifecycle_state='published' AND
is_active=true) matches LoadProceeding's WHERE clause.
- Results ordered by (proceeding_type_id, sequence_order) so the
frontend can render without re-sorting.
Tests (internal/services/lookup_events_test.go):
- Live-DB driven (skipped without TEST_DATABASE_URL, matches the
existing TestCalculateRule pattern).
- Cases: UPC-jurisdiction returns the UPC corpus only;
party=defendant scopes anchor matches to defendant rules;
unknown jurisdiction falls through; appeal_target=endentscheidung
returns the merits rules from B1 mig 134;
appeal_target=schadensbemessung returns empty (no rules seeded).
No schema delta. No frontend wiring (the new HTTP endpoint at
GET /api/tools/lookup-events can land in a follow-up slice — the
package + paliad-side impl are the deliverable here).
|
|||
| 07acf7b4a2 |
feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.
m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).
Migration 134 (additive only):
- ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
- ADD deadline_rules.applies_to_target text[] (CHECK each element
in the 5 slugs)
- INSERT the unified upc.apl row (inherits sort/color from
upc.apl.merits)
- Audit-first RAISE NOTICE pass listing every row about to be
touched + a post-migration sanity check
- Reassign rule rows: merits → applies_to_target={endentscheidung},
cost → {kostenentscheidung}, order → {anordnung}
- Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
so historical FKs stay intact
- Down migration restores is_active=true on the 3 old types, points
rules back by their applies_to_target stamp, drops the unified
row, drops both columns. Safe.
Package additions (pkg/litigationplanner):
- AppealTarget* constants + AppealTargets[] ordered list +
IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
so a stale frontend chip doesn't break the render)
- ProceedingType.AppealTarget *string field (top-level marker;
NULL on non-appeal proceedings)
- Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
- CalcOptions.AppealTarget string (engine filter — when set,
keeps only rules whose AppliesToTarget contains the slug)
Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.
paliad-side wiring:
- deadline_rule_service.go: ruleColumns + proceedingTypeColumns
extended to scan the new columns
- handlers/fristenrechner.go: AppealTarget JSON field on the
request payload, threaded into CalcOptions
Frontend (verfahrensablauf surface only):
- Single "Berufung" tile replaces the 3 separate Berufung tiles
- New 5-chip appeal-target row, shown only when upc.apl is picked
- URL state ?target=<slug>; default endentscheidung when none set
- APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
upc.apl (1 entry)
- i18n keys (DE + EN) for the new tile + the 5 chip labels +
the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
- calculateDeadlines threads appealTarget through to the API
Acceptance:
- go build clean, go test all green (existing test suite — no new
tests on the engine filter as a follow-up; the migration's
sanity-check DO block guards the rule-reassignment count)
- Live audit before drafting confirmed: 3 active UPC appeal
proceeding_types, 16 rules total, primary_party already conforms
to 4-value vocab on all proceeding-bound rules
|
|||
| 5f0a85fa83 |
refactor(litigationplanner): extract Fristen/Verfahrensablauf calc into pkg/litigationplanner (Slice A, t-paliad-298 / m/paliad#124)
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
|