feat(deadlines): mig 137 backfill applies_to_target — Schadensbemessung (merits) + Bucheinsicht (order)
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

After Slice B1's Berufung unification (mig 134), the picker exposed
five appeal targets but only three carried rules. Schadensbemessung and
Bucheinsicht returned empty timelines.

m's 2026-05-26 decision (#134): R.224 is uniform across substantive
R.118 decisions, and R.220.2 / R.224.2.b / R.235.2 / R.237 / R.238.2 are
uniform across the orders they appeal — so the existing merits-track
and order-track rules can carry the missing targets via a non-destructive
applies_to_target extension.

Audit of live `paliad.deadline_rules` for upc.apl.unified (proceeding_type_id=160):
- 7 endentscheidung rules → extend with 'schadensbemessung'
- 7 anordnung rules        → extend with 'bucheinsicht'
- 2 kostenentscheidung rules — untouched (distinct leave-to-appeal track)

Migration:
- set_config('paliad.audit_reason', …) at top of UP and DOWN — required
  by the mig 079 deadline_rule_audit_trigger on every UPDATE.
- Audit-first DO block lists every row to be touched (pre/post state)
  and RAISE EXCEPTIONs on pre-condition drift (missing proceeding_type,
  wrong rule counts, partial-run carry-over of the new targets).
- Two narrow UPDATEs keyed off upc.apl.unified + existing target +
  absence of new target.
- Post-sanity asserts schad=7, buch=7, end=7, anord=7, cost=2 — hard
  RAISE EXCEPTION on any drift.
- DOWN strips both new targets via array_remove with the same WHERE.
- No deadline_rules.updated_at writes; column exists but the migration
  is single-purpose and leaves it as-is.

Dry-run via Supabase MCP confirmed:
- UP yields {schad:7, buch:7, end:7, anord:7, cost:2} on prod.
- DOWN restores {schad:0, buch:0, end:7, anord:7, cost:2}.
- DB returned to pre-state; the real golang-migrate boot path will
  apply 137 cleanly at next deploy.

Test:
- lookup_events_test.go: the schadensbemessung empty-result assertion
  becomes the inverse (rules expected). Adds a parallel bucheinsicht
  assertion. Same anchor-row shape check as the existing endentscheidung
  case (DepthFromAnchor=1, target ∈ AppliesToTarget, proceeding_type
  = upc.apl.unified).
- `go test ./...` green.

Refs: m/paliad#134, t-paliad-303.
Lessons applied from mig 134 hotfixes: audit_reason set_config, no
updated_at writes, audit live DB before drafting, RAISE EXCEPTION on
integrity violations.
This commit is contained in:
mAi
2026-05-26 15:40:51 +02:00
parent 16ec8c490a
commit 3c6456cc03
3 changed files with 366 additions and 3 deletions

View File

@@ -0,0 +1,71 @@
-- 137_appeal_target_backfill_merits_order DOWN — t-paliad-303, m/paliad#134
--
-- Removes 'schadensbemessung' from the merits-track rules and
-- 'bucheinsicht' from the order-track rules, restoring the pre-137
-- shape (endentscheidung-only / anordnung-only / kostenentscheidung-only).
-- ---------------------------------------------------------------
-- 0. Audit reason (required by mig 079 trigger for any UPDATE on
-- paliad.deadline_rules).
-- ---------------------------------------------------------------
SELECT set_config(
'paliad.audit_reason',
'mig 137 DOWN: t-paliad-303 — strip Schadensbemessung/Bucheinsicht from applies_to_target per m/paliad#134',
true);
-- ---------------------------------------------------------------
-- 1. Strip new targets via array_remove.
--
-- WHERE clauses pinned to upc.apl.unified to avoid touching unrelated
-- rules that might have been added later under other proceeding types.
-- ---------------------------------------------------------------
-- 1a. Remove schadensbemessung from merits-track rows.
UPDATE paliad.deadline_rules dr
SET applies_to_target = array_remove(dr.applies_to_target, 'schadensbemessung')
FROM paliad.proceeding_types pt
WHERE pt.id = dr.proceeding_type_id
AND pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'schadensbemessung' = ANY(dr.applies_to_target);
-- 1b. Remove bucheinsicht from order-track rows.
UPDATE paliad.deadline_rules dr
SET applies_to_target = array_remove(dr.applies_to_target, 'bucheinsicht')
FROM paliad.proceeding_types pt
WHERE pt.id = dr.proceeding_type_id
AND pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'bucheinsicht' = ANY(dr.applies_to_target);
-- ---------------------------------------------------------------
-- 2. Sanity check — no row may carry the new targets after the down.
-- ---------------------------------------------------------------
DO $$
DECLARE
schad_left int;
buch_left int;
BEGIN
SELECT COUNT(*) INTO schad_left
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'schadensbemessung' = ANY(dr.applies_to_target);
SELECT COUNT(*) INTO buch_left
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'bucheinsicht' = ANY(dr.applies_to_target);
IF schad_left > 0 THEN
RAISE EXCEPTION '[mig 137 DOWN] FAILED — % rows still carry schadensbemessung', schad_left;
END IF;
IF buch_left > 0 THEN
RAISE EXCEPTION '[mig 137 DOWN] FAILED — % rows still carry bucheinsicht', buch_left;
END IF;
RAISE NOTICE '[mig 137 DOWN] stripped schadensbemessung + bucheinsicht from upc.apl.unified rules';
END $$;

View File

@@ -0,0 +1,232 @@
-- 137_appeal_target_backfill_merits_order — t-paliad-303, m/paliad#134
--
-- Slice B1 (mig 134) introduced the unified upc.apl.unified proceeding type
-- with 5 appeal_target enum values: endentscheidung, kostenentscheidung,
-- anordnung, schadensbemessung, bucheinsicht. The first three each carry
-- rules; schadensbemessung and bucheinsicht returned an empty timeline
-- because no rules referenced them yet.
--
-- m's 2026-05-26 decision (#134): extend applies_to_target on the existing
-- rules — Schadensbemessung := merits track (R.224 anchored on R.118
-- substantive decisions), Bucheinsicht := order track (R.220.2 +
-- R.224.2.b + R.235.2 + R.237 + R.238.2 etc.). Legal premise verified
-- against the 16 live rules — every endentscheidung rule is a generic
-- R.224 merits step, every anordnung rule is a generic R.220/224/235/237/
-- 238 order step. No rule carries content specific to a particular kind
-- of underlying decision/order. Audit on the comment trail of #134.
-- ---------------------------------------------------------------
-- 0. Audit reason (required by mig 079 trigger for any UPDATE on
-- paliad.deadline_rules — both UPDATEs below trigger it).
-- ---------------------------------------------------------------
SELECT set_config(
'paliad.audit_reason',
'mig 137: t-paliad-303 — extend applies_to_target for Schadensbemessung (merits) + Bucheinsicht (order) per m/paliad#134',
true);
-- ---------------------------------------------------------------
-- 1. Audit-first DO block.
--
-- Resolve upc.apl.unified, count the rows we are about to touch, and
-- RAISE EXCEPTION if anything looks wrong (proceeding type missing,
-- merits/order rule counts off, or a rule already carries the new
-- target — which would mean an earlier partial run).
-- ---------------------------------------------------------------
DO $$
DECLARE
rec record;
upc_apl_id int;
merits_count int;
order_count int;
schad_already int;
buch_already int;
BEGIN
SELECT id INTO upc_apl_id
FROM paliad.proceeding_types
WHERE code = 'upc.apl.unified';
IF upc_apl_id IS NULL THEN
RAISE EXCEPTION '[mig 137] upc.apl.unified proceeding_type not found — mig 134 must run first';
END IF;
RAISE NOTICE '[mig 137] upc.apl.unified proceeding_type_id = %', upc_apl_id;
SELECT COUNT(*) INTO merits_count
FROM paliad.deadline_rules
WHERE proceeding_type_id = upc_apl_id
AND is_active = true
AND 'endentscheidung' = ANY(applies_to_target);
SELECT COUNT(*) INTO order_count
FROM paliad.deadline_rules
WHERE proceeding_type_id = upc_apl_id
AND is_active = true
AND 'anordnung' = ANY(applies_to_target);
RAISE NOTICE '[mig 137] live counts: endentscheidung=% anordnung=%', merits_count, order_count;
IF merits_count <> 7 THEN
RAISE EXCEPTION '[mig 137] expected 7 endentscheidung rules under upc.apl.unified, got %', merits_count;
END IF;
IF order_count <> 7 THEN
RAISE EXCEPTION '[mig 137] expected 7 anordnung rules under upc.apl.unified, got %', order_count;
END IF;
SELECT COUNT(*) INTO schad_already
FROM paliad.deadline_rules
WHERE proceeding_type_id = upc_apl_id
AND is_active = true
AND 'schadensbemessung' = ANY(applies_to_target);
SELECT COUNT(*) INTO buch_already
FROM paliad.deadline_rules
WHERE proceeding_type_id = upc_apl_id
AND is_active = true
AND 'bucheinsicht' = ANY(applies_to_target);
IF schad_already > 0 THEN
RAISE EXCEPTION '[mig 137] % rules already carry schadensbemessung — partial run?', schad_already;
END IF;
IF buch_already > 0 THEN
RAISE EXCEPTION '[mig 137] % rules already carry bucheinsicht — partial run?', buch_already;
END IF;
RAISE NOTICE '[mig 137] rules to extend with schadensbemessung (merits track):';
FOR rec IN
SELECT dr.id, dr.rule_code, dr.legal_source, dr.name, dr.applies_to_target
FROM paliad.deadline_rules dr
WHERE dr.proceeding_type_id = upc_apl_id
AND dr.is_active = true
AND 'endentscheidung' = ANY(dr.applies_to_target)
ORDER BY dr.sequence_order, dr.rule_code NULLS LAST
LOOP
RAISE NOTICE '[mig 137] merits % % % pre=% → post=%',
COALESCE(rec.rule_code, '(no-code)'),
COALESCE(rec.legal_source, '(no-source)'),
rec.name,
rec.applies_to_target,
rec.applies_to_target || 'schadensbemessung'::text;
END LOOP;
RAISE NOTICE '[mig 137] rules to extend with bucheinsicht (order track):';
FOR rec IN
SELECT dr.id, dr.rule_code, dr.legal_source, dr.name, dr.applies_to_target
FROM paliad.deadline_rules dr
WHERE dr.proceeding_type_id = upc_apl_id
AND dr.is_active = true
AND 'anordnung' = ANY(dr.applies_to_target)
ORDER BY dr.sequence_order, dr.rule_code NULLS LAST
LOOP
RAISE NOTICE '[mig 137] order % % % pre=% → post=%',
COALESCE(rec.rule_code, '(no-code)'),
COALESCE(rec.legal_source, '(no-source)'),
rec.name,
rec.applies_to_target,
rec.applies_to_target || 'bucheinsicht'::text;
END LOOP;
END $$;
-- ---------------------------------------------------------------
-- 2. Extend applies_to_target.
--
-- Narrow WHERE clauses key off upc.apl.unified + existing target +
-- absence of new target, so the UPDATEs are idempotent in spirit
-- (the audit block above already RAISE EXCEPTIONed if any row
-- already had the new value).
-- ---------------------------------------------------------------
-- 2a. Schadensbemessung := merits track (7 rules expected).
UPDATE paliad.deadline_rules dr
SET applies_to_target = applies_to_target || 'schadensbemessung'::text
FROM paliad.proceeding_types pt
WHERE pt.id = dr.proceeding_type_id
AND pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'endentscheidung' = ANY(dr.applies_to_target)
AND NOT ('schadensbemessung' = ANY(dr.applies_to_target));
-- 2b. Bucheinsicht := order track (7 rules expected).
UPDATE paliad.deadline_rules dr
SET applies_to_target = applies_to_target || 'bucheinsicht'::text
FROM paliad.proceeding_types pt
WHERE pt.id = dr.proceeding_type_id
AND pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'anordnung' = ANY(dr.applies_to_target)
AND NOT ('bucheinsicht' = ANY(dr.applies_to_target));
-- ---------------------------------------------------------------
-- 3. Post-migration sanity check.
--
-- Hard-fail on any divergence: the two new targets must each cover
-- 7 rules, the original three targets must be unchanged in count,
-- and no rule has lost its prior target.
-- ---------------------------------------------------------------
DO $$
DECLARE
schad_post int;
buch_post int;
end_post int;
anord_post int;
cost_post int;
target_distribution record;
BEGIN
SELECT COUNT(*) INTO schad_post
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'schadensbemessung' = ANY(dr.applies_to_target);
SELECT COUNT(*) INTO buch_post
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'bucheinsicht' = ANY(dr.applies_to_target);
SELECT COUNT(*) INTO end_post
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'endentscheidung' = ANY(dr.applies_to_target);
SELECT COUNT(*) INTO anord_post
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'anordnung' = ANY(dr.applies_to_target);
SELECT COUNT(*) INTO cost_post
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified'
AND dr.is_active = true
AND 'kostenentscheidung' = ANY(dr.applies_to_target);
RAISE NOTICE '[mig 137] post: schadensbemessung=% bucheinsicht=% endentscheidung=% anordnung=% kostenentscheidung=%',
schad_post, buch_post, end_post, anord_post, cost_post;
IF schad_post <> 7 THEN
RAISE EXCEPTION '[mig 137] FAILED — expected 7 schadensbemessung rules, got %', schad_post;
END IF;
IF buch_post <> 7 THEN
RAISE EXCEPTION '[mig 137] FAILED — expected 7 bucheinsicht rules, got %', buch_post;
END IF;
IF end_post <> 7 THEN
RAISE EXCEPTION '[mig 137] FAILED — endentscheidung count drifted: expected 7, got %', end_post;
END IF;
IF anord_post <> 7 THEN
RAISE EXCEPTION '[mig 137] FAILED — anordnung count drifted: expected 7, got %', anord_post;
END IF;
IF cost_post <> 2 THEN
RAISE EXCEPTION '[mig 137] FAILED — kostenentscheidung count drifted: expected 2, got %', cost_post;
END IF;
FOR target_distribution IN
SELECT unnest(applies_to_target) AS target, COUNT(*) AS n
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE pt.code = 'upc.apl.unified' AND dr.is_active = true
GROUP BY unnest(applies_to_target)
ORDER BY 1
LOOP
RAISE NOTICE '[mig 137] post: applies_to_target=% count=%',
target_distribution.target, target_distribution.n;
END LOOP;
END $$;

View File

@@ -144,7 +144,7 @@ func TestLookupEvents(t *testing.T) {
}
})
t.Run("appeal_target=schadensbemessung returns empty (no rules seeded yet)", func(t *testing.T) {
t.Run("appeal_target=schadensbemessung returns upc.apl merits rules (mig 137 backfill)", func(t *testing.T) {
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
Jurisdiction: "UPC",
AppealTarget: lp.AppealTargetSchadensbemessung,
@@ -152,8 +152,68 @@ func TestLookupEvents(t *testing.T) {
if err != nil {
t.Fatalf("LookupEvents: %v", err)
}
if len(matches) != 0 {
t.Errorf("schadensbemessung should be empty until rules seeded; got %d rows", len(matches))
// mig 137 (t-paliad-303, m/paliad#134) extends the 7 merits-track
// rules under upc.apl.unified with applies_to_target ⊇ {schadensbemessung}
// because R.224 is uniform across substantive R.118 decisions.
if len(matches) == 0 {
t.Fatal("expected upc.apl schadensbemessung rules after mig 137 backfill")
}
for _, m := range matches {
if m.DepthFromAnchor != 1 {
continue
}
found := false
for _, t := range m.Rule.AppliesToTarget {
if t == lp.AppealTargetSchadensbemessung {
found = true
break
}
}
if !found {
t.Errorf("anchor row %s missing schadensbemessung target: %v",
m.Rule.Name, m.Rule.AppliesToTarget)
}
if m.ProceedingType.Code != "upc.apl.unified" {
t.Errorf("anchor row %s came from %s, want upc.apl.unified",
m.Rule.Name, m.ProceedingType.Code)
}
}
})
t.Run("appeal_target=bucheinsicht returns upc.apl order rules (mig 137 backfill)", func(t *testing.T) {
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
Jurisdiction: "UPC",
AppealTarget: lp.AppealTargetBucheinsicht,
}, lp.EventLookupDepthAllFollowing)
if err != nil {
t.Fatalf("LookupEvents: %v", err)
}
// mig 137 (t-paliad-303, m/paliad#134) extends the 7 order-track
// rules under upc.apl.unified with applies_to_target ⊇ {bucheinsicht}
// because R.220.2 / R.224.2.b / R.235.2 / R.237 / R.238.2 are
// uniform across the orders they appeal.
if len(matches) == 0 {
t.Fatal("expected upc.apl bucheinsicht rules after mig 137 backfill")
}
for _, m := range matches {
if m.DepthFromAnchor != 1 {
continue
}
found := false
for _, t := range m.Rule.AppliesToTarget {
if t == lp.AppealTargetBucheinsicht {
found = true
break
}
}
if !found {
t.Errorf("anchor row %s missing bucheinsicht target: %v",
m.Rule.Name, m.Rule.AppliesToTarget)
}
if m.ProceedingType.Code != "upc.apl.unified" {
t.Errorf("anchor row %s came from %s, want upc.apl.unified",
m.Rule.Name, m.ProceedingType.Code)
}
}
})
}