feat(t-paliad-184): mig 085 — Pipeline C data-move (77 rows)

Phase 3 Slice 3 Step C (design §3.C). INSERT 77 active rows from
paliad.event_deadlines into paliad.deadline_rules so the unified
backend can serve both pipelines. Source rows preserved (mig 086
wraps the source table in a read-only trigger; Slice 9 drops it).

Mapping:
  trigger_event_id              ← event_deadlines.trigger_event_id (bigint, mig 028)
  name (DE, NOT NULL)           ← event_deadlines.title_de         (NOT NULL DEFAULT '')
  name_en (NOT NULL)            ← event_deadlines.title            (EN, NOT NULL)
  duration_value / unit         ← event_deadlines.duration_value / unit
  timing                        ← event_deadlines.timing           (before / after)
  alt_duration_value / unit     ← event_deadlines.alt_duration_*
  combine_op                    ← event_deadlines.combine_op       (mig 078 column)
  deadline_notes (DE)           ← event_deadlines.notes  (DE; NULLIF '' so empty
                                                          stays NULL on dr side)
  deadline_notes_en             ← event_deadlines.notes_en (mig 036)
  legal_source                  ← event_deadlines.legal_source
  published_at                  ← event_deadlines.created_at        (chronological audit)
  sequence_order = 1000 + ed.id (large offset so Pipeline-C rules
                                  sort after any hand-authored
                                  Pipeline-A sequence_orders; preserves
                                  source ordering within Pipeline C)
  lifecycle_state = 'published' / priority = 'mandatory' / is_active = ed.is_active

Pipeline-A-only fields stay NULL on the new rows: proceeding_type_id,
parent_id, spawn_proceeding_type_id, code, primary_party, event_type,
condition_expr, condition_flag. is_court_set = false (no court-set
rules in the Pipeline-C corpus today; legal-review pass can flip
Zustellung-* later via a separate slice).

Idempotency: WHERE NOT EXISTS guard on (trigger_event_id, name).
Re-running the migration is a no-op.

Hard assertion at end: COUNT(deadline_rules WHERE trigger_event_id
IS NOT NULL) must equal COUNT(event_deadlines WHERE is_active=true)
post-mig. RAISE EXCEPTION on mismatch — better to fail the migration
loudly than to ship a partial Pipeline-C corpus and poison Slice 4.

Audit-reason set via set_config so the mig 079 trigger writes 77
paliad.deadline_rule_audit rows with the design §3.C citation
preserved as the rationale. That's the persistent compliance trail
for the data-move.

No mandatory bool on event_deadlines (the head instruction sketch
suggested mapping it; the schema doesn't have one) — Pipeline-C
rules default priority='mandatory', consistent with the statutory
nature of the corpus.
This commit is contained in:
mAi
2026-05-15 00:40:50 +02:00
parent 238c4d7cf0
commit 88d5656a35
2 changed files with 201 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
-- t-paliad-184 down — reverts the Pipeline-C data-move from
-- 085_pipeline_c_data_move.up.sql. Deletes every paliad.deadline_rules
-- row carrying a non-NULL trigger_event_id (those are exactly the rows
-- the up-migration created — before mig 085 no Pipeline-A rule ever
-- carried trigger_event_id, and Slice 9 hasn't dropped the source
-- table yet so the rows can be regenerated).
--
-- Audit-reason set so the mig 079 trigger captures the rollback
-- rationale and doesn't raise on DELETE.
SELECT set_config(
'paliad.audit_reason',
'rollback 085: delete Pipeline-C unified rows (source preserved in event_deadlines)',
true);
DELETE FROM paliad.deadline_rules
WHERE trigger_event_id IS NOT NULL;

View File

@@ -0,0 +1,184 @@
-- t-paliad-184 / Fristen Phase 3 Slice 3 Step C — data-move 77 rows
-- from paliad.event_deadlines → paliad.deadline_rules so the Phase-3
-- unified backend can serve both pipelines.
--
-- Source rows are PRESERVED (mig 086's read-only trigger blocks
-- further writes; mig 090 in Slice 9 drops the table once every
-- caller has cut over). The data-move is one-way; legacy callers
-- continue reading event_deadlines via plain SELECTs until Slice 9.
--
-- Mapping (per design §3.C):
--
-- paliad.event_deadlines → paliad.deadline_rules
-- ------------------------- ----------------------
-- id (new gen_random_uuid())
-- trigger_event_id trigger_event_id (Phase 3 column from mig 078)
-- title (EN, NOT NULL) name_en (NOT NULL)
-- title_de (DE, NOT NULL DEFAULT '') name (NOT NULL — every row has non-empty title_de in live data)
-- duration_value duration_value
-- duration_unit (days/weeks/months/working_days) duration_unit
-- timing (before/after) timing
-- notes (DE) deadline_notes (DE)
-- notes_en (EN, nullable) deadline_notes_en (EN, nullable)
-- alt_duration_value alt_duration_value
-- alt_duration_unit alt_duration_unit
-- combine_op (max/min, nullable) combine_op (Phase 3 column from mig 078)
-- legal_source legal_source
-- is_active is_active
-- created_at published_at (preserves chronology — lifecycle_state='published' on every row)
-- updated_at = now() (this is the publish event)
--
-- Pipeline-A-only fields default:
-- proceeding_type_id = NULL (event-rooted, no proceeding)
-- parent_id = NULL (Pipeline C is flat, no chain)
-- spawn_proceeding_type_id = NULL (no spawn)
-- code = NULL (no local rule code in Pipeline C)
-- primary_party = NULL (event_deadlines has no party column)
-- event_type = NULL (filing/hearing/decision is a
-- Pipeline-A category)
-- is_court_set = false (no court-set Pipeline-C rules
-- in the corpus; legal-review
-- pass can flip Zustellung-* if
-- those ever land here)
-- is_spawn = false
-- is_mandatory = true (Pipeline C has no mandatory
-- bool; design §2.3 says default
-- 'mandatory' is correct for
-- statutory event-driven deadlines)
-- is_optional = false
-- priority = 'mandatory'
-- condition_expr = NULL (Pipeline C has no flag gating)
-- condition_flag = NULL
-- sequence_order = 1000 + event_deadlines.id
-- (large offset so Pipeline-C
-- rows sort AFTER any future
-- hand-edited Pipeline-A
-- sequence_orders without
-- colliding with the
-- existing 0171 range)
-- lifecycle_state = 'published'
--
-- Idempotency: WHERE NOT EXISTS guard on (trigger_event_id, name) skips
-- rows that already exist in deadline_rules. Re-running the migration
-- is a no-op.
--
-- Hard assertion at end: COUNT(deadline_rules WHERE trigger_event_id
-- IS NOT NULL) == COUNT(event_deadlines WHERE is_active = true) (77 = 77).
-- RAISE EXCEPTION on mismatch so a partial move fails the migration
-- loudly instead of poisoning Slice 4.
--
-- Audit-reason cites design §3.C — the rationale persists in the
-- paliad.deadline_rule_audit log forever via the mig 079 trigger.
SELECT set_config(
'paliad.audit_reason',
'pipeline C migration 085: data-move event_deadlines → deadline_rules per design §3.C — '
|| 'preserves source rows; mig 086 wraps the source table read-only',
true);
INSERT INTO paliad.deadline_rules (
id,
proceeding_type_id,
parent_id,
trigger_event_id,
spawn_proceeding_type_id,
code,
name,
name_en,
primary_party,
event_type,
is_mandatory,
is_optional,
is_court_set,
is_spawn,
duration_value,
duration_unit,
timing,
alt_duration_value,
alt_duration_unit,
combine_op,
rule_code,
deadline_notes,
deadline_notes_en,
legal_source,
condition_expr,
condition_flag,
sequence_order,
is_active,
priority,
lifecycle_state,
draft_of,
published_at,
created_at,
updated_at
)
SELECT
gen_random_uuid() AS id,
NULL::integer AS proceeding_type_id,
NULL::uuid AS parent_id,
ed.trigger_event_id AS trigger_event_id,
NULL::integer AS spawn_proceeding_type_id,
NULL::text AS code,
ed.title_de AS name,
ed.title AS name_en,
NULL::text AS primary_party,
NULL::text AS event_type,
true AS is_mandatory,
false AS is_optional,
false AS is_court_set,
false AS is_spawn,
ed.duration_value AS duration_value,
ed.duration_unit AS duration_unit,
ed.timing AS timing,
ed.alt_duration_value AS alt_duration_value,
ed.alt_duration_unit AS alt_duration_unit,
ed.combine_op AS combine_op,
NULL::text AS rule_code,
NULLIF(ed.notes, '') AS deadline_notes,
ed.notes_en AS deadline_notes_en,
ed.legal_source AS legal_source,
NULL::jsonb AS condition_expr,
NULL::text[] AS condition_flag,
(1000 + ed.id)::integer AS sequence_order,
ed.is_active AS is_active,
'mandatory' AS priority,
'published' AS lifecycle_state,
NULL::uuid AS draft_of,
ed.created_at AS published_at,
ed.created_at AS created_at,
now() AS updated_at
FROM paliad.event_deadlines ed
WHERE ed.is_active = true
AND NOT EXISTS (
SELECT 1
FROM paliad.deadline_rules dr
WHERE dr.trigger_event_id = ed.trigger_event_id
AND dr.name = ed.title_de
);
-- Hard assertion: every active event_deadlines row must have a matching
-- deadline_rules row by (trigger_event_id, name). If the counts diverge,
-- something in the WHERE NOT EXISTS clause (likely a stale duplicate)
-- prevented a real insert — fail the migration rather than ship a
-- partial Pipeline-C corpus.
DO $$
DECLARE
n_source int;
n_target int;
BEGIN
SELECT count(*) INTO n_source
FROM paliad.event_deadlines WHERE is_active = true;
SELECT count(*) INTO n_target
FROM paliad.deadline_rules WHERE trigger_event_id IS NOT NULL;
RAISE NOTICE 'mig 085: event_deadlines(active)=%, deadline_rules(trigger_event_id IS NOT NULL)=%',
n_source, n_target;
IF n_target <> n_source THEN
RAISE EXCEPTION 'mig 085: data-move incomplete — expected % unified rows, got %. '
'Investigate event_deadlines (trigger_event_id, title_de) duplicates '
'OR re-applied migration on dirtied target.',
n_source, n_target;
END IF;
END $$;