Commit Graph

3 Commits

Author SHA1 Message Date
mAi
3c840c0366 fix(litigationplanner): respect trigger_event_id + suppress optional from default (yoUPC#178 + #2568/#2570)
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 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).
2026-05-28 00:04:30 +02:00
mAi
cd5f752a0e feat(litigationplanner): scenarios — paliad.scenarios jsonb table + Catalog API + engine adapter (Slice D, t-paliad-306, m/paliad#124 §5)
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
A scenario is a named composition of existing proceedings + flags +
per-card choices + anchor dates. Users compose, they don't author —
spec references existing rules by submission_code; never creates new
rules. Per m's 2026-05-26 AskUserQuestion picks (doc commit 6e58595):
  Q1 composition: primary + spawned (v1); multi-proceeding peer
                  compose is the v2 goal (spec.proceedings[] array)
  Q2 scope:       per-project + abstract (project_id NULL = abstract)
  Q3 trigger:     per-anchor overrides over one base date
  Q4 storage:     NEW paliad.scenarios table with jsonb spec
                  (NOT a project_event_choices column extension)

Migration 145 — additive only. Pre-flight coordination check:
  - On-disk max: 138 (Berufung backfill, just merged).
  - Live DB tracker: 106 (significantly behind — many migs pending
    deploy).
  - curie's #93 B.2-B.6 migs not pushed yet — reserved 139-143 + 144
    as buffer; claimed 145 as the safe minimum that won't collide.
  - paliad.scenarios has audit_reason NOT applicable (no audit
    trigger on the table); updated_at trigger added on the table
    itself.
  - paliad.projects gains active_scenario_id uuid NULL FK with ON
    DELETE SET NULL (mig 134 lesson — no updated_at clauses on
    proceeding_types-style assumptions).

Schema:
  paliad.scenarios (
    id uuid pk,
    project_id uuid NULL FK → projects(id) ON DELETE CASCADE,
    name text NOT NULL CHECK char_length > 0,
    description text NULL,
    spec jsonb NOT NULL CHECK jsonb_typeof = 'object',
    created_by uuid NULL FK → users(id) ON DELETE SET NULL,
    created_at + updated_at timestamptz,
    UNIQUE NULLS NOT DISTINCT (project_id, created_by, name)
  );
  paliad.projects.active_scenario_id uuid NULL FK;
  RLS: project-scoped → can_see_project; abstract → created_by = auth.uid();
  Trigger: scenarios_touch_updated_at_trg.

pkg/litigationplanner additions:
  - Scenario struct (db + json tags)
  - ScenarioSpec / ScenarioProceeding / ScenarioCardChoice — parsed
    view of the jsonb (version-1 today, v2 multi-peer-ready)
  - ParseSpec(raw) + ScenarioSpec.PrimaryProceeding() + CalcOptionsFromSpec()
  - ScenarioFilter + Catalog.LoadScenarios + Catalog.MatchScenario
  - CalculateFromScenario(scenario, catalog, holidays, courts) — high-
    level engine entry: parses spec → builds CalcOptions → delegates
    to Calculate
  - Sentinel errors: ErrUnknownScenario, ErrInvalidScenario,
    ErrScenarioNoPrimary

paliadCatalog impl:
  - LoadScenarios with progressively-built WHERE clauses (project-id
    filter, abstract-for-user filter, or all)
  - MatchScenario by id — returns ErrUnknownScenario on not-found
  - Services connection bypasses RLS; ScenarioService enforces
    visibility at the application layer (mirrors EventChoiceService
    pattern from t-paliad-265)

SnapshotCatalog impl (embedded/upc):
  - LoadScenarios returns empty slice (no scenarios in the snapshot)
  - MatchScenario returns ErrUnknownScenario

internal/services/scenario_service.go:
  - Create / Get / ListForProject / ListAbstractForUser / Patch /
    SetActive / Delete with visibility checks
  - validateSpec checks version, base_trigger_date format, every
    proceedings[*].code resolves to an active paliad.proceeding_types
    row, every appeal_target is valid, every anchor_overrides date
    parses, every role ∈ {primary, peer}
  - SetActive validates the scenario belongs to the requested project
    (a scenario from a different project can't be active here)
  - Returns ErrScenarioNotVisible for failed visibility checks

REST endpoints (registered in handlers.go):
  GET    /api/scenarios?project=<id>             — list project's
  GET    /api/scenarios?abstract=true            — list user's abstract
  GET    /api/scenarios/{id}                     — one
  POST   /api/scenarios                          — create
  PATCH  /api/scenarios/{id}                     — partial update
  DELETE /api/scenarios/{id}                     — remove
  PUT    /api/projects/{id}/active-scenario      — set / clear active

Handler error mapping:
  - ErrUnknownScenario / ErrScenarioNotVisible → 404
  - ErrInvalidInput / ErrInvalidScenario / ErrScenarioNoPrimary → 400
  - everything else → 500

Tests:
  - pkg/litigationplanner/scenarios_test.go: ParseSpec roundtrip
    (well-formed + unknown version + malformed json),
    PrimaryProceeding zero/multi/single, CalcOptionsFromSpec full
    unpack, trigger_date_override path, no-base-trigger safety check.
    8 cases total, all DB-free.

Wired in cmd/server/main.go alongside EventChoice — same pattern,
nil-safe when DATABASE_URL is unset (handlers 503 in that mode).

Acceptance:
  - go build ./... clean
  - go test ./... all green (incl. new scenarios tests)
  - Pre-flight audit confirmed mig 145 number is safe vs curie's
    pending B.2-B.6 range
2026-05-26 17:48:56 +02:00
mAi
1d704f6e04 fix(litigationplanner): R.109.1/R.109.4 mis-anchor + duplicate 'both' row in columns view (t-paliad-304, m/paliad#135)
Two bugs surfaced on /tools/verfahrensablauf?side=defendant for upc.inf.cfi:

1. Anchor regression for timing='before' children of court-set parents.
   Rules R.109.1 (translation_request) and R.109.4 (interpreter_cost)
   anchor on the oral hearing (parent_id=upc.inf.cfi.oral, IsCourtSet)
   but were computing dates BEFORE the Statement of Claim — 1 month
   resp. 2 weeks before the SoC instead of before the oral hearing.

   Root cause: engine walked rules in sequence_order, and the two
   "before"-timed children carry sequence_order 45/46 (their chronological
   position, before the oral hearing at 50). Their parent had therefore
   not been processed yet when the children were, so courtSet[oral.ID]
   was still empty → parentIsCourtSet=false → the engine fell back to
   the trigger date as the base.

   Fix: walk rules in topological order (parent-first) during the
   compute pass, then restore sequence_order on the output slice so
   the wire shape and the linear timeline view's render order stay
   identical to the legacy behaviour modulo the bug fix.

2. Duplicate "Antrag auf Simultanübersetzung" row in columns view.
   With primary_party='both' and an explicit side pick (?side=defendant),
   the bucketing mirrored the card into both 'Unsere Seite' and
   'Gegnerseite' — the same card on the same row, visible as a
   duplicate.

   Fix: when the user has committed to a perspective (side picked)
   but no appellant axis applies, collapse 'both' rows into ours.
   The '↔ beide Seiten' indicator is suppressed in that path to match
   the existing appellant-collapse semantics (no sibling row to mirror
   to). Legacy mirror behaviour is preserved when side is null.

DB audit ruled out a data-level duplicate: exactly one published+active
row per submission_code in paliad.deadline_rules.

Tests:
  - pkg/litigationplanner/before_court_set_anchor_test.go: synthetic
    rules pinning the conditional-on-court-set-parent contract plus
    the override path (1mo before user-pinned oral).
  - frontend/src/client/views/verfahrensablauf-core.test.ts: two new
    cases pinning the side-collapse routing for party='both'.
2026-05-26 15:54:02 +02:00