docs(litigation-planner): Slice B design — Berufung unification + multi-axis catalog query + primary_party CHECK (m/paliad#124)
Adds §18 to the design doc folding in m's three 2026-05-26 decisions:
§18.1 Berufung unification — collapse 3 active UPC appeal proceeding_types
(upc.apl.merits / upc.apl.cost / upc.apl.order, 16 rules total) into ONE
upc.apl + appeal_target discriminator. 5 targets: Endentscheidung,
Kostenentscheidung, Anordnung, Schadensbemessung, Bucheinsicht. Adds
proceeding_types.appeal_target + deadline_rules.applies_to_target[]
columns; archives the 3 old codes; CalcOptions gains AppealTarget filter.
Migration 134 with pre-migration audit pass. Q to m on whether
Schadensbemessung-as-appeal shares the merits rule set (R) or has its own.
§18.2 Multi-axis catalog query API — new Catalog.LookupEvents method
taking optional {jurisdiction, proceeding_type_id, party,
event_category_id, appeal_target} axes + EventLookupDepth control
("next" / "all-following"). No schema delta — reuses existing parent_id
+ sequence_order graph. Returns EventMatch with priority + depth metadata.
§18.3 primary_party enum tightening — CHECK constraint on
deadline_rules.primary_party against canonical four-value vocab
(claimant/defendant/court/both, plus NULL for orphan concept seeds).
Live audit confirmed all 26+26+38+63 proceeding-bound rows already
conform; the 78 NULL rows are all proceeding_type_id IS NULL orphans
(cross-cutting concepts) and stay NULL. Migration 135 with audit-first
RAISE NOTICE pass. Package exposes PrimaryParties[] + IsValidPrimaryParty().
§18.4 revises §10 slice plan: B1 (Berufung), B2 (catalog query), B3
(enum tightening). Independent + parallel-friendly.
Branch: mai/cronus/inventor-litigation-slice-b (off main d1d0cf9).
NOT reusing the merged Slice A branch.
This commit is contained in:
@@ -1141,4 +1141,302 @@ Slice F is a youpc-side task; it needs a worker with youpc-go familiarity (a sep
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## §18 Slice B — Catalog Interface + Unifications (2026-05-26)
|
||||||
|
|
||||||
|
Slice A landed atomically at `d1d0cf9`. Before Slice B's coder shift begins, three additional decisions m confirmed today need to be folded into the package design:
|
||||||
|
|
||||||
|
- §18.1 — **Berufung unification**. Collapse the 3 active UPC appeal proceeding_types (`upc.apl.merits`, `upc.apl.cost`, `upc.apl.order`) into ONE `upc.apl` proceeding type + an `appeal_target` discriminator.
|
||||||
|
- §18.2 — **Multi-axis catalog query API**. New `Catalog.LookupEvents` method taking any subset of `{jurisdiction, proceeding_type_id, party, event_category_id}` axes + a depth control (`next` / `all-following`).
|
||||||
|
- §18.3 — **`primary_party` enum tightening**. Convert the free-text `paliad.deadline_rules.primary_party` column to a CHECK constraint matching the four-value vocabulary `claimant / defendant / court / both`.
|
||||||
|
|
||||||
|
Each subsection follows the same shape: motivation → schema impact → API shape → acceptance criteria.
|
||||||
|
|
||||||
|
### §18.0 Live state on main (audit summary)
|
||||||
|
|
||||||
|
Confirmed via Supabase before drafting (`mai/cronus/inventor-litigation-slice-b` branch off `main`):
|
||||||
|
|
||||||
|
- **9 active UPC proceeding_types**: `upc.inf.cfi` (25 rules), `upc.rev.cfi` (17), `upc.pi.cfi` (7), `upc.dmgs.cfi` (8), `upc.disc.cfi` (4), `upc.ccr.cfi` (0 — sub-track), `upc.apl.merits` (7), `upc.apl.cost` (2), `upc.apl.order` (7).
|
||||||
|
- **3 appeal-flavoured proceeding_types** = 16 rules across 3 codes. Schadensbemessung + Bucheinsicht are SEPARATE first-instance proceedings today (`upc.dmgs.cfi`, `upc.disc.cfi`), NOT appeal sub-tracks.
|
||||||
|
- **`paliad.deadline_rules.primary_party`** value distribution: `claimant=26`, `defendant=26`, `court=38`, `both=63`, `NULL=78`. The 78 NULL rows are ALL `proceeding_type_id IS NULL` orphans (cross-cutting concept seeds: Wiedereinsetzung, Versäumnisurteil-Einspruch, Schriftsatznachreichung, Weiterbehandlung — 8 distinct concepts × N rules). Every proceeding-bound rule already has a four-value `primary_party`.
|
||||||
|
- **`paliad.event_categories.party`** column shape: `text[]` (array). Live distinct values: `{claimant}`, `{defendant}`, NULL. No `court` or `both` in event_categories.party today. The semantic is "from whose perspective is this event triggered?" — narrower than `primary_party` which is "who files this submission".
|
||||||
|
|
||||||
|
### §18.1 Berufung unification
|
||||||
|
|
||||||
|
#### Motivation
|
||||||
|
|
||||||
|
m's framing (2026-05-26 09:55, t-paliad-298 instructions): *"the Verfahrensablauf event picker has 4-5 separate proceeding_types … plus Berufung Schadensbemessung and Berufung Bucheinsicht variants. m doesn't like the pre-separation. He wants ONE 'Berufung' entry in the picker, and the user then picks what the appeal is directed AT … the system derives the correct frist sequence from that target."*
|
||||||
|
|
||||||
|
Today's 3 codes (`upc.apl.merits`, `upc.apl.cost`, `upc.apl.order`) are a leaky abstraction of "appeal" — the user has to know whether it's a merits/cost/order appeal BEFORE they enter the picker, even though that branching question is "what's being appealed?" not "what kind of appeal?". Schadensbemessung + Bucheinsicht aren't in the appeal taxonomy at all today; appeals against those decisions silently fall into `upc.apl.merits`, blurring the rule sequence (RoP.220 vs RoP.221 vs RoP.224 timing).
|
||||||
|
|
||||||
|
The five appeal-target kinds are:
|
||||||
|
|
||||||
|
| Target | Source decision | Typical RoP track | Current proceeding code |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Endentscheidung | Final merits decision (UPC.RoP.118.1 / 219) | 2-month notice + 4-month grounds (R.224.1.a / R.224.2.a) | `upc.apl.merits` |
|
||||||
|
| Kostenentscheidung | Cost decision (R.150 / R.221.1) | 15-day leave-to-appeal (R.221.1) | `upc.apl.cost` |
|
||||||
|
| Anordnung | Order during proceedings (R.220) | 15-day track (R.220.2 / R.220.3 / R.224.2.b) | `upc.apl.order` |
|
||||||
|
| Schadensbemessung | Damages-determination decision (R.118.4 + R.140.2.b damages award) | Same merits track (2/4 month), but conceptually distinct anchor | (today maps to `upc.apl.merits`, silently) |
|
||||||
|
| Bucheinsicht | Lay-open-books decision (R.142) | 15-day track (R.220.2 — order-flavoured) OR merits track depending on the underlying decision shape | (today maps to `upc.apl.merits`, silently) |
|
||||||
|
|
||||||
|
#### Schema impact
|
||||||
|
|
||||||
|
**Migration plan (single `134_berufung_unification.up.sql`)**:
|
||||||
|
|
||||||
|
1. **Add column** `paliad.proceeding_types.appeal_target text NULL` — discriminator on the unified `upc.apl` row.
|
||||||
|
2. **Add CHECK** on `appeal_target`: NULL OR one of `endentscheidung | kostenentscheidung | anordnung | schadensbemessung | bucheinsicht`. Slugged in English-lowercase to match the package's English-identifier rule; the user-facing label is i18n'd in frontend.
|
||||||
|
3. **Insert** a new unified row `upc.apl` (name="Berufungsverfahren", name_en="Appeal", jurisdiction="UPC", category="fristenrechner", `appeal_target=NULL`).
|
||||||
|
4. **Re-target rule rows** by `appeal_target`:
|
||||||
|
- Today's 7 `upc.apl.merits` rules → keep `proceeding_type_id` pointing at the new `upc.apl` row, set a NEW column on `paliad.deadline_rules` called `applies_to_target text NULL` (CHECK matching the five-value vocab) to `'endentscheidung'`.
|
||||||
|
- Today's 2 `upc.apl.cost` rules → `applies_to_target='kostenentscheidung'`.
|
||||||
|
- Today's 7 `upc.apl.order` rules → `applies_to_target='anordnung'`.
|
||||||
|
- The 7 merits rules ALSO carry the implicit "applies to Schadensbemessung" semantic (the merits track is shared) — explicit duplication or a multi-value applies_to_target array? See §18.1 "Open question" below.
|
||||||
|
5. **Archive** the 3 old proceeding_types — set `category='archived'`, `is_active=false`. Keep the rows for FK integrity (project_event_choices, etc. may reference them historically; the archive flag stops them surfacing in the picker).
|
||||||
|
6. **Add 5 stable proceeding-type alias rows** OR **just emit one chip per appeal_target in the frontend**. Recommended (see API shape below): emit chips from the package's catalog, no DB row per target.
|
||||||
|
|
||||||
|
**Two new columns added by this migration:**
|
||||||
|
- `paliad.proceeding_types.appeal_target text NULL` (CHECK on 5 slugs OR NULL — NULL means "not an appeal").
|
||||||
|
- `paliad.deadline_rules.applies_to_target text[] NULL` (CHECK each element ∈ the 5 slugs — array because the merits track applies to BOTH endentscheidung AND schadensbemessung today).
|
||||||
|
|
||||||
|
**Migration audit pass first**: before running step 4 the migration should `RAISE NOTICE` for any rule row whose `applies_to_target` derivation is ambiguous (e.g. an old `upc.apl.merits` rule that has a `condition_flag` that doesn't fit any target). In practice the 16 rules all map cleanly, but the audit pattern matches Phase 2 Step E discipline (see `docs/design-fristen-phase2-2026-05-15.md` §3.E).
|
||||||
|
|
||||||
|
**Down-migration**: re-insert the 3 archived proceeding_types, restore `proceeding_type_id` on rules from the saved `applies_to_target`, drop the two new columns. Standard down-symmetry per `docs/design-fristen-phase2-2026-05-15.md`.
|
||||||
|
|
||||||
|
#### API shape
|
||||||
|
|
||||||
|
The package's existing `Catalog.LoadProceeding(ctx, code, hint)` already returns a `ProceedingType` + `[]Rule`. The Berufung unification fits cleanly:
|
||||||
|
|
||||||
|
- `LoadProceeding(ctx, "upc.apl", hint)` returns the unified Berufung proceeding + ALL appeal rules across the 5 targets.
|
||||||
|
- A new optional field on the request narrows by target: extend `CalcOptions` with `AppealTarget string`. When non-empty, the engine filters the returned rule list to rules whose `applies_to_target` contains the requested target.
|
||||||
|
- The package exposes the 5 target slugs as constants:
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
AppealTargetEndentscheidung = "endentscheidung"
|
||||||
|
AppealTargetKostenentscheidung = "kostenentscheidung"
|
||||||
|
AppealTargetAnordnung = "anordnung"
|
||||||
|
AppealTargetSchadensbemessung = "schadensbemessung"
|
||||||
|
AppealTargetBucheinsicht = "bucheinsicht"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppealTargets is the canonical ordered list for UI chip rendering.
|
||||||
|
var AppealTargets = []string{
|
||||||
|
AppealTargetEndentscheidung,
|
||||||
|
AppealTargetKostenentscheidung,
|
||||||
|
AppealTargetAnordnung,
|
||||||
|
AppealTargetSchadensbemessung,
|
||||||
|
AppealTargetBucheinsicht,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ProceedingType` gains a field: `AppealTarget *string ` db:"appeal_target" json:"appealTarget,omitempty"`` (per-row tag for clarity; redundant with the unified row's `code='upc.apl'` but useful for non-appeal proceedings that may carry NULL).
|
||||||
|
- `Rule` gains a field: `AppliesToTarget []string ` db:"applies_to_target" json:"appliesToTarget,omitempty"`` (per-row applies-to set).
|
||||||
|
|
||||||
|
Frontend logic:
|
||||||
|
- Verfahrensablauf picker shows one "Berufung" entry (the `upc.apl` proceeding).
|
||||||
|
- After picking Berufung, a chip group renders the 5 `AppealTargets` slugs (i18n labels in `frontend/src/client/i18n.ts`).
|
||||||
|
- Selecting a target sets `?target=<slug>` query param → backend includes `opts.AppealTarget=<slug>` in the request → engine filters.
|
||||||
|
|
||||||
|
#### Acceptance criteria (Slice B sub-tasks for this fold-in)
|
||||||
|
|
||||||
|
1. Migration `134_berufung_unification.up.sql` + paired `.down.sql` apply cleanly against a fresh paliad DB.
|
||||||
|
2. After migration, `SELECT code FROM paliad.proceeding_types WHERE jurisdiction='UPC' AND is_active=true AND category='fristenrechner'` returns one less row (the 3 old appeal codes collapsed to 1 new code).
|
||||||
|
3. `Catalog.LoadProceeding(ctx, "upc.apl", hint)` returns the merged 16-rule set; with `opts.AppealTarget="endentscheidung"` it returns exactly 7 rules.
|
||||||
|
4. Verfahrensablauf renders one "Berufung" picker entry. The 5 target chips render below it post-pick; switching chips re-renders the timeline.
|
||||||
|
5. Existing project rows that referenced the old `upc.apl.merits` / `upc.apl.cost` / `upc.apl.order` codes still load (the FK integrity is preserved via the archived old rows).
|
||||||
|
6. The `paliad.proceeding_type_history` follow-up (not in scope here) can later migrate those project FKs to the new `upc.apl` + `appeal_target` field — that's a follow-up.
|
||||||
|
|
||||||
|
#### Open question for m (escalate via `mai instruct head`)
|
||||||
|
|
||||||
|
**Q18.1.1 — Is Schadensbemessung-as-appeal a duplicate of Endentscheidung, or a distinct set?**
|
||||||
|
|
||||||
|
Three interpretations:
|
||||||
|
- A. **Shared rule set**: appeal of a Schadensbemessung uses the SAME rules as appeal of an Endentscheidung (both run the 2/4-month merits track). `applies_to_target=['endentscheidung','schadensbemessung']` on each rule. (R) — simplest, matches what the live `upc.apl.merits` corpus does today (no explicit target distinction).
|
||||||
|
- B. **Distinct rule set**: Schadensbemessung-appeal has its own anchor + sequence (different trigger event, different timing). Would need 7 new rule rows specifically for `applies_to_target=['schadensbemessung']`. No live evidence for this today.
|
||||||
|
- C. **Defer**: ship Berufung unification with only 3 targets (endentscheidung / kostenentscheidung / anordnung) for v1; add Schadensbemessung + Bucheinsicht as a follow-up.
|
||||||
|
|
||||||
|
Recommendation: **A** — share rules, multi-valued `applies_to_target` array. Frontend renders all 5 chips from day 1; the merits 7 rules show under endentscheidung + schadensbemessung; the order 7 rules show under anordnung + bucheinsicht (the 15-day track DOES apply to Bucheinsicht under R.142+R.220.2). No new rule rows needed.
|
||||||
|
|
||||||
|
**Q18.1.2 — User-facing label for "appeal_target": "Worauf richtet sich die Berufung?" (DE) / "Appeal against:" (EN)?**
|
||||||
|
|
||||||
|
Recommendation: yes, those exact strings; defer i18n decisions to the coder shift.
|
||||||
|
|
||||||
|
### §18.2 Multi-axis catalog query API
|
||||||
|
|
||||||
|
#### Motivation
|
||||||
|
|
||||||
|
The current `Catalog` interface (added in Slice A) supports proceeding-code lookups only. The new scenarios surface (Slice D) + the Determinator cascade (t-paliad-166) + a future "show me all next-step events when I'm in state X" need a generalised query that takes any subset of axes and returns matching events.
|
||||||
|
|
||||||
|
m's brief (2026-05-26 13:33): *"any subset of these axes (all optional): jurisdiction, proceeding_type_id, party, event_category_id. Returns matching events with the priority flag and a sequence-depth control: caller picks 'next' (1 hop downstream) or 'all-following' (full chain)."*
|
||||||
|
|
||||||
|
Today the cascade reconstructs this client-side via fanned-out calls to `/api/tools/fristenrechner` etc. — fragile + duplicated logic. The new method centralises the graph walk in the package.
|
||||||
|
|
||||||
|
#### Schema impact
|
||||||
|
|
||||||
|
**None new.** The query reads existing tables:
|
||||||
|
|
||||||
|
- `paliad.proceeding_types` (jurisdiction, id, code)
|
||||||
|
- `paliad.deadline_rules` (parent_id, sequence_order, primary_party, priority, event_category_id via concept_id → event_category_concepts)
|
||||||
|
- `paliad.event_categories` (id, party, parent_id for the cascade hierarchy)
|
||||||
|
- `paliad.event_category_concepts` (junction; concept_id → event_category_id)
|
||||||
|
|
||||||
|
The depth control is a runtime graph walk — `next` returns one hop from the matched parent, `all-following` walks `parent_id` recursively until leaves.
|
||||||
|
|
||||||
|
The audit found one schema gap worth flagging but NOT changing in Slice B:
|
||||||
|
- `paliad.deadline_rules` has no direct `event_category_id` column — it goes through `concept_id → deadline_concepts → event_category_concepts → event_categories`. The join is well-trodden but introduces an extra hop. v1 of the catalog API uses the join; a future denormalisation (`paliad.deadline_rules.event_category_id` cached column) is out of scope.
|
||||||
|
|
||||||
|
#### API shape
|
||||||
|
|
||||||
|
```go
|
||||||
|
// EventLookupAxes carries the optional filter axes for LookupEvents. All
|
||||||
|
// fields are optional; the empty value is "no filter on this axis". When
|
||||||
|
// multiple axes are set the engine applies them as AND (a rule must
|
||||||
|
// match ALL non-zero axes).
|
||||||
|
type EventLookupAxes struct {
|
||||||
|
Jurisdiction string // "UPC" | "DE" | "EPA" | "DPMA" — empty = any
|
||||||
|
ProceedingTypeID *int // narrow to one proceeding — nil = any
|
||||||
|
Party string // "claimant" | "defendant" | "court" | "both" — empty = any
|
||||||
|
EventCategoryID *uuid.UUID // narrow to one event_categories row — nil = any
|
||||||
|
AppealTarget string // §18.1 fold-in — empty = any
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventLookupDepth controls the sequence-depth of the returned events.
|
||||||
|
type EventLookupDepth string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EventLookupDepthNext returns immediate children of the matched
|
||||||
|
// anchor (1 hop downstream). Default for "what comes next from
|
||||||
|
// this point?" queries.
|
||||||
|
EventLookupDepthNext EventLookupDepth = "next"
|
||||||
|
// EventLookupDepthAllFollowing returns the entire downstream
|
||||||
|
// chain (parent_id walk to leaves). Default for "show me the
|
||||||
|
// whole sequence from here onward" queries.
|
||||||
|
EventLookupDepthAllFollowing EventLookupDepth = "all-following"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventMatch is one result row from LookupEvents.
|
||||||
|
type EventMatch struct {
|
||||||
|
Rule Rule `json:"rule"` // full rule row
|
||||||
|
ProceedingType ProceedingType `json:"proceedingType"` // owning proceeding
|
||||||
|
Priority string `json:"priority"` // mandatory|recommended|optional|informational
|
||||||
|
DepthFromAnchor int `json:"depthFromAnchor"` // 1 = next, 2+ = deeper
|
||||||
|
// ParentRuleID populated when the match has a parent_id in the
|
||||||
|
// returned set (so the frontend can render a tree).
|
||||||
|
ParentRuleID *uuid.UUID `json:"parentRuleId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupEvents on the Catalog interface returns events matching any
|
||||||
|
// subset of axes, at the requested sequence depth. Returns an empty
|
||||||
|
// slice (NOT an error) when no events match.
|
||||||
|
//
|
||||||
|
// Implementation must respect the catalog's "published + active" rule
|
||||||
|
// gate that LoadProceeding already enforces.
|
||||||
|
type Catalog interface {
|
||||||
|
// ... existing methods (LoadProceeding, LoadProceedingByID, ...)
|
||||||
|
|
||||||
|
LookupEvents(ctx context.Context, axes EventLookupAxes, depth EventLookupDepth) ([]EventMatch, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
paliad's `paliadCatalog` impl builds one SQL query with optional WHERE clauses + the existing `deadline_concept_event_types` JOIN for the event_category_id axis. youpc.org's embedded snapshot impl runs the same axis-filter pass on the in-memory rule slice.
|
||||||
|
|
||||||
|
#### Acceptance criteria (Slice B sub-tasks)
|
||||||
|
|
||||||
|
1. `Catalog.LookupEvents` exists on the interface + has both a paliad-side impl (SQL) and a stub for the future embedded/upc snapshot impl.
|
||||||
|
2. Round-trip test: `LookupEvents(ctx, EventLookupAxes{Jurisdiction:"UPC"}, EventLookupDepthAllFollowing)` returns all 77 UPC rules (matches the count from §0).
|
||||||
|
3. Combined-axis test: `EventLookupAxes{Jurisdiction:"UPC", Party:"claimant"}` returns the claimant-perspective subset.
|
||||||
|
4. Depth test: with a specific `ProceedingTypeID` + `Party:"defendant"`, `EventLookupDepthNext` returns only 1-hop children of the proceeding's root; `EventLookupDepthAllFollowing` returns the full chain.
|
||||||
|
5. New axis-driven endpoint at `GET /api/tools/lookup-events?…` proxies the call (separate slice — out of scope for the package-side acceptance, but listed for the coder).
|
||||||
|
|
||||||
|
### §18.3 `primary_party` enum tightening
|
||||||
|
|
||||||
|
#### Motivation
|
||||||
|
|
||||||
|
Today `paliad.deadline_rules.primary_party` is free-text. The live values confirm a stable four-value vocabulary (`claimant=26, defendant=26, court=38, both=63, NULL=78`) but nothing prevents a future rule editor from typing `clamant` or `Court` and silently breaking the appellant-context propagation in `engine.go`.
|
||||||
|
|
||||||
|
m's brief: *"Tighten to a check constraint matching event_categories.party's allowed values: claimant/defendant/court/both. Migration must audit + clean existing rows first; surface dirty rows to m if any don't fit the four-value vocabulary."*
|
||||||
|
|
||||||
|
Note: `event_categories.party` is a `text[]` (array) with current live values `{claimant}`, `{defendant}`, NULL. It does NOT carry `court` or `both` today. The brief's "matching event_categories.party's allowed values" is taken to mean the SEMANTIC vocabulary (claimant/defendant/court/both), not the literal current rows of event_categories.party. The package owns the canonical list.
|
||||||
|
|
||||||
|
#### Schema impact
|
||||||
|
|
||||||
|
**Migration `135_primary_party_check.up.sql`**:
|
||||||
|
|
||||||
|
1. **Audit pass** (DO $$ block): COUNT rules where `proceeding_type_id IS NOT NULL AND primary_party NOT IN ('claimant', 'defendant', 'court', 'both', NULL)`. RAISE NOTICE for each non-matching row's `(id, name, primary_party)`. If COUNT > 0, RAISE EXCEPTION 'dirty rows — see notice; manual cleanup required'.
|
||||||
|
2. **Add CHECK constraint**: `ALTER TABLE paliad.deadline_rules ADD CONSTRAINT deadline_rules_primary_party_chk CHECK (primary_party IS NULL OR primary_party IN ('claimant', 'defendant', 'court', 'both'))`.
|
||||||
|
3. **No data change** — every proceeding-bound rule already has a valid four-value value; the 78 NULL rows are orphan concept seeds and stay NULL.
|
||||||
|
4. **Down-migration**: `ALTER TABLE paliad.deadline_rules DROP CONSTRAINT deadline_rules_primary_party_chk`. No data revert needed.
|
||||||
|
|
||||||
|
**Why NULL stays valid:**
|
||||||
|
- The 78 NULL rows are cross-cutting concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch, Schriftsatznachreichung, Weiterbehandlung) that have NO proceeding binding. They're not in the calculator's path; loosening the CHECK to `IS NULL OR IN (...)` keeps them valid without further schema gymnastics.
|
||||||
|
- A stricter "NOT NULL when proceeding_type_id is NOT NULL" partial constraint would be cleaner but adds a multi-column rule that's harder to maintain. The simpler form suffices given today's invariant.
|
||||||
|
|
||||||
|
**Should the same vocabulary be propagated to `paliad.event_categories.party`?**
|
||||||
|
|
||||||
|
Recommendation: **NO, not in this migration**. event_categories.party is array-shaped (a category can apply to multiple perspectives) and today carries only `{claimant}` / `{defendant}` per its narrower semantic ("from whose perspective is this category triggered?"). Tightening it to require court/both would force backfill of rows where neither perspective is the trigger. Out of scope for Slice B; flag as a follow-up.
|
||||||
|
|
||||||
|
#### API shape
|
||||||
|
|
||||||
|
The package's `Rule.PrimaryParty` field (already `*string`) stays as-is — the type doesn't change. A new package-level set of constants:
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
PrimaryPartyClaimant = "claimant"
|
||||||
|
PrimaryPartyDefendant = "defendant"
|
||||||
|
PrimaryPartyCourt = "court"
|
||||||
|
PrimaryPartyBoth = "both"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrimaryParties is the canonical ordered list for validation +
|
||||||
|
// admin-UI rendering.
|
||||||
|
var PrimaryParties = []string{
|
||||||
|
PrimaryPartyClaimant,
|
||||||
|
PrimaryPartyDefendant,
|
||||||
|
PrimaryPartyCourt,
|
||||||
|
PrimaryPartyBoth,
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidPrimaryParty returns true for empty (NULL-equivalent) or any
|
||||||
|
// of the four canonical values. Used by the rule-editor (Slice E2 if
|
||||||
|
// ever revisited) to validate writes before they hit the CHECK.
|
||||||
|
func IsValidPrimaryParty(s string) bool { … }
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule-editor service (`internal/services/rule_editor_service.go`) gains a validation call against `lp.IsValidPrimaryParty` before any UPDATE — surfaces a user-friendly 400 before the DB CHECK fires with the less-pretty error.
|
||||||
|
|
||||||
|
#### Acceptance criteria (Slice B sub-tasks)
|
||||||
|
|
||||||
|
1. Migration `135_primary_party_check.up.sql` + paired `.down.sql` apply cleanly against a fresh paliad DB.
|
||||||
|
2. Pre-migration audit pass surfaces zero dirty rows on the current live corpus (verified via Supabase audit before migration drafted).
|
||||||
|
3. Post-migration, attempting to UPDATE a rule's primary_party to `'foo'` raises a DB CHECK violation.
|
||||||
|
4. The package exposes the four constants + the `PrimaryParties` slice + the `IsValidPrimaryParty` predicate.
|
||||||
|
5. Rule-editor service surfaces 400 with a clear message when a write violates the constraint (instead of leaking the raw PG error).
|
||||||
|
|
||||||
|
### §18 Summary table
|
||||||
|
|
||||||
|
| § | Topic | Schema delta | API delta | Migration |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 18.1 | Berufung unification | +2 columns (proceeding_types.appeal_target + deadline_rules.applies_to_target[]); collapse 3→1 active appeal codes | `Catalog` returns merged proceeding + Rule.AppliesToTarget; CalcOptions.AppealTarget filter; AppealTargets[] constants | `134_berufung_unification.up.sql` |
|
||||||
|
| 18.2 | Multi-axis catalog query | None new (uses existing joins) | `Catalog.LookupEvents(axes, depth)` new method + EventLookupAxes / EventMatch types | None |
|
||||||
|
| 18.3 | `primary_party` enum | +CHECK constraint on deadline_rules.primary_party | `PrimaryParties[]` constants + `IsValidPrimaryParty()` predicate | `135_primary_party_check.up.sql` |
|
||||||
|
|
||||||
|
### §18.4 Slice plan refinement (revises §10 for Slice B)
|
||||||
|
|
||||||
|
The original §10 listed Slice B as "Catalog / HolidayCalendar / CourtRegistry interfaces + paliad's default loaders." Slice A already folded those interfaces in (the engine.Calculate signature accepts them). Slice B's revised scope:
|
||||||
|
|
||||||
|
1. **Slice B1 — Berufung unification** (§18.1): migration 134 + package constants + `appeal_target` field on ProceedingType + `applies_to_target[]` on Rule + `CalcOptions.AppealTarget` filter. Frontend updates (verfahrensablauf chip group) follow in the same PR.
|
||||||
|
2. **Slice B2 — Multi-axis catalog query API** (§18.2): `Catalog.LookupEvents` method + paliad impl + tests. New `GET /api/tools/lookup-events` endpoint optional (slice C may want it earlier).
|
||||||
|
3. **Slice B3 — primary_party enum tightening** (§18.3): migration 135 + package constants + rule-editor validation hook.
|
||||||
|
|
||||||
|
B1 / B2 / B3 are independently shippable and can land in any order. B1 has the most user-facing impact (the picker change is what m flagged); B3 is the smallest hardening; B2 is the largest API surface.
|
||||||
|
|
||||||
|
### §18.5 Open questions escalated to head
|
||||||
|
|
||||||
|
- §18.1 Q1 — Schadensbemessung-as-appeal: shared vs distinct vs deferred (R: shared, multi-valued `applies_to_target`).
|
||||||
|
- §18.1 Q2 — i18n label for "Worauf richtet sich die Berufung?" (R: yes, defer to coder).
|
||||||
|
- §18.3 — Should event_categories.party be tightened in the same migration? (R: no, separate follow-up.)
|
||||||
|
|
||||||
|
No `AskUserQuestion` per inventor protocol; head escalates to m if material.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*End of design doc.*
|
*End of design doc.*
|
||||||
|
|||||||
Reference in New Issue
Block a user