Commit Graph

8 Commits

Author SHA1 Message Date
mAi
a75731a902 Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 15:45:15 +02:00
mAi
3097df3918 mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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).
2026-05-26 15:43:30 +02:00
mAi
9da4715137 feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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.
2026-05-26 15:37:10 +02:00
mAi
ce28ea972e feat(litigationplanner): embedded UPC snapshot + generator (Slice C, m/paliad#124 §19)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Lays the foundation for youpc.org's cross-repo integration: an
in-package UPC subset of paliad's deadline corpus, embedded as JSON,
that any consumer can use to run the litigationplanner engine without
DB access.

Generator (cmd/gen-upc-snapshot):
  - Reads paliad's live DB (DATABASE_URL), applies pending migrations
    to match schema HEAD, SELECTs the UPC subset
    (proceeding_types WHERE jurisdiction='UPC' AND is_active=true,
    deadline_rules WHERE lifecycle_state='published' AND is_active=true
    on those proceedings, referenced trigger_events, DE+UPC holidays,
    UPC courts).
  - Writes pretty-printed JSON to
    pkg/litigationplanner/embedded/upc/{proceeding_types, rules,
    trigger_events, holidays, courts, meta}.json.
  - Idempotent — same DB state → same output (modulo
    meta.generated_at + auto-versioned suffix).
  - Date-stamped versioning (YYYY-MM-DD-N) with same-day suffix bump.
  - Operator runbook in cmd/gen-upc-snapshot/README.md.

Embedded subpackage (pkg/litigationplanner/embedded/upc/):
  - embed.go    — //go:embed *.json + LoadMeta()
  - snapshot.go — SnapshotCatalog (full lp.Catalog impl: LoadProceeding
    / LoadProceedingByID / LoadRuleByID / LoadRuleByCode /
    LoadRulesByTriggerEvent / LoadTriggerEventsByIDs / LookupEvents);
    O(1) map lookups; LookupEvents linear over the < 100-row UPC corpus.
  - holidays.go — SnapshotHolidayCalendar implementing lp.HolidayCalendar
    (IsNonWorkingDay / Adjust* with structured AdjustmentReason).
  - courts.go   — SnapshotCourtRegistry implementing lp.CourtRegistry.
  - Compile-time assertions (_ lp.X = (*Snapshot*)(nil)) catch
    interface drift.

Wire-up for consumers:
  cat, _ := upc.NewCatalog()
  hc, _  := upc.NewHolidayCalendar()
  cr, _  := upc.NewCourtRegistry()
  timeline, _ := lp.Calculate(ctx, "upc.inf.cfi", "2026-05-26",
                              lp.CalcOptions{}, cat, hc, cr)

Tests (snapshot_test.go, all DB-free):
  - meta parses cleanly, non-zero counts
  - LoadProceeding(upc.inf.cfi) returns expected proc + rules
  - LoadProceeding(unknown) returns ErrUnknownProceedingType
  - LookupEvents(Jurisdiction:UPC, all-following) covers corpus
  - LookupEvents(party=defendant, next) scopes anchors correctly
  - engine end-to-end via lp.Calculate against the embedded snapshot
  - holiday calendar (weekends, DE closures, UPC vacation block)
  - court registry (empty courtID fallback, known + unknown court)

Placeholder data shipped (2 proceedings, 2 rules, 5 holidays, 2
courts) so tests run without a live DB. Operator regenerates against
prod via `make snapshot-upc` once migrations 134 (B1) and 135 (B3)
have landed on prod — see cmd/gen-upc-snapshot/README.md for the
runbook. The placeholder's meta.version is suffixed `-placeholder`
to make the regeneration delta obvious.

Makefile target:
  make snapshot-upc — wraps the generator + reruns the snapshot tests

Design (§19 of docs/design-litigation-planner-2026-05-26.md):
  - Embedding format: go:embed JSON (diff-friendly, no compile coupling)
  - Generator entry: cmd/gen-upc-snapshot/main.go (idiomatic Go cmd path)
  - Versioning: meta.json carries semver + generated_at + paliad_commit
  - Regeneration: manual via Make target or `go generate`; no CI cron in v1
  - Out of scope: snapshot signing, DE/EPA/DPMA snapshots, snapshot
    diff tooling

Acceptance:
  - go build clean, go test all green (incl. 6 new tests in
    pkg/litigationplanner/embedded/upc, all DB-free)
  - SnapshotCatalog passes the compile-time lp.Catalog assertion
  - Generator binary builds + runs (Idempotence verified by re-running
    against the same source data)
2026-05-26 15:11:07 +02:00
mAi
989941c648 feat(litigationplanner): primary_party CHECK constraint + IsValidPrimaryParty helper (Slice B3, m/paliad#124 §18.3)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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
2026-05-26 13:58:33 +02:00
mAi
d5bf82314a feat(litigationplanner): multi-axis catalog query API (Slice B2, m/paliad#124 §18.2)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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).
2026-05-26 13:54:57 +02:00
mAi
07acf7b4a2 feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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
2026-05-26 13:49:03 +02:00
mAi
5f0a85fa83 refactor(litigationplanner): extract Fristen/Verfahrensablauf calc into pkg/litigationplanner (Slice A, t-paliad-298 / m/paliad#124)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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
2026-05-26 13:01:07 +02:00