feat(deadline-system): P1 — upc.apl re-split into merits/cost/order (m/paliad#149)

Phase 2 P1 / m's Q5 divergence (2026-05-27, verbatim):

  "Reverse the unification as suggested in 3. They are different
   proceedings, I only wanted the approach to be unified in the
   'determinator' — but they are actually different proceedings!"

Mig 155 reverts the mig-096 unification:

  Before: id=160 upc.apl.unified active (16 rules), id=11/19/20 inactive
  After:  id=11 upc.apl.merits (7 rules), id=19 upc.apl.cost (2 rules),
          id=20 upc.apl.order (7 rules) all active; id=160 inactive

The 16 rules under id=160 split cleanly by event_code prefix; all 10
parent_id edges among them are bucket-local (pre-flight audit), so
the tree shape survives the rebind unchanged.

Spawn FK retarget: pi.cfi.appeal_spawn flips from 11 (merits) → 20
(orders track) per design §3.1 — PI appeals land on orders, not
merits. The inf/rev/dmgs spawns keep target=11 (merits), now active.

Determinator routing layer (proceeding_mapping.go) keeps its single
"Berufung" front door per m's intent — only the data shape changes.

Pre-flight verified: 0 projects bound to id=160, 0 scenarios reference
upc.apl. Zero data migration on the project side.

Tests: lookup_events_test.go assertions on the three appeal_target
buckets updated to the new codes (endentscheidung → upc.apl.merits,
schadensbemessung → upc.apl.merits, bucheinsicht → upc.apl.order).
Same rule set, post-split coordinates.

Snapshot regen (pkg/litigationplanner/embedded/upc/) deferred: the
current snapshot only contains inf+rev so the apl re-split doesn't
shift its contents; regenerating would surface unrelated active PTs
and pollute this slice. Tracked as a follow-up.

Verified: go vet clean, go test ./internal/services/... -run
LookupEvents|proceeding_codes clean.

Design: docs/design-deadline-system-revision-2026-05-27.md §3.1
(re-split mig), §1.3 (spawn graph post-Q5). t-paliad-331.
This commit is contained in:
mAi
2026-05-27 15:11:48 +02:00
parent 3533d79a25
commit 3a4e99cb92
3 changed files with 255 additions and 16 deletions

View File

@@ -0,0 +1,43 @@
-- 155_upc_apl_resplit.down — t-paliad-331 / m/paliad#149 Phase 2 P1
--
-- Best-effort rollback. Restores from the same-TX snapshots written by
-- mig 155. Drops the snapshots once restoration is verified.
BEGIN;
SELECT set_config(
'paliad.audit_reason',
'mig 155 down: revert upc.apl re-split (restore unified id=160)',
true
);
-- ----------------------------------------------------------------
-- 1. Restore proceeding_types.is_active from snapshot.
-- ----------------------------------------------------------------
UPDATE paliad.proceeding_types pt
SET is_active = pre.is_active
FROM paliad.proceeding_types_pre_155 pre
WHERE pt.id = pre.id
AND pt.is_active IS DISTINCT FROM pre.is_active;
-- ----------------------------------------------------------------
-- 2. Restore rule bindings from snapshot.
-- ----------------------------------------------------------------
UPDATE paliad.sequencing_rules sr
SET proceeding_type_id = pre.proceeding_type_id,
spawn_proceeding_type_id = pre.spawn_proceeding_type_id
FROM paliad.sequencing_rules_pre_155 pre
WHERE sr.id = pre.id
AND (sr.proceeding_type_id IS DISTINCT FROM pre.proceeding_type_id
OR sr.spawn_proceeding_type_id IS DISTINCT FROM pre.spawn_proceeding_type_id);
-- ----------------------------------------------------------------
-- 3. Drop the snapshots.
-- ----------------------------------------------------------------
DROP TABLE IF EXISTS paliad.sequencing_rules_pre_155;
DROP TABLE IF EXISTS paliad.proceeding_types_pre_155;
COMMIT;

View File

@@ -0,0 +1,191 @@
-- 155_upc_apl_resplit — t-paliad-331 / m/paliad#149 Phase 2 P1
--
-- Reverts the upc.apl unification that mig 096 introduced. m's Q5
-- (2026-05-27, verbatim):
--
-- "Reverse the unification as suggested in 3. They are different
-- proceedings, I only wanted the approach to be unified in the
-- 'determinator' — but they are actually different proceedings!"
--
-- The current state (audited 2026-05-27, mig 155 pre-flight):
--
-- id=160 upc.apl.unified is_active=true (carries all 16 rules)
-- id=11 upc.apl.merits is_active=false
-- id=19 upc.apl.cost is_active=false
-- id=20 upc.apl.order is_active=false
--
-- The 16 rules under id=160 split cleanly by event_code prefix:
-- 7 rows match 'upc.apl.merits.%' → target id=11
-- 2 rows match 'upc.apl.cost.%' → target id=19
-- 7 rows match 'upc.apl.order.%' → target id=20
--
-- Every parent_id chain among those 16 rows stays inside its bucket
-- (audited: 10/10 parent edges are bucket-local), so retargeting by
-- event_code prefix preserves the tree shape — no extra parent_id
-- surgery needed.
--
-- Spawn FKs: 4 rules currently target id=11 (was inactive — this is
-- the R3 finding athena flagged, re-interpreted by m's Q5 as correct
-- intent rather than broken state):
--
-- upc.inf.cfi.appeal_spawn → 11 (merits) — keep
-- upc.rev.cfi.appeal_spawn → 11 (merits) — keep
-- upc.dmgs.cfi.appeal_spawn → 11 (merits) — keep
-- upc.pi.cfi.appeal_spawn → 11 (merits) — RETARGET to 20 (order),
-- since PI appeals
-- land on the orders
-- track per design §3.1.
--
-- Active scenarios / projects pointing at id=160: zero (verified
-- pre-flight: 0 projects, 0 scenarios reference 'upc.apl'). No data
-- migration on the project side; no production traffic is mid-flight
-- on id=160.
--
-- Mig 153's `projects_proceeding_type_kind_check` trigger gates
-- inserts/updates against kind='proceeding'. id=11/19/20 already
-- carry kind='proceeding' (verified pre-flight), so the trigger
-- won't fire on the re-activations.
BEGIN;
SELECT set_config(
'paliad.audit_reason',
'mig 155: upc.apl re-split — reactivate merits/cost/order, retire unified (t-paliad-331 / m/paliad#149 P1)',
true
);
-- ----------------------------------------------------------------
-- 1. Snapshot for audit + rollback.
-- ----------------------------------------------------------------
CREATE TABLE paliad.proceeding_types_pre_155 AS
SELECT * FROM paliad.proceeding_types WHERE id IN (11, 19, 20, 160);
CREATE TABLE paliad.sequencing_rules_pre_155 AS
SELECT * FROM paliad.sequencing_rules
WHERE proceeding_type_id = 160
OR (is_spawn AND spawn_proceeding_type_id IN (11, 19, 20, 160));
COMMENT ON TABLE paliad.proceeding_types_pre_155 IS
'Snapshot of the 4 appeal-related proceeding_types rows taken in '
'the same TX as mig 155 (upc.apl re-split). Audit + rollback safety.';
COMMENT ON TABLE paliad.sequencing_rules_pre_155 IS
'Snapshot of the 16 rules under id=160 + the 4 spawn rules targeting '
'the appeal cluster, taken in the same TX as mig 155. Audit + rollback.';
-- ----------------------------------------------------------------
-- 2. Re-activate the three discrete appeal PTs; retire the unified row.
-- ----------------------------------------------------------------
UPDATE paliad.proceeding_types SET is_active = true WHERE id IN (11, 19, 20);
UPDATE paliad.proceeding_types SET is_active = false WHERE id = 160;
DO $$
DECLARE
n_active int;
n_inactive int;
BEGIN
SELECT COUNT(*) INTO n_active FROM paliad.proceeding_types
WHERE id IN (11, 19, 20) AND is_active = true;
SELECT COUNT(*) INTO n_inactive FROM paliad.proceeding_types
WHERE id = 160 AND is_active = false;
IF n_active <> 3 OR n_inactive <> 1 THEN
RAISE EXCEPTION '[mig 155] activation check failed — active(11,19,20)=% / inactive(160)=%', n_active, n_inactive;
END IF;
END $$;
-- ----------------------------------------------------------------
-- 3. Retarget the 16 rules on id=160 to merits/cost/order by event_code
-- prefix. parent_id stays intact (all parent edges are bucket-local
-- per pre-flight audit).
-- ----------------------------------------------------------------
UPDATE paliad.sequencing_rules sr
SET proceeding_type_id = 11
FROM paliad.procedural_events pe
WHERE pe.id = sr.procedural_event_id
AND sr.proceeding_type_id = 160
AND pe.code LIKE 'upc.apl.merits.%';
UPDATE paliad.sequencing_rules sr
SET proceeding_type_id = 19
FROM paliad.procedural_events pe
WHERE pe.id = sr.procedural_event_id
AND sr.proceeding_type_id = 160
AND pe.code LIKE 'upc.apl.cost.%';
UPDATE paliad.sequencing_rules sr
SET proceeding_type_id = 20
FROM paliad.procedural_events pe
WHERE pe.id = sr.procedural_event_id
AND sr.proceeding_type_id = 160
AND pe.code LIKE 'upc.apl.order.%';
DO $$
DECLARE
remaining int;
merits int; cost int; ord int;
BEGIN
SELECT COUNT(*) INTO remaining
FROM paliad.sequencing_rules WHERE proceeding_type_id = 160;
IF remaining <> 0 THEN
RAISE EXCEPTION '[mig 155] rebind failed — % rules still on id=160 (expected 0)', remaining;
END IF;
SELECT COUNT(*) INTO merits
FROM paliad.sequencing_rules WHERE proceeding_type_id = 11;
SELECT COUNT(*) INTO cost
FROM paliad.sequencing_rules WHERE proceeding_type_id = 19;
SELECT COUNT(*) INTO ord
FROM paliad.sequencing_rules WHERE proceeding_type_id = 20;
IF merits <> 7 OR cost <> 2 OR ord <> 7 THEN
RAISE EXCEPTION
'[mig 155] post-rebind counts wrong — merits=% (want 7) / cost=% (want 2) / order=% (want 7)',
merits, cost, ord;
END IF;
RAISE NOTICE '[mig 155] rebind OK — merits=% cost=% order=%', merits, cost, ord;
END $$;
-- ----------------------------------------------------------------
-- 4. Retarget the upc.pi.cfi.appeal_spawn rule to id=20 (orders track).
-- PI appeals don't go to the merits track — they're orders.
-- The inf/rev/dmgs spawns keep target=11 (now active, was inactive
-- by accident of the unification).
-- ----------------------------------------------------------------
UPDATE paliad.sequencing_rules
SET spawn_proceeding_type_id = 20
WHERE is_spawn = true
AND procedural_event_id = (
SELECT id FROM paliad.procedural_events WHERE code = 'upc.pi.cfi.appeal_spawn'
)
AND spawn_proceeding_type_id = 11;
DO $$
DECLARE
pi_target int;
others int;
BEGIN
SELECT spawn_proceeding_type_id INTO pi_target
FROM paliad.sequencing_rules sr
JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
WHERE pe.code = 'upc.pi.cfi.appeal_spawn' AND sr.is_spawn = true
LIMIT 1;
IF pi_target IS DISTINCT FROM 20 THEN
RAISE EXCEPTION '[mig 155] pi.cfi spawn retarget failed — got %, want 20', pi_target;
END IF;
SELECT COUNT(*) INTO others
FROM paliad.sequencing_rules sr
JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
WHERE sr.is_spawn = true
AND sr.spawn_proceeding_type_id = 11
AND pe.code IN ('upc.inf.cfi.appeal_spawn',
'upc.rev.cfi.appeal_spawn',
'upc.dmgs.cfi.appeal_spawn');
IF others <> 3 THEN
RAISE EXCEPTION '[mig 155] inf/rev/dmgs spawn target check failed — % rows point at 11 (want 3)', others;
END IF;
RAISE NOTICE '[mig 155] spawn graph OK — pi → 20 (order); inf/rev/dmgs → 11 (merits)';
END $$;
COMMIT;

View File

@@ -117,10 +117,13 @@ func TestLookupEvents(t *testing.T) {
if err != nil {
t.Fatalf("LookupEvents: %v", err)
}
// Should hit the 7 rules under the unified upc.apl that
// carry applies_to_target={endentscheidung} (Slice B1 mig 134).
// Should hit the 7 merits-track rules that carry
// applies_to_target={endentscheidung} (Slice B1 mig 134).
// Post-mig 155 (m/paliad#149 P1): the unified upc.apl was split
// back into merits/cost/order — the endentscheidung anchors live
// under upc.apl.merits (id=11).
if len(matches) == 0 {
t.Fatal("expected upc.apl endentscheidung rules after B1 mig")
t.Fatal("expected upc.apl.merits endentscheidung rules after B1 mig")
}
for _, m := range matches {
if m.DepthFromAnchor != 1 {
@@ -137,8 +140,8 @@ func TestLookupEvents(t *testing.T) {
t.Errorf("anchor row %s missing endentscheidung 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",
if m.ProceedingType.Code != "upc.apl.merits" {
t.Errorf("anchor row %s came from %s, want upc.apl.merits",
m.Rule.Name, m.ProceedingType.Code)
}
}
@@ -153,10 +156,11 @@ func TestLookupEvents(t *testing.T) {
t.Fatalf("LookupEvents: %v", err)
}
// mig 138 (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.
// rules with applies_to_target ⊇ {schadensbemessung} because
// R.224 is uniform across substantive R.118 decisions. Post-mig
// 155 the merits track lives at upc.apl.merits (id=11).
if len(matches) == 0 {
t.Fatal("expected upc.apl schadensbemessung rules after mig 138 backfill")
t.Fatal("expected upc.apl.merits schadensbemessung rules after mig 138 backfill")
}
for _, m := range matches {
if m.DepthFromAnchor != 1 {
@@ -173,8 +177,8 @@ func TestLookupEvents(t *testing.T) {
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",
if m.ProceedingType.Code != "upc.apl.merits" {
t.Errorf("anchor row %s came from %s, want upc.apl.merits",
m.Rule.Name, m.ProceedingType.Code)
}
}
@@ -189,11 +193,12 @@ func TestLookupEvents(t *testing.T) {
t.Fatalf("LookupEvents: %v", err)
}
// mig 138 (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.
// rules 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. Post-mig 155 the order track lives at
// upc.apl.order (id=20).
if len(matches) == 0 {
t.Fatal("expected upc.apl bucheinsicht rules after mig 138 backfill")
t.Fatal("expected upc.apl.order bucheinsicht rules after mig 138 backfill")
}
for _, m := range matches {
if m.DepthFromAnchor != 1 {
@@ -210,8 +215,8 @@ func TestLookupEvents(t *testing.T) {
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",
if m.ProceedingType.Code != "upc.apl.order" {
t.Errorf("anchor row %s came from %s, want upc.apl.order",
m.Rule.Name, m.ProceedingType.Code)
}
}