Files
paliad/pkg/litigationplanner/catalog.go
mAi d5bf82314a
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
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).
2026-05-26 13:54:57 +02:00

63 lines
3.1 KiB
Go

package litigationplanner
import "context"
// Catalog supplies proceeding-type metadata + rules for the calculator.
//
// Implementations:
// - paliad: reads paliad.deadline_rules + paliad.proceeding_types,
// filtered to lifecycle_state='published' AND is_active=true.
// ProjectHint scopes future per-project rule merges.
// - embedded/upc (Slice C): in-memory map keyed by code, populated
// once at init from the embedded JSON snapshot.
//
// All methods return ErrUnknownProceedingType / ErrUnknownRule when the
// caller asks for a code/id that doesn't exist in the catalog.
type Catalog interface {
// LoadProceeding returns the proceeding-type metadata + the full
// rule list (sorted by sequence_order). Caller passes the user-
// facing proceeding code (e.g. "upc.inf.cfi"). The hint scopes a
// future per-project rule merge — implementations that don't
// support projects ignore it.
LoadProceeding(ctx context.Context, code string, hint ProjectHint) (*ProceedingType, []Rule, error)
// LoadProceedingByID is the resolver used by CalculateRule when it
// has a rule + needs the rule's parent proceeding metadata.
LoadProceedingByID(ctx context.Context, id int) (*ProceedingType, error)
// LoadRuleByID resolves a rule UUID to the rule row. Used by
// CalculateRule when the caller supplies CalcRuleParams.RuleID.
LoadRuleByID(ctx context.Context, ruleID string) (*Rule, error)
// LoadRuleByCode resolves a rule by (proceedingCode, submissionCode)
// + returns the parent proceeding for use in the response identity.
// Used by CalculateRule when the caller supplies the (code, local)
// pair from a concept-card pill.
LoadRuleByCode(ctx context.Context, proceedingCode, submissionCode string) (*Rule, *ProceedingType, error)
// LoadRulesByTriggerEvent lists Pipeline-C trigger-event-rooted
// rules (rules whose trigger_event_id matches). Used by
// EventDeadlineService → Calculate via CalcOptions.TriggerEventIDFilter.
LoadRulesByTriggerEvent(ctx context.Context, triggerEventID int64) ([]Rule, error)
// LoadTriggerEventsByIDs bulk-loads paliad.trigger_events rows
// for the conditional-label override (t-paliad-294 /
// m/paliad#126). Returns a map keyed by event id; missing ids
// are simply absent (caller treats absence as "no override").
// Empty input returns an empty map without a DB roundtrip.
LoadTriggerEventsByIDs(ctx context.Context, ids []int64) (map[int64]TriggerEvent, error)
// LookupEvents returns deadline rules matching any subset of the
// requested axes, at the requested sequence depth (Slice B2,
// m/paliad#124 §18.2). Used by the Determinator cascade, the
// scenarios surface (Slice D), and any future "show me events
// matching X" query. Empty result is NOT an error.
//
// Implementations must respect the catalog's "published + active"
// rule gate (rules with lifecycle_state='draft' or is_active=false
// must NEVER appear in the result). Sort order is
// (proceeding_type_id, sequence_order) so the frontend can render
// without re-sorting.
LookupEvents(ctx context.Context, axes EventLookupAxes, depth EventLookupDepth) ([]EventMatch, error)
}