Commit Graph

3 Commits

Author SHA1 Message Date
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
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
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