From 251f5a250f0b7e76f6e9f8e99bc55d5c60f2e620 Mon Sep 17 00:00:00 2001 From: mAi Date: Fri, 15 May 2026 00:19:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(t-paliad-182):=20mig=20078=20=E2=80=94=20u?= =?UTF-8?q?nified=20rule=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 Slice 1 Step A (design §3.1). Additive only; no drops, no data change. Adds nine columns to paliad.deadline_rules so the calculator + rule editor can converge on a single rule shape over the following slices: trigger_event_id (bigint, FK trigger_events.id) spawn_proceeding_type_id (int, FK proceeding_types.id) combine_op (text, CHECK 'max'|'min') condition_expr (jsonb) priority (text, DEFAULT 'mandatory', 4-way CHECK) is_court_set (bool, DEFAULT false) lifecycle_state (text, DEFAULT 'published', 3-way CHECK) draft_of (uuid, self-FK) published_at (timestamptz) FK types follow the actual referenced columns (bigint on trigger_events, int4 serial on proceeding_types) — the design doc's "int FK" shorthand is widened to the precise widths. FKs are DEFERRABLE INITIALLY IMMEDIATE so Slice 3's data-move can defer FK checks within a single transaction without disturbing normal-statement semantics. Indexes: partial WHERE NOT NULL on the two FK columns (sparse; most rules have neither); plain btree on lifecycle_state so the admin filter on 'published' is O(log n). --- .../078_unified_rule_columns.down.sql | 27 +++ .../078_unified_rule_columns.up.sql | 173 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 internal/db/migrations/078_unified_rule_columns.down.sql create mode 100644 internal/db/migrations/078_unified_rule_columns.up.sql diff --git a/internal/db/migrations/078_unified_rule_columns.down.sql b/internal/db/migrations/078_unified_rule_columns.down.sql new file mode 100644 index 0000000..ce83f54 --- /dev/null +++ b/internal/db/migrations/078_unified_rule_columns.down.sql @@ -0,0 +1,27 @@ +-- t-paliad-182 down — reverses 078_unified_rule_columns.up.sql. +-- +-- Drops in reverse dependency order: indexes → CHECK constraints → +-- FKs → columns. Idempotent (IF EXISTS guards everywhere). + +DROP INDEX IF EXISTS paliad.deadline_rules_lifecycle_state_idx; +DROP INDEX IF EXISTS paliad.deadline_rules_spawn_proceeding_type_id_idx; +DROP INDEX IF EXISTS paliad.deadline_rules_trigger_event_id_idx; + +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_lifecycle_state_check; +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_priority_check; +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_combine_op_check; + +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_draft_of_fkey; +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_spawn_proceeding_type_id_fkey; +ALTER TABLE paliad.deadline_rules DROP CONSTRAINT IF EXISTS deadline_rules_trigger_event_id_fkey; + +ALTER TABLE paliad.deadline_rules + DROP COLUMN IF EXISTS published_at, + DROP COLUMN IF EXISTS draft_of, + DROP COLUMN IF EXISTS lifecycle_state, + DROP COLUMN IF EXISTS is_court_set, + DROP COLUMN IF EXISTS priority, + DROP COLUMN IF EXISTS condition_expr, + DROP COLUMN IF EXISTS combine_op, + DROP COLUMN IF EXISTS spawn_proceeding_type_id, + DROP COLUMN IF EXISTS trigger_event_id; diff --git a/internal/db/migrations/078_unified_rule_columns.up.sql b/internal/db/migrations/078_unified_rule_columns.up.sql new file mode 100644 index 0000000..6c9163f --- /dev/null +++ b/internal/db/migrations/078_unified_rule_columns.up.sql @@ -0,0 +1,173 @@ +-- t-paliad-182 / Fristen Phase 3 Slice 1 (Step A of +-- docs/design-fristen-phase2-2026-05-15.md §3.1). +-- +-- Additive only: extends paliad.deadline_rules with the unified-rule +-- columns the Phase 3 calculator + rule editor will use. +-- +-- NO drops in this slice. Legacy columns (is_mandatory, is_optional, +-- condition_flag, condition_rule_id) stay live until Slice 9. Compat- +-- mode readers consume both shapes during the transition window +-- (design §3.2 "Cutover ordering"). +-- +-- Column-by-column rationale: +-- trigger_event_id — event-rooted dispatch (Pipeline C unification, §2.5). +-- spawn_proceeding_type_id — cross-proceeding spawn resolution (Q7, §2.6). +-- combine_op — composite-rule arithmetic 'max'/'min' (R.198/R.213). +-- condition_expr — jsonb condition grammar replacing condition_flag (Q6, §2.4). +-- priority — 4-way enum mandatory|recommended|optional|informational (Q3, §2.3). +-- is_court_set — explicit replacement of the runtime heuristic (Q12). +-- lifecycle_state — draft|published|archived for the rule editor (Q5, §4.2). +-- draft_of — draft self-FK pointing at the published row it replaces. +-- published_at — promotion timestamp, NULL while draft. +-- +-- FK type notes: +-- trigger_event_id is BIGINT (paliad.trigger_events.id is bigint, mig 028). +-- spawn_proceeding_type_id is INTEGER (paliad.proceeding_types.id is +-- serial = int4, mig 003). +-- draft_of is UUID (self-FK on paliad.deadline_rules.id). +-- The design doc (§2.1) calls them "int FK" loosely; the actual schemas +-- demand the precise int width, hence bigint/integer here. +-- +-- Indexes: +-- FK lookups for trigger_event_id + spawn_proceeding_type_id (sparse, +-- most rules have neither — partial WHERE NOT NULL keeps the index +-- small). +-- lifecycle_state is queried by the admin /admin/rules listing's +-- default filter (state='published'); plain btree is fine, no +-- WHERE clause so 'draft' / 'archived' rows index too. +-- +-- Idempotent: every ADD COLUMN uses IF NOT EXISTS. Re-applying is a +-- no-op. Tracker advances 77 → 78. + +-- ============================================================================= +-- 1. New columns on paliad.deadline_rules +-- ============================================================================= + +ALTER TABLE paliad.deadline_rules + ADD COLUMN IF NOT EXISTS trigger_event_id bigint, + ADD COLUMN IF NOT EXISTS spawn_proceeding_type_id integer, + ADD COLUMN IF NOT EXISTS combine_op text, + ADD COLUMN IF NOT EXISTS condition_expr jsonb, + ADD COLUMN IF NOT EXISTS priority text NOT NULL DEFAULT 'mandatory', + ADD COLUMN IF NOT EXISTS is_court_set boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS lifecycle_state text NOT NULL DEFAULT 'published', + ADD COLUMN IF NOT EXISTS draft_of uuid, + ADD COLUMN IF NOT EXISTS published_at timestamptz; + +COMMENT ON COLUMN paliad.deadline_rules.trigger_event_id IS + 'Optional FK to paliad.trigger_events. When non-NULL, this rule is ' + 'event-rooted (Pipeline C unification, design §2.5). When NULL the ' + 'rule is proceeding-rooted via proceeding_type_id. Exactly one of ' + 'the two must be set after Slice 3 backfill (enforced by a CHECK ' + 'constraint added in Slice 9 after legacy callers retire).'; + +COMMENT ON COLUMN paliad.deadline_rules.spawn_proceeding_type_id IS + 'When is_spawn=true, points at the target proceeding whose rule set ' + 'the calculator follows when this rule fires (cross-proceeding ' + 'spawn, design §2.6). Backfilled in Slice 7 for the 8 live spawn ' + 'rules.'; + +COMMENT ON COLUMN paliad.deadline_rules.combine_op IS + 'NULL = single-anchor arithmetic. ''max'' / ''min'' = composite-rule ' + 'arithmetic combining (duration_value, duration_unit) with ' + '(alt_duration_value, alt_duration_unit). Used by R.198 / R.213 ' + '("31d OR 20 working_days, whichever is longer / shorter").'; + +COMMENT ON COLUMN paliad.deadline_rules.condition_expr IS + 'jsonb gating expression replacing condition_flag (Q6, design §2.4). ' + 'Grammar: {"flag": ""} | {"op":"and"|"or", "args":[...]} | ' + '{"op":"not", "args":[]}. NULL or {} = unconditional. ' + 'Backfilled in Slice 2 from condition_flag; new code reads this, ' + 'falls back to condition_flag during the transition window.'; + +COMMENT ON COLUMN paliad.deadline_rules.priority IS + 'Unified 4-way enum (Q3, design §2.3) replacing the is_mandatory + ' + 'is_optional pair. Allowed: mandatory | recommended | optional | ' + 'informational. Default ''mandatory'' on new rows; legacy rows get ' + 'backfilled in Slice 2 from the (is_mandatory, is_optional) pair.'; + +COMMENT ON COLUMN paliad.deadline_rules.is_court_set IS + 'Replaces the runtime heuristic (primary_party=''court'' OR ' + 'event_type IN (...)) with an explicit column (Q12). Default false ' + 'on new rows; Slice 2 backfills from the heuristic so behaviour is ' + 'unchanged at first.'; + +COMMENT ON COLUMN paliad.deadline_rules.lifecycle_state IS + 'Rule-editor lifecycle (Q5, design §4.2). draft = work-in-progress ' + 'admin edit; published = live, calculator-visible; archived = ' + 'historical (kept for audit). Default ''published'' so every ' + 'existing row stays live without an UPDATE.'; + +COMMENT ON COLUMN paliad.deadline_rules.draft_of IS + 'When lifecycle_state=''draft'', points at the published rule this ' + 'draft will replace on publish. NULL on published or archived ' + 'rows. NULL also on net-new drafts (no prior published peer).'; + +COMMENT ON COLUMN paliad.deadline_rules.published_at IS + 'Timestamp this row entered lifecycle_state=''published''. NULL ' + 'while draft, populated on publish, retained through archive. ' + 'Distinct from updated_at (which moves on every edit).'; + +-- ============================================================================= +-- 2. Foreign keys +-- ============================================================================= +-- +-- DEFERRABLE INITIALLY IMMEDIATE keeps normal-statement semantics +-- intact while still letting backfill migrations defer until end-of- +-- transaction if they need to (e.g. when Slice 3 inserts a rule row +-- whose trigger_event_id references a row inserted in the same tx). + +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_trigger_event_id_fkey + FOREIGN KEY (trigger_event_id) + REFERENCES paliad.trigger_events(id) + ON DELETE SET NULL + DEFERRABLE INITIALLY IMMEDIATE; + +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_spawn_proceeding_type_id_fkey + FOREIGN KEY (spawn_proceeding_type_id) + REFERENCES paliad.proceeding_types(id) + ON DELETE SET NULL + DEFERRABLE INITIALLY IMMEDIATE; + +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_draft_of_fkey + FOREIGN KEY (draft_of) + REFERENCES paliad.deadline_rules(id) + ON DELETE SET NULL + DEFERRABLE INITIALLY IMMEDIATE; + +-- ============================================================================= +-- 3. CHECK constraints on enum-style columns +-- ============================================================================= +-- +-- combine_op: NULL (unset) or one of two values. +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_combine_op_check + CHECK (combine_op IS NULL OR combine_op IN ('max', 'min')); + +-- priority: 4-way enum. +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_priority_check + CHECK (priority IN ('mandatory', 'recommended', 'optional', 'informational')); + +-- lifecycle_state: 3-way enum. +ALTER TABLE paliad.deadline_rules + ADD CONSTRAINT deadline_rules_lifecycle_state_check + CHECK (lifecycle_state IN ('draft', 'published', 'archived')); + +-- ============================================================================= +-- 4. Indexes +-- ============================================================================= + +CREATE INDEX IF NOT EXISTS deadline_rules_trigger_event_id_idx + ON paliad.deadline_rules (trigger_event_id) + WHERE trigger_event_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS deadline_rules_spawn_proceeding_type_id_idx + ON paliad.deadline_rules (spawn_proceeding_type_id) + WHERE spawn_proceeding_type_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS deadline_rules_lifecycle_state_idx + ON paliad.deadline_rules (lifecycle_state);